// 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.codeInspection.streamMigration;

import com.intellij.codeInsight.ExceptionUtil;
import com.intellij.codeInsight.intention.impl.StreamRefactoringUtil;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.options.OptPane;
import com.intellij.java.JavaBundle;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.pom.java.JavaFeature;
import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
import com.intellij.psi.*;
import com.intellij.psi.controlFlow.*;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import com.siyeh.ig.callMatcher.CallMatcher;
import com.siyeh.ig.psiutils.*;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;

import static com.intellij.codeInspection.options.OptPane.checkbox;
import static com.intellij.codeInspection.options.OptPane.pane;
import static com.intellij.codeInspection.streamMigration.OperationReductionMigration.SUM_OPERATION;
import static com.intellij.util.ObjectUtils.tryCast;
import static com.siyeh.ig.psiutils.ControlFlowUtils.InitializerUsageStatus.UNKNOWN;

public final class StreamApiMigrationInspection extends AbstractBaseJavaLocalInspectionTool {
  private static final Logger LOG = Logger.getInstance(StreamApiMigrationInspection.class);

  @SuppressWarnings("PublicField")
  public boolean REPLACE_TRIVIAL_FOREACH;
  @SuppressWarnings("PublicField")
  public boolean SUGGEST_FOREACH;
  private static final String SHORT_NAME = "Convert2streamapi";

  @Override
  public @NotNull OptPane getOptionsPane() {
    return pane(
      checkbox("SUGGEST_FOREACH", JavaBundle.message("checkbox.warn.if.only.foreach.replacement.is.available")),
      checkbox("REPLACE_TRIVIAL_FOREACH", JavaBundle.message("checkbox.warn.if.the.loop.is.trivial"))
    );
  }

  @Override
  public @Nls @NotNull String getGroupDisplayName() {
    return InspectionsBundle.message("group.names.language.level.specific.issues.and.migration.aids");
  }

  @Override
  public boolean isEnabledByDefault() {
    return true;
  }

  @Override
  public @NotNull String getShortName() {
    return SHORT_NAME;
  }

  @Override
  public @NotNull Set<@NotNull JavaFeature> requiredFeatures() {
    return Set.of(JavaFeature.STREAM_OPTIONAL);
  }

  @Override
  public @NotNull PsiElementVisitor buildVisitor(final @NotNull ProblemsHolder holder, boolean isOnTheFly) {
    return new StreamApiMigrationVisitor(holder, isOnTheFly);
  }

  @Contract("null, null -> true; null, !null -> false")
  private static boolean sameReference(PsiExpression expr1, PsiExpression expr2) {
    if (expr1 == null && expr2 == null) return true;
    if (!(expr1 instanceof PsiReferenceExpression ref1) || !(expr2 instanceof PsiReferenceExpression ref2)) return false;
    return Objects.equals(ref1.getReferenceName(), ref2.getReferenceName()) && 
           sameReference(ref1.getQualifierExpression(), ref2.getQualifierExpression());
  }

  /**
   * Extracts an addend from assignment expression like {@code x+=addend} or {@code x = x+addend}
   *
   * @param assignment assignment expression to extract an addend from
   * @return extracted addend expression or null if supplied assignment statement is not an addition
   */
  static @Nullable PsiExpression extractAddend(PsiAssignmentExpression assignment) {
    return extractOperand(assignment, JavaTokenType.PLUSEQ);
  }


  static @Nullable PsiExpression extractOperand(PsiAssignmentExpression assignment, IElementType compoundAssignmentOp) {
    if (compoundAssignmentOp.equals(assignment.getOperationTokenType())) {
      return assignment.getRExpression();
    }
    else if (JavaTokenType.EQ.equals(assignment.getOperationTokenType())) {
      if (assignment.getRExpression() instanceof PsiBinaryExpression binOp) {
        IElementType op = TypeConversionUtil.convertEQtoOperation(compoundAssignmentOp);
        if (op.equals(binOp.getOperationTokenType())) {
          if (sameReference(binOp.getLOperand(), assignment.getLExpression())) {
            return binOp.getROperand();
          }
          if (sameReference(binOp.getROperand(), assignment.getLExpression())) {
            return binOp.getLOperand();
          }
        }
      }
    }
    return null;
  }


  static @Nullable PsiVariable extractSumAccumulator(PsiAssignmentExpression assignment) {
    return extractAccumulator(assignment, JavaTokenType.PLUSEQ);
  }


  static @Nullable PsiVariable extractAccumulator(PsiAssignmentExpression assignment, IElementType compoundAssignmentOp) {
    PsiReferenceExpression lExpr = tryCast(assignment.getLExpression(), PsiReferenceExpression.class);
    if (lExpr == null) return null;
    PsiVariable var = tryCast(lExpr.resolve(), PsiVariable.class);
    if (var == null) return null;
    if (compoundAssignmentOp.equals(assignment.getOperationTokenType())) {
      return var;
    }
    else if (JavaTokenType.EQ.equals(assignment.getOperationTokenType())) {
      if (assignment.getRExpression() instanceof PsiBinaryExpression binOp) {
        IElementType op = TypeConversionUtil.convertEQtoOperation(compoundAssignmentOp);
        if (op.equals(binOp.getOperationTokenType())) {
          PsiExpression left = binOp.getLOperand();
          PsiExpression right = binOp.getROperand();
          if (sameReference(left, lExpr) || sameReference(right, lExpr)) {
            return var;
          }
        }
      }
    }
    return null;
  }

  /**
   * Extract incremented value from expression which looks like {@code x++}, {@code ++x}, {@code x = x + 1} or {@code x += 1}
   *
   * @param expression expression to extract the incremented value
   * @return an extracted incremented value or null if increment pattern is not detected in the supplied expression
   */
  @Contract("null -> null")
  static PsiExpression extractIncrementedLValue(PsiExpression expression) {
    if (expression instanceof PsiUnaryExpression) {
      if (JavaTokenType.PLUSPLUS.equals(((PsiUnaryExpression)expression).getOperationTokenType())) {
        return ((PsiUnaryExpression)expression).getOperand();
      }
    }
    else if (expression instanceof PsiAssignmentExpression assignment && ExpressionUtils.isLiteral(extractAddend(assignment), 1)) {
      return assignment.getLExpression();
    }
    return null;
  }

  private static @Nullable PsiLocalVariable getIncrementedVariable(PsiExpression expression, TerminalBlock tb, List<PsiVariable> variables) {
    // have only one non-final variable
    if (variables.size() != 1) return null;

    // have single expression which is either ++x or x++ or x+=1 or x=x+1
    PsiReferenceExpression operand = tryCast(extractIncrementedLValue(expression), PsiReferenceExpression.class);
    if (operand == null) return null;
    PsiLocalVariable variable = tryCast(operand.resolve(), PsiLocalVariable.class);

    // the referred variable is the same as non-final variable and not used in intermediate operations
    if (variable != null && variables.contains(variable) && !tb.isReferencedInOperations(variable)) {
      return variable;
    }
    return null;
  }

  private static @Nullable PsiVariable getAccumulatedVariable(TerminalBlock tb,
                                                              List<PsiVariable> variables,
                                                              OperationReductionMigration.ReductionOperation operation) {
    IElementType compoundAssignmentOp = operation.getCompoundAssignmentOp();
    // have only one non-final variable
    if (variables.size() != 1) return null;

    PsiAssignmentExpression assignment = tb.getSingleExpression(PsiAssignmentExpression.class);
    if (assignment == null) return null;
    PsiVariable var = extractAccumulator(assignment, compoundAssignmentOp);

    // the referred variable is the same as non-final variable
    if (var == null || !variables.contains(var)) return null;
    if (!operation.getAccumulatorRestriction().test(var)) return null;

    // the referred variable is not used in intermediate operations
    if (tb.isReferencedInOperations(var)) return null;
    PsiExpression operand = extractOperand(assignment, compoundAssignmentOp);
    LOG.assertTrue(operand != null);
    if (VariableAccessUtils.variableIsUsed(var, operand)) return null;
    return var;
  }

  static boolean isAddAllCall(TerminalBlock tb) {
    PsiMethodCallExpression call = tb.getSingleMethodCall();
    if (call == null || tb.getVariable().getType() instanceof PsiPrimitiveType) return false;
    if (!ExpressionUtils.isReferenceTo(call.getArgumentList().getExpressions()[0], tb.getVariable())) return false;
    if (!"add".equals(call.getMethodExpression().getReferenceName())) return false;
    PsiExpression qualifierExpression = call.getMethodExpression().getQualifierExpression();
    if (qualifierExpression == null || qualifierExpression instanceof PsiThisExpression) {
      PsiMethod method = PsiTreeUtil.getParentOfType(call, PsiMethod.class);
      return method == null || !method.getName().equals("addAll");
    }
    return !(qualifierExpression instanceof PsiMethodCallExpression);
  }

  @Contract("null, _, _ -> false")
  static boolean isCallOf(PsiMethodCallExpression call, String className, String... methodNames) {
    if (call == null) return false;
    PsiReferenceExpression methodExpression = call.getMethodExpression();
    String name = methodExpression.getReferenceName();
    if (!ArrayUtil.contains(name, methodNames)) return false;
    PsiMethod maybeMapMethod = call.resolveMethod();
    if (maybeMapMethod == null ||
        maybeMapMethod.getParameterList().getParametersCount() != call.getArgumentList().getExpressionCount()) {
      return false;
    }
    PsiClass containingClass = maybeMapMethod.getContainingClass();
    if (containingClass == null) return false;
    if (className.equals(containingClass.getQualifiedName())) return true;
    PsiMethod[] superMethods = maybeMapMethod.findDeepestSuperMethods();
    return StreamEx.of(superMethods).map(PsiMember::getContainingClass).nonNull().map(PsiClass::getQualifiedName).has(className);
  }

  private static boolean isCountOperation(List<PsiVariable> nonFinalVariables, TerminalBlock tb) {
    PsiLocalVariable variable = getIncrementedVariable(tb.getSingleExpression(PsiExpression.class), tb, nonFinalVariables);
    PsiExpression counter = tb.getCountExpression();
    if (counter == null) {
      return variable != null;
    }
    if (tb.isEmpty()) {
      // like "if(++count == limit) break"
      variable = getIncrementedVariable(counter, tb, nonFinalVariables);
    }
    else if (!ExpressionUtils.isReferenceTo(counter, variable)) {
      return false;
    }
    return variable != null &&
           ExpressionUtils.isZero(variable.getInitializer()) &&
           ControlFlowUtils.getInitializerUsageStatus(variable, tb.getStreamSourceStatement()) != UNKNOWN;
  }

  private static boolean isTrivial(TerminalBlock tb) {
    PsiVariable variable = tb.getVariable();
    final PsiExpression candidate = LambdaCanBeMethodReferenceInspection
      .canBeMethodReferenceProblem(tb.getSingleStatement(),
                                   new PsiVariable[]{variable}, createDefaultConsumerType(variable.getProject(), variable), null);
    if (!(candidate instanceof PsiCallExpression)) return true;
    final PsiMethod method = ((PsiCallExpression)candidate).resolveMethod();
    return method == null;
  }

  private static @Nullable PsiClassType createDefaultConsumerType(Project project, PsiVariable variable) {
    final JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(project);
    final PsiClass consumerClass = psiFacade.findClass(CommonClassNames.JAVA_UTIL_FUNCTION_CONSUMER, variable.getResolveScope());
    return consumerClass != null ? psiFacade.getElementFactory().createType(consumerClass, variable.getType()) : null;
  }

  static boolean isVariableSuitableForStream(PsiVariable variable, PsiStatement statement, TerminalBlock tb) {
    PsiElement block = PsiUtil.getVariableCodeBlock(variable, statement);
    if (block != null) {
      Predicate<PsiElement> notAllowedWrite = e -> e instanceof PsiReferenceExpression ref && 
                                                   PsiUtil.isAccessedForWriting(ref) &&
                                                   ref.isReferenceTo(variable) &&
                                                   tb.operations().noneMatch(op -> op.isWriteAllowed(variable, ref));
      if (PsiTreeUtil.processElements(block, notAllowedWrite.negate()::test)) return true;
    }
    return ControlFlowUtil.isEffectivelyFinal(variable, statement);
  }

  static String tryUnbox(PsiVariable variable) {
    PsiType type = variable.getType();
    String mapOp = null;
    if (type.equals(PsiTypes.intType())) {
      mapOp = "mapToInt";
    }
    else if (type.equals(PsiTypes.longType())) {
      mapOp = "mapToLong";
    }
    else if (type.equals(PsiTypes.doubleType())) {
      mapOp = "mapToDouble";
    }
    return mapOp == null ? "" : "." + mapOp + "(" + variable.getName() + " -> " + variable.getName() + ")";
  }

  static boolean isExpressionDependsOnUpdatedCollections(PsiExpression condition,
                                                         PsiExpression qualifierExpression) {
    final PsiElement collection = qualifierExpression instanceof PsiReferenceExpression
                                  ? ((PsiReferenceExpression)qualifierExpression).resolve()
                                  : null;
    if (collection != null) {
      return collection instanceof PsiVariable && VariableAccessUtils.variableIsUsed((PsiVariable)collection, condition);
    }

    final boolean[] dependsOnCollection = {false};
    condition.accept(new JavaRecursiveElementWalkingVisitor() {
      @Override
      public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) {
        super.visitMethodCallExpression(expression);
        final PsiExpression callQualifier = expression.getMethodExpression().getQualifierExpression();
        if (callQualifier == null ||
            callQualifier instanceof PsiThisExpression && ((PsiThisExpression)callQualifier).getQualifier() == null ||
            callQualifier instanceof PsiSuperExpression && ((PsiSuperExpression)callQualifier).getQualifier() == null) {
          dependsOnCollection[0] = true;
        }
      }

      @Override
      public void visitThisExpression(@NotNull PsiThisExpression expression) {
        super.visitThisExpression(expression);
        if (expression.getQualifier() == null && expression.getParent() instanceof PsiExpressionList) {
          dependsOnCollection[0] = true;
        }
      }

      @Override
      public void visitClass(@NotNull PsiClass aClass) {}

      @Override
      public void visitLambdaExpression(@NotNull PsiLambdaExpression expression) {}
    });

    return dependsOnCollection[0];
  }

  private class StreamApiMigrationVisitor extends JavaElementVisitor {
    private final ProblemsHolder myHolder;
    private final boolean myIsOnTheFly;

    StreamApiMigrationVisitor(ProblemsHolder holder, boolean isOnTheFly) {
      myHolder = holder;
      myIsOnTheFly = isOnTheFly;
    }

    @Override
    public void visitForeachStatement(@NotNull PsiForeachStatement statement) {
      super.visitForeachStatement(statement);
      processLoop(statement);
    }

    @Override
    public void visitWhileStatement(@NotNull PsiWhileStatement statement) {
      super.visitWhileStatement(statement);
      processLoop(statement);
    }

    @Override
    public void visitForStatement(@NotNull PsiForStatement statement) {
      super.visitForStatement(statement);
      processLoop(statement);
    }

    void processLoop(PsiLoopStatement statement) {
      final PsiStatement body = statement.getBody();
      if (body == null) return;
      StreamSource source = StreamSource.tryCreate(statement);
      if (source == null) return;
      if (!ExceptionUtil.getThrownCheckedExceptions(body).isEmpty()) return;
      TerminalBlock tb = TerminalBlock.from(source, body);

      BaseStreamApiMigration migration = findMigration(statement, body, tb, SUGGEST_FOREACH, REPLACE_TRIVIAL_FOREACH);
      if (migration == null || (!myIsOnTheFly && !migration.isShouldWarn())) return;
      MigrateToStreamFix[] fixes = {new MigrateToStreamFix(migration)};
      if (migration instanceof ForEachMigration && !(tb.getLastOperation() instanceof CollectionStream)) { //for .stream()
        fixes = ArrayUtil.append(fixes, new MigrateToStreamFix(new ForEachMigration(migration.isShouldWarn(), "forEachOrdered")));
      }
      ProblemHighlightType highlightType =
        migration.isShouldWarn() ? ProblemHighlightType.GENERIC_ERROR_OR_WARNING : ProblemHighlightType.INFORMATION;
      String message = JavaBundle.message("inspection.stream.api.migration.can.be.replaced.with.call", migration.getReplacement());
      TextRange range = getRange(migration.isShouldWarn(), statement, myIsOnTheFly);
      myHolder.registerProblem(statement, message, highlightType, range.shiftRight(-statement.getTextOffset()), fixes);
    }
  }


  static @Nullable BaseStreamApiMigration findMigration(PsiStatement loop,
                                                        PsiElement body,
                                                        TerminalBlock tb,
                                                        boolean suggestForeach,
                                                        boolean replaceTrivialForEach) {
    final ControlFlow controlFlow;
    try {
      controlFlow = ControlFlowFactory.getInstance(loop.getProject())
        .getControlFlow(body, LocalsOrMyInstanceFieldsControlFlowPolicy.getInstance());
    }
    catch (AnalysisCanceledException ignored) {
      return null;
    }
    int startOffset = controlFlow.getStartOffset(body);
    int endOffset = controlFlow.getEndOffset(body);
    if (startOffset < 0 || endOffset < 0) return null;
    PsiElement surrounder = PsiTreeUtil.getParentOfType(loop, PsiLambdaExpression.class, PsiClass.class);
    final List<PsiVariable> nonFinalVariables = StreamEx.of(ControlFlowUtil.getUsedVariables(controlFlow, startOffset, endOffset))
      .remove(variable -> variable instanceof PsiField)
      .remove(variable -> PsiTreeUtil.getParentOfType(variable, PsiLambdaExpression.class, PsiClass.class) != surrounder)
      .remove(variable -> isVariableSuitableForStream(variable, loop, tb)).toList();

    if (isCountOperation(nonFinalVariables, tb)) {
      return new CountMigration(true);
    }
    CollectMigration.CollectTerminal terminal = CollectMigration.extractCollectTerminal(tb, nonFinalVariables);
    if (terminal != null) {
      boolean addAll = loop instanceof PsiForeachStatement && !tb.hasOperations() && isAddAllCall(tb);
      // Don't suggest to convert the loop which can be trivially replaced via addAll:
      // this is covered by UseBulkOperationInspection and ManualArrayToCollectionCopyInspection
      if (addAll) return null;
      boolean shouldWarn = replaceTrivialForEach ||
                           tb.hasOperations() ||
                           tb.getLastOperation() instanceof BufferedReaderLines ||
                           !terminal.isTrivial();
      return new CollectMigration(shouldWarn, terminal.getMethodName());
    }
    if (JoiningMigration.extractTerminal(tb, nonFinalVariables) != null) {
      return new JoiningMigration(true);
    }
    if (tb.getCountExpression() != null || tb.isEmpty()) return null;
    if (nonFinalVariables.isEmpty() && extractArray(tb) != null) {
      return new ToArrayMigration(true);
    }
    if (getAccumulatedVariable(tb, nonFinalVariables, SUM_OPERATION) != null) {
      return new SumMigration(true);
    }
    FindExtremumMigration.ExtremumTerminal extremumTerminal = FindExtremumMigration.extract(tb, nonFinalVariables);
    if (extremumTerminal != null) {
      return new FindExtremumMigration(true, FindExtremumMigration.getOperation(extremumTerminal.isMax()));
    }
    for (OperationReductionMigration.ReductionOperation reductionOperation : OperationReductionMigration.OPERATIONS) {
      if (getAccumulatedVariable(tb, nonFinalVariables, reductionOperation) != null) {
        return new OperationReductionMigration(true, reductionOperation);
      }
    }
    Collection<PsiStatement> exitPoints = tb.findExitPoints(controlFlow);
    if (exitPoints == null) return null;
    boolean onlyNonLabeledContinue = ContainerUtil.and(
      exitPoints, statement -> statement instanceof PsiContinueStatement && ((PsiContinueStatement)statement).getLabelIdentifier() == null);
    if (onlyNonLabeledContinue && nonFinalVariables.isEmpty()) {
      boolean shouldWarn = suggestForeach &&
                           (replaceTrivialForEach ||
                            tb.hasOperations() ||
                            ForEachMigration.tryExtractMapExpression(tb) != null ||
                            !isTrivial(tb));
      return new ForEachMigration(shouldWarn, "forEach");
    }
    if (nonFinalVariables.isEmpty() && tb.getSingleStatement() instanceof PsiReturnStatement) {
      return findMigrationForReturn(loop, tb, replaceTrivialForEach);
    }
    // Source and intermediate ops should not refer to non-final variables
    if (tb.intermediateAndSourceExpressions()
      .flatCollection(expr -> PsiTreeUtil.collectElementsOfType(expr, PsiReferenceExpression.class))
      .map(PsiReferenceExpression::resolve).select(PsiVariable.class).anyMatch(nonFinalVariables::contains)) {
      return null;
    }
    PsiStatement[] statements = tb.getStatements();
    if (statements.length == 2) {
      PsiStatement breakStatement = statements[1];
      if (loop instanceof PsiLoopStatement && ControlFlowUtils.statementBreaksLoop(breakStatement, (PsiLoopStatement)loop) &&
          exitPoints.size() == 1 &&
          exitPoints.contains(breakStatement)) {
        return findMigrationForBreak(tb, nonFinalVariables, statements[0], replaceTrivialForEach);
      }
    }
    return null;
  }

  private static @Nullable BaseStreamApiMigration findMigrationForBreak(TerminalBlock tb,
                                                                        List<PsiVariable> nonFinalVariables,
                                                                        PsiStatement statement,
                                                                        boolean replaceTrivialForEach) {
    boolean shouldWarn = replaceTrivialForEach || tb.hasOperations();
    if (ReferencesSearch.search(tb.getVariable(), new LocalSearchScope(statement)).findFirst() == null) {
      return new MatchMigration(shouldWarn, "anyMatch()/noneMatch()/allMatch");
    }
    if (nonFinalVariables.isEmpty() && statement instanceof PsiExpressionStatement) {
      return new FindFirstMigration(shouldWarn);
    }
    if (nonFinalVariables.size() == 1) {
      PsiAssignmentExpression assignment = ExpressionUtils.getAssignment(statement);
      if (assignment == null) return null;
      PsiReferenceExpression lValue = tryCast(assignment.getLExpression(), PsiReferenceExpression.class);
      if (lValue == null) return null;
      PsiVariable var = tryCast(lValue.resolve(), PsiVariable.class);
      if (var == null || !nonFinalVariables.contains(var)) return null;
      PsiExpression rValue = assignment.getRExpression();
      if (rValue == null || VariableAccessUtils.variableIsUsed(var, rValue)) return null;
      if (tb.getVariable().getType() instanceof PsiPrimitiveType && !ExpressionUtils.isReferenceTo(rValue, tb.getVariable())) return null;
      return new FindFirstMigration(shouldWarn);
    }
    return null;
  }

  private static @Nullable BaseStreamApiMigration findMigrationForReturn(PsiStatement statement, TerminalBlock tb, boolean replaceTrivialForEach) {
    boolean shouldWarn = replaceTrivialForEach || tb.hasOperations();
    PsiReturnStatement returnStatement = (PsiReturnStatement)tb.getSingleStatement();
    if (returnStatement == null) return null;
    PsiExpression value = PsiUtil.skipParenthesizedExprDown(returnStatement.getReturnValue());
    PsiReturnStatement nextReturnStatement = ControlFlowUtils.getNextReturnStatement(statement);
    if (nextReturnStatement != null && value instanceof PsiLiteralExpression) {
      Boolean foundResult = tryCast(((PsiLiteralExpression)value).getValue(), Boolean.class);
      if (foundResult != null) {
        String methodName;
        if (foundResult) {
          methodName = "anyMatch";
        }
        else {
          methodName = "noneMatch";
          FilterOp lastFilter = tb.getLastOperation(FilterOp.class);
          if (lastFilter != null && (lastFilter.isNegated() ^ BoolUtils.isNegation(lastFilter.getExpression()))) {
            methodName = "allMatch";
          }
        }
        if (nextReturnStatement.getParent() == statement.getParent() ||
            ExpressionUtils.isLiteral(nextReturnStatement.getReturnValue(), !foundResult)) {
          return new MatchMigration(shouldWarn, methodName);
        }
      }
    }
    if (!VariableAccessUtils.variableIsUsed(tb.getVariable(), value)) {
      if (!replaceTrivialForEach && !tb.hasOperations() ||
          (tb.getLastOperation() instanceof FilterOp && tb.operations().count() == 2)) {
        return null;
      }
      return new MatchMigration(shouldWarn, "anyMatch");
    }
    if (nextReturnStatement != null && ExpressionUtils.isSafelyRecomputableExpression(nextReturnStatement.getReturnValue())
        && (!(tb.getVariable().getType() instanceof PsiPrimitiveType) || ExpressionUtils.isReferenceTo(value, tb.getVariable()))) {
      return new FindFirstMigration(shouldWarn);
    }
    return null;
  }

  private static @NotNull TextRange getRange(boolean shouldWarn, PsiStatement statement, boolean isOnTheFly) {
    boolean wholeStatement =
      isOnTheFly && (!shouldWarn || InspectionProjectProfileManager.isInformationLevel(SHORT_NAME, statement));
    if (statement instanceof PsiForeachStatement) {
      PsiJavaToken rParenth = ((PsiForeachStatement)statement).getRParenth();
      if (wholeStatement && rParenth != null) {
        return new TextRange(statement.getTextOffset(), rParenth.getTextOffset() + 1);
      }
      PsiExpression iteratedValue = ((PsiForeachStatement)statement).getIteratedValue();
      LOG.assertTrue(iteratedValue != null);
      return iteratedValue.getTextRange();
    }
    else if (statement instanceof PsiForStatement) {
      PsiJavaToken rParenth = ((PsiForStatement)statement).getRParenth();
      if (wholeStatement && rParenth != null) {
        return new TextRange(statement.getTextOffset(), rParenth.getTextOffset() + 1);
      }
      PsiStatement initialization = ((PsiForStatement)statement).getInitialization();
      LOG.assertTrue(initialization != null);
      return initialization.getTextRange();
    }
    else if (statement instanceof PsiWhileStatement) {
      PsiJavaToken rParenth = ((PsiWhileStatement)statement).getRParenth();
      if (wholeStatement && rParenth != null) {
        return new TextRange(statement.getTextOffset(), rParenth.getTextOffset() + 1);
      }
      return statement.getFirstChild().getTextRange();
    }
    else {
      throw new IllegalStateException("Unexpected statement type: " + statement);
    }
  }

  static @Nullable PsiLocalVariable extractArray(TerminalBlock tb) {
    CountingLoopSource loop = tb.getLastOperation(CountingLoopSource.class);
    if (loop == null || loop.myIncluding) return null;
    PsiAssignmentExpression assignment = tb.getSingleExpression(PsiAssignmentExpression.class);
    if (assignment == null || !assignment.getOperationTokenType().equals(JavaTokenType.EQ)) return null;
    PsiArrayAccessExpression arrayAccess = tryCast(assignment.getLExpression(), PsiArrayAccessExpression.class);
    if (arrayAccess == null) return null;
    if (!ExpressionUtils.isReferenceTo(arrayAccess.getIndexExpression(), loop.getVariable())) return null;
    PsiReferenceExpression arrayReference = tryCast(arrayAccess.getArrayExpression(), PsiReferenceExpression.class);
    if (arrayReference == null) return null;
    PsiLocalVariable arrayVariable = tryCast(arrayReference.resolve(), PsiLocalVariable.class);
    if (arrayVariable == null || ControlFlowUtils.getInitializerUsageStatus(arrayVariable, tb.getStreamSourceStatement()) == UNKNOWN) {
      return null;
    }
    PsiNewExpression initializer = tryCast(arrayVariable.getInitializer(), PsiNewExpression.class);
    if (initializer == null) return null;
    PsiArrayType arrayType = tryCast(initializer.getType(), PsiArrayType.class);
    if (arrayType == null || !StreamApiUtil.isSupportedStreamElement(arrayType.getComponentType())) return null;
    PsiExpression dimension = ArrayUtil.getFirstElement(initializer.getArrayDimensions());
    if (dimension == null) return null;
    PsiExpression bound = PsiUtil.skipParenthesizedExprDown(loop.myBound);
    if (!isArrayLength(arrayVariable, dimension, bound)) return null;
    if (VariableAccessUtils.variableIsUsed(arrayVariable, assignment.getRExpression())) return null;
    return arrayVariable;
  }

  private static boolean isArrayLength(PsiLocalVariable arrayVariable, PsiExpression dimension, PsiExpression bound) {
    if (ExpressionUtils.isReferenceTo(ExpressionUtils.getArrayFromLengthExpression(bound), arrayVariable)) return true;
    if (EquivalenceChecker.getCanonicalPsiEquivalence().expressionsAreEquivalent(dimension, bound)) return true;
    if (bound instanceof PsiMethodCallExpression) {
      PsiExpression qualifier = ((PsiMethodCallExpression)bound).getMethodExpression().getQualifierExpression();
      if (qualifier != null && CollectionUtils.isCollectionOrMapSize(dimension, qualifier)) return true;
    }
    if (dimension instanceof PsiMethodCallExpression) {
      PsiExpression qualifier = ((PsiMethodCallExpression)dimension).getMethodExpression().getQualifierExpression();
      if (qualifier != null && CollectionUtils.isCollectionOrMapSize(bound, qualifier)) return true;
    }
    return false;
  }

  /**
   * Intermediate stream operation representation
   */
  abstract static class Operation {
    final PsiExpression myExpression;
    final PsiVariable myVariable;

    protected Operation(PsiExpression expression, PsiVariable variable) {
      myExpression = expression;
      myVariable = variable;
    }

    void cleanUp() {}

    public PsiVariable getVariable() {
      return myVariable;
    }

    PsiExpression getExpression() {
      return myExpression;
    }

    StreamEx<PsiExpression> expressions() {
      return StreamEx.ofNullable(myExpression);
    }

    abstract String createReplacement(CommentTracker ct);

    boolean isWriteAllowed(PsiVariable variable, PsiExpression reference) {
      return false;
    }

    boolean canReassignVariable(PsiVariable variable) {
      return true;
    }
  }

  static class FilterOp extends Operation {
    private final boolean myNegated;

    FilterOp(PsiExpression condition, PsiVariable variable, boolean negated) {
      super(condition, variable);
      myNegated = negated;
    }

    public boolean isNegated() {
      return myNegated;
    }

    @Override
    public String createReplacement(CommentTracker ct) {
      PsiElementFactory factory = JavaPsiFacade.getElementFactory(myExpression.getProject());
      PsiExpression intermediate = makeIntermediateExpression(ct, factory);
      PsiExpression expression =
        myNegated ? factory.createExpressionFromText(BoolUtils.getNegatedExpressionText(intermediate, ct), myExpression) : intermediate;
      return "." + getOpName() + "(" + LambdaUtil.createLambda(myVariable, expression) + ")";
    }

    @NotNull
    String getOpName() {
      return "filter";
    }

    PsiExpression makeIntermediateExpression(CommentTracker ct, PsiElementFactory factory) {
      return ct.markUnchanged(myExpression);
    }
  }

  static class TakeWhileOp extends FilterOp {
