use crate::camera::ViewportExtent;
use crate::gpu::BackendTag;
use crate::{RenderError, Result};

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ClearColor {
    r: f64,
    g: f64,
    b: f64,
    a: f64,
}

impl ClearColor {
    #[must_use]
    pub const fn new(r: f64, g: f64, b: f64, a: f64) -> Self {
        Self { r, g, b, a }
    }

    #[must_use]
    pub const fn opaque(r: f64, g: f64, b: f64) -> Self {
        Self::new(r, g, b, 1.0)
    }

    #[must_use]
    pub fn to_rgba8(self) -> [u8; 4] {
        [
            channel_to_u8(self.r),
            channel_to_u8(self.g),
            channel_to_u8(self.b),
            channel_to_u8(self.a),
        ]
    }

    #[must_use]
    #[allow(
        clippy::cast_possible_truncation,
        reason = "clear-color channels are bounded [0, 1]"
    )]
    pub fn to_rgba_array(self) -> [f32; 4] {
        [self.r as f32, self.g as f32, self.b as f32, self.a as f32]
    }
}

impl From<ClearColor> for wgpu::Color {
    fn from(c: ClearColor) -> Self {
        Self {
            r: c.r,
            g: c.g,
            b: c.b,
            a: c.a,
        }
    }
}

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

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct GridStyle {
    minor: ClearColor,
    major: ClearColor,
    axis_x: ClearColor,
    axis_y: ClearColor,
    origin: ClearColor,
    line_width_px: f32,
    axis_width_px: f32,
    origin_radius_px: f32,
    minor_spacing_target_px: f32,
}

impl GridStyle {
    pub const DEFAULT: Self = Self {
        minor: ClearColor::new(1.0, 1.0, 1.0, 0.08),
        major: ClearColor::new(1.0, 1.0, 1.0, 0.22),
        axis_x: ClearColor::new(1.0, 0.0, 0.0, 1.0),
        axis_y: ClearColor::new(0.0, 1.0, 0.0, 1.0),
        origin: ClearColor::new(1.0, 1.0, 1.0, 0.95),
        line_width_px: 1.0,
        axis_width_px: 1.6,
        origin_radius_px: 4.0,
        minor_spacing_target_px: 24.0,
    };

    #[must_use]
    pub const fn minor(self) -> ClearColor {
        self.minor
    }

    #[must_use]
    pub const fn major(self) -> ClearColor {
        self.major
    }

    #[must_use]
    pub const fn axis_x(self) -> ClearColor {
        self.axis_x
    }

    #[must_use]
    pub const fn axis_y(self) -> ClearColor {
        self.axis_y
    }

    #[must_use]
    pub const fn origin(self) -> ClearColor {
        self.origin
    }

    #[must_use]
    pub const fn line_width_px(self) -> f32 {
        self.line_width_px
    }

    #[must_use]
    pub const fn axis_width_px(self) -> f32 {
        self.axis_width_px
    }

    #[must_use]
    pub const fn origin_radius_px(self) -> f32 {
        self.origin_radius_px
    }

    #[must_use]
    pub const fn minor_spacing_target_px(self) -> f32 {
        self.minor_spacing_target_px
    }
}

impl Default for GridStyle {
    fn default() -> Self {
        Self::DEFAULT
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct StrokeStyle {
    stroke: ClearColor,
    construction: ClearColor,
    stroke_width_px: f32,
    point_radius_px: f32,
    construction_dash_period_px: f32,
    construction_dash_on_ratio: f32,
}

impl StrokeStyle {
    pub const DEFAULT: Self = Self {
        stroke: ClearColor::new(0.95, 0.95, 0.98, 1.0),
        construction: ClearColor::new(0.55, 0.75, 1.0, 0.85),
        stroke_width_px: 1.5,
        point_radius_px: 3.0,
        construction_dash_period_px: 8.0,
        construction_dash_on_ratio: 0.5,
    };

    #[must_use]
    pub const fn stroke(self) -> ClearColor {
        self.stroke
    }

    #[must_use]
    pub const fn construction(self) -> ClearColor {
        self.construction
    }

    #[must_use]
    pub const fn stroke_width_px(self) -> f32 {
        self.stroke_width_px
    }

    #[must_use]
    pub const fn point_radius_px(self) -> f32 {
        self.point_radius_px
    }

    #[must_use]
    pub const fn construction_dash_period_px(self) -> f32 {
        self.construction_dash_period_px
    }

    #[must_use]
    pub const fn construction_dash_on_ratio(self) -> f32 {
        self.construction_dash_on_ratio
    }
}

impl Default for StrokeStyle {
    fn default() -> Self {
        Self::DEFAULT
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct GlyphStyle {
    color: ClearColor,
    offset_px: f32,
    tile_px: f32,
}

impl GlyphStyle {
    pub const DEFAULT: Self = Self {
        color: ClearColor::new(0.35, 0.78, 0.35, 1.0),
        offset_px: 18.0,
        tile_px: 22.0,
    };

    #[must_use]
    pub const fn color(self) -> ClearColor {
        self.color
    }

    #[must_use]
    pub const fn offset_px(self) -> f32 {
        self.offset_px
    }

    #[must_use]
    pub const fn tile_px(self) -> f32 {
        self.tile_px
    }
}

impl Default for GlyphStyle {
    fn default() -> Self {
        Self::DEFAULT
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct TextStyle {
    color: ClearColor,
    font_size_px: f32,
}

impl TextStyle {
    pub const DEFAULT: Self = Self {
        color: ClearColor::new(0.95, 0.95, 0.98, 1.0),
        font_size_px: 14.0,
    };

    #[must_use]
    pub const fn color(self) -> ClearColor {
        self.color
    }

    #[must_use]
    pub const fn font_size_px(self) -> f32 {
        self.font_size_px
    }

    #[must_use]
    pub const fn with_font_size_px(self, font_size_px: f32) -> Self {
        Self {
            font_size_px,
            ..self
        }
    }
}

impl Default for TextStyle {
    fn default() -> Self {
        Self::DEFAULT
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Style {
    background: ClearColor,
    grid: GridStyle,
    strokes: StrokeStyle,
    glyphs: GlyphStyle,
    text: TextStyle,
}

impl Style {
    #[must_use]
    pub const fn new(background: ClearColor) -> Self {
        Self {
            background,
            grid: GridStyle::DEFAULT,
            strokes: StrokeStyle::DEFAULT,
            glyphs: GlyphStyle::DEFAULT,
            text: TextStyle::DEFAULT,
        }
    }

    #[must_use]
    pub const fn background(self) -> ClearColor {
        self.background
    }

    #[must_use]
    pub const fn grid(self) -> GridStyle {
        self.grid
    }

    #[must_use]
    pub const fn strokes(self) -> StrokeStyle {
        self.strokes
    }

    #[must_use]
    pub const fn glyphs(self) -> GlyphStyle {
        self.glyphs
    }

    #[must_use]
    pub const fn text(self) -> TextStyle {
        self.text
    }

    #[must_use]
    pub const fn with_text(self, text: TextStyle) -> Self {
        Self { text, ..self }
    }
}

impl Default for Style {
    fn default() -> Self {
        Self::new(ClearColor::opaque(0.05, 0.05, 0.07))
    }
}

#[derive(Clone, Debug)]
pub struct SnapshotFrame {
    extent: ViewportExtent,
    rgba: Vec<u8>,
    backend: BackendTag,
}

impl SnapshotFrame {
    pub(crate) fn new(extent: ViewportExtent, rgba: Vec<u8>, backend: BackendTag) -> Self {
        Self {
            extent,
            rgba,
            backend,
        }
    }

    #[must_use]
    pub fn extent(&self) -> ViewportExtent {
        self.extent
    }

    #[must_use]
    pub fn rgba(&self) -> &[u8] {
        &self.rgba
    }

    #[must_use]
    pub fn backend(&self) -> BackendTag {
        self.backend
    }
}

pub fn encode_png(frame: &SnapshotFrame) -> Result<Vec<u8>> {
    let mut out: Vec<u8> = Vec::new();
    {
        let mut encoder = png::Encoder::new(
            &mut out,
            frame.extent.width().value(),
            frame.extent.height().value(),
        );
        encoder.set_color(png::ColorType::Rgba);
        encoder.set_depth(png::BitDepth::Eight);
        let mut writer = encoder.write_header()?;
        writer.write_image_data(frame.rgba())?;
    }
    Ok(out)
}

pub fn decode_png(bytes: &[u8]) -> Result<(ViewportExtent, Vec<u8>)> {
    use crate::camera::ViewportPx;
    let decoder = png::Decoder::new(bytes);
    let mut reader = decoder.read_info()?;
    {
        let info = reader.info();
        if info.color_type != png::ColorType::Rgba || info.bit_depth != png::BitDepth::Eight {
            return Err(RenderError::PngFormat {
                color_type: info.color_type,
                bit_depth: info.bit_depth,
            });
        }
    }
    let mut buf = vec![0_u8; reader.output_buffer_size()];
    let info = reader.next_frame(&mut buf)?;
    let extent = ViewportExtent::new(ViewportPx::new(info.width), ViewportPx::new(info.height));
    buf.truncate(info.buffer_size());
    Ok((extent, buf))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::camera::ViewportPx;
    use crate::gpu::BackendTag;

    fn tag() -> BackendTag {
        BackendTag::from_backend(wgpu::Backend::Noop)
    }

    #[test]
    fn clear_color_rounds_to_nearest_u8() {
        let c = ClearColor::opaque(0.0, 0.5, 1.0);
        assert_eq!(c.to_rgba8(), [0, 128, 255, 255]);
    }

    #[test]
    fn style_default_is_navy_opaque() {
        let rgba = Style::default().background().to_rgba8();
        assert_eq!(rgba[3], 255);
        assert!(rgba[0] < 32 && rgba[1] < 32 && rgba[2] < 32);
    }

    fn encode_grayscale(width: u32, height: u32, pixels: &[u8]) -> Vec<u8> {
        let mut bytes: Vec<u8> = Vec::new();
        {
            let mut enc = png::Encoder::new(&mut bytes, width, height);
            enc.set_color(png::ColorType::Grayscale);
            enc.set_depth(png::BitDepth::Eight);
            let Ok(mut writer) = enc.write_header() else {
                panic!("grayscale png header write failed");
            };
            assert!(
                writer.write_image_data(pixels).is_ok(),
                "grayscale png body write failed",
            );
        }
        bytes
    }

    #[test]
    fn decode_png_rejects_non_rgba8() {
        let bytes = encode_grayscale(4, 4, &[0_u8; 16]);
        let Err(RenderError::PngFormat {
            color_type,
            bit_depth,
        }) = decode_png(&bytes)
        else {
            panic!("expected PngFormat rejection");
        };
        assert_eq!(color_type, png::ColorType::Grayscale);
        assert_eq!(bit_depth, png::BitDepth::Eight);
    }

    #[test]
    fn png_encode_decode_round_trips_solid_frame() {
        let extent = ViewportExtent::new(ViewportPx::new(8), ViewportPx::new(4));
        let pixel = [200_u8, 80, 40, 255];
        let rgba: Vec<u8> = (0..extent.pixel_count())
            .flat_map(|_| pixel.into_iter())
            .collect();
        let frame = SnapshotFrame::new(extent, rgba.clone(), tag());

        let Ok(encoded) = encode_png(&frame) else {
            panic!("encode_png failed");
        };
        let Ok((decoded_extent, decoded)) = decode_png(&encoded) else {
            panic!("decode_png failed");
        };
        assert_eq!(decoded_extent, extent);
        assert_eq!(decoded, rgba);
    }
}
