/*
 * Copyright 2007-2008 Dave Griffith
 *
 * 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 org.jetbrains.plugins.groovy.codeInspection.utils;

import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiType;
import com.intellij.psi.tree.IElementType;
import com.intellij.util.ArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.lang.psi.api.GrRangeExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.GrListOrMap;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.*;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentLabel;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrNamedArgument;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrOpenBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.branch.*;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.clauses.GrCaseSection;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.clauses.GrForClause;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.*;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrIndexProperty;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameter;
import org.jetbrains.plugins.groovy.lang.psi.api.types.GrCodeReferenceElement;
import org.jetbrains.plugins.groovy.lang.psi.api.types.GrTypeElement;
import org.jetbrains.plugins.groovy.lang.psi.util.PsiUtil;

import java.util.Objects;

@SuppressWarnings({
  "MethodWithMultipleLoops",
    "OverlyComplexMethod",
    "OverlyLongMethod",
    "SwitchStatementWithTooManyBranches",
    "SwitchStatement",
    "OverlyComplexClass",
    "ClassWithTooManyMethods"})
public final class EquivalenceChecker {

  private EquivalenceChecker() {
    super();
  }

  private static final int LITERAL_EXPRESSION = 1;
  private static final int REFERENCE_EXPRESSION = 3;
  private static final int CALL_EXPRESSION = 5;
  private static final int NEW_EXPRESSION = 6;
  private static final int ARRAY_LITERAL_EXPRESSION = 7;
  private static final int CLOSABLE_BLOCK_EXPRESSION = 8;
  private static final int PREFIX_EXPRESSION = 10;
  private static final int POSTFIX_EXPRESSION = 11;
  private static final int BINARY_EXPRESSION = 12;
  private static final int CONDITIONAL_EXPRESSION = 13;
  private static final int ASSIGNMENT_EXPRESSION = 14;
  private static final int ELVIS_EXPRESSION = 15;
  private static final int TYPE_CAST_EXPRESSION = 16;
  private static final int SAFE_CAST_EXPRESSION = 17;
  private static final int INSTANCEOF_EXPRESSION = 18;
  private static final int RANGE_EXPRESSION = 19;
  private static final int LIST_OR_MAP_EXPRESSION = 20;
  private static final int INDEX_EXPRESSION = 21;
  private static final int PROPERTY_SELECTION_EXPRESSION = 22;

  private static final int BLOCK_STATEMENT = 1;
  private static final int BREAK_STATEMENT = 2;
  private static final int CONTINUE_STATEMENT = 3;
  private static final int VAR_STATEMENT = 4;
  private static final int EMPTY_STATEMENT = 6;
  private static final int EXPRESSION_STATEMENT = 8;
  private static final int FOR_STATEMENT = 9;
  private static final int IF_STATEMENT = 10;
  private static final int RETURN_STATEMENT = 12;
  private static final int SWITCH_STATEMENT = 14;
  private static final int THROW_STATEMENT = 16;
  private static final int TRY_STATEMENT = 17;
  private static final int WHILE_STATEMENT = 18;
  private static final int SYNCHRONIZED_STATEMENT = 19;
  private static final int ASSERT_STATEMENT = 20;
  private static final int APPLICATION_STATEMENT = 21;

  public static boolean statementsAreEquivalent(@Nullable GrStatement exp1,
                                                @Nullable GrStatement exp2) {
    if (exp1 == null && exp2 == null) {
      return true;
    }
    if (exp1 == null || exp2 == null) {
      return false;
    }
    final int type1 = getStatementType(exp1);
    final int type2 = getStatementType(exp2);
    if (type1 != type2) {
      return false;
    }
    return switch (type1) {
      case BLOCK_STATEMENT -> blockStatementsAreEquivalent((GrBlockStatement)exp1, (GrBlockStatement)exp2);
      case BREAK_STATEMENT, CONTINUE_STATEMENT, EMPTY_STATEMENT -> true;
      case VAR_STATEMENT -> varStatementsAreEquivalent((GrVariableDeclaration)exp1, (GrVariableDeclaration)exp2);
      case APPLICATION_STATEMENT -> applicationStatementsAreEquivalent((GrApplicationStatement)exp1, (GrApplicationStatement)exp2);
      case EXPRESSION_STATEMENT -> expressionStatementsAreEquivalent((GrExpression)exp1, (GrExpression)exp2);
      case FOR_STATEMENT -> forInStatementsAreEquivalent((GrForStatement)exp1, (GrForStatement)exp2);
      case IF_STATEMENT -> ifStatementsAreEquivalent((GrIfStatement)exp1, (GrIfStatement)exp2);
      case RETURN_STATEMENT -> returnStatementsAreEquivalent((GrReturnStatement)exp1, (GrReturnStatement)exp2);
      case SWITCH_STATEMENT -> switchStatementsAreEquivalent((GrSwitchStatement)exp1, (GrSwitchStatement)exp2);
      case THROW_STATEMENT -> throwStatementsAreEquivalent((GrThrowStatement)exp1, (GrThrowStatement)exp2);
      case TRY_STATEMENT -> tryStatementsAreEquivalent((GrTryCatchStatement)exp1, (GrTryCatchStatement)exp2);
      case WHILE_STATEMENT -> whileStatementsAreEquivalent((GrWhileStatement)exp1, (GrWhileStatement)exp2);
      case SYNCHRONIZED_STATEMENT -> synchronizedStatementsAreEquivalent((GrSynchronizedStatement)exp1, (GrSynchronizedStatement)exp2);
      case ASSERT_STATEMENT -> assertStatementsAreEquivalent((GrAssertStatement)exp1, (GrAssertStatement)exp2);
      default -> false;
    };
  }

  private static boolean applicationStatementsAreEquivalent(GrApplicationStatement statement1,
                                                            GrApplicationStatement statement2) {
    final GrExpression funExpression1 = statement1.getInvokedExpression();
    final GrExpression funExpression2 = statement2.getInvokedExpression();
    if (!expressionsAreEquivalent(funExpression1, funExpression2)) {
      return false;
    }

    final GrArgumentList argumentList1 = statement1.getArgumentList();
    final GrArgumentList argumentList2 = statement2.getArgumentList();
    final GrExpression[] args1 = argumentList1.getExpressionArguments();
    final GrExpression[] args2 = argumentList2.getExpressionArguments();
    if (!expressionListsAreEquivalent(args1, args2)) {
      return false;
    }
    final GrNamedArgument[] namedArgs1 = argumentList1.getNamedArguments();
    final GrNamedArgument[] namedArgs2 = argumentList2.getNamedArguments();
    if (!namedArgumentListsAreEquivalent(namedArgs1, namedArgs2)) {
      return false;
    }
    return true;
  }

  private static boolean assertStatementsAreEquivalent(GrAssertStatement statement1, GrAssertStatement statement2) {
    return expressionsAreEquivalent(statement1.getAssertion(), statement2.getAssertion()) &&
      expressionsAreEquivalent(statement1.getErrorMessage(), statement2.getErrorMessage());
  }

  private static boolean synchronizedStatementsAreEquivalent(GrSynchronizedStatement statement1,
                                                             GrSynchronizedStatement statement2) {
    return expressionsAreEquivalent(statement1.getMonitor(), statement2.getMonitor()) &&
        openBlocksAreEquivalent(statement1.getBody(), statement2.getBody());
  }

  private static boolean varStatementsAreEquivalent(@NotNull GrVariableDeclaration statement1,
                                                    @NotNull GrVariableDeclaration statement2) {
    final GrVariable[] variables1 = statement1.getVariables();
    final GrVariable[] variables2 = statement2.getVariables();
    return ArrayUtil.areEqual(variables1, variables2, (v1, v2) -> variablesAreEquivalent(v1, v2));
  }

  private static boolean variablesAreEquivalent(@NotNull GrVariable var1,
                                                @NotNull GrVariable var2) {
    final GrExpression initializer1 = var1.getInitializerGroovy();
    final GrExpression initializer2 = var2.getInitializerGroovy();
    if (!expressionsAreEquivalent(initializer1, initializer2)) {
      return false;
    }
    final PsiType type1 = var1.getType();
    final PsiType type2 = var2.getType();
    if (!typesAreEquivalent(type1, type2)) {
      return false;
    }
    final String name1 = var1.getName();
    final String name2 = var2.getName();
    return name1.equals(name2);
  }

  private static boolean tryStatementsAreEquivalent(@NotNull GrTryCatchStatement statement1,
                                                    @NotNull GrTryCatchStatement statement2) {
    final GrOpenBlock tryBlock1 = statement1.getTryBlock();
    final GrOpenBlock tryBlock2 = statement2.getTryBlock();
    if (!openBlocksAreEquivalent(tryBlock1, tryBlock2)) {
      return false;
    }
    final GrFinallyClause finallyBlock1 = statement1.getFinallyClause();
    final GrFinallyClause finallyBlock2 = statement2.getFinallyClause();
    if (finallyBlock1 != null) {
      if (finallyBlock2 == null || !openBlocksAreEquivalent(finallyBlock1.getBody(), finallyBlock2.getBody())) {
        return false;
      }
    } else if (finallyBlock2 != null) {
      return false;
    }
    final GrCatchClause[] catchBlocks1 = statement1.getCatchClauses();
    final GrCatchClause[] catchBlocks2 = statement2.getCatchClauses();
    return ArrayUtil.areEqual(catchBlocks1, catchBlocks2, (c1, c2) -> catchClausesAreEquivalent(c1, c2));
  }

  private static boolean catchClausesAreEquivalent(GrCatchClause clause1, GrCatchClause clause2) {
    return parametersAreEquivalent(clause1.getParameter(), clause2.getParameter()) &&
        openBlocksAreEquivalent(clause1.getBody(), clause2.getBody());
  }

  private static boolean parametersAreEquivalent(@Nullable GrParameter parameter1,
                                                 @Nullable GrParameter parameter2) {
    if (parameter1 == null || parameter2 == null) {
      return false;
    }
    final PsiType type1 = parameter1.getType();
    final PsiType type2 = parameter2.getType();
    if (!typesAreEquivalent(type1, type2)) {
      return false;
    }
    final String name1 = parameter1.getName();
    final String name2 = parameter2.getName();
    return name1.equals(name2);
  }

  private static boolean typesAreEquivalent(@Nullable PsiType type1, @Nullable PsiType type2) {
    return Objects.equals(type1, type2);
  }

  private static boolean whileStatementsAreEquivalent(@NotNull GrWhileStatement statement1,
                                                      @NotNull GrWhileStatement statement2) {
    final GrExpression condition1 = statement1.getCondition();
    final GrExpression condition2 = statement2.getCondition();
    final GrStatement body1 = statement1.getBody();
    final GrStatement body2 = statement2.getBody();
    return expressionsAreEquivalent(condition1, condition2) &&
        statementsAreEquivalent(body1, body2);
  }

  private static boolean forInStatementsAreEquivalent(@NotNull GrForStatement statement1,
                                                      @NotNull GrForStatement statement2) {
    final GrForClause clause1 = statement1.getClause();
    final GrForClause clause2 = statement2.getClause();
    if (!forClausesAreEquivalent(clause1, clause2)) {
      return false;
    }
    final GrStatement body1 = statement1.getBody();
    final GrStatement body2 = statement2.getBody();
    return statementsAreEquivalent(body1, body2);
  }

  private static boolean forClausesAreEquivalent(@Nullable GrForClause statement1,
                                                 @Nullable GrForClause statement2) {
    if (statement1 == null && statement2 == null) return true;
    if (statement1 == null || statement2 == null) return false;
    GrVariable[] variables1 = statement1.getDeclaredVariables();
    GrVariable[] variables2 = statement2.getDeclaredVariables();
    return ArrayUtil.areEqual(variables1, variables2, (var1, var2) -> 
      (var1 == null && var2 == null) || (var1 != null && var2 != null && variablesAreEquivalent(var1, var2)));
  }

  private static boolean switchStatementsAreEquivalent(@NotNull GrSwitchStatement statement1,
                                                       @NotNull GrSwitchStatement statement2) {
    final GrExpression switchExpression1 = statement1.getCondition();
    final GrExpression switchExpression2 = statement2.getCondition();
    if (!expressionsAreEquivalent(switchExpression1, switchExpression2)) {
      return false;
    }
    final GrCaseSection[] clauses1 = statement1.getCaseSections();
    final GrCaseSection[] clauses2 = statement2.getCaseSections();
    return ArrayUtil.areEqual(clauses1, clauses2, (c1, c2) -> caseClausesAreEquivalent(c1, c2));
  }

  private static boolean caseClausesAreEquivalent(GrCaseSection clause1, GrCaseSection clause2) {
    return ArrayUtil.areEqual(clause1.getExpressions(), clause2.getExpressions(), (l1, l2) -> expressionsAreEquivalent(l1, l2)) &&
           ArrayUtil.areEqual(clause1.getStatements(), clause2.getStatements(), (s1, s2) -> statementsAreEquivalent(s1, s2));
  }

  private static boolean blockStatementsAreEquivalent(@NotNull GrBlockStatement statement1,
                                                      @NotNull GrBlockStatement statement2) {
    final GrOpenBlock block1 = statement1.getBlock();
    final GrOpenBlock block2 = statement2.getBlock();
    return openBlocksAreEquivalent(block1, block2);
  }

  private static boolean openBlocksAreEquivalent(@Nullable GrOpenBlock block1, @Nullable GrOpenBlock block2) {
    if (block1 == null || block2 == null) return false;
    return ArrayUtil.areEqual(block1.getStatements(), block2.getStatements(), (s1, s2) -> statementsAreEquivalent(s1, s2));
  }

  private static boolean ifStatementsAreEquivalent(@NotNull GrIfStatement statement1,
                                                   @NotNull GrIfStatement statement2) {
    final GrExpression condition1 = statement1.getCondition();
    final GrExpression condition2 = statement2.getCondition();
    final GrStatement thenBranch1 = statement1.getThenBranch();
    final GrStatement thenBranch2 = statement2.getThenBranch();
    final GrStatement elseBranch1 = statement1.getElseBranch();
    final GrStatement elseBranch2 = statement2.getElseBranch();
    return expressionsAreEquivalent(condition1, condition2) &&
        statementsAreEquivalent(thenBranch1, thenBranch2) &&
        statementsAreEquivalent(elseBranch1, elseBranch2);
  }

  private static boolean expressionStatementsAreEquivalent(@NotNull GrExpression statement1,
                                                           @NotNull GrExpression statement2) {
    return expressionsAreEquivalent(statement1, statement2);
  }

  private static boolean returnStatementsAreEquivalent(@NotNull GrReturnStatement statement1,
                                                       @NotNull GrReturnStatement statement2) {
    final GrExpression returnValue1 = statement1.getReturnValue();
    final GrExpression returnValue2 = statement2.getReturnValue();
    return expressionsAreEquivalent(returnValue1, returnValue2);
  }

  private static boolean throwStatementsAreEquivalent(@NotNull GrThrowStatement statement1,
                                                      @NotNull GrThrowStatement statement2) {
    final GrExpression exception1 = statement1.getException();
    final GrExpression exception2 = statement2.getException();
    return expressionsAreEquivalent(exception1, exception2);
  }

  @SuppressWarnings({"ConstantConditions"})
  public static boolean expressionsAreEquivalent(@Nullable GrExpression exp1,
                                                 @Nullable GrExpression exp2) {
    if (exp1 == null && exp2 == null) {
      return true;
    }
    if (exp1 == null || exp2 == null) {
      return false;
    }
    GrExpression expToCompare1 = (GrExpression)PsiUtil.skipParentheses(exp1, false);
    GrExpression expToCompare2 = (GrExpression)PsiUtil.skipParentheses(exp2, false);
    final int type1 = getExpressionType(expToCompare1);
    final int type2 = getExpressionType(expToCompare2);
    if (type1 != type2) {
      return false;
    }
    return switch (type1) {
      case LITERAL_EXPRESSION, REFERENCE_EXPRESSION -> {
        final String text1 = expToCompare1.getText();
        final String text2 = expToCompare2.getText();
        yield text1.equals(text2);
      }
      case CALL_EXPRESSION -> methodCallExpressionsAreEquivalent((GrMethodCall)expToCompare1, (GrMethodCall)expToCompare2);
      case NEW_EXPRESSION -> newExpressionsAreEquivalent((GrNewExpression)expToCompare1, (GrNewExpression)expToCompare2);
      case PREFIX_EXPRESSION -> prefixExpressionsAreEquivalent((GrUnaryExpression)expToCompare1, (GrUnaryExpression)expToCompare2);
      case POSTFIX_EXPRESSION -> postfixExpressionsAreEquivalent((GrUnaryExpression)expToCompare1, (GrUnaryExpression)expToCompare2);
      case BINARY_EXPRESSION -> binaryExpressionsAreEquivalent((GrBinaryExpression)expToCompare1, (GrBinaryExpression)expToCompare2);
      case ASSIGNMENT_EXPRESSION ->
        assignmentExpressionsAreEquivalent((GrAssignmentExpression)expToCompare1, (GrAssignmentExpression)expToCompare2);
      case CONDITIONAL_EXPRESSION ->
        conditionalExpressionsAreEquivalent((GrConditionalExpression)expToCompare1, (GrConditionalExpression)expToCompare2);
      case ELVIS_EXPRESSION -> elvisExpressionsAreEquivalent((GrElvisExpression)expToCompare1, (GrElvisExpression)expToCompare2);
      case RANGE_EXPRESSION -> rangeExpressionsAreEquivalent((GrRangeExpression)expToCompare1, (GrRangeExpression)expToCompare2);
      case TYPE_CAST_EXPRESSION ->
        typecastExpressionsAreEquivalent((GrTypeCastExpression)expToCompare1, (GrTypeCastExpression)expToCompare2);
      case SAFE_CAST_EXPRESSION ->
        safeCastExpressionsAreEquivalent((GrSafeCastExpression)expToCompare1, (GrSafeCastExpression)expToCompare2);
      case INSTANCEOF_EXPRESSION ->
        instanceofExpressionsAreEquivalent((GrInstanceOfExpression)expToCompare1, (GrInstanceOfExpression)expToCompare2);
      case INDEX_EXPRESSION ->
        indexExpressionsAreEquivalent((GrIndexProperty)expToCompare1, (GrIndexProperty)expToCompare2);
      case LIST_OR_MAP_EXPRESSION -> listOrMapExpressionsAreEquivalent((GrListOrMap)expToCompare1, (GrListOrMap)expToCompare2);
      case CLOSABLE_BLOCK_EXPRESSION ->
        closableBlockExpressionsAreEquivalent((GrClosableBlock)expToCompare1, (GrClosableBlock)expToCompare2);
      case PROPERTY_SELECTION_EXPRESSION -> // todo
        textOfExpressionsIsEquivalent(expToCompare1, expToCompare2);
      default -> false;
    };
  }

  private static boolean textOfExpressionsIsEquivalent(GrExpression expToCompare1,
                                                       GrExpression expToCompare2) {
    final String text1 = expToCompare1.getText();
    final String text2 = expToCompare2.getText();
    return text1.equals(text2);
  }

  private static boolean closableBlockExpressionsAreEquivalent(GrClosableBlock closableBlock1,
                                                               GrClosableBlock closableBlock2) {
    return ArrayUtil.areEqual(closableBlock1.getStatements(), closableBlock2.getStatements(), (s1, s2) -> statementsAreEquivalent(s1, s2)) &&
           ArrayUtil.areEqual(closableBlock1.getParameters(), closableBlock2.getParameters(), (p1, p2) -> parametersAreEquivalent(p1, p2));
  }

  private static boolean listOrMapExpressionsAreEquivalent(GrListOrMap expression1, GrListOrMap expression2) {
    return expression1.isMap() == expression2.isMap() &&
           expressionListsAreEquivalent(expression1.getInitializers(), expression2.getInitializers()) &&
           namedArgumentListsAreEquivalent(expression1.getNamedArguments(), expression2.getNamedArguments());
  }

  private static boolean arrayDeclarationsAreEquivalent(GrArrayDeclaration declaration1, GrArrayDeclaration declaration2) {
    final int count1 = declaration1.getArrayCount();
    final int count2 = declaration2.getArrayCount();
    if (count1 != count2) {
      return false;
    }
    final GrExpression[] bounds1 = declaration1.getBoundExpressions();
    final GrExpression[] bounds2 = declaration2.getBoundExpressions();
    return expressionListsAreEquivalent(bounds1, bounds2);
  }

  private static boolean instanceofExpressionsAreEquivalent(GrInstanceOfExpression expression1,
                                                            GrInstanceOfExpression expression2) {
    final GrExpression operand1 = expression1.getOperand();
    final GrExpression operand2 = expression2.getOperand();
    if (!expressionsAreEquivalent(operand1, operand2)) {
      return false;
    }
    GrTypeElement typeElement1 = expression1.getTypeElement();
    GrTypeElement typeElement2 = expression2.getTypeElement();
    if (typeElement1 == null || typeElement2 == null) return false;

    final PsiType type1 = typeElement1.getType();
    final PsiType type2 = typeElement2.getType();
    return typesAreEquivalent(type1, type2);
  }

  private static boolean indexExpressionsAreEquivalent(GrIndexProperty expression1,
                                                       GrIndexProperty expression2) {
    return expressionsAreEquivalent(expression1.getInvokedExpression(), expression2.getInvokedExpression()) &&
           argumentListsAreEquivalent(expression1.getArgumentList(), expression2.getArgumentList());
  }

  private static boolean typecastExpressionsAreEquivalent(GrTypeCastExpression expression1,
                                                          GrTypeCastExpression expression2) {
    final GrExpression operand1 = expression1.getOperand();
    final GrExpression operand2 = expression2.getOperand();
    if (!expressionsAreEquivalent(operand1, operand2)) {
      return false;
    }
    final PsiType type1 = expression1.getCastTypeElement().getType();
    final PsiType type2 = expression2.getCastTypeElement().getType();
    return typesAreEquivalent(type1, type2);
  }

  private static boolean safeCastExpressionsAreEquivalent(GrSafeCastExpression expression1,
                                                          GrSafeCastExpression expression2) {
    final GrExpression operand1 = expression1.getOperand();
    final GrExpression operand2 = expression2.getOperand();
    if (!expressionsAreEquivalent(operand1, operand2)) {
      return false;
    }
    final GrTypeElement typeElement1 = expression1.getCastTypeElement();
    final GrTypeElement typeElement2 = expression2.getCastTypeElement();
    final PsiType safe1 = typeElement1 == null ? null : typeElement1.getType();
    final PsiType safe2 = typeElement2 == null ? null : typeElement2.getType();
    return typesAreEquivalent(safe1, safe2);
  }

  private static boolean methodCallExpressionsAreEquivalent(@NotNull GrMethodCall methodExp1,
                                                            @NotNull GrMethodCall methodExp2) {
    final GrExpression methodExpression1 = methodExp1.getInvokedExpression();
    final GrExpression methodExpression2 = methodExp2.getInvokedExpression();
    if (!expressionsAreEquivalent(methodExpression1, methodExpression2)) {
      return false;
    }
    final GrClosableBlock[] closures1 = methodExp1.getClosureArguments();
    final GrClosableBlock[] closures2 = methodExp2.getClosureArguments();
    if (!expressionListsAreEquivalent(closures1, closures2)) {
      return false;
    }

    return argumentListsAreEquivalent(methodExp1.getArgumentList(), methodExp2.getArgumentList());
  }

  private static boolean argumentListsAreEquivalent(@Nullable GrArgumentList list1, @Nullable GrArgumentList list2) {
    if (list1 == null && list2 == null) {
      return true;
    }
    if (list1 == null || list2 == null) {
      return false;
    }
    final GrExpression[] args1 = list1.getExpressionArguments();
    final GrExpression[] args2 = list2.getExpressionArguments();
    if (!expressionListsAreEquivalent(args1, args2)) {
      return false;
    }
    final GrNamedArgument[] namedArgs1 = list1.getNamedArguments();
    final GrNamedArgument[] namedArgs2 = list2.getNamedArguments();
    if (!namedArgumentListsAreEquivalent(namedArgs1, namedArgs2)) {
      return false;
    }
    return true;
  }

  private static boolean namedArgumentListsAreEquivalent(GrNamedArgument[] namedArgs1, GrNamedArgument[] namedArgs2) {
    if (namedArgs1.length != namedArgs2.length) {
      return false;
    }
    for (GrNamedArgument arg1 : namedArgs1) {
      final GrArgumentLabel label1 = arg1.getLabel();
      if (label1 == null) {
        return false;
      }
      final String name1 = label1.getName();
      boolean found = false;
      final GrExpression expression1 = arg1.getExpression();
      for (GrNamedArgument arg2 : namedArgs2) {
        final GrArgumentLabel label2 = arg2.getLabel();
        if (label2 == null) {
          return false;
        }
        final String name2 = label2.getName();
        final GrExpression expression2 = arg2.getExpression();
        if (name1 == null) {
          if (name2 == null &&
              expressionsAreEquivalent(((GrExpression)label1.getNameElement()), (GrExpression)label2.getNameElement()) &&
              expressionsAreEquivalent(expression1, expression2)) {
            found = true;
            break;
          }
        }
        else if (name1.equals(name2) && expressionsAreEquivalent(expression1, expression2)) {
          found = true;
          break;
        }
      }
      if (!found) {
        return false;
      }
    }
    return true;
  }

  private static boolean newExpressionsAreEquivalent(@NotNull GrNewExpression newExp1, @NotNull GrNewExpression newExp2) {
    if (newExp1.getArrayCount() == newExp2.getArrayCount() && newExp1.getArrayCount() != 0) {
      GrCodeReferenceElement referenceElement1 = newExp1.getReferenceElement();
      GrCodeReferenceElement referenceElement2 = newExp2.getReferenceElement();
      if (referenceElement1 == null || referenceElement2 == null) {
        return referenceElement1 == referenceElement2;
      }
      if (referenceElement1.resolve() != referenceElement2.resolve()) {
        return false;
      }
      GrArrayDeclaration arrayDeclaration1 = newExp1.getArrayDeclaration();
      GrArrayDeclaration arrayDeclaration2 = newExp2.getArrayDeclaration();
      if (arrayDeclaration1 == null || arrayDeclaration2 == null) {
        return arrayDeclaration1 == arrayDeclaration2;
      }
      return arrayDeclarationsAreEquivalent(arrayDeclaration1, arrayDeclaration2);
    }
    else {
      final PsiMethod constructor1 = newExp1.resolveMethod();
      final PsiMethod constructor2 = newExp2.resolveMethod();
      if (constructor1 == null || constructor2 == null || !constructor1.equals(constructor2)) {
        return false;
      }
      return argumentListsAreEquivalent(newExp1.getArgumentList(), newExp2.getArgumentList());
    }
  }

  private static boolean prefixExpressionsAreEquivalent(@NotNull GrUnaryExpression prefixExp1,
                                                        @NotNull GrUnaryExpression prefixExp2) {
    final IElementType sign1 = prefixExp1.getOperationTokenType();
    final IElementType sign2 = prefixExp2.getOperationTokenType();
    if (sign1 != sign2) {
      return false;
    }
    final GrExpression operand1 = prefixExp1.getOperand();
    final GrExpression operand2 = prefixExp2.getOperand();
    return expressionsAreEquivalent(operand1, operand2);
  }

  private static boolean postfixExpressionsAreEquivalent(@NotNull GrUnaryExpression postfixExp1,
                                                         @NotNull GrUnaryExpression postfixExp2) {
    final IElementType sign1 = postfixExp1.getOperationTokenType();
    final IElementType sign2 = postfixExp2.getOperationTokenType();
    if (sign1 != sign2) {
      return false;
    }
    final GrExpression operand1 = postfixExp1.getOperand();
    final GrExpression operand2 = postfixExp2.getOperand();
    return expressionsAreEquivalent(operand1, operand2);
  }

  private static boolean binaryExpressionsAreEquivalent(@NotNull GrBinaryExpression binaryExp1,
                                                        @NotNull GrBinaryExpression binaryExp2) {
    final IElementType sign1 = binaryExp1.getOperationTokenType();
    final IElementType sign2 = binaryExp2.getOperationTokenType();
    if (sign1 != sign2) {
      return false;
    }
    final GrExpression lhs1 = binaryExp1.getLeftOperand();
    final GrExpression lhs2 = binaryExp2.getLeftOperand();
    final GrExpression rhs1 = binaryExp1.getRightOperand();
    final GrExpression rhs2 = binaryExp2.getRightOperand();
    return expressionsAreEquivalent(lhs1, lhs2)
        && expressionsAreEquivalent(rhs1, rhs2);
  }

  private static boolean rangeExpressionsAreEquivalent(@NotNull GrRangeExpression rangeExp1,
                                                       @NotNull GrRangeExpression rangeExp2) {
    return expressionsAreEquivalent(rangeExp1.getFrom(), rangeExp2.getFrom()) &&
           expressionsAreEquivalent(rangeExp1.getTo(), rangeExp2.getTo()) &&
           rangeExp1.getBoundaryType() == rangeExp2.getBoundaryType();
  }

  private static boolean assignmentExpressionsAreEquivalent(@NotNull GrAssignmentExpression assignExp1,
                                                            @NotNull GrAssignmentExpression assignExp2) {
    final IElementType sign1 = assignExp1.getOperationTokenType();
    final IElementType sign2 = assignExp2.getOperationTokenType();
    if (sign1 != sign2) {
      return false;
    }
    final GrExpression lhs1 = assignExp1.getLValue();
    final GrExpression lhs2 = assignExp2.getLValue();
    final GrExpression rhs1 = assignExp1.getRValue();
    final GrExpression rhs2 = assignExp2.getRValue();
    return expressionsAreEquivalent(lhs1, lhs2)
        && expressionsAreEquivalent(rhs1, rhs2);
  }

  private static boolean conditionalExpressionsAreEquivalent(@NotNull GrConditionalExpression condExp1,
                                                             @NotNull GrConditionalExpression condExp2) {
    final GrExpression condition1 = condExp1.getCondition();
    final GrExpression condition2 = condExp2.getCondition();
    final GrExpression thenExpression1 = condExp1.getThenBranch();
    final GrExpression thenExpression2 = condExp2.getThenBranch();
    final GrExpression elseExpression1 = condExp1.getElseBranch();
    final GrExpression elseExpression2 = condExp2.getElseBranch();
    return expressionsAreEquivalent(condition1, condition2) &&
           expressionsAreEquivalent(thenExpression1, thenExpression2) &&
           expressionsAreEquivalent(elseExpression1, elseExpression2);
  }

  private static boolean elvisExpressionsAreEquivalent(@NotNull GrElvisExpression condExp1,
                                                       @NotNull GrElvisExpression condExp2) {
    final GrExpression condition1 = condExp1.getCondition();
    final GrExpression condition2 = condExp2.getCondition();
    final GrExpression elseExpression1 = condExp1.getElseBranch();
    final GrExpression elseExpression2 = condExp2.getElseBranch();
    return expressionsAreEquivalent(condition1, condition2)
        && expressionsAreEquivalent(elseExpression1, elseExpression2);
  }

  private static boolean expressionListsAreEquivalent(GrExpression @Nullable [] expressions1,
                                                      GrExpression @Nullable [] expressions2) {
    if (expressions1 == null && expressions2 == null) {
      return true;
    }
    if (expressions1 == null || expressions2 == null) {
      return false;
    }
    return ArrayUtil.areEqual(expressions1, expressions2, (e1, e2) -> expressionsAreEquivalent(e1, e2));
  }

  private static int getExpressionType(@Nullable GrExpression exp) {
