// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.java.decompiler.modules.decompiler;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.java.decompiler.code.CodeConstants;
import org.jetbrains.java.decompiler.main.DecompilerContext;
import org.jetbrains.java.decompiler.main.collectors.CounterContainer;
import org.jetbrains.java.decompiler.modules.decompiler.StatEdge.EdgeType;
import org.jetbrains.java.decompiler.modules.decompiler.exps.*;
import org.jetbrains.java.decompiler.modules.decompiler.stats.*;
import org.jetbrains.java.decompiler.modules.decompiler.vars.VarProcessor;
import org.jetbrains.java.decompiler.struct.consts.PooledConstant;
import org.jetbrains.java.decompiler.struct.consts.PrimitiveConstant;
import org.jetbrains.java.decompiler.struct.gen.VarType;
import org.jetbrains.java.decompiler.util.VBStyleCollection;

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static org.jetbrains.java.decompiler.ClassNameConstants.JAVA_LANG_OBJECT;
import static org.jetbrains.java.decompiler.ClassNameConstants.JAVA_UTIL_OBJECTS;
import static org.jetbrains.java.decompiler.modules.decompiler.PatternHelper.*;
import static org.jetbrains.java.decompiler.modules.decompiler.SwitchHelper.SwitchOnCandidate;
import static org.jetbrains.java.decompiler.modules.decompiler.SwitchHelper.TempVarAssignmentItem;
import static org.jetbrains.java.decompiler.modules.decompiler.exps.ExitExprent.EXIT_THROW;
import static org.jetbrains.java.decompiler.modules.decompiler.stats.IfStatement.IFTYPE_IF;
import static org.jetbrains.java.decompiler.struct.gen.VarType.*;

/**
 * The SwitchPatternHelper class provides utility methods to work with switch statement patterns.
 */
@SuppressWarnings("SSBasedInspection")
public final class SwitchPatternHelper {

  public static boolean isBootstrapSwitch(@NotNull Exprent headExprent) {
    if (!(headExprent instanceof SwitchExprent switchExprent)) {
      return false;
    }
    Exprent switchSelector = switchExprent.getValue();
    if (switchSelector instanceof InvocationExprent invocationExprent) {
      if (invocationExprent.isDynamicCall("typeSwitch", 2)) return true;
      if (invocationExprent.isDynamicCall("enumSwitch", 2)) return true;
    }
    return false;
  }

  /**
   * A class that implements the SwitchHelper.SwitchRecognizer interface and provides a method to recognize a switch statement.
   *
   * @see JavacReferenceFinder for details
   */
  static class JavacReferenceRecognizer implements SwitchHelper.SwitchRecognizer {

    @Override
    public @Nullable SwitchOnCandidate recognize(@NotNull SwitchStatement statement, @NotNull InvocationExprent switchSelector) {
      return new JavacReferenceFinder(null, new HashSet<>(), statement, switchSelector).findCandidate();
    }
  }

  /**
   * The JavacReferenceFinder class is responsible for finding reference candidates for a switch statement with patterns in bytecode.
   * This finder tries to find only javac familiar structures for patterns.
   * For deconstructions, only common structures are trying to be found; some cases are left
   */
  private static class JavacReferenceFinder {

    /**
     * The {@code FullCase} class represents a case in a switch statement. It contains the statement,
     * the list of expressions (exprents), and the list of statement edges related to the case.
     *
     * @see SwitchStatement
     */
    private record FullCase(@NotNull Statement statement, @NotNull List<Exprent> exprents, @NotNull List<StatEdge> edges) {
    }

    /**
     * Represents a case value with an associated edge from a switch statement
     *
     * @param exprent The expression value of the case.
     * @param edge    The edge associated with the case.
     * @see SwitchStatement
     */
    private record CaseValueWithEdge(@Nullable Exprent exprent, @Nullable StatEdge edge) {
    }


    /**
     * Represents the root DoStatement and exprents, which contain initializers.
     *
     * @param firstStatement    The firstStatement statement.
     * @param firstExprents     The list of expression statements.
     * @param doParentStatement The parent DoStatement object.
     */
    private record Root(Statement firstStatement, List<Exprent> firstExprents, DoStatement doParentStatement) {
    }

    /**
     * not null for nested switch
     */
    private final @Nullable VarTracker myVarTracker;

    /**
     * Represents a set of type variables, which is used as second parameter in bootstrap call.
     */
    private final @NotNull Set<Exprent> myTypeVars;
    private final @NotNull SwitchStatement myRootSwitchStatement;
    private final @NotNull InvocationExprent mySwitchSelector;

    private final @NotNull List<TempVarAssignmentItem> myTempVarAssignments = new ArrayList<>();

    private final @NotNull Set<SwitchStatement> myUsedSwitch = new HashSet<>();

    private JavacReferenceFinder(@Nullable VarTracker tracker,
                                 @NotNull Set<Exprent> typeVars,
                                 @NotNull SwitchStatement rootSwitchStatement,
                                 @NotNull InvocationExprent switchSelector) {
      this.myVarTracker = tracker;
      this.myTypeVars = new HashSet<>(typeVars);
      this.myRootSwitchStatement = rootSwitchStatement;
      this.mySwitchSelector = switchSelector;
      myUsedSwitch.add(myRootSwitchStatement);
    }

    public @Nullable SwitchOnReferenceCandidate findCandidate() {
      Exprent instance = mySwitchSelector.getInstance();
      if (instance == null) {
        return null;
      }
      if (myRootSwitchStatement.getHeadExprent() == null) {
        return null;
      }

      if (myRootSwitchStatement.getCaseValues().size() != myRootSwitchStatement.getCaseEdges().size() ||
          myRootSwitchStatement.getCaseValues().size() != myRootSwitchStatement.getCaseStatements().size()) {
        return null;
      }

      if (!(instance instanceof VarExprent instanceVarExprent)) return null;
      if (!isBootstrapSwitch(myRootSwitchStatement.getHeadExprent())) return null;
      if (checkBootstrap()) return null;

      List<Exprent> parameters = mySwitchSelector.getParameters();
      if (!instance.equals(parameters.get(0))) {
        return null;
      }
      Exprent typeVar = parameters.get(1);
      Root root = getRoot();
      if (root == null) return null;

      if (root.firstExprents() == null) {
        return null;
      }

      if (myVarTracker != null) {
        processAtLeastOneBlock(myVarTracker, root.firstStatement());
      }

      Initializer initializer = findInitializer(root, typeVar);

      if (initializer.initVar2() == null) {
        return null;
      }

      Set<AssignmentExprent> usedTypeVarAssignments = new HashSet<>();
      usedTypeVarAssignments.add(initializer.initVar2());
      myTypeVars.add(typeVar);

      Map<Statement, List<PatternVariableCandidate>> candidates =
        collectPatterns(instanceVarExprent, root.doParentStatement(), usedTypeVarAssignments);
      if (candidates == null) {
        return null;
      }
      candidates.values()
        .stream()
        .flatMap(t -> t.stream())
        .forEach(candidate -> myTempVarAssignments.addAll(candidate.getTempAssignments()));
      PatternContainer patternContainer = collectGuards(candidates, typeVar, root.doParentStatement(), usedTypeVarAssignments);
      if (root.doParentStatement() != null) {
        if (checkReinitVar(typeVar, root.doParentStatement(), usedTypeVarAssignments)) {
          return null;
        }
      }
      else {
        if (checkReinitVar(typeVar, myRootSwitchStatement, usedTypeVarAssignments)) {
          return null;
        }
      }


      List<FullCase> resortedCases = resortForSwitchBootstrap(myRootSwitchStatement);
      if (resortedCases == null) {
        return null;
      }

      List<Exprent> finalFirstExprents = root.firstExprents();
      Exprent finalNonNullCheck = initializer.nonNullCheck();

      return new SwitchOnReferenceCandidate(myRootSwitchStatement, mySwitchSelector, initializer.instance(), resortedCases,
                                            patternContainer,
                                            myTempVarAssignments, myUsedSwitch,
                                            usedTypeVarAssignments, root.doParentStatement(),
                                            initializer.nonNullCheck() != null, () -> {
        if (finalNonNullCheck != null) {
          finalFirstExprents.remove(finalNonNullCheck);
        }
      });
    }

    private @NotNull SwitchPatternHelper.JavacReferenceFinder.Initializer findInitializer(@NotNull Root root, Exprent typeVar) {
      Exprent instance = Objects.requireNonNull(mySwitchSelector.getInstance());

      Exprent nonNullCheck = null;
      AssignmentExprent initVar2 = null;

      for (Exprent exprent : root.firstExprents()) {
        if (exprent instanceof AssignmentExprent firstAssignment && instance.equals(firstAssignment.getLeft())) {
          instance = firstAssignment.getRight();
          if (firstAssignment.getLeft() instanceof VarExprent varExprent) {
            myTempVarAssignments.add(new TempVarAssignmentItem(varExprent, root.firstStatement()));
          }
        }
      }
      for (Exprent exprent : root.firstExprents()) {
        if (isNonNullCheck(exprent, instance)) {
          nonNullCheck = exprent;
        }
        if (exprent instanceof AssignmentExprent firstAssignment && typeVar.equals(firstAssignment.getLeft()) &&
            firstAssignment.getRight() instanceof ConstExprent constExprent &&
            constExprent.getValue() instanceof Integer value &&
            value == 0) {
          initVar2 = firstAssignment;

          if (firstAssignment.getLeft() instanceof VarExprent varExprent) {
            myTempVarAssignments.add(new TempVarAssignmentItem(varExprent, root.firstStatement()));
          }
        }
      }
      return new Initializer(instance, nonNullCheck, initVar2);
    }

    /**
     * Represents an initializer for a switch statement.
     *
     * @param instance     The qualifier which is used to call bootstrap methods.
     * @param nonNullCheck non-null-check for instance.
     * @param initVar2     second argument, which represent a type.
     */
    private record Initializer(Exprent instance, Exprent nonNullCheck, AssignmentExprent initVar2) {
    }

    /**
     * This method matches the structure from myRootSwitchStatement with common patterns to find root DoStatement.
     * DoStatement is used for pattern matching to repeat searching if a previous assumption is not correct.
     *
     * @return The `Root` object representing the root `DoStatement` and expression statements, or null if not found.
     */
    private @Nullable Root getRoot() {
      Statement first = myRootSwitchStatement.getFirst();
      if (first == null) {
        return null;
      }
      List<Exprent> firstExprents = first.getExprents();

      DoStatement doParentStatement = null;

      //possible structure (only for nested switch):
      //SequenceStatement
      //  ....
      //  Basic
      //  DoStatement
      //    DoStatement
      //      Switch
      if (myVarTracker != null && myRootSwitchStatement.getParent() instanceof DoStatement nestedDoStatement &&
          nestedDoStatement.getConditionExprent() == null &&
          nestedDoStatement.getParent() instanceof DoStatement upperDoStatement &&
          upperDoStatement.getConditionExprent() == null &&
          upperDoStatement.getParent() instanceof SequenceStatement sequenceStatement &&
          sequenceStatement.getStats().size() >= 2 && sequenceStatement.getStats().getLast() == upperDoStatement &&
          sequenceStatement.getStats().get(sequenceStatement.getStats().size() - 2) instanceof BasicBlockStatement basicBlockStatement) {
        firstExprents = basicBlockStatement.getExprents();
        first = basicBlockStatement;
        doParentStatement = nestedDoStatement;
      }
      //possible structure (only for nested switch):
      //SequenceStatement
      //  Switch
      //    default
      //    case
      //  DoStatement
      //    DoStatement
      //      Switch
      else if (myVarTracker != null && myRootSwitchStatement.getParent() instanceof DoStatement nestedDoStatement &&
               nestedDoStatement.getConditionExprent() == null &&
               nestedDoStatement.getParent() instanceof DoStatement upperDoStatement &&
               upperDoStatement.getConditionExprent() == null &&
               upperDoStatement.getParent() instanceof SequenceStatement sequenceStatement &&
               sequenceStatement.getStats().size() == 2 && sequenceStatement.getStats().get(1) == upperDoStatement &&
               sequenceStatement.getStats().get(0) instanceof SwitchStatement upperSwitchStatement &&
               upperSwitchStatement.getCaseStatements().size() == 2) {
        int indexDefault = upperSwitchStatement.getCaseEdges().get(0).contains(upperSwitchStatement.getDefaultEdge()) ? 0 :
                           (upperSwitchStatement.getCaseEdges().get(1).contains(upperSwitchStatement.getDefaultEdge()) ? 1 : -1);
        if (indexDefault == -1) {
          return null;
        }
        int other = indexDefault == 0 ? 1 : 0;
        Statement statement = upperSwitchStatement.getCaseStatements().get(other);
        if (statement.getStats().isEmpty()) {
          return null;
        }

        Statement last = statement.getStats().getLast();
        firstExprents = last.getExprents();
        first = last;
        doParentStatement = nestedDoStatement;
      }

      //possible structure (main case):
      //SequenceStatement
      //  ....
      //  BasicBlockStatement
      //    exprents with init values
      //  DoStatement
      //    SwitchStatement
      //  ....
      else if (firstExprents == null || firstExprents.stream().noneMatch(t -> t instanceof AssignmentExprent)) {
        Statement parent = myRootSwitchStatement.getParent();
        DoStatement doStatement = null;
        if (parent instanceof DoStatement) {
          doStatement = (DoStatement)parent;
        }
        if (doStatement == null && myRootSwitchStatement.getParent().getParent() instanceof DoStatement nextStatement) {
          doStatement = nextStatement;
        }
        if (doStatement == null ||
            doStatement.getLoopType() != DoStatement.LoopType.DO ||
            doStatement.getStats().size() != 1 ||
            !(doStatement.getParent() instanceof SequenceStatement upperStatement) ||
            upperStatement.getExprents() != null) {
          return null;
        }
        VBStyleCollection<Statement, Integer> upperStatementStats = upperStatement.getStats();
        int indexOfDo = upperStatementStats.indexOf(doStatement);
        if (indexOfDo == -1 || indexOfDo == 0) {
          return null;
        }
        if (upperStatementStats.get(indexOfDo) == doStatement &&
            upperStatementStats.get(indexOfDo - 1) instanceof BasicBlockStatement basicBlockStatement &&
            basicBlockStatement.getStats().isEmpty() &&
            basicBlockStatement.getExprents() != null &&
            !basicBlockStatement.getExprents().isEmpty()) {
          firstExprents = basicBlockStatement.getExprents();
          doParentStatement = doStatement;
          first = basicBlockStatement;
        }
        //if previous is CatchStatement and initializers are inside CatchStatement
        else if (upperStatementStats.get(indexOfDo) == doStatement &&
                 upperStatementStats.get(indexOfDo - 1) instanceof CatchStatement catchStatement &&
                 catchStatement.getStats().size() >= 2 &&
                 catchStatement.getStats().get(1).getExprents() != null &&
                 !Objects.requireNonNull(catchStatement.getStats().get(1).getExprents()).isEmpty()) {
          firstExprents = catchStatement.getStats().get(1).getExprents();
          doParentStatement = doStatement;
          first = catchStatement.getStats().get(1);
        }
        else {
          return null;
        }
      }

      //check that this doParentStatement is not refered outside
      if (doParentStatement != null) {
        for (StatEdge edge : doParentStatement.getLabelEdges()) {
          if (edge.labeled && edge.explicit) {
            Statement source = edge.getSource();
            if (myRootSwitchStatement.containsStatement(source)) {
              continue;
            }
            Statement parent = myRootSwitchStatement.getParent();
            if (!(parent instanceof SequenceStatement sequenceStatement && sequenceStatement.getStats().size() == 2)) {
              return null;
            }
            int index = sequenceStatement.getStats().indexOf(myRootSwitchStatement);
            if (index != 0) {
              return null;
            }
            if (!(sequenceStatement.getStats().get(1) instanceof DoStatement firstDo &&
                  firstDo.getStats().size() == 1 && firstDo.getStats().get(0) instanceof DoStatement nestedDo)) {
              return null;
            }
            if (nestedDo.containsStatement(source)) {
              continue;
            }
            return null;
          }
        }
      }
      return new Root(first, firstExprents, doParentStatement);
    }

    /**
     * @return true if the bootstrap arguments of the switch selector can be processed (they represent Integer, String or Class),
     * false otherwise.
     */
    private boolean checkBootstrap() {
      List<PooledConstant> bootstrapArguments = mySwitchSelector.getBootstrapArguments();
      if (bootstrapArguments == null) {
        return true;
      }
      for (PooledConstant bootstrapArgument : bootstrapArguments) {
        if (!(bootstrapArgument instanceof PrimitiveConstant primitiveConstant)) {
          return true;
        }
        int type = primitiveConstant.type;
        if (!(type == CodeConstants.CONSTANT_Integer || type == CodeConstants.CONSTANT_String || type == CodeConstants.CONSTANT_Class)) {
          return true;
        }
      }
      return false;
    }

    private static @Nullable List<FullCase> resortForSwitchBootstrap(@NotNull SwitchStatement statement) {
      for (Statement caseStatement : statement.getCaseStatements()) {
        if (caseStatement == null) {
          return null;
        }
      }
      @NotNull List<List<@Nullable Exprent>> values = statement.getCaseValues();
      List<List<Exprent>> sortedCaseValue = new ArrayList<>();
      List<List<StatEdge>> sortedEdges = new ArrayList<>();
      for (int i = 0; i < values.size(); i++) {
        List<Exprent> caseValue = statement.getCaseValues().get(i);
        for (Exprent exprent : caseValue) {
          if (exprent == null) {
            continue;
          }
          if (!(exprent instanceof ConstExprent constCaseValue)) {
            return null;
          }
          if (!(constCaseValue.getConstType() == VARTYPE_INT ||
                constCaseValue.getConstType() == VARTYPE_BYTECHAR ||
                constCaseValue.getConstType() == VARTYPE_CHAR ||
                constCaseValue.getConstType() == VARTYPE_BYTE)) {
            return null;
          }
        }
        List<StatEdge> edges = statement.getCaseEdges().get(i);
        if (edges.size() != caseValue.size()) {
          return null;
        }
        List<CaseValueWithEdge> sorted =
          IntStream.range(0, edges.size()).mapToObj(ind -> new CaseValueWithEdge(caseValue.get(ind), edges.get(ind)))
            .sorted(
              Comparator.<CaseValueWithEdge, Boolean>comparing(o -> o.edge == statement.getDefaultEdge())
                .thenComparingLong(o -> o.exprent instanceof ConstExprent c ? c.getIntValue() : Long.MIN_VALUE))
            .toList();
        sortedEdges.add(sorted.stream().map(t -> t.edge).collect(Collectors.toList()));
        sortedCaseValue.add(sorted.stream().map(t -> t.exprent).collect(Collectors.toList()));
      }

      List<FullCase> sortedAll = IntStream.range(0, statement.getCaseValues().size())
        .mapToObj(ind -> new FullCase(statement.getCaseStatements().get(ind),
                                      sortedCaseValue.get(ind),
                                      sortedEdges.get(ind)))
        .sorted(Comparator.<FullCase, Boolean>comparing(
            fullCase -> !fullCase.edges().isEmpty() && fullCase.edges().get(fullCase.exprents.size() - 1) == statement.getDefaultEdge())
                  .thenComparingLong(o -> !o.exprents.isEmpty() && o.exprents.get(o.exprents.size() - 1) instanceof ConstExprent c
                                          ? c.getIntValue()
                                          : Long.MIN_VALUE))
        .collect(Collectors.toList());

      for (int i = 0; i < sortedAll.size(); i++) {
        if (i == sortedAll.size() - 1) {
          break;
        }
        FullCase current = sortedAll.get(i);
        FullCase next = sortedAll.get(i + 1);
        if (current.exprents.isEmpty() || next.exprents.isEmpty()) {
          return null;
        }

        Exprent currentFirstExpr = current.exprents.get(0);
        Exprent nextFirstExpr = next.exprents.get(0);
        if (currentFirstExpr instanceof ConstExprent constExprent1 && nextFirstExpr instanceof ConstExprent constExprent2) {
          if (constExprent1.getIntValue() > constExprent2.getIntValue() &&
              constExprent1.getIntValue() != -1 &&
              constExprent2.getIntValue() != -1) {
            if (statement.getDefaultEdge() != next.edges.get(next.edges.size() - 1)) {
              return null;
            }
          }
        }
        List<StatEdge> edges = current.statement.getSuccessorEdges(EdgeType.DIRECT_ALL);
        if (edges.size() == 1) {
          StatEdge currentEdge = edges.get(0);
          if (currentEdge.getType() == EdgeType.REGULAR) {
            Statement destination = currentEdge.getDestination();
            if (destination != next.statement()) {
              return null;
            }
          }
        }
      }
      return sortedAll;
    }

    private static @NotNull VarExprent createDefaultPatternVal(@NotNull String className) {
      VarProcessor processor = DecompilerContext.getVarProcessor();
      VarExprent varExprent = new VarExprent(DecompilerContext.getCounterContainer().getCounterAndIncrement(CounterContainer.VAR_COUNTER),
                                             new VarType(CodeConstants.TYPE_OBJECT, 0, className),
                                             processor);
      varExprent.setDefinition(true);
      processor.setVarName(varExprent.getVarVersion(), VarExprent.getName(varExprent.getVarVersion()));
      return varExprent;
    }

    /**
     * @return A map of case statements from switch and their pattern variable candidates, or null if the collection fails.
     */
    private @Nullable Map<Statement, List<PatternVariableCandidate>> collectPatterns(@NotNull VarExprent instance,
                                                                           @Nullable DoStatement doStatement,
                                                                           @NotNull Set<AssignmentExprent> assignmentExprents) {

      Map<Statement, List<PatternVariableCandidate>> candidates = new HashMap<>();
      List<PooledConstant> bootstrapArguments = mySwitchSelector.getBootstrapArguments();
      Map<Integer, String> mapCaseClasses = getMapCaseClasses(bootstrapArguments);
      @NotNull List<List<@Nullable Exprent>> values = myRootSwitchStatement.getCaseValues();
      for (int caseIndex = 0; caseIndex < values.size(); caseIndex++) {
        List<Exprent> caseValues = values.get(caseIndex);
        for (int valueIndex = 0; valueIndex < caseValues.size(); valueIndex++) {
          Exprent caseValue = caseValues.get(valueIndex);
          if (!(caseValue instanceof ConstExprent constCaseValue)) {
            continue;
          }
          int expectedValue = constCaseValue.getIntValue();
          String className = mapCaseClasses.get(expectedValue);
          //if it doesn't have className, it is not a pattern (enum or literal), and can be processed during transformation
          if (className == null) {
            continue;
          }

          Statement caseStatement = myRootSwitchStatement.getCaseStatements().get(caseIndex);

          VarTracker currentVarTracker = myVarTracker;
          if (currentVarTracker == null) {
            RecordVarExprent recordVarExprent = new RecordVarExprent(instance);
            currentVarTracker = new VarTracker(recordVarExprent);
            currentVarTracker.put(instance, recordVarExprent, myRootSwitchStatement);
          }

          ConstExprent checkType = new ConstExprent(new VarType(className), null, null);
          currentVarTracker = currentVarTracker.copy();
          if (currentVarTracker == null) {
            continue;
          }

          PatternVariableCandidate candidate = null;

          //example:
          // some blocks
          // var2 = 2 // here var2 is second initializer
          // break
          //'break' breaks graph for inner statements, that's why it is needed to be excluded temporary
          if (caseStatement instanceof SequenceStatement sequenceStatement && !sequenceStatement.getStats().isEmpty()) {
            VBStyleCollection<Statement, Integer> sequenceStatementStats = sequenceStatement.getStats();
            Statement last = sequenceStatementStats.get(sequenceStatementStats.size() - 1);
            if (last instanceof BasicBlockStatement && last.getExprents() != null && last.getExprents().size() == 1) {
              Exprent exprent = last.getExprents().get(0);
              if (exprent instanceof AssignmentExprent assignmentExprent &&
                  assignmentExprent.getLeft() instanceof VarExprent left && myTypeVars.contains(left)) {
                //new sequence without break
                SequenceStatement newSequence = new SequenceStatement(sequenceStatementStats.subList(0, sequenceStatementStats.size() - 1));
                Map<Statement, Statement> previousParents = new HashMap<>();
                for (Statement currentStat : newSequence.getStats()) {
                  previousParents.put(currentStat, currentStat.getParent());
                  currentStat.setParent(newSequence);
                }
                newSequence.setParent(sequenceStatement.getParent());
                candidate = findNextPatternVarCandidate(newSequence, instance, checkType, currentVarTracker, newSequence);
                //'last' is not added here, because it must be processed in guards

                //return 'break' back
                for (Statement currentStat : newSequence.getStats()) {
                  Statement currentParent = previousParents.get(currentStat);
                  if (currentParent != null) {
                    currentStat.setParent(currentParent);
                  }
                }

                //candidate.nextStatement has broken edge here, it refers to 'last' (break)
                //it is expected if it has a guard
                if (candidate != null) {
                  candidate =
                    normalizeCandidateWithBrokenEdges(candidate, newSequence, sequenceStatement, last, doStatement, assignmentExprents,
                                                      assignmentExprent);
                  //it is impossible to normalize
                  if (candidate == null) {
                    return null;
                  }
                }
              }
            }
          }

          if (candidate == null) {
            candidate = findNextPatternVarCandidate(caseStatement, instance, checkType, currentVarTracker, caseStatement);
          }

          if (candidate != null) {
            List<PatternVariableCandidate> nestedSwitches =
              tryToFindNestedSwitch(candidate, caseStatement, assignmentExprents, currentVarTracker);
            if (nestedSwitches == null || nestedSwitches.isEmpty()) {
              //something broken
              return null;
            }
            candidates.put(caseStatement, nestedSwitches);
          }
          //there are no patterns for JAVA_LANG_OBJECT, add them as is
          else if (JAVA_LANG_OBJECT.equals(className)) {
            List<PatternVariableCandidate> value = new ArrayList<>();
            value.add(
              new PatternVariableCandidate(createDefaultPatternVal(className), caseStatement, new HashSet<>(), new ArrayList<>(), () -> {
              }));
            candidates.put(caseStatement, value);
          }
        }
        //collect from default case
        if (myRootSwitchStatement.getCaseEdges().get(caseIndex).contains(myRootSwitchStatement.getDefaultEdge())) {
          Statement defaultStatement = myRootSwitchStatement.getCaseStatements().get(caseIndex);
          if (defaultStatement instanceof BasicBlockStatement && defaultStatement.getExprents() != null &&
              defaultStatement.getExprents().size() == 1 &&
              defaultStatement.getExprents().get(0) instanceof AssignmentExprent assignmentExprent &&
              myTypeVars.contains(assignmentExprent.getLeft())) {
            assignmentExprents.add(assignmentExprent);
          }
        }
      }
      return candidates;
    }

    /**
     * Collects the guards and returns a PatternContainer object.
     *
     * @param candidates        a map containing the candidates for each case statement
     * @param typeVar           the type variable
     * @param doParentStatement the parent statement of the do statement
     * @param usedAssignments   a set of used assignments
     * @return a PatternContainer object containing the collected guards
     */
    private @NotNull SwitchPatternHelper.PatternContainer collectGuards(@NotNull Map<Statement, List<PatternVariableCandidate>> candidates,
                                                               @NotNull Exprent typeVar,
                                                               @Nullable Statement doParentStatement,
                                                               @NotNull Set<AssignmentExprent> usedAssignments) {
      PatternContainer container = new PatternContainer();
      @NotNull List<Statement> statements = myRootSwitchStatement.getCaseStatements();
      for (int i = 0; i < statements.size(); i++) {
        Statement previousValueStatement = statements.get(i);
        Statement caseStatement = statements.get(i);
        List<PatternVariableCandidate> variableCandidates = candidates.get(caseStatement);
        if (variableCandidates != null) {
          for (PatternVariableCandidate variableCandidate : variableCandidates) {
            container.addPattern(previousValueStatement, variableCandidate.getGuards(), variableCandidate.getNextStatement(),
                                 variableCandidate.getVarExprent());
          }
          //java 21, only one pattern can be with guard
          //todo check with unnamed patterns and variables
          if (variableCandidates.size() != 1) {
            continue;
          }
          caseStatement = variableCandidates.get(0).getNextStatement();
        }
        if (caseStatement.getStats().size() < 2) {
          continue;
        }
        if (doParentStatement == null) {
          continue;
        }
        List<@Nullable Exprent> exprents = myRootSwitchStatement.getCaseValues().get(i);
        OptionalInt maxCaseValue = exprents.stream().filter(t -> t instanceof ConstExprent)
          .mapToInt(t -> ((ConstExprent)t).getIntValue())
          .max();
        if (maxCaseValue.isEmpty()) {
          continue;
        }
        boolean guardIsNegated = false;

        if (!(caseStatement.getStats().get(0) instanceof IfStatement ifStatement &&
              ifStatement.getIfstat() != null &&
              ifStatement.getElsestat() == null &&
              ifStatement.iftype == IFTYPE_IF)) {
          continue;
        }
        AssignmentExprent assignmentExprent = null;

        if (ifStatement.getIfstat().getExprents() != null &&
            ifStatement.getIfstat().getExprents().size() == 1 &&
            ifStatement.getIfstat().getExprents().get(0) instanceof AssignmentExprent expectedAssignmentExprent &&
            expectedAssignmentExprent.getLeft() != typeVar && expectedAssignmentExprent.getRight() instanceof ConstExprent constExprent &&
            constExprent.getValue() instanceof Integer index && index > maxCaseValue.getAsInt()) {
          assignmentExprent = expectedAssignmentExprent;
