// Copyright 2000-2025 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.intention.impl.StreamRefactoringUtil;
import com.intellij.codeInspection.dataFlow.DfaPsiUtil;
import com.intellij.codeInspection.dataFlow.rangeSet.LongRangeSet;
import com.intellij.codeInspection.dataFlow.value.RelationType;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.SmartList;
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.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Predicate;

import static com.intellij.codeInspection.streamMigration.StreamApiMigrationInspection.isCallOf;
import static com.intellij.psi.CommonClassNames.JAVA_LANG_CHAR_SEQUENCE;
import static com.intellij.psi.controlFlow.ControlFlowUtil.isEffectivelyFinal;
import static com.intellij.util.ObjectUtils.tryCast;
import static com.siyeh.ig.psiutils.ControlFlowUtils.getInitializerUsageStatus;
import static com.siyeh.ig.psiutils.ExpressionUtils.resolveLocalVariable;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;

final class JoiningMigration extends BaseStreamApiMigration {
  JoiningMigration(boolean shouldWarn) {
    super(shouldWarn, "collect");
  }

  @Override
  PsiElement migrate(@NotNull Project project, @NotNull PsiElement body, @NotNull TerminalBlock tb) {
    JoiningTerminal terminal = extractTerminal(tb, null);
    if(terminal == null) return null;

    TerminalBlock block = terminal.getTerminalBlock();
    PsiStatement loopStatement = block.getStreamSourceStatement();
    CommentTracker ct = new CommentTracker();
    String stream = terminal.generateStreamCode(ct);
    PsiVariable builder = terminal.getBuilder();
    terminal.preCleanUp(ct);
    ControlFlowUtils.InitializerUsageStatus status = getInitializerUsageStatus(builder, loopStatement);
    if(builder instanceof PsiLocalVariable) {
      PsiElement result = replaceInitializer(loopStatement, builder, builder.getInitializer(), stream, status, ct);
      terminal.cleanUp((PsiLocalVariable)builder);
      JoiningTerminal.replaceUsages((PsiLocalVariable)terminal.getBuilder());
      return result;
    } else {
      return new CommentTracker().replaceAndRestoreComments(tb.getStreamSourceStatement(), builder.getName() + ".append(" +  stream + ");");
    }
  }

  static @Nullable JoiningTerminal extractTerminal(@NotNull TerminalBlock terminalBlock,
                                                   @Nullable("when fix applied") List<PsiVariable> nonFinalVariables) {
    List<BiFunction<TerminalBlock, List<PsiVariable>, JoiningTerminal>> extractors = Arrays.asList(
      JoiningTerminal.CountedLoopJoiningTerminal::extractCountedLoopTerminal,
      JoiningTerminal.PlainJoiningTerminal::extractPlainJoining,
      JoiningTerminal.LengthBasedJoiningTerminal::extractLengthBasedTerminal,
      JoiningTerminal.BoolFlagJoiningTerminal::extractBoolFlagTerminal,
      JoiningTerminal.LengthTruncateJoiningTerminal::extractLengthTruncateTerminal,
      JoiningTerminal.DelimiterRewriteJoiningTerminal::extractDelimiterRewritingTerminal,
      JoiningTerminal.IndexBasedJoiningTerminal::extractIndexBasedTerminal
    );
    if (terminalBlock.operations().select(StreamApiMigrationInspection.LimitOp.class)
      .anyMatch(limitOp -> limitOp.getCounterVariable() == null)) {
      return null;
    }
    return StreamEx.of(extractors)
      .map(extractor -> extractor.apply(terminalBlock, nonFinalVariables))
      .nonNull()
      .findFirst()
      .orElse(null);
  }

  static class JoiningTerminal {
    private static final CallMatcher APPEND = CallMatcher.anyOf(
      CallMatcher.instanceCall(CommonClassNames.JAVA_LANG_STRING_BUILDER, "append").parameterCount(1),
      CallMatcher.instanceCall(CommonClassNames.JAVA_LANG_STRING_BUFFER, "append").parameterCount(1)
    );


    private static final CallMatcher LENGTH =
      CallMatcher.instanceCall(CommonClassNames.JAVA_LANG_ABSTRACT_STRING_BUILDER, "length").parameterCount(0);
    private static final CallMatcher EMPTY_LENGTH =
      CallMatcher.instanceCall(CommonClassNames.JAVA_LANG_ABSTRACT_STRING_BUILDER, "isEmpty").parameterCount(0);
    private static final CallMatcher SET_LENGTH =
      CallMatcher.instanceCall(CommonClassNames.JAVA_LANG_ABSTRACT_STRING_BUILDER, "setLength").parameterCount(1);


    private static final EquivalenceChecker ourEquivalence = EquivalenceChecker.getCanonicalPsiEquivalence();

    private final @NotNull TerminalBlock myTerminalBlock;
    private final @NotNull PsiVariable myBuilder;
    private final @NotNull PsiVariable myLoopVariable;
    private final @NotNull List<PsiExpression> myMainJoinParts;
    private final @NotNull List<PsiExpression> myPrefixJoinParts;
    private final @NotNull List<PsiExpression> mySuffixJoinParts;
    private final @NotNull List<PsiExpression> myDelimiterJoinParts;
    private final @Nullable PsiMethodCallExpression myBeforeLoopAppend;
    private final @Nullable PsiMethodCallExpression myAfterLoopAppend;

    public @NotNull TerminalBlock getTerminalBlock() {
      return myTerminalBlock;
    }

    public @NotNull PsiVariable getBuilder() {
      return myBuilder;
    }

    protected JoiningTerminal(@NotNull TerminalBlock block,
                              @NotNull PsiVariable targetBuilder,
                              @NotNull PsiVariable variable,
                              @NotNull List<PsiExpression> mainJoinParts,
                              @NotNull List<PsiExpression> prefixJoinParts,
                              @NotNull List<PsiExpression> suffixJoinParts,
                              @NotNull List<PsiExpression> delimiterJoinParts,
                              @Nullable PsiMethodCallExpression beforeLoopAppend,
                              @Nullable PsiMethodCallExpression afterLoopAppend) {
      myTerminalBlock = block;
      myBuilder = targetBuilder;
      myLoopVariable = variable;
      myMainJoinParts = mainJoinParts;
      myPrefixJoinParts = prefixJoinParts;
      mySuffixJoinParts = suffixJoinParts;
      myDelimiterJoinParts = delimiterJoinParts;
      myBeforeLoopAppend = beforeLoopAppend;
      myAfterLoopAppend = afterLoopAppend;
    }

    void cleanUp(@NotNull PsiLocalVariable target) {
      replaceInitializer(target);
      replaceUsages(target);
    }

    void preCleanUp(CommentTracker ct) {
      cleanUpCall(ct, myBeforeLoopAppend);
      cleanUpCall(ct, myAfterLoopAppend);
    }

    @NotNull
    String generateStreamCode(CommentTracker ct) {
      return myTerminalBlock.generate(ct) + generateIntermediate(ct) + generateTerminal(ct);
    }

    private static void replaceInitializer(@NotNull PsiLocalVariable target) {
      PsiElementFactory factory = JavaPsiFacade.getElementFactory(target.getProject());
      target.getTypeElement().replace(factory.createTypeElementFromText(CommonClassNames.JAVA_LANG_STRING, target));
      PsiExpression initializer = target.getInitializer();
      String initialText = ConstructionUtils.getStringBuilderInitializerText(initializer);
      if (initialText != null) {
        initializer.replace(factory.createExpressionFromText("\"\"", target));
      }
    }

    private static boolean canBeMadeNonFinal(@NotNull PsiLocalVariable variable, @NotNull PsiStatement sourceStatement) {
      NavigatablePsiElement loopBound = PsiTreeUtil.getParentOfType(sourceStatement, PsiMember.class, PsiLambdaExpression.class);

      Predicate<PsiReference> referenceBoundPredicate;
      if (sourceStatement instanceof PsiLoopStatement) {
        referenceBoundPredicate =
          (reference) -> PsiTreeUtil.getParentOfType(reference.getElement(), PsiMember.class, PsiLambdaExpression.class) == loopBound;
      }
      else {
        referenceBoundPredicate = (reference) -> {
          PsiLambdaExpression lambda = PsiTreeUtil.getParentOfType(reference.getElement(), PsiLambdaExpression.class);
          return PsiTreeUtil.getParentOfType(lambda, PsiMember.class, PsiLambdaExpression.class) == loopBound;
        };
      }
      return ReferencesSearch.search(variable).allMatch(referenceBoundPredicate) && FinalUtils.canBeFinal(variable);
    }

    String generateTerminal(CommentTracker ct) {
      final String collectArguments;
      if (myDelimiterJoinParts.isEmpty() && myPrefixJoinParts.isEmpty() && mySuffixJoinParts.isEmpty()) {
        collectArguments = "";
      }
      else {
        String delimiter = myDelimiterJoinParts.isEmpty() ? "\"\"" : getExpressionText(ct, myDelimiterJoinParts);
        if (mySuffixJoinParts.isEmpty() && myPrefixJoinParts.isEmpty()) {
          collectArguments = delimiter;
        }
        else {
          String suffix = mySuffixJoinParts.isEmpty() ? "\"\"" : getExpressionText(ct, mySuffixJoinParts);
          String prefix = myPrefixJoinParts.isEmpty() ? "\"\"" : getExpressionText(ct, myPrefixJoinParts);
          collectArguments = delimiter + "," + prefix + "," + suffix;
        }
      }
      return ".collect(" + CommonClassNames.JAVA_UTIL_STREAM_COLLECTORS + ".joining(" + collectArguments + "))";
    }

    String generateIntermediate(CommentTracker ct) {
      if (TypeUtils.isJavaLangString(myLoopVariable.getType()) &&
          myMainJoinParts.size() == 1 &&
          myMainJoinParts.getFirst() instanceof PsiReferenceExpression) {
        return "";
      }
      PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(myLoopVariable.getProject());
      String joinTransformation = getExpressionText(ct, myMainJoinParts);
      PsiExpression mapping = elementFactory.createExpressionFromText(joinTransformation, myLoopVariable);
      return StreamRefactoringUtil.generateMapOperation(myLoopVariable, null, mapping);
    }


    private static void replaceUsages(PsiLocalVariable target) {
      Collection<PsiReference> usages = ReferencesSearch.search(target).findAll();
      for (PsiReference usage : usages) {
        PsiElement element = usage.getElement();
        if (element.isValid() && element instanceof PsiExpression) {
          PsiMethodCallExpression call = ExpressionUtils.getCallForQualifier((PsiExpression)element);
          if (call != null && "toString".equals(call.getMethodExpression().getReferenceName())) {
            new CommentTracker().replaceAndRestoreComments(call, element);
          }
        }
      }
    }

    private static void cleanUpCall(CommentTracker ct, PsiMethodCallExpression call) {
      if (call != null) {
        if (call.getParent() instanceof PsiExpressionStatement) {
          ct.delete(call);
        }
        else {
          PsiMethodCallExpression nextCall = ExpressionUtils.getCallForQualifier(call);
          PsiExpression qualifier = call.getMethodExpression().getQualifierExpression();
          if (nextCall != null && qualifier != null) {
            ct.replace(nextCall, qualifier);
          }
        }
      }
    }


    private static String getExpressionText(CommentTracker ct, @NotNull List<PsiExpression> joinParts) {
      StringJoiner joiner = new StringJoiner("+");
      int size = joinParts.size();
      for (int i = 0; i < joinParts.size(); i++) {
        PsiExpression joinPart = joinParts.get(i);
        String partText;
        if (i == 0) {
          boolean neighborIsString = false;
          if (joinParts.size() > 1) {
            PsiExpression second = joinParts.get(1);
            if (TypeUtils.isJavaLangString(second.getType())) {
              neighborIsString = true;
            }
          }
          partText = expressionToCharSequence(ct, joinPart, size, neighborIsString);
        }
        else {
          partText = expressionToCharSequence(ct, joinPart, size, true);
        }
        joiner.add(partText);
      }
      return joiner.toString();
    }

    private static @Nullable String computeConstant(@NotNull PsiExpression expression) {
      Object constantExpression = ExpressionUtils.computeConstantExpression(expression);
      if (constantExpression != null)  {
        return String.valueOf(constantExpression);
      } else {
        PsiLocalVariable variable = resolveLocalVariable(expression);
        if(variable == null) return null;
        PsiElement parent = variable.getParent();
        PsiExpression initializer = variable.getInitializer();
        if(parent == null || initializer == null) return null;
        if(!isEffectivelyFinal(variable, parent)) return null;
        Object initializerConstant = ExpressionUtils.computeConstantExpression(initializer);
        if(initializerConstant == null) return null;
        return String.valueOf(initializerConstant);
      }
    }

    private static @Nullable String computeConstant(@NotNull List<PsiExpression> joinParts) {
      StringBuilder sb = new StringBuilder();
      for (PsiExpression expression : joinParts) {
        String constant = computeConstant(expression);
        if(constant == null) return null;
        sb.append(constant);
      }
      return sb.toString();
    }

    private static @NotNull String expressionToCharSequence(CommentTracker ct,
                                                            @NotNull PsiExpression expression,
                                                            int expressionCount,
                                                            boolean neighborIsString) {
      PsiType type = expression.getType();
      if (expression instanceof PsiMethodCallExpression callExpression &&
          isCallOf(callExpression, CommonClassNames.JAVA_LANG_STRING, "charAt")) {
        PsiExpression qualifierExpression = callExpression.getMethodExpression().getQualifierExpression();
        PsiExpression[] expressions = callExpression.getArgumentList().getExpressions();
        if (expressions.length == 1) {
          PsiExpression first = expressions[0];
          if (qualifierExpression != null && ExpressionUtils.computeConstantExpression(first) instanceof Integer intValue) {
            String endIndex = String.valueOf(intValue + 1);
            return ct.text(qualifierExpression) + ".substring(" + ct.text(first) + "," + endIndex + ")";
          }
        }
      }
      if (!InheritanceUtil.isInheritor(type, JAVA_LANG_CHAR_SEQUENCE)) {
        if (!neighborIsString || (type instanceof PsiArrayType && ((PsiArrayType)type).getComponentType().equals(PsiTypes.charType()))) {
          PsiLiteralExpression literalExpression = tryCast(expression, PsiLiteralExpression.class);
          if (literalExpression != null) {
            Object value = literalExpression.getValue();
            if (value instanceof Character) {
              String text = ct.text(literalExpression);
              if ("'\"'".equals(text)) return "\"\\\"\"";
              return "\"" + text.substring(1, text.length() - 1) + "\"";
            }
          }
          return CommonClassNames.JAVA_LANG_STRING + ".valueOf(" + ct.text(expression) + ")";
        }
        if (ParenthesesUtils.getPrecedence(expression) > ParenthesesUtils.ADDITIVE_PRECEDENCE ||
            (expression.getType() instanceof PsiPrimitiveType &&
             ParenthesesUtils.getPrecedence(expression) == ParenthesesUtils.ADDITIVE_PRECEDENCE) ||
            expressionCount == 1) {
          return "(" + ct.text(expression) + ")";
        }
        return ct.text(expression);
      }
      String expressionText = ct.text(expression);
      if(ParenthesesUtils.getPrecedence(expression) > ParenthesesUtils.ADDITIVE_PRECEDENCE && expressionCount > 1) {
        expressionText = "(" + expressionText + ")";
      }
      return expressionText;
    }

    /**
     * from statement like sb.append(a).append(b) extracts sb
     */
    //
    private static @Nullable("when failed to extract") PsiVariable extractStringBuilder(@NotNull PsiStatement statement) {
      PsiExpressionStatement expressionStatement = tryCast(statement, PsiExpressionStatement.class);
      if (expressionStatement == null) return null;
      PsiMethodCallExpression methodCallExpression = tryCast(expressionStatement.getExpression(), PsiMethodCallExpression.class);
      if (methodCallExpression == null) return null;
      PsiMethodCallExpression currentExpression = methodCallExpression;
      while (APPEND.test(methodCallExpression)) {
        PsiExpression qualifierExpression = currentExpression.getMethodExpression().getQualifierExpression();
        PsiMethodCallExpression callerExpression = MethodCallUtils.getQualifierMethodCall(currentExpression);
        if (callerExpression == null) {
          PsiReferenceExpression refExpression = tryCast(qualifierExpression, PsiReferenceExpression.class);
          if(refExpression == null) return null;
          return tryCast(refExpression.resolve(), PsiVariable.class);
        }
        currentExpression = callerExpression;
      }
      return null;
    }

    private static @Nullable("when failed to extract") List<PsiExpression> extractJoinParts(@Nullable PsiExpression expression) {
      List<PsiExpression> joinParts = new SmartList<>();
      if (expression == null) return joinParts;
      return tryExtractJoinPart(expression, joinParts) ? joinParts : null;
    }

    /**
     * @param statements list of statements. Only appends expected inside.
     * @return list of joining expressions
     */
    private static @Nullable("when failed to extract") List<PsiExpression> extractJoinParts(@NotNull List<? extends PsiStatement> statements) {
      List<PsiExpression> joinParts = new ArrayList<>();
      for (PsiStatement statement : statements) {
        PsiExpressionStatement expressionStatement = tryCast(statement, PsiExpressionStatement.class);
        if (expressionStatement == null) return null;
        PsiExpression expression = expressionStatement.getExpression();
        if (!tryExtractJoinPart(expression, joinParts)) {
          return null;
        }
      }
      return joinParts;
    }

    /**
     * @param joinParts list to append joining parts into it
     * @return true on success
     */
    private static boolean tryExtractJoinPart(@NotNull PsiExpression expression,
                                              @NotNull List<PsiExpression> joinParts) {
      PsiMethodCallExpression methodCallExpression = tryCast(expression, PsiMethodCallExpression.class);
      if (methodCallExpression != null) {
        if (!APPEND.test(methodCallExpression)) return false;
        PsiExpression appendArgument = methodCallExpression.getArgumentList().getExpressions()[0];
        PsiExpression qualifierExpression = methodCallExpression.getMethodExpression().getQualifierExpression();
        if (qualifierExpression == null) return false;
        PsiReferenceExpression referenceExpression = tryCast(qualifierExpression, PsiReferenceExpression.class);
        if (referenceExpression == null) { // assume expr like sb.append(a).append(b).append(c)
          if (!tryExtractJoinPart(qualifierExpression, joinParts)) return false;
        }
        if (!tryExtractConcatenationParts(appendArgument, joinParts)) return false;
        return true;
      }
      return false;
    }

    private static boolean tryExtractConcatenationParts(@NotNull PsiExpression expression, @NotNull List<PsiExpression> joinParts) {
      PsiType type = expression.getType();
      if (type == null) return false;
      if (!TypeUtils.isJavaLangString(type)) {
        joinParts.add(expression);
        return true;
      }
      PsiBinaryExpression binaryExpression = tryCast(expression, PsiBinaryExpression.class);
      if (binaryExpression != null) {
        if (binaryExpression.getOperationTokenType().equals(JavaTokenType.PLUS)) {
          PsiExpression lOperand = PsiUtil.skipParenthesizedExprDown(binaryExpression.getLOperand());
          PsiExpression rOperand = PsiUtil.skipParenthesizedExprDown(binaryExpression.getROperand());
          if (lOperand == null || rOperand == null) return false;
          PsiType lOperandType = lOperand.getType();
          PsiType rOperandType = rOperand.getType();
          if (lOperandType == null || rOperandType == null) return false;
          if (!tryExtractConcatenationParts(lOperand, joinParts) ||
              !tryExtractConcatenationParts(rOperand, joinParts)) {
            return false;
          }
          return true;
        }
      }
      joinParts.add(expression);
      return true;
    }

    private static @Nullable("when failed to extract join parts from initializer statement") List<PsiExpression> extractStringBuilderInitializer(PsiExpression construction) {
      List<PsiExpression> joinParts = new ArrayList<>();
      PsiExpression expression = construction;
      PsiMethodCallExpression current = tryCast(construction, PsiMethodCallExpression.class);
      while(current != null) {
        if (APPEND.test(current)) {
          joinParts.add(current.getArgumentList().getExpressions()[0]);
        }
        else {
          return null;
        }
        expression = current.getMethodExpression().getQualifierExpression();
        current = MethodCallUtils.getQualifierMethodCall(current);
      }

      PsiNewExpression newExpression = tryCast(PsiUtil.skipParenthesizedExprDown(expression), PsiNewExpression.class);
      if (newExpression == null) return null;
      final PsiJavaCodeReferenceElement classReference = newExpression.getClassReference();
      if (classReference == null) return null;
      PsiClass aClass = tryCast(classReference.resolve(), PsiClass.class);
      if (aClass == null) return null;
      final String qualifiedName = aClass.getQualifiedName();
      if (!CommonClassNames.JAVA_LANG_STRING_BUILDER.equals(qualifiedName) &&
          !CommonClassNames.JAVA_LANG_STRING_BUFFER.equals(qualifiedName)) {
        return null;
      }
      final PsiExpressionList argumentList = newExpression.getArgumentList();
      if (argumentList == null) return null;
      final PsiExpression[] arguments = argumentList.getExpressions();
      if (arguments.length != 0) {
        if(arguments.length != 1) return null;
        final PsiExpression argument = arguments[0];
        final PsiType argumentType = argument.getType();
        if (!PsiTypes.intType().equals(argumentType)) {
          joinParts.add(argument);
        }
      }
      Collections.reverse(joinParts);
      return joinParts;
    }

    private static @Nullable AppendChain getCallAfterStatement(PsiStatement statement, PsiVariable receiver) {
      PsiElement next = PsiTreeUtil.skipWhitespacesAndCommentsForward(statement);
      return getAppendCallExpression(receiver, next);
    }

    private static @Nullable AppendChain getCallBeforeStatement(@NotNull PsiStatement statement,
                                                                @NotNull PsiVariable receiver,
                                                                @NotNull List<PsiDeclarationStatement> declarationsToSkip) {
      PsiElement previous = PsiTreeUtil.skipWhitespacesAndCommentsBackward(statement);
      PsiDeclarationStatement previousDeclaration = tryCast(previous, PsiDeclarationStatement.class);
      while (previousDeclaration != null && declarationsToSkip.contains(previousDeclaration)) {
        previous = PsiTreeUtil.skipWhitespacesAndCommentsBackward(previousDeclaration);
        previousDeclaration = tryCast(previous, PsiDeclarationStatement.class);
      }
      return getAppendCallExpression(receiver, previous);
    }

    @Contract("_, null -> null")
    private static @Nullable AppendChain getAppendCallExpression(@NotNull PsiVariable target,
                                                                 @Nullable PsiElement element) {
      if (!(element instanceof PsiExpressionStatement exprStatement)) return null;
      if (!(exprStatement.getExpression() instanceof PsiMethodCallExpression topCall)) return null;
      PsiMethodCallExpression call = topCall;
      while (true) {
        if (!APPEND.test(call)) return null;
        PsiExpression qualifier = call.getMethodExpression().getQualifierExpression();
        if (qualifier instanceof PsiMethodCallExpression) {
          call = (PsiMethodCallExpression)qualifier;
        }
        else if (ExpressionUtils.isReferenceTo(qualifier, target)) {
          return new AppendChain(call, topCall);
        }
        else {
          return null;
        }
      }
    }

    private record AppendChain(@NotNull PsiMethodCallExpression first, @NotNull PsiMethodCallExpression outermost) {
    }

    private static boolean areReferencesAllowed(@NotNull List<PsiElement> refs,
                                                @NotNull Set<PsiMethodCallExpression> allowedReferencePlaces) {
      return StreamEx.of(refs).select(PsiExpression.class).allMatch(expression -> {
        PsiMethodCallExpression usage = ExpressionUtils.getCallForQualifier(expression);
        if (usage != null) {
          if (allowedReferencePlaces.contains(usage)) return true;
          PsiExpression[] usageArgs = usage.getArgumentList().getExpressions();
          String name = usage.getMethodExpression().getReferenceName();
          if (usageArgs.length == 0 && ("toString".equals(name) || "length".equals(name))) return true;
        }
        PsiElement parent = PsiUtil.skipParenthesizedExprUp(expression.getParent());
        if (parent instanceof PsiPolyadicExpression &&
            ((PsiPolyadicExpression)parent).getOperationTokenType().equals(JavaTokenType.PLUS)) {
          return true;
        }
        if (parent instanceof PsiAssignmentExpression &&
            ((PsiAssignmentExpression)parent).getOperationTokenType().equals(JavaTokenType.PLUSEQ)) {
          return true;
        }
        return false;
      });
    }

    private static @Nullable PsiMethodCallExpression tryExtractCombinedToString(PsiMethodCallExpression afterLoopAppend,
                                                                                List<PsiElement> refs) {
      if (refs.size() == 1 && afterLoopAppend == null) { // case like return sb.append(postfix).toString();
        PsiMethodCallExpression usage = ExpressionUtils.getCallForQualifier((PsiExpression)refs.getFirst());
        if (APPEND.test(usage)) {
          PsiMethodCallExpression nextCall = ExpressionUtils.getCallForQualifier(usage);
          if (nextCall != null && "toString".equals(nextCall.getMethodExpression().getReferenceName())) {
            return usage;
          }
        }
      }
      return null;
    }


    private static boolean joinPartsAreEquivalent(@NotNull List<PsiExpression> joinParts1, @NotNull List<PsiExpression> joinParts2) {
      if (joinParts1.size() != joinParts2.size()) return false;
      for (int i = 0, size = joinParts1.size(); i < size; i++) {
        PsiExpression joinPart1 = joinParts1.get(i);
        PsiExpression joinPart2 = joinParts2.get(i);
        if (!ourEquivalence.expressionsAreEquivalent(joinPart1, joinPart2)) return false;
      }
      return true;
    }

    /**
     * Like: if(!sb.isEmpty()) => prefixLength == 0 or if(sb.length() > 2) => prefixLength == 2
     */
    private static @Nullable Integer extractConditionPrefixLength(@NotNull PsiExpression expression, PsiVariable targetBuilder) {
      Integer explicitLengthCondition = extractExplicitLengthCheck(expression, targetBuilder);
      if (explicitLengthCondition != null) return explicitLengthCondition;
      return extractEmptyLengthCheck(expression, targetBuilder);
    }

    private static @Nullable Integer extractEmptyLengthCheck(@NotNull PsiExpression expression, PsiVariable targetBuilder) {
      PsiMethodCallExpression maybeEmptyCall = tryCast(BoolUtils.getNegated(expression), PsiMethodCallExpression.class);
      if (!EMPTY_LENGTH.test(maybeEmptyCall)) return null; // extract call matcher
      if (!ExpressionUtils.isReferenceTo(maybeEmptyCall.getMethodExpression().getQualifierExpression(), targetBuilder)) return null;
      return 0;
    }

    private static @Nullable("when failed to extract length") Integer extractExplicitLengthCheck(@NotNull PsiExpression expression, PsiVariable targetBuilder) {
      PsiBinaryExpression condition = tryCast(expression, PsiBinaryExpression.class);
      if (condition == null) return null;
      PsiExpression rOperand = condition.getROperand();
      if (rOperand == null) return null;


      PsiExpression lOperand = condition.getLOperand();
      RelationType relation = DfaPsiUtil.getRelationByToken(condition.getOperationTokenType());
      if (relation == null) return null;
      int lSize = computeConstantIntExpression(lOperand);
      if (lSize >= 0) {
        return extractLength(rOperand, relation.getFlipped(), lSize, targetBuilder);
      }
      else {
        int rSize = computeConstantIntExpression(condition.getROperand());
        return rSize >= 0 ? extractLength(lOperand, relation, rSize, targetBuilder) : null;
      }
    }

    private static @Nullable Integer extractLength(PsiExpression rOperand,
                                                   RelationType relation,
                                                   int size,
                                                   PsiVariable targetBuilder) {
      if (!isStringBuilderLengthCall(rOperand, targetBuilder)) return null;
      LongRangeSet rangeSet = LongRangeSet.point(size).fromRelation(relation);
      if (rangeSet == null || rangeSet.max() != Long.MAX_VALUE) return null;
      long min = rangeSet.min();
      return min > 0 ? (int)(min - 1) : null;
    }

    private static boolean isStringBuilderLengthCall(@NotNull PsiExpression expression, PsiVariable targetBuilder) {
      PsiMethodCallExpression methodCallExpression = tryCast(expression, PsiMethodCallExpression.class);
      return LENGTH.test(methodCallExpression) &&
             ExpressionUtils.isReferenceTo(methodCallExpression.getMethodExpression().getQualifierExpression(), targetBuilder);
    }

    /**
     * @param expression that is expected to be positive or 0
     * @return evaluated value or -1 when error
     */
    private static int computeConstantIntExpression(@NotNull PsiExpression expression) {
      Object constantExpression = ExpressionUtils.computeConstantExpression(expression);
      if (!(constantExpression instanceof Integer)) return -1;
      return (int)constantExpression;
    }

    private static class PrefixSuffixContext {
      private final @Nullable PsiMethodCallExpression myBeforeLoopStatement;
      private final @Nullable PsiMethodCallExpression myAfterLoopStatement;
      private final @NotNull List<PsiExpression> myPrefixJoinParts;
      private final @NotNull List<PsiExpression> mySuffixJoinParts;

      PrefixSuffixContext(@Nullable PsiMethodCallExpression beforeLoopStatement,
                                 @Nullable PsiMethodCallExpression afterLoopStatement,
                                 @NotNull List<PsiExpression> prefixJoinParts,
                                 @NotNull List<PsiExpression> suffixJoinParts) {
        myBeforeLoopStatement = beforeLoopStatement;
        myAfterLoopStatement = afterLoopStatement;
        myPrefixJoinParts = prefixJoinParts;
        mySuffixJoinParts = suffixJoinParts;
      }

      public @Nullable PsiMethodCallExpression getBeforeLoopStatement() {
        return myBeforeLoopStatement;
      }

      public @Nullable PsiMethodCallExpression getAfterLoopStatement() {
        return myAfterLoopStatement;
      }

      public @NotNull List<PsiExpression> getPrefixJoinParts() {
        return myPrefixJoinParts;
      }

      public @NotNull List<PsiExpression> getSuffixJoinParts() {
        return mySuffixJoinParts;
      }

      /**
       * @param finalAppendPredecessor        - statement, after and which expected suffix append (loop statement generally)
       * @param firstAppendSuccessor - statement before which expected prefix append and possibly some declarations used in loop
       * @param targetBuilder - string builder used
       * @param possibleVariablesBeforeLoop - variable, which declarations that can be before loop
       * @return prefix and suffix data
       */
      static @Nullable PrefixSuffixContext extractAndVerifyRefs(@NotNull PsiStatement finalAppendPredecessor,
