/* 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 "Highlight.h"

#include "AbstractRange.h"
#include "Document.h"
#include "HighlightRegistry.h"
#include "Selection.h"
#include "mozilla/AlreadyAddRefed.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/RefPtr.h"
#include "mozilla/StaticAnalysisFunctions.h"
#include "mozilla/dom/HighlightBinding.h"
#include "nsFrameSelection.h"
#include "nsIContentInlines.h"
#include "nsLayoutUtils.h"
#include "nsPIDOMWindow.h"
#include "nsPresContext.h"

namespace mozilla::dom {

NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Highlight, mRanges, mWindow)

NS_IMPL_CYCLE_COLLECTING_ADDREF(Highlight)
NS_IMPL_CYCLE_COLLECTING_RELEASE(Highlight)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Highlight)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

Highlight::Highlight(
    const Sequence<OwningNonNull<AbstractRange>>& aInitialRanges,
    nsPIDOMWindowInner* aWindow, ErrorResult& aRv)
    : mWindow(aWindow) {
  for (RefPtr<AbstractRange> range : aInitialRanges) {
    Add(*range, aRv);
    if (aRv.Failed()) {
      return;
    }
  }
}

already_AddRefed<Highlight> Highlight::Constructor(
    const GlobalObject& aGlobal,
    const Sequence<OwningNonNull<AbstractRange>>& aInitialRanges,
    ErrorResult& aRv) {
  MOZ_ASSERT(NS_IsMainThread());
  nsCOMPtr<nsPIDOMWindowInner> window =
      do_QueryInterface(aGlobal.GetAsSupports());
  if (!window) {
    aRv.ThrowUnknownError(
        "There is no window associated to "
        "this highlight object!");
    return nullptr;
  }

  RefPtr<Highlight> highlight = new Highlight(aInitialRanges, window, aRv);
  return aRv.Failed() ? nullptr : highlight.forget();
}

void Highlight::AddToHighlightRegistry(HighlightRegistry& aHighlightRegistry,
                                       nsAtom& aHighlightName) {
  mHighlightRegistries.LookupOrInsert(&aHighlightRegistry)
      .Insert(&aHighlightName);
}

void Highlight::RemoveFromHighlightRegistry(
    HighlightRegistry& aHighlightRegistry, nsAtom& aHighlightName) {
  if (auto entry = mHighlightRegistries.Lookup(&aHighlightRegistry)) {
    auto& highlightNames = entry.Data();
    highlightNames.Remove(&aHighlightName);
    if (highlightNames.IsEmpty()) {
      entry.Remove();
    }
  }
}

void Highlight::Repaint() {
  for (const RefPtr<HighlightRegistry>& registry :
       mHighlightRegistries.Keys()) {
    registry->RepaintHighlightSelection(*this);
  }
}

void Highlight::SetPriority(int32_t aPriority) {
  if (mPriority == aPriority) {
    return;
  }
  mPriority = aPriority;
  Repaint();
}

void Highlight::SetType(HighlightType aHighlightType) {
  if (mHighlightType == aHighlightType) {
    return;
  }
  mHighlightType = aHighlightType;
  Repaint();
}

Highlight* Highlight::Add(AbstractRange& aRange, ErrorResult& aRv) {
  // Manually check if the range `aKey` is already present in this highlight,
  // because `SetlikeHelpers::Add()` doesn't indicate this.
  // To keep the setlike and the mirrored array in sync, the range must not
  // be added to `mRanges` if it was already present.
  // `SetlikeHelpers::Has()` is much faster in checking this than
  // `nsTArray<>::Contains()`.
  if (Highlight_Binding::SetlikeHelpers::Has(this, aRange, aRv) ||
      aRv.Failed()) {
    return this;
  }
  Highlight_Binding::SetlikeHelpers::Add(this, aRange, aRv);
  if (aRv.Failed()) {
    return this;
  }

  MOZ_ASSERT(!mRanges.Contains(&aRange),
             "setlike and DOM mirror are not in sync");

  mRanges.AppendElement(&aRange);
  AutoFrameSelectionBatcher selectionBatcher(__FUNCTION__,
                                             mHighlightRegistries.Count());
  for (const RefPtr<HighlightRegistry>& registry :
       mHighlightRegistries.Keys()) {
    auto frameSelection = registry->GetFrameSelection();
    selectionBatcher.AddFrameSelection(frameSelection);
    // since this is run in a context guarded by a selection batcher,
    // no strong reference is needed to keep `registry` alive.
    MOZ_KnownLive(registry)->MaybeAddRangeToHighlightSelection(aRange, *this);
    if (aRv.Failed()) {
      return this;
    }
  }
  return this;
}

void Highlight::Clear(ErrorResult& aRv) {
  Highlight_Binding::SetlikeHelpers::Clear(this, aRv);
  if (!aRv.Failed()) {
    mRanges.Clear();
    AutoFrameSelectionBatcher selectionBatcher(__FUNCTION__,
                                               mHighlightRegistries.Count());

    for (const RefPtr<HighlightRegistry>& registry :
         mHighlightRegistries.Keys()) {
      auto frameSelection = registry->GetFrameSelection();
      selectionBatcher.AddFrameSelection(frameSelection);
      // since this is run in a context guarded by a selection batcher,
      // no strong reference is needed to keep `registry` alive.
      MOZ_KnownLive(registry)->RemoveHighlightSelection(*this);
    }
  }
}

bool Highlight::Delete(AbstractRange& aRange, ErrorResult& aRv) {
  if (Highlight_Binding::SetlikeHelpers::Delete(this, aRange, aRv)) {
    mRanges.RemoveElement(&aRange);
    AutoFrameSelectionBatcher selectionBatcher(__FUNCTION__,
                                               mHighlightRegistries.Count());

    for (const RefPtr<HighlightRegistry>& registry :
         mHighlightRegistries.Keys()) {
      auto frameSelection = registry->GetFrameSelection();
      selectionBatcher.AddFrameSelection(frameSelection);
      // since this is run in a context guarded by a selection batcher,
      // no strong reference is needed to keep `registry` alive.
      MOZ_KnownLive(registry)->MaybeRemoveRangeFromHighlightSelection(aRange,
                                                                      *this);
    }
    return true;
  }
  return false;
}

// Callback that sets mHit if any rect covers (xAppUnits, yAppUnits).
struct PointHitCallback : public RectCallback {
  const nscoord mX, mY;
  bool mHit = false;
  PointHitCallback(nscoord aX, nscoord aY) : mX(aX), mY(aY) {}
  void AddRect(const nsRect& aRect) override {
    if (!mHit) {
      mHit = aRect.Contains(mX, mY);
    }
  }
};

// https://drafts.csswg.org/css-highlight-api-1/#dom-highlightregistry-highlightsfrompoint
nsTArray<RefPtr<AbstractRange>> Highlight::RangesAtPoint(
    float aX, float aY,
    const Sequence<OwningNonNull<mozilla::dom::ShadowRoot>>& aShadowRoots,
    Element* aElementAtPoint) const {
  if (!aElementAtPoint) {
    return {};
  }

  AutoTArray<RefPtr<AbstractRange>, 4> rangesAtPoint;

  // Convert once; all client rects from CollectClientRects() are in app units.
  const nscoord xAppUnits = nsPresContext::CSSPixelsToAppUnits(aX);
  const nscoord yAppUnits = nsPresContext::CSSPixelsToAppUnits(aY);

  // 3.2. For each AbstractRange abstractRange in highlight:
  ShadowRoot* const containingShadowAtPoint =
      aElementAtPoint->GetContainingShadowForSelection();
  ShadowRoot* const shadowRootCoveredByElementAtPoint = [&]() {
    // If aElementAtPoint is an inclusive descendant of an assigned node of a
    // <slot>, the range in an assigned node hides the shadow at the point.
    // Then, we should not append ranges that appear in the shadow.
    ShadowRoot* const closestShadowRootInFlattenedTree =
        aElementAtPoint->GetClosestShadowRootInFlattenedTreeForSelection();
    return containingShadowAtPoint != closestShadowRootInFlattenedTree
               ? closestShadowRootInFlattenedTree
               : nullptr;
  }();
  for (const auto& range : mRanges) {
    // 3.2.1. If abstractRange is an invalid StaticRange, then continue.
    if (range->IsStaticRange() && !range->AsStaticRange()->IsValid()) {
      continue;
    }
    if (!range->IsPositioned()) {
      continue;
    }

    // The spec leaves hit-testing details out of scope. As an implementation
    // choice we filter ranges by shadow tree membership relative to the point:
    //  - If the point is in shadow tree T (aPointShadowRoot), only include
    //    ranges whose closestCommonAncestorContainer is also in T. Highlights
    //    rooted in the light DOM don’t paint over shadow DOM content, even if
    //    their client rects happen to cover the shadow host’s layout box.
    //  - If the point is in the light DOM, skip ranges whose
    //    closestCommonAncestorContainer is in a shadow tree not listed in
    //    options.shadowRoots.
    const nsINode* const closestCommonAncestor =
        range->GetClosestCommonInclusiveAncestor();
    if (!closestCommonAncestor) {
      continue;
    }
    const ShadowRoot* const containingShadow =
        closestCommonAncestor->GetContainingShadow();
    if (containingShadowAtPoint) {
      if (containingShadow != containingShadowAtPoint) {
        // The range is not selecting content in the shadow root.
        continue;
      }
    } else if (closestCommonAncestor->IsInShadowTree() &&
               !aShadowRoots.Contains(containingShadow)) {
      // The range is in a shadow, but the shadow root is not listed in
      // options.shadowRoots.
      continue;
    }

    // The range in the shadow should not be appended when the element at the
    // point is an assigned slot of a <slot> in the shadow since the element
    // covers the shadow and is not contained by the shadow.
    if (shadowRootCoveredByElementAtPoint &&
        containingShadow == shadowRootCoveredByElementAtPoint) {
      continue;
    }

    // 3.2.2. Let range be a new Range whose start/end nodes and offsets are
    //        set to abstractRange’s.
    // Omitted: getClientRects() is implemented directly on AbstractRange, so no
    // temporary Range object is needed.
    // 3.2.3. If the coordinates x,y fall inside at least one of the DOMRects
    //        returned by calling getClientRects() on range, then append
    //        abstractRange to result.ranges.
    // Note: Layout was already flushed at the callsite.
    PointHitCallback hitTest(xAppUnits, yAppUnits);
    range->CollectClientRects(hitTest, /* aClampToEdge */ true);
    if (hitTest.mHit) {
      rangesAtPoint.AppendElement(range);
    }
  }
  return std::move(rangesAtPoint);
}

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

}  // namespace mozilla::dom
