/* 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_PendingStyles_h
#define mozilla_PendingStyles_h

#include "mozilla/EditorDOMPoint.h"
#include "mozilla/EditorForwards.h"
#include "mozilla/EventForwards.h"
#include "mozilla/Maybe.h"
#include "mozilla/UniquePtr.h"
#include "nsAtom.h"
#include "nsCOMPtr.h"
#include "nsCycleCollectionParticipant.h"
#include "nsGkAtoms.h"
#include "nsISupportsImpl.h"
#include "nsString.h"
#include "nsTArray.h"
#include "nscore.h"

class nsINode;

namespace mozilla {
namespace dom {
class MouseEvent;
class Selection;
}  // namespace dom

enum class SpecifiedStyle : uint8_t { Preserve, Discard };

class PendingStyle final {
 public:
  PendingStyle() = delete;
  PendingStyle(nsStaticAtom* aTag, nsAtom* aAttribute, const nsAString& aValue,
               SpecifiedStyle aSpecifiedStyle = SpecifiedStyle::Preserve)
      : mTag(aTag),
        mAttribute(aAttribute != nsGkAtoms::_empty ? aAttribute : nullptr),
        mAttributeValueOrCSSValue(aValue),
        mSpecifiedStyle(aSpecifiedStyle) {
    MOZ_COUNT_CTOR(PendingStyle);
  }
  MOZ_COUNTED_DTOR(PendingStyle)

  MOZ_KNOWN_LIVE nsStaticAtom* GetTag() const { return mTag; }
  MOZ_KNOWN_LIVE nsAtom* GetAttribute() const { return mAttribute; }
  const nsString& AttributeValueOrCSSValueRef() const {
    return mAttributeValueOrCSSValue;
  }
  void UpdateAttributeValueOrCSSValue(const nsAString& aNewValue) {
    mAttributeValueOrCSSValue = aNewValue;
  }
  SpecifiedStyle GetSpecifiedStyle() const { return mSpecifiedStyle; }

  EditorInlineStyle ToInlineStyle() const;
  EditorInlineStyleAndValue ToInlineStyleAndValue() const;

 private:
  MOZ_KNOWN_LIVE nsStaticAtom* const mTag = nullptr;
  // TODO: Once we stop using `HTMLEditor::SetInlinePropertiesAsSubAction` to
  //       add any attributes of <a href>, we can make this `nsStaticAtom*`.
  MOZ_KNOWN_LIVE const RefPtr<nsAtom> mAttribute;
  // If the editor is in CSS mode, this value is the property value.
  // If the editor is in HTML mode, this value is not empty only when
  // - mAttribute is not nullptr
  // - mTag is CSS invertible style and "-moz-editor-invert-value"
  nsString mAttributeValueOrCSSValue;
  // Whether the class and style attribute should be preserved or discarded.
  const SpecifiedStyle mSpecifiedStyle = SpecifiedStyle::Preserve;
};

class PendingStyleCache final {
 public:
  PendingStyleCache() = delete;
  PendingStyleCache(const nsStaticAtom& aTag, const nsStaticAtom* aAttribute,
                    const nsAString& aValue)
      // Needs const_cast hack here because the this class users may want
      // non-const nsStaticAtom reference/pointer due to bug 1794954
      : mTag(const_cast<nsStaticAtom&>(aTag)),
        mAttribute(const_cast<nsStaticAtom*>(aAttribute)),
        mAttributeValueOrCSSValue(aValue) {}
  PendingStyleCache(const nsStaticAtom& aTag, const nsStaticAtom* aAttribute,
                    nsAString&& aValue)
      // Needs const_cast hack here because the this class users may want
      // non-const nsStaticAtom reference/pointer due to bug 1794954
      : mTag(const_cast<nsStaticAtom&>(aTag)),
        mAttribute(const_cast<nsStaticAtom*>(aAttribute)),
        mAttributeValueOrCSSValue(std::move(aValue)) {}

  MOZ_KNOWN_LIVE nsStaticAtom& TagRef() const { return mTag; }
  MOZ_KNOWN_LIVE nsStaticAtom* GetAttribute() const { return mAttribute; }
  const nsString& AttributeValueOrCSSValueRef() const {
    return mAttributeValueOrCSSValue;
  }

  EditorInlineStyle ToInlineStyle() const;

 private:
  MOZ_KNOWN_LIVE nsStaticAtom& mTag;
  MOZ_KNOWN_LIVE nsStaticAtom* const mAttribute;
  const nsString mAttributeValueOrCSSValue;
};

class MOZ_STACK_CLASS AutoPendingStyleCacheArray final
    : public AutoTArray<PendingStyleCache, 21> {
 public:
  [[nodiscard]] index_type IndexOf(const nsStaticAtom& aTag,
                                   const nsStaticAtom* aAttribute) const {
    for (index_type index = 0; index < Length(); ++index) {
      const PendingStyleCache& styleCache = ElementAt(index);
      if (&styleCache.TagRef() == &aTag &&
          styleCache.GetAttribute() == aAttribute) {
        return index;
      }
    }
    return NoIndex;
  }

  [[nodiscard]] bool Contains(const nsStaticAtom& aTag,
                              const nsStaticAtom* aAttribute) const {
    return IndexOf(aTag, aAttribute) != NoIndex;
  }
};

enum class PendingStyleState {
  // Will be not changed in new content
  NotUpdated,
  // Will be applied to new content
  BeingPreserved,
  // Will be cleared from new content
  BeingCleared,
};

/******************************************************************************
 * PendingStyles stores pending inline styles which WILL be applied to new
 * content when it'll be inserted.  I.e., updating styles of this class means
 * that it does NOT update the DOM tree immediately.
 ******************************************************************************/
class PendingStyles final {
 public:
  NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(PendingStyles)
  NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(PendingStyles)

  PendingStyles() = default;

  void Reset() {
    mClearingStyles.Clear();
    mPreservingStyles.Clear();
  }

  nsresult UpdateSelState(const HTMLEditor& aHTMLEditor);

  /**
   * PreHandleMouseEvent() is called when `HTMLEditorEventListener` receives
   * "mousedown" and "mouseup" events.  Note that `aMouseDownOrUpEvent` may not
   * be acceptable event for the `HTMLEditor`, but this is called even in
   * the case because the event may cause a following `OnSelectionChange()`
   * call.
   */
  void PreHandleMouseEvent(const dom::MouseEvent& aMouseDownOrUpEvent);

  void PreHandleSelectionChangeCommand(Command aCommand);
  void PostHandleSelectionChangeCommand(const HTMLEditor& aHTMLEditor,
                                        Command aCommand);

  void OnSelectionChange(const HTMLEditor& aHTMLEditor, int16_t aReason);

  /**
   * Preserve the style for next inserting content.  E.g., when user types next
   * character, an inline element which provides the style will be inserted
   * and the typing character will appear in it.
   *
   * @param aHTMLProperty       The HTML tag name which represents the style.
   *                            For example, nsGkAtoms::b for bold text.
   * @param aAttribute          nullptr or attribute name which represents the
   *                            style with aHTMLProperty.  E.g., nsGkAtoms::size
   *                            for nsGkAtoms::font.
   * @param aAttributeValueOrCSSValue
   *                            New value of aAttribute or new CSS value if the
   *                            editor is in the CSS mode.
   */
  void PreserveStyle(nsStaticAtom& aHTMLProperty, nsAtom* aAttribute,
                     const nsAString& aAttributeValueOrCSSValue);

  /**
   * Preserve the styles with new values in aStylesToPreserve.
   * See above for the detail.
   */
  void PreserveStyles(
      const nsTArray<EditorInlineStyleAndValue>& aStylesToPreserve);

  /**
   * Preserve the style with new value specified with aStyleToPreserve.
   * See above for the detail.
   */
  void PreserveStyle(const PendingStyleCache& aStyleToPreserve) {
    PreserveStyle(aStyleToPreserve.TagRef(), aStyleToPreserve.GetAttribute(),
                  aStyleToPreserve.AttributeValueOrCSSValueRef());
  }

  /**
   * Clear the style when next content is inserted.  E.g., when user types next
   * character, it'll will be inserted after parent element which provides the
   * style and clones element which represents other styles in the parent
   * element.
   *
   * @param aHTMLProperty       The HTML tag name which represents the style.
   *                            For example, nsGkAtoms::b for bold text.
   * @param aAttribute          nullptr or attribute name which represents the
   *                            style with aHTMLProperty.  E.g., nsGkAtoms::size
   *                            for nsGkAtoms::font.
   */
  void ClearStyle(nsStaticAtom& aHTMLProperty, nsAtom* aAttribute) {
    ClearStyleInternal(&aHTMLProperty, aAttribute);
  }

  /**
   * Clear all styles specified by aStylesToClear when next content is inserted.
   * See above for the detail.
   */
  void ClearStyles(const nsTArray<EditorInlineStyle>& aStylesToClear);

  /**
   * Clear all styles when next inserting content.  E.g., when user types next
   * character, it will be inserted outside any inline parents which provides
   * current text style.
   */
  void ClearAllStyles() {
    // XXX Why don't we clear mClearingStyles first?
    ClearStyleInternal(nullptr, nullptr);
  }

  /**
   * Clear <a> element and discard styles which is applied by it.
   */
  void ClearLinkAndItsSpecifiedStyle() {
    ClearStyleInternal(nsGkAtoms::a, nullptr, SpecifiedStyle::Discard);
  }

  /**
   * TakeClearingStyle() hands back next property item on the clearing styles.
   * This must be used only for handling to clear the styles from inserting
   * content.
   */
  UniquePtr<PendingStyle> TakeClearingStyle() {
    if (mClearingStyles.IsEmpty()) {
      return nullptr;
    }
    return mClearingStyles.PopLastElement();
  }

  /**
   * TakeAllPreservedStyles() moves all preserved styles and values to
   * aOutStylesAndValues.
   */
  void TakeAllPreservedStyles(
      nsTArray<EditorInlineStyleAndValue>& aOutStylesAndValues);

  /**
   * TakeRelativeFontSize() hands back relative font value, which is then
   * cleared out.
   */
  int32_t TakeRelativeFontSize();

  /**
   * GetStyleState() returns whether the style will be applied to new content,
   * removed from new content or not changed.
   *
   * @param aHTMLProperty       The HTML tag name which represents the style.
   *                            For example, nsGkAtoms::b for bold text.
   * @param aAttribute          nullptr or attribute name which represents the
   *                            style with aHTMLProperty.  E.g., nsGkAtoms::size
   *                            for nsGkAtoms::font.
   * @param aOutNewAttributeValueOrCSSValue
   *                            [out, optional] If applied to new content, this
   *                            is set to the new value.
   */
  PendingStyleState GetStyleState(
      nsStaticAtom& aHTMLProperty, nsAtom* aAttribute = nullptr,
      nsString* aOutNewAttributeValueOrCSSValue = nullptr) const;

 protected:
  virtual ~PendingStyles() { Reset(); };

  void ClearStyleInternal(
      nsStaticAtom* aHTMLProperty, nsAtom* aAttribute,
      SpecifiedStyle aSpecifiedStyle = SpecifiedStyle::Preserve);

  void CancelPreservingStyle(nsStaticAtom* aHTMLProperty, nsAtom* aAttribute);
  void CancelClearingStyle(nsStaticAtom& aHTMLProperty, nsAtom* aAttribute);

  Maybe<size_t> IndexOfPreservingStyle(nsStaticAtom& aHTMLProperty,
                                       nsAtom* aAttribute,
                                       nsAString* aOutValue = nullptr) const {
    return IndexOfStyleInArray(&aHTMLProperty, aAttribute, aOutValue,
                               mPreservingStyles);
  }
  Maybe<size_t> IndexOfClearingStyle(nsStaticAtom* aHTMLProperty,
                                     nsAtom* aAttribute) const {
    return IndexOfStyleInArray(aHTMLProperty, aAttribute, nullptr,
                               mClearingStyles);
  }

  bool IsLinkStyleSet() const {
    return IndexOfPreservingStyle(*nsGkAtoms::a, nullptr).isSome();
  }
  bool IsExplicitlyLinkStyleCleared() const {
    return IndexOfClearingStyle(nsGkAtoms::a, nullptr).isSome();
  }
  bool IsOnlyLinkStyleCleared() const {
    return mClearingStyles.Length() == 1 && IsExplicitlyLinkStyleCleared();
  }
  bool IsStyleCleared(nsStaticAtom* aHTMLProperty, nsAtom* aAttribute) const {
    return IndexOfClearingStyle(aHTMLProperty, aAttribute).isSome() ||
           AreAllStylesCleared();
  }
  bool AreAllStylesCleared() const {
    return IndexOfClearingStyle(nullptr, nullptr).isSome();
  }
  bool AreSomeStylesSet() const { return !mPreservingStyles.IsEmpty(); }
  bool AreSomeStylesCleared() const { return !mClearingStyles.IsEmpty(); }

  static Maybe<size_t> IndexOfStyleInArray(
      nsStaticAtom* aHTMLProperty, nsAtom* aAttribute, nsAString* aOutValue,
      const nsTArray<UniquePtr<PendingStyle>>& aArray);

  nsTArray<UniquePtr<PendingStyle>> mPreservingStyles;
  nsTArray<UniquePtr<PendingStyle>> mClearingStyles;
  EditorDOMPoint mLastSelectionPoint;
  int32_t mRelativeFontSize = 0;
  Command mLastSelectionCommand = Command::DoNothing;
  bool mMouseDownFiredInLinkElement = false;
  bool mMouseUpFiredInLinkElement = false;
};

}  // namespace mozilla

#endif  // #ifndef mozilla_PendingStyles_h
