/*
 * Copyright (c) 2024, MacDue <macdue@dueutil.tech>
 * Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include "BasicShapeStyleValue.h"
#include <LibGfx/Path.h>
#include <LibWeb/CSS/Serialize.h>
#include <LibWeb/CSS/StyleValues/BorderRadiusRectStyleValue.h>
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
#include <LibWeb/CSS/StyleValues/RadialSizeStyleValue.h>
#include <LibWeb/CSS/ValueType.h>
#include <LibWeb/Painting/BorderRadiiData.h>
#include <LibWeb/Painting/Paintable.h>
#include <LibWeb/SVG/Path.h>

namespace Web::CSS {

static Gfx::Path path_from_resolved_rect(float top, float right, float bottom, float left)
{
    Gfx::Path path;
    path.move_to(Gfx::FloatPoint { left, top });
    path.line_to(Gfx::FloatPoint { right, top });
    path.line_to(Gfx::FloatPoint { right, bottom });
    path.line_to(Gfx::FloatPoint { left, bottom });
    path.close();
    return path;
}

// https://drafts.csswg.org/css-shapes/#funcdef-basic-shape-inset
Gfx::Path Inset::to_path(CSSPixelRect reference_box, Layout::Node const& node) const
{
    auto resolved_top = LengthPercentageOrAuto::from_style_value(top).to_px_or_zero(node, reference_box.height()).to_float();
    auto resolved_right = LengthPercentageOrAuto::from_style_value(right).to_px_or_zero(node, reference_box.width()).to_float();
    auto resolved_bottom = LengthPercentageOrAuto::from_style_value(bottom).to_px_or_zero(node, reference_box.height()).to_float();
    auto resolved_left = LengthPercentageOrAuto::from_style_value(left).to_px_or_zero(node, reference_box.width()).to_float();

    // A pair of insets in either dimension that add up to more than the used dimension
    // (such as left and right insets of 75% apiece) use the CSS Backgrounds 3 § 4.5 Overlapping Curves rules
    // to proportionally reduce the inset effect to 100%.
    if (resolved_top + resolved_bottom > reference_box.height().to_float() || resolved_left + resolved_right > reference_box.width().to_float()) {
        // https://drafts.csswg.org/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. If
        // f < 1, then all corner radii are reduced by multiplying them by f.

        // NB: We only care about vertical and horizontal here as top = bottom and left = right
        auto s_vertical = resolved_top + resolved_bottom;
        auto s_horizontal = resolved_left + resolved_right;

        auto f = min(reference_box.height() / s_vertical, reference_box.width() / s_horizontal);

        resolved_top *= f;
        resolved_right *= f;
        resolved_bottom *= f;
        resolved_left *= f;
    }

    float left_edge = resolved_left;
    float top_edge = resolved_top;
    float right_edge = reference_box.width().to_float() - resolved_right;
    float bottom_edge = reference_box.height().to_float() - resolved_bottom;

    CSSPixelRect inset_rect {
        CSSPixels(left_edge), CSSPixels(top_edge),
        CSSPixels(right_edge - left_edge), CSSPixels(bottom_edge - top_edge)
    };

    auto const& border_radius_rect = border_radius->as_border_radius_rect();

    auto to_border_radius_data = [](StyleValue const& corner) -> CSS::BorderRadiusData {
        auto const& br = corner.as_border_radius();
        return CSS::BorderRadiusData {
            LengthPercentage::from_style_value(br.horizontal_radius()),
            LengthPercentage::from_style_value(br.vertical_radius())
        };
    };

    auto radii = Painting::normalize_border_radii_data(
        node,
        inset_rect,
        reference_box,
        to_border_radius_data(*border_radius_rect.top_left()),
        to_border_radius_data(*border_radius_rect.top_right()),
        to_border_radius_data(*border_radius_rect.bottom_right()),
        to_border_radius_data(*border_radius_rect.bottom_left()));

    if (!radii.has_any_radius())
        return path_from_resolved_rect(top_edge, right_edge, bottom_edge, left_edge);

    auto top_left_horizontal_radius = radii.top_left.horizontal_radius.to_float();
    auto top_left_vertical_radius = radii.top_left.vertical_radius.to_float();

    auto top_right_horizontal_radius = radii.top_right.horizontal_radius.to_float();
    auto top_right_vertical_radius = radii.top_right.vertical_radius.to_float();

    auto bottom_right_horizontal_radius = radii.bottom_right.horizontal_radius.to_float();
    auto bottom_right_vertical_radius = radii.bottom_right.vertical_radius.to_float();

    auto bottom_left_horizontal_radius = radii.bottom_left.horizontal_radius.to_float();
    auto bottom_left_vertical_radius = radii.bottom_left.vertical_radius.to_float();

    Gfx::Path path;
    path.move_to({ left_edge + top_left_horizontal_radius, top_edge });
    path.line_to({ right_edge - top_right_horizontal_radius, top_edge });

    if (top_right_horizontal_radius > 0 && top_right_vertical_radius > 0)
        path.elliptical_arc_to({ right_edge, top_edge + top_right_vertical_radius }, { top_right_horizontal_radius, top_right_vertical_radius }, 0, false, true);

    path.line_to({ right_edge, bottom_edge - bottom_right_vertical_radius });
    if (bottom_right_horizontal_radius > 0 && bottom_right_vertical_radius > 0)
        path.elliptical_arc_to({ right_edge - bottom_right_horizontal_radius, bottom_edge }, { bottom_right_horizontal_radius, bottom_right_vertical_radius }, 0, false, true);

    path.line_to({ left_edge + bottom_left_horizontal_radius, bottom_edge });
    if (bottom_left_horizontal_radius > 0 && bottom_left_vertical_radius > 0)
        path.elliptical_arc_to({ left_edge, bottom_edge - bottom_left_vertical_radius }, { bottom_left_horizontal_radius, bottom_left_vertical_radius }, 0, false, true);

    path.line_to({ left_edge, top_edge + top_left_vertical_radius });
    if (top_left_horizontal_radius > 0 && top_left_vertical_radius > 0)
        path.elliptical_arc_to({ left_edge + top_left_horizontal_radius, top_edge }, { top_left_horizontal_radius, top_left_vertical_radius }, 0, false, true);

    path.close();
    return path;
}

void Inset::serialize(StringBuilder& builder, SerializationMode mode) const
{
    builder.append("inset("sv);
    builder.append(serialize_a_positional_value_list({ top, right, bottom, left }, mode));

    auto serialized_border_radius = border_radius->to_string(mode);

    if (serialized_border_radius != "0px"sv)
        builder.appendff(" round {}", serialized_border_radius);

    builder.append(')');
}

void Xywh::serialize(StringBuilder& builder, SerializationMode mode) const
{
    builder.append("xywh("sv);
    x->serialize(builder, mode);
    builder.append(' ');
    y->serialize(builder, mode);
    builder.append(' ');
    width->serialize(builder, mode);
    builder.append(' ');
    height->serialize(builder, mode);

    auto serialized_border_radius = border_radius->to_string(mode);
    if (serialized_border_radius != "0px"sv)
        builder.appendff(" round {}", serialized_border_radius);

    builder.append(')');
}

void Rect::serialize(StringBuilder& builder, SerializationMode mode) const
{
    builder.append("rect("sv);
    top->serialize(builder, mode);
    builder.append(' ');
    right->serialize(builder, mode);
    builder.append(' ');
    bottom->serialize(builder, mode);
    builder.append(' ');
    left->serialize(builder, mode);

    auto serialized_border_radius = border_radius->to_string(mode);
    if (serialized_border_radius != "0px"sv)
        builder.appendff(" round {}", serialized_border_radius);

    builder.append(')');
}

Gfx::Path Circle::to_path(CSSPixelRect reference_box, Layout::Node const& node) const
{
    // Translating the reference box because PositionStyleValues are resolved to an absolute position.
    auto translated_reference_box = reference_box.translated(-reference_box.x(), -reference_box.y());

    // https://www.w3.org/TR/css-shapes/#funcdef-basic-shape-circle
    // The <position> argument defines the center of the circle. Unless otherwise specified, this defaults to center if omitted.
    RefPtr<PositionStyleValue const> resolved_position = PositionStyleValue::create_computed_center();
    if (position)
        resolved_position = position->as_position();

    auto center = resolved_position->resolved(node, translated_reference_box);

    auto radius_px = radius->as_radial_size().resolve_circle_size(center, translated_reference_box, node).to_float();

    Gfx::Path path;
    path.move_to(Gfx::FloatPoint { center.x().to_float(), center.y().to_float() + radius_px });
    path.arc_to(Gfx::FloatPoint { center.x().to_float(), center.y().to_float() - radius_px }, radius_px, true, true);
    path.arc_to(Gfx::FloatPoint { center.x().to_float(), center.y().to_float() + radius_px }, radius_px, true, true);
    return path;
}

void Circle::serialize(StringBuilder& builder, SerializationMode mode) const
{
    builder.append("circle("sv);
    auto serialized_radius = radius->to_string(mode);

    bool has_radius = serialized_radius != "closest-side"sv;
    if (has_radius)
        builder.append(serialized_radius);

    if (position) {
        if (has_radius)
            builder.append(' ');
        builder.append("at "sv);
        position->serialize(builder, mode);
    }

    builder.append(')');
}

Gfx::Path Ellipse::to_path(CSSPixelRect reference_box, Layout::Node const& node) const
{
    // Translating the reference box because PositionStyleValues are resolved to an absolute position.
    auto translated_reference_box = reference_box.translated(-reference_box.x(), -reference_box.y());

    // https://www.w3.org/TR/css-shapes/#funcdef-basic-shape-circle
    // The <position> argument defines the center of the ellipse. Unless otherwise specified, this defaults to center if omitted.
    RefPtr<PositionStyleValue const> resolved_position = PositionStyleValue::create_computed_center();
    if (position)
        resolved_position = position->as_position();

    auto center = resolved_position->resolved(node, translated_reference_box);
    auto size = radius->as_radial_size().resolve_ellipse_size(center, translated_reference_box, node);

    Gfx::Path path;
    path.move_to(Gfx::FloatPoint { center.x().to_float(), center.y().to_float() + size.height().to_float() });
    path.elliptical_arc_to(Gfx::FloatPoint { center.x().to_float(), center.y().to_float() - size.height().to_float() }, Gfx::FloatSize { size.width().to_float(), size.height().to_float() }, 0, true, true);
    path.elliptical_arc_to(Gfx::FloatPoint { center.x().to_float(), center.y().to_float() + size.height().to_float() }, Gfx::FloatSize { size.width().to_float(), size.height().to_float() }, 0, true, true);
    return path;
}

void Ellipse::serialize(StringBuilder& builder, SerializationMode mode) const
{
    builder.append("ellipse("sv);
    auto serialized_radius = radius->to_string(mode);

    bool has_radius = serialized_radius != "closest-side closest-side"sv;
    if (has_radius)
        builder.append(serialized_radius);

    if (position) {
        if (has_radius)
            builder.append(' ');
        builder.append("at "sv);
        position->serialize(builder, mode);
    }

    builder.append(')');
}

Gfx::Path Polygon::to_path(CSSPixelRect reference_box, Layout::Node const& node) const
{
    Gfx::Path path;
    path.set_fill_type(fill_rule);
    bool first = true;
    for (auto const& point : points) {
        Gfx::FloatPoint resolved_point {
            LengthPercentage::from_style_value(point.x).to_px(node, reference_box.width()).to_float(),
            LengthPercentage::from_style_value(point.y).to_px(node, reference_box.height()).to_float()
        };
        if (first)
            path.move_to(resolved_point);
        else
            path.line_to(resolved_point);
        first = false;
    }
    path.close();
    return path;
}

void Polygon::serialize(StringBuilder& builder, SerializationMode mode) const
{
    builder.append("polygon("sv);
    bool first = true;
    switch (fill_rule) {
    case Gfx::WindingRule::Nonzero:
        break;
    case Gfx::WindingRule::EvenOdd:
        first = false;
        builder.append("evenodd"sv);
    }
    for (auto const& point : points) {
        if (!first)
            builder.append(", "sv);
        first = false;
        point.x->serialize(builder, mode);
        builder.append(' ');
        point.y->serialize(builder, mode);
    }
    builder.append(')');
}

Gfx::Path Path::to_path(CSSPixelRect, Layout::Node const&) const
{
    auto result = path_instructions.to_gfx_path();
    result.set_fill_type(fill_rule);
    return result;
}

// https://drafts.csswg.org/css-shapes/#basic-shape-serialization
void Path::serialize(StringBuilder& builder, SerializationMode mode) const
{
    builder.append("path("sv);

    // For serializing computed values, component values are computed, and omitted when possible without changing the meaning.
    // NB: So, we don't include `nonzero` in that case.
    if (!(mode == SerializationMode::ResolvedValue && fill_rule == Gfx::WindingRule::Nonzero)) {
        switch (fill_rule) {
        case Gfx::WindingRule::Nonzero:
            builder.append("nonzero, "sv);
            break;
        case Gfx::WindingRule::EvenOdd:
            builder.append("evenodd, "sv);
        }
    }

    serialize_a_string(builder, path_instructions.serialize());

    builder.append(')');
}

BasicShapeStyleValue::~BasicShapeStyleValue() = default;

Gfx::Path BasicShapeStyleValue::to_path(CSSPixelRect reference_box, Layout::Node const& node) const
{
    return m_basic_shape.visit([&](auto const& shape) -> Gfx::Path {
        // NB: Xywh and Rect don't require to_path functions as we should have already converted them to their
        //     respective Inset equivalents during absolutization
        if constexpr (requires { shape.to_path(reference_box, node); }) {
            return shape.to_path(reference_box, node);
        }

        VERIFY_NOT_REACHED();
    });
}

void BasicShapeStyleValue::serialize(StringBuilder& builder, SerializationMode mode) const
{
    m_basic_shape.visit([&](auto const& shape) {
        shape.serialize(builder, mode);
    });
}

// https://www.w3.org/TR/css-shapes-1/#basic-shape-computed-values
ValueComparingNonnullRefPtr<StyleValue const> BasicShapeStyleValue::absolutized(ComputationContext const& computation_context) const
{
    // The values in a <basic-shape> function are computed as specified, with these exceptions:
    // - Omitted values are included and compute to their defaults.
    // FIXME: - A <position> value in circle() or ellipse() is computed as a pair of offsets (horizontal then vertical) from the top left origin, each given as a <length-percentage>.
    // FIXME: - A <'border-radius'> value in a <basic-shape-rect> function is computed as an expanded list of all eight <length-percentage> values.
    // - All <basic-shape-rect> functions compute to the equivalent inset() function.

    CalculationContext calculation_context { .percentages_resolve_as = ValueType::Length };

    auto const one_hundred_percent_minus = [&](Vector<NonnullRefPtr<StyleValue const>> const& values, CalculationContext const& calculation_context) {
        Vector<NonnullRefPtr<CalculationNode const>> sum_components = { NumericCalculationNode::create(Percentage { 100 }, calculation_context) };

        for (auto const& value : values)
            sum_components.append(NegateCalculationNode::create(CalculationNode::from_style_value(value, calculation_context)));

        return CalculatedStyleValue::create(SumCalculationNode::create(sum_components), NumericType { NumericType::BaseType::Length, 1 }, calculation_context);
    };

    auto const absolutize_if_nonnull = [&](RefPtr<StyleValue const> const& value) -> ValueComparingRefPtr<StyleValue const> {
        if (!value)
            return nullptr;
        return value->absolutized(computation_context);
    };

    auto absolutized_shape = m_basic_shape.visit(
        [&](Inset const& shape) -> BasicShape {
            auto absolutized_top = shape.top->absolutized(computation_context);
            auto absolutized_right = shape.right->absolutized(computation_context);
            auto absolutized_bottom = shape.bottom->absolutized(computation_context);
            auto absolutized_left = shape.left->absolutized(computation_context);

            auto absolutized_border_radius = shape.border_radius->absolutized(computation_context);

            if (absolutized_top == shape.top && absolutized_right == shape.right && absolutized_bottom == shape.bottom && absolutized_left == shape.left && absolutized_border_radius == shape.border_radius)
                return shape;

            return Inset { absolutized_top, absolutized_right, absolutized_bottom, absolutized_left, absolutized_border_radius };
        },
        [&](Xywh const& shape) -> BasicShape {
            // Note: Given xywh(x y w h), the equivalent function is inset(y calc(100% - x - w) calc(100% - y - h) x).
            auto absolutized_top = shape.y->absolutized(computation_context);
            auto absolutized_right = one_hundred_percent_minus({ shape.x, shape.width }, calculation_context)->absolutized(computation_context);
            auto absolutized_bottom = one_hundred_percent_minus({ shape.y, shape.height }, calculation_context)->absolutized(computation_context);
            auto absolutized_left = shape.x->absolutized(computation_context);
            auto absolutized_border_radius = shape.border_radius->absolutized(computation_context);

            return Inset { *absolutized_top, *absolutized_right, *absolutized_bottom, *absolutized_left, absolutized_border_radius };
        },
        [&](Rect const& shape) -> BasicShape {
            // Note: Given rect(t r b l), the equivalent function is inset(t calc(100% - r) calc(100% - b) l).

            auto resolve_auto = [](ValueComparingNonnullRefPtr<StyleValue const> const& style_value, Percentage value_of_auto) -> ValueComparingNonnullRefPtr<StyleValue const> {
                // An auto value makes the edge of the box coincide with the corresponding edge of the reference box:
                // it’s equivalent to 0% as the first (top) or fourth (left) value, and equivalent to 100% as the second
                // (right) or third (bottom) value.
                if (style_value->is_keyword()) {
                    VERIFY(style_value->to_keyword() == Keyword::Auto);
                    return PercentageStyleValue::create(value_of_auto);
                }

                return style_value;
            };

            auto absolutized_top = resolve_auto(shape.top, Percentage { 0 })->absolutized(computation_context);
            auto absolutized_right = one_hundred_percent_minus({ resolve_auto(shape.right, Percentage { 100 }) }, calculation_context)->absolutized(computation_context);
            auto absolutized_bottom = one_hundred_percent_minus({ resolve_auto(shape.bottom, Percentage { 100 }) }, calculation_context)->absolutized(computation_context);
            auto absolutized_left = resolve_auto(shape.left, Percentage { 0 })->absolutized(computation_context);
            auto absolutized_border_radius = shape.border_radius->absolutized(computation_context);

            return Inset { *absolutized_top, *absolutized_right, *absolutized_bottom, *absolutized_left, absolutized_border_radius };
        },
        [&](Circle const& shape) -> BasicShape {
            auto absolutized_radius = shape.radius->absolutized(computation_context);
            auto absolutized_position = absolutize_if_nonnull(shape.position);

            if (absolutized_radius == shape.radius && absolutized_position == shape.position)
                return shape;

            return Circle { absolutized_radius, absolutized_position };
        },
        [&](Ellipse const& shape) -> BasicShape {
            auto absolutized_radius = shape.radius->absolutized(computation_context);
            auto absolutized_position = absolutize_if_nonnull(shape.position);

            if (absolutized_radius == shape.radius && absolutized_position == shape.position)
                return shape;

            return Ellipse { absolutized_radius, absolutized_position };
        },
        [&](Polygon const& shape) -> BasicShape {
            Vector<Polygon::Point> absolutized_points;
            absolutized_points.ensure_capacity(shape.points.size());

            bool any_point_required_absolutization = false;

            for (auto const& point : shape.points) {
                auto absolutized_x = point.x->absolutized(computation_context);
                auto absolutized_y = point.y->absolutized(computation_context);

                if (absolutized_x == point.x && absolutized_y == point.y) {
                    absolutized_points.append(point);
                    continue;
                }

                any_point_required_absolutization = true;
                absolutized_points.append({ absolutized_x, absolutized_y });
            }

            if (!any_point_required_absolutization)
                return shape;

            return Polygon { shape.fill_rule, absolutized_points };
        },
        [&](Path const& shape) -> BasicShape {
            return shape;
        });

    if (absolutized_shape == m_basic_shape)
        return *this;

    return BasicShapeStyleValue::create(absolutized_shape);
}

}
