package act.inject.param;

/*-
 * #%L
 * ACT Framework
 * %%
 * Copyright (C) 2014 - 2017 ActFramework
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import act.Act;
import act.app.ActionContext;
import act.app.App;
import act.inject.DependencyInjector;
import act.util.ActContext;
import org.osgl.inject.BeanSpec;
import org.osgl.mvc.result.BadRequest;
import org.osgl.mvc.util.Binder;
import org.osgl.util.S;
import org.osgl.util.StringValueResolver;

import java.lang.reflect.Type;
import java.util.*;

class CollectionLoader implements ParamValueLoader {

    private final ParamKey key;
    private final Class<? extends Collection> collectionClass;
    private final Type elementType;
    private final DependencyInjector<?> injector;
    private final StringValueResolver resolver;
    private final Binder binder;
    private final Map<ParamKey, ParamValueLoader> childLoaders = new HashMap<ParamKey, ParamValueLoader>();
    private final ParamValueLoaderService manager;
    private final boolean isChar;
    private final BeanSpec targetSpec;

    CollectionLoader(
            ParamKey key,
            Class<? extends Collection> collection,
            Type elementType,
            BeanSpec targetSpec,
            DependencyInjector<?> injector,
            ParamValueLoaderService manager
    ) {
        this.key = key;
        this.collectionClass = collection;
        this.elementType = elementType;
        this.isChar = char.class == elementType || Character.class == elementType;
        this.injector = injector;
        this.manager = manager;
        App app = Act.app();
        Class<?> rawType = BeanSpec.rawTypeOf(elementType);
        this.binder = app.binderManager().binder(rawType);
        this.targetSpec = targetSpec;
        if (null == binder) {
            this.resolver = App.instance().resolverManager().resolver(rawType, targetSpec);
        } else {
            this.resolver = null;
        }
//        if (null == this.binder && null == this.resolver) {
//            throw new IllegalArgumentException(S.fmt("Cannot find binder and resolver for %s", elementType));
//        }
    }

    @Override
    public String bindName() {
        return key.toString();
    }

    @Override
    public Object load(Object bean, ActContext<?> context, boolean noDefaultValue) {
        ParamTree tree = ParamValueLoaderService.ensureParamTree(context);
        ParamTreeNode node = tree.node(key);
        if (null == node) {
            return noDefaultValue ? null : injector.get(collectionClass);
        }
        Collection collection = null == bean ? injector.get(collectionClass) : (Collection) bean;
        if (node.isList()) {
            List<ParamTreeNode> nodes = node.list();
            if (nodes.size() > 0) {
                String value = nodes.get(0).value();
                //if (S.notBlank(value)) {
                    for (int i = 0; i < nodes.size(); ++i) {
                        ParamTreeNode elementNode = nodes.get(i);
                        if (!elementNode.isLeaf()) {
                            throw new BadRequest("cannot parse param: expect leaf node, found: \n%s", node.debug());
                        }
                        context.attribute(ActionContext.ATTR_CURRENT_FILE_INDEX, i);
                        if (null != binder) {
                            collection.add(binder.resolve(null, elementNode.value(), context));
                        } else {
                            collection.add(resolver.resolve(elementNode.value()));
                        }
                    }
                //}
            }
        } else if (node.isMap()) {
            Set<String> childrenKeys = node.mapKeys();
            for (String s : childrenKeys) {
                int id;
                try {
                    id = Integer.parseInt(s);
                } catch (NumberFormatException e) {
                    throw new BadRequest("cannot parse param: list index is not a number: %s", s);
                }
                ParamTreeNode child = node.child(s);
                if (child.isLeaf()) {
                    if (null != child.value()) {
                        if (null != binder) {
                            addToCollection(collection, id, binder.resolve(null, child.value(), context));
                        } else {
                            addToCollection(collection, id, resolver.resolve(child.value()));
                        }
                    }
                } else {
                    ParamValueLoader childLoader = childLoader(child.key());
                    addToCollection(collection, id, childLoader.load(null, context, false));
                }
            }
        } else {
            resolveInto(collection, node.value(), context);
        }
        return collection;
    }

    private ParamValueLoader childLoader(ParamKey key) {
        ParamValueLoader loader = childLoaders.get(key);
        if (null == loader) {
            loader = buildChildLoader(key);
            childLoaders.put(key, loader);
        }
        return loader;
    }

    private ParamValueLoader buildChildLoader(ParamKey key) {
        return manager.buildLoader(key, targetSpec.componentSpec());
    }

    private static void addToCollection(Collection collection, int index, Object bean) {
        if (collection instanceof List) {
            addToList((List) collection, index, bean);
        } else {
            collection.add(bean);
        }
    }

    private static void addToList(List list, int index, Object bean) {
        while (list.size() < index + 1) {
            list.add(null);
        }
        list.set(index, bean);
    }

    private void resolveInto(Collection collection, String value, ActContext context) {
        if (S.blank(value)) {
            return;
        }
        // support multiple path variables like /foo/id1,id2
        String[] sa = value.split("[,;]+");
        boolean isChar = this.isChar;
        for (String s : sa) {
            if (isChar) {
                char[] ca = s.toCharArray();
                for (char c: ca) {
                    collection.add(c);
                }
            } else {
                if (null != binder) {
                    collection.add(binder.resolve(null, s, context));
                } else {
                    collection.add(resolver.resolve(s));
                }
            }
        }
    }
}
