/* -*- 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/. */

#include "ContentEventHandler.h"
#include "IMEContentObserver.h"
#include "IMEStateManager.h"
#include "nsContentUtils.h"
#include "nsIContent.h"
#include "nsIMutationObserver.h"
#include "nsPresContext.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/EditorBase.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/IMEStateManager.h"
#include "mozilla/IntegerRange.h"
#include "mozilla/MiscEvents.h"
#include "mozilla/PresShell.h"
#include "mozilla/RangeBoundary.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPrefs_intl.h"
#include "mozilla/TextComposition.h"
#include "mozilla/TextEvents.h"
#include "mozilla/Unused.h"
#include "mozilla/dom/BrowserParent.h"

#ifdef XP_MACOSX
// Some defiens will be conflict with OSX SDK
#  define TextRange _TextRange
#  define TextRangeArray _TextRangeArray
#  define Comment _Comment
#endif

#ifdef XP_MACOSX
#  undef TextRange
#  undef TextRangeArray
#  undef Comment
#endif

using namespace mozilla::widget;

namespace mozilla {

#define IDEOGRAPHIC_SPACE (u"\x3000"_ns)

/******************************************************************************
 * TextComposition
 ******************************************************************************/

bool TextComposition::sHandlingSelectionEvent = false;

TextComposition::TextComposition(nsPresContext* aPresContext, nsINode* aNode,
                                 BrowserParent* aBrowserParent,
                                 WidgetCompositionEvent* aCompositionEvent)
    : mPresContext(aPresContext),
      mNode(aNode),
      mBrowserParent(aBrowserParent),
      mNativeContext(aCompositionEvent->mNativeIMEContext),
      mCompositionStartOffset(0),
      mTargetClauseOffsetInComposition(0),
      mCompositionStartOffsetInTextNode(UINT32_MAX),
      mCompositionLengthInTextNode(UINT32_MAX),
      mIsSynthesizedForTests(aCompositionEvent->mFlags.mIsSynthesizedForTests),
      mIsComposing(false),
      mIsEditorHandlingEvent(false),
      mIsRequestingCommit(false),
      mIsRequestingCancel(false),
      mRequestedToCommitOrCancel(false),
      mHasDispatchedDOMTextEvent(false),
      mHasReceivedCommitEvent(false),
      mWasNativeCompositionEndEventDiscarded(false),
      mAllowControlCharacters(
          StaticPrefs::dom_compositionevent_allow_control_characters()),
      mWasCompositionStringEmpty(true) {
  MOZ_ASSERT(aCompositionEvent->mNativeIMEContext.IsValid());
}

void TextComposition::Destroy() {
  mPresContext = nullptr;
  mNode = nullptr;
  mBrowserParent = nullptr;
  mContainerTextNode = nullptr;
  mCompositionStartOffsetInTextNode = UINT32_MAX;
  mCompositionLengthInTextNode = UINT32_MAX;
  // TODO: If the editor is still alive and this is held by it, we should tell
  //       this being destroyed for cleaning up the stuff.
}

void TextComposition::OnCharacterDataChanged(
    Text& aText, const CharacterDataChangeInfo& aInfo) {
  if (mContainerTextNode != &aText ||
      mCompositionStartOffsetInTextNode == UINT32_MAX ||
      mCompositionLengthInTextNode == UINT32_MAX) {
    return;
  }

  // Ignore changes after composition string.
  if (aInfo.mChangeStart >=
      mCompositionStartOffsetInTextNode + mCompositionLengthInTextNode) {
    return;
  }

  // If the change ends before the composition string, we need only to adjust
  // the start offset.
  if (aInfo.mChangeEnd <= mCompositionStartOffsetInTextNode) {
    MOZ_ASSERT(aInfo.LengthOfRemovedText() <=
               mCompositionStartOffsetInTextNode);
    mCompositionStartOffsetInTextNode -= aInfo.LengthOfRemovedText();
    mCompositionStartOffsetInTextNode += aInfo.mReplaceLength;
    return;
  }

  // If this is caused by a splitting text node, the composition string
  // may be split out to the new right node.  In the case,
  // CompositionTransaction::DoTransaction handles it with warking the
  // following text nodes.  Therefore, we should NOT shrink the composing
  // range for avoind breaking the fix of bug 1310912.  Although the handling
  // looks buggy so that we need to move the handling into here later.
  if (aInfo.mDetails &&
      aInfo.mDetails->mType == CharacterDataChangeInfo::Details::eSplit) {
    return;
  }

  // If the change removes/replaces the last character of the composition
  // string, we should shrink the composition range before the change start.
  // Then, the replace string will be never updated by coming composition
  // updates.
  if (aInfo.mChangeEnd >=
      mCompositionStartOffsetInTextNode + mCompositionLengthInTextNode) {
    // If deleting the first character of the composition string, collapse IME
    // selection temporarily.  Updating composition string will insert new
    // composition string there.
    if (aInfo.mChangeStart <= mCompositionStartOffsetInTextNode) {
      mCompositionStartOffsetInTextNode = aInfo.mChangeStart;
      mCompositionLengthInTextNode = 0u;
      return;
    }
    // If some characters in the composition still stay, composition range
    // should be shrunken.
    MOZ_ASSERT(aInfo.mChangeStart > mCompositionStartOffsetInTextNode);
    mCompositionLengthInTextNode =
        aInfo.mChangeStart - mCompositionStartOffsetInTextNode;
    return;
  }

  // If removed range starts in the composition string, we need only adjust
  // the length to make composition range contain the replace string.
  if (aInfo.mChangeStart >= mCompositionStartOffsetInTextNode) {
    MOZ_ASSERT(aInfo.LengthOfRemovedText() <= mCompositionLengthInTextNode);
    mCompositionLengthInTextNode -= aInfo.LengthOfRemovedText();
    mCompositionLengthInTextNode += aInfo.mReplaceLength;
    return;
  }

  // If preceding characers of the composition string is also removed,  new
  // composition start will be there and new composition ends at current
  // position.
  const uint32_t removedLengthInCompositionString =
      aInfo.mChangeEnd - mCompositionStartOffsetInTextNode;
  mCompositionStartOffsetInTextNode = aInfo.mChangeStart;
  mCompositionLengthInTextNode -= removedLengthInCompositionString;
  mCompositionLengthInTextNode += aInfo.mReplaceLength;
}

bool TextComposition::IsValidStateForComposition(nsIWidget* aWidget) const {
  return !Destroyed() && aWidget && !aWidget->Destroyed() &&
         mPresContext->GetPresShell() &&
         !mPresContext->PresShell()->IsDestroying();
}

bool TextComposition::MaybeDispatchCompositionUpdate(
    const WidgetCompositionEvent* aCompositionEvent) {
  MOZ_RELEASE_ASSERT(!mBrowserParent);

  if (!IsValidStateForComposition(aCompositionEvent->mWidget)) {
    return false;
  }

  // Note that we don't need to dispatch eCompositionUpdate event even if
  // mHasDispatchedDOMTextEvent is false and eCompositionCommit event is
  // dispatched with empty string immediately after eCompositionStart
  // because composition string has never been changed from empty string to
  // non-empty string in such composition even if selected string was not
  // empty string (mLastData isn't set to selected text when this receives
  // eCompositionStart).
  if (mLastData == aCompositionEvent->mData) {
    return true;
  }
  CloneAndDispatchAs(aCompositionEvent, eCompositionUpdate);
  return IsValidStateForComposition(aCompositionEvent->mWidget);
}

BaseEventFlags TextComposition::CloneAndDispatchAs(
    const WidgetCompositionEvent* aCompositionEvent, EventMessage aMessage,
    nsEventStatus* aStatus, EventDispatchingCallback* aCallBack) {
  MOZ_RELEASE_ASSERT(!mBrowserParent);

  MOZ_ASSERT(IsValidStateForComposition(aCompositionEvent->mWidget),
             "Should be called only when it's safe to dispatch an event");

  WidgetCompositionEvent compositionEvent(aCompositionEvent->IsTrusted(),
                                          aMessage, aCompositionEvent->mWidget);
  compositionEvent.mTimeStamp = aCompositionEvent->mTimeStamp;
  compositionEvent.mData = aCompositionEvent->mData;
  compositionEvent.mNativeIMEContext = aCompositionEvent->mNativeIMEContext;
  compositionEvent.mOriginalMessage = aCompositionEvent->mMessage;
  compositionEvent.mFlags.mIsSynthesizedForTests =
      aCompositionEvent->mFlags.mIsSynthesizedForTests;

  nsEventStatus dummyStatus = nsEventStatus_eConsumeNoDefault;
  nsEventStatus* status = aStatus ? aStatus : &dummyStatus;
  if (aMessage == eCompositionUpdate) {
    mLastData = compositionEvent.mData;
    mLastRanges = aCompositionEvent->mRanges;
  }

  DispatchEvent(&compositionEvent, status, aCallBack, aCompositionEvent);
  return compositionEvent.mFlags;
}

void TextComposition::DispatchEvent(
    WidgetCompositionEvent* aDispatchEvent, nsEventStatus* aStatus,
    EventDispatchingCallback* aCallBack,
    const WidgetCompositionEvent* aOriginalEvent) {
  if (aDispatchEvent->mMessage == eCompositionChange) {
    aDispatchEvent->mFlags.mOnlySystemGroupDispatchInContent = true;
  }
  RefPtr<nsINode> node = mNode;
  RefPtr<nsPresContext> presContext = mPresContext;
  EventDispatcher::Dispatch(node, presContext, aDispatchEvent, nullptr, aStatus,
                            aCallBack);

  OnCompositionEventDispatched(aDispatchEvent);
}

void TextComposition::OnCompositionEventDiscarded(
    WidgetCompositionEvent* aCompositionEvent) {
  // Note that this method is never called for synthesized events for emulating
  // commit or cancel composition.

  MOZ_ASSERT(aCompositionEvent->IsTrusted(),
             "Shouldn't be called with untrusted event");

  if (mBrowserParent) {
    // The composition event should be discarded in the child process too.
    Unused << mBrowserParent->SendCompositionEvent(*aCompositionEvent);
  }

  // XXX If composition events are discarded, should we dispatch them with
  //     runnable event?  However, even if we do so, it might make native IME
  //     confused due to async modification.  Especially when native IME is
  //     TSF.
  if (!aCompositionEvent->CausesDOMCompositionEndEvent()) {
    return;
  }

  mWasNativeCompositionEndEventDiscarded = true;
}

static inline bool IsControlChar(uint32_t aCharCode) {
  return aCharCode < ' ' || aCharCode == 0x7F;
}

static size_t FindFirstControlCharacter(const nsAString& aStr) {
  const char16_t* sourceBegin = aStr.BeginReading();
  const char16_t* sourceEnd = aStr.EndReading();

  for (const char16_t* source = sourceBegin; source < sourceEnd; ++source) {
    if (*source != '\t' && IsControlChar(*source)) {
      return source - sourceBegin;
    }
  }

  return -1;
}

static void RemoveControlCharactersFrom(nsAString& aStr,
                                        TextRangeArray* aRanges) {
  size_t firstControlCharOffset = FindFirstControlCharacter(aStr);
  if (firstControlCharOffset == (size_t)-1) {
    return;
  }

  nsAutoString copy(aStr);
  const char16_t* sourceBegin = copy.BeginReading();
  const char16_t* sourceEnd = copy.EndReading();

  char16_t* dest = aStr.BeginWriting();
  if (NS_WARN_IF(!dest)) {
    return;
  }

  char16_t* curDest = dest + firstControlCharOffset;
  size_t i = firstControlCharOffset;
  for (const char16_t* source = sourceBegin + firstControlCharOffset;
       source < sourceEnd; ++source) {
    if (*source == '\t' || *source == '\n' || !IsControlChar(*source)) {
      *curDest = *source;
      ++curDest;
      ++i;
    } else if (aRanges) {
      aRanges->RemoveCharacter(i);
    }
  }

  aStr.SetLength(curDest - dest);
}

nsString TextComposition::CommitStringIfCommittedAsIs() const {
  nsString result(mLastData);
  if (!mAllowControlCharacters) {
    RemoveControlCharactersFrom(result, nullptr);
  }
  if (StaticPrefs::intl_ime_remove_placeholder_character_at_commit() &&
      mLastData == IDEOGRAPHIC_SPACE) {
    return EmptyString();
  }
  return result;
}

void TextComposition::DispatchCompositionEvent(
    WidgetCompositionEvent* aCompositionEvent, nsEventStatus* aStatus,
    EventDispatchingCallback* aCallBack, bool aIsSynthesized) {
  mWasCompositionStringEmpty = mString.IsEmpty();

  if (aCompositionEvent->IsFollowedByCompositionEnd()) {
    mHasReceivedCommitEvent = true;
  }

  // If this instance has requested to commit or cancel composition but
  // is not synthesizing commit event, that means that the IME commits or
  // cancels the composition asynchronously.  Typically, iBus behaves so.
  // Then, synthesized events which were dispatched immediately after
  // the request has already committed our editor's composition string and
  // told it to web apps.  Therefore, we should ignore the delayed events.
  if (mRequestedToCommitOrCancel && !aIsSynthesized) {
    *aStatus = nsEventStatus_eConsumeNoDefault;
    return;
  }

  // If the content is a container of BrowserParent, composition should be in
  // the remote process.
  if (mBrowserParent) {
    Unused << mBrowserParent->SendCompositionEvent(*aCompositionEvent);
    aCompositionEvent->StopPropagation();
    if (aCompositionEvent->CausesDOMTextEvent()) {
      mLastData = aCompositionEvent->mData;
      mLastRanges = aCompositionEvent->mRanges;
      // Although, the composition event hasn't been actually handled yet,
      // emulate an editor to be handling the composition event.
      EditorWillHandleCompositionChangeEvent(aCompositionEvent);
      EditorDidHandleCompositionChangeEvent();
    }
    return;
  }

  if (!mAllowControlCharacters) {
    RemoveControlCharactersFrom(aCompositionEvent->mData,
                                aCompositionEvent->mRanges);
  }
  if (aCompositionEvent->mMessage == eCompositionCommitAsIs) {
    NS_ASSERTION(!aCompositionEvent->mRanges,
                 "mRanges of eCompositionCommitAsIs should be null");
    aCompositionEvent->mRanges = nullptr;
    NS_ASSERTION(aCompositionEvent->mData.IsEmpty(),
                 "mData of eCompositionCommitAsIs should be empty string");
    if (StaticPrefs::intl_ime_remove_placeholder_character_at_commit() &&
        mLastData == IDEOGRAPHIC_SPACE) {
      // If the last data is an ideographic space (FullWidth space), it might be
      // a placeholder character of some Chinese IME.  So, committing with
      // this data might not be expected by users.  Let's use empty string.
      aCompositionEvent->mData.Truncate();
    } else {
      aCompositionEvent->mData = mLastData;
    }
  } else if (aCompositionEvent->mMessage == eCompositionCommit) {
    NS_ASSERTION(!aCompositionEvent->mRanges,
                 "mRanges of eCompositionCommit should be null");
    aCompositionEvent->mRanges = nullptr;
  }

  if (!IsValidStateForComposition(aCompositionEvent->mWidget)) {
    *aStatus = nsEventStatus_eConsumeNoDefault;
    return;
  }

  // IME may commit composition with empty string for a commit request or
  // with non-empty string for a cancel request.  We should prevent such
  // unexpected result.  E.g., web apps may be confused if they implement
  // autocomplete which attempts to commit composition forcibly when the user
  // selects one of suggestions but composition string is cleared by IME.
  // Note that most Chinese IMEs don't expose actual composition string to us.
  // They typically tell us an IDEOGRAPHIC SPACE or empty string as composition
  // string.  Therefore, we should hack it only when:
  // 1. committing string is empty string at requesting commit but the last
  //    data isn't IDEOGRAPHIC SPACE.
  // 2. non-empty string is committed at requesting cancel.
  if (!aIsSynthesized && (mIsRequestingCommit || mIsRequestingCancel)) {
    nsString* committingData = nullptr;
    switch (aCompositionEvent->mMessage) {
      case eCompositionEnd:
      case eCompositionChange:
      case eCompositionCommitAsIs:
      case eCompositionCommit:
        committingData = &aCompositionEvent->mData;
        break;
      default:
        NS_WARNING(
            "Unexpected event comes during committing or "
            "canceling composition");
        break;
    }
    if (committingData) {
      if (mIsRequestingCommit && committingData->IsEmpty() &&
          mLastData != IDEOGRAPHIC_SPACE) {
        committingData->Assign(mLastData);
      } else if (mIsRequestingCancel && !committingData->IsEmpty()) {
        committingData->Truncate();
      }
    }
  }

  bool dispatchEvent = true;
  bool dispatchDOMTextEvent = aCompositionEvent->CausesDOMTextEvent();

  // When mIsComposing is false but the committing string is different from
  // the last data (E.g., previous eCompositionChange event made the
  // composition string empty or didn't have clause information), we don't
  // need to dispatch redundant DOM text event.  (But note that we need to
  // dispatch eCompositionChange event if we have not dispatched
  // eCompositionChange event yet and commit string replaces selected string
  // with empty string since selected string hasn't been replaced with empty
  // string yet.)
  if (dispatchDOMTextEvent &&
      aCompositionEvent->mMessage != eCompositionChange && !mIsComposing &&
      mHasDispatchedDOMTextEvent && mLastData == aCompositionEvent->mData) {
    dispatchEvent = dispatchDOMTextEvent = false;
  }

  // widget may dispatch redundant eCompositionChange event
  // which modifies neither composition string, clauses nor caret
  // position.  In such case, we shouldn't dispatch DOM events.
  if (dispatchDOMTextEvent &&
      aCompositionEvent->mMessage == eCompositionChange &&
      mLastData == aCompositionEvent->mData && mRanges &&
      aCompositionEvent->mRanges &&
      mRanges->Equals(*aCompositionEvent->mRanges)) {
    dispatchEvent = dispatchDOMTextEvent = false;
  }

  if (dispatchDOMTextEvent) {
    if (!MaybeDispatchCompositionUpdate(aCompositionEvent)) {
      return;
    }
  }

  if (dispatchEvent) {
    // If the composition event should cause a DOM text event, we should
    // overwrite the event message as eCompositionChange because due to
    // the limitation of mapping between event messages and DOM event types,
    // we cannot map multiple event messages to a DOM event type.
    if (dispatchDOMTextEvent &&
        aCompositionEvent->mMessage != eCompositionChange) {
      mHasDispatchedDOMTextEvent = true;
      aCompositionEvent->mFlags = CloneAndDispatchAs(
          aCompositionEvent, eCompositionChange, aStatus, aCallBack);
    } else {
      if (aCompositionEvent->mMessage == eCompositionChange) {
        mHasDispatchedDOMTextEvent = true;
      }
      DispatchEvent(aCompositionEvent, aStatus, aCallBack);
    }
  } else {
    *aStatus = nsEventStatus_eConsumeNoDefault;
  }

  if (!IsValidStateForComposition(aCompositionEvent->mWidget)) {
    return;
  }

  // Emulate editor behavior of compositionchange event (DOM text event) handler
  // if no editor handles composition events.
  if (dispatchDOMTextEvent && !HasEditor()) {
    EditorWillHandleCompositionChangeEvent(aCompositionEvent);
    EditorDidHandleCompositionChangeEvent();
  }

  if (aCompositionEvent->CausesDOMCompositionEndEvent()) {
    // Dispatch a compositionend event if it's necessary.
    if (aCompositionEvent->mMessage != eCompositionEnd) {
      CloneAndDispatchAs(aCompositionEvent, eCompositionEnd);
    }
    MOZ_ASSERT(!mIsComposing, "Why is the editor still composing?");
    MOZ_ASSERT(!HasEditor(), "Why does the editor still keep to hold this?");
  }

  MaybeNotifyIMEOfCompositionEventHandled(aCompositionEvent);
}

// static
void TextComposition::HandleSelectionEvent(
    nsPresContext* aPresContext, BrowserParent* aBrowserParent,
    WidgetSelectionEvent* aSelectionEvent) {
  // If the content is a container of BrowserParent, composition should be in
  // the remote process.
  if (aBrowserParent) {
    Unused << aBrowserParent->SendSelectionEvent(*aSelectionEvent);
    aSelectionEvent->StopPropagation();
    return;
  }

  AutoRestore<bool> saveHandlingSelectionEvent(sHandlingSelectionEvent);
  sHandlingSelectionEvent = true;

  if (RefPtr<IMEContentObserver> contentObserver =
          IMEStateManager::GetActiveContentObserver()) {
    contentObserver->MaybeHandleSelectionEvent(aPresContext, aSelectionEvent);
    return;
  }

  ContentEventHandler handler(aPresContext);
  // XXX During setting selection, a selection listener may change selection
  //     again.  In such case, sHandlingSelectionEvent doesn't indicate if
  //     the selection change is caused by a selection event.  However, it
  //     must be non-realistic scenario.
  handler.OnSelectionEvent(aSelectionEvent);
}

uint32_t TextComposition::GetSelectionStartOffset() {
  nsCOMPtr<nsIWidget> widget = mPresContext->GetRootWidget();
  WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
                                                 widget);
  // Due to a bug of widget, mRanges may not be nullptr even though composition
  // string is empty.  So, we need to check it here for avoiding to return
  // odd start offset.
  if (!mLastData.IsEmpty() && mRanges && mRanges->HasClauses()) {
    querySelectedTextEvent.InitForQuerySelectedText(
        ToSelectionType(mRanges->GetFirstClause()->mRangeType));
  } else {
    NS_WARNING_ASSERTION(
        !mLastData.IsEmpty() || !mRanges || !mRanges->HasClauses(),
        "Shouldn't have empty clause info when composition string is empty");
    querySelectedTextEvent.InitForQuerySelectedText(SelectionType::eNormal);
  }

  // The editor which has this composition is observed by active
  // IMEContentObserver, we can use the cache of it.
  RefPtr<IMEContentObserver> contentObserver =
      IMEStateManager::GetActiveContentObserver();
  bool doQuerySelection = true;
  if (contentObserver) {
    if (contentObserver->IsManaging(*this)) {
      doQuerySelection = false;
      contentObserver->HandleQueryContentEvent(&querySelectedTextEvent);
    }
    // If another editor already has focus, we cannot retrieve selection
    // in the editor which has this composition...
    else if (NS_WARN_IF(contentObserver->GetPresContext() == mPresContext)) {
      return 0;  // XXX Is this okay?
    }
  }

  // Otherwise, using slow path (i.e., compute every time with
  // ContentEventHandler)
  if (doQuerySelection) {
    ContentEventHandler handler(mPresContext);
    handler.HandleQueryContentEvent(&querySelectedTextEvent);
  }

  if (NS_WARN_IF(querySelectedTextEvent.DidNotFindSelection())) {
    return 0;  // XXX Is this okay?
  }
  return querySelectedTextEvent.mReply->AnchorOffset();
}

void TextComposition::OnCompositionEventDispatched(
    const WidgetCompositionEvent* aCompositionEvent) {
  MOZ_RELEASE_ASSERT(!mBrowserParent);

  if (!IsValidStateForComposition(aCompositionEvent->mWidget)) {
    return;
  }

  // Every composition event may cause changing composition start offset,
  // especially when there is no composition string.  Therefore, we need to
  // update mCompositionStartOffset with the latest offset.

  MOZ_ASSERT(aCompositionEvent->mMessage != eCompositionStart ||
                 mWasCompositionStringEmpty,
             "mWasCompositionStringEmpty should be true if the dispatched "
             "event is eCompositionStart");

  if (mWasCompositionStringEmpty &&
      !aCompositionEvent->CausesDOMCompositionEndEvent()) {
    // If there was no composition string, current selection start may be the
    // offset for inserting composition string.
    // Update composition start offset with current selection start.
    mCompositionStartOffset = GetSelectionStartOffset();
    mTargetClauseOffsetInComposition = 0;
  }

  if (aCompositionEvent->CausesDOMTextEvent()) {
    mTargetClauseOffsetInComposition = aCompositionEvent->TargetClauseOffset();
  }
}

void TextComposition::OnStartOffsetUpdatedInChild(uint32_t aStartOffset) {
  mCompositionStartOffset = aStartOffset;
}

void TextComposition::MaybeNotifyIMEOfCompositionEventHandled(
    const WidgetCompositionEvent* aCompositionEvent) {
  if (aCompositionEvent->mMessage != eCompositionStart &&
      !aCompositionEvent->CausesDOMTextEvent()) {
    return;
  }

  RefPtr<IMEContentObserver> contentObserver =
      IMEStateManager::GetActiveContentObserver();
  // When IMEContentObserver is managing the editor which has this composition,
  // composition event handled notification should be sent after the observer
  // notifies all pending notifications.  Therefore, we should use it.
  // XXX If IMEContentObserver suddenly loses focus after here and notifying
  //     widget of pending notifications, we won't notify widget of composition
  //     event handled.  Although, this is a bug but it should be okay since
  //     destroying IMEContentObserver notifies IME of blur.  So, native IME
  //     handler can treat it as this notification too.
  if (contentObserver && contentObserver->IsManaging(*this)) {
    contentObserver->MaybeNotifyCompositionEventHandled();
    return;
  }
  // Otherwise, e.g., this composition is in non-active window, we should
  // notify widget directly.
  NotifyIME(NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED);
}

void TextComposition::DispatchCompositionEventRunnable(
    EventMessage aEventMessage, const nsAString& aData,
    bool aIsSynthesizingCommit) {
  nsContentUtils::AddScriptRunner(new CompositionEventDispatcher(
      this, mNode, aEventMessage, aData, aIsSynthesizingCommit));
}

nsresult TextComposition::RequestToCommit(nsIWidget* aWidget, bool aDiscard) {
  // If this composition is already requested to be committed or canceled,
  // or has already finished in IME, we don't need to request it again because
  // request from this instance shouldn't cause committing nor canceling current
  // composition in IME, and even if the first request failed, new request
  // won't success, probably.  And we shouldn't synthesize events for
  // committing or canceling composition twice or more times.
  if (!CanRequsetIMEToCommitOrCancelComposition()) {
    return NS_OK;
  }

  RefPtr<TextComposition> kungFuDeathGrip(this);
  const nsAutoString lastData(mLastData);

  if (IMEStateManager::CanSendNotificationToWidget()) {
    AutoRestore<bool> saveRequestingCancel(mIsRequestingCancel);
    AutoRestore<bool> saveRequestingCommit(mIsRequestingCommit);
    if (aDiscard) {
      mIsRequestingCancel = true;
      mIsRequestingCommit = false;
    } else {
      mIsRequestingCancel = false;
      mIsRequestingCommit = true;
    }
    // FYI: CompositionEvents caused by a call of NotifyIME() may be
    //      discarded by PresShell if it's not safe to dispatch the event.
    nsresult rv = aWidget->NotifyIME(
        IMENotification(aDiscard ? REQUEST_TO_CANCEL_COMPOSITION
                                 : REQUEST_TO_COMMIT_COMPOSITION));
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
  }

  mRequestedToCommitOrCancel = true;

  // If the request is performed synchronously, this must be already destroyed.
  if (Destroyed()) {
    return NS_OK;
  }

  // Otherwise, synthesize the commit in content.
  nsAutoString data(aDiscard ? EmptyString() : lastData);
  if (data == mLastData) {
    DispatchCompositionEventRunnable(eCompositionCommitAsIs, u""_ns, true);
  } else {
    DispatchCompositionEventRunnable(eCompositionCommit, data, true);
  }
  return NS_OK;
}

nsresult TextComposition::NotifyIME(IMEMessage aMessage) {
  NS_ENSURE_TRUE(mPresContext, NS_ERROR_NOT_AVAILABLE);
  return IMEStateManager::NotifyIME(aMessage, mPresContext, mBrowserParent);
}

void TextComposition::EditorWillHandleCompositionChangeEvent(
    const WidgetCompositionEvent* aCompositionChangeEvent) {
  mIsComposing = aCompositionChangeEvent->IsComposing();
  mRanges = aCompositionChangeEvent->mRanges;
  mIsEditorHandlingEvent = true;

  MOZ_ASSERT(
      mLastData == aCompositionChangeEvent->mData,
      "The text of a compositionchange event must be same as previous data "
      "attribute value of the latest compositionupdate event");
}

void TextComposition::OnEditorDestroyed() {
  MOZ_RELEASE_ASSERT(!mBrowserParent);

  MOZ_ASSERT(!mIsEditorHandlingEvent,
             "The editor should have stopped listening events");
  nsCOMPtr<nsIWidget> widget = GetWidget();
  if (NS_WARN_IF(!widget)) {
    // XXX If this could happen, how do we notify IME of destroying the editor?
    return;
  }

  // Try to cancel the composition.
  RequestToCommit(widget, true);
}

void TextComposition::EditorDidHandleCompositionChangeEvent() {
  mString = mLastData;
  mIsEditorHandlingEvent = false;
}

void TextComposition::StartHandlingComposition(EditorBase* aEditorBase) {
  MOZ_RELEASE_ASSERT(!mBrowserParent);

  MOZ_ASSERT(!HasEditor(), "There is a handling editor already");
  mEditorBaseWeak = do_GetWeakReference(static_cast<nsIEditor*>(aEditorBase));
}

void TextComposition::EndHandlingComposition(EditorBase* aEditorBase) {
  MOZ_RELEASE_ASSERT(!mBrowserParent);

#ifdef DEBUG
  RefPtr<EditorBase> editorBase = GetEditorBase();
  MOZ_ASSERT(!editorBase || editorBase == aEditorBase,
             "Another editor handled the composition?");
#endif  // #ifdef DEBUG
  mEditorBaseWeak = nullptr;
}

already_AddRefed<EditorBase> TextComposition::GetEditorBase() const {
  nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditorBaseWeak);
  RefPtr<EditorBase> editorBase = static_cast<EditorBase*>(editor.get());
  return editorBase.forget();
}

bool TextComposition::HasEditor() const {
  return mEditorBaseWeak && mEditorBaseWeak->IsAlive();
}

RawRangeBoundary TextComposition::FirstIMESelectionStartRef() const {
  RefPtr<EditorBase> editorBase = GetEditorBase();
  if (!editorBase) {
    return RawRangeBoundary();
  }

  nsISelectionController* selectionController =
      editorBase->GetSelectionController();
  if (NS_WARN_IF(!selectionController)) {
    return RawRangeBoundary();
  }

  const nsRange* firstRange = nullptr;
  static const SelectionType kIMESelectionTypes[] = {
      SelectionType::eIMERawClause, SelectionType::eIMESelectedRawClause,
      SelectionType::eIMEConvertedClause, SelectionType::eIMESelectedClause};
  for (auto selectionType : kIMESelectionTypes) {
    dom::Selection* selection =
        selectionController->GetSelection(ToRawSelectionType(selectionType));
    if (!selection) {
      continue;
    }
    const uint32_t rangeCount = selection->RangeCount();
    for (const uint32_t i : IntegerRange(rangeCount)) {
      MOZ_ASSERT(selection->RangeCount() == rangeCount);
      const nsRange* range = selection->GetRangeAt(i);
      MOZ_ASSERT(range);
      if (MOZ_UNLIKELY(NS_WARN_IF(!range)) ||
          MOZ_UNLIKELY(NS_WARN_IF(!range->GetStartContainer()))) {
        continue;
      }
      if (!firstRange) {
        firstRange = range;
        continue;
      }
      // In most cases, all composition string should be in same text node.
      if (firstRange->GetStartContainer() == range->GetStartContainer()) {
        if (firstRange->StartOffset() > range->StartOffset()) {
          firstRange = range;
        }
        continue;
      }
      // However, if web apps have inserted different nodes in composition
      // string, composition string may span 2 or more nodes.
      if (firstRange->GetStartContainer()->GetNextSibling() ==
          range->GetStartContainer()) {
        // Fast path for some known applications like Google Keep.
        firstRange = range;
        continue;
      }
      // Unfortunately, really slow path.
      // The ranges should always have a common ancestor, hence, be comparable.
      if (*nsContentUtils::ComparePoints(range->StartRef(),
                                         firstRange->StartRef()) == -1) {
        firstRange = range;
      }
    }
  }
  return firstRange ? firstRange->StartRef().AsRaw() : RawRangeBoundary();
}

RawRangeBoundary TextComposition::LastIMESelectionEndRef() const {
  RefPtr<EditorBase> editorBase = GetEditorBase();
  if (!editorBase) {
    return RawRangeBoundary();
  }

  nsISelectionController* selectionController =
      editorBase->GetSelectionController();
  if (NS_WARN_IF(!selectionController)) {
    return RawRangeBoundary();
  }

  const nsRange* lastRange = nullptr;
  static const SelectionType kIMESelectionTypes[] = {
      SelectionType::eIMERawClause, SelectionType::eIMESelectedRawClause,
      SelectionType::eIMEConvertedClause, SelectionType::eIMESelectedClause};
  for (auto selectionType : kIMESelectionTypes) {
    dom::Selection* selection =
        selectionController->GetSelection(ToRawSelectionType(selectionType));
    if (!selection) {
      continue;
    }
    const uint32_t rangeCount = selection->RangeCount();
    for (const uint32_t i : IntegerRange(rangeCount)) {
      MOZ_ASSERT(selection->RangeCount() == rangeCount);
      const nsRange* range = selection->GetRangeAt(i);
      MOZ_ASSERT(range);
      if (MOZ_UNLIKELY(NS_WARN_IF(!range)) ||
          MOZ_UNLIKELY(NS_WARN_IF(!range->GetEndContainer()))) {
        continue;
      }
      if (!lastRange) {
        lastRange = range;
        continue;
      }
      // In most cases, all composition string should be in same text node.
      if (lastRange->GetEndContainer() == range->GetEndContainer()) {
        if (lastRange->EndOffset() < range->EndOffset()) {
          lastRange = range;
        }
        continue;
      }
      // However, if web apps have inserted different nodes in composition
      // string, composition string may span 2 or more nodes.
      if (lastRange->GetEndContainer() ==
          range->GetEndContainer()->GetNextSibling()) {
        // Fast path for some known applications like Google Keep.
        lastRange = range;
        continue;
      }
      // Unfortunately, really slow path.
      // The ranges should always have a common ancestor, hence, be comparable.
      if (*nsContentUtils::ComparePoints(lastRange->EndRef(),
                                         range->EndRef()) == -1) {
        lastRange = range;
      }
    }
  }
  return lastRange ? lastRange->EndRef().AsRaw() : RawRangeBoundary();
}

/******************************************************************************
 * TextComposition::CompositionEventDispatcher
 ******************************************************************************/

TextComposition::CompositionEventDispatcher::CompositionEventDispatcher(
    TextComposition* aComposition, nsINode* aEventTarget,
    EventMessage aEventMessage, const nsAString& aData,
    bool aIsSynthesizedEvent)
    : Runnable("TextComposition::CompositionEventDispatcher"),
      mTextComposition(aComposition),
      mEventTarget(aEventTarget),
      mData(aData),
      mEventMessage(aEventMessage),
      mIsSynthesizedEvent(aIsSynthesizedEvent) {}

NS_IMETHODIMP
TextComposition::CompositionEventDispatcher::Run() {
  // The widget can be different from the widget which has dispatched
  // composition events because GetWidget() returns a widget which is proper
  // for calling NotifyIME().  However, this must no be problem since both
  // widget should share native IME context.  Therefore, even if an event
  // handler uses the widget for requesting IME to commit or cancel, it works.
  nsCOMPtr<nsIWidget> widget(mTextComposition->GetWidget());
  if (!mTextComposition->IsValidStateForComposition(widget)) {
    return NS_OK;  // cannot dispatch any events anymore
  }

  RefPtr<nsPresContext> presContext = mTextComposition->mPresContext;
  nsCOMPtr<nsINode> eventTarget = mEventTarget;
  RefPtr<BrowserParent> browserParent = mTextComposition->mBrowserParent;
  nsEventStatus status = nsEventStatus_eIgnore;
  switch (mEventMessage) {
    case eCompositionStart: {
      WidgetCompositionEvent compStart(true, eCompositionStart, widget);
      compStart.mNativeIMEContext = mTextComposition->mNativeContext;
      WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
                                                     widget);
      ContentEventHandler handler(presContext);
      handler.OnQuerySelectedText(&querySelectedTextEvent);
      NS_ASSERTION(querySelectedTextEvent.Succeeded(),
                   "Failed to get selected text");
      if (querySelectedTextEvent.FoundSelection()) {
        compStart.mData = querySelectedTextEvent.mReply->DataRef();
      }
      compStart.mFlags.mIsSynthesizedForTests =
          mTextComposition->IsSynthesizedForTests();
      IMEStateManager::DispatchCompositionEvent(
          eventTarget, presContext, browserParent, &compStart, &status, nullptr,
          mIsSynthesizedEvent);
      break;
    }
    case eCompositionChange:
    case eCompositionCommitAsIs:
    case eCompositionCommit: {
      WidgetCompositionEvent compEvent(true, mEventMessage, widget);
      compEvent.mNativeIMEContext = mTextComposition->mNativeContext;
      if (mEventMessage != eCompositionCommitAsIs) {
        compEvent.mData = mData;
      }
      compEvent.mFlags.mIsSynthesizedForTests =
          mTextComposition->IsSynthesizedForTests();
      IMEStateManager::DispatchCompositionEvent(
          eventTarget, presContext, browserParent, &compEvent, &status, nullptr,
          mIsSynthesizedEvent);
      break;
    }
    default:
      MOZ_CRASH("Unsupported event");
  }
  return NS_OK;
}

/******************************************************************************
 * TextCompositionArray
 ******************************************************************************/

TextCompositionArray::index_type TextCompositionArray::IndexOf(
    const NativeIMEContext& aNativeIMEContext) {
  if (!aNativeIMEContext.IsValid()) {
    return NoIndex;
  }
  for (index_type i = Length(); i > 0; --i) {
    if (ElementAt(i - 1)->GetNativeIMEContext() == aNativeIMEContext) {
      return i - 1;
    }
  }
  return NoIndex;
}

TextCompositionArray::index_type TextCompositionArray::IndexOf(
    nsIWidget* aWidget) {
  return IndexOf(aWidget->GetNativeIMEContext());
}

TextCompositionArray::index_type TextCompositionArray::IndexOf(
    nsPresContext* aPresContext) {
  for (index_type i = Length(); i > 0; --i) {
    if (ElementAt(i - 1)->GetPresContext() == aPresContext) {
      return i - 1;
    }
  }
  return NoIndex;
}

TextCompositionArray::index_type TextCompositionArray::IndexOf(
    nsPresContext* aPresContext, nsINode* aNode) {
  index_type index = IndexOf(aPresContext);
  if (index == NoIndex) {
    return NoIndex;
  }
  nsINode* node = ElementAt(index)->GetEventTargetNode();
  return node == aNode ? index : NoIndex;
}

TextComposition* TextCompositionArray::GetCompositionFor(nsIWidget* aWidget) {
  index_type i = IndexOf(aWidget);
  if (i == NoIndex) {
    return nullptr;
  }
  return ElementAt(i);
}

TextComposition* TextCompositionArray::GetCompositionFor(
    const WidgetCompositionEvent* aCompositionEvent) {
  index_type i = IndexOf(aCompositionEvent->mNativeIMEContext);
  if (i == NoIndex) {
    return nullptr;
  }
  return ElementAt(i);
}

TextComposition* TextCompositionArray::GetCompositionFor(
    nsPresContext* aPresContext) {
  index_type i = IndexOf(aPresContext);
  if (i == NoIndex) {
    return nullptr;
  }
  return ElementAt(i);
}

TextComposition* TextCompositionArray::GetCompositionFor(
    nsPresContext* aPresContext, nsINode* aNode) {
  index_type i = IndexOf(aPresContext, aNode);
  if (i == NoIndex) {
    return nullptr;
  }
  return ElementAt(i);
}

TextComposition* TextCompositionArray::GetCompositionInContent(
    nsPresContext* aPresContext, nsIContent* aContent) {
  // There should be only one composition per content object.
  for (index_type i = Length(); i > 0; --i) {
    nsINode* node = ElementAt(i - 1)->GetEventTargetNode();
    if (node && node->IsInclusiveDescendantOf(aContent)) {
      return ElementAt(i - 1);
    }
  }
  return nullptr;
}

}  // namespace mozilla