package autodagger.compiler;

import com.google.auto.common.MoreElements;
import com.google.auto.common.MoreTypes;
import com.google.common.collect.ImmutableSet;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;

import org.apache.commons.lang3.StringUtils;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

import autodagger.AutoComponent;
import autodagger.AutoSubcomponent;
import autodagger.compiler.utils.AutoComponentClassNameUtil;
import processorworkflow.AbstractComposer;
import processorworkflow.AbstractProcessing;
import processorworkflow.Errors;
import processorworkflow.Logger;
import processorworkflow.ProcessingBuilder;

/**
 * @author Lukasz Piliszczuk - [email protected]
 */
public class ComponentProcessing extends AbstractProcessing<ComponentSpec, State> {

    /**
     * Build all extractors first, then build all builders, because
     * we want to gather all targets first
     */
    private final Set<ComponentExtractor> extractors;

    public ComponentProcessing(Elements elements, Types types, Errors errors, State state) {
        super(elements, types, errors, state);
        extractors = new HashSet<>();
    }

    @Override
    public Set<Class<? extends Annotation>> supportedAnnotations() {
        Set set = ImmutableSet.of(AutoComponent.class);
        return set;
    }

    @Override
    protected void processElements(Set<? extends Element> annotationElements) {
        super.processElements(annotationElements);
        if (errors.hasErrors()) {
            return;
        }

        processExtractors();
    }

    @Override
    public boolean processElement(Element element, Errors.ElementErrors elementErrors) {
        if (ElementKind.ANNOTATION_TYPE.equals(element.getKind())) {
            // @AutoComponent is applied on another annotation, find out the targets of that annotation
            Set<? extends Element> targetElements = roundEnvironment.getElementsAnnotatedWith(MoreElements.asType(element));
            for (Element targetElement : targetElements) {
                process(targetElement, element);
                if (errors.hasErrors()) {
                    return false;
                }
            }
            return true;
        }

        process(element, element);

        if (errors.hasErrors()) {
            return false;
        }

        return true;
    }

    private void process(Element targetElement, Element element) {
        ComponentExtractor extractor = new ComponentExtractor(targetElement, element, types, elements, errors);
        if (errors.hasErrors()) {
            return;
        }

        extractors.add(extractor);
    }

    private void processExtractors() {
        for (ComponentExtractor extractor : extractors) {
            ComponentSpec spec = new Builder(extractor, errors).build();
            if (errors.hasErrors()) {
                return;
            }

            specs.add(spec);
        }
    }


    @Override
    public AbstractComposer<ComponentSpec> createComposer() {
        return new ComponentComposer(specs);
    }

    private class Builder extends ProcessingBuilder<ComponentExtractor, ComponentSpec> {

        public Builder(ComponentExtractor extractor, Errors errors) {
            super(extractor, errors);
        }

        @Override
        protected ComponentSpec build() {
            ComponentSpec componentSpec = new ComponentSpec(AutoComponentClassNameUtil.getComponentClassName(extractor.getComponentElement()));
            componentSpec.setTargetTypeName(TypeName.get(extractor.getTargetTypeMirror()));

            if (extractor.getScopeAnnotationTypeMirror() != null) {
                componentSpec.setScopeAnnotationSpec(AnnotationSpec.get(extractor.getScopeAnnotationTypeMirror()));
            }

            // injectors
            componentSpec.setInjectorSpecs(ProcessingUtil.getAdditions(extractor.getTargetTypeMirror(), state.getInjectorExtractors()));

            // exposed
            componentSpec.setExposeSpecs(ProcessingUtil.getAdditions(extractor.getTargetTypeMirror(), state.getExposeExtractors()));

            // dependencies
            componentSpec.setDependenciesTypeNames(getDependencies());

            // superinterfaces
            componentSpec.setSuperinterfacesTypeNames(ProcessingUtil.getTypeNames(extractor.getSuperinterfacesTypeMirrors()));

            // modules
            componentSpec.setModulesTypeNames(ProcessingUtil.getTypeNames(extractor.getModulesTypeMirrors()));

            // subcomponents
            componentSpec.setSubcomponentsSpecs(getSubcomponents());

            return componentSpec;
        }

        private List<MethodSpec> getSubcomponents() {
            if (extractor.getSubcomponentsTypeMirrors().isEmpty()) {
                return Collections.emptyList();
            }

            List<MethodSpec> methodSpecs = new ArrayList<>(extractor.getSubcomponentsTypeMirrors().size());
            for (TypeMirror typeMirror : extractor.getSubcomponentsTypeMirrors()) {
                Element e = MoreTypes.asElement(typeMirror);
                TypeName typeName;
                String name;
                if (MoreElements.isAnnotationPresent(e, AutoSubcomponent.class)) {
                    ClassName cls = AutoComponentClassNameUtil.getComponentClassName(e);
                    typeName = cls;
                    name = cls.simpleName();
                } else {
                    typeName = TypeName.get(typeMirror);
                    name = e.getSimpleName().toString();
                }

                List<TypeMirror> modules = state.getSubcomponentModules(typeMirror);
                List<ParameterSpec> parameterSpecs;
                if(modules != null) {
                    parameterSpecs = new ArrayList<>(modules.size());
                    int count = 0;
                    for (TypeMirror moduleTypeMirror : modules) {
                        parameterSpecs.add(ParameterSpec.builder(TypeName.get(moduleTypeMirror), String.format("module%d", ++count)).build());
                    }
                } else {
                    parameterSpecs = new ArrayList<>(0);
                }

                methodSpecs.add(MethodSpec.methodBuilder("plus" + name)
                        .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
                        .addParameters(parameterSpecs)
                        .returns(typeName)
                        .build());
            }

            return methodSpecs;
        }

        private List<TypeName> getDependencies() {
            List<TypeName> typeNames = new ArrayList<>();
            if (extractor.getDependenciesTypeMirrors() == null) {
                return typeNames;
            }

            mainLoop:
            for (TypeMirror typeMirror : extractor.getDependenciesTypeMirrors()) {
                // check if dependency type mirror references an @AutoComponent target
                // if so, build the TypeName that matches the target component
                for (ComponentExtractor componentExtractor : extractors) {
                    if (componentExtractor == extractor) {
                        // ignore self
                        continue;
                    }

                    if (ProcessingUtil.areTypesEqual(componentExtractor.getTargetTypeMirror(), typeMirror)) {
                        typeNames.add(AutoComponentClassNameUtil.getComponentClassName(componentExtractor.getComponentElement()));
                        continue mainLoop;
                    }
                }

                typeNames.add(TypeName.get(typeMirror));
            }

            return typeNames;
        }
    }
}