// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Tests for clipping.

use crate::renderer::Renderer;
use crate::util::{circular_star, crossed_line_star, stops_green_blue};
use std::f64::consts::PI;
use vello_common::color::palette::css::{
    BLACK, BLUE, DARK_BLUE, DARK_GREEN, GREEN, REBECCA_PURPLE, RED,
};
use vello_common::kurbo::{Affine, BezPath, Circle, Point, Rect, Shape, Stroke};
use vello_common::peniko::Color;
use vello_common::peniko::Fill;
use vello_common::tile::Tile;
use vello_cpu::peniko::{
    Gradient, LinearGradientPosition, RadialGradientPosition, SweepGradientPosition,
};
use vello_dev_macros::vello_test;

#[vello_test(hybrid_tolerance = 1)]
fn clip_triangle_with_star(ctx: &mut impl Renderer) {
    let mut triangle_path = BezPath::new();
    triangle_path.move_to((10.0, 10.0));
    triangle_path.line_to((90.0, 20.0));
    triangle_path.line_to((20.0, 90.0));
    triangle_path.close_path();

    let stroke = Stroke::new(1.0);
    ctx.set_paint(DARK_BLUE);
    ctx.set_stroke(stroke);
    ctx.stroke_path(&triangle_path);

    let star_path = circular_star(Point::new(50., 50.), 13, 25., 45.);

    ctx.push_clip_layer(&star_path);
    ctx.set_paint(REBECCA_PURPLE);
    ctx.fill_path(&triangle_path);
    ctx.pop_layer();
}

#[vello_test]
fn clip_rectangle_with_star_nonzero(ctx: &mut impl Renderer) {
    let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
    // Create a self-intersecting star shape that will show the difference between fill rules
    let star_path = crossed_line_star();

    // Set the fill rule to NonZero before applying the clip
    ctx.set_fill_rule(Fill::NonZero);
    // Apply the star as a clip
    ctx.push_clip_layer(&star_path);
    // Draw a rectangle that should be clipped by the star
    // The NonZero fill rule will treat self-intersecting regions as filled
    ctx.set_paint(REBECCA_PURPLE);
    ctx.fill_rect(&rect);
    ctx.pop_layer();
}

#[vello_test]
fn clip_rectangle_with_star_evenodd(ctx: &mut impl Renderer) {
    let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
    // Create a self-intersecting star shape that will show the difference between fill rules
    let star_path = crossed_line_star();

    // Set the fill rule to EvenOdd before applying the clip
    ctx.set_fill_rule(Fill::EvenOdd);
    // Apply the star as a clip
    ctx.push_clip_layer(&star_path);
    // Draw a rectangle that should be clipped by the star
    // The EvenOdd rule should create a "hole" in the middle where the paths overlap
    ctx.set_paint(REBECCA_PURPLE);
    ctx.fill_rect(&rect);
    ctx.pop_layer();
}

#[vello_test(cpu_u8_tolerance = 1)]
fn clip_deeply_nested_circles(ctx: &mut impl Renderer) {
    const INITIAL_RADIUS: f64 = 48.0;
    const RADIUS_DECREMENT: f64 = 2.5;
    const INNER_COUNT: usize = 10;
    // `.ceil()` is not constant-evaluatable, so we have to do this at runtime.
    let outer_count: usize =
        (INITIAL_RADIUS / RADIUS_DECREMENT / INNER_COUNT as f64).ceil() as usize;
    const COLORS: [Color; INNER_COUNT] = [
        RED,
        DARK_BLUE,
        DARK_GREEN,
        REBECCA_PURPLE,
        BLACK,
        BLUE,
        GREEN,
        RED,
        DARK_BLUE,
        DARK_GREEN,
    ];

    const COVER_RECT: Rect = Rect::new(0.0, 0.0, 100.0, 100.0);
    const CENTER: Point = Point::new(50.0, 50.0);
    let mut radius = INITIAL_RADIUS;

    for _ in 0..outer_count {
        for color in COLORS.iter() {
            let clip_circle = Circle::new(CENTER, radius).to_path(0.1);
            draw_clipping_outline(ctx, &clip_circle);
            ctx.push_clip_layer(&clip_circle);

            ctx.set_paint(*color);
            ctx.fill_rect(&COVER_RECT);

            radius -= RADIUS_DECREMENT;
        }
    }
    for _ in 0..outer_count {
        for _ in COLORS.iter() {
            ctx.pop_layer();
        }
    }
}

#[vello_test(cpu_u8_tolerance = 1)]
fn clip_rectangle_and_circle(ctx: &mut impl Renderer) {
    // Create first clipping region - a rectangle on the left side
    let clip_rect = Rect::new(10.0, 30.0, 50.0, 70.0);

    // Create second clipping region - a circle on the right side
    let circle_center = Point::new(65.0, 50.0);
    let circle_radius = 30.0;
    let clip_circle = Circle::new(circle_center, circle_radius).to_path(0.1);

    // Draw outlines of our clipping regions to visualize them
    let stroke = Stroke::new(1.0);
    ctx.set_paint(DARK_BLUE);
    ctx.set_stroke(stroke);
    ctx.stroke_rect(&clip_rect);
    ctx.stroke_path(&clip_circle);

    // Apply both clips
    ctx.push_clip_layer(&clip_rect.to_path(0.1));
    ctx.push_clip_layer(&clip_circle);

    // Then a filled rectangle that covers most of the canvas
    let large_rect = Rect::new(0.0, 0.0, 100.0, 100.0);
    ctx.set_paint(REBECCA_PURPLE);
    ctx.fill_rect(&large_rect);
    ctx.pop_layer();
    ctx.pop_layer();
}

#[vello_test(width = 100, height = 60)]
fn clip_rect_cull_alignment(ctx: &mut impl Renderer) {
    const {
        assert!(
            Tile::HEIGHT >= 4 && Tile::HEIGHT <= 16,
            "This test only shows regressions if the tile height remains between 4 and 16 inclusive"
        );
    }

    // A strip-aligned Y value.
    const TOP_Y: f64 = Tile::HEIGHT as f64;

    // Another strip-aligned Y value lower down.
    // FIXME: make const once MSRV >= 1.90
    let bot_y: f64 = (46. / Tile::HEIGHT as f64).ceil() * Tile::HEIGHT as f64;

    let clip_rect = Rect::new(18.0, TOP_Y + 2., 78.0, bot_y - 2.);
    ctx.set_paint(DARK_GREEN.with_alpha(0.1));
    ctx.fill_rect(&clip_rect);

    // Draw a near-rectangle with slightly curved top and bottom edges to hit interesting
    // flattening paths.
    //
    // The shape's top segment is outside and above the clip path, so it may end up being culled.
    // However, the shape does not extend above the strip *row*, so the vertical segments of the
    // shape that connect to the horizontal segment do not contribute coarse winding. Therefore,
    // the horizontal segment cannot be ignored, as winding between the two vertical segments in
    // its strip row needs to be correctly accounted.
    //
    // The same holds for the bottom segment, except that that case is simpler in our pipeline:
    // coarse winding is accounted when geometry of the path passes the strip's top edge, which
    // means this strip row will either get a sparse fill, or will get dense tiles due to other
    // unculled geometry existing.
    let mut path = BezPath::new();
    let top_y = TOP_Y + 1.;
    let bot_y = bot_y - 1.;
    path.move_to((30.0, bot_y));
    path.line_to((30.0, top_y));
    path.curve_to((45.0, top_y - 0.5), (55.0, top_y - 0.5), (70.0, top_y));
    path.line_to((70.0, bot_y));
    path.curve_to((55.0, bot_y + 0.5), (45.0, bot_y + 0.5), (30.0, bot_y));
    ctx.push_clip_path(&clip_rect.to_path(0.1));
    ctx.set_paint(REBECCA_PURPLE);
    ctx.fill_path(&path);
    ctx.pop_clip_path();
}

#[vello_test]
fn clip_with_translation(ctx: &mut impl Renderer) {
    // Apply a translation transform
    ctx.set_transform(Affine::translate((30.0, 30.0)));

    // Create and apply a clipping rectangle
    let clip_rect = Rect::new(0.0, 0.0, 40.0, 40.0);
    draw_clipping_outline(ctx, &clip_rect.to_path(0.1));
    ctx.push_clip_layer(&clip_rect.to_path(0.1));

    // Draw a rectangle that should be clipped
    let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
    ctx.set_paint(REBECCA_PURPLE);
    ctx.fill_rect(&rect);
    ctx.pop_layer();
}

#[vello_test]
fn clip_with_scale(ctx: &mut impl Renderer) {
    ctx.set_transform(Affine::scale(2.0));

    // Create and apply a clipping rectangle
    let clip_rect = Rect::new(10.0, 10.0, 40.0, 40.0);
    draw_clipping_outline(ctx, &clip_rect.to_path(0.1));
    ctx.push_clip_layer(&clip_rect.to_path(0.1));

    // Draw a rectangle that should be clipped
    let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
    ctx.set_paint(REBECCA_PURPLE);
    ctx.fill_rect(&rect);
    ctx.pop_layer();
}

#[vello_test]
fn clip_with_rotate(ctx: &mut impl Renderer) {
    ctx.set_transform(Affine::rotate_about(
        45.0 * PI / 180.0,
        Point::new(50.0, 50.0),
    ));

    // Create and apply a clipping rectangle
    let clip_rect = Rect::new(20.0, 20.0, 80.0, 80.0);
    draw_clipping_outline(ctx, &clip_rect.to_path(0.1));
    ctx.push_clip_layer(&clip_rect.to_path(0.1));

    // Draw a rectangle that should be clipped
    let rect = Rect::new(20.0, 20.0, 80.0, 80.0);
    ctx.set_paint(REBECCA_PURPLE);
    ctx.fill_rect(&rect);
    ctx.pop_layer();
}

#[vello_test]
fn clip_transformed_rect(ctx: &mut impl Renderer) {
    let clip_rect = Rect::new(20.0, 20.0, 80.0, 80.0);

    draw_clipping_outline(ctx, &clip_rect.to_path(0.1));

    ctx.push_clip_layer(&clip_rect.to_path(0.1));

    ctx.set_transform(Affine::rotate_about(
        45.0 * PI / 180.0,
        Point::new(50.0, 50.0),
    ));

    // Draw a smaller rectangle that should be clipped
    let rect = Rect::new(20.0, 20.0, 80.0, 80.0);
    ctx.set_paint(REBECCA_PURPLE);
    ctx.fill_rect(&rect);
    ctx.pop_layer();
}

#[vello_test]
fn clip_with_multiple_transforms(ctx: &mut impl Renderer) {
    // Apply initial transform
    ctx.set_transform(Affine::rotate_about(
        45.0 * PI / 180.0,
        Point::new(50.0, 50.0),
    ));

    // Create and apply first clip
    let clip_rect1 = Rect::new(20.0, 20.0, 80.0, 80.0);
    draw_clipping_outline(ctx, &clip_rect1.to_path(0.1));
    ctx.push_clip_layer(&clip_rect1.to_path(0.1));

    // Apply another transform
    ctx.set_transform(Affine::scale(1.5));

    // Create and apply second clip
    let clip_rect2 = Rect::new(30.0, 30.0, 70.0, 70.0);
    draw_clipping_outline(ctx, &clip_rect2.to_path(0.1));
    ctx.push_clip_layer(&clip_rect2.to_path(0.1));

    // Draw a rectangle that should be clipped by both regions
    let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
    ctx.set_paint(REBECCA_PURPLE);
    ctx.fill_rect(&rect);
    ctx.pop_layer();
    ctx.pop_layer();
}

#[vello_test(hybrid_tolerance = 1)]
fn clip_with_save_restore(ctx: &mut impl Renderer) {
    // Create first clipping region - a rectangle on the left side
    let clip_rect1 = Rect::new(10.0, 30.0, 50.0, 70.0);
    draw_clipping_outline(ctx, &clip_rect1.to_path(0.1));
    ctx.push_clip_layer(&clip_rect1.to_path(0.1));

    // Add second clipping region - a circle on the right side
    let circle_center = Point::new(65.0, 50.0);
    let circle_radius = 30.0;
    let clip_circle = Circle::new(circle_center, circle_radius).to_path(0.1);
    draw_clipping_outline(ctx, &clip_circle);
    ctx.push_clip_layer(&clip_circle);

    // Draw a rectangle that should be clipped by both regions
    let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
    ctx.set_paint(REBECCA_PURPLE);
    ctx.fill_rect(&rect);

    // Restore to state before second clip
    ctx.pop_layer();

    // Draw another rectangle that should only be clipped by the first region
    let rect2 = Rect::new(0.0, 0.0, 100.0, 100.0);
    ctx.set_paint(DARK_GREEN.with_alpha(0.5));
    ctx.fill_rect(&rect2);
    ctx.pop_layer();
}

#[vello_test]
fn clip_with_opacity(ctx: &mut impl Renderer) {
    // Main body of the shape should be RGB 128, 128, 128. Anti-aliased part should be
    // 191, 191, 191.
    let clip_rect = Rect::new(10.5, 10.5, 89.5, 89.5);
    ctx.push_clip_layer(&clip_rect.to_path(0.1));
    ctx.set_paint(BLACK.with_alpha(0.5));
    ctx.fill_rect(&Rect::new(0.0, 0.0, 100.0, 100.0));
    ctx.pop_layer();
}

fn draw_clipping_outline(ctx: &mut impl Renderer, path: &BezPath) {
    let stroke = Stroke::new(1.0);
    ctx.set_paint(DARK_BLUE);
    ctx.set_stroke(stroke);
    ctx.stroke_path(path);
}

// See <https://github.com/linebender/vello/issues/917>
#[vello_test(no_ref)]
fn clip_exceeding_viewport(ctx: &mut impl Renderer) {
    ctx.push_clip_layer(&Rect::new(0.0, 0.0, 500.0, 10.0).to_path(0.1));
    ctx.fill_rect(&Rect::new(0.0, 0.0, 100.0, 100.0));
    ctx.pop_layer();
}

#[vello_test(width = 16, height = 16)]
fn clip_non_isolated_outside_canvas(ctx: &mut impl Renderer) {
    // Should be completely clipped.
    let clip_rect = Rect::new(0.0, 0.0, 16.0, 16.0);
    ctx.push_clip_path(&clip_rect.to_path(0.1));

    let rect = Rect::new(16.0, -16.0, 32.0, 0.0);
    ctx.set_paint(REBECCA_PURPLE);
    ctx.fill_rect(&rect);
    ctx.pop_clip_path();
}

#[vello_test]
fn clip_non_isolated_with_rect(ctx: &mut impl Renderer) {
    let clip_rect = Rect::new(10.0, 10.0, 90.0, 90.0);
    ctx.push_clip_path(&clip_rect.to_path(0.1));

    ctx.set_paint(BLUE);
    ctx.fill_rect(&Rect::new(0.0, 0.0, 100.0, 100.0));
    ctx.pop_clip_path();
}

#[vello_test]
fn clip_non_isolated_with_rotated_rect(ctx: &mut impl Renderer) {
    let clip_rect = Rect::new(10.0, 10.0, 90.0, 90.0);
    ctx.push_clip_path(&clip_rect.to_path(0.1));

    ctx.set_transform(Affine::rotate_about(
        25.0 * PI / 180.0,
        Point::new(50.0, 50.0),
    ));
    ctx.set_paint(BLUE);
    ctx.fill_rect(&Rect::new(0.0, 0.0, 100.0, 100.0));
    ctx.pop_clip_path();
}

#[vello_test]
fn clip_non_isolated_with_scaled_rect(ctx: &mut impl Renderer) {
    let clip_rect = Rect::new(10.0, 10.0, 90.0, 90.0);
    ctx.push_clip_path(&clip_rect.to_path(0.1));

    ctx.set_transform(
        Affine::translate((50.0, 50.0)) * Affine::scale(4.0) * Affine::translate((-50.0, -50.0)),
    );
    ctx.set_paint(BLUE);
    ctx.fill_rect(&Rect::new(40.0, 40.0, 60.0, 60.0));
    ctx.pop_clip_path();
}

#[vello_test]
fn clip_non_isolated_with_aa_with_rect(ctx: &mut impl Renderer) {
    let clip_rect = Rect::new(10.5, 10.5, 89.5, 89.5);
    ctx.push_clip_path(&clip_rect.to_path(0.1));

    ctx.set_paint(BLUE);
    ctx.fill_rect(&Rect::new(0.0, 0.0, 100.0, 100.0));
    ctx.pop_clip_path();
}

#[vello_test]
fn clip_non_isolated_with_aa_with_rect_aa(ctx: &mut impl Renderer) {
    // In theory, anti-aliasing should be 50% here, but due to conflation artifacts it will be
    // 25% instead.
    let rect = Rect::new(10.5, 10.5, 89.5, 89.5);
    ctx.push_clip_path(&rect.to_path(0.1));

    ctx.set_paint(BLUE);
    ctx.fill_rect(&rect);
    ctx.pop_clip_path();
}

#[vello_test]
fn clip_non_isolated_rectangle_with_star_evenodd(ctx: &mut impl Renderer) {
    let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
    let star_path = crossed_line_star();

    ctx.set_fill_rule(Fill::EvenOdd);
    ctx.push_clip_path(&star_path);
    ctx.set_paint(REBECCA_PURPLE);
    ctx.fill_rect(&rect);
    ctx.pop_clip_path();
}

#[vello_test(cpu_u8_tolerance = 1)]
fn clip_non_isolated_deeply_nested_circles(ctx: &mut impl Renderer) {
    const INITIAL_RADIUS: f64 = 48.0;
    const RADIUS_DECREMENT: f64 = 2.5;
    const INNER_COUNT: usize = 10;
    // `.ceil()` is not constant-evaluatable, so we have to do this at runtime.
    let outer_count: usize =
        (INITIAL_RADIUS / RADIUS_DECREMENT / INNER_COUNT as f64).ceil() as usize;
    const COLORS: [Color; INNER_COUNT] = [
        RED,
        DARK_BLUE,
        DARK_GREEN,
        REBECCA_PURPLE,
        BLACK,
        BLUE,
        GREEN,
        RED,
        DARK_BLUE,
        DARK_GREEN,
    ];

    const COVER_RECT: Rect = Rect::new(0.0, 0.0, 100.0, 100.0);
    const CENTER: Point = Point::new(50.0, 50.0);
    let mut radius = INITIAL_RADIUS;

    for _ in 0..outer_count {
        for color in COLORS.iter() {
            let clip_circle = Circle::new(CENTER, radius).to_path(0.1);
            draw_clipping_outline(ctx, &clip_circle);
            ctx.push_clip_path(&clip_circle);

            ctx.set_paint(*color);
            ctx.fill_rect(&COVER_RECT);

            radius -= RADIUS_DECREMENT;
        }
    }
    for _ in 0..outer_count {
        for _ in COLORS.iter() {
            ctx.pop_clip_path();
        }
    }
}

#[vello_test(width = 600, height = 200)]
fn clip_with_linear_gradient_fill(ctx: &mut impl Renderer) {
    // Border to see crop.
    ctx.fill_rect(&Rect::new(0., 0., 600., 200.));

    // Crop 10px border around image.
    let clip = Rect::new(10., 10., 590., 190.0);
    ctx.push_clip_layer(&clip.to_path(0.));

    // Fill a full screen rect with a gradient.
    let box_rect = Rect::new(0., 0., 600.0, 200.0);

    let gradient = Gradient {
        kind: LinearGradientPosition {
            start: Point::new(100.0, 100.0),
            end: Point::new(300.0, 100.0),
        }
        .into(),
        stops: stops_green_blue(),
        ..Default::default()
    };

    ctx.set_paint(gradient);
    ctx.fill_rect(&box_rect);

    ctx.pop_layer();
}

#[vello_test(width = 600, height = 200)]
fn clip_with_radial_gradient_fill(ctx: &mut impl Renderer) {
    // Border to see crop.
    ctx.fill_rect(&Rect::new(0., 0., 600., 200.));

    // Crop 10px border around image.
    let clip = Rect::new(10., 10., 590., 190.0);
    ctx.push_clip_layer(&clip.to_path(0.));

    // Fill a full screen rect with a gradient.
    let box_rect = Rect::new(0., 0., 600.0, 200.0);

    let gradient = Gradient {
        kind: RadialGradientPosition {
            start_center: Point::new(300.0, 100.0),
            start_radius: 0.0,
            end_center: Point::new(300.0, 100.0),
            end_radius: 150.0,
        }
        .into(),
        stops: stops_green_blue(),
        ..Default::default()
    };

    ctx.set_paint(gradient);
    ctx.fill_rect(&box_rect);

    ctx.pop_layer();
}

#[vello_test(width = 600, height = 200)]
fn clip_with_sweep_gradient_fill(ctx: &mut impl Renderer) {
    // Border to see crop.
    ctx.fill_rect(&Rect::new(0., 0., 600., 200.));

    // Crop 10px border around image.
    let clip = Rect::new(10., 10., 590., 190.0);
    ctx.push_clip_layer(&clip.to_path(0.));

    // Fill a full screen rect with a gradient.
    let box_rect = Rect::new(0., 0., 600.0, 200.0);

    let gradient = Gradient {
        kind: SweepGradientPosition {
            center: Point::new(300.0, 100.0),
            start_angle: 0.0,
            end_angle: std::f32::consts::TAU,
        }
        .into(),
        stops: stops_green_blue(),
        ..Default::default()
    };

    ctx.set_paint(gradient);
    ctx.fill_rect(&box_rect);

    ctx.pop_layer();
}

#[vello_test(width = 300, height = 30)]
fn clip_layer_encloses_viewport_via_left_cull(ctx: &mut impl Renderer) {
    let clip = Rect::new(-100.0, -100.0, 400.0, 130.0).to_path(0.1);

    ctx.push_clip_layer(&clip);
    ctx.set_paint(REBECCA_PURPLE);
    ctx.fill_rect(&Rect::new(0.0, 0.0, 300.0, 30.0));
    ctx.pop_layer();
}
