// 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.plugins.groovy.formatter.processors;

import com.intellij.formatting.Block;
import com.intellij.formatting.Spacing;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.psi.formatter.FormatterUtil;
import com.intellij.psi.impl.source.SourceTreeToPsiMap;
import com.intellij.psi.impl.source.tree.CompositeElement;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.GroovyLanguage;
import org.jetbrains.plugins.groovy.codeStyle.GroovyCodeStyleSettings;
import org.jetbrains.plugins.groovy.ext.spock.SpecificationKt;
import org.jetbrains.plugins.groovy.formatter.FormattingContext;
import org.jetbrains.plugins.groovy.formatter.GeeseUtil;
import org.jetbrains.plugins.groovy.formatter.blocks.GroovyBlock;
import org.jetbrains.plugins.groovy.formatter.blocks.GroovyBlockGenerator;
import org.jetbrains.plugins.groovy.formatter.blocks.SyntheticGroovyBlock;
import org.jetbrains.plugins.groovy.formatter.models.spacing.SpacingTokens;
import org.jetbrains.plugins.groovy.lang.groovydoc.lexer.GroovyDocTokenTypes;
import org.jetbrains.plugins.groovy.lang.groovydoc.parser.GroovyDocElementTypes;
import org.jetbrains.plugins.groovy.lang.groovydoc.psi.api.*;
import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes;
import org.jetbrains.plugins.groovy.lang.lexer.TokenSets;
import org.jetbrains.plugins.groovy.lang.parser.GrBlockElementType;
import org.jetbrains.plugins.groovy.lang.parser.GroovyElementTypes;
import org.jetbrains.plugins.groovy.lang.parser.GroovyEmptyStubElementTypes;
import org.jetbrains.plugins.groovy.lang.parser.GroovyStubElementTypes;
import org.jetbrains.plugins.groovy.lang.psi.GroovyElementVisitor;
import org.jetbrains.plugins.groovy.lang.psi.GroovyFileBase;
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElement;
import org.jetbrains.plugins.groovy.lang.psi.api.*;
import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.GrListOrMap;
import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.annotation.GrAnnotation;
import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.annotation.GrAnnotationArgumentList;
import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.annotation.GrAnnotationArrayInitializer;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.*;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrNamedArgument;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrSpreadArgument;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrOpenBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.branch.GrAssertStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.clauses.GrForInClause;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.*;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrStringInjection;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrIndexProperty;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrMethodCallExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameterList;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrAnonymousClassDefinition;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrRecordDefinition;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinitionBody;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrAnnotationMethod;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrEnumConstant;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod;
import org.jetbrains.plugins.groovy.lang.psi.api.toplevel.GrTopStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.types.GrArrayTypeElement;
import org.jetbrains.plugins.groovy.lang.psi.api.types.GrTypeArgumentList;
import org.jetbrains.plugins.groovy.lang.psi.api.types.GrTypeParameterList;
import org.jetbrains.plugins.groovy.lang.psi.impl.PsiImplUtil;

import java.util.Iterator;

import static org.jetbrains.plugins.groovy.formatter.blocks.BlocksKt.shouldHandleAsSimpleClosure;
import static org.jetbrains.plugins.groovy.lang.psi.GroovyElementTypes.*;
import static org.jetbrains.plugins.groovy.lang.psi.GroovyTokenSets.*;

public class GroovySpacingProcessor extends GroovyElementVisitor {
  private PsiElement myParent;

  private final GroovyCodeStyleSettings myGroovySettings;
  private final CommonCodeStyleSettings mySettings;
  private final boolean myForbidNewLineInSpacing;

  private Spacing myResult;
  private ASTNode myChild1;
  private ASTNode myChild2;
  private IElementType myType1;
  private IElementType myType2;

  public GroovySpacingProcessor(GroovyBlock block1, GroovyBlock block2, FormattingContext context) {
    mySettings = context.getSettings();
    myGroovySettings = context.getGroovySettings();
    myForbidNewLineInSpacing = context.isForbidNewLineInSpacing();

    final ASTNode node = block2.getNode();

    if (init(node)) return;
    if (manageComments()) return;

    if (myParent instanceof GroovyPsiElement) {
      ((GroovyPsiElement) myParent).accept(this);
    }
  }

  private boolean init(ASTNode node) {
    _init(node);

    if (myChild1 == null || myChild2 == null) {
      return true;
    }

    PsiElement psi1 = myChild1.getPsi();
    PsiElement psi2 = myChild2.getPsi();
    if (psi1 == null || psi2 == null) {
      return true;
    }

    if (psi1.getLanguage() != GroovyLanguage.INSTANCE || psi2.getLanguage() != GroovyLanguage.INSTANCE) {
      return true;
    }
    return false;
  }

  private void _init(final @Nullable ASTNode child) {
    if (child == null) return;

    ASTNode treePrev = child.getTreePrev();
    while (treePrev != null && isWhiteSpace(treePrev)) {
      treePrev = treePrev.getTreePrev();
    }

    if (treePrev == null) {
      _init(child.getTreeParent());
    }
    else {
      myChild2 = child;
      myType2 = myChild2.getElementType();

      myChild1 = treePrev;
      myType1 = myChild1.getElementType();
      final CompositeElement parent = (CompositeElement)treePrev.getTreeParent();
      myParent = SourceTreeToPsiMap.treeElementToPsi(parent);
    }
  }

  private boolean manageComments() {
    if (mySettings.KEEP_FIRST_COLUMN_COMMENT && TokenSets.COMMENT_SET.contains(myType2)) {
      if (!isAfterElementOrSemi(GroovyStubElementTypes.IMPORT)) {
        myResult = Spacing.createKeepingFirstColumnSpacing(0, Integer.MAX_VALUE, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
        return true;
      }
      return false;
    }

    ASTNode prev = FormatterUtil.getPreviousNonWhitespaceLeaf(myChild2);
    if (prev != null && prev.getElementType() == GroovyTokenTypes.mNLS) {
      prev = FormatterUtil.getPreviousNonWhitespaceLeaf(prev);
    }
    if (prev != null && prev.getElementType() == GroovyTokenTypes.mSL_COMMENT) {
      myResult = Spacing.createSpacing(0, Integer.MAX_VALUE, 1, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
      return true;
    }
    return false;
  }

  @Override
  public void visitLiteralExpression(@NotNull GrLiteral literal) {
    createSpaceInCode(false);
  }

  @Override
  public void visitGStringInjection(@NotNull GrStringInjection injection) {
    createSpaceInCode(false);
  }

  @Override
  public void visitLabeledStatement(@NotNull GrLabeledStatement labeledStatement) {
    if (myType1 == GroovyTokenTypes.mCOLON) {
      if (myGroovySettings.INDENT_LABEL_BLOCKS && !(myType2 == GroovyElementTypes.LITERAL)) {
        createLF(true);
      }
      else {
        createSpaceInCode(true);
      }
    }
  }

  @Override
  public void visitAnnotation(@NotNull GrAnnotation annotation) {
    if (myType2 == GroovyEmptyStubElementTypes.ANNOTATION_ARGUMENT_LIST) {
      createSpaceInCode(mySettings.SPACE_BEFORE_ANOTATION_PARAMETER_LIST);
    }
  }

  @Override
  public void visitAnnotationArgumentList(@NotNull GrAnnotationArgumentList annotationArgumentList) {
    if (isWithinParentheses()) {
      createSpaceInCode(mySettings.SPACE_WITHIN_ANNOTATION_PARENTHESES);
    }
  }

  @Override
  public void visitArgumentList(@NotNull GrArgumentList list) {
    if (list.getParent() instanceof GrIndexProperty) {
      processBrackets(
        mySettings.SPACE_WITHIN_BRACKETS,
        null,
        mySettings.CALL_PARAMETERS_LPAREN_ON_NEXT_LINE,
        mySettings.CALL_PARAMETERS_RPAREN_ON_NEXT_LINE
      );
    }
    else {
      processParentheses(
        mySettings.SPACE_WITHIN_METHOD_CALL_PARENTHESES,
        mySettings.SPACE_WITHIN_EMPTY_METHOD_CALL_PARENTHESES,
        mySettings.CALL_PARAMETERS_LPAREN_ON_NEXT_LINE,
        mySettings.CALL_PARAMETERS_RPAREN_ON_NEXT_LINE
      );
    }
  }

  private void createDependentLFSpacing(final boolean isLineFeed, final boolean isSpace, final @NotNull TextRange range) {
    if (isLineFeed) {
      myResult = Spacing.createDependentLFSpacing(isSpace ? 1 : 0, isSpace ? 1 : 0, range, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
    }
    else {
      createSpaceInCode(isSpace);
    }
  }

  @Override
  public void visitLambdaExpression(@NotNull GrLambdaExpression expression) {
    boolean spaceAroundArrow = mySettings.SPACE_AROUND_LAMBDA_ARROW;

    if (myType2 == T_ARROW) {
      createSpaceProperty(spaceAroundArrow, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
    }
    if (myType2 == BLOCK_LAMBDA_BODY) {
      createSpaceBeforeLBrace(spaceAroundArrow, mySettings.LAMBDA_BRACE_STYLE, null, mySettings.KEEP_SIMPLE_LAMBDAS_IN_ONE_LINE);
    }
    if (myType2 == EXPRESSION_LAMBDA_BODY) {
      createSpaceProperty(spaceAroundArrow, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
    }
  }

  @Override
  public void visitBlockLambdaBody(@NotNull GrBlockLambdaBody body) {
    if (myType2 == GroovyTokenTypes.mLCURLY) {
      myResult = Spacing.createSpacing(0, 0, 0, false, 0);
    }
    if (myType1 == GroovyTokenTypes.mLCURLY || myType2 == GroovyTokenTypes.mRCURLY) {
      boolean simpleLambda = shouldHandleAsSimpleLambda(body);
      int spaces = simpleLambda && mySettings.SPACE_WITHIN_BRACES ? 1 : 0;
      myResult = Spacing.createSpacing(spaces, spaces, simpleLambda ? 0 : 1, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
    }
  }

  private boolean shouldHandleAsSimpleLambda(GrBlockLambdaBody lambda) {
    return mySettings.KEEP_SIMPLE_LAMBDAS_IN_ONE_LINE && !lambda.textContains('\n');
  }

  @Override
  public void visitConditionalExpression(@NotNull GrConditionalExpression expression) {
    if (myType2 == GroovyTokenTypes.mQUESTION) {
      createSpaceInCode(mySettings.SPACE_BEFORE_QUEST);
    }
    else if (myType1 == GroovyTokenTypes.mQUESTION) {
      createSpaceInCode(mySettings.SPACE_AFTER_QUEST);
    }
    else if (myType2 == GroovyTokenTypes.mCOLON) {
      createSpaceInCode(mySettings.SPACE_BEFORE_COLON);
    }
    else if (myType1 == GroovyTokenTypes.mCOLON) {
      createSpaceInCode(mySettings.SPACE_AFTER_COLON);
    }
  }

  @Override
  public void visitElvisExpression(@NotNull GrElvisExpression expression) {
    if (myType1 == GroovyTokenTypes.mELVIS) {
      createSpaceInCode(mySettings.SPACE_AFTER_COLON);
    }
    else if (myType2 == GroovyTokenTypes.mELVIS) {
      createSpaceInCode(mySettings.SPACE_BEFORE_QUEST);
    }
  }

  @Override
  public void visitMethodCallExpression(@NotNull GrMethodCallExpression methodCallExpression) {
    if (myType2 == GroovyElementTypes.ARGUMENTS) {
      manageSpaceBeforeCallLParenth();
    }
    else if (myType2 == GroovyElementTypes.CLOSABLE_BLOCK) {
      createSpaceInCode(myGroovySettings.SPACE_BEFORE_CLOSURE_LBRACE);
    }
  }

  private void manageSpaceBeforeCallLParenth() {
    createSpaceInCode(mySettings.SPACE_BEFORE_METHOD_CALL_PARENTHESES);
  }

  @Override
  public void visitApplicationStatement(@NotNull GrApplicationStatement applicationStatement) {
    if (myType2 == GroovyElementTypes.ARGUMENTS) manageSpaceBeforeCallLParenth();
  }

  @Override
  public void visitIndexProperty(@NotNull GrIndexProperty expression) {
    if (myType2 == GroovyTokenTypes.mQUESTION) createSpaceInCode(false);
    if (myType2 == GroovyElementTypes.ARGUMENTS) manageSpaceBeforeCallLParenth();
  }

  @Override
  public void visitConstructorInvocation(@NotNull GrConstructorInvocation invocation) {
    if (myType2 == GroovyElementTypes.ARGUMENTS) manageSpaceBeforeCallLParenth();
  }

  @Override
  public void visitNewExpression(@NotNull GrNewExpression newExpression) {
    if (myType1 == GroovyTokenTypes.kNEW) {
      createSpaceInCode(true);
    }
    else if (myType2 == GroovyElementTypes.ARGUMENTS) {
      manageSpaceBeforeCallLParenth();
    }
    else if (myType2 == GroovyElementTypes.ARRAY_DECLARATOR) {
      createSpaceInCode(false);
    }
    else if (myType2 == ARRAY_INITIALIZER) {
      createSpaceInCode(mySettings.SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE);
    }
  }

  @Override
  public void visitArrayDeclaration(@NotNull GrArrayDeclaration arrayDeclaration) {
    createSpaceInCode(false);
  }

  @Override
  public void visitArrayTypeElement(@NotNull GrArrayTypeElement typeElement) {
    createSpaceInCode(false);
  }

  private void manageSpaceInTuple() {
    if (isWithinParentheses()) {
      createSpaceInCode(myGroovySettings.SPACE_WITHIN_TUPLE_EXPRESSION);
    }
  }

  @Override
  public void visitEnumConstant(@NotNull GrEnumConstant enumConstant) {
    if (myType1 == GroovyStubElementTypes.MODIFIER_LIST) {
      createSpaceInCode(true);
    } else  {
      manageSpaceBeforeCallLParenth();
    }
  }

  @Override
  public void visitVariableDeclaration(@NotNull GrVariableDeclaration variableDeclaration) {
    manageSpaceInTuple();
  }


  @Override
  public void visitTuple(@NotNull GrTuple tuple) {
    manageSpaceInTuple();
  }

  @Override
  public void visitSpreadArgument(@NotNull GrSpreadArgument spreadArgument) {
    if (myType1 == GroovyTokenTypes.mSTAR) {
      createSpaceInCode(mySettings.SPACE_AROUND_UNARY_OPERATOR);
    }
  }

  @Override
  public void visitFile(@NotNull GroovyFileBase file) {
    if (isAfterElementOrSemi(GroovyStubElementTypes.PACKAGE_DEFINITION)) {
      myResult = Spacing.createSpacing(0, 0, mySettings.BLANK_LINES_AFTER_PACKAGE + 1, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
    }
    else if (myType2 == GroovyStubElementTypes.PACKAGE_DEFINITION) {
      myResult = Spacing.createSpacing(0, 0, mySettings.BLANK_LINES_BEFORE_PACKAGE + 1, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
    }
    else if (isLeftOrRight(TokenSets.TYPE_DEFINITIONS)) {
      if (myType1 == GroovyDocElementTypes.GROOVY_DOC_COMMENT) {
        if (isGroovyDocFormattingAllowed()) createLF(true);
      }
      else {
        myResult = Spacing.createSpacing(0, 0, mySettings.BLANK_LINES_AROUND_CLASS + 1, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
      }
    }
    else if (isAfterElementOrSemi(GroovyStubElementTypes.IMPORT) && myType2 != GroovyStubElementTypes.IMPORT) { //after imports
      myResult = Spacing.createSpacing(0, 0, mySettings.BLANK_LINES_AFTER_IMPORTS + 1, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
    }
    else if (myType1 != GroovyStubElementTypes.IMPORT && !isSemiAfter(GroovyStubElementTypes.IMPORT) && myType2 ==
                                                                                                        GroovyStubElementTypes.IMPORT) { //before imports
      myResult = Spacing.createSpacing(0, 0, mySettings.BLANK_LINES_BEFORE_IMPORTS, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
    }
    else if (isAfterElementOrSemi(GroovyStubElementTypes.IMPORT) && myType2 == GroovyStubElementTypes.IMPORT) {
      myResult = Spacing.createSpacing(0, 0, 1, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
    }
    else {
      processClassMembers(null);
    }
  }

  private boolean isAfterElementOrSemi(final IElementType elementType) {
    return myType1 == elementType && myType2 != GroovyTokenTypes.mSEMI || isSemiAfter(elementType);
  }

  private boolean isSemiAfter(@NotNull IElementType statement) {
    return myType1 == GroovyTokenTypes.mSEMI && getStatementTypeBySemi(myChild1) == statement;
  }

  private boolean isSemiAfter(@NotNull TokenSet set) {
    return myType1 == GroovyTokenTypes.mSEMI && set.contains(getStatementTypeBySemi(myChild1));
  }

  private static @Nullable IElementType getStatementTypeBySemi(@NotNull ASTNode semi) {
    final GrTopStatement statement = getStatementBySemicolon(semi.getPsi());
    if (statement == null) return null;
    return statement.getNode().getElementType();
  }

  private static @Nullable GrTopStatement getStatementBySemicolon(@NotNull PsiElement semi) {
    PsiElement prev = semi.getPrevSibling();
    while (prev != null &&
           TokenSets.WHITE_SPACES_OR_COMMENTS.contains(prev.getNode().getElementType()) &&
           prev.getText().indexOf('\n') < 0) {
      prev = prev.getPrevSibling();
    }

    if (prev instanceof GrTopStatement) return (GrTopStatement)prev;
    return null;
  }

  @Override
  public void visitClosure(@NotNull GrClosableBlock closure) {
    ASTNode rBraceAtTheEnd = GeeseUtil.getClosureRBraceAtTheEnd(myChild1);
    boolean spacesWithinBraces = closure.getParent() instanceof GrStringInjection
                                 ? myGroovySettings.SPACE_WITHIN_GSTRING_INJECTION_BRACES
                                 : mySettings.SPACE_WITHIN_BRACES;
    boolean simpleClosure = shouldHandleAsSimpleClosure(closure, mySettings) || myForbidNewLineInSpacing;
    int spaces = simpleClosure && spacesWithinBraces ? 1 : 0;
    if (myGroovySettings.USE_FLYING_GEESE_BRACES && myType2 == GroovyTokenTypes.mRCURLY && rBraceAtTheEnd != null) {
      String text = rBraceAtTheEnd.getTreeParent().getText();
      if (text.indexOf('\n') < 0) {
        /* the case:
       foo {
         bar {print x}<we are here>}
        */
        myResult = Spacing.createSpacing(1, 1, simpleClosure ? 0 : 1, false, 1);
      }
      else {
        myResult = Spacing.createSpacing(0, 0, 0, true, 100, 0);
      }
    }
    else if (myType1 == GroovyTokenTypes.mLCURLY && myType2 == GroovyTokenTypes.mRCURLY) {  //empty closure
      createSpaceInCode(false);
    }
    else if (myType1 == GroovyTokenTypes.mLCURLY && myType2 != GroovyEmptyStubElementTypes.PARAMETER_LIST && myType2 !=
                                                                                                             GroovyTokenTypes.mCLOSABLE_BLOCK_OP || myType2 ==
                                                                                                                                            GroovyTokenTypes.mRCURLY) { //spaces between statements
      myResult = Spacing.createSpacing(spaces, spaces, simpleClosure ? 0 : 1, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
    }
    else if (myType1 == GroovyTokenTypes.mCLOSABLE_BLOCK_OP) {
      myResult = GroovySpacingProcessorBasic.createDependentSpacingForClosure(mySettings, myGroovySettings, closure, true);
    }
    else if (myType1 == GroovyTokenTypes.mLCURLY) {
      createSpaceInCode(spacesWithinBraces);
    }

  }

  @Override
  public void visitOpenBlock(@NotNull GrOpenBlock block) {
    boolean isMethod = block.getParent() instanceof GrMethod;
    boolean keepInOneLine = myForbidNewLineInSpacing ||
                            isMethod ? mySettings.KEEP_SIMPLE_METHODS_IN_ONE_LINE : mySettings.KEEP_SIMPLE_BLOCKS_IN_ONE_LINE;

    if (myType1 == GroovyTokenTypes.mLCURLY && myType2 == GroovyTokenTypes.mRCURLY) {
      createLF(!keepInOneLine);
    }
    else if (myType1 == GroovyTokenTypes.mLCURLY) {
      if (keepInOneLine) {
        createDependentLFSpacing(true, mySettings.SPACE_WITHIN_BRACES, block.getTextRange());
      }
      else {
        int lineFeedsCount = isMethod ? mySettings.BLANK_LINES_BEFORE_METHOD_BODY + 1 : 1;
        myResult = Spacing.createSpacing(0, 0, lineFeedsCount, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
      }
    }
    else if (myType2 == GroovyTokenTypes.mRCURLY) {
      if (keepInOneLine) {
        createDependentLFSpacing(true, mySettings.SPACE_WITHIN_BRACES, block.getTextRange());
      }
      else {
        createLF(true);
      }
    }
  }

  @Override
  public void visitTypeDefinition(@NotNull GrTypeDefinition typeDefinition) {
    if (myType2 == GroovyEmptyStubElementTypes.CLASS_BODY) {
      if (typeDefinition instanceof GrAnonymousClassDefinition) {
        createSpaceProperty(mySettings.SPACE_BEFORE_CLASS_LBRACE, true, 100); //don't manually remove line feeds because this line is ambiguous
      }
      else {
        PsiElement nameIdentifier = typeDefinition.getNameIdentifierGroovy();
        int dependenceStart = nameIdentifier.getTextRange().getStartOffset();
        final TextRange range = new TextRange(dependenceStart, myChild1.getTextRange().getEndOffset());
        createSpaceBeforeLBrace(mySettings.SPACE_BEFORE_CLASS_LBRACE, mySettings.CLASS_BRACE_STYLE, range, false);
      }
    }
    else if (myType2 == GroovyEmptyStubElementTypes.TYPE_PARAMETER_LIST) {
      createSpaceInCode(false);
    }
    else if (myType2 == GroovyElementTypes.ARGUMENTS) {
      manageSpaceBeforeCallLParenth();
    }
  }

  @Override
  public void visitTypeDefinitionBody(@NotNull GrTypeDefinitionBody typeDefinitionBody) {
    if (myType1 == GroovyTokenTypes.mLCURLY && myType2 == GroovyTokenTypes.mRCURLY) {
      if (mySettings.KEEP_SIMPLE_CLASSES_IN_ONE_LINE) {
        createSpaceInCode(false);
      }
      else {
        createLF(true);
      }
    }
    else if (myType1 == GroovyTokenTypes.mLCURLY) {
      myResult = Spacing.createSpacing(0, 0, mySettings.BLANK_LINES_AFTER_CLASS_HEADER + 1, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
    }
    else if (myType2 == GroovyTokenTypes.mRCURLY) {
      createLF(true);
    }
    else {
      processClassMembers(typeDefinitionBody);
    }
  }

  private void processClassMembers(@Nullable GrTypeDefinitionBody typeDefinitionBody) {
    final boolean isInterface = typeDefinitionBody != null && ((GrTypeDefinition)typeDefinitionBody.getParent()).isInterface();

    if (myType2 == GroovyTokenTypes.mSEMI) return;

    if (typeDefinitionBody != null) { //check variable definitions only inside class body
      if ((myType1 == GroovyStubElementTypes.VARIABLE_DECLARATION || isSemiAfter(GroovyStubElementTypes.VARIABLE_DECLARATION)) && TokenSets.METHOD_DEFS.contains(myType2)) {
        final int minBlankLines = Math.max(
          isInterface ? mySettings.BLANK_LINES_AROUND_METHOD_IN_INTERFACE : mySettings.BLANK_LINES_AROUND_METHOD,
          isInterface ? mySettings.BLANK_LINES_AROUND_FIELD_IN_INTERFACE : mySettings.BLANK_LINES_AROUND_FIELD
        );
        myResult = Spacing.createSpacing(0, 0, minBlankLines + 1, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
        return;
      }
      else if (myType1 == GroovyStubElementTypes.VARIABLE_DECLARATION || isSemiAfter(GroovyStubElementTypes.VARIABLE_DECLARATION) || myType2 ==
                                                                                                                                     GroovyStubElementTypes.VARIABLE_DECLARATION) {
        final int minBlankLines =
          isInterface ? mySettings.BLANK_LINES_AROUND_FIELD_IN_INTERFACE : mySettings.BLANK_LINES_AROUND_FIELD;
        myResult = Spacing.createSpacing(0, 0, minBlankLines + 1, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
        return;
      }
    }

    if (TokenSets.METHOD_DEFS.contains(myType1) || isSemiAfter(TokenSets.METHOD_DEFS) || TokenSets.METHOD_DEFS.contains((myType2))) {
      if (myType1 == GroovyDocElementTypes.GROOVY_DOC_COMMENT) {
        if (isGroovyDocFormattingAllowed()) createLF(true);
      }
      else {
        final int minBlankLines = isInterface ? mySettings.BLANK_LINES_AROUND_METHOD_IN_INTERFACE : mySettings.BLANK_LINES_AROUND_METHOD;
        myResult = Spacing.createSpacing(0, 0, minBlankLines + 1, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
      }
      return;
    }

    if (TokenSets.TYPE_DEFINITIONS.contains(myType1) || isSemiAfter(TokenSets.TYPE_DEFINITIONS) || TokenSets.TYPE_DEFINITIONS.contains((myType2)) ) {
      if (myType1 == GroovyDocElementTypes.GROOVY_DOC_COMMENT) {
        if (isGroovyDocFormattingAllowed()) createLF(true);
      }
      else {
        final int minBlankLines = mySettings.BLANK_LINES_AROUND_CLASS;
        myResult = Spacing.createSpacing(0, 0, minBlankLines + 1, mySettings.KEEP_LINE_BREAKS, keepBlankLines());
      }
    }
  }

  @Override
  public void visitTypeArgumentList(@NotNull GrTypeArgumentList typeArgumentList) {
    if (myType1 == GroovyTokenTypes.mLT || myType2 == GroovyTokenTypes.mGT) {
      createSpaceInCode(false);
    }
  }

  @Override
  public void visitTypeParameterList(@NotNull GrTypeParameterList list) {
    if (myType1 == GroovyTokenTypes.mCOMMA) {
      createSpaceInCode(mySettings.SPACE_AFTER_COMMA_IN_TYPE_ARGUMENTS);
    }
    else if (myType1 == GroovyTokenTypes.mLT && myType2 == GroovyStubElementTypes.TYPE_PARAMETER ||
             myType1 == GroovyStubElementTypes.TYPE_PARAMETER && myType2 == GroovyTokenTypes.mGT) {
      createSpaceInCode(false);
    }
  }

  @Override
  public void visitForInClause(@NotNull GrForInClause forInClause) {
    if (myType1 == GroovyStubElementTypes.PARAMETER && myType2 == GroovyTokenTypes.mCOLON) {
      createSpaceInCode(true);
    }
  }

  @Override
  public void visitCastExpression(@NotNull GrTypeCastExpression typeCastExpression) {
    if (SpacingTokens.LEFT_BRACES.contains(myType1) || SpacingTokens.RIGHT_BRACES.contains(myType2)) {
      createSpaceInCode(mySettings.SPACE_WITHIN_CAST_PARENTHESES);
    }
    else if (myType1 == GroovyTokenTypes.mRPAREN) {
      createSpaceInCode(mySettings.SPACE_AFTER_TYPE_CAST);
    }
  }

  @Override
  public void visitMethod(@NotNull GrMethod method) {
    if (myType2 == PARAMETER_LIST) {
      createSpaceInCode(mySettings.SPACE_BEFORE_METHOD_PARENTHESES);
    }
    else if (myType1 == PARAMETER_LIST && myType2 == GroovyStubElementTypes.THROWS_CLAUSE) {
      if (mySettings.THROWS_KEYWORD_WRAP == CommonCodeStyleSettings.WRAP_ALWAYS) {
        createLF(true);
      }
      else {
        createSpaceInCode(true);
      }
    }
    else if (isOpenBlock(myType2)) {
      PsiElement methodName = method.getNameIdentifier();
      int dependencyStart = methodName == null ? myParent.getTextRange().getStartOffset() : methodName.getTextRange().getStartOffset();
      createSpaceBeforeLBrace(mySettings.SPACE_BEFORE_METHOD_LBRACE, mySettings.METHOD_BRACE_STYLE,
                              new TextRange(dependencyStart, myChild1.getTextRange().getEndOffset()),
                              mySettings.KEEP_SIMPLE_METHODS_IN_ONE_LINE);
    }
    else if (myType2 == GroovyEmptyStubElementTypes.TYPE_PARAMETER_LIST) {
      createSpaceInCode(true);
    }
  }

  @Override
  public void visitRecordDefinition(@NotNull GrRecordDefinition recordDefinition) {
    if (myType2 == PARAMETER_LIST) {
      createSpaceInCode(myGroovySettings.SPACE_BEFORE_RECORD_PARENTHESES);
    }
    visitTypeDefinition(recordDefinition);
  }

  @Override
  public void visitParameterList(@NotNull GrParameterList parameterList) {
    processParentheses(
      mySettings.SPACE_WITHIN_METHOD_PARENTHESES,
      mySettings.SPACE_WITHIN_EMPTY_METHOD_PARENTHESES,
      mySettings.METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE,
      mySettings.METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE
    );
  }

  private boolean processBrackets(@NotNull Boolean spaceWithin,
                                  @Nullable Boolean spaceWithinEmpty,
                                  @Nullable Boolean leftLF,
                                  @Nullable Boolean rightLF) {
    return processParentheses(spaceWithin, spaceWithinEmpty, leftLF, rightLF, T_LBRACK, T_RBRACK);
  }

  private boolean processParentheses(@NotNull Boolean spaceWithin,
                                     @Nullable Boolean spaceWithinEmpty,
                                     @Nullable Boolean leftLF,
                                     @Nullable Boolean rightLF) {
    return processParentheses(spaceWithin, spaceWithinEmpty, leftLF, rightLF, T_LPAREN, T_RPAREN);
  }

  private boolean processBraces(@NotNull Boolean spaceWithin,
                                @Nullable Boolean spaceWithinEmpty,
                                @Nullable Boolean leftLF,
                                @Nullable Boolean rightLF) {
    return processParentheses(spaceWithin, spaceWithinEmpty, leftLF, rightLF, T_LBRACE, T_RBRACE);
  }

  private boolean processParentheses(@NotNull Boolean spaceWithin,
                                     @Nullable Boolean spaceWithinEmpty,
                                     @Nullable Boolean leftLF,
                                     @Nullable Boolean rightLF,
                                     @NotNull IElementType leftType,
                                     @NotNull IElementType rightType) {
    if (myType1 == leftType && myType2 == rightType && spaceWithinEmpty != null) {
      createSpaceInCode(spaceWithinEmpty);
      return true;
    }
    else if (myType1 == leftType) {
      final ASTNode rparenth = findFrom(myChild1, rightType, true);
      if (rparenth == null || leftLF == null) {
        createSpaceInCode(spaceWithin);
      }
      else {
        final TextRange range = new TextRange(myChild1.getStartOffset(), rparenth.getTextRange().getEndOffset());
        createDependentLFSpacing(leftLF, spaceWithin, range);
      }
      return true;
    }
    else if (myType2 == rightType) {
      final ASTNode lparenth = findFrom(myChild1, leftType, false);
      if (lparenth == null || rightLF == null) {
        createSpaceInCode(spaceWithin);
      }
      else {
        final TextRange range = new TextRange(lparenth.getStartOffset(), myChild2.getTextRange().getEndOffset());
        createDependentLFSpacing(rightLF, spaceWithin, range);
      }
      return true;
    }
    else {
      return false;
    }
  }

  private boolean isGroovyDocFormattingAllowed() {
    return myGroovySettings.ENABLE_GROOVYDOC_FORMATTING;
  }

  @Override
  public void visitAnnotationMethod(@NotNull GrAnnotationMethod annotationMethod) {
    if (myType2 == GroovyTokenTypes.kDEFAULT) {
      createSpaceInCode(true);
    }
    else {
      super.visitAnnotationMethod(annotationMethod);
    }
  }

  @Override
  public void visitDocMethodReference(@NotNull GrDocMethodReference reference) {
    visitDocMember();
  }

  @Override
  public void visitDocFieldReference(@NotNull GrDocFieldReference reference) {
    visitDocMember();
  }

  private void visitDocMember() {
    if (isGroovyDocFormattingAllowed()) {
      createSpaceProperty(false, false, 0);
    } else {
      createLazySpace();
    }
  }

  @Override
  public void visitDocMethodParameterList(@NotNull GrDocMethodParams params) {
    if (!isGroovyDocFormattingAllowed()) {
      createLazySpace();
      return;
    }
    if (myType1 == GroovyDocTokenTypes.mGDOC_TAG_VALUE_LPAREN || myType2 == GroovyDocTokenTypes.mGDOC_TAG_VALUE_RPAREN || myType2 ==
                                                                                                                          GroovyDocTokenTypes.mGDOC_TAG_VALUE_COMMA) {
      createSpaceProperty(false, false, 0);
    }
    else {
      createSpaceInCode(true);
    }
  }

  @Override
  public void visitDocMethodParameter(@NotNull GrDocMethodParameter parameter) {
    if (!isGroovyDocFormattingAllowed()) {
      createLazySpace();
      return;
    }
    if (myChild1.getTreePrev() == null) {
      createSpaceInCode(true);
    }
  }

  @Override
  public void visitWhileStatement(@NotNull GrWhileStatement statement) {
    if (myType2 == GroovyTokenTypes.mLPAREN) {
      createSpaceInCode(mySettings.SPACE_BEFORE_WHILE_PARENTHESES);
    }
    else if (isWithinParentheses()) {
      createSpaceInCode(mySettings.SPACE_WITHIN_WHILE_PARENTHESES);
    }
    else if (myChild2.getPsi() instanceof GrBlockStatement) {
