/**
 * 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.experiment.generators;

import com.google.common.collect.ImmutableList;
import com.sun.codemodel.*;
import org.androidtransfuse.adapter.*;
import org.androidtransfuse.adapter.classes.ASTClassFactory;
import org.androidtransfuse.analysis.ManualSuperGenerator;
import org.androidtransfuse.analysis.astAnalyzer.ListenerAspect;
import org.androidtransfuse.analysis.astAnalyzer.ManualSuperAspect;
import org.androidtransfuse.event.SuperCaller;
import org.androidtransfuse.experiment.*;
import org.androidtransfuse.gen.ClassGenerationUtil;
import org.androidtransfuse.gen.InvocationBuilder;
import org.androidtransfuse.gen.UniqueVariableNamer;
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.*;

public class MethodCallbackGenerator implements Generation {

    private final ASTType eventAnnotation;
    private final InvocationBuilder invocationBuilder;
    private final ASTMethod eventMethod;
    private final ASTMethod creationMethod;
    private final ASTType superCallerType;
    private final JCodeModel codeModel;
    private final UniqueVariableNamer namer;
    private final ClassGenerationUtil generationUtil;
    private final boolean nullDelegateCheck;

    @Inject
    public MethodCallbackGenerator(/*@Assisted*/ ASTType eventAnnotation, /*@Assisted*/ @Named("eventMethod") ASTMethod eventMethod, /*@Assisted */ @Named("creationMethod") ASTMethod creationMethod, /*@Assisted*/ boolean nullDelegateCheck, InvocationBuilder invocationBuilder, ASTClassFactory astClassFactory, JCodeModel codeModel, UniqueVariableNamer namer, ClassGenerationUtil generationUtil) {
        this.eventAnnotation = eventAnnotation;
        this.invocationBuilder = invocationBuilder;
        this.eventMethod = eventMethod;
        this.creationMethod = creationMethod;
        this.codeModel = codeModel;
        this.namer = namer;
        this.generationUtil = generationUtil;
        this.superCallerType = astClassFactory.getType(SuperCaller.class);
        this.nullDelegateCheck = nullDelegateCheck;
    }

    @Override
    public String getName() {
        return "Method Callback Generator @" + eventAnnotation.getPackageClass().getClassName() + " -> " + eventMethod;
    }

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

        builder.add(creationMethod, GenerationPhase.POSTINJECTION, new ComponentMethodGenerator() {
            @Override
            public void generate(MethodDescriptor methodDescriptor, JBlock block) {
                for (Map.Entry<InjectionNode, TypedExpression> injectionNodeJExpressionEntry : builder.getExpressionMap().entrySet()) {
                    ListenerAspect methodCallbackAspect = injectionNodeJExpressionEntry.getKey().getAspect(ListenerAspect.class);
                    final TypedExpression eventReceiverExpression = injectionNodeJExpressionEntry.getValue();

                    if (methodCallbackAspect != null && methodCallbackAspect.contains(eventAnnotation)) {
                        Set<ASTMethod> methods = methodCallbackAspect.getListeners(eventAnnotation);

                        for (final ASTMethod methodCallback : methods) {

                            final boolean containsSuperCaller = containsSuperCaller(methodCallback.getParameters());
                            if(containsSuperCaller){
                                if(!injectionNodeJExpressionEntry.getKey().containsAspect(ManualSuperAspect.class)){
                                    injectionNodeJExpressionEntry.getKey().addAspect(new ManualSuperAspect());
                                }

                                ManualSuperAspect manualSuperAspect = injectionNodeJExpressionEntry.getKey().getAspect(ManualSuperAspect.class);
                                manualSuperAspect.add(eventMethod);
                            }

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

                                    Map<ASTType, Queue<JExpression>> methodArgumentExpressions = buildMethodArgumentExpressions(methodDescriptor);
                                    if(containsSuperCaller) {
                                        methodArgumentExpressions.put(superCallerType, new LinkedList<JExpression>(ImmutableList.of(buildSuperCaller(eventMethod))));
                                    }

                                    List<JExpression> matchedExpressions = matchMethodArguments(methodCallback.getParameters(), methodArgumentExpressions);

                                    JStatement methodCall = invocationBuilder.buildMethodCall(
                                            new ASTJDefinedClassType(builder.getDefinedClass()),
                                            new ASTJDefinedClassType(builder.getDefinedClass()),
                                            methodCallback,
                                            matchedExpressions,
                                            eventReceiverExpression
                                    );

                                    if(nullDelegateCheck){
                                        block._if(eventReceiverExpression.getExpression().ne(JExpr._null()))._then().block()
                                                .add(methodCall);
                                    }
                                    else{
                                        block.add(methodCall);
                                    }

                                }
                            });
                        }
                    }
                }
            }
        });
    }

    private JExpression buildSuperCaller(ASTMethod eventMethod) {

        JDefinedClass superCallerClass = codeModel.anonymousClass(SuperCaller.class);

        JMethod callMethod = superCallerClass.method(JMod.PUBLIC, Object.class, SuperCaller.CALL_METHOD);
        JVar objectVarargs = callMethod.varParam(Object.class, namer.generateName(Object.class));

        JInvocation superInvocation = JExpr.ref(ManualSuperGenerator.SUPER_NAME).invoke(eventMethod.getName());

        for(int i = 0; i < eventMethod.getParameters().size(); i++){
            ASTParameter parameter = eventMethod.getParameters().get(i);
            superInvocation = superInvocation.arg(JExpr.cast(generationUtil.ref(parameter.getASTType()), objectVarargs.component(JExpr.lit(i))));
        }

        JBlock body = callMethod.body();
        if(eventMethod.getReturnType().equals(ASTVoidType.VOID)) {
            body.add(superInvocation);
            body._return(JExpr._null());
        }
        else{
            body._return(superInvocation);
        }

        return JExpr._new(superCallerClass);
    }

    private boolean containsSuperCaller(ImmutableList<ASTParameter> parameters) {

        for (ASTParameter parameter : parameters) {
            if(parameter.getASTType().equals(superCallerType)){
                return true;
            }
        }
        return false;
    }

    private List<JExpression> matchMethodArguments(List<ASTParameter> parametersToMatch, Map<ASTType, Queue<JExpression>> expressions) {
        List<JExpression> arguments = new ArrayList<JExpression>();

        for (ASTParameter callParameter : parametersToMatch) {
            ASTType type = callParameter.getASTType();
            if(expressions.containsKey(type) && !expressions.get(type).isEmpty()){
                arguments.add(expressions.get(type).remove());
            }
            else{
                //todo: validation error
            }
        }

        return arguments;
    }

    private Map<ASTType, Queue<JExpression>> buildMethodArgumentExpressions(MethodDescriptor method){
        Map<ASTType, Queue<JExpression>> argumentExpressions = new HashMap<ASTType, Queue<JExpression>>();
        for (ASTParameter parameter : method.getASTMethod().getParameters()) {
            ASTType type = parameter.getASTType();
            if(!argumentExpressions.containsKey(type)){
                argumentExpressions.put(type, new LinkedList<JExpression>());
            }

            argumentExpressions.get(type).add(method.getParameter(parameter).getExpression());
        }
        return argumentExpressions;
    }
}