package fr.adrienbrault.idea.symfony2plugin.config.yaml.completion;

import com.intellij.codeInsight.completion.CompletionParameters;
import com.intellij.codeInsight.completion.CompletionProvider;
import com.intellij.codeInsight.completion.CompletionResultSet;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.util.ProcessingContext;
import com.intellij.util.containers.ContainerUtil;
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
import fr.adrienbrault.idea.symfony2plugin.util.ProjectUtil;
import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper;
import org.apache.commons.lang.StringUtils;
import org.apache.xerces.dom.CommentImpl;
import org.apache.xerces.dom.DeferredTextImpl;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.yaml.psi.YAMLCompoundValue;
import org.jetbrains.yaml.psi.YAMLDocument;
import org.jetbrains.yaml.psi.YAMLKeyValue;
import org.w3c.dom.*;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author Daniel Espendiller <[email protected]>
 */
public class ConfigCompletionProvider extends CompletionProvider<CompletionParameters> {

    @Override
    protected void addCompletions(@NotNull CompletionParameters completionParameters, ProcessingContext processingContext, @NotNull CompletionResultSet completionResultSet) {

        PsiElement element = completionParameters.getPosition();
        if(!Symfony2ProjectComponent.isEnabled(element)) {
            return;
        }

        PsiElement yamlScalar = element.getParent();
        if(yamlScalar == null) {
            return;
        }

        PsiElement yamlCompount = yamlScalar.getParent();

        // yaml document root context
        if(yamlCompount.getParent() instanceof YAMLDocument) {
            attachRootConfig(completionResultSet, element);
            return;
        }

        // check inside yaml key value context
        if(!(yamlCompount instanceof YAMLCompoundValue || yamlCompount instanceof YAMLKeyValue)) {
            return;
        }

        // get all parent yaml keys
        List<String> items = YamlHelper.getParentArrayKeys(element);
        if(items.size() == 0) {
            return;
        }

        // normalize for xml
        items = ContainerUtil.map(items, s -> s.replace('_', '-'));

        // reverse to get top most item first
        Collections.reverse(items);

        Document document = getConfigTemplate(ProjectUtil.getProjectDir(element));
        if(document == null) {
            return;
        }

        Node configNode = getMatchingConfigNode(document, items);
        if(configNode == null) {
            return;
        }

        getConfigPathLookupElements(completionResultSet, configNode, false);

        // map shortcuts like eg <dbal default-connection="">
        if(configNode instanceof Element) {
            NamedNodeMap attributes = configNode.getAttributes();
            for (int i = 0; i < attributes.getLength(); i++) {
                String attributeName = attributes.item(i).getNodeName();
                if(attributeName.startsWith("default-")) {
                    Node defaultNode = getElementByTagNameWithUnPluralize((Element) configNode, attributeName.substring("default-".length()));
                    if(defaultNode != null) {
                        getConfigPathLookupElements(completionResultSet, defaultNode, true);
                    }
                }
            }
        }

    }

    private Document getConfigTemplate(VirtualFile projectBaseDir) {

        Document document;

        try {
            DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();

            VirtualFile virtualFile = VfsUtil.findRelativeFile(projectBaseDir, ".idea", "symfony2-config.xml");
            if(virtualFile != null) {
                document = builder.parse(VfsUtil.virtualToIoFile(virtualFile));
            } else {
                document = builder.parse(ConfigCompletionProvider.class.getResourceAsStream("/symfony2-config.xml"));
            }

            return document;

        } catch (ParserConfigurationException | SAXException | IOException e) {
            return null;
        }
    }

    private void getConfigPathLookupElements(CompletionResultSet completionResultSet, Node configNode, boolean isShortcut) {

        // get config on node attributes
        NamedNodeMap attributes = configNode.getAttributes();
        if(attributes.getLength() > 0) {
            Map<String, String> nodeDocVars = getNodeCommentVars(configNode);
            for (int i = 0; i < attributes.getLength(); i++) {
                completionResultSet.addElement(getNodeAttributeLookupElement(attributes.item(i), nodeDocVars, isShortcut));
            }
        }


        // check for additional child node
        if(configNode instanceof Element) {

            NodeList nodeList1 = ((Element) configNode).getElementsByTagName("*");
            for (int i = 0; i < nodeList1.getLength(); i++) {
                LookupElementBuilder nodeTagLookupElement = getNodeTagLookupElement(nodeList1.item(i), isShortcut);
                if(nodeTagLookupElement != null) {
                    completionResultSet.addElement(nodeTagLookupElement);
                }
            }

        }


    }

    private LookupElementBuilder getNodeAttributeLookupElement(Node node, Map<String, String> nodeVars, boolean isShortcut) {

        String nodeName = getNodeName(node);
        LookupElementBuilder lookupElementBuilder = LookupElementBuilder.create(nodeName).withIcon(Symfony2Icons.CONFIG_VALUE);

        String textContent = node.getTextContent();
        if(StringUtils.isNotBlank(textContent)) {
            lookupElementBuilder = lookupElementBuilder.withTailText("(" + textContent + ")", true);
        }

        if(nodeVars.containsKey(nodeName)) {
            lookupElementBuilder = lookupElementBuilder.withTypeText(StringUtil.shortenTextWithEllipsis(nodeVars.get(nodeName), 100, 0), true);
        }

        if(isShortcut) {
            lookupElementBuilder = lookupElementBuilder.withIcon(Symfony2Icons.CONFIG_VALUE_SHORTCUT);
        }

        return lookupElementBuilder;
    }

    @Nullable
    private LookupElementBuilder getNodeTagLookupElement(Node node, boolean isShortcut) {

        String nodeName = getNodeName(node);
        boolean prototype = isPrototype(node);

        // prototype "connection" must be "connections" so pluralize
        if(prototype) {
            nodeName = StringUtil.pluralize(nodeName);
        }

        LookupElementBuilder lookupElementBuilder = LookupElementBuilder.create(nodeName).withIcon(Symfony2Icons.CONFIG_PROTOTYPE);

        if(prototype) {
            lookupElementBuilder = lookupElementBuilder.withTypeText("Prototype", true);
        }

        if(isShortcut) {
            lookupElementBuilder = lookupElementBuilder.withIcon(Symfony2Icons.CONFIG_VALUE_SHORTCUT);
        }

        return lookupElementBuilder;
    }

    private String getNodeName(Node node) {
        return node.getNodeName().replace("-", "_");
    }

    @Nullable
    private Element getElementByTagNameWithUnPluralize(@NotNull Element element, String tagName) {

        NodeList nodeList = element.getElementsByTagName(tagName);
        if(nodeList.getLength() > 0) {
            return (Element) nodeList.item(0);
        }

        String unpluralize = StringUtil.unpluralize(tagName);
        if(unpluralize == null) {
            return null;
        }

        nodeList = element.getElementsByTagName(unpluralize);
        if(nodeList.getLength() > 0) {
            return (Element) nodeList.item(0);
        }

        return null;

    }

    @Nullable
    private Element getElementByTagName(Document element, String tagName) {
        NodeList nodeList = element.getElementsByTagName(tagName);

        if(nodeList.getLength() > 0) {
            return (Element) nodeList.item(0);
        }

        return null;

    }

    @Nullable
    private Node getMatchingConfigNode(Document document, List<String> items) {

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

        Element currentNodeItem = getElementByTagName(document, items.get(0));
        if(currentNodeItem == null) {
            return null;
        }

        for (int i = 1; i < items.size(); i++) {

            currentNodeItem = getElementByTagNameWithUnPluralize(currentNodeItem, items.get(i));
            if(currentNodeItem == null) {
                return null;
            }

            if(isPrototype(currentNodeItem)) {
                i++;
            }

        }

        return currentNodeItem;

    }

    private boolean isPrototype(@Nullable Node node) {
        if(node == null) return false;

        Node previousSibling = node.getPreviousSibling();
        if(previousSibling == null) {
            return false;
        }

        // we can have multiple comments similar to docblock to a node
        // search for prototype
        Node comment = previousSibling.getPreviousSibling();
        while (comment instanceof CommentImpl || comment instanceof DeferredTextImpl) {

            if(comment instanceof CommentImpl) {
                if(comment.getTextContent().toLowerCase().matches("\\s*prototype.*")) {
                    return true;
                }
            }

            comment = comment.getPreviousSibling();
        }

        return false;
    }

    @NotNull
    private Map<String, String> getNodeCommentVars(@Nullable Node node) {
        Map<String, String> comments = new HashMap<>();

        if(node == null) return comments;

        Node previousSibling = node.getPreviousSibling();
        if(previousSibling == comments) {
            return comments;
        }

        // get variable decl: "foo: test"
        Pattern compile = Pattern.compile("^\\s*([\\w_-]+)\\s*:\\s*(.*?)$");

        Node comment = previousSibling.getPreviousSibling();
        while (comment instanceof CommentImpl || comment instanceof DeferredTextImpl) {

            if(comment instanceof CommentImpl) {

                // try to find a var decl
                String trim = StringUtils.trim(comment.getTextContent());
                Matcher matcher = compile.matcher(trim);
                if (matcher.find()) {
                    comments.put(matcher.group(1).replace("-", "_"), matcher.group(2));
                }

            }

            comment = comment.getPreviousSibling();
        }

        return comments;
    }


    private void attachRootConfig(CompletionResultSet completionResultSet, PsiElement element) {

        Document document = getConfigTemplate(ProjectUtil.getProjectDir(element));
        if(document == null) {
            return;
        }

        try {

            // attach config aliases
            NodeList nodeList  = (NodeList) XPathFactory.newInstance().newXPath().compile("//config/*").evaluate(document, XPathConstants.NODESET);
            for (int i = 0; i < nodeList.getLength(); i++) {
                completionResultSet.addElement(LookupElementBuilder.create(getNodeName(nodeList.item(i))).withIcon(Symfony2Icons.CONFIG_VALUE));
            }

        } catch (XPathExpressionException ignored) {
        }

    }

}