use bone_types::Tolerance;

use crate::camera::ViewportExtent;
use crate::snapshot::SnapshotFrame;

#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct PixelDiffThreshold(f64);

impl PixelDiffThreshold {
    pub const EXACT: Self = Self(0.0);

    #[must_use]
    pub fn new(channel_fraction: f64) -> Self {
        Self(channel_fraction.clamp(0.0, 1.0))
    }

    #[must_use]
    pub const fn value(self) -> f64 {
        self.0
    }

    #[must_use]
    #[allow(
        clippy::cast_possible_truncation,
        clippy::cast_sign_loss,
        reason = "value is clamped to [0, 1] before scaling to u8"
    )]
    pub fn as_u8(self) -> u8 {
        (self.0.clamp(0.0, 1.0) * 255.0).round() as u8
    }
}

impl From<Tolerance> for PixelDiffThreshold {
    fn from(t: Tolerance) -> Self {
        Self::new(t.value())
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct PixelMismatch {
    x: u32,
    y: u32,
    channel_delta: [u8; 4],
}

impl PixelMismatch {
    #[must_use]
    pub const fn x(self) -> u32 {
        self.x
    }

    #[must_use]
    pub const fn y(self) -> u32 {
        self.y
    }

    #[must_use]
    pub const fn channel_delta(self) -> [u8; 4] {
        self.channel_delta
    }

    #[must_use]
    pub fn max_delta(self) -> u8 {
        self.channel_delta.into_iter().max().unwrap_or(0)
    }
}

#[derive(Clone, Debug, Default)]
pub struct PixelDiffReport {
    worst: Option<PixelMismatch>,
    over_threshold: u32,
}

impl PixelDiffReport {
    #[must_use]
    pub const fn worst(&self) -> Option<PixelMismatch> {
        self.worst
    }

    #[must_use]
    pub const fn over_threshold(&self) -> u32 {
        self.over_threshold
    }

    #[must_use]
    pub const fn is_clean(&self) -> bool {
        self.over_threshold == 0
    }
}

#[derive(Debug, thiserror::Error)]
pub enum PixelDiffError {
    #[error(
        "pixel buffer length mismatch: actual={actual}, expected={expected}, required={required}"
    )]
    ShapeMismatch {
        actual: usize,
        expected: usize,
        required: usize,
    },
    #[error("viewport dimension is zero")]
    ZeroExtent,
    #[error("pixel count {pixels} exceeds u32::MAX")]
    ExtentOverflow { pixels: u64 },
}

pub struct PixelDiff;

impl PixelDiff {
    pub fn compare_bytes(
        extent: ViewportExtent,
        actual: &[u8],
        expected: &[u8],
        threshold: PixelDiffThreshold,
    ) -> core::result::Result<PixelDiffReport, PixelDiffError> {
        let width = extent.width().value();
        let height = extent.height().value();
        if width == 0 || height == 0 {
            return Err(PixelDiffError::ZeroExtent);
        }
        let pixel_count = extent.pixel_count();
        if pixel_count > u64::from(u32::MAX) {
            return Err(PixelDiffError::ExtentOverflow {
                pixels: pixel_count,
            });
        }
        let Ok(required) = usize::try_from(pixel_count * 4) else {
            return Err(PixelDiffError::ExtentOverflow {
                pixels: pixel_count,
            });
        };
        if actual.len() != required || expected.len() != required {
            return Err(PixelDiffError::ShapeMismatch {
                actual: actual.len(),
                expected: expected.len(),
                required,
            });
        }
        let limit = threshold.as_u8();
        let report = actual
            .chunks_exact(4)
            .zip(expected.chunks_exact(4))
            .zip(0_u32..)
            .filter_map(|((a, e), idx)| {
                let delta: [u8; 4] = [
                    a[0].abs_diff(e[0]),
                    a[1].abs_diff(e[1]),
                    a[2].abs_diff(e[2]),
                    a[3].abs_diff(e[3]),
                ];
                let max = delta[0].max(delta[1]).max(delta[2]).max(delta[3]);
                (max > limit).then_some((idx, delta))
            })
            .fold(PixelDiffReport::default(), |acc, (idx, delta)| {
                let m = PixelMismatch {
                    x: idx % width,
                    y: idx / width,
                    channel_delta: delta,
                };
                let worst = match acc.worst {
                    Some(prev) if prev.max_delta() >= m.max_delta() => Some(prev),
                    _ => Some(m),
                };
                PixelDiffReport {
                    worst,
                    over_threshold: acc.over_threshold + 1,
                }
            });
        Ok(report)
    }

    pub fn compare(
        actual: &SnapshotFrame,
        expected: &[u8],
        threshold: PixelDiffThreshold,
    ) -> core::result::Result<PixelDiffReport, PixelDiffError> {
        Self::compare_bytes(actual.extent(), actual.rgba(), expected, threshold)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::camera::{ViewportExtent, ViewportPx};

    fn extent(w: u32, h: u32) -> ViewportExtent {
        ViewportExtent::new(ViewportPx::new(w), ViewportPx::new(h))
    }

    fn run(
        extent: ViewportExtent,
        actual: &[u8],
        expected: &[u8],
        threshold: PixelDiffThreshold,
    ) -> PixelDiffReport {
        let Ok(report) = PixelDiff::compare_bytes(extent, actual, expected, threshold) else {
            panic!("compare_bytes rejected inputs");
        };
        report
    }

    #[test]
    fn exact_match_is_clean() {
        let buf = vec![10_u8, 20, 30, 255, 10, 20, 30, 255];
        let report = run(extent(2, 1), &buf, &buf, PixelDiffThreshold::EXACT);
        assert!(report.is_clean());
        assert!(report.worst().is_none());
    }

    #[test]
    fn single_channel_delta_under_threshold_is_clean() {
        let a = vec![10_u8, 20, 30, 255];
        let b = vec![12_u8, 20, 30, 255];
        let report = run(extent(1, 1), &a, &b, PixelDiffThreshold::new(4.0 / 255.0));
        assert!(report.is_clean());
    }

    #[test]
    fn over_threshold_reports_worst_pixel_coords() {
        let a = vec![0_u8, 0, 0, 255, 200, 0, 0, 255];
        let b = vec![0_u8, 0, 0, 255, 50, 0, 0, 255];
        let report = run(extent(2, 1), &a, &b, PixelDiffThreshold::EXACT);
        let Some(worst) = report.worst() else {
            panic!("worst pixel present");
        };
        assert_eq!(worst.x(), 1);
        assert_eq!(worst.y(), 0);
        assert_eq!(worst.max_delta(), 150);
        assert_eq!(report.over_threshold(), 1);
    }

    #[test]
    fn tolerance_maps_to_threshold() {
        let t = Tolerance::new(8.0 / 255.0);
        let th: PixelDiffThreshold = t.into();
        assert_eq!(th.as_u8(), 8);
    }

    #[test]
    fn coords_index_by_row_major() {
        let a = vec![0_u8; 32];
        let mut b = a.clone();
        let pixel = 5_usize;
        b[pixel * 4] = 200;
        let report = run(extent(4, 2), &a, &b, PixelDiffThreshold::EXACT);
        let Some(worst) = report.worst() else {
            panic!("worst present");
        };
        assert_eq!(worst.x(), 1);
        assert_eq!(worst.y(), 1);
    }

    #[test]
    fn shape_mismatch_is_rejected() {
        let a = vec![0_u8; 12];
        let b = vec![0_u8; 8];
        let Err(err) = PixelDiff::compare_bytes(extent(2, 1), &a, &b, PixelDiffThreshold::EXACT)
        else {
            panic!("expected ShapeMismatch");
        };
        let PixelDiffError::ShapeMismatch {
            actual,
            expected,
            required,
        } = err
        else {
            panic!("wrong error variant: {err:?}");
        };
        assert_eq!(actual, 12);
        assert_eq!(expected, 8);
        assert_eq!(required, 8);
    }

    #[test]
    fn zero_extent_is_rejected() {
        let buf: Vec<u8> = Vec::new();
        let Err(err) =
            PixelDiff::compare_bytes(extent(0, 4), &buf, &buf, PixelDiffThreshold::EXACT)
        else {
            panic!("expected ZeroExtent");
        };
        assert!(matches!(err, PixelDiffError::ZeroExtent));
    }
}
