//! Render a URL as a terminal-friendly QR code for the OAuth login
//! flow (`/login`, `/codingplan`).
//!
//! Two styles, both Unicode:
//! * `Dense1x2` (default): 1×2 modules per char using `▀▄█`.
//!   ≈ 45 cols × 23 rows. Block elements (U+2580–U+259F) are
//!   Unicode-Neutral width, so no terminal renders them at double
//!   width. QR aspect stays 1:1 and scanners decode reliably under
//!   any configuration.
//! * `Braille` (opt-in): packs 2×4 modules into one U+2800–U+28FF
//!   char. ≈ 23 cols × 12 rows — about half the size. But Braille
//!   is Unicode-Ambiguous width and gets stretched 2× horizontally
//!   on terminals that default ambiguous-width to double (iTerm2's
//!   "Treat ambiguous-width characters as double width", on by
//!   default in many profiles). Use only when you know your
//!   terminal renders braille at single cell width.
//!
//! There is no ASCII rendering: at typical 1:2 monospace cell
//! ratios, an ASCII QR with `module_dimensions(2, 1)` exceeds 90
//! columns for any usable URL — wider than every realistic terminal
//! window. Callers (`compose_login_chrome`) must short-circuit to
//! URL-only output when `TerminalCaps::unicode_symbols` is false.

use qrcode::render::unicode::Dense1x2;
use qrcode::{Color, QrCode};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QrStyle {
    Braille,
    Dense1x2,
}

/// Render `url` as a multi-line QR string. Returns `None` only when QR
/// encoding itself fails (URL exceeds version 40 capacity ~4 KB —
/// never the case for OAuth URLs in practice).
pub fn render_login_qr(url: &str, style: QrStyle) -> Option<String> {
    let code = QrCode::new(url.as_bytes()).ok()?;
    let s = match style {
        QrStyle::Braille => render_braille(&code),
        QrStyle::Dense1x2 => code
            .render::<Dense1x2>()
            .dark_color(Dense1x2::Dark)
            .light_color(Dense1x2::Light)
            .quiet_zone(true)
            .build(),
    };
    Some(s)
}

/// Visible column count of the widest line in `rendered`. Both styles
/// emit exactly one terminal cell per char, so `chars().count()` is
/// the correct cell count.
pub fn block_cols(rendered: &str) -> usize {
    rendered
        .lines()
        .map(|l| l.chars().count())
        .max()
        .unwrap_or(0)
}

/// Pack a QR matrix into Braille glyphs (2 cols × 4 rows of modules
/// per char). Standard 4-module quiet zone is added before packing —
/// scanners need the white margin to find the corner finder patterns.
///
/// Braille bit layout (Unicode 6.0):
/// ```text
///   col 0 row 0 → 0x01     col 1 row 0 → 0x08
///   col 0 row 1 → 0x02     col 1 row 1 → 0x10
///   col 0 row 2 → 0x04     col 1 row 2 → 0x20
///   col 0 row 3 → 0x40     col 1 row 3 → 0x80
/// ```
fn render_braille(code: &QrCode) -> String {
    const BRAILLE_BITS: [[u32; 4]; 2] = [
        [0x01, 0x02, 0x04, 0x40], // left column
        [0x08, 0x10, 0x20, 0x80], // right column
    ];
    const QUIET: usize = 4;

    let width = code.width();
    let colors = code.to_colors();
    let total = width + 2 * QUIET;

    // Light (false) outside the data area, including the quiet zone.
    let is_dark = |x: usize, y: usize| -> bool {
        if x < QUIET || x >= QUIET + width || y < QUIET || y >= QUIET + width {
            return false;
        }
        matches!(colors[(x - QUIET) + (y - QUIET) * width], Color::Dark)
    };

    let cols = total.div_ceil(2);
    let rows = total.div_ceil(4);
    let mut out = String::with_capacity(rows * (cols * 3 + 1));

    for cy in 0..rows {
        for cx in 0..cols {
            let mut bits: u32 = 0;
            for dx in 0..2usize {
                for dy in 0..4usize {
                    if is_dark(cx * 2 + dx, cy * 4 + dy) {
                        bits |= BRAILLE_BITS[dx][dy];
                    }
                }
            }
            // 0x2800 + bits is always within U+2800..=U+28FF (8-bit
            // payload), so unwrap is safe.
            out.push(char::from_u32(0x2800 + bits).unwrap());
        }
        if cy + 1 < rows {
            out.push('\n');
        }
    }
    out
}

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

    const SAMPLE_URL: &str =
        "https://acs.atomgit.com/auth/login?state=abcd1234efgh5678ijkl9012mnop3456";

    #[test]
    fn braille_path_emits_braille_chars() {
        let rendered = render_login_qr(SAMPLE_URL, QrStyle::Braille).expect("qr encodes");
        assert!(
            rendered
                .chars()
                .any(|c| (c as u32) >= 0x2800 && (c as u32) <= 0x28FF),
            "expected at least one braille char in QR, got: {:?}",
            rendered.chars().take(40).collect::<String>()
        );
    }

    #[test]
    fn dense1x2_path_emits_block_glyphs() {
        let rendered = render_login_qr(SAMPLE_URL, QrStyle::Dense1x2).expect("qr encodes");
        assert!(
            rendered
                .chars()
                .any(|c| matches!(c, '\u{2588}' | '\u{2580}' | '\u{2584}')),
            "expected at least one block glyph in dense1x2 QR, got: {:?}",
            rendered.chars().take(40).collect::<String>()
        );
    }

    #[test]
    fn rendered_lines_have_uniform_width() {
        for style in [QrStyle::Braille, QrStyle::Dense1x2] {
            let rendered = render_login_qr(SAMPLE_URL, style).expect("qr encodes");
            let widths: Vec<usize> = rendered.lines().map(|l| l.chars().count()).collect();
            let first = widths[0];
            assert!(
                widths.iter().all(|w| *w == first),
                "rendered QR ({:?}) has uneven line widths: {:?}",
                style,
                widths
            );
        }
    }

    #[test]
    fn braille_is_about_half_size_of_dense1x2() {
        // Braille packs 2×4 modules per char, Dense1x2 packs 1×2.
        // So braille should be ~½ width and ~½ height of Dense1x2.
        let braille = render_login_qr(SAMPLE_URL, QrStyle::Braille).unwrap();
        let dense = render_login_qr(SAMPLE_URL, QrStyle::Dense1x2).unwrap();
        let (bw, bh) = (block_cols(&braille), braille.lines().count());
        let (dw, dh) = (block_cols(&dense), dense.lines().count());
        // Allow ±1 cell slack from ceiling rounding.
        assert!(
            bw * 2 <= dw + 1,
            "braille width {} should be ~½ of dense1x2 width {}",
            bw,
            dw
        );
        assert!(
            bh * 2 <= dh + 1,
            "braille height {} should be ~½ of dense1x2 height {}",
            bh,
            dh
        );
    }

    #[test]
    fn block_cols_matches_widest_line() {
        let rendered = "###\n#####\n##";
        assert_eq!(block_cols(rendered), 5);
    }
}