// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.tasks.youtrack.lang.codeinsight;

import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiFile;
import com.intellij.psi.impl.DebugUtil;
import com.intellij.tasks.youtrack.YouTrackIntellisense;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;

import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import static com.intellij.tasks.youtrack.YouTrackIntellisense.CompletionItem;

/**
 * @author Mikhail Golubev
 */
public class YouTrackCompletionContributor extends CompletionContributor {
  private static final Logger LOG = Logger.getInstance(YouTrackCompletionContributor.class);
  private static final int TIMEOUT = 2000; // ms

  private static final InsertHandler<LookupElement> INSERT_HANDLER = new MyInsertHandler();

  @Override
  public void fillCompletionVariants(final @NotNull CompletionParameters parameters, @NotNull CompletionResultSet result) {
    if (LOG.isDebugEnabled()) {
      LOG.debug(DebugUtil.psiToString(parameters.getOriginalFile(), false));
    }

    super.fillCompletionVariants(parameters, result);

    PsiFile file = parameters.getOriginalFile();
    final YouTrackIntellisense intellisense = file.getUserData(YouTrackIntellisense.INTELLISENSE_KEY);
    if (intellisense == null) {
      return;
    }

    final Application application = ApplicationManager.getApplication();
    Future<List<CompletionItem>> future = application.executeOnPooledThread(
      () -> intellisense.fetchCompletion(parameters.getOriginalFile().getText(), parameters.getOffset()));
    try {
      final List<CompletionItem> suggestions = future.get(TIMEOUT, TimeUnit.MILLISECONDS);
      // actually backed by original CompletionResultSet
      result = result.withPrefixMatcher(extractPrefix(parameters)).caseInsensitive();
      result.addAllElements(ContainerUtil.map(suggestions, (Function<CompletionItem, LookupElement>)item -> LookupElementBuilder.create(item, item.getOption())
        .withTypeText(item.getDescription(), true)
        .withInsertHandler(INSERT_HANDLER)));
    }
    catch (Exception ignored) {
      //noinspection InstanceofCatchParameter
      if (ignored instanceof TimeoutException) {
        LOG.debug(String.format("YouTrack request took more than %d ms to complete", TIMEOUT));
      }
      LOG.debug(ignored);
    }
  }

  /**
   * Find first word left boundary before cursor and strip leading braces and '#' signs
   */
  private static @NotNull String extractPrefix(CompletionParameters parameters) {
    String text = parameters.getOriginalFile().getText();
    final int caretOffset = parameters.getOffset();
    if (text.isEmpty() || caretOffset == 0) {
      return "";
    }
    int stopAt = text.lastIndexOf('{', caretOffset - 1);
    // caret isn't inside braces
    if (stopAt <= text.lastIndexOf('}', caretOffset - 1)) {
      // we stay right after colon
      if (text.charAt(caretOffset - 1) == ':') {
        stopAt = caretOffset - 1;
      }
      // use rightmost word boundary as last resort
      else {
        stopAt = text.lastIndexOf(' ', caretOffset - 1);
      }
    }
    //int start = CharArrayUtil.shiftForward(text, lastSpace < 0 ? 0 : lastSpace + 1, "#{ ");
    int prefixStart = stopAt + 1;
    if (prefixStart < caretOffset && text.charAt(prefixStart) == '#') {
      prefixStart++;
    }
    return StringUtil.trimLeading(text.substring(prefixStart, caretOffset));
  }


  /**
   * Inserts additional braces around values that contains spaces, colon after attribute names
   * and '#' before short-cut attributes if any
   */
  private static class MyInsertHandler implements InsertHandler<LookupElement> {
    @Override
    public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) {
      final CompletionItem completionItem = (CompletionItem)item.getObject();
      final Document document = context.getDocument();
      final Editor editor = context.getEditor();

      context.commitDocument();
      context.setAddCompletionChar(false);

      final String prefix = completionItem.getPrefix();
      final String suffix = completionItem.getSuffix();
      String text = document.getText();
      int offset = context.getStartOffset();
      // skip possible spaces after '{', e.g. "{  My Project <caret>"
      if (prefix.endsWith("{")) {
        while (offset > prefix.length() && Character.isWhitespace(text.charAt(offset - 1))) {
          offset--;
        }
      }
      if (!prefix.isEmpty() && !hasPrefixAt(document.getText(), offset - prefix.length(), prefix)) {
        document.insertString(offset, prefix);
      }
      offset = context.getTailOffset();
      text = document.getText();
      if (suffix.startsWith("} ")) {
        while (offset < text.length() - suffix.length() && Character.isWhitespace(text.charAt(offset))) {
          offset++;
        }
      }
      if (!suffix.isEmpty() && !hasPrefixAt(text, offset, suffix)) {
        document.insertString(offset, suffix);
      }
      editor.getCaretModel().moveToOffset(context.getTailOffset());
    }
  }

  static boolean hasPrefixAt(String text, int offset, String prefix) {
    if (offset < 0 || offset >= text.length()) {
      return false;
    }
    return text.regionMatches(true, offset, prefix, 0, prefix.length());
  }
}


