// Copyright 2025 Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/dom/css_pseudo_element.h"

#include "third_party/blink/renderer/bindings/core/v8/v8_box_quad_options.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_convert_coordinate_options.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_dom_quad_init.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_union_csspseudoelement_document_element_text.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_union_csspseudoelement_element.h"
#include "third_party/blink/renderer/core/css/parser/css_parser_context.h"
#include "third_party/blink/renderer/core/css/parser/css_selector_parser.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/geometry_utils.h"
#include "third_party/blink/renderer/core/execution_context/security_context.h"
#include "third_party/blink/renderer/core/geometry/dom_point.h"
#include "third_party/blink/renderer/core/geometry/dom_quad.h"
#include "third_party/blink/renderer/core/geometry/dom_rect_read_only.h"
#include "third_party/blink/renderer/core/layout/layout_object.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"

namespace blink {

bool CSSPseudoElement::IsSupportedTypeForCSSPseudoElement(PseudoId pseudo_id) {
  switch (pseudo_id) {
    case kPseudoIdBefore:
    case kPseudoIdAfter:
    case kPseudoIdMarker:
    case kPseudoIdScrollMarker:
      return true;
    case kPseudoIdBackdrop:
      return RuntimeEnabledFeatures::CSSPseudoElementBackdropEnabled();
    case kPseudoIdViewTransition:
    case kPseudoIdViewTransitionGroup:
    case kPseudoIdViewTransitionImagePair:
    case kPseudoIdViewTransitionOld:
    case kPseudoIdViewTransitionNew:
      return RuntimeEnabledFeatures::CSSPseudoElementViewTransitionsEnabled();
    default:
      return false;
  }
}

namespace {

PseudoId GetViewTransitionPseudoParentId(PseudoId pseudo_id) {
  switch (pseudo_id) {
    case kPseudoIdViewTransitionGroup:
      return kPseudoIdViewTransition;
    case kPseudoIdViewTransitionImagePair:
      return kPseudoIdViewTransitionGroup;
    case kPseudoIdViewTransitionOld:
    case kPseudoIdViewTransitionNew:
      return kPseudoIdViewTransitionImagePair;
    default:
      return kPseudoIdNone;
  }
}

}  // namespace

// static
std::pair<PseudoId, AtomicString> CSSPseudoElement::GetViewTransitionParent(
    PseudoId pseudo_id,
    const AtomicString& pseudo_argument) {
  PseudoId parent_id = GetViewTransitionPseudoParentId(pseudo_id);
  if (parent_id == kPseudoIdNone) {
    return {kPseudoIdNone, g_null_atom};
  }
  return {parent_id, (parent_id == kPseudoIdViewTransition) ? g_null_atom
                                                            : pseudo_argument};
}

CSSPseudoElement::CSSPseudoElement(Element& originating_element,
                                   PseudoId pseudo_id,
                                   const AtomicString& pseudo_argument)
    : pseudo_id_(pseudo_id),
      pseudo_argument_(pseudo_argument),
      element_(originating_element),
      parent_(MakeGarbageCollected<V8UnionCSSPseudoElementOrElement>(
          &originating_element)) {}

CSSPseudoElement::CSSPseudoElement(CSSPseudoElement& originating_pseudo_element,
                                   PseudoId pseudo_id,
                                   const AtomicString& pseudo_argument)
    : pseudo_id_(pseudo_id),
      pseudo_argument_(pseudo_argument),
      element_(originating_pseudo_element.element_),
      parent_(MakeGarbageCollected<V8UnionCSSPseudoElementOrElement>(
          &originating_pseudo_element)) {}

String CSSPseudoElement::type() const {
  return PseudoElementTagName(pseudo_id_).ToString();
}

std::pair<PseudoId, AtomicString>
CSSPseudoElement::ConvertTypeToSupportedPseudoId(const AtomicString& type) {
  HeapVector<CSSSelector> arena;
  CSSParserTokenStream stream(type);
  base::span<CSSSelector> vector = CSSSelectorParser::ParseSelector(
      stream,
      MakeGarbageCollected<CSSParserContext>(
          kHTMLStandardMode, SecureContextMode::kInsecureContext),
      CSSNestingType::kNone, /*parent_rule_for_nesting=*/nullptr,
      /*semicolon_aborts_nested_selector=*/false, nullptr, arena);
  if (vector.size() != 1) {
    return {kPseudoIdInvalid, g_null_atom};
  }
  const CSSSelector& selector = vector.front();
  PseudoId pseudo_id = CSSSelector::GetPseudoId(selector.GetPseudoType());

  AtomicString argument;
  if (IsTransitionPseudoElement(pseudo_id) &&
      pseudo_id != kPseudoIdViewTransition) {
    if (selector.IdentList().size() != 1 ||
        selector.IdentList()[0] == CSSSelector::UniversalSelectorAtom()) {
      return {kPseudoIdInvalid, g_null_atom};
    }
    argument = selector.IdentList()[0];
  } else {
    argument = selector.Argument();
  }

  return {pseudo_id, argument};
}

// static
CSSPseudoElement* CSSPseudoElement::From(PseudoElement* pseudo_element) {
  // Return nullptr for null or disconnected pseudo-elements. A disconnected
  // pseudo cannot navigate to its originating element.
  if (!pseudo_element || !pseudo_element->isConnected()) {
    return nullptr;
  }
  // Build the pseudo-id chain from innermost to outermost by walking
  // parentElement(). e.g. for ::after::marker: [kPseudoIdMarker,
  // kPseudoIdAfter]
  HeapVector<PseudoId> chain;
  for (auto* p = pseudo_element; p;
       p = DynamicTo<PseudoElement>(p->parentElement())) {
    if (!p->isConnected() ||
        !IsSupportedTypeForCSSPseudoElement(p->GetPseudoId())) {
      return nullptr;
    }
    chain.push_back(p->GetPseudoId());
  }
  // Start from the outermost pseudo on the originating element.
  CSSPseudoElement* css_pseudo =
      pseudo_element->UltimateOriginatingElement().EnsureCSSPseudoElement(
          chain.back());
  // Walk inward through each nested level using PseudoId directly.
  if (chain.size() > 1) {
    for (wtf_size_t i = chain.size() - 1; i; --i) {
      css_pseudo = css_pseudo->pseudo(chain[i - 1]);
      if (!css_pseudo) {
        return nullptr;
      }
    }
  }
  return css_pseudo;
}

CSSPseudoElement* CSSPseudoElement::pseudo(
    PseudoId pseudo_id,
    const AtomicString& pseudo_argument) {
  if (!IsSupportedTypeForCSSPseudoElement(pseudo_id)) {
    return nullptr;
  }

  // View transition pseudo-elements enforce a strict hierarchy.
  // A CSSPseudoElement proxy can only create or return its direct children.
  // For example, `::view-transition` can create `::view-transition-group`,
  // but it cannot create `::view-transition-image-pair` directly.
  if (IsTransitionPseudoElement(pseudo_id)) {
    auto [expected_parent_id, expected_parent_arg] =
        GetViewTransitionParent(pseudo_id, pseudo_argument);

    if (pseudo_id_ != expected_parent_id ||
        pseudo_argument_ != expected_parent_arg) {
      return nullptr;
    }
  }

  if (!css_pseudo_elements_data_) {
    css_pseudo_elements_data_ =
        MakeGarbageCollected<CSSPseudoElementsCacheData>();
  }
  if (CSSPseudoElement* existing =
          css_pseudo_elements_data_->GetCSSPseudoElement(pseudo_id,
                                                         pseudo_argument)) {
    return existing;
  }
  auto* css_pseudo_element =
      MakeGarbageCollected<CSSPseudoElement>(*this, pseudo_id, pseudo_argument);
  css_pseudo_elements_data_->CacheCSSPseudoElement(pseudo_id, pseudo_argument,
                                                   *css_pseudo_element);
  return css_pseudo_element;
}

CSSPseudoElement* CSSPseudoElement::pseudo(const AtomicString& type) {
  auto [pseudo_id, pseudo_argument] = ConvertTypeToSupportedPseudoId(type);
  return pseudo(pseudo_id, pseudo_argument);
}

namespace {

// Helper to get the PseudoElement from the originating element hierarchy.
PseudoElement* GetPseudoElementForCSSPseudoElement(
    const V8UnionCSSPseudoElementOrElement* parent,
    PseudoId pseudo_id,
    const AtomicString& pseudo_argument) {
  CHECK(parent);

  // Walk up the chain from the current parent to the ultimate originating
  // Element, collecting pseudo-ids along the way (immediate parent first).
  Vector<PseudoId> pseudo_chain;
  const V8UnionCSSPseudoElementOrElement* current_parent = parent;
  while (current_parent && current_parent->IsCSSPseudoElement()) {
    CSSPseudoElement* parent_css_pseudo =
        current_parent->GetAsCSSPseudoElement();
    pseudo_chain.push_back(parent_css_pseudo->GetPseudoId());
    current_parent = parent_css_pseudo->parent();
  }

  // current_parent should now be the originating Element.
  if (!current_parent || !current_parent->IsElement()) {
    return nullptr;
  }
  Element* base_element = current_parent->GetAsElement();

  // Although the CSSPseudoElement proxy for view transitions reflects a
  // hierarchical tree structure (e.g. ::view-transition-group's parent is
  // ::view-transition), the underlying PseudoElement instances are accessed
  // directly from the originating Element using GetStyledPseudoElement,
  // rather than by traversing down a nested chain of PseudoElements.
  if (IsTransitionPseudoElement(pseudo_id)) {
    return DynamicTo<PseudoElement>(
        base_element->GetStyledPseudoElement(pseudo_id, pseudo_argument));
  }

  // CSSPseudoElement is a proxy representation of PseudoElement. To resolve a
  // nested PseudoElement from a CSSPseudoElement received from JS, we first
  // walk up the chain of CSSPseudoElements to the ultimate originating Element,
  // collecting the pseudo-ids along the way. Then we walk back down from the
  // Element, using those pseudo-ids to build the actual PseudoElement chain.
  // This is necessary because PseudoElements can only be accessed via
  // (Element/PseudoElement).GetPseudoElement(pseudo_id), starting from a real
  // Element.
  // Walk down the pseudo-elements chains, starting from the ultimate
  // originating element, using the collected pseudo-ids (in reverse order) to
  // resolve nested pseudo-elements.
  // E.g. for ::before::marker, first get ::before from the element, then (after
  // cycle) get ::marker from that ::before pseudo-element.
  Element* current_owner = base_element;
  for (wtf_size_t i = pseudo_chain.size(); i > 0; --i) {
    PseudoId id = pseudo_chain[i - 1];
    PseudoElement* next_pseudo = current_owner->GetPseudoElement(id);
    if (!next_pseudo) {
      return nullptr;
    }
    current_owner = next_pseudo;
  }

  return current_owner->GetPseudoElement(pseudo_id, pseudo_argument);
}

}  // namespace

LayoutObject* CSSPseudoElement::GetLayoutObject() const {
  CHECK(element_);
  PseudoElement* pseudo_element = GetPseudoElementForCSSPseudoElement(
      parent_, pseudo_id_, pseudo_argument_);
  if (!pseudo_element) {
    return nullptr;
  }
  return pseudo_element->GetLayoutObject();
}

PseudoElement* CSSPseudoElement::GetPseudoElement() const {
  return GetPseudoElementForCSSPseudoElement(parent_, pseudo_id_,
                                              pseudo_argument_);
}

HeapVector<Member<DOMQuad>> CSSPseudoElement::getBoxQuads(
    const BoxQuadOptions* options,
    ExceptionState& exception_state) const {
  CHECK(RuntimeEnabledFeatures::GeometryUtilsForCSSPseudoElementEnabled());
  return geometry_utils::GetBoxQuads(element_, this, options, exception_state);
}

DOMQuad* CSSPseudoElement::convertQuadFromNode(
    DOMQuadInit* quad,
    const V8UnionCSSPseudoElementOrDocumentOrElementOrText* from,
    const ConvertCoordinateOptions* options,
    ExceptionState& exception_state) const {
  CHECK(RuntimeEnabledFeatures::GeometryUtilsForCSSPseudoElementEnabled());
  return geometry_utils::ConvertQuadFromNode(quad, element_, this, from,
                                             options, exception_state);
}

DOMQuad* CSSPseudoElement::convertRectFromNode(
    DOMRectReadOnly* rect,
    const V8UnionCSSPseudoElementOrDocumentOrElementOrText* from,
    const ConvertCoordinateOptions* options,
    ExceptionState& exception_state) const {
  CHECK(RuntimeEnabledFeatures::GeometryUtilsForCSSPseudoElementEnabled());
  return geometry_utils::ConvertRectFromNode(rect, element_, this, from,
                                             options, exception_state);
}

DOMPoint* CSSPseudoElement::convertPointFromNode(
    DOMPointInit* point,
    const V8UnionCSSPseudoElementOrDocumentOrElementOrText* from,
    const ConvertCoordinateOptions* options,
    ExceptionState& exception_state) const {
  CHECK(RuntimeEnabledFeatures::GeometryUtilsForCSSPseudoElementEnabled());
  return geometry_utils::ConvertPointFromNode(point, element_, this, from,
                                              options, exception_state);
}

void CSSPseudoElement::Trace(Visitor* v) const {
  v->Trace(element_);
  v->Trace(parent_);
  v->Trace(css_pseudo_elements_data_);
  ScriptWrappable::Trace(v);
}

void CSSPseudoElementsCacheData::CacheCSSPseudoElement(
    PseudoId pseudo_id,
    const AtomicString& pseudo_argument,
    CSSPseudoElement& pseudo_element) {
  PseudoElementCacheKey key(pseudo_id, pseudo_argument);
  // insert() keeps any existing entry, so the first cached pseudo-element for
  // a key stays cached.
  pseudo_elements_map_.insert(key, &pseudo_element);
}

CSSPseudoElement* CSSPseudoElementsCacheData::GetCSSPseudoElement(
    PseudoId pseudo_id,
    const AtomicString& pseudo_argument) {
  PseudoElementCacheKey key(pseudo_id, pseudo_argument);
  auto it = pseudo_elements_map_.find(key);
  if (it == pseudo_elements_map_.end()) {
    return nullptr;
  }
  return it->value;
}

void CSSPseudoElementsCacheData::Trace(Visitor* v) const {
  v->Trace(pseudo_elements_map_);
  NodeRareDataField::Trace(v);
}

}  // namespace blink
