/*
 * Copyright 2000-2016 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.intellij.openapi.editor.impl;

import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.TransactionGuard;
import com.intellij.openapi.application.TransactionGuardImpl;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Attachment;
import com.intellij.openapi.diagnostic.ExceptionWithAttachments;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId;
import com.intellij.openapi.editor.actionSystem.ReadonlyFragmentModificationHandler;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.openapi.editor.ex.*;
import com.intellij.openapi.editor.impl.event.DocumentEventImpl;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.reference.SoftReference;
import com.intellij.util.*;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.IntArrayList;
import com.intellij.util.text.CharArrayUtil;
import com.intellij.util.text.ImmutableCharSequence;
import gnu.trove.TIntObjectHashMap;
import gnu.trove.TObjectProcedure;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class DocumentImpl extends UserDataHolderBase implements DocumentEx {
  private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.DocumentImpl");

  private final LockFreeCOWSortedArray<DocumentListener> myDocumentListeners =
    new LockFreeCOWSortedArray<>(PrioritizedDocumentListener.COMPARATOR, DocumentListener.ARRAY_FACTORY);
  private final List<DocumentBulkUpdateListener> myBulkDocumentInternalListeners = ContainerUtil.createLockFreeCopyOnWriteList();
  private final RangeMarkerTree<RangeMarkerEx> myRangeMarkers = new RangeMarkerTree<>(this);
  private final RangeMarkerTree<RangeMarkerEx> myPersistentRangeMarkers = new RangeMarkerTree<>(this);
  private final List<RangeMarker> myGuardedBlocks = new ArrayList<>();
  private ReadonlyFragmentModificationHandler myReadonlyFragmentModificationHandler;

  @SuppressWarnings("RedundantStringConstructorCall") private final Object myLineSetLock = new String("line set lock");
  private volatile LineSet myLineSet;
  private volatile ImmutableCharSequence myText;
  private volatile SoftReference<String> myTextString;
  private volatile FrozenDocument myFrozen;

  private boolean myIsReadOnly;
  private volatile boolean isStripTrailingSpacesEnabled = true;
  private volatile long myModificationStamp;
  private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this);

  private final List<EditReadOnlyListener> myReadOnlyListeners = ContainerUtil.createLockFreeCopyOnWriteList();

  private int myCheckGuardedBlocks;
  private boolean myGuardsSuppressed;
  private boolean myEventsHandling;
  private final boolean myAssertThreading;
  private volatile boolean myDoingBulkUpdate;
  private volatile Throwable myBulkUpdateEnteringTrace;
  private boolean myUpdatingBulkModeStatus;
  private volatile boolean myAcceptSlashR;
  private boolean myChangeInProgress;
  private volatile int myBufferSize;
  private final CharSequence myMutableCharSequence = new CharSequence() {
    @Override
    public int length() {
      return myText.length();
    }

    @Override
    public char charAt(int index) {
      return myText.charAt(index);
    }

    @Override
    public CharSequence subSequence(int start, int end) {
      return myText.subSequence(start, end);
    }

    @NotNull
    @Override
    public String toString() {
      return doGetText();
    }
  };
  private final AtomicInteger sequence = new AtomicInteger();

  public DocumentImpl(@NotNull String text) {
    this(text, false);
  }

  public DocumentImpl(@NotNull CharSequence chars) {
    this(chars, false);
  }

  /**
   * NOTE: if client sets forUseInNonAWTThread to true it's supposed that client will completely control document and its listeners.
   * The noticeable peculiarity of DocumentImpl behavior in this mode is that DocumentImpl won't suppress ProcessCancelledException
   * thrown from listeners during changedUpdate event, so the exception will be rethrown and rest of the listeners WON'T be notified.
   */
  public DocumentImpl(@NotNull CharSequence chars, boolean forUseInNonAWTThread) {
    this(chars, false, forUseInNonAWTThread);
  }

  public DocumentImpl(@NotNull CharSequence chars, boolean acceptSlashR, boolean forUseInNonAWTThread) {
    setAcceptSlashR(acceptSlashR);
    assertValidSeparators(chars);
    myText = CharArrayUtil.createImmutableCharSequence(chars);
    setCyclicBufferSize(0);
    setModificationStamp(LocalTimeCounter.currentTime());
    myAssertThreading = !forUseInNonAWTThread;
  }

  public boolean setAcceptSlashR(boolean accept) {
    try {
      return myAcceptSlashR;
    }
    finally {
      myAcceptSlashR = accept;
    }
  }

  public boolean acceptsSlashR() {
    return myAcceptSlashR;
  }

  private LineSet getLineSet() {
    LineSet lineSet = myLineSet;
    if (lineSet == null) {
      synchronized (myLineSetLock) {
        lineSet = myLineSet;
        if (lineSet == null) {
          lineSet = LineSet.createLineSet(myText);
          myLineSet = lineSet;
        }
      }
    }

    return lineSet;
  }

  @Override
  @NotNull
  public char[] getChars() {
    return CharArrayUtil.fromSequence(myText);
  }

  @Override
  public void setStripTrailingSpacesEnabled(boolean isEnabled) {
    isStripTrailingSpacesEnabled = isEnabled;
  }

  @TestOnly
  public boolean stripTrailingSpaces(Project project) {
    return stripTrailingSpaces(project, false);
  }

  @TestOnly
  public boolean stripTrailingSpaces(Project project, boolean inChangedLinesOnly) {
    return stripTrailingSpaces(project, inChangedLinesOnly, true, new int[0]);
  }

  /**
   * @return true if stripping was completed successfully, false if the document prevented stripping by e.g. caret(s) being in the way
   */
  boolean stripTrailingSpaces(@Nullable final Project project,
                              boolean inChangedLinesOnly,
                              boolean skipCaretLines,
                              @NotNull int[] caretOffsets) {
    if (!isStripTrailingSpacesEnabled) {
      return true;
    }
    List<StripTrailingSpacesFilter> filters = new ArrayList<>();
    for (StripTrailingSpacesFilterFactory filterFactory : StripTrailingSpacesFilterFactory.EXTENSION_POINT.getExtensions()) {
      StripTrailingSpacesFilter filter = filterFactory.createFilter(project, this);
      if (filter == StripTrailingSpacesFilter.NOT_ALLOWED) {
        return true;
      }
      else if (filter == StripTrailingSpacesFilter.POSTPONED) {
        return false;
      }
      else {
        filters.add(filter);
      }
    }

    boolean markAsNeedsStrippingLater = false;
    CharSequence text = myText;
    TIntObjectHashMap<List<RangeMarker>> caretMarkers = new TIntObjectHashMap<>(caretOffsets.length);
    try {
      if (skipCaretLines) {
        for (int caretOffset : caretOffsets) {
          if (caretOffset < 0 || caretOffset > getTextLength()) {
            continue;
          }
          int line = getLineNumber(caretOffset);
          List<RangeMarker> markers = caretMarkers.get(line);
          if (markers == null) {
            markers = new ArrayList<>();
            caretMarkers.put(line, markers);
          }
          RangeMarker marker = createRangeMarker(caretOffset, caretOffset);
          markers.add(marker);
        }
      }
      lineLoop:
      for (int line = 0; line < getLineCount(); line++) {
        LineSet lineSet = getLineSet();
        int maxSpacesToLeave = getMaxSpacesToLeave(line, filters);
        if (inChangedLinesOnly && !lineSet.isModified(line) || maxSpacesToLeave < 0) continue;
        int whiteSpaceStart = -1;
        final int lineEnd = lineSet.getLineEnd(line) - lineSet.getSeparatorLength(line);
        int lineStart = lineSet.getLineStart(line);
        for (int offset = lineEnd - 1; offset >= lineStart; offset--) {
          char c = text.charAt(offset);
          if (c != ' ' && c != '\t') {
            break;
          }
          whiteSpaceStart = offset;
        }
        if (whiteSpaceStart == -1) continue;
        if (skipCaretLines) {
          List<RangeMarker> markers = caretMarkers.get(line);
          if (markers != null) {
            for (RangeMarker marker : markers) {
              if (marker.getStartOffset() >= 0 && whiteSpaceStart < marker.getStartOffset()) {
                // mark this as a document that needs stripping later
                // otherwise the caret would jump madly
                markAsNeedsStrippingLater = true;
                continue lineLoop;
              }
            }
          }
        }
        final int finalStart = whiteSpaceStart + maxSpacesToLeave;
        if (finalStart < lineEnd) {
          // document must be unblocked by now. If not, some Save handler attempted to modify PSI
          // which should have been caught by assertion in com.intellij.pom.core.impl.PomModelImpl.runTransaction
          DocumentUtil.writeInRunUndoTransparentAction(new DocumentRunnable(DocumentImpl.this, project) {
            @Override
            public void run() {
              deleteString(finalStart, lineEnd);
            }
          });
        }
        text = myText;
      }
    }
    finally {
      caretMarkers.forEachValue(markerList -> {
        if (markerList != null) {
          for (RangeMarker marker : markerList) {
            try {
              marker.dispose();
            }
            catch (Exception e) {
              LOG.error(e);
            }
          }
        }
        return true;
      });
    }
    return markAsNeedsStrippingLater;
  }

  private static int getMaxSpacesToLeave(int line, @NotNull List<StripTrailingSpacesFilter> filters) {
    for (StripTrailingSpacesFilter filter :  filters) {
      if (filter instanceof SmartStripTrailingSpacesFilter) {
        return ((SmartStripTrailingSpacesFilter)filter).getTrailingSpacesToLeave(line);
      }
      else  if (!filter.isStripSpacesAllowedForLine(line)) {
        return -1;
      }
    }
    return 0;
  }

  @Override
  public void setReadOnly(boolean isReadOnly) {
    if (myIsReadOnly != isReadOnly) {
      myIsReadOnly = isReadOnly;
      myPropertyChangeSupport.firePropertyChange(Document.PROP_WRITABLE, !isReadOnly, isReadOnly);
    }
  }

  ReadonlyFragmentModificationHandler getReadonlyFragmentModificationHandler() {
    return myReadonlyFragmentModificationHandler;
  }

  void setReadonlyFragmentModificationHandler(final ReadonlyFragmentModificationHandler readonlyFragmentModificationHandler) {
    myReadonlyFragmentModificationHandler = readonlyFragmentModificationHandler;
  }

  @Override
  public boolean isWritable() {
    return !myIsReadOnly;
  }

  private RangeMarkerTree<RangeMarkerEx> treeFor(@NotNull RangeMarkerEx rangeMarker) {
    return rangeMarker instanceof PersistentRangeMarker ? myPersistentRangeMarkers : myRangeMarkers;
  }
  @Override
  public boolean removeRangeMarker(@NotNull RangeMarkerEx rangeMarker) {
    return treeFor(rangeMarker).removeInterval(rangeMarker);
  }

  @Override
  public void registerRangeMarker(@NotNull RangeMarkerEx rangeMarker,
                                  int start,
                                  int end,
                                  boolean greedyToLeft,
                                  boolean greedyToRight,
                                  int layer) {
    treeFor(rangeMarker).addInterval(rangeMarker, start, end, greedyToLeft, greedyToRight, layer);
  }

  @TestOnly
  int getRangeMarkersSize() {
    return myRangeMarkers.size() + myPersistentRangeMarkers.size();
  }

  @TestOnly
  int getRangeMarkersNodeSize() {
    return myRangeMarkers.nodeSize()+myPersistentRangeMarkers.nodeSize();
  }

  @Override
  @NotNull
  public RangeMarker createGuardedBlock(int startOffset, int endOffset) {
    LOG.assertTrue(startOffset <= endOffset, "Should be startOffset <= endOffset");
    RangeMarker block = createRangeMarker(startOffset, endOffset, true);
    myGuardedBlocks.add(block);
    return block;
  }

  @Override
  public void removeGuardedBlock(@NotNull RangeMarker block) {
    myGuardedBlocks.remove(block);
  }

  @Override
  @NotNull
  public List<RangeMarker> getGuardedBlocks() {
    return myGuardedBlocks;
  }

  @Override
  @SuppressWarnings("ForLoopReplaceableByForEach") // Way too many garbage is produced otherwise in AbstractList.iterator()
  public RangeMarker getOffsetGuard(int offset) {
    for (int i = 0; i < myGuardedBlocks.size(); i++) {
      RangeMarker block = myGuardedBlocks.get(i);
      if (offsetInRange(offset, block.getStartOffset(), block.getEndOffset())) return block;
    }

    return null;
  }

  @Override
  public RangeMarker getRangeGuard(int start, int end) {
    for (RangeMarker block : myGuardedBlocks) {
      if (rangesIntersect(start, end, true, true,
                          block.getStartOffset(), block.getEndOffset(), block.isGreedyToLeft(), block.isGreedyToRight())) {
        return block;
      }
    }

    return null;
  }

  @Override
  public void startGuardedBlockChecking() {
    myCheckGuardedBlocks++;
  }

  @Override
  public void stopGuardedBlockChecking() {
    LOG.assertTrue(myCheckGuardedBlocks > 0, "Unpaired start/stopGuardedBlockChecking");
    myCheckGuardedBlocks--;
  }

  private static boolean offsetInRange(int offset, int start, int end) {
    return start <= offset && offset < end;
  }

  private static boolean rangesIntersect(int start0, int end0, boolean start0Inclusive, boolean end0Inclusive,
                                         int start1, int end1, boolean start1Inclusive, boolean end1Inclusive) {
    if (start0 > start1 || start0 == start1 && !start0Inclusive) {
      if (end1 == start0) return start0Inclusive && end1Inclusive;
      return end1 > start0;
    }
    if (end0 == start1) return start1Inclusive && end0Inclusive;
    return end0 > start1;
  }

  @Override
  @NotNull
  public RangeMarker createRangeMarker(int startOffset, int endOffset) {
    return createRangeMarker(startOffset, endOffset, false);
  }

  @Override
  @NotNull
  public RangeMarker createRangeMarker(int startOffset, int endOffset, boolean surviveOnExternalChange) {
    if (!(0 <= startOffset && startOffset <= endOffset && endOffset <= getTextLength())) {
      LOG.error("Incorrect offsets: startOffset=" + startOffset + ", endOffset=" + endOffset + ", text length=" + getTextLength());
    }
    return surviveOnExternalChange
           ? new PersistentRangeMarker(this, startOffset, endOffset, true)
           : new RangeMarkerImpl(this, startOffset, endOffset, true);
  }

  @Override
  public long getModificationStamp() {
    return myModificationStamp;
  }

  @Override
  public void setModificationStamp(long modificationStamp) {
    myModificationStamp = modificationStamp;
  }

  @Override
  public void replaceText(@NotNull CharSequence chars, long newModificationStamp) {
    replaceString(0, getTextLength(), chars, newModificationStamp, true); //TODO: optimization!!!
    clearLineModificationFlags();
  }

  @Override
  public void insertString(int offset, @NotNull CharSequence s) {
    if (offset < 0) throw new IndexOutOfBoundsException("Wrong offset: " + offset);
    if (offset > getTextLength()) {
      throw new IndexOutOfBoundsException(
        "Wrong offset: " + offset + "; documentLength: " + getTextLength() + "; " + s.subSequence(Math.max(0, s.length() - 20), s.length())
      );
    }
    assertWriteAccess();
    assertValidSeparators(s);

    if (!isWritable()) throw new ReadOnlyModificationException(this);
    if (s.length() == 0) return;

    RangeMarker marker = getRangeGuard(offset, offset);
    if (marker != null) {
      throwGuardedFragment(marker, offset, "", s);
    }

    ImmutableCharSequence newText = myText.insert(offset, s);
    ImmutableCharSequence newString = newText.subtext(offset, offset + s.length());
    updateText(newText, offset, "", newString, false, LocalTimeCounter.currentTime(), offset, 0);
    trimToSize();
  }

  private void trimToSize() {
    if (myBufferSize != 0 && getTextLength() > myBufferSize) {
      deleteString(0, getTextLength() - myBufferSize);
    }
  }

  @Override
  public void deleteString(int startOffset, int endOffset) {
    assertBounds(startOffset, endOffset);

    assertWriteAccess();
    if (!isWritable()) throw new ReadOnlyModificationException(this);
    if (startOffset == endOffset) return;

    RangeMarker marker = getRangeGuard(startOffset, endOffset);
    if (marker != null) {
      throwGuardedFragment(marker, startOffset, myText.subSequence(startOffset, endOffset), "");
    }

    ImmutableCharSequence newText = myText.delete(startOffset, endOffset);
    ImmutableCharSequence oldString = myText.subtext(startOffset, endOffset);
    updateText(newText, startOffset, oldString, "", false, LocalTimeCounter.currentTime(), startOffset, endOffset - startOffset);
  }

  @Override
  public void moveText(int srcStart, int srcEnd, int dstOffset) {
    assertBounds(srcStart, srcEnd);
    if (dstOffset == srcEnd) return;
    ProperTextRange srcRange = new ProperTextRange(srcStart, srcEnd);
    assert !srcRange.containsOffset(dstOffset) : "Can't perform text move from range [" +srcStart+ "; " + srcEnd+ ") to offset "+dstOffset;

    String replacement = getCharsSequence().subSequence(srcStart, srcEnd).toString();

    insertString(dstOffset, replacement);
    int shift = 0;
    if (dstOffset < srcStart) {
      shift = srcEnd - srcStart;
    }
    fireMoveText(srcStart + shift, srcEnd + shift, dstOffset);

    deleteString(srcStart + shift, srcEnd + shift);
  }

  private void fireMoveText(int start, int end, int newBase) {
    for (DocumentListener listener : getListeners()) {
      if (listener instanceof PrioritizedInternalDocumentListener) {
        ((PrioritizedInternalDocumentListener)listener).moveTextHappened(start, end, newBase);
      }
    }
  }

  @Override
  public void replaceString(int startOffset, int endOffset, @NotNull CharSequence s) {
    replaceString(startOffset, endOffset, s, LocalTimeCounter.currentTime(), false);
  }

  private void replaceString(int startOffset, int endOffset, @NotNull CharSequence s, final long newModificationStamp, boolean wholeTextReplaced) {
    assertBounds(startOffset, endOffset);

    assertWriteAccess();
    assertValidSeparators(s);

    if (!isWritable()) {
      throw new ReadOnlyModificationException(this);
    }

    int initialStartOffset = startOffset;
    int initialOldLength = endOffset - startOffset;

    final int newStringLength = s.length();
    final CharSequence chars = myText;
    int newStartInString = 0;
    while (newStartInString < newStringLength &&
           startOffset < endOffset &&
           s.charAt(newStartInString) == chars.charAt(startOffset)) {
      startOffset++;
      newStartInString++;
    }

    int newEndInString = newStringLength;
    while (endOffset > startOffset &&
           newEndInString > newStartInString &&
           s.charAt(newEndInString - 1) == chars.charAt(endOffset - 1)) {
      newEndInString--;
      endOffset--;
    }

    if (startOffset == 0 && endOffset == getTextLength()) {
      wholeTextReplaced = true;
    }

    CharSequence changedPart = s.subSequence(newStartInString, newEndInString);
    CharSequence sToDelete = myText.subtext(startOffset, endOffset);
    RangeMarker guard = getRangeGuard(startOffset, endOffset);
    if (guard != null) {
      throwGuardedFragment(guard, startOffset, sToDelete, changedPart);
    }

    ImmutableCharSequence newText;
    if (wholeTextReplaced && s instanceof ImmutableCharSequence) {
      newText = (ImmutableCharSequence)s;
    }
    else {
      newText = myText.delete(startOffset, endOffset).insert(startOffset, changedPart);
      changedPart = newText.subtext(startOffset, startOffset + changedPart.length());
    }
    updateText(newText, startOffset, sToDelete, changedPart, wholeTextReplaced, newModificationStamp, initialStartOffset, initialOldLength);
    trimToSize();
  }

  private void assertBounds(final int startOffset, final int endOffset) {
    if (startOffset < 0 || startOffset > getTextLength()) {
      throw new IndexOutOfBoundsException("Wrong startOffset: " + startOffset + "; documentLength: " + getTextLength());
    }
    if (endOffset < 0 || endOffset > getTextLength()) {
      throw new IndexOutOfBoundsException("Wrong endOffset: " + endOffset + "; documentLength: " + getTextLength());
    }
    if (endOffset < startOffset) {
      throw new IllegalArgumentException(
        "endOffset < startOffset: " + endOffset + " < " + startOffset + "; documentLength: " + getTextLength());
    }
  }

  private void assertWriteAccess() {
    if (myAssertThreading) {
      final Application application = ApplicationManager.getApplication();
      if (application != null) {
        application.assertWriteAccessAllowed();
        VirtualFile file = FileDocumentManager.getInstance().getFile(this);
        if (file != null && file.isInLocalFileSystem()) {
          ((TransactionGuardImpl)TransactionGuard.getInstance()).assertWriteActionAllowed();
        }
      }
    }
  }

  private void assertValidSeparators(@NotNull CharSequence s) {
    if (myAcceptSlashR) return;
    StringUtil.assertValidSeparators(s);
  }

  /**
   * All document change actions follows the algorithm below:
   * <pre>
   * <ol>
   *   <li>
   *     All {@link #addDocumentListener(DocumentListener) registered listeners} are notified
   *     {@link DocumentListener#beforeDocumentChange(DocumentEvent) before the change};
   *   </li>
   *   <li>The change is performed </li>
   *   <li>
   *     All {@link #addDocumentListener(DocumentListener) registered listeners} are notified
   *     {@link DocumentListener#documentChanged(DocumentEvent) after the change};
   *   </li>
   * </ol>
   * </pre>
   * <p/>
   * There is a possible case that {@code 'before change'} notification produces new change. We have a problem then - imagine
   * that initial change was {@code 'replace particular range at document end'} and {@code 'nested change'} was to
   * {@code 'remove text at document end'}. That means that when initial change will be actually performed, the document may be
   * not long enough to contain target range.
   * <p/>
   * Current method allows to check if document change is a {@code 'nested call'}.
   *
   * @throws IllegalStateException if this method is called during a {@code 'nested document modification'}
   */
  private void assertNotNestedModification() throws IllegalStateException {
    if (myChangeInProgress) {
      throw new IllegalStateException("Detected document modification from DocumentListener");
    }
  }

  private void throwGuardedFragment(@NotNull RangeMarker guard, int offset, @NotNull CharSequence oldString, @NotNull CharSequence newString) {
    if (myCheckGuardedBlocks > 0 && !myGuardsSuppressed) {
      DocumentEvent event = new DocumentEventImpl(this, offset, oldString, newString, myModificationStamp, false);
      throw new ReadOnlyFragmentModificationException(event, guard);
    }
  }

  @Override
  public void suppressGuardedExceptions() {
    myGuardsSuppressed = true;
  }

  @Override
  public void unSuppressGuardedExceptions() {
    myGuardsSuppressed = false;
  }

  @Override
  public boolean isInEventsHandling() {
    return myEventsHandling;
  }

  @Override
  public void clearLineModificationFlags() {
    myLineSet = getLineSet().clearModificationFlags();
    myFrozen = null;
  }

  public void clearLineModificationFlags(int startLine, int endLine) {
    myLineSet = getLineSet().clearModificationFlags(startLine, endLine);
    myFrozen = null;
  }

  void clearLineModificationFlagsExcept(@NotNull int[] caretLines) {
    IntArrayList modifiedLines = new IntArrayList(caretLines.length);
    LineSet lineSet = getLineSet();
    for (int line : caretLines) {
      if (line >= 0 && line < lineSet.getLineCount() && lineSet.isModified(line)) {
        modifiedLines.add(line);
      }
    }
    lineSet = lineSet.clearModificationFlags();
    for (int i = 0; i < modifiedLines.size(); i++) {
      lineSet = lineSet.setModified(modifiedLines.get(i));
    }
    myLineSet = lineSet;
    myFrozen = null;
  }

  private void updateText(@NotNull ImmutableCharSequence newText,
                          int offset,
                          @NotNull CharSequence oldString,
                          @NotNull CharSequence newString,
                          boolean wholeTextReplaced,
                          long newModificationStamp,
                          int initialStartOffset,
                          int initialOldLength) {
    assertNotNestedModification();
    myChangeInProgress = true;
    try {
      DocumentEvent event = new DocumentEventImpl(this, offset, oldString, newString, myModificationStamp, wholeTextReplaced, initialStartOffset, initialOldLength);
      beforeChangedUpdate(event);
      myTextString = null;
      ImmutableCharSequence prevText = myText;
      myText = newText;
      sequence.incrementAndGet(); // increment sequence before firing events so that modification sequence on commit will match this sequence now
      changedUpdate(event, newModificationStamp, prevText);
    }
    finally {
      myChangeInProgress = false;
    }
  }

  @Override
  public int getModificationSequence() {
    return sequence.get();
  }

  private void beforeChangedUpdate(DocumentEvent event) {
    Application app = ApplicationManager.getApplication();
    if (app != null) {
      FileDocumentManager manager = FileDocumentManager.getInstance();
      VirtualFile file = manager.getFile(this);
      if (file != null && !file.isValid()) {
        LOG.error("File of this document has been deleted.");
      }
    }
    assertInsideCommand();

    getLineSet(); // initialize line set to track changed lines

    if (!ShutDownTracker.isShutdownHookRunning()) {
      DocumentListener[] listeners = getListeners();
      for (int i = listeners.length - 1; i >= 0; i--) {
        try {
          listeners[i].beforeDocumentChange(event);
        }
        catch (Throwable e) {
          LOG.error(e);
        }
      }
    }

    myEventsHandling = true;
  }

  private void assertInsideCommand() {
    if (!myAssertThreading) return;
    CommandProcessor commandProcessor = CommandProcessor.getInstance();
    if (!commandProcessor.isUndoTransparentActionInProgress() &&
        commandProcessor.getCurrentCommand() == null) {
      throw new IncorrectOperationException("Must not change document outside command or undo-transparent action. See com.intellij.openapi.command.WriteCommandAction or com.intellij.openapi.command.CommandProcessor");
    }
  }

  private void changedUpdate(@NotNull DocumentEvent event, long newModificationStamp, @NotNull CharSequence prevText) {
    try {
      if (LOG.isDebugEnabled()) LOG.debug(event.toString());

      assert event.getOldFragment().length() ==  event.getOldLength() : "event.getOldFragment().length() = " + event.getOldFragment().length()+"; event.getOldLength() = " + event.getOldLength();
      assert event.getNewFragment().length() ==  event.getNewLength() : "event.getNewFragment().length() = " + event.getNewFragment().length()+"; event.getNewLength() = " + event.getNewLength();
      assert prevText.length() + event.getNewLength() - event.getOldLength() == getTextLength() : "prevText.length() = " + prevText.length()+ "; event.getNewLength() = " + event.getNewLength()+ "; event.getOldLength() = " + event.getOldLength()+ "; getTextLength() = " + getTextLength();

      myLineSet = getLineSet().update(prevText, event.getOffset(), event.getOffset() + event.getOldLength(), event.getNewFragment(), event.isWholeTextReplaced());
      assert getTextLength() == myLineSet.getLength() : "getTextLength() = " + getTextLength()+ "; myLineSet.getLength() = " + myLineSet.getLength();

      myFrozen = null;
      setModificationStamp(newModificationStamp);

      if (!ShutDownTracker.isShutdownHookRunning()) {
        DocumentListener[] listeners = getListeners();
        for (DocumentListener listener : listeners) {
          try {
            listener.documentChanged(event);
          }
          catch (ProcessCanceledException e) {
            if (!myAssertThreading) {
              throw e;
            }
            else {
              LOG.error("ProcessCanceledException must not be thrown from document listeners for real document", new Throwable(e));
            }
          }
          catch (Throwable e) {
            LOG.error(e);
          }
        }
      }
    }
    finally {
      myEventsHandling = false;
    }
  }

  @NotNull
  @Override
  public String getText() {
    return ApplicationManager.getApplication().runReadAction((Computable<String>)() -> doGetText());
  }

  @NotNull
  private String doGetText() {
    String s = SoftReference.dereference(myTextString);
    if (s == null) {
      myTextString = new SoftReference<>(s = myText.toString());
    }
    return s;
  }

  @NotNull
  @Override
  public String getText(@NotNull final TextRange range) {
    return ApplicationManager.getApplication().runReadAction(
      (Computable<String>)() -> myText.subSequence(range.getStartOffset(), range.getEndOffset()).toString());
  }

  @Override
  public int getTextLength() {
    return myText.length();
  }

  @Override
  @NotNull
  public CharSequence getCharsSequence() {
    return myMutableCharSequence;
  }

  @NotNull
  @Override
  public CharSequence getImmutableCharSequence() {
    return myText;
  }

  @Override
  public void addDocumentListener(@NotNull DocumentListener listener) {
    if (ArrayUtil.contains(listener, getListeners())) {
      LOG.error("Already registered: " + listener);
    }
    myDocumentListeners.add(listener);
  }

  @Override
  public void addDocumentListener(@NotNull final DocumentListener listener, @NotNull Disposable parentDisposable) {
    addDocumentListener(listener);
    Disposer.register(parentDisposable, new DocumentListenerDisposable(myDocumentListeners, listener));
  }

  // this contortion is for avoiding document leak when the listener is leaked
  private static class DocumentListenerDisposable implements Disposable {
    @NotNull private final LockFreeCOWSortedArray<DocumentListener> myList;
    @NotNull private final DocumentListener myListener;

    DocumentListenerDisposable(@NotNull LockFreeCOWSortedArray<DocumentListener> list, @NotNull DocumentListener listener) {
      myList = list;
      myListener = listener;
    }

    @Override
    public void dispose() {
      myList.remove(myListener);
    }
  }

  @Override
  public void removeDocumentListener(@NotNull DocumentListener listener) {
    boolean success = myDocumentListeners.remove(listener);
    if (!success) {
      LOG.error("Can't remove document listener (" + listener + "). Registered listeners: " + Arrays.toString(getListeners()));
    }
  }

  void addInternalBulkModeListener(@NotNull DocumentBulkUpdateListener listener) {
    myBulkDocumentInternalListeners.add(listener);
  }

  void removeInternalBulkModeListener(@NotNull DocumentBulkUpdateListener listener) {
    myBulkDocumentInternalListeners.remove(listener);
  }

  @Override
  public int getLineNumber(final int offset) {
    return getLineSet().findLineIndex(offset);
  }

  @Override
  @NotNull
  public LineIterator createLineIterator() {
    return getLineSet().createIterator();
  }

  @Override
  public final int getLineStartOffset(final int line) {
    if (line == 0) return 0; // otherwise it crashed for zero-length document
    return getLineSet().getLineStart(line);
  }

  @Override
  public final int getLineEndOffset(int line) {
    if (getTextLength() == 0 && line == 0) return 0;
    int result = getLineSet().getLineEnd(line) - getLineSeparatorLength(line);
    assert result >= 0;
    return result;
  }

  @Override
  public final int getLineSeparatorLength(int line) {
    int separatorLength = getLineSet().getSeparatorLength(line);
    assert separatorLength >= 0;
    return separatorLength;
  }

  @Override
  public final int getLineCount() {
    int lineCount = getLineSet().getLineCount();
    assert lineCount >= 0;
    return lineCount;
  }

  @NotNull
  private DocumentListener[] getListeners() {
    return myDocumentListeners.getArray();
  }

  @Override
  public void fireReadOnlyModificationAttempt() {
    for (EditReadOnlyListener listener : myReadOnlyListeners) {
      listener.readOnlyModificationAttempt(this);
    }
  }

  @Override
  public void addEditReadOnlyListener(@NotNull EditReadOnlyListener listener) {
    myReadOnlyListeners.add(listener);
  }

  @Override
  public void removeEditReadOnlyListener(@NotNull EditReadOnlyListener listener) {
    myReadOnlyListeners.remove(listener);
  }


  @Override
  public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) {
    myPropertyChangeSupport.addPropertyChangeListener(listener);
  }

  @Override
  public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) {
    myPropertyChangeSupport.removePropertyChangeListener(listener);
  }

  @Override
  public void setCyclicBufferSize(int bufferSize) {
    assert bufferSize >= 0 : bufferSize;
    myBufferSize = bufferSize;
  }

  @Override
  public void setText(@NotNull final CharSequence text) {
    Runnable runnable = () -> replaceString(0, getTextLength(), text, LocalTimeCounter.currentTime(), true);
    if (CommandProcessor.getInstance().isUndoTransparentActionInProgress()) {
      runnable.run();
    }
    else {
      CommandProcessor.getInstance().executeCommand(null, runnable, "", DocCommandGroupId.noneGroupId(this));
    }

    clearLineModificationFlags();
  }

  @Override
  @NotNull
  public RangeMarker createRangeMarker(@NotNull final TextRange textRange) {
    return createRangeMarker(textRange.getStartOffset(), textRange.getEndOffset());
  }

  @Override
  public final boolean isInBulkUpdate() {
    return myDoingBulkUpdate;
  }

  @Override
  public final void setInBulkUpdate(boolean value) {
    if (myAssertThreading) {
      ApplicationManager.getApplication().assertIsDispatchThread();
    }
    if (myUpdatingBulkModeStatus) {
      throw new IllegalStateException("Detected bulk mode status update from DocumentBulkUpdateListener");
    }
    if (myDoingBulkUpdate == value) {
      return;
    }
    myUpdatingBulkModeStatus = true;
    try {
      if (value) {
        getPublisher().updateStarted(this);
        notifyInternalListenersOnBulkModeStarted();
        myBulkUpdateEnteringTrace = new Throwable();
        myDoingBulkUpdate = true;
      }
      else {
        myDoingBulkUpdate = false;
        myBulkUpdateEnteringTrace = null;
        notifyInternalListenersOnBulkModeFinished();
        getPublisher().updateFinished(this);
      }
    }
    finally {
      myUpdatingBulkModeStatus = false;
    }
  }

  private void notifyInternalListenersOnBulkModeStarted() {
    for (DocumentBulkUpdateListener listener : myBulkDocumentInternalListeners) {
      listener.updateStarted(this);
    }
  }

  private void notifyInternalListenersOnBulkModeFinished() {
    for (DocumentBulkUpdateListener listener : myBulkDocumentInternalListeners) {
      listener.updateFinished(this);
    }
  }

  private static class DocumentBulkUpdateListenerHolder {
    private static final DocumentBulkUpdateListener ourBulkChangePublisher =
      ApplicationManager.getApplication().getMessageBus().syncPublisher(DocumentBulkUpdateListener.TOPIC);
  }

  @NotNull
  private static DocumentBulkUpdateListener getPublisher() {
    return DocumentBulkUpdateListenerHolder.ourBulkChangePublisher;
  }

  @Override
  public boolean processRangeMarkers(@NotNull Processor<? super RangeMarker> processor) {
    return processRangeMarkersOverlappingWith(0, getTextLength(), processor);
  }

  @Override
  public boolean processRangeMarkersOverlappingWith(int start, int end, @NotNull Processor<? super RangeMarker> processor) {
    TextRangeInterval interval = new TextRangeInterval(start, end);
    MarkupIterator<RangeMarkerEx> iterator = IntervalTreeImpl
      .mergingOverlappingIterator(myRangeMarkers, interval, myPersistentRangeMarkers, interval, RangeMarker.BY_START_OFFSET);
    try {
      return ContainerUtil.process(iterator, processor);
    }
    finally {
      iterator.dispose();
    }
  }

  @NotNull
  public String dumpState() {
    @NonNls StringBuilder result = new StringBuilder();
    result.append(", intervals:\n");
    for (int line = 0; line < getLineCount(); line++) {
      result.append(line).append(": ").append(getLineStartOffset(line)).append("-")
        .append(getLineEndOffset(line)).append(", ");
    }
    if (result.length() > 0) {
      result.setLength(result.length() - 1);
    }
    return result.toString();
  }

  @Override
  public String toString() {
    return "DocumentImpl[" + FileDocumentManager.getInstance().getFile(this) + "]";
  }

  @NotNull
  public FrozenDocument freeze() {
    FrozenDocument frozen = myFrozen;
    if (frozen == null) {
      synchronized (myLineSetLock) {
        frozen = myFrozen;
        if (frozen == null) {
          frozen = new FrozenDocument(myText, myLineSet, myModificationStamp, SoftReference.dereference(myTextString));
        }
      }
    }
    return frozen;
  }

  public void assertNotInBulkUpdate() {
    if (myDoingBulkUpdate) throw new UnexpectedBulkUpdateStateException(myBulkUpdateEnteringTrace);
  }

  private static class UnexpectedBulkUpdateStateException extends RuntimeException implements ExceptionWithAttachments {
    private final Attachment[] myAttachments;

    private UnexpectedBulkUpdateStateException(Throwable enteringTrace) {
      myAttachments = enteringTrace == null ? Attachment.EMPTY_ARRAY
                                            : new Attachment[] {new Attachment("enteringTrace.txt", enteringTrace)};
    }

    @NotNull
    @Override
    public Attachment[] getAttachments() {
      return myAttachments;
    }
  }
}
