/*
 * 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.codeInsight.template.postfix.templates;

import com.google.common.collect.Sets;
import com.intellij.codeInsight.template.CustomLiveTemplateBase;
import com.intellij.codeInsight.template.CustomTemplateCallback;
import com.intellij.codeInsight.template.impl.CustomLiveTemplateLookupElement;
import com.intellij.codeInsight.template.impl.TemplateSettings;
import com.intellij.codeInsight.template.postfix.completion.PostfixTemplateLookupElement;
import com.intellij.codeInsight.template.postfix.settings.PostfixTemplatesSettings;
import com.intellij.diagnostic.AttachmentFactory;
import com.intellij.featureStatistics.FeatureUsageTracker;
import com.intellij.lang.Language;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.command.undo.UndoConstants;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Conditions;
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.PsiFileFactory;
import com.intellij.psi.impl.source.PsiFileImpl;
import com.intellij.psi.templateLanguages.TemplateLanguageUtil;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collection;
import java.util.Set;

public class PostfixLiveTemplate extends CustomLiveTemplateBase {
  public static final String POSTFIX_TEMPLATE_ID = "POSTFIX_TEMPLATE_ID";
  private static final Logger LOG = Logger.getInstance(PostfixLiveTemplate.class);

  @NotNull
  public Set<String> getAllTemplateKeys(PsiFile file, int offset) {
    Set<String> keys = Sets.newHashSet();
    Language language = PsiUtilCore.getLanguageAtOffset(file, offset);
    for (PostfixTemplateProvider provider : LanguagePostfixTemplate.LANG_EP.allForLanguage(language)) {
      keys.addAll(getKeys(provider));
    }
    return keys;
  }

  @Nullable
  private static String computeTemplateKeyWithoutContextChecking(@NotNull PostfixTemplateProvider provider,
                                                                 @NotNull CharSequence documentContent,
                                                                 int currentOffset) {
    int startOffset = currentOffset;
    if (documentContent.length() < startOffset) {
      return null;
    }

    while (startOffset > 0) {
      char currentChar = documentContent.charAt(startOffset - 1);
      if (!Character.isJavaIdentifierPart(currentChar)) {
        if (!provider.isTerminalSymbol(currentChar)) {
          return null;
        }
        startOffset--;
        break;
      }
      startOffset--;
    }
    return String.valueOf(documentContent.subSequence(startOffset, currentOffset));
  }

  @Nullable
  @Override
  public String computeTemplateKey(@NotNull CustomTemplateCallback callback) {
    Editor editor = callback.getEditor();
    CharSequence charsSequence = editor.getDocument().getCharsSequence();
    int offset = editor.getCaretModel().getOffset();
    for (PostfixTemplateProvider provider : LanguagePostfixTemplate.LANG_EP.allForLanguage(getLanguage(callback))) {
      String key = computeTemplateKeyWithoutContextChecking(provider, charsSequence, offset);
      if (key != null && isApplicableTemplate(provider, key, callback.getFile(), editor)) {
        return key;
      }
    }
    return null;
  }

  @Nullable
  @Override
  public String computeTemplateKeyWithoutContextChecking(@NotNull CustomTemplateCallback callback) {
    Editor editor = callback.getEditor();
    int currentOffset = editor.getCaretModel().getOffset();
    for (PostfixTemplateProvider provider : LanguagePostfixTemplate.LANG_EP.allForLanguage(getLanguage(callback))) {
      String key = computeTemplateKeyWithoutContextChecking(provider, editor.getDocument().getCharsSequence(), currentOffset);
      if (key != null) return key;
    }
    return null;
  }

  @Override
  public boolean supportsMultiCaret() {
    return false;
  }

  @Override
  public void expand(@NotNull final String key, @NotNull final CustomTemplateCallback callback) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    FeatureUsageTracker.getInstance().triggerFeatureUsed("editing.completion.postfix");

    Editor editor = callback.getEditor();
    for (PostfixTemplateProvider provider : LanguagePostfixTemplate.LANG_EP.allForLanguage(getLanguage(callback))) {
      PostfixTemplate postfixTemplate = getTemplate(provider, key);
      if (postfixTemplate != null) {
        final PsiFile file = callback.getContext().getContainingFile();
        if (isApplicableTemplate(provider, key, file, editor)) {
          int offset = deleteTemplateKey(file, editor, key);
          try {
            provider.preExpand(file, editor);
            PsiElement context = CustomTemplateCallback.getContext(file, positiveOffset(offset));
            expandTemplate(postfixTemplate, editor, context);
          }
          finally {
            provider.afterExpand(file, editor);
          }
        }
        // don't care about errors in multiCaret mode
        else if (editor.getCaretModel().getAllCarets().size() == 1) {
          LOG.error("Template not found by key: " + key + "; offset = " + callback.getOffset(),
                    AttachmentFactory.createAttachment(callback.getFile().getVirtualFile()));
        }
        return;
      }
    }

    // don't care about errors in multiCaret mode
    if (editor.getCaretModel().getAllCarets().size() == 1) {
      LOG.error("Template not found by key: " + key + "; offset = " + callback.getOffset(),
                    AttachmentFactory.createAttachment(callback.getFile().getVirtualFile()));
    }
  }

  @Override
  public boolean isApplicable(PsiFile file, int offset, boolean wrapping) {
    PostfixTemplatesSettings settings = PostfixTemplatesSettings.getInstance();
    if (wrapping || file == null || settings == null || !settings.isPostfixTemplatesEnabled()) {
      return false;
    }
    Language language = PsiUtilCore.getLanguageAtOffset(file, offset);
    for (PostfixTemplateProvider provider : LanguagePostfixTemplate.LANG_EP.allForLanguage(language)) {
      if (StringUtil.isNotEmpty(computeTemplateKeyWithoutContextChecking(provider, file.getText(), offset + 1))) {
        return true;
      }
    }
    return false;
  }

  @Override
  public boolean supportsWrapping() {
    return false;
  }

  @Override
  public void wrap(@NotNull String selection, @NotNull CustomTemplateCallback callback) {
    throw new UnsupportedOperationException();
  }

  @NotNull
  @Override
  public String getTitle() {
    return "Postfix";
  }

  @Override
  public char getShortcut() {
    PostfixTemplatesSettings settings = PostfixTemplatesSettings.getInstance();
    return settings != null ? (char)settings.getShortcut() : TemplateSettings.TAB_CHAR;
  }

  @Override
  public boolean hasCompletionItem(@NotNull PsiFile file, int offset) {
    return true;
  }

  @NotNull
  @Override
  public Collection<? extends CustomLiveTemplateLookupElement> getLookupElements(@NotNull PsiFile file,
                                                                                 @NotNull Editor editor,
                                                                                 int offset) {
    Collection<CustomLiveTemplateLookupElement> result = ContainerUtil.newHashSet();
    CustomTemplateCallback callback = new CustomTemplateCallback(editor, file);
    for (PostfixTemplateProvider provider : LanguagePostfixTemplate.LANG_EP.allForLanguage(getLanguage(callback))) {
      String key = computeTemplateKeyWithoutContextChecking(callback);
      if (key != null && editor.getCaretModel().getCaretCount() == 1) {
        Condition<PostfixTemplate> isApplicationTemplateFunction = createIsApplicationTemplateFunction(provider, key, file, editor);
        for (PostfixTemplate postfixTemplate : provider.getTemplates()) {
          if (isApplicationTemplateFunction.value(postfixTemplate)) {
            result.add(new PostfixTemplateLookupElement(this, postfixTemplate, postfixTemplate.getKey(), provider, false));
          }
        }
      }
    }

    return result;
  }

  private static void expandTemplate(@NotNull final PostfixTemplate template,
                                     @NotNull final Editor editor,
                                     @NotNull final PsiElement context) {
    if (template.startInWriteAction()) {
      ApplicationManager.getApplication().runWriteAction(() -> CommandProcessor.getInstance()
        .executeCommand(context.getProject(), () -> template.expand(context, editor), "Expand postfix template", POSTFIX_TEMPLATE_ID));
    }
    else {
      template.expand(context, editor);
    }
  }


  private static int deleteTemplateKey(@NotNull final PsiFile file, @NotNull final Editor editor, @NotNull final String key) {
    ApplicationManager.getApplication().assertIsDispatchThread();

    final int currentOffset = editor.getCaretModel().getOffset();
    final int newOffset = currentOffset - key.length();
    ApplicationManager.getApplication().runWriteAction(() -> CommandProcessor.getInstance().runUndoTransparentAction(() -> {
      Document document = editor.getDocument();
      document.deleteString(newOffset, currentOffset);
      editor.getCaretModel().moveToOffset(newOffset);
      PsiDocumentManager.getInstance(file.getProject()).commitDocument(document);
    }));
    return newOffset;
  }

  private static Condition<PostfixTemplate> createIsApplicationTemplateFunction(@NotNull final PostfixTemplateProvider provider,
                                                                                @NotNull String key,
                                                                                @NotNull PsiFile file,
                                                                                @NotNull Editor editor) {
    int currentOffset = editor.getCaretModel().getOffset();
    final int newOffset = currentOffset - key.length();
    CharSequence fileContent = editor.getDocument().getCharsSequence();
    StringBuilder fileContentWithoutKey = new StringBuilder();
    fileContentWithoutKey.append(fileContent.subSequence(0, newOffset));
    fileContentWithoutKey.append(fileContent.subSequence(currentOffset, fileContent.length()));
    PsiFile copyFile = copyFile(file, fileContentWithoutKey);
    Document copyDocument = copyFile.getViewProvider().getDocument();
    if (copyDocument == null) {
      return Conditions.alwaysFalse();
    }

    copyFile = provider.preCheck(copyFile, editor, newOffset);
    copyDocument = copyFile.getViewProvider().getDocument();
    if (copyDocument == null) {
      return Conditions.alwaysFalse();
    }

    final PsiElement context = CustomTemplateCallback.getContext(copyFile, positiveOffset(newOffset));
    final Document finalCopyDocument = copyDocument;
    return template -> template != null && template.isEnabled(provider) && template.isApplicable(context, finalCopyDocument, newOffset);
  }

  @NotNull
  public static PsiFile copyFile(@NotNull PsiFile file, @NotNull StringBuilder fileContentWithoutKey) {
    final PsiFileFactory psiFileFactory = PsiFileFactory.getInstance(file.getProject());
    PsiFile copy = psiFileFactory.createFileFromText(file.getName(), file.getFileType(), fileContentWithoutKey);

    if (copy instanceof PsiFileImpl) {
      ((PsiFileImpl)copy).setOriginalFile(TemplateLanguageUtil.getBaseFile(file));
    }

    VirtualFile vFile = copy.getVirtualFile();
    if (vFile != null) {
      vFile.putUserData(UndoConstants.DONT_RECORD_UNDO, Boolean.TRUE);
    }
    return copy;
  }

  public static boolean isApplicableTemplate(@NotNull PostfixTemplateProvider provider,
                                             @NotNull String key,
                                             @NotNull PsiFile file,
                                             @NotNull Editor editor) {
    return createIsApplicationTemplateFunction(provider, key, file, editor).value(getTemplate(provider, key));
  }

  @NotNull
  private static Set<String> getKeys(@NotNull PostfixTemplateProvider provider) {
    Set<String> result = ContainerUtil.newHashSet();
    for (PostfixTemplate template : provider.getTemplates()) {
      result.add(template.getKey());
    }

    return result;
  }

  @Nullable
  private static PostfixTemplate getTemplate(@NotNull PostfixTemplateProvider provider, @Nullable String key) {
    for (PostfixTemplate template : provider.getTemplates()) {
      if (template.getKey().equals(key)) {
        return template;
      }
    }
    return null;
  }

  private static Language getLanguage(@NotNull CustomTemplateCallback callback) {
    return callback.getContext().getLanguage();
  }

  private static int positiveOffset(int offset) {
    return offset > 0 ? offset - 1 : offset;
  }
}
