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

#include <LibGfx/Font/Font.h>
#include <LibGfx/TextLayout.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Position.h>
#include <LibWeb/HTML/FormAssociatedElement.h>
#include <LibWeb/HTML/LocalNavigable.h>
#include <LibWeb/Layout/BlockContainer.h>
#include <LibWeb/Layout/InlineNode.h>
#include <LibWeb/Layout/TextNode.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/DisplayListRecorder.h>
#include <LibWeb/Painting/HitTestDisplayList.h>
#include <LibWeb/Painting/PaintableWithLines.h>
#include <LibWeb/Painting/ShadowPainting.h>
#include <LibWeb/Painting/StackingContext.h>
#include <LibWeb/VisualLines.h>

namespace Web::Painting {

static void paint_text_decoration(DisplayListRecordingContext&, Layout::TextNode const&, PaintableFragment::FragmentSpan const&);
static Gfx::Path build_triangle_wave_path(Gfx::IntPoint from, Gfx::IntPoint to, float amplitude);
static void compute_render_spans(PaintableFragment const&, Vector<PaintableFragment::FragmentSpan, 4>&);
static void paint_text_fragment(DisplayListRecordingContext&, PaintableFragment::FragmentSpan const&);

static bool layout_node_is_visible(Layout::Node const& layout_node)
{
    auto const& computed_values = layout_node.computed_values();
    return computed_values.visibility() == CSS::Visibility::Visible && computed_values.opacity() != 0;
}

NonnullRefPtr<PaintableWithLines> PaintableWithLines::create(Layout::BlockContainer const& block_container)
{
    return adopt_ref(*new PaintableWithLines(block_container));
}

NonnullRefPtr<PaintableWithLines> PaintableWithLines::create(Layout::InlineNode const& inline_node, size_t line_index)
{
    return adopt_ref(*new PaintableWithLines(inline_node, line_index));
}

PaintableWithLines::PaintableWithLines(Layout::BlockContainer const& layout_box)
    : Paintable(layout_box)
{
}

PaintableWithLines::PaintableWithLines(Layout::InlineNode const& inline_node, size_t line_index)
    : Paintable(inline_node)
    , m_line_index(line_index)
{
}

PaintableWithLines::~PaintableWithLines()
{
}

void PaintableWithLines::reset_for_relayout()
{
    Paintable::reset_for_relayout();
    m_fragments.clear();
}

void PaintableWithLines::paint_text_fragment_debug_highlight(DisplayListRecordingContext& context, PaintableFragment const& fragment)
{
    auto fragment_absolute_rect = fragment.absolute_rect();
    auto fragment_absolute_device_rect = context.enclosing_device_rect(fragment_absolute_rect);
    context.display_list_recorder().draw_rect(fragment_absolute_device_rect.to_type<int>(), Color::Green);

    auto baseline_start = context.rounded_device_point(fragment_absolute_rect.top_left().translated(0, fragment.baseline())).to_type<int>();
    auto baseline_end = context.rounded_device_point(fragment_absolute_rect.top_right().translated(-1, fragment.baseline())).to_type<int>();
    context.display_list_recorder().draw_line(baseline_start, baseline_end, Color::Red);
}

void PaintableWithLines::record_hit_test_items(DisplayListRecordingContext& context, PaintPhase phase) const
{
    Paintable::record_hit_test_items(context, phase);

    if (phase != PaintPhase::Foreground)
        return;

    auto* hit_test_display_list = context.hit_test_display_list();
    if (!hit_test_display_list)
        return;

    if (m_fragments.is_empty()
        && !has_children()
        && layout_node().dom_node()
        && layout_node().dom_node()->is_editable_or_editing_host()
        && is_visible()
        && visible_for_hit_testing()) {
        hit_test_display_list->append_empty_editable(*this, absolute_border_box_rect(), accumulated_visual_context_index());
        return;
    }

    for (auto const& fragment : m_fragments) {
        if (fragment.is_block_level_box())
            continue;
        hit_test_display_list->append_text_fragment(fragment, accumulated_visual_context_for_descendants_index());
    }

    record_empty_line_caret_items(*hit_test_display_list, accumulated_visual_context_for_descendants_index());

    if (stacking_context()
        && is_inline()
        && is_visible()
        && visible_for_hit_testing()) {
        hit_test_display_list->append_box(*this, const_cast<PaintableWithLines&>(*this), absolute_border_box_rect(), accumulated_visual_context_index(), border_radii_data());
    }
}

Vector<PaintableWithLines::EmptyLineCaretTarget> PaintableWithLines::empty_line_caret_targets() const
{
    if (m_fragments.is_empty())
        return {};

    // Line boxes without fragments (e.g. the blank line between two consecutive newlines in a textarea) produce no
    // fragments to hit test or paint a caret in. When all fragments belong to a single text node with preserved
    // newlines, we can derive the caret offset of each empty line and compute caret targets for them.
    auto const* text_layout_node = as_if<Layout::TextNode>(m_fragments.first().layout_node());
    if (!text_layout_node)
        return {};
    if (!white_space_preserves_newlines(*text_layout_node))
        return {};

    // FIXME: Support vertical writing modes.
    if (computed_values().writing_mode() != CSS::WritingMode::HorizontalTb)
        return {};

    auto const* dom_text = text_layout_node->dom_text();
    if (!dom_text)
        return {};
    if (!text_contains_empty_visual_line_positions(dom_text->data().utf16_view()))
        return {};

    for (auto const& fragment : m_fragments) {
        if (&fragment.layout_node() != text_layout_node)
            return {};
    }

    auto lines = collect_visual_lines(*dom_text);

    // The interpolation below requires visual lines to correspond 1:1 to this block's line boxes.
    for (size_t i = 0; i < lines.size(); ++i) {
        if (!lines[i].fragments.is_empty() && lines[i].fragments.first()->line_box_data().index != i)
            return {};
    }

    // For each line, the index of the closest fragment-backed line before/after it.
    Vector<Optional<size_t>> previous_fragment_line;
    previous_fragment_line.resize(lines.size());
    Vector<Optional<size_t>> next_fragment_line;
    next_fragment_line.resize(lines.size());
    Optional<size_t> last_seen;
    for (size_t i = 0; i < lines.size(); ++i) {
        previous_fragment_line[i] = last_seen;
        if (!lines[i].fragments.is_empty())
            last_seen = i;
    }
    last_seen = {};
    for (size_t i = lines.size(); i-- > 0;) {
        next_fragment_line[i] = last_seen;
        if (!lines[i].fragments.is_empty())
            last_seen = i;
    }

    auto line_box_rect_for_line = [&](size_t index) {
        return lines[index].fragments.first()->absolute_line_box_rect();
    };

    Vector<EmptyLineCaretTarget> targets;
    auto content_rect = absolute_rect();
    for (size_t i = 0; i < lines.size(); ++i) {
        if (!lines[i].fragments.is_empty())
            continue;

        auto previous_line = previous_fragment_line[i];
        auto next_line = next_fragment_line[i];

        // Empty line boxes are not retained after layout, so interpolate each empty line's rect from its
        // fragment-backed neighbors. Consecutive empty lines share their gap evenly.
        CSSPixelRect line_rect;
        if (previous_line.has_value() && next_line.has_value()) {
            auto previous_rect = line_box_rect_for_line(*previous_line);
            auto next_rect = line_box_rect_for_line(*next_line);
            auto count = static_cast<int>(*next_line - *previous_line - 1);
            auto position_in_gap = static_cast<int>(i - *previous_line - 1);
            auto line_height = (next_rect.top() - previous_rect.bottom()) / count;
            line_rect = { content_rect.x(), previous_rect.bottom() + line_height * position_in_gap, content_rect.width(), line_height };
        } else if (previous_line.has_value()) {
            auto previous_rect = line_box_rect_for_line(*previous_line);
            auto steps = static_cast<int>(i - *previous_line);
            line_rect = { content_rect.x(), previous_rect.bottom() + previous_rect.height() * (steps - 1), content_rect.width(), previous_rect.height() };
        } else if (next_line.has_value()) {
            auto next_rect = line_box_rect_for_line(*next_line);
            auto steps = static_cast<int>(*next_line - i);
            line_rect = { content_rect.x(), next_rect.top() - next_rect.height() * steps, content_rect.width(), next_rect.height() };
        } else {
            // NB: Unreachable; m_fragments is non-empty, so at least one line has fragments.
            return targets;
        }

        targets.append({ lines[i].start_offset, i, line_rect });
    }
    return targets;
}

// A cursor on a line box with no fragments (e.g. a blank line in a textarea) has no fragment to position itself in;
// it is placed at the start of the empty line.
Optional<CSSPixelRect> PaintableWithLines::empty_line_caret_rect(DOM::Position const& position) const
{
    if (m_fragments.is_empty())
        return {};
    auto const* text_layout_node = as_if<Layout::TextNode>(m_fragments.first().layout_node());
    if (!text_layout_node || position.node() != text_layout_node->dom_text())
        return {};
    for (auto const& target : empty_line_caret_targets()) {
        if (target.offset == position.offset())
            return target.rect;
    }
    return {};
}

void PaintableWithLines::record_empty_line_caret_items(HitTestDisplayList& hit_test_display_list, VisualContextIndex visual_context_index) const
{
    for (auto const& target : empty_line_caret_targets())
        hit_test_display_list.append_empty_line(m_fragments.first(), target.offset, target.line_box_index, target.rect, visual_context_index);
}

static void resolve_text_fragment_properties(PaintableWithLines const& paintable_with_lines)
{
    for (auto& fragment : const_cast<PaintableWithLines&>(paintable_with_lines).fragments()) {
        auto const* text_node = as_if<Layout::TextNode>(fragment.layout_node());
        if (!text_node)
            continue;

        auto const& font = text_node->first_available_font();
        auto const glyph_height = CSSPixels::nearest_value_for(font.pixel_size());
        auto const line_thickness = [&] {
            auto const& thickness = text_node->computed_values().text_decoration_thickness();
            return thickness.value.visit(
                [glyph_height](CSS::TextDecorationThickness::Auto) {
                    // https://drafts.csswg.org/css-text-decor-4/#valdef-text-decoration-thickness-auto
                    // The UA chooses an appropriate thickness for text decoration lines; see below.
                    return max(glyph_height.scaled(0.1), 1);
                },
                [glyph_height](CSS::TextDecorationThickness::FromFont) {
                    // https://drafts.csswg.org/css-text-decor-4/#valdef-text-decoration-thickness-from-font
                    // If the first available font has metrics indicating a preferred underline width, use that width,
                    // otherwise behaves as auto.
                    // FIXME: Implement this properly.
                    return max(glyph_height.scaled(0.1), 1);
                },
                [&](CSS::LengthPercentage const& length_percentage) {
                    // https://drafts.csswg.org/css-text-decor-4/#valdef-text-decoration-thickness-length-percentage
                    auto resolved_length = length_percentage.resolved(CSS::Length(1, CSS::LengthUnit::Em).to_px(*text_node)).to_px(*text_node);
                    return max(resolved_length, 1);
                });
        }();
        fragment.set_text_decoration_thickness(line_thickness);

        auto const& text_shadow = text_node->computed_values().text_shadow();
        Vector<ShadowData> resolved_shadow_data;
        if (!text_shadow.is_empty()) {
            resolved_shadow_data.ensure_capacity(text_shadow.size());
            for (auto const& layer : text_shadow)
                resolved_shadow_data.append(ShadowData::from_css(layer));
        }
        fragment.set_shadows(move(resolved_shadow_data));
    }
}

void PaintableWithLines::paint(DisplayListRecordingContext& context, PaintPhase phase) const
{
    if (!is_visible())
        return;

    // An inline's per-line paintable that only hosts an interrupting block's phantom fragment generates no box
    // of its own; painting its background/border would draw a degenerate box at the block's corner.
    if (is<Layout::InlineNode>(layout_node()) && has_only_block_level_fragments())
        return;

    Paintable::paint(context, phase);

    if (phase == PaintPhase::Foreground) {
        resolve_text_fragment_properties(*this);

        Vector<PaintableFragment::FragmentSpan, 4> spans;
        for (auto const& fragment : m_fragments)
            compute_render_spans(fragment, spans);

        for (auto const& span : spans) {
            if (span.background_color.alpha() > 0) {
                auto selection_rect = context.rounded_device_rect(span.fragment.selection_rect()).to_type<int>();
                context.display_list_recorder().fill_rect(selection_rect, span.background_color);
            }
        }

        for (auto const& span : spans)
            paint_text_shadow(context, span);

        for (auto const& span : spans)
            paint_text_fragment(context, span);

        if (document().cursor_position())
            paint_cursor(context);
    }
}

void compute_render_spans(PaintableFragment const& fragment, Vector<PaintableFragment::FragmentSpan, 4>& spans)
{
    if (fragment.is_block_level_box())
        return;

    auto const* text_node = as_if<Layout::TextNode>(fragment.layout_node());
    if (!text_node) {
        // Non-text fragments still need shadow painting.
        spans.append({
            .fragment = fragment,
            .start_code_unit = 0,
            .end_code_unit = 0,
            .text_color = Color::Transparent,
            .background_color = Color::Transparent,
            .shadow_layers = {},
            .text_decoration = {},
        });
        return;
    }

    if (!layout_node_is_visible(*text_node))
        return;

    auto text_color = text_node->computed_values().webkit_text_fill_color();
    auto selection_offsets = fragment.selection_offsets();

    // No selection: single span with base styling.
    if (!selection_offsets.has_value()) {
        spans.append({
            .fragment = fragment,
            .start_code_unit = 0,
            .end_code_unit = fragment.length_in_code_units(),
            .text_color = text_color,
            .background_color = Color::Transparent,
            .shadow_layers = {},
            .text_decoration = {},
        });
        return;
    }

    auto [selection_start, selection_end] = *selection_offsets;
    auto selection_style = Paintable::selection_style_for_node(*text_node, text_node->dom_text());
    auto selection_text_color = selection_style.text_color.value_or(text_color);

    // Convert selection text decoration to fragment text decoration data.
    Optional<PaintableFragment::TextDecorationData> selection_text_decoration;
    if (selection_style.text_decoration.has_value()) {
        selection_text_decoration = PaintableFragment::TextDecorationData {
            .line = move(selection_style.text_decoration->line),
            .style = selection_style.text_decoration->style,
            .color = selection_style.text_decoration->color,
        };
    }

    // Before selection.
    if (selection_start > 0) {
        spans.append({
            .fragment = fragment,
            .start_code_unit = 0,
            .end_code_unit = selection_start,
            .text_color = text_color,
            .background_color = Color::Transparent,
            .shadow_layers = {},
            .text_decoration = {},
        });
    }

    // Selected portion.
    if (selection_start < selection_end) {
        spans.append({
            .fragment = fragment,
            .start_code_unit = selection_start,
            .end_code_unit = selection_end,
            .text_color = selection_text_color,
            .background_color = selection_style.background_color,
            .shadow_layers = move(selection_style.text_shadow),
            .text_decoration = move(selection_text_decoration),
        });
    }

    // After selection.
    if (selection_end < fragment.length_in_code_units()) {
        spans.append({
            .fragment = fragment,
            .start_code_unit = selection_end,
            .end_code_unit = fragment.length_in_code_units(),
            .text_color = text_color,
            .background_color = Color::Transparent,
            .shadow_layers = {},
            .text_decoration = {},
        });
    }
}

void paint_text_fragment(DisplayListRecordingContext& context, PaintableFragment::FragmentSpan const& span)
{
    auto const& fragment = span.fragment;

    // Skip non-text spans (they're only for shadow painting).
    if (span.start_code_unit == span.end_code_unit)
        return;

    auto const& text_node = as<Layout::TextNode>(fragment.layout_node());

    if (context.should_show_line_box_borders())
        PaintableWithLines::paint_text_fragment_debug_highlight(context, fragment);

    auto glyph_run = fragment.glyph_run();
    if (!glyph_run)
        return;

    auto& painter = context.display_list_recorder();
    auto fragment_absolute_rect = fragment.absolute_rect();
    auto fragment_device_rect = context.enclosing_device_rect(fragment_absolute_rect).to_type<int>();
    auto scale = context.device_pixels_per_css_pixel();
    auto baseline_start = Gfx::FloatPoint {
        fragment_absolute_rect.x().to_float(),
        fragment_absolute_rect.y().to_float() + fragment.baseline().to_float(),
    } * scale;

    // Paint text, clipped to span range if not full fragment.
    bool is_full_fragment = span.start_code_unit == 0 && span.end_code_unit == fragment.length_in_code_units();
    if (is_full_fragment) {
        painter.draw_glyph_run(baseline_start, *glyph_run, span.text_color, fragment_device_rect, scale, fragment.orientation());
    } else {
        auto range_rect = fragment.range_rect(Paintable::SelectionState::StartAndEnd,
            fragment.dom_start_offset_in_node() + span.start_code_unit,
            fragment.dom_start_offset_in_node() + span.end_code_unit);
        auto span_rect = context.rounded_device_rect(range_rect).to_type<int>();
        painter.save();
        painter.add_clip_rect(span_rect);
        painter.draw_glyph_run(baseline_start, *glyph_run, span.text_color, fragment_device_rect, scale, fragment.orientation());
        painter.restore();
    }

    paint_text_decoration(context, text_node, span);
}

Optional<PaintableFragment const&> PaintableWithLines::fragment_at_position(DOM::Position const& position) const
{
    PaintableFragment const* fallback_fragment = nullptr;
    for (auto const& fragment : m_fragments) {
        auto const* text_node = as_if<Layout::TextNode>(fragment.layout_node());
        if (!text_node || position.node() != text_node->dom_text())
            continue;
        switch (fragment.caret_match(position.offset(), position.affinity())) {
        case PaintableFragment::CaretMatch::None:
            continue;
        case PaintableFragment::CaretMatch::SoftWrapFallback:
            if (!fallback_fragment)
                fallback_fragment = &fragment;
            continue;
        case PaintableFragment::CaretMatch::Direct:
            return fragment;
        }
    }
    if (fallback_fragment)
        return *fallback_fragment;
    return {};
}

void PaintableWithLines::paint_cursor(DisplayListRecordingContext& context) const
{
    if (!document().cursor_blink_state() || !document().navigable()->is_focused())
        return;

    auto cursor_position = document().cursor_position();
    VERIFY(cursor_position);

    auto const* dom_node = layout_node().dom_node();
    if (!dom_node)
        return;

    auto focused_text_control_is_editable = false;
    if (auto const* text_control = as_if<HTML::FormAssociatedTextControlElement>(document().focused_area().ptr()))
        focused_text_control_is_editable = text_control->text_control_to_html_element().is_mutable();
    if (!focused_text_control_is_editable && !dom_node->is_editable_or_editing_host())
        return;

    auto fragment = fragment_at_position(*cursor_position);

    CSSPixelRect cursor_rect;
    Color caret_color;

    if (fragment.has_value()) {
        caret_color = fragment->layout_node().computed_values().caret_color();
        cursor_rect = fragment->range_rect(SelectionState::StartAndEnd, cursor_position->offset(), cursor_position->offset());
    } else if (auto empty_line_rect = empty_line_caret_rect(*cursor_position); empty_line_rect.has_value()) {
        caret_color = m_fragments.first().layout_node().computed_values().caret_color();
        cursor_rect = { empty_line_rect->x(), empty_line_rect->y(), 1, empty_line_rect->height() };
    } else {
        // Empty editable elements have no fragments, but should still draw a cursor.
        if (cursor_position->node() != dom_node)
            return;

        caret_color = computed_values().caret_color();
        auto content_box = absolute_padding_box_rect();
        cursor_rect = { content_box.x(), content_box.y(), 1, computed_values().line_height() };
    }

    if (caret_color.alpha() == 0)
        return;

    auto cursor_device_rect = context.rounded_device_rect(cursor_rect).to_type<int>();

    context.display_list_recorder().fill_rect(cursor_device_rect, caret_color);
}

struct DecorationSegment {
    int start_x;
    int end_x;
};

// https://drafts.csswg.org/css-text-decor-4/#text-decoration-skip-ink-property
static Vector<DecorationSegment> compute_skip_ink_segments(
    PaintableFragment const& fragment,
    DisplayListRecordingContext const& context,
    int span_start_x,
    int span_end_x,
    int line_y,
    int line_thickness,
    float font_size)
{
    auto glyph_run = fragment.glyph_run();
    if (!glyph_run)
        return { { span_start_x, span_end_x } };

    // The text blob is drawn at baseline_start on the canvas. Compute that same origin so we can convert between
    // device-pixel coordinates and blob-local coordinates.
    auto scale = context.device_pixels_per_css_pixel();
    auto fragment_absolute_rect = fragment.absolute_rect();
    float blob_origin_x = fragment_absolute_rect.x().to_float() * static_cast<float>(scale);
    float blob_origin_y = (fragment_absolute_rect.y().to_float() + fragment.baseline().to_float()) * static_cast<float>(scale);

    // Convert the underline's y-band from device pixels to blob-local coordinates.
    float half_thickness = line_thickness / 2.f;
    float y_top = line_y - half_thickness - blob_origin_y;
    float y_bottom = line_y + half_thickness - blob_origin_y;

    auto intervals = glyph_run->get_glyph_intercepts(scale, y_top, y_bottom);
    if (intervals.is_empty())
        return { { span_start_x, span_end_x } };

    // Use the full fragment's X range for gap computation so intercepts aren't cut off at span boundaries.
    auto full_fragment_rect = context.rounded_device_rect(fragment_absolute_rect);
    int fragment_start_x = full_fragment_rect.left().value();

    // Convert intercepts from blob-local x to device pixels, and dilate to create visible gaps.
    float dilation = max(font_size / 20.f, 2.f) * static_cast<float>(scale);

    Vector<DecorationSegment> segments;
    int current_x = fragment_start_x;
    for (size_t i = 0; i + 1 < intervals.size(); i += 2) {
        int gap_start = static_cast<int>(floorf(intervals[i] + blob_origin_x - dilation));
        int gap_end = static_cast<int>(ceilf(intervals[i + 1] + blob_origin_x + dilation));

        int seg_start = max(current_x, span_start_x);
        int seg_end = min(gap_start, span_end_x);
        if (seg_start < seg_end)
            segments.append({ seg_start, seg_end });
        current_x = max(gap_end, current_x);
    }

    int seg_start = max(current_x, span_start_x);
    if (seg_start < span_end_x)
        segments.append({ seg_start, span_end_x });

    return segments;
}

void paint_text_decoration(DisplayListRecordingContext& context, Layout::TextNode const& text_node, PaintableFragment::FragmentSpan const& span)
{
    auto const& fragment = span.fragment;
    auto& recorder = context.display_list_recorder();
    auto& font = fragment.layout_node().first_available_font();
    CSSPixels glyph_height = CSSPixels::nearest_value_for(font.pixel_size());
    auto baseline = fragment.baseline();

    // Use span's text decoration if explicitly set, otherwise use the element's computed values.
    Color line_color;
    CSS::TextDecorationStyle line_style;
    Vector<CSS::TextDecorationLine> text_decoration_lines;
    if (span.text_decoration.has_value()) {
        line_color = span.text_decoration->color;
        line_style = span.text_decoration->style;
        text_decoration_lines = span.text_decoration->line;
    } else {
        line_color = text_node.computed_values().text_decoration_color();
        line_style = text_node.computed_values().text_decoration_style();
        text_decoration_lines = text_node.computed_values().text_decoration_line();
    }

    // Compute the decoration box for this span.
    auto fragment_box = fragment.absolute_rect();
    if (span.start_code_unit != 0 || span.end_code_unit != fragment.length_in_code_units()) {
        auto span_rect = fragment.range_rect(Paintable::SelectionState::StartAndEnd,
            fragment.dom_start_offset_in_node() + span.start_code_unit,
            fragment.dom_start_offset_in_node() + span.end_code_unit);
        fragment_box.set_x(span_rect.x());
        fragment_box.set_width(span_rect.width());
    }
    auto text_underline_offset = text_node.computed_values().text_underline_offset();
    auto text_underline_position = text_node.computed_values().text_underline_position();
    for (auto line : text_decoration_lines) {
        auto line_thickness = fragment.text_decoration_thickness();

        if (line == CSS::TextDecorationLine::SpellingError) {
            // https://drafts.csswg.org/css-text-decor-4/#valdef-text-decoration-line-spelling-error
            // This value indicates the type of text decoration used by the user agent to highlight spelling mistakes.
            // Its appearance is UA-defined, and may be platform-dependent. It is often rendered as a red wavy underline.
            line_color = Color::Red;
            line_thickness = CSSPixels(1);
            line_style = CSS::TextDecorationStyle::Wavy;
            line = CSS::TextDecorationLine::Underline;

            // https://drafts.csswg.org/css-text-decor-4/#underline-offset
            // When the value of the text-decoration-line property is either spelling-error or grammar-error, the UA
            // must ignore the value of text-underline-position.
            text_underline_offset = CSS::InitialValues::text_underline_offset();
        } else if (line == CSS::TextDecorationLine::GrammarError) {
            // https://drafts.csswg.org/css-text-decor-4/#valdef-text-decoration-line-grammar-error
            // This value indicates the type of text decoration used by the user agent to highlight grammar mistakes.
            // Its appearance is UA defined, and may be platform-dependent. It is often rendered as a green wavy underline.
            line_color = Color::DarkGreen;
            line_thickness = CSSPixels(1);
            line_style = CSS::TextDecorationStyle::Wavy;
            line = CSS::TextDecorationLine::Underline;

            // https://drafts.csswg.org/css-text-decor-4/#underline-offset
            // When the value of the text-decoration-line property is either spelling-error or grammar-error, the UA
            // must ignore the value of text-underline-position.
            text_underline_offset = CSS::InitialValues::text_underline_offset();
        }

        auto device_line_thickness = context.rounded_device_pixels(line_thickness);

        // Compute the center Y of the decoration stroke. For underline and overline, offset by half the thickness
        // so the near edge of the stroke aligns with the intended position.
        CSSPixels line_center_y;
        switch (line) {
        case CSS::TextDecorationLine::None:
            return;
        case CSS::TextDecorationLine::Underline: {
            // https://drafts.csswg.org/css-text-decor-4/#text-underline-position-property
            auto underline_top_edge = [&]() {
                // FIXME: Support text-decoration: underline on vertical text
                switch (text_underline_position.horizontal) {
                case CSS::TextUnderlinePositionHorizontal::Auto:
                    // The user agent may use any algorithm to determine the underline’s position; however it must be
                    // placed at or under the alphabetic baseline.

                    // Spec Note: It is suggested that the default underline position be close to the alphabetic
                    //            baseline,
                    // FIXME:     unless that would either cross subscripted (or otherwise lowered) text or draw over
                    //            glyphs from Asian scripts such as Han or Tibetan for which an alphabetic underline is
                    //            too high: in such cases, shifting the underline lower or aligning to the em box edge
                    //            as described for under may be more appropriate.
                    return fragment.baseline() + text_underline_offset;
                case CSS::TextUnderlinePositionHorizontal::FromFont:
                    // FIXME: If the first available font has metrics indicating a preferred underline offset, use that
                    //        offset, otherwise behaves as auto.
                    return fragment.baseline() + text_underline_offset;
                case CSS::TextUnderlinePositionHorizontal::Under:
                    // The underline is positioned under the element’s text content. In this case the underline usually
                    // does not cross the descenders. (This is sometimes called “accounting” underline.)
                    return fragment.baseline() + CSSPixels { font.pixel_metrics().descent } + text_underline_offset;
                }
                VERIFY_NOT_REACHED();
            }();
            line_center_y = underline_top_edge + line_thickness / 2;
            break;
        }
        case CSS::TextDecorationLine::Overline:
            line_center_y = baseline - glyph_height - line_thickness / 2;
            break;
        case CSS::TextDecorationLine::LineThrough: {
            auto x_height = font.x_height();
            line_center_y = baseline - x_height * CSSPixels(0.5f);
            break;
        }
        case CSS::TextDecorationLine::Blink:
            // Conforming user agents may simply not blink the text
            return;
        case CSS::TextDecorationLine::SpellingError:
        case CSS::TextDecorationLine::GrammarError:
            // Handled above.
            VERIFY_NOT_REACHED();
        }

        auto line_start_point = context.rounded_device_point(fragment_box.top_left().translated(0, line_center_y));
        auto line_end_point = context.rounded_device_point(fragment_box.top_right().translated(0, line_center_y));

        // https://drafts.csswg.org/css-text-decor-4/#text-decoration-skip-ink-property
        // FIXME: For text-decoration-skip-ink: auto, skip CJK ideographs and symbols from the intercept
        //        computation, since their complex strokes would create too many gaps in the decoration line.
        auto skip_ink = text_node.computed_values().text_decoration_skip_ink();
        bool should_skip_ink = skip_ink != CSS::TextDecorationSkipInk::None
            && first_is_one_of(line, CSS::TextDecorationLine::Underline, CSS::TextDecorationLine::Overline);

        auto draw_line_for_segment = [&](DecorationSegment segment, int y, Gfx::LineStyle style = Gfx::LineStyle::Solid) {
            recorder.draw_line({ segment.start_x, y }, { segment.end_x, y }, line_color, device_line_thickness.value(), style);
        };

        auto segments = [&] -> Vector<DecorationSegment> {
            if (!should_skip_ink)
                return { { line_start_point.x().value(), line_end_point.x().value() } };
            return compute_skip_ink_segments(fragment, context, line_start_point.x().value(), line_end_point.x().value(),
                line_start_point.y().value(), device_line_thickness.value(), font.pixel_size());
        }();

        auto line_y = line_start_point.y().value();

        switch (line_style) {
        case CSS::TextDecorationStyle::Solid:
            for (auto segment : segments)
                draw_line_for_segment(segment, line_y);
            break;
        case CSS::TextDecorationStyle::Double: {
            // Two parallel lines with a 1px gap, expanding away from the text.
            int step = device_line_thickness.value() + 1;
            int first_y = line_y;
            int second_y = line_y;
            switch (line) {
            case CSS::TextDecorationLine::Underline:
                second_y += step;
                break;
            case CSS::TextDecorationLine::Overline:
                second_y -= step;
                break;
            case CSS::TextDecorationLine::LineThrough:
                first_y -= step / 2;
                second_y = first_y + step;
                break;
            default:
                VERIFY_NOT_REACHED();
            }
            for (auto segment : segments) {
                draw_line_for_segment(segment, first_y);
                draw_line_for_segment(segment, second_y);
            }
            break;
        }
        case CSS::TextDecorationStyle::Dashed:
            for (auto segment : segments)
                draw_line_for_segment(segment, line_y, Gfx::LineStyle::Dashed);
            break;
        case CSS::TextDecorationStyle::Dotted:
            for (auto segment : segments)
                draw_line_for_segment(segment, line_y, Gfx::LineStyle::Dotted);
            break;
        case CSS::TextDecorationStyle::Wavy: {
            // The wave oscillates amplitude/2 above and below its center, so shift the center away from the text so the
            // near peaks don’t overlap it.
            int amplitude = device_line_thickness.value() * 3;
            int wave_y = line_y;
            switch (line) {
            case CSS::TextDecorationLine::Underline:
                wave_y += device_line_thickness.value() / 2 + 1;
                break;
            case CSS::TextDecorationLine::Overline:
                wave_y -= device_line_thickness.value() / 2 + 1;
                break;
            case CSS::TextDecorationLine::LineThrough:
                break;
            default:
                VERIFY_NOT_REACHED();
            }
            for (auto segment : segments) {
                Gfx::IntPoint from { segment.start_x, wave_y };
                Gfx::IntPoint to { segment.end_x, wave_y };
                recorder.stroke_path({
                    .cap_style = Gfx::Path::CapStyle::Round,
                    .join_style = Gfx::Path::JoinStyle::Round,
                    .miter_limit = 0,
                    .dash_array = {},
                    .dash_offset = 0,
                    .path = build_triangle_wave_path(from, to, amplitude),
                    .paint_style_or_color = line_color,
                    .thickness = static_cast<float>(device_line_thickness.value()),
                });
            }
            break;
        }
        }
    }
}

Gfx::Path build_triangle_wave_path(Gfx::IntPoint from, Gfx::IntPoint to, float amplitude)
{
    Gfx::Path path;
    if (from.y() != to.y()) {
        dbgln("FIXME: Support more than horizontal waves");
        return path;
    }

    path.move_to(from.to_type<float>());

    float const wavelength = amplitude * 2.0f;
    float const half_wavelength = amplitude;
    float const quarter_wavelength = amplitude / 2.0f;

    auto position = from.to_type<float>();
    auto remaining = abs(to.x() - position.x());
    while (remaining > wavelength) {
        // Draw a whole wave
        path.line_to({ position.x() + quarter_wavelength, position.y() - quarter_wavelength });
        path.line_to({ position.x() + quarter_wavelength + half_wavelength, position.y() + quarter_wavelength });
        path.line_to({ position.x() + wavelength, (float)position.y() });
        position.translate_by({ wavelength, 0 });
        remaining = abs(to.x() - position.x());
    }

    // Up
    if (remaining > quarter_wavelength) {
        path.line_to({ position.x() + quarter_wavelength, position.y() - quarter_wavelength });
        position.translate_by({ quarter_wavelength, 0 });
        remaining = abs(to.x() - position.x());
    } else if (remaining >= 1) {
        auto fraction = remaining / quarter_wavelength;
        path.line_to({ position.x() + (fraction * quarter_wavelength), position.y() - (fraction * quarter_wavelength) });
        remaining = 0;
    }

    // Down
    if (remaining > half_wavelength) {
        path.line_to({ position.x() + half_wavelength, position.y() + quarter_wavelength });
        position.translate_by(half_wavelength, 0);
        remaining = abs(to.x() - position.x());
    } else if (remaining >= 1) {
        auto fraction = remaining / half_wavelength;
        path.line_to({ position.x() + (fraction * half_wavelength), position.y() - quarter_wavelength + (fraction * half_wavelength) });
        remaining = 0;
    }

    // Back to middle
    if (remaining >= 1) {
        auto fraction = remaining / quarter_wavelength;
        path.line_to({ position.x() + (fraction * quarter_wavelength), position.y() + ((1 - fraction) * quarter_wavelength) });
    }

    return path;
}

} // namespace Web::Painting
