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

import com.intellij.codeInsight.BlockUtils;
import com.intellij.codeInsight.daemon.QuickFixBundle;
import com.intellij.codeInsight.daemon.impl.quickfix.DeleteElementFix;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.options.OptPane;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.java.analysis.JavaAnalysisBundle;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.PsiUpdateModCommandQuickFix;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.pom.java.JavaFeature;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiLiteralUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.siyeh.HardcodedMethodConstants;
import com.siyeh.InspectionGadgetsBundle;
import com.siyeh.ig.PsiReplacementUtil;
import com.siyeh.ig.callMatcher.CallMapper;
import com.siyeh.ig.callMatcher.CallMatcher;
import com.siyeh.ig.psiutils.*;
import org.jetbrains.annotations.*;

import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Function;

import static com.intellij.codeInspection.options.OptPane.checkbox;
import static com.intellij.codeInspection.options.OptPane.pane;
import static com.intellij.psi.CommonClassNames.*;
import static com.siyeh.HardcodedMethodConstants.EQUALS_IGNORE_CASE;
import static com.siyeh.HardcodedMethodConstants.TO_STRING;
import static com.siyeh.InspectionGadgetsBundle.BUNDLE;
import static com.siyeh.ig.callMatcher.CallMatcher.*;

public final class RedundantStringOperationInspection extends AbstractBaseJavaLocalInspectionTool implements CleanupLocalInspectionTool {
  enum FixType {
    REPLACE_WITH_QUALIFIER,
    REPLACE_WITH_ARGUMENTS
  }

  public static final String CONTENT_EQUALS = "contentEquals";

  private static final CallMatcher BYTE_ARRAY_OUTPUT_STREAM_INTO_BYTE_ARRAY =
    exactInstanceCall(JAVA_IO_BYTE_ARRAY_OUTPUT_STREAM, "toByteArray").parameterCount(0);
  private static final CallMatcher STRING_TO_STRING = exactInstanceCall(JAVA_LANG_STRING, TO_STRING).parameterCount(0);
  private static final CallMatcher STRING_INTERN = exactInstanceCall(JAVA_LANG_STRING, "intern").parameterCount(0);
  private static final CallMatcher STRING_LENGTH = exactInstanceCall(JAVA_LANG_STRING, HardcodedMethodConstants.LENGTH).parameterCount(0);
  private static final CallMatcher STRING_SUBSTRING_ONE_ARG = exactInstanceCall(JAVA_LANG_STRING, "substring").parameterTypes("int");
  private static final CallMatcher STRING_BUILDER_SUBSTRING_ONE_ARG = exactInstanceCall(JAVA_LANG_ABSTRACT_STRING_BUILDER, "substring").parameterTypes("int");
  private static final CallMatcher STRING_SUBSTRING_TWO_ARG = exactInstanceCall(JAVA_LANG_STRING, "substring").parameterTypes("int", "int");
  private static final CallMatcher STRING_BUILDER_SUBSTRING_TWO_ARG = exactInstanceCall(JAVA_LANG_ABSTRACT_STRING_BUILDER, "substring").parameterTypes("int", "int");
  private static final CallMatcher STRING_SUBSTRING = anyOf(STRING_SUBSTRING_ONE_ARG, STRING_SUBSTRING_TWO_ARG);
  private static final CallMatcher SUBSTRING = anyOf(STRING_BUILDER_SUBSTRING_ONE_ARG, STRING_BUILDER_SUBSTRING_TWO_ARG,
                                                     STRING_SUBSTRING_ONE_ARG, STRING_SUBSTRING_TWO_ARG);
  private static final CallMatcher STRING_BUILDER_APPEND =
    instanceCall(JAVA_LANG_ABSTRACT_STRING_BUILDER, "append").parameterTypes(JAVA_LANG_STRING);
  private static final CallMatcher SINGLE_ARG_STRING_BUILDER_APPEND =
    instanceCall(JAVA_LANG_ABSTRACT_STRING_BUILDER, "append").parameterCount(1);
  private static final CallMatcher STRING_BUILDER_TO_STRING = instanceCall(JAVA_LANG_ABSTRACT_STRING_BUILDER, TO_STRING).parameterCount(0);
  private static final CallMatcher PRINTSTREAM_PRINTLN = instanceCall("java.io.PrintStream", "println")
    .parameterTypes(JAVA_LANG_STRING);
  private static final CallMatcher METHOD_WITH_REDUNDANT_ZERO_AS_SECOND_PARAMETER =
    exactInstanceCall(JAVA_LANG_STRING, "indexOf", "startsWith").parameterCount(2);
  private static final CallMatcher STRING_LAST_INDEX_OF = exactInstanceCall(JAVA_LANG_STRING, "lastIndexOf").parameterCount(2);
  private static final CallMatcher STRING_IS_EMPTY = exactInstanceCall(JAVA_LANG_STRING, "isEmpty").parameterCount(0);
  private static final CallMatcher CASE_CHANGE = exactInstanceCall(JAVA_LANG_STRING, "toUpperCase", "toLowerCase");
  private static final CallMatcher STRING_EQUALS = exactInstanceCall(JAVA_LANG_STRING, HardcodedMethodConstants.EQUALS).parameterTypes(JAVA_LANG_OBJECT);
  private static final CallMatcher STRING_EQUALS_IGNORE_CASE =
    exactInstanceCall(JAVA_LANG_STRING, "equalsIgnoreCase").parameterTypes(JAVA_LANG_STRING);
  private static final CallMatcher CHANGE_CASE = anyOf(exactInstanceCall(JAVA_LANG_STRING, "toLowerCase").parameterCount(0),
                                                       exactInstanceCall(JAVA_LANG_STRING, "toUpperCase").parameterCount(0));
  private static final CallMatcher STRING_VALUE_OF = staticCall(JAVA_LANG_STRING, "valueOf").parameterTypes("char[]");
  private static final CallMatcher STRING_STRIP =
    exactInstanceCall(JAVA_LANG_STRING, "strip", "stripLeading", "stripTrailing").parameterCount(0);
  private static final CallMatcher STRING_TO_CHAR_ARRAY = exactInstanceCall(JAVA_LANG_STRING, "toCharArray").parameterCount(0);

  public boolean ignoreStringConstructor = false;
  public boolean ignoreSingleArgSubstring = true;

  @Override
  public @NotNull OptPane getOptionsPane() {
    return pane(
      checkbox("ignoreStringConstructor",
               InspectionGadgetsBundle.message("inspection.redundant.string.option.do.not.report.string.constructors")),
      checkbox("ignoreSingleArgSubstring",
               InspectionGadgetsBundle.message("inspection.redundant.string.option.do.not.report.single.argument.substring")));
  }

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

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

  private static class RedundantStringOperationVisitor extends JavaElementVisitor {
    private final CallMapper<ProblemDescriptor> myProcessors = new CallMapper<ProblemDescriptor>()
      .register(STRING_TO_STRING, call -> getProblem(call, "inspection.redundant.string.call.message"))
      .register(SUBSTRING, this::getSubstringProblem)
      .register(SUBSTRING, this::getAppendSubstringProblem)
      .register(STRING_BUILDER_APPEND, this::getAppendProblem)
      .register(STRING_BUILDER_TO_STRING, this::getStringBuilderToStringProblem)
      .register(STRING_INTERN, this::getInternProblem)
      .register(PRINTSTREAM_PRINTLN, call ->
        getRedundantArgumentProblem(getSingleEmptyStringArgument(call), "inspection.redundant.empty.string.argument.message"))
      .register(METHOD_WITH_REDUNDANT_ZERO_AS_SECOND_PARAMETER, this::getRedundantZeroAsSecondParameterProblem)
      .register(STRING_LAST_INDEX_OF, this::getLastIndexOfProblem)
      .register(STRING_IS_EMPTY, this::getRedundantCaseChangeProblem)
      .register(STRING_EQUALS, this::getRedundantSubstringEqualsProblem)
      .register(anyOf(STRING_EQUALS, STRING_EQUALS_IGNORE_CASE), this::getRedundantCaseEqualsProblem)
      .register(STRING_VALUE_OF, call -> getValueOfProblem(call.getArgumentList()))
      .register(STRING_IS_EMPTY, this::getStripIsEmptyProblem);
    private final InspectionManager myManager;
    private final ProblemsHolder myHolder;
    private final boolean myIsOnTheFly;
    private final RedundantStringOperationInspection myInspection;

    RedundantStringOperationVisitor(ProblemsHolder holder, boolean isOnTheFly, RedundantStringOperationInspection inspection) {
      myHolder = holder;
      myIsOnTheFly = isOnTheFly;
      myInspection = inspection;
      myManager = myHolder.getManager();
    }

    @Override
    public void visitMethodCallExpression(@NotNull PsiMethodCallExpression call) {
      PsiExpression qualifier = call.getMethodExpression().getQualifierExpression();
      if (qualifier == null) return;
      myProcessors.mapAll(call).forEach(myHolder::registerProblem);
    }

    @Override
    public void visitNewExpression(@NotNull PsiNewExpression expression) {
      PsiJavaCodeReferenceElement classRef = expression.getClassReference();
      ProblemDescriptor descriptor = null;
      if (ConstructionUtils.isReferenceTo(classRef, JAVA_LANG_STRING_BUILDER, JAVA_LANG_STRING_BUFFER)) {
        String key = "inspection.redundant.empty.string.argument.message";
        descriptor = getRedundantArgumentProblem(getSingleEmptyStringArgument(expression), key);
      }
      else if (ConstructionUtils.isReferenceTo(classRef, JAVA_LANG_STRING) && !myInspection.ignoreStringConstructor) {
        descriptor = getStringConstructorProblem(expression);
      }
      if (descriptor != null) {
        myHolder.registerProblem(descriptor);
      }
    }

    @Override
    public void visitReferenceExpression(@NotNull PsiReferenceExpression expression) {
      PsiExpression qualifier = PsiUtil.skipParenthesizedExprDown(expression.getQualifierExpression());
      if (!(qualifier instanceof PsiMethodCallExpression call) ||
          !"length".equals(expression.getReferenceName()) ||
          !STRING_TO_CHAR_ARRAY.test(call)) {
        return;
      }
      myHolder.registerProblem(Objects.requireNonNull(call.getMethodExpression().getReferenceNameElement()),
                               InspectionGadgetsBundle.message("unnecessary.tostring.call.problem.descriptor"),
                               new RemoveToCharArrayCallFix());
    }

    @Override
    public void visitTemplateExpression(@NotNull PsiTemplateExpression element) {
      if (!PsiUtil.isAvailable(JavaFeature.STRING_TEMPLATES, element) || element.getLiteralExpression() == null) {
        return;
      }
      PsiExpression processor = element.getProcessor();
      if (!(PsiUtil.skipParenthesizedExprDown(processor) instanceof PsiReferenceExpression reference)) {
        return;
      }
      PsiElement target = reference.resolve();
      if (!(target instanceof PsiField field) || !"STR".equals(field.getName())) {
        return;
      }
      PsiClass containingClass = field.getContainingClass();
      if (containingClass == null || !JAVA_LANG_STRING_TEMPLATE.equals(containingClass.getQualifiedName())) {
        return;
      }
      myHolder.registerProblem(processor,
                               InspectionGadgetsBundle.message("inspection.redundant.string.fix.remove.str.processor.description"),
                               new RemoveStrTemplateProcessorFix());
    }

    private ProblemDescriptor getStringConstructorProblem(PsiNewExpression expression) {
      PsiExpressionList argumentList = expression.getArgumentList();
      if (argumentList == null) return null;
      final PsiJavaCodeReferenceElement anchor = expression.getClassOrAnonymousClassReference();
      if (anchor == null) return null;
      if (argumentList.isEmpty()) {
        LocalQuickFix[] fixes = {
          new StringConstructorFix(true),
          LocalQuickFix.from(new UpdateInspectionOptionFix(
            myInspection, "ignoreStringConstructor",
            InspectionGadgetsBundle.message("inspection.redundant.string.option.do.not.report.string.constructors"), true))};
        return myManager.createProblemDescriptor(anchor, (TextRange)null,
                                                 InspectionGadgetsBundle.message("inspection.redundant.string.constructor.message"),
                                                 ProblemHighlightType.GENERIC_ERROR_OR_WARNING, myIsOnTheFly, fixes);
      }
      final PsiExpression[] args = argumentList.getExpressions();

      if (isNewStringFromByteArrayParams(args)) {
        PsiMethodCallExpression methodCall = getMethodCallExpression(args[0]);

        if (BYTE_ARRAY_OUTPUT_STREAM_INTO_BYTE_ARRAY.test(methodCall)) {
          final PsiElement qualifier = methodCall.getMethodExpression().getQualifier();
          if (qualifier == null) return null;

          String newExpressionText = qualifier.getText() + ".toString(" + (args.length == 2 ? args[1].getText() : "") + ")";

          return myManager.createProblemDescriptor(anchor, (TextRange)null,
                                                   InspectionGadgetsBundle.message("inspection.byte.array.output.stream.to.string.message"),
                                                   ProblemHighlightType.WARNING, myIsOnTheFly,
                                                   new ByteArrayOutputStreamToStringFix(newExpressionText));
        }
      }
      if (argumentList.getExpressionCount() == 1) {
        final CharArrayCreationArgument charArrayCreationArgument = CharArrayCreationArgument.from(argumentList);
        if (charArrayCreationArgument != null) {
          LocalQuickFix[] fixes = {
            new ReplaceWithValueOfFix(),
            LocalQuickFix.from(new UpdateInspectionOptionFix(
              myInspection, "ignoreStringConstructor",
              InspectionGadgetsBundle.message("inspection.redundant.string.option.do.not.report.string.constructors"), true))};
          return myManager.createProblemDescriptor(anchor, (TextRange)null,
                                                   JavaAnalysisBundle.message("inspection.can.be.replaced.with.message", "String.valueOf()"),
                                                   ProblemHighlightType.WARNING, myIsOnTheFly, fixes);
        }
        PsiExpression arg = argumentList.getExpressions()[0];
        if (TypeUtils.isJavaLangString(arg.getType()) &&
            (PsiUtil.isLanguageLevel7OrHigher(expression) || !STRING_SUBSTRING.matches(arg))) {
          LocalQuickFix[] fixes = {
            new StringConstructorFix(false),
            LocalQuickFix.from(new UpdateInspectionOptionFix(
              myInspection, "ignoreStringConstructor",
              InspectionGadgetsBundle.message("inspection.redundant.string.option.do.not.report.string.constructors"), true))};
          return myManager.createProblemDescriptor(anchor, (TextRange)null,
                                                   InspectionGadgetsBundle.message("inspection.redundant.string.constructor.message"),
                                                   ProblemHighlightType.GENERIC_ERROR_OR_WARNING, myIsOnTheFly, fixes);
        }
      }
      else if (isNewStringCreatedFromEntireArray(args)) {
        LocalQuickFix fix = new RemoveRedundantOffsetAndLengthArgumentsFix(args[1], args[2]);
        return myManager.createProblemDescriptor(args[1], args[2],
                                                 InspectionGadgetsBundle.message("inspection.redundant.arguments.message"),
                                                 ProblemHighlightType.LIKE_UNUSED_SYMBOL, myIsOnTheFly, fix);
      }
      return null;
    }

    /**
     * Checks that a new string is created from an entire array
     *
     * @param args arguments passed to the string constructor
     *
     * @return {@code true} if a new string is created from an entire array and the constructor
     * call can be simplified by removing redundant arguments, otherwise - {@code false}
     *
     * @see String#String(byte[], int, int)
     * @see String#String(char[], int, int)
     * @see String#String(byte[], int, int, java.nio.charset.Charset)
     * @see String#String(byte[], int, int, String)
     */
    private static boolean isNewStringCreatedFromEntireArray(PsiExpression[] args) {
      if (args.length < 3 || !ExpressionUtils.isZero(args[1])) return false;
      PsiExpression arrayExpression = ExpressionUtils.getArrayFromLengthExpression(args[2]);
      EquivalenceChecker equivalence = EquivalenceChecker.getCanonicalPsiEquivalence();
      if (!equivalence.expressionsAreEquivalent(args[0], arrayExpression)) return false;
      return args.length == 3 && (TypeUtils.typeEquals("byte[]", args[0].getType()) || TypeUtils.typeEquals("char[]", args[0].getType())) ||
             args.length == 4 &&
             TypeUtils.typeEquals("byte[]", args[0].getType()) &&
             (TypeUtils.isJavaLangString(args[3].getType()) || TypeUtils.typeEquals(JAVA_NIO_CHARSET_CHARSET, args[3].getType()));
    }

    private static boolean isNewStringFromByteArrayParams(PsiExpression[] args) {
      if (args.length == 0 || !TypeUtils.typeEquals("byte[]", args[0].getType())) {
        return false;
      }
      if (args.length == 1) return true;
      if (args.length == 2) {
        PsiType type = args[1].getType();
        final LanguageLevel languageLevel = PsiUtil.getLanguageLevel(args[1]);
        return TypeUtils.isJavaLangString(type) ||
               (TypeUtils.typeEquals(JAVA_NIO_CHARSET_CHARSET, type) && languageLevel.isAtLeast(LanguageLevel.JDK_10));
      }
      return false;
    }

    private @Nullable ProblemDescriptor getRedundantCaseEqualsProblem(PsiMethodCallExpression call) {
      PsiExpression equalTo = PsiUtil.skipParenthesizedExprDown(call.getArgumentList().getExpressions()[0]);
      if (equalTo == null) return null;
      //case: "foo".equals(s.toLowerCase())
      if (equalTo instanceof PsiMethodCallExpression equalsToCallExpression &&
          isChangeCaseCall(equalsToCallExpression, call.getMethodExpression().getQualifierExpression())) {
        PsiElement anchor = equalsToCallExpression.getMethodExpression().getReferenceNameElement();
        if (anchor == null) {
          return null;
        }
        return createChangeCaseProblem(equalsToCallExpression, anchor, RemoveRedundantChangeCaseFix.PlaceCaseEqualType.RIGHT);
      }

      PsiMethodCallExpression qualifierCall = MethodCallUtils.getQualifierMethodCall(call);
      if (qualifierCall == null) return null;
      PsiExpression receiver = qualifierCall.getMethodExpression().getQualifierExpression();
      if (receiver == null) return null;

      PsiElement anchor = qualifierCall.getMethodExpression().getReferenceNameElement();
      if (anchor == null) {
        return null;
      }
      if (isChangeCaseCall(qualifierCall, equalTo)) {
        //case: text1.toLowerCase().equals("test2")
        return createChangeCaseProblem(qualifierCall, anchor, RemoveRedundantChangeCaseFix.PlaceCaseEqualType.LEFT);
      }

      //case: text1.toLowerCase().equals(text2.toLowerCase())
      if (equalTo instanceof PsiMethodCallExpression secondCall && isEqualChangeCaseCall(qualifierCall, secondCall)) {
        return createChangeCaseProblem(secondCall, anchor, RemoveRedundantChangeCaseFix.PlaceCaseEqualType.BOTH);
      }

      return null;
    }

    private @Nullable ProblemDescriptor createChangeCaseProblem(PsiMethodCallExpression equalsToCallExpression,
                                                                PsiElement anchor,
                                                                RemoveRedundantChangeCaseFix.PlaceCaseEqualType type) {
      String nameMethod = equalsToCallExpression.getMethodExpression().getReferenceName();
      if (nameMethod == null) {
        return null;
      }
      return myManager.createProblemDescriptor(anchor, (TextRange)null,
                                               InspectionGadgetsBundle.message("inspection.x.call.can.be.replaced.with.y",
                                                                               EQUALS_IGNORE_CASE),
                                               ProblemHighlightType.GENERIC_ERROR_OR_WARNING, myIsOnTheFly,
                                               new RemoveRedundantChangeCaseFix(nameMethod, type));
    }


    private static boolean isChangeCaseCall(@NotNull PsiMethodCallExpression qualifierCall,
                                            @Nullable PsiExpression constant) {
      if (constant == null) return false;
      if (!CHANGE_CASE.test(qualifierCall)) return false;
      if (!(ExpressionUtils.computeConstantExpression(constant) instanceof String constValue)) return false;
      // Do not suggest incorrect fix for "HELLO".equals(s.toLowerCase())
      String methodName = qualifierCall.getMethodExpression().getReferenceName();
      String normalized = "toUpperCase".equals(methodName) ? constValue.toUpperCase(Locale.ROOT) : constValue.toLowerCase(Locale.ROOT);
      return constValue.equals(normalized);
    }

    private static boolean isEqualChangeCaseCall(@NotNull PsiMethodCallExpression qualifierCall,
                                                 @NotNull PsiMethodCallExpression secondCall) {
      return CHANGE_CASE.test(qualifierCall) &&
             CHANGE_CASE.test(secondCall) &&
             qualifierCall.getMethodExpression().getReferenceName() != null &&
             qualifierCall.getMethodExpression().getReferenceName().equals(secondCall.getMethodExpression().getReferenceName());
    }

    private ProblemDescriptor getRedundantSubstringEqualsProblem(PsiMethodCallExpression call) {
      PsiMethodCallExpression qualifierCall = MethodCallUtils.getQualifierMethodCall(call);
      if (qualifierCall == null) return null;
      PsiExpression receiver = qualifierCall.getMethodExpression().getQualifierExpression();
      if (receiver == null) return null;
      if (STRING_SUBSTRING_TWO_ARG.test(qualifierCall)) {
        PsiExpression equalTo = call.getArgumentList().getExpressions()[0];
        PsiExpression[] args = qualifierCall.getArgumentList().getExpressions();
        boolean lengthMatches = lengthMatches(equalTo, args[0], args[1]);
        if (lengthMatches) {
          PsiElement anchor = qualifierCall.getMethodExpression().getReferenceNameElement();
          if (anchor != null) {
            if (equalTo instanceof PsiLiteralExpression literal &&
                literal.getValue() instanceof String str &&
                StringUtil.length(str) == 1) {
              return createSubstringToCharAtProblemDescriptor(call, anchor);
            }
            RemoveRedundantSubstringFix fix = new RemoveRedundantSubstringFix(isLengthOf(args[1], receiver) ? "endsWith" : "startsWith");
            return myManager.createProblemDescriptor(anchor, (TextRange)null,
                                                     InspectionGadgetsBundle.message("inspection.redundant.string.call.message"),
                                                     ProblemHighlightType.GENERIC_ERROR_OR_WARNING, myIsOnTheFly,
                                                     fix);
          }
        }
      }
      if (STRING_SUBSTRING_ONE_ARG.test(qualifierCall)) {
        PsiExpression equalTo = call.getArgumentList().getExpressions()[0];
        PsiExpression from = qualifierCall.getArgumentList().getExpressions()[0];
        PsiExpression to = getLengthExpression(receiver, JavaPsiFacade.getElementFactory(myHolder.getProject()));
        boolean lengthMatches = lengthMatches(equalTo, from, to);
        if (lengthMatches) {
          PsiElement anchor = qualifierCall.getMethodExpression().getReferenceNameElement();
          if (anchor != null) {
            return myManager.createProblemDescriptor(anchor, (TextRange)null,
                                                     InspectionGadgetsBundle.message("inspection.redundant.string.call.message"),
                                                     ProblemHighlightType.GENERIC_ERROR_OR_WARNING, myIsOnTheFly,
                                                     new RemoveRedundantSubstringFix("endsWith"));
          }
        }
      }
      return null;
    }

    /**
     * This method is meant to generate a {@link ProblemDescriptor} for a call like
     * <pre>
     * stringValue.substring(i, i + 1).equals("_")
     * </pre>
     * which can be replaced with a more readable version
     * <pre>
     *   stringValue.charAt(i) == '_'
     * </pre>
     * <hr />
     * <p>
     * <strong>IMPORTANT:</strong> this method is meant to be called only from
     * {@link RedundantStringOperationVisitor#getRedundantSubstringEqualsProblem(PsiMethodCallExpression)}
     * </p>
     *
     * @param call the original expression to generate a {@link ProblemDescriptor} for
     * @return generated instance of {@link ProblemDescriptor}
     */
    private @NotNull ProblemDescriptor createSubstringToCharAtProblemDescriptor(@NotNull PsiMethodCallExpression call, @NotNull PsiElement anchor) {
      final String converted = SubstringToCharAtQuickFix.getTargetString(call, PsiElement::getText);
      assert converted != null : "Message cannot be null";

      return myManager.createProblemDescriptor(anchor,
                                               InspectionGadgetsBundle.message("inspection.x.call.can.be.replaced.with.y", "charAt"),
                                               new SubstringToCharAtQuickFix(getOutermostEquals(call).getText(), converted, true),
                                               ProblemHighlightType.GENERIC_ERROR_OR_WARNING, myIsOnTheFly);
    }

    private static boolean isNegated(final @NotNull PsiExpression start, boolean negated) {
      final PsiExpression negation = BoolUtils.findNegation(start);
      if (negation == null) return negated;
      else return isNegated(negation, !negated);
    }

    private static PsiExpression getOutermostEquals(final @NotNull PsiExpression start) {
      final PsiExpression negation = BoolUtils.findNegation(start);
      if (negation == null) return start;
      else return getOutermostEquals(negation);
    }

    private boolean lengthMatches(PsiExpression equalTo, PsiExpression from, PsiExpression to) {
      if (!ExpressionUtils.hasStringType(equalTo)) return false;
      PsiElementFactory factory = JavaPsiFacade.getElementFactory(myHolder.getProject());
      if (ExpressionUtils.computeConstantExpression(equalTo) instanceof String str) {
        PsiExpression lengthExpression = factory.createExpressionFromText(String.valueOf(str.length()), equalTo);
        if (ExpressionUtils.isDifference(from, to, lengthExpression)) return true;
      }
      PsiExpression lengthExpression = getLengthExpression(equalTo, factory);
      return ExpressionUtils.isDifference(from, to, lengthExpression);
    }

    private static PsiExpression getLengthExpression(PsiExpression string, PsiElementFactory factory) {
      String text = ParenthesesUtils.getText(string, ParenthesesUtils.METHOD_CALL_PRECEDENCE);
      return factory.createExpressionFromText(text + ".length()", string);
    }

    private @Nullable ProblemDescriptor getRedundantCaseChangeProblem(PsiMethodCallExpression call) {
      PsiMethodCallExpression qualifierCall = MethodCallUtils.getQualifierMethodCall(call);
      if (CASE_CHANGE.test(qualifierCall)) {
        return getProblem(qualifierCall, "inspection.redundant.string.call.message");
      }
      return null;
    }

    private @Nullable ProblemDescriptor getStripIsEmptyProblem(PsiMethodCallExpression call) {
      PsiMethodCallExpression qualifierCall = MethodCallUtils.getQualifierMethodCall(call);
      if (!STRING_STRIP.test(qualifierCall)) return null;
      PsiElement anchor = qualifierCall.getMethodExpression().getReferenceNameElement();
      if (anchor == null) return null;
      String message = InspectionGadgetsBundle.message("inspection.x.call.can.be.replaced.with.y", "isBlank");
      return myManager.createProblemDescriptor(anchor, (TextRange)null, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, myIsOnTheFly,
                                               new StripIsEmptyToIsBlankFix());
    }

    private @Nullable ProblemDescriptor getAppendProblem(PsiMethodCallExpression call) {
      return getSingleEmptyStringArgument(call) != null ? getProblem(call, "inspection.redundant.string.call.message") : null;
    }

    private @Nullable ProblemDescriptor getStringBuilderToStringProblem(final @NotNull PsiMethodCallExpression call) {
      final ProblemDescriptor descriptor = getRedundantStringBuilderToStringProblem(call);
      if (descriptor != null) return descriptor;
      return getUseContentEqualsProblem(call);
    }

    private @Nullable ProblemDescriptor getRedundantStringBuilderToStringProblem(final @NotNull PsiMethodCallExpression call) {
      final PsiMethodCallExpression substringCall = PsiTreeUtil.getParentOfType(call, PsiMethodCallExpression.class);
      if (substringCall == null) return null;

      final PsiExpression qualifier = PsiUtil.skipParenthesizedExprDown(substringCall.getMethodExpression().getQualifierExpression());
      if (qualifier != call || !STRING_SUBSTRING.test(substringCall)) return null;

      return getProblem(call, "inspection.redundant.string.call.message");
    }

    private @Nullable ProblemDescriptor getUseContentEqualsProblem(final @NotNull PsiMethodCallExpression call) {
      PsiElement parent = PsiUtil.skipParenthesizedExprUp(call.getParent());
      if (parent instanceof PsiExpressionList list &&
          list.getExpressionCount() == 1 &&
          parent.getParent() instanceof PsiMethodCallExpression parentCall &&
          STRING_EQUALS.test(parentCall)) {
        PsiElement nameElement = Objects.requireNonNull(call.getMethodExpression().getReferenceNameElement());
        final String message = InspectionGadgetsBundle.message("inspection.x.call.can.be.replaced.with.y", CONTENT_EQUALS);
        return myManager.createProblemDescriptor(nameElement, message, new UseContentEqualsFix(),
                                                 ProblemHighlightType.GENERIC_ERROR_OR_WARNING, myIsOnTheFly);
      }
      return null;
    }

    private @Nullable ProblemDescriptor getInternProblem(PsiMethodCallExpression call) {
      return PsiUtil.isConstantExpression(call.getMethodExpression().getQualifierExpression())
             ? getProblem(call, "inspection.redundant.string.intern.on.constant.message")
             : null;
    }

    private @Nullable ProblemDescriptor getLastIndexOfProblem(PsiMethodCallExpression call) {
      PsiExpression secondArg = call.getArgumentList().getExpressions()[1];
      PsiExpression stripped = PsiUtil.skipParenthesizedExprDown(secondArg);
      // s.lastIndexOf(..., s.length()) or s.lastIndexOf(..., s.length() - 1)
      if (stripped instanceof PsiBinaryExpression binOp && binOp.getOperationTokenType() == JavaTokenType.MINUS &&
          ExpressionUtils.isLiteral(PsiUtil.skipParenthesizedExprDown(binOp.getROperand()), 1)) {
        stripped = binOp.getLOperand();
      }
      PsiExpression qualifier = call.getMethodExpression().getQualifierExpression();
      if (!isLengthOf(stripped, qualifier)) return null;
      return getRedundantArgumentProblem(secondArg, "inspection.redundant.string.length.argument.message");
    }

    private @Nullable ProblemDescriptor getRedundantZeroAsSecondParameterProblem(PsiMethodCallExpression call) {
      PsiExpression secondArg = call.getArgumentList().getExpressions()[1];
      if (ExpressionUtils.isLiteral(PsiUtil.skipParenthesizedExprDown(secondArg), 0)) {
        return getRedundantArgumentProblem(secondArg, "inspection.redundant.zero.argument.message");
      }
      return null;
    }

    private @Nullable ProblemDescriptor getRedundantArgumentProblem(@Nullable PsiExpression argument,
                                                                    @NotNull @PropertyKey(resourceBundle = BUNDLE) String key) {
      if (argument == null) return null;
      LocalQuickFix fix = LocalQuickFix.from(new DeleteElementFix(
        argument, InspectionGadgetsBundle.message("inspection.redundant.string.remove.argument.fix.name")));
      return myManager.createProblemDescriptor(argument,
                                               InspectionGadgetsBundle.message(key),
                                               myIsOnTheFly,
                                               new LocalQuickFix[]{fix},
                                               ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
    }

    private static @Nullable PsiExpression getSingleEmptyStringArgument(PsiCall call) {
      PsiExpressionList argList = call.getArgumentList();
      if (argList == null) return null;
      PsiExpression[] args = argList.getExpressions();
      if (args.length != 1) return null;
      return ExpressionUtils.isLiteral(PsiUtil.skipParenthesizedExprDown(args[0]), "") ? args[0] : null;
    }
