/*
 * Copyright 2005 Sascha Weinreuter
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.intellij.lang.xpath.xslt.util;

import com.intellij.codeInsight.PsiEquivalenceUtil;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.*;
import com.intellij.util.containers.ContainerUtil;
import org.intellij.lang.xpath.psi.*;
import org.intellij.lang.xpath.xslt.XsltSupport;
import org.intellij.lang.xpath.xslt.psi.XsltElement;
import org.intellij.lang.xpath.xslt.psi.XsltElementFactory;
import org.intellij.lang.xpath.xslt.psi.XsltTemplate;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;

public final class XsltCodeInsightUtil {
    public static final Comparator<PsiElement> POSITION_COMPARATOR = Comparator.comparingInt(PsiElement::getTextOffset);

    private XsltCodeInsightUtil() {
    }

    public static @Nullable XmlTag getTemplateTag(@NotNull PsiElement location, boolean isExpression, boolean requireName) {
        PsiElement p = isExpression ? location.getContainingFile().getContext() : location;
        while ((p = PsiTreeUtil.getParentOfType(p, XmlTag.class)) != null) {
            final XmlTag _p = ((XmlTag)p);
            if (XsltSupport.isTemplate(_p, requireName)) return _p;
        }
        return null;
    }

    public static @Nullable XmlTag getTemplateTag(@NotNull PsiElement location, boolean isExpression) {
        return getTemplateTag(location, isExpression, false);
    }

    public static @Nullable XsltTemplate getTemplate(@NotNull PsiElement location, boolean isExpression) {
        final XmlTag templateTag = getTemplateTag(location, isExpression);
        return templateTag != null ? XsltElementFactory.getInstance().wrapElement(templateTag, XsltTemplate.class) : null;
    }

    public static @Nullable PsiElement findFirstRealTagChild(@NotNull XmlTag xmlTag) {
        final PsiElement[] child = new PsiElement[1];
        xmlTag.processElements(new PsiElementProcessor() {
            @Override
            public boolean execute(@NotNull PsiElement element) {
                if (element instanceof XmlToken) {
                    if (((XmlToken)element).getTokenType() == XmlTokenType.XML_TAG_END) {
                        child[0] = element.getNextSibling();
                        return false;
                    }
                }
                return true;
            }
        }, xmlTag);
        return child[0];
    }

    public static @Nullable XPathExpression getXPathExpression(XsltElement xsltElement, String attributeName) {
        final XmlAttribute attribute = xsltElement.getTag().getAttribute(attributeName, null);
        if (attribute != null) {
            final PsiFile[] files = XsltSupport.getFiles(attribute);
            if (files.length > 0) {
                assert files.length == 1 : "Unexpected number of XPathFiles in @" + attributeName + ": " + Arrays.toString(files);
                return PsiTreeUtil.getChildOfType(files[0], XPathExpression.class);
            }
        }
        return null;
    }

    public static boolean areExpressionsEquivalent(XPathExpression x1, XPathExpression x2) {
        if (x1.getType() != x2.getType()) return false;
        // another hidden and hard-to-find goodie from the dark non-OpenAPI world
        return PsiEquivalenceUtil.areElementsEquivalent(x1, x2);
    }

    public static @Nullable XmlTag findLastParam(XmlTag templateTag) {
        return SyntaxTraverser.psiTraverser(templateTag).filter(XmlTag.class).filter(XsltSupport::isParam).last();
    }

  public static @NotNull TextRange getRangeInsideHostingFile(XPathElement expr) {
        final PsiLanguageInjectionHost host = PsiTreeUtil.getContextOfType(expr, PsiLanguageInjectionHost.class, true);
        assert host != null;
        final List<Pair<PsiElement,TextRange>> psi = InjectedLanguageManager.getInstance(host.getProject()).getInjectedPsiFiles(host);
        assert psi != null;
        for (Pair<PsiElement, TextRange> pair : psi) {
            if (PsiTreeUtil.isAncestor(pair.first, expr, false)) {
                return expr.getTextRange().shiftRight(pair.second.getStartOffset() + host.getTextRange().getStartOffset());
            }
        }
        assert false;
        return null;
    }

    public static @NotNull TextRange getRangeInsideHost(XPathElement expr) {
        final PsiLanguageInjectionHost host = PsiTreeUtil.getContextOfType(expr, PsiLanguageInjectionHost.class, true);
        assert host != null;
        final List<Pair<PsiElement,TextRange>> psi = InjectedLanguageManager.getInstance(host.getProject()).getInjectedPsiFiles(host);
        assert psi != null;
        for (Pair<PsiElement, TextRange> pair : psi) {
            if (PsiTreeUtil.isAncestor(pair.first, expr, false)) {
                return pair.second;
            }
        }
        assert false;
        return null;
    }

    public static XmlTag findLastWithParam(XmlTag templateTag) {
        final XmlTag[] lastParam = new XmlTag[1];
        templateTag.processElements(new PsiElementProcessor() {
            @Override
            public boolean execute(@NotNull PsiElement element) {
                if (element instanceof XmlTag) {
                    if ("with-param".equals(((XmlTag)element).getLocalName())) {
                        lastParam[0] = (XmlTag)element;
                    } else {
                        return false;
                    }
                }
                return true;
            }
        }, templateTag);
        return lastParam[0];
    }

    public static XmlTag findVariableInsertionPoint(final XmlTag currentUsageTag, PsiElement usageBlock, final String referenceName, XmlTag... moreUsages) {
      // sort tags by document order
      final Set<XmlTag> usages = new TreeSet<>(POSITION_COMPARATOR);
      usages.add(currentUsageTag);
      ContainerUtil.addAll(usages, moreUsages);

      // collect all other possible unresolved references with the same name in the current template
      usageBlock.accept(new PsiRecursiveElementVisitor() {
        @Override
        public void visitElement(@NotNull PsiElement element) {
          if (element instanceof XPathVariableReference) {
            visitXPathVariableReference(((XPathVariableReference)element));
          }
          else {
            super.visitElement(element);
          }
        }

        private void visitXPathVariableReference(XPathVariableReference reference) {
          if (referenceName.equals(reference.getReferencedName())) {
            if (reference.resolve() == null) {
              usages.add(PsiTreeUtil.getContextOfType(reference, XmlTag.class, true));
            }
          }
        }

        public void visitXmlAttribute(XmlAttribute attribute) {
          if (XsltSupport.isXPathAttribute(attribute)) {
            final PsiFile[] xpathFiles = XsltSupport.getFiles(attribute);
            for (PsiFile xpathFile : xpathFiles) {
              xpathFile.accept(this);
            }
          }
        }
      });

      final Iterator<XmlTag> it = usages.iterator();
      final XmlTag firstUsage = it.next();

      // find broadest scope to create the variable in
      XmlTag tag = firstUsage;
      while (it.hasNext()) {
        XmlTag xmlTag = it.next();
        final PsiElement t = PsiTreeUtil.findCommonParent(tag, xmlTag);
        if (t instanceof XmlTag) {
          tag = (XmlTag)t;
        }
        else {
          break;
        }
      }

      // find the actual tag to create the variable before
      final XmlTag[] subTags = tag.getSubTags();
      for (XmlTag xmlTag : subTags) {
        if (xmlTag.getTextOffset() > firstUsage.getTextOffset()) break;
        tag = xmlTag;
      }

      final XmlTag parentTag = tag.getParentTag();
      if (parentTag == null) return tag;

      final String parentName = parentTag.getLocalName();
      if ("apply-templates".equals(parentName) || "call-template".equals(parentName)
          || "when".equals(parentName) || "choose".equals(parentName)) {
        if ("when".equals(parentName)) tag = tag.getParentTag();
        assert tag != null;
        tag = tag.getParentTag();
      }
      assert tag != null;
      return tag;
    }

    public static @Nullable PsiElement getUsageBlock(XPathExpression reference) {
        final XmlTag template = getTemplateTag(reference, true);
        final XmlTag tag = PsiTreeUtil.getContextOfType(reference, XmlTag.class, true);
        assert tag != null;
        return template != null ? template.getNavigationElement() : tag.getParentTag();
    }

    public static @NotNull XmlDocument getDocument(@NotNull XmlElement element) {
        final XmlDocument document = PsiTreeUtil.getParentOfType(element, XmlDocument.class, false);
        assert document != null;
        return document;
    }

    public static XmlDocument getDocument(@NotNull XsltElement element) {
        return getDocument(element.getTag());
    }

  public static @Nullable XPathType getDeclaredType(XmlTag element) {
    final XmlAttribute typeAttr = element.getAttribute("as");
    final XPathType returnType;
    if (typeAttr != null) {
      final String value = typeAttr.getValue();
      returnType = value != null ? XPath2Type.fromName(QNameUtil.createQName(value, element)) : null;
      return returnType != null ? returnType : XPathType.UNKNOWN;
    }
    return null;
  }
}
