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

/*
 * Implementation of mozilla::SelectionChangeEventDispatcher
 */

#include "SelectionChangeEventDispatcher.h"

#include "mozilla/AsyncEventDispatcher.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/IntegerRange.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Selection.h"
#include "nsCOMPtr.h"
#include "nsContentUtils.h"
#include "nsFrameSelection.h"
#include "nsIContentInlines.h"
#include "nsRange.h"

namespace mozilla {

using namespace dom;

SelectionChangeEventDispatcher::RawRangeData::RawRangeData(
    const nsRange* aRange) {
  if (aRange->IsPositioned()) {
    mStartContainer = aRange->GetStartContainer();
    mEndContainer = aRange->GetEndContainer();
    mStartOffset = aRange->StartOffset();
    mEndOffset = aRange->EndOffset();
  } else {
    mStartContainer = nullptr;
    mEndContainer = nullptr;
    mStartOffset = 0;
    mEndOffset = 0;
  }
}

bool SelectionChangeEventDispatcher::RawRangeData::Equals(
    const nsRange* aRange) {
  if (!aRange->IsPositioned()) {
    return !mStartContainer;
  }
  return mStartContainer == aRange->GetStartContainer() &&
         mEndContainer == aRange->GetEndContainer() &&
         mStartOffset == aRange->StartOffset() &&
         mEndOffset == aRange->EndOffset();
}

inline void ImplCycleCollectionTraverse(
    nsCycleCollectionTraversalCallback& aCallback,
    SelectionChangeEventDispatcher::RawRangeData& aField, const char* aName,
    uint32_t aFlags = 0) {
  ImplCycleCollectionTraverse(aCallback, aField.mStartContainer,
                              "mStartContainer", aFlags);
  ImplCycleCollectionTraverse(aCallback, aField.mEndContainer, "mEndContainer",
                              aFlags);
}

NS_IMPL_CYCLE_COLLECTION_CLASS(SelectionChangeEventDispatcher)

NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(SelectionChangeEventDispatcher)
  tmp->mOldRanges.Clear();
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(SelectionChangeEventDispatcher)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOldRanges);
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

void SelectionChangeEventDispatcher::OnSelectionChange(Document* aDoc,
                                                       Selection* aSel,
                                                       int16_t aReason) {
  // Check if the ranges have actually changed
  // Don't bother checking this if we are hiding changes.
  if (mOldRanges.Length() == aSel->RangeCount() &&
      !aSel->IsBlockingSelectionChangeEvents()) {
    bool changed = mOldDirection != aSel->GetDirection();
    if (!changed) {
      for (const uint32_t i : IntegerRange(mOldRanges.Length())) {
        if (!mOldRanges[i].Equals(aSel->GetRangeAt(i))) {
          changed = true;
          break;
        }
      }
    }

    // Even if the raw ranges have not changed, it is possible that there
    // has been some change to the DOM which moved the live ranges given
    // at the time of the last selectionchange. So we still fire
    // selectionchange if the nsRange mutation observer caused a selection
    // range to be updated.
    if (!changed && !mSelectionRangeObservedMutation) {
      return;
    }
  }

  // The ranges have actually changed, update the mOldRanges array
  mOldRanges.ClearAndRetainStorage();
  for (const uint32_t i : IntegerRange(aSel->RangeCount())) {
    mOldRanges.AppendElement(RawRangeData(aSel->GetRangeAt(i)));
  }
  mOldDirection = aSel->GetDirection();
  mSelectionRangeObservedMutation = false;

  // If we are hiding changes, then don't do anything else. We do this after we
  // update mOldRanges so that changes after the changes stop being hidden don't
  // incorrectly trigger a change, even though they didn't change anything
  if (aSel->IsBlockingSelectionChangeEvents()) {
    return;
  }

  const Document* doc = aSel->GetParentObject();
  if (MOZ_UNLIKELY(!doc)) {
    return;
  }
  const nsPIDOMWindowInner* inner = doc->GetInnerWindow();
  if (MOZ_UNLIKELY(!inner)) {
    return;
  }
  const bool maybeHasSelectionChangeEventListeners =
      !inner || inner->HasSelectionChangeEventListeners();
  const bool maybeHasFormSelectEventListeners =
      !inner || inner->HasFormSelectEventListeners();
  if (!maybeHasSelectionChangeEventListeners &&
      !maybeHasFormSelectEventListeners) {
    return;
  }

  // Be aware, don't call GetTextControlFromSelectionLimiter once you might
  // run script because selection limit may have already been changed by it.
  const RefPtr<Element> textControlElement = [&]() -> Element* {
    if (!(maybeHasFormSelectEventListeners &&
          (aReason & nsISelectionListener::JS_REASON)) &&
        !maybeHasSelectionChangeEventListeners) {
      return nullptr;
    }
    const nsFrameSelection* fs = aSel->GetFrameSelection();
    if (!fs || !fs->IsIndependentSelection()) {
      return nullptr;
    }
    Element* textControl = fs->GetIndependentSelectionRootParentElement();
    MOZ_ASSERT_IF(textControl, textControl->IsTextControlElement());
    MOZ_ASSERT_IF(textControl, !textControl->IsInNativeAnonymousSubtree());
    return textControl;
  }();

  // Selection changes with non-JS reason only cares about whether the new
  // selection is collapsed or not. See TextInputListener::OnSelectionChange.
  if (textControlElement && maybeHasFormSelectEventListeners &&
      (aReason & nsISelectionListener::JS_REASON)) {
    RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher(
        textControlElement, eFormSelect, CanBubble::eYes);
    asyncDispatcher->PostDOMEvent();
  }

  if (!maybeHasSelectionChangeEventListeners) {
    return;
  }

  // The spec currently doesn't say that we should dispatch this event on text
  // controls, so for now we only support doing that under a pref, disabled by
  // default.
  // See https://github.com/w3c/selection-api/issues/53.
  if (textControlElement &&
      !StaticPrefs::dom_select_events_textcontrols_selectionchange_enabled()) {
    return;
  }

  nsINode* target = textControlElement
                        ? static_cast<nsINode*>(textControlElement.get())
                        : aDoc;
  if (!target) {
    return;
  }

  if (target->HasScheduledSelectionChangeEvent()) {
    return;
  }

  target->SetHasScheduledSelectionChangeEvent();

  CanBubble canBubble = textControlElement ? CanBubble::eYes : CanBubble::eNo;
  RefPtr<AsyncEventDispatcher> asyncDispatcher =
      new AsyncSelectionChangeEventDispatcher(target, eSelectionChange,
                                              canBubble);
  asyncDispatcher->PostDOMEvent();
}

}  // namespace mozilla
