/**
   * Arranges given PSI root contents that belong to the given ranges.
   *
   * @param file target PSI root
   * @param ranges target ranges to use within the given root
   */
  @SuppressWarnings("MethodMayBeStatic")
  public void arrange(
      @NotNull PsiFile file,
      @NotNull Collection<TextRange> ranges,
      @Nullable final ArrangementCallback callback) {
    final Document document = PsiDocumentManager.getInstance(file.getProject()).getDocument(file);
    if (document == null) {
      return;
    }

    final Rearranger<?> rearranger = Rearranger.EXTENSION.forLanguage(file.getLanguage());
    if (rearranger == null) {
      return;
    }

    final CodeStyleSettings settings =
        CodeStyleSettingsManager.getInstance(file.getProject()).getCurrentSettings();
    ArrangementSettings arrangementSettings =
        settings.getCommonSettings(file.getLanguage()).getArrangementSettings();
    if (arrangementSettings == null && rearranger instanceof ArrangementStandardSettingsAware) {
      arrangementSettings = ((ArrangementStandardSettingsAware) rearranger).getDefaultSettings();
    }

    if (arrangementSettings == null) {
      return;
    }

    final DocumentEx documentEx;
    if (document instanceof DocumentEx && !((DocumentEx) document).isInBulkUpdate()) {
      documentEx = (DocumentEx) document;
    } else {
      documentEx = null;
    }

    final Context<? extends ArrangementEntry> context =
        Context.from(rearranger, document, file, ranges, arrangementSettings, settings);

    ApplicationManager.getApplication()
        .runWriteAction(
            new Runnable() {
              @Override
              public void run() {
                if (documentEx != null) {
                  // documentEx.setInBulkUpdate(true);
                }
                try {
                  doArrange(context);
                  if (callback != null) {
                    callback.afterArrangement(context.moveInfos);
                  }
                } finally {
                  if (documentEx != null) {
                    // documentEx.setInBulkUpdate(false);
                  }
                }
              }
            });
  }
    @SuppressWarnings("AssignmentToForLoopParameter")
    @Override
    public void replace(
        @NotNull ArrangementEntryWrapper<E> newWrapper,
        @NotNull ArrangementEntryWrapper<E> oldWrapper,
        @Nullable ArrangementEntryWrapper<E> previous,
        @NotNull Context<E> context) {
      // Calculate blank lines before the arrangement.
      int blankLinesBefore = oldWrapper.getBlankLinesBefore();

      ArrangementEntryWrapper<E> parentWrapper = oldWrapper.getParent();
      int desiredBlankLinesNumber =
          context.rearranger.getBlankLines(
              context.settings,
              parentWrapper == null ? null : parentWrapper.getEntry(),
              previous == null ? null : previous.getEntry(),
              newWrapper.getEntry());
      if ((desiredBlankLinesNumber < 0 || desiredBlankLinesNumber == blankLinesBefore)
          && newWrapper.equals(oldWrapper)) {
        return;
      }

      int lineFeedsDiff = desiredBlankLinesNumber - blankLinesBefore;
      int insertionOffset = oldWrapper.getStartOffset();
      if (oldWrapper.getStartOffset() > newWrapper.getStartOffset()) {
        insertionOffset -= newWrapper.getEndOffset() - newWrapper.getStartOffset();
      }
      if (newWrapper.getStartOffset() != oldWrapper.getStartOffset()
          || !newWrapper.equals(oldWrapper)) {
        context.addMoveInfo(
            newWrapper.getStartOffset(), newWrapper.getEndOffset(), oldWrapper.getStartOffset());
        myDocument.moveText(
            newWrapper.getStartOffset(), newWrapper.getEndOffset(), oldWrapper.getStartOffset());
        for (int i = myWrappers.size() - 1; i >= 0; i--) {
          ArrangementEntryWrapper<E> w = myWrappers.get(i);
          if (w == newWrapper) {
            continue;
          }
          if (w.getStartOffset() >= oldWrapper.getStartOffset()
              && w.getStartOffset() < newWrapper.getStartOffset()) {
            w.applyShift(newWrapper.getEndOffset() - newWrapper.getStartOffset());
          } else if (w.getStartOffset() < oldWrapper.getStartOffset()
              && w.getStartOffset() > newWrapper.getStartOffset()) {
            w.applyShift(newWrapper.getStartOffset() - newWrapper.getEndOffset());
          }
        }
      }

      if (desiredBlankLinesNumber >= 0 && lineFeedsDiff > 0) {
        myDocument.insertString(insertionOffset, StringUtil.repeat("\n", lineFeedsDiff));
        shiftOffsets(lineFeedsDiff, insertionOffset);
      }

      if (desiredBlankLinesNumber >= 0 && lineFeedsDiff < 0) {
        // Cut exceeding blank lines.
        int replacementStartOffset = getBlankLineOffset(-lineFeedsDiff, insertionOffset);
        myDocument.deleteString(replacementStartOffset, insertionOffset);
        shiftOffsets(replacementStartOffset - insertionOffset, insertionOffset);
      }

      // Update wrapper ranges.
      if (desiredBlankLinesNumber < 0 || lineFeedsDiff == 0 || parentWrapper == null) {
        return;
      }

      Deque<ArrangementEntryWrapper<E>> parents = new ArrayDeque<ArrangementEntryWrapper<E>>();
      do {
        parents.add(parentWrapper);
        parentWrapper.setEndOffset(parentWrapper.getEndOffset() + lineFeedsDiff);
        parentWrapper = parentWrapper.getParent();
      } while (parentWrapper != null);

      while (!parents.isEmpty()) {
        for (ArrangementEntryWrapper<E> wrapper = parents.removeLast().getNext();
            wrapper != null;
            wrapper = wrapper.getNext()) {
          wrapper.applyShift(lineFeedsDiff);
        }
      }
    }
    @SuppressWarnings("AssignmentToForLoopParameter")
    @Override
    public void replace(
        @NotNull ArrangementEntryWrapper<E> newWrapper,
        @NotNull ArrangementEntryWrapper<E> oldWrapper,
        @Nullable ArrangementEntryWrapper<E> previous,
        @NotNull Context<E> context) {
      // Calculate blank lines before the arrangement.
      int blankLinesBefore = 0;
      TIntArrayList lineFeedOffsets = new TIntArrayList();
      int oldStartLine = context.document.getLineNumber(oldWrapper.getStartOffset());
      if (oldStartLine > 0) {
        int lastLineFeed = context.document.getLineStartOffset(oldStartLine) - 1;
        lineFeedOffsets.add(lastLineFeed);
        for (int i = lastLineFeed - 1 - myParentShift; i >= 0; i--) {
          i = CharArrayUtil.shiftBackward(myParentText, i, " \t");
          if (myParentText.charAt(i) == '\n') {
            blankLinesBefore++;
            lineFeedOffsets.add(i + myParentShift);
          } else {
            break;
          }
        }
      }

      ArrangementEntryWrapper<E> parentWrapper = oldWrapper.getParent();
      int desiredBlankLinesNumber =
          context.rearranger.getBlankLines(
              context.settings,
              parentWrapper == null ? null : parentWrapper.getEntry(),
              previous == null ? null : previous.getEntry(),
              newWrapper.getEntry());
      if (desiredBlankLinesNumber == blankLinesBefore && newWrapper.equals(oldWrapper)) {
        return;
      }

      String newEntryText =
          myParentText.substring(
              newWrapper.getStartOffset() - myParentShift,
              newWrapper.getEndOffset() - myParentShift);
      int lineFeedsDiff = desiredBlankLinesNumber - blankLinesBefore;
      if (lineFeedsDiff == 0 || desiredBlankLinesNumber < 0) {
        context.addMoveInfo(
            newWrapper.getStartOffset() - myParentShift,
            newWrapper.getEndOffset() - myParentShift,
            oldWrapper.getStartOffset());
        context.document.replaceString(
            oldWrapper.getStartOffset(), oldWrapper.getEndOffset(), newEntryText);
        return;
      }

      if (lineFeedsDiff > 0) {
        // Insert necessary number of blank lines.
        StringBuilder buffer = new StringBuilder(StringUtil.repeat("\n", lineFeedsDiff));
        buffer.append(newEntryText);
        context.document.replaceString(
            oldWrapper.getStartOffset(), oldWrapper.getEndOffset(), buffer);
      } else {
        // Cut exceeding blank lines.
        int replacementStartOffset = lineFeedOffsets.get(-lineFeedsDiff) + 1;
        context.document.replaceString(
            replacementStartOffset, oldWrapper.getEndOffset(), newEntryText);
      }

      // Update wrapper ranges.
      ArrangementEntryWrapper<E> parent = oldWrapper.getParent();
      if (parent == null) {
        return;
      }

      Deque<ArrangementEntryWrapper<E>> parents = new ArrayDeque<ArrangementEntryWrapper<E>>();
      do {
        parents.add(parent);
        parent.setEndOffset(parent.getEndOffset() + lineFeedsDiff);
        parent = parent.getParent();
      } while (parent != null);

      while (!parents.isEmpty()) {

        for (ArrangementEntryWrapper<E> wrapper = parents.removeLast().getNext();
            wrapper != null;
            wrapper = wrapper.getNext()) {
          wrapper.applyShift(lineFeedsDiff);
        }
      }
    }