/*
 * Copyright 2000-2016 JetBrains s.r.o.
 *
 * 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 com.intellij.psi.impl.source.xml;

import com.intellij.codeInsight.AutoPopupController;
import com.intellij.codeInsight.completion.InsertHandler;
import com.intellij.codeInsight.completion.InsertionContext;
import com.intellij.codeInsight.completion.PrioritizedLookupElement;
import com.intellij.codeInsight.completion.XmlTagInsertHandler;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.impl.source.html.dtd.HtmlElementDescriptorImpl;
import com.intellij.psi.meta.PsiPresentableMetaData;
import com.intellij.psi.search.EverythingGlobalScope;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.psi.xml.XmlToken;
import com.intellij.psi.xml.XmlTokenType;
import com.intellij.util.Processor;
import com.intellij.util.Processors;
import com.intellij.util.indexing.FileBasedIndex;
import com.intellij.xml.XmlElementDescriptor;
import com.intellij.xml.XmlExtension;
import com.intellij.xml.XmlNamespaceHelper;
import com.intellij.xml.XmlTagNameProvider;
import com.intellij.xml.index.XmlNamespaceIndex;
import com.intellij.xml.index.XsdNamespaceBuilder;
import com.intellij.xml.util.XmlUtil;
import org.jetbrains.annotations.NotNull;

import java.util.*;

public class DefaultXmlTagNameProvider implements XmlTagNameProvider {

  private static final Logger LOG = Logger.getInstance(DefaultXmlTagNameProvider.class);

  @Override
  public void addTagNameVariants(List<LookupElement> elements, @NotNull XmlTag tag, String prefix) {
    final List<String> namespaces;
    if (prefix.isEmpty()) {
      namespaces = new ArrayList<>(Arrays.asList(tag.knownNamespaces()));
      namespaces.add(XmlUtil.EMPTY_URI); // empty namespace
    }
    else {
      namespaces = new ArrayList<>(Collections.singletonList(tag.getNamespace()));
    }
    PsiFile psiFile = tag.getContainingFile();
    XmlExtension xmlExtension = XmlExtension.getExtension(psiFile);
    List<String> nsInfo = new ArrayList<>();
    List<XmlElementDescriptor> variants = TagNameVariantCollector.getTagDescriptors(tag, namespaces, nsInfo);

    if (variants.isEmpty() && psiFile instanceof XmlFile && ((XmlFile)psiFile).getRootTag() == tag) {
      getRootTagsVariants(tag, elements);
      return;
    }

    final Set<String> visited = new HashSet<>();
    for (int i = 0; i < variants.size(); i++) {
      XmlElementDescriptor descriptor = variants.get(i);
      String qname = descriptor.getName(tag);
      if (!visited.add(qname)) continue;
      if (!prefix.isEmpty() && qname.startsWith(prefix + ":")) {
        qname = qname.substring(prefix.length() + 1);
      }

      PsiElement declaration = descriptor.getDeclaration();
      if (declaration != null && !declaration.isValid()) {
        LOG.error(descriptor + " contains invalid declaration: " + declaration);
      }
      LookupElementBuilder lookupElement = declaration == null ? LookupElementBuilder.create(qname) : LookupElementBuilder.create(declaration, qname);
      final int separator = qname.indexOf(':');
      if (separator > 0) {
        lookupElement = lookupElement.withLookupString(qname.substring(separator + 1));
      }
      String ns = nsInfo.get(i);
      if (StringUtil.isNotEmpty(ns)) {
        lookupElement = lookupElement.withTypeText(ns, true);
      }
      if (descriptor instanceof PsiPresentableMetaData) {
        lookupElement = lookupElement.withIcon(((PsiPresentableMetaData)descriptor).getIcon());
      }
      if (xmlExtension.useXmlTagInsertHandler()) {
        lookupElement = lookupElement.withInsertHandler(XmlTagInsertHandler.INSTANCE);
      }
      lookupElement = lookupElement.withCaseSensitivity(!(descriptor instanceof HtmlElementDescriptorImpl));
      elements.add(PrioritizedLookupElement.withPriority(lookupElement, separator > 0 ? 0 : 1));
    }
  }

  private static List<LookupElement> getRootTagsVariants(final XmlTag tag, final List<LookupElement> elements) {

    elements.add(LookupElementBuilder.create("?xml version=\"1.0\" encoding=\"\" ?>").withPresentableText("<?xml version=\"1.0\" encoding=\"\" ?>").withInsertHandler(
      new InsertHandler<LookupElement>() {
        @Override
        public void handleInsert(InsertionContext context, LookupElement item) {
          int offset = context.getEditor().getCaretModel().getOffset();
          context.getEditor().getCaretModel().moveToOffset(offset - 4);
          AutoPopupController.getInstance(context.getProject()).scheduleAutoPopup(context.getEditor());
        }
      }));
    final FileBasedIndex fbi = FileBasedIndex.getInstance();
    Collection<String> result = new ArrayList<>();
    Processor<String> processor = Processors.cancelableCollectProcessor(result);
    fbi.processAllKeys(XmlNamespaceIndex.NAME, processor, tag.getProject());

    final GlobalSearchScope scope = new EverythingGlobalScope();
    for (final String ns : result) {
      if (ns.startsWith("file://")) continue;
      fbi.processValues(XmlNamespaceIndex.NAME, ns, null, new FileBasedIndex.ValueProcessor<XsdNamespaceBuilder>() {
        @Override
        public boolean process(@NotNull final VirtualFile file, XsdNamespaceBuilder value) {
          List<String> tags = value.getRootTags();
          for (String s : tags) {
            elements.add(LookupElementBuilder.create(s).withTypeText(ns).withInsertHandler(new XmlTagInsertHandler() {
              @Override
              public void handleInsert(InsertionContext context, LookupElement item) {
                final Editor editor = context.getEditor();
                final Document document = context.getDocument();
                final int caretOffset = editor.getCaretModel().getOffset();
                final RangeMarker caretMarker = document.createRangeMarker(caretOffset, caretOffset);
                caretMarker.setGreedyToRight(true);

                XmlFile psiFile = (XmlFile)context.getFile();
                boolean incomplete = XmlUtil.getTokenOfType(tag, XmlTokenType.XML_TAG_END) == null && XmlUtil.getTokenOfType(tag, XmlTokenType.XML_EMPTY_ELEMENT_END) == null;
                XmlNamespaceHelper.getHelper(psiFile).insertNamespaceDeclaration(psiFile, editor, Collections.singleton(ns), null, null);
                editor.getCaretModel().moveToOffset(caretMarker.getEndOffset());

                XmlTag rootTag = psiFile.getRootTag();
                if (incomplete) {
                  XmlToken token = XmlUtil.getTokenOfType(rootTag, XmlTokenType.XML_EMPTY_ELEMENT_END);
                  if (token != null) token.delete(); // remove tag end added by smart attribute adder :(
                  PsiDocumentManager.getInstance(context.getProject()).doPostponedOperationsAndUnblockDocument(document);
                  super.handleInsert(context, item);
                }
              }
            }));
          }
          return true;
        }
      }, scope);
    }
    return elements;
  }
}
