//! Renders a generated [`World`] to a PNG — a *viewer* for eyeballing the
//! generator, deliberately kept out of the pure library (the core stores no
//! color; presentation lives here, exactly like `reikhelm-viz` owns the dungeon
//! palette).
//!
//! The PNG encoder is hand-rolled with **no dependencies**: 8-bit truecolor,
//! filter 0, and a zlib stream of uncompressed ("stored") DEFLATE blocks. That's
//! all std, so the same code path works headless or in a future WASM/server
//! target without pulling an image crate.
//!
//! Usage:
//! ```text
//! cargo run --release --example world_png -p reikhelm-core [seed] [pixel_scale]
//! ```
//! Writes `world.png` in the current directory.

use std::env;
use std::fs::File;
use std::io::{self, Write};

use reikhelm_core::world::biome::Biome;
use reikhelm_core::world::{generate, World, WorldConfig};
use reikhelm_core::geometry::Point;

fn main() -> io::Result<()> {
    let mut args = env::args().skip(1);
    let seed: u64 = args.next().and_then(|s| s.parse().ok()).unwrap_or(2026);
    let scale: u32 = args.next().and_then(|s| s.parse().ok()).unwrap_or(4).max(1);

    let cfg = WorldConfig::default();
    let world = generate(cfg, seed).expect("default WorldConfig is valid");

    let pixels = render(&world, scale);
    let (pw, ph) = (world.width * scale, world.height * scale);
    let mut file = File::create("world.png")?;
    write_png(&mut file, pw, ph, &pixels)?;

    println!(
        "wrote world.png ({}x{} px) — seed {seed}, {:.0}% land, wind {:?}",
        pw,
        ph,
        world.land_fraction() * 100.0,
        cfg.wind
    );
    Ok(())
}

/// Renders the world into a row-major RGB buffer, upscaled by `scale` so each
/// world cell becomes a `scale × scale` block of pixels.
fn render(world: &World, scale: u32) -> Vec<u8> {
    let (w, h) = (world.width, world.height);
    let (pw, ph) = (w * scale, h * scale);
    let mut buf = vec![0u8; (pw as usize) * (ph as usize) * 3];

    for cy in 0..h as i32 {
        for cx in 0..w as i32 {
            let p = Point::new(cx, cy);
            let biome = world.biome.get(p).copied().unwrap_or(Biome::DeepOcean);
            let [r, g, b] = shade(world, p, palette(biome));

            // Splat the cell color across its scale×scale pixel block.
            for sy in 0..scale {
                let py = cy as u32 * scale + sy;
                let row = (py as usize) * (pw as usize) * 3;
                for sx in 0..scale {
                    let px = cx as u32 * scale + sx;
                    let i = row + (px as usize) * 3;
                    buf[i] = r;
                    buf[i + 1] = g;
                    buf[i + 2] = b;
                }
            }
        }
    }
    buf
}

/// The base color for each biome (relief/depth shading is applied separately).
fn palette(b: Biome) -> [u8; 3] {
    match b {
        Biome::DeepOcean => [28, 52, 94],
        Biome::ShallowOcean => [54, 94, 140],
        Biome::Lake => [58, 108, 158],
        Biome::River => [72, 122, 178],
        Biome::Beach => [208, 196, 150],
        Biome::Snow => [240, 244, 248],
        Biome::Tundra => [168, 172, 158],
        Biome::Taiga => [76, 110, 92],
        Biome::Bare => [128, 120, 110],
        Biome::Grassland => [132, 168, 92],
        Biome::Shrubland => [162, 160, 104],
        Biome::TemperateForest => [78, 132, 76],
        Biome::TemperateRainforest => [54, 110, 78],
        Biome::Desert => [214, 198, 134],
        Biome::Savanna => [186, 178, 100],
        Biome::TropicalSeasonalForest => [102, 152, 70],
        Biome::TropicalRainforest => [40, 112, 58],
    }
}

/// Modulates a base color by terrain: ocean darkens with depth, land gets a
/// gentle NW hillshade from the elevation gradient so ridges and valleys read.
fn shade(world: &World, p: Point, base: [u8; 3]) -> [u8; 3] {
    let e = *world.elevation.get(p).unwrap_or(&0.0);
    let sea = world.config.sea_level;

    let factor = if world.biome.get(p).map(|b| b.is_water()).unwrap_or(false) {
        // Deeper water → darker. Normalize depth against sea level.
        let depth = ((sea - e) / sea).clamp(0.0, 1.0);
        0.95 - 0.45 * depth
    } else {
        // Elevation tint (higher = a touch brighter) times a hillshade.
        let tint = 0.88 + 0.30 * ((e - sea) / (1.0 - sea)).clamp(0.0, 1.0);
        tint * hillshade(world, p)
    };

    [
        (base[0] as f32 * factor).clamp(0.0, 255.0) as u8,
        (base[1] as f32 * factor).clamp(0.0, 255.0) as u8,
        (base[2] as f32 * factor).clamp(0.0, 255.0) as u8,
    ]
}

/// A simple Lambert hillshade with the light from the north-west, returning a
/// brightness multiplier around 1.0. Steep slopes facing the light brighten;
/// those facing away darken, which is what makes mountains pop.
fn hillshade(world: &World, p: Point) -> f32 {
    let at = |q: Point| *world.elevation.get(q).unwrap_or(world.elevation.get(p).unwrap_or(&0.0));
    // Central differences for the elevation gradient (vertical exaggeration k).
    let k = 12.0;
    let dx = (at(p.offset(1, 0)) - at(p.offset(-1, 0))) * k;
    let dy = (at(p.offset(0, 1)) - at(p.offset(0, -1))) * k;

    // Surface normal (-dx, -dy, 1) dotted with a normalized NW-up light.
    let (lx, ly, lz) = (-0.45, -0.45, 0.77);
    let nlen = (dx * dx + dy * dy + 1.0).sqrt();
    let dot = (-dx * lx - dy * ly + lz) / nlen;
    // Map the dot product into a tame [0.7, 1.15] brightness range.
    (0.70 + 0.55 * dot.clamp(0.0, 1.0)).clamp(0.70, 1.15)
}

// --- Zero-dependency PNG encoder -----------------------------------------

/// Writes an 8-bit RGB PNG (`pixels` is row-major, `w*h*3` bytes) to `out`.
fn write_png(out: &mut impl Write, w: u32, h: u32, pixels: &[u8]) -> io::Result<()> {
    out.write_all(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A])?;

    // IHDR: width, height, bit depth 8, color type 2 (RGB), no compression/
    // filter/interlace.
    let mut ihdr = Vec::with_capacity(13);
    ihdr.extend_from_slice(&w.to_be_bytes());
    ihdr.extend_from_slice(&h.to_be_bytes());
    ihdr.extend_from_slice(&[8, 2, 0, 0, 0]);
    write_chunk(out, b"IHDR", &ihdr)?;

    // IDAT: zlib(stored DEFLATE) of the filtered scanlines (filter byte 0 per row).
    let mut raw = Vec::with_capacity((w as usize * 3 + 1) * h as usize);
    for y in 0..h as usize {
        raw.push(0); // filter type 0 (None)
        let start = y * w as usize * 3;
        raw.extend_from_slice(&pixels[start..start + w as usize * 3]);
    }
    write_chunk(out, b"IDAT", &zlib_stored(&raw))?;

    write_chunk(out, b"IEND", &[])?;
    Ok(())
}

/// Emits one PNG chunk: length, type, data, CRC32(type ++ data).
fn write_chunk(out: &mut impl Write, kind: &[u8; 4], data: &[u8]) -> io::Result<()> {
    out.write_all(&(data.len() as u32).to_be_bytes())?;
    out.write_all(kind)?;
    out.write_all(data)?;
    let mut crc = Crc::new();
    crc.update(kind);
    crc.update(data);
    out.write_all(&crc.finish().to_be_bytes())?;
    Ok(())
}

/// Wraps `data` as a zlib stream whose DEFLATE body is uncompressed "stored"
/// blocks — valid zlib that any PNG decoder accepts, with no compression code.
fn zlib_stored(data: &[u8]) -> Vec<u8> {
    let mut out = Vec::with_capacity(data.len() + data.len() / 65535 * 5 + 16);
    out.extend_from_slice(&[0x78, 0x01]); // zlib header (deflate, 32K window)

    let mut chunks = data.chunks(0xFFFF).peekable();
    if chunks.peek().is_none() {
        // Empty image still needs one final empty stored block.
        out.extend_from_slice(&[0x01, 0x00, 0x00, 0xFF, 0xFF]);
    }
    while let Some(chunk) = chunks.next() {
        let last = chunks.peek().is_none();
        out.push(if last { 1 } else { 0 }); // BFINAL, BTYPE=00 (stored)
        let len = chunk.len() as u16;
        out.extend_from_slice(&len.to_le_bytes());
        out.extend_from_slice(&(!len).to_le_bytes()); // one's complement of len
        out.extend_from_slice(chunk);
    }

    out.extend_from_slice(&adler32(data).to_be_bytes());
    out
}

/// Adler-32 checksum (the zlib trailer).
fn adler32(data: &[u8]) -> u32 {
    const MOD: u32 = 65521;
    let (mut a, mut b) = (1u32, 0u32);
    for &byte in data {
        a = (a + byte as u32) % MOD;
        b = (b + a) % MOD;
    }
    (b << 16) | a
}

/// CRC-32 (the PNG chunk checksum), computed with the standard reflected
/// polynomial — no lookup table, the bit-at-a-time form is plenty fast here.
struct Crc {
    value: u32,
}
impl Crc {
    fn new() -> Self {
        Crc { value: 0xFFFF_FFFF }
    }
    fn update(&mut self, data: &[u8]) {
        for &byte in data {
            self.value ^= byte as u32;
            for _ in 0..8 {
                let mask = (self.value & 1).wrapping_neg();
                self.value = (self.value >> 1) ^ (0xEDB8_8320 & mask);
            }
        }
    }
    fn finish(self) -> u32 {
        self.value ^ 0xFFFF_FFFF
    }
}
