/*
 * 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.impl.references;

import com.intellij.codeInsight.completion.CompletionInitializationContext;
import com.intellij.codeInsight.daemon.EmptyResolveMessageProvider;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.LocalQuickFixProvider;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.xml.*;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.containers.ContainerUtil;
import org.intellij.lang.xpath.completion.NamespaceLookup;
import org.intellij.lang.xpath.psi.impl.ResolveUtil;
import org.intellij.lang.xpath.xslt.XsltSupport;
import org.intellij.lang.xpath.xslt.context.XsltNamespaceContext;
import org.intellij.lang.xpath.xslt.impl.XsltChecker;
import org.intellij.lang.xpath.xslt.psi.impl.ImplicitModeElement;
import org.intellij.lang.xpath.xslt.util.MatchTemplateMatcher;
import org.intellij.lang.xpath.xslt.util.QNameUtil;
import org.intellij.lang.xpath.xslt.util.XsltCodeInsightUtil;
import org.intellij.plugins.xpathView.XPathBundle;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.xml.namespace.QName;
import java.util.*;

class ModeReference extends SimpleAttributeReference implements PsiPolyVariantReference, EmptyResolveMessageProvider {
  private final boolean myIsDeclaration;
  private final ImplicitModeElement myImplicitModeElement;

  ModeReference(XmlAttribute attribute, boolean isDeclaration) {
    super(attribute);
    if (isDeclaration) {
      myIsDeclaration = true;
    }
    else {
      final PsiFile file = attribute.getContainingFile();
      if (file != null && XsltSupport.getXsltLanguageLevel(file) == XsltChecker.LanguageLevel.V2) {
        final String value = attribute.getValue();
        myIsDeclaration = "#current".equals(value) || "#default".equals(value);
      }
      else {
        myIsDeclaration = false;
      }
    }
    myImplicitModeElement = new ImplicitModeElement(attribute);
  }

  @Override
  protected @NotNull TextRange getTextRange() {
    return myImplicitModeElement.getModeRange();
  }

  @Override
  public Object @NotNull [] getVariants() {
    final PsiFile containingFile = myAttribute.getContainingFile();
    if (containingFile instanceof XmlFile && XsltSupport.isXsltFile(containingFile)) {
      final List<Object> l = new ArrayList<>();
      if (!myImplicitModeElement.hasPrefix()) {
        final Object[] prefixes = getPrefixCompletions(myAttribute);
        ContainerUtil.addAll(l, prefixes);
      }

      if (myImplicitModeElement.getQName() != null) {
        final PsiElement[] modes = ResolveUtil.collect(getMatcher().variantMatcher());
        ContainerUtil.addAll(l, modes);
      }
      return ArrayUtil.toObjectArray(l);
    }
    return ArrayUtilRt.EMPTY_OBJECT_ARRAY;
  }

  @Override
  public boolean isSoft() {
    return myIsDeclaration;
  }

  @Override
  public @Nullable PsiElement resolveImpl() {
    if (myIsDeclaration) {
      return myImplicitModeElement;
    }
    else {
      final ResolveResult[] results = multiResolve(false);
      return results.length == 1 ? results[0].getElement() : null;
    }
  }

  @Override
  public ResolveResult @NotNull [] multiResolve(final boolean incompleteCode) {
    final PsiFile containingFile = myAttribute.getContainingFile();
    if (containingFile instanceof XmlFile && XsltSupport.isXsltFile(containingFile) && myImplicitModeElement.getQName() != null) {
      return PsiElementResolveResult.createResults(ResolveUtil.collect(getMatcher()));
    }
    return ResolveResult.EMPTY_ARRAY;
  }

  private MyModeMatcher getMatcher() {
    return new MyModeMatcher(myAttribute, myImplicitModeElement.getQName());
  }

  public static PsiReference[] create(XmlAttribute attribute, boolean isDeclaration) {
    final String value = attribute.getValue();
    if (value == null) {
      return PsiReference.EMPTY_ARRAY;
    }
    if (value.trim().indexOf(' ') != -1) {
      // TODO: fix for xslt 2.0
      return PsiReference.EMPTY_ARRAY;
    }
    return createImpl(attribute, isDeclaration, value);
  }

  private static PsiReference[] createImpl(XmlAttribute attribute, boolean isDeclaration, String value) {
    final int p = value.indexOf(':');
    if (p == -1) {
      return new PsiReference[]{new ModeReference(attribute, isDeclaration)};
    }
    else if (p == value.length() - 1) {
      return new PsiReference[]{new MyPrefixReference(attribute)};
    }
    else {
      return new PsiReference[]{
        new MyPrefixReference(attribute),
        new ModeReference(attribute, isDeclaration)
      };
    }
  }

  static Object[] getPrefixCompletions(XmlAttribute attribute) {
    final ModeReference.MyModeMatcher matcher = new ModeReference.MyModeMatcher(attribute, QNameUtil.ANY);
    final PsiElement[] modes = ResolveUtil.collect(matcher);
    final Collection<String> prefixes = XsltNamespaceContext.getPrefixes(attribute);
    final Set<NamespaceLookup> lookups = new HashSet<>(prefixes.size());

    for (PsiElement mode : modes) {
      final QName qName = ((ImplicitModeElement)mode).getQName();
      if (qName == null) continue;
      final String prefix = qName.getPrefix();
      if (!prefixes.contains(prefix)) continue;

      lookups.add(new NamespaceLookup(prefix));
    }

    return ArrayUtil.toObjectArray(lookups);
  }

  private static class MyModeMatcher extends MatchTemplateMatcher {
    MyModeMatcher(XmlDocument document, QName mode) {
      super(document, mode);
    }

    MyModeMatcher(XmlElement element, QName mode) {
      super(XsltCodeInsightUtil.getDocument(element), mode);
    }

    @Override
    protected PsiElement transform(XmlTag element) {
      return new ImplicitModeElement(element.getAttribute("mode", null));
    }

    @Override
    public boolean matches(XmlTag element) {
      final String s = element.getAttributeValue("mode");
      return myMode != null &&
             s != null && !s.contains(CompletionInitializationContext.DUMMY_IDENTIFIER_TRIMMED) &&
             super.matches(element);
    }

    @Override
    protected ResolveUtil.Matcher changeDocument(XmlDocument document) {
      return new MyModeMatcher(document, myMode);
    }

    @Override
    public ResolveUtil.Matcher variantMatcher() {
      return new MyModeMatcher(myDocument, myMode != null ? QNameUtil.createAnyLocalName(myMode.getNamespaceURI()) : null);
    }
  }

  @Override
  public @NotNull String getUnresolvedMessagePattern() {
    final QName qName = myImplicitModeElement.getQName();
    if (qName != null && qName != QNameUtil.UNRESOLVED) {
      return XPathBundle.message("inspection.message.undefined.mode", qName);
    }
    else {
      return XPathBundle.partialMessage("inspection.message.undefined.mode", 1);
    }
  }

  private static class MyPrefixReference extends PrefixReference implements LocalQuickFixProvider {
    MyPrefixReference(XmlAttribute attribute) {
      super(attribute);
    }

    @Override
    public @NotNull LocalQuickFix @Nullable [] getQuickFixes() {
      // TODO: This should actually scan all (reachable) xslt files for mode-declarations with the same local name
      return LocalQuickFix.EMPTY_ARRAY;
    }

    @Override
    public Object @NotNull [] getVariants() {
      return getPrefixCompletions(myAttribute);
    }
  }
}
