/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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_TextComposition_h
#define mozilla_TextComposition_h

#include "nsCOMPtr.h"
#include "nsINode.h"
#include "nsIWeakReference.h"
#include "nsIWidget.h"
#include "nsTArray.h"
#include "nsThreadUtils.h"
#include "nsPresContext.h"
#include "mozilla/Attributes.h"
#include "mozilla/EventForwards.h"
#include "mozilla/TextRange.h"
#include "mozilla/dom/TabParent.h"

class nsIEditor;

namespace mozilla {

class EventDispatchingCallback;
class IMEStateManager;

/**
 * TextComposition represents a text composition.  This class stores the
 * composition event target and its presContext.  At dispatching the event via
 * this class, the instances use the stored event target.
 */

class TextComposition final
{
  friend class IMEStateManager;

  NS_INLINE_DECL_REFCOUNTING(TextComposition)

public:
  typedef dom::TabParent TabParent;

  static bool IsHandlingSelectionEvent() { return sHandlingSelectionEvent; }

  TextComposition(nsPresContext* aPresContext,
                  nsINode* aNode,
                  TabParent* aTabParent,
                  WidgetCompositionEvent* aCompositionEvent);

  bool Destroyed() const { return !mPresContext; }
  nsPresContext* GetPresContext() const { return mPresContext; }
  nsINode* GetEventTargetNode() const { return mNode; }
  // The latest CompositionEvent.data value except compositionstart event.
  // This value is modified at dispatching compositionupdate.
  const nsString& LastData() const { return mLastData; }
  // The composition string which is already handled by the focused editor.
  // I.e., this value must be same as the composition string on the focused
  // editor.  This value is modified at a call of
  // EditorDidHandleCompositionChangeEvent().
  // Note that mString and mLastData are different between dispatcing
  // compositionupdate and compositionchange event handled by focused editor.
  const nsString& String() const { return mString; }
  // The latest clauses range of the composition string.
  // During compositionupdate event, GetRanges() returns old ranges.
  // So if getting on compositionupdate, Use GetLastRange instead of GetRange().
  TextRangeArray* GetLastRanges() const { return mLastRanges; }
  // Returns the clauses and/or caret range of the composition string.
  // This is modified at a call of EditorWillHandleCompositionChangeEvent().
  // This may return null if there is no clauses and caret.
  // XXX We should return |const TextRangeArray*| here, but it causes compile
  //     error due to inaccessible Release() method.
  TextRangeArray* GetRanges() const { return mRanges; }
  // Returns the widget which is proper to call NotifyIME().
  nsIWidget* GetWidget() const
  {
    return mPresContext ? mPresContext->GetRootWidget() : nullptr;
  }
  // Returns true if the composition is started with synthesized event which
  // came from nsDOMWindowUtils.
  bool IsSynthesizedForTests() const { return mIsSynthesizedForTests; }

  const widget::NativeIMEContext& GetNativeIMEContext() const
  {
    return mNativeContext;
  }

  /**
   * This is called when IMEStateManager stops managing the instance.
   */
  void Destroy();

  /**
   * Request to commit (or cancel) the composition to IME.  This method should
   * be called only by IMEStateManager::NotifyIME().
   */
  nsresult RequestToCommit(nsIWidget* aWidget, bool aDiscard);

  /**
   * Send a notification to IME.  It depends on the IME or platform spec what
   * will occur (or not occur).
   */
  nsresult NotifyIME(widget::IMEMessage aMessage);

  /**
   * the offset of first composition string
   */
  uint32_t NativeOffsetOfStartComposition() const
  {
    return mCompositionStartOffset;
  }

  /**
   * the offset of first selected clause or start of composition
   */
  uint32_t NativeOffsetOfTargetClause() const
  {
    return mCompositionStartOffset + mTargetClauseOffsetInComposition;
  }

  /**
   * Returns true if there is non-empty composition string and it's not fixed.
   * Otherwise, false.
   */
  bool IsComposing() const { return mIsComposing; }

  /**
   * Returns true while editor is handling an event which is modifying the
   * composition string.
   */
  bool IsEditorHandlingEvent() const
  {
    return mIsEditorHandlingEvent;
  }

  /**
   * StartHandlingComposition() and EndHandlingComposition() are called by
   * editor when it holds a TextComposition instance and release it.
   */
  void StartHandlingComposition(nsIEditor* aEditor);
  void EndHandlingComposition(nsIEditor* aEditor);

  /**
   * OnEditorDestroyed() is called when the editor is destroyed but there is
   * active composition.
   */
  void OnEditorDestroyed();

  /**
   * CompositionChangeEventHandlingMarker class should be created at starting
   * to handle text event in focused editor.  This calls
   * EditorWillHandleCompositionChangeEvent() and
   * EditorDidHandleCompositionChangeEvent() automatically.
   */
  class MOZ_STACK_CLASS CompositionChangeEventHandlingMarker
  {
  public:
    CompositionChangeEventHandlingMarker(
      TextComposition* aComposition,
      const WidgetCompositionEvent* aCompositionChangeEvent)
      : mComposition(aComposition)
    {
      mComposition->EditorWillHandleCompositionChangeEvent(
                      aCompositionChangeEvent);
    }

    ~CompositionChangeEventHandlingMarker()
    {
      mComposition->EditorDidHandleCompositionChangeEvent();
    }

  private:
    RefPtr<TextComposition> mComposition;
    CompositionChangeEventHandlingMarker();
    CompositionChangeEventHandlingMarker(
      const CompositionChangeEventHandlingMarker& aOther);
  };

private:
  // Private destructor, to discourage deletion outside of Release():
  ~TextComposition()
  {
    // WARNING: mPresContext may be destroying, so, be careful if you touch it.
  }

  // sHandlingSelectionEvent is true while TextComposition sends a selection
  // event to ContentEventHandler.
  static bool sHandlingSelectionEvent;

  // This class holds nsPresContext weak.  This instance shouldn't block
  // destroying it.  When the presContext is being destroyed, it's notified to
  // IMEStateManager::OnDestroyPresContext(), and then, it destroy
  // this instance.
  nsPresContext* mPresContext;
  nsCOMPtr<nsINode> mNode;
  RefPtr<TabParent> mTabParent;

  // This is the clause and caret range information which is managed by
  // the focused editor.  This may be null if there is no clauses or caret.
  RefPtr<TextRangeArray> mRanges;
  // Same as mRange, but mRange will have old data during compositionupdate.
  // So this will be valied during compositionupdate.
  RefPtr<TextRangeArray> mLastRanges;

  // mNativeContext stores a opaque pointer.  This works as the "ID" for this
  // composition.  Don't access the instance, it may not be available.
  widget::NativeIMEContext mNativeContext;

  // mEditorWeak is a weak reference to the focused editor handling composition.
  nsWeakPtr mEditorWeak;

  // mLastData stores the data attribute of the latest composition event (except
  // the compositionstart event).
  nsString mLastData;

  // mString stores the composition text which has been handled by the focused
  // editor.
  nsString mString;

  // Offset of the composition string from start of the editor
  uint32_t mCompositionStartOffset;
  // Offset of the selected clause of the composition string from
  // mCompositionStartOffset
  uint32_t mTargetClauseOffsetInComposition;

  // See the comment for IsSynthesizedForTests().
  bool mIsSynthesizedForTests;

  // See the comment for IsComposing().
  bool mIsComposing;

  // mIsEditorHandlingEvent is true while editor is modifying the composition
  // string.
  bool mIsEditorHandlingEvent;

  // mIsRequestingCommit or mIsRequestingCancel is true *only* while we're
  // requesting commit or canceling the composition.  In other words, while
  // one of these values is true, we're handling the request.
  bool mIsRequestingCommit;
  bool mIsRequestingCancel;

  // mRequestedToCommitOrCancel is true *after* we requested IME to commit or
  // cancel the composition.  In other words, we already requested of IME that
  // it commits or cancels current composition.
  // NOTE: Before this is set true, both mIsRequestingCommit and
  //       mIsRequestingCancel are set false.
  bool mRequestedToCommitOrCancel;

  // mWasNativeCompositionEndEventDiscarded is true if this composition was
  // requested commit or cancel itself but native compositionend event is
  // discarded by PresShell due to not safe to dispatch events.
  bool mWasNativeCompositionEndEventDiscarded;

  // Allow control characters appear in composition string.
  // When this is false, control characters except
  // CHARACTER TABULATION (horizontal tab) are removed from
  // both composition string and data attribute of compositionupdate
  // and compositionend events.
  bool mAllowControlCharacters;

  // mWasCompositionStringEmpty is true if the composition string was empty
  // when DispatchCompositionEvent() is called.
  bool mWasCompositionStringEmpty;

  // Hide the default constructor and copy constructor.
  TextComposition()
    : mPresContext(nullptr)
    , mNativeContext(nullptr)
    , mCompositionStartOffset(0)
    , mTargetClauseOffsetInComposition(0)
    , mIsSynthesizedForTests(false)
    , mIsComposing(false)
    , mIsEditorHandlingEvent(false)
    , mIsRequestingCommit(false)
    , mIsRequestingCancel(false)
    , mRequestedToCommitOrCancel(false)
    , mWasNativeCompositionEndEventDiscarded(false)
    , mAllowControlCharacters(false)
    , mWasCompositionStringEmpty(true)
  {}
  TextComposition(const TextComposition& aOther);

  /**
   * GetEditor() returns nsIEditor pointer of mEditorWeak.
   */
  already_AddRefed<nsIEditor> GetEditor() const;

  /**
   * HasEditor() returns true if mEditorWeak holds nsIEditor instance which is
   * alive.  Otherwise, false.
   */
  bool HasEditor() const;

  /**
   * EditorWillHandleCompositionChangeEvent() must be called before the focused
   * editor handles the compositionchange event.
   */
  void EditorWillHandleCompositionChangeEvent(
         const WidgetCompositionEvent* aCompositionChangeEvent);

  /**
   * EditorDidHandleCompositionChangeEvent() must be called after the focused
   * editor handles a compositionchange event.
   */
  void EditorDidHandleCompositionChangeEvent();

  /**
   * IsValidStateForComposition() returns true if it's safe to dispatch an event
   * to the DOM tree.  Otherwise, false.
   * WARNING: This doesn't check script blocker state.  It should be checked
   *          before dispatching the first event.
   */
  bool IsValidStateForComposition(nsIWidget* aWidget) const;

  /**
   * DispatchCompositionEvent() dispatches the aCompositionEvent to the mContent
   * synchronously. The caller must ensure that it's safe to dispatch the event.
   */
  void DispatchCompositionEvent(WidgetCompositionEvent* aCompositionEvent,
                                nsEventStatus* aStatus,
                                EventDispatchingCallback* aCallBack,
                                bool aIsSynthesized);

  /**
   * Simply calling EventDispatcher::Dispatch() with plugin event.
   * If dispatching event has no orginal clone, aOriginalEvent can be null.
   */
  void DispatchEvent(WidgetCompositionEvent* aDispatchEvent,
                     nsEventStatus* aStatus,
                     EventDispatchingCallback* aCallback,
                     const WidgetCompositionEvent *aOriginalEvent = nullptr);

  /**
   * HandleSelectionEvent() sends the selection event to ContentEventHandler
   * or dispatches it to the focused child process.
   */
  void HandleSelectionEvent(WidgetSelectionEvent* aSelectionEvent)
  {
    HandleSelectionEvent(mPresContext, mTabParent, aSelectionEvent);
  }
  static void HandleSelectionEvent(nsPresContext* aPresContext,
                                   TabParent* aTabParent,
                                   WidgetSelectionEvent* aSelectionEvent);

  /**
   * MaybeDispatchCompositionUpdate() may dispatch a compositionupdate event
   * if aCompositionEvent changes composition string.
   * @return Returns false if dispatching the compositionupdate event caused
   *         destroying this composition.
   */
  bool MaybeDispatchCompositionUpdate(
         const WidgetCompositionEvent* aCompositionEvent);

  /**
   * CloneAndDispatchAs() dispatches a composition event which is
   * duplicateed from aCompositionEvent and set the aMessage.
   *
   * @return Returns BaseEventFlags which is the result of dispatched event.
   */
  BaseEventFlags CloneAndDispatchAs(
                   const WidgetCompositionEvent* aCompositionEvent,
                   EventMessage aMessage,
                   nsEventStatus* aStatus = nullptr,
                   EventDispatchingCallback* aCallBack = nullptr);

  /**
   * If IME has already dispatched compositionend event but it was discarded
   * by PresShell due to not safe to dispatch, this returns true.
   */
  bool WasNativeCompositionEndEventDiscarded() const
  {
    return mWasNativeCompositionEndEventDiscarded;
  }

  /**
   * OnCompositionEventDiscarded() is called when PresShell discards
   * compositionupdate, compositionend or compositionchange event due to not
   * safe to dispatch event.
   */
  void OnCompositionEventDiscarded(WidgetCompositionEvent* aCompositionEvent);

  /**
   * OnCompositionEventDispatched() is called after a composition event is
   * dispatched.
   */
  void OnCompositionEventDispatched(
         const WidgetCompositionEvent* aDispatchEvent);

  /**
   * MaybeNotifyIMEOfCompositionEventHandled() notifies IME of composition
   * event handled.  This should be called after dispatching a composition
   * event which came from widget.
   */
  void MaybeNotifyIMEOfCompositionEventHandled(
         const WidgetCompositionEvent* aCompositionEvent);

  /**
   * GetSelectionStartOffset() returns normal selection start offset in the
   * editor which has this composition.
   * If it failed or lost focus, this would return 0.
   */
  uint32_t GetSelectionStartOffset();

  /**
   * OnStartOffsetUpdatedInChild() is called when composition start offset
   * is updated in the child process.  I.e., this is called and never called
   * if the composition is in this process.
   * @param aStartOffset        New composition start offset with native
   *                            linebreaks.
   */
  void OnStartOffsetUpdatedInChild(uint32_t aStartOffset);

  /**
   * CompositionEventDispatcher dispatches the specified composition (or text)
   * event.
   */
  class CompositionEventDispatcher : public Runnable
  {
  public:
    CompositionEventDispatcher(TextComposition* aTextComposition,
                               nsINode* aEventTarget,
                               EventMessage aEventMessage,
                               const nsAString& aData,
                               bool aIsSynthesizedEvent = false);
    NS_IMETHOD Run() override;

  private:
    RefPtr<TextComposition> mTextComposition;
    nsCOMPtr<nsINode> mEventTarget;
    nsString mData;
    EventMessage mEventMessage;
    bool mIsSynthesizedEvent;

    CompositionEventDispatcher() : mIsSynthesizedEvent(false) {};
  };

  /**
   * DispatchCompositionEventRunnable() dispatches a composition event to the
   * content.  Be aware, if you use this method, nsPresShellEventCB isn't used.
   * That means that nsIFrame::HandleEvent() is never called.
   * WARNING: The instance which is managed by IMEStateManager may be
   *          destroyed by this method call.
   *
   * @param aEventMessage       Must be one of composition events.
   * @param aData               Used for mData value.
   * @param aIsSynthesizingCommit   true if this is called for synthesizing
   *                                commit or cancel composition.  Otherwise,
   *                                false.
   */
  void DispatchCompositionEventRunnable(EventMessage aEventMessage,
                                        const nsAString& aData,
                                        bool aIsSynthesizingCommit = false);
};

/**
 * TextCompositionArray manages the instances of TextComposition class.
 * Managing with array is enough because only one composition is typically
 * there.  Even if user switches native IME context, it's very rare that
 * second or more composition is started.
 * It's assumed that this is used by IMEStateManager for storing all active
 * compositions in the process.  If the instance is it, each TextComposition
 * in the array can be destroyed by calling some methods of itself.
 */

class TextCompositionArray final :
  public AutoTArray<RefPtr<TextComposition>, 2>
{
public:
  // Looking for per native IME context.
  index_type IndexOf(const widget::NativeIMEContext& aNativeIMEContext);
  index_type IndexOf(nsIWidget* aWidget);

  TextComposition* GetCompositionFor(nsIWidget* aWidget);
  TextComposition* GetCompositionFor(
                     const WidgetCompositionEvent* aCompositionEvent);

  // Looking for per nsPresContext
  index_type IndexOf(nsPresContext* aPresContext);
  index_type IndexOf(nsPresContext* aPresContext, nsINode* aNode);

  TextComposition* GetCompositionFor(nsPresContext* aPresContext);
  TextComposition* GetCompositionFor(nsPresContext* aPresContext,
                                     nsINode* aNode);
  TextComposition* GetCompositionInContent(nsPresContext* aPresContext,
                                           nsIContent* aContent);
};

} // namespace mozilla

#endif // #ifndef mozilla_TextComposition_h