/* -*- 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 "nsAutoPtr.h"
#include "nsString.h"
#include "mozilla/Attributes.h"
#include "mozilla/EventForwards.h"
#include "mozilla/TextEventDispatcherListener.h"
#include "mozilla/TextRange.h"

class nsIWidget;

namespace mozilla {
namespace widget {

struct IMENotification;

/**
 * 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()
  {
  }

  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 BeginInputTransactionForTests(
             TextEventDispatcherListener* aListener);

  /**
   * 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();

  /**
   * 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().
   */
  bool IsComposing() const { return mIsComposing; }

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

  /**
   * StartComposition() starts composition explicitly.
   */
  nsresult StartComposition(nsEventStatus& aStatus);

  /**
   * CommitComposition() commits composition.
   *
   * @param aCommitString   If this is null, commits with the last composition
   *                        string.  Otherwise, commits the composition with
   *                        this value.
   */
   nsresult CommitComposition(nsEventStatus& aStatus,
                              const nsAString* aCommitString = 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 aAttribute      One of NS_TEXTRANGE_RAWINPUT,
   *                        NS_TEXTRANGE_SELECTEDRAWTEXT,
   *                        NS_TEXTRANGE_CONVERTEDTEXT or
   *                        NS_TEXTRANGE_SELECTEDCONVERTEDTEXT.
   */
  nsresult AppendClauseToPendingComposition(uint32_t aLength,
                                            uint32_t aAttribute)
  {
    return mPendingComposition.AppendClause(aLength, aAttribute);
  }

  /**
   * 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);
  }

  /**
   * 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().
   */
  nsresult FlushPendingComposition(nsEventStatus& aStatus)
  {
    return mPendingComposition.Flush(this, aStatus);
  }

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

  /**
   * @see nsIWidget::NotifyIME()
   */
  nsresult NotifyIME(const IMENotification& aIMENotification);


  /**
   * DispatchTo indicates whether the event may be dispatched to its parent
   * process first (if there is) or not.  If the event is dispatched to the
   * parent process, APZ will handle it first.  Otherwise, the event won't be
   * handled by APZ if it's in a child process.
   */
  enum DispatchTo
  {
    // The event may be dispatched to its parent process if there is a parent.
    // In such case, the event will be handled asynchronously.  Additionally,
    // the event may be sent to its child process if a child process (including
    // the dispatching process) has focus.
    eDispatchToParentProcess = 0,
    // The event must be dispatched in the current process.  But of course,
    // the event may be sent to a child process when it has focus.  If there is
    // no child process, the event may be handled synchronously.
    eDispatchToCurrentProcess = 1
  };

  /**
   * 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 aDispatchTo     See comments of DispatchTo.
   * @return                true if an event is dispatched.  Otherwise, false.
   */
  bool DispatchKeyboardEvent(EventMessage aMessage,
                             const WidgetKeyboardEvent& aKeyboardEvent,
                             nsEventStatus& aStatus,
                             DispatchTo aDispatchTo = eDispatchToParentProcess);

  /**
   * 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 aDispatchTo     See comments of DispatchTo.
   * @return                true if one or more events are dispatched.
   *                        Otherwise, false.
   */
  bool MaybeDispatchKeypressEvents(const WidgetKeyboardEvent& aKeyboardEvent,
                                   nsEventStatus& aStatus,
                                   DispatchTo aDispatchTo =
                                     eDispatchToParentProcess);

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;

  // 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, uint32_t aAttribute);
    nsresult SetCaret(uint32_t aOffset, uint32_t aLength);
    nsresult Flush(TextEventDispatcher* aDispatcher, nsEventStatus& aStatus);
    void Clear();

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

    void EnsureClauseArray();
  };
  PendingComposition mPendingComposition;

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

  bool mForTests;
  // See IsComposing().
  bool mIsComposing;

  // If this is true, keydown and keyup events are dispatched even when there
  // is a composition.
  static bool sDispatchKeyEventsDuringComposition;

  nsresult BeginInputTransactionInternal(
             TextEventDispatcherListener* aListener,
             bool aForTests);

  /**
   * 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.
   *
   * @param aDispatchTo     See comments of DispatchTo.
   */
  nsresult DispatchInputEvent(nsIWidget* aWidget,
                              WidgetInputEvent& aEvent,
                              nsEventStatus& aStatus,
                              DispatchTo aDispatchTo);

  /**
   * 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.
   * @return                Only when something unexpected occurs, this returns
   *                        an error.  Otherwise, returns NS_OK even if aStatus
   *                        is nsEventStatus_eConsumeNoDefault.
   */
  nsresult StartCompositionAutomaticallyIfNecessary(nsEventStatus& aStatus);

  /**
   * 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 aDispatchTo     See comments of DispatchTo.
   * @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.
   * @return                true if an event is dispatched.  Otherwise, false.
   */
  bool DispatchKeyboardEventInternal(EventMessage aMessage,
                                     const WidgetKeyboardEvent& aKeyboardEvent,
                                     nsEventStatus& aStatus,
                                     DispatchTo aDispatchTo =
                                       eDispatchToParentProcess,
                                     uint32_t aIndexOfKeypress = 0);
};

} // namespace widget
} // namespace mozilla

#endif // #ifndef mozilla_widget_textcompositionsynthesizer_h_
