// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.groovy.lang.psi.controlFlow.impl;

import com.intellij.openapi.diagnostic.Attachment;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.FList;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.codeInspection.utils.ControlFlowUtils;
import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes;
import org.jetbrains.plugins.groovy.lang.psi.GroovyFileBase;
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElement;
import org.jetbrains.plugins.groovy.lang.psi.GroovyRecursiveElementVisitor;
import org.jetbrains.plugins.groovy.lang.psi.api.*;
import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.GrCondition;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.*;
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.clauses.GrForInClause;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.clauses.GrTraditionalForClause;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.*;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrStringInjection;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameter;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrAnonymousClassDefinition;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod;
import org.jetbrains.plugins.groovy.lang.psi.api.util.GrStatementOwner;
import org.jetbrains.plugins.groovy.lang.psi.controlFlow.*;
import org.jetbrains.plugins.groovy.lang.psi.impl.statements.expressions.TypesUtil;
import org.jetbrains.plugins.groovy.lang.psi.util.GroovyConstantExpressionEvaluator;
import org.jetbrains.plugins.groovy.lang.psi.util.PsiUtil;

import java.util.*;

import static org.jetbrains.plugins.groovy.lang.psi.GroovyElementTypes.*;
import static org.jetbrains.plugins.groovy.lang.psi.controlFlow.impl.VariableDescriptorFactory.createDescriptor;
import static org.jetbrains.plugins.groovy.lang.psi.util.PsiUtilKt.isNullLiteral;

public class ControlFlowBuilder extends GroovyRecursiveElementVisitor {
  private static final Logger LOG = Logger.getInstance(ControlFlowBuilder.class);

  private final List<InstructionImpl> myInstructions = new ArrayList<>();

  private final Deque<InstructionImpl> myProcessingStack = new ArrayDeque<>();
  private GroovyPsiElement myScope;
  private final Deque<GrFunctionalExpression> myFunctionalScopeStack = new ArrayDeque<>();

  private final boolean isFlatFlow;

  /**
   * stack of current catch blocks
   */
  private final Deque<ExceptionInfo> myCaughtExceptionInfos = new ArrayDeque<>();

  /**
   * stack of current conditions
   */
  private FList<ConditionInstruction> myConditions = FList.emptyList();

  /**
   * count of finally blocks surrounding current statement
   */
  private int myFinallyCount;

  /**
   * last visited node
   */
  private InstructionImpl myHead;

  /**
   * list of pending nodes and corresponding scopes sorted by scopes from the biggest to smallest.
   */
  private List<Pair<InstructionImpl, GroovyPsiElement>> myPending = new ArrayList<>();

  /**
   * Storage of all variables
   */
  private final Map<VariableDescriptor, Integer> myVariableMapping;

  private int myInstructionNumber;
  private final GrControlFlowPolicy myPolicy;

  private ControlFlowBuilder(GrControlFlowPolicy policy, boolean isFlatFlow) {
    myPolicy = policy;
    this.isFlatFlow = isFlatFlow;
    this.myVariableMapping = new HashMap<>();
  }

  @Override
  public void visitOpenBlock(@NotNull GrOpenBlock block) {
    final PsiElement parent = block.getParent();
    final PsiElement lbrace = block.getLBrace();
    if (lbrace != null && parent instanceof GrMethod) {
      for (GrParameter parameter : ((GrMethod)parent).getParameters()) {
        if (myPolicy.isVariableInitialized(parameter)) {
          addNode(new ReadWriteVariableInstruction(getDescriptorId(createDescriptor(parameter)), parameter, ReadWriteVariableInstruction.WRITE));
        }
      }
    }
    super.visitOpenBlock(block);

    if (!(block.getParent() instanceof GrBlockStatement && block.getParent().getParent() instanceof GrLoopStatement)) {
      final GrStatement[] statements = block.getStatements();
      if (statements.length > 0) {
        handlePossibleYield(statements[statements.length - 1]);
        handlePossibleReturn(statements[statements.length - 1]);
      }
    }
  }

  @Override
  public void visitExpressionLambdaBody(@NotNull GrExpressionLambdaBody body) {
    addFunctionalExpressionParameters(body.getLambdaExpression());
    body.getExpression().accept(this);
  }

  @Override
  public void visitBlockLambdaBody(@NotNull GrBlockLambdaBody body) {
    addFunctionalExpressionParameters(body.getLambdaExpression());
    addControlFlowInstructions(body);
  }

  @Override
  public void visitFile(@NotNull GroovyFileBase file) {
    super.visitFile(file);
    final GrStatement[] statements = file.getStatements();
    if (statements.length > 0) {
      handlePossibleReturn(statements[statements.length - 1]);
    }
  }

  private @Nullable InstructionImpl handlePossibleReturn(@NotNull GrStatement possibleReturn) {
    if (possibleReturn instanceof GrExpression && ControlFlowBuilderUtil.isCertainlyReturnStatement(possibleReturn)) {
      return addNodeAndCheckPending(new MaybeReturnInstruction((GrExpression)possibleReturn));
    }
    return null;
  }

  public static @NotNull GroovyControlFlow buildControlFlow(@NotNull GroovyPsiElement scope) {
    return buildControlFlow(scope, GrResolverPolicy.getInstance());
  }

  /**
   * Flat control flow includes all functional blocks without skipping. It is an experimental feature, aiming to improve performance of the
   * type DFA.
   */
  @ApiStatus.Experimental
  public static @NotNull GroovyControlFlow buildFlatControlFlow(@NotNull GroovyPsiElement scope) {
    return new ControlFlowBuilder(GrResolverPolicy.getInstance(), true).doBuildFlatControlFlow(scope);
  }

  public static @NotNull GroovyControlFlow buildControlFlow(@NotNull GroovyPsiElement scope, @NotNull GrControlFlowPolicy policy) {
    return new ControlFlowBuilder(policy, false).doBuildControlFlow(scope);
  }

  private GroovyControlFlow doBuildFlatControlFlow(GroovyPsiElement scope) {
    var topOwner = ControlFlowUtils.getTopmostOwner(scope);
    if (topOwner == null) {
      return new GroovyControlFlow(Instruction.EMPTY_ARRAY, getDescriptorMapping());
    }

    myFinallyCount = 0;
    myInstructionNumber = 0;

    myScope = scope;

    startNode(null);
    topOwner.accept(this);

    InstructionImpl end = startNode(null);
    checkPending(end);
    return new GroovyControlFlow(myInstructions.toArray(Instruction.EMPTY_ARRAY), getDescriptorMapping());
  }

  private int getDescriptorId(VariableDescriptor actualDescriptor) {
    myVariableMapping.putIfAbsent(actualDescriptor, myVariableMapping.size() + 1);
    return myVariableMapping.get(actualDescriptor);
  }

  private VariableDescriptor[] getDescriptorMapping() {
    VariableDescriptor[] array = new VariableDescriptor[myVariableMapping.size() + 1];
    for (var entry : myVariableMapping.entrySet()) {
      array[entry.getValue()] = entry.getKey();
    }
    return array;
  }

  private GroovyControlFlow doBuildControlFlow(GroovyPsiElement scope) {
    if (scope instanceof GrLambdaExpression) {
      GrLambdaBody body = ((GrLambdaExpression)scope).getBody();

      Instruction[] flow = body != null ? body.getControlFlow() : Instruction.EMPTY_ARRAY;
      return new GroovyControlFlow(flow, getDescriptorMapping());
    }
    myFinallyCount = 0;
    myInstructionNumber = 0;

    myScope = scope;

    startNode(null);

    if (scope instanceof GrClosableBlock) {
      addFunctionalExpressionParameters((GrFunctionalExpression)scope);
      addControlFlowInstructions((GrStatementOwner)scope);
    }
    else {
      scope.accept(this);
    }

    final InstructionImpl end = startNode(null);
    checkPending(end); //collect return edges


    return new GroovyControlFlow(myInstructions.toArray(Instruction.EMPTY_ARRAY), getDescriptorMapping());
  }

  private void addControlFlowInstructions(final GrStatementOwner owner) {
    PsiElement child = owner.getFirstChild();
    while (child != null) {
      if (child instanceof GroovyPsiElement) {
        ((GroovyPsiElement)child).accept(this);
      }
      child = child.getNextSibling();
    }

    final GrStatement[] statements = owner.getStatements();
    if (statements.length > 0) {
      handlePossibleReturn(statements[statements.length - 1]);
    }
  }

  private void addFunctionalExpressionParameters(GrFunctionalExpression expression) {
    for (GrParameter parameter : expression.getAllParameters()) {
      if (myPolicy.isVariableInitialized(parameter)) {
        addNode(new ReadWriteVariableInstruction(getDescriptorId(createDescriptor(parameter)), parameter, ReadWriteVariableInstruction.WRITE));
      }
    }
  }

  private <T extends InstructionImpl> T addNode(T instruction) {
    instruction.setNumber(myInstructionNumber++);
    myInstructions.add(instruction);
    if (myHead != null) {
      addEdge(myHead, instruction);
    }
    myHead = instruction;
    return instruction;
  }

  private <T extends InstructionImpl> T addNodeAndCheckPending(T i) {
    addNode(i);
    checkPending(i);
    return i;
  }

  private static void addEdge(InstructionImpl begin, InstructionImpl end) {
    begin.addSuccessor(end);
    end.addPredecessor(begin);

    if (!(begin instanceof MixinTypeInstruction)) {
      end.addNegationsFrom(begin);
    }
  }

  @Override
  public void visitLambdaExpression(@NotNull GrLambdaExpression expression) {
    GrLambdaBody body = expression.getBody();
    if (body == null) return;
    if (isFlatFlow) {
      FunctionalBlockBeginInstruction startClosure = new FunctionalBlockBeginInstruction(expression);
      myFunctionalScopeStack.addLast(expression);
      addNode(startClosure);
      addPendingEdge(expression, startClosure);
      addFunctionalExpressionParameters(expression);
      if (body instanceof GrBlockLambdaBody) {
        addControlFlowInstructions((GrStatementOwner)body);
      } else {
        super.visitLambdaBody(body);
      }
      InstructionImpl endClosure = new FunctionalBlockEndInstruction(startClosure);
      addNode(endClosure);
      checkPending(expression.getParent(), endClosure);
      myFunctionalScopeStack.removeLast();
    } else {
      List<kotlin.Pair<ReadWriteVariableInstruction, VariableDescriptor>> reads = ControlFlowBuilderUtil.getReadsWithoutPriorWrites(ControlFlowUtils.getGroovyControlFlow(body), false);
      addReadFromNestedControlFlow(expression, reads);
    }
  }

  @Override
  public void visitClosure(@NotNull GrClosableBlock closure) {
    if (isFlatFlow) {
      FunctionalBlockBeginInstruction startClosure = new FunctionalBlockBeginInstruction(closure);
      myFunctionalScopeStack.addLast(closure);
      addNodeAndCheckPending(startClosure);
      addPendingEdge(closure, startClosure);
      addFunctionalExpressionParameters(closure);
      addControlFlowInstructions(closure);
      InstructionImpl endClosure = new FunctionalBlockEndInstruction(startClosure);
      addNode(endClosure);
      checkPending(closure.getParent(), endClosure);
      myFunctionalScopeStack.removeLast();
    } else {
      //do not go inside closures except gstring injections
      if (closure.getParent() instanceof GrStringInjection) {
        addFunctionalExpressionParameters(closure);

        super.visitClosure(closure);
        return;
      }

      List<kotlin.Pair<ReadWriteVariableInstruction, VariableDescriptor>> reads = ControlFlowBuilderUtil.getReadsWithoutPriorWrites(ControlFlowUtils.getGroovyControlFlow(closure), false);
      addReadFromNestedControlFlow(closure, reads);
    }
  }

  private void addReadFromNestedControlFlow(@NotNull PsiElement anchor, List<kotlin.Pair<ReadWriteVariableInstruction, VariableDescriptor>> reads) {
    for (kotlin.Pair<ReadWriteVariableInstruction, VariableDescriptor> read : reads) {
      PsiElement element = read.getFirst().getElement();
      if (!(element instanceof GrReferenceExpression) || myPolicy.isReferenceAccepted((GrReferenceExpression)element)) {
        addNodeAndCheckPending(new ReadWriteVariableInstruction(getDescriptorId(read.getSecond()), anchor, ReadWriteVariableInstruction.READ));
      }
    }

    addNodeAndCheckPending(new InstructionImpl(anchor));
  }

  @Override
  public void visitBreakStatement(@NotNull GrBreakStatement breakStatement) {
    super.visitBreakStatement(breakStatement);
    GrStatement target = breakStatement.resolveLabel();
    if (target == null) target = breakStatement.findTargetStatement();
    if (target != null) {
      if (myHead != null) {
        addPendingEdge(target, myHead);
      }
      readdPendingEdge(target);
    }
    interruptFlow();
  }

  @Override
  public void visitContinueStatement(@NotNull GrContinueStatement continueStatement) {
    super.visitContinueStatement(continueStatement);
    GrStatement target = continueStatement.resolveLabel();
    if (target == null) target = continueStatement.findTargetStatement();
    if (target != null) {
      final InstructionImpl instruction = findInstruction(target);
      if (instruction != null) {
        if (myHead != null) {
          addEdge(myHead, instruction);
        }
        checkPending(continueStatement, instruction);
      }
      interruptFlow();
    }
  }

  @Override
  public void visitReturnStatement(@NotNull GrReturnStatement returnStatement) {
    boolean isNodeNeeded = myHead == null || myHead.getElement() != returnStatement;
    final GrExpression value = returnStatement.getReturnValue();
    if (value != null) value.accept(this);

    GroovyPsiElement scopeElement = myFunctionalScopeStack.isEmpty() ? null : returnStatement;

    if (isNodeNeeded) {
      InstructionImpl returnInstruction = startNode(returnStatement);
      addPendingEdge(scopeElement, myHead);
      finishNode(returnInstruction);
    }
    else {
      addPendingEdge(scopeElement, myHead);
    }
    interruptFlow();
  }

  @Override
  public void visitAssertStatement(@NotNull GrAssertStatement assertStatement) {
    final InstructionImpl assertInstruction = startNode(assertStatement);

    final GrExpression assertion = assertStatement.getAssertion();
    if (assertion != null) {
      assertion.accept(this);

      InstructionImpl positiveHead = myHead;

      List<GotoInstruction> negations = collectAndRemoveAllPendingNegations(assertStatement);
      if (!negations.isEmpty()) {
        interruptFlow();
        reduceAllNegationsIntoInstruction(assertStatement, negations);
      }

      GrExpression errorMessage = assertStatement.getErrorMessage();
      if (errorMessage != null) {
        errorMessage.accept(this);
      }
      addNode(new ThrowingInstruction(assertStatement));

      final PsiType type = TypesUtil.createTypeByFQClassName(CommonClassNames.JAVA_LANG_ASSERTION_ERROR, assertStatement);
      ExceptionInfo info = findCatch(type);
      if (info != null) {
        info.myThrowers.add(myHead);
      }
      else {
        addPendingEdge(null, myHead);
      }

      myHead = positiveHead;
    }
    finishNode(assertInstruction);
  }

  @Override
  public void visitThrowStatement(@NotNull GrThrowStatement throwStatement) {
    final GrExpression exception = throwStatement.getException();
    if (exception == null) return;

    exception.accept(this);
    final InstructionImpl throwInstruction = new ThrowingInstruction(throwStatement);
    addNodeAndCheckPending(throwInstruction);

    interruptFlow();
    final PsiType type = getNominalTypeNoRecursion(exception);

    // it is impossible to follow the consequences of throwing in closure, so throw will just interrupt the closure it was invoked within
    final GroovyPsiElement scopeElement = myFunctionalScopeStack.isEmpty() ? null : throwStatement;

    if (type != null) {
      ExceptionInfo info = findCatch(type);
      if (info != null) {
        info.myThrowers.add(throwInstruction);
      }
      else {
        addPendingEdge(scopeElement, throwInstruction);
      }
    }
    else {
      addPendingEdge(scopeElement, throwInstruction);
    }
  }

  private static @Nullable PsiType getNominalTypeNoRecursion(final @NotNull GrExpression expression) {
    if (expression instanceof GrNewExpression) {
      return expression.getType();
    }
    else if (expression instanceof GrReferenceExpression && ((GrReferenceExpression)expression).getQualifier() == null) {
      return getTypeByRef((GrReferenceExpression)expression);
    }
    return null;
  }

  private static @Nullable PsiType getTypeByRef(@NotNull GrReferenceExpression invoked) {
    PsiElement resolved = invoked.getStaticReference().resolve();
    return resolved instanceof PsiVariable ? ((PsiVariable)resolved).getType() : null;
  }

  private void interruptFlow() {
    myHead = null;
  }

  private @Nullable ExceptionInfo findCatch(PsiType thrownType) {
    final Iterator<ExceptionInfo> iterator = myCaughtExceptionInfos.descendingIterator();
    while (iterator.hasNext()) {
      final ExceptionInfo info = iterator.next();
      final GrCatchClause clause = info.myClause;
      final GrParameter parameter = clause.getParameter();
      if (parameter != null) {
        final PsiType type = parameter.getType();
        if (type.isAssignableFrom(thrownType)) return info;
      }
    }
    return null;
  }

  @Override
  public void visitLabeledStatement(@NotNull GrLabeledStatement labeledStatement) {
    final InstructionImpl instruction = startNode(labeledStatement);
    super.visitLabeledStatement(labeledStatement);
    finishNode(instruction);
  }

  @Override
  public void visitAssignmentExpression(@NotNull GrAssignmentExpression expression) {
    GrExpression lValue = expression.getLValue();
    if (expression.isOperatorAssignment() || expression.getOperationTokenType() == T_ELVIS_ASSIGN) {
      if (lValue instanceof GrReferenceExpression && myPolicy.isReferenceAccepted((GrReferenceExpression)lValue)) {
        VariableDescriptor descriptor = createDescriptor((GrReferenceExpression)lValue);
        if (descriptor != null) {
          addNodeAndCheckPending(new ReadWriteVariableInstruction(getDescriptorId(descriptor), lValue, ReadWriteVariableInstruction.READ));
        }
      }
    }

    GrExpression rValue = expression.getRValue();
    if (rValue != null) {
      rValue.accept(this);
    }
    lValue.accept(this);
  }

  @Override
  public void visitTupleAssignmentExpression(@NotNull GrTupleAssignmentExpression expression) {
    GrExpression rValue = expression.getRValue();
    if (rValue != null) {
      rValue.accept(this);
    }
    expression.getLValue().accept(this);
  }

  @Override
  public void visitParenthesizedExpression(@NotNull GrParenthesizedExpression expression) {
    final GrExpression operand = expression.getOperand();
    if (operand != null) operand.accept(this);
  }

  @Override
  public void visitUnaryExpression(@NotNull GrUnaryExpression expression) {
    final GrExpression operand = expression.getOperand();
    if (operand == null) return;

    if (expression.getOperationTokenType() != GroovyTokenTypes.mLNOT) {
      operand.accept(this);
      visitCall(expression);
      return;
    }

    FList<ConditionInstruction> conditionsBefore = myConditions;
    ConditionInstruction cond = registerCondition(expression, false);
    addNodeAndCheckPending(cond);

    operand.accept(this);
    visitCall(expression);

    myConditions = conditionsBefore;

    List<GotoInstruction> negations = collectAndRemoveAllPendingNegations(expression);

    InstructionImpl head = myHead;
    addNodeAndCheckPending(new PositiveGotoInstruction(expression, cond));
    handlePossibleReturn(expression);
    addPendingEdge(expression, myHead);

    if (negations.isEmpty()) {
      myHead = head;
    }
    else {
      myHead = reduceAllNegationsIntoInstruction(expression, negations);
    }
  }

  private @Nullable InstructionImpl reduceAllNegationsIntoInstruction(GroovyPsiElement currentScope, List<? extends GotoInstruction> negations) {
    if (negations.size() > 1) {
      InstructionImpl instruction = addNode(new InstructionImpl(currentScope));
      for (GotoInstruction negation : negations) {
        addEdge(negation, instruction);
      }
      return instruction;
    }
    else if (negations.size() == 1) {
      GotoInstruction instruction = negations.get(0);
      myHead = instruction;
      return instruction;
    }
    return null;
  }

  private List<GotoInstruction> collectAndRemoveAllPendingNegations(GroovyPsiElement currentScope) {
    List<GotoInstruction> negations = new ArrayList<>();
    for (Iterator<Pair<InstructionImpl, GroovyPsiElement>> iterator = myPending.iterator(); iterator.hasNext(); ) {
      Pair<InstructionImpl, GroovyPsiElement> pair = iterator.next();
      InstructionImpl instruction = pair.first;
      GroovyPsiElement scope = pair.second;

      if (!PsiTreeUtil.isAncestor(scope, currentScope, true) && instruction instanceof GotoInstruction) {
        negations.add((GotoInstruction)instruction);
        iterator.remove();
      }
    }
    return negations;
  }

  @Override
  public void visitInstanceofExpression(@NotNull GrInstanceOfExpression expression) {
    expression.getOperand().accept(this);
    processInstanceOf(expression, GrInstanceOfExpression.isNegated(expression));
  }

  @Override
  public void visitReferenceExpression(@NotNull GrReferenceExpression refExpr) {
    super.visitReferenceExpression(refExpr);

    if (myPolicy.isReferenceAccepted(refExpr)) {
      VariableDescriptor descriptor = createDescriptor(refExpr);
      if (descriptor == null) return;

      if (ControlFlowUtils.isIncOrDecOperand(refExpr)) {
        final InstructionImpl i = new ReadWriteVariableInstruction(getDescriptorId(descriptor), refExpr, ReadWriteVariableInstruction.READ);
        addNodeAndCheckPending(i);
        addNode(new ReadWriteVariableInstruction(getDescriptorId(descriptor), refExpr, ReadWriteVariableInstruction.WRITE));
      }
      else {
        final int type = PsiUtil.isLValue(refExpr) ? ReadWriteVariableInstruction.WRITE : ReadWriteVariableInstruction.READ;
        addNodeAndCheckPending(new ReadWriteVariableInstruction(getDescriptorId(descriptor), refExpr, type));
      }
    }

    if (refExpr.isQualified() && !(refExpr.getParent() instanceof GrCall)) {
      visitCall(refExpr);
    }
  }

  @Override
  public void visitMethodCall(@NotNull GrMethodCall call) {
    super.visitMethodCall(call);
    addNodeAndCheckPending(new ArgumentsInstruction(call, myVariableMapping));
    visitCall(call);
  }

  @Override
  public void visitConstructorInvocation(@NotNull GrConstructorInvocation invocation) {
    super.visitConstructorInvocation(invocation);
    addNodeAndCheckPending(new ArgumentsInstruction(invocation, myVariableMapping));
    visitCall(invocation);
  }

  @Override
  public void visitNewExpression(@NotNull GrNewExpression newExpression) {
    super.visitNewExpression(newExpression);
    addNodeAndCheckPending(new ArgumentsInstruction(newExpression, myVariableMapping));
    visitCall(newExpression);
  }

  @Override
  public void visitBinaryExpression(@NotNull GrBinaryExpression expression) {
    final GrExpression left = expression.getLeftOperand();
    final GrExpression right = expression.getRightOperand();
    final IElementType opType = expression.getOperationTokenType();

    if (ControlFlowBuilderUtil.isInstanceOfBinary(expression)) {
      expression.getLeftOperand().accept(this);
      processInstanceOf(expression, GrInExpression.isNegated((GrInExpression)expression));
      return;
    }
    if (opType == T_EQ || opType == T_NEQ) {
      if (isNullLiteral(right)) {
        left.accept(this);
        processInstanceOf(expression, opType == T_NEQ);
        return;
      }
      else if (right != null && isNullLiteral(left)) {
        right.accept(this);
        processInstanceOf(expression, opType == T_NEQ);
        return;
      }
    }

    if (opType != GroovyTokenTypes.mLOR && opType != GroovyTokenTypes.mLAND && opType != GroovyTokenTypes.mIMPL && opType != KW_IN && opType != T_NOT_IN) {
      left.accept(this);
      if (right != null) {
        right.accept(this);
      }
      visitCall(expression);
      return;
    }

    FList<ConditionInstruction> conditionsBefore = myConditions;
    ConditionInstruction condition = registerCondition(expression, false);
    addNodeAndCheckPending(condition);

    left.accept(this);

    if (right == null) return;

    final List<GotoInstruction> negations = collectAndRemoveAllPendingNegations(expression);

    visitCall(expression);

    if (opType == GroovyTokenTypes.mLAND) {
      InstructionImpl head = myHead;
      if (negations.isEmpty()) {
        addNode(new NegatingGotoInstruction(expression, condition));
        handlePossibleReturn(expression);
        addPendingEdge(expression, myHead);
      }
      else {
        for (GotoInstruction negation : negations) {
          myHead = negation;
          handlePossibleReturn(expression);
          addPendingEdge(expression, myHead);
        }
      }
      myHead = head;
    }
    else /*if (opType == mLOR)*/ {
      final InstructionImpl instruction =
        addNodeAndCheckPending(new InstructionImpl(expression));//collect all pending edges from left argument
      handlePossibleReturn(expression);
      addPendingEdge(expression, myHead);
      myHead = instruction;

      InstructionImpl head = reduceAllNegationsIntoInstruction(expression, negations);
      if (head != null) myHead = head;
      //addNode(new NegatingGotoInstruction(expression, myInstructionNumber++, condition));
    }
    myConditions = conditionsBefore;

    right.accept(this);
  }

  private void processInstanceOf(GrExpression expression, boolean negated) {
    FList<ConditionInstruction> conditionsBefore = myConditions;
    ConditionInstruction cond = registerCondition(expression, negated);
    addNodeAndCheckPending(cond);

    addNode(new InstanceOfInstruction(expression, cond, myVariableMapping));
    NegatingGotoInstruction negation = new NegatingGotoInstruction(expression, cond);
    addNode(negation);
    InstructionImpl possibleReturn = handlePossibleReturn(expression);
    addPendingEdge(expression, possibleReturn != null ? possibleReturn : negation);

    myHead = cond;
    addNode(new InstanceOfInstruction(expression, cond, myVariableMapping));
    myConditions = conditionsBefore;
  }

  /**
   * Emulates throwing an exception from method call. Should be inserted into all places where method or closure is called, because it
   * can throw something unexpectedly
   */
  private void visitCall(GroovyPsiElement call) {
    //optimization: don't add call instruction if there is no catch or finally block in the context
    if (myCaughtExceptionInfos.size() <= 0 && myFinallyCount <= 0) {
      return;
    }
    final InstructionImpl instruction = new ThrowingInstruction(call);
    addNodeAndCheckPending(instruction);

    for (ExceptionInfo info : myCaughtExceptionInfos) {
      info.myThrowers.add(instruction);
    }

    if (myFinallyCount > 0) {
      addPendingEdge(null, instruction);
    }
  }

  @Override
  public void visitIfStatement(@NotNull GrIfStatement ifStatement) {
    InstructionImpl ifInstruction = startNode(ifStatement);
    FList<ConditionInstruction> conditionsBefore = myConditions;

    final GrCondition condition = ifStatement.getCondition();
    final GrStatement thenBranch = ifStatement.getThenBranch();
    final GrStatement elseBranch = ifStatement.getElseBranch();

    InstructionImpl conditionEnd = null;
    InstructionImpl thenEnd = null;
    InstructionImpl elseEnd = null;

    if (condition != null) {
      condition.accept(this);
      conditionEnd = myHead;
    }

    List<GotoInstruction> negations = collectAndRemoveAllPendingNegations(ifStatement);

    if (thenBranch != null) {
      thenBranch.accept(this);
      handlePossibleReturn(thenBranch);
      thenEnd = myHead;
      interruptFlow();
      readdPendingEdge(ifStatement);
    }

    myHead = reduceAllNegationsIntoInstruction(ifStatement, negations);
    if (myHead == null && conditionEnd != null) {
      myHead = conditionEnd;
    }
    if (elseBranch != null) {
      elseBranch.accept(this);
      handlePossibleReturn(elseBranch);
      elseEnd = myHead;
      interruptFlow();
    }

    if (thenBranch != null || elseBranch != null) {
      if (thenEnd != null || elseEnd != null || elseBranch == null) {
        final InstructionImpl end = new IfEndInstruction(ifStatement);
        addNode(end);

        if (thenEnd != null) {
          addEdge(thenEnd, end);
        }

        if (elseEnd != null) {
          addEdge(elseEnd, end);
        }
        else if (elseBranch == null) {
      //    addEdge(conditionEnd != null ? conditionEnd : ifInstruction, end);
        }
      }
    }

    myConditions = conditionsBefore;
    finishNode(ifInstruction);
  }

  private @NotNull ConditionInstruction registerCondition(@NotNull PsiElement element, boolean negated) {
    ConditionInstruction condition = new ConditionInstruction(element, negated, myConditions);
    myConditions = myConditions.prepend(condition);
    return condition;
  }

  private void acceptNullable(@Nullable GroovyPsiElement element) {
    if (element != null) {
      element.accept(this);
    }
  }

  @Override
  public void visitForStatement(@NotNull GrForStatement forStatement) {
    final GrForClause clause = forStatement.getClause();

    processForLoopInitializer(clause);

    InstructionImpl start = startNode(forStatement);

    addForLoopBreakingEdge(forStatement, clause);

    flushForeachLoopVariable(clause);

    final GrStatement body = forStatement.getBody();
    if (body != null) {
      InstructionImpl bodyInstruction = startNode(body);
      body.accept(this);
      finishNode(bodyInstruction);
    }
    checkPending(start); //check for breaks targeted here

    if (clause instanceof GrTraditionalForClause) {
      acceptNullable(((GrTraditionalForClause)clause).getUpdate());
    }
    if (myHead != null) addEdge(myHead, start);  //loop
    interruptFlow();

    finishNode(start);
  }

  private void processForLoopInitializer(@Nullable GrForClause clause) {
    if (clause instanceof GrTraditionalForClause) {
      acceptNullable(((GrTraditionalForClause)clause).getInitialization());
    }
    else if (clause instanceof GrForInClause forInClause) {
      acceptNullable(forInClause.getIteratedExpression());
      GrVariable indexVariable = forInClause.getIndexVariable();
      if (indexVariable != null) {
        addNode(new ReadWriteVariableInstruction(getDescriptorId(createDescriptor(indexVariable)),
                                                                indexVariable,
                                                                ReadWriteVariableInstruction.WRITE));
      }
      addNodeAndCheckPending(new ReadWriteVariableInstruction(
        getDescriptorId(new LoopIteratorVariableDescriptor(forInClause)),
        forInClause,
        ReadWriteVariableInstruction.WRITE
      ));
    }
  }

  private void addForLoopBreakingEdge(GrForStatement forStatement, @Nullable GrForClause clause) {
    final GroovyPsiElement target = forStatement.getParent() instanceof GrLabeledStatement
                                    ? (GroovyPsiElement)forStatement.getParent()
                                    : forStatement;
    if (clause instanceof GrTraditionalForClause) {
