// 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.codeInsight.completion;

import com.intellij.codeInsight.*;
import com.intellij.codeInsight.completion.util.CompletionStyleUtil;
import com.intellij.codeInsight.completion.util.ParenthesesInsertHandler;
import com.intellij.codeInsight.daemon.impl.quickfix.CreateClassKind;
import com.intellij.codeInsight.lookup.*;
import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.java.codeserver.core.JavaPsiSwitchUtil;
import com.intellij.java.syntax.parser.JavaKeywords;
import com.intellij.modcompletion.ModCompletionItemProvider;
import com.intellij.openapi.editor.CaretModel;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.Conditions;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.patterns.ElementPattern;
import com.intellij.patterns.PsiElementPattern;
import com.intellij.pom.java.JavaFeature;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.filters.FilterPositionUtil;
import com.intellij.psi.javadoc.PsiDocComment;
import com.intellij.psi.templateLanguages.OuterLanguageElement;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.JavaPsiPatternUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.Consumer;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.siyeh.ig.psiutils.ExpressionUtils;
import com.siyeh.ig.psiutils.SealedUtils;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

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

import static com.intellij.codeInsight.completion.JavaCompletionContributor.IN_CASE_LABEL_ELEMENT_LIST;
import static com.intellij.openapi.util.Conditions.notInstanceOf;
import static com.intellij.patterns.PsiJavaPatterns.*;
import static com.intellij.psi.SyntaxTraverser.psiApi;

public class JavaKeywordCompletion {
  public static final ElementPattern<PsiElement> AFTER_DOT = psiElement().afterLeaf(".");

  static final ElementPattern<PsiElement> VARIABLE_AFTER_FINAL =
    psiElement().afterLeaf(JavaKeywords.FINAL).inside(PsiDeclarationStatement.class);

  private static final ElementPattern<PsiElement> INSIDE_PARAMETER_LIST =
    psiElement().withParent(
      psiElement(PsiJavaCodeReferenceElement.class).insideStarting(
        psiElement().withTreeParent(
          psiElement(PsiParameterList.class).andNot(psiElement(PsiAnnotationParameterList.class)))));
  private static final ElementPattern<PsiElement> INSIDE_RECORD_HEADER =
    psiElement().withParent(
      psiElement(PsiJavaCodeReferenceElement.class).insideStarting(
        or(
          psiElement().withTreeParent(
            psiElement(PsiRecordComponent.class)),
          psiElement().withTreeParent(
            psiElement(PsiRecordHeader.class)
          )
        )
      ));
  private static final InsertHandler<LookupElementDecorator<?>> ADJUST_LINE_OFFSET = (context, item) -> {
    item.getDelegate().handleInsert(context);
    context.commitDocument();
    CodeStyleManager.getInstance(context.getProject()).adjustLineIndent(context.getFile(), context.getStartOffset());
  };

  private static boolean isStatementCodeFragment(PsiFile file) {
    return file instanceof JavaCodeFragment &&
           !(file instanceof PsiExpressionCodeFragment ||
             file instanceof PsiJavaCodeReferenceCodeFragment ||
             file instanceof PsiTypeCodeFragment);
  }

  static boolean isEndOfBlock(@NotNull PsiElement element) {
    PsiElement prev = prevSignificantLeaf(element);
    if (prev == null) {
      PsiFile file = element.getContainingFile();
      return !(file instanceof PsiCodeFragment) || isStatementCodeFragment(file);
    }

    if (psiElement().inside(psiAnnotation()).accepts(prev)) return false;

    if (prev instanceof OuterLanguageElement) return true;
    if (psiElement().withText(string().oneOf("{", "}", ";", ":", "else")).accepts(prev)) return true;
    if (prev.textMatches(")")) {
      PsiElement parent = prev.getParent();
      if (parent instanceof PsiParameterList) {
        return PsiTreeUtil.getParentOfType(PsiTreeUtil.prevVisibleLeaf(element), PsiDocComment.class) != null;
      }

      return !(parent instanceof PsiExpressionList || parent instanceof PsiTypeCastExpression
               || parent instanceof PsiRecordHeader);
    }

    return false;
  }

  static final ElementPattern<PsiElement> START_SWITCH =
    psiElement().afterLeaf(psiElement().withText("{").withParents(PsiCodeBlock.class, PsiSwitchBlock.class));

  private static final ElementPattern<PsiElement> SUPER_OR_THIS_PATTERN =
    and(JavaSmartCompletionContributor.INSIDE_EXPRESSION,
        not(psiElement().afterLeaf(JavaKeywords.CASE)),
        not(psiElement().afterLeaf(psiElement().withText(".").afterLeaf(JavaKeywords.THIS, JavaKeywords.SUPER))),
        not(psiElement().inside(PsiAnnotation.class)),
        not(START_SWITCH),
        not(JavaMemberNameCompletionContributor.INSIDE_TYPE_PARAMS_PATTERN));

  static final PsiElementPattern<PsiElement, ?> START_FOR = psiElement().afterLeaf(psiElement().withText("(").afterLeaf("for"));
  private static final ElementPattern<PsiElement> CLASS_REFERENCE =
    psiElement().withParent(psiReferenceExpression().referencing(psiClass().andNot(psiElement(PsiTypeParameter.class))));

  private final CompletionParameters myParameters;
  private final JavaCompletionSession mySession;
  private final PsiElement myPosition;
  private final PrefixMatcher myKeywordMatcher;
  private final List<LookupElement> myResults = new ArrayList<>();
  private final PsiElement myPrevLeaf;

  JavaKeywordCompletion(CompletionParameters parameters, JavaCompletionSession session, boolean isSmart) {
    myParameters = parameters;
    mySession = session;
    myKeywordMatcher = new StartOnlyMatcher(session.getMatcher());
    myPosition = parameters.getPosition();
    myPrevLeaf = prevSignificantLeaf(myPosition);
    if (ModCompletionItemProvider.modCommandCompletionEnabled()) {
      // Replaced with ModCommand-based completion
      return;
    }
    if (!isSmart) {
      addKeywords();
    }
    addEnumCases();
    addEnhancedCases();
  }

  private static PsiElement prevSignificantLeaf(PsiElement position) {
    return FilterPositionUtil.searchNonSpaceNonCommentBack(position);
  }

  private void addKeyword(LookupElement element) {
    if (myKeywordMatcher.isStartMatch(element.getLookupString())) {
      myResults.add(element);
    }
  }

  List<LookupElement> getResults() {
    return myResults;
  }

  public static boolean isInsideParameterList(PsiElement position) {
    PsiElement prev = PsiTreeUtil.prevVisibleLeaf(position);
    PsiModifierList modifierList = PsiTreeUtil.getParentOfType(prev, PsiModifierList.class);
    if (modifierList != null) {
      if (PsiTreeUtil.isAncestor(modifierList, position, false)) {
        return false;
      }
      PsiElement parent = modifierList.getParent();
      return parent instanceof PsiParameterList || parent instanceof PsiParameter && parent.getParent() instanceof PsiParameterList;
    }
    return INSIDE_PARAMETER_LIST.accepts(position);
  }

  private static TailType getReturnTail(PsiElement position) {
    PsiElement scope = position;
    while (true) {
      if (scope instanceof PsiFile || scope instanceof PsiClassInitializer) {
        return TailTypes.noneType();
      }

      if (scope instanceof PsiMethod method) {
        if (method.isConstructor() || PsiTypes.voidType().equals(method.getReturnType())) {
          return TailTypes.semicolonType();
        }

        return TailTypes.humbleSpaceBeforeWordType();
      }
      if (scope instanceof PsiLambdaExpression lambda) {
        final PsiType returnType = LambdaUtil.getFunctionalInterfaceReturnType(lambda);
        if (PsiTypes.voidType().equals(returnType)) {
          return TailTypes.semicolonType();
        }
        return TailTypes.humbleSpaceBeforeWordType();
      }
      scope = scope.getParent();
    }
  }

  private void addStatementKeywords() {
    if (psiElement()
      .withText("}")
      .withParent(psiElement(PsiCodeBlock.class).withParent(or(psiElement(PsiTryStatement.class), psiElement(PsiCatchSection.class))))
      .accepts(myPrevLeaf)) {
      addKeyword(new OverridableSpace(createKeyword(JavaKeywords.CATCH), JavaTailTypes.CATCH_LPARENTH));
      addKeyword(new OverridableSpace(createKeyword(JavaKeywords.FINALLY), JavaTailTypes.FINALLY_LBRACE));
      List<LookupElement> elements = CatchLookupElement.create(myPrevLeaf);
      for (LookupElement element : elements) {
        addKeyword(element);
      }
      if (myPrevLeaf.getParent().getNextSibling() instanceof PsiErrorElement) {
        return;
      }
    }

    addKeyword(new OverridableSpace(createKeyword(JavaKeywords.SWITCH), JavaTailTypes.SWITCH_LPARENTH));
    addKeyword(new OverridableSpace(createKeyword(JavaKeywords.WHILE), JavaTailTypes.WHILE_LPARENTH));
    addKeyword(new OverridableSpace(createKeyword(JavaKeywords.DO), JavaTailTypes.DO_LBRACE));
    addKeyword(new OverridableSpace(createKeyword(JavaKeywords.FOR), JavaTailTypes.FOR_LPARENTH));
    addKeyword(new OverridableSpace(createKeyword(JavaKeywords.IF), JavaTailTypes.IF_LPARENTH));
    addKeyword(new OverridableSpace(createKeyword(JavaKeywords.TRY), JavaTailTypes.TRY_LBRACE));
    addKeyword(new OverridableSpace(createKeyword(JavaKeywords.THROW), TailTypes.insertSpaceType()));
    addKeyword(new OverridableSpace(createKeyword(JavaKeywords.NEW), TailTypes.insertSpaceType()));
    addKeyword(new OverridableSpace(createKeyword(JavaKeywords.SYNCHRONIZED), JavaTailTypes.SYNCHRONIZED_LPARENTH));

    if (PsiUtil.isAvailable(JavaFeature.ASSERTIONS, myPosition)) {
      addKeyword(new OverridableSpace(createKeyword(JavaKeywords.ASSERT), TailTypes.insertSpaceType()));
    }

    if (!psiElement().inside(PsiSwitchExpression.class).accepts(myPosition) ||
        psiElement().inside(PsiLambdaExpression.class).accepts(myPosition)) {
      addKeyword(createReturnKeyword());
    }

    if (psiElement().withText(";").withSuperParent(2, PsiIfStatement.class).accepts(myPrevLeaf) ||
        psiElement().withText("}").withSuperParent(3, PsiIfStatement.class).accepts(myPrevLeaf)) {
      LookupElement elseKeyword = new OverridableSpace(createKeyword(JavaKeywords.ELSE), TailTypes.humbleSpaceBeforeWordType());
      CharSequence text = myParameters.getEditor().getDocument().getCharsSequence();
      int offset = myParameters.getOffset();
      while (text.length() > offset && Character.isWhitespace(text.charAt(offset))) {
        offset++;
      }
      if (text.length() > offset + JavaKeywords.ELSE.length() &&
          text.subSequence(offset, offset + JavaKeywords.ELSE.length()).toString().equals(JavaKeywords.ELSE) &&
          Character.isWhitespace(text.charAt(offset + JavaKeywords.ELSE.length()))) {
        elseKeyword = PrioritizedLookupElement.withPriority(elseKeyword, -1);
      }
      addKeyword(elseKeyword);
    }
  }

  private @NotNull LookupElement createReturnKeyword() {
    TailType returnTail = getReturnTail(myPosition);
    LookupElement ret = createKeyword(JavaKeywords.RETURN);
    if (returnTail != TailTypes.noneType()) {
      ret = new OverridableSpace(ret, returnTail);
    }
    return ret;
  }

  void addKeywords() {
    if (!canAddKeywords()) return;

    PsiFile file = myPosition.getContainingFile();
    if (PsiJavaModule.MODULE_INFO_FILE.equals(file.getName()) && PsiUtil.isAvailable(JavaFeature.MODULES, file)) {
      addModuleKeywords();
      return;
    }

    if (addWildcardExtendsSuper()) {
      return;
    }
    addFinal();
    addWhen();
    boolean statementPosition = isStatementPosition(myPosition);
    if (statementPosition) {

      if (START_SWITCH.accepts(myPosition)) {
        return;
      }

      addBreakContinue();
      addStatementKeywords();

      if (myPrevLeaf.textMatches("}") &&
          myPrevLeaf.getParent() instanceof PsiCodeBlock &&
          myPrevLeaf.getParent().getParent() instanceof PsiTryStatement &&
          myPrevLeaf.getParent().getNextSibling() instanceof PsiErrorElement) {
        return;
      }
    }
    else {
      PsiSwitchLabeledRuleStatement rule = findEnclosingSwitchRule(myPosition);
      if (rule != null) {
        addSwitchRuleKeywords(rule);
      }
    }

    addThisSuper();

    addExpressionKeywords(statementPosition);

    addFileHeaderKeywords();

    addInstanceof();

    addClassKeywords();

    addMethodHeaderKeywords();

    addPrimitiveTypes(this::addKeyword, myPosition, mySession);

    addVar();

    addClassLiteral();

    addExtendsImplements();

    addCaseNullToSwitch();
  }

  void addEnhancedCases() {
    if (!canAddKeywords()) return;

    boolean statementPosition = isStatementPosition(myPosition);
    if (statementPosition) {
      addCaseDefault();

      addPatternMatchingInSwitchCases();
    }
    if (IN_CASE_LABEL_ELEMENT_LIST.accepts(myPosition)) {
      addCaseAfterNullDefault();
    }
  }

  private void addCaseAfterNullDefault() {
    if (!PsiUtil.isAvailable(JavaFeature.PATTERNS_IN_SWITCH, myPosition)) return;
    PsiCaseLabelElementList labels = PsiTreeUtil.getParentOfType(myPosition, PsiCaseLabelElementList.class);
    if (labels == null || labels.getElementCount() != 2 ||
        !(labels.getElements()[0] instanceof PsiLiteralExpression literalExpression &&
          ExpressionUtils.isNullLiteral(literalExpression))) {
      return;
    }

    PsiSwitchBlock switchBlock = PsiTreeUtil.getParentOfType(labels, PsiSwitchBlock.class);
    if (switchBlock == null) return;
    List<PsiSwitchLabelStatementBase> allBranches =
      PsiTreeUtil.getChildrenOfTypeAsList(switchBlock.getBody(), PsiSwitchLabelStatementBase.class);
    if (allBranches.isEmpty() || allBranches.getLast().getCaseLabelElementList() != labels) {
      return;
    }
    if (JavaPsiSwitchUtil.findDefaultElement(switchBlock) != null) {
      return;
    }

    final OverridableSpace defaultCaseRule =
      new OverridableSpace(createKeyword(JavaKeywords.DEFAULT), JavaTailTypes.forSwitchLabel(switchBlock));
    addKeyword(prioritizeForRule(LookupElementDecorator.withInsertHandler(defaultCaseRule, ADJUST_LINE_OFFSET), switchBlock));
  }

  private boolean canAddKeywords() {
    if (PsiTreeUtil.getNonStrictParentOfType(myPosition, PsiLiteralExpression.class, PsiComment.class) != null) {
      return false;
    }

    if (psiElement().afterLeaf("::").accepts(myPosition)) {
      return false;
    }

    if (JavaCompletionContributor.findAnnotationWhoseAttributeIsCompleted(myPosition) != null) {
      return false;
    }
    return true;
  }

  private void addWhen() {
    if (!PsiUtil.isAvailable(JavaFeature.PATTERN_GUARDS_AND_RECORD_PATTERNS, myPosition)) {
      return;
    }
    PsiElement element = PsiTreeUtil.skipWhitespacesAndCommentsForward(myPrevLeaf);
    if (element instanceof PsiErrorElement) {
      return;
    }

    element = PsiTreeUtil.skipWhitespacesAndCommentsBackward(PsiTreeUtil.prevLeaf(myPosition));
    if (element instanceof PsiErrorElement) {
      return;
    }

    PsiPattern psiPattern =
      PsiTreeUtil.getParentOfType(myPrevLeaf, PsiPattern.class, true, PsiStatement.class, PsiMember.class, PsiClass.class);
    if (psiPattern == null ||
        (psiPattern instanceof PsiTypeTestPattern testPattern &&
         testPattern.getPatternVariable() != null &&
         testPattern.getPatternVariable().getNameIdentifier() == myPosition)) {
      return;
    }
    PsiElement parentOfPattern = PsiTreeUtil.skipParentsOfType(psiPattern, PsiPattern.class, PsiDeconstructionList.class);
    if (!(parentOfPattern instanceof PsiCaseLabelElementList)) {
      return;
    }
    addKeyword(new OverridableSpace(createKeyword(JavaKeywords.WHEN), TailTypes.insertSpaceType()));
  }

  private void addSwitchRuleKeywords(@NotNull PsiSwitchLabeledRuleStatement rule) {
    addKeyword(new OverridableSpace(createKeyword(JavaKeywords.THROW), TailTypes.insertSpaceType()));
    addKeyword(wrapRuleIntoBlock(new OverridableSpace(createKeyword(JavaKeywords.ASSERT), TailTypes.insertSpaceType())));
    addKeyword(wrapRuleIntoBlock(new OverridableSpace(createKeyword(JavaKeywords.WHILE), JavaTailTypes.WHILE_LPARENTH)));
    addKeyword(wrapRuleIntoBlock(new OverridableSpace(createKeyword(JavaKeywords.DO), JavaTailTypes.DO_LBRACE)));
    addKeyword(wrapRuleIntoBlock(new OverridableSpace(createKeyword(JavaKeywords.FOR), JavaTailTypes.FOR_LPARENTH)));
    addKeyword(wrapRuleIntoBlock(new OverridableSpace(createKeyword(JavaKeywords.IF), JavaTailTypes.IF_LPARENTH)));
    addKeyword(wrapRuleIntoBlock(new OverridableSpace(createKeyword(JavaKeywords.TRY), JavaTailTypes.TRY_LBRACE)));
    if (rule.getEnclosingSwitchBlock() instanceof PsiSwitchStatement) {
      addKeyword(wrapRuleIntoBlock(createReturnKeyword()));
    }
    else {
      addKeyword(wrapRuleIntoBlock(new OverridableSpace(createKeyword(JavaKeywords.YIELD), TailTypes.insertSpaceType())));
    }
  }

  private static LookupElement wrapRuleIntoBlock(LookupElement element) {
    return new LookupElementDecorator<>(element) {
      @Override
      public void handleInsert(@NotNull InsertionContext context) {
        PsiStatement statement = PsiTreeUtil.getParentOfType(context.getFile().findElementAt(context.getStartOffset()), PsiStatement.class);
        boolean isAfterArrow = false;
        if (statement != null) {
          if (statement.getParent() instanceof PsiCodeBlock) {
            PsiElement prevLeaf = PsiTreeUtil.prevCodeLeaf(statement);
            if (PsiUtil.isJavaToken(prevLeaf, JavaTokenType.ARROW) && prevLeaf.getParent() instanceof PsiSwitchLabeledRuleStatement) {
              isAfterArrow = true;
            }
          }
          else if (statement.getParent() instanceof PsiSwitchLabeledRuleStatement) {
            isAfterArrow = true;
          }
        }
        if (isAfterArrow) {
          CaretModel model = context.getEditor().getCaretModel();
          int origPos = model.getOffset();
          int start = statement.getTextRange().getStartOffset();
          PsiStatement updatedStatement = BlockUtils.expandSingleStatementToBlockStatement(statement);
          int updatedStart = updatedStatement.getTextRange().getStartOffset();
          model.moveToOffset(origPos + updatedStart - start);
          PsiDocumentManager.getInstance(context.getProject()).doPostponedOperationsAndUnblockDocument(context.getDocument());
          context.setTailOffset(model.getOffset());
        }
        super.handleInsert(context);
      }
    };
  }

  private static PsiSwitchLabeledRuleStatement findEnclosingSwitchRule(PsiElement position) {
    PsiElement parent = position.getParent();
    return parent.getParent() instanceof PsiExpressionStatement stmt &&
           stmt.getParent() instanceof PsiSwitchLabeledRuleStatement rule ? rule : null;
  }

  private void addCaseNullToSwitch() {
    if (!isInsideCaseLabel()) return;

    final PsiSwitchBlock switchBlock = PsiTreeUtil.getParentOfType(myPosition, PsiSwitchBlock.class, false, PsiMember.class);
    if (switchBlock == null) return;

    final PsiType selectorType = getSelectorType(switchBlock);
    if (selectorType instanceof PsiPrimitiveType) return;

    addKeyword(createKeyword(JavaKeywords.NULL));
  }

  private boolean isInsideCaseLabel() {
    if (!PsiUtil.isAvailable(JavaFeature.PATTERNS_IN_SWITCH, myPosition)) return false;
    return psiElement().withSuperParent(2, PsiCaseLabelElementList.class).accepts(myPosition);
  }

  private void addVar() {
    if (isVarAllowed()) {
      addKeyword(createKeyword(JavaKeywords.VAR));
    }
  }

  private boolean isVarAllowed() {
    if (PsiUtil.isAvailable(JavaFeature.VAR_LAMBDA_PARAMETER, myPosition) && isLambdaParameterType()) {
      return true;
    }

    if (!PsiUtil.isAvailable(JavaFeature.LVTI, myPosition)) return false;

    if (isAtCatchOrResourceVariableStart(myPosition) && PsiTreeUtil.getParentOfType(myPosition, PsiCatchSection.class) == null) {
      return true;
    }

    return isVariableTypePosition(myPosition) &&
           PsiTreeUtil.getParentOfType(myPosition, PsiCodeBlock.class, true, PsiMember.class, PsiLambdaExpression.class) != null;
  }

  private boolean isLambdaParameterType() {
    PsiElement position = myParameters.getOriginalPosition();
    PsiParameterList paramList = PsiTreeUtil.getParentOfType(position, PsiParameterList.class);
    if (paramList != null && paramList.getParent() instanceof PsiLambdaExpression) {
      PsiParameter param = PsiTreeUtil.getParentOfType(position, PsiParameter.class);
      PsiTypeElement type = param == null ? null : param.getTypeElement();
      return type == null || PsiTreeUtil.isAncestor(type, position, false);
    }
    return false;
  }

  private boolean addWildcardExtendsSuper() {
    if (JavaMemberNameCompletionContributor.INSIDE_TYPE_PARAMS_PATTERN.accepts(myPosition)) {
      for (String keyword : ContainerUtil.ar(JavaKeywords.EXTENDS, JavaKeywords.SUPER)) {
        LookupElement item = BasicExpressionCompletionContributor.createKeywordLookupItem(myPosition, keyword);
        addKeyword(new OverridableSpace(item, TailTypes.humbleSpaceBeforeWordType()));
      }
      return true;
    }
    return false;
  }

  private void addMethodHeaderKeywords() {
    if (psiElement().withText(")").withParents(PsiParameterList.class, PsiMethod.class).accepts(myPrevLeaf)) {
      assert myPrevLeaf != null;
      if (myPrevLeaf.getParent().getParent() instanceof PsiAnnotationMethod) {
        addKeyword(new OverridableSpace(createKeyword(JavaKeywords.DEFAULT), TailTypes.humbleSpaceBeforeWordType()));
      }
      else {
        addKeyword(new OverridableSpace(createKeyword(JavaKeywords.THROWS), TailTypes.humbleSpaceBeforeWordType()));
      }
    }
  }

  private void addCaseDefault() {
    PsiSwitchBlock switchBlock = getSwitchFromLabelPosition(myPosition);
    if (switchBlock == null) return;
    PsiElement defaultElement = JavaPsiSwitchUtil.findDefaultElement(switchBlock);
    if (defaultElement != null && defaultElement.getTextRange().getStartOffset() < myPosition.getTextRange().getStartOffset()) return;
    addKeyword(new OverridableSpace(createKeyword(JavaKeywords.CASE), TailTypes.insertSpaceType()));
    if (defaultElement != null) {
      return;
    }
    final OverridableSpace defaultCaseRule =
      new OverridableSpace(createKeyword(JavaKeywords.DEFAULT), JavaTailTypes.forSwitchLabel(switchBlock));
    addKeyword(prioritizeForRule(LookupElementDecorator.withInsertHandler(defaultCaseRule, ADJUST_LINE_OFFSET), switchBlock));
  }

  private void addPatternMatchingInSwitchCases() {
    if (!PsiUtil.isAvailable(JavaFeature.PATTERNS_IN_SWITCH, myPosition)) return;

    PsiSwitchBlock switchBlock = getSwitchFromLabelPosition(myPosition);
    if (switchBlock == null) return;

    final PsiType selectorType = getSelectorType(switchBlock);
    if (selectorType == null || selectorType instanceof PsiPrimitiveType) return;

    PsiElement defaultElement = JavaPsiSwitchUtil.findDefaultElement(switchBlock);
    if (defaultElement != null && defaultElement.getTextRange().getStartOffset() < myPosition.getTextRange().getStartOffset()) return;

    final TailType caseRuleTail = JavaTailTypes.forSwitchLabel(switchBlock);
    Set<String> containedLabels = getSwitchCoveredLabels(switchBlock, myPosition);
    if (!containedLabels.contains(JavaKeywords.NULL)) {
      addKeyword(createCaseRule(JavaKeywords.NULL, caseRuleTail, switchBlock));
      if (!containedLabels.contains(JavaKeywords.DEFAULT)) {
        addKeyword(createCaseRule(JavaKeywords.NULL + ", " + JavaKeywords.DEFAULT, caseRuleTail, switchBlock));
      }
    }
    addSealedHierarchyCases(selectorType, containedLabels);
  }

  private static @NotNull Set<String> getSwitchCoveredLabels(@Nullable PsiSwitchBlock block, PsiElement position) {
    HashSet<String> labels = new HashSet<>();
    if (block == null) {
      return labels;
    }
    PsiCodeBlock body = block.getBody();
    if (body == null) {
      return labels;
    }
    int offset = position.getTextRange().getStartOffset();
    for (PsiStatement statement : body.getStatements()) {
      if (!(statement instanceof PsiSwitchLabelStatementBase labelStatement)) continue;
      if (labelStatement.isDefaultCase()) {
        labels.add(JavaKeywords.DEFAULT);
        continue;
      }
      if (labelStatement.getGuardExpression() != null) {
        continue;
      }
      PsiCaseLabelElementList list = labelStatement.getCaseLabelElementList();
      if (list == null) {
        continue;
      }
      for (PsiCaseLabelElement element : list.getElements()) {
        if (element instanceof PsiExpression expr &&
            ExpressionUtils.isNullLiteral(expr)) {
          labels.add(JavaKeywords.NULL);
          continue;
        }
        if (element instanceof PsiDefaultCaseLabelElement) {
          labels.add(JavaKeywords.DEFAULT);
        }
        if (element.getTextRange().getStartOffset() >= offset) {
          break;
        }
        PsiType patternType = JavaPsiPatternUtil.getPatternType(element);
        if (patternType != null && JavaPsiPatternUtil.isUnconditionalForType(element, patternType)) {
          PsiClass psiClass = PsiUtil.resolveClassInClassTypeOnly(patternType);
          if (psiClass == null) {
            continue;
          }
          labels.add(psiClass.getQualifiedName());
        }
      }
    }
    return labels;
  }

  @Contract(pure = true)
  private static @Nullable PsiType getSelectorType(@NotNull PsiSwitchBlock switchBlock) {

    final PsiExpression selector = switchBlock.getExpression();
    if (selector == null) return null;

    return selector.getType();
  }

  private void addSealedHierarchyCases(@NotNull PsiType type, Set<String> containedLabels) {
    final PsiResolveHelper resolver = JavaPsiFacade.getInstance(myPosition.getProject()).getResolveHelper();
    PsiClass aClass = resolver.resolveReferencedClass(type.getCanonicalText(), null);
    if (aClass == null) {
      aClass = PsiUtil.resolveClassInClassTypeOnly(type);
    }
    if (aClass == null || aClass.isEnum() || !aClass.hasModifierProperty(PsiModifier.SEALED)) return;

    for (PsiClass inheritor : SealedUtils.findSameFileInheritorsClasses(aClass)) {
      //we don't check hierarchy here, because it is time-consuming
      if (containedLabels.contains(inheritor.getQualifiedName())) {
        continue;
      }
      final JavaPsiClassReferenceElement item = AllClassesGetter.createLookupItem(inheritor, AllClassesGetter.TRY_SHORTENING);
      item.setForcedPresentableName("case " + inheritor.getName());
      addKeyword(PrioritizedLookupElement.withPriority(item, 1));
    }
  }

  private static @NotNull LookupElement createCaseRule(@NotNull String caseRuleName,
                                                       TailType tailType,
                                                       @Nullable PsiSwitchBlock switchBlock) {
    final String prefix = "case ";

    final LookupElement lookupElement = LookupElementBuilder
      .create(prefix + caseRuleName)
      .bold()
      .withPresentableText(prefix)
      .withTailText(caseRuleName)
      .withLookupString(caseRuleName);

    JavaCompletionContributor.IndentingDecorator decorator =
      new JavaCompletionContributor.IndentingDecorator(TailTypeDecorator.withTail(lookupElement, tailType));
    return prioritizeForRule(decorator, switchBlock);
  }

  private static PsiSwitchBlock getSwitchFromLabelPosition(PsiElement position) {
    PsiStatement statement = PsiTreeUtil.getParentOfType(position, PsiStatement.class, false, PsiMember.class);
    if (statement == null || statement.getTextRange().getStartOffset() != position.getTextRange().getStartOffset()) {
      return null;
    }

    if (!(statement instanceof PsiSwitchLabelStatementBase) && statement.getParent() instanceof PsiCodeBlock) {
      return ObjectUtils.tryCast(statement.getParent().getParent(), PsiSwitchBlock.class);
    }
    return null;
  }

  void addEnumCases() {
    PsiSwitchBlock switchBlock = getSwitchFromLabelPosition(myPosition);
    PsiExpression expression = switchBlock == null ? null : switchBlock.getExpression();
    PsiClass switchType = expression == null ? null : PsiUtil.resolveClassInClassTypeOnly(expression.getType());
    if (switchType == null || !switchType.isEnum()) return;

    Set<PsiField> used = ReferenceExpressionCompletionContributor.findConstantsUsedInSwitch(switchBlock);
    TailType tailType = JavaTailTypes.forSwitchLabel(switchBlock);
    for (PsiField field : switchType.getAllFields()) {
      String name = field.getName();
      if (!(field instanceof PsiEnumConstant) || used.contains(CompletionUtil.getOriginalOrSelf(field))) {
        continue;
      }
      String prefix = "case ";
      LookupElementBuilder caseConst = LookupElementBuilder
        .create(field, prefix + name)
        .bold()
        .withPresentableText(prefix)
        .withTailText(name)
        .withLookupString(name);
      LookupElement withPriority = prioritizeForRule(
        new JavaCompletionContributor.IndentingDecorator(TailTypeDecorator.withTail(caseConst, tailType)),
        switchBlock);
      myResults.add(withPriority);
    }
  }

  private static @NotNull LookupElement prioritizeForRule(@NotNull LookupElement decorator, @Nullable PsiSwitchBlock switchBlock) {
    if (switchBlock == null) {
      return decorator;
    }
    PsiCodeBlock body = switchBlock.getBody();
    if (body == null) {
      return decorator;
    }
    PsiStatement[] statements = body.getStatements();
    if (statements.length == 0) {
      return decorator;
    }
    if (statements[0] instanceof PsiSwitchLabeledRuleStatement) {
      return PrioritizedLookupElement.withPriority(decorator, 1);
    }
    return decorator;
  }

  private void addFinal() {
    PsiStatement statement = PsiTreeUtil.getParentOfType(myPosition, PsiExpressionStatement.class, PsiDeclarationStatement.class);
    if (statement != null && statement.getTextRange().getStartOffset() == myPosition.getTextRange().getStartOffset()) {
      if (!psiElement().withSuperParent(2, PsiSwitchBlock.class).afterLeaf("{").accepts(statement)) {
        PsiTryStatement tryStatement = PsiTreeUtil.getParentOfType(myPrevLeaf, PsiTryStatement.class);
        if (tryStatement == null ||
            tryStatement.getCatchSections().length > 0 ||
            tryStatement.getFinallyBlock() != null || tryStatement.getResourceList() != null) {
          LookupElement finalKeyword = new OverridableSpace(createKeyword(JavaKeywords.FINAL), TailTypes.humbleSpaceBeforeWordType());
          if (statement.getParent() instanceof PsiSwitchLabeledRuleStatement) {
            finalKeyword = wrapRuleIntoBlock(finalKeyword);
          }
          addKeyword(finalKeyword);
          return;
        }
      }
    }

    if ((isInsideParameterList(myPosition) || isAtCatchOrResourceVariableStart(myPosition)) &&
        !psiElement().afterLeaf(JavaKeywords.FINAL).accepts(myPosition) &&
        !AFTER_DOT.accepts(myPosition)) {
      addKeyword(TailTypeDecorator.withTail(createKeyword(JavaKeywords.FINAL), TailTypes.humbleSpaceBeforeWordType()));
    }
  }

  private void addThisSuper() {
    if (SUPER_OR_THIS_PATTERN.accepts(myPosition)) {
      final boolean afterDot = AFTER_DOT.accepts(myPosition);
      final boolean insideQualifierClass = isInsideQualifierClass();
      final boolean insideInheritorClass = PsiUtil.isAvailable(JavaFeature.EXTENSION_METHODS, myPosition) && isInsideInheritorClass();
      if (!afterDot || insideQualifierClass || insideInheritorClass) {
        if (!afterDot || insideQualifierClass) {
          addKeyword(createKeyword(JavaKeywords.THIS));
        }

        final LookupElement superItem = createKeyword(JavaKeywords.SUPER);
        if (psiElement().afterLeaf(psiElement().withText("{").withSuperParent(2, psiMethod().constructor(true))).accepts(myPosition)) {
          final PsiMethod method = PsiTreeUtil.getParentOfType(myPosition, PsiMethod.class, false, PsiClass.class);
          assert method != null;
          final boolean hasParams = superConstructorHasParameters(method);
          addKeyword(LookupElementDecorator.withInsertHandler(superItem, new ParenthesesInsertHandler<LookupElement>() {
            @Override
            protected boolean placeCaretInsideParentheses(InsertionContext context, LookupElement item) {
