/**
 * Copyright 2011-2015 John Ericksen
 *
 * 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.
 */
package org.androidtransfuse.gen.componentBuilder;

import com.sun.codemodel.*;
import org.androidtransfuse.TransfuseAnalysisException;
import org.androidtransfuse.adapter.ASTJDefinedClassType;
import org.androidtransfuse.adapter.ASTMethod;
import org.androidtransfuse.adapter.ASTType;
import org.androidtransfuse.adapter.PackageClass;
import org.androidtransfuse.adapter.element.ASTElementFactory;
import org.androidtransfuse.analysis.astAnalyzer.NonConfigurationAspect;
import org.androidtransfuse.annotations.Factory;
import org.androidtransfuse.experiment.*;
import org.androidtransfuse.gen.ClassGenerationUtil;
import org.androidtransfuse.gen.ClassNamer;
import org.androidtransfuse.gen.InvocationBuilder;
import org.androidtransfuse.gen.UniqueVariableNamer;
import org.androidtransfuse.gen.variableDecorator.TypedExpressionFactory;
import org.androidtransfuse.model.FieldInjectionPoint;
import org.androidtransfuse.model.InjectionNode;
import org.androidtransfuse.model.MethodDescriptor;
import org.androidtransfuse.model.TypedExpression;

import javax.inject.Inject;
import javax.inject.Named;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author John Ericksen
 */
public class NonConfigurationInstanceGenerator implements Generation {

    private final UniqueVariableNamer variableNamer;
    private final ClassNamer classNamer;
    private final ClassGenerationUtil generationUtil;
    private final InvocationBuilder invocationBuilder;
    private final TypedExpressionFactory typeExpressionFactory;
    private final ASTElementFactory astElementFactory;
    private final ASTMethod creationMethod;
    private final ASTType componentType;
    private final String getMethod;
    private final String setMethod;

    @Factory public interface NonconfigurationInstanceGeneratorFactory{
        NonConfigurationInstanceGenerator build(ASTMethod creationMethod, ASTType componentType, @Named("getMethod") String getMethod, @Named("setMethod") String setMethod);
    }

    @Inject
    public NonConfigurationInstanceGenerator(/*@Assisted*/ ASTMethod creationMethod, ASTType componentType, /*@Assisted*/ @Named("getMethod") String getMethod, /*@Assisted*/ @Named("setMethod") String setMethod, UniqueVariableNamer variableNamer, ClassNamer classNamer, ClassGenerationUtil generationUtil, InvocationBuilder invocationBuilder, TypedExpressionFactory typeExpressionFactory, ASTElementFactory astElementFactory) {
        this.variableNamer = variableNamer;
        this.classNamer = classNamer;
        this.generationUtil = generationUtil;
        this.invocationBuilder = invocationBuilder;
        this.typeExpressionFactory = typeExpressionFactory;
        this.astElementFactory = astElementFactory;
        this.creationMethod = creationMethod;
        this.setMethod = setMethod;
        this.getMethod = getMethod;
        this.componentType = componentType;
    }

    @Override
    public String getName() {
        return "NonConfigurationInstance Generator";
    }

    @Override
    public void schedule(final ComponentBuilder builder, ComponentDescriptor descriptor) {

        builder.add(creationMethod, GenerationPhase.POSTINJECTION, new ComponentMethodGenerator() {
            @Override
            public void generate(MethodDescriptor methodDescriptor, JBlock block) {
                final List<InjectionNode> nonConfigurationComponents = buildNonConfigurationComponents(builder.getExpressionMap());

                if (!nonConfigurationComponents.isEmpty()) {
                    try {
                        //generate holder type
                        final JDefinedClass nonConfigurationInstance = builder.getDefinedClass()._class(JMod.PRIVATE | JMod.STATIC | JMod.FINAL, classNamer.numberedClassName(new PackageClass(null, "NonConfigurationInstance")).build().getClassName());

                        JMethod constructor = nonConfigurationInstance.constructor(JMod.PRIVATE);
                        final Map<FieldInjectionPoint, JFieldVar> fieldMap = configureConstructor(constructor, nonConfigurationInstance, nonConfigurationComponents);

                        builder.add(creationMethod, GenerationPhase.REGISTRATION, new ComponentMethodGenerator() {
                            @Override
                            public void generate(MethodDescriptor methodDescriptor, JBlock block) {

                                //add on create init
                                //super.getLastNonConfigurationInstance()
                                JVar bodyVar = block.decl(nonConfigurationInstance, variableNamer.generateName(nonConfigurationInstance), JExpr.cast(nonConfigurationInstance, JExpr.invoke(getMethod)));
                                JBlock conditional = block._if(bodyVar.ne(JExpr._null()))._then();

                                //assign variables
                                for (InjectionNode nonConfigurationComponent : nonConfigurationComponents) {

                                    NonConfigurationAspect aspect = nonConfigurationComponent.getAspect(NonConfigurationAspect.class);
                                    for (FieldInjectionPoint nonConfigurationField : aspect.getFields()) {
                                        TypedExpression fieldExpression = typeExpressionFactory.build(nonConfigurationField.getInjectionNode().getASTType(), JExpr.ref(bodyVar, fieldMap.get(nonConfigurationField)));
                                        conditional.add(
                                                invocationBuilder.buildFieldSet(
                                                        new ASTJDefinedClassType(builder.getDefinedClass()),
                                                        fieldExpression,
                                                        nonConfigurationField,
                                                        builder.getExpressionMap().get(nonConfigurationComponent).getExpression())
                                        );
                                    }
                                }
                            }
                        });

                        ASTMethod onRetainNonConfigurationInstanceMethod = astElementFactory.findMethod(componentType, setMethod);
                        builder.add(onRetainNonConfigurationInstanceMethod, GenerationPhase.REGISTRATION, new ComponentMethodGenerator() {
                            @Override
                            public void generate(MethodDescriptor methodDescriptor, JBlock block) {
                                JInvocation construction = JExpr._new(nonConfigurationInstance);
                                JVar instanceDecl = block.decl(nonConfigurationInstance, variableNamer.generateName(nonConfigurationInstance)
                                        , construction);

                                for (InjectionNode injectionNode : nonConfigurationComponents) {

                                    NonConfigurationAspect aspect = injectionNode.getAspect(NonConfigurationAspect.class);
                                    for (FieldInjectionPoint fieldInjectionPoint : aspect.getFields()) {
                                        construction.arg(invocationBuilder.buildFieldGet(
                                                new ASTJDefinedClassType(builder.getDefinedClass()),
                                                fieldInjectionPoint.getField(),
                                                injectionNode.getASTType(),
                                                builder.getExpressionMap().get(injectionNode)
                                        ));
                                    }
                                }

                                block._return(instanceDecl);
                            }
                        });


                    } catch (JClassAlreadyExistsException e) {
                        throw new TransfuseAnalysisException("Class already defined", e);
                    }
                }
            }
        });
    }

    private Map<FieldInjectionPoint, JFieldVar> configureConstructor(JMethod constructor, JDefinedClass nonConfigurationInstance, List<InjectionNode> nonConfigurationComponents) {
        Map<FieldInjectionPoint, JFieldVar> fieldMap = new HashMap<FieldInjectionPoint, JFieldVar>();
        for (InjectionNode injectionNode : nonConfigurationComponents) {
            NonConfigurationAspect aspect = injectionNode.getAspect(NonConfigurationAspect.class);
            for (FieldInjectionPoint fieldInjectionPoint : aspect.getFields()) {
                //add all fields to constructor in order
                JClass fieldNodeType = generationUtil.ref(fieldInjectionPoint.getInjectionNode().getASTType());
                JVar param = constructor.param(fieldNodeType, variableNamer.generateName(fieldInjectionPoint.getInjectionNode()));
                JFieldVar field = nonConfigurationInstance.field(JMod.PRIVATE, fieldNodeType, variableNamer.generateName(injectionNode));
                constructor.body().assign(field, param);
                fieldMap.put(fieldInjectionPoint, field);
            }
        }
        return fieldMap;
    }

    private List<InjectionNode> buildNonConfigurationComponents(Map<InjectionNode, TypedExpression> expressionMap) {
        List<InjectionNode> nonConfigurationComponents = new ArrayList<InjectionNode>();
        for (Map.Entry<InjectionNode, TypedExpression> expressionEntry : expressionMap.entrySet()) {

            if (expressionEntry.getKey().containsAspect(NonConfigurationAspect.class)) {
                nonConfigurationComponents.add(expressionEntry.getKey());
            }

        }
        return nonConfigurationComponents;
    }
}
