//! TGA-Decoder → fertig dekodiertes RGBA8-`Image`.
//!
//! Neutral: kennt weder GPU noch UI noch Fonts. Liefert *immer* RGBA8,
//! Ursprung oben-links, vollständig konvertiert — der Aufrufer macht keine
//! Nachbearbeitung. (Anders als irl3d, wo der Loader BGR zurückgab und die
//! RGB-Umrechnung jedem Konsumenten einzeln überließ.) Konsumenten ziehen
//! ein `Image` und legen es ab, wie sie es brauchen: Renderer → GPU-Textur,
//! UI/Font → eigene Interpretation. Die UV-/V-Achsen-Konvention ist damit
//! Sache des Konsumenten beim Sampling, nicht des Loaders.
//!
//! Unterstützt True-Color unkomprimiert (Typ 2) und RLE (Typ 10) mit 24
//! oder 32 bpp, sowie Graustufen (Typ 3/11, 8 bpp → zu RGBA expandiert) für
//! Font-Maps. Paletten-TGAs gibt es nicht mehr (wds ist 15-bit True-Color).

pub struct Image {
    pub width:  u32,
    pub height: u32,
    /// `width * height * 4`, Ursprung oben-links.
    pub rgba:   Vec<u8>,
}

pub fn load(path: &str) -> Result<Image, String> {
    let bytes = std::fs::read(path).map_err(|e| format!("tga load {path}: {e}"))?;
    decode(&bytes).map_err(|e| format!("tga {path}: {e}"))
}

pub fn decode(d: &[u8]) -> Result<Image, String> {
    if d.len() < 18 { return Err("Header zu kurz".into()); }
    let id_len     = d[0] as usize;
    let cmap_type  = d[1];
    let image_type = d[2];
    let width  = u16::from_le_bytes([d[12], d[13]]) as u32;
    let height = u16::from_le_bytes([d[14], d[15]]) as u32;
    let depth  = d[16];
    let descriptor = d[17];

    if cmap_type != 0 {
        return Err("Color-Map-TGAs nicht unterstützt (True-Color only)".into());
    }
    // Pixeldaten beginnen nach Header + ID-Feld (Color-Map ist leer).
    let off = 18 + id_len;

    let channels = match (image_type, depth) {
        (2 | 10, 24) => 3,
        (2 | 10, 32) => 4,
        (3 | 11, 8)  => 1,
        (t, b)       => return Err(format!("nicht unterstützt: Typ {t}, {b} bpp")),
    };
    let n = (width as usize) * (height as usize);
    if n == 0 { return Err("Nullgröße".into()); }

    // Rohpixel (channels Bytes je Pixel), bei RLE entpackt.
    let raw_len = n * channels;
    let raw: Vec<u8> = match image_type {
        2 | 3   => d.get(off..off + raw_len).ok_or("Pixeldaten zu kurz")?.to_vec(),
        10 | 11 => decode_rle(d.get(off..).ok_or("RLE-Daten fehlen")?, raw_len, channels)?,
        _ => unreachable!(),
    };

    // → RGBA8. True-Color liegt im TGA als BGR(A) vor; Graustufen replizieren.
    let mut rgba = vec![0u8; n * 4];
    for i in 0..n {
        let (r, g, b, a) = match channels {
            1 => { let v = raw[i]; (v, v, v, 255) }
            3 => (raw[i * 3 + 2], raw[i * 3 + 1], raw[i * 3], 255),
            4 => (raw[i * 4 + 2], raw[i * 4 + 1], raw[i * 4], raw[i * 4 + 3]),
            _ => unreachable!(),
        };
        let o = i * 4;
        rgba[o] = r; rgba[o + 1] = g; rgba[o + 2] = b; rgba[o + 3] = a;
    }

    // Ursprung oben-links erzwingen. Descriptor-Bit 5 gesetzt = top-origin.
    if (descriptor >> 5) & 1 == 0 {
        flip_vertical(&mut rgba, width as usize, height as usize);
    }
    Ok(Image { width, height, rgba })
}

/// TGA-RLE: Pakete operieren auf ganzen Pixeln (`ch` Bytes).
fn decode_rle(src: &[u8], raw_len: usize, ch: usize) -> Result<Vec<u8>, String> {
    let mut out = Vec::with_capacity(raw_len);
    let mut si = 0;
    while out.len() < raw_len {
        let header = *src.get(si).ok_or("RLE: Quelle zu kurz")?;
        si += 1;
        let count = (header & 0x7f) as usize + 1;
        if header & 0x80 != 0 {
            // RLE-Paket: ein Pixel count-mal.
            let px = src.get(si..si + ch).ok_or("RLE: Pixel zu kurz")?;
            si += ch;
            for _ in 0..count { out.extend_from_slice(px); }
        } else {
            // Raw-Paket: count Pixel am Stück.
            let bytes = count * ch;
            let chunk = src.get(si..si + bytes).ok_or("RLE: Chunk zu kurz")?;
            si += bytes;
            out.extend_from_slice(chunk);
        }
    }
    out.truncate(raw_len);
    Ok(out)
}

fn flip_vertical(rgba: &mut [u8], w: usize, h: usize) {
    let row = w * 4;
    for y in 0..h / 2 {
        let (a, b) = (y * row, (h - 1 - y) * row);
        for x in 0..row { rgba.swap(a + x, b + x); }
    }
}

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

    #[test]
    fn bgr_to_rgba_uncompressed() {
        // Minimal-TGA: Typ 2, 2×1, 24 bpp, bottom-origin (descriptor 0).
        // Zwei Pixel BGR (10,20,30) und (40,50,60).
        let mut d = vec![0u8; 18];
        d[2] = 2; d[12] = 2; d[14] = 1; d[16] = 24;
        d.extend_from_slice(&[10, 20, 30, 40, 50, 60]);
        let img = decode(&d).unwrap();
        assert_eq!((img.width, img.height), (2, 1));
        assert_eq!(&img.rgba[0..4], &[30, 20, 10, 255]);
        assert_eq!(&img.rgba[4..8], &[60, 50, 40, 255]);
    }

    #[test]
    fn rle_truecolor_expands() {
        // Typ 10, 4×1, 24 bpp: ein RLE-Paket (4× BGR 1,2,3).
        let mut d = vec![0u8; 18];
        d[2] = 10; d[12] = 4; d[14] = 1; d[16] = 24;
        d.push(0x80 | 3);                 // RLE, count = 4
        d.extend_from_slice(&[1, 2, 3]);  // ein Pixel
        let img = decode(&d).unwrap();
        assert_eq!(img.rgba.len(), 4 * 4);
        assert!(img.rgba.chunks(4).all(|p| p == [3, 2, 1, 255]));
    }

    #[test]
    fn decodes_real_placeholder() {
        let bytes = std::fs::read("assets/textures/placeholder.tga").unwrap();
        let img = decode(&bytes).unwrap();
        assert_eq!((img.width, img.height), (256, 256));
        assert_eq!(img.rgba.len(), 256 * 256 * 4);
        assert!(img.rgba.chunks(4).all(|p| p[3] == 255)); // 24bpp → Alpha 255
    }
}
