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

use crate::gradient::tan_45;
use crate::load_image;
use crate::mask::example_mask;
use crate::renderer::Renderer;
use crate::util::crossed_line_star;
use std::f64::consts::PI;
use std::sync::Arc;
use vello_common::color::palette::css::REBECCA_PURPLE;
use vello_common::kurbo::{Affine, Point, Rect};
use vello_common::kurbo::{Shape, Triangle};
use vello_common::paint::{Image, ImageSource, Tint, TintMode};
use vello_common::peniko::Color;
use vello_common::peniko::ImageSampler;
use vello_common::peniko::{BlendMode, Compose, Extend, ImageQuality, Mix};
use vello_dev_macros::vello_test;

fn rgb_img_10x10(ctx: &mut impl Renderer) -> ImageSource {
    ctx.get_image_source(load_image!("rgb_image_10x10"))
}

fn rgb_img_10x10_alpha_multiplied(ctx: &mut impl Renderer, alpha: u8) -> ImageSource {
    let mut pix = load_image!("rgb_image_10x10");
    Arc::make_mut(&mut pix).multiply_alpha(alpha);
    ctx.get_image_source(pix)
}

fn rgb_img_2x2(ctx: &mut impl Renderer) -> ImageSource {
    ctx.get_image_source(load_image!("rgb_image_2x2"))
}

fn rgb_img_2x3(ctx: &mut impl Renderer) -> ImageSource {
    ctx.get_image_source(load_image!("rgb_image_2x3"))
}

fn rgba_img_10x10(ctx: &mut impl Renderer) -> ImageSource {
    ctx.get_image_source(load_image!("rgba_image_10x10"))
}

fn luma_img_10x10(ctx: &mut impl Renderer) -> ImageSource {
    ctx.get_image_source(load_image!("luma_image_10x10"))
}

fn lumaa_img_10x10(ctx: &mut impl Renderer) -> ImageSource {
    ctx.get_image_source(load_image!("lumaa_image_10x10"))
}

fn repeat(ctx: &mut impl Renderer, x_extend: Extend, y_extend: Extend) {
    let rect = Rect::new(10.0, 10.0, 90.0, 90.0);
    let image_source = rgb_img_10x10(ctx);

    ctx.set_paint_transform(Affine::translate((45.0, 45.0)));
    ctx.set_paint(Image {
        image: image_source,
        sampler: ImageSampler {
            x_extend,
            y_extend,
            quality: ImageQuality::Low,
            alpha: 1.0,
        },
    });
    ctx.fill_rect(&rect);
}

#[vello_test]
fn image_reflect_x_pad_y(ctx: &mut impl Renderer) {
    repeat(ctx, Extend::Reflect, Extend::Pad);
}

#[vello_test]
fn image_pad_x_repeat_y(ctx: &mut impl Renderer) {
    repeat(ctx, Extend::Pad, Extend::Repeat);
}

#[vello_test]
fn image_reflect_x_reflect_y(ctx: &mut impl Renderer) {
    repeat(ctx, Extend::Reflect, Extend::Reflect);
}

#[vello_test]
fn image_repeat_x_repeat_y(ctx: &mut impl Renderer) {
    repeat(ctx, Extend::Repeat, Extend::Repeat);
}

#[vello_test]
fn image_pad_x_pad_y(ctx: &mut impl Renderer) {
    repeat(ctx, Extend::Pad, Extend::Pad);
}

// TODO: Doesn't work correctly in Hybrid due to a difference in sampling behavior.
#[vello_test(skip_hybrid)]
fn image_bilinear_repeat_x_pad_y(ctx: &mut impl Renderer) {
    let rect = Rect::new(10.0, 10.0, 90.0, 90.0);
    let image_source = rgb_img_2x2(ctx);

    ctx.set_paint_transform(Affine::scale(10.0));
    ctx.set_paint(Image {
        image: image_source,
        sampler: ImageSampler {
            x_extend: Extend::Repeat,
            y_extend: Extend::Pad,
            quality: ImageQuality::Medium,
            alpha: 1.0,
        },
    });
    ctx.fill_rect(&rect);
}

fn transform(ctx: &mut impl Renderer, transform: Affine, l: f64, t: f64, r: f64, b: f64) {
    let rect = Rect::new(l, t, r, b);
    let image_source = rgb_img_10x10(ctx);

    let image = Image {
        image: image_source,
        sampler: ImageSampler {
            x_extend: Extend::Repeat,
            y_extend: Extend::Repeat,
            quality: ImageQuality::Low,
            alpha: 1.0,
        },
    };

    ctx.set_transform(transform);
    ctx.set_paint(image);
    ctx.fill_rect(&rect);
}

#[vello_test]
fn image_with_transform_identity(ctx: &mut impl Renderer) {
    transform(ctx, Affine::IDENTITY, 25.0, 25.0, 75.0, 75.0);
}

#[vello_test]
fn image_with_transform_translate(ctx: &mut impl Renderer) {
    transform(ctx, Affine::translate((25.0, 25.0)), 0.0, 0.0, 50.0, 50.0);
}

#[vello_test]
fn image_with_transform_scale(ctx: &mut impl Renderer) {
    transform(ctx, Affine::scale(2.0), 12.5, 12.5, 37.5, 37.5);
}

#[vello_test]
fn image_with_transform_negative_scale(ctx: &mut impl Renderer) {
    transform(
        ctx,
        Affine::translate((100.0, 100.0)) * Affine::scale(-2.0),
        12.5,
        12.5,
        37.5,
        37.5,
    );
}

#[vello_test]
fn image_with_transform_scale_and_translate(ctx: &mut impl Renderer) {
    transform(
        ctx,
        Affine::new([2.0, 0.0, 0.0, 2.0, 25.0, 25.0]),
        0.0,
        0.0,
        25.0,
        25.0,
    );
}

#[vello_test(ignore = "fails in Windows CI for some reason.")]
fn image_with_transform_rotate_1(ctx: &mut impl Renderer) {
    transform(
        ctx,
        Affine::rotate_about(PI / 4.0, Point::new(50.0, 50.0)),
        25.0,
        25.0,
        75.0,
        75.0,
    );
}

#[vello_test(ignore = "fails in Windows CI for some reason.")]
fn image_with_transform_rotate_2(ctx: &mut impl Renderer) {
    transform(
        ctx,
        Affine::rotate_about(-PI / 4.0, Point::new(50.0, 50.0)),
        25.0,
        25.0,
        75.0,
        75.0,
    );
}

#[vello_test]
fn image_with_transform_scaling_non_uniform(ctx: &mut impl Renderer) {
    transform(
        ctx,
        Affine::scale_non_uniform(1.0, 2.0),
        25.0,
        12.5,
        75.0,
        37.5,
    );
}

#[vello_test]
fn image_with_transform_skew_x_1(ctx: &mut impl Renderer) {
    transform(
        ctx,
        Affine::translate((-50.0, 0.0)) * Affine::skew(tan_45(), 0.0),
        25.0,
        25.0,
        75.0,
        75.0,
    );
}

#[vello_test]
fn image_with_transform_skew_x_2(ctx: &mut impl Renderer) {
    transform(
        ctx,
        Affine::translate((50.0, 0.0)) * Affine::skew(-tan_45(), 0.0),
        25.0,
        25.0,
        75.0,
        75.0,
    );
}

#[vello_test]
fn image_with_transform_skew_y_1(ctx: &mut impl Renderer) {
    transform(
        ctx,
        Affine::translate((0.0, 50.0)) * Affine::skew(0.0, -tan_45()),
        25.0,
        25.0,
        75.0,
        75.0,
    );
}

#[vello_test]
fn image_with_transform_skew_y_2(ctx: &mut impl Renderer) {
    transform(
        ctx,
        Affine::translate((0.0, -50.0)) * Affine::skew(0.0, tan_45()),
        25.0,
        25.0,
        75.0,
        75.0,
    );
}

#[vello_test]
fn image_complex_shape(ctx: &mut impl Renderer) {
    let path = crossed_line_star();
    let image_source = rgb_img_10x10(ctx);

    let image = Image {
        image: image_source,
        sampler: ImageSampler {
            x_extend: Extend::Repeat,
            y_extend: Extend::Repeat,
            quality: ImageQuality::Low,
            alpha: 1.0,
        },
    };

    ctx.set_paint(image);
    ctx.fill_path(&path);
}

#[vello_test]
fn image_global_alpha(ctx: &mut impl Renderer) {
    let rect = Rect::new(10.0, 10.0, 90.0, 90.0);

    let image = Image {
        image: rgb_img_10x10_alpha_multiplied(ctx, 75),
        sampler: ImageSampler {
            x_extend: Extend::Repeat,
            y_extend: Extend::Repeat,
            quality: ImageQuality::Low,
            alpha: 1.0,
        },
    };

    ctx.set_paint(image);
    ctx.fill_rect(&rect);
}

#[vello_test]
fn image_with_opacity(ctx: &mut impl Renderer) {
    ctx.set_paint(REBECCA_PURPLE);
    ctx.fill_rect(&Rect::new(0.0, 0.0, 100.0, 100.0));

    ctx.push_opacity_layer(0.5);

    let rect = Rect::new(10.0, 10.0, 90.0, 90.0);
    let image_source = rgb_img_10x10(ctx);

    let image = Image {
        image: image_source,
        sampler: ImageSampler {
            x_extend: Extend::Repeat,
            y_extend: Extend::Repeat,
            quality: ImageQuality::Low,
            alpha: 1.0,
        },
    };

    ctx.set_paint(image);
    ctx.fill_rect(&rect);

    ctx.pop_layer();
}

fn image_format(ctx: &mut impl Renderer, image_source: ImageSource) {
    let rect = Rect::new(10.0, 10.0, 90.0, 90.0);

    let image = Image {
        image: image_source,
        sampler: ImageSampler {
            x_extend: Extend::Repeat,
            y_extend: Extend::Repeat,
            quality: ImageQuality::Low,
            alpha: 1.0,
        },
    };

    ctx.set_paint(image);
    ctx.fill_rect(&rect);
}

#[vello_test]
fn image_rgb_image(ctx: &mut impl Renderer) {
    let image_source = rgb_img_10x10(ctx);
    image_format(ctx, image_source);
}

#[vello_test]
fn image_rgba_image(ctx: &mut impl Renderer) {
    let image_source = rgba_img_10x10(ctx);
    image_format(ctx, image_source);
}

#[vello_test]
fn image_luma_image(ctx: &mut impl Renderer) {
    let image_source = luma_img_10x10(ctx);
    image_format(ctx, image_source);
}

#[vello_test]
fn image_lumaa_image(ctx: &mut impl Renderer) {
    let image_source = lumaa_img_10x10(ctx);
    image_format(ctx, image_source);
}

#[vello_test]
fn image_with_anti_aliasing(ctx: &mut impl Renderer) {
    let rect = Rect::new(10.5, 10.5, 90.5, 90.5);
    let image = Image {
        image: rgb_img_10x10(ctx),
        sampler: ImageSampler {
            x_extend: Extend::Repeat,
            y_extend: Extend::Repeat,
            quality: ImageQuality::Low,
            alpha: 1.0,
        },
    };

    ctx.set_paint(image);
    ctx.fill_rect(&rect);
}

#[vello_test]
fn image_opaque_with_mask(ctx: &mut impl Renderer) {
    let rect = Rect::new(10.0, 10.0, 90.0, 90.0);
    let image = Image {
        image: rgb_img_2x2(ctx),
        sampler: ImageSampler {
            x_extend: Extend::Repeat,
            y_extend: Extend::Repeat,
            quality: ImageQuality::Low,
            alpha: 1.0,
        },
    };

    ctx.set_mask(example_mask(true));
    ctx.set_paint(image);
    ctx.set_paint_transform(Affine::scale(50.0));
    ctx.fill_rect(&rect);
}

#[vello_test]
fn image_opaque_with_blend_mode(ctx: &mut impl Renderer) {
    let rect = Rect::new(10.0, 10.0, 90.0, 90.0);
    let image = Image {
        image: rgb_img_2x2(ctx),
        sampler: ImageSampler {
            x_extend: Extend::Repeat,
            y_extend: Extend::Repeat,
            quality: ImageQuality::Low,
            alpha: 1.0,
        },
    };

    ctx.set_paint(REBECCA_PURPLE);
    ctx.fill_rect(&rect);
    ctx.set_blend_mode(BlendMode::new(Mix::Difference, Compose::SrcOver));
    ctx.set_paint(image);
    ctx.set_paint_transform(Affine::scale(50.0));
    ctx.fill_rect(&rect);
}

fn quality(
    ctx: &mut impl Renderer,
    transform: Affine,
    image_source: ImageSource,
    quality: ImageQuality,
    extend: Extend,
) {
    let rect = Rect::new(10.0, 10.0, 90.0, 90.0);

    ctx.set_paint_transform(transform);
    let image = Image {
        image: image_source,
        sampler: ImageSampler {
            x_extend: extend,
            y_extend: extend,
            quality,
            alpha: 1.0,
        },
    };

    ctx.set_paint(image);
    ctx.fill_rect(&rect);
}

// Outputs of those tests were compared against Blend2D and tiny-skia.

#[vello_test]
fn image_bilinear_identity(ctx: &mut impl Renderer) {
    let image_source = rgb_img_2x2(ctx);
    quality(
        ctx,
        Affine::IDENTITY,
        image_source,
        ImageQuality::Medium,
        Extend::Reflect,
    );
}

#[vello_test]
fn image_bilinear_2x_scale(ctx: &mut impl Renderer) {
    let image_source = rgb_img_2x2(ctx);
    quality(
        ctx,
        Affine::scale(2.0),
        image_source,
        ImageQuality::Medium,
        Extend::Reflect,
    );
}

#[vello_test]
fn image_bilinear_5x_scale(ctx: &mut impl Renderer) {
    let image_source = rgb_img_2x2(ctx);
    quality(
        ctx,
        Affine::scale(5.0),
        image_source,
        ImageQuality::Medium,
        Extend::Reflect,
    );
}

#[vello_test]
fn image_bilinear_10x_scale(ctx: &mut impl Renderer) {
    let image_source = rgb_img_2x2(ctx);
    quality(
        ctx,
        Affine::scale(10.0),
        image_source,
        ImageQuality::Medium,
        Extend::Reflect,
    );
}

#[vello_test]
fn image_bilinear_with_rotation(ctx: &mut impl Renderer) {
    let image_source = rgb_img_2x2(ctx);
    quality(
        ctx,
        Affine::scale(5.0) * Affine::rotate(45.0_f64.to_radians()),
        image_source,
        ImageQuality::Medium,
        Extend::Reflect,
    );
}

#[vello_test]
fn image_bilinear_with_translation(ctx: &mut impl Renderer) {
    let image_source = rgb_img_2x2(ctx);
    quality(
        ctx,
        Affine::scale(5.0) * Affine::translate((10.0, 10.0)),
        image_source,
        ImageQuality::Medium,
        Extend::Reflect,
    );
}

#[vello_test]
fn image_bilinear_10x_scale_2(ctx: &mut impl Renderer) {
    let image_source = rgb_img_2x3(ctx);
    quality(
        ctx,
        Affine::scale(10.0),
        image_source,
        ImageQuality::Medium,
        Extend::Reflect,
    );
}

// This one looks slightly different from tiny-skia. In tiny-skia, it looks exactly the same as with
// `Nearest`, while in our case it looks overall a bit darker. I'm not 100% sure who is right here,
// but I think ours should be correct, because AFAIK for bicubic scaling, the output image does
// not necessarily need to look the same as with `Nearest` with identity scaling. Would be nice to
// verify this somehow, though.
//
// We also ported the cubic polynomials directly from current Skia, while tiny-skia (seems?) to use
// either an outdated version or a slightly adapted one.
#[vello_test]
fn image_bicubic_identity(ctx: &mut impl Renderer) {
    let image_source = rgb_img_2x2(ctx);
    quality(
        ctx,
        Affine::IDENTITY,
        image_source,
        ImageQuality::High,
        Extend::Reflect,
    );
}

#[vello_test(hybrid_tolerance = 3)]
fn image_bicubic_2x_scale(ctx: &mut impl Renderer) {
    let image_source = rgb_img_2x2(ctx);
    quality(
        ctx,
        Affine::scale(2.0),
        image_source,
        ImageQuality::High,
        Extend::Reflect,
    );
}

#[vello_test(hybrid_tolerance = 5)]
fn image_bicubic_5x_scale(ctx: &mut impl Renderer) {
    let image_source = rgb_img_2x2(ctx);
    quality(
        ctx,
        Affine::scale(5.0),
        image_source,
        ImageQuality::High,
        Extend::Reflect,
    );
}

#[vello_test(hybrid_tolerance = 7)]
fn image_bicubic_10x_scale(ctx: &mut impl Renderer) {
    let image_source = rgb_img_2x2(ctx);
    quality(
        ctx,
        Affine::scale(10.0),
        image_source,
        ImageQuality::High,
        Extend::Reflect,
    );
}

#[vello_test(hybrid_tolerance = 7)]
fn image_bicubic_with_rotation(ctx: &mut impl Renderer) {
    let image_source = rgb_img_2x2(ctx);
    quality(
        ctx,
        Affine::scale(5.0) * Affine::rotate(45.0_f64.to_radians()),
        image_source,
        ImageQuality::High,
        Extend::Reflect,
    );
}

#[vello_test(hybrid_tolerance = 5)]
fn image_bicubic_with_translation(ctx: &mut impl Renderer) {
    let image_source = rgb_img_2x2(ctx);
    quality(
        ctx,
        Affine::scale(5.0) * Affine::translate((10.0, 10.0)),
        image_source,
        ImageQuality::High,
        Extend::Reflect,
    );
}

#[vello_test(hybrid_tolerance = 7)]
fn image_bicubic_10x_scale_2(ctx: &mut impl Renderer) {
    let image_source = rgb_img_2x3(ctx);
    quality(
        ctx,
        Affine::scale(10.0),
        image_source,
        ImageQuality::High,
        Extend::Reflect,
    );
}

#[vello_test]
fn image_with_multiple_clip_layers(ctx: &mut impl Renderer) {
    let image_source = rgb_img_2x2(ctx);
    let image_rect = Rect::new(10.0, 10.0, 90.0, 90.0);
    let clipped_area1 = Rect::new(20.0, 20.0, 80.0, 80.0);
    let clipped_area2 = Triangle::new(
        Point::new(90.0, 10.0),
        Point::new(32.0, 46.0),
        Point::new(54.0, 68.0),
    );

    ctx.push_clip_layer(&clipped_area1.to_path(0.1));
    ctx.push_clip_layer(&clipped_area2.to_path(0.1));
    ctx.set_paint_transform(Affine::IDENTITY);
    ctx.set_paint(Image {
        image: image_source,
        sampler: ImageSampler {
            x_extend: Extend::Repeat,
            y_extend: Extend::Repeat,
            quality: ImageQuality::Low,
            alpha: 1.0,
        },
    });
    ctx.fill_rect(&image_rect);
    ctx.pop_layer();
    ctx.pop_layer();
}

/// Parameters for a glyph sprite from an atlas.
#[derive(Clone, Copy)]
struct Sprite {
    /// X position of the sprite within the atlas
    atlas_x: f64,
    /// Y position of the sprite within the atlas
    atlas_y: f64,
    /// Width of the sprite
    width: f64,
    /// Height of the sprite
    height: f64,
    /// Vertical offset from baseline (positive = render lower)
    y_offset: f64,
}

impl Sprite {
    const fn new(atlas_x: f64, atlas_y: f64, width: f64, height: f64, y_offset: f64) -> Self {
        Self {
            atlas_x,
            atlas_y,
            width,
            height,
            y_offset,
        }
    }
}

/// Sprites for "hello" from the glyph atlas.
const HELLO_WORLD: &[Sprite] = &[
    Sprite::new(1.0, 46.0, 10.0, 14.0, 0.0), // 'h'
    Sprite::new(68.0, 0.0, 10.0, 12.0, 2.0), // 'e'
    Sprite::new(27.0, 53.0, 5.0, 14.0, 0.0), // 'l'
    Sprite::new(27.0, 53.0, 5.0, 14.0, 0.0), // 'l'
    Sprite::new(80.0, 0.0, 10.0, 12.0, 2.0), // 'o'
];

/// Test rendering "hello" from a glyph atlas (spritesheet-style).
/// Uses `ImageSource::OpaqueId` to demonstrate the image registry pattern.
#[vello_test(width = 60, height = 30, skip_hybrid)]
fn image_spritesheet(ctx: &mut impl Renderer) {
    let atlas_id = ctx.register_image(load_image!("glyph_atlas"));
    let atlas_src = ImageSource::opaque_id(atlas_id);

    let start_x = 10.0;
    let start_y = 8.0;

    let mut cursor_x = start_x;

    for glyph in HELLO_WORLD {
        render_sprite(ctx, &atlas_src, glyph, cursor_x, start_y, None);
        cursor_x += glyph.width;
    }
}

/// Render a sprite from an atlas/spritesheet at a screen position, with an optional tint color.
fn render_sprite(
    ctx: &mut impl Renderer,
    atlas_src: &ImageSource,
    glyph: &Sprite,
    screen_x: f64,
    screen_y: f64,
    tint: Option<Color>,
) {
    ctx.set_tint(tint.map(|color| Tint {
        color,
        mode: TintMode::AlphaMask,
    }));
    ctx.set_transform(Affine::translate((screen_x, screen_y + glyph.y_offset)));
    ctx.set_paint_transform(Affine::translate((-glyph.atlas_x, -glyph.atlas_y)));
    ctx.set_paint(Image {
        image: atlas_src.clone(),
        sampler: ImageSampler {
            x_extend: Extend::Pad,
            y_extend: Extend::Pad,
            quality: ImageQuality::Low,
            alpha: 1.0,
        },
    });
    ctx.fill_rect(&Rect::new(0.0, 0.0, glyph.width, glyph.height));
}

/// Same as `image_spritesheet`, but renders "hello world" with a purple tint.
#[vello_test(width = 60, height = 30, skip_multithreaded)]
fn image_spritesheet_tinted(ctx: &mut impl Renderer) {
    let atlas_id = ctx.register_image(load_image!("glyph_atlas"));
    let atlas_src = ImageSource::opaque_id(atlas_id);

    let start_x = 10.0;
    let start_y = 8.0;

    let mut cursor_x = start_x;

    for glyph in HELLO_WORLD {
        render_sprite(
            ctx,
            &atlas_src,
            glyph,
            cursor_x,
            start_y,
            Some(REBECCA_PURPLE),
        );
        cursor_x += glyph.width;
    }
}

#[vello_test]
fn image_fully_transparent_tint(ctx: &mut impl Renderer) {
    let image = Image {
        image: rgb_img_10x10(ctx),
        sampler: ImageSampler {
            quality: ImageQuality::Low,
            ..ImageSampler::default()
        },
    };

    for (x, mode) in [(0.0, TintMode::AlphaMask), (50.0, TintMode::Multiply)] {
        ctx.set_transform(Affine::translate((x, 0.0)) * Affine::scale(5.0));
        ctx.set_tint(Some(Tint {
            color: Color::from_rgba8(255, 255, 255, 0),
            mode,
        }));
        ctx.set_paint(image.clone());
        ctx.fill_rect(&Rect::new(0.0, 0.0, 10.0, 10.0));
    }
}
