/* 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/. */

#ifndef mozilla_TextServicesDocument_h
#define mozilla_TextServicesDocument_h

#include "mozilla/Maybe.h"
#include "mozilla/UniquePtr.h"
#include "nsCOMPtr.h"
#include "nsCycleCollectionParticipant.h"
#include "nsIEditActionListener.h"
#include "nsISupportsImpl.h"
#include "nsStringFwd.h"
#include "nsTArray.h"
#include "nscore.h"

class nsIContent;
class nsIEditor;
class nsINode;
class nsISelectionController;
class nsRange;

namespace mozilla {

class EditorBase;
class FilteredContentIterator;
class OffsetEntry;

namespace dom {
class AbstractRange;
class Document;
class Element;
class StaticRange;
};  // namespace dom

/**
 * The TextServicesDocument presents the document in as a bunch of flattened
 * text blocks. Each text block can be retrieved as an nsString.
 */
class TextServicesDocument final : public nsIEditActionListener {
 private:
  enum class IteratorStatus : uint8_t {
    // No iterator (I), or iterator doesn't point to anything valid.
    eDone = 0,
    // I points to first text node (TN) in current block (CB).
    eValid,
    // No TN in CB, I points to first TN in prev block.
    ePrev,
    // No TN in CB, I points to first TN in next block.
    eNext,
  };

  class OffsetEntryArray final : public nsTArray<UniquePtr<OffsetEntry>> {
   public:
    /**
     * Init() initializes this array with aFilteredIter.
     *
     * @param[in]  aIterRange   Can be nullptr.
     * @param[out] aAllTextInBlock
     *                          Returns all text in the block.
     */
    Result<IteratorStatus, nsresult> Init(
        FilteredContentIterator& aFilteredIter, IteratorStatus aIteratorStatus,
        nsRange* aIterRange, nsAString* aAllTextInBlock = nullptr);

    /**
     * Returns index of first `OffsetEntry` which manages aTextNode.
     */
    Maybe<size_t> FirstIndexOf(const dom::Text& aTextNode) const;

    /**
     * FindWordRange() returns a word range starting from aStartPointToScan
     * in aAllTextInBlock.
     */
    Result<EditorDOMRangeInTexts, nsresult> FindWordRange(
        nsAString& aAllTextInBlock, const EditorRawDOMPoint& aStartPointToScan);

    /**
     * SplitElementAt() splits an `OffsetEntry` at aIndex if aOffsetInTextNode
     * is middle of the range in the text node.
     *
     * @param aIndex    Index of the entry which you want to split.
     * @param aOffsetInTextNode
     *                  Offset in the text node.  I.e., the offset should be
     *                  greater than 0 and less than `mLength`.
     */
    nsresult SplitElementAt(size_t aIndex, uint32_t aOffsetInTextNode);

    /**
     * Remove all `OffsetEntry` elements whose `mIsValid` is set to false.
     */
    void RemoveInvalidElements();

    /**
     * Called when non-collapsed selection will be deleted.
     */
    nsresult WillDeleteSelection();

    /**
     * Called when non-collapsed selection is deleteded.
     */
    OffsetEntry* DidDeleteSelection();

    /**
     * Called when aInsertedText is inserted.
     */
    MOZ_CAN_RUN_SCRIPT nsresult DidInsertText(dom::Selection* aSelection,
                                              const nsAString& aInsertedString);

    /**
     * Called when selection range will be applied to the DOM Selection.
     */
    Result<EditorRawDOMRangeInTexts, nsresult> WillSetSelection(
        uint32_t aOffsetInTextInBlock, uint32_t aLength);

    class Selection final {
     public:
      size_t StartIndex() const {
        MOZ_ASSERT(IsIndexesSet());
        return *mStartIndex;
      }
      size_t EndIndex() const {
        MOZ_ASSERT(IsIndexesSet());
        return *mEndIndex;
      }

      uint32_t StartOffsetInTextInBlock() const {
        MOZ_ASSERT(IsSet());
        return *mStartOffsetInTextInBlock;
      }
      uint32_t EndOffsetInTextInBlock() const {
        MOZ_ASSERT(IsSet());
        return *mEndOffsetInTextInBlock;
      }
      uint32_t LengthInTextInBlock() const {
        MOZ_ASSERT(IsSet());
        return *mEndOffsetInTextInBlock - *mStartOffsetInTextInBlock;
      }

      bool IsCollapsed() {
        return !IsSet() || (IsInSameElement() && StartOffsetInTextInBlock() ==
                                                     EndOffsetInTextInBlock());
      }

      bool IsIndexesSet() const {
        return mStartIndex.isSome() && mEndIndex.isSome();
      }
      bool IsSet() const {
        return IsIndexesSet() && mStartOffsetInTextInBlock.isSome() &&
               mEndOffsetInTextInBlock.isSome();
      }
      bool IsInSameElement() const {
        return IsIndexesSet() && StartIndex() == EndIndex();
      }

      void Reset() {
        mStartIndex.reset();
        mEndIndex.reset();
        mStartOffsetInTextInBlock.reset();
        mEndOffsetInTextInBlock.reset();
      }
      void SetIndex(size_t aIndex) { mEndIndex = mStartIndex = Some(aIndex); }
      void Set(size_t aIndex, uint32_t aOffsetInTextInBlock) {
        mEndIndex = mStartIndex = Some(aIndex);
        mStartOffsetInTextInBlock = mEndOffsetInTextInBlock =
            Some(aOffsetInTextInBlock);
      }
      void SetIndexes(size_t aStartIndex, size_t aEndIndex) {
        MOZ_DIAGNOSTIC_ASSERT(aStartIndex <= aEndIndex);
        mStartIndex = Some(aStartIndex);
        mEndIndex = Some(aEndIndex);
      }
      void Set(size_t aStartIndex, size_t aEndIndex,
               uint32_t aStartOffsetInTextInBlock,
               uint32_t aEndOffsetInTextInBlock) {
        MOZ_DIAGNOSTIC_ASSERT(aStartIndex <= aEndIndex);
        mStartIndex = Some(aStartIndex);
        mEndIndex = Some(aEndIndex);
        mStartOffsetInTextInBlock = Some(aStartOffsetInTextInBlock);
        mEndOffsetInTextInBlock = Some(aEndOffsetInTextInBlock);
      }

      void CollapseToStart() {
        MOZ_ASSERT(mStartIndex.isSome());
        MOZ_ASSERT(mStartOffsetInTextInBlock.isSome());
        mEndIndex = mStartIndex;
        mEndOffsetInTextInBlock = mStartOffsetInTextInBlock;
      }

     private:
      Maybe<size_t> mStartIndex;
      Maybe<size_t> mEndIndex;
      // Selected start and end offset in all text in a block element.
      Maybe<uint32_t> mStartOffsetInTextInBlock;
      Maybe<uint32_t> mEndOffsetInTextInBlock;
    };
    Selection mSelection;
  };

  RefPtr<dom::Document> mDocument;
  nsCOMPtr<nsISelectionController> mSelCon;
  RefPtr<EditorBase> mEditorBase;
  RefPtr<FilteredContentIterator> mFilteredIter;
  nsCOMPtr<nsIContent> mPrevTextBlock;
  nsCOMPtr<nsIContent> mNextTextBlock;
  OffsetEntryArray mOffsetTable;
  RefPtr<nsRange> mExtent;

  uint32_t mTxtSvcFilterType;
  IteratorStatus mIteratorStatus;

 protected:
  virtual ~TextServicesDocument() = default;

 public:
  TextServicesDocument();

  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
  NS_DECL_CYCLE_COLLECTION_CLASS(TextServicesDocument)

  /**
   * Initializes the text services document to use a particular editor. The
   * text services document will use the DOM document and presentation shell
   * used by the editor.
   *
   * @param aEditor             The editor to use.
   */
  nsresult InitWithEditor(nsIEditor* aEditor);

  /**
   * Sets the range/extent over which the text services document will iterate.
   * Note that InitWithEditor() should have been called prior to calling this
   * method.  If this method is never called, the text services defaults to
   * iterating over the entire document.
   *
   * @param aAbstractRange      The range to use. aAbstractRange must point to a
   *                            valid range object.
   */
  nsresult SetExtent(const dom::AbstractRange* aAbstractRange);

  /**
   * Expands the end points of the range so that it spans complete words.  This
   * call does not change any internal state of the text services document.
   *
   * @param aStaticRange        [in/out] The range to be expanded/adjusted.
   */
  nsresult ExpandRangeToWordBoundaries(dom::StaticRange* aStaticRange);

  /**
   * Sets the filter type to be used while iterating over content.
   * This will clear the current filter type if it's not either
   * FILTERTYPE_NORMAL or FILTERTYPE_MAIL.
   *
   * @param aFilterType         The filter type to be used while iterating over
   *                            content.
   */
  nsresult SetFilterType(uint32_t aFilterType);

  /**
   * Returns the text in the current text block.
   *
   * @param aStr                [OUT] This will contain the text.
   */
  nsresult GetCurrentTextBlock(nsAString& aStr);

  /**
   * Tells the document to point to the first text block in the document.  This
   * method does not adjust the current cursor position or selection.
   */
  nsresult FirstBlock();

  enum class BlockSelectionStatus {
    // There is no text block (TB) in or before the selection (S).
    eBlockNotFound = 0,
    // No TB in S, but found one before/after S.
    eBlockOutside,
    // S extends beyond the start and end of TB.
    eBlockInside,
    // TB contains entire S.
    eBlockContains,
    // S begins or ends in TB but extends outside of TB.
    eBlockPartial,
  };

  /**
   * Tells the document to point to the last text block that contains the
   * current selection or caret.
   *
   * @param aSelectionStatus    [OUT] This will contain the text block
   *                            selection status.
   * @param aSelectionOffset    [OUT] This will contain the offset into the
   *                            string returned by GetCurrentTextBlock() where
   *                            the selection begins.
   * @param aLength             [OUT] This will contain the number of
   *                            characters that are selected in the string.
   */
  MOZ_CAN_RUN_SCRIPT
  nsresult LastSelectedBlock(BlockSelectionStatus* aSelStatus,
                             uint32_t* aSelOffset, uint32_t* aSelLength);

  /**
   * Tells the document to point to the text block before the current one.
   * This method will return NS_OK, even if there is no previous block.
   * Callers should call IsDone() to check if we have gone beyond the first
   * text block in the document.
   */
  nsresult PrevBlock();

  /**
   * Tells the document to point to the text block after the current one.
   * This method will return NS_OK, even if there is no next block. Callers
   * should call IsDone() to check if we have gone beyond the last text block
   * in the document.
   */
  nsresult NextBlock();

  /**
   * IsDone() will always set aIsDone == false unless the document contains
   * no text, PrevBlock() was called while the document was already pointing
   * to the first text block in the document, or NextBlock() was called while
   * the document was already pointing to the last text block in the document.
   *
   * @param aIsDone             [OUT] This will contain the result.
   */
  nsresult IsDone(bool* aIsDone);

  /**
   * SetSelection() allows the caller to set the selection based on an offset
   * into the string returned by GetCurrentTextBlock().  A length of zero
   * places the cursor at that offset. A positive non-zero length "n" selects
   * n characters in the string.
   *
   * @param aOffset             Offset into string returned by
   *                            GetCurrentTextBlock().
   * @param aLength             Number of characters selected.
   */
  MOZ_CAN_RUN_SCRIPT nsresult SetSelection(uint32_t aOffset, uint32_t aLength);

  /**
   * Scrolls the document so that the current selection is visible.
   */
  nsresult ScrollSelectionIntoView();

  /**
   * Deletes the text selected by SetSelection(). Calling DeleteSelection()
   * with nothing selected, or with a collapsed selection (cursor) does
   * nothing and returns NS_OK.
   */
  MOZ_CAN_RUN_SCRIPT
  nsresult DeleteSelection();

  /**
   * Inserts the given text at the current cursor position.  If there is a
   * selection, it will be deleted before the text is inserted.
   */
  MOZ_CAN_RUN_SCRIPT
  nsresult InsertText(const nsAString& aText);

  /**
   * nsIEditActionListener method implementations.
   */
  NS_DECL_NSIEDITACTIONLISTENER

  /**
   * Actual edit action listeners.  When you add new method here for listening
   * to new edit action, you need to make it called by EditorBase.
   * Additionally, you need to call it from proper method of
   * nsIEditActionListener too because if this is created not for inline
   * spell checker of the editor, edit actions will be notified via
   * nsIEditActionListener (slow path, though).
   */
  void DidDeleteContent(const nsIContent& aChildContent);
  void DidJoinContents(const EditorRawDOMPoint& aJoinedPoint,
                       const nsIContent& aRemovedContent);

 private:
  // TODO: We should get rid of this method since `aAbstractRange` has
  //       enough simple API to get them.
  static nsresult GetRangeEndPoints(const dom::AbstractRange* aAbstractRange,
                                    nsINode** aStartContainer,
                                    uint32_t* aStartOffset,
                                    nsINode** aEndContainer,
                                    uint32_t* aEndOffset);

  nsresult CreateFilteredContentIterator(
      const dom::AbstractRange* aAbstractRange,
      FilteredContentIterator** aFilteredIter);

  dom::Element* GetDocumentContentRootNode() const;
  already_AddRefed<nsRange> CreateDocumentContentRange();
  already_AddRefed<nsRange> CreateDocumentContentRootToNodeOffsetRange(
      nsINode* aParent, uint32_t aOffset, bool aToStart);
  nsresult CreateDocumentContentIterator(
      FilteredContentIterator** aFilteredIter);

  nsresult AdjustContentIterator();

  static nsresult FirstTextNode(FilteredContentIterator* aFilteredIter,
                                IteratorStatus* aIteratorStatus);
  static nsresult LastTextNode(FilteredContentIterator* aFilteredIter,
                               IteratorStatus* aIteratorStatus);

  static nsresult FirstTextNodeInCurrentBlock(
      FilteredContentIterator* aFilteredIter);
  static nsresult FirstTextNodeInPrevBlock(
      FilteredContentIterator* aFilteredIter);
  static nsresult FirstTextNodeInNextBlock(
      FilteredContentIterator* aFilteredIter);

  nsresult GetFirstTextNodeInPrevBlock(nsIContent** aContent);
  nsresult GetFirstTextNodeInNextBlock(nsIContent** aContent);

  static bool DidSkip(FilteredContentIterator* aFilteredIter);
  static void ClearDidSkip(FilteredContentIterator* aFilteredIter);

  static bool HasSameBlockNodeParent(dom::Text& aTextNode1,
                                     dom::Text& aTextNode2);

  MOZ_CAN_RUN_SCRIPT nsresult SetSelectionInternal(uint32_t aOffset,
                                                   uint32_t aLength,
                                                   bool aDoUpdate);
  MOZ_CAN_RUN_SCRIPT nsresult GetSelection(BlockSelectionStatus* aSelStatus,
                                           uint32_t* aSelOffset,
                                           uint32_t* aSelLength);
  MOZ_CAN_RUN_SCRIPT nsresult
  GetCollapsedSelection(BlockSelectionStatus* aSelStatus, uint32_t* aSelOffset,
                        uint32_t* aSelLength);
  nsresult GetUncollapsedSelection(BlockSelectionStatus* aSelStatus,
                                   uint32_t* aSelOffset, uint32_t* aSelLength);
};

}  // namespace mozilla

#endif  // #ifndef mozilla_TextServicesDocument_h
