// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.siyeh.ig.migration;

import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.UpdateInspectionOptionFix;
import com.intellij.codeInspection.options.OptPane;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.PsiUpdateModCommandQuickFix;
import com.intellij.openapi.project.Project;
import com.intellij.pom.java.JavaFeature;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleSettings;
import com.intellij.psi.codeStyle.VariableKind;
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.containers.ContainerUtil;
import com.siyeh.HardcodedMethodConstants;
import com.siyeh.InspectionGadgetsBundle;
import com.siyeh.ig.BaseInspection;
import com.siyeh.ig.BaseInspectionVisitor;
import com.siyeh.ig.psiutils.*;
import org.intellij.lang.annotations.Pattern;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
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.util.ObjectUtils.tryCast;
import static com.siyeh.ig.psiutils.ParenthesesUtils.getParentSkipParentheses;

public final class ForCanBeForeachInspection extends BaseInspection {
  @SuppressWarnings("PublicField")
  public boolean REPORT_INDEXED_LOOP = true;
  @SuppressWarnings("PublicField")
  public boolean ignoreUntypedCollections;

  @Override
  protected LocalQuickFix @NotNull [] buildFixes(Object... infos) {
    boolean indexed = (boolean)infos[0];
    if (indexed) {
      return new LocalQuickFix[]{
        new ForCanBeForeachFix(ignoreUntypedCollections),
        LocalQuickFix.from(new UpdateInspectionOptionFix(this, "REPORT_INDEXED_LOOP",
                                                         InspectionGadgetsBundle.message("for.can.be.foreach.fix.no.indexed"), false)),
      };
    }
    return new LocalQuickFix[]{new ForCanBeForeachFix(ignoreUntypedCollections)};
  }

  @Override
  public @NotNull OptPane getOptionsPane() {
    return pane(
      checkbox("REPORT_INDEXED_LOOP", InspectionGadgetsBundle.message("for.can.be.foreach.option")),
      checkbox("ignoreUntypedCollections", InspectionGadgetsBundle.message("for.can.be.foreach.option2")));
  }

  static boolean isIndexedListLoopStatement(PsiForStatement forStatement, boolean ignoreUntypedCollections) {
    final PsiStatement initialization = forStatement.getInitialization();
    if (!(initialization instanceof PsiDeclarationStatement declaration)) {
      return false;
    }
    final PsiElement[] declaredElements = declaration.getDeclaredElements();
    final PsiElement secondDeclaredElement;
    if (declaredElements.length == 1) {
      secondDeclaredElement = null;
    }
    else if (declaredElements.length == 2) {
      secondDeclaredElement = declaredElements[1];
    }
    else {
      return false;
    }
    final PsiElement declaredElement = declaredElements[0];
    if (!(declaredElement instanceof PsiVariable indexVariable)) {
      return false;
    }
    final PsiExpression initialValue = indexVariable.getInitializer();
    final Object constant = ExpressionUtils.computeConstantExpression(initialValue);
    if (!(constant instanceof Number number)) {
      return false;
    }
    if (number.intValue() != 0) {
      return false;
    }
    final PsiExpression condition = forStatement.getCondition();
    final Holder collectionHolder = getCollectionFromSizeComparison(condition, indexVariable, secondDeclaredElement);
    if (collectionHolder == null) {
      return false;
    }
    final PsiStatement update = forStatement.getUpdate();
    if (!VariableAccessUtils.variableIsIncremented(indexVariable, update)) {
      return false;
    }
    final PsiStatement body = forStatement.getBody();
    if (!isIndexVariableOnlyUsedAsListIndex(collectionHolder, indexVariable, body)) {
      return false;
    }
    if (!collectionHolder.isDummyVariable()) {
      final PsiVariable collection = collectionHolder.getVariable();
      final PsiClassType collectionType = (PsiClassType)collection.getType();
      final PsiType[] parameters = collectionType.getParameters();
      if (ignoreUntypedCollections && parameters.length == 0) {
        return false;
      }
      return !VariableAccessUtils.variableIsAssigned(collection, body) &&
             !collectionMayBeChangedBeforeLoop(forStatement, collectionHolder);
    }
    return !collectionMayBeChangedBeforeLoop(forStatement, collectionHolder);
  }

  static boolean isArrayLoopStatement(PsiForStatement forStatement) {
    final PsiStatement initialization = forStatement.getInitialization();
    if (!(initialization instanceof PsiDeclarationStatement declaration)) {
      return false;
    }
    final PsiElement[] declaredElements = declaration.getDeclaredElements();
    final PsiElement secondDeclaredElement;
    if (declaredElements.length == 1) {
      secondDeclaredElement = null;
    }
    else if (declaredElements.length == 2) {
      secondDeclaredElement = declaredElements[1];
    }
    else {
      return false;
    }
    final PsiElement declaredElement = declaredElements[0];
    if (!(declaredElement instanceof PsiVariable indexVariable)) {
      return false;
    }
    final PsiExpression initialValue = indexVariable.getInitializer();
    final Object constant = ExpressionUtils.computeConstantExpression(initialValue);
    if (!(constant instanceof Integer integer)) {
      return false;
    }
    if (integer.intValue() != 0) {
      return false;
    }
    final PsiStatement update = forStatement.getUpdate();
    if (!VariableAccessUtils.variableIsIncremented(indexVariable, update)) {
      return false;
    }
    final PsiExpression condition = forStatement.getCondition();
    final PsiReferenceExpression arrayReference = getVariableReferenceFromCondition(condition, indexVariable, secondDeclaredElement);
    if (arrayReference == null) {
      return false;
    }
    if (!(arrayReference.getType() instanceof PsiArrayType)) {
      return false;
    }
    final PsiElement element = arrayReference.resolve();
    if (!(element instanceof PsiVariable arrayVariable)) {
      return false;
    }
    final PsiStatement body = forStatement.getBody();
    return body == null ||
           isIndexVariableOnlyUsedAsIndex(arrayVariable, indexVariable, body) &&
           !VariableAccessUtils.variableIsAssigned(arrayVariable, body) &&
           !VariableAccessUtils.arrayContentsAreAssigned(arrayVariable, body) &&
           !arrayMayBeChangedBeforeLoop(arrayVariable, arrayReference, forStatement);
  }

  private static boolean arrayMayBeChangedBeforeLoop(@NotNull PsiVariable arrayVariable,
                                                     @NotNull PsiReferenceExpression from,
                                                     @Nullable PsiStatement to) {
    if (to == null) return false;
    int fromOffset = from.getTextOffset();
    int toOffset = to.getTextOffset();
    if (fromOffset >= toOffset) return false;
    PsiElement context = PsiTreeUtil.findCommonParent(from, to);
    PsiStatement fromStatement = PsiTreeUtil.getParentOfType(from, PsiStatement.class);
    if(fromStatement == null) return false;
    if (context == null) return false;
    if (arrayVariable instanceof PsiLocalVariable || arrayVariable instanceof PsiParameter) {
      return processFromAssignmentToLoop(fromStatement, to, getIsAssignedPredicateWithSkipOptions(arrayVariable));
    }
    else {
      return processFromAssignmentToLoop(fromStatement, to, getIsAssignedPredicateWithSkipOptions(arrayVariable),
                                         getNonLocalSideEffectPredicateWithSkipOptions());
    }
  }


  private static Function<Predicate<PsiElement>, Predicate<PsiElement>> getIsAssignedPredicateWithSkipOptions(PsiVariable variable) {
    return predicate -> {
      VariableAssignedVisitor visitor = new VariableAssignedVisitor(variable, predicate, true);
      return (Predicate<PsiElement>)element -> {
        element.accept(visitor);
        return visitor.isAssigned();
      };
    };
  }

  private static Function<Predicate<PsiElement>, Predicate<PsiElement>> getNonLocalSideEffectPredicateWithSkipOptions() {
    return predicate -> (Predicate<PsiElement>)element -> SideEffectChecker.mayHaveNonLocalSideEffects(element, predicate);
  }

  private static Function<Predicate<PsiElement>, Predicate<PsiElement>> getSideEffectPredicateWithSkipOptions() {
    return predicate -> (Predicate<PsiElement>)element -> SideEffectChecker.mayHaveSideEffects(element, predicate);
  }

  private static boolean processFromAssignmentToLoop(@NotNull PsiStatement fromAssignment,
                                                     @NotNull PsiStatement untilLoop,
                                                     Function<Predicate<PsiElement>, Predicate<PsiElement>>... predicatesWithSkipOptions) {
    int fromOffset = fromAssignment.getTextOffset();
    int toOffset = untilLoop.getTextOffset();
    if (fromOffset >= toOffset) return false;
    PsiElement context = PsiTreeUtil.findCommonParent(fromAssignment, untilLoop);
    if (context == null) return false;
    if (fromAssignment.getParent() != context) return false;
    //small optimization
    if (untilLoop.getParent() == context) {
      PsiStatement nextSibling = PsiTreeUtil.getNextSiblingOfType(fromAssignment, PsiStatement.class);
      if (nextSibling == untilLoop) {
        return false;
      }
      PsiStatement nextNextSibling = PsiTreeUtil.getNextSiblingOfType(nextSibling, PsiStatement.class);
      if (nextNextSibling == untilLoop) {
        context = nextSibling;
      }
    }
    List<Predicate<PsiElement>> predicates = Arrays.stream(predicatesWithSkipOptions)
      .map(t -> t.apply(element -> !(element.getTextOffset() < toOffset && element.getTextOffset() + element.getTextLength() > fromOffset)))
      .toList();
    PsiElement finalContext = context;
    return ContainerUtil.exists(predicates, t -> t.test(finalContext));
  }

  private static boolean isIndexVariableOnlyUsedAsIndex(
    @NotNull PsiVariable arrayVariable,
    @NotNull PsiVariable indexVariable,
    @Nullable PsiStatement body) {
    if (body == null) {
      return true;
    }
    final VariableOnlyUsedAsIndexVisitor visitor =
      new VariableOnlyUsedAsIndexVisitor(arrayVariable, indexVariable);
    body.accept(visitor);
    return visitor.isIndexVariableUsedOnlyAsIndex();
  }

  private static boolean isIndexVariableOnlyUsedAsListIndex(
    Holder collectionHolder, PsiVariable indexVariable,
    PsiStatement body) {
    if (body == null) {
      return true;
    }
    final VariableOnlyUsedAsListIndexVisitor visitor = new VariableOnlyUsedAsListIndexVisitor(collectionHolder, indexVariable);
    body.accept(visitor);
    return visitor.isIndexVariableUsedOnlyAsIndex();
  }

  static boolean isCollectionLoopStatement(PsiForStatement forStatement, boolean ignoreUntypedCollections) {
    final PsiStatement initialization = forStatement.getInitialization();
    final PsiVariable variable = getIterableVariable(initialization, ignoreUntypedCollections);
    if (variable == null) {
      return false;
    }
    final PsiExpression condition = forStatement.getCondition();
    if (!isHasNext(condition, variable)) {
      return false;
    }
    final PsiStatement update = forStatement.getUpdate();
    if (update != null && !(update instanceof PsiEmptyStatement)) {
      return false;
    }
    final PsiStatement body = forStatement.getBody();
    if (body == null) {
      return false;
    }
    return hasSimpleNextCall(variable, body);
  }

  static PsiVariable getIterableVariable(PsiStatement statement, boolean ignoreUntypedCollections) {
    if (!(statement instanceof PsiDeclarationStatement declaration)) {
      return null;
    }
    final PsiElement[] declaredElements = declaration.getDeclaredElements();
    if (declaredElements.length != 1) {
      return null;
    }
    final PsiElement declaredElement = declaredElements[0];
    if (!(declaredElement instanceof PsiVariable variable)) {
      return null;
    }
    if (!TypeUtils.variableHasTypeOrSubtype(variable, CommonClassNames.JAVA_UTIL_ITERATOR, "java.util.ListIterator")) {
      return null;
    }
    final PsiExpression initialValue = PsiUtil.skipParenthesizedExprDown(variable.getInitializer());
    if (!(initialValue instanceof PsiMethodCallExpression initialCall)) {
      return null;
    }
    if (!initialCall.getArgumentList().isEmpty()) {
      return null;
    }
    final PsiReferenceExpression initialMethodExpression = initialCall.getMethodExpression();
    final @NonNls String initialCallName = initialMethodExpression.getReferenceName();
    if (!HardcodedMethodConstants.ITERATOR.equals(initialCallName) && !"listIterator".equals(initialCallName)) {
      return null;
    }
    final PsiExpression qualifier = ExpressionUtils.getEffectiveQualifier(initialMethodExpression);
    if (qualifier == null || qualifier instanceof PsiSuperExpression) {
      return null;
    }
    final PsiType qualifierType = qualifier.getType();
    if (!(qualifierType instanceof PsiClassType classType)) {
      return null;
    }
    final PsiClass qualifierClass = classType.resolve();
    if (ignoreUntypedCollections) {
      final PsiClassType type = (PsiClassType)variable.getType();
      final PsiType[] parameters = type.getParameters();
      final PsiType[] parameters1 = classType.getParameters();
      if (parameters.length == 0 && parameters1.length == 0) {
        return null;
      }
    }
    if (!InheritanceUtil.isInheritor(qualifierClass, CommonClassNames.JAVA_LANG_ITERABLE)) {
      return null;
    }
    return variable;
  }

  static boolean hasSimpleNextCall(PsiVariable iterator, PsiElement context) {
    if (context == null) return false;
    final IteratorNextVisitor visitor = new IteratorNextVisitor(iterator, context);
    context.accept(visitor);
    return visitor.hasSimpleNextCall();
  }

  static boolean isHasNext(PsiExpression condition, PsiVariable iterator) {
    condition = PsiUtil.skipParenthesizedExprDown(condition);
    if (!(condition instanceof PsiMethodCallExpression call)) {
      return false;
    }
    if (!call.getArgumentList().isEmpty()) {
      return false;
    }
    final PsiReferenceExpression methodExpression = call.getMethodExpression();
    final String methodName = methodExpression.getReferenceName();
    if (!HardcodedMethodConstants.HAS_NEXT.equals(methodName)) {
      return false;
    }
    final PsiExpression qualifier = methodExpression.getQualifierExpression();
    return ExpressionUtils.isReferenceTo(qualifier, iterator);
  }

  private static @Nullable PsiReferenceExpression getVariableReferenceFromCondition(PsiExpression condition,
                                                                                    PsiVariable variable,
                                                                                    PsiElement secondDeclaredElement) {
    final PsiBinaryExpression binaryExpression = tryCast(PsiUtil.skipParenthesizedExprDown(condition), PsiBinaryExpression.class);
    if (binaryExpression == null) return null;
    final IElementType tokenType = binaryExpression.getOperationTokenType();
    final PsiExpression lhs = PsiUtil.skipParenthesizedExprDown(binaryExpression.getLOperand());
    final PsiExpression rhs = PsiUtil.skipParenthesizedExprDown(binaryExpression.getROperand());
    if (lhs == null || rhs == null) return null;
    PsiReferenceExpression referenceExpression;
    if (tokenType.equals(JavaTokenType.LT)) {
      if (!ExpressionUtils.isReferenceTo(lhs, variable) || !(rhs instanceof PsiReferenceExpression)) {
        return null;
      }
      referenceExpression = (PsiReferenceExpression)rhs;
    }
    else if (tokenType.equals(JavaTokenType.GT)) {
      if (!ExpressionUtils.isReferenceTo(rhs, variable) || !(lhs instanceof PsiReferenceExpression)) {
        return null;
      }
      referenceExpression = (PsiReferenceExpression)lhs;
    }
    else {
      return null;
    }
    if (ExpressionUtils.getArrayFromLengthExpression(referenceExpression) == null) {
      final PsiElement target = referenceExpression.resolve();
      if (secondDeclaredElement != null && !secondDeclaredElement.equals(target)) return null;
      if (target instanceof PsiVariable maxVariable) {
        final PsiCodeBlock context = PsiTreeUtil.getParentOfType(maxVariable, PsiCodeBlock.class);
        if (context == null || VariableAccessUtils.variableIsAssigned(maxVariable, context)) return null;
        final PsiExpression expression = PsiUtil.skipParenthesizedExprDown(maxVariable.getInitializer());
        if (!(expression instanceof PsiReferenceExpression)) return null;
        referenceExpression = (PsiReferenceExpression)expression;
        if (ExpressionUtils.getArrayFromLengthExpression(referenceExpression) == null) {
          return null;
        }
      }
    }
    else {
      if (secondDeclaredElement != null) return null;
    }
    final PsiExpression qualifierExpression = PsiUtil.skipParenthesizedExprDown(referenceExpression.getQualifierExpression());
    if (qualifierExpression instanceof PsiReferenceExpression) {
      return (PsiReferenceExpression)qualifierExpression;
    }
    else if (qualifierExpression instanceof PsiThisExpression ||
             qualifierExpression instanceof PsiSuperExpression ||
             qualifierExpression == null) {
      return referenceExpression;
    }
    else {
      return null;
    }
  }

  private static @Nullable Holder getCollectionFromSizeComparison(PsiExpression condition, PsiVariable variable, PsiElement secondDeclaredElement) {
    condition = PsiUtil.skipParenthesizedExprDown(condition);
    if (!(condition instanceof PsiBinaryExpression binaryExpression)) {
      return null;
    }
    final IElementType tokenType = binaryExpression.getOperationTokenType();
    final PsiExpression rhs = binaryExpression.getROperand();
    final PsiExpression lhs = binaryExpression.getLOperand();
    if (tokenType.equals(JavaTokenType.LT)) {
      if (!ExpressionUtils.isReferenceTo(lhs, variable)) {
        return null;
      }
      return getCollectionFromListMethodCall(rhs, HardcodedMethodConstants.SIZE, secondDeclaredElement);
    }
    if (tokenType.equals(JavaTokenType.GT)) {
      if (!ExpressionUtils.isReferenceTo(rhs, variable)) {
        return null;
      }
      return getCollectionFromListMethodCall(lhs, HardcodedMethodConstants.SIZE, secondDeclaredElement);
    }
    return null;
  }

  static boolean expressionIsListGetLookup(PsiExpression expression) {
    expression = PsiUtil.skipParenthesizedExprDown(expression);
    if (!(expression instanceof PsiMethodCallExpression reference)) {
      return false;
    }
    final PsiReferenceExpression methodExpression = reference.getMethodExpression();
    final PsiElement resolved = methodExpression.resolve();
    if (!(resolved instanceof PsiMethod method)) {
      return false;
    }
    if (!HardcodedMethodConstants.GET.equals(method.getName())) {
      return false;
    }
    final PsiClass aClass = method.getContainingClass();
    return InheritanceUtil.isInheritor(aClass, CommonClassNames.JAVA_UTIL_LIST);
  }

  private static @Nullable Holder getCollectionFromListMethodCall(PsiExpression expression, String methodName, PsiElement secondDeclaredElement) {
    expression = PsiUtil.skipParenthesizedExprDown(expression);
    if (expression instanceof PsiReferenceExpression referenceExpression) {
      final PsiElement target = referenceExpression.resolve();
      if (secondDeclaredElement != null && !secondDeclaredElement.equals(target)) {
        return null;
      }
      if (!(target instanceof PsiVariable variable)) {
        return null;
      }
      final PsiCodeBlock context = PsiTreeUtil.getParentOfType(variable, PsiCodeBlock.class);
      if (context == null) {
        return null;
      }
      if (VariableAccessUtils.variableIsAssigned(variable, context)) {
        return null;
      }
      expression = PsiUtil.skipParenthesizedExprDown(variable.getInitializer());
    }
    else if (secondDeclaredElement != null) {
      return null;
    }
    if (!(expression instanceof PsiMethodCallExpression methodCallExpression)) {
      return null;
    }
    final PsiReferenceExpression methodExpression = methodCallExpression.getMethodExpression();
    final String referenceName = methodExpression.getReferenceName();
    if (!methodName.equals(referenceName)) {
      return null;
    }
    final PsiMethod method = methodCallExpression.resolveMethod();
    if (method == null) {
      return null;
    }
    final PsiClass containingClass = method.getContainingClass();
    if (!InheritanceUtil.isInheritor(containingClass, CommonClassNames.JAVA_UTIL_LIST)) {
      return null;
    }
    final PsiExpression qualifierExpression = PsiUtil.skipParenthesizedExprDown(methodExpression.getQualifierExpression());
    if (qualifierExpression == null ||
        qualifierExpression instanceof PsiThisExpression ||
        qualifierExpression instanceof PsiSuperExpression) {
      return new Holder(methodExpression);
    }
    if (!(qualifierExpression instanceof PsiReferenceExpression referenceExpression)) {
      return null;
    }
    final PsiElement target = referenceExpression.resolve();
    if (!(target instanceof PsiVariable variable)) {
      return null;
    }
    return new Holder(variable, methodExpression);
  }

  private static boolean collectionMayBeChangedBeforeLoop(PsiForStatement forStatement,
                                                          Holder holder) {
    PsiStatement sizeStatement = PsiTreeUtil.getParentOfType(holder.getMethodExpression(), PsiStatement.class);
    if (forStatement == null || sizeStatement == null) return false;
    PsiElement context = PsiTreeUtil.findCommonParent(sizeStatement, forStatement);
    if (context == null) return false;
    if (sizeStatement.getParent() != context) return false;
    int toOffset = forStatement.getTextOffset();
    int fromOffset = sizeStatement.getTextOffset();
    if (toOffset <= fromOffset) return false;
    if (holder.isDummyVariable()) {
      return processFromAssignmentToLoop(sizeStatement, forStatement, getSideEffectPredicateWithSkipOptions());
    }
    else {
      return processFromAssignmentToLoop(sizeStatement, forStatement,
                                         getIsAssignedPredicateWithSkipOptions(holder.getVariable()),
                                         getNonLocalSideEffectPredicateWithSkipOptions());
    }
  }

  @Pattern(VALID_ID_PATTERN)
  @Override
  public @NotNull String getID() {
    return "ForLoopReplaceableByForEach";
  }

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

  @Override
  protected @NotNull String buildErrorString(Object... infos) {
    return InspectionGadgetsBundle.message("for.can.be.foreach.problem.descriptor");
  }

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

  @Override
  public @NotNull BaseInspectionVisitor buildVisitor() {
    return new ForCanBeForeachVisitor();
  }

  static @Nullable PsiType getContentType(PsiType type, String containerClassName) {
    final PsiType parameterType = PsiUtil.substituteTypeParameter(type, containerClassName, 0, true);
    return GenericsUtil.getVariableTypeByExpressionType(parameterType);
  }

  static void replaceIteratorNext(PsiElement element, String contentVariableName,
                                  PsiVariable iterator, PsiType contentType, PsiElement childToSkip,
                                  CommentTracker commentTracker, StringBuilder out) {
    if (isIteratorNext(element, iterator, contentType)) {
      if (!(element.getParent() instanceof PsiExpressionStatement)) {
        out.append(contentVariableName);
      }
    }
    else {
      final PsiElement[] children = element.getChildren();
      if (children.length == 0) {
        if (PsiUtil.isJavaToken(element, JavaTokenType.INSTANCEOF_KEYWORD) && out.charAt(out.length() - 1) != ' ') {
          out.append(' ');
        }
        out.append(commentTracker.text(element));
      }
      else {
        boolean skippingWhiteSpace = false;
        for (final PsiElement child : children) {
          if (shouldSkip(iterator, contentType, child) || child.equals(childToSkip)) {
            skippingWhiteSpace = true;
          }
          else if (!(child instanceof PsiWhiteSpace) || !skippingWhiteSpace) {
            skippingWhiteSpace = false;
            replaceIteratorNext(child, contentVariableName, iterator, contentType, childToSkip, commentTracker, out);
          }
        }
      }
    }
  }

  static boolean shouldSkip(PsiVariable iterator, PsiType contentType, PsiElement child) {
    if (!(child instanceof PsiExpressionStatement expressionStatement)) {
      return false;
    }
    final PsiExpression expression = expressionStatement.getExpression();
    return isIteratorNext(expression, iterator, contentType);
  }

  static boolean isIteratorNextDeclaration(
    PsiStatement statement, PsiVariable iterator,
    PsiType contentType) {
    if (!(statement instanceof PsiDeclarationStatement declarationStatement)) {
      return false;
    }
    final PsiElement[] declaredElements =
      declarationStatement.getDeclaredElements();
    if (declaredElements.length != 1) {
      return false;
    }
    final PsiElement declaredElement = declaredElements[0];
    if (!(declaredElement instanceof PsiVariable variable)) {
      return false;
    }
    final PsiExpression initializer = variable.getInitializer();
    return isIteratorNext(initializer, iterator, contentType);
  }

  static boolean isIteratorNext(PsiElement element, PsiVariable iterator, @NotNull PsiType contentType) {
    if (element == null) {
      return false;
    }
    if (element instanceof PsiParenthesizedExpression) {
      return isIteratorNext(((PsiParenthesizedExpression)element).getExpression(), iterator, contentType);
    }
    if (element instanceof PsiTypeCastExpression castExpression) {
      final PsiType type = castExpression.getType();
      if (!contentType.equals(type)) {
        return false;
      }
      final PsiExpression operand = castExpression.getOperand();
      return isIteratorNext(operand, iterator, contentType);
    }
    if (!(element instanceof PsiMethodCallExpression callExpression)) {
      return false;
    }
    if (!callExpression.getArgumentList().isEmpty()) {
      return false;
    }
    final PsiReferenceExpression reference = callExpression.getMethodExpression();
    final @NonNls String referenceName = reference.getReferenceName();
    if (!HardcodedMethodConstants.NEXT.equals(referenceName)) {
      return false;
    }

    final PsiExpression expression = reference.getQualifierExpression();
    return ExpressionUtils.isReferenceTo(expression, iterator);
  }

  static String createNewVariableName(@NotNull PsiElement scope, PsiType type, @Nullable String containerName) {
    return new VariableNameGenerator(scope, VariableKind.PARAMETER).byCollectionName(containerName).byType(type)
      .byName("value", "item", "element").generate(true);
  }

  static @Nullable PsiStatement getFirstStatement(PsiStatement body) {
    if (!(body instanceof PsiBlockStatement block)) {
      return body;
    }
    final PsiCodeBlock codeBlock = block.getCodeBlock();
    return ControlFlowUtils.getFirstStatementInBlock(codeBlock);
  }

  private static @NotNull String getVariableReferenceText(PsiReferenceExpression reference, PsiVariable variable, PsiElement context) {
    final String text = reference.getText();
    final PsiResolveHelper resolveHelper = PsiResolveHelper.getInstance(context.getProject());
    PsiExpression qualifier = reference.getQualifierExpression();
    while (qualifier != null) {
      if (!(qualifier instanceof PsiReferenceExpression)) return text;
      qualifier = ((PsiReferenceExpression)qualifier).getQualifierExpression();
    }
    final PsiVariable target = resolveHelper.resolveReferencedVariable(text, context);
    if (variable != target) {
      final PsiExpression effectiveQualifier = ExpressionUtils.getEffectiveQualifier(reference);
      if (effectiveQualifier != null) {
        return effectiveQualifier.getText() + "." + text;
      }
    }
    return text;
  }

  private static final class IteratorNextVisitor extends JavaRecursiveElementWalkingVisitor {

    private int numCallsToIteratorNext = 0;
    private boolean iteratorUsed = false;
    private final PsiVariable iterator;
    private final PsiElement context;

    private IteratorNextVisitor(PsiVariable iterator, PsiElement context) {
      this.iterator = iterator;
      this.context = context;
    }

    @Override
    public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) {
      if (iteratorUsed || numCallsToIteratorNext > 1) return;
      if (expression.getArgumentList().isEmpty()) {
        final PsiReferenceExpression methodExpression = expression.getMethodExpression();
        final @NonNls String methodName = methodExpression.getReferenceName();
        if (HardcodedMethodConstants.NEXT.equals(methodName)) {
          final PsiExpression qualifier = methodExpression.getQualifierExpression();
          if (ExpressionUtils.isReferenceTo(qualifier, iterator) && isExecutedExactlyOnce(expression)) {
            numCallsToIteratorNext++;
            return;
          }
        }
      }
      super.visitMethodCallExpression(expression);
    }

    @Override
    public void visitReferenceExpression(@NotNull PsiReferenceExpression expression) {
      if (iteratorUsed || numCallsToIteratorNext > 1) return;
      super.visitReferenceExpression(expression);
      if (ExpressionUtils.isReferenceTo(expression, iterator)) {
        iteratorUsed = true;
        stopWalking();
      }
    }

    boolean hasSimpleNextCall() {
      return numCallsToIteratorNext == 1 && !iteratorUsed;
    }

    private boolean isExecutedExactlyOnce(@NotNull PsiElement element) {
      PsiElement parent = element.getParent();
      while (parent != null && parent != context) {
        if (parent instanceof PsiForStatement) {
          if (element != ((PsiForStatement)parent).getInitialization()) return false;
        }
        else if (parent instanceof PsiLoopStatement) return false;
        else if (parent instanceof PsiTryStatement) {
          if (element != ((PsiTryStatement)parent).getFinallyBlock()) return false;
        }
        else if (parent instanceof PsiIfStatement) {
          if (element != ((PsiIfStatement)parent).getCondition()) return false;
        }
        else if (parent instanceof PsiConditionalExpression) {
          if (element != ((PsiConditionalExpression)parent).getCondition()) return false;
        }
        else if (parent instanceof PsiSwitchBlock) {
          if (element != ((PsiSwitchBlock)parent).getExpression()) return false;
        }
        else if (parent instanceof PsiPolyadicExpression polyadicExpression) {
          IElementType tokenType = polyadicExpression.getOperationTokenType();
          if (polyadicExpression.getOperands()[0] != element &&
              (JavaTokenType.ANDAND == tokenType || JavaTokenType.OROR == tokenType)) return false;
        }
        else if (parent instanceof PsiLambdaExpression) return false;
        else if (parent instanceof PsiClass) return false;
        element = parent;
        parent = element.getParent();
      }
      return true;
    }
  }

  private static final class VariableOnlyUsedAsIndexVisitor extends JavaRecursiveElementWalkingVisitor {

    private boolean indexVariableUsedOnlyAsIndex = true;
    private boolean arrayAccessed = false;
    private final PsiVariable arrayVariable;
    private final PsiVariable indexVariable;

    private VariableOnlyUsedAsIndexVisitor(PsiVariable arrayVariable, PsiVariable indexVariable) {
      this.arrayVariable = arrayVariable;
      this.indexVariable = indexVariable;
    }

    @Override
    public void visitElement(@NotNull PsiElement element) {
      if (indexVariableUsedOnlyAsIndex) {
        super.visitElement(element);
      }
    }

    @Override
