//! Software renderer for the fallback lock surface.
//!
//! Deliberately spartan: a dark field, a row of PIN dots, a 3x4 keypad,
//! digits from a 5x7 bitmap font. This surface exists for the seconds
//! before the shell takes over at boot and for the case where the shell is
//! a corpse — it competes on availability, not looks. The rich lockscreen
//! (swipe, widgets) stays in the shell.
//!
//! All drawing targets a raw ARGB8888 buffer (little-endian: B,G,R,A).

#![allow(dead_code)]

pub const BG: u32 = 0xFF0E0E12;
pub const KEY_FILL: u32 = 0xFF1D1D26;
pub const KEY_PRESSED: u32 = 0xFF3C3C4C;
pub const FG: u32 = 0xFFE6E6F0;
pub const DOT_EMPTY: u32 = 0xFF44444F;
pub const FAIL: u32 = 0xFFC63A4A;
pub const BUSY: u32 = 0xFF8A8A99;

/// Keypad cells in layout order: 1-9, backspace, 0, enter.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Key {
    Digit(u8),
    Backspace,
    Enter,
}

#[derive(Debug, Clone, Copy)]
pub struct Rect {
    pub x: i32,
    pub y: i32,
    pub w: i32,
    pub h: i32,
}

impl Rect {
    pub fn contains(&self, px: f64, py: f64) -> bool {
        px >= self.x as f64
            && py >= self.y as f64
            && px < (self.x + self.w) as f64
            && py < (self.y + self.h) as f64
    }
}

/// 5x7 glyphs, one bit per pixel, top row first.
fn glyph(key: Key) -> [u8; 7] {
    match key {
        Key::Digit(0) => [0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110],
        Key::Digit(1) => [0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
        Key::Digit(2) => [0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111],
        Key::Digit(3) => [0b11111, 0b00010, 0b00100, 0b00010, 0b00001, 0b10001, 0b01110],
        Key::Digit(4) => [0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010],
        Key::Digit(5) => [0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110],
        Key::Digit(6) => [0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110],
        Key::Digit(7) => [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000],
        Key::Digit(8) => [0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110],
        Key::Digit(9) => [0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100],
        Key::Digit(_) => [0; 7],
        // "<" — take one off
        Key::Backspace => [0b00010, 0b00100, 0b01000, 0b10000, 0b01000, 0b00100, 0b00010],
        // check mark — submit
        Key::Enter => [0b00000, 0b00001, 0b00010, 0b00100, 0b10100, 0b01000, 0b00000],
    }
}

/// Visual feedback state, decided by the caller's state machine.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mood {
    Entering,
    Checking,
    Failed,
}

pub struct Scene {
    pub pin_len: usize,
    pub mood: Mood,
    pub pressed: Option<Key>,
    /// Boot-handoff face: render nothing but the dark field. The keypad
    /// appears on first input, so a normal boot (shell takes over in
    /// seconds) never flashes a second PIN style at the user.
    pub quiet: bool,
    /// Number of failed authentication attempts. Displayed below the PIN
    /// dots so the user knows how many tries they've had. Advisory only —
    /// the actual lockout policy lives in PAM.
    pub failed_attempts: u32,
}

/// The keypad geometry for a surface of w x h. Pure function of size so
/// input hit-testing and drawing can never disagree.
pub fn keypad_layout(w: i32, h: i32) -> Vec<(Rect, Key)> {
    let keys = [
        Key::Digit(1),
        Key::Digit(2),
        Key::Digit(3),
        Key::Digit(4),
        Key::Digit(5),
        Key::Digit(6),
        Key::Digit(7),
        Key::Digit(8),
        Key::Digit(9),
        Key::Backspace,
        Key::Digit(0),
        Key::Enter,
    ];
    // Grid occupies the lower ~55% of the surface, centered, capped width.
    let grid_w = (w * 8 / 10).min(520);
    let grid_h = h * 55 / 100;
    let x0 = (w - grid_w) / 2;
    let y0 = h - grid_h - h / 20;
    let cell_w = grid_w / 3;
    let cell_h = grid_h / 4;
    let pad = (cell_w / 10).max(4);

    keys.iter()
        .enumerate()
        .map(|(i, &k)| {
            let col = (i % 3) as i32;
            let row = (i / 3) as i32;
            (
                Rect {
                    x: x0 + col * cell_w + pad,
                    y: y0 + row * cell_h + pad,
                    w: cell_w - 2 * pad,
                    h: cell_h - 2 * pad,
                },
                k,
            )
        })
        .collect()
}

pub fn hit_test(w: i32, h: i32, px: f64, py: f64) -> Option<Key> {
    keypad_layout(w, h)
        .into_iter()
        .find(|(r, _)| r.contains(px, py))
        .map(|(_, k)| k)
}

fn fill_rect(buf: &mut [u32], w: i32, h: i32, r: Rect, color: u32) {
    let x0 = r.x.max(0);
    let y0 = r.y.max(0);
    let x1 = (r.x + r.w).min(w);
    let y1 = (r.y + r.h).min(h);
    for y in y0..y1 {
        let row = (y * w) as usize;
        for x in x0..x1 {
            buf[row + x as usize] = color;
        }
    }
}

fn fill_circle(buf: &mut [u32], w: i32, h: i32, cx: i32, cy: i32, radius: i32, color: u32) {
    let r2 = radius * radius;
    for y in (cy - radius).max(0)..(cy + radius + 1).min(h) {
        for x in (cx - radius).max(0)..(cx + radius + 1).min(w) {
            let dx = x - cx;
            let dy = y - cy;
            if dx * dx + dy * dy <= r2 {
                buf[(y * w + x) as usize] = color;
            }
        }
    }
}

fn draw_glyph(buf: &mut [u32], w: i32, h: i32, key: Key, cell: Rect, color: u32) {
    let g = glyph(key);
    let scale = (cell.h / 12).max(2);
    let gw = 5 * scale;
    let gh = 7 * scale;
    let ox = cell.x + (cell.w - gw) / 2;
    let oy = cell.y + (cell.h - gh) / 2;
    for (row, bits) in g.iter().enumerate() {
        for col in 0..5 {
            if bits & (1 << (4 - col)) != 0 {
                fill_rect(
                    buf,
                    w,
                    h,
                    Rect {
                        x: ox + col as i32 * scale,
                        y: oy + row as i32 * scale,
                        w: scale,
                        h: scale,
                    },
                    color,
                );
            }
        }
    }
}

/// Render the whole scene into an ARGB8888 buffer of exactly w*h pixels.
pub fn render(buf: &mut [u32], w: i32, h: i32, scene: &Scene) {
    debug_assert_eq!(buf.len(), (w * h) as usize);
    buf.fill(BG);
    if scene.quiet {
        return;
    }

    // PIN dots, centered in the upper region above the keypad.
    let dot_color = match scene.mood {
        Mood::Entering => FG,
        Mood::Checking => BUSY,
        Mood::Failed => FAIL,
    };
    let shown = scene.pin_len.min(12).max(0);
    let slots = shown.max(4); // always draw at least 4 positions
    let radius = (w / 60).clamp(5, 12);
    let gap = radius * 4;
    let total = (slots as i32 - 1) * gap;
    let cy = h / 5;
    let cx0 = w / 2 - total / 2;
    for i in 0..slots {
        let cx = cx0 + i as i32 * gap;
        if i < shown {
            fill_circle(buf, w, h, cx, cy, radius, dot_color);
        } else {
            fill_circle(buf, w, h, cx, cy, radius, DOT_EMPTY);
            fill_circle(buf, w, h, cx, cy, radius - 2, BG);
        }
    }

    // Failed attempt counter: rendered as a red number below the PIN dots.
    // Only visible after at least one failed attempt. Uses the same digit
    // glyphs as the keypad.
    if scene.failed_attempts > 0 {
        let counter_y = cy + radius * 3;
        let digits: Vec<u8> = scene.failed_attempts
            .to_string()
            .bytes()
            .map(|b| b - b'0')
            .collect();
        let digit_w = (w / 20).clamp(12, 24);
        let digit_h = digit_w * 7 / 5;
        let counter_total = digits.len() as i32 * digit_w;
        let counter_x0 = w / 2 - counter_total / 2;
        for (i, &d) in digits.iter().enumerate() {
            let cell = Rect {
                x: counter_x0 + i as i32 * digit_w,
                y: counter_y,
                w: digit_w,
                h: digit_h,
            };
            draw_glyph(buf, w, h, Key::Digit(d), cell, FAIL);
        }
    }

    for (rect, key) in keypad_layout(w, h) {
        let fill = if scene.pressed == Some(key) {
            KEY_PRESSED
        } else {
            KEY_FILL
        };
        fill_rect(buf, w, h, rect, fill);
        draw_glyph(buf, w, h, key, rect, FG);
    }
}

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

    #[test]
    fn layout_and_hit_test_agree() {
        let (w, h) = (1080, 2160);
        for (rect, key) in keypad_layout(w, h) {
            let cx = rect.x as f64 + rect.w as f64 / 2.0;
            let cy = rect.y as f64 + rect.h as f64 / 2.0;
            assert_eq!(hit_test(w, h, cx, cy), Some(key));
        }
        assert_eq!(hit_test(w, h, 5.0, 5.0), None);
    }

    #[test]
    fn render_fills_buffer() {
        let (w, h) = (400, 800);
        let mut buf = vec![0u32; (w * h) as usize];
        render(
            &mut buf,
            w,
            h,
            &Scene { pin_len: 3, mood: Mood::Entering, pressed: Some(Key::Digit(5)), quiet: false, failed_attempts: 0 },
        );
        assert!(buf.iter().any(|&p| p == BG));
        assert!(buf.iter().any(|&p| p == KEY_FILL));
        assert!(buf.iter().any(|&p| p == KEY_PRESSED));
    }

    #[test]
    fn quiet_scene_is_bare_field() {
        let (w, h) = (400, 800);
        let mut buf = vec![0u32; (w * h) as usize];
        render(
            &mut buf,
            w,
            h,
            &Scene { pin_len: 0, mood: Mood::Entering, pressed: None, quiet: true, failed_attempts: 0 },
        );
        assert!(buf.iter().all(|&p| p == BG));
    }

    #[test]
    fn failed_attempts_renders_red_digits() {
        let (w, h) = (400, 800);
        let mut buf_no_fail = vec![0u32; (w * h) as usize];
        render(
            &mut buf_no_fail,
            w,
            h,
            &Scene { pin_len: 0, mood: Mood::Entering, pressed: None, quiet: false, failed_attempts: 0 },
        );
        let mut buf_with_fail = vec![0u32; (w * h) as usize];
        render(
            &mut buf_with_fail,
            w,
            h,
            &Scene { pin_len: 0, mood: Mood::Failed, pressed: None, quiet: false, failed_attempts: 3 },
        );
        // The failed-attempts render should introduce FAIL-colored pixels
        // that aren't in the zero-attempts render.
        let has_fail_color = buf_with_fail.iter().zip(buf_no_fail.iter())
            .any(|(&new, &old)| new == FAIL && old != FAIL);
        assert!(has_fail_color, "failed attempt counter should render FAIL-colored pixels");
    }
}
