/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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_textcompositionsynthesizer_h_
#define mozilla_textcompositionsynthesizer_h_

#include "mozilla/RefPtr.h"
#include "nsString.h"
#include "mozilla/Attributes.h"
#include "mozilla/EventForwards.h"
#include "mozilla/Maybe.h"
#include "mozilla/TextEventDispatcherListener.h"
#include "mozilla/TextRange.h"
#include "mozilla/widget/IMEData.h"
#include "WritingModes.h"

class nsIWidget;

namespace mozilla {
namespace widget {

class PuppetWidget;

/**
 * TextEventDispatcher is a helper class for dispatching widget events defined
 * in TextEvents.h.  Currently, this is a helper for dispatching
 * WidgetCompositionEvent and WidgetKeyboardEvent.  This manages the behavior
 * of them for conforming to DOM Level 3 Events.
 * An instance of this class is created by nsIWidget instance and owned by it.
 * This is typically created only by the top level widgets because only they
 * handle IME.
 */

class TextEventDispatcher final {
  ~TextEventDispatcher() = default;

  NS_INLINE_DECL_REFCOUNTING(TextEventDispatcher)

 public:
  explicit TextEventDispatcher(nsIWidget* aWidget);

  /**
   * Initializes the instance for IME or automated test.  Either IME or tests
   * need to call one of them before starting composition.  If they return
   * NS_ERROR_ALREADY_INITIALIZED, it means that the listener already listens
   * notifications from TextEventDispatcher for same purpose (for IME or tests).
   * If this returns another error, the caller shouldn't keep starting
   * composition.
   *
   * @param aListener       Specify the listener to listen notifications and
   *                        requests.  This must not be null.
   *                        NOTE: aListener is stored as weak reference in
   *                              TextEventDispatcher.  See mListener
   *                              definition below.
   */
  nsresult BeginInputTransaction(TextEventDispatcherListener* aListener);
  nsresult BeginTestInputTransaction(TextEventDispatcherListener* aListener,
                                     bool aIsAPZAware);
  nsresult BeginNativeInputTransaction();

  /**
   * BeginInputTransactionFor() should be used when aPuppetWidget dispatches
   * a composition or keyboard event coming from its parent process.
   */
  nsresult BeginInputTransactionFor(const WidgetGUIEvent* aEvent,
                                    PuppetWidget* aPuppetWidget);

  /**
   * EndInputTransaction() should be called when the listener stops using
   * the TextEventDispatcher.
   *
   * @param aListener       The listener using the TextEventDispatcher instance.
   */
  void EndInputTransaction(TextEventDispatcherListener* aListener);

  /**
   * OnDestroyWidget() is called when mWidget is being destroyed.
   */
  void OnDestroyWidget();

  nsIWidget* GetWidget() const { return mWidget; }

  /**
   * Return true starting from ending handling focus notification and until
   * receiving blur notification.
   */
  bool HasFocus() const { return mHasFocus; }

  const IMENotificationRequests& IMENotificationRequestsRef() const {
    return mIMENotificationRequests;
  }

  /**
   * OnWidgetChangeIMENotificationRequests() is called when aWidget's
   * IMENotificationRequest is maybe modified by unusual path.  E.g.,
   * modified in an async path.
   */
  void OnWidgetChangeIMENotificationRequests(nsIWidget* aWidget) {
    MOZ_ASSERT(aWidget);
    if (mWidget == aWidget) {
      UpdateNotificationRequests();
    }
  }

  /**
   * GetState() returns current state of this class.
   *
   * @return        NS_OK: Fine to compose text.
   *                NS_ERROR_NOT_INITIALIZED: BeginInputTransaction() or
   *                                          BeginInputTransactionForTests()
   *                                          should be called.
   *                NS_ERROR_NOT_AVAILABLE: The widget isn't available for
   *                                        composition.
   */
  nsresult GetState() const;

  /**
   * IsComposing() returns true after calling StartComposition() and before
   * calling CommitComposition().  In other words, native IME has composition
   * when this returns true.
   */
  bool IsComposing() const { return mIsComposing; }

  /**
   * IsHandlingComposition() returns true after calling StartComposition() and
   * content has not handled eCompositionCommit(AsIs) event.  In other words,
   * our content has composition when this returns true.
   */
  bool IsHandlingComposition() const { return mIsHandlingComposition; }

  /**
   * IsInNativeInputTransaction() returns true if native IME handler began a
   * transaction and it's not finished yet.
   */
  bool IsInNativeInputTransaction() const {
    return mInputTransactionType == eNativeInputTransaction;
  }

  /**
   * IsDispatchingEvent() returns true while this instance dispatching an event.
   */
  bool IsDispatchingEvent() const { return mDispatchingEvent > 0; }

  /**
   * GetPseudoIMEContext() returns pseudo native IME context if there is an
   * input transaction whose type is not for native event handler.
   * Otherwise, returns nullptr.
   */
  void* GetPseudoIMEContext() const {
    if (mInputTransactionType == eNoInputTransaction ||
        mInputTransactionType == eNativeInputTransaction) {
      return nullptr;
    }
    return const_cast<TextEventDispatcher*>(this);
  }

  /**
   * Return writing mode at selection while this has focus.  Otherwise, or
   * never exists selection ranges, this returns Nothing.
   */
  const Maybe<WritingMode>& MaybeWritingModeRefAtSelection() const {
    return mWritingMode;
  }

  /**
   * MaybeQueryWritingModeAtSelection() returns writing mode at current
   * selection even if this does not have focus.  If this is not focused, this
   * queries selection.  Then, chrome script can run due to flushing the layout
   * if an element in chrome has focus (but it should not cause any problem
   * hopefully).
   */
  MOZ_CAN_RUN_SCRIPT Maybe<WritingMode> MaybeQueryWritingModeAtSelection()
      const;

  /**
   * StartComposition() starts composition explicitly.
   *
   * @param aEventTime  If this is not nullptr, WidgetCompositionEvent will
   *                    be initialized with this.  Otherwise, initialized
   *                    with the time at initializing.
   */
  nsresult StartComposition(nsEventStatus& aStatus,
                            const WidgetEventTime* aEventTime = nullptr);

  /**
   * CommitComposition() commits composition.
   *
   * @param aCommitString   If this is null, commits with the last composition
   *                        string.  Otherwise, commits the composition with
   *                        this value.
   * @param aEventTime      If this is not nullptr, WidgetCompositionEvent will
   *                        be initialized with this.  Otherwise, initialized
   *                        with the time at initializing.
   */
  nsresult CommitComposition(nsEventStatus& aStatus,
                             const nsAString* aCommitString = nullptr,
                             const WidgetEventTime* aEventTime = nullptr);

  /**
   * SetPendingCompositionString() sets new composition string which will be
   * dispatched with eCompositionChange event by calling Flush().
   *
   * @param aString         New composition string.
   */
  nsresult SetPendingCompositionString(const nsAString& aString) {
    return mPendingComposition.SetString(aString);
  }

  /**
   * AppendClauseToPendingComposition() appends a clause information to
   * the pending composition string.
   *
   * @param aLength         Length of the clause.
   * @param aTextRangeType  One of TextRangeType::eRawClause,
   *                        TextRangeType::eSelectedRawClause,
   *                        TextRangeType::eConvertedClause or
   *                        TextRangeType::eSelectedClause.
   */
  nsresult AppendClauseToPendingComposition(uint32_t aLength,
                                            TextRangeType aTextRangeType) {
    return mPendingComposition.AppendClause(aLength, aTextRangeType);
  }

  /**
   * SetCaretInPendingComposition() sets caret position in the pending
   * composition string and its length.  This is optional.  If IME doesn't
   * want to show caret, it shouldn't need to call this.
   *
   * @param aOffset         Offset of the caret in the pending composition
   *                        string.  This should not be larger than the length
   *                        of the pending composition string.
   * @param aLength         Caret width.  If this is 0, caret will be collapsed.
   *                        Note that Gecko doesn't supported wide caret yet,
   *                        therefore, this is ignored for now.
   */
  nsresult SetCaretInPendingComposition(uint32_t aOffset, uint32_t aLength) {
    return mPendingComposition.SetCaret(aOffset, aLength);
  }

  /**
   * SetPendingComposition() is useful if native IME handler already creates
   * array of clauses and/or caret information.
   *
   * @param aString         Composition string.  This may include native line
   *                        breakers since they will be replaced with XP line
   *                        breakers automatically.
   * @param aRanges         This should include the ranges of clauses and/or
   *                        a range of caret.  Note that this method allows
   *                        some ranges overlap each other and the range order
   *                        is not from start to end.
   */
  nsresult SetPendingComposition(const nsAString& aString,
                                 const TextRangeArray* aRanges) {
    return mPendingComposition.Set(aString, aRanges);
  }

  /**
   * FlushPendingComposition() sends the pending composition string
   * to the widget of the store DOM window.  Before calling this, IME needs to
   * set pending composition string with SetPendingCompositionString(),
   * AppendClauseToPendingComposition() and/or
   * SetCaretInPendingComposition().
   *
   * @param aEventTime      If this is not nullptr, WidgetCompositionEvent will
   *                        be initialized with this.  Otherwise, initialized
   *                        with the time at initializing.
   */
  nsresult FlushPendingComposition(
      nsEventStatus& aStatus, const WidgetEventTime* aEventTime = nullptr) {
    return mPendingComposition.Flush(this, aStatus, aEventTime);
  }

  /**
   * ClearPendingComposition() makes this instance forget pending composition.
   */
  void ClearPendingComposition() { mPendingComposition.Clear(); }

  /**
   * GetPendingCompositionClauses() returns text ranges which was appended by
   * AppendClauseToPendingComposition() or SetPendingComposition().
   */
  const TextRangeArray* GetPendingCompositionClauses() const {
    return mPendingComposition.GetClauses();
  }

  /**
   * @see nsIWidget::NotifyIME()
   */
  // Calling NotifyIME may call OS's API so that everything could happen.
  // We should mark it MOZ_CAN_RUN_SCRIPT later.
  MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
  NotifyIME(const IMENotification& aIMENotification);

  /**
   * DispatchKeyboardEvent() maybe dispatches aKeyboardEvent.
   *
   * @param aMessage        Must be eKeyDown or eKeyUp.
   *                        Use MaybeDispatchKeypressEvents() for dispatching
   *                        eKeyPress.
   * @param aKeyboardEvent  A keyboard event.
   * @param aStatus         If dispatching event should be marked as consumed,
   *                        set nsEventStatus_eConsumeNoDefault.  Otherwise,
   *                        set nsEventStatus_eIgnore.  After dispatching
   *                        a event and it's consumed this returns
   *                        nsEventStatus_eConsumeNoDefault.
   * @param aData           Calling this method may cause calling
   *                        WillDispatchKeyboardEvent() of the listener.
   *                        aData will be set to its argument.
   * @return                true if an event is dispatched.  Otherwise, false.
   */
  bool DispatchKeyboardEvent(EventMessage aMessage,
                             const WidgetKeyboardEvent& aKeyboardEvent,
                             nsEventStatus& aStatus, void* aData = nullptr);

  /**
   * MaybeDispatchKeypressEvents() maybe dispatches a keypress event which is
   * generated from aKeydownEvent.
   *
   * @param aKeyboardEvent  A keyboard event.
   * @param aStatus         Sets the result when the caller dispatches
   *                        aKeyboardEvent.  Note that if the value is
   *                        nsEventStatus_eConsumeNoDefault, this does NOT
   *                        dispatch keypress events.
   *                        When this method dispatches one or more keypress
   *                        events and one of them is consumed, this returns
   *                        nsEventStatus_eConsumeNoDefault.
   * @param aData           Calling this method may cause calling
   *                        WillDispatchKeyboardEvent() of the listener.
   *                        aData will be set to its argument.
   * @param aNeedsCallback  Set true when caller needs to initialize each
   *                        eKeyPress event immediately before dispatch.
   *                        Then, WillDispatchKeyboardEvent() is always called.
   * @return                true if one or more events are dispatched.
   *                        Otherwise, false.
   */
  bool MaybeDispatchKeypressEvents(const WidgetKeyboardEvent& aKeyboardEvent,
                                   nsEventStatus& aStatus,
                                   void* aData = nullptr,
                                   bool aNeedsCallback = false);

 private:
  // mWidget is owner of the instance.  When this is created, this is set.
  // And when mWidget is released, this is cleared by OnDestroyWidget().
  // Note that mWidget may be destroyed already (i.e., mWidget->Destroyed() may
  // return true).
  nsIWidget* mWidget;
  // mListener is a weak reference to TextEventDispatcherListener.  That might
  // be referred by JS.  Therefore, the listener might be difficult to release
  // itself if this is a strong reference.  Additionally, it's difficult to
  // check if a method to uninstall the listener is called by valid instance.
  // So, using weak reference is the best way in this case.
  nsWeakPtr mListener;
  // mIMENotificationRequests should store current IME's notification requests.
  // So, this may be invalid when IME doesn't have focus.
  IMENotificationRequests mIMENotificationRequests;
  // mWritingMode stores writing mode at current selection starting from
  // receiving focus notification and until receiving blur notification.  When
  // selection is changed, this is updated by every selection change
  // notification.
  Maybe<WritingMode> mWritingMode;

  // mPendingComposition stores new composition string temporarily.
  // These values will be used for dispatching eCompositionChange event
  // in Flush().  When Flush() is called, the members will be cleared
  // automatically.
  class PendingComposition {
   public:
    PendingComposition();
    nsresult SetString(const nsAString& aString);
    nsresult AppendClause(uint32_t aLength, TextRangeType aTextRangeType);
    nsresult SetCaret(uint32_t aOffset, uint32_t aLength);
    nsresult Set(const nsAString& aString, const TextRangeArray* aRanges);
    nsresult Flush(TextEventDispatcher* aDispatcher, nsEventStatus& aStatus,
                   const WidgetEventTime* aEventTime);
    const TextRangeArray* GetClauses() const { return mClauses; }
    void Clear();

   private:
    nsString mString;
    RefPtr<TextRangeArray> mClauses;
    TextRange mCaret;
    bool mReplacedNativeLineBreakers;

    void EnsureClauseArray();

    /**
     * ReplaceNativeLineBreakers() replaces "\r\n" and "\r" to "\n" and adjust
     * each clause information and the caret information.
     */
    void ReplaceNativeLineBreakers();

    /**
     * AdjustRange() adjusts aRange as in the string with XP line breakers.
     *
     * @param aRange            The reference to a range in aNativeString.
     *                          This will be modified.
     * @param aNativeString     The string with native line breakers.
     *                          This may include "\r\n" and/or "\r".
     */
    static void AdjustRange(TextRange& aRange, const nsAString& aNativeString);
  };
  PendingComposition mPendingComposition;

  // While dispatching an event, this is incremented.
  uint16_t mDispatchingEvent;

  enum InputTransactionType : uint8_t {
    // No input transaction has been started.
    eNoInputTransaction,
    // Input transaction for native IME or keyboard event handler.  Note that
    // keyboard events may be dispatched via parent process if there is.
    // In remote processes, this is also used when events come from the parent
    // process and are not for tests because we cannot distinguish if
    // TextEventDispatcher has which type of transaction when it dispatches
    // (eNativeInputTransaction or eSameProcessSyncInputTransaction).
    eNativeInputTransaction,
    // Input transaction for automated tests which are APZ-aware.  Note that
    // keyboard events may be dispatched via parent process if there is.
    eAsyncTestInputTransaction,
    // Input transaction for automated tests which assume events are fired
    // synchronously.  I.e., keyboard events are always dispatched in the
    // current process.
    // In remote processes, this is also used when events come from the parent
    // process and are not dispatched by the instance itself for APZ-aware
    // tests because this instance won't dispatch the events via the parent
    // process again.
    eSameProcessSyncTestInputTransaction,
    // Input transaction for others (currently, only FuzzingFunctions).
    // Events are fired synchronously in the process.
    // XXX Should we make this async for testing default action handlers in
    //     the main process?
    eSameProcessSyncInputTransaction
  };

  InputTransactionType mInputTransactionType;

  bool IsForTests() const {
    return mInputTransactionType == eAsyncTestInputTransaction ||
           mInputTransactionType == eSameProcessSyncTestInputTransaction;
  }

  // ShouldSendInputEventToAPZ() returns true when WidgetInputEvent should
  // be dispatched via its parent process (if there is) for APZ.  Otherwise,
  // when the input transaction is for IME of B2G or automated tests which
  // isn't APZ-aware, WidgetInputEvent should be dispatched form current
  // process directly.
  bool ShouldSendInputEventToAPZ() const {
    switch (mInputTransactionType) {
      case eNativeInputTransaction:
      case eAsyncTestInputTransaction:
        return true;
      case eSameProcessSyncTestInputTransaction:
      case eSameProcessSyncInputTransaction:
        return false;
      case eNoInputTransaction:
        NS_WARNING(
            "Why does the caller need to dispatch an event when "
            "there is no input transaction?");
        return true;
      default:
        MOZ_CRASH("Define the behavior of new InputTransactionType");
    }
  }

  // See IsComposing().
  bool mIsComposing;

  // See IsHandlingComposition().
  bool mIsHandlingComposition;

  // true while NOTIFY_IME_OF_FOCUS is received but NOTIFY_IME_OF_BLUR has not
  // received yet.  Otherwise, false.
  bool mHasFocus;

  nsresult BeginInputTransactionInternal(TextEventDispatcherListener* aListener,
                                         InputTransactionType aType);

  /**
   * InitEvent() initializes aEvent.  This must be called before dispatching
   * the event.
   */
  void InitEvent(WidgetGUIEvent& aEvent) const;

  /**
   * DispatchEvent() dispatches aEvent on aWidget.
   */
  nsresult DispatchEvent(nsIWidget* aWidget, WidgetGUIEvent& aEvent,
                         nsEventStatus& aStatus);

  /**
   * DispatchInputEvent() dispatches aEvent on aWidget.
   */
  nsresult DispatchInputEvent(nsIWidget* aWidget, WidgetInputEvent& aEvent,
                              nsEventStatus& aStatus);

  /**
   * StartCompositionAutomaticallyIfNecessary() starts composition if it hasn't
   * been started it yet.
   *
   * @param aStatus         If it succeeded to start composition normally, this
   *                        returns nsEventStatus_eIgnore.  Otherwise, e.g.,
   *                        the composition is canceled during dispatching
   *                        compositionstart event, this returns
   *                        nsEventStatus_eConsumeNoDefault.  In this case,
   *                        the caller shouldn't keep doing its job.
   * @param aEventTime      If this is not nullptr, WidgetCompositionEvent will
   *                        be initialized with this.  Otherwise, initialized
   *                        with the time at initializing.
   * @return                Only when something unexpected occurs, this returns
   *                        an error.  Otherwise, returns NS_OK even if aStatus
   *                        is nsEventStatus_eConsumeNoDefault.
   */
  nsresult StartCompositionAutomaticallyIfNecessary(
      nsEventStatus& aStatus, const WidgetEventTime* aEventTime);

  /**
   * DispatchKeyboardEventInternal() maybe dispatches aKeyboardEvent.
   *
   * @param aMessage        Must be eKeyDown, eKeyUp or eKeyPress.
   * @param aKeyboardEvent  A keyboard event.  If aMessage is eKeyPress and
   *                        the event is for second or later character, its
   *                        mKeyValue should be empty string.
   * @param aStatus         If dispatching event should be marked as consumed,
   *                        set nsEventStatus_eConsumeNoDefault.  Otherwise,
   *                        set nsEventStatus_eIgnore.  After dispatching
   *                        a event and it's consumed this returns
   *                        nsEventStatus_eConsumeNoDefault.
   * @param aData           Calling this method may cause calling
   *                        WillDispatchKeyboardEvent() of the listener.
   *                        aData will be set to its argument.
   * @param aIndexOfKeypress    This must be 0 if aMessage isn't eKeyPress or
   *                            aKeyboard.mKeyNameIndex isn't
   *                            KEY_NAME_INDEX_USE_STRING.  Otherwise, i.e.,
   *                            when an eKeyPress event causes inputting
   *                            text, this must be between 0 and
   *                            mKeyValue.Length() - 1 since keypress events
   *                            sending only one character per event.
   * @param aNeedsCallback  Set true when caller needs to initialize each
   *                        eKeyPress event immediately before dispatch.
   *                        Then, WillDispatchKeyboardEvent() is always called.
   * @return                true if an event is dispatched.  Otherwise, false.
   */
  // TODO: Mark this as MOZ_CAN_RUN_SCRIPT instead.
  MOZ_CAN_RUN_SCRIPT_BOUNDARY bool DispatchKeyboardEventInternal(
      EventMessage aMessage, const WidgetKeyboardEvent& aKeyboardEvent,
      nsEventStatus& aStatus, void* aData, uint32_t aIndexOfKeypress = 0,
      bool aNeedsCallback = false);

  /**
   * ClearNotificationRequests() clears mIMENotificationRequests.
   */
  void ClearNotificationRequests();

  /**
   * UpdateNotificationRequests() updates mIMENotificationRequests with
   * current state.  If the instance doesn't have focus, this clears
   * mIMENotificationRequests.  Otherwise, updates it with both requests of
   * current listener and native listener.
   */
  void UpdateNotificationRequests();
};

}  // namespace widget
}  // namespace mozilla

#endif  // #ifndef mozilla_widget_textcompositionsynthesizer_h_
