// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.

package com.intellij.formatting;

import com.intellij.formatting.engine.ExpandableIndent;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import consulo.logging.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.psi.formatter.FormatterUtil;
import com.intellij.psi.formatter.FormattingDocumentModelImpl;
import com.intellij.psi.formatter.PsiBasedFormattingModel;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.SequentialTask;
import com.intellij.util.text.CharArrayUtil;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import javax.inject.Singleton;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import static com.intellij.formatting.FormatProcessor.FormatOptions;

@Singleton
public class FormatterImpl extends FormatterEx implements IndentFactory, WrapFactory, AlignmentFactory, SpacingFactory, FormattingModelFactory {
  private static final Logger LOG = Logger.getInstance(FormatterImpl.class);

  private final AtomicReference<FormattingProgressTask> myProgressTask = new AtomicReference<>();

  private final AtomicInteger myIsDisabledCount = new AtomicInteger();
  private final IndentImpl NONE_INDENT = new IndentImpl(Indent.Type.NONE, false, false);
  private final IndentImpl myAbsoluteNoneIndent = new IndentImpl(Indent.Type.NONE, true, false);
  private final IndentImpl myLabelIndent = new IndentImpl(Indent.Type.LABEL, false, false);
  private final IndentImpl myContinuationIndentRelativeToDirectParent = new IndentImpl(Indent.Type.CONTINUATION, false, true);
  private final IndentImpl myContinuationIndentNotRelativeToDirectParent = new IndentImpl(Indent.Type.CONTINUATION, false, false);
  private final IndentImpl myContinuationWithoutFirstIndentRelativeToDirectParent = new IndentImpl(Indent.Type.CONTINUATION_WITHOUT_FIRST, false, true);
  private final IndentImpl myContinuationWithoutFirstIndentNotRelativeToDirectParent = new IndentImpl(Indent.Type.CONTINUATION_WITHOUT_FIRST, false, false);
  private final IndentImpl myAbsoluteLabelIndent = new IndentImpl(Indent.Type.LABEL, true, false);
  private final IndentImpl myNormalIndentRelativeToDirectParent = new IndentImpl(Indent.Type.NORMAL, false, true);
  private final IndentImpl myNormalIndentNotRelativeToDirectParent = new IndentImpl(Indent.Type.NORMAL, false, false);
  private final SpacingImpl myReadOnlySpacing = new SpacingImpl(0, 0, 0, true, false, true, 0, false, 0);

  @Override
  public Alignment createAlignment(boolean applyToNonFirstBlocksOnLine, @Nonnull Alignment.Anchor anchor) {
    return new AlignmentImpl(applyToNonFirstBlocksOnLine, anchor);
  }

  @Override
  public Alignment createChildAlignment(final Alignment base) {
    AlignmentImpl result = new AlignmentImpl();
    result.setParent(base);
    return result;
  }

  @Override
  public Indent getNormalIndent(boolean relative) {
    return relative ? myNormalIndentRelativeToDirectParent : myNormalIndentNotRelativeToDirectParent;
  }

  @Override
  public Indent getNoneIndent() {
    return NONE_INDENT;
  }

  @Override
  public void setProgressTask(@Nonnull FormattingProgressTask progressIndicator) {
    if (!FormatterUtil.isFormatterCalledExplicitly()) {
      return;
    }
    myProgressTask.set(progressIndicator);
  }

  @Override
  public int getSpacingForBlockAtOffset(FormattingModel model, int offset) {
    SpacingImpl spacing = getSpacingBeforeBlockAtOffset(model, offset);
    if (spacing != null) {
      int minSpaces = spacing.getMinSpaces();
      if (minSpaces >= 0) {
        return minSpaces;
      }
    }
    return -1;
  }

  @Override
  public int getMinLineFeedsBeforeBlockAtOffset(FormattingModel model, int offset) {
    SpacingImpl spacing = getSpacingBeforeBlockAtOffset(model, offset);
    if (spacing != null) {
      int minLineFeeds = spacing.getMinLineFeeds();
      if (minLineFeeds >= 0) {
        return minLineFeeds;
      }
    }
    return -1;
  }

  private static SpacingImpl getSpacingBeforeBlockAtOffset(FormattingModel model, int offset) {
    Couple<Block> blockWithParent = getBlockAtOffset(null, model.getRootBlock(), offset);
    if (blockWithParent != null) {
      Block parentBlock = blockWithParent.first;
      Block targetBlock = blockWithParent.second;
      if (parentBlock != null && targetBlock != null) {
        Block prevBlock = findPreviousSibling(parentBlock, targetBlock);
        if (prevBlock != null) return (SpacingImpl)parentBlock.getSpacing(prevBlock, targetBlock);
      }
    }
    return null;
  }

  @Nullable
  private static Couple<Block> getBlockAtOffset(@Nullable Block parent, @Nonnull Block block, int offset) {
    TextRange textRange = block.getTextRange();
    int startOffset = textRange.getStartOffset();
    int endOffset = textRange.getEndOffset();
    if (startOffset == offset) {
      return Couple.of(parent, block);
    }
    if (startOffset > offset || endOffset < offset || block.isLeaf()) {
      return null;
    }
    for (Block subBlock : block.getSubBlocks()) {
      Couple<Block> result = getBlockAtOffset(block, subBlock, offset);
      if (result != null) {
        return result;
      }
    }
    return null;
  }

  @Nullable
  private static Block findPreviousSibling(@Nonnull Block parent, Block block) {
    Block result = null;
    for (Block subBlock : parent.getSubBlocks()) {
      if (subBlock == block) {
        return result;
      }
      result = subBlock;
    }
    return null;
  }

  @Override
  public Wrap createWrap(WrapType type, boolean wrapFirstElement) {
    return new WrapImpl(type, wrapFirstElement);
  }

  @Override
  public Wrap createChildWrap(final Wrap parentWrap, final WrapType wrapType, final boolean wrapFirstElement) {
    final WrapImpl result = new WrapImpl(wrapType, wrapFirstElement);
    result.registerParent((WrapImpl)parentWrap);
    return result;
  }

  @Override
  @Nonnull
  public Spacing createSpacing(int minOffset, int maxOffset, int minLineFeeds, final boolean keepLineBreaks, final int keepBlankLines) {
    return getSpacingImpl(minOffset, maxOffset, minLineFeeds, false, false, keepLineBreaks, keepBlankLines, false, 0);
  }

  @Override
  @Nonnull
  public Spacing getReadOnlySpacing() {
    return myReadOnlySpacing;
  }

  @Nonnull
  @Override
  public Spacing createDependentLFSpacing(int minSpaces, int maxSpaces, @Nonnull TextRange dependencyRange, boolean keepLineBreaks, int keepBlankLines, @Nonnull DependentSpacingRule rule) {
    return new DependantSpacingImpl(minSpaces, maxSpaces, dependencyRange, keepLineBreaks, keepBlankLines, rule);
  }

  @Nonnull
  @Override
  public Spacing createDependentLFSpacing(int minSpaces, int maxSpaces, @Nonnull List<TextRange> dependentRegion, boolean keepLineBreaks, int keepBlankLines, @Nonnull DependentSpacingRule rule) {
    return new DependantSpacingImpl(minSpaces, maxSpaces, dependentRegion, keepLineBreaks, keepBlankLines, rule);
  }

  @Nonnull
  private FormattingProgressCallback getProgressCallback() {
    FormattingProgressCallback result = myProgressTask.get();
    return result == null ? FormattingProgressCallback.EMPTY : result;
  }

  @Override
  public void format(final FormattingModel model, final CodeStyleSettings settings, final CommonCodeStyleSettings.IndentOptions indentOptions, final FormatTextRanges affectedRanges)
          throws IncorrectOperationException {
    try {
      validateModel(model);
      SequentialTask task = new MyFormattingTask() {
        @Nonnull
        @Override
        protected FormatProcessor buildProcessor() {
          FormatOptions options = new FormatOptions(settings, indentOptions, affectedRanges);
          FormatProcessor processor = new FormatProcessor(model.getDocumentModel(), model.getRootBlock(), options, getProgressCallback());
          processor.format(model, true);
          return processor;
        }
      };
      execute(task);
    }
    catch (FormattingModelInconsistencyException e) {
      LOG.error(e);
    }
  }

  public void formatWithoutModifications(final FormattingDocumentModel model,
                                         final Block rootBlock,
                                         final CodeStyleSettings settings,
                                         final CommonCodeStyleSettings.IndentOptions indentOptions,
                                         final TextRange affectedRange) throws IncorrectOperationException {
    SequentialTask task = new MyFormattingTask() {
      @Nonnull
      @Override
      protected FormatProcessor buildProcessor() {
        FormatProcessor result = new FormatProcessor(model, rootBlock, settings, indentOptions, new FormatTextRanges(affectedRange, true), FormattingProgressCallback.EMPTY);
        result.formatWithoutRealModifications();
        return result;
      }
    };
    execute(task);
  }

  private void execute(@Nonnull SequentialTask task) {
    disableFormatting();
    Application application = ApplicationManager.getApplication();
    FormattingProgressTask progressTask = myProgressTask.getAndSet(null);
    if (progressTask == null || !application.isDispatchThread() || application.isUnitTestMode()) {
      try {
        task.prepare();
        while (!task.isDone()) {
          task.iteration();
        }
      }
      finally {
        enableFormatting();
      }
    }
    else {
      progressTask.setTask(task);
      Runnable callback = () -> enableFormatting();
      for (FormattingProgressCallback.EventType eventType : FormattingProgressCallback.EventType.values()) {
        progressTask.addCallback(eventType, callback);
      }
      ProgressManager.getInstance().run(progressTask);
    }
  }

  @Override
  public void adjustLineIndentsForRange(final FormattingModel model, final CodeStyleSettings settings, final CommonCodeStyleSettings.IndentOptions indentOptions, final TextRange rangeToAdjust) {
    disableFormatting();
    try {
      validateModel(model);
      final FormattingDocumentModel documentModel = model.getDocumentModel();
      final Block block = model.getRootBlock();
      final FormatProcessor processor = buildProcessorAndWrapBlocks(documentModel, block, settings, indentOptions, new FormatTextRanges(rangeToAdjust, true));
      LeafBlockWrapper tokenBlock = processor.getFirstTokenBlock();
      while (tokenBlock != null) {
        final WhiteSpace whiteSpace = tokenBlock.getWhiteSpace();
        whiteSpace.setLineFeedsAreReadOnly(true);
        if (!whiteSpace.containsLineFeeds()) {
          whiteSpace.setIsReadOnly(true);
        }
        tokenBlock = tokenBlock.getNextBlock();
      }
      processor.formatWithoutRealModifications();
      processor.performModifications(model);
    }
    catch (FormattingModelInconsistencyException e) {
      LOG.error(e);
    }
    finally {
      enableFormatting();
    }
  }

  @Override
  public void formatAroundRange(FormattingModel model, CodeStyleSettings settings, PsiFile file, TextRange textRange) {
    disableFormatting();
    try {
      validateModel(model);
      final FormattingDocumentModel documentModel = model.getDocumentModel();
      final Block block = model.getRootBlock();
      final FormatProcessor processor = buildProcessorAndWrapBlocks(documentModel, block, settings, settings.getIndentOptionsByFile(file), null);
      LeafBlockWrapper tokenBlock = processor.getFirstTokenBlock();
      while (tokenBlock != null) {
        final WhiteSpace whiteSpace = tokenBlock.getWhiteSpace();

        if (whiteSpace.getEndOffset() < textRange.getStartOffset() || whiteSpace.getEndOffset() > textRange.getEndOffset() + 1) {
          whiteSpace.setIsReadOnly(true);
        }
        else if (whiteSpace.getStartOffset() > textRange.getStartOffset() && whiteSpace.getEndOffset() < textRange.getEndOffset()) {
          if (whiteSpace.containsLineFeeds()) {
            whiteSpace.setLineFeedsAreReadOnly(true);
          }
          else {
            whiteSpace.setIsReadOnly(true);
          }
        }

        tokenBlock = tokenBlock.getNextBlock();
      }
      processor.formatWithoutRealModifications();
      processor.performModifications(model);
    }
    catch (FormattingModelInconsistencyException e) {
      LOG.error(e);
    }
    finally {
      enableFormatting();
    }
  }

  @Override
  public int adjustLineIndent(final FormattingModel model, final CodeStyleSettings settings, final CommonCodeStyleSettings.IndentOptions indentOptions, final int offset, final TextRange affectedRange)
          throws IncorrectOperationException {
    disableFormatting();
    try {
      validateModel(model);
      if (model instanceof PsiBasedFormattingModel) {
        ((PsiBasedFormattingModel)model).canModifyAllWhiteSpaces();
      }
      final FormattingDocumentModel documentModel = model.getDocumentModel();
      final FormatProcessor processor = buildProcessorAndWrapBlocks(model, settings, indentOptions, affectedRange, offset);
      final LeafBlockWrapper blockAfterOffset = processor.getBlockRangesMap().getBlockAtOrAfter(offset);
      if (blockAfterOffset != null && blockAfterOffset.contains(offset)) {
        return offset;
      }
      WhiteSpace whiteSpace = blockAfterOffset != null ? blockAfterOffset.getWhiteSpace() : processor.getLastWhiteSpace();
      return adjustLineIndent(offset, documentModel, processor, indentOptions, model, whiteSpace, blockAfterOffset != null ? blockAfterOffset.getNode() : null);
    }
    catch (FormattingModelInconsistencyException e) {
      LOG.error(e);
    }
    finally {
      enableFormatting();
    }
    return offset;
  }

  @Nonnull
  private static FormatProcessor buildProcessorAndWrapBlocks(final FormattingModel model,
                                                             CodeStyleSettings settings,
                                                             CommonCodeStyleSettings.IndentOptions indentOptions,
                                                             @Nullable TextRange affectedRange,
                                                             int offset) {
    FormattingDocumentModel docModel = model.getDocumentModel();
    Block rootBlock = model.getRootBlock();
    return buildProcessorAndWrapBlocks(docModel, rootBlock, settings, indentOptions, new FormatTextRanges(affectedRange, true), offset);
  }

  private static FormatProcessor buildProcessorAndWrapBlocks(final FormattingDocumentModel docModel,
                                                             Block rootBlock,
                                                             CodeStyleSettings settings,
                                                             CommonCodeStyleSettings.IndentOptions indentOptions,
                                                             @Nullable FormatTextRanges affectedRanges) {
    return buildProcessorAndWrapBlocks(docModel, rootBlock, settings, indentOptions, affectedRanges, -1);
  }

  private static FormatProcessor buildProcessorAndWrapBlocks(final FormattingDocumentModel docModel,
                                                             Block rootBlock,
                                                             CodeStyleSettings settings,
                                                             CommonCodeStyleSettings.IndentOptions indentOptions,
                                                             @Nullable FormatTextRanges affectedRanges,
                                                             int interestingOffset) {
    FormatOptions options = new FormatOptions(settings, indentOptions, affectedRanges, interestingOffset);
    FormatProcessor processor = new FormatProcessor(docModel, rootBlock, options, FormattingProgressCallback.EMPTY);
    //noinspection StatementWithEmptyBody
    while (!processor.iteration()) ;
    return processor;
  }

  private static int adjustLineIndent(final int offset,
                                      final FormattingDocumentModel documentModel,
                                      final FormatProcessor processor,
                                      final CommonCodeStyleSettings.IndentOptions indentOptions,
                                      final FormattingModel model,
                                      final WhiteSpace whiteSpace,
                                      ASTNode nodeAfter) {
    boolean wsContainsCaret = whiteSpace.getStartOffset() <= offset && offset < whiteSpace.getEndOffset();

    int lineStartOffset = getLineStartOffset(offset, whiteSpace, documentModel);

    final IndentInfo indent = calcIndent(offset, documentModel, processor, whiteSpace);

    final String newWS = whiteSpace.generateWhiteSpace(indentOptions, lineStartOffset, indent).toString();
    if (!whiteSpace.equalsToString(newWS)) {
      try {
        if (model instanceof FormattingModelEx) {
          ((FormattingModelEx)model).replaceWhiteSpace(whiteSpace.getTextRange(), nodeAfter, newWS);
        }
        else {
          model.replaceWhiteSpace(whiteSpace.getTextRange(), newWS);
        }
      }
      finally {
        model.commitChanges();
      }
    }

    final int defaultOffset = offset - whiteSpace.getLength() + newWS.length();

    if (wsContainsCaret) {
      final int ws = whiteSpace.getStartOffset() + CharArrayUtil.shiftForward(newWS, Math.max(0, lineStartOffset - whiteSpace.getStartOffset()), " \t");
      return Math.max(defaultOffset, ws);
    }
    else {
      return defaultOffset;
    }
  }

  private static boolean hasContentAfterLineBreak(final FormattingDocumentModel documentModel, final int offset, final WhiteSpace whiteSpace) {
    return documentModel.getLineNumber(offset) == documentModel.getLineNumber(whiteSpace.getEndOffset()) && documentModel.getTextLength() != whiteSpace.getEndOffset();
  }

  @Override
  public String getLineIndent(final FormattingModel model,
                              final CodeStyleSettings settings,
                              final CommonCodeStyleSettings.IndentOptions indentOptions,
                              final int offset,
                              final TextRange affectedRange) {
    final FormattingDocumentModel documentModel = model.getDocumentModel();
    final Block block = model.getRootBlock();
    if (block.getTextRange().isEmpty()) return null; // handing empty document case
    final FormatProcessor processor = buildProcessorAndWrapBlocks(model, settings, indentOptions, affectedRange, offset);
    WhiteSpace whiteSpace = getWhiteSpaceAtOffset(offset, processor);
    if (whiteSpace != null) {
      final IndentInfo indent = calcIndent(offset, documentModel, processor, whiteSpace);
      return indent.generateNewWhiteSpace(indentOptions);
    }
    return null;
  }

  @Nullable
  private static WhiteSpace getWhiteSpaceAtOffset(int offset, @Nonnull FormatProcessor formatProcessor) {
    final LeafBlockWrapper blockAfterOffset = formatProcessor.getBlockRangesMap().getBlockAtOrAfter(offset);
    if (blockAfterOffset != null) {
      if (!blockAfterOffset.contains(offset)) return blockAfterOffset.getWhiteSpace();
    }
    else {
      if (offset >= formatProcessor.getLastWhiteSpace().getStartOffset()) {
        return formatProcessor.getLastWhiteSpace();
      }
    }
    return null;
  }

  private static IndentInfo calcIndent(int offset, FormattingDocumentModel documentModel, FormatProcessor processor, WhiteSpace whiteSpace) {
    processor.setAllWhiteSpacesAreReadOnly();
    whiteSpace.setLineFeedsAreReadOnly(true);
    final IndentInfo indent;
    if (hasContentAfterLineBreak(documentModel, offset, whiteSpace)) {
      whiteSpace.setReadOnly(false);
      processor.formatWithoutRealModifications();
      indent = new IndentInfo(0, whiteSpace.getIndentOffset(), whiteSpace.getSpaces());
    }
    else {
      indent = processor.getIndentAt(offset);
    }
    return indent;
  }

  public static String getText(final FormattingDocumentModel documentModel) {
    return getCharSequence(documentModel).toString();
  }

  private static CharSequence getCharSequence(final FormattingDocumentModel documentModel) {
    return documentModel.getText(new TextRange(0, documentModel.getTextLength()));
  }

  private static int getLineStartOffset(final int offset, final WhiteSpace whiteSpace, final FormattingDocumentModel documentModel) {
    int lineStartOffset = offset;

    CharSequence text = getCharSequence(documentModel);
    lineStartOffset = CharArrayUtil.shiftBackwardUntil(text, lineStartOffset, " \t\n");
    if (lineStartOffset > whiteSpace.getStartOffset()) {
      if (lineStartOffset >= text.length()) lineStartOffset = text.length() - 1;
      final int wsStart = whiteSpace.getStartOffset();
      int prevEnd;

      if (text.charAt(lineStartOffset) == '\n' && wsStart <= (prevEnd = documentModel.getLineStartOffset(documentModel.getLineNumber(lineStartOffset - 1))) &&
          documentModel.getText(new TextRange(prevEnd, lineStartOffset)).toString().trim().length() == 0 // ws consists of space only, it is not true for <![CDATA[
      ) {
        lineStartOffset--;
      }
      lineStartOffset = CharArrayUtil.shiftBackward(text, lineStartOffset, "\t ");
      if (lineStartOffset < 0) lineStartOffset = 0;
      if (lineStartOffset != offset && text.charAt(lineStartOffset) == '\n') {
        lineStartOffset++;
      }
    }
    return lineStartOffset;
  }


  @Override
  public FormattingModel createFormattingModelForPsiFile(@Nonnull final PsiFile file, @Nonnull final Block rootBlock, final CodeStyleSettings settings) {
    return new PsiBasedFormattingModel(file, rootBlock, FormattingDocumentModelImpl.createOn(file));
  }

  @Override
  public Indent getSpaceIndent(final int spaces, final boolean relative) {
    return getIndent(Indent.Type.SPACES, spaces, relative, false);
  }

  @Override
  public Indent getIndent(@Nonnull Indent.Type type, boolean relativeToDirectParent, boolean enforceIndentToChildren) {
    return getIndent(type, 0, relativeToDirectParent, enforceIndentToChildren);
  }

  @Override
  public Indent getSmartIndent(@Nonnull Indent.Type type) {
    return new ExpandableIndent(type);
  }

  @Override
  public Indent getIndent(@Nonnull Indent.Type type, int spaces, boolean relativeToDirectParent, boolean enforceIndentToChildren) {
    return new IndentImpl(type, false, spaces, relativeToDirectParent, enforceIndentToChildren);
  }

  @Override
  public Indent getAbsoluteLabelIndent() {
    return myAbsoluteLabelIndent;
  }

  @Override
  @Nonnull
  public Spacing createSafeSpacing(final boolean shouldKeepLineBreaks, final int keepBlankLines) {
    return getSpacingImpl(0, 0, 0, false, true, shouldKeepLineBreaks, keepBlankLines, false, 0);
  }

  @Override
  @Nonnull
  public Spacing createKeepingFirstColumnSpacing(final int minSpace, final int maxSpace, final boolean keepLineBreaks, final int keepBlankLines) {
    return getSpacingImpl(minSpace, maxSpace, -1, false, false, keepLineBreaks, keepBlankLines, true, 0);
  }

  @Override
  @Nonnull
  public Spacing createSpacing(final int minSpaces, final int maxSpaces, final int minLineFeeds, final boolean keepLineBreaks, final int keepBlankLines, final int prefLineFeeds) {
    return getSpacingImpl(minSpaces, maxSpaces, minLineFeeds, false, false, keepLineBreaks, keepBlankLines, false, prefLineFeeds);
  }

  private final Map<SpacingImpl, SpacingImpl> ourSharedProperties = new HashMap<>();
  private final SpacingImpl ourSharedSpacing = new SpacingImpl(-1, -1, -1, false, false, false, -1, false, 0);

  private SpacingImpl getSpacingImpl(final int minSpaces,
                                     final int maxSpaces,
                                     final int minLineFeeds,
                                     final boolean readOnly,
                                     final boolean safe,
                                     final boolean keepLineBreaksFlag,
                                     final int keepLineBreaks,
                                     final boolean keepFirstColumn,
                                     int prefLineFeeds) {
    synchronized (ourSharedSpacing) {
      ourSharedSpacing.init(minSpaces, maxSpaces, minLineFeeds, readOnly, safe, keepLineBreaksFlag, keepLineBreaks, keepFirstColumn, prefLineFeeds);
      SpacingImpl spacing = ourSharedProperties.get(ourSharedSpacing);

      if (spacing == null) {
        spacing = new SpacingImpl(minSpaces, maxSpaces, minLineFeeds, readOnly, safe, keepLineBreaksFlag, keepLineBreaks, keepFirstColumn, prefLineFeeds);
        ourSharedProperties.put(spacing, spacing);
      }
      return spacing;
    }
  }

  @Override
  public Indent getAbsoluteNoneIndent() {
    return myAbsoluteNoneIndent;
  }

  @Override
  public Indent getLabelIndent() {
    return myLabelIndent;
  }

  @Override
  public Indent getContinuationIndent(boolean relative) {
    return relative ? myContinuationIndentRelativeToDirectParent : myContinuationIndentNotRelativeToDirectParent;
  }

  //is default
  @Override
  public Indent getContinuationWithoutFirstIndent(boolean relative) {
    return relative ? myContinuationWithoutFirstIndentRelativeToDirectParent : myContinuationWithoutFirstIndentNotRelativeToDirectParent;
  }

  @Override
  public boolean isDisabled() {
    return myIsDisabledCount.get() > 0;
  }

  private void disableFormatting() {
    myIsDisabledCount.incrementAndGet();
  }

  private void enableFormatting() {
    int old = myIsDisabledCount.getAndDecrement();
    if (old <= 0) {
      LOG.error("enableFormatting()/disableFormatting() not paired. DisabledLevel = " + old);
    }
  }

  @Nullable
  public <T> T runWithFormattingDisabled(@Nonnull Computable<T> runnable) {
    disableFormatting();
    try {
      return runnable.compute();
    }
    finally {
      enableFormatting();
    }
  }

  private abstract static class MyFormattingTask implements SequentialTask {
    private FormatProcessor myProcessor;
    private boolean myDone;

    @Override
    public void prepare() {
      myProcessor = buildProcessor();
    }

    @Override
    public boolean isDone() {
      return myDone;
    }

    @Override
    public boolean iteration() {
      return myDone = myProcessor.iteration();
    }

    @Override
    public void stop() {
      myProcessor.stopSequentialProcessing();
      myDone = true;
    }

    @Nonnull
    protected abstract FormatProcessor buildProcessor();
  }

  private static void validateModel(FormattingModel model) throws FormattingModelInconsistencyException {
    FormattingDocumentModel documentModel = model.getDocumentModel();
    Document document = documentModel.getDocument();
    Block rootBlock = model.getRootBlock();
    if (rootBlock instanceof ASTBlock) {
      PsiElement rootElement = ((ASTBlock)rootBlock).getNode().getPsi();
      if (!rootElement.isValid()) {
        throw new FormattingModelInconsistencyException("Invalid root block PSI element");
      }
      PsiFile file = rootElement.getContainingFile();
      Project project = file.getProject();
      PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project);
      if (documentManager.isUncommited(document)) {
        throw new FormattingModelInconsistencyException("Uncommitted document");
      }
      if (document.getTextLength() != file.getTextLength()) {
        throw new FormattingModelInconsistencyException("Document length " + document.getTextLength() + " doesn't match PSI file length " + file.getTextLength() + ", language: " + file.getLanguage());
      }
    }
  }

  private static class FormattingModelInconsistencyException extends Exception {
    FormattingModelInconsistencyException(String message) {
      super(message);
    }
  }
}