/*
 * 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;

import com.intellij.ide.highlighter.XmlFileType;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors;
import com.intellij.openapi.editor.colors.TextAttributesKey;
import com.intellij.openapi.util.Key;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.impl.PsiFileEx;
import com.intellij.psi.util.*;
import com.intellij.psi.xml.*;
import com.intellij.util.SmartList;
import com.intellij.util.text.CharSequenceReader;
import com.intellij.util.xml.NanoXmlUtil;
import icons.XpathIcons;
import org.intellij.lang.xpath.XPathFile;
import org.intellij.lang.xpath.xslt.impl.XsltChecker;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

public final class XsltSupport {
  private static final String XALAN_EXTENSION_PREFIX = "http://xml.apache.org/xalan/";
  public static final String XSLT_NS = "http://www.w3.org/1999/XSL/Transform";
  public static final String PLUGIN_EXTENSIONS_NS = "urn:idea:xslt-plugin#extensions";
  private static final Key<ParameterizedCachedValue<XsltChecker.LanguageLevel, PsiFile>> FORCE_XSLT_KEY = Key.create("FORCE_XSLT");
  public static final TextAttributesKey XSLT_DIRECTIVE =
    TextAttributesKey.createTextAttributesKey("XSLT_DIRECTIVE", DefaultLanguageHighlighterColors.TEMPLATE_LANGUAGE_COLOR);

  private static final Map<String, String> XPATH_ATTR_MAP = new HashMap<>(10);
  private static final Map<String, Set<String>> XPATH_AVT_MAP = new HashMap<>(10);

  static {
    XPATH_ATTR_MAP.put("select", "");
    XPATH_ATTR_MAP.put("match", "");
    XPATH_ATTR_MAP.put("test", "");
    XPATH_ATTR_MAP.put("use-when", "");
    XPATH_ATTR_MAP.put("group-by", "");
    XPATH_ATTR_MAP.put("group-adjacent", "");
    XPATH_ATTR_MAP.put("count", "number");
    XPATH_ATTR_MAP.put("from", "number");
    XPATH_ATTR_MAP.put("value", "number");
    XPATH_ATTR_MAP.put("use", "key");

    XPATH_AVT_MAP.put("element", Set.of("name", "namespace"));
    XPATH_AVT_MAP.put("attribute", Set.of("name", "namespace"));
    XPATH_AVT_MAP.put("namespace", Set.of("name"));
    XPATH_AVT_MAP.put("processing-instruction", Set.of("name"));

    XPATH_AVT_MAP.put("number", Set.of("format", "lang", "letter-value", "grouping-separator", "grouping-size", "ordinal"));
    XPATH_AVT_MAP.put("sort", Set.of("lang", "data-type", "order", "case-order", "collation"));

    XPATH_AVT_MAP.put("message", Set.of("terminate"));
    XPATH_AVT_MAP.put("value-of", Set.of("separator"));

    XPATH_AVT_MAP.put("result-document", Set.of("format", "href", "method", "byte-order-mark",
                                                           "cdata-section-elements", "doctype-public", "doctype-system",
                                                           "encoding", "escape-uri-attributes", "include-content-type",
                                                           "indent", "media-type", "normalization-form",
                                                           "omit-xml-declaration", "standalone", "undeclare-prefixes",
                                                           "output-version"));
  }

  private XsltSupport() {
  }

  public static PsiFile @NotNull [] getFiles(XmlAttribute attribute) {
    final XmlAttributeValue value = attribute.getValueElement();
    if (value != null) {
      final List<PsiFile> files = new SmartList<>();
      InjectedLanguageManager.getInstance(value.getProject()).enumerate(value, (injectedPsi, places) -> {
        if (injectedPsi instanceof XPathFile) {
          files.add(injectedPsi);
        }
      });
      return files.isEmpty() ? PsiFile.EMPTY_ARRAY : PsiUtilCore.toPsiFileArray(files);
    }
    return PsiFile.EMPTY_ARRAY;
  }

  public static boolean isXsltAttribute(@NotNull XmlAttribute attribute) {
    XmlTag parent = attribute.getParent();
    return parent != null && isXsltTag(parent);
  }

  public static boolean isXsltTag(@NotNull XmlTag tag) {
    if (!tag.isValid()) return false;
    final String s = tag.getNamespace();
    return XSLT_NS.equals(s) || s.startsWith(XALAN_EXTENSION_PREFIX);
  }

  private static boolean isXsltCoreTag(@NotNull XmlTag tag) {
    return tag.isValid() && XSLT_NS.equals(tag.getNamespace());
  }

  public static boolean isXPathAttribute(@NotNull XmlAttribute attribute) {
    if (attribute.getValueElement() == null) return false;

    final String name = attribute.getName();
    if (isXsltAttribute(attribute)) {
      final String tagName = attribute.getParent().getLocalName();
      final String s = XPATH_ATTR_MAP.get(name);
      if ((s == null || !s.isEmpty()) && !tagName.equals(s)) {
        if (!isAttributeValueTemplate(attribute, true)) {
          return false;
        }
      }
    }
    else {
      if (!isAttributeValueTemplate(attribute, false)) {
        return false;
      }
    }

    final PsiFile file = attribute.getContainingFile();
    if (file != null) {
      XsltChecker.LanguageLevel level = getXsltLanguageLevel(file);
      if (level != XsltChecker.LanguageLevel.NONE) {
        return true;
      }
    }
    return false;
  }

  private static boolean isAttributeValueTemplate(@NotNull XmlAttribute attribute, boolean isXsltAttribute) {
    return (!isXsltAttribute || mayBeAVT(attribute)) && getAVTOffset(attribute.getValue(), 0) != -1;
  }

  public static boolean isVariableOrParamName(@NotNull XmlAttribute attribute) {
    return isXsltNameAttribute(attribute) && isVariableOrParam(attribute.getParent());
  }

  public static boolean isVariableOrParam(@NotNull XmlTag tag) {
    final String localName = tag.getLocalName();
    return ("variable".equals(localName) || "param".equals(localName)) && isXsltCoreTag(tag);
  }

  public static boolean isVariable(@NotNull XmlAttribute attribute) {
    return isXsltNameAttribute(attribute) && isVariable(attribute.getParent());
  }

  public static boolean isVariable(@NotNull XmlTag tag) {
    final String localName = tag.getLocalName();
    return "variable".equals(localName) && isXsltCoreTag(tag);
  }

  public static boolean isParam(@NotNull XmlAttribute attribute) {
    return isXsltNameAttribute(attribute) && isParam(attribute.getParent());
  }

  public static boolean isParam(@NotNull XmlTag tag) {
    final String localName = tag.getLocalName();
    return "param".equals(localName) && isXsltCoreTag(tag);
  }

  public static boolean isPatternAttribute(@NotNull XmlAttribute attribute) {
    if (!isXsltAttribute(attribute)) return false;

    final String name = attribute.getName();
    if ("match".equals(name)) {
      return true;
    }
    else if ("count".equals(name) || "from".equals(name)) {
      return "number".equals(attribute.getParent().getLocalName());
    }
    return false;
  }

  public static boolean isTemplateCall(@NotNull XmlTag tag) {
    return "call-template".equals(tag.getLocalName()) && hasNameAttribute(tag) && isXsltCoreTag(tag);
  }

  public static boolean isFunction(@NotNull XmlTag tag) {
    return "function".equals(tag.getLocalName()) && hasNameAttribute(tag) && isXsltCoreTag(tag);
  }

  public static boolean isApplyTemplates(@NotNull XmlTag tag) {
    final String localName = tag.getLocalName();
    return "apply-templates".equals(localName) && isXsltCoreTag(tag);
  }

  private static boolean hasNameAttribute(@NotNull XmlTag tag) {
    return tag.getAttribute("name", null) != null;
  }

  public static boolean isTemplateCallName(@NotNull XmlAttribute attribute) {
    return isXsltNameAttribute(attribute) && isTemplateCall(attribute.getParent());
  }

  private static boolean isXsltNameAttribute(@NotNull XmlAttribute attribute) {
    return "name".equals(attribute.getName()) && isXsltAttribute(attribute);
  }

  public static boolean isTemplateName(@NotNull XmlAttribute attribute) {
    return isXsltNameAttribute(attribute) && isTemplate(attribute.getParent());
  }

  public static boolean isFunctionName(@NotNull XmlAttribute attribute) {
    return isXsltNameAttribute(attribute) && isFunction(attribute.getParent());
  }

  public static boolean isTemplate(@NotNull XmlTag element) {
    return isTemplate(element, true);
  }

  public static boolean isTemplate(@NotNull XmlTag element, boolean requireName) {
    return "template".equals(element.getLocalName()) && (!requireName || hasNameAttribute(element)) && isXsltCoreTag(element);
  }

  public static boolean isXsltFile(@NotNull PsiFile psiFile) {
    if (psiFile.getFileType() != XmlFileType.INSTANCE) return false;

    if (!(psiFile instanceof XmlFile)) return false;

    final XsltChecker.LanguageLevel level = getXsltLanguageLevel(psiFile);
    return level != XsltChecker.LanguageLevel.NONE;
  }

  public static XsltChecker.LanguageLevel getXsltLanguageLevel(@NotNull PsiFile psiFile) {
    final CachedValuesManager mgr = CachedValuesManager.getManager(psiFile.getProject());
    return mgr.getParameterizedCachedValue(psiFile, FORCE_XSLT_KEY, XsltSupportProvider.INSTANCE, false, psiFile);
  }

  public static boolean isXsltRootTag(@NotNull XmlTag tag) {
    final String localName = tag.getLocalName();
    return ("stylesheet".equals(localName) || "transform".equals(localName)) && isXsltCoreTag(tag);
  }

  public static boolean isTemplateCallParamName(@NotNull XmlAttribute attribute) {
    return isXsltNameAttribute(attribute) && isTemplateCallParam(attribute.getParent());
  }

  private static boolean isTemplateCallParam(@NotNull XmlTag parent) {
    return "with-param".equals(parent.getLocalName()) && hasNameAttribute(parent) && isXsltCoreTag(parent);
  }

  public static boolean isTopLevelElement(XmlTag tag) {
    XmlTag p = tag;
    // not really necessary, XSLT doesn't allow literal result elements on top level anyway
    while ((p = p.getParentTag()) != null) {
      if (isXsltTag(p)) {
        return isXsltRootTag(p);
      }
    }
    return false;
  }

  public static boolean isIncludeOrImportHref(XmlAttribute xmlattribute) {
    if (xmlattribute == null || !isXsltAttribute(xmlattribute)) return false;
    final String localName = xmlattribute.getParent().getLocalName();
    return isIncludeOrImport(localName) && "href".equals(xmlattribute.getName());
  }

  private static boolean isIncludeOrImport(String localName) {
    // treat import and include the same. right now it doesn't seem necessary to distinguish them
    return ("import".equals(localName) || "include".equals(localName));
  }

  public static boolean isIncludeOrImport(XmlTag tag) {
    if (tag == null) return false;
    return isIncludeOrImport(tag.getLocalName()) && isXsltCoreTag(tag) && tag.getAttribute("href", null) != null;
  }

  public static boolean isImport(XmlTag tag) {
    if (tag == null) return false;
    return "import".equals(tag.getLocalName()) && isXsltCoreTag(tag) && tag.getAttribute("href", null) != null;
  }

  public static @Nullable PsiElement getAttValueToken(@NotNull XmlAttribute attribute) {
    final XmlAttributeValue valueElement = attribute.getValueElement();
    if (valueElement != null) {
      final PsiElement firstChild = valueElement.getFirstChild();
      if (firstChild != null) {
        final PsiElement nextSibling = firstChild.getNextSibling();
        return nextSibling instanceof XmlToken && ((XmlToken)nextSibling).getTokenType() == XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN
               ? nextSibling
               : null;
      }
    }
    return null;
  }

  public static boolean isMode(XmlAttribute xmlattribute) {
    if ("mode".equals(xmlattribute.getName())) {
      final XmlTag parent = xmlattribute.getParent();
      return isApplyTemplates(parent) || isTemplate(parent, false);
    }
    return false;
  }

  public static int getAVTOffset(String value, int i) {
    while (true) {
      i = value.indexOf('{', i);
      if (i != -1 && i == value.indexOf("{{", i)) {
        i += 2;
      }
      else {
        break;
      }
    }
    return i;
  }

  public static boolean mayBeAVT(@NotNull XmlAttribute attribute) {
    if (isXsltAttribute(attribute)) {
      final String tagName = attribute.getParent().getLocalName();

      final Set<String> allowedAttrs = XPATH_AVT_MAP.get(tagName);
      if (allowedAttrs == null) return isExtensionAvtAttribute(attribute);

      return allowedAttrs.contains(attribute.getName());
    }
    else {
      return true;
    }
  }

  private static boolean isExtensionAvtAttribute(XmlAttribute attribute) {
    final String namespace = attribute.getParent().getNamespace();
    return namespace.startsWith(XALAN_EXTENSION_PREFIX) && "file".equals(attribute.getName());
  }

  public static Icon createXsltIcon(Icon icon) {
    return XpathIcons.XsltFiletypeOverlay;
  }

  private static class XsltSupportProvider implements ParameterizedCachedValueProvider<XsltChecker.LanguageLevel, PsiFile> {
    public static final ParameterizedCachedValueProvider<XsltChecker.LanguageLevel, PsiFile> INSTANCE = new XsltSupportProvider();

    @Override
    public CachedValueProvider.Result<XsltChecker.LanguageLevel> compute(PsiFile psiFile) {
      if (!(psiFile instanceof XmlFile xmlFile)) {
        return CachedValueProvider.Result.create(XsltChecker.LanguageLevel.NONE, PsiModificationTracker.MODIFICATION_COUNT);
      }

      if (psiFile instanceof PsiFileEx) {
        if (((PsiFileEx)psiFile).isContentsLoaded()) {
          final XmlDocument doc = xmlFile.getDocument();
          if (doc != null) {
            final XmlTag rootTag = doc.getRootTag();
            if (rootTag != null) {
              XmlAttribute v;
              XsltChecker.LanguageLevel level;
              if (isXsltRootTag(rootTag)) {
                v = rootTag.getAttribute("version");
                level = v != null ? XsltChecker.getLanguageLevel(v.getValue()) : XsltChecker.LanguageLevel.NONE;
              }
              else {
                v = rootTag.getAttribute("version", XSLT_NS);
                level = v != null ? XsltChecker.getLanguageLevel(v.getValue()) : XsltChecker.LanguageLevel.NONE;
              }
              return CachedValueProvider.Result.create(level, rootTag);
            }
          }
        }
      }

      final XsltChecker xsltChecker = new XsltChecker();
      CharSequenceReader reader = new CharSequenceReader(psiFile.getViewProvider().getContents());
      NanoXmlUtil.parse(reader, xsltChecker);
      return CachedValueProvider.Result.create(xsltChecker.getLanguageLevel(), psiFile);
    }
  }
}
