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

//! Helpers for performing probing to verify the basic capabilities of the device we are
//! running on.

use crate::color::{AlphaColor, palette::css};
use crate::filter_effects::{EdgeMode, Filter, FilterPrimitive};
#[cfg(not(feature = "std"))]
use crate::kurbo::common::FloatFuncs as _;
use crate::kurbo::{Affine, BezPath, Circle, Point, Rect, Shape};
use crate::paint::{Image, ImageSource, PaintType};
use crate::peniko::{
    BlendMode, ColorStop, ColorStops, Compose, Extend, Gradient, ImageQuality, ImageSampler,
    LinearGradientPosition, Mix,
};
use crate::pixmap::Pixmap;
use alloc::vec::Vec;

const REFERENCE_RGBA: &[u8] = include_bytes!("../assets/probe.rgba");

const ELEMENTS_PER_ROW: usize = 3;
const ELEMENT_MARGIN: f64 = 1.0;

const RECT_SIZE: f64 = 10.0;
const CIRCLE_RADIUS: f64 = 5.0;
const CIRCLE_CENTER_OFFSET_X: f64 = 1.5;
const IMAGE_SOURCE_SIZE: f64 = 5.0;
const PATH_TOLERANCE: f64 = 0.1;

/// The active elements used in the probe.
pub const PROBE_ELEMENTS: [ProbeFeature; 8] = [
    ProbeFeature::SolidRect,
    ProbeFeature::AlphaBlending,
    ProbeFeature::Gradient,
    ProbeFeature::ImageNearest,
    // Temporarily disabled.
    // ProbeFeature::Filter,
    ProbeFeature::ImageBilinear,
    ProbeFeature::OpacityLayer,
    ProbeFeature::Blending,
    ProbeFeature::Transformed,
];
/// Per-channel absolute tolerance used when comparing probe pixels.
const CHANNEL_TOLERANCE: u8 = 3;

/// Result of running the renderer probe.
#[derive(Debug, Clone)]
pub enum Probe<E> {
    /// The probe matched the bundled reference image.
    Success,
    /// The probe did not match the bundled reference image.
    Error(ProbeResult),
    /// Rendering the probe scene produces an error.
    RenderError(E),
}

/// Probe failure output.
#[derive(Debug, Clone)]
pub struct ProbeResult {
    /// The expected probe image.
    pub expected: ProbeImage,
    /// The actual probe image.
    pub actual: ProbeImage,
}

/// A feature exercised by the renderer probe.
///
/// Each discriminant is the stable bit index used by [`ProbeStatistics::difference_mask`].
/// Existing discriminants must not be changed when features are reordered, disabled, or added.
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ProbeFeature {
    /// Drawing a solid rectangle.
    SolidRect = 0,
    /// Alpha blending overlapping shapes.
    AlphaBlending = 1,
    /// Drawing a linear gradient.
    Gradient = 2,
    /// Drawing an image with nearest-neighbor sampling.
    ImageNearest = 3,
    /// Applying a filter effect.
    Filter = 4,
    /// Drawing an image with bilinear sampling.
    ImageBilinear = 5,
    /// Drawing within a layer with reduced opacity.
    OpacityLayer = 6,
    /// Drawing within a layer with a blend mode.
    Blending = 7,
    /// Drawing with a non-identity transform.
    Transformed = 8,
}

/// Summary of the differences between the expected and actual probe images.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ProbeStatistics {
    /// Number of active features exercised by the probe.
    pub element_count: u8,
    /// Width and height of the actual probe image.
    pub actual_size: (u16, u16),
    /// Number of pixels whose channels differ by more than the probe tolerance.
    pub different_pixel_count: u32,
    /// Largest absolute difference between corresponding red, green, blue, and alpha channels.
    pub max_channel_discrepancy: [u8; 4],
    /// Bitmask identifying probe features containing a pixel outside the probe tolerance.
    ///
    /// Bit `n` corresponds to the [`ProbeFeature`] whose discriminant is `n`.
    pub difference_mask: u32,
}

impl ProbeStatistics {
    /// Returns whether `feature` contained a pixel outside the probe tolerance.
    pub fn differs(&self, feature: ProbeFeature) -> bool {
        self.difference_mask & (1_u32 << feature as u8) != 0
    }
}

impl ProbeResult {
    /// Return the statistics of the probe.
    pub fn statistics(&self) -> ProbeStatistics {
        let layout = GridLayout::from_elements(&PROBE_ELEMENTS);
        let mut statistics = ProbeStatistics {
            element_count: PROBE_ELEMENTS.len() as u8,
            actual_size: (self.actual.width, self.actual.height),
            ..Default::default()
        };

        for (pixel_index, (expected, actual)) in self
            .expected
            .data
            .chunks_exact(4)
            .zip(self.actual.data.chunks_exact(4))
            .enumerate()
        {
            if expected[3] != 0 || actual[3] != 0 {
                for (max_discrepancy, (expected, actual)) in statistics
                    .max_channel_discrepancy
                    .iter_mut()
                    .zip(expected.iter().zip(actual))
                {
                    *max_discrepancy = (*max_discrepancy).max(expected.abs_diff(*actual));
                }
            }

            if !pixels_within_tolerance(expected, actual, CHANNEL_TOLERANCE) {
                statistics.different_pixel_count += 1;

                let cell_index = layout.cell_index_for_pixel(pixel_index);
                if let Some(feature) = PROBE_ELEMENTS.get(cell_index) {
                    statistics.difference_mask |= 1_u32 << *feature as u8;
                }
            }
        }

        statistics
    }
}

/// A probe image stored as RGBA8 bytes.
#[derive(Debug, Clone)]
pub struct ProbeImage {
    /// Width of the image in pixels.
    pub width: u16,
    /// Height of the image in pixels.
    pub height: u16,
    /// The image data as RGBA8 bytes.
    pub data: Vec<u8>,
}

impl<E> Probe<E> {
    /// Returns `true` when the probe matched the bundled reference image.
    pub fn is_success(&self) -> bool {
        matches!(self, Self::Success)
    }

    /// Construct a new probe result by inspecting the provided pixmap and comparing it
    /// against the reference output.
    pub fn from_actual(actual: Pixmap) -> Self {
        let (width, height) = canvas_size();
        let expected = ProbeImage {
            width,
            height,
            data: REFERENCE_RGBA.to_vec(),
        };
        let actual = ProbeImage::from_pixmap(actual);
        let matches_reference = expected.width == actual.width
            && expected.height == actual.height
            && expected.data.len() == actual.data.len()
            && expected
                .data
                .chunks_exact(4)
                .zip(actual.data.chunks_exact(4))
                .all(|(expected, actual)| {
                    pixels_within_tolerance(expected, actual, CHANNEL_TOLERANCE)
                });

        if matches_reference {
            Self::Success
        } else {
            Self::Error(ProbeResult { expected, actual })
        }
    }
}

impl ProbeImage {
    fn from_pixmap(pixmap: Pixmap) -> Self {
        Self {
            width: pixmap.width(),
            height: pixmap.height(),
            data: bytemuck::cast_slice(&pixmap.take_unpremultiplied()).to_vec(),
        }
    }
}

/// API necessary to draw the probe scene.
pub trait ProbeRenderer {
    fn set_transform(&mut self, transform: Affine);
    fn set_paint(&mut self, paint: PaintType);
    fn fill_path(&mut self, path: &BezPath);
    fn fill_rect(&mut self, rect: &Rect);
    fn push_layer(&mut self, blend_mode: Option<BlendMode>, opacity: Option<f32>);
    fn push_filter_layer(&mut self, filter: Filter);
    fn pop_layer(&mut self);
    fn set_paint_transform(&mut self, paint_transform: Affine);
    fn reset_paint_transform(&mut self);
}

#[derive(Clone, Copy, Debug)]
struct GridLayout {
    columns: usize,
    rows: usize,
    cell_width: f64,
    cell_height: f64,
}

impl GridLayout {
    fn from_elements(elements: &[ProbeFeature]) -> Self {
        let columns = ELEMENTS_PER_ROW.min(elements.len());
        let rows = elements.len().div_ceil(columns);
        let (cell_width, cell_height) = elements
            .iter()
            .copied()
            .map(ProbeFeature::bounds)
            .fold((0.0_f64, 0.0_f64), |(max_w, max_h), (w, h)| {
                (max_w.max(w), max_h.max(h))
            });

        Self {
            columns,
            rows,
            cell_width,
            cell_height,
        }
    }

    fn canvas_size(self) -> (u16, u16) {
        let (cell_stride_x, cell_stride_y) = self.cell_stride();
        // Margin only exists between cells, so subtract one.
        let width = self.columns as f64 * cell_stride_x - ELEMENT_MARGIN;
        let height = self.rows as f64 * cell_stride_y - ELEMENT_MARGIN;
        (width.ceil() as u16, height.ceil() as u16)
    }

    fn canvas_rect(self) -> Rect {
        let (width, height) = self.canvas_size();
        Rect::new(0.0, 0.0, f64::from(width), f64::from(height))
    }

    fn cell_stride(self) -> (f64, f64) {
        (
            self.cell_width + ELEMENT_MARGIN,
            self.cell_height + ELEMENT_MARGIN,
        )
    }

    fn cell_rect(self, index: usize) -> Rect {
        let column = index % self.columns;
        let row = index / self.columns;
        let (cell_stride_x, cell_stride_y) = self.cell_stride();
        let x0 = column as f64 * cell_stride_x;
        let y0 = row as f64 * cell_stride_y;
        Rect::new(x0, y0, x0 + self.cell_width, y0 + self.cell_height)
    }

    fn cell_index_for_pixel(self, pixel_index: usize) -> usize {
        let (cell_stride_x, cell_stride_y) = self.cell_stride();
        let image_width = usize::from(self.canvas_size().0);
        let x = pixel_index % image_width;
        let y = pixel_index / image_width;
        let column = x / cell_stride_x as usize;
        let row = y / cell_stride_y as usize;
        row * self.columns + column
    }
}

impl ProbeFeature {
    fn bounds(self) -> (f64, f64) {
        let (width, height) = match self {
            Self::SolidRect
            | Self::Gradient
            | Self::ImageNearest
            | Self::ImageBilinear
            | Self::Filter
            | Self::OpacityLayer => (RECT_SIZE, RECT_SIZE),
            Self::Transformed => (
                RECT_SIZE * core::f64::consts::SQRT_2,
                RECT_SIZE * core::f64::consts::SQRT_2,
            ),
            Self::AlphaBlending | Self::Blending => (
                CIRCLE_RADIUS * 2.0 + CIRCLE_CENTER_OFFSET_X * 2.0,
                CIRCLE_RADIUS * 2.0,
            ),
        };
        (width + ELEMENT_MARGIN * 2.0, height + ELEMENT_MARGIN * 2.0)
    }
}

/// Return the canvas size of the shared probe scene.
pub fn canvas_size() -> (u16, u16) {
    GridLayout::from_elements(&PROBE_ELEMENTS).canvas_size()
}

/// Return the pixmap that is referenced when drawing images in the scene.
pub fn probe_image_pixmap() -> Pixmap {
    let mut pixmap = Pixmap::new(IMAGE_SOURCE_SIZE as u16, IMAGE_SOURCE_SIZE as u16);
    for y in 0..pixmap.height() {
        for x in 0..pixmap.width() {
            pixmap.set_pixel(
                x,
                y,
                AlphaColor::from_rgba8(255, 0, 0, 255)
                    .premultiply()
                    .to_rgba8(),
            );
        }
    }
    pixmap.set_may_have_transparency(false);
    pixmap
}

fn image_paint(image: ImageSource, quality: ImageQuality) -> PaintType {
    Image {
        image,
        sampler: ImageSampler {
            x_extend: Extend::Pad,
            y_extend: Extend::Pad,
            quality,
            alpha: 1.0,
        },
    }
    .into()
}

/// Draw the full shared probe scene into a rendering context.
pub fn draw_scene<T: ProbeRenderer>(ctx: &mut T, image: ImageSource) {
    let layout = GridLayout::from_elements(&PROBE_ELEMENTS);
    let image_nearest = image_paint(image.clone(), ImageQuality::Low);
    let image_bilinear = image_paint(image, ImageQuality::Medium);
    ctx.set_transform(Affine::IDENTITY);
    ctx.set_paint(css::WHITE.into());
    ctx.fill_rect(&layout.canvas_rect());

    for (index, element) in PROBE_ELEMENTS.iter().copied().enumerate() {
        draw_probe_element(
            ctx,
            layout.cell_rect(index),
            element,
            &image_nearest,
            &image_bilinear,
        );
    }
}

fn pixels_within_tolerance(expected: &[u8], actual: &[u8], channel_tolerance: u8) -> bool {
    if expected[3] == 0 && actual[3] == 0 {
        return true;
    }

    expected
        .iter()
        .zip(actual)
        .all(|(expected, actual)| expected.abs_diff(*actual) <= channel_tolerance)
}

fn draw_probe_element(
    ctx: &mut impl ProbeRenderer,
    cell: Rect,
    element: ProbeFeature,
    image_nearest: &PaintType,
    image_bilinear: &PaintType,
) {
    match element {
        ProbeFeature::SolidRect => {
            ctx.set_paint(css::BLUE.into());
            ctx.fill_rect(&centered_rect(cell, RECT_SIZE, RECT_SIZE));
        }
        ProbeFeature::Transformed => {
            draw_transformed_rect(ctx, centered_rect(cell, RECT_SIZE, RECT_SIZE));
        }
        ProbeFeature::AlphaBlending => {
            let center = cell.center();
            ctx.set_paint(css::YELLOW.with_alpha(0.5).into());
            ctx.fill_path(
                &Circle::new((center.x - CIRCLE_CENTER_OFFSET_X, center.y), CIRCLE_RADIUS)
                    .to_path(PATH_TOLERANCE),
            );
            ctx.set_paint(css::GREEN.with_alpha(0.5).into());
            ctx.fill_path(
                &Circle::new((center.x + CIRCLE_CENTER_OFFSET_X, center.y), CIRCLE_RADIUS)
                    .to_path(PATH_TOLERANCE),
            );
        }
        ProbeFeature::Gradient => {
            let rect = centered_rect(cell, RECT_SIZE, RECT_SIZE);
            ctx.set_paint(linear_gradient(&rect).into());
            ctx.fill_rect(&rect);
        }
        ProbeFeature::ImageNearest => draw_centered_padded_image(ctx, cell, image_nearest),
        ProbeFeature::Filter => draw_blurred_rect(ctx, centered_rect(cell, RECT_SIZE, RECT_SIZE)),
        ProbeFeature::ImageBilinear => draw_centered_padded_image(ctx, cell, image_bilinear),
        ProbeFeature::OpacityLayer => {
            draw_opacity_layer_rect(ctx, centered_rect(cell, RECT_SIZE, RECT_SIZE));
        }
        ProbeFeature::Blending => draw_layered_difference_circles(ctx, cell),
    }
}

fn centered_rect(cell: Rect, width: f64, height: f64) -> Rect {
    let center = cell.center();
    Rect::new(
        center.x - width * 0.5,
        center.y - height * 0.5,
        center.x + width * 0.5,
        center.y + height * 0.5,
    )
}

fn draw_centered_padded_image(ctx: &mut impl ProbeRenderer, cell: Rect, image_paint: &PaintType) {
    let dst_rect = centered_rect(cell, RECT_SIZE, RECT_SIZE);
    let image_origin = (
        dst_rect.x0 + (RECT_SIZE - IMAGE_SOURCE_SIZE) * 0.5,
        dst_rect.y0 + (RECT_SIZE - IMAGE_SOURCE_SIZE) * 0.5,
    );
    ctx.set_paint(image_paint.clone());
    ctx.set_paint_transform(Affine::translate(image_origin));
    ctx.fill_rect(&dst_rect);
    ctx.reset_paint_transform();
}

fn draw_transformed_rect(ctx: &mut impl ProbeRenderer, rect: Rect) {
    let center = rect.center();
    ctx.set_transform(
        Affine::translate((center.x, center.y))
            * Affine::rotate(core::f64::consts::FRAC_PI_4)
            * Affine::translate((-center.x, -center.y)),
    );
    ctx.set_paint(css::BLUE.into());
    ctx.fill_rect(&rect);
    ctx.set_transform(Affine::IDENTITY);
}

#[allow(dead_code, reason = "Will be re-enabled in the future.")]
fn draw_blurred_rect(ctx: &mut impl ProbeRenderer, rect: Rect) {
    let blur = Filter::from_primitive(FilterPrimitive::GaussianBlur {
        std_deviation: 0.5,
        edge_mode: EdgeMode::None,
    });
    ctx.push_filter_layer(blur);
    ctx.set_paint(css::REBECCA_PURPLE.into());
    ctx.fill_rect(&rect);
    ctx.pop_layer();
}

fn draw_opacity_layer_rect(ctx: &mut impl ProbeRenderer, rect: Rect) {
    ctx.push_layer(None, Some(0.5));
    ctx.set_paint(css::ORANGE_RED.into());
    ctx.fill_rect(&rect);
    ctx.pop_layer();
}

fn draw_layered_difference_circles(ctx: &mut impl ProbeRenderer, cell: Rect) {
    let center = cell.center();

    ctx.push_layer(None, None);
    ctx.set_paint(css::YELLOW.with_alpha(0.5).into());
    ctx.fill_path(
        &Circle::new((center.x - CIRCLE_CENTER_OFFSET_X, center.y), CIRCLE_RADIUS)
            .to_path(PATH_TOLERANCE),
    );

    ctx.push_layer(
        Some(BlendMode::new(Mix::Difference, Compose::SrcOver)),
        None,
    );
    ctx.set_paint(css::GREEN.with_alpha(0.5).into());
    ctx.fill_path(
        &Circle::new((center.x + CIRCLE_CENTER_OFFSET_X, center.y), CIRCLE_RADIUS)
            .to_path(PATH_TOLERANCE),
    );
    ctx.pop_layer();
    ctx.pop_layer();
}

fn linear_gradient(rect: &Rect) -> Gradient {
    Gradient {
        kind: LinearGradientPosition {
            start: Point::new(rect.x0, rect.y0),
            end: Point::new(rect.x1, rect.y0),
        }
        .into(),
        stops: ColorStops::from(
            [
                ColorStop::from((0.0, css::BLUE)),
                ColorStop::from((1.0, css::RED)),
            ]
            .as_slice(),
        ),
        extend: Extend::Pad,
        ..Default::default()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloc::vec;

    #[test]
    fn probe_result_reports_pixel_and_cell_differences() {
        let (width, height) = canvas_size();
        let pixel_count = usize::from(width) * usize::from(height);
        let expected = ProbeImage {
            width,
            height,
            data: vec![255; pixel_count * 4],
        };
        let mut actual = expected.clone();
        let layout = GridLayout::from_elements(&PROBE_ELEMENTS);

        let set_channel = |actual: &mut ProbeImage, cell_index: usize, channel: usize, value| {
            let center = layout.cell_rect(cell_index).center();
            let x = center.x.floor() as usize;
            let y = center.y.floor() as usize;
            actual.data[(y * usize::from(width) + x) * 4 + channel] = value;
        };

        // This stays within the probe tolerance.
        set_channel(&mut actual, 0, 0, 254);

        set_channel(&mut actual, 1, 0, 249);
        set_channel(&mut actual, 5, 1, 0);
        set_channel(&mut actual, 5, 3, 100);

        let result = ProbeResult { expected, actual };
        let statistics = result.statistics();
        assert_eq!(
            statistics,
            ProbeStatistics {
                element_count: PROBE_ELEMENTS.len() as u8,
                actual_size: (width, height),
                different_pixel_count: 2,
                max_channel_discrepancy: [6, 255, 0, 155],
                difference_mask: (1 << 1) | (1 << 6),
            }
        );
        assert!(statistics.differs(ProbeFeature::AlphaBlending));
        assert!(statistics.differs(ProbeFeature::OpacityLayer));
        assert!(!statistics.differs(ProbeFeature::Filter));
        assert!(!statistics.differs(ProbeFeature::ImageBilinear));
    }

    #[test]
    fn probe_statistics_reports_actual_size() {
        let result = ProbeResult {
            expected: ProbeImage {
                width: 1,
                height: 1,
                data: vec![0; 4],
            },
            actual: ProbeImage {
                width: 2,
                height: 1,
                data: vec![0; 8],
            },
        };

        assert_eq!(result.statistics().actual_size, (2, 1));
    }
}
