package fr.adrienbrault.idea.symfony2plugin.form.util;

import com.intellij.codeInsight.completion.CompletionResultSet;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.impl.source.xml.XmlDocumentImpl;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.jetbrains.php.PhpIndex;
import com.jetbrains.php.lang.parser.PhpElementTypes;
import com.jetbrains.php.lang.psi.PhpPsiUtil;
import com.jetbrains.php.lang.psi.elements.*;
import com.jetbrains.php.lang.psi.elements.impl.PhpTypedElementImpl;
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
import fr.adrienbrault.idea.symfony2plugin.form.FormTypeLookup;
import fr.adrienbrault.idea.symfony2plugin.form.dict.EnumFormTypeSource;
import fr.adrienbrault.idea.symfony2plugin.form.dict.FormTypeClass;
import fr.adrienbrault.idea.symfony2plugin.form.dict.FormTypeServiceParser;
import fr.adrienbrault.idea.symfony2plugin.util.MethodMatcher;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
import fr.adrienbrault.idea.symfony2plugin.util.psi.PsiElementAssertUtil;
import fr.adrienbrault.idea.symfony2plugin.util.service.ServiceXmlParserFactory;
import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.yaml.psi.YAMLFile;
import org.jetbrains.yaml.psi.YAMLKeyValue;

import java.util.*;

/**
 * @author Daniel Espendiller <[email protected]>
 */
public class FormUtil {

    final public static String ABSTRACT_FORM_INTERFACE = "\\Symfony\\Component\\Form\\FormTypeInterface";
    final public static String FORM_EXTENSION_INTERFACE = "\\Symfony\\Component\\Form\\FormTypeExtensionInterface";

    public static MethodMatcher.CallToSignature[] PHP_FORM_BUILDER_SIGNATURES = new MethodMatcher.CallToSignature[] {
        new MethodMatcher.CallToSignature("\\Symfony\\Component\\Form\\FormBuilderInterface", "add"),
        new MethodMatcher.CallToSignature("\\Symfony\\Component\\Form\\FormBuilderInterface", "create"),
        new MethodMatcher.CallToSignature("\\Symfony\\Component\\Form\\FormInterface", "add"),
        new MethodMatcher.CallToSignature("\\Symfony\\Component\\Form\\FormInterface", "create")
    };

    @Nullable
    public static PhpClass getFormTypeToClass(Project project, @Nullable String formType) {
        return new FormTypeCollector(project).collect().getFormTypeToClass(formType);
    }

    public static Collection<LookupElement> getFormTypeLookupElements(Project project) {

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

        FormUtil.FormTypeCollector collector = new FormUtil.FormTypeCollector(project).collect();

        for(Map.Entry<String, FormTypeClass> entry: collector.getFormTypesMap().entrySet()) {
            String name = entry.getValue().getName();
            String typeText = entry.getValue().getPhpClassName();

            PhpClass phpClass = entry.getValue().getPhpClass();
            if(phpClass != null) {
                typeText = phpClass.getName();
            }

            FormTypeLookup formTypeLookup = new FormTypeLookup(typeText, name);
            if(entry.getValue().getSource() == EnumFormTypeSource.INDEX) {
                formTypeLookup.withWeak(true);
            }

            lookupElements.add(formTypeLookup);
        }

        return lookupElements;
    }

    public static MethodReference[] getFormBuilderTypes(Method method) {
        List<MethodReference> methodReferences = new ArrayList<>();

        PsiTreeUtil.collectElements(method, methodReference -> {
            if (methodReference instanceof MethodReference) {
                String methodName = ((MethodReference) methodReference).getName();
                if (methodName != null && (methodName.equals("add") || methodName.equals("create"))) {
                    if(PhpElementsUtil.isMethodReferenceInstanceOf((MethodReference) methodReference, FormUtil.PHP_FORM_BUILDER_SIGNATURES)) {
                        methodReferences.add((MethodReference) methodReference);
                        return true;
                    }
                }
            }

            return false;
        });

        return methodReferences.toArray(new MethodReference[methodReferences.size()]);

    }

    /**
     * $form->get ..
     */
    @Nullable
    private static PhpClass resolveFormGetterCall(MethodReference methodReference) {

        // "$form"->get('field_name');
        PhpPsiElement variable = methodReference.getFirstPsiChild();
        if(!(variable instanceof Variable)) {
            return null;
        }

        // find "$form = $this->createForm" createView call
        PsiElement variableDecl = ((Variable) variable).resolve();
        if(variableDecl == null) {
            return null;
        }

        // $form = "$this->createForm(new Type(), $entity)";
        PsiElement assignmentExpression = variableDecl.getParent();
        if(!(assignmentExpression instanceof AssignmentExpression)) {
            return null;
        }

        // $form = "$this->"createForm(new Type(), $entity)";
        PhpPsiElement calledMethodReference = ((AssignmentExpression) assignmentExpression).getValue();
        if(!(calledMethodReference instanceof MethodReference)) {
            return null;
        }

        return getFormTypeClass((MethodReference) calledMethodReference);

    }

    private static PhpClass getFormTypeClass(@Nullable MethodReference calledMethodReference) {
        if(calledMethodReference == null || !PhpElementsUtil.isMethodReferenceInstanceOf(calledMethodReference, "\\Symfony\\Component\\Form\\FormFactory", "create")) {
            return null;
        }

        // $form = "$this->createForm("new Type()", $entity)";
        PsiElement formType = PsiElementUtils.getMethodParameterPsiElementAt(calledMethodReference, 0);
        if(formType == null) {
            return null;
        }

        return getFormTypeClassOnParameter(formType);
    }

    /**
     * Get form builder field for
     * $form->get('field');
     */
    @Nullable
    public static Method resolveFormGetterCallMethod(MethodReference methodReference) {
        PhpClass formPhpClass = FormUtil.resolveFormGetterCall(methodReference);
        if(formPhpClass == null) {
            return null;
        }

        Method method = formPhpClass.findMethodByName("buildForm");
        if (method == null) {
            return null;
        }

        return method;
    }

    /**
     * Get form builder field for
     * $form->get('field', 'file');
     * $form->get('field', new FileType());
     */
    @Nullable
    public static PhpClass getFormTypeClassOnParameter(@NotNull PsiElement psiElement) {

        if(psiElement instanceof StringLiteralExpression) {
            return getFormTypeToClass(psiElement.getProject(), ((StringLiteralExpression) psiElement).getContents());
        }

        if(psiElement instanceof PhpTypedElementImpl) {
            String typeName = ((PhpTypedElementImpl) psiElement).getType().toString();
            return getFormTypeToClass(psiElement.getProject(), typeName);
        }

        if(psiElement instanceof ClassConstantReference) {
            return PhpElementsUtil.getClassConstantPhpClass((ClassConstantReference) psiElement);
        }

        return null;
    }

    @NotNull
    public static Collection<String> getFormAliases(@NotNull PhpClass phpClass) {
        // check class implements form interface
        if(!PhpElementsUtil.isInstanceOf(phpClass, ABSTRACT_FORM_INTERFACE)) {
            return Collections.emptySet();
        }

        String className = FormUtil.getFormNameOfPhpClass(phpClass);
        if(className == null) {
            return Collections.emptySet();
        }

        return Collections.singleton(className);
    }

    public static void attachFormAliasesCompletions(@NotNull PhpClass phpClass, @NotNull CompletionResultSet completionResultSet) {
        for(String alias: getFormAliases(phpClass)) {
            completionResultSet.addElement(LookupElementBuilder.create(alias).withIcon(Symfony2Icons.FORM_TYPE).withTypeText(phpClass.getPresentableFQN(), true));
        }
    }

    /**
     * acme_demo.form.type.gender:
     *  class: espend\Form\TypeBundle\Form\FooType
     *  tags:
     *   - { name: form.type, alias: foo_type_alias  }
     *   - { name: foo  }
     */
    @NotNull
    public static Map<String, Set<String>> getTags(@NotNull YAMLFile yamlFile) {
        Map<String, Set<String>> map = new HashMap<>();

        for(YAMLKeyValue yamlServiceKeyValue : YamlHelper.getQualifiedKeyValuesInFile(yamlFile, "services")) {
            String serviceName = yamlServiceKeyValue.getName();
            Set<String> serviceTagMap = YamlHelper.collectServiceTags(yamlServiceKeyValue);
            if(serviceTagMap.size() > 0) {
                map.put(serviceName, serviceTagMap);
            }
        }

        return map;
    }

    public static Map<String, Set<String>> getTags(@NotNull XmlFile psiFile) {
        Map<String, Set<String>> map = new HashMap<>();

        XmlDocumentImpl document = PsiTreeUtil.getChildOfType(psiFile, XmlDocumentImpl.class);
        if(document == null) {
            return map;
        }

        /*
         * <services>
         *   <service id="espend_form.foo_type" class="%espend_form.foo_type.class%">
         *     <tag name="form.type" alias="foo_type_alias" />
         *   </service>
         * </services>
         */

        XmlTag xmlTags[] = PsiTreeUtil.getChildrenOfType(psiFile.getFirstChild(), XmlTag.class);
        if(xmlTags == null) {
            return map;
        }

        for(XmlTag xmlTag: xmlTags) {
            if(xmlTag.getName().equals("container")) {
                for(XmlTag servicesTag: xmlTag.getSubTags()) {
                    if(servicesTag.getName().equals("services")) {
                        for(XmlTag serviceTag: servicesTag.getSubTags()) {
                            XmlAttribute attrValue = serviceTag.getAttribute("id");
                            if(attrValue != null) {

                                // <service id="foo.bar" class="Class\Name">
                                String serviceNameId = attrValue.getValue();
                                if(serviceNameId != null) {

                                    Set<String> serviceTags = getTags(serviceTag);
                                    if(serviceTags.size() > 0) {
                                        map.put(serviceNameId, serviceTags);
                                    }
                                }

                            }
                        }
                    }
                }
            }
        }

        return map;
    }

    public static Set<String> getTags(XmlTag serviceTag) {

        Set<String> tags = new HashSet<>();

        for(XmlTag serviceSubTag: serviceTag.getSubTags()) {
            if("tag".equals(serviceSubTag.getName())) {
                XmlAttribute attribute = serviceSubTag.getAttribute("name");
                if(attribute != null) {
                    String tagName = attribute.getValue();
                    if(tagName != null && StringUtils.isNotBlank(tagName)) {
                        tags.add(tagName);
                    }
                }

            }
        }

        return tags;
    }

    @NotNull
    public static Map<String, FormTypeClass> getFormTypeClasses(@NotNull Project project) {

        Map<String, FormTypeClass> map = new HashMap<>();

        for(PhpClass phpClass: PhpIndex.getInstance(project).getAllSubclasses(ABSTRACT_FORM_INTERFACE)) {
            if(!isValidFormPhpClass(phpClass)) {
                continue;
            }

            String name = FormUtil.getFormNameOfPhpClass(phpClass);
            if (name == null) {
                continue;
            }

            map.put(name, new FormTypeClass(name, phpClass, EnumFormTypeSource.INDEX));
        }

        return map;
    }

    public static boolean isValidFormPhpClass(PhpClass phpClass) {
        return !(phpClass.isAbstract() || phpClass.isInterface() || PhpElementsUtil.isTestClass(phpClass));
    }

    public static class FormTypeCollector {

        private final Map<String, FormTypeClass> formTypesMap;
        private final Project project;

        FormTypeCollector(Project project) {
            this.project = project;
            this.formTypesMap = new HashMap<>();
        }

        public FormTypeCollector collect() {

            // on indexer, compiler wins...
            formTypesMap.putAll(FormUtil.getFormTypeClasses(project));

            // find on registered formtype aliases on compiled container
            FormTypeServiceParser formTypeServiceParser = ServiceXmlParserFactory.getInstance(project, FormTypeServiceParser.class);
            for(Map.Entry<String, String> entry: formTypeServiceParser.getFormTypeMap().getMap().entrySet()) {
                String formTypeName = entry.getValue();
                formTypesMap.put(formTypeName, new FormTypeClass(formTypeName, entry.getKey(), EnumFormTypeSource.COMPILER));
            }

            return this;
        }

        @Nullable
        public PhpClass getFormTypeToClass(@Nullable String formType) {

            if(formType == null) {
                return null;
            }

            // formtype can also be a direct class name
            if(formType.contains("\\")) {
                PhpClass phpClass = PhpElementsUtil.getClass(PhpIndex.getInstance(project), formType);
                if(phpClass != null) {
                    return phpClass;
                }
            }

            return this.getFormTypeClass(formType);

        }

        @Nullable
        public PhpClass getFormTypeClass(String formTypeName) {

            // find on registered formtype aliases on compiled container

            FormTypeClass serviceName = this.formTypesMap.get(formTypeName);

            // compiled container resolve
            if(serviceName != null) {
                PhpClass phpClass = serviceName.getPhpClass(project);
                if (phpClass != null) {
                    return phpClass;
                }
            }

            // on indexer
            Map<String, FormTypeClass> forms = FormUtil.getFormTypeClasses(project);
            if(!forms.containsKey(formTypeName)) {
                return null;
            }

            return forms.get(formTypeName).getPhpClass();
        }

        public Map<String, FormTypeClass> getFormTypesMap() {
            return formTypesMap;
        }
    }

    /**
     * Replace "hidden" with HiddenType:class
     * @throws Exception
     */
    public static void replaceFormStringAliasWithClassConstant(@NotNull StringLiteralExpression psiElement) throws Exception {
        String contents = psiElement.getContents();
        if(StringUtils.isBlank(contents)) {
            throw new Exception("Empty content");
        }

        PhpClass phpClass = getFormTypeToClass(psiElement.getProject(), contents);
        if(phpClass == null) {
            throw new Exception("No class found");
        }

        PhpElementsUtil.replaceElementWithClassConstant(phpClass, psiElement);
    }

    /**
     * Finds form parent by "getParent" method
     *
     * Concatenation "__NAMESPACE__.'\Foo', "Foo::class" and string 'foo' supported
     */
    @NotNull
    public static Collection<String> getFormParentOfPhpClass(@NotNull PhpClass phpClass) {
        Method getParent = phpClass.findMethodByName("getParent");
        if(getParent == null) {
            return Collections.emptyList();
        }

        Collection<String> parents = new HashSet<>();

        for (PhpReturn phpReturn : PsiTreeUtil.collectElementsOfType(getParent, PhpReturn.class)) {
            PhpPsiElement firstPsiChild = phpReturn.getFirstPsiChild();

            if(firstPsiChild instanceof TernaryExpression) {
                // true ? 'foobar' : Foo::class
                parents.addAll(PhpElementsUtil.getTernaryExpressionConditionStrings((TernaryExpression) firstPsiChild));
            } else if(firstPsiChild instanceof BinaryExpression && PsiElementAssertUtil.isNotNullAndIsElementType(firstPsiChild, PhpElementTypes.CONCATENATION_EXPRESSION)) {
                // Symfony core: __NAMESPACE__.'\Foo'
                PsiElement leftOperand = ((BinaryExpression) firstPsiChild).getLeftOperand();
                ConstantReference constantReference = PsiElementAssertUtil.getInstanceOfOrNull(leftOperand, ConstantReference.class);
                if(constantReference == null || !"__NAMESPACE__".equals(constantReference.getName())) {
                    continue;
                }

                StringLiteralExpression stringValue = PsiElementAssertUtil.getInstanceOfOrNull(((BinaryExpression) firstPsiChild).getRightOperand(), StringLiteralExpression.class);
                if(stringValue == null) {
                    continue;
                }

                String contents = stringValue.getContents();
                if(StringUtils.isBlank(contents)) {
                    continue;
                }

                parents.add(StringUtils.strip(phpClass.getNamespaceName(), "\\") + contents);
            }

            // fallback try to resolve string value
            String contents = PhpElementsUtil.getStringValue(firstPsiChild);
            if(StringUtils.isNotBlank(contents)) {
                parents.add(contents);
            }
        }

        return parents;
    }

    /**
     * Finds form name by "getName" method
     *
     * Symfony < 2.8
     * 'foo_bar'
     *
     * Symfony 2.8
     * "$this->getName()" -> "$this->getBlockPrefix()" -> return 'datetime';
     * "UserProfileType" => "user_profile" and namespace removal
     *
     * Symfony 3.0
     * Use class name: Foo\Class
     *
     */
    @Nullable
    public static String getFormNameOfPhpClass(@NotNull PhpClass phpClass) {
        Method method = phpClass.findOwnMethodByName("getName");

        // @TODO: think of interface switches

        // method not found so use class fqn
        if(method == null) {
            return StringUtils.stripStart(phpClass.getFQN(), "\\");
        }

        for (PhpReturn phpReturn : PsiTreeUtil.collectElementsOfType(method, PhpReturn.class)) {
            PhpPsiElement firstPsiChild = phpReturn.getFirstPsiChild();

            // $this->getBlockPrefix()
            if(firstPsiChild instanceof MethodReference) {
                PhpExpression classReference = ((MethodReference) firstPsiChild).getClassReference();
                if(classReference != null && "this".equals(classReference.getName())) {
                    String name = firstPsiChild.getName();
                    if(name != null && "getBlockPrefix".equals(name)) {
                        if(phpClass.findOwnMethodByName("getBlockPrefix") != null) {
                            return PhpElementsUtil.getMethodReturnAsString(phpClass, name);
                        } else {
                            // method has no custom overwrite; rebuild expression here:
                            // FooBarType -> foo_bar
                            String className = phpClass.getName();

                            // strip Type and type
                            if(className.toLowerCase().endsWith("type") && className.length() > 4) {
                                className = className.substring(0, className.length() - 4);
                            }

                            return fr.adrienbrault.idea.symfony2plugin.util.StringUtils.underscore(className);
                        }
                    }
                }

                continue;
            }

            // string value fallback
            String stringValue = PhpElementsUtil.getStringValue(firstPsiChild);
            if(stringValue != null) {
                return stringValue;
            }
        }


        return null;
    }

    /**
     * Get getExtendedType and getExtendedTypes (Symfony >= 4.2) as string
     *
     * "return Foo::class;"
     * "return 'foobar';"
     * "return [Foo::class, FooBar::class];"
     * "return true === true ? FileType::class : Form:class;"
     * "yield Foo::class;"
     */
    @NotNull
    public static Collection<String> getFormExtendedType(@NotNull PhpClass phpClass) {
        Collection<String> types = new HashSet<>();

        // public function getExtendedType() { return FileType::class; }
        // public function getExtendedType() { return true === true ? FileType::class : Form:class; }
        Method extendedType = phpClass.findMethodByName("getExtendedType");
        if(extendedType != null) {
            for (PhpReturn phpReturn : PsiTreeUtil.collectElementsOfType(extendedType, PhpReturn.class)) {
                PhpPsiElement firstPsiChild = phpReturn.getFirstPsiChild();

                // true ? 'foo' : 'foo'
                if(firstPsiChild instanceof TernaryExpression) {
                    types.addAll(PhpElementsUtil.getTernaryExpressionConditionStrings((TernaryExpression) firstPsiChild));
                }

                String stringValue = PhpElementsUtil.getStringValue(firstPsiChild);
                if(stringValue != null) {
                    types.add(stringValue);
                }
            }
        }

        // Symfony 4.2: Support improved form type extensions:
        // public static function getExtendedTypes(): iterable:
        // https://symfony.com/blog/new-in-symfony-4-2-improved-form-type-extensions
        Method extendedTypes = phpClass.findMethodByName("getExtendedTypes");
        if (extendedTypes != null) {
            // [Foo::class, FooBar::class]
            for (PhpReturn phpReturn : PsiTreeUtil.collectElementsOfType(extendedTypes, PhpReturn.class)) {
                PhpPsiElement arrayCreationExpression = phpReturn.getFirstPsiChild();
                if (arrayCreationExpression instanceof ArrayCreationExpression) {
                    Collection<PsiElement> arrayValues = PhpPsiUtil.getChildren(arrayCreationExpression, psiElement ->
                            psiElement.getNode().getElementType() == PhpElementTypes.ARRAY_VALUE
                    );

                    for (PsiElement child : arrayValues) {
                        String stringValue = PhpElementsUtil.getStringValue(child.getFirstChild());
                        if (stringValue != null) {
                            types.add(stringValue);
                        }
                    }
                }
            }

            // yield Foo::class
            for (PhpYield phpReturn : PsiTreeUtil.collectElementsOfType(extendedTypes, PhpYield.class)) {
                PsiElement yieldArgument = phpReturn.getArgument();
                if (yieldArgument == null) {
                    continue;
                }

                String stringValue = PhpElementsUtil.getStringValue(yieldArgument);
                if (stringValue != null) {
                    types.add(stringValue);
                }
            }
        }

        return types;
    }

    /**
     * Find form php class scope: FormType or FormExtension
     */
    @Nullable
    public static String getFormTypeClassFromScope(@NotNull PsiElement psiElement) {
        Method methodScope = PsiTreeUtil.getParentOfType(psiElement, Method.class);

        if(methodScope != null) {
            PhpClass phpClass = methodScope.getContainingClass();
            if(phpClass != null && (PhpElementsUtil.isInstanceOf(phpClass, "\\Symfony\\Component\\Form\\FormTypeInterface") || PhpElementsUtil.isInstanceOf(phpClass, "\\Symfony\\Component\\Form\\FormExtensionInterface"))) {
                return phpClass.getFQN();
            }
        }

        return null;
    }
}