/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "SelectionState.h"

#include "AutoClonedRangeArray.h"  // for AutoClonedRangeArray
#include "EditorUtils.h"           // for EditorUtils
#include "EditorLineBreak.h"       // for EditorLineBreak
#include "HTMLEditHelpers.h"       // for DeleteRangeResult

#include "ErrorList.h"
#include "mozilla/Assertions.h"    // for MOZ_ASSERT, etc.
#include "mozilla/IntegerRange.h"  // for IntegerRange
#include "mozilla/Likely.h"        // For MOZ_LIKELY and MOZ_UNLIKELY
#include "mozilla/RangeUtils.h"    // for RangeUtils
#include "mozilla/dom/RangeBinding.h"
#include "mozilla/dom/Selection.h"  // for Selection
#include "nsAString.h"              // for nsAString::Length
#include "nsCycleCollectionParticipant.h"
#include "nsDebug.h"          // for NS_WARNING, etc.
#include "nsError.h"          // for NS_OK, etc.
#include "nsIContent.h"       // for nsIContent
#include "nsISupportsImpl.h"  // for nsRange::Release
#include "nsRange.h"          // for nsRange

namespace mozilla {

using namespace dom;

/*****************************************************************************
 * mozilla::RangeItem
 *****************************************************************************/

nsINode* RangeItem::GetRoot() const {
  if (MOZ_UNLIKELY(!IsPositioned())) {
    return nullptr;
  }
  nsINode* rootNode = RangeUtils::ComputeRootNode(mStartContainer);
  if (mStartContainer == mEndContainer) {
    return rootNode;
  }
  return MOZ_LIKELY(rootNode == RangeUtils::ComputeRootNode(mEndContainer))
             ? rootNode
             : nullptr;
}

/******************************************************************************
 * mozilla::SelectionState
 *
 * Class for recording selection info.  Stores selection as collection of
 * { {startnode, startoffset} , {endnode, endoffset} } tuples.  Can't store
 * ranges since dom gravity will possibly change the ranges.
 ******************************************************************************/

template nsresult RangeUpdater::SelAdjCreateNode(const EditorDOMPoint& aPoint);
template nsresult RangeUpdater::SelAdjCreateNode(
    const EditorRawDOMPoint& aPoint);
template nsresult RangeUpdater::SelAdjInsertNode(const EditorDOMPoint& aPoint);
template nsresult RangeUpdater::SelAdjInsertNode(
    const EditorRawDOMPoint& aPoint);

SelectionState::SelectionState(const AutoClonedSelectionRangeArray& aRanges)
    : mDirection(aRanges.GetDirection()) {
  mArray.SetCapacity(aRanges.Ranges().Length());
  for (const OwningNonNull<nsRange>& range : aRanges.Ranges()) {
    RefPtr rangeItem = MakeRefPtr<RangeItem>();
    rangeItem->StoreRange(range);
    mArray.AppendElement(std::move(rangeItem));
  }
}

void SelectionState::SaveSelection(Selection& aSelection) {
  // if we need more items in the array, new them
  if (mArray.Length() < aSelection.RangeCount()) {
    for (uint32_t i = mArray.Length(); i < aSelection.RangeCount(); i++) {
      mArray.AppendElement();
      mArray[i] = new RangeItem();
    }
  } else if (mArray.Length() > aSelection.RangeCount()) {
    // else if we have too many, delete them
    mArray.TruncateLength(aSelection.RangeCount());
  }

  // now store the selection ranges
  const uint32_t rangeCount = aSelection.RangeCount();
  for (const uint32_t i : IntegerRange(rangeCount)) {
    MOZ_ASSERT(aSelection.RangeCount() == rangeCount);
    const nsRange* range = aSelection.GetRangeAt(i);
    MOZ_ASSERT(range);
    if (MOZ_UNLIKELY(NS_WARN_IF(!range))) {
      continue;
    }
    mArray[i]->StoreRange(*range);
  }

  mDirection = aSelection.GetDirection();
}

nsresult SelectionState::RestoreSelection(Selection& aSelection) {
  // clear out selection
  IgnoredErrorResult ignoredError;
  aSelection.RemoveAllRanges(ignoredError);
  NS_WARNING_ASSERTION(!ignoredError.Failed(),
                       "Selection::RemoveAllRanges() failed, but ignored");

  aSelection.SetDirection(mDirection);

  ErrorResult error;
  const CopyableAutoTArray<RefPtr<RangeItem>, 10> rangeItems(mArray);
  for (const RefPtr<RangeItem>& rangeItem : rangeItems) {
    RefPtr<nsRange> range = rangeItem->GetRange();
    if (!range) {
      NS_WARNING("RangeItem::GetRange() failed");
      return NS_ERROR_FAILURE;
    }
    aSelection.AddRangeAndSelectFramesAndNotifyListeners(*range, error);
    if (error.Failed()) {
      NS_WARNING(
          "Selection::AddRangeAndSelectFramesAndNotifyListeners() failed");
      return error.StealNSResult();
    }
  }
  return NS_OK;
}

void SelectionState::ApplyTo(AutoClonedSelectionRangeArray& aRanges) {
  aRanges.RemoveAllRanges();
  aRanges.SetDirection(mDirection);
  for (const RefPtr<RangeItem>& rangeItem : mArray) {
    RefPtr<nsRange> range = rangeItem->GetRange();
    if (MOZ_UNLIKELY(!range)) {
      continue;
    }
    aRanges.Ranges().AppendElement(std::move(range));
  }
}

bool SelectionState::Equals(const SelectionState& aOther) const {
  if (mArray.Length() != aOther.mArray.Length()) {
    return false;
  }
  if (mArray.IsEmpty()) {
    return false;  // XXX Why?
  }
  if (mDirection != aOther.mDirection) {
    return false;
  }

  for (uint32_t i : IntegerRange(mArray.Length())) {
    if (NS_WARN_IF(!mArray[i]) || NS_WARN_IF(!aOther.mArray[i]) ||
        !mArray[i]->Equals(*aOther.mArray[i])) {
      return false;
    }
  }
  // if we got here, they are equal
  return true;
}

/******************************************************************************
 * mozilla::RangeUpdater
 *
 * Class for updating nsRanges in response to editor actions.
 ******************************************************************************/

RangeUpdater::RangeUpdater() : mLocked(false) {}

void RangeUpdater::RegisterRangeItem(RangeItem& aRangeItem) {
  if (mArray.Contains(&aRangeItem)) {
    NS_ERROR("tried to register an already registered range");
    return;  // don't register it again.  It would get doubly adjusted.
  }
  mArray.AppendElement(&aRangeItem);
}

void RangeUpdater::DropRangeItem(RangeItem& aRangeItem) {
  NS_WARNING_ASSERTION(
      mArray.Contains(&aRangeItem),
      "aRangeItem is not in the range, but tried to removed from it");
  mArray.RemoveElement(&aRangeItem);
}

void RangeUpdater::RegisterSelectionState(SelectionState& aSelectionState) {
  for (RefPtr<RangeItem>& rangeItem : aSelectionState.mArray) {
    if (NS_WARN_IF(!rangeItem)) {
      continue;
    }
    RegisterRangeItem(*rangeItem);
  }
}

void RangeUpdater::DropSelectionState(SelectionState& aSelectionState) {
  for (RefPtr<RangeItem>& rangeItem : aSelectionState.mArray) {
    if (NS_WARN_IF(!rangeItem)) {
      continue;
    }
    DropRangeItem(*rangeItem);
  }
}

// gravity methods:

template <typename PT, typename CT>
nsresult RangeUpdater::SelAdjCreateNode(
    const EditorDOMPointBase<PT, CT>& aPoint) {
  if (mLocked) {
    // lock set by Will/DidReplaceParent, etc...
    return NS_OK;
  }
  if (mArray.IsEmpty()) {
    return NS_OK;
  }

  if (NS_WARN_IF(!aPoint.IsSetAndValid())) {
    return NS_ERROR_INVALID_ARG;
  }

  for (RefPtr<RangeItem>& rangeItem : mArray) {
    if (NS_WARN_IF(!rangeItem)) {
      return NS_ERROR_FAILURE;
    }
    if (rangeItem->mStartContainer == aPoint.GetContainer() &&
        rangeItem->mStartOffset > aPoint.Offset()) {
      rangeItem->mStartOffset++;
    }
    if (rangeItem->mEndContainer == aPoint.GetContainer() &&
        rangeItem->mEndOffset > aPoint.Offset()) {
      rangeItem->mEndOffset++;
    }
  }
  return NS_OK;
}

template <typename PT, typename CT>
nsresult RangeUpdater::SelAdjInsertNode(
    const EditorDOMPointBase<PT, CT>& aPoint) {
  nsresult rv = SelAdjCreateNode(aPoint);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "RangeUpdater::SelAdjCreateNode() failed");
  return rv;
}

void RangeUpdater::SelAdjDeleteNode(nsINode& aNodeToDelete) {
  if (mLocked) {
    // lock set by Will/DidReplaceParent, etc...
    return;
  }

  if (mArray.IsEmpty()) {
    return;
  }

  EditorRawDOMPoint atNodeToDelete(&aNodeToDelete);
  NS_ASSERTION(atNodeToDelete.IsSetAndValid(),
               "aNodeToDelete must be an orphan node or this is called "
               "during mutation");
  // check for range endpoints that are after aNodeToDelete and in the same
  // parent
  for (RefPtr<RangeItem>& rangeItem : mArray) {
    MOZ_ASSERT(rangeItem);

    if (rangeItem->mStartContainer == atNodeToDelete.GetContainer() &&
        rangeItem->mStartOffset > atNodeToDelete.Offset()) {
      rangeItem->mStartOffset--;
    }
    if (rangeItem->mEndContainer == atNodeToDelete.GetContainer() &&
        rangeItem->mEndOffset > atNodeToDelete.Offset()) {
      rangeItem->mEndOffset--;
    }

    // check for range endpoints that are in aNodeToDelete
    if (rangeItem->mStartContainer == &aNodeToDelete) {
      rangeItem->mStartContainer = atNodeToDelete.GetContainer();
      rangeItem->mStartOffset = atNodeToDelete.Offset();
    }
    if (rangeItem->mEndContainer == &aNodeToDelete) {
      rangeItem->mEndContainer = atNodeToDelete.GetContainer();
      rangeItem->mEndOffset = atNodeToDelete.Offset();
    }

    // check for range endpoints that are in descendants of aNodeToDelete
    bool updateEndBoundaryToo = false;
    if (EditorUtils::IsDescendantOf(*rangeItem->mStartContainer,
                                    aNodeToDelete)) {
      updateEndBoundaryToo =
          rangeItem->mStartContainer == rangeItem->mEndContainer;
      rangeItem->mStartContainer = atNodeToDelete.GetContainer();
      rangeItem->mStartOffset = atNodeToDelete.Offset();
    }

    // avoid having to call IsDescendantOf() for common case of range startnode
    // == range endnode.
    if (updateEndBoundaryToo ||
        EditorUtils::IsDescendantOf(*rangeItem->mEndContainer, aNodeToDelete)) {
      rangeItem->mEndContainer = atNodeToDelete.GetContainer();
      rangeItem->mEndOffset = atNodeToDelete.Offset();
    }
  }
}

nsresult RangeUpdater::SelAdjSplitNode(nsIContent& aOriginalContent,
                                       uint32_t aSplitOffset,
                                       nsIContent& aNewContent) {
  if (mLocked) {
    // lock set by Will/DidReplaceParent, etc...
    return NS_OK;
  }

  if (mArray.IsEmpty()) {
    return NS_OK;
  }

  EditorRawDOMPoint atNewNode(&aNewContent);
  if (NS_WARN_IF(!atNewNode.IsSetAndValid())) {
    return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
  }

  auto AdjustDOMPoint = [&](nsCOMPtr<nsINode>& aContainer,
                            uint32_t& aOffset) -> void {
    if (aContainer == atNewNode.GetContainer()) {
      // When we create a right node, we insert it after the left node.
      // In this case,
      // - `{}<left/>` should become `{}<left/><right/>` (0 -> 0)
      // - `<left/>{}` should become `<left/><right/>{}` (1 -> 2)
      // - `{<left/>}` should become `{<left/><right/>}` (0 -> 0, 1 -> 2}
      // Therefore, we need to increate the offset only when the offset equals
      // or is larger than the offset at the right node.
      if (aOffset >= atNewNode.Offset()) {
        aOffset++;
      }
    }
    // If point is in the range which are moved from aOriginalContent to
    // aNewContent, we need to change its container to aNewContent and may need
    // to adjust the offset. If point is in the range which are not moved from
    // aOriginalContent, we may need to adjust the offset.
    if (aContainer != &aOriginalContent) {
      return;
    }
    if (aOffset >= aSplitOffset) {
      aContainer = &aNewContent;
      aOffset -= aSplitOffset;
    }
  };

  for (RefPtr<RangeItem>& rangeItem : mArray) {
    if (NS_WARN_IF(!rangeItem)) {
      return NS_ERROR_FAILURE;
    }
    AdjustDOMPoint(rangeItem->mStartContainer, rangeItem->mStartOffset);
    AdjustDOMPoint(rangeItem->mEndContainer, rangeItem->mEndOffset);
  }
  return NS_OK;
}

nsresult RangeUpdater::SelAdjJoinNodes(
    const EditorRawDOMPoint& aStartOfRightContent,
    const nsIContent& aRemovedContent,
    const EditorDOMPoint& aOldPointAtRightContent) {
  MOZ_ASSERT(aStartOfRightContent.IsSetAndValid());
  MOZ_ASSERT(aOldPointAtRightContent.IsSet());  // Invalid point in most cases
  MOZ_ASSERT(aOldPointAtRightContent.HasOffset());

  if (mLocked) {
    // lock set by Will/DidReplaceParent, etc...
    return NS_OK;
  }

  if (mArray.IsEmpty()) {
    return NS_OK;
  }

  auto AdjustDOMPoint = [&](nsCOMPtr<nsINode>& aContainer,
                            uint32_t& aOffset) -> void {
    // FYI: Typically, containers of aOldPointAtRightContent and
    //      aStartOfRightContent are same.  They are different when one of the
    //      node was moved to somewhere and they are joined by undoing splitting
    //      a node.
    if (aContainer == &aRemovedContent) {
      // If the point is in the removed content, move the point to the new
      // point in the joined node.  If left node content is moved into
      // right node, the offset should be same.  Otherwise, we need to advance
      // the offset to length of the removed content.
      aContainer = aStartOfRightContent.GetContainer();
      aOffset += aStartOfRightContent.Offset();
    }
    // TODO: If aOldPointAtRightContent.GetContainer() was in aRemovedContent,
    //       we fail to adjust container and offset here because we need to
    //       make point to where aRemoveContent was.  However, collecting all
    //       ancestors of the right content may be expensive.  What's the best
    //       approach to fix this?
    else if (aContainer == aOldPointAtRightContent.GetContainer()) {
      // If the point is in common parent of joined content nodes and it
      // pointed after the right content node, decrease the offset.
      if (aOffset > aOldPointAtRightContent.Offset()) {
        aOffset--;
      }
      // If it pointed the right content node, adjust it to point ex-first
      // content of the right node.
      else if (aOffset == aOldPointAtRightContent.Offset()) {
        aContainer = aStartOfRightContent.GetContainer();
        aOffset = aStartOfRightContent.Offset();
      }
    }
  };

  for (RefPtr<RangeItem>& rangeItem : mArray) {
    if (NS_WARN_IF(!rangeItem)) {
      return NS_ERROR_FAILURE;
    }
    AdjustDOMPoint(rangeItem->mStartContainer, rangeItem->mStartOffset);
    AdjustDOMPoint(rangeItem->mEndContainer, rangeItem->mEndOffset);
  }

  return NS_OK;
}

void RangeUpdater::SelAdjReplaceText(const Text& aTextNode, uint32_t aOffset,
                                     uint32_t aReplacedLength,
                                     uint32_t aInsertedLength) {
  if (mLocked) {
    // lock set by Will/DidReplaceParent, etc...
    return;
  }

  // First, adjust selection for insertion because when offset is in the
  // replaced range, it's adjusted to aOffset and never modified by the
  // insertion if we adjust selection for deletion first.
  SelAdjInsertText(aTextNode, aOffset, aInsertedLength);

  // Then, adjust selection for deletion.
  SelAdjDeleteText(aTextNode, aOffset, aReplacedLength);
}

void RangeUpdater::SelAdjInsertText(const Text& aTextNode, uint32_t aOffset,
                                    uint32_t aInsertedLength) {
  if (mLocked) {
    // lock set by Will/DidReplaceParent, etc...
    return;
  }

  for (RefPtr<RangeItem>& rangeItem : mArray) {
    MOZ_ASSERT(rangeItem);

    if (rangeItem->mStartContainer == &aTextNode &&
        rangeItem->mStartOffset > aOffset) {
      rangeItem->mStartOffset += aInsertedLength;
    }
    if (rangeItem->mEndContainer == &aTextNode &&
        rangeItem->mEndOffset > aOffset) {
      rangeItem->mEndOffset += aInsertedLength;
    }
  }
}

void RangeUpdater::SelAdjDeleteText(const Text& aTextNode, uint32_t aOffset,
                                    uint32_t aDeletedLength) {
  if (mLocked) {
    // lock set by Will/DidReplaceParent, etc...
    return;
  }

  for (RefPtr<RangeItem>& rangeItem : mArray) {
    MOZ_ASSERT(rangeItem);

    if (rangeItem->mStartContainer == &aTextNode &&
        rangeItem->mStartOffset > aOffset) {
      if (rangeItem->mStartOffset >= aDeletedLength) {
        rangeItem->mStartOffset -= aDeletedLength;
      } else {
        rangeItem->mStartOffset = 0;
      }
    }
    if (rangeItem->mEndContainer == &aTextNode &&
        rangeItem->mEndOffset > aOffset) {
      if (rangeItem->mEndOffset >= aDeletedLength) {
        rangeItem->mEndOffset -= aDeletedLength;
      } else {
        rangeItem->mEndOffset = 0;
      }
    }
  }
}

void RangeUpdater::DidReplaceContainer(const Element& aRemovedElement,
                                       Element& aInsertedElement) {
  if (NS_WARN_IF(!mLocked)) {
    return;
  }
  mLocked = false;

  for (RefPtr<RangeItem>& rangeItem : mArray) {
    if (NS_WARN_IF(!rangeItem)) {
      return;
    }

    if (rangeItem->mStartContainer == &aRemovedElement) {
      rangeItem->mStartContainer = &aInsertedElement;
    }
    if (rangeItem->mEndContainer == &aRemovedElement) {
      rangeItem->mEndContainer = &aInsertedElement;
    }
  }
}

void RangeUpdater::DidRemoveContainer(const Element& aRemovedElement,
                                      nsINode& aRemovedElementContainerNode,
                                      uint32_t aOldOffsetOfRemovedElement,
                                      uint32_t aOldChildCountOfRemovedElement) {
  if (NS_WARN_IF(!mLocked)) {
    return;
  }
  mLocked = false;

  for (RefPtr<RangeItem>& rangeItem : mArray) {
    if (NS_WARN_IF(!rangeItem)) {
      return;
    }

    if (rangeItem->mStartContainer == &aRemovedElement) {
      rangeItem->mStartContainer = &aRemovedElementContainerNode;
      rangeItem->mStartOffset += aOldOffsetOfRemovedElement;
    } else if (rangeItem->mStartContainer == &aRemovedElementContainerNode &&
               rangeItem->mStartOffset > aOldOffsetOfRemovedElement) {
      rangeItem->mStartOffset += aOldChildCountOfRemovedElement - 1;
    }

    if (rangeItem->mEndContainer == &aRemovedElement) {
      rangeItem->mEndContainer = &aRemovedElementContainerNode;
      rangeItem->mEndOffset += aOldOffsetOfRemovedElement;
    } else if (rangeItem->mEndContainer == &aRemovedElementContainerNode &&
               rangeItem->mEndOffset > aOldOffsetOfRemovedElement) {
      rangeItem->mEndOffset += aOldChildCountOfRemovedElement - 1;
    }
  }
}

void RangeUpdater::DidMoveNodes(
    const nsTArray<SimpleEditorDOMPoint>& aOldPoints,
    const SimpleEditorDOMPoint& aExpectedDestination,
    const nsTArray<SimpleEditorDOMPoint>& aNewPoints) {
  if (mLocked) {
    // Do nothing if moving nodes is occurred while changing the container.
    return;
  }

  AutoTArray<SimpleEditorRawDOMPoint, 12> oldPoints;
  oldPoints.SetCapacity(aOldPoints.Length());
  for (const size_t i : IntegerRange(aOldPoints.Length())) {
    const SimpleEditorDOMPoint& oldPoint = aOldPoints[i];
    if (MOZ_UNLIKELY(oldPoints.IsEmpty())) {
      oldPoints.AppendElement(SimpleEditorRawDOMPoint(
          oldPoint.mContainer, oldPoint.mChild, oldPoint.Offset()));
      continue;
    }
    // Adjust offset as if preceding children removed before because if each
    // node is moved separately, the old offset does not contain the old
    // previous siblings which were moved togather.  Therefore, if moved in the
    // same container and before the offset, we don't need to adjust the offset.
    // If we moved to previous point in the same container, the old point does
    // not need to be adjusted.
    if (MOZ_UNLIKELY(aExpectedDestination.mContainer == oldPoint.mContainer) &&
        aExpectedDestination.Offset() < oldPoint.Offset()) {
      oldPoints.AppendElement(SimpleEditorRawDOMPoint(
          oldPoint.mContainer, oldPoint.mChild, oldPoint.Offset()));
      continue;
    }
    // If we moved the next sibling of the previously moved content, the
    // sibling start position is same as the previous one.
    if (oldPoints.LastElement().mChild->GetNextSibling() == oldPoint.mChild) {
      oldPoints.AppendElement(
          SimpleEditorRawDOMPoint(oldPoint.mContainer, oldPoint.mChild,
                                  oldPoints.LastElement().Offset()));
      continue;
    }
    // If the moved content was not the next sibling of the previous moved
    // content, we need to count the number of previous siblings which we
    // moved and decrease the count from the offset.  That's the offset if we
    // moved each node separately.
    uint32_t offset = oldPoint.Offset();
    for (const SimpleEditorRawDOMPoint& precedingPoint : oldPoints) {
      if (precedingPoint.mContainer == oldPoint.mContainer &&
          precedingPoint.Offset() < oldPoint.Offset()) {
        offset--;
      }
    }
    oldPoints.AppendElement(
        SimpleEditorRawDOMPoint{oldPoint.mContainer, oldPoint.mChild, offset});
  }

  size_t newPointIndex = 0;
  for (const SimpleEditorRawDOMPoint& oldPoint : oldPoints) {
    const SimpleEditorDOMPoint* newPoint =
        newPointIndex < aNewPoints.Length() &&
                oldPoint.mChild == aNewPoints[newPointIndex].mChild
            ? &aNewPoints[newPointIndex++]
            : nullptr;
    auto AdjustDOMPoint = [&](nsCOMPtr<nsINode>& aNode, uint32_t& aOffset) {
      // If the node was removed, we should adjust the point around aOldPoint.
      if (!newPoint || !newPoint->mContainer) {
        // If the point was in the removed container, the point should be moved
        // where the node was.
        if (aNode->IsInclusiveDescendantOf(oldPoint.mChild)) {
          aNode = oldPoint.mContainer;
          aOffset = std::min(oldPoint.Offset(), aNode->Length());
          return;
        }
        // If the point was where the node was or latter, fix the position.
        if (aNode == oldPoint.mContainer) {
          if (aOffset > oldPoint.Offset()) {
            aOffset--;
          }
          if (aOffset > aNode->Length()) {
            aOffset = aNode->Length();
          }
          return;
        }
        return;
      }
      // The node was moved to another point, at least.
      if (aNode == oldPoint.mContainer) {
        // If previously pointed the moved content, it should keep pointing it.
        if (aOffset == oldPoint.Offset()) {
          aNode = newPoint->mContainer;
          aOffset = newPoint->Offset();
        } else if (aOffset > oldPoint.Offset()) {
          aOffset--;
        }
        return;
      }
      if (aNode == newPoint->mContainer) {
        if (aOffset > newPoint->Offset()) {
          aOffset++;
        }
      }
    };
    for (RefPtr<RangeItem>& rangeItem : mArray) {
      if (NS_WARN_IF(!rangeItem)) {
        return;
      }

      AdjustDOMPoint(rangeItem->mStartContainer, rangeItem->mStartOffset);
      AdjustDOMPoint(rangeItem->mEndContainer, rangeItem->mEndOffset);
    }
  }
}

/******************************************************************************
 * mozilla::RangeItem
 *
 * Helper struct for SelectionState.  This stores range endpoints.
 ******************************************************************************/

NS_IMPL_CYCLE_COLLECTION(RangeItem, mStartContainer, mEndContainer)

void RangeItem::StoreRange(const nsRange& aRange) {
  mStartContainer = aRange.GetStartContainer();
  mStartOffset = aRange.StartOffset();
  mEndContainer = aRange.GetEndContainer();
  mEndOffset = aRange.EndOffset();
}

already_AddRefed<nsRange> RangeItem::GetRange() const {
  RefPtr<nsRange> range = nsRange::Create(
      mStartContainer, mStartOffset, mEndContainer, mEndOffset, IgnoreErrors());
  NS_WARNING_ASSERTION(range, "nsRange::Create() failed");
  return range.forget();
}

/******************************************************************************
 * mozilla::AutoTrackDOMPoint
 ******************************************************************************/

AutoTrackDOMPoint::AutoTrackDOMPoint(RangeUpdater& aRangeUpdater,
                                     CaretPoint* aCaretPoint)
    : AutoTrackDOMPoint(aRangeUpdater, &aCaretPoint->mCaretPoint) {}

/******************************************************************************
 * mozilla::AutoTrackDOMMoveNodeResult
 ******************************************************************************/

AutoTrackDOMMoveNodeResult::AutoTrackDOMMoveNodeResult(
    RangeUpdater& aRangeUpdater, MoveNodeResult* aMoveNodeResult)
    : mTrackCaretPoint(aRangeUpdater,
                       static_cast<CaretPoint*>(aMoveNodeResult)),
      mTrackNextInsertionPoint(aRangeUpdater,
                               &aMoveNodeResult->mNextInsertionPoint),
      mTrackMovedContentRange(aRangeUpdater,
                              &aMoveNodeResult->mMovedContentRange) {}

/******************************************************************************
 * mozilla::AutoTrackDeleteRangeResult
 ******************************************************************************/

AutoTrackDOMDeleteRangeResult::AutoTrackDOMDeleteRangeResult(
    RangeUpdater& aRangeUpdater, DeleteRangeResult* aDeleteRangeResult)
    : mTrackCaretPoint(aRangeUpdater,
                       static_cast<CaretPoint*>(aDeleteRangeResult)),
      mTrackDeleteRange(aRangeUpdater, &aDeleteRangeResult->mDeleteRange) {}

/******************************************************************************
 * mozilla::AutoTrackLineBreak
 ******************************************************************************/

AutoTrackLineBreak::AutoTrackLineBreak(RangeUpdater& aRangeUpdater,
                                       EditorLineBreak* aLineBreak)
    : mLineBreak(aLineBreak->IsPreformattedLineBreak() ? aLineBreak : nullptr),
      mPoint(mLineBreak ? mLineBreak->To<EditorDOMPoint>() : EditorDOMPoint()),
      mTracker(aRangeUpdater, &mPoint) {
  MOZ_ASSERT(aLineBreak->IsPreformattedLineBreak());
}

void AutoTrackLineBreak::Flush(enum StopTracking aStopTracking) {
  if (!mLineBreak) {
    return;
  }
  mTracker.Flush(aStopTracking);
  if (mPoint.GetContainer() == mLineBreak->mContent) {
    mLineBreak->mOffsetInText = Some(mPoint.Offset());
  }
  mLineBreak = nullptr;
}

}  // namespace mozilla
