// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.tasks.jira.jql.codeinsight;

import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.completion.util.ParenthesesInsertHandler;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.patterns.PsiElementPattern;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.filters.ElementFilter;
import com.intellij.psi.filters.position.FilterPattern;
import com.intellij.psi.impl.DebugUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.tasks.jira.jql.JqlTokenTypes;
import com.intellij.tasks.jira.jql.psi.*;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import static com.intellij.patterns.PlatformPatterns.psiElement;

/**
 * @author Mikhail Golubev
 */
public class JqlCompletionContributor extends CompletionContributor {
  private static final Logger LOG = Logger.getInstance(JqlCompletionContributor.class);

  private static final FilterPattern BEGINNING_OF_LINE = new FilterPattern(new ElementFilter() {
    @Override
    public boolean isAcceptable(Object element, @Nullable PsiElement context) {
      if (!(element instanceof PsiElement p)) return false;
      PsiFile file = p.getContainingFile().getOriginalFile();
      char[] chars = file.textToCharArray();
      for (int offset = p.getTextOffset() - 1; offset >= 0; offset--) {
        char c = chars[offset];
        if (c == '\n') return true;
        if (!StringUtil.isWhiteSpace(c)) return false;
      }
      return true;
    }

    @Override
    public boolean isClassAcceptable(Class hintClass) {
      return true;
    }
  });

  private static FilterPattern rightAfterElement(final PsiElementPattern.Capture<? extends PsiElement> pattern) {
    return new FilterPattern(new ElementFilter() {
      @Override
      public boolean isAcceptable(Object element, @Nullable PsiElement context) {
        if (!(element instanceof PsiElement)) return false;
        PsiElement prevLeaf = PsiTreeUtil.prevVisibleLeaf((PsiElement)element);
        if (prevLeaf == null) return false;
        PsiElement parent = PsiTreeUtil.findFirstParent(prevLeaf, element1 -> pattern.accepts(element1));
        if (parent == null) return false;
        if (PsiTreeUtil.hasErrorElements(parent)) return false;
        return prevLeaf.getTextRange().getEndOffset() == parent.getTextRange().getEndOffset();
      }

      @Override
      public boolean isClassAcceptable(Class hintClass) {
        return true;
      }
    });
  }

  private static FilterPattern rightAfterElement(Class<? extends PsiElement> aClass) {
    return rightAfterElement(psiElement(aClass));
  }

  // Patterns:

  private static final PsiElementPattern.Capture<PsiElement> AFTER_CLAUSE_WITH_HISTORY_PREDICATE =
    psiElement().and(rightAfterElement(JqlClauseWithHistoryPredicates.class));

  private static final PsiElementPattern.Capture<PsiElement> AFTER_ANY_CLAUSE =
    psiElement().andOr(
      rightAfterElement(JqlTerminalClause.class),
      // in other words after closing parenthesis
      rightAfterElement(JqlSubClause.class));

  private static final PsiElementPattern.Capture<PsiElement> AFTER_ORDER_KEYWORD =
    psiElement().afterLeaf(psiElement(JqlTokenTypes.ORDER_KEYWORD));

  private static final PsiElementPattern.Capture<PsiElement> AFTER_FIELD_IN_CLAUSE =
    psiElement().and(rightAfterElement(
      psiElement(JqlIdentifier.class).
        andNot(psiElement().inside(JqlFunctionCall.class)).
        andNot(psiElement().inside(JqlOrderBy.class))));


  /**
   * e.g. "not | ...", "status = closed and |" or "status = closed or |"
   */
  private static final PsiElementPattern.Capture<PsiElement> BEGINNING_OF_CLAUSE = psiElement().andOr(
    BEGINNING_OF_LINE,
    psiElement().afterLeaf(psiElement().andOr(
      psiElement().withElementType(JqlTokenTypes.AND_OPERATORS),
      psiElement().withElementType(JqlTokenTypes.OR_OPERATORS),
      psiElement().withElementType(JqlTokenTypes.NOT_OPERATORS).
        andNot(psiElement().inside(JqlTerminalClause.class)),
      psiElement().withElementType(JqlTokenTypes.LPAR).
        andNot(psiElement().inside(JqlTerminalClause.class))
    )));

  /**
   * e.g. "status changed on |"
   */
  private static final PsiElementPattern.Capture<PsiElement> AFTER_KEYWORD_IN_HISTORY_PREDICATE = psiElement().
    inside(JqlHistoryPredicate.class). // do not consider "by" inside "order by"
    afterLeaf(psiElement().withElementType(JqlTokenTypes.HISTORY_PREDICATES));

  /**
   * e.g. "duedate > |" or "type was in |"
   */
  private static final PsiElementPattern.Capture<PsiElement> AFTER_OPERATOR_EXCEPT_IS = psiElement().
    inside(JqlTerminalClause.class).
    afterLeaf(
      psiElement().andOr(
        psiElement().withElementType(JqlTokenTypes.SIMPLE_OPERATORS),
        psiElement(JqlTokenTypes.WAS_KEYWORD),
        psiElement(JqlTokenTypes.IN_KEYWORD),
        // "not" is considered only as part of other complex operators
        // "is" and "is not" are not suitable also
        psiElement(JqlTokenTypes.NOT_KEYWORD).
          afterLeaf(psiElement(JqlTokenTypes.WAS_KEYWORD))));
  /**
   * e.g. "foo is |" or "foo is not |"
   */
  private static final PsiElementPattern.Capture<PsiElement> AFTER_IS_OPERATOR = psiElement().
    inside(JqlTerminalClause.class).andOr(
    psiElement().afterLeaf(psiElement(JqlTokenTypes.IS_KEYWORD)),
    psiElement().afterLeaf(psiElement(JqlTokenTypes.NOT_KEYWORD).
      afterLeaf(psiElement(JqlTokenTypes.IS_KEYWORD)))
  );

  /**
   * e.g. "commentary ~ 'spam' order by |" or "assignee = currentUser() order by duedate desc, |"
   */
  private static final PsiElementPattern.Capture<PsiElement> BEGINNING_OF_SORT_KEY = psiElement().
    inside(JqlOrderBy.class).
    andOr(
      psiElement().afterLeaf(psiElement(JqlTokenTypes.COMMA)),
      psiElement().afterLeaf(psiElement(JqlTokenTypes.BY_KEYWORD))
    );

  /**
   * e.g. "status = 'in progress' order by reported |"
   */
  private static final PsiElementPattern.Capture<PsiElement> AFTER_FIELD_IN_SORT_KEY = psiElement().
    afterLeaf(psiElement().withElementType(JqlTokenTypes.VALID_FIELD_NAMES).inside(JqlSortKey.class));

  private static final PsiElementPattern.Capture<PsiElement> INSIDE_LIST = psiElement().
    inside(JqlList.class).
    afterLeaf(
      psiElement().andOr(
        psiElement(JqlTokenTypes.LPAR),
        psiElement(JqlTokenTypes.COMMA)
        // e.g. assignee in ('mark', 'bob', currentUser() | )
      ).andNot(psiElement().inside(JqlFunctionCall.class))
    );

  public JqlCompletionContributor() {
    addKeywordsCompletion();
    addFieldNamesCompletion();
    addFunctionNamesCompletion();
    addEmptyOrNullCompletion();
  }

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

  private void addKeywordsCompletion() {
    extend(CompletionType.BASIC,
           AFTER_ANY_CLAUSE,
           new JqlKeywordCompletionProvider("and", "or", "order by"));
    extend(CompletionType.BASIC,
           AFTER_CLAUSE_WITH_HISTORY_PREDICATE,
           new JqlKeywordCompletionProvider("on", "before", "after", "during", "from", "to", "by"));
    extend(CompletionType.BASIC,
           AFTER_FIELD_IN_CLAUSE,
           new JqlKeywordCompletionProvider("was", "in", "not", "is", "changed"));
    extend(CompletionType.BASIC,
           psiElement().andOr(
             BEGINNING_OF_CLAUSE,
             psiElement().inside(JqlTerminalClause.class).andOr(
               psiElement().afterLeaf(psiElement(JqlTokenTypes.WAS_KEYWORD)),
               psiElement().afterLeaf(psiElement(JqlTokenTypes.IS_KEYWORD)))),
           new JqlKeywordCompletionProvider("not"));
    extend(CompletionType.BASIC,
           psiElement().afterLeaf(
             psiElement().andOr(
               psiElement(JqlTokenTypes.NOT_KEYWORD).
                 andNot(psiElement().afterLeaf(
                   psiElement(JqlTokenTypes.IS_KEYWORD))).
                 andNot(psiElement().withParent(JqlNotClause.class)),
               psiElement(JqlTokenTypes.WAS_KEYWORD))),
           new JqlKeywordCompletionProvider("in"));
    extend(CompletionType.BASIC,
           AFTER_ORDER_KEYWORD,
           new JqlKeywordCompletionProvider("by"));
    extend(CompletionType.BASIC,
           AFTER_FIELD_IN_SORT_KEY,
           new JqlKeywordCompletionProvider("asc", "desc"));
  }

  private void addFieldNamesCompletion() {
    extend(CompletionType.BASIC,
           psiElement().andOr(
             BEGINNING_OF_CLAUSE,
             BEGINNING_OF_SORT_KEY),
           new JqlFieldCompletionProvider(JqlFieldType.UNKNOWN));
  }

  private void addFunctionNamesCompletion() {
    extend(CompletionType.BASIC,
           psiElement().andOr(
             AFTER_OPERATOR_EXCEPT_IS,
             INSIDE_LIST,
             // NOTE: function calls can't be used as other functions arguments according to grammar
             AFTER_KEYWORD_IN_HISTORY_PREDICATE),
           new JqlFunctionCompletionProvider());
  }

  private void addEmptyOrNullCompletion() {
    extend(CompletionType.BASIC,
           AFTER_IS_OPERATOR,
           new JqlKeywordCompletionProvider("empty", "null"));
  }

  private static final class JqlKeywordCompletionProvider extends CompletionProvider<CompletionParameters> {
    private final String[] myKeywords;

    private JqlKeywordCompletionProvider(String... keywords) {
      myKeywords = keywords;
    }

    @Override
    protected void addCompletions(@NotNull CompletionParameters parameters,
                                  @NotNull ProcessingContext context,
                                  @NotNull CompletionResultSet result) {
      for (String keyword : myKeywords) {
        result.addElement(LookupElementBuilder.create(keyword).withBoldness(true));
      }
    }
  }

  private static class JqlFunctionCompletionProvider extends CompletionProvider<CompletionParameters> {

    @Override
    protected void addCompletions(@NotNull CompletionParameters parameters,
                                  @NotNull ProcessingContext context,
                                  @NotNull CompletionResultSet result) {
      JqlFieldType operandType;
      boolean listFunctionExpected;
      PsiElement curElem = parameters.getPosition();
      JqlHistoryPredicate predicate = PsiTreeUtil.getParentOfType(curElem, JqlHistoryPredicate.class);
      if (predicate != null) {
        listFunctionExpected = false;
        JqlHistoryPredicate.Type predicateType = predicate.getType();
        operandType = switch (predicateType) {
          case BEFORE, AFTER, DURING, ON -> JqlFieldType.DATE;
          case BY -> JqlFieldType.USER;
          // from, to
          default -> findTypeOfField(curElem);
        };
      }
      else {
        operandType = findTypeOfField(curElem);
        listFunctionExpected = insideClauseWithListOperator(curElem);
      }
      for (String functionName : JqlStandardFunction.allOfType(operandType, listFunctionExpected)) {
        result.addElement(LookupElementBuilder.create(functionName)
          .withInsertHandler(ParenthesesInsertHandler.NO_PARAMETERS));
      }
    }

    private static JqlFieldType findTypeOfField(PsiElement element) {
      JqlTerminalClause clause = PsiTreeUtil.getParentOfType(element, JqlTerminalClause.class);
      if (clause != null) {
        return JqlStandardField.typeOf(clause.getFieldName());
      }
      return JqlFieldType.UNKNOWN;
    }

    private static boolean insideClauseWithListOperator(PsiElement element) {
      JqlTerminalClause clause = PsiTreeUtil.getParentOfType(element, JqlTerminalClause.class);
      if (clause == null || clause.getType() == null) {
        return false;
      }
      return clause.getType().isListOperator();
    }
  }

  private static final class JqlFieldCompletionProvider extends CompletionProvider<CompletionParameters> {
    private final JqlFieldType myFieldType;

    private JqlFieldCompletionProvider(JqlFieldType fieldType) {
      myFieldType = fieldType;
    }

    @Override
    protected void addCompletions(@NotNull CompletionParameters parameters,
                                  @NotNull ProcessingContext context,
                                  @NotNull CompletionResultSet result) {
      for (String field : JqlStandardField.allOfType(myFieldType)) {
        result.addElement(LookupElementBuilder.create(field));
      }
    }
  }
}
