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

import com.intellij.codeInsight.completion.CompletionInitializationContext;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlElement;
import com.intellij.ui.IconManager;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import org.intellij.lang.xpath.XPathFile;
import org.intellij.lang.xpath.XPathTokenTypes;
import org.intellij.lang.xpath.context.ContextProvider;
import org.intellij.lang.xpath.context.NamespaceContext;
import org.intellij.lang.xpath.context.VariableContext;
import org.intellij.lang.xpath.context.XPathVersion;
import org.intellij.lang.xpath.context.functions.Function;
import org.intellij.lang.xpath.psi.*;
import org.jetbrains.annotations.NotNull;

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

public final class CompletionLists {
  public static final String INTELLIJ_IDEA_RULEZ = CompletionInitializationContext.DUMMY_IDENTIFIER_TRIMMED;

  private CompletionLists() {
  }

  public static final Set<String> NODE_TYPE_FUNCS = Set.of("text", "node", "comment", "processing-instruction");

  public static final Set<String> NODE_TYPE_FUNCS_V2 = Set.of("text", "node", "comment", "processing-instruction", "attribute", "element", "schema-element", "schema-attribute",
         "document-node");

  public static final Set<String> OPERATORS = Set.of("mul", "div", "and", "or");

  public static final Set<String> AXIS_NAMES = Set.of(
    "ancestor",
    "ancestor-or-self",
    "attribute",
    "child",
    "descendant",
    "descendant-or-self",
    "following",
    "following-sibling",
    "namespace",
    "parent",
    "preceding",
    "preceding-sibling",
    "self"
  );

  private static final com.intellij.util.Function<String,LookupElement> FUNCTION_MAPPING = s -> {
    if (s.equals("processing-instruction")) {
      return new FunctionLookup(s, s + "(pi-target?)");
    } else {
      return new FunctionLookup(s, s + "()");
    }
  };

  public static Collection<LookupElement> getFunctionCompletions(XPathElement element) {
    final XPathFile xpathFile = (XPathFile)element.getContainingFile();

    final ContextProvider contextProvider = ContextProvider.getContextProvider(xpathFile);
    final NamespaceContext nsContext = contextProvider.getNamespaceContext();

    String uri;
    PrefixedName qn = null;
    if (element instanceof QNameElement) {
      qn = ((QNameElement)element).getQName();
      if (qn != null) {
        QName qName = contextProvider.getQName(qn, element);
        if (qn.getPrefix() != null) {
          if (qName != null) {
            uri = qName.getNamespaceURI();
          } else {
            return Collections.emptySet();
          }
        } else {
          uri = null;
        }
      } else {
        uri = null;
      }
    } else {
      uri = null;
    }

    final Map<Pair<QName, Integer>, ? extends Function> functions = contextProvider.getFunctionContext().getFunctions();
    final List<LookupElement> lookups = new ArrayList<>(functions.size());
    for (Map.Entry<Pair<QName, Integer>, ? extends Function> entry : functions.entrySet()) {
      final Function functionDecl = entry.getValue();
      final QName f = entry.getKey().first;
      final String p;
      if (nsContext != null) {
        String namespaceURI = f.getNamespaceURI();
        if (uri != null && !namespaceURI.equals(uri)) {
          continue;
        }
        final String prefixForURI = nsContext.getPrefixForURI(namespaceURI, PsiTreeUtil.getContextOfType(element, XmlElement.class, true));
        if (prefixForURI == null && !namespaceURI.isEmpty()) {
          continue;
        }
        p = qn == null || qn.getPrefix() == null ? makePrefix(prefixForURI) : "";
      } else {
        p = "";
      }
      lookups.add(FunctionLookup.newFunctionLookup(p + f.getLocalPart(), functionDecl));
    }
    return lookups;
  }

  public static Collection<LookupElement> getVariableCompletions(XPathElement reference) {
    final ContextProvider contextProvider = ContextProvider.getContextProvider(reference);
    final VariableContext resolver = contextProvider.getVariableContext();
    if (resolver != null) {
      final Object[] variablesInScope = resolver.getVariablesInScope(reference);
      final List<LookupElement> lookups = new ArrayList<>(variablesInScope.length);
      for (final Object o : variablesInScope) {
        if (o instanceof PsiNamedElement) {
          final String type;
          if (o instanceof XPathVariable) {
            final XPathType t = ((XPathVariable)o).getType();
            if (t != XPathType.UNKNOWN) {
              type = t.getName();
            } else {
              type = "";
            }
          } else {
            type = "";
          }
          final String name = ((PsiNamedElement)o).getName();
          lookups.add(new VariableLookup("$" + name, type, ((PsiNamedElement)o).getIcon(0), (PsiElement)o));
        } else {
          lookups.add(new VariableLookup("$" + o, IconManager.getInstance().getPlatformIcon(com.intellij.ui.PlatformIcons.Variable)));
        }
      }
      return lookups;
    } else {
      return Collections.emptySet();
    }
  }

  public static Collection<LookupElement> getNodeTestCompletions(final XPathNodeTest element) {
    if (!element.isNameTest()) {
      return Collections.emptyList();
    }

    final PrefixedName prefixedName = element.getQName();
    assert prefixedName != null;

    final String canonicalText = prefixedName.toString();
    final String suffix = canonicalText.substring(canonicalText.indexOf(INTELLIJ_IDEA_RULEZ));

    final XPathAxisSpecifier axisSpecifier = element.getStep().getAxisSpecifier();

    final ContextProvider contextProvider = ContextProvider.getContextProvider(element);
    final XmlElement context = contextProvider.getContextElement();

    final boolean insidePrefix = suffix.contains(INTELLIJ_IDEA_RULEZ + ":");

    final Set<LookupElement> list = new HashSet<>();
    addNameCompletions(contextProvider, element, list);

    final String namespacePrefix = prefixedName.getPrefix();
    if (namespacePrefix == null || insidePrefix) {
      addNamespaceCompletions(contextProvider.getNamespaceContext(), list, context);
    }

    final XPathNodeTest.PrincipalType principalType = addContextNames(element, contextProvider, prefixedName, list);

    if (namespacePrefix == null && !insidePrefix) {
      if (axisSpecifier == null || axisSpecifier.isDefaultAxis()) {
        list.addAll(getAxisCompletions());

        // wow, this code sux. find a better implementation
        PsiElement sibling = element.getParent().getPrevSibling();
        while (sibling instanceof PsiWhiteSpace) {
          sibling = sibling.getPrevSibling();
        }

        boolean check = sibling != null;
        if (!check) {
          XPathLocationPath lp = null;
          do {
            lp = PsiTreeUtil.getParentOfType(lp == null ? element : lp, XPathLocationPath.class, true);
          } while (lp != null && lp.getPrevSibling() == null);

          if (lp != null) {
            sibling = lp.getPrevSibling();
          }
        }
        if (sibling instanceof XPathToken && XPathTokenTypes.PATH_OPS.contains(((XPathToken)sibling).getTokenType())) {
          // xx/yy<caret> : prevSibl = /
        }
        else {
          list.addAll(getFunctionCompletions(element));
          list.addAll(getVariableCompletions(element));
        }
      }
      if (principalType == XPathNodeTest.PrincipalType.ELEMENT && prefixedName.getPrefix() == null) {
        list.addAll(getNodeTypeCompletions(element));
      }
    }

    return list;
  }

  private static XPathNodeTest.PrincipalType addContextNames(XPathNodeTest element, ContextProvider contextProvider, PrefixedName prefixedName, Set<? super LookupElement> list) {
    final NamespaceContext namespaceContext = contextProvider.getNamespaceContext();
    final XmlElement context = contextProvider.getContextElement();

    final XPathNodeTest.PrincipalType principalType = element.getPrincipalType();
    if (principalType == XPathNodeTest.PrincipalType.ELEMENT) {
      final Set<QName> elementNames = contextProvider.getElements(false);
      if (elementNames != null) {
        for (QName pair : elementNames) {
          if ("*".equals(pair.getLocalPart())) continue;

          if (namespaceMatches(prefixedName, pair.getNamespaceURI(), namespaceContext, context, true)) {
            if (prefixedName.getPrefix() == null && namespaceContext != null) {
              final String p = namespaceContext.getPrefixForURI(pair.getNamespaceURI(), context);
              list.add(new NodeLookup(makePrefix(p) + pair.getLocalPart(), XPathNodeTest.PrincipalType.ELEMENT));
            } else {
              list.add(new NodeLookup(pair.getLocalPart(), XPathNodeTest.PrincipalType.ELEMENT));
            }
          }
        }
      }
    } else if (principalType == XPathNodeTest.PrincipalType.ATTRIBUTE) {
      final Set<QName> attributeNames = contextProvider.getAttributes(false);
      if (attributeNames != null) {
        for (QName pair : attributeNames) {
          if ("*".equals(pair.getLocalPart())) continue;

          if (namespaceMatches(prefixedName, pair.getNamespaceURI(), namespaceContext, context, false)) {
            if (prefixedName.getPrefix() == null && namespaceContext != null) {
              final String p = namespaceContext.getPrefixForURI(pair.getNamespaceURI(), context);
              list.add(new NodeLookup(makePrefix(p) + pair.getLocalPart(), XPathNodeTest.PrincipalType.ATTRIBUTE));
            } else {
              list.add(new NodeLookup(pair.getLocalPart(), XPathNodeTest.PrincipalType.ATTRIBUTE));
            }
          }
        }
      }
    }
    return principalType;
  }

  private static String makePrefix(String p) {
    return (p != null && !p.isEmpty() ? p + ":" : "");
  }

  private static void addNamespaceCompletions(NamespaceContext namespaceContext, Set<? super LookupElement> list, XmlElement context) {
    if (namespaceContext != null) {
      final Collection<String> knownPrefixes = namespaceContext.getKnownPrefixes(context);
      for (String prefix : knownPrefixes) {
        if (prefix != null && !prefix.isEmpty()) {
          list.add(new NamespaceLookup(prefix));
        }
      }
    }
  }

  private static void addNameCompletions(ContextProvider contextProvider, final XPathNodeTest element, final Set<? super LookupElement> list) {
    final PrefixedName prefixedName = element.getQName();
    final XPathNodeTest.PrincipalType principalType = element.getPrincipalType();

    final Set<PsiFile> files = new HashSet<>();
    final XPathFile file = (XPathFile)element.getContainingFile();
    files.add(file);
    ContainerUtil.addAll(files, contextProvider.getRelatedFiles(file));

    for (PsiFile xpathFile : files) {
      xpathFile.accept(new PsiRecursiveElementVisitor() {
        @Override
        public void visitElement(@NotNull PsiElement e) {
          if (e instanceof XPathNodeTest nodeTest) {

            final XPathNodeTest.PrincipalType _principalType = nodeTest.getPrincipalType();
            if (_principalType == principalType) {
              final PrefixedName _prefixedName = nodeTest.getQName();
              if (_prefixedName != null && prefixedName != null) {
                final String localName = _prefixedName.getLocalName();
                if (!"*".equals(localName) && !localName.contains(INTELLIJ_IDEA_RULEZ)) {
                  if (Objects.equals(_prefixedName.getPrefix(), prefixedName.getPrefix())) {
                    list.add(new NodeLookup(localName, _principalType));
                  } else if (prefixedName.getPrefix() == null) {
                    list.add(new NodeLookup(_prefixedName.toString(), _principalType));
                  }
                }
              }
            }
          }
          super.visitElement(e);
        }
      });
    }
  }

  private static boolean namespaceMatches(PrefixedName prefixedName,
                                          String uri,
                                          NamespaceContext namespaceContext,
                                          XmlElement context,
                                          boolean allowDefault) {
    if (namespaceContext == null) return true;

    final String namespaceURI;
    if (prefixedName.getPrefix() != null) {
      if (uri == null || uri.isEmpty()) return false;

      namespaceURI = namespaceContext.getNamespaceURI(prefixedName.getPrefix(), context);
    } else {
      if (!allowDefault) return (uri == null || uri.isEmpty());

      if ((namespaceURI = namespaceContext.getDefaultNamespace(context)) == null) {
        return (uri == null || uri.isEmpty());
      }
    }
    return uri.equals(namespaceURI);
  }

  public static Collection<LookupElement> getNodeTypeCompletions(XPathElement context) {
    final Set<String> funcs = context.getXPathVersion() == XPathVersion.V1 ?
            NODE_TYPE_FUNCS : NODE_TYPE_FUNCS_V2;

    return ContainerUtil.map(funcs, FUNCTION_MAPPING);
  }

  public static Collection<LookupElement> getAxisCompletions() {
    final ArrayList<LookupElement> lookups = new ArrayList<>(AXIS_NAMES.size());
    for (String s : AXIS_NAMES) {
      lookups.add(new AxisLookup(s));
    }
    return lookups;
  }

  @SuppressWarnings({"RawUseOfParameterizedType"})
  public static Class[] getAllInterfaces(Class<?> clazz) {
    Set<Class<?>> set = new HashSet<>();
    do {
      ContainerUtil.addAll(set, clazz.getInterfaces());
      clazz = clazz.getSuperclass();
    } while (clazz != null);
    return set.toArray(ArrayUtil.EMPTY_CLASS_ARRAY);
  }
}
