package com.yheriatovych.reductor.processor.autoreducer;

import com.google.auto.common.MoreElements;
import com.google.auto.common.MoreTypes;
import com.yheriatovych.reductor.annotations.ActionCreator;
import com.yheriatovych.reductor.annotations.AutoReducer;
import com.yheriatovych.reductor.processor.*;
import com.yheriatovych.reductor.processor.actioncreator.ActionCreatorElement;

import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class ReduceAction {

    public final String action;
    public final List<VariableElement> args;
    public final ExecutableElement executableElement;
    public final boolean generateActionCreator;

    private ReduceAction(String action, List<VariableElement> args, ExecutableElement executableElement, boolean generateActionCreator) {
        this.executableElement = executableElement;
        this.args = args;
        this.action = action;
        this.generateActionCreator = generateActionCreator;
    }

    public static ReduceAction parseReduceAction(Env env, TypeMirror stateType, ExecutableElement element, Map<String, ActionCreatorElement> knownActionCreators) throws ValidationException {
        AutoReducer.Action action = element.getAnnotation(AutoReducer.Action.class);
        if (action == null) return null;

        String actionNameConstant = action.value();
        boolean generateActionCreator = action.generateActionCreator();
        TypeMirror actionCreatorType = getCreator(action, env.getElements(), env, element);

        ValidationUtils.validateReturnsState(env, stateType, element);
        ValidationUtils.validateIsNotPrivate(element);

        List<? extends VariableElement> parameters = element.getParameters();
        if (parameters.size() == 0) {
            throw new ValidationException(element, "Method %s should have at least 1 arguments: state of type %s", element, stateType);
        }

        List<? extends VariableElement> argumentVariables = parameters.subList(1, parameters.size());
        ArrayList<VariableElement> args = new ArrayList<>();
        for (VariableElement argumentVariable : argumentVariables) {
            args.add(argumentVariable);
        }
        VariableElement firstParam = parameters.get(0);
        if (!env.getTypes().isAssignable(stateType, firstParam.asType())) {
            throw new ValidationException(firstParam, "First parameter %s of method %s should have the same type as state (%s)", firstParam, element, stateType);
        }

        if (actionCreatorType != null) {
            validateActionCreator(element, actionNameConstant, actionCreatorType, args, knownActionCreators, env);
        }

        return new ReduceAction(actionNameConstant, args, element, generateActionCreator);
    }

    private static TypeMirror getCreator(AutoReducer.Action action, Elements elements, Env env, ExecutableElement element) {
        TypeMirror typeMirror;
        try {
            Class<?> fromClass = action.from();
            String className = fromClass.getCanonicalName();
            typeMirror = elements.getTypeElement(className).asType();
        } catch (MirroredTypeException mte) {
            typeMirror = mte.getTypeMirror();
        }

        //Void is used by default meaning it's not linked to any action creator
        return env.getTypes().isSameType(typeMirror, env.asType(Void.class))
                ? null
                : typeMirror;
    }

    private static void validateActionCreator(ExecutableElement element,
                                              String actionName,
                                              TypeMirror actionCreator,
                                              ArrayList<VariableElement> args,
                                              Map<String, ActionCreatorElement> knownActionCreators,
                                              Env env) throws ValidationException {
        Element actionCreatorElement = MoreTypes.asElement(actionCreator);
        if (!MoreElements.isAnnotationPresent(actionCreatorElement, ActionCreator.class)) {
            throw new ValidationException(element, "Action creator %s should be annotated with @%s", actionCreator, ActionCreator.class.getSimpleName());
        }

        ActionCreatorElement creatorElement = knownActionCreators.get(env.getElements().getBinaryName((TypeElement) actionCreatorElement).toString());
        if (creatorElement == null) {
            throw new ElementNotReadyException();
        }
        if (!creatorElement.hasAction(actionName, args)) {
            throw new ValidationException(element, "Cannot find action creator for action \"%s\" and args %s in interface %s", actionName, toString(args), creatorElement.getName(env));
        }
    }

    private static String toString(List<VariableElement> arguments) {
        return "[" + Utils.join(", ", Utils.map(arguments, new Utils.Func1<VariableElement, String>() {
            @Override
            public String call(VariableElement arg) {
                return arg.asType().toString();
            }
        })) + "]";
    }

    public String getMethodName() {
        return executableElement.getSimpleName().toString();
    }

}