/* 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_a11y_NotificationController_h_
#define mozilla_a11y_NotificationController_h_

#include <utility>

#include "EventQueue.h"
#include "nsClassHashtable.h"
#include "nsCycleCollectionParticipant.h"
#include "nsIFrame.h"
#include "nsRefreshObservers.h"
#include "nsTHashSet.h"

#ifdef A11Y_LOG
#  include "Logging.h"
#endif

namespace mozilla {

class PresShell;

namespace a11y {

class DocAccessible;

/**
 * Notification interface.
 */
class Notification {
 public:
  NS_INLINE_DECL_REFCOUNTING(mozilla::a11y::Notification)

  /**
   * Process notification.
   */
  virtual void Process() = 0;

  Notification(const Notification&) = delete;
  Notification& operator=(const Notification&) = delete;

 protected:
  Notification() = default;

  /**
   * Protected destructor, to discourage deletion outside of Release():
   */
  virtual ~Notification() = default;
};

/**
 * Template class for generic notification.
 *
 * @note  Instance is kept as a weak ref, the caller must guarantee it exists
 *        longer than the document accessible owning the notification controller
 *        that this notification is processed by.
 */
template <class Class, class... Args>
class TNotification : public Notification {
 public:
  typedef void (Class::*Callback)(Args*...);

  TNotification(Class* aInstance, Callback aCallback, Args*... aArgs)
      : mInstance(aInstance), mCallback(aCallback), mArgs(aArgs...) {}
  virtual ~TNotification() { mInstance = nullptr; }

  TNotification(const TNotification&) = delete;
  TNotification& operator=(const TNotification&) = delete;

  virtual void Process() override {
    ProcessHelper(std::index_sequence_for<Args...>{});
  }

 private:
  template <size_t... Indices>
  void ProcessHelper(std::index_sequence<Indices...>) {
    (mInstance->*mCallback)(std::get<Indices>(mArgs)...);
  }

  Class* mInstance;
  Callback mCallback;
  std::tuple<RefPtr<Args>...> mArgs;
};

/**
 * Used to process notifications from core for the document accessible.
 */
class NotificationController final : public EventQueue,
                                     public nsARefreshObserver {
 public:
  NotificationController(DocAccessible* aDocument, PresShell* aPresShell);

  NotificationController(const NotificationController&) = delete;
  NotificationController& operator=(const NotificationController&) = delete;

  NS_IMETHOD_(MozExternalRefCountType) AddRef(void) override;
  NS_IMETHOD_(MozExternalRefCountType) Release(void) override;

  NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(NotificationController)

  /**
   * Shutdown the notification controller.
   */
  void Shutdown();

  /**
   * Add an accessible event into the queue to process it later.
   */
  void QueueEvent(AccEvent* aEvent) {
    if (PushEvent(aEvent)) {
      ScheduleProcessing();
    }
  }

  /**
   * Queue a mutation event to emit if not coalesced away.  Returns true if the
   * event was queued and has not yet been coalesced.
   */
  bool QueueMutationEvent(AccTreeMutationEvent* aEvent);

  /**
   * Coalesce all queued mutation events.
   */
  void CoalesceMutationEvents();

  /**
   * Schedule binding the child document to the tree of this document.
   */
  void ScheduleChildDocBinding(DocAccessible* aDocument);

  /**
   * Schedule the accessible tree update because of rendered text changes.
   */
  inline void ScheduleTextUpdate(nsIContent* aTextNode) {
    // Make sure we are not called with a node that is not in the DOM tree or
    // not visible.
    MOZ_ASSERT(aTextNode->GetParentNode(), "A text node is not in DOM");
    MOZ_ASSERT(aTextNode->GetPrimaryFrame(),
               "A text node doesn't have a frame");
    MOZ_ASSERT(aTextNode->GetPrimaryFrame()->StyleVisibility()->IsVisible(),
               "A text node is not visible");

    mTextArray.AppendElement(aTextNode);

    ScheduleProcessing();
  }

  /**
   * Pend accessible tree update for content insertion.
   */
  void ScheduleContentInsertion(LocalAccessible* aContainer,
                                nsTArray<nsCOMPtr<nsIContent>>& aInsertions);

  /**
   * Pend an accessible subtree relocation.
   */
  void ScheduleRelocation(LocalAccessible* aOwner) {
    if (!mRelocations.Contains(aOwner)) {
      // XXX(Bug 1631371) Check if this should use a fallible operation as it
      // pretended earlier, or change the return type to void.
      mRelocations.AppendElement(aOwner);
      ScheduleProcessing();
    }
  }

  /**
   * Start to observe refresh to make notifications and events processing after
   * layout.
   */
  void ScheduleProcessing();

  /**
   * Process the generic notification synchronously if there are no pending
   * layout changes and no notifications are pending or being processed right
   * now. Otherwise, queue it up to process asynchronously.
   *
   * @note  The caller must guarantee that the given instance still exists when
   *        the notification is processed.
   */
  template <class Class, class... Args>
  inline void HandleNotification(
      Class* aInstance,
      typename TNotification<Class, Args...>::Callback aMethod,
      Args*... aArgs) {
    if (!IsUpdatePending()) {
#ifdef A11Y_LOG
      if (mozilla::a11y::logging::IsEnabled(
              mozilla::a11y::logging::eNotifications)) {
        mozilla::a11y::logging::Text("sync notification processing");
      }
#endif
      (aInstance->*aMethod)(aArgs...);
      return;
    }

    RefPtr<Notification> notification =
        new TNotification<Class, Args...>(aInstance, aMethod, aArgs...);
    if (notification) {
      // XXX(Bug 1631371) Check if this should use a fallible operation as it
      // pretended earlier.
      mNotifications.AppendElement(notification);
      ScheduleProcessing();
    }
  }

  /**
   * Schedule the generic notification to process asynchronously.
   *
   * @note  The caller must guarantee that the given instance still exists when
   *        the notification is processed.
   */
  template <class Class>
  inline void ScheduleNotification(
      Class* aInstance, typename TNotification<Class>::Callback aMethod) {
    RefPtr<Notification> notification =
        new TNotification<Class>(aInstance, aMethod);
    if (notification) {
      // XXX(Bug 1631371) Check if this should use a fallible operation as it
      // pretended earlier.
      mNotifications.AppendElement(notification);
      ScheduleProcessing();
    }
  }

  template <class Class, class Arg>
  inline void ScheduleNotification(
      Class* aInstance, typename TNotification<Class, Arg>::Callback aMethod,
      Arg* aArg) {
    RefPtr<Notification> notification =
        new TNotification<Class, Arg>(aInstance, aMethod, aArg);
    if (notification) {
      // XXX(Bug 1631371) Check if this should use a fallible operation as it
      // pretended earlier.
      mNotifications.AppendElement(notification);
      ScheduleProcessing();
    }
  }

#ifdef DEBUG
  bool IsUpdating() const {
    return mObservingState == eRefreshProcessingForUpdate;
  }
#endif

  /**
   * Return true if the accessible tree state update is pending.
   */
  bool IsUpdatePending() const;

  void ProcessRelocations();

 protected:
  virtual ~NotificationController();

  nsCycleCollectingAutoRefCnt mRefCnt;
  NS_DECL_OWNINGTHREAD

  /**
   * Return true if we should wait for processing from the parent before we can
   * process our own queue.
   */
  bool WaitingForParent() const;

 private:
  // nsARefreshObserver
  virtual void WillRefresh(mozilla::TimeStamp aTime) override;

 private:
  /**
   * Remove a specific hide event if it should not be propagated.
   */
  void CoalesceHideEvent(AccHideEvent* aHideEvent);

  /**
   * get rid of a mutation event that is no longer necessary.
   */
  void DropMutationEvent(AccTreeMutationEvent* aEvent);

  /**
   * For content process documents:
   *   Assess and queue all necessary mutation events. This function queues the
   *   events on DocAccessibleChild. To fire the queued events, call
   *   DocAccessibleChild::SendQueuedMutationEvents. This function may fire
   *   events that must occur before mutation events.
   * For parent process documents:
   *   Fire all necessary mutation events immediately.
   */
  void ProcessMutationEvents();

  /**
   * Indicates whether we're waiting on an event queue processing from our
   * notification controller to flush events.
   */
  enum eObservingState {
    eNotObservingRefresh,
    eRefreshObserving,
    eRefreshProcessing,
    eRefreshProcessingForUpdate
  };
  eObservingState mObservingState;

  /**
   * The presshell of the document accessible.
   */
  PresShell* mPresShell;

  /**
   * Child documents that needs to be bound to the tree.
   */
  nsTArray<RefPtr<DocAccessible>> mHangingChildDocuments;

  /**
   * Pending accessible tree update notifications for content insertions.
   */
  nsClassHashtable<nsRefPtrHashKey<LocalAccessible>,
                   nsTArray<nsCOMPtr<nsIContent>>>
      mContentInsertions;

  template <class T>
  class nsCOMPtrHashKey : public PLDHashEntryHdr {
   public:
    typedef T* KeyType;
    typedef const T* KeyTypePointer;

    explicit nsCOMPtrHashKey(const T* aKey) : mKey(const_cast<T*>(aKey)) {}
    nsCOMPtrHashKey(nsCOMPtrHashKey<T>&& aOther)
        : PLDHashEntryHdr(std::move(aOther)), mKey(std::move(aOther.mKey)) {}
    ~nsCOMPtrHashKey() = default;

    KeyType GetKey() const { return mKey; }
    bool KeyEquals(KeyTypePointer aKey) const { return aKey == mKey; }

    static KeyTypePointer KeyToPointer(KeyType aKey) { return aKey; }
    static PLDHashNumber HashKey(KeyTypePointer aKey) {
      return NS_PTR_TO_INT32(aKey) >> 2;
    }

    enum { ALLOW_MEMMOVE = true };

   protected:
    nsCOMPtr<T> mKey;
  };

  /**
   * Pending accessible tree update notifications for rendered text changes.
   * When there are a lot of nearby text insertions (e.g. during a reflow), it
   * is much more performant to process them in order because we then benefit
   * from the layout line cursor. Therefore, we use an array here.
   */
  nsTArray<nsCOMPtr<nsIContent>> mTextArray;

  /**
   * Other notifications like DOM events. Don't make this an AutoTArray; we
   * use SwapElements() on it.
   */
  nsTArray<RefPtr<Notification>> mNotifications;

  /**
   * Holds all scheduled relocations.
   */
  nsTArray<RefPtr<LocalAccessible>> mRelocations;

  /**
   * A list of all mutation events we may want to emit.  Ordered from the first
   * event that should be emitted to the last one to emit.
   */
  RefPtr<AccTreeMutationEvent> mFirstMutationEvent;
  RefPtr<AccTreeMutationEvent> mLastMutationEvent;

  /**
   * A class to map an accessible and event type to an event.
   */
  class EventMap {
   public:
    enum EventType {
      ShowEvent = 0x0,
      HideEvent = 0x1,
      ReorderEvent = 0x2,
    };

    void PutEvent(AccTreeMutationEvent* aEvent);
    AccTreeMutationEvent* GetEvent(LocalAccessible* aTarget, EventType aType);
    void RemoveEvent(AccTreeMutationEvent* aEvent);
    void Clear() { mTable.Clear(); }

   private:
    EventType GetEventType(AccTreeMutationEvent* aEvent);

    nsRefPtrHashtable<nsUint64HashKey, AccTreeMutationEvent> mTable;
  };

  EventMap mMutationMap;
  uint32_t mEventGeneration;
};

}  // namespace a11y
}  // namespace mozilla

#endif  // mozilla_a11y_NotificationController_h_
