//! Software rendering canvas backed by tiny-skia + fontdue.
//!
//! Mirrors the ltk Canvas API so forge's overlay UIs (lockscreen, OSD) can
//! render into an RGBA pixel buffer and upload it as a single GL texture
//! instead of composing dozens of individual textures.

use fontdue::{Font, FontSettings};
use tiny_skia::{Paint, PathBuilder, Pixmap, Transform};

/// RGBA color as four floats in 0..1.
#[derive(Clone, Copy)]
pub struct Color {
    pub r: f32,
    pub g: f32,
    pub b: f32,
    pub a: f32,
}

impl Color {
    pub const fn rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
        Self {
            r: r as f32 / 255.0,
            g: g as f32 / 255.0,
            b: b as f32 / 255.0,
            a: a as f32 / 255.0,
        }
    }

    fn to_tiny_skia(self) -> tiny_skia::Color {
        tiny_skia::Color::from_rgba(self.r, self.g, self.b, self.a).unwrap_or(tiny_skia::Color::TRANSPARENT)
    }
}

/// Software rendering surface.
pub struct SoftCanvas {
    pixmap: Pixmap,
    font: Font,
}

impl SoftCanvas {
    pub fn new(width: u32, height: u32) -> Self {
        let font_bytes = std::fs::read(find_font()).expect("font not found");
        let font = Font::from_bytes(font_bytes.as_slice(), FontSettings::default())
            .expect("bad font");
        Self {
            pixmap: Pixmap::new(width.max(1), height.max(1)).expect("pixmap"),
            font,
        }
    }

    /// Resize the pixel buffer (reuses the font).
    pub fn resize(&mut self, width: u32, height: u32) {
        self.pixmap = Pixmap::new(width.max(1), height.max(1)).expect("pixmap");
    }

    pub fn width(&self) -> u32 {
        self.pixmap.width()
    }

    pub fn height(&self) -> u32 {
        self.pixmap.height()
    }

    /// Clear to fully transparent.
    pub fn clear(&mut self) {
        self.pixmap.fill(tiny_skia::Color::TRANSPARENT);
    }

    /// Fill the entire canvas with a solid color.
    pub fn fill(&mut self, color: Color) {
        self.pixmap.fill(color.to_tiny_skia());
    }

    /// Fill a rectangle with optional corner radius.
    pub fn fill_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: Color, radius: f32) {
        let Some(ts_rect) = tiny_skia::Rect::from_xywh(x, y, w, h) else { return };
        let mut paint = Paint::default();
        paint.set_color(color.to_tiny_skia());
        paint.anti_alias = true;
        if radius > 0.0 {
            if let Some(path) = build_rounded_rect(ts_rect, radius) {
                self.pixmap.fill_path(
                    &path,
                    &paint,
                    tiny_skia::FillRule::Winding,
                    Transform::identity(),
                    None,
                );
            }
        } else {
            self.pixmap.fill_rect(ts_rect, &paint, Transform::identity(), None);
        }
    }

    /// Render text at (x, y) where y is the baseline.
    pub fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: Color) {
        let pw = self.pixmap.width() as f32;
        let ph = self.pixmap.height() as f32;
        let line_h = size * 1.5;
        if y + line_h < 0.0 || y - line_h > ph || x > pw { return; }

        let glyphs: Vec<(fontdue::Metrics, Vec<u8>, f32)> = {
            let mut cursor_x = x;
            text.chars().map(|ch| {
                let (metrics, bitmap) = self.font.rasterize(ch, size);
                let advance = metrics.advance_width;
                let start_x = cursor_x;
                cursor_x += advance;
                (metrics, bitmap, start_x)
            }).collect()
        };

        let w = self.pixmap.width() as i32;
        let h = self.pixmap.height() as i32;
        let cr = (color.r * 255.0) as u8;
        let cg = (color.g * 255.0) as u8;
        let cb = (color.b * 255.0) as u8;
        let color_a = color.a;
        let pixels = self.pixmap.data_mut();

        for (metrics, bitmap, cursor_x) in glyphs {
            for (i, &alpha) in bitmap.iter().enumerate() {
                if alpha == 0 { continue; }
                let px = cursor_x as i32 + metrics.xmin + (i % metrics.width) as i32;
                let py = y as i32 - metrics.ymin as i32 - metrics.height as i32 + 1
                    + (i / metrics.width) as i32;
                if px < 0 || py < 0 || px >= w || py >= h { continue; }
                let idx = (py as usize * w as usize + px as usize) * 4;
                let a = (alpha as f32 / 255.0) * color_a;
                let inv = 1.0 - a;
                pixels[idx]     = (cr as f32 * a + pixels[idx]     as f32 * inv) as u8;
                pixels[idx + 1] = (cg as f32 * a + pixels[idx + 1] as f32 * inv) as u8;
                pixels[idx + 2] = (cb as f32 * a + pixels[idx + 2] as f32 * inv) as u8;
                pixels[idx + 3] = 255;
            }
        }
    }

    /// Measure text advance width.
    pub fn measure_text(&self, text: &str, size: f32) -> f32 {
        text.chars().map(|ch| {
            let m = self.font.metrics(ch, size);
            m.advance_width
        }).sum()
    }

    /// Measure text height (ascent + descent based on font metrics at given size).
    pub fn measure_text_height(&self, size: f32) -> f32 {
        // Use a reference character to get line metrics
        let m = self.font.metrics('M', size);
        m.height as f32
    }

    /// Blit raw RGBA image data at (dest_x, dest_y), nearest-neighbor, no scaling.
    pub fn draw_image_rgba(&mut self, rgba: &[u8], img_w: u32, img_h: u32, dest_x: i32, dest_y: i32) {
        let cw = self.pixmap.width() as i32;
        let ch = self.pixmap.height() as i32;
        let pixels = self.pixmap.data_mut();

        for row in 0..img_h as i32 {
            let py = dest_y + row;
            if py < 0 || py >= ch { continue; }
            for col in 0..img_w as i32 {
                let px = dest_x + col;
                if px < 0 || px >= cw { continue; }
                let src_idx = ((row as u32 * img_w + col as u32) * 4) as usize;
                let dst_idx = (py as usize * cw as usize + px as usize) * 4;
                let sa = rgba[src_idx + 3] as f32 / 255.0;
                if sa < 0.001 { continue; }
                let inv = 1.0 - sa;
                pixels[dst_idx]     = (rgba[src_idx]     as f32 * sa + pixels[dst_idx]     as f32 * inv) as u8;
                pixels[dst_idx + 1] = (rgba[src_idx + 1] as f32 * sa + pixels[dst_idx + 1] as f32 * inv) as u8;
                pixels[dst_idx + 2] = (rgba[src_idx + 2] as f32 * sa + pixels[dst_idx + 2] as f32 * inv) as u8;
                pixels[dst_idx + 3] = ((sa + pixels[dst_idx + 3] as f32 / 255.0 * inv) * 255.0) as u8;
            }
        }
    }

    /// Get the raw RGBA pixel data.
    pub fn data(&self) -> &[u8] {
        self.pixmap.data()
    }

    /// Zero the alpha of pixels that fall in the corner regions outside
    /// the rounded-rect of `radius_px`. Used to clip a freshly composed
    /// image into the same rounded silhouette the panel background uses.
    pub fn mask_outside_rounded_rect(&mut self, radius_px: f32) {
        if radius_px <= 0.5 { return; }
        let w = self.pixmap.width() as i32;
        let h = self.pixmap.height() as i32;
        let r = radius_px;
        let r_sq = r * r;
        let r_int = r.ceil() as i32;
        let pixels = self.pixmap.data_mut();
        let corners: [(f32, f32, i32, i32); 4] = [
            ( r,                r,                -1, -1 ),
            ( w as f32 - r,     r,                 1, -1 ),
            ( r,                h as f32 - r,     -1,  1 ),
            ( w as f32 - r,     h as f32 - r,      1,  1 ),
        ];
        for (cx, cy, sx, sy) in corners {
            for dy in 0..r_int {
                for dx in 0..r_int {
                    let px = cx as i32 + sx * dx;
                    let py = cy as i32 + sy * dy;
                    if px < 0 || px >= w || py < 0 || py >= h { continue; }
                    let fx = px as f32 + 0.5 - cx;
                    let fy = py as f32 + 0.5 - cy;
                    if fx * fx + fy * fy > r_sq {
                        let idx = (py as usize * w as usize + px as usize) * 4;
                        if idx + 3 < pixels.len() {
                            pixels[idx]     = 0;
                            pixels[idx + 1] = 0;
                            pixels[idx + 2] = 0;
                            pixels[idx + 3] = 0;
                        }
                    }
                }
            }
        }
    }
}

fn build_rounded_rect(rect: tiny_skia::Rect, radius: f32) -> Option<tiny_skia::Path> {
    let r = radius.min(rect.width() / 2.0).min(rect.height() / 2.0);
    let k = 0.5523_f32;
    let kk = r * k;

    let x0 = rect.left();
    let y0 = rect.top();
    let x1 = rect.right();
    let y1 = rect.bottom();

    let mut pb = PathBuilder::new();
    pb.move_to(x0 + r, y0);
    pb.line_to(x1 - r, y0);
    pb.cubic_to(x1 - r + kk, y0, x1, y0 + r - kk, x1, y0 + r);
    pb.line_to(x1, y1 - r);
    pb.cubic_to(x1, y1 - r + kk, x1 - r + kk, y1, x1 - r, y1);
    pb.line_to(x0 + r, y1);
    pb.cubic_to(x0 + r - kk, y1, x0, y1 - r + kk, x0, y1 - r);
    pb.line_to(x0, y0 + r);
    pb.cubic_to(x0, y0 + r - kk, x0 + r - kk, y0, x0 + r, y0);
    pb.close();
    pb.finish()
}

fn find_font() -> String {
    // Try a static list of common distro paths first — cheap and avoids
    // forking fc-match for the common cases.
    let candidates = [
        "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
        "/usr/share/fonts/liberation/LiberationSans-Regular.ttf",
        "/usr/share/fonts/truetype/freefont/FreeSans.ttf",
        "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
        "/usr/share/fonts/dejavu/DejaVuSans.ttf",
        "/usr/share/fonts/dejavu-sans-fonts/DejaVuSans.ttf",
        "/usr/share/fonts/TTF/DejaVuSans.ttf",
        // Fedora / Bazzite / immutable distros default to Cantarell.
        "/usr/share/fonts/abattis-cantarell-fonts/Cantarell-Regular.otf",
        "/usr/share/fonts/cantarell/Cantarell-Regular.otf",
        "/usr/share/fonts/google-noto/NotoSans-Regular.ttf",
    ];
    if let Some(path) = candidates.iter().find(|p| std::path::Path::new(p).exists()) {
        return path.to_string();
    }

    // Fall back to fontconfig — covers Bazzite (Noto Sans VF) and any
    // distro layout we haven't enumerated. fc-match prints "<path>:
    // <family>: <style>" by default; `-f "%{file}"` gives just the path.
    if let Ok(out) = std::process::Command::new("fc-match")
        .args(["-f", "%{file}", "sans-serif"])
        .output()
    {
        if out.status.success() {
            let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
            if !path.is_empty() && std::path::Path::new(&path).exists() {
                return path;
            }
        }
    }

    // Last-resort walk: pick any TTF/OTF under /usr/share/fonts. Better
    // to render with the wrong font than to panic the whole compositor.
    if let Ok(walker) = std::fs::read_dir("/usr/share/fonts") {
        for top in walker.flatten() {
            if let Ok(inner) = std::fs::read_dir(top.path()) {
                for entry in inner.flatten() {
                    let p = entry.path();
                    let ext = p.extension().and_then(|e| e.to_str()).unwrap_or("");
                    if (ext.eq_ignore_ascii_case("ttf") || ext.eq_ignore_ascii_case("otf"))
                        && p.to_string_lossy().to_lowercase().contains("sans")
                    {
                        if let Some(s) = p.to_str() {
                            return s.to_string();
                        }
                    }
                }
            }
        }
    }

    panic!("No system font found (tried {} static paths, fc-match, and /usr/share/fonts walk)", candidates.len());
}
