/*
 * Copyright (c) 2022-2023, Andreas Kling <andreas@ladybird.org>
 * Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
 * Copyright (c) 2026, Jelle Raaijmakers <jelle@ladybird.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/SystemColor.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Layout/TextNode.h>
#include <LibWeb/Layout/TextOffsetMapping.h>
#include <LibWeb/Painting/DisplayListRecorder.h>
#include <LibWeb/Painting/DisplayListRecordingContext.h>
#include <LibWeb/Painting/Paintable.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Painting/PaintableWithLines.h>
#include <LibWeb/Painting/StackingContext.h>
#include <LibWeb/Painting/ViewportPaintable.h>

namespace Web::Painting {

Paintable::Paintable(Layout::Node const& layout_node)
    : m_layout_node(layout_node)
{
    auto& computed_values = layout_node.computed_values();
    if ((layout_node.is_flex_item() || layout_node.is_grid_item()) && computed_values.z_index().has_value()) {
        // https://drafts.csswg.org/css-flexbox-1/#painting
        // https://drafts.csswg.org/css-grid-2/#z-order
        // Flex and grid items with z-index values other than "auto" behave as if position were "relative".
        m_positioned = true;
    } else {
        m_positioned = computed_values.position() != CSS::Positioning::Static;
    }

    m_fixed_position = computed_values.position() == CSS::Positioning::Fixed;
    m_sticky_position = computed_values.position() == CSS::Positioning::Sticky;
    m_absolutely_positioned = computed_values.position() == CSS::Positioning::Absolute;
    m_floating = layout_node.is_floating();
    m_inline = layout_node.is_inline();
    m_display = layout_node.display();
}

Paintable::~Paintable() = default;

String Paintable::debug_description() const
{
    return MUST(String::formatted("{}({})", class_name(), layout_node().debug_description()));
}

DOM::Document const& Paintable::document() const
{
    return layout_node().document();
}

DOM::Document& Paintable::document()
{
    return layout_node().document();
}

RefPtr<PaintableBox> Paintable::containing_block() const
{
    if (m_containing_block.has_value()) {
        if (auto containing_block = m_containing_block->strong_ref())
            return containing_block;
    }

    auto containing_block = [&] -> RefPtr<PaintableBox> {
        auto containing_layout_box = layout_node().containing_block();
        if (!containing_layout_box)
            return nullptr;
        auto paintable_box = containing_layout_box->paintable_box();
        if (!paintable_box)
            return nullptr;
        return const_cast<PaintableBox&>(*paintable_box);
    }();
    m_containing_block = containing_block;
    return containing_block;
}

CSS::ImmutableComputedValues const& Paintable::computed_values() const
{
    return layout_node().computed_values();
}

bool Paintable::visible_for_hit_testing() const
{
    if (auto node = dom_node(); node && node->is_inert())
        return false;
    return computed_values().pointer_events() != CSS::PointerEvents::None;
}

void Paintable::set_dom_node(GC::Ptr<DOM::Node> dom_node)
{
    m_dom_node = dom_node.ptr();
}

GC::Ptr<DOM::Node> Paintable::dom_node()
{
    return m_dom_node.ptr();
}

GC::Ptr<DOM::Node const> Paintable::dom_node() const
{
    return m_dom_node.ptr();
}

GC::Ptr<HTML::Navigable> Paintable::navigable() const
{
    return document().navigable();
}

bool Paintable::handle_mousewheel(Badge<EventHandler>, CSSPixelPoint, unsigned, unsigned, double, double)
{
    return false;
}

TraversalDecision Paintable::hit_test(CSSPixelPoint, HitTestType, Function<TraversalDecision(HitTestResult)> const&) const
{
    return TraversalDecision::Continue;
}

bool Paintable::has_stacking_context() const
{
    if (auto const* paintable_box = as_if<PaintableBox>(this))
        return paintable_box->stacking_context();
    return false;
}

RefPtr<StackingContext> Paintable::enclosing_stacking_context()
{
    for (auto ancestor = parent(); ancestor; ancestor = ancestor->parent()) {
        auto* paintable_box = as_if<PaintableBox>(ancestor.ptr());
        if (!paintable_box)
            continue;
        if (auto stacking_context = paintable_box->stacking_context())
            return stacking_context;
    }
    // We should always reach the viewport's stacking context.
    VERIFY_NOT_REACHED();
}

void Paintable::paint_inspector_overlay(DisplayListRecordingContext& context) const
{
    auto& display_list_recorder = context.display_list_recorder();
    RefPtr<PaintableBox const> paintable_box;
    if (is<PaintableBox>(*this))
        paintable_box = static_cast<PaintableBox const&>(*this);
    else
        paintable_box = first_ancestor_of_type<PaintableBox>();

    if (paintable_box) {
        auto viewport_paintable = document().paintable();
        VERIFY(viewport_paintable);
        auto& visual_context_tree = const_cast<ViewportPaintable&>(*viewport_paintable).visual_context_tree();
        auto visual_context_index = paintable_box->accumulated_visual_context_index();

        if (visual_context_index.value()) {
            Vector<VisualContextIndex> relevant_indices;
            for (auto i = visual_context_index; i.value(); i = visual_context_tree.node_at(i).parent_index) {
                auto should_keep = visual_context_tree.node_at(i).data.visit(
                    [](ScrollData const&) { return true; },
                    [](ClipData const&) { return false; },
                    [](TransformData const&) { return true; },
                    [](PerspectiveData const&) { return true; },
                    [](ClipPathData const&) { return false; },
                    [](EffectsData const&) { return false; },
                    [](ScrollCompensation const&) { return true; });
                if (should_keep)
                    relevant_indices.append(i);
            }

            VisualContextIndex overlay_visual_context_index {};
            for (auto const& source_visual_context_index : relevant_indices.in_reverse())
                overlay_visual_context_index = visual_context_tree.append(visual_context_tree.node_at(source_visual_context_index).data, overlay_visual_context_index);

            if (overlay_visual_context_index.value())
                display_list_recorder.set_accumulated_visual_context(overlay_visual_context_index);
        }
    }

    paint_inspector_overlay_internal(context);
    display_list_recorder.set_accumulated_visual_context({});
}

void Paintable::set_needs_repaint(InvalidateDisplayList should_invalidate_display_list)
{
    if (should_invalidate_display_list == InvalidateDisplayList::Yes) {
        if (auto containing_block = this->containing_block())
            containing_block->invalidate_paint_cache();
    }
    document().set_needs_repaint(Badge<Painting::Paintable> {}, should_invalidate_display_list);
}

CSSPixelPoint Paintable::box_type_agnostic_position() const
{
    if (auto const* paintable_box = as_if<PaintableBox>(this))
        return paintable_box->absolute_position();

    VERIFY(is_inline());

    CSSPixelPoint position;
    if (auto block = containing_block(); block && is<Painting::PaintableWithLines>(*block)) {
        auto const& fragments = static_cast<Painting::PaintableWithLines const&>(*block).fragments();
        if (!fragments.is_empty()) {
            position = fragments[0].absolute_rect().location();
        }
    }

    return position;
}

Painting::BorderRadiiData normalize_border_radii_data(Layout::Node const& node, CSSPixelRect const& border_rect, CSSPixelRect const& reference_rect, CSS::BorderRadiusData const& top_left_radius, CSS::BorderRadiusData const& top_right_radius, CSS::BorderRadiusData const& bottom_right_radius, CSS::BorderRadiusData const& bottom_left_radius)
{
    Painting::BorderRadiiData radii_px {
        .top_left = {
            top_left_radius.horizontal_radius.to_px(node, reference_rect.width()),
            top_left_radius.vertical_radius.to_px(node, reference_rect.height()) },
        .top_right = { top_right_radius.horizontal_radius.to_px(node, reference_rect.width()), top_right_radius.vertical_radius.to_px(node, reference_rect.height()) },
        .bottom_right = { bottom_right_radius.horizontal_radius.to_px(node, reference_rect.width()), bottom_right_radius.vertical_radius.to_px(node, reference_rect.height()) },
        .bottom_left = { bottom_left_radius.horizontal_radius.to_px(node, reference_rect.width()), bottom_left_radius.vertical_radius.to_px(node, reference_rect.height()) }
    };

    // Scale overlapping curves according to https://www.w3.org/TR/css-backgrounds-3/#corner-overlap
    // Let f = min(Li/Si), where i ∈ {top, right, bottom, left},
    // Si is the sum of the two corresponding radii of the corners on side i,
    // and Ltop = Lbottom = the width of the box, and Lleft = Lright = the height of the box.
    //
    // NOTE: We iterate twice as a form of iterative refinement. A single scaling pass using
    // fixed-point arithmetic can result in small rounding errors, causing the scaled radii to
    // still slightly overflow the box dimensions. A second pass corrects this remaining error.
    for (int iteration = 0; iteration < 2; ++iteration) {
        auto s_top = radii_px.top_left.horizontal_radius + radii_px.top_right.horizontal_radius;
        auto s_right = radii_px.top_right.vertical_radius + radii_px.bottom_right.vertical_radius;
        auto s_bottom = radii_px.bottom_right.horizontal_radius + radii_px.bottom_left.horizontal_radius;
        auto s_left = radii_px.bottom_left.vertical_radius + radii_px.top_left.vertical_radius;

        CSSPixelFraction f = 1;
        if (s_top > border_rect.width())
            f = min(f, border_rect.width() / s_top);
        if (s_right > border_rect.height())
            f = min(f, border_rect.height() / s_right);
        if (s_bottom > border_rect.width())
            f = min(f, border_rect.width() / s_bottom);
        if (s_left > border_rect.height())
            f = min(f, border_rect.height() / s_left);

        // If f is 1 or more, the radii fit perfectly and no more scaling is needed
        if (f >= 1)
            break;

        Painting::BorderRadiusData* corners[] = {
            &radii_px.top_left, &radii_px.top_right, &radii_px.bottom_right, &radii_px.bottom_left
        };

        for (auto* corner : corners) {
            corner->horizontal_radius *= f;
            corner->vertical_radius *= f;
        }
    }

    return radii_px;
}

// https://drafts.csswg.org/css-pseudo-4/#highlight-styling
// FIXME: Support additional ::selection properties: text-underline-offset, text-underline-position, stroke-color,
//        fill-color, stroke-width, and CSS custom properties.
Paintable::SelectionStyle Paintable::selection_style() const
{
    auto color_scheme = computed_values().color_scheme();
    SelectionStyle default_style { CSS::SystemColor::highlight(color_scheme), {}, {}, {} };

    // For text nodes, check the parent element since text nodes don't have computed properties.
    auto node = dom_node();
    if (!node)
        return default_style;

    DOM::Element const* element = as_if<DOM::Element>(*node);
    if (!element)
        element = node->parent_element();
    if (!element)
        return default_style;

    auto style_from_element = [&](DOM::Element const& element) -> Optional<SelectionStyle> {
        auto element_layout_node = element.layout_node();
        if (!element_layout_node)
            return {};

        auto computed_selection_style = element.computed_properties(CSS::PseudoElement::Selection);
        if (!computed_selection_style)
            return {};

        auto context = CSS::ColorResolutionContext::for_layout_node_with_style(*element_layout_node);

        SelectionStyle style;
        style.background_color = computed_selection_style->color(CSS::PropertyID::BackgroundColor, context);

        // Only use text color if it was explicitly set in the ::selection rule, not inherited.
        if (!computed_selection_style->is_property_inherited(CSS::PropertyID::Color))
            style.text_color = computed_selection_style->color(CSS::PropertyID::Color, context);

        // Only use text-shadow if it was explicitly set in the ::selection rule, not inherited.
        if (!computed_selection_style->is_property_inherited(CSS::PropertyID::TextShadow)) {
            auto const& css_shadows = computed_selection_style->text_shadow(*element_layout_node);
            Vector<ShadowData> shadows;
            shadows.ensure_capacity(css_shadows.size());
            for (auto const& shadow : css_shadows)
                shadows.unchecked_append(ShadowData::from_css(shadow, *element_layout_node));
            style.text_shadow = move(shadows);
        }

        // Only use text-decoration if it was explicitly set in the ::selection rule, not inherited.
        if (!computed_selection_style->is_property_inherited(CSS::PropertyID::TextDecorationLine)) {
            style.text_decoration = TextDecorationStyle {
                .line = computed_selection_style->text_decoration_line(),
                .style = computed_selection_style->text_decoration_style(),
                .color = computed_selection_style->color(CSS::PropertyID::TextDecorationColor, context),
            };
        }

        // Only return a style if there's a meaningful customization. This allows us to continue checking shadow hosts
        // when the current element only has UA default styles.
        if (!style.has_styling())
            return {};

        return style;
    };

    // Check the element itself.
    if (auto style = style_from_element(*element); style.has_value())
        return style.release_value();

    // If inside a shadow tree, check the shadow host. This enables ::selection styling on elements like <input> to
    // apply to text rendered inside their shadow DOM.
    if (auto shadow_root = element->containing_shadow_root(); shadow_root && shadow_root->is_user_agent_internal()) {
        if (auto const* host = shadow_root->host()) {
            if (auto style = style_from_element(*host); style.has_value())
                return style.release_value();
        }
    }

    return default_style;
}

void Paintable::set_selection_state(SelectionState state)
{
    if (m_selection_state == state)
        return;
    m_selection_state = state;
    if (auto* box = as_if<PaintableBox>(this)) {
        box->invalidate_paint_cache();
    } else if (auto containing_block = this->containing_block()) {
        containing_block->invalidate_paint_cache();
        for (auto const* ancestor = layout_node().parent(); ancestor && ancestor != &containing_block->layout_node(); ancestor = ancestor->parent()) {
            for (auto& paintable : ancestor->paintables()) {
                if (auto* ancestor_box = as_if<PaintableBox>(paintable.ptr()))
                    ancestor_box->invalidate_paint_cache();
            }
        }
    }
}

void Paintable::scroll_ancestor_to_offset_into_view(size_t offset)
{
    auto scroll_to_cursor = [&](PaintableFragment const& fragment, Paintable const& fragment_paintable) {
        auto cursor_rect = fragment.range_rect(SelectionState::StartAndEnd, offset, offset);
        for (auto ancestor = fragment_paintable.containing_block(); ancestor; ancestor = ancestor->containing_block()) {
            if (ancestor->has_scrollable_overflow()) {
                ancestor->scroll_into_view(cursor_rect);
                return;
            }
        }
    };

    // Find the paintable fragment containing the cursor offset and scroll it into view.
    auto scan_layout_fragment = [&](Paintable const& slice_paintable) -> bool {
        auto paintable_with_lines = slice_paintable.first_ancestor_of_type<PaintableWithLines>();
        if (!paintable_with_lines)
            return false;
        for (auto const& fragment : paintable_with_lines->fragments()) {
            if (&fragment.paintable() != &slice_paintable)
                continue;
            if (offset < fragment.dom_start_offset_in_node() || offset > fragment.dom_end_offset_in_node())
                continue;
            scroll_to_cursor(fragment, slice_paintable);
            return true;
        }
        return false;
    };

    if (auto const* text = as_if<DOM::Text>(dom_node().ptr())) {
        Layout::TextOffsetMapping mapping { *text };
        bool scrolled = false;
        mapping.for_each_fragment([&](Layout::TextNode const& slice) {
            if (scrolled)
                return;
            if (auto slice_paintable = slice.first_paintable()) {
                if (scan_layout_fragment(*slice_paintable))
                    scrolled = true;
            }
        });
        return;
    }

    scan_layout_fragment(*this);
}

}
