package de.espend.idea.shopware.reference;

import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.project.Project;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.ProcessingContext;
import com.jetbrains.php.PhpIcons;
import com.jetbrains.php.PhpIndex;
import com.jetbrains.php.lang.PhpLanguage;
import com.jetbrains.php.lang.parser.PhpElementTypes;
import com.jetbrains.php.lang.psi.elements.*;
import de.espend.idea.shopware.ShopwarePluginIcons;
import de.espend.idea.shopware.ShopwareProjectComponent;
import de.espend.idea.shopware.reference.provider.ControllerActionReferenceProvider;
import de.espend.idea.shopware.reference.provider.ControllerReferenceProvider;
import de.espend.idea.shopware.reference.provider.SmartyTemplateProvider;
import de.espend.idea.shopware.reference.provider.StringReferenceProvider;
import de.espend.idea.shopware.util.HookSubscriberUtil;
import de.espend.idea.shopware.util.ShopwareUtil;
import fr.adrienbrault.idea.symfony2plugin.util.MethodMatcher;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author Daniel Espendiller <[email protected]>
 */
public class EventSubscriberReferenceContributor extends PsiReferenceContributor {

    public static MethodMatcher.CallToSignature[] EVENT_SIGNATURES = new MethodMatcher.CallToSignature[] {
        new MethodMatcher.CallToSignature("\\Shopware_Components_Plugin_Bootstrap", "subscribeEvent"),
        new MethodMatcher.CallToSignature("\\Shopware_Components_Plugin_Bootstrap", "createEvent"),
    };

    public static MethodMatcher.CallToSignature[] TEMPLATE = new MethodMatcher.CallToSignature[] {
        new MethodMatcher.CallToSignature("\\Enlight_Template_Default", "extendsTemplate"),
        new MethodMatcher.CallToSignature("\\Enlight_View_Default", "loadTemplate"),
    };

    public static MethodMatcher.CallToSignature[] RESOURCE = new MethodMatcher.CallToSignature[] {
        new MethodMatcher.CallToSignature("\\Shopware\\Components\\Api\\Manager", "getResource"),
    };

    @Override
    public void registerReferenceProviders(@NotNull PsiReferenceRegistrar psiReferenceRegistrar) {

        psiReferenceRegistrar.registerReferenceProvider(
            PlatformPatterns.psiElement(StringLiteralExpression.class).withLanguage(PhpLanguage.INSTANCE),
            new PsiReferenceProvider() {
                @NotNull
                @Override
                public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) {

                    if(!ShopwareProjectComponent.isValidForProject(psiElement)) {
                        return new PsiReference[0];
                    }

                    if (MethodMatcher.getMatchedSignatureWithDepth(psiElement, EVENT_SIGNATURES) == null) {
                        return new PsiReference[0];
                    }

                    return new PsiReference[]{ new EventReference((StringLiteralExpression) psiElement) };

                }
            }
        );

        psiReferenceRegistrar.registerReferenceProvider(
            PlatformPatterns.psiElement(StringLiteralExpression.class).withLanguage(PhpLanguage.INSTANCE),
            new PsiReferenceProvider() {
                @NotNull
                @Override
                public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) {

                    if(!ShopwareProjectComponent.isValidForProject(psiElement)) {
                        return new PsiReference[0];
                    }

                    if (MethodMatcher.getMatchedSignatureWithDepth(psiElement, EVENT_SIGNATURES, 1) == null) {
                        return new PsiReference[0];
                    }

                    return new PsiReference[]{ new MethodReferenceProvider((StringLiteralExpression) psiElement) };

                }
            }
        );

        psiReferenceRegistrar.registerReferenceProvider(
            PlatformPatterns.psiElement(StringLiteralExpression.class).withLanguage(PhpLanguage.INSTANCE),
            new PsiReferenceProvider() {
                @NotNull
                @Override
                public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) {

                    if(!ShopwareProjectComponent.isValidForProject(psiElement)) {
                        return new PsiReference[0];
                    }

                    if (MethodMatcher.getMatchedSignatureWithDepth(psiElement, TEMPLATE) == null) {
                        return new PsiReference[0];
                    }

                    return new PsiReference[]{ new SmartyTemplateProvider((StringLiteralExpression) psiElement) };

                }
            }
        );

        psiReferenceRegistrar.registerReferenceProvider(
            PlatformPatterns.psiElement(StringLiteralExpression.class).withLanguage(PhpLanguage.INSTANCE),
            new PsiReferenceProvider() {
                @NotNull
                @Override
                public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) {

                    if(!ShopwareProjectComponent.isValidForProject(psiElement)) {
                        return new PsiReference[0];
                    }

                    if (MethodMatcher.getMatchedSignatureWithDepth(psiElement, RESOURCE) == null) {
                        return new PsiReference[0];
                    }

                    return new PsiReference[]{ new ResourcesReference((StringLiteralExpression) psiElement) };

                }
            }
        );

        // for your lazy developers to get unknown "extendsTemplate" calls
        psiReferenceRegistrar.registerReferenceProvider(
            PlatformPatterns.psiElement(StringLiteralExpression.class).withLanguage(PhpLanguage.INSTANCE),
            new PsiReferenceProvider() {
                @NotNull
                @Override
                public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) {

                    if(!ShopwareProjectComponent.isValidForProject(psiElement)) {
                        return new PsiReference[0];
                    }

                    if (!(psiElement instanceof StringLiteralExpression) || MethodMatcher.getMatchedSignatureWithDepth(psiElement, TEMPLATE) != null) {
                        return new PsiReference[0];
                    }

                    String tplName = ((StringLiteralExpression) psiElement).getContents();
                    if(StringUtils.isBlank(tplName) || !tplName.endsWith(".tpl")) {
                        return new PsiReference[0];
                    }

                    tplName = tplName.toLowerCase();
                    if(tplName.startsWith("\\")) {
                        tplName = tplName.substring(1);
                    }

                    if(!tplName.startsWith("frontend") && !tplName.startsWith("backend") && !tplName.startsWith("widgets") && !tplName.startsWith("documents")) {
                        return new PsiReference[0];
                    }

                    return new PsiReference[]{ new SmartyTemplateProvider((StringLiteralExpression) psiElement) };

                }
            }
        );

        // for your lazy developers to get unknown "extendsTemplate" calls
        psiReferenceRegistrar.registerReferenceProvider(
                PlatformPatterns.psiElement(StringLiteralExpression.class).inside(
                    PlatformPatterns.psiElement(ParameterList.class)
                )
            ,
            new PsiReferenceProvider() {
                @NotNull
                @Override
                public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) {

                    if(!ShopwareProjectComponent.isValidForProject(psiElement)) {
                        return new PsiReference[0];
                    }

                    if(!(psiElement instanceof StringLiteralExpression)) {
                        return new PsiReference[0];
                    }

                    ParameterList parameterList = PsiTreeUtil.getParentOfType(psiElement, ParameterList.class);
                    if (parameterList == null) {
                        return new PsiReference[0];
                    }

                    if(!(parameterList.getContext() instanceof MethodReference)) {
                        return new PsiReference[0];
                    }

                    if(PhpElementsUtil.isMethodReferenceInstanceOf((MethodReference) parameterList.getContext(), "\\Enlight_Controller_Router", "assemble")) {
                        return new PsiReference[0];
                    }

                    ArrayCreationExpression arrayCreation = PsiTreeUtil.getParentOfType(psiElement, ArrayCreationExpression.class);

                    if(arrayCreation != null) {
                        PsiElement arrayValue = psiElement.getParent();
                        if(arrayValue.getNode().getElementType() == PhpElementTypes.ARRAY_VALUE) {
                            PsiElement arrayHashElement = arrayValue.getParent();
                            if(arrayHashElement instanceof ArrayHashElement) {
                                PhpPsiElement key = ((ArrayHashElement) arrayHashElement).getKey();
                                if(key instanceof StringLiteralExpression) {
                                    String contents = ((StringLiteralExpression) key).getContents();

                                    if("controller".equals(contents)) {
                                        PsiElement arrayCreationElement = arrayHashElement.getParent();
                                        String module = null;
                                        if(arrayCreationElement instanceof ArrayCreationExpression) {
                                            module = PhpElementsUtil.getArrayHashValue((ArrayCreationExpression) arrayCreationElement, "module");
                                        }

                                        return new PsiReference[] { new ControllerReferenceProvider((StringLiteralExpression) psiElement, module) };
                                    }

                                    if("action".equals(contents)) {

                                        PsiElement arrayCreationElement = arrayHashElement.getParent();
                                        if(arrayCreationElement instanceof ArrayCreationExpression) {
                                            String controller = PhpElementsUtil.getArrayHashValue((ArrayCreationExpression) arrayCreationElement, "controller");
                                            String module = PhpElementsUtil.getArrayHashValue((ArrayCreationExpression) arrayCreationElement, "module");
                                            if(controller != null) {
                                                return new PsiReference[] { new ControllerActionReferenceProvider((StringLiteralExpression) psiElement, controller, module) };
                                            }
                                        }
                                    }

                                    if("module".equals(contents)) {
                                        return new PsiReference[] { new StringReferenceProvider((StringLiteralExpression) psiElement, "backend", "frontend", "widgets") };
                                    }

                                }
                            }
                        }

                        return new PsiReference[0];
                    }

                    return new PsiReference[0];
                }
            }
        );

        /**
         * public static function getSubscribedEvents() {
         *   return [
         *     'sBasket::sGetBasket::before' => '<caret>',
         *     'sBasket::sGetBasket::before' => ['<caret>', 123],
         *   ];
         * }
         */
        psiReferenceRegistrar.registerReferenceProvider(
            PlatformPatterns.psiElement(StringLiteralExpression.class)
                .withParent(PlatformPatterns.psiElement().withElementType(PhpElementTypes.ARRAY_VALUE))
                .inside(PlatformPatterns.psiElement(Method.class).withName("getSubscribedEvents"))
                .withLanguage(PhpLanguage.INSTANCE),
            new PsiReferenceProvider() {
                @NotNull
                @Override
                public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) {

                    if(!(psiElement instanceof StringLiteralExpression) || !ShopwareProjectComponent.isValidForProject(psiElement)) {
                        return new PsiReference[0];
                    }

                    PsiElement parent = psiElement.getParent();
                    if(parent == null || parent.getNode().getElementType() != PhpElementTypes.ARRAY_VALUE) {
                        return new PsiReference[0];
                    }

                    PsiElement arrayHashElement = parent.getContext();
                    if(arrayHashElement instanceof ArrayHashElement) {
                        // 'foo' => 'method'
                        PhpPsiElement arrayKey = ((ArrayHashElement) arrayHashElement).getKey();
                        if(arrayKey instanceof StringLiteralExpression) {
                            PsiElement arrayCreationExpression = arrayHashElement.getContext();
                            if(arrayCreationExpression instanceof ArrayCreationExpression) {
                                return new PsiReference[]{ new MethodReferenceProvider((StringLiteralExpression) psiElement) };
                            }
                        }
                    } else if(arrayHashElement instanceof ArrayCreationExpression) {
                        // 'foo' => ['method', 123]
                        PhpPsiElement firstPsiChild = ((ArrayCreationExpression) arrayHashElement).getFirstPsiChild();
                        if(firstPsiChild != null && firstPsiChild.getNode().getElementType() == PhpElementTypes.ARRAY_VALUE) {
                            StringLiteralExpression stringLiteral = ObjectUtils.tryCast(firstPsiChild.getFirstPsiChild(), StringLiteralExpression.class);
                            if(stringLiteral != null) {
                                return new PsiReference[]{ new MethodReferenceProvider(stringLiteral) };
                            }
                        }
                    }

                    return new PsiReference[0];
                }
            }
        );

    }

    public static class MethodReferenceProvider extends PsiPolyVariantReferenceBase<PsiElement> {

        final private String valueName;

        public MethodReferenceProvider(@NotNull StringLiteralExpression element) {
            super(element);
            this.valueName = element.getContents();
        }

        @NotNull
        @Override
        public ResolveResult[] multiResolve(boolean incompleteCode) {
            final List<ResolveResult> results = new ArrayList<>();

            PhpClass phpClass = PsiTreeUtil.getParentOfType(getElement(), PhpClass.class);
            if(phpClass == null) {
                return results.toArray(new ResolveResult[0]);
            }

            for(Method method: phpClass.getMethods()) {
                if(method.getModifier().isPublic() && !method.getName().startsWith("_") && method.getName().equals(this.valueName)) {
                    results.add(new PsiElementResolveResult(method));
                }
            }

            return results.toArray(new ResolveResult[0]);

        }

        @NotNull
        @Override
        public Object[] getVariants() {

            final List<LookupElement> lookupElements = new ArrayList<>();

            PhpClass phpClass = PsiTreeUtil.getParentOfType(getElement(), PhpClass.class);
            if(phpClass == null) {
                return lookupElements.toArray();
            }

            for(Method method: phpClass.getMethods()) {
                if(method.getModifier().isPublic() && !method.getName().startsWith("_")) {
                    lookupElements.add(LookupElementBuilder.create(method.getName()).withIcon(PhpIcons.METHOD).withTypeText("Method", true));
                }
            }

            return lookupElements.toArray();
        }
    }

    public static class EventReference extends PsiPolyVariantReferenceBase<PsiElement> {

        final private String valueName;

        public EventReference(@NotNull StringLiteralExpression element) {
            super(element);
            this.valueName = element.getContents();
        }

        @NotNull
        @Override
        public ResolveResult[] multiResolve(boolean incompleteCode) {
            final List<ResolveResult> results = new ArrayList<>();

            collectEvents(getElement().getProject(), (psiElement, value) -> {
                if(value.equals(valueName)) {
                    results.add(new PsiElementResolveResult(psiElement));
                }
            });

            return results.toArray(new ResolveResult[0]);

        }

        @NotNull
        @Override
        public Object[] getVariants() {

            final Set<String> events = new HashSet<>(HookSubscriberUtil.NOTIFY_EVENTS_MAP.keySet());

            final List<LookupElement> lookupElements = new ArrayList<>();
            collectEvents(getElement().getProject(), (psiElement, value) -> events.add(value));

            for(String event: events) {
                lookupElements.add(LookupElementBuilder.create(event).withIcon(ShopwarePluginIcons.SHOPWARE).withTypeText("Event", true));
            }

            return lookupElements.toArray();
        }
    }

    public static class ResourcesReference extends PsiPolyVariantReferenceBase<PsiElement> {

        final private String valueName;

        public ResourcesReference(@NotNull StringLiteralExpression element) {
            super(element);
            this.valueName = element.getContents();
        }

        @NotNull
        @Override
        public ResolveResult[] multiResolve(boolean incompleteCode) {
            List<ResolveResult> results = new ArrayList<>();

            PhpClass phpClass = ShopwareUtil.getResourceClass(getElement().getProject(), valueName);
            if(phpClass != null) {
                results.add(new PsiElementResolveResult(phpClass));
            }

            return results.toArray(new ResolveResult[0]);

        }

        @NotNull
        @Override
        public Object[] getVariants() {

            List<LookupElement> lookupElements = new ArrayList<>();

            for(Map.Entry<String, PhpClass> entry: ShopwareUtil.getResourceClasses(getElement().getProject()).entrySet()) {
                lookupElements.add(LookupElementBuilder.createWithIcon(entry.getValue()).withLookupString(entry.getKey()).withTypeText(entry.getValue().getPresentableFQN(), true));
            }

            return lookupElements.toArray();
        }
    }

    public static void collectControllerEvents(Project project, Collector collector) {

        PhpIndex phpIndex = PhpIndex.getInstance(project);

        Collection<PhpClass> phpClasses = phpIndex.getAllSubclasses("\\Enlight_Controller_Action");

        Pattern pattern = Pattern.compile(".*_(Frontend|Backend|Core|Widgets)_(\\w+)");


        for (PhpClass phpClass : phpClasses) {

            String className = phpClass.getName();
            Matcher matcher = pattern.matcher(className);

            if(matcher.find()) {
                String moduleName = matcher.group(1);
                String controller = matcher.group(2);


                // http://wiki.shopware.de/Shopware-4-Event-Auflistung-System-Events_detail_988.html
                // http://wiki.shopware.de/Shopware-4.1-Upgrade-Guide-fuer-Entwickler_detail_1297.html
                collector.collect(phpClass, String.format("Enlight_Controller_Action_PostDispatch_%s_%s", moduleName, controller));
                collector.collect(phpClass, String.format("Enlight_Controller_Action_PostDispatchSecure_%s_%s", moduleName, controller));
                collector.collect(phpClass, String.format("Enlight_Controller_Action_PreDispatch_%s_%s", moduleName, controller));

                collector.collect(phpClass, String.format("Enlight_Controller_Dispatcher_ControllerPath_%s_%s", moduleName, controller));
            }

        }
    }

    public static void collectEvents(Project project, Collector collector) {
        collectControllerEvents(project, collector);

        PhpIndex phpIndex = PhpIndex.getInstance(project);

        Collection<PhpClass> phpClasses = phpIndex.getAllSubclasses("\\Shopware_Components_Plugin_Bootstrap");

        for (PhpClass phpClass : phpClasses) {
            for (String methodName : new String[]{"install", "update"}) {
                Method method = phpClass.findMethodByName(methodName);

                if (method != null) {

                    PsiElement[] methodReferences = PsiTreeUtil.collectElements(method, element -> {
                        if (element instanceof MethodReference) {
                            return "subscribeEvent".equals(((MethodReference) element).getName());
                        }

                        return false;
                    });

                    for (PsiElement methodReference : methodReferences) {
                        if (methodReference instanceof MethodReference) {
                            PsiElement parameter = PsiElementUtils.getMethodParameterPsiElementAt((MethodReference) methodReference, 0);
                            String parameterString = PhpElementsUtil.getStringValue(parameter);
                            if (parameterString != null) {
                                collector.collect(parameter, parameterString);
                            }
                        }
                    }

                }
            }

        }

        for (final Map.Entry<String, Collection<String>> entry : HookSubscriberUtil.NOTIFY_EVENTS_MAP.entrySet()) {
            for (String value : entry.getValue()) {
                String[] split = value.split("\\.");
                Method classMethod = PhpElementsUtil.getClassMethod(project, split[0], split[1]);
                if(classMethod == null) {
                    continue;
                }

                final PsiElement[] count = {classMethod};
                classMethod.acceptChildren(new PsiRecursiveElementWalkingVisitor() {
                           @Override
                           public void visitElement(PsiElement element) {
                               if ((element instanceof StringLiteralExpression) && ((StringLiteralExpression) element).getContents().equals(entry.getKey())) {
                                   count[0] = element;
                               }
                               super.visitElement(element);
                           }
                       });

                collector.collect(count[0], entry.getKey());
            }
        }
    }

    public interface Collector {
        void collect(PsiElement psiElement, String value);
    }

}