use std::io::Cursor;

use bone_text::{FontFace, FontWeight, ShapeRequest, ShapedLine, Shaper, load_font};
use swash::{
    FontRef,
    scale::{Render, ScaleContext, Source, image::Image},
    zeno::Format,
};

use crate::layout::{LayoutPx, LayoutRect};
use crate::strings::StringTable;
use crate::theme::{Color, SurfaceLevel, Theme};
use crate::widgets::{HorizontalAlign, PaintPrim, WidgetPaint, lower_paint};

const MARK_FONT_SCALE: f32 = 0.7;

#[derive(Debug, thiserror::Error)]
pub enum PngError {
    #[error("png encode failed: {0}")]
    Encode(String),
    #[error("png decode failed: {0}")]
    Decode(String),
    #[error("png dimensions {got_w}x{got_h} differ from expected {want_w}x{want_h}")]
    Dimensions {
        got_w: CanvasPx,
        got_h: CanvasPx,
        want_w: CanvasPx,
        want_h: CanvasPx,
    },
    #[error("png decoded buffer size {got} differs from expected {want}")]
    Length { got: usize, want: usize },
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CanvasPx(u32);

impl CanvasPx {
    #[must_use]
    pub const fn new(value: u32) -> Self {
        Self(value)
    }

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

impl core::fmt::Display for CanvasPx {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        self.0.fmt(f)
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct CanvasSize {
    pub width: CanvasPx,
    pub height: CanvasPx,
}

impl CanvasSize {
    #[must_use]
    pub const fn new(width: CanvasPx, height: CanvasPx) -> Self {
        Self { width, height }
    }

    #[must_use]
    pub const fn pixel_count(self) -> usize {
        (self.width.value() as usize) * (self.height.value() as usize)
    }
}

#[derive(Clone, Debug)]
struct Canvas {
    size: CanvasSize,
    pixels: Vec<[f32; 4]>,
}

impl Canvas {
    fn new(size: CanvasSize, clear: Color) -> Self {
        let fill = clear.linear_rgba_premul();
        Self {
            size,
            pixels: vec![fill; size.pixel_count()],
        }
    }

    fn fill_rect(&mut self, rect: LayoutRect, color: Color) {
        let bounds = self.bounds(rect);
        let src = color.linear_rgba_premul();
        composite_band(&mut self.pixels, self.size.width.value(), bounds, src);
    }

    fn outline_rect(&mut self, rect: LayoutRect, color: Color, thickness: f32) {
        let outer = self.bounds(rect);
        if outer.is_empty() {
            return;
        }
        #[allow(
            clippy::cast_possible_truncation,
            clippy::cast_sign_loss,
            reason = "stroke widths are small non-negative pixel counts"
        )]
        let t = thickness.max(1.0).round() as u32;
        let h = outer.y1 - outer.y0;
        let w = outer.x1 - outer.x0;
        let top_h = t.min(h);
        let bot_h = t.min(h - top_h);
        let left_w = t.min(w);
        let right_w = t.min(w - left_w);
        let mid_y0 = outer.y0 + top_h;
        let mid_y1 = outer.y1 - bot_h;
        let src = color.linear_rgba_premul();
        let bands = [
            PixelBounds {
                x0: outer.x0,
                y0: outer.y0,
                x1: outer.x1,
                y1: mid_y0,
            },
            PixelBounds {
                x0: outer.x0,
                y0: mid_y1,
                x1: outer.x1,
                y1: outer.y1,
            },
            PixelBounds {
                x0: outer.x0,
                y0: mid_y0,
                x1: outer.x0 + left_w,
                y1: mid_y1,
            },
            PixelBounds {
                x0: outer.x1 - right_w,
                y0: mid_y0,
                x1: outer.x1,
                y1: mid_y1,
            },
        ];
        bands.iter().for_each(|band| {
            composite_band(&mut self.pixels, self.size.width.value(), *band, src);
        });
    }

    fn bounds(&self, rect: LayoutRect) -> PixelBounds {
        let x0 = clamp_axis(rect.min_x(), self.size.width);
        let y0 = clamp_axis(rect.min_y(), self.size.height);
        let x1 = clamp_axis(rect.max_x(), self.size.width);
        let y1 = clamp_axis(rect.max_y(), self.size.height);
        PixelBounds {
            x0,
            y0,
            x1: x1.max(x0),
            y1: y1.max(y0),
        }
    }

    fn into_srgb_rgba8(self) -> Vec<u8> {
        self.pixels
            .iter()
            .flat_map(|p| pixel_to_srgb_u8(*p))
            .collect()
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
struct PixelBounds {
    x0: u32,
    y0: u32,
    x1: u32,
    y1: u32,
}

impl PixelBounds {
    fn is_empty(self) -> bool {
        self.x0 >= self.x1 || self.y0 >= self.y1
    }
}

fn clamp_axis(value: LayoutPx, limit: CanvasPx) -> u32 {
    let raw = value.value();
    #[allow(
        clippy::cast_precision_loss,
        reason = "canvas dimensions far below f32 precision boundary"
    )]
    let bounded = raw.clamp(0.0, limit.value() as f32);
    #[allow(
        clippy::cast_possible_truncation,
        clippy::cast_sign_loss,
        reason = "value clamped to [0, limit] before cast"
    )]
    let pixel = bounded.round() as u32;
    pixel
}

fn composite_band(pixels: &mut [[f32; 4]], stride: u32, bounds: PixelBounds, src: [f32; 4]) {
    if bounds.is_empty() {
        return;
    }
    let inv_alpha = 1.0 - src[3];
    (bounds.y0..bounds.y1).for_each(|y| {
        let row_offset = (y as usize) * (stride as usize);
        (bounds.x0..bounds.x1).for_each(|x| {
            let idx = row_offset + (x as usize);
            let dst = pixels[idx];
            pixels[idx] = [
                src[0] + dst[0] * inv_alpha,
                src[1] + dst[1] * inv_alpha,
                src[2] + dst[2] * inv_alpha,
                src[3] + dst[3] * inv_alpha,
            ];
        });
    });
}

fn pixel_to_srgb_u8(premul: [f32; 4]) -> [u8; 4] {
    let alpha = premul[3].clamp(0.0, 1.0);
    if alpha <= 0.0 {
        return [0, 0, 0, 0];
    }
    let inv = 1.0 / alpha;
    let to_u8 = |c: f32| {
        let unpremul = (c * inv).clamp(0.0, 1.0);
        unit_to_byte(linear_to_srgb(unpremul))
    };
    [
        to_u8(premul[0]),
        to_u8(premul[1]),
        to_u8(premul[2]),
        unit_to_byte(alpha),
    ]
}

fn unit_to_byte(unit: f32) -> u8 {
    #[allow(
        clippy::cast_possible_truncation,
        clippy::cast_sign_loss,
        reason = "value clamped to [0, 255] before cast"
    )]
    let byte = (unit * 255.0).round().clamp(0.0, 255.0) as u8;
    byte
}

fn linear_to_srgb(linear: f32) -> f32 {
    if linear <= 0.003_130_8 {
        12.92 * linear
    } else {
        1.055 * linear.powf(1.0 / 2.4) - 0.055
    }
}

#[must_use]
pub fn rasterize(
    theme: &Theme,
    paint: &[WidgetPaint],
    size: CanvasSize,
    strings: &StringTable,
) -> Vec<u8> {
    let mut canvas = Canvas::new(size, theme.colors.surface(SurfaceLevel::L0));
    let mut painter = GlyphPainter::new();
    paint
        .iter()
        .for_each(|p| draw(&mut canvas, p, theme, strings, &mut painter));
    canvas.into_srgb_rgba8()
}

fn draw(
    canvas: &mut Canvas,
    paint: &WidgetPaint,
    theme: &Theme,
    strings: &StringTable,
    painter: &mut GlyphPainter,
) {
    match paint {
        WidgetPaint::Label {
            rect,
            text,
            color,
            role,
        } => {
            painter.paint_text(
                canvas,
                *rect,
                text.resolve(strings),
                TextStyle {
                    font_size_px: role.size.as_px_f32(),
                    face: role.face,
                    weight: role.weight,
                    color: *color,
                    align: HorizontalAlign::Center,
                },
            );
        }
        WidgetPaint::AlignedLabel {
            rect,
            text,
            color,
            role,
            align,
        } => {
            painter.paint_text(
                canvas,
                *rect,
                text.resolve(strings),
                TextStyle {
                    font_size_px: role.size.as_px_f32(),
                    face: role.face,
                    weight: role.weight,
                    color: *color,
                    align: *align,
                },
            );
        }
        WidgetPaint::Mark { rect, kind, color } => {
            painter.paint_text(
                canvas,
                *rect,
                kind.glyph(),
                TextStyle {
                    font_size_px: rect.size.height.value() * MARK_FONT_SCALE,
                    face: FontFace::Sans,
                    weight: FontWeight::Regular,
                    color: *color,
                    align: HorizontalAlign::Center,
                },
            );
        }
        other => {
            let prim = lower_paint(theme, other);
            draw_prim(canvas, &prim);
        }
    }
}

fn draw_prim(canvas: &mut Canvas, prim: &PaintPrim) {
    if prim.fill.alpha() > 0.0 {
        canvas.fill_rect(prim.rect, prim.fill);
    }
    if let Some(b) = prim.border {
        canvas.outline_rect(prim.rect, b.color, b.width.value_px());
    }
}

struct GlyphPainter {
    shaper: Shaper,
    scale_ctx: ScaleContext,
    sans: FontRef<'static>,
    mono: FontRef<'static>,
}

impl GlyphPainter {
    fn new() -> Self {
        Self {
            shaper: Shaper::new(),
            scale_ctx: ScaleContext::new(),
            sans: load_font(FontFace::Sans),
            mono: load_font(FontFace::Mono),
        }
    }

    fn font_for(&self, face: FontFace) -> &FontRef<'static> {
        match face {
            FontFace::Sans => &self.sans,
            FontFace::Mono => &self.mono,
        }
    }

    fn paint_text(&mut self, canvas: &mut Canvas, rect: LayoutRect, text: &str, style: TextStyle) {
        if text.is_empty() || style.font_size_px <= 0.0 {
            return;
        }
        let layout = self.shaper.shape(
            text,
            ShapeRequest {
                face: style.face,
                size_px: style.font_size_px,
                weight: style.weight,
                line_height_px: 0.0,
                letter_spacing_px: 0.0,
                max_width: None,
            },
        );
        let metrics = self
            .font_for(style.face)
            .metrics(&[])
            .scale(style.font_size_px);
        let visible_advance = layout
            .lines
            .first()
            .map_or(0.0, ShapedLine::visible_advance_px);
        let rx = rect.origin.x.value();
        let ry = rect.origin.y.value();
        let rw = rect.size.width.value();
        let rh = rect.size.height.value();
        let start_x = match style.align {
            HorizontalAlign::Center => rx + ((rw - visible_advance) * 0.5).max(0.0),
            HorizontalAlign::Start => rx,
            HorizontalAlign::End => rx + (rw - visible_advance).max(0.0),
        };
        let baseline_y = ry + (rh + metrics.cap_height) * 0.5;
        let font = *self.font_for(style.face);
        let mut scaler = self
            .scale_ctx
            .builder(font)
            .size(style.font_size_px)
            .hint(false)
            .build();
        layout
            .lines
            .iter()
            .flat_map(|l| l.runs.iter())
            .for_each(|run| {
                run.glyphs.iter().for_each(|g| {
                    let Ok(glyph_id_u16) = u16::try_from(g.id.raw()) else {
                        return;
                    };
                    let mut image = Image::new();
                    let rendered = Render::new(&[Source::Outline])
                        .format(Format::Alpha)
                        .render_into(&mut scaler, glyph_id_u16, &mut image);
                    if !rendered {
                        return;
                    }
                    composite_alpha_mask(
                        canvas,
                        &image,
                        start_x + run.origin_x_px + g.x_px,
                        baseline_y,
                        style.color,
                    );
                });
            });
    }
}

#[derive(Copy, Clone)]
struct TextStyle {
    font_size_px: f32,
    face: FontFace,
    weight: FontWeight,
    color: Color,
    align: HorizontalAlign,
}

fn composite_alpha_mask(
    canvas: &mut Canvas,
    image: &Image,
    origin_x_px: f32,
    baseline_y_px: f32,
    color: Color,
) {
    let placement = image.placement;
    let mask_w = placement.width;
    let mask_h = placement.height;
    if mask_w == 0 || mask_h == 0 || image.data.len() < (mask_w as usize) * (mask_h as usize) {
        return;
    }
    #[allow(
        clippy::cast_precision_loss,
        reason = "glyph placement offsets stay well within f32 mantissa precision"
    )]
    let base_horiz = origin_x_px + placement.left as f32;
    #[allow(
        clippy::cast_precision_loss,
        reason = "glyph placement offsets stay well within f32 mantissa precision"
    )]
    let base_vert = baseline_y_px - placement.top as f32;
    let canvas_w = canvas.size.width.value();
    let canvas_h = canvas.size.height.value();
    let stride = canvas_w as usize;
    let color_premul = color.linear_rgba_premul();
    (0..mask_h).for_each(|py| {
        let Some(dst_y) = pixel_index(base_vert, py, canvas_h) else {
            return;
        };
        let row = (py as usize) * (mask_w as usize);
        (0..mask_w).for_each(|px| {
            let Some(dst_x) = pixel_index(base_horiz, px, canvas_w) else {
                return;
            };
            let mask = f32::from(image.data[row + (px as usize)]) / 255.0;
            if mask <= 0.0 {
                return;
            }
            let src = [
                color_premul[0] * mask,
                color_premul[1] * mask,
                color_premul[2] * mask,
                color_premul[3] * mask,
            ];
            let inv_alpha = 1.0 - src[3];
            let idx = (dst_y as usize) * stride + (dst_x as usize);
            let dst = canvas.pixels[idx];
            canvas.pixels[idx] = [
                src[0] + dst[0] * inv_alpha,
                src[1] + dst[1] * inv_alpha,
                src[2] + dst[2] * inv_alpha,
                src[3] + dst[3] * inv_alpha,
            ];
        });
    });
}

#[allow(
    clippy::cast_precision_loss,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    reason = "glyph mask offsets stay within f32 mantissa precision and the rounded coordinate is guarded against negatives and the canvas limit"
)]
fn pixel_index(base: f32, offset: u32, limit: u32) -> Option<u32> {
    let pos = (base + offset as f32).round();
    if !pos.is_finite() || pos < 0.0 {
        return None;
    }
    let value = pos as u32;
    (value < limit).then_some(value)
}

pub fn encode_png(rgba: &[u8], size: CanvasSize) -> Result<Vec<u8>, PngError> {
    let expected = size.pixel_count() * 4;
    if rgba.len() != expected {
        return Err(PngError::Length {
            got: rgba.len(),
            want: expected,
        });
    }
    let mut out = Vec::new();
    {
        let mut encoder = png::Encoder::new(&mut out, size.width.value(), size.height.value());
        encoder.set_color(png::ColorType::Rgba);
        encoder.set_depth(png::BitDepth::Eight);
        encoder.set_compression(png::Compression::Best);
        let mut writer = encoder
            .write_header()
            .map_err(|e| PngError::Encode(e.to_string()))?;
        writer
            .write_image_data(rgba)
            .map_err(|e| PngError::Encode(e.to_string()))?;
    }
    Ok(out)
}

pub fn decode_png(bytes: &[u8], size: CanvasSize) -> Result<Vec<u8>, PngError> {
    let decoder = png::Decoder::new(Cursor::new(bytes));
    let mut reader = decoder
        .read_info()
        .map_err(|e| PngError::Decode(e.to_string()))?;
    let info = reader.info();
    if info.width != size.width.value() || info.height != size.height.value() {
        return Err(PngError::Dimensions {
            got_w: CanvasPx::new(info.width),
            got_h: CanvasPx::new(info.height),
            want_w: size.width,
            want_h: size.height,
        });
    }
    let mut buf = vec![0_u8; reader.output_buffer_size()];
    let frame = reader
        .next_frame(&mut buf)
        .map_err(|e| PngError::Decode(e.to_string()))?;
    if frame.color_type != png::ColorType::Rgba {
        return Err(PngError::Decode(format!(
            "expected rgba color type, got {:?}",
            frame.color_type,
        )));
    }
    let rgba = buf[..frame.buffer_size()].to_vec();
    let expected = size.pixel_count() * 4;
    if rgba.len() != expected {
        return Err(PngError::Length {
            got: rgba.len(),
            want: expected,
        });
    }
    Ok(rgba)
}

#[cfg(test)]
mod tests {
    use super::{Canvas, CanvasPx, CanvasSize, rasterize};
    use crate::gallery::{GALLERY_CANVAS, GalleryState, render};
    use crate::strings::StringTable;
    use crate::theme::Theme;
    use std::sync::Arc;

    #[test]
    fn rasterizer_emits_canvas_sized_buffer() {
        let mut state = GalleryState::new();
        let paint = render(Arc::new(Theme::light()), &mut state);
        let pixels = rasterize(
            &Theme::light(),
            &paint,
            GALLERY_CANVAS,
            StringTable::empty(),
        );
        assert_eq!(pixels.len(), GALLERY_CANVAS.pixel_count() * 4);
    }

    #[test]
    fn light_and_dark_buffers_differ() {
        let render_for = |theme: Theme| {
            let mut state = GalleryState::new();
            let paint = render(Arc::new(theme.clone()), &mut state);
            rasterize(&theme, &paint, GALLERY_CANVAS, StringTable::empty())
        };
        let light = render_for(Theme::light());
        let dark = render_for(Theme::dark());
        assert_ne!(light, dark, "themes must produce distinct rasterizations");
    }

    #[test]
    fn outline_rect_corners_render_once_under_translucent_color() {
        use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
        use crate::theme::Color;
        let size = CanvasSize::new(CanvasPx::new(8), CanvasPx::new(8));
        let bg = Color::TRANSPARENT.with_alpha(1.0);
        let stroke = Theme::light().colors.focus_ring().with_alpha(0.5);
        let mut canvas = Canvas::new(size, bg);
        canvas.outline_rect(
            LayoutRect::new(
                LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
                LayoutSize::new(LayoutPx::new(8.0), LayoutPx::new(8.0)),
            ),
            stroke,
            1.0,
        );
        let bytes = canvas.into_srgb_rgba8();
        let pixel = |x: usize, y: usize| {
            let i = (y * size.width.value() as usize + x) * 4;
            [bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]]
        };
        assert_eq!(
            pixel(0, 0),
            pixel(3, 0),
            "corner pixel must match a non-corner top-edge pixel; double-paint detected",
        );
    }
}
