/* 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_EditorUtils_h
#define mozilla_EditorUtils_h

#include "mozilla/EditorBase.h"      // for EditorBase
#include "mozilla/EditorDOMPoint.h"  // for EditorDOMPoint, EditorDOMRange, etc
#include "mozilla/EditorForwards.h"
#include "mozilla/IntegerRange.h"       // for IntegerRange
#include "mozilla/Maybe.h"              // for Maybe
#include "mozilla/Result.h"             // for Result<>
#include "mozilla/dom/DataTransfer.h"   // for dom::DataTransfer
#include "mozilla/dom/Element.h"        // for dom::Element
#include "mozilla/dom/HTMLBRElement.h"  // for dom::HTMLBRElement
#include "mozilla/dom/Selection.h"      // for dom::Selection
#include "mozilla/dom/Text.h"           // for dom::Text

#include "nsAtom.h"          // for nsStaticAtom
#include "nsCOMPtr.h"        // for nsCOMPtr
#include "nsContentUtils.h"  // for nsContentUtils
#include "nsDebug.h"         // for NS_WARNING, etc
#include "nsError.h"         // for NS_SUCCESS_* and NS_ERROR_*
#include "nsRange.h"         // for nsRange
#include "nsString.h"        // for nsAString, nsString, etc

class nsITransferable;

namespace mozilla {

enum class StyleWhiteSpace : uint8_t;

enum class SuggestCaret {
  // If specified, the method returns NS_OK when there is no recommended caret
  // position.
  OnlyIfHasSuggestion,
  // If specified and if EditorBase::AllowsTransactionsToChangeSelection
  // returns false, the method does nothing and returns NS_OK.
  OnlyIfTransactionsAllowedToDoIt,
  // If specified, the method returns
  // NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR even if
  // EditorBase::CollapseSelectionTo returns an error except when
  // NS_ERROR_EDITOR_DESTROYED.
  AndIgnoreTrivialError,
};

/******************************************************************************
 * CaretPoint is a wrapper of EditorDOMPoint and provides a helper method to
 * collapse Selection there, or move it to a local variable.  This is typically
 * used as the ok type of Result or a base class of DoSomethingResult classes.
 ******************************************************************************/
class MOZ_STACK_CLASS CaretPoint {
 public:
  explicit CaretPoint(const EditorDOMPoint& aPointToPutCaret)
      : mCaretPoint(aPointToPutCaret) {}
  explicit CaretPoint(EditorDOMPoint&& aPointToPutCaret)
      : mCaretPoint(std::move(aPointToPutCaret)) {}

  CaretPoint(const CaretPoint&) = delete;
  CaretPoint& operator=(const CaretPoint&) = delete;
  CaretPoint(CaretPoint&&) = default;
  CaretPoint& operator=(CaretPoint&&) = default;

  /**
   * Suggest caret position to aEditorBase.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult SuggestCaretPointTo(
      EditorBase& aEditorBase, const SuggestCaretOptions& aOptions) const;

  /**
   * IgnoreCaretPointSuggestion() should be called if the method does not want
   * to use caret position recommended by this instance.
   */
  void IgnoreCaretPointSuggestion() const { mHandledCaretPoint = true; }

  /**
   * When propagating the result, it may not want to the caller modify
   * selection.  In such case, this can clear the caret point.  Use
   * IgnoreCaretPointSuggestion() in the caller side instead.
   */
  void ForgetCaretPointSuggestion() { mCaretPoint.Clear(); }

  bool HasCaretPointSuggestion() const { return mCaretPoint.IsSet(); }
  constexpr const EditorDOMPoint& CaretPointRef() const { return mCaretPoint; }
  constexpr EditorDOMPoint&& UnwrapCaretPoint() {
    mHandledCaretPoint = true;
    return std::move(mCaretPoint);
  }
  bool CopyCaretPointTo(EditorDOMPoint& aPointToPutCaret,
                        const SuggestCaretOptions& aOptions) const {
    MOZ_ASSERT(!aOptions.contains(SuggestCaret::AndIgnoreTrivialError));
    MOZ_ASSERT(
        !aOptions.contains(SuggestCaret::OnlyIfTransactionsAllowedToDoIt));
    mHandledCaretPoint = true;
    if (aOptions.contains(SuggestCaret::OnlyIfHasSuggestion) &&
        !mCaretPoint.IsSet()) {
      return false;
    }
    aPointToPutCaret = mCaretPoint;
    return true;
  }
  bool CopyCaretPointTo(CaretPoint& aCaretPoint,
                        const SuggestCaretOptions& aOptions) const {
    return CopyCaretPointTo(aCaretPoint.mCaretPoint, aOptions);
  }
  bool MoveCaretPointTo(EditorDOMPoint& aPointToPutCaret,
                        const SuggestCaretOptions& aOptions) {
    MOZ_ASSERT(!aOptions.contains(SuggestCaret::AndIgnoreTrivialError));
    MOZ_ASSERT(
        !aOptions.contains(SuggestCaret::OnlyIfTransactionsAllowedToDoIt));
    if (aOptions.contains(SuggestCaret::OnlyIfHasSuggestion) &&
        !mCaretPoint.IsSet()) {
      return false;
    }
    aPointToPutCaret = UnwrapCaretPoint();
    return true;
  }
  bool MoveCaretPointTo(CaretPoint& aCaretPoint,
                        const SuggestCaretOptions& aOptions) {
    return MoveCaretPointTo(aCaretPoint.mCaretPoint, aOptions);
  }
  bool CopyCaretPointTo(EditorDOMPoint& aPointToPutCaret,
                        const EditorBase& aEditorBase,
                        const SuggestCaretOptions& aOptions) const;
  bool MoveCaretPointTo(EditorDOMPoint& aPointToPutCaret,
                        const EditorBase& aEditorBase,
                        const SuggestCaretOptions& aOptions);

 protected:
  constexpr bool CaretPointHandled() const { return mHandledCaretPoint; }

  void SetCaretPoint(const EditorDOMPoint& aCaretPoint) {
    mHandledCaretPoint = false;
    mCaretPoint = aCaretPoint;
  }
  void SetCaretPoint(EditorDOMPoint&& aCaretPoint) {
    mHandledCaretPoint = false;
    mCaretPoint = std::move(aCaretPoint);
  }

  void UnmarkAsHandledCaretPoint() { mHandledCaretPoint = true; }

  CaretPoint() = default;

 private:
  EditorDOMPoint mCaretPoint;
  bool mutable mHandledCaretPoint = false;

  friend class AutoTrackDOMPoint;
};

/***************************************************************************
 * EditActionResult is useful to return the handling state of edit sub actions
 * without out params.
 */
class MOZ_STACK_CLASS EditActionResult {
 public:
  bool Canceled() const { return mCanceled; }
  bool Handled() const { return mHandled; }
  bool Ignored() const { return !mCanceled && !mHandled; }

  void MarkAsCanceled() { mCanceled = true; }
  void MarkAsHandled() { mHandled = true; }

  EditActionResult& operator|=(const EditActionResult& aOther) {
    mCanceled |= aOther.mCanceled;
    mHandled |= aOther.mHandled;
    return *this;
  }

  static EditActionResult IgnoredResult() {
    return EditActionResult(false, false);
  }
  static EditActionResult HandledResult() {
    return EditActionResult(false, true);
  }
  static EditActionResult CanceledResult() {
    return EditActionResult(true, true);
  }

  EditActionResult(const EditActionResult&) = delete;
  EditActionResult& operator=(const EditActionResult&) = delete;
  EditActionResult(EditActionResult&&) = default;
  EditActionResult& operator=(EditActionResult&&) = default;

 protected:
  EditActionResult(bool aCanceled, bool aHandled)
      : mCanceled(aCanceled), mHandled(aHandled) {}

  EditActionResult() : mCanceled(false), mHandled(false) {}

  void UnmarkAsCanceled() { mCanceled = false; }

 private:
  bool mCanceled = false;
  bool mHandled = false;
};

/***************************************************************************
 * CreateNodeResultBase is a simple class for CreateSomething() methods
 * which want to return new node.
 */
template <typename NodeType>
class MOZ_STACK_CLASS CreateNodeResultBase final : public CaretPoint {
  using SelfType = CreateNodeResultBase<NodeType>;

 public:
  bool Handled() const { return mNode; }
  NodeType* GetNewNode() const { return mNode; }
  RefPtr<NodeType> UnwrapNewNode() { return std::move(mNode); }

  CreateNodeResultBase() = delete;
  explicit CreateNodeResultBase(NodeType& aNode) : mNode(&aNode) {}
  explicit CreateNodeResultBase(NodeType& aNode,
                                const EditorDOMPoint& aCandidateCaretPoint)
      : CaretPoint(aCandidateCaretPoint), mNode(&aNode) {}
  explicit CreateNodeResultBase(NodeType& aNode,
                                EditorDOMPoint&& aCandidateCaretPoint)
      : CaretPoint(std::move(aCandidateCaretPoint)), mNode(&aNode) {}

  template <typename NT>
  explicit CreateNodeResultBase(RefPtr<NT>&& aNode)
      : mNode(std::forward<RefPtr<NT>>(aNode)) {}
  template <typename NT>
  explicit CreateNodeResultBase(RefPtr<NT>&& aNode,
                                const EditorDOMPoint& aCandidateCaretPoint)
      : CaretPoint(aCandidateCaretPoint),
        mNode(std::forward<RefPtr<NT>>(aNode)) {
    MOZ_ASSERT(mNode);
  }
  template <typename NT>
  explicit CreateNodeResultBase(RefPtr<NT>&& aNode,
                                EditorDOMPoint&& aCandidateCaretPoint)
      : CaretPoint(std::move(aCandidateCaretPoint)),
        mNode(std::forward<RefPtr<NT>>(aNode)) {
    MOZ_ASSERT(mNode);
  }

  [[nodiscard]] static SelfType NotHandled() {
    return SelfType(EditorDOMPoint());
  }
  [[nodiscard]] static SelfType NotHandled(
      const EditorDOMPoint& aPointToPutCaret) {
    SelfType result(aPointToPutCaret);
    return result;
  }
  [[nodiscard]] static SelfType NotHandled(EditorDOMPoint&& aPointToPutCaret) {
    SelfType result(std::move(aPointToPutCaret));
    return result;
  }

#ifdef DEBUG
  ~CreateNodeResultBase() {
    MOZ_ASSERT(!HasCaretPointSuggestion() || CaretPointHandled());
  }
#endif

  CreateNodeResultBase(const SelfType& aOther) = delete;
  SelfType& operator=(const SelfType& aOther) = delete;
  CreateNodeResultBase(SelfType&& aOther) = default;
  SelfType& operator=(SelfType&& aOther) = default;

 private:
  explicit CreateNodeResultBase(const EditorDOMPoint& aCandidateCaretPoint)
      : CaretPoint(aCandidateCaretPoint) {}
  explicit CreateNodeResultBase(EditorDOMPoint&& aCandidateCaretPoint)
      : CaretPoint(std::move(aCandidateCaretPoint)) {}

  RefPtr<NodeType> mNode;
};

/**
 * This is a result of inserting text.  If the text inserted as a part of
 * composition, this does not return CaretPoint.  Otherwise, must return
 * CaretPoint which is typically same as end of inserted text.
 */
class MOZ_STACK_CLASS InsertTextResult final : public CaretPoint {
 public:
  InsertTextResult() : CaretPoint(EditorDOMPoint()) {}
  template <typename EditorDOMPointType>
  explicit InsertTextResult(const EditorDOMPointType& aEndOfInsertedText)
      : CaretPoint(EditorDOMPoint()),
        mEndOfInsertedText(aEndOfInsertedText.template To<EditorDOMPoint>()) {}
  explicit InsertTextResult(EditorDOMPoint&& aEndOfInsertedText)
      : CaretPoint(EditorDOMPoint()),
        mEndOfInsertedText(std::move(aEndOfInsertedText)) {}
  template <typename PT, typename CT>
  InsertTextResult(EditorDOMPoint&& aEndOfInsertedText,
                   const EditorDOMPointBase<PT, CT>& aCaretPoint)
      : CaretPoint(aCaretPoint.template To<EditorDOMPoint>()),
        mEndOfInsertedText(std::move(aEndOfInsertedText)) {}
  InsertTextResult(EditorDOMPoint&& aEndOfInsertedText,
                   CaretPoint&& aCaretPoint)
      : CaretPoint(std::move(aCaretPoint)),
        mEndOfInsertedText(std::move(aEndOfInsertedText)) {
    UnmarkAsHandledCaretPoint();
  }
  InsertTextResult(InsertTextResult&& aOther, EditorDOMPoint&& aCaretPoint)
      : CaretPoint(std::move(aCaretPoint)),
        mEndOfInsertedText(std::move(aOther.mEndOfInsertedText)) {}

  [[nodiscard]] bool Handled() const { return mEndOfInsertedText.IsSet(); }
  const EditorDOMPoint& EndOfInsertedTextRef() const {
    return mEndOfInsertedText;
  }

 private:
  EditorDOMPoint mEndOfInsertedText;
};

/***************************************************************************
 * stack based helper class for calling EditorBase::EndTransaction() after
 * EditorBase::BeginTransaction().  This shouldn't be used in editor classes
 * or helper classes while an edit action is being handled.  Use
 * AutoTransactionBatch in such cases since it uses non-virtual internal
 * methods.
 ***************************************************************************/
class MOZ_RAII AutoTransactionBatchExternal final {
 public:
  MOZ_CAN_RUN_SCRIPT explicit AutoTransactionBatchExternal(
      EditorBase& aEditorBase)
      : mEditorBase(aEditorBase) {
    MOZ_KnownLive(mEditorBase).BeginTransaction();
  }

  MOZ_CAN_RUN_SCRIPT ~AutoTransactionBatchExternal() {
    MOZ_KnownLive(mEditorBase).EndTransaction();
  }

 private:
  EditorBase& mEditorBase;
};

/******************************************************************************
 * AutoSelectionRangeArray stores all ranges in `aSelection`.
 * Note that modifying the ranges means modifing the selection ranges.
 *****************************************************************************/
class MOZ_STACK_CLASS AutoSelectionRangeArray final {
 public:
  explicit AutoSelectionRangeArray(dom::Selection& aSelection) {
    for (const uint32_t i : IntegerRange(aSelection.RangeCount())) {
      MOZ_ASSERT(aSelection.GetRangeAt(i));
      mRanges.AppendElement(*aSelection.GetRangeAt(i));
    }
  }

  AutoTArray<mozilla::OwningNonNull<nsRange>, 8> mRanges;
};

/******************************************************************************
 * AutoTrackDataTransferForPaste keeps track of whether the paste event handler
 * in JS has modified the clipboard.
 *****************************************************************************/
class MOZ_STACK_CLASS AutoTrackDataTransferForPaste {
 public:
  MOZ_CAN_RUN_SCRIPT AutoTrackDataTransferForPaste(
      const EditorBase& aEditorBase,
      RefPtr<dom::DataTransfer>& aDataTransferForPaste)
      : mEditorBase(aEditorBase),
        mDataTransferForPaste(aDataTransferForPaste.get_address()) {
    mEditorBase.GetDocument()->ClearClipboardCopyTriggered();
  }

  ~AutoTrackDataTransferForPaste() { FlushAndStopTracking(); }

 private:
  void FlushAndStopTracking() {
    if (!mDataTransferForPaste ||
        !mEditorBase.GetDocument()->IsClipboardCopyTriggered()) {
      return;
    }
    // The paste event copied new data to the clipboard, so we need to use
    // that data to paste into the DOM element below.
    if (*mDataTransferForPaste) {
      (*mDataTransferForPaste)->ClearForPaste();
    }
    // Just null this out so this data won't be used and we will get it directly
    // from the clipboard in the future.
    *mDataTransferForPaste = nullptr;
    mDataTransferForPaste = nullptr;
  }

  MOZ_KNOWN_LIVE const EditorBase& mEditorBase;
  RefPtr<dom::DataTransfer>* mDataTransferForPaste;
};

class EditorUtils final {
 public:
  using EditorType = EditorBase::EditorType;
  using Selection = dom::Selection;

  /**
   * IsDescendantOf() checks if aNode is a child or a descendant of aParent.
   * aOutPoint is set to the child of aParent.
   *
   * @return            true if aNode is a child or a descendant of aParent.
   */
  static bool IsDescendantOf(const nsINode& aNode, const nsINode& aParent,
                             EditorRawDOMPoint* aOutPoint = nullptr);
  static bool IsDescendantOf(const nsINode& aNode, const nsINode& aParent,
                             EditorDOMPoint* aOutPoint);

  /**
   * Returns true if aContent is a <br> element and it's marked as padding for
   * empty editor.
   */
  static bool IsPaddingBRElementForEmptyEditor(const nsIContent& aContent) {
    const dom::HTMLBRElement* brElement =
        dom::HTMLBRElement::FromNode(&aContent);
    return brElement && brElement->IsPaddingForEmptyEditor();
  }

  /**
   * Returns true if aContent is a <br> element and it's marked as padding for
   * empty last line.
   */
  static bool IsPaddingBRElementForEmptyLastLine(const nsIContent& aContent) {
    const dom::HTMLBRElement* brElement =
        dom::HTMLBRElement::FromNode(&aContent);
    return brElement && brElement->IsPaddingForEmptyLastLine();
  }

  /**
   * IsEditableContent() returns true if aContent's data or children is ediable
   * for the given editor type.  Be aware, returning true does NOT mean the
   * node can be removed from its parent node, and returning false does NOT
   * mean the node cannot be removed from the parent node.
   * XXX May be the anonymous nodes in TextEditor not editable?  If it's not
   *     so, we can get rid of aEditorType.
   */
  static bool IsEditableContent(const nsIContent& aContent,
                                EditorType aEditorType) {
    if (aEditorType == EditorType::HTML &&
        (!aContent.IsEditable() || !aContent.IsInComposedDoc())) {
      // FIXME(emilio): Why only for HTML editors? All content from the root
      // content in text editors is also editable, so afaict we can remove the
      // special-case.
      return false;
    }
    return IsElementOrText(aContent);
  }

  /**
   * Returns true if aContent is a usual element node (not padding <br> element
   * for empty editor) or a text node.  In other words, returns true if
   * aContent is a usual element node or visible data node.
   */
  static bool IsElementOrText(const nsIContent& aContent) {
    if (aContent.IsText()) {
      return true;
    }
    return aContent.IsElement() && !IsPaddingBRElementForEmptyEditor(aContent);
  }

  /**
   * Get the two longhands that make up computed white-space style of aContent.
   */
  static Maybe<std::pair<StyleWhiteSpaceCollapse, StyleTextWrapMode>>
  GetComputedWhiteSpaceStyles(const nsIContent& aContent);

  /**
   * IsWhiteSpacePreformatted() checks the style info for the node for the
   * preformatted text style.  This does NOT flush layout.
   */
  static bool IsWhiteSpacePreformatted(const nsIContent& aContent);

  /**
   * IsNewLinePreformatted() checks whether the linefeed characters are
   * preformatted or white-spaces.  This does NOT flush layout.
   * Be aware that even if this returns false, the linefeed characters may be
   * rendered as non-collapsible white-spaces.  Therefore, if you want to check
   * whether linefeeds are collapsible or not, you should refer the result of
   * IsWhiteSpacePreformatted().
   */
  static bool IsNewLinePreformatted(const nsIContent& aContent);

  /**
   * IsOnlyNewLinePreformatted() checks whether the linefeed characters are
   * preformated but white-spaces are collapsed, or otherwise.  I.e., this
   * returns true only when `white-space-collapse:pre-line`.
   */
  static bool IsOnlyNewLinePreformatted(const nsIContent& aContent);

  static nsStaticAtom* GetTagNameAtom(const nsAString& aTagName) {
    if (aTagName.IsEmpty()) {
      return nullptr;
    }
    nsAutoString lowerTagName;
    nsContentUtils::ASCIIToLower(aTagName, lowerTagName);
    return NS_GetStaticAtom(lowerTagName);
  }

  static nsStaticAtom* GetAttributeAtom(const nsAString& aAttribute) {
    if (aAttribute.IsEmpty()) {
      return nullptr;  // Don't use nsGkAtoms::_empty for attribute.
    }
    return NS_GetStaticAtom(aAttribute);
  }

  /**
   * Helper method for deletion.  When this returns true, Selection will be
   * computed with nsFrameSelection that also requires flushed layout
   * information.
   */
  template <typename SelectionOrAutoClonedRangeArray>
  static bool IsFrameSelectionRequiredToExtendSelection(
      nsIEditor::EDirection aDirectionAndAmount,
      SelectionOrAutoClonedRangeArray& aSelectionOrAutoClonedRangeArray) {
    switch (aDirectionAndAmount) {
      case nsIEditor::eNextWord:
      case nsIEditor::ePreviousWord:
      case nsIEditor::eToBeginningOfLine:
      case nsIEditor::eToEndOfLine:
        return true;
      case nsIEditor::ePrevious:
      case nsIEditor::eNext:
        return aSelectionOrAutoClonedRangeArray.IsCollapsed();
      default:
        return false;
    }
  }

  /**
   * Create an nsITransferable instance which has kTextMime and
   * kMozTextInternal flavors.
   */
  static Result<nsCOMPtr<nsITransferable>, nsresult>
  CreateTransferableForPlainText(const dom::Document& aDocument);
};

}  // namespace mozilla

#endif  // #ifndef mozilla_EditorUtils_h
