/* 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 https://mozilla.org/MPL/2.0/. */

#include "EditContext.h"

#include "mozilla/ClearOnShutdown.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/HTMLEditor.h"
#include "mozilla/IMEContentObserver.h"
#include "mozilla/IMEStateManager.h"
#include "mozilla/InputEventOptions.h"
#include "mozilla/MiscEvents.h"
#include "mozilla/TextComposition.h"
#include "mozilla/TextEvents.h"
#include "mozilla/dom/AnonymousContent.h"
#include "mozilla/dom/CharacterBoundsUpdateEvent.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/Text.h"
#include "mozilla/dom/TextFormatUpdateEvent.h"
#include "mozilla/dom/TextUpdateEvent.h"
#include "mozilla/intl/Segmenter.h"
#include "nsDOMCSSDeclaration.h"
#include "nsFocusManager.h"
#include "nsGenericHTMLElement.h"
#include "nsLayoutUtils.h"
#include "nsTextNode.h"

namespace mozilla::dom {

mozilla::LazyLogModule gEditContextLog("EditContext");

using InlineDir = WritingMode::InlineDir;
using LineStyle = TextRangeStyle::LineStyle;

NS_IMPL_ADDREF_INHERITED(EditContext, DOMEventTargetHelper)
NS_IMPL_RELEASE_INHERITED(EditContext, DOMEventTargetHelper)
NS_IMPL_CYCLE_COLLECTION_CLASS(EditContext)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(EditContext,
                                                DOMEventTargetHelper)
  tmp->UnsuppressNotifyingIME();
  MOZ_ASSERT(!tmp->mSuppressNotifyingIMETimer);
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mAssociatedElement, mText, mTextContainer)
  NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(EditContext,
                                                  DOMEventTargetHelper)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAssociatedElement, mText, mTextContainer,
                                    mSuppressNotifyingIMETimer)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EditContext)
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)

already_AddRefed<EditContext> EditContext::Constructor(
    const GlobalObject& aGlobal, const EditContextInit& aInit,
    ErrorResult& aRv) {
  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
  if (!global) {
    aRv.Throw(NS_ERROR_FAILURE);
    return nullptr;
  }
  RefPtr<EditContext> context = new EditContext(global);
  context->Init(aInit, aRv);
  if (aRv.Failed()) {
    return nullptr;
  }
  return context.forget();
}

JSObject* EditContext::WrapObject(JSContext* aCx,
                                  JS::Handle<JSObject*> aGivenProto) {
  return EditContext_Binding::Wrap(aCx, this, aGivenProto);
}

static StaticAutoPtr<nsTHashMap<const Element*, RefPtr<EditContext>>>
    sEditContextHashMap;

// static
EditContext* EditContext::GetForElement(const Element& aElement) {
  MOZ_ASSERT(NS_IsMainThread());
  if (!sEditContextHashMap || !aElement.HasFlag(ELEMENT_HAS_EDIT_CONTEXT)) {
    return nullptr;
  }
  auto entry = sEditContextHashMap->Lookup(&aElement);
  MOZ_ASSERT(entry,
             "Should be in hash map if ELEMENT_HAS_EDIT_CONTEXT is set.");
  return entry.Data();
}

// static
void EditContext::SetForElement(const Element& aElement,
                                mozilla::dom::EditContext* aEditContext) {
  MOZ_ASSERT(NS_IsMainThread());
  if (!sEditContextHashMap) {
    if (!aEditContext) {
      return;
    }
    sEditContextHashMap = new nsTHashMap<const Element*, RefPtr<EditContext>>;
    ClearOnShutdown(&sEditContextHashMap);
  }
  if (aEditContext) {
    sEditContextHashMap->InsertOrUpdate(&aElement, aEditContext);
    MOZ_LOG_FMT(gEditContextLog, LogLevel::Info,
                "[{}] Attach EditContext to {}",
                static_cast<void*>(aEditContext), aElement);
  } else {
    if (MOZ_LOG_TEST(gEditContextLog, LogLevel::Info)) {
      const auto* htmlElement = nsGenericHTMLElement::FromNode(&aElement);
      EditContext* editContext =
          htmlElement ? htmlElement->GetEditContext() : nullptr;
      if (editContext) {
        MOZ_LOG_FMT(gEditContextLog, LogLevel::Info,
                    "[{}] Detach EditContext from {}",
                    static_cast<void*>(editContext), aElement);
      }
    }
    sEditContextHashMap->Remove(&aElement);
  }
}

void EditContext::Deactivate() {
  MOZ_LOG_FMT(gEditContextLog, LogLevel::Info, "[{}] Deactivate EditContext",
              static_cast<void*>(this));
  MOZ_ASSERT(!mIsComposing,
             "Should have committed the composition before calling this.");
  UnsuppressNotifyingIME();
}

bool EditContext::IsActive() const {
  return mAssociatedElement &&
         mAssociatedElement->OwnerDoc()->GetActiveEditContext() == this;
}

// static
bool EditContext::IsAnyAttached() {
  MOZ_ASSERT(NS_IsMainThread());
  return sEditContextHashMap && !sEditContextHashMap->IsEmpty();
}

EditContext::EditContext(nsIGlobalObject* aGlobalObject)
    : DOMEventTargetHelper(aGlobalObject) {}

void EditContext::Init(const EditContextInit& aInit, ErrorResult& aRv) {
  nsPIDOMWindowInner* window = GetParentObject()->GetAsInnerWindow();
  MOZ_ASSERT(window);
  RefPtr<Document> document = window->GetDoc();
  MOZ_ASSERT(document);
  RefPtr<AnonymousContent> anonymousContent =
      document->InsertAnonymousContent(aRv);
  if (NS_WARN_IF(!anonymousContent)) {
    aRv.Throw(NS_ERROR_FAILURE);
    return;
  }
  RefPtr<Element> textContainer =
      document->CreateElem(u"div"_ns, nullptr, kNameSpaceID_XHTML);
  if (NS_WARN_IF(!textContainer)) {
    aRv.Throw(NS_ERROR_FAILURE);
    return;
  }
  mTextContainer = nsGenericHTMLElement::FromNode(textContainer);
  MOZ_ASSERT(mTextContainer);
  mText = document->CreateTextNode(u""_ns);
  mText->MarkAsMaybeModifiedFrequently();
  mTextContainer->AppendChild(*mText, aRv);
  anonymousContent->Root()->AppendChild(*mTextContainer, aRv);
  mText->SetEditableFlag(true);
  mTextContainer->SetEditableFlag(true);
  mTextContainer->Style()->SetProperty("visibility"_ns, "hidden"_ns, ""_ns,
                                       aRv);
  // For computing target ranges for backspace, etc.,
  // we shouldn't treat consecutive spaces as collapsed.
  mTextContainer->Style()->SetProperty("white-space"_ns, "pre"_ns, ""_ns, aRv);
  // Document-level anonymous content (created with InsertAnonymousContent)
  // all lives in the same container. So we need to set position: absolute
  // on all of them to prevent them from being positioned relative to
  // each other.
  mTextContainer->Style()->SetProperty("position"_ns, "absolute"_ns, ""_ns,
                                       aRv);
  mSelectionStart = aInit.mSelectionStart;
  mSelectionEnd = aInit.mSelectionEnd;
  UpdateTextInternal(0, 0, aInit.mText, aRv);
}

void EditContext::SetAssociatedElement(nsGenericHTMLElement* aElement) {
  mAssociatedElement = aElement;
  if (aElement && aElement->IsHTMLElement(nsGkAtoms::canvas)) {
    aElement->OwnerDoc()->SetUseCounter(eUseCounter_custom_EditContextCanvas);
  }
}

void EditContext::GetText(nsAString& aText) const { mText->GetData(aText); }

void EditContext::GetTextSubstring(uint32_t aStart, uint32_t aEnd,
                                   nsAString& aText) const {
  mText->SubstringData(aStart, aEnd - aStart, aText, IgnoreErrors());
}

RefPtr<DOMRect> EditContext::ToDOMRect(const Rect& aCopy) const {
  return MakeRefPtr<DOMRect>(GetRelevantGlobal(), aCopy.x, aCopy.y, aCopy.width,
                             aCopy.height);
}

auto EditContext::ToRect(const DOMRect& aRect) const -> Rect {
  return Rect(aRect.X(), aRect.Y(), aRect.Width(), aRect.Height());
}

LayoutDeviceIntRect EditContext::ToRootRelativeDeviceRect(
    const nsPresContext& aPresContext, const Rect& aRect) {
  CSSIntRect cssRect;
  aRect.ToIntRect(&cssRect);
  return ToRootRelativeDeviceRect(aPresContext, Rect::ToAppUnits(cssRect));
}

LayoutDeviceIntRect EditContext::ToRootRelativeDeviceRect(
    const nsPresContext& aPresContext, const nsRect& aRect) {
  nsRect rect = aRect;
  if (!aPresContext.IsRoot()) {
    nsPresContext* rootPC = aPresContext.GetRootPresContext();
    if (NS_WARN_IF(!rootPC)) {
      return {0, 0, 1, 1};
    }
    nsIFrame* documentRootFrame = aPresContext.PresShell()->GetRootFrame();
    nsIFrame* topLevelRootFrame = rootPC->PresShell()->GetRootFrame();
    if (NS_WARN_IF(!documentRootFrame) || NS_WARN_IF(!topLevelRootFrame)) {
      return {0, 0, 1, 1};
    }
    rect = nsLayoutUtils::TransformFrameRectToAncestor(documentRootFrame, rect,
                                                       topLevelRootFrame);
  }
  LayoutDeviceIntRect deviceRect = LayoutDeviceIntRect::FromAppUnitsToOutside(
      rect, aPresContext.AppUnitsPerDevPixel());
  // ContentCache, etc. is confused if the rectangles are empty,
  // so ensure that they aren't.
  deviceRect.width = std::max(1, deviceRect.width);
  deviceRect.height = std::max(1, deviceRect.height);
  return deviceRect;
}

// Suppress IME notifications until this goes out of scope.
class MOZ_STACK_CLASS EditContext::AutoSuppressIMENotifications {
 public:
  AutoSuppressIMENotifications() {
    if (IMEContentObserver* observer =
            IMEStateManager::GetActiveContentObserver()) {
      MOZ_LOG_FMT(
          gEditContextLog, LogLevel::Verbose,
          "[AutoSuppressIMENotifications] Suppressing IME notifications");
      observer->SuppressNotifyingIME();
      mObserver = observer;
    }
  }

  ~AutoSuppressIMENotifications() {
    if (mObserver) {
      MOZ_LOG_FMT(
          gEditContextLog, LogLevel::Verbose,
          "[AutoSuppressIMENotifications] Unsuppressing IME notifications");
      mObserver->UnsuppressNotifyingIME();
    }
  }

 private:
  RefPtr<IMEContentObserver> mObserver;
};

void EditContext::SuppressNotifyingIME(IsFromFocus aIsFromFocus) {
  IMEContentObserver* observer = IMEStateManager::GetActiveContentObserver();
  if (!observer) {
    return;
  }

  // We want to avoid suppressing NOTIFY_IME_OF_FOCUS for too long, since
  // it may cause text input not to be handled by the IME. So we have a shorter
  // timeout, and don't extend it even if new character bounds are needed.
  if (aIsFromFocus == IsFromFocus::Yes) {
    mIsSuppressingFocusNotification = true;
  } else if (mIsSuppressingFocusNotification) {
    return;
  }

  // We don't want to suppress forever - if the web app doesn't give
  // us character bounds quickly, we give up and unsuppress notifications.
  const char* suppressNotifyingIMETimeoutPref =
      aIsFromFocus == IsFromFocus::Yes
          ? "dom.editcontext.suppress_notifying_ime_timeout_focus"
          : "dom.editcontext.suppress_notifying_ime_timeout";
  const uint32_t timeoutMillis =
      Preferences::GetUint(suppressNotifyingIMETimeoutPref);
  WeakPtr<EditContext> editContextWeak(this);
  auto unsuppressCallback =
      [editContextWeak]([[maybe_unused]] nsITimer* aTimer) {
        EditContext* editContext = editContextWeak.get();
        MOZ_ASSERT(editContext,
                   "We should have cancelled the timer when the EditContext "
                   "was destroyed.");
        editContext->UnsuppressNotifyingIME();
      };
  auto result = NS_NewTimerWithCallback(
      unsuppressCallback, timeoutMillis, nsITimer::TYPE_ONE_SHOT,
      "EditContext::UnsuppressNotifyingIME"_ns);
  if (result.isErr()) {
    NS_WARNING("NS_NewTimerWithCallback() failed.");
    return;
  }
  MOZ_LOG_FMT(
      gEditContextLog, LogLevel::Verbose,
      "[{}] Suppressing IME notifications for {}ms (had previous timer = {})",
      static_cast<void*>(this), timeoutMillis,
      YesOrNo(mSuppressNotifyingIMETimer));
  if (mSuppressNotifyingIMETimer) {
    mSuppressNotifyingIMETimer->Cancel();
  } else {
    observer->SuppressNotifyingIME();
  }
  mSuppressNotifyingIMETimer = result.unwrap();
}

void EditContext::UpdateText(uint32_t aRangeStart, uint32_t aRangeEnd,
                             const nsAString& aText, ErrorResult& aRv) {
  MOZ_LOG_FMT(gEditContextLog, LogLevel::Info,
              "[{}] updateText() with range {}-{} and text {}",
              static_cast<void*>(this), aRangeStart, aRangeEnd,
              ToString(aText));
  UpdateTextInternal(aRangeStart, aRangeEnd, aText, aRv);
  FireCharacterBoundsUpdateIfNeeded();
}

void EditContext::UpdateSelection(uint32_t aStart, uint32_t aEnd) {
  if (aStart == mSelectionStart && aEnd == mSelectionEnd) {
    return;
  }
  MOZ_LOG_FMT(gEditContextLog, LogLevel::Info, "[{}] Update selection to {}-{}",
              static_cast<void*>(this), aStart, aEnd);

  mSelectionStart = aStart;
  mSelectionEnd = aEnd;
  // Changing selection, so there may now be different
  // text around the caret.
  mTextNextToCaretChangedByTextUpdateHandler = true;

  if (IsActive()) {
    FireCharacterBoundsUpdateIfNeeded();
    if (IMEContentObserver* observer =
            IMEStateManager::GetActiveContentObserver()) {
      observer->EditContextSelectionChanged();
    }
  }
}

template <typename T>
static nsCString SequenceOwningNonNullToString(
    const Sequence<OwningNonNull<T>>& aSequence) {
  nsCString string("[");
  for (size_t i = 0; i < aSequence.Length(); i++) {
    const T& element = aSequence.ElementAt(i);
    if (i > 0) {
      string.AppendLiteral(", ");
    }
    if (i > 8) {
      string.AppendLiteral("...");
      break;
    }
    string.Append(ToString(element));
  }
  string.Append(']');
  return string;
}

void EditContext::UpdateCharacterBounds(
    uint32_t aRangeStart,
    const Sequence<OwningNonNull<DOMRect>>& aCharacterBounds) {
  MOZ_LOG_FMT(gEditContextLog, LogLevel::Debug,
              "[{}] Update character bounds starting at {} to {}"
              " (firing characterboundsupdate = {})",
              static_cast<void*>(this), aRangeStart,
              SequenceOwningNonNullToString(aCharacterBounds),
              mIsFiringCharacterBoundsUpdate);
  mCodepointRectsStartIndex = aRangeStart;
  mCodepointRects.Clear();
  mCodepointRects.SetCapacity(aCharacterBounds.Length());
  for (const auto& rect : aCharacterBounds) {
    mCodepointRects.AppendElement(ToRect(rect));
  }

  // Reset last requested character bounds.
  // If we request the bounds for some range A, and then later
  // the web app provides bounds for range B which we didn't request,
  // then we want the character bounds for range A again, we should fire
  // another characterboundsupdate event.
  mLastRequestedCharacterBoundsRange = {};

  if (!mIsFiringCharacterBoundsUpdate && IsActive()) {
    // Web app sent new character bounds of its own accord, without
    // a characterboundsupdate event - inform IME that position may
    // have changed
    if (IMEContentObserver* observer =
            IMEStateManager::GetActiveContentObserver()) {
      observer->EditContextPositionChanged();
    }
  }
  UnsuppressNotifyingIME();
}

void EditContext::CharacterBounds(nsTArray<RefPtr<DOMRect>>& aRetVal) const {
  aRetVal.SetCapacity(mCodepointRects.Length());
  for (const Rect& rect : mCodepointRects) {
    aRetVal.AppendElement(ToDOMRect(rect));
  }
}

uint32_t EditContext::TextLength() const { return mText->TextLength(); }

WritingMode EditContext::WritingMode() const {
  if (!mAssociatedElement) {
    return mozilla::WritingMode();
  }
  nsIFrame* frame = mAssociatedElement->GetPrimaryFrame();
  if (!frame) {
    return mozilla::WritingMode();
  }
  return frame->GetWritingMode();
}

void EditContext::UpdateTextInternal(uint32_t aRangeStart, uint32_t aRangeEnd,
                                     const nsAString& aText, ErrorResult& aRv) {
  if (NS_WARN_IF(!mText->DataBuffer().CanGrowBy(aText.Length()))) {
    aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
    return;
  }
  const uint32_t prevSelectionStart = SelectionStartClamped();
  const uint32_t prevSelectionEnd = SelectionEndClamped();
  uint32_t start = std::min(aRangeStart, aRangeEnd);
  start = std::min(start, TextLength());
  uint32_t end = std::max(aRangeStart, aRangeEnd);
  end = std::min(end, TextLength());
  if (mSelectionStart == mSelectionEnd && start <= mSelectionStart &&
      end >= mSelectionStart) {
    // The character before or after the caret has changed.
    // This may affect how we render the caret for BiDi text.
    mTextNextToCaretChangedByTextUpdateHandler = true;
  }
  mText->ReplaceData(start, end - start, aText, IgnoreErrors());
  // Check if the existing codepoint rects are affected by this change.
  // If the text being changed is after the last stored codepoint rect,
  // then the codepoint rects most likely won't be affected, so we don't
  // need to fire characterboundsupdate again.
  if (start < CodepointRectsEndIndex()) {
    mCodepointRectsTextChanged = true;
  }
  // XXX: Perhaps mSelectionStart/End should be clamped to new length
  //      of text? See https://github.com/w3c/edit-context/issues/88
  if (IsActive()) {
    if (IMEContentObserver* observer =
            IMEStateManager::GetActiveContentObserver()) {
      if (SelectionStartClamped() != prevSelectionStart ||
          SelectionEndClamped() != prevSelectionEnd) {
        observer->EditContextSelectionChanged();
      }
      observer->EditContextTextChanged(start, end, aText);
    }
  }
}

void EditContext::UpdateControlBounds(const DOMRect& aControlBounds) {
  Rect newRect = ToRect(aControlBounds);
  if (mControlBounds == Some(newRect)) {
    // Same rect as before - don't notify IME.
    return;
  }
  MOZ_LOG_FMT(gEditContextLog, LogLevel::Info,
              "[{}] Update control bounds to {}", static_cast<void*>(this),
              ToString(aControlBounds));
  mControlBounds = Some(newRect);
  // Character bounds may have changed due to editor rectangle changing.
  FireCharacterBoundsUpdateIfNeeded();
  if (IMEContentObserver* observer =
          IMEStateManager::GetActiveContentObserver()) {
    observer->EditContextPositionChanged();
  }
}

void EditContext::UpdateSelectionBounds(const DOMRect& aSelectionBounds) {
  mSelectionBounds = Some(ToRect(aSelectionBounds));
  MOZ_LOG_FMT(gEditContextLog, LogLevel::Info,
              "[{}] Update selection bounds to {}", static_cast<void*>(this),
              ToString(aSelectionBounds));
}

void EditContext::UpdateTextAndFireEvent(
    uint32_t aStart, uint32_t aEnd, const nsAString& aString,
    PreventSetSelection aPreventSetSelection) {
  aStart = std::min(aStart, TextLength());
  aEnd = std::min(aEnd, TextLength());
  if (aStart == aEnd && aString.IsEmpty()) {
    // Edit doesn't change anything
    return;
  }
  if (aStart > aEnd) {
    std::swap(aStart, aEnd);
  }

  // Suppress IME notifications at least until this method returns.
  // FireCharacterBoundsUpdateIfNeeded may cause it to be suppressed
  // for longer.
  AutoSuppressIMENotifications suppress;
  IgnoredErrorResult rv;
  UpdateTextInternal(aStart, aEnd, aString, rv);
  if (rv.Failed()) {
    return;
  }
  if (aPreventSetSelection == PreventSetSelection::Yes) {
    // Don't move selection to end of replaced text - just
    // fix up the offsets if they are inside/after the replaced text.
    for (uint32_t* offset : {&mSelectionStart, &mSelectionEnd}) {
      if (*offset >= aStart && *offset < aEnd) {
        *offset = aStart;
      } else if (*offset >= aEnd) {
        *offset += aString.Length() - (aEnd - aStart);
      }
    }
  } else {
    mSelectionStart = mSelectionEnd = aStart + aString.Length();
  }
  FireTextUpdate(aStart, aEnd, aString);
  // Request new character bounds.
  FireCharacterBoundsUpdateIfNeeded();
}

void EditContext::FireTextUpdate(uint32_t aUpdateRangeStart,
                                 uint32_t aUpdateRangeEnd,
                                 const nsAString& aText) {
  TextUpdateEventInit options;
  options.mText = aText;
  options.mSelectionStart = mSelectionStart;
  options.mSelectionEnd = mSelectionEnd;
  options.mUpdateRangeStart = aUpdateRangeStart;
  options.mUpdateRangeEnd = aUpdateRangeEnd;
  options.mBubbles = false;
  options.mCancelable = true;
  RefPtr<TextUpdateEvent> e =
      TextUpdateEvent::Constructor(this, u"textupdate"_ns, options);
  e->SetTrusted(true);
  mTextNextToCaretChangedByTextUpdateHandler = false;

  MOZ_LOG_FMT(gEditContextLog, LogLevel::Debug,
              "[{}] Dispatch textupdate event selection={}-{}, "
              "updateRange={}-{}, text={}",
              static_cast<void*>(this), options.mSelectionStart,
              options.mSelectionEnd, options.mUpdateRangeStart,
              options.mUpdateRangeEnd, ToString(options.mText));

  // textupdate can be fired recursively if the editor is blurred
  // during a composition (since that cancels the composition).
  // XXX: Some web apps may not handle this properly
  //      - perhaps add mPendingTextUpdateEvent/TextFormatUpdateEvent?
  AutoRestore restore(mIsFiringTextUpdate);
  mIsFiringTextUpdate = true;
  DispatchEvent(*e);
}

void EditContext::DoSetSelection(WidgetSelectionEvent& aEvent) {
  MOZ_LOG_FMT(gEditContextLog, LogLevel::Debug,
              "[{}] {} with offset={} length={} direction={} "
              "expandToClusterBoundary={}",
              static_cast<void*>(this), __func__, aEvent.mOffset,
              aEvent.mLength, aEvent.mDirection,
              aEvent.mExpandToClusterBoundary);
  TextRange range(std::min(aEvent.mOffset, TextLength()),
                  std::min(aEvent.mOffset + aEvent.mLength, TextLength()));
  if (aEvent.ShouldExpandToClusterBoundary()) {
    range = ExpandRangeToClusterBoundaries(range);
  }
  mSelectionStart = range.mStart;
  mSelectionEnd = range.mEnd;
  if (aEvent.IsReversed()) {
    std::swap(mSelectionStart, mSelectionEnd);
  }
  if (IMEContentObserver* observer =
          IMEStateManager::GetActiveContentObserver()) {
    observer->EditContextSelectionChanged();
  }
  // We need to fire textupdate so that the web app is aware that the
  // selection has changed.
  FireTextUpdate(0, 0, u""_ns);
  FireCharacterBoundsUpdateIfNeeded();
}

void EditContext::StartComposition(const WidgetCompositionEvent& aEvent) {
  MOZ_LOG_FMT(gEditContextLog, LogLevel::Debug, "[{}] Start composition",
              static_cast<void*>(this));
  MOZ_ASSERT(!mIsComposing);
  WidgetCompositionEvent event(aEvent);
  RefPtr presContext = mText->OwnerDoc()->GetPresContext();
  EventDispatcher::Dispatch(this, presContext, &event);
  mIsComposing = true;
}

void EditContext::EndComposition(const WidgetCompositionEvent& aEvent) {
  MOZ_LOG_FMT(gEditContextLog, LogLevel::Debug, "[{}] End composition",
              static_cast<void*>(this));
  MOZ_ASSERT(mIsComposing);
  WidgetCompositionEvent event(aEvent);
  RefPtr presContext = mText->OwnerDoc()->GetPresContext();
  EventDispatcher::Dispatch(this, presContext, &event);
  mIsComposing = false;
}

void EditContext::DoContentCommandReplaceText(
    WidgetContentCommandEvent& aEvent) {
  MOZ_ASSERT(aEvent.mMessage == eContentCommandReplaceText);
  MOZ_ASSERT(aEvent.mString);
  if (!aEvent.mString) {
    aEvent.mSucceeded = false;
    return;
  }
  MOZ_ASSERT(IsActive(), "Should be the active EditContext.");
  nsAutoString text;
  const uint32_t replaceOffset = aEvent.mSelection.mOffset;
  const uint32_t replaceLength = aEvent.mSelection.mReplaceSrcString.Length();
  MOZ_LOG_FMT(gEditContextLog, LogLevel::Debug,
              "[{}] Do ContentCommandReplaceText offset={} replaceSrcString={} "
              "string={}",
              static_cast<void*>(this), replaceOffset,
              ToString(aEvent.mSelection.mReplaceSrcString),
              ToString(aEvent.mString));
  mText->SubstringData(replaceOffset, replaceLength, text, IgnoreErrors());
  if (text != aEvent.mSelection.mReplaceSrcString) {
    // String to replace doesn't match the text.
    aEvent.mSucceeded = false;
    return;
  }
  // Dispatch beforeinput
  // XXX: We can't really determine the target ranges here.
  //      See https://github.com/w3c/edit-context/issues/133
  InputEventOptions options(*aEvent.mString,
                            InputEventOptions::NeverCancelable::No);
  nsEventStatus status = nsEventStatus_eIgnore;
  RefPtr<nsGenericHTMLElement> associatedElement = GetAssociatedElement();
  MOZ_ASSERT(associatedElement);
  RefPtr<HTMLEditor> htmlEditor =
      associatedElement->OwnerDoc()->GetHTMLEditor();
  MOZ_ASSERT(htmlEditor);
  // We are using the insertText inputType instead of insertReplacementText,
  // since insertReplacementText is used for spellcheck with no textupdate
  // fired, so editors with spellcheck enabled will do the replacement
  // twice if we fire both insertReplacementText and textupdate.
  nsresult rv = nsContentUtils::DispatchInputEvent(
      associatedElement, eEditorBeforeInput, EditorInputType::eInsertText,
      htmlEditor, std::move(options), &status);
  if (NS_FAILED(rv) || status == nsEventStatus_eConsumeNoDefault ||
      !IsActive()) {
    aEvent.mSucceeded = false;
    return;
  }
  // Dispatch textupdate
  UpdateTextAndFireEvent(replaceOffset, replaceOffset + replaceLength,
                         *aEvent.mString,
                         aEvent.mSelection.mPreventSetSelection);
  aEvent.mSucceeded = true;
}

static UnderlineStyle ToDOMStyle(LineStyle aStyle) {
  switch (aStyle) {
    case LineStyle::None:
      return UnderlineStyle::None;
    case LineStyle::Dashed:
      return UnderlineStyle::Dashed;
    case LineStyle::Dotted:
      return UnderlineStyle::Dotted;
    case LineStyle::Wavy:
      return UnderlineStyle::Wavy;
    case LineStyle::Solid:
      return UnderlineStyle::Solid;
    case LineStyle::Double:
      // Current EditContext spec doesn't have "double" style.
      return UnderlineStyle::Solid;
  }
  MOZ_CRASH("Invalid LineStyle.");
  return UnderlineStyle::None;
}

void EditContext::FireTextFormatUpdate(const TextRangeArray* aRanges,
                                       uint32_t aCompositionOffset) {
  TextFormatUpdateEventInit eventOptions;
  eventOptions.mBubbles = false;
  eventOptions.mCancelable = true;
  if (aRanges) {
    for (const mozilla::TextRange& range : *aRanges) {
      if (range.Length() == 0) {
        // Empty ranges probably aren't useful?
        continue;
      }
      TextFormatInit formatOptions;
      formatOptions.mRangeStart = range.mStartOffset + aCompositionOffset;
      formatOptions.mRangeEnd = range.mEndOffset + aCompositionOffset;
      if (mText->OwnerDoc()->ShouldResistFingerprinting(RFPTarget::IMEStyle)) {
        // Don't expose details of how IME styles text to the web app.
        // Instead, always use thick solid underline for selected clause,
        // and thin solid underline for other clauses.
        formatOptions.mUnderlineStyle = UnderlineStyle::Solid;
        formatOptions.mUnderlineThickness =
            range.mRangeType == TextRangeType::eSelectedClause ||
                    range.mRangeType == TextRangeType::eSelectedRawClause
                ? UnderlineThickness::Thick
                : UnderlineThickness::Thin;
      } else if (range.mRangeStyle.IsLineStyleDefined() &&
                 range.mRangeStyle.mLineStyle != LineStyle::None) {
        formatOptions.mUnderlineStyle =
            ToDOMStyle(range.mRangeStyle.mLineStyle);
        formatOptions.mUnderlineThickness = range.mRangeStyle.mIsBoldLine
                                                ? UnderlineThickness::Thick
                                                : UnderlineThickness::Thin;
      }
      OwningNonNull<TextFormat> format =
          MakeRefPtr<TextFormat>(GetRelevantGlobal(), formatOptions);
      [[maybe_unused]] auto* element =
          eventOptions.mTextFormats.AppendElement(std::move(format), fallible);
      NS_WARNING_ASSERTION(element, "TextFormat array allocation failed");
    }
  }
  MOZ_LOG_FMT(gEditContextLog, LogLevel::Debug,
              "[{}] Fire textformatupdate with formats {}",
              static_cast<void*>(this),
              SequenceOwningNonNullToString(eventOptions.mTextFormats));

  RefPtr<TextFormatUpdateEvent> e = MakeRefPtr<TextFormatUpdateEvent>(
      this, u"textformatupdate"_ns, eventOptions);
  e->SetTrusted(true);
  DispatchEvent(*e);
}

static InlineDir ReverseInlineDir(InlineDir dir) {
  switch (dir) {
    case InlineDir::LTR:
      return InlineDir::RTL;
    case InlineDir::RTL:
      return InlineDir::LTR;
    case InlineDir::BTT:
      return InlineDir::TTB;
    case InlineDir::TTB:
      return InlineDir::BTT;
  }
  MOZ_ASSERT(false, "invalid InlineDir");
  return InlineDir::LTR;
}

void EditContext::UnsuppressNotifyingIME() {
  if (mSuppressNotifyingIMETimer) {
    MOZ_LOG_FMT(gEditContextLog, LogLevel::Verbose,
                "[{}] Unsuppressing IME notifications",
                static_cast<void*>(this));
    mSuppressNotifyingIMETimer->Cancel();
    mSuppressNotifyingIMETimer = nullptr;
    mIsSuppressingFocusNotification = false;
    if (IMEContentObserver* observer =
            IMEStateManager::GetActiveContentObserver()) {
      observer->UnsuppressNotifyingIME();
    }
  }
}

bool EditContext::ShouldFireNewCharacterBoundsUpdateForRange(
    TextRange aRange) const {
  if (mCodepointRectsTextChanged) {
    // Text at or before existing rects has changed, so we should fire again.
    return true;
  }
  if (mControlBoundsAtLastCharacterBoundsUpdate !=
      GetControlBoundsOrClientRect()) {
    // Control bounds changed, so we should fire again.
    return true;
  }
  if (aRange.IsContainedIn(mCodepointRectsStartIndex,
                           CodepointRectsEndIndex())) {
    // We already have these character bounds.
    return false;
  }
  if (aRange.IsContainedIn(mLastRequestedCharacterBoundsRange)) {
    // We already requested these character bounds.
    return false;
  }
  return true;
}

auto EditContext::ExpandRangeToClusterBoundaries(TextRange aRange) const
    -> TextRange {
  // Number of code units of context to use when determining the
  // grapheme cluster boundaries.
  constexpr uint32_t kContext = 16;
  const uint32_t start = aRange.mStart;
  const uint32_t end = aRange.mEnd;
  uint32_t startExtendedToGraphemeCluster = start;
  {
    nsAutoString startText;
    uint32_t startTextOffset = std::max(start, kContext) - kContext;
    GetTextSubstring(startTextOffset, std::min(start + kContext, TextLength()),
                     startText);
    intl::GraphemeClusterBreakIteratorUtf16 iter(startText);
    // find first grapheme cluster break before or equal to aStart
    while (Maybe<uint32_t> i = iter.Next()) {
      if (startTextOffset + *i > start) {
        break;
      }
      startExtendedToGraphemeCluster = startTextOffset + *i;
    }
  }
  uint32_t endExtendedToGraphemeCluster = end;
  {
    nsAutoString endText;
    uint32_t endTextOffset = std::max(end, kContext) - kContext;
    GetTextSubstring(endTextOffset, std::min(end + kContext, TextLength()),
                     endText);
    // find first grapheme cluster break after or equal to aEnd
    intl::GraphemeClusterBreakIteratorUtf16 iter(endText);
    while (Maybe<uint32_t> i = iter.Next()) {
      if (endTextOffset + *i >= end) {
        endExtendedToGraphemeCluster = endTextOffset + *i;
        break;
      }
    }
  }
  return {startExtendedToGraphemeCluster, endExtendedToGraphemeCluster};
}

void EditContext::FireCharacterBoundsUpdateIfNeeded(IsFromFocus aIsFromFocus) {
  // If characterboundsupdate handler changes text or something, don't fire
  // another characterboundsupdate to avoid infinite recursion.
  if (mIsFiringCharacterBoundsUpdate || !IsActive()) {
    return;
  }
  uint32_t start = SelectionMinClamped();
  uint32_t end = SelectionMaxClamped();
  MOZ_ASSERT(mAssociatedElement, "Should have associated element if active.");
  if (HTMLEditor* editor = mAssociatedElement->OwnerDoc()->GetHTMLEditor()) {
    if (TextComposition* composition = editor->GetComposition()) {
      start = composition->ClampedStartOffsetInTextNode();
      end = composition->EndOffsetMaybeInFollowingTextNode();
    }
  }
  if (start == end) {
    // Extend empty range to include one character.
    if (end < TextLength()) {
      end++;
    } else if (start) {
      start--;
    } else {
      // Text is empty - can't request character bounds.
      return;
    }
  }
  MOZ_ASSERT(start < end);

  // Extend requested range to grapheme cluster boundaries,
  // in case web app has poor handling of ranges starting/ending
  // in the middle of a grapheme cluster.
  const TextRange requestRange = ExpandRangeToClusterBoundaries({start, end});
  // If we already have the requested character bounds or we already
  // requested them, and nothing relevant has changed, don't fire
  // characterboundsupdate again.
  if (!ShouldFireNewCharacterBoundsUpdateForRange(requestRange)) {
    return;
  }
  // We set all this stuff here instead of in updateCharacterBounds(),
  // since we don't want to fire characterupdatebounds repeatedly even
  // if the web app never calls updateCharacterBounds().
  mControlBoundsAtLastCharacterBoundsUpdate = GetControlBoundsOrClientRect();
  mLastRequestedCharacterBoundsRange = requestRange;
  mCodepointRectsTextChanged = false;

  // We want to suppress notifying the IME of
  // NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED, etc. until the character bounds
  // are provided, to avoid the IME user interface moving around too much.
  SuppressNotifyingIME(aIsFromFocus);

  CharacterBoundsUpdateEventInit eventOptions;
  eventOptions.mBubbles = false;
  eventOptions.mCancelable = true;
  eventOptions.mRangeStart = requestRange.mStart;
  eventOptions.mRangeEnd = requestRange.mEnd;
  mIsFiringCharacterBoundsUpdate = true;
  MOZ_LOG_FMT(gEditContextLog, LogLevel::Debug,
              "[{}] Fire characterboundsupdate with range {}-{} "
              "(isFromFocus = {})",
              static_cast<void*>(this), eventOptions.mRangeStart,
              eventOptions.mRangeEnd,
              YesOrNo(aIsFromFocus == IsFromFocus::Yes));
  RefPtr event = CharacterBoundsUpdateEvent::Constructor(
      this, u"characterboundsupdate"_ns, eventOptions);
  event->SetTrusted(true);
  DispatchEvent(*event);
  mIsFiringCharacterBoundsUpdate = false;
  if (!requestRange.IsContainedIn(mCodepointRectsStartIndex,
                                  CodepointRectsEndIndex())) {
    MOZ_LOG_FMT(gEditContextLog, LogLevel::Debug,
                "[{}] characterboundsupdate handler didn't provide requested "
                "bounds synchronously - want {}-{}, but have {}-{}",
                static_cast<void*>(this), requestRange.mStart,
                requestRange.mEnd, mCodepointRectsStartIndex,
                CodepointRectsEndIndex());
    if (!mWarnedAboutUpdateCharacterBoundsNotCalled) {
      nsContentUtils::ReportToConsole(
          nsIScriptError::warningFlag, "DOM"_ns, mText->OwnerDoc(),
          PropertiesFile::DOM_PROPERTIES, "EditContextCharacterBoundsWarning");
      mWarnedAboutUpdateCharacterBoundsNotCalled = true;
    }
  };
}

nsresult EditContext::GetCharacterBounds(
    uint32_t aStart, uint32_t aEnd,
    nsTArray<LayoutDeviceIntRect>& aRects) const {
  MOZ_LOG_FMT(gEditContextLog, LogLevel::Verbose,
              "[{}] Getting character bounds for range {}-{}, have {}-{}",
              static_cast<const void*>(this), aStart, aEnd,
              mCodepointRectsStartIndex, CodepointRectsEndIndex());
  MOZ_ASSERT(aRects.IsEmpty());
  MOZ_ASSERT(aStart <= aEnd);
  aStart = std::min(aStart, TextLength());
  aEnd = std::min(aEnd, TextLength());
  enum class CollapseDirection {
    // Don't collapse returned rectangle
    None,
    // Collapse returned rectangle in the direction of the previous character
    // (e.g. for LTR, set width = 0)
    Previous,
    // Collapse returned rectangle in the direction of the next character
    // (e.g. for LTR, set x += width, width = 0)
    Next,
  };
  CollapseDirection collapse = CollapseDirection::None;
  if (aStart == aEnd) {
    // In this case, ContentEventHandler still wants a rectangle for the caret
    if (TextLength() == 0) {
      aRects.AppendElement(FallbackBounds());
      return NS_OK;
    }
    if (aEnd < TextLength()) {
      // If requested range is before end of text, query the next character,
      // and collapse its rectangle in the opposite direction of the writing.
      aEnd++;
      collapse = CollapseDirection::Previous;
    } else {
      // If requested range is at end of text, query the previous character,
      // and collapse its rectangle in the direction of the writing.
      MOZ_ASSERT(aStart > 0);
      aStart--;
      collapse = CollapseDirection::Next;
    }
  }
  nsPresContext* presContext = mText->OwnerDoc()->GetPresContext();
  if (NS_WARN_IF(!presContext)) {
    return NS_ERROR_FAILURE;
  }

  aRects.SetCapacity(aEnd - aStart);
  if (mCodepointRects.IsEmpty()) {
    // Web app never provided any character bounds - in this case,
    // for DOM-based EditContext, we try to get the rectangles from the DOM
    // in ContentEventHandler. For canvas-based EditContext, we end up
    // using EditContext::FallbackBounds().
    return NS_ERROR_FAILURE;
  }
  for (int64_t i = aStart; i < aEnd; i++) {
    const int64_t indexInCodepointRects = i - mCodepointRectsStartIndex;
    const int64_t clampedIndex = std::clamp<int64_t>(
        indexInCodepointRects, 0, mCodepointRects.Length() - 1);
    Rect cssRect = mCodepointRects[clampedIndex];
    if (clampedIndex != indexInCodepointRects) {
      // Web app didn't provide the requested bounds -
      // use either the selection or the closest available
      // character bound, whichever is closer.
      int64_t distanceFromSelection = std::abs(i - mSelectionStart);
      int64_t distanceFromClamped =
          std::abs(indexInCodepointRects - clampedIndex);
      if (mSelectionBounds && distanceFromSelection < distanceFromClamped) {
        cssRect = *mSelectionBounds;
      }
    }
    LayoutDeviceIntRect deviceRect =
        ToRootRelativeDeviceRect(*presContext, cssRect);
    aRects.AppendElement(deviceRect);
  }
  if (collapse != CollapseDirection::None) {
    // collapse should only be set when we just want one rectangle
    MOZ_ASSERT(aRects.Length() == 1);
    if (aRects.IsEmpty()) {
      // Don't index out of bounds
      return NS_ERROR_FAILURE;
    }
    LayoutDeviceIntRect& rect = aRects[0];
    InlineDir dir = WritingMode().GetInlineDir();
    if (collapse == CollapseDirection::Next) {
      dir = ReverseInlineDir(dir);
    }
    switch (dir) {
      case InlineDir::LTR:
        // Collapse rect to the left.
        rect.width = 1;
        break;
      case InlineDir::RTL:
        // Collapse rect to the right.
        rect.x += rect.width;
        rect.width = 1;
        break;
      case InlineDir::BTT:
        // Collapse rect to the bottom.
        rect.y += rect.height;
        rect.height = 1;
        break;
      case InlineDir::TTB:
        // Collapse rect to the top.
        rect.height = 1;
        break;
    }
  }
  return NS_OK;
}

Maybe<LayoutDeviceIntRect> EditContext::GetControlBounds() const {
  nsPresContext* presContext = mText->OwnerDoc()->GetPresContext();
  if (!presContext || !mControlBounds) {
    // Control bounds were never set.
    return Nothing();
  }
  return Some(ToRootRelativeDeviceRect(*presContext, *mControlBounds));
}

Maybe<LayoutDeviceIntRect> EditContext::GetSelectionBounds() const {
  nsPresContext* presContext = mText->OwnerDoc()->GetPresContext();
  if (!presContext || !mSelectionBounds) {
    // Selection bounds were never set.
    return Nothing();
  }
  return Some(ToRootRelativeDeviceRect(*presContext, *mSelectionBounds));
}

Maybe<nsRect> EditContext::GetControlBoundsOrClientRect() const {
  if (mControlBounds) {
    CSSIntRect intRect;
    mControlBounds->ToIntRect(&intRect);
    return Some(Rect::ToAppUnits(intRect));
  }
  if (!mAssociatedElement || !mAssociatedElement->GetPrimaryFrame()) {
    return Nothing();
  }
  return Some(mAssociatedElement->GetPrimaryFrame()->GetRect());
}

LayoutDeviceIntRect EditContext::FallbackBounds() const {
  if (Maybe<LayoutDeviceIntRect> bounds = GetSelectionBounds()) {
    return *bounds;
  }
  if (Maybe<LayoutDeviceIntRect> bounds = GetControlBounds()) {
    return *bounds;
  }
  Maybe<nsRect> appUnitsRect = GetControlBoundsOrClientRect();
  if (NS_WARN_IF(!appUnitsRect)) {
    // Nothing good we can return here.
    return {0, 0, 1, 1};
  }
  nsPresContext* presContext =
      mAssociatedElement->GetPrimaryFrame()->PresContext();
  return ToRootRelativeDeviceRect(*presContext, *appUnitsRect);
}

// static
void EditContext::NotifyActiveEditContextChanged(Document& aDocument) {
  if (RefPtr newEditContext = aDocument.GetActiveEditContext()) {
    newEditContext->FireCharacterBoundsUpdateIfNeeded(IsFromFocus::Yes);
  }

  RefPtr<HTMLEditor> editor = aDocument.GetHTMLEditor();
  if (!editor) {
    return;
  }

  RefPtr<Element> focusedElement = editor->GetFocusedElement();
  if (!focusedElement ||
      focusedElement != IMEStateManager::GetFocusedElement()) {
    // We will handle this in IMEStateManager::OnChangeFocus.
    // Calling UpdateIMEState() here is only needed when the editor
    // changes between EditContext an non-EditContext without the
    // focused element changing.
    return;
  }
  auto newStateOrError = editor->GetPreferredIMEState();
  // Currently, HTMLEditor::GetPreferredIMEState cannot fail.
  MOZ_ASSERT(newStateOrError.isOk(),
             "HTMLEditor::GetPreferredIMEState() failed");
  const widget::IMEState defaultState(widget::IMEEnabled::Disabled);
  IMEStateManager::UpdateIMEState(newStateOrError.unwrapOr(defaultState),
                                  focusedElement, *editor);
  // (Note that window may have been destroyed by UpdateIMEState.)
}

bool EditContext::IsCanvas() const {
  return mAssociatedElement &&
         mAssociatedElement->IsHTMLElement(nsGkAtoms::canvas);
}

std::ostream& operator<<(std::ostream& aStream,
                         const EditContext& aEditContext) {
  nsAutoString debugText;
  aEditContext.GetText(debugText);
  if (debugText.Length() > 15) {
    debugText.Truncate(12);
    debugText.AppendLiteral(u"...");
  }
  debugText.ReplaceSubstring(u"\"", u"\\\"");
  debugText.ReplaceSubstring(u"\\", u"\\\\");
  debugText.ReplaceSubstring(u"\n", u"\\n");
  aStream << "EditContext@" << &aEditContext << " { text=\"" << debugText
          << "\", selection=" << aEditContext.SelectionStart() << "-"
          << aEditContext.SelectionEnd() << ", associatedElement=";
  if (Element* element = aEditContext.GetAssociatedElement()) {
    aStream << *element;
  } else {
    aStream << "none";
  }
  return aStream << ", isComposing=" << TrueOrFalse(aEditContext.mIsComposing)
                 << " }";
}

}  // namespace mozilla::dom
