// 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.jira.jql;

import com.intellij.lang.ASTNode;
import com.intellij.lang.PsiBuilder;
import com.intellij.lang.PsiParser;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import com.intellij.tasks.TaskBundle;
import org.jetbrains.annotations.NotNull;

/**
 * @author Mikhail Golubev
 */
public final class JqlParser implements PsiParser {
  private static final Logger LOG = Logger.getInstance(JqlParser.class);

  @Override
  public @NotNull ASTNode parse(IElementType root, PsiBuilder builder) {
    //builder.setDebugMode(true);
    PsiBuilder.Marker rootMarker = builder.mark();
    // Parser should accept empty string
    if (!builder.eof()) {
      parseQuery(builder);
      while (!builder.eof()) {
        builder.error(TaskBundle.message("parsing.error.illegal.query.part"));
        builder.advanceLexer();
      }
    }
    rootMarker.done(root);
    return builder.getTreeBuilt();
  }

  private boolean parseQuery(PsiBuilder builder) {
    PsiBuilder.Marker marker = builder.mark();
    parseORClause(builder);
    if (builder.getTokenType() == JqlTokenTypes.ORDER_KEYWORD) {
      parseOrderBy(builder);
    }
    marker.done(JqlElementTypes.QUERY);
    return true;
  }

  private boolean parseORClause(PsiBuilder builder) {
    PsiBuilder.Marker marker = builder.mark();
    if (!parseANDClause(builder)) {
      marker.drop();
      return false;
    }
    while (advanceIfMatches(builder, JqlTokenTypes.OR_OPERATORS)) {
      if (!parseANDClause(builder)) {
        builder.error(TaskBundle.message("parsing.error.expecting.clause"));
      }
      marker.done(JqlElementTypes.OR_CLAUSE);
      marker = marker.precede();
    }
    marker.drop();
    return true;
  }

  private boolean parseANDClause(PsiBuilder builder) {
    PsiBuilder.Marker marker = builder.mark();
    if (!parseNOTClause(builder)) {
      marker.drop();
      return false;
    }
    while (advanceIfMatches(builder, JqlTokenTypes.AND_OPERATORS)) {
      if (!parseNOTClause(builder)) {
        builder.error(TaskBundle.message("parsing.error.expecting.clause"));
      }
      marker.done(JqlElementTypes.AND_CLAUSE);
      marker = marker.precede();
    }
    marker.drop();
    return true;
  }

  private boolean parseNOTClause(PsiBuilder builder) {
    if (JqlTokenTypes.NOT_OPERATORS.contains(builder.getTokenType())) {
      PsiBuilder.Marker marker = builder.mark();
      builder.advanceLexer();
      if (!parseNOTClause(builder)) {
        builder.error(TaskBundle.message("parsing.error.expecting.clause"));
      }
      marker.done(JqlElementTypes.NOT_CLAUSE);
      return true;
    }
    else if (builder.getTokenType() == JqlTokenTypes.LPAR) {
      return parseSubClause(builder);
    }
    return parseTerminalClause(builder);
  }

  private boolean parseSubClause(PsiBuilder builder) {
    LOG.assertTrue(builder.getTokenType() == JqlTokenTypes.LPAR);
    PsiBuilder.Marker marker = builder.mark();
    if (!advanceIfMatches(builder, JqlTokenTypes.LPAR)) {
      marker.drop();
      return false;
    }
    parseORClause(builder);
    if (!advanceIfMatches(builder, JqlTokenTypes.RPAR)) {
      builder.error(TaskBundle.message("parsing.error.expecting"));
    }
    marker.done(JqlElementTypes.SUB_CLAUSE);
    return true;
  }

  private boolean parseTerminalClause(PsiBuilder builder) {
    PsiBuilder.Marker marker = builder.mark();
    if (!parseFieldName(builder)) {
      marker.drop();
      return false;
    }
    if (advanceIfMatches(builder, JqlTokenTypes.IS_KEYWORD)) {
      advanceIfMatches(builder, JqlTokenTypes.NOT_KEYWORD);
      parseOperand(builder);
    }
    else if (advanceIfMatches(builder, JqlTokenTypes.SIMPLE_OPERATORS) || advanceIfMatches(builder, JqlTokenTypes.IN_KEYWORD)) {
      parseOperand(builder);
    }
    else if (advanceIfMatches(builder, JqlTokenTypes.NOT_KEYWORD)) {
      if (!advanceIfMatches(builder, JqlTokenTypes.IN_KEYWORD)) {
        builder.error(TaskBundle.message("parsing.error.expecting.in"));
      }
      parseOperand(builder);
    }
    else if (builder.getTokenType() == JqlTokenTypes.WAS_KEYWORD) {
      parseWASClause(builder);
      marker.done(JqlElementTypes.WAS_CLAUSE);
      return true;
    }
    else if (builder.getTokenType() == JqlTokenTypes.CHANGED_KEYWORD) {
      parseCHANGEDClause(builder);
      marker.done(JqlElementTypes.CHANGED_CLAUSE);
      return true;
    }
    else {
      builder.error(TaskBundle.message("parsing.error.expecting.operator"));
    }
    marker.done(JqlElementTypes.SIMPLE_CLAUSE);
    return true;
  }

  private void parseCHANGEDClause(PsiBuilder builder) {
    LOG.assertTrue(builder.getTokenType() == JqlTokenTypes.CHANGED_KEYWORD);
    if (!advanceIfMatches(builder, JqlTokenTypes.CHANGED_KEYWORD)) {
      return;
    }
    while (JqlTokenTypes.HISTORY_PREDICATES.contains(builder.getTokenType())) {
      parseHistoryPredicate(builder);
    }
  }

  private void parseWASClause(PsiBuilder builder) {
    LOG.assertTrue(builder.getTokenType() == JqlTokenTypes.WAS_KEYWORD);
    if (!advanceIfMatches(builder, JqlTokenTypes.WAS_KEYWORD)) {
      return;
    }
    advanceIfMatches(builder, JqlTokenTypes.NOT_KEYWORD);
    advanceIfMatches(builder, JqlTokenTypes.IN_KEYWORD);
    parseOperand(builder);
    while (JqlTokenTypes.HISTORY_PREDICATES.contains(builder.getTokenType())) {
      parseHistoryPredicate(builder);
    }
  }

  private void parseHistoryPredicate(PsiBuilder builder) {
    LOG.assertTrue(JqlTokenTypes.HISTORY_PREDICATES.contains(builder.getTokenType()));
    PsiBuilder.Marker marker = builder.mark();
    if (!advanceIfMatches(builder, JqlTokenTypes.HISTORY_PREDICATES)) {
      marker.drop();
      return;
    }
    parseOperand(builder);
    marker.done(JqlElementTypes.HISTORY_PREDICATE);
  }

  private boolean parseOperand(PsiBuilder builder) {
    PsiBuilder.Marker marker = builder.mark();
    boolean parsed = true;
    if (builder.getTokenType() == JqlTokenTypes.LPAR) {
      marker.drop();
      parsed = parseList(builder);
    }
    else if (advanceIfMatches(builder, JqlTokenTypes.EMPTY_VALUES)) {
      marker.done(JqlElementTypes.EMPTY);
    }
    else if (advanceIfMatches(builder, JqlTokenTypes.LITERALS)) {
      if (builder.getTokenType() == JqlTokenTypes.LPAR) {
        marker.done(JqlElementTypes.IDENTIFIER);
        marker = marker.precede();
        parseArgumentList(builder);
        marker.done(JqlElementTypes.FUNCTION_CALL);
      }
      else {
        marker.done(JqlElementTypes.LITERAL);
      }
    }
    else {
      marker.drop();
      parsed = false;
    }
    if (!parsed) {
      builder.error(TaskBundle.message("parsing.error.expecting.either.literal.list.or.function.call"));
    }
    return parsed;
  }

  private boolean parseList(PsiBuilder builder) {
    LOG.assertTrue(builder.getTokenType() == JqlTokenTypes.LPAR);
    PsiBuilder.Marker marker = builder.mark();
    if (!advanceIfMatches(builder, JqlTokenTypes.LPAR)) {
      marker.drop();
      return false;
    }
    if (parseOperand(builder)) {
      while (advanceIfMatches(builder, JqlTokenTypes.COMMA)) {
        if (!parseOperand(builder)) {
          break;
        }
      }
    }
    if (!advanceIfMatches(builder, JqlTokenTypes.RPAR)) {
      builder.error(TaskBundle.message("parsing.error.expecting"));
    }
    marker.done(JqlElementTypes.LIST);
    return true;
  }

  private static boolean parseFieldName(PsiBuilder builder) {
    PsiBuilder.Marker marker = builder.mark();
    if (!advanceIfMatches(builder, JqlTokenTypes.VALID_FIELD_NAMES)) {
      builder.error(TaskBundle.message("parsing.error.expecting.field.name"));
      marker.drop();
      return false;
    }
    marker.done(JqlElementTypes.IDENTIFIER);
    return true;
  }

  private static void parseArgumentList(PsiBuilder builder) {
    LOG.assertTrue(builder.getTokenType() == JqlTokenTypes.LPAR);
    PsiBuilder.Marker marker = builder.mark();
    if (!advanceIfMatches(builder, JqlTokenTypes.LPAR)) {
      marker.drop();
      return;
    }
    // empty argument list
    if (advanceIfMatches(builder, JqlTokenTypes.RPAR)) {
      marker.done(JqlElementTypes.ARGUMENT_LIST);
      return;
    }
    PsiBuilder.Marker argument = builder.mark();
    if (!advanceIfMatches(builder, JqlTokenTypes.VALID_ARGUMENTS)) {
      builder.error(TaskBundle.message("parsing.error.expecting.argument"));
      argument.drop();
    }
    else {
      // valid argument is always literal, at least in current implementation of JQU
      argument.done(JqlElementTypes.LITERAL);
      while (advanceIfMatches(builder, JqlTokenTypes.COMMA)) {
        argument = builder.mark();
        if (!advanceIfMatches(builder, JqlTokenTypes.VALID_ARGUMENTS)) {
          marker.drop();
          builder.error(TaskBundle.message("parsing.error.expecting.argument"));
          break;
        }
        argument.done(JqlElementTypes.LITERAL);
      }
    }
    if (!advanceIfMatches(builder, JqlTokenTypes.RPAR)) {
      builder.error(TaskBundle.message("parsing.error.expecting"));
    }
    marker.done(JqlElementTypes.ARGUMENT_LIST);
  }

  private void parseOrderBy(PsiBuilder builder) {
    PsiBuilder.Marker marker = builder.mark();
    if (!advanceIfMatches(builder, JqlTokenTypes.ORDER_KEYWORD)) {
      marker.drop();
      return;
    }
    if (!advanceIfMatches(builder, JqlTokenTypes.BY_KEYWORD)) {
      builder.error(TaskBundle.message("parsing.error.expecting.by"));
      marker.done(JqlElementTypes.ORDER_BY);
      return;
    }
    if (parseSortKey(builder)) {
      while (advanceIfMatches(builder, JqlTokenTypes.COMMA)) {
        if (!parseSortKey(builder)) {
          break;
        }
      }
    }
    marker.done(JqlElementTypes.ORDER_BY);
  }

  private boolean parseSortKey(PsiBuilder builder) {
    PsiBuilder.Marker marker = builder.mark();
    if (!parseFieldName(builder)) {
      marker.drop();
      return false;
    }
    advanceIfMatches(builder, JqlTokenTypes.SORT_ORDERS);
    marker.done(JqlElementTypes.SORT_KEY);
    return true;
  }


  private static boolean advanceIfMatches(PsiBuilder builder, IElementType type) {
    if (builder.getTokenType() == type) {
      builder.advanceLexer();
      return true;
    }
    return false;
  }

  private static boolean advanceIfMatches(PsiBuilder builder, TokenSet set) {
    if (set.contains(builder.getTokenType())) {
      builder.advanceLexer();
      return true;
    }
    return false;
  }
}