//! UI-Overlay: **state-driven** und **immediate-mode**.
//!
//! [`layout`] ist eine reine Funktion des [`Session`]-Zustands — es hält
//! keinen eigenen mutierbaren Zustand (anders als irl3ds Widget-Bäume, die
//! neben dem Spielzustand desyncen konnten). Jeden Frame wird aus dem
//! aktuellen [`Mode`] neu gebaut: Dialog → Panel + Choice-Buttons, Menü →
//! Menü-Buttons, usw. Klicks erzeugen denselben Eingabe-String, den auch
//! CLI und stdin liefern, und laufen durch dasselbe `Session::exec` — ein
//! einziger Trichter in den State, kein UI-Backdoor.
//!
//! Koordinaten sind interne Pixel (das Overlay wird vor dem Blit gezeichnet,
//! teilt also den Lo-Fi-Look). Gezeichnet wird über [`crate::render::sprite`].

use crate::engine::tga::Image;
use crate::render::font::{self, Font, Fonts};
use crate::render::sprite::{SpriteBatch, SpriteVertex};
use crate::session::{Dialog, Mode, Session};

/// Feste Textur-Indizes im Sprite-Pass — Reihenfolge = `ui_textures` in
/// render::run. `WHITE` ist ein 1×1-Weiß-Texel für Solid-Fills (Farbe kommt
/// rein aus dem Tint), `FONT_*` die CP437-Glyph-Atlanten, `CURSORS` das
/// 2×2-Raster der 16×16-Cursor, `ORN` der 9-Slice-Ornamentrahmen.
pub(crate) const WHITE:    usize = 0;
pub(crate) const FONT_EGA: usize = 1;
pub(crate) const FONT_CGA: usize = 2;
pub(crate) const CURSORS:  usize = 3;
pub(crate) const ORN:      usize = 4;

/// Cursor-Indizes im 2×2-Raster (Pivot mittig je 16×16-Zelle).
pub(crate) const CUR_MOUSE:          usize = 0; // Maus, normal
pub(crate) const CUR_MOUSE_INTERACT: usize = 1; // Maus, über Klickziel
pub(crate) const CUR_HUD:            usize = 2; // First-Person-Pointer, normal
pub(crate) const CUR_HUD_INTERACT:   usize = 3; // dito, über einem Klick-Ziel

const CURSOR_PX: f32 = 16.0;
const ORN_CORNER: f32 = 8.0; // Eckgröße im Ornament-Atlas (und im Panel)

/// 1×1 weißes RGBA8-`Image` für getintete Solid-Flächen.
pub(crate) fn white_pixel() -> Image {
    Image { width: 1, height: 1, rgba: vec![255, 255, 255, 255] }
}

/// Weiß-auf-schwarz-Sprite (Font, Cursor, Ornament) → getintbare Maske:
/// rgb = weiß, alpha = Luminanz. So blendet der Sprite-Pass den schwarzen
/// Hintergrund weg und der Tint setzt die Farbe. Reine CPU-Nachbearbeitung
/// beim Laden.
pub(crate) fn key_luminance(img: &Image) -> Image {
    let mut rgba = img.rgba.clone();
    for px in rgba.chunks_mut(4) {
        let lum = px[0].max(px[1]).max(px[2]);
        px[0] = 255; px[1] = 255; px[2] = 255; px[3] = lum;
    }
    Image { width: img.width, height: img.height, rgba }
}

/// Cursor, wie ihn die App pro Frame an [`layout`] gibt.
pub(crate) struct Cursor {
    /// Position in internen Pixeln (über die Letterbox gemappt).
    pub(crate) pos:     [f32; 2],
    /// Maus gefangen (Flycam aktiv) → First-Person-Pointer mittig statt
    /// Maus-Cursor an `pos`.
    pub(crate) grabbed: bool,
    /// Fadenkreuz zielt auf ein Klick-Ziel (`Session::pick`) → Interakt-
    /// Variante des HUD-Pointers.
    pub(crate) hud_interact: bool,
}

/// Akkumulierte Overlay-Geometrie eines Frames, fertig für den Sprite-Pass.
#[derive(Default)]
pub(crate) struct Ui {
    pub(crate) verts:   Vec<SpriteVertex>,
    pub(crate) batches: Vec<SpriteBatch>,
}

impl Ui {
    /// Ein getintetes Quad. `rect` = (x, y, w, h) in internen Pixeln
    /// (oben-links), `uv` = (u0, v0, u1, v1) in 0..1. Aufeinanderfolgende
    /// Quads derselben Textur landen im selben Batch.
    pub(crate) fn quad(&mut self, tex: usize, rect: [f32; 4], uv: [f32; 4], color: [f32; 4]) {
        let [x, y, w, h] = rect;
        let [u0, v0, u1, v1] = uv;
        let base = self.verts.len() as u32;
        // Zwei Dreiecke (TL, BL, BR / TL, BR, TR).
        let tl = SpriteVertex { pos: [x,     y    ], uv: [u0, v0], color };
        let bl = SpriteVertex { pos: [x,     y + h], uv: [u0, v1], color };
        let br = SpriteVertex { pos: [x + w, y + h], uv: [u1, v1], color };
        let tr = SpriteVertex { pos: [x + w, y    ], uv: [u1, v0], color };
        self.verts.extend([tl, bl, br, tl, br, tr]);

        // An letzten Batch anhängen, wenn gleiche Textur, sonst neuen öffnen.
        match self.batches.last_mut() {
            Some(b) if b.texture == tex => b.count += 6,
            _ => self.batches.push(SpriteBatch { texture: tex, start: base, count: 6 }),
        }
    }

    /// Getintetes Solid-Rechteck (weißer Texel).
    pub(crate) fn fill(&mut self, rect: [f32; 4], color: [f32; 4]) {
        self.quad(WHITE, rect, [0.0, 0.0, 1.0, 1.0], color);
    }

    /// Eine Textzeile ab (x, y) oben-links in `font`, proportional gesetzt
    /// (variable Laufweite, leere Glyphen werden übersprungen). `\n` springt
    /// eine Zeile weiter. Gibt die x-Endposition zurück.
    pub(crate) fn text(&mut self, font: &Font, x: f32, y: f32, s: &str, color: [f32; 4]) -> f32 {
        let (mut cx, mut cy) = (x, y);
        for ch in s.chars() {
            if ch == '\n' { cx = x; cy += font.glyph_h; continue; }
            let code = font::cp437(ch);
            if let Some((uv, w)) = font.glyph_quad(code) {
                self.quad(font.tex(), [cx, cy, w, font.glyph_h], uv, color);
            }
            cx += font.advance(code);
        }
        cx
    }

    /// Ein Cursor-Sprite (16×16, Pivot mittig) an `pos`, getintet.
    pub(crate) fn cursor(&mut self, idx: usize, pos: [f32; 2], color: [f32; 4]) {
        let rect = [pos[0] - CURSOR_PX * 0.5, pos[1] - CURSOR_PX * 0.5, CURSOR_PX, CURSOR_PX];
        self.quad(CURSORS, rect, cursor_uv(idx), color);
    }

    /// Opakes Panel mit Ornamentrahmen. `rect` = Außenbounds. Erst der
    /// Hintergrund (Solid, getintet), dann der 9-Slice-Rahmen darüber: feste
    /// 8×8-Ecken, die Kanten dazwischen gekachelt bis zur nächsten Ecke.
    pub(crate) fn panel(&mut self, rect: [f32; 4], bg: [f32; 4], frame: [f32; 4]) {
        self.fill(rect, bg);
        self.ornament_frame(rect, frame);
    }

    fn ornament_frame(&mut self, rect: [f32; 4], color: [f32; 4]) {
        let [x, y, w, h] = rect;
        let c = ORN_CORNER;
        let a = ORN_ATLAS;
        // Vier Ecken (1:1 aus dem Atlas).
        self.quad(ORN, [x,         y,         c, c], orn_uv(0.0,    0.0,    c, c), color);
        self.quad(ORN, [x + w - c, y,         c, c], orn_uv(a - c,  0.0,    c, c), color);
        self.quad(ORN, [x,         y + h - c, c, c], orn_uv(0.0,    a - c,  c, c), color);
        self.quad(ORN, [x + w - c, y + h - c, c, c], orn_uv(a - c,  a - c,  c, c), color);
        // Kanten zwischen den Ecken kacheln.
        self.tile_h(x + c, x + w - c, y,         c, [c, 0.0],   color); // oben
        self.tile_h(x + c, x + w - c, y + h - c, c, [c, a - c], color); // unten
        self.tile_v(y + c, y + h - c, x,         c, [0.0, c],   color); // links
        self.tile_v(y + c, y + h - c, x + w - c, c, [a - c, c], color); // rechts
    }

    /// Horizontale Kante von `x0` bis `x1` bei `y` (Höhe `c`) mit dem
    /// `c`-breiten Atlas-Sample ab `src` (Pixel) kacheln; letzte Kachel wird
    /// passend beschnitten.
    fn tile_h(&mut self, x0: f32, x1: f32, y: f32, c: f32, src: [f32; 2], color: [f32; 4]) {
        let mut cx = x0;
        while cx < x1 - 0.5 {
            let seg = (x1 - cx).min(ORN_EDGE);
            self.quad(ORN, [cx, y, seg, c], orn_uv(src[0], src[1], seg, c), color);
            cx += seg;
        }
    }

    fn tile_v(&mut self, y0: f32, y1: f32, x: f32, c: f32, src: [f32; 2], color: [f32; 4]) {
        let mut cy = y0;
        while cy < y1 - 0.5 {
            let seg = (y1 - cy).min(ORN_EDGE);
            self.quad(ORN, [x, cy, c, seg], orn_uv(src[0], src[1], c, seg), color);
            cy += seg;
        }
    }
}

const ORN_ATLAS: f32 = 32.0; // Ornament-Atlasgröße (32×32)
const ORN_EDGE:  f32 = 16.0; // Kanten-Sample-Länge (zwischen den 8px-Ecken)

/// Atlas-Pixel-Rechteck → UV (0..1) im 32×32-Ornamentatlas.
fn orn_uv(x: f32, y: f32, w: f32, h: f32) -> [f32; 4] {
    [x / ORN_ATLAS, y / ORN_ATLAS, (x + w) / ORN_ATLAS, (y + h) / ORN_ATLAS]
}

/// UV-Rechteck einer Cursor-Zelle im 2×2-Raster (16×16 je Zelle, Atlas 32×32).
fn cursor_uv(idx: usize) -> [f32; 4] {
    let col = (idx % 2) as f32;
    let row = (idx / 2) as f32;
    let (x0, y0) = (col * CURSOR_PX, row * CURSOR_PX);
    [x0 / 32.0, y0 / 32.0, (x0 + CURSOR_PX) / 32.0, (y0 + CURSOR_PX) / 32.0]
}

/// Ergebnis eines Frames: die Overlay-Geometrie und — falls der Cursor über
/// einem Klickziel steht — dessen Aktion (Eingabe-String für `Session::exec`).
pub(crate) struct Screen {
    pub(crate) ui:           Ui,
    pub(crate) hover_action: Option<String>,
}

/// Punkt in Rechteck (x, y, w, h)?
fn in_rect(p: [f32; 2], r: [f32; 4]) -> bool {
    p[0] >= r[0] && p[0] < r[0] + r[2] && p[1] >= r[1] && p[1] < r[1] + r[3]
}

/// Ein klickbarer Button: Fläche + zentriertes Label. Steht der Cursor
/// darüber, wird er hervorgehoben und seine `action` in `hover` vermerkt
/// (der Klick wirkt dann auf genau diese Aktion).
fn button(
    ui: &mut Ui, font: &Font, rect: [f32; 4], label: &str, action: &str,
    cur: [f32; 2], hover: &mut Option<String>,
) {
    let hot = in_rect(cur, rect);
    if hot { *hover = Some(action.to_string()); }
    let bg = if hot { [0.34, 0.22, 0.46, 1.0] } else { [0.15, 0.12, 0.22, 1.0] };
    ui.fill(rect, bg);
    let tx = rect[0] + (rect[2] - font.text_width(label)) * 0.5;
    let ty = rect[1] + (rect[3] - font.glyph_h) * 0.5;
    ui.text(font, tx.round(), ty.round(), label, [0.95, 0.95, 1.0, 1.0]);
}

/// Innenabstand vom Panelrand zum Inhalt: 8px Ornamentrahmen + 3px Luft.
const INSET: f32 = ORN_CORNER + 3.0;
const PANEL_BG:    [f32; 4] = [0.06, 0.05, 0.10, 1.0];
const PANEL_FRAME: [f32; 4] = [0.85, 0.75, 0.95, 1.0];
const TEXT:        [f32; 4] = [0.92, 0.92, 1.0, 1.0];

/// Das Overlay für den aktuellen Frame bauen — verzweigt nach `session.mode`.
pub(crate) fn layout(internal: [f32; 2], fonts: &Fonts, session: &Session, cur: &Cursor) -> Screen {
    let mut ui = Ui::default();
    let mut hover: Option<String> = None;
    // EGA 8×14 ist der UI-Standard-Font: bei 640×480 der klassische
    // 80-Spalten-VGA-Textmodus. CGA 8×8 bleibt für kleines HUD-Zeug.
    let f = &fonts.ega;

    // Im Flycam-Modus (Maus gefangen) zählt die freie Sicht → kein Panel.
    // Sonst je nach Modus Menü oder Dialog. Hit-Tests laufen nur bei freier
    // Maus (im Dialog ist die Maus ohnehin nie gefangen).
    if !cur.grabbed {
        match &session.mode {
            // Spiel und Flycam: nur HUD/Cursor, kein Panel (beide laufen
            // ohnehin gefangen — dieser Zweig greift nur im Übergang).
            Mode::FirstPerson | Mode::Free => {}
            Mode::Menu      => menu(&mut ui, f, internal, cur.pos, &mut hover),
            Mode::Dialog(d) => dialog(&mut ui, f, internal, d, cur.pos, &mut hover),
        }
    }

    // Cursor zuletzt → über allem. Gefangen → HUD-Pointer mittig; sonst
    // Maus-Cursor, „interagierbar" wenn er über einem Klickziel steht.
    if cur.grabbed {
        let idx = if cur.hud_interact { CUR_HUD_INTERACT } else { CUR_HUD };
        ui.cursor(idx, [internal[0] * 0.5, internal[1] * 0.5], [1.0; 4]);
    } else {
        let idx = if hover.is_some() { CUR_MOUSE_INTERACT } else { CUR_MOUSE };
        ui.cursor(idx, cur.pos, [1.0; 4]);
    }

    Screen { ui, hover_action: hover }
}

/// Menu-Modus: das (noch demohafte) Menü, zentriert.
fn menu(ui: &mut Ui, f: &Font, internal: [f32; 2], cur: [f32; 2], hover: &mut Option<String>) {
    let items = [
        ("Fortsetzen",  "menu"), // schließt das Menü → ins Spiel (Play)
        ("Hilfe",       "help"),
        ("KV anzeigen", "kv"),
        ("Beenden",     "quit"),
    ];
    let (bw, bh, gap) = (176.0, 20.0, 6.0);
    let title_h = f.glyph_h;
    let body_h = title_h + gap + items.len() as f32 * bh + (items.len() as f32 - 1.0) * gap;
    let pw = bw + 2.0 * INSET;
    let ph = body_h + 2.0 * INSET;
    let px = ((internal[0] - pw) * 0.5).round();
    let py = ((internal[1] - ph) * 0.5).round();

    ui.panel([px, py, pw, ph], PANEL_BG, PANEL_FRAME);
    ui.text(f, px + INSET, py + INSET, "Menü", [0.85, 0.8, 0.6, 1.0]);

    let bx = px + INSET;
    let mut by = py + INSET + title_h + gap;
    for (label, action) in items {
        button(ui, f, [bx, by, bw, bh], label, action, cur, hover);
        by += bh + gap;
    }
}

/// Dialog-Modus: Textpanel unten, darunter ein Button je Choice. Ohne Choices
/// ein „Weiter"-Button, dessen Aktion die Leereingabe ist (blättert weiter) —
/// derselbe Pfad wie Enter im Terminal (`dialog_input`).
fn dialog(ui: &mut Ui, f: &Font, internal: [f32; 2], d: &Dialog, cur: [f32; 2], hover: &mut Option<String>) {
    let margin = 24.0;
    let (bh, gap) = (20.0, 6.0);
    let pw = internal[0] - 2.0 * margin;
    let line_h = f.glyph_h + 2.0;

    // Text auf die Panelbreite umbrechen.
    let lines = wrap(f, &d.text, pw - 2.0 * INSET);
    let text_h = lines.len() as f32 * line_h;

    // Buttonzeilen: je Choice eine, mindestens die „Weiter"-Zeile.
    let n = d.choices.len().max(1) as f32;
    let buttons_h = n * bh + (n - 1.0) * gap;

    let ph = 2.0 * INSET + text_h + gap + buttons_h;
    let py = (internal[1] - margin - ph).max(margin).round();
    let px = margin;

    ui.panel([px, py, pw, ph], PANEL_BG, PANEL_FRAME);

    let mut ty = py + INSET;
    for line in &lines {
        ui.text(f, px + INSET, ty, line, TEXT);
        ty += line_h;
    }

    let (cbx, cbw) = (px + INSET, pw - 2.0 * INSET);
    let mut cby = ty + gap;
    if d.choices.is_empty() {
        button(ui, f, [cbx, cby, cbw, bh], "Weiter", "", cur, hover);
    } else {
        for (i, opt) in d.choices.iter().enumerate() {
            let label = format!("{}) {opt}", i + 1);
            button(ui, f, [cbx, cby, cbw, bh], &label, &(i + 1).to_string(), cur, hover);
            cby += bh + gap;
        }
    }
}

/// Text in Zeilen umbrechen, die in `max_w` Pixel passen. Vorhandene
/// `\n` sind harte Umbrüche; sonst wird wortweise gegriffen. Ein Wort breiter
/// als `max_w` bleibt allein in seiner Zeile (Überlauf statt Endlosschleife).
fn wrap(font: &Font, text: &str, max_w: f32) -> Vec<String> {
    let mut lines = Vec::new();
    for para in text.split('\n') {
        let mut line = String::new();
        for word in para.split_whitespace() {
            let trial = if line.is_empty() { word.to_string() } else { format!("{line} {word}") };
            if line.is_empty() || font.text_width(&trial) <= max_w {
                line = trial;
            } else {
                lines.push(std::mem::take(&mut line));
                line = word.to_string();
            }
        }
        lines.push(line);
    }
    lines
}

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

    #[test]
    fn same_texture_quads_merge_into_one_batch() {
        let mut ui = Ui::default();
        ui.fill([0.0, 0.0, 10.0, 10.0], [1.0; 4]);
        ui.fill([20.0, 0.0, 10.0, 10.0], [1.0; 4]);
        assert_eq!(ui.verts.len(), 12);          // 2 Quads · 6 Verts
        assert_eq!(ui.batches.len(), 1);          // gleiche Textur → ein Batch
        assert_eq!(ui.batches[0].count, 12);
    }

    #[test]
    fn different_textures_split_batches() {
        let mut ui = Ui::default();
        ui.fill([0.0, 0.0, 10.0, 10.0], [1.0; 4]);          // WHITE
        ui.quad(1, [0.0, 0.0, 10.0, 10.0], [0.0, 0.0, 1.0, 1.0], [1.0; 4]); // Textur 1
        ui.fill([0.0, 0.0, 10.0, 10.0], [1.0; 4]);          // WHITE wieder
        assert_eq!(ui.batches.len(), 3);
        assert_eq!((ui.batches[0].start, ui.batches[1].start, ui.batches[2].start), (0, 6, 12));
    }

    #[test]
    fn key_luminance_white_opaque_black_transparent() {
        let img = Image { width: 2, height: 1, rgba: vec![255, 255, 255, 255,  0, 0, 0, 255] };
        let k = key_luminance(&img);
        assert_eq!(&k.rgba[0..4], &[255, 255, 255, 255]); // weiß → opak
        assert_eq!(&k.rgba[4..8], &[255, 255, 255, 0]);   // schwarz → transparent
    }

    #[test]
    fn button_registers_hover_only_when_cursor_inside() {
        // Leerer 256×64-Atlas → CGA-Font mit lauter leeren Glyphen; text_width
        // bleibt endlich, Button funktioniert trotzdem.
        let atlas = Image { width: 256, height: 64, rgba: vec![0; 256 * 64 * 4] };
        let font = Font::cga(FONT_CGA, &atlas);
        let rect = [10.0, 10.0, 100.0, 20.0];

        let mut ui = Ui::default();
        let mut hover = None;
        button(&mut ui, &font, rect, "X", "do_x", [50.0, 15.0], &mut hover);
        assert_eq!(hover.as_deref(), Some("do_x"));

        let mut outside = None;
        button(&mut ui, &font, rect, "X", "do_x", [200.0, 15.0], &mut outside);
        assert!(outside.is_none());
    }

    #[test]
    fn wrap_breaks_on_width_and_hard_newlines() {
        // Leerer Atlas → jedes Zeichen rückt um space_adv (cga: 4px) vor.
        let atlas = Image { width: 256, height: 64, rgba: vec![0; 256 * 64 * 4] };
        let f = Font::cga(FONT_CGA, &atlas);
        // 3-Zeichen-Wörter = 12px. Bei max 12 passt je ein Wort pro Zeile.
        assert_eq!(wrap(&f, "aaa bbb ccc", 12.0), vec!["aaa", "bbb", "ccc"]);
        // Bei max 28 passen zwei ("aaa bbb" = 7·4 = 28).
        assert_eq!(wrap(&f, "aaa bbb ccc", 28.0), vec!["aaa bbb", "ccc"]);
        // Harte Umbrüche bleiben erhalten.
        assert_eq!(wrap(&f, "a\nb", 999.0), vec!["a", "b"]);
    }

    #[test]
    fn cursor_uv_picks_quadrant() {
        // idx 3 = (col 1, row 1) → rechte untere 16×16-Zelle.
        assert_eq!(cursor_uv(3), [0.5, 0.5, 1.0, 1.0]);
        assert_eq!(cursor_uv(0), [0.0, 0.0, 0.5, 0.5]);
    }

    #[test]
    fn ornament_frame_tiles_edges_and_places_corners() {
        let mut ui = Ui::default();
        // 48×48-Panel: Kanten je 48-16=32 px → 32/16 = 2 Kacheln pro Seite.
        // Erwartung: 4 Ecken + 4·2 Kanten = 12 Ornament-Quads (nach dem fill).
        ui.panel([0.0, 0.0, 48.0, 48.0], [0.0; 4], [1.0; 4]);
        let orn_quads = ui.verts.len() / 6 - 1; // minus das Hintergrund-fill
        assert_eq!(orn_quads, 12);
    }
}
