/*
 * Copyright (c) 2026, Andreas Kling <andreas@ladybird.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <LibWeb/CSS/CSSKeyframesRule.h>
#include <LibWeb/CSS/CSSStyleRule.h>
#include <LibWeb/CSS/CSSStyleSheet.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/Invalidation/InvalidationSetMatcher.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/SelectorEngine.h>
#include <LibWeb/CSS/StyleScope.h>
#include <LibWeb/CSS/StyleSheetInvalidation.h>
#include <LibWeb/CSS/StyleValues/CustomIdentStyleValue.h>
#include <LibWeb/CSS/StyleValues/StringStyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/HTML/HTMLSlotElement.h>
#include <LibWeb/HTML/HTMLTableCellElement.h>
#include <LibWeb/HTML/HTMLTableElement.h>

namespace Web::CSS {

bool selector_may_match_light_dom_under_shadow_host(Selector const& selector)
{
    if (selector.is_slotted())
        return true;

    if (!selector.contains_pseudo_class(PseudoClass::Host))
        return false;

    // A bare :host selector only targets the host itself, and pseudo-element transitions such as :host::part() stay
    // anchored on that same host. We only escape to light-DOM once the selector keeps walking through a real DOM
    // combinator after matching :host.
    for (size_t i = 1; i < selector.compound_selectors().size(); ++i) {
        if (selector.compound_selectors()[i].combinator != Selector::Combinator::PseudoElement)
            return true;
    }
    return false;
}

static bool selector_may_match_shadow_host(Selector const&);
static bool selector_may_match_light_dom_outside_shadow_host_via_positive_selector_list(Selector const&);

static bool pseudo_class_has_positive_selector_list_arguments(PseudoClass pseudo_class)
{
    return first_is_one_of(pseudo_class, PseudoClass::Is, PseudoClass::Where);
}

static bool simple_selector_may_match_shadow_host(Selector::SimpleSelector const& simple_selector)
{
    if (simple_selector.type != Selector::SimpleSelector::Type::PseudoClass)
        return false;

    auto const& pseudo_class = simple_selector.pseudo_class();
    if (pseudo_class.type == PseudoClass::Host)
        return true;

    if (!pseudo_class_has_positive_selector_list_arguments(pseudo_class.type))
        return false;

    return any_of(pseudo_class.argument_selector_list, selector_may_match_shadow_host);
}

static bool selector_may_match_shadow_host(Selector const& selector)
{
    return any_of(selector.compound_selectors(), [](auto const& compound_selector) {
        return any_of(compound_selector.simple_selectors, [](auto const& simple_selector) {
            return simple_selector_may_match_shadow_host(simple_selector);
        });
    });
}

static bool compound_selector_may_match_shadow_host(Selector::CompoundSelector const& compound_selector)
{
    return any_of(compound_selector.simple_selectors, simple_selector_may_match_shadow_host);
}

static bool simple_selector_may_match_light_dom_outside_shadow_host(Selector::SimpleSelector const& simple_selector)
{
    if (simple_selector.type != Selector::SimpleSelector::Type::PseudoClass)
        return false;

    auto const& pseudo_class = simple_selector.pseudo_class();
    if (!pseudo_class_has_positive_selector_list_arguments(pseudo_class.type))
        return false;

    return any_of(pseudo_class.argument_selector_list, selector_may_match_light_dom_outside_shadow_host_via_positive_selector_list);
}

static bool compound_selector_may_match_light_dom_outside_shadow_host(Selector::CompoundSelector const& compound_selector)
{
    return any_of(compound_selector.simple_selectors, simple_selector_may_match_light_dom_outside_shadow_host);
}

static bool selector_may_match_light_dom_outside_shadow_host_via_positive_selector_list(Selector const& selector)
{
    auto const& compound_selectors = selector.compound_selectors();
    for (size_t i = 0; i < compound_selectors.size(); ++i) {
        if (compound_selector_may_match_light_dom_outside_shadow_host(compound_selectors[i]))
            return true;

        if (!compound_selector_may_match_shadow_host(compound_selectors[i]))
            continue;

        for (size_t j = i + 1; j < compound_selectors.size(); ++j) {
            if (first_is_one_of(compound_selectors[j].combinator, Selector::Combinator::NextSibling, Selector::Combinator::SubsequentSibling))
                return true;
        }
    }
    return false;
}

bool selector_may_match_light_dom_outside_shadow_host(Selector const& selector)
{
    return selector_may_match_light_dom_outside_shadow_host_via_positive_selector_list(selector);
}

bool selector_may_match_light_dom_under_shadow_host(StringView selector_text)
{
    CSS::Parser::ParsingParams parsing_params;
    auto selectors = parse_selector(parsing_params, selector_text);
    if (!selectors.has_value() || selectors->size() != 1)
        return false;
    return selector_may_match_light_dom_under_shadow_host(selectors->first());
}

bool selector_may_match_light_dom_outside_shadow_host(StringView selector_text)
{
    CSS::Parser::ParsingParams parsing_params;
    auto selectors = parse_selector(parsing_params, selector_text);
    if (!selectors.has_value() || selectors->size() != 1)
        return false;
    return selector_may_match_light_dom_outside_shadow_host(selectors->first());
}

static bool is_universal_only_compound(Selector::CompoundSelector const& compound_selector)
{
    if (compound_selector.simple_selectors.is_empty())
        return false;
    for (auto const& simple_selector : compound_selector.simple_selectors) {
        if (simple_selector.type != Selector::SimpleSelector::Type::Universal)
            return false;
    }
    return true;
}

static bool is_pseudo_element_targeting_compound(Selector::CompoundSelector const& compound_selector)
{
    return !compound_selector.simple_selectors.is_empty()
        && compound_selector.simple_selectors.first().type == Selector::SimpleSelector::Type::PseudoElement;
}

// :has() in the rightmost set filters by an element's previously recorded "affected by :has()" flags. Those flags
// are not yet set for a brand-new rule that has never been matched, so :has() on its own can't narrow the walk.
// Treat a rightmost set whose only targetable feature is :has() as if it had no targetable features, so we fall
// back to whole-subtree invalidation.
static bool rightmost_set_has_targetable_features(InvalidationSet const& rightmost_set)
{
    bool found_targetable = false;
    rightmost_set.for_each_property([&](InvalidationSet::Property const& property) {
        if (property.type == InvalidationSet::Property::Type::PseudoClass
            && property.value.get<PseudoClass>() == PseudoClass::Has) {
            return IterationDecision::Continue;
        }
        found_targetable = true;
        return IterationDecision::Break;
    });
    return found_targetable;
}

struct AnchorInvalidationRule {
    InvalidationSet anchor_set;
    RefPtr<Selector> anchor_selector;
    GC::Ptr<CSSStyleSheet const> style_sheet_for_rule;
};

enum class AllowSkippingUntargetablePseudoElementCompounds {
    No,
    Yes,
};

// If the leading compounds (everything except the rightmost) of `compound_selectors` produce a usable invalidation
// set, return an anchor rule built from them.
static Optional<AnchorInvalidationRule> try_build_anchor_for_leading_compounds(Vector<Selector::CompoundSelector> const& compound_selectors, StyleInvalidationData& throwaway_data, GC::Ptr<CSSStyleSheet const> style_sheet_for_rule, AllowSkippingUntargetablePseudoElementCompounds allow_skipping_untargetable_pseudo_element_compounds = AllowSkippingUntargetablePseudoElementCompounds::No)
{
    if (compound_selectors.size() < 2)
        return {};

    auto anchor_compound_count = compound_selectors.size() - 1;
    while (anchor_compound_count > 0) {
        auto const& rightmost_anchor = compound_selectors[anchor_compound_count - 1];

        InvalidationSet anchor_set;
        for (auto const& simple : rightmost_anchor.simple_selectors)
            build_invalidation_sets_for_simple_selector(simple, anchor_set, ExcludePropertiesNestedInNotPseudoClass::No, throwaway_data, InsideNthChildPseudoClass::No);

        if (anchor_set.has_properties()) {
            Vector<Selector::CompoundSelector> anchor_compound_selectors;
            anchor_compound_selectors.ensure_capacity(anchor_compound_count);
            for (size_t i = 0; i < anchor_compound_count; ++i)
                anchor_compound_selectors.append(compound_selectors[i]);

            RefPtr<Selector> anchor_selector;
            if (anchor_compound_selectors.size() > 1)
                anchor_selector = Selector::create(move(anchor_compound_selectors));

            return AnchorInvalidationRule { move(anchor_set), move(anchor_selector), style_sheet_for_rule };
        }

        if (allow_skipping_untargetable_pseudo_element_compounds == AllowSkippingUntargetablePseudoElementCompounds::No
            || !is_pseudo_element_targeting_compound(rightmost_anchor)) {
            return {};
        }

        --anchor_compound_count;
    }

    return {};
}

void extend_style_sheet_invalidation_set_with_style_rule(StyleSheetInvalidationSet& result, CSSStyleRule const& style_rule)
{
    auto* style_sheet_for_rule = const_cast<CSSStyleRule&>(style_rule).parent_style_sheet();

    StyleInvalidationData throwaway_data;

    for (auto const& selector : style_rule.absolutized_selectors()) {
        result.may_match_light_dom_under_shadow_host |= selector_may_match_light_dom_under_shadow_host(*selector);
        result.may_match_light_dom_outside_shadow_host |= selector_may_match_light_dom_outside_shadow_host(*selector);
        result.may_match_shadow_host |= selector->contains_pseudo_class(PseudoClass::Host);

        auto const& compound_selectors = selector->compound_selectors();
        if (compound_selectors.is_empty())
            continue;

        auto const& rightmost = compound_selectors.last();

        InvalidationSet rightmost_set;
        for (auto const& simple : rightmost.simple_selectors)
            build_invalidation_sets_for_simple_selector(simple, rightmost_set, ExcludePropertiesNestedInNotPseudoClass::No, throwaway_data, InsideNthChildPseudoClass::No);

        if (rightmost_set_has_targetable_features(rightmost_set)) {
            result.invalidation_set.include_all_from(rightmost_set);
            continue;
        }

        // OPTIMIZATION: When the rightmost compound has no class/tag/id/attr feature but does have a
        //               structural-position pseudo-class (:first-child, :last-child, or :only-child)
        //               as its only "feature", we can target the boundary children instead of falling
        //               back to a whole-subtree invalidation. We restrict this to the case with no
        //               other targetable feature so that selectors like `.foo:first-child` keep
        //               matching only `.foo` elements rather than every first-child in the document.
        bool rightmost_has_only_structural_pseudo = false;
        for (auto const& simple : rightmost.simple_selectors) {
            if (simple.type != Selector::SimpleSelector::Type::PseudoClass)
                continue;
            auto pseudo_class_type = simple.pseudo_class().type;
            if (pseudo_class_type == PseudoClass::FirstChild
                || pseudo_class_type == PseudoClass::LastChild
                || pseudo_class_type == PseudoClass::OnlyChild) {
                rightmost_has_only_structural_pseudo = true;
                break;
            }
        }
        if (rightmost_has_only_structural_pseudo) {
            for (auto const& simple : rightmost.simple_selectors) {
                if (simple.type != Selector::SimpleSelector::Type::PseudoClass)
                    continue;
                auto pseudo_class_type = simple.pseudo_class().type;
                if (pseudo_class_type == PseudoClass::FirstChild
                    || pseudo_class_type == PseudoClass::LastChild
                    || pseudo_class_type == PseudoClass::OnlyChild) {
                    result.invalidation_set.set_needs_invalidate_pseudo_class(pseudo_class_type);
                }
            }
            continue;
        }

        // The rightmost compound has no targetable properties on its own, but we can still avoid a whole-subtree
        // invalidation if the leading compounds carry an anchor invalidation set that we can match against.
        if (is_pseudo_element_targeting_compound(rightmost)) {
            // Pseudo-element normalization can leave untargetable pseudo-element-transition compounds before the
            // final pseudo-element, e.g. `x-host::part(foo):dir(rtl)::before`. Anchor those rules on the nearest
            // targetable leading compound instead of broadening to the whole subtree.
            if (auto anchor = try_build_anchor_for_leading_compounds(compound_selectors, throwaway_data, style_sheet_for_rule, AllowSkippingUntargetablePseudoElementCompounds::Yes); anchor.has_value()) {
                result.pseudo_element_rules.append({
                    .anchor_set = move(anchor->anchor_set),
                    .anchor_selector = move(anchor->anchor_selector),
                    .style_sheet_for_rule = anchor->style_sheet_for_rule,
                });
                continue;
            }
        }

        if (is_universal_only_compound(rightmost)) {
            if (auto anchor = try_build_anchor_for_leading_compounds(compound_selectors, throwaway_data, style_sheet_for_rule); anchor.has_value()) {
                result.trailing_universal_rules.append({
                    .anchor_set = move(anchor->anchor_set),
                    .anchor_selector = move(anchor->anchor_selector),
                    .combinator = rightmost.combinator,
                    .style_sheet_for_rule = anchor->style_sheet_for_rule,
                });
                continue;
            }
        }

        result.invalidation_set.set_needs_invalidate_whole_subtree();
        return;
    }
}

static GC::Ptr<DOM::Element const> shadow_host_for_targeted_shadow_root_invalidation(DOM::Node const& root)
{
    if (auto const* shadow_root = as_if<DOM::ShadowRoot>(root))
        return shadow_root->host();
    return nullptr;
}

template<typename Rule>
static bool element_matches_anchor_rule(DOM::Element const& element, Rule const& rule, GC::Ptr<DOM::Element const> shadow_host, GC::Ptr<DOM::ShadowRoot const> rule_shadow_root)
{
    if (!Invalidation::element_matches_any_invalidation_set_property(element, rule.anchor_set))
        return false;
    if (rule.anchor_selector) {
        SelectorEngine::MatchContext context {
            .style_sheet_for_rule = rule.style_sheet_for_rule,
            .rule_shadow_root = rule_shadow_root,
        };
        if (!SelectorEngine::matches(*rule.anchor_selector, element, shadow_host, context))
            return false;
    }
    return true;
}

static void apply_trailing_universal_combinator(Selector::Combinator combinator, DOM::Element& anchor, DOM::Node& target_root)
{
    switch (combinator) {
    case Selector::Combinator::ImmediateChild:
        target_root.for_each_child_of_type<DOM::Element>([](DOM::Element& child) {
            child.set_needs_style_update(true);
            return IterationDecision::Continue;
        });
        break;
    case Selector::Combinator::Descendant:
        target_root.for_each_in_subtree_of_type<DOM::Element>([](DOM::Element& descendant) {
            descendant.set_needs_style_update(true);
            return TraversalDecision::Continue;
        });
        break;
    case Selector::Combinator::NextSibling:
        if (auto* sibling = anchor.next_element_sibling())
            sibling->set_needs_style_update(true);
        break;
    case Selector::Combinator::SubsequentSibling:
        for (auto* sibling = anchor.next_element_sibling(); sibling; sibling = sibling->next_element_sibling())
            sibling->set_needs_style_update(true);
        break;
    case Selector::Combinator::Column:
        // The column combinator relates a <col>/<colgroup> anchor to the table cells in the represented column. We do
        // not yet have a more precise invalidation primitive for column membership, so keep the work bounded to the
        // current table instead of dropping the invalidation.
        for (auto* ancestor = &anchor; ancestor; ancestor = ancestor->parent_element()) {
            if (auto* table = as_if<HTML::HTMLTableElement>(*ancestor)) {
                table->for_each_in_subtree_of_type<HTML::HTMLTableCellElement>([](HTML::HTMLTableCellElement& cell) {
                    cell.set_needs_style_update(true);
                    return TraversalDecision::Continue;
                });
                break;
            }
        }
        break;
    case Selector::Combinator::None:
        break;
    case Selector::Combinator::PseudoElement:
        break;
    }
}

// Walk `root` once and, for each element, check the primary invalidation set plus every anchor rule in one pass. Doing
// this as a single traversal instead of one walk per rule kind keeps the dirtying cost proportional to the tree size
// rather than to tree_size * rule_count.
static void invalidate_elements_matching_invalidation_set_and_anchor_rules(
    DOM::Node& root,
    StyleSheetInvalidationSet const& result,
    GC::Ptr<DOM::Element const> shadow_host,
    GC::Ptr<DOM::ShadowRoot const> rule_shadow_root)
{
    auto const& invalidation_set = result.invalidation_set;
    bool const has_primary_set = invalidation_set.has_properties();
    bool const has_anchor_rules = !result.pseudo_element_rules.is_empty() || !result.trailing_universal_rules.is_empty();
    if (!has_primary_set && !has_anchor_rules)
        return;

    root.for_each_in_inclusive_subtree_of_type<DOM::Element>([&](DOM::Element& element) {
        if (has_primary_set && !element.needs_style_update()
            && Invalidation::element_matches_any_invalidation_set_property(element, invalidation_set)) {
            element.set_needs_style_update(true);
        }

        if (!element.needs_style_update()) {
            for (auto const& rule : result.pseudo_element_rules) {
                if (element_matches_anchor_rule(element, rule, shadow_host, rule_shadow_root)) {
                    element.set_needs_style_update(true);
                    break;
                }
            }
        }

        // Trailing-universal rules dirty *other* elements (siblings or children of the anchor), so we still have to
        // check them even if `element` is already marked dirty.
        for (auto const& rule : result.trailing_universal_rules) {
            if (element_matches_anchor_rule(element, rule, shadow_host, rule_shadow_root))
                apply_trailing_universal_combinator(rule.combinator, element, element);
        }

        return TraversalDecision::Continue;
    });
}

static bool slot_or_ancestor_needs_style_update(HTML::HTMLSlotElement const& slot, DOM::ShadowRoot const& shadow_root)
{
    for (auto const* node = static_cast<DOM::Node const*>(&slot); node && node != &shadow_root; node = node->parent_node()) {
        if (node->needs_style_update())
            return true;
    }
    return false;
}

// Slotted light-DOM nodes inherit from their assigned <slot>. If a targeted shadow-root invalidation dirties the slot
// or one of its ancestors, we must also dirty the flattened assignees outside the shadow subtree.
void invalidate_assigned_elements_for_dirty_slots(DOM::ShadowRoot& shadow_root)
{
    shadow_root.for_each_in_inclusive_subtree_of_type<HTML::HTMLSlotElement>([&shadow_root](HTML::HTMLSlotElement& slot) {
        if (!slot_or_ancestor_needs_style_update(slot, shadow_root))
            return TraversalDecision::Continue;
        for (auto const& assigned_element : slot.assigned_elements({ .flatten = true }))
            assigned_element->set_needs_style_update(true);
        return TraversalDecision::Continue;
    });
}

void invalidate_root_for_style_sheet_change(DOM::Node& root, StyleSheetInvalidationSet const& result, DOM::StyleInvalidationReason reason, bool force_broad_invalidation)
{
    auto const& invalidation_set = result.invalidation_set;
    if (force_broad_invalidation || invalidation_set.needs_invalidate_whole_subtree()) {
        root.invalidate_style(reason);
        if (auto* shadow_root = as_if<DOM::ShadowRoot>(root)) {
            shadow_root->for_each_in_inclusive_subtree_of_type<HTML::HTMLSlotElement>([](HTML::HTMLSlotElement& slot) {
                slot.set_needs_style_update(true);
                return TraversalDecision::Continue;
            });
            invalidate_assigned_elements_for_dirty_slots(*shadow_root);

            if (auto* host = shadow_root->host()) {
                // Broad shadow-root mutations are only allowed to escape the shadow tree when the stylesheet can
                // actually reach host-side nodes. Keep host-contained escapes bounded to the host subtree; a
                // layer-order-only change, for example, still needs a full restyle inside the shadow tree, but it
                // should not turn into a document-wide invalidation for unrelated light-DOM. Selectors with
                // host-side sibling reach still need a root-side invalidation.
                if (result.may_match_light_dom_outside_shadow_host) {
                    host->root().invalidate_style(reason);
                } else if (result.may_match_light_dom_under_shadow_host) {
                    host->invalidate_style(reason);
                } else if (result.may_match_shadow_host) {
                    host->invalidate_style(reason);
                    shadow_root->set_needs_style_update(true);
                }
            }
        }
        return;
    }

    auto* rule_shadow_root = as_if<DOM::ShadowRoot>(root);
    invalidate_elements_matching_invalidation_set_and_anchor_rules(
        root, result, shadow_host_for_targeted_shadow_root_invalidation(root), rule_shadow_root);

    if (auto* shadow_root = as_if<DOM::ShadowRoot>(root)) {
        invalidate_assigned_elements_for_dirty_slots(*shadow_root);

        if (auto* host = shadow_root->host()) {
            // Slotted and host-contained :host selectors only need to walk the current host's light-DOM subtree.
            // :host selectors with sibling combinators can escape to siblings or other nodes rooted alongside the host,
            // so they still need the broader host-root walk below.
            if (result.may_match_light_dom_outside_shadow_host) {
                invalidate_elements_matching_invalidation_set_and_anchor_rules(host->root(), result, host, shadow_root);
            } else if (result.may_match_light_dom_under_shadow_host) {
                invalidate_elements_matching_invalidation_set_and_anchor_rules(*host, result, host, shadow_root);
            } else if (result.may_match_shadow_host) {
                bool host_or_shadow_tree_needs_style_update = false;
                if (Invalidation::element_matches_any_invalidation_set_property(*host, invalidation_set))
                    host_or_shadow_tree_needs_style_update = true;
                for (auto const& rule : result.pseudo_element_rules) {
                    if (element_matches_anchor_rule(*host, rule, host, shadow_root)) {
                        host_or_shadow_tree_needs_style_update = true;
                        break;
                    }
                }
                if (host_or_shadow_tree_needs_style_update) {
                    host->set_needs_style_update(true);
                    host->invalidate_style(reason);
                    // A targeted :host rule can change inherited values seen by both shadow descendants and the
                    // host's light-DOM subtree even when no selector directly matches there. Mark both subtrees dirty
                    // so recursive style update descends into those inherited-value dependents too.
                    shadow_root->set_needs_style_update(true);
                }
                for (auto const& rule : result.trailing_universal_rules) {
                    // Host-anchored trailing-universal selectors such as :host > * and :host * target the host's
                    // light-DOM tree, not descendants inside the shadow root. Keep the host as the selector anchor
                    // and dirty host-side nodes from there.
                    if (element_matches_anchor_rule(*host, rule, host, shadow_root))
                        apply_trailing_universal_combinator(rule.combinator, *host, *host);
                }
            }
        }
    }
}

static bool rule_requires_broad_add_or_remove_invalidation(CSSRule const& rule)
{
    switch (rule.type()) {
    case CSSRule::Type::Property:
    case CSSRule::Type::CounterStyle:
    case CSSRule::Type::LayerBlock:
    case CSSRule::Type::LayerStatement:
    // @font-feature-values changes how font-variant-alternates resolves at computed-style time, so
    // a declaration-time mutation still requires a whole-subtree restyle.
    case CSSRule::Type::FontFeatureValues:
        return true;
    // OPTIMIZATION: @font-face declares a resource whose effect on computed style is deferred until
    //               the font actually loads (handled by CSSFontLoaded). Adding or removing the
    //               at-rule itself doesn't change any element's computed style.
    case CSSRule::Type::FontFace:
    // OPTIMIZATION: @keyframes rules only matter for elements that already reference the named
    //               animation. Sheet add/remove handles each contained @keyframes rule with a
    //               targeted invalidation (see invalidate_root_for_keyframes_rules_in_sheet)
    //               instead of forcing a whole-subtree restyle here.
    case CSSRule::Type::Keyframes:
    default:
        break;
    }

    return false;
}

void invalidate_style_for_stylesheet_change(DOM::Node& document_or_shadow_root, CSSStyleSheet const& sheet, DOM::StyleInvalidationReason reason)
{
    if (sheet.rules().length() == 0) {
        // NOTE: If the added sheet has no rules, we don't have to invalidate anything.
        return;
    }

    if (auto* shadow_root = as_if<DOM::ShadowRoot>(document_or_shadow_root)) {
        shadow_root->style_scope().invalidate_rule_cache();
    } else {
        document_or_shadow_root.document().style_scope().invalidate_rule_cache();
    }

    if (!document_or_shadow_root.is_shadow_root() && document_or_shadow_root.entire_subtree_needs_style_update()) {
        // NOTE: If the entire subtree is already marked for style update,
        //       there's no point spending time building invalidation sets.
        //       Shadow roots are special: :host(...) and ::slotted(...) can
        //       still require follow-up invalidation outside that subtree.
        return;
    }

    // OPTIMIZATION: Build the targeted invalidation set and check for broad-invalidation-triggering rule kinds in
    //               a single walk over the sheet's effective rules, so the sheet's @media gates evaluate once and
    //               every rule is visited once instead of twice.
    StyleSheetInvalidationSet invalidation_set_result;
    bool sheet_contains_broad_invalidation_rule = false;
    sheet.for_each_effective_rule(TraversalOrder::Preorder, [&](CSSRule const& rule) {
        // Add/remove invalidation still has to look at effective non-style rules such as @keyframes or @layer, but
        // inactive top-level media sheets should contribute nothing at all here.
        if (!sheet_contains_broad_invalidation_rule && rule_requires_broad_add_or_remove_invalidation(rule))
            sheet_contains_broad_invalidation_rule = true;
        if (invalidation_set_result.invalidation_set.needs_invalidate_whole_subtree())
            return;
        if (auto const* style_rule = as_if<CSSStyleRule>(rule))
            extend_style_sheet_invalidation_set_with_style_rule(invalidation_set_result, *style_rule);
    });

    bool requires_broad_invalidation = invalidation_set_result.invalidation_set.needs_invalidate_whole_subtree()
        || sheet_contains_broad_invalidation_rule;
    if (requires_broad_invalidation) {
        if (auto* shadow_root = as_if<DOM::ShadowRoot>(document_or_shadow_root)) {
            auto effects = determine_shadow_root_stylesheet_effects(*shadow_root);
            invalidation_set_result.may_match_shadow_host |= effects.may_match_shadow_host;
            invalidation_set_result.may_match_light_dom_under_shadow_host |= effects.may_match_light_dom_under_shadow_host;
            invalidation_set_result.may_match_light_dom_outside_shadow_host |= effects.may_match_light_dom_outside_shadow_host;
        }
    }

    invalidate_root_for_style_sheet_change(document_or_shadow_root, invalidation_set_result, reason, requires_broad_invalidation);

    // OPTIMIZATION: Walk @keyframes rules in the new sheet and dirty only the elements that already
    //               reference each animation-name, so a sheet add carrying @keyframes does not have
    //               to escalate to a whole-subtree restyle.
    invalidate_root_for_keyframes_rules_in_sheet(document_or_shadow_root, sheet);
}

void invalidate_owners_for_inserted_style_rule(CSSStyleSheet const& style_sheet, CSSStyleRule const& style_rule, DOM::StyleInvalidationReason reason)
{
    StyleSheetInvalidationSet invalidation_set;
    extend_style_sheet_invalidation_set_with_style_rule(invalidation_set, style_rule);

    for (auto& document_or_shadow_root : style_sheet.owning_documents_or_shadow_roots()) {
        auto& style_scope = document_or_shadow_root->is_shadow_root()
            ? as<DOM::ShadowRoot>(*document_or_shadow_root).style_scope()
            : document_or_shadow_root->document().style_scope();
        style_scope.invalidate_rule_cache();

        // A dirty shadow subtree can still need follow-up invalidation on the host side for :host(...) and
        // ::slotted(...) matches, so we don't skip shadow roots even when their entire subtree is already marked.
        invalidate_root_for_style_sheet_change(*document_or_shadow_root, invalidation_set, reason);
    }
}

static void for_each_tree_affected_by_shadow_root_stylesheet_change(
    DOM::Node& root,
    bool include_host,
    bool include_light_dom_under_shadow_host,
    Function<void(DOM::Node&)> const& callback)
{
    callback(root);

    if (auto* shadow_root = as_if<DOM::ShadowRoot>(root)) {
        auto* host = shadow_root->host();
        if (!host)
            return;

        // Slotted selectors only reach the current host subtree, but :host combinators can also reach siblings rooted
        // alongside the host. A bare :host selector, however, only needs the host itself.
        if (include_host && include_light_dom_under_shadow_host)
            callback(host->root());
        else if (include_host || include_light_dom_under_shadow_host)
            callback(*host);
    }
}

static bool style_value_references_animation_name(StyleValue const& value, FlyString const& animation_name)
{
    if (value.is_custom_ident())
        return value.as_custom_ident().custom_ident() == animation_name;
    if (value.is_string())
        return value.as_string().string_value() == animation_name;

    if (!value.is_value_list())
        return false;

    for (auto const& item : value.as_value_list().values()) {
        if (item->is_custom_ident() && item->as_custom_ident().custom_ident() == animation_name)
            return true;
        if (item->is_string() && item->as_string().string_value() == animation_name)
            return true;
    }

    return false;
}

static bool element_or_pseudo_references_animation_name(DOM::Element const& element, FlyString const& animation_name)
{
    auto references_animation_name_in_properties = [&](CSS::ComputedProperties const& computed_properties) {
        return style_value_references_animation_name(computed_properties.property(PropertyID::AnimationName), animation_name);
    };

    if (auto computed_properties = element.computed_properties(); computed_properties && references_animation_name_in_properties(*computed_properties))
        return true;

    for (u8 i = 0; i < to_underlying(CSS::PseudoElement::KnownPseudoElementCount); ++i) {
        auto pseudo_element = static_cast<CSS::PseudoElement>(i);
        if (auto computed_properties = element.computed_properties(pseudo_element); computed_properties && references_animation_name_in_properties(*computed_properties))
            return true;
    }

    return false;
}

static void invalidate_elements_affected_by_inserted_keyframes_rule(DOM::Node& root, FlyString const& animation_name)
{
    auto invalidate_matching_element = [&](DOM::Element& element) {
        // A new @keyframes rule only matters for elements or pseudo-elements that were already referencing the
        // inserted animation-name.
        if (element_or_pseudo_references_animation_name(element, animation_name))
            element.set_needs_style_update(true);
        return TraversalDecision::Continue;
    };

    if (root.is_document()) {
        // Document styles are inherited by existing shadow trees too, so document-scoped @keyframes insertions must
        // walk the shadow-including tree instead of stopping at shadow hosts.
        root.for_each_shadow_including_inclusive_descendant([&](DOM::Node& node) {
            if (auto* element = as_if<DOM::Element>(node))
                return invalidate_matching_element(*element);
            return TraversalDecision::Continue;
        });
        return;
    }

    root.for_each_in_inclusive_subtree_of_type<DOM::Element>(invalidate_matching_element);
}

static ShadowRootStylesheetEffects determine_shadow_root_stylesheet_effects_for_sheet(CSSStyleSheet const& style_sheet, DOM::ShadowRoot const& shadow_root)
{
    ShadowRootStylesheetEffects effects;

    Vector<GC::Ptr<HTML::HTMLSlotElement const>> slots;
    shadow_root.for_each_in_inclusive_subtree_of_type<HTML::HTMLSlotElement>([&](HTML::HTMLSlotElement const& slot) {
        slots.append(slot);
        return TraversalDecision::Continue;
    });

    auto selector_may_affect_assigned_nodes_via_slot_inheritance = [&](Selector const& selector) {
        for (auto const& slot : slots) {
            for (auto const* node = static_cast<DOM::Node const*>(slot.ptr()); node && node != &shadow_root; node = node->parent_node()) {
                auto const* element = as_if<DOM::Element>(node);
                if (!element)
                    continue;
                SelectorEngine::MatchContext context {
                    .style_sheet_for_rule = style_sheet,
                    .subject = *element,
                    .rule_shadow_root = &shadow_root,
                };
                if (SelectorEngine::matches(selector, *element, shadow_root.host(), context))
                    return true;
            }
        }
        return false;
    };

    style_sheet.for_each_effective_style_producing_rule([&](CSSRule const& rule) {
        if (effects.all_set())
            return;

        if (!is<CSSStyleRule>(rule))
            return;

        auto const& style_rule = as<CSSStyleRule>(rule);
        for (auto const& selector : style_rule.absolutized_selectors()) {
            effects.may_match_shadow_host |= selector->contains_pseudo_class(PseudoClass::Host);
            effects.may_match_light_dom_under_shadow_host |= selector_may_match_light_dom_under_shadow_host(*selector);
            effects.may_match_light_dom_outside_shadow_host |= selector_may_match_light_dom_outside_shadow_host(*selector);

            if (!effects.may_affect_assigned_nodes_via_slots && !slots.is_empty())
                effects.may_affect_assigned_nodes_via_slots = selector_may_affect_assigned_nodes_via_slot_inheritance(*selector);

            if (effects.all_set())
                return;
        }
    });

    return effects;
}

ShadowRootStylesheetEffects determine_shadow_root_stylesheet_effects(DOM::ShadowRoot const& shadow_root)
{
    ShadowRootStylesheetEffects effects;

    shadow_root.for_each_active_css_style_sheet([&](CSSStyleSheet& style_sheet) {
        auto sheet_effects = determine_shadow_root_stylesheet_effects_for_sheet(style_sheet, shadow_root);
        effects.may_match_shadow_host |= sheet_effects.may_match_shadow_host;
        effects.may_match_light_dom_under_shadow_host |= sheet_effects.may_match_light_dom_under_shadow_host;
        effects.may_match_light_dom_outside_shadow_host |= sheet_effects.may_match_light_dom_outside_shadow_host;
        effects.may_affect_assigned_nodes_via_slots |= sheet_effects.may_affect_assigned_nodes_via_slots;
    });

    return effects;
}

ShadowRootStylesheetEffects determine_shadow_root_stylesheet_effects(CSSStyleSheet const& style_sheet)
{
    ShadowRootStylesheetEffects effects;

    for (auto& document_or_shadow_root : style_sheet.owning_documents_or_shadow_roots()) {
        auto* shadow_root = as_if<DOM::ShadowRoot>(*document_or_shadow_root);
        if (!shadow_root)
            continue;

        auto sheet_effects = determine_shadow_root_stylesheet_effects_for_sheet(style_sheet, *shadow_root);
        effects.may_match_shadow_host |= sheet_effects.may_match_shadow_host;
        effects.may_match_light_dom_under_shadow_host |= sheet_effects.may_match_light_dom_under_shadow_host;
        effects.may_match_light_dom_outside_shadow_host |= sheet_effects.may_match_light_dom_outside_shadow_host;
        effects.may_affect_assigned_nodes_via_slots |= sheet_effects.may_affect_assigned_nodes_via_slots;

        if (effects.all_set())
            break;
    }

    return effects;
}

static bool invalidate_shadow_host_side_for_style_sheet_change(DOM::ShadowRoot& shadow_root, DOM::StyleInvalidationReason reason, ShadowRootStylesheetEffects const& effects)
{
    auto* host = shadow_root.host();
    if (!host)
        return false;

    if (effects.may_match_light_dom_outside_shadow_host) {
        host->root().invalidate_style(reason);
    } else if (effects.may_match_light_dom_under_shadow_host) {
        host->invalidate_style(reason);
    } else if (effects.may_affect_assigned_nodes_via_slots) {
        host->invalidate_style(reason);
    } else if (effects.may_match_shadow_host) {
        host->invalidate_style(reason);
        shadow_root.set_needs_style_update(true);
    } else {
        return false;
    }

    return true;
}

void invalidate_style_for_style_sheet_owners(CSSStyleSheet const& style_sheet, DOM::StyleInvalidationReason reason, ShouldInvalidateRuleCache rule_cache_invalidation, ShadowRootStylesheetEffects const* previous_sheet_effects)
{
    style_sheet.for_each_owning_style_scope([&](StyleScope& style_scope) {
        if (rule_cache_invalidation == ShouldInvalidateRuleCache::Yes)
            style_scope.invalidate_rule_cache();

        style_scope.node().invalidate_style(reason);

        auto* shadow_root = as_if<DOM::ShadowRoot>(style_scope.node());
        if (!shadow_root)
            return;

        invalidate_assigned_elements_for_dirty_slots(*shadow_root);
        bool invalidated_host_side = invalidate_shadow_host_side_for_style_sheet_change(*shadow_root, reason, determine_shadow_root_stylesheet_effects(*shadow_root));

        if (!invalidated_host_side && previous_sheet_effects)
            invalidate_shadow_host_side_for_style_sheet_change(*shadow_root, reason, *previous_sheet_effects);
    });
}

void invalidate_root_for_keyframes_rules_in_sheet(DOM::Node& root, CSSStyleSheet const& sheet)
{
    // Shadow-scoped keyframes can still affect elements outside the shadow subtree when an active rule in the same
    // scope sets `animation-name` via :host or ::slotted(...). Mirror the fan-out used by the per-rule helper so the
    // walk reaches the host (and host-side light DOM) when the shadow scope can match those elements.
    bool include_host = false;
    bool include_light_dom_under_shadow_host = false;
    if (auto* shadow_root = as_if<DOM::ShadowRoot>(root)) {
        auto effects = determine_shadow_root_stylesheet_effects(*shadow_root);
        include_host = effects.may_match_shadow_host;
        include_light_dom_under_shadow_host = effects.may_match_light_dom_under_shadow_host;
    }

    sheet.for_each_effective_rule(TraversalOrder::Preorder, [&](CSSRule const& rule) {
        auto const* keyframes_rule = as_if<CSSKeyframesRule>(rule);
        if (!keyframes_rule)
            return;
        for_each_tree_affected_by_shadow_root_stylesheet_change(
            root,
            include_host,
            include_light_dom_under_shadow_host,
            [&](DOM::Node& affected_root) {
                invalidate_elements_affected_by_inserted_keyframes_rule(affected_root, keyframes_rule->name());
            });
    });
}

void invalidate_owners_for_inserted_keyframes_rule(CSSStyleSheet const& style_sheet, CSSKeyframesRule const& keyframes_rule)
{
    for (auto& document_or_shadow_root : style_sheet.owning_documents_or_shadow_roots()) {
        auto& style_scope = document_or_shadow_root->is_shadow_root()
            ? as<DOM::ShadowRoot>(*document_or_shadow_root).style_scope()
            : document_or_shadow_root->document().style_scope();
        style_scope.invalidate_rule_cache();

        if (!document_or_shadow_root->is_shadow_root() && document_or_shadow_root->entire_subtree_needs_style_update())
            continue;

        bool include_host = false;
        bool include_light_dom_under_shadow_host = false;
        if (auto* shadow_root = as_if<DOM::ShadowRoot>(*document_or_shadow_root)) {
            auto effects = determine_shadow_root_stylesheet_effects(*shadow_root);
            include_host = effects.may_match_shadow_host;
            include_light_dom_under_shadow_host = effects.may_match_light_dom_under_shadow_host;
        }

        for_each_tree_affected_by_shadow_root_stylesheet_change(
            *document_or_shadow_root,
            include_host,
            include_light_dom_under_shadow_host,
            [&](DOM::Node& affected_root) {
                invalidate_elements_affected_by_inserted_keyframes_rule(affected_root, keyframes_rule.name());
            });
    }
}

}
