// 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;

import com.intellij.codeInspection.util.InspectionMessage;
import com.intellij.java.JavaBundle;
import com.intellij.java.codeserver.core.JavaPsiSwitchUtil;
import com.intellij.java.syntax.parser.JavaKeywords;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.PsiUpdateModCommandQuickFix;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.pom.java.JavaFeature;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.JavaElementType;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.JavaPsiPatternUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.refactoring.extractMethod.InputVariables;
import com.intellij.refactoring.util.duplicates.DuplicatesFinder;
import com.intellij.refactoring.util.duplicates.Match;
import com.intellij.refactoring.util.duplicates.ReturnValue;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.containers.ContainerUtil;
import com.siyeh.ig.migration.TryWithIdenticalCatchesInspection;
import com.siyeh.ig.psiutils.CommentTracker;
import com.siyeh.ig.psiutils.ControlFlowUtils;
import com.siyeh.ig.psiutils.ExpressionUtils;
import com.siyeh.ig.psiutils.SwitchUtils;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.jetbrains.annotations.*;

import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Stream;


public final class DuplicateBranchesInSwitchInspection extends LocalInspectionTool {
  private static final Logger LOG = Logger.getInstance(DuplicateBranchesInSwitchInspection.class);

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

  private static class DuplicateBranchesVisitor extends JavaElementVisitor {
    private final ProblemsHolder myHolder;

    DuplicateBranchesVisitor(ProblemsHolder holder) { myHolder = holder; }

    @Override
    public void visitSwitchStatement(@NotNull PsiSwitchStatement switchStatement) {
      super.visitSwitchStatement(switchStatement);

      visitSwitchBlock(switchStatement);
    }

    @Override
    public void visitSwitchExpression(@NotNull PsiSwitchExpression switchExpression) {
      super.visitSwitchExpression(switchExpression);

      visitSwitchBlock(switchExpression);
    }

    private void visitSwitchBlock(PsiSwitchBlock switchBlock) {
      if (SwitchUtils.isRuleFormatSwitch(switchBlock)) {
        visitEnhancedSwitch(switchBlock);
        return;
      }

      for (List<Branch> branches : collectProbablySimilarBranches(switchBlock)) {
        registerProblems(branches);
      }
    }

    private void visitEnhancedSwitch(@NotNull PsiSwitchBlock switchBlock) {
      Collection<List<Rule>> probablySimilarRules = collectProbablySimilarRules(switchBlock);
      for (List<Rule> rules : probablySimilarRules) {
        registerProblems(rules);
      }
    }

    void registerProblems(List<? extends BranchBase<?>> branches) {
      int size = branches.size();
      if (size > 1) {
        boolean[] isDuplicate = new boolean[size];

        int defaultIndex = ContainerUtil.indexOf(branches, BranchBase::isDefault);
        if (defaultIndex >= 0) {
          BranchBase<?> defaultBranch = branches.get(defaultIndex);
          for (int index = 0; index < size; index++) {
            if (index != defaultIndex) {
              BranchBase<?> branch = branches.get(index);
              LevelType state = canBeJoined(defaultBranch, branch);
              if (state != LevelType.NO) {
                isDuplicate[index] = isDuplicate[defaultIndex] = true;
                highlightDefaultDuplicate(branch, defaultBranch, state);
              }
            }
          }
        }

        int compareCount = 0;
        for (int index = 0; index < size - 1; index++) {
          if (isDuplicate[index]) continue;
          BranchBase<?> branch = branches.get(index);

          for (int otherIndex = index + 1; otherIndex < size; otherIndex++) {
            if (isDuplicate[otherIndex]) continue;
            if (++compareCount > 200) return; // avoid quadratic loop over too large list, but at least try to do something in that case
            BranchBase<?> otherBranch = branches.get(otherIndex);

            LevelType state = canBeJoined(branch, otherBranch);
            if (state != LevelType.NO) {
              isDuplicate[otherIndex] = true;
              LocalQuickFix fix = branch.mergeCasesFix(otherBranch);
              if (fix != null) {
                registerProblem(otherBranch, otherBranch.getCaseBranchMessage(), state, fix);
              }
            }
          }
        }
      }
    }

    private void highlightDefaultDuplicate(@NotNull BranchBase<?> branch, BranchBase<?> defaultBranch, LevelType state) {
      List<LocalQuickFix> fixes = new ArrayList<>();
      LocalQuickFix deleteCaseFix = branch.deleteCaseFix();
      ContainerUtil.addIfNotNull(fixes, deleteCaseFix);
      LocalQuickFix mergeWithDefaultFix = branch.mergeWithDefaultFix(defaultBranch);
      ContainerUtil.addIfNotNull(fixes, mergeWithDefaultFix);
      if (!fixes.isEmpty()) {
        registerProblem(branch, branch.getDefaultBranchMessage(), state, fixes.toArray(LocalQuickFix.EMPTY_ARRAY));
      }
    }

    private void registerProblem(@NotNull BranchBase<?> duplicate,
                                 @NotNull @InspectionMessage String message,
                                 @NotNull DuplicateBranchesInSwitchInspection.LevelType state,
                                 @NotNull LocalQuickFix @NotNull ... fixes) {
      PsiElement[] elements = duplicate.myStatements;
      if (state == LevelType.NO) {
        return;
      }
      if (state == LevelType.INFO && !myHolder.isOnTheFly()) {
        return;
      }
      InspectionManager inspectionManager = InspectionManager.getInstance(myHolder.getProject());
      ProblemDescriptor descriptor;
      if (state == LevelType.INFO) {
        descriptor = inspectionManager
          .createProblemDescriptor(elements[0], elements[elements.length - 1],
                                   message, ProblemHighlightType.INFORMATION, myHolder.isOnTheFly(), fixes);
      }
      else {
        descriptor = inspectionManager
          .createProblemDescriptor(elements[0],
                                   message, myHolder.isOnTheFly(), fixes, ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
      }
      myHolder.registerProblem(descriptor);
    }
  }

  private static @NotNull Collection<List<Rule>> collectProbablySimilarRules(@NotNull PsiSwitchBlock switchBlock) {
    PsiCodeBlock switchBody = switchBlock.getBody();
    if (switchBody == null) {
      return Collections.emptyList();
    }

    Int2ObjectMap<List<Rule>> rulesByHash = new Int2ObjectOpenHashMap<>();
    List<String> commentTexts = new ArrayList<>();
    for (PsiElement element = switchBody.getFirstChild(); element != null; element = element.getNextSibling()) {
      TryWithIdenticalCatchesInspection.collectCommentTexts(element, commentTexts);
      if (element instanceof PsiSwitchLabeledRuleStatement ruleStatement) {
        PsiStatement body = ruleStatement.getBody();
        if (body != null) {
          //noinspection AssignmentToForLoopParameter
          element = collectCommentsUntilNewLine(commentTexts, element);
          Rule rule = new Rule(ruleStatement, ruleStatement, body, ArrayUtilRt.toStringArray(commentTexts));
          commentTexts.clear();
          rulesByHash.computeIfAbsent(rule.hash(), __ -> new ArrayList<>()).add(rule);
        }
      }
    }
    return new ArrayList<>(rulesByHash.values());
  }

  private static @NotNull PsiElement collectCommentsUntilNewLine(List<String> commentTexts, PsiElement element) {
    List<String> mightBeCommentTexts = new ArrayList<>();
    for (PsiElement currentSibling = element.getNextSibling(); currentSibling != null; currentSibling = currentSibling.getNextSibling()) {
      if (currentSibling instanceof PsiWhiteSpace whiteSpace) {
        if (whiteSpace.textContains('\n')) {
          commentTexts.addAll(mightBeCommentTexts);
          return currentSibling;
        }
        else {
          continue;
        }
      }
      else if (currentSibling instanceof PsiComment) {
        TryWithIdenticalCatchesInspection.collectCommentTexts(currentSibling, mightBeCommentTexts);
        continue;
      }
      return currentSibling;
    }
    return element;
  }

  static @NotNull Collection<List<Branch>> collectProbablySimilarBranches(@NotNull PsiSwitchBlock switchBlock) {
    PsiCodeBlock body = switchBlock.getBody();
    if (body == null) return Collections.emptyList();

    List<PsiStatement> statementList = null;
    Comments comments = new Comments();

    Branch previousBranch = null;
    Int2ObjectMap<List<Branch>> branchesByHash = new Int2ObjectOpenHashMap<>();
    for (PsiElement child = body.getFirstChild(); child != null; child = child.getNextSibling()) {
      if (child instanceof PsiSwitchLabelStatement switchLabel) {
        previousBranch = addBranchToMap(branchesByHash, statementList, hasImplicitBreak(switchLabel), comments, previousBranch);

        statementList = null;
        comments.addFrom(switchLabel);
      }
      else if (child instanceof PsiStatement statement) {
        if (statementList == null) {
          statementList = new ArrayList<>();
        }
        statementList.add(statement);
        comments.addFrom(statement);
      }
      else {
        comments.addPending(child);
      }
    }

    addBranchToMap(branchesByHash, statementList, true, comments, previousBranch);
    return List.copyOf(branchesByHash.values());
  }

  private static @Nullable Branch addBranchToMap(@NotNull Int2ObjectMap<List<Branch>> branchesByHash,
                                                 @Nullable List<PsiStatement> statementList,
                                                 boolean hasImplicitBreak,
                                                 @NotNull Comments comments,
                                                 @Nullable Branch previousBranch) {
    if (statementList == null || statementList.isEmpty()) {
      return previousBranch;
    }
    PsiStatement first = statementList.getFirst();
    PsiSwitchLabelStatement[] labels = Branch.collectLabels(first);
    if (labels.length == 0) {
      return previousBranch; // the code without a label is not allowed in 'switch', just ignore it
    }
    Branch branch = new Branch(first, labels, statementList, hasImplicitBreak, comments.fetchTexts());
    if (!branch.canFallThrough() && (previousBranch == null || !previousBranch.canFallThrough())) {
      int hash = branch.hash();
      List<Branch> branches = branchesByHash.get(hash);
      if (branches == null) branchesByHash.put(hash, branches = new ArrayList<>());
      branches.add(branch);
    }
    return branch;
  }


  static LevelType canBeJoined(@NotNull BranchBase<?> branch, @NotNull BranchBase<?> otherBranch) {
    if (branch.isSimpleExit() != otherBranch.isSimpleExit() ||
        branch.canFallThrough() != otherBranch.canFallThrough() ||
        branch.effectiveLength() != otherBranch.effectiveLength()) {
      return LevelType.NO;
    }
    if (!branch.canBeJoinedWith(otherBranch)) return LevelType.NO;

    Match match = branch.match(otherBranch);
    if (match != null) {
      Match otherMatch = otherBranch.match(branch);
      if (otherMatch != null) {
        if (!Arrays.equals(branch.myCommentTexts, otherBranch.myCommentTexts)) {
          return LevelType.INFO;
        }
        return ReturnValue.areEquivalent(match.getReturnValue(), otherMatch.getReturnValue()) ? LevelType.WARN : LevelType.NO;
      }
    }
    return LevelType.NO;
  }

  private static boolean hasImplicitBreak(@NotNull PsiStatement statement) {
    while (statement instanceof PsiSwitchLabelStatement) {
      statement = PsiTreeUtil.getNextSiblingOfType(statement, PsiStatement.class);
    }
    return statement == null || isBreakWithoutLabel(statement);
  }

  @Contract("null -> false")
  private static boolean isBreakWithoutLabel(@Nullable PsiStatement statement) {
    return statement instanceof PsiBreakStatement breakStatement && breakStatement.getLabelIdentifier() == null;
  }

  @Contract("_,null -> false")
  private static boolean isRedundantComment(@NotNull @Unmodifiable Set<String> existingComments, @Nullable PsiElement element) {
    if (element instanceof PsiComment comment) {
      String text = TryWithIdenticalCatchesInspection.getCommentText(comment);
      return text.isEmpty() || existingComments.contains(text);
    }
    return false;
  }

  private static class MergeBranchesFix extends PsiUpdateModCommandQuickFix {
    private final @NotNull String mySwitchLabelText;

    MergeBranchesFix(@NotNull String switchLabelText) {
      mySwitchLabelText = switchLabelText;
    }

    @Override
    public @Nls(capitalization = Nls.Capitalization.Sentence) @NotNull String getFamilyName() {
      return JavaBundle.message("inspection.duplicate.branches.in.switch.merge.fix.family.name");
    }

    @Override
    public @Nls(capitalization = Nls.Capitalization.Sentence) @NotNull String getName() {
      return JavaBundle.message("inspection.duplicate.branches.in.switch.merge.fix.name", mySwitchLabelText);
    }

    @Override
    protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
      BranchFixContext context = new BranchFixContext();
      if (context.prepare(element, branch -> mySwitchLabelText.equals(branch.getSwitchLabelText()))) {
        context.moveBranchLabel();
        context.deleteRedundantComments();
        context.deleteStatements();
      }
    }
  }

  private static class MergeWithDefaultBranchFix extends PsiUpdateModCommandQuickFix {
    @Override
    public @Nls(capitalization = Nls.Capitalization.Sentence) @NotNull String getFamilyName() {
      return JavaBundle.message("inspection.duplicate.branches.in.switch.merge.with.default.fix.name");
    }

    @Override
    protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
      BranchFixContext context = new BranchFixContext();
      if (context.prepare(element, Branch::isDefault)) {
        context.moveBranchLabel();
        context.deleteRedundantComments();
        context.deleteStatements();
      }
    }
  }

  /**
   * Used to merge 'case null -> ...' with 'default -> ...' to get as a result 'case null, default -> ...'
   */
  private static class MergeWithDefaultRuleFix extends MergeWithDefaultBranchFix {
    @Override
    protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
      RuleFixContext context = new RuleFixContext();
      if (context.prepare(element, BranchBase::isDefault)) {
        context.copyCaseValues(true);
        context.deleteRedundantComments();
        context.deleteRule();
      }
    }
  }

  private static class DeleteRedundantBranchFix extends PsiUpdateModCommandQuickFix {

    @Override
    public @Nls(capitalization = Nls.Capitalization.Sentence) @NotNull String getName() {
      return JavaBundle.message("inspection.duplicate.branches.in.switch.delete.fix.name");
    }

    @Override
    public @Nls(capitalization = Nls.Capitalization.Sentence) @NotNull String getFamilyName() {
      return JavaBundle.message("inspection.duplicate.branches.in.switch.delete.fix.family.name");
    }

    @Override
    protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
      BranchFixContext context = new BranchFixContext();
      if (context.prepare(element, BranchBase::isDefault)) {
        context.deleteBranchLabel();
        if (!context.myBranchToDelete.hasSingleNullCase()) {
          /*
           switch (obj) {
             case R():
             case null:
             case S():
               <caret>return 42; // Branch in 'switch' is a duplicate of the default branch
             case String s:
               return 0;
             default:
               return 42;
           }

           The 'case R():' and 'case S():' statements can be removed as redundant,
           because the corresponding branch is a duplicate of the default branch.
           But the 'default' case does not handle null values, so we cannot delete
           the 'case null:' and the 'return 42;' statement.

           See com.intellij.java.codeInspection.DuplicateBranchesInSwitchFixTest [DeleteRedundantBranch7.java]
          */
          context.deleteStatements();
        }
      }
    }
  }

  private static class BranchFixContext {
    private Branch myBranchToDelete;
    private Branch myBranchToMergeWith;
    private List<PsiElement> myBranchPrefixToMove;
    private PsiSwitchLabelStatement myLabelToMergeWith;
    private @Unmodifiable Set<String> myCommentsToMergeWith;
    private PsiElement myNextFromLabelToMergeWith;

    private boolean prepare(PsiElement startElement, Predicate<? super Branch> shouldMergeWith) {
      PsiSwitchBlock switchBlock = PsiTreeUtil.getParentOfType(startElement, PsiSwitchBlock.class);
      if (switchBlock == null) return false;

      List<Branch> candidateBranches = null;
      for (List<Branch> branches : collectProbablySimilarBranches(switchBlock)) {
        myBranchToDelete = ContainerUtil.find(branches, branch -> branch.myStatements[0] == startElement);
        if (myBranchToDelete != null) {
          candidateBranches = branches;
          break;
        }
      }
      if (myBranchToDelete == null || candidateBranches == null) return false;

      for (Branch branch : candidateBranches) {
        if (shouldMergeWith.test(branch) && canBeJoined(myBranchToDelete, branch) != LevelType.NO) {
          myBranchToMergeWith = branch;
          break;
        }
      }
      if (myBranchToMergeWith == null) return false;

      myBranchPrefixToMove = myBranchToDelete.getBranchPrefix();
      if (myBranchPrefixToMove.isEmpty()) return false;

      myLabelToMergeWith = PsiTreeUtil.getPrevSiblingOfType(myBranchToMergeWith.myStatements[0], PsiSwitchLabelStatement.class);
      if (myLabelToMergeWith == null) return false;

      myNextFromLabelToMergeWith = PsiTreeUtil.skipWhitespacesForward(myLabelToMergeWith);

      myCommentsToMergeWith = Collections.unmodifiableSet(ContainerUtil.newHashSet(myBranchToMergeWith.myCommentTexts));
      return true;
    }

    void moveBranchLabel() {
      PsiElement firstElementToMove = myBranchPrefixToMove.getFirst();
      PsiElement lastElementToMove = myBranchPrefixToMove.getLast();

      PsiElement moveTarget = myLabelToMergeWith;
      if (myLabelToMergeWith.isDefaultCase()) {
        PsiElement prevElement = PsiTreeUtil.skipWhitespacesAndCommentsBackward(myLabelToMergeWith);
        if (prevElement != null) moveTarget = prevElement;
      }
      if (PsiUtil.isAvailable(JavaFeature.SWITCH_EXPRESSION, moveTarget) && moveTarget instanceof PsiSwitchLabelStatement labelStatement &&
          myBranchToDelete.canCopyCaseValues(myBranchToMergeWith) && !SwitchUtils.isCaseNull(labelStatement) &&
          !SwitchUtils.isDefaultLabel(labelStatement)) {
        for (PsiElement element : myBranchPrefixToMove) {
          if (element instanceof PsiSwitchLabelStatement statement) {
            if (SwitchUtils.isCaseNull(statement) && myLabelToMergeWith.isDefaultCase()) {
              copyCaseValues(statement, myLabelToMergeWith, true);
            }
            else {
              copyCaseValues(statement, labelStatement, false);
            }
          }
        }
      }
      else {
        if (myBranchToMergeWith.isDefault()) {
          for (PsiElement current = lastElementToMove; current != firstElementToMove; current = current.getPrevSibling()) {
            if (current instanceof PsiWhiteSpace) continue;
            if (current instanceof PsiSwitchLabelStatement labelStatement && SwitchUtils.isCaseNull(labelStatement)) {
              copyCaseValues(labelStatement, myLabelToMergeWith, true);
            }
            else {
              moveTarget.getParent().addAfter(current, moveTarget);
            }
          }
        }
        else {
          moveTarget.getParent().addRangeAfter(firstElementToMove, lastElementToMove, moveTarget);
        }
      }
      firstElementToMove.getParent().deleteChildRange(firstElementToMove, lastElementToMove);
    }

    void deleteStatements() {
      CommentTracker tracker = new CommentTracker();
      PsiStatement[] statementsToDelete = myBranchToDelete.getStatementsToDelete();
      for (PsiStatement statement : statementsToDelete) {
        PsiTreeUtil.processElements(statement, child -> {
          if (isRedundantComment(myCommentsToMergeWith, child)) {
            tracker.markUnchanged(child);
          }
          return true;
        });
      }
      for (int i = 0; i < statementsToDelete.length - 1; i++) {
        tracker.delete(statementsToDelete[i]);
      }
      tracker.deleteAndRestoreComments(statementsToDelete[statementsToDelete.length - 1]);
    }

    void deleteRedundantComments() {
      List<PsiElement> redundantComments = new ArrayList<>();
      for (PsiElement element = myLabelToMergeWith.getNextSibling();
           element != null && element != myNextFromLabelToMergeWith;
           element = element.getNextSibling()) {
        PsiTreeUtil.processElements(element, child -> {
          if (isRedundantComment(myCommentsToMergeWith, child)) {
            redundantComments.add(child);
          }
          return true;
        });
      }
      redundantComments.forEach(PsiElement::delete);
    }

    void deleteBranchLabel() {
      List<PsiElement> toDelete = new ArrayList<>();
      CommentTracker tracker = new CommentTracker();

      for (PsiElement element : myBranchPrefixToMove) {
        if (element instanceof PsiWhiteSpace) {
          continue;
        }
        toDelete.add(element);
        PsiTreeUtil.processElements(element, child -> {
          if (isRedundantComment(myCommentsToMergeWith, child)) {
            tracker.markUnchanged(child);
          }
          return true;
        });
      }
      int size = toDelete.size();
      if (size != 0) {
        for (int i = 0; i < size - 1; i++) {
          deleteKeepingCaseNull(toDelete.get(i), tracker, false);
        }
        deleteKeepingCaseNull(toDelete.get(size - 1), tracker, true);
      }
    }

    /**
     * Deletes the given PsiElement if it's not 'case null:'. The 'default' label does not handle null values,
     * so 'case null' is not redundant and cannot be removed.
     * <p>
     * We cannot get labels like 'case String s, null' or 'case "hello", null' here.
     *
     * @param element             element to delete
     * @param ct                  CommentTracker to use
     * @param needRestoreComments if {@code true} {@link CommentTracker#deleteAndRestoreComments(PsiElement)} is called,
     *                            {@link CommentTracker#delete(PsiElement)} otherwise
     */
    private static void deleteKeepingCaseNull(@NotNull PsiElement element, @NotNull CommentTracker ct, boolean needRestoreComments) {
      if (!(element instanceof PsiSwitchLabelStatement labelStatement) || !SwitchUtils.isCaseNull(labelStatement)) {
        if (needRestoreComments) {
          ct.delete(element);
        }
        else {
          new CommentTracker().deleteAndRestoreComments(element);
        }
      }
    }

    private static @Nullable PsiCaseLabelElementList getCaseLabelElementList(@NotNull PsiSwitchLabelStatement label) {
      if (label.isDefaultCase()) {
        PsiElementFactory factory = PsiElementFactory.getInstance(label.getProject());
        PsiSwitchLabelStatement labelStatement = (PsiSwitchLabelStatement)factory.createStatementFromText("case default:", null);
        PsiSwitchLabelStatement newLabelStatement = (PsiSwitchLabelStatement)label.replace(labelStatement);
        return newLabelStatement.getCaseLabelElementList();
      }
      return label.getCaseLabelElementList();
    }

    private static void copyCaseValues(@NotNull PsiSwitchLabelStatement from,
                                       @NotNull PsiSwitchLabelStatement to,
                                       boolean mergeWithDefault) {
      @Nullable PsiCaseLabelElementList fromCaseLabelElementList = getCaseLabelElementList(from);
      @Nullable PsiCaseLabelElementList toCaseLabelElementList = getCaseLabelElementList(to);
      DuplicateBranchesInSwitchInspection.copyCaseValues(fromCaseLabelElementList, toCaseLabelElementList, mergeWithDefault);
    }
  }

  private abstract static class BranchBase<T extends PsiSwitchLabelStatementBase> {
    protected final @NotNull T myFirstLabel;
    protected final PsiStatement @NotNull [] myStatements;
    protected final String @NotNull [] myCommentTexts;
    private final boolean myIsDefault;
    private DuplicatesFinder myFinder;
    protected final boolean myCanDeleteRedundantBranch;
    /**
     * Represents the context in which this branch is defined.
     * It can contain any element inside the current branch.
     */
    private final PsiElement myContext;

    BranchBase(@NotNull PsiElement context, T @NotNull [] labels, PsiStatement @NotNull [] statements, String @NotNull [] commentTexts) {
      LOG.assertTrue(labels.length != 0, "labels.length");
      LOG.assertTrue(statements.length != 0, "statements.length");
      myContext = context;
      myFirstLabel = labels[0];
      myStatements = statements;
      myCommentTexts = commentTexts;
      myIsDefault = ContainerUtil.exists(labels, label -> label.isDefaultCase() || SwitchUtils.isCaseNullDefault(label));
      myCanDeleteRedundantBranch = labels.length > 1 || !ContainerUtil.exists(labels, BranchBase::hasNullCase);
    }

    private static boolean hasNullCase(@NotNull PsiSwitchLabelStatementBase label) {
      PsiCaseLabelElementList labelElementList = label.getCaseLabelElementList();
      if (labelElementList == null) return false;
      PsiCaseLabelElement[] elements = labelElementList.getElements();
      return ContainerUtil.exists(elements, el -> el instanceof PsiExpression expr && ExpressionUtils.isNullLiteral(expr));
    }

    boolean isDefault() {
      return myIsDefault;
    }

    @Nullable
    String getSwitchLabelText() {
      return getSwitchLabelText(myFirstLabel);
    }

    abstract boolean isSimpleExit();

    abstract boolean canFallThrough();

    abstract boolean canMergeBranch();

    abstract PsiSwitchLabelStatementBase[] getLabels();

    private boolean canBeJoinedWith(@NotNull BranchBase<?> other) {
      if (isDefault() && !hasDominanceAfter(other, this)) return true;
      if (other.isDefault() && !hasDominanceAfter(this, other)) return true;

      //fast exist
      if (!this.canMergeBranch()) return false;
      if (!other.canMergeBranch()) return false;
      boolean thisPattern = hasPattern(getLabels());
      boolean otherPattern = hasPattern(other.getLabels());
      if (otherPattern != thisPattern) return false;
      //usual switch, it doesn't have guards and dominance
      if (!thisPattern) return true;


      if (this instanceof Rule) {
        PsiExpression thisGuard = myFirstLabel.getGuardExpression();
        PsiExpression otherGuard = other.myFirstLabel.getGuardExpression();
        //it's impossible to merge if one case has guard, but another doesn't
        if ((thisGuard == null && otherGuard != null) ||
            (thisGuard != null && otherGuard == null)) {
          return false;
        }

        if (thisGuard != null) {
          //IDEA always moves to upper position, so it cannot be dominated
          return createFinder(new PsiElement[]{thisGuard}).isDuplicate(otherGuard, true) != null;
        }
      }

      if (this instanceof Branch) {
        PsiExpression thisGuard = myFirstLabel.getGuardExpression();
        PsiExpression otherGuard = other.myFirstLabel.getGuardExpression();
        if (thisGuard != null && otherGuard != null) {
          //IDEA always moves to upper position, so it cannot be dominated
          return true;
        }
      }

      return isNearby(other);
    }

    /**
     * @return true if the given BranchBase is right next or previous from current
     * It is a simplification, not to check dominance of intermediate labels every time
     */
    boolean isNearby(@NotNull BranchBase<?> other) {
      PsiSwitchLabelStatementBase[] thisLabels = getLabels();
      PsiSwitchLabelStatementBase[] otherLabels = other.getLabels();
      IElementType baseElementType = other instanceof Rule ? JavaElementType.SWITCH_LABELED_RULE : JavaElementType.SWITCH_LABEL_STATEMENT;
      return PsiTreeUtil.findSiblingBackward(thisLabels[0], baseElementType, true, null) == otherLabels[otherLabels.length - 1] ||
             PsiTreeUtil.findSiblingForward(thisLabels[thisLabels.length - 1], baseElementType, true, null) == otherLabels[0];
    }

    private static boolean hasDominanceAfter(@NotNull BranchBase<?> branch,
                                             @NotNull BranchBase<?> other) {
      PsiElement context = branch.myContext;
      PsiSwitchLabelStatementBase overWhom = PsiTreeUtil.getParentOfType(branch.myContext, PsiSwitchLabelStatementBase.class, false);
      if (overWhom == null) return false;
      PsiCaseLabelElementList whomCaseLabelElementList = overWhom.getCaseLabelElementList();
      if (whomCaseLabelElementList == null) return false;
      PsiSwitchLabelStatementBase until = PsiTreeUtil.getParentOfType(other.myContext, PsiSwitchLabelStatementBase.class, false);
      PsiSwitchBlock switchBlock = PsiTreeUtil.getParentOfType(context, PsiSwitchBlock.class, false);
      if (switchBlock == null) return false;
      boolean start = false;
      PsiExpression switchBlockExpression = switchBlock.getExpression();
      if (switchBlockExpression == null) return false;
      PsiType expressionType = switchBlockExpression.getType();
      if (expressionType == null) return false;
      for (PsiElement switchBranch : JavaPsiSwitchUtil.getSwitchBranches(switchBlock)) {
        if (!start) {
          if (PsiTreeUtil.isAncestor(context, switchBranch, false)) {
            start = true;
          }
          continue;
        }
        if (PsiTreeUtil.isAncestor(until, switchBranch, false)) {
          return false;
        }
        if (ContainerUtil.or(whomCaseLabelElementList.getElements(),
                             element ->
                               JavaPsiSwitchUtil.isDominated(element, switchBranch, expressionType))) {
          return true;
        }
      }
      return false;
    }

    int effectiveLength() {
      return myStatements.length == 1 && myStatements[0] instanceof PsiBlockStatement blockStatement 
             ? blockStatement.getCodeBlock().getStatementCount() 
             : myStatements.length;
    }

    int hash() {
      return hashStatements(myStatements);
    }

    abstract @Nullable LocalQuickFix mergeCasesFix(BranchBase<?> otherBranch);

    abstract @Nullable LocalQuickFix deleteCaseFix();

    abstract @Nullable LocalQuickFix mergeWithDefaultFix(BranchBase<?> defaultBranch);

    @Nullable
    Match match(BranchBase<?> other) {
      return getFinder().isDuplicate(other.myStatements[0], true);
    }

    private @NotNull DuplicatesFinder getFinder() {
      if (myFinder == null) {
        myFinder = createFinder(myStatements);
      }
      return myFinder;
    }

    @InspectionMessage String getCaseBranchMessage() {
      return JavaBundle.message("inspection.duplicate.branches.in.switch.message");
    }

    @InspectionMessage String getDefaultBranchMessage() {
      return JavaBundle.message("inspection.duplicate.branches.in.switch.default.message");
    }

    @Override
    public String toString() {
      return StringUtil.notNullize(getSwitchLabelText());
    }

    static @Nullable String getSwitchLabelText(@Nullable PsiSwitchLabelStatementBase switchLabel) {
      if (switchLabel != null) {
        if (switchLabel.isDefaultCase()) {
          return JavaKeywords.DEFAULT;
        }
        PsiCaseLabelElementList labelElementList = switchLabel.getCaseLabelElementList();
        if (labelElementList != null) {
