/**
   * Merges if necessary change stored at {@link #myChanges changes collection} at the given index
   * with adjacent changes.
   *
   * @param insertionIndex index of the change that can potentially be merged with adjacent changes
   */
  private void mergeIfNecessary(int insertionIndex) {
    // Merge with previous if necessary.
    ChangeEntry toMerge = myChanges.get(insertionIndex);
    if (insertionIndex > 0) {
      ChangeEntry left = myChanges.get(insertionIndex - 1);
      if (left.getClientEndOffset() == toMerge.clientStartOffset
          && left.change.getEnd() == toMerge.change.getStart()) {
        String text = left.change.getText().toString() + toMerge.change.getText();
        left.change = new TextChangeImpl(text, left.change.getStart(), toMerge.change.getEnd());
        myChanges.remove(insertionIndex);
        insertionIndex--;
      }
    }

    // Merge with next if necessary.
    toMerge = myChanges.get(insertionIndex);
    if (insertionIndex < myChanges.size() - 1) {
      ChangeEntry right = myChanges.get(insertionIndex + 1);
      if (toMerge.getClientEndOffset() == right.clientStartOffset
          && toMerge.change.getEnd() == right.change.getStart()) {
        String text = toMerge.change.getText().toString() + right.change.getText();
        toMerge.change = new TextChangeImpl(text, toMerge.change.getStart(), right.change.getEnd());
        myChanges.remove(insertionIndex + 1);
      }
    }
  }
  /**
   * Stores given change at the current storage and returns its index at {@link #myChanges changes
   * collection} (if any).
   *
   * @param change change to store
   * @return non-negative value that indicates index under which given change is stored at the
   *     {@link #myChanges changes collection}; negative value if given change only modifies
   *     sub-range of already registered range
   */
  @SuppressWarnings({"AssignmentToForLoopParameter"})
  private int doStore(@NotNull TextChange change) {
    int newChangeStart = change.getStart();
    int newChangeEnd = change.getEnd();
    int insertionIndex = getChangeIndex(change.getStart());
    int clientShift =
        0; // 'Client text' shift before the given change to store. I.e. this value can be
           // subtracted from the
    // given change's start/end offsets in order to get original document range affected by the
    // given change.
    int changeDiff = change.getText().length() - (change.getEnd() - change.getStart());
    boolean updateClientOffsetOnly = false;

    if (insertionIndex < 0) {
      insertionIndex = -insertionIndex - 1;
      if (insertionIndex >= myChanges.size()) {
        if (!myChanges.isEmpty()) {
          ChangeEntry changeEntry = myChanges.get(myChanges.size() - 1);
          clientShift =
              changeEntry.clientStartOffset
                  - changeEntry.change.getStart()
                  + changeEntry.change.getDiff();
        }
        myChanges.add(
            new ChangeEntry(
                new TextChangeImpl(
                    change.getText(),
                    change.getStart() - clientShift,
                    change.getEnd() - clientShift),
                change.getStart()));
        return insertionIndex;
      } else if (insertionIndex > 0 && !myChanges.isEmpty()) {
        ChangeEntry changeEntry = myChanges.get(insertionIndex - 1);
        clientShift =
            changeEntry.clientStartOffset
                - changeEntry.change.getStart()
                + changeEntry.change.getDiff();
      }
    } else {
      ChangeEntry changeEntry = myChanges.get(insertionIndex);
      clientShift = changeEntry.clientStartOffset - changeEntry.change.getStart();
    }

    for (int i = insertionIndex; i < myChanges.size(); i++) {
      ChangeEntry changeEntry = myChanges.get(i);
      int storedClientStart = changeEntry.change.getStart() + clientShift;
      CharSequence storedText = changeEntry.change.getText();
      int storedClientEnd = storedClientStart + storedText.length();

      // Stored change lays after the new one.
      if (!updateClientOffsetOnly && storedClientStart > newChangeEnd) {
        if (changeDiff != 0) {
          updateClientOffsetOnly = true;
        } else {
          break;
        }
      }

      if (updateClientOffsetOnly) {
        changeEntry.clientStartOffset += changeDiff;
        continue;
      }

      // Stored change lays before the new one.
      if (storedClientEnd <= newChangeStart) {
        clientShift += changeEntry.change.getDiff();
        insertionIndex = i + 1;
        continue;
      }

      // Check if given change targets sub-range of the stored one.
      if (storedClientStart <= newChangeStart && storedClientEnd >= newChangeEnd) {
        StringBuilder adjustedText = new StringBuilder();
        if (storedClientStart < newChangeStart) {
          adjustedText.append(storedText.subSequence(0, newChangeStart - storedClientStart));
        }
        adjustedText.append(change.getText());
        if (storedClientEnd > newChangeEnd) {
          adjustedText.append(
              storedText.subSequence(newChangeEnd - storedClientStart, storedText.length()));
        }

        if (adjustedText.length() == 0
            && changeEntry.change.getStart() == changeEntry.change.getEnd()) {
          myChanges.remove(i);
          insertionIndex = -1;
          updateClientOffsetOnly = true;
          i--; // Assuming that 'i' is incremented at the 'for' loop.
          continue;
        }

        changeEntry.change =
            new TextChangeImpl(
                adjustedText, changeEntry.change.getStart(), changeEntry.change.getEnd());
        insertionIndex = -1;
        updateClientOffsetOnly = true;
        continue;
      }

      // Check if given change completely contains stored change range.
      if (newChangeStart <= storedClientStart && newChangeEnd >= storedClientEnd) {
        myChanges.remove(i);
        insertionIndex = i;
        newChangeEnd -= changeEntry.change.getDiff();
        i--; // Assuming that 'i' is incremented at the 'for' loop.
        continue;
      }

      // Check if given change intersects stored change range from the left.
      if (newChangeStart <= storedClientStart && newChangeEnd > storedClientStart) {
        int numberOfStoredChangeSymbolsToRemove = newChangeEnd - storedClientStart;
        CharSequence adjustedText =
            storedText.subSequence(numberOfStoredChangeSymbolsToRemove, storedText.length());
        changeEntry.change =
            new TextChangeImpl(
                adjustedText, changeEntry.change.getStart(), changeEntry.change.getEnd());
        changeEntry.clientStartOffset += changeDiff + numberOfStoredChangeSymbolsToRemove;
        newChangeEnd -= numberOfStoredChangeSymbolsToRemove;
        insertionIndex = i;
        continue;
      }

      // Check if given change intersects stored change range from the right.
      if (newChangeStart < storedClientEnd && newChangeEnd >= storedClientEnd) {
        CharSequence adjustedText = storedText.subSequence(0, newChangeStart - storedClientStart);
        TextChangeImpl adjusted =
            new TextChangeImpl(
                adjustedText, changeEntry.change.getStart(), changeEntry.change.getEnd());
        changeEntry.change = adjusted;
        clientShift += adjusted.getDiff();
        newChangeEnd -= storedClientEnd - newChangeStart;
        insertionIndex = i + 1;
        continue;
      }

      // Check if given change is left-adjacent to the stored change.
      if (newChangeEnd == storedClientStart) {
        changeEntry.clientStartOffset += changeDiff;
      }
    }

    if (insertionIndex >= 0) {
      myChanges.add(
          insertionIndex,
          new ChangeEntry(
              new TextChangeImpl(
                  change.getText(), newChangeStart - clientShift, newChangeEnd - clientShift),
              change.getStart()));
    }

    return insertionIndex;
  }