package sk.sorien.pimpleplugin.pimple;

import com.intellij.codeInsight.completion.InsertHandler;
import com.intellij.codeInsight.completion.InsertionContext;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtilCore;
import com.jetbrains.php.PhpIndex;
import com.jetbrains.php.lang.psi.elements.*;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collection;

/**
 * @author Stanislav Turza
 */
public class Utils {

    public static final InsertHandler<LookupElement> CONTAINER_INSERT_HANDLER = new InsertHandler<LookupElement>() {
        @Override
        public void handleInsert(InsertionContext context, LookupElement item) {
            PsiElement element = PsiUtilCore.getElementAtOffset(context.getFile(), context.getStartOffset());
            context.getDocument().deleteString(context.getStartOffset() + item.getLookupString().length(), context.getStartOffset() + element.getText().length());
            // move caret after ]
            context.getEditor().getCaretModel().moveCaretRelatively(2, 0, false, false, true);
        }
    };

    private static Container findContainerForPimpleArrayAccess(ArrayAccessExpression arrayAccessElement, Boolean onlyParentContainers) {

        PsiElement children;
        PsiElement element = arrayAccessElement;
        while ((children = PsiTreeUtil.getChildOfType(element, ArrayAccessExpression.class)) != null) {
            element = children;
        }

        // check if var is pimple container
        Signature signature = new Signature();

        PsiElement signatureElement = PsiTreeUtil.getChildOfAnyType(element, Variable.class, FieldReference.class);
        if (signatureElement == null) {
            return null;
        }

        if (signatureElement instanceof Variable) {
            signature.set(((Variable) signatureElement).getSignature());
        }

        if (signatureElement instanceof FieldReference) {
            signature.set(((FieldReference) signatureElement).getSignature());
        }

        PhpIndex phpIndex = PhpIndex.getInstance(arrayAccessElement.getProject());

        ArrayList<String> parameters = new ArrayList<String>();
        if (!findPimpleContainer(phpIndex, signature.base, parameters)) {
            return null;
        }

        Container container = ContainerResolver.get(arrayAccessElement.getProject());

        // find proper base container from signature
        for (String parameter : parameters) {
            container = container.getContainers().get(getResolvedParameter(phpIndex, parameter));
            if (container == null)
                return null;
        }

        PsiElement lastElement = onlyParentContainers ? arrayAccessElement : arrayAccessElement.getParent();

        // find proper container
        while (!element.isEquivalentTo(lastElement) ) {

            ArrayIndex arrayIndex = ((ArrayAccessExpression)element).getIndex();
            if (arrayIndex == null) {
                return null;
            }

            PsiElement arrayIndexElement = arrayIndex.getValue();
            if (arrayIndexElement == null) {
                return null;
            }

            String containerName;

            if (arrayIndexElement instanceof StringLiteralExpression) {
                containerName = ((StringLiteralExpression) arrayIndexElement).getContents();
            }
            else if (arrayIndexElement instanceof MemberReference) {
                containerName = getResolvedParameter(phpIndex, ((MemberReference) arrayIndexElement).getSignature());
            }
            else return null;

            container = container.getContainers().get(containerName);
            if (container == null) {
                return null;
            }

            element = element.getParent();
        }

        return container;

    }

    public static Container findContainerForPimpleArrayAccessLiteral(StringLiteralExpression stringLiteralExpression) {

        PsiElement element = stringLiteralExpression.getParent();
        if (!(element instanceof ArrayIndex)) {
            return null;
        }

        element = element.getParent();
        if (element instanceof ArrayAccessExpression) {
            return findContainerForPimpleArrayAccess((ArrayAccessExpression) element, true);
        }

        return null;
    }

    public static Container findContainerForFirstParameterOfPimpleMethod(StringLiteralExpression stringLiteralExpression) {
        PsiElement parameterList = stringLiteralExpression.getParent();
        if (!(parameterList instanceof ParameterList)) {
            return null;
        }

        PsiElement[] params = ((ParameterList) parameterList).getParameters();
        if (!(params.length > 0 && params[0].isEquivalentTo(stringLiteralExpression))) {
            return null;
        }

        PsiElement methodReference = parameterList.getParent();
        if (!(methodReference instanceof MethodReference)) {
            return null;
        }

        // we have extend/raw method
        String methodReferenceName = ((MethodReference) methodReference).getName();
        if ((methodReferenceName == null) || !(methodReferenceName.equals("extend") || methodReferenceName.equals("raw"))) {
            return null;
        }

        return findContainerForMethodReference((MethodReference)methodReference);
    }

    public static Container findContainerForMethodReference(MethodReference methodReference) {
        Signature signature = new Signature();

        PsiElement signatureElement = PsiTreeUtil.getChildOfAnyType(methodReference, Variable.class, FieldReference.class, ArrayAccessExpression.class);
        if (signatureElement == null) {
            return null;
        }

        PhpIndex phpIndex = PhpIndex.getInstance(methodReference.getProject());

        Container container;

        if (signatureElement instanceof Variable || signatureElement instanceof FieldReference) {
            signature.set(((PhpReference) signatureElement).getSignature());

            ArrayList<String> parameters = new ArrayList<String>();
            if (!Utils.findPimpleContainer(phpIndex, signature.base, parameters)) {
                return null;
            }

            container = ContainerResolver.get(methodReference.getProject());

            // find proper base container from signature
            for (String parameter : parameters) {
                container = container.getContainers().get(getResolvedParameter(phpIndex, parameter));
                if (container == null)
                    return null;
            }

            return container;
        }

        if (signatureElement instanceof ArrayAccessExpression) {
            return findContainerForPimpleArrayAccess((ArrayAccessExpression) signatureElement, false);
        }

        return null;
    }

    public static Boolean isPimpleContainerClass(PhpClass phpClass) {

        if (phpClass == null) {
            return false;
        } else if (isPimpleContainerBaseClass(phpClass.getFQN())) {
            return true;
        } else {
            Integer counter = 0;

            while ((phpClass = phpClass.getSuperClass()) != null && counter < 5) {

                if (isPimpleContainerBaseClass(phpClass.getFQN())) {
                    return true;
                }

                counter++;
            }
            return false;
        }
    }

    private static Boolean isPimpleContainerBaseClass(String className) {
        return className != null && (className.equals("\\Silex\\Application") || className.equals("\\Pimple\\Container") || className.equals("\\Pimple"));
    }

    public static Boolean findPimpleContainer(PhpIndex phpIndex, String expression, ArrayList<String> parameters) {
        return findPimpleContainer(phpIndex, expression, parameters, 0);
    }

    private static Boolean findPimpleContainer(PhpIndex phpIndex, String expression, ArrayList<String> parameters, int depth) {

        if (++depth > 5) {
            return false;
        }

        Signature signature = new Signature(expression);
        Collection<? extends PhpNamedElement> collection;

        if (expression.startsWith("#")) {
            collection = phpIndex.getBySignature(signature.base, null, 0);
        } else {
            collection = phpIndex.getClassesByFQN(signature.base);
        }

        if (collection.size() == 0) {
            return false;
        }

        PhpNamedElement element = collection.iterator().next();

        if (element instanceof PhpClass) {
            if (Utils.isPimpleContainerClass((PhpClass) element)) {
                if (signature.hasParameter()) {
                    parameters.add(signature.parameter);
                }
                return true;
            }
        }

        for (String type : element.getType().getTypes()) {

            if (findPimpleContainer(phpIndex, type, parameters, depth)) {
                if (signature.hasParameter()) {
                    parameters.add(signature.parameter);
                }
                return true;
            }
        }

        return false;
    }

    public static String normalizedString(StringLiteralExpression text) {
        return  text.isSingleQuote() ? text.getContents(): text.getContents().replace("\\\\", "\\");
    }

    private static String getStringValue(@Nullable PsiElement psiElement) {
        return getStringValue(psiElement, 0);
    }

    @Nullable
    private static String getStringValue(@Nullable PsiElement psiElement, int depth) {

        if (psiElement == null || ++depth > 5) {
            return null;
        }

        if (psiElement instanceof StringLiteralExpression) {
            String resolvedString = normalizedString((StringLiteralExpression) psiElement);
            if (StringUtils.isEmpty(resolvedString)) {
                return null;
            }

            return resolvedString;
        }

        if (psiElement instanceof Field) {
            return getStringValue(((Field) psiElement).getDefaultValue(), depth);
        }

        if (psiElement instanceof PhpReference) {

            PsiReference psiReference = psiElement.getReference();
            if (psiReference == null) {
                return null;
            }

            PsiElement ref = psiReference.resolve();
            if (ref instanceof PhpReference) {
                return getStringValue(psiElement, depth);
            }

            if (ref instanceof Field) {
                PsiElement resolved = ((Field) ref).getDefaultValue();

                if (resolved instanceof StringLiteralExpression) {
                    return normalizedString((StringLiteralExpression) resolved);
                }
            }
        }

        return null;
    }

    public static String getResolvedParameter(PhpIndex phpIndex, String parameter) {

        // PHP 5.5 class constant: "Class\Foo::class"
        if(parameter.startsWith("#K#C")) {
            // PhpStorm9: #K#C\Class\Foo.class
            if(parameter.endsWith(".class")) {
                return parameter.substring(5, parameter.length() - 6);
            }

            // PhpStorm8: #K#C\Class\Foo.
            // workaround since signature has empty type
            if(parameter.endsWith(".")) {
                return parameter.substring(5, parameter.length() - 1);
            }
        }

        // #P#C\Class\Foo.property
        // #K#C\Class\Foo.CONST
        if(parameter.startsWith("#")) {

            Collection<? extends PhpNamedElement> signTypes = phpIndex.getBySignature(parameter);
            if(signTypes.size() == 0) {
                return "";
            }

            parameter = Utils.getStringValue(signTypes.iterator().next());
            if(parameter == null) {
                return "";
            }
        }

        return parameter;
    }

    public static Boolean isParameter(PsiElement element, PsiElement[] parameters, Integer index) {
        return (parameters.length > index) && (parameters[index].isEquivalentTo(element));
    }
}