use std::path::{Path, PathBuf};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GalleryPreset {
    Materials620,
    Materials420,
    Renderer760,
    Renderer480,
}

impl GalleryPreset {
    pub const ALL: [Self; 4] = [
        Self::Materials620,
        Self::Materials420,
        Self::Renderer760,
        Self::Renderer480,
    ];

    pub const fn label(self) -> &'static str {
        match self {
            Self::Materials620 => "Materials · 620",
            Self::Materials420 => "Materials · 420",
            Self::Renderer760 => "Renderer · 760",
            Self::Renderer480 => "Renderer · 480",
        }
    }

    pub const fn width(self) -> f32 {
        match self {
            Self::Materials620 => 620.0,
            Self::Materials420 => 420.0,
            Self::Renderer760 => 760.0,
            Self::Renderer480 => 480.0,
        }
    }

    pub const fn target_height(self) -> f32 {
        match self {
            Self::Materials620 => 539.0,
            Self::Materials420 => 735.0,
            Self::Renderer760 => 1233.0,
            Self::Renderer480 => 828.0,
        }
    }

    pub const fn file_name(self) -> &'static str {
        match self {
            Self::Materials620 => "materials-wide-620.png",
            Self::Materials420 => "materials-narrow-420.png",
            Self::Renderer760 => "static-mesh-renderer-wide-760.png",
            Self::Renderer480 => "static-mesh-renderer-narrow-480.png",
        }
    }

    pub const fn is_renderer(self) -> bool {
        matches!(self, Self::Renderer760 | Self::Renderer480)
    }

    /// Production-content canvas inside the exported Penpot reference.
    ///
    /// Material boards include the Inspector border and its 12 px horizontal / 10 px top inset;
    /// renderer boards are already complete component canvases.
    pub fn content_rect(self, canvas: egui::Rect) -> egui::Rect {
        if self.is_renderer() {
            egui::Rect::from_min_max(
                canvas.min + egui::vec2(0.0, 12.0),
                egui::pos2(canvas.right(), canvas.bottom()),
            )
        } else {
            egui::Rect::from_min_max(
                canvas.min + egui::vec2(12.0, 10.0),
                egui::pos2(canvas.right() - 12.0, canvas.bottom()),
            )
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComparisonMode {
    Live,
    SideBySide,
    Overlay,
    Difference,
}

impl ComparisonMode {
    pub const ALL: [Self; 4] = [
        Self::Live,
        Self::SideBySide,
        Self::Overlay,
        Self::Difference,
    ];

    pub const fn label(self) -> &'static str {
        match self {
            Self::Live => "Live",
            Self::SideBySide => "Side by side",
            Self::Overlay => "Overlay",
            Self::Difference => "Difference",
        }
    }
}

pub struct ReferenceImage {
    pub source_size: [usize; 2],
    pub texture: egui::TextureHandle,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ReferenceCrop {
    origin: [usize; 2],
    size: [usize; 2],
}

fn reference_crop(source_size: [usize; 2], preset: GalleryPreset) -> ReferenceCrop {
    let target_width = preset.width().round().max(1.0) as usize;
    let target_height = preset.target_height().round().max(1.0) as usize;
    // Penpot's material exports include the frame's one-pixel outer stroke in their viewBox,
    // producing 622/422 px PNGs for 620/420 px boards. That stroke is export overscan, not part of
    // the authored canvas. Cropping it preserves direct 1:1 pixels; scaling it into the canvas
    // blurs text and introduces a false global geometry delta.
    let overscan = source_size[0].saturating_sub(target_width);
    let inset = usize::from(overscan == 2);
    ReferenceCrop {
        origin: [inset, inset],
        size: [
            target_width.min(source_size[0].saturating_sub(inset)),
            target_height.min(source_size[1].saturating_sub(inset)),
        ],
    }
}

impl ReferenceImage {
    pub fn load(ctx: &egui::Context, root: &Path, preset: GalleryPreset) -> Result<Self, String> {
        let path = reference_path(root, preset);
        let bytes = std::fs::read(&path).map_err(|error| format!("{}: {error}", path.display()))?;
        let decoded = image::load_from_memory(&bytes)
            .map_err(|error| format!("{}: {error}", path.display()))?
            .to_rgba8();
        let source_size = [decoded.width() as usize, decoded.height() as usize];
        let pixels = egui::ColorImage::from_rgba_unmultiplied(source_size, decoded.as_raw());
        let texture = ctx.load_texture(
            format!("penpot-reference-{}", preset.label()),
            pixels.clone(),
            egui::TextureOptions::NEAREST,
        );
        Ok(Self {
            source_size,
            texture,
        })
    }

    pub fn paint(&self, ui: &mut egui::Ui, preset: GalleryPreset, opacity: f32) -> egui::Response {
        let size = egui::vec2(preset.width(), preset.target_height());
        let crop = reference_crop(self.source_size, preset);
        let uv_min = egui::pos2(
            crop.origin[0] as f32 / self.source_size[0] as f32,
            crop.origin[1] as f32 / self.source_size[1] as f32,
        );
        let uv_max = egui::pos2(
            (crop.origin[0] + crop.size[0]) as f32 / self.source_size[0] as f32,
            (crop.origin[1] + crop.size[1]) as f32 / self.source_size[1] as f32,
        );
        ui.add(
            egui::Image::new((self.texture.id(), size))
                .uv(egui::Rect::from_min_max(uv_min, uv_max))
                .tint(egui::Color32::from_white_alpha((opacity * 255.0) as u8)),
        )
    }
}

pub fn difference_image(
    screenshot: &egui::ColorImage,
    live_rect: egui::Rect,
    reference_rect: egui::Rect,
    pixels_per_point: f32,
    preset: GalleryPreset,
) -> (egui::ColorImage, DifferenceStats) {
    let width = preset.width().round().max(1.0) as usize;
    let height = preset.target_height().round().max(1.0) as usize;
    let mut output =
        egui::ColorImage::new([width, height], vec![egui::Color32::BLACK; width * height]);
    let mut total = 0_u64;
    let mut max_delta = 0_u8;
    let mut changed = 0_u64;
    for y in 0..height {
        for x in 0..width {
            let live =
                sample_screenshot(screenshot, live_rect, pixels_per_point, x, y, width, height);
            let reference_color = sample_screenshot(
                screenshot,
                reference_rect,
                pixels_per_point,
                x,
                y,
                width,
                height,
            );
            let delta = [
                live.r().abs_diff(reference_color.r()),
                live.g().abs_diff(reference_color.g()),
                live.b().abs_diff(reference_color.b()),
            ];
            let peak = *delta.iter().max().unwrap_or(&0);
            max_delta = max_delta.max(peak);
            total += delta.into_iter().map(u64::from).sum::<u64>();
            changed += u64::from(peak > 8);
            let amplified = |value: u8| value.saturating_mul(4);
            output[(x, y)] = egui::Color32::from_rgb(
                amplified(delta[0]),
                amplified(delta[1]),
                amplified(delta[2]),
            );
        }
    }
    let samples = (width * height * 3).max(1) as f32;
    let pixels = (width * height).max(1) as f32;
    (
        output,
        DifferenceStats {
            mean_channel_delta: total as f32 / samples,
            max_channel_delta: max_delta,
            changed_percent: changed as f32 * 100.0 / pixels,
        },
    )
}

#[derive(Debug, Clone, Copy, Default)]
pub struct DifferenceStats {
    pub mean_channel_delta: f32,
    pub max_channel_delta: u8,
    pub changed_percent: f32,
}

fn reference_path(root: &Path, preset: GalleryPreset) -> PathBuf {
    root.join("docs/editor/evaluations/content-workspace-m2/penpot")
        .join(preset.file_name())
}

fn sample_screenshot(
    image: &egui::ColorImage,
    rect: egui::Rect,
    pixels_per_point: f32,
    x: usize,
    y: usize,
    width: usize,
    height: usize,
) -> egui::Color32 {
    let px = ((rect.left() + (x as f32 + 0.5) * rect.width() / width as f32) * pixels_per_point)
        .floor() as isize;
    let py = ((rect.top() + (y as f32 + 0.5) * rect.height() / height as f32) * pixels_per_point)
        .floor() as isize;
    if px < 0 || py < 0 || px as usize >= image.size[0] || py as usize >= image.size[1] {
        return egui::Color32::BLACK;
    }
    image[(px as usize, py as usize)]
}

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

    #[test]
    fn material_presets_supply_exported_slot_widths() {
        for (preset, expected_width) in [
            (GalleryPreset::Materials620, 596.0),
            (GalleryPreset::Materials420, 396.0),
        ] {
            let canvas = egui::Rect::from_min_size(
                egui::Pos2::ZERO,
                egui::vec2(preset.width(), preset.target_height()),
            );
            let content = preset.content_rect(canvas);
            assert_eq!(content.left(), 12.0);
            assert_eq!(content.top(), 10.0);
            assert_eq!(content.width(), expected_width);
        }
    }

    #[test]
    fn renderer_presets_preserve_the_exported_top_inset() {
        for preset in [GalleryPreset::Renderer760, GalleryPreset::Renderer480] {
            let canvas = egui::Rect::from_min_size(
                egui::Pos2::ZERO,
                egui::vec2(preset.width(), preset.target_height()),
            );
            let content = preset.content_rect(canvas);
            assert_eq!(content.left(), 0.0);
            assert_eq!(content.top(), 12.0);
            assert_eq!(content.width(), preset.width());
        }
    }

    #[test]
    fn material_export_overscan_is_cropped_instead_of_scaled() {
        assert_eq!(
            reference_crop([622, 661], GalleryPreset::Materials620),
            ReferenceCrop {
                origin: [1, 1],
                size: [620, 539],
            }
        );
        assert_eq!(
            reference_crop([422, 857], GalleryPreset::Materials420),
            ReferenceCrop {
                origin: [1, 1],
                size: [420, 735],
            }
        );
        assert_eq!(
            reference_crop([760, 1233], GalleryPreset::Renderer760),
            ReferenceCrop {
                origin: [0, 0],
                size: [760, 1233],
            }
        );
    }
}
