// 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.psi.formatter.java;

import com.intellij.formatting.Block;
import com.intellij.formatting.Spacing;
import com.intellij.lang.ASTNode;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.lang.java.JavaParserDefinition;
import com.intellij.lexer.Lexer;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.psi.codeStyle.JavaCodeStyleSettings;
import com.intellij.psi.formatter.FormatterUtil;
import com.intellij.psi.impl.source.SourceTreeToPsiMap;
import com.intellij.psi.impl.source.javadoc.PsiDocMethodOrFieldRef;
import com.intellij.psi.impl.source.tree.*;
import com.intellij.psi.impl.source.tree.java.EnumConstantElement;
import com.intellij.psi.javadoc.*;
import com.intellij.psi.jsp.IJspClassLevelDeclarationStatement;
import com.intellij.psi.jsp.IJspCodeBlock;
import com.intellij.psi.jsp.IJspJavaComment;
import com.intellij.psi.tree.ChildRoleBase;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import com.intellij.psi.tree.java.IJavaElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

import static com.intellij.openapi.util.Pair.pair;
import static com.intellij.psi.codeStyle.CommonCodeStyleSettings.*;

public final class JavaSpacePropertyProcessor extends JavaElementVisitor {
  private static final Logger LOG = Logger.getInstance(JavaSpacePropertyProcessor.class);

  private static final TokenSet REF_LIST_KEYWORDS = TokenSet.create(
    JavaTokenType.EXTENDS_KEYWORD, JavaTokenType.IMPLEMENTS_KEYWORD,
    JavaTokenType.THROWS_KEYWORD, JavaTokenType.PERMITS_KEYWORD, JavaTokenType.WITH_KEYWORD
  );

  private static final TokenSet ESCAPED_TOKENS = TokenSet.create(
    JavaTokenType.LT, JavaTokenType.LTLT, JavaTokenType.LTLTEQ,
    JavaTokenType.GT, JavaTokenType.GTGT, JavaTokenType.GTGTEQ, JavaTokenType.GTGTGTEQ,
    JavaTokenType.AND, JavaTokenType.ANDAND, JavaTokenType.ANDEQ,
    JavaTokenType.STRING_LITERAL);

  private static final Map<Pair<IElementType, IElementType>, Boolean> ourTokenStickingMatrix = new ConcurrentHashMap<>();

  private Spacing myResult;
  private PsiElement myParent;
  private int myRole1;
  private int myRole2;
  private ASTNode myChild1;
  private ASTNode myChild2;
  private IElementType myType1;
  private IElementType myType2;
  private CommonCodeStyleSettings mySettings;
  private JavaCodeStyleSettings myJavaSettings;

  private JavaSpacePropertyProcessor(Block block, CommonCodeStyleSettings settings, JavaCodeStyleSettings javaSettings) {
    ASTNode child = AbstractJavaBlock.getTreeNode(block);
    if (child == null || isErrorElement(child)) {
      myResult = Spacing.getReadOnlySpacing();
      return;
    }

    init(child);
    mySettings = settings;
    myJavaSettings = javaSettings;

    if (myChild1 == null) {
      // Given node corresponds to the first document block.
      if (FormatterUtil.isFormatterCalledExplicitly()) {
        createSpaceInCode(false);
      }
      return;
    }

    PsiElement myChild1Psi1 = myChild1.getPsi();
    PsiElement myChild1Psi2 = myChild2.getPsi();
    if (myChild1Psi1 == null || myChild1Psi1.getLanguage() != JavaLanguage.INSTANCE ||
        myChild1Psi2 == null || myChild1Psi2.getLanguage() != JavaLanguage.INSTANCE) {
      return;
    }

    if (block instanceof TextLineBlock) {
      myResult = ((TextLineBlock)block).getSpacing();
      return;
    }
    if (block instanceof CStyleCommentBlock) {
      myResult = ((CStyleCommentBlock)block).getSpacing();
      return;
    }

    if (myChild2 != null && StdTokenSets.COMMENT_BIT_SET.contains(myType2)) {
      if (mySettings.KEEP_FIRST_COLUMN_COMMENT) {
        myResult = Spacing.createKeepingFirstColumnSpacing(0, Integer.MAX_VALUE, true, mySettings.KEEP_BLANK_LINES_IN_CODE);
      }
      else {
        myResult = Spacing.createSpacing(0, Integer.MAX_VALUE, 0, true, mySettings.KEEP_BLANK_LINES_IN_CODE);
      }
    }
    else if (myParent != null) {
      myParent.accept(this);

      if (myResult == null) {
        ASTNode prev = getPrevElementType(myChild2);
        if (prev != null && prev.getElementType() == JavaTokenType.END_OF_LINE_COMMENT) {
          myResult = Spacing.createSpacing(0, 0, 1, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_CODE);
        }
        else if (!canStickChildrenTogether(myChild1, myChild2)) {
          myResult = Spacing.createSpacing(1, Integer.MIN_VALUE, 0, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_CODE);
        }
        else if (myType1 == JavaTokenType.C_STYLE_COMMENT) {
          myResult = null;
        }
        else if (!shouldKeepSpace(myParent)) {
          myResult = Spacing.createSpacing(0, 0, 0, true, mySettings.KEEP_BLANK_LINES_IN_CODE);
        }
      }
    }
  }

  private static boolean isErrorElement(ASTNode child) {
    return child != null && child.getPsi() instanceof PsiErrorElement;
  }

  private static boolean shouldKeepSpace(PsiElement parent) {
    ASTNode node = parent.getNode();
    if (node == null) {
      return true;
    }

    IElementType type = node.getElementType();
    if (type == JavaDocElementType.DOC_TAG_VALUE_ELEMENT) {
      return PsiTreeUtil.getParentOfType(parent, PsiDocMethodOrFieldRef.class) != null;
    }

    return JavaDocElementType.DOC_COMMENT_TOKENS.contains(type) || type == JavaDocElementType.DOC_TAG || type == JavaDocElementType.DOC_INLINE_TAG;
  }

  private void init(ASTNode child) {
    if (child == null) return;
    ASTNode treePrev = child.getTreePrev();
    while (treePrev != null && FormatterUtil.containsWhiteSpacesOnly(treePrev)) {
      treePrev = treePrev.getTreePrev();
    }
    if (treePrev == null) {
      init(child.getTreeParent());
    }
    else {
      myChild2 = child;
      myChild1 = treePrev;
      CompositeElement parent = (CompositeElement)treePrev.getTreeParent();
      myParent = SourceTreeToPsiMap.treeElementToPsi(parent);
      myRole1 = parent.getChildRole(treePrev);
      myType1 = treePrev.getElementType();
      myRole2 = parent.getChildRole(child);
      myType2 = child.getElementType();
    }
  }

  @Override
  public void visitArrayAccessExpression(@NotNull PsiArrayAccessExpression expression) {
    if (myRole1 == ChildRole.ARRAY && myRole2 == ChildRole.LBRACKET) {
      createSpaceInCode(false);
    }
    else if (myRole1 == ChildRole.LBRACKET || myRole2 == ChildRole.RBRACKET) {
      createSpaceInCode(mySettings.SPACE_WITHIN_BRACKETS);
    }
  }

  private void createSpaceInCode(boolean space) {
    createSpaceProperty(space, mySettings.KEEP_BLANK_LINES_IN_CODE);
  }

  @Override
  public void visitNewExpression(@NotNull PsiNewExpression expression) {
    if (myRole2 == ChildRole.ARRAY_INITIALIZER) {
      createSpaceInCode(mySettings.SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE);
    }
    else if (myRole1 == ChildRole.NEW_KEYWORD) {
      createSpaceInCode(true);
    }
    else if (myRole2 == ChildRole.ARGUMENT_LIST) {
      createSpaceInCode(mySettings.SPACE_BEFORE_METHOD_CALL_PARENTHESES);
    }
    // We don't want to insert space between brackets in case of expression like 'new int[] {1}', hence, we check that exactly
    // one of the children is bracket.
    else if (myRole1 == ChildRole.LBRACKET ^ myRole2 == ChildRole.RBRACKET) {
      createSpaceInCode(mySettings.SPACE_WITHIN_BRACKETS);
    }
  }

  @Override
  public void visitArrayInitializerExpression(@NotNull PsiArrayInitializerExpression expression) {
    visitArrayInitializer();
  }

  @Override
  public void visitClass(@NotNull PsiClass aClass) {
    if (JavaDocElementType.DOC_COMMENT_TOKENS.contains(myType1)) {
      myResult = Spacing.createSpacing(0, 0, 1, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
      return;
    }
    if (myRole2 == ChildRole.LBRACE) {
      myResult = getSpaceBeforeClassLBrace(aClass);
    }
    else if (myRole1 == ChildRole.LBRACE || isEndOfLineCommentAfterLBrace(myChild1)) {
      if (aClass.isEnum()) {
        createSpacingForEnumBraces(true);
      }
      else if (myRole2 == ChildRole.RBRACE && mySettings.KEEP_SIMPLE_CLASSES_IN_ONE_LINE) {
        int spaces = mySettings.SPACE_WITHIN_BRACES ? 1 : 0;
        myResult = Spacing.createSpacing(spaces, spaces, 0, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
      }
      else if (aClass instanceof PsiAnonymousClass) {
        if (myRole2 == ChildRole.CLASS_INITIALIZER && isTheOnlyClassMember(myChild2)) {
          myResult = Spacing.createSpacing(0, 0, 0,
                                           mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
        }
        else {
          myResult = Spacing.createSpacing(0, 0, mySettings.BLANK_LINES_AFTER_ANONYMOUS_CLASS_HEADER + 1,
                                           mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
        }
      }
      else {
        myResult = Spacing.createSpacing(1, 1, mySettings.BLANK_LINES_AFTER_CLASS_HEADER + 1,
                                         mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
      }
    }
    else if (myRole2 == ChildRole.RBRACE && aClass.isEnum()) {
      createSpacingForEnumBraces(false);
    }
    else if (myRole1 == ChildRole.COMMA && aClass.isEnum() && isJavadocHoldingEnumConstant(myChild2)) {
      createParenthSpace(true, true);
    }
    else if (aClass instanceof PsiAnonymousClass && ElementType.JAVA_PLAIN_COMMENT_BIT_SET.contains(myType1)) {
      ASTNode prev = myChild1.getTreePrev();
      if (prev.getElementType() == TokenType.WHITE_SPACE && !StringUtil.containsLineBreak(prev.getChars())) {
        prev = prev.getTreePrev();
      }
      if (prev.getElementType() == JavaTokenType.LBRACE) {
        myResult = Spacing.createSpacing(0, 0, mySettings.BLANK_LINES_AFTER_ANONYMOUS_CLASS_HEADER + 1,
                                         mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
      }
      else {
        processClassBody();
      }
    }
    else if (myChild1.getElementType() == JavaTokenType.COMMA && myType2 == JavaTokenType.SEMICOLON && aClass.isEnum()) {
      createParenthSpace(true, false);
    }
    else if (aClass instanceof PsiAnonymousClass && myRole2 == ChildRole.ARGUMENT_LIST) {
      createSpaceInCode(mySettings.SPACE_BEFORE_METHOD_CALL_PARENTHESES);
    }
    else {
      processClassBody();
    }
  }

  private void createSpacingForEnumBraces(boolean isLbrace) {
    ASTNode node = myParent.getNode().findChildByType(JavaTokenType.LBRACE);
    final int startOffset;
    if (node == null) {
      startOffset = myParent.getTextOffset();
    } else {
      startOffset = node.getTextRange().getStartOffset();
    }
    int spaces = myJavaSettings.SPACE_INSIDE_ONE_LINE_ENUM_BRACES ? 1 : 0;
    TextRange textRange = new TextRange(startOffset, myParent.getTextRange().getEndOffset());
    int blankLinesCount = isLbrace ? mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS : mySettings.KEEP_BLANK_LINES_BEFORE_RBRACE;
    myResult = Spacing.createDependentLFSpacing(spaces, spaces, textRange, mySettings.KEEP_LINE_BREAKS, blankLinesCount);
  }

  private static boolean isJavadocHoldingEnumConstant(@NotNull ASTNode node) {
    if (!(node instanceof EnumConstantElement)) return false;
    ASTNode firstChildNode = node.getFirstChildNode();
    if (!(firstChildNode instanceof PsiDocComment)) return false;
    return true;
  }

  private @NotNull Spacing getSpaceBeforeMethodLBrace(@NotNull PsiMethod method) {
    int space = mySettings.SPACE_BEFORE_METHOD_LBRACE ? 1 : 0;
    int methodBraceStyle = mySettings.METHOD_BRACE_STYLE;

    if (methodBraceStyle == END_OF_LINE) {
      return createNonLFSpace(space, null);
    }
    else if (methodBraceStyle == NEXT_LINE_IF_WRAPPED) {
      TextRange headerRange = new TextRange(getMethodHeaderStartOffset(method), getMethodHeaderEndOffset(method));
      return createNonLFSpace(space, headerRange);
    }
    else if (shouldHandleAsSimpleMethod(method)) {
      TextRange rangeWithoutAnnotations = new TextRange(getMethodHeaderStartOffset(method), method.getTextRange().getEndOffset());
      return createNonLFSpace(space, rangeWithoutAnnotations);
    }

    return Spacing.createSpacing(space, space, 1, false, mySettings.KEEP_BLANK_LINES_IN_CODE);
  }

  private static int getMethodHeaderEndOffset(@NotNull PsiMethod method) {
    PsiElement headerEnd = method.getBody() != null ? method.getBody().getPrevSibling() : null;
    if (headerEnd != null) {
      return headerEnd.getTextRange().getEndOffset();
    }
    return method.getTextRange().getEndOffset();
  }

  private @NotNull Spacing getSpaceBeforeClassLBrace(@NotNull PsiClass aClass) {
    int space = mySettings.SPACE_BEFORE_CLASS_LBRACE ? 1 : 0;
    int classBraceStyle = mySettings.CLASS_BRACE_STYLE;

    if (classBraceStyle == END_OF_LINE || shouldHandleAsSimpleClass(aClass)) {
      return createNonLFSpace(space, null);
    }
    else if (classBraceStyle == NEXT_LINE_IF_WRAPPED) {
      PsiIdentifier nameIdentifier = aClass.getNameIdentifier();
      int startOffset = nameIdentifier == null ? myParent.getTextRange().getStartOffset() : nameIdentifier.getTextRange().getStartOffset();
      TextRange range = new TextRange(startOffset, myChild1.getTextRange().getEndOffset());
      return createNonLFSpace(space, range);
    }

    return Spacing.createSpacing(space, space, 1, false, mySettings.KEEP_BLANK_LINES_IN_CODE);
  }

  private Spacing getSpaceBeforeLBrace(@NotNull ASTNode lBraceBlock, boolean spaceBeforeLbrace, @Nullable TextRange nextLineIfWrappedOptionRange) {
    int space = spaceBeforeLbrace ? 1 : 0;

    if (mySettings.BRACE_STYLE == END_OF_LINE) {
      return createNonLFSpace(space, null);
    }
    else if (mySettings.BRACE_STYLE == NEXT_LINE_IF_WRAPPED) {
      return createNonLFSpace(space, nextLineIfWrappedOptionRange);
    }
    else if (shouldHandleAsSimpleBlock(lBraceBlock)) {
      return createNonLFSpace(space, lBraceBlock.getTextRange());
    }

    return Spacing.createSpacing(space, space, 1, false, mySettings.KEEP_BLANK_LINES_IN_CODE);
  }

  private boolean shouldHandleAsSimpleClass(@NotNull PsiClass aClass) {
    if (!mySettings.KEEP_SIMPLE_CLASSES_IN_ONE_LINE) return false;

    PsiElement lBrace = aClass.getLBrace();
    PsiElement rBrace = aClass.getRBrace();
    if (lBrace != null && rBrace != null) {
      PsiElement beforeLBrace = lBrace.getPrevSibling();
      if (beforeLBrace instanceof PsiWhiteSpace && beforeLBrace.textContains('\n')) {
        return false;
      }

      PsiElement betweenBraces = lBrace.getNextSibling();
      if (betweenBraces == rBrace || isWhiteSpaceWithoutLineFeeds(betweenBraces) && betweenBraces.getNextSibling() == rBrace) {
        return true;
      }
    }

    return false;
  }

  private static boolean isWhiteSpaceWithoutLineFeeds(@Nullable PsiElement betweenBraces) {
    return betweenBraces instanceof PsiWhiteSpace && !betweenBraces.textContains('\n');
  }

  private boolean shouldHandleAsSimpleBlock(@NotNull ASTNode node) {
    if (!mySettings.KEEP_SIMPLE_BLOCKS_IN_ONE_LINE) return false;

    PsiElement prev = node.getPsi().getPrevSibling();
    if (prev instanceof PsiWhiteSpace && prev.textContains('\n')) {
      return false;
    }
    PsiElement element = node.getPsi();

    return !node.textContains('\n') &&
           (element instanceof PsiCodeBlock body && shouldAddNewLineWhenBodyIsPresent(body) ||
            element instanceof PsiBlockStatement statement && shouldAddNewLineWhenBodyIsPresent(statement.getCodeBlock()));
  }

  private boolean shouldAddNewLineWhenBodyIsPresent(@NotNull PsiCodeBlock body) {
    return !myJavaSettings.NEW_LINE_WHEN_BODY_IS_PRESENTED || body.isEmpty();
  }

  private boolean shouldHandleAsSimpleMethod(@NotNull PsiMethod method) {
    if (!mySettings.KEEP_SIMPLE_METHODS_IN_ONE_LINE) return false;
    PsiCodeBlock body = method.getBody();
    return body != null && !body.textContains('\n') && shouldAddNewLineWhenBodyIsPresent(body);
  }

  private static int getMethodHeaderStartOffset(@NotNull PsiMethod method) {
    PsiTypeParameterList typeParameterList = PsiTreeUtil.findChildOfType(method, PsiTypeParameterList.class);
    if (typeParameterList != null) {
      PsiElement nextNonWsElem = PsiTreeUtil.skipWhitespacesForward(typeParameterList);
      if (nextNonWsElem != null) {
        return nextNonWsElem.getTextRange().getStartOffset();
      }
    }
    return method.getTextRange().getStartOffset();
  }

  private static boolean isEndOfLineCommentAfterLBrace(@NotNull ASTNode node) {
    if (node.getPsi() instanceof PsiComment) {
      PsiElement ws = node.getPsi().getPrevSibling();
      if (isWhiteSpaceWithoutLineFeeds(ws)) {
        PsiElement beforeWs = ws.getPrevSibling();
        if (PsiUtil.isJavaToken(beforeWs, JavaTokenType.LBRACE)) {
          return true;
        }
      }
    }
    return false;
  }

  private static boolean isTheOnlyClassMember(ASTNode node) {
    ASTNode next = node.getTreeNext();
    if (next == null || !(next.getElementType() == JavaTokenType.RBRACE)) return false;

    ASTNode prev = node.getTreePrev();
    if (prev == null || !(prev.getElementType() == JavaTokenType.LBRACE)) return false;

    return true;
  }

  private void processClassBody() {
    if (myChild1 instanceof IJspJavaComment || myChild2 instanceof IJspJavaComment) {
      myResult = Spacing.createSpacing(0, 0, 1, mySettings.KEEP_LINE_BREAKS, 0);
    }
    else if (processMethod()) {
      /* do nothing */
    }
    else if (myRole2 == ChildRole.CLASS_INITIALIZER) {
      if (myRole1 == ChildRole.LBRACE) {
        myResult = Spacing.createSpacing(0, 0, 1, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_CODE);
      }
      else if (myRole1 == ChildRole.FIELD) {
        int blankLines = myJavaSettings.BLANK_LINES_AROUND_INITIALIZER + 1;
        myResult = Spacing.createSpacing(0, mySettings.SPACE_BEFORE_CLASS_LBRACE ? 1 : 0, blankLines, mySettings.KEEP_LINE_BREAKS,
                                         mySettings.KEEP_BLANK_LINES_BEFORE_RBRACE);
      }
      else if (myRole1 == ChildRole.CLASS) {
        setAroundClassSpacing();
      }
      else {
        int blankLines = getLinesAroundMethod() + 1;
        myResult = Spacing.createSpacing(0, 0, blankLines, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
      }
    }
    else if (myRole1 == ChildRole.CLASS_INITIALIZER) {
      if (myRole2 == ChildRole.RBRACE) {
        int minLineFeeds = getMinLineFeedsBetweenRBraces(myChild1);
        myResult =
          Spacing.createSpacing(0, Integer.MAX_VALUE, minLineFeeds, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_BEFORE_RBRACE);
      }
      else if (myRole2 == ChildRole.CLASS) {
        setAroundClassSpacing();
      }
      else {
        int blankLines = myJavaSettings.BLANK_LINES_AROUND_INITIALIZER + 1;
        myResult =
          Spacing.createSpacing(0, Integer.MAX_VALUE, blankLines, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
      }
    }
    else if (myRole1 == ChildRole.CLASS) {
      if (myRole2 == ChildRole.RBRACE) {
        myResult = Spacing.createSpacing(0, Integer.MAX_VALUE, getLineBreaksBeforeClassEnd(), mySettings.KEEP_LINE_BREAKS,
                                         mySettings.KEEP_BLANK_LINES_BEFORE_RBRACE);
      }
      else {
        int blankLines = mySettings.BLANK_LINES_AROUND_CLASS + 1;
        myResult =
          Spacing.createSpacing(0, Integer.MAX_VALUE, blankLines, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
      }
    }
    else if (myRole2 == ChildRole.CLASS) {
      if (myRole1 == ChildRole.LBRACE) {
        myResult = Spacing.createSpacing(0, 0, 1, mySettings.KEEP_LINE_BREAKS, 0);
      }
      else {
        int blankLines = mySettings.BLANK_LINES_AROUND_CLASS + 1;
        myResult =
          Spacing.createSpacing(0, Integer.MAX_VALUE, blankLines, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
      }
    }

    else if (myRole2 == ChildRole.FIELD) {
      if (myRole1 == ChildRole.COMMA) {
        createSpaceProperty(true, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_CODE);
      }
      else if (myRole1 == ChildRole.LBRACE) {
        myResult = Spacing.createSpacing(0, 0, 1, mySettings.KEEP_LINE_BREAKS, 0);
      }
      else {
        int blankLines = getLinesAroundField() + 1;
        myResult =
          Spacing.createSpacing(0, Integer.MAX_VALUE, blankLines, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
      }
    }

    else if (myRole1 == ChildRole.FIELD) {
      if (myRole2 == ChildRole.COMMA) {
        ASTNode lastChildNode = myChild1.getLastChildNode();
        if (lastChildNode != null && lastChildNode.getElementType() == JavaTokenType.SEMICOLON) {
          myResult = Spacing.createSpacing(0, 0, 1, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
        }
        else {
          createSpaceProperty(false, false, 0);
        }
      }
      else if (myRole2 == ChildRole.RBRACE) {
        myResult = Spacing.createSpacing(0, Integer.MAX_VALUE,
                                         getLineBreaksBeforeClassEnd(),
                                         mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_BEFORE_RBRACE);
      }
      else {
        int blankLines = getLinesAroundField() + 1;
        myResult =
          Spacing.createSpacing(0, Integer.MAX_VALUE, blankLines, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
      }
    }
    else if (myRole2 == ChildRole.COMMA || myType2 == JavaTokenType.SEMICOLON) {
      createSpaceProperty(false, false, 0);
    }
    else if (myRole1 == ChildRole.COMMA) {
      createSpaceProperty(mySettings.SPACE_AFTER_COMMA, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
    }

    else if (myRole1 == ChildRole.MODIFIER_LIST) {
      processModifierList();
    }

    else if (myRole1 == ChildRole.LBRACE && myRole2 == ChildRole.RBRACE) {
      myResult = Spacing.createSpacing(0, 0, 1, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_BEFORE_RBRACE);
    }

    else if (myRole2 == ChildRole.EXTENDS_LIST || myRole2 == ChildRole.IMPLEMENTS_LIST || myRole2 == ChildRole.PERMITS_LIST) {
      createSpaceInCode(true);
    }

    else if (myRole2 == ChildRole.TYPE_PARAMETER_LIST) {
      createSpaceInCode(myJavaSettings.SPACE_BEFORE_OPENING_ANGLE_BRACKET_IN_TYPE_PARAMETER);
    }

    else if (myRole2 == ChildRole.ARGUMENT_LIST) {
      createSpaceInCode(false);
    }
    else if (myRole2 == ChildRole.RBRACE) {
      myResult = Spacing.createSpacing(0, 0, 1, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_BEFORE_RBRACE);
    }
  }

  private int getLineBreaksBeforeClassEnd() {
    return myParent instanceof PsiAnonymousClass ? 1 : mySettings.BLANK_LINES_BEFORE_CLASS_END + 1;
  }

  /**
   * Initializes {@link #myResult} property with {@link Spacing} which {@code 'min line feeds'} property is defined
   * from {@link CommonCodeStyleSettings#BLANK_LINES_AROUND_CLASS} value.
   */
  private void setAroundClassSpacing() {
    myResult = Spacing.createSpacing(0, Integer.MAX_VALUE, mySettings.BLANK_LINES_AROUND_CLASS + 1,
                                     mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
  }

  private boolean processMethod() {
    if (myRole2 == ChildRole.METHOD || myType2 == JavaElementType.METHOD) {
      if (myRole1 == ChildRole.LBRACE) {
        myResult = Spacing.createSpacing(0, 0, 1, mySettings.KEEP_LINE_BREAKS, 0);
      }
      else if (myRole1 == ChildRole.CLASS_INITIALIZER) {
        int blankLines = myJavaSettings.BLANK_LINES_AROUND_INITIALIZER + 1;
        myResult =
          Spacing.createSpacing(0, Integer.MAX_VALUE, blankLines, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_BEFORE_RBRACE);
      }
      else {
        int blankLines = getLinesAroundMethod() + 1;
        myResult = Spacing.createSpacing(0, 0, blankLines, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
      }
    }
    else if (myRole1 == ChildRole.METHOD || myType1 == JavaElementType.METHOD) {
      if (myRole1 == ChildRole.LBRACE) {
        myResult = Spacing.createSpacing(0, 0, 1, mySettings.KEEP_LINE_BREAKS, 0);
      }
      else {
        int blankLines = getLinesAroundMethod() + 1;
        myResult = Spacing.createSpacing(0, 0, blankLines, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
      }
      if (myRole2 == ChildRole.RBRACE) {
        myResult = Spacing.createSpacing(0, Integer.MAX_VALUE, getLineBreaksBeforeClassEnd(),
                                         mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_BEFORE_RBRACE);
      }
      else {
        int blankLines = getLinesAroundMethod() + 1;
        myResult =
          Spacing.createSpacing(0, Integer.MAX_VALUE, blankLines, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
      }
    }
    return myResult != null;
  }

  /**
   * Allows to calculate {@code 'min line feed'} setting of the {@link Spacing} to be used between two closing braces
   * (assuming that left AST node that ends with closing brace is given to this method).
   *
   * @param leftNode left AST node that ends with closing brace
   * @return {@code 'min line feed'} setting of {@link Spacing} object to use for the given AST node and
   * closing brace
   */
  private static int getMinLineFeedsBetweenRBraces(ASTNode leftNode) {
    // The general idea is to return zero in situation when opening curly braces goes one after other, e.g.
    //     new Expectations() {{
    //         foo();}}
    // We don't want line feed between closing curly braces here.

    if (leftNode == null || leftNode.getElementType() != JavaElementType.CLASS_INITIALIZER) {
      return 1;
    }

    ASTNode lbraceCandidate = leftNode.getTreePrev();
    return lbraceCandidate != null && lbraceCandidate.getElementType() == JavaTokenType.LBRACE ? 0 : 1;
  }

  private int getLinesAroundMethod() {
    boolean useInterfaceMethodSpacing = !isClass(myParent) || isAbstractMethod(myChild1) && isAbstractMethod(myChild2);
    return useInterfaceMethodSpacing ? mySettings.BLANK_LINES_AROUND_METHOD_IN_INTERFACE : mySettings.BLANK_LINES_AROUND_METHOD;
  }

  private int getLinesAroundField() {
    boolean isParentAClass = isClass(myParent);
    if (isParentAClass && JavaFormatterAnnotationUtil.isFieldWithAnnotations(myChild2)) {
      return myJavaSettings.BLANK_LINES_AROUND_FIELD_WITH_ANNOTATIONS;
    } else if (isParentAClass) {
      return mySettings.BLANK_LINES_AROUND_FIELD;
    }
    else {
      return mySettings.BLANK_LINES_AROUND_FIELD_IN_INTERFACE;
    }
  }

  private static boolean isClass(PsiElement parent) {
    if (parent instanceof PsiClass) {
      return !((PsiClass)parent).isInterface();
    }
    return false;
  }

  private static boolean isAbstractMethod(ASTNode node) {
    return node.getPsi() instanceof PsiMethod method && JavaFormatterUtil.isExplicitlyAbstract(method);
  }

  @Override
  public void visitInstanceOfExpression(@NotNull PsiInstanceOfExpression expression) {
    createSpaceInCode(true);
  }

  @Override
  public void visitEnumConstantInitializer(@NotNull PsiEnumConstantInitializer enumConstantInitializer) {
    if (myRole2 == ChildRole.EXTENDS_LIST || myRole2 == ChildRole.IMPLEMENTS_LIST || myRole2 == ChildRole.PERMITS_LIST) {
      createSpaceInCode(true);
    }
    else {
      processMethod();
    }
  }

  @Override
  public void visitPatternVariable(@NotNull PsiPatternVariable variable) {
    super.visitPatternVariable(variable);
    if (myType1 == JavaElementType.TYPE && myType2 == JavaTokenType.IDENTIFIER) {
      createSpaceInCode(true);
    }
  }

  private ImportHelperBase myImportHelper;
  @Override
  public void visitImportList(@NotNull PsiImportList list) {
    if (ElementType.IMPORT_STATEMENT_BASE_BIT_SET.contains(myType1) &&
        ElementType.IMPORT_STATEMENT_BASE_BIT_SET.contains(myType2)) {
      if (myImportHelper == null) myImportHelper = new ImportHelperBase(myJavaSettings);
      int emptyLines = myImportHelper.getEmptyLinesBetween(
        SourceTreeToPsiMap.treeToPsiNotNull(myChild1),
        SourceTreeToPsiMap.treeToPsiNotNull(myChild2)
      ) + 1;
      myResult = Spacing.createSpacing(0, 0, emptyLines,
                                       mySettings.KEEP_LINE_BREAKS,
                                       mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
    }
  }

  @Override
  public void visitFile(@NotNull PsiFile psiFile) {
    if (myType1 == JavaElementType.PACKAGE_STATEMENT) {
      int lf = mySettings.BLANK_LINES_AFTER_PACKAGE + 1;
      myResult = Spacing.createSpacing(0, 0, lf, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
    }

    else if (myType2 == JavaElementType.PACKAGE_STATEMENT) {
      int lf = mySettings.BLANK_LINES_BEFORE_PACKAGE + 1;
      // special case when header before package
      int keepLines = myType1 == JavaTokenType.C_STYLE_COMMENT
                      ? mySettings.KEEP_BLANK_LINES_BETWEEN_PACKAGE_DECLARATION_AND_HEADER
                      : mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS;
      myResult = Spacing.createSpacing(0, 0, lf, mySettings.KEEP_LINE_BREAKS, keepLines);
    }

    else if (myType1 == JavaElementType.IMPORT_LIST) {
      int lf = mySettings.BLANK_LINES_AFTER_IMPORTS + 1;
      myResult = Spacing.createSpacing(0, 0, lf, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
    }

    else if (myType2 == JavaElementType.IMPORT_LIST) {
      int lf = mySettings.BLANK_LINES_BEFORE_IMPORTS + 1;
      myResult = Spacing.createSpacing(0, 0, lf, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
    }
    else if (myType2 == JavaElementType.CLASS) {
      int lf = mySettings.BLANK_LINES_AROUND_CLASS + 1;
      myResult = Spacing.createSpacing(0, 0, lf, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_DECLARATIONS);
    }
  }

  @Override
  public void visitWhileStatement(@NotNull PsiWhileStatement statement) {
    if (myRole2 == ChildRole.LPARENTH) {
      createSpaceInCode(mySettings.SPACE_BEFORE_WHILE_PARENTHESES);
    }
    else if (myRole1 == ChildRole.LPARENTH || myRole2 == ChildRole.RPARENTH) {
      createSpaceInCode(mySettings.SPACE_WITHIN_WHILE_PARENTHESES);
    }
    else if (myRole2 == ChildRole.LOOP_BODY || myType2 == JavaElementType.CODE_BLOCK) {
      if (myType2 == JavaElementType.BLOCK_STATEMENT) {
        myResult = getSpaceBeforeLBrace(myChild2, mySettings.SPACE_BEFORE_WHILE_LBRACE, null);
      }
      else {
        createSpacingBeforeElementInsideControlStatement();
      }
    }
  }

  @Override
  public void visitDoWhileStatement(@NotNull PsiDoWhileStatement statement) {
    if (myRole1 == ChildRole.WHILE_KEYWORD && myRole2 == ChildRole.LPARENTH) {
      createSpaceInCode(mySettings.SPACE_BEFORE_WHILE_PARENTHESES);
    }
    else if (myRole1 == ChildRole.LPARENTH || myRole2 == ChildRole.RPARENTH) {
      createSpaceInCode(mySettings.SPACE_WITHIN_WHILE_PARENTHESES);
    }
    else if (myRole2 == ChildRole.LOOP_BODY) {
      if (myType2 == JavaElementType.BLOCK_STATEMENT) {
        myResult = getSpaceBeforeLBrace(myChild2, mySettings.SPACE_BEFORE_DO_LBRACE, null);
      }
      else {
        createSpacingBeforeElementInsideControlStatement();
      }
    }
    else if (myRole1 == ChildRole.LOOP_BODY || myType2 == JavaElementType.CODE_BLOCK) {
      processOnNewLineCondition(mySettings.WHILE_ON_NEW_LINE, mySettings.SPACE_BEFORE_WHILE_KEYWORD);
    }
  }

  private void processOnNewLineCondition(boolean onNewLine, boolean createSpaceInline) {
    if (onNewLine) {
      if (!mySettings.KEEP_SIMPLE_BLOCKS_IN_ONE_LINE) {
        myResult = Spacing.createSpacing(1, 1, 1, mySettings.KEEP_LINE_BREAKS, mySettings.KEEP_BLANK_LINES_IN_CODE);
      }
      else {
        myResult =
