package fr.adrienbrault.idea.symfony2plugin.util.psi.matcher;

import com.intellij.openapi.util.Pair;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.patterns.PsiElementPattern;
import com.intellij.psi.PsiElement;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.php.lang.parser.PhpElementTypes;
import com.jetbrains.php.lang.patterns.PhpPatterns;
import com.jetbrains.php.lang.psi.elements.*;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
import fr.adrienbrault.idea.symfony2plugin.util.psi.PsiElementAssertUtil;
import org.apache.commons.lang.ArrayUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.List;

/**
 * @author Daniel Espendiller <daniel@espendiller.net>
 */
public class ArrayValueWithKeyAndNewExpressionMatcher {

    /**
     * $menu->addChild([
     *   'route' => '<caret>',
     * ]);
     */
    @NotNull
    public static PsiElementPattern.Capture<PsiElement> pattern() {
        return PhpPatterns.psiElement().withParent(
            PlatformPatterns.psiElement(StringLiteralExpression.class).withParent(
                PlatformPatterns.psiElement(PhpElementTypes.ARRAY_VALUE).inside(ParameterList.class)
            )
        );
    }

    /**
     * A array value inside array and method reference
     *
     * $menu->addChild([
     *   'route' => '<caret>',
     * ]);
     */
    @Nullable
    public static Result match(@NotNull PsiElement psiElement, @NotNull Matcher matcher) {
        PsiElement arrayValue = psiElement.getParent();
        if(PsiElementAssertUtil.isNotNullAndIsElementType(arrayValue, PhpElementTypes.ARRAY_VALUE)) {
            PsiElement arrayHashElement = arrayValue.getParent();
            if(arrayHashElement instanceof ArrayHashElement) {
                PhpPsiElement arrayKey = ((ArrayHashElement) arrayHashElement).getKey();
                if(arrayKey != null && ArrayUtils.contains(matcher.getArrayKeys(), PhpElementsUtil.getStringValue(arrayKey))) {
                    PsiElement arrayCreationExpression = arrayHashElement.getParent();
                    if(arrayCreationExpression instanceof ArrayCreationExpression) {
                        Pair<NewExpressionCall, NewExpression> matchPair = matchesMethodCall((ArrayCreationExpression) arrayCreationExpression, matcher.getNewExpressionCalls());
                        if(matchPair != null) {
                            return new Result(matchPair.getFirst(), matchPair.getSecond(), arrayKey);
                        }
                    }
                }
            }
        }

        return null;
    }

    @Nullable
    private static Pair<NewExpressionCall, NewExpression> matchesMethodCall(@NotNull ArrayCreationExpression arrayCreationExpression, @NotNull NewExpressionCall[] classes) {

        // get parameter, we less then 0 we are not inside method reference
        int parameterIndex = PsiElementUtils.getParameterIndexValue(arrayCreationExpression);
        if(parameterIndex < 0) {
            return null;
        }

        // filter index
        List<NewExpressionCall> filter = ContainerUtil.filter(classes, expressionCall ->
            expressionCall.getIndex() == parameterIndex
        );

        if(filter.size() == 0) {
            return null;
        }

        PsiElement parameterList = arrayCreationExpression.getParent();
        if(!(parameterList instanceof ParameterList)) {
            return null;
        }

        PsiElement newExpression = parameterList.getParent();
        if (!(newExpression instanceof NewExpression)) {
            return null;
        }

        for (NewExpressionCall aClass : filter) {
            if(PhpElementsUtil.getNewExpressionPhpClassWithInstance((NewExpression) newExpression, aClass.getClazz()) == null) {
                continue;
            }

            return Pair.create(aClass, (NewExpression) newExpression);
        }

        return null;
    }

    public static class Matcher {
        @NotNull
        private final String[] arrayKeys;

        @NotNull
        private final NewExpressionCall[] classes;

        public Matcher(@NotNull String[] arrayKeys, @NotNull NewExpressionCall... classes) {
            this.arrayKeys = arrayKeys;
            this.classes = classes;
        }

        public Matcher(@NotNull String arrayKeys, @NotNull NewExpressionCall... classes) {
            this(new String[] {arrayKeys}, classes);
        }

        public Matcher(@NotNull String arrayKeys, @NotNull String clazz) {
            this(new String[] {arrayKeys}, new NewExpressionCall(clazz, 0));
        }

        @NotNull
        public String[] getArrayKeys() {
            return arrayKeys;
        }

        @NotNull
        public NewExpressionCall[] getNewExpressionCalls() {
            return classes;
        }
    }

    public static class Result {
        @NotNull
        private final NewExpression newExpression;

        @NotNull
        private final NewExpressionCall expressionCall;

        @NotNull
        private final PsiElement arrayKey;

        public Result(@NotNull NewExpressionCall expressionCall, @NotNull NewExpression newExpression, @NotNull PsiElement arrayKey) {
            this.expressionCall = expressionCall;
            this.newExpression = newExpression;
            this.arrayKey = arrayKey;
        }

        @NotNull
        public NewExpression getNewExpression() {
            return newExpression;
        }

        @NotNull
        public NewExpressionCall getExpressionCall() {
            return expressionCall;
        }

        @NotNull
        public PsiElement getArrayKey() {
            return arrayKey;
        }
    }

    public static class NewExpressionCall {

        @NotNull
        private final String clazz;

        private int index = 0;

        public NewExpressionCall(@NotNull String clazz) {
            this(clazz, 0);
        }

        public NewExpressionCall(@NotNull String clazz, int index) {
            this.clazz = clazz;
            this.index = index;
        }

        @NotNull
        public String getClazz() {
            return clazz;
        }

        public int getIndex() {
            return index;
        }
    }
}
