// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.indentation;

import com.intellij.lang.PsiBuilder;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public final class OperationParserHelper {
  private static boolean parsePostfixOperation(final @NotNull BinaryOperationParser parser) {
    final PsiBuilder.Marker tempMarker = parser.mark();
    PsiBuilder.Marker lastMarker = tempMarker;
    boolean result = parsePrefixOperation(parser);
    boolean tempMarkerDeleted = false;
    while (parser.getPostfixOperators().contains(parser.getTokenType()) &&
           !parser.getWhitespaceTokenSet().contains(parser.rawLookup(-1)) && !parser.isNewLine()) {
      final PsiBuilder.Marker operationMarker = lastMarker.precede();
      if (!tempMarkerDeleted) {
        tempMarker.drop();
        tempMarkerDeleted = true;
      }
      lastMarker = operationMarker;
      parser.advance();
      parser.done(operationMarker, parser.getPostfixExpressionElementType());
      result = true;
    }
    if (!tempMarkerDeleted) {
      tempMarker.drop();
    }
    return result;
  }

  private static boolean parsePrefixOperation(final @NotNull BinaryOperationParser parser) {
    int prefixCount = 0;
    while (parser.getPrefixOperators().contains(parser.lookAhead(prefixCount))) {
      prefixCount++;
    }
    final PsiBuilder.Marker[] prefixMarkers = new PsiBuilder.Marker[prefixCount];
    final IElementType[] elementTypes = new IElementType[prefixCount];
    for (int i = 0; i < prefixCount; i++) {
      prefixMarkers[i] = parser.mark();
      elementTypes[i] = parser.getPrefixExpressionElementType();
      parser.advance();
    }
    final boolean result = parser.parseSimpleExpression() || prefixCount > 0;
    for (int i = prefixCount - 1; i >= 0; i--) {
      parser.done(prefixMarkers[i], elementTypes[i]);
    }
    return result;
  }

  public static boolean callParsingBinaryOperation(final @NotNull BinaryOperationParser parser, int level) {
    if (level < 0) {
      return parsePostfixOperation(parser);
    }
    return parseBinaryOperation(parser, level);
  }

  private static boolean isBinaryOperator(final @NotNull BinaryOperationParser parser, int level) {
    if (parser instanceof CustomBinaryOperationParser) {
      return ((CustomBinaryOperationParser)parser).isBinaryOperator(level);
    }
    final IElementType tokenType = parser.getTokenType();
    return parser.getOperatorsByPriority()[level].contains(tokenType);
  }

  private static void parseBinaryOperator(final @NotNull BinaryOperationParser parser) {
    if (parser instanceof CustomBinaryOperationParser) {
      ((CustomBinaryOperationParser)parser).parseBinaryOperator();
    } else {
      parser.advance();
    }
  }

  /**
   * Parses arithmetic mult, arithmetic sum, bit operation, relation operation
   * @param level 0 for mult, 1, for sum, 2 for bit, 3 for relations
   */
  private static boolean parseBinaryOperation(final @NotNull BinaryOperationParser parser, int level) {
    final PsiBuilder.Marker tempMarker = parser.mark();
    PsiBuilder.Marker lastMarker = tempMarker;
    boolean result = callParsingBinaryOperation(parser, level - 1);
    boolean tempMarkerDeleted = false;
    while (isBinaryOperator(parser, level) && !parser.isNewLine()) {
      final PsiBuilder.Marker operationMarker = lastMarker.precede();
      if (!tempMarkerDeleted) {
        tempMarker.drop();
        tempMarkerDeleted = true;
      }
      lastMarker = operationMarker;
      parseBinaryOperator(parser);
      callParsingBinaryOperation(parser, level - 1);
      parser.done(operationMarker, parser.getOperationElementTypes()[level]);
      result = true;
    }
    if (!tempMarkerDeleted) {
      tempMarker.drop();
    }
    return result;
  }

  public interface BinaryOperationParser {
    /**
     * Gets the TokenType from PsiBuilder
     * @return IElementType of current element
     */
    IElementType getTokenType();

    /**
     * Checks current token starts the line
     * @return true if new line
     */
    boolean isNewLine();

    /**
     * Advance current position of PsiBuilder
     */
    void advance();

    /**
     * See what token type is in {@code step} ahead / benind (including whitespaces)
     * @param step 0 is current token, -1 is previous, 1 is next and so on
     * @return IElementType of the required element
     */
    IElementType rawLookup(int step);

    /**
     * See what token type is in {@code step} ahead (not including whitespaces)
     * @param step 0 is current token, 1 is next and so on
     * @return IElementType of the required element
     */
    IElementType lookAhead(int step);

    /**
     * Create new marker
     * @return PsiBuilder.Marker of created marker
     */
    PsiBuilder.Marker mark();

    /**
     * Close marker with element type
     * @param marker to close
     * @param elementType to close marker as
     */
    void done(PsiBuilder.Marker marker, IElementType elementType);

    /**
     * Parses operand
     * @return boolean if success
     */
    boolean parseSimpleExpression();

    /**
     * Provides all whitespace tokens
     * @return TokenSet of whitespaces
     */
    TokenSet getWhitespaceTokenSet();

    /**
     * Provides prefix operators
     * @return TokenSet of prefix operators
     */
    TokenSet getPrefixOperators();

    /**
     * Provides postfix operators
     * @return TokenSet of prefix operators
     */
    TokenSet getPostfixOperators();

    /**
     * Provides operation priority and operands
     * @return array of TokenSets
     */
    TokenSet @NotNull [] getOperatorsByPriority();

    /**
     * Provides element types to finish postfix element marker
     * @return IElementType
     */
    @Nullable
    IElementType getPostfixExpressionElementType();

    /**
     * Provides element types to finish prefix element marker
     * @return IElementType
     */
    @Nullable
    IElementType getPrefixExpressionElementType();

    /**
     * Provides element types to finish binary operation element
     * @return array of Element Types
     */
    IElementType @NotNull [] getOperationElementTypes();
  }

  public interface CustomBinaryOperationParser {
    boolean isBinaryOperator(int level);

    void parseBinaryOperator();
  }
}
