//! Deterministic, integer, **byte-exact generative synthesis** — the
//! `intent = create` analog of the integer-exact codecs.
//!
//! Every other generative path (`wai.semantic.*`) regenerates with a float model
//! and conforms by *similarity*: the output legitimately varies by generator and
//! by hardware, so it can never go in a receipt. This module is instead a
//! generator whose ENTIRE pipeline is integer/fixed-point, pinned to
//! `(model, prompt, seed, steps)`, so the generated image is **byte-identical on
//! every machine**. There is no original to reconstruct — the content is created
//! — yet its hash is portable, so a publisher can pin "this prompt+seed under
//! this model produces exactly these bytes" and anyone can verify it client-side.
//! That is *generation-equivalence*: the create-side of "conformance is a hash".
//!
//! The synthesis is fixed-point spectral composition — a sum of integer-LUT
//! sinusoids whose frequencies, amplitudes and phases are drawn by a pure-integer
//! PRNG seeded from the prompt and seed (the byte-exact analog of FFT-based image
//! synthesis). The prompt's *meaning* shapes the image through a pinned integer
//! **lexicon** ([`SpectralModel::intent`]): recognized words set a per-channel
//! colour (`ocean`→blue, `fire`→red), a frequency tendency (`calm`→smooth,
//! `chaos`→busy) and an orientation (`horizon`→horizontal, `tall`→vertical),
//! while the prompt+seed still fix the exact draw — so the conditioning is
//! semantic yet fully deterministic. The model ([`SpectralModel`], from
//! `tools/wai_generate_model.py`) carries the integer sine LUT, the lexicon and
//! the draw configuration; the float `sin()` that bakes the LUT runs once at
//! model-build time, never here. Pure i64, no `ort`, no float — like
//! `int_synth`/`int_hyper_synth`, it compiles to wasm32 and is bit-checkable with
//! no ML runtime.

use std::collections::HashMap;

use serde_json::Value;

/// FNV-1a 64-bit — the dependency-free checksum shared across the integer paths,
/// also used here to fold the prompt into the PRNG seed.
pub fn fnv1a(bytes: &[u8]) -> u64 {
    let mut hash: u64 = 14695981039346656037;
    for &b in bytes {
        hash = (hash ^ b as u64).wrapping_mul(1099511628211);
    }
    hash
}

/// `splitmix64` — a pure-integer PRNG. Deterministic on every machine (no float,
/// no platform RNG); the fixed-point sampler the generative-replicate contract
/// requires.
fn splitmix64(s: &mut u64) -> u64 {
    *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
    let mut z = *s;
    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
    z ^ (z >> 31)
}

/// Integer `atan2(dy, dx)` as a LUT index in `[0, t)` (`t` = a full turn), via
/// **CORDIC** (vectoring) — shifts and adds only, no float, so the angle is
/// deterministic on every machine. The accuracy (≈±1 step) doesn't matter; the
/// *determinism* does. The step table is `round(atan(2^-i)/2π · 1024)`; `t` is
/// assumed to be the LUT size the table was tabulated for (1024). This is the
/// integer angle the angular / spiral component types need.
fn iatan2(dy: i64, dx: i64, t: i64) -> i64 {
    if dx == 0 {
        return if dy == 0 { 0 } else if dy > 0 { t / 4 } else { 3 * t / 4 };
    }
    if dy == 0 {
        return if dx > 0 { 0 } else { t / 2 };
    }
    const ATAN: [i64; 14] = [128, 76, 40, 20, 10, 5, 3, 1, 1, 0, 0, 0, 0, 0];
    // first-quadrant magnitudes, scaled up for precision
    let (mut x, mut y, mut ang) = (dx.abs() << 12, dy.abs() << 12, 0i64);
    for (i, &a) in ATAN.iter().enumerate() {
        if y > 0 {
            let (nx, ny) = (x + (y >> i), y - (x >> i));
            x = nx;
            y = ny;
            ang += a;
        } else if y < 0 {
            let (nx, ny) = (x - (y >> i), y + (x >> i));
            x = nx;
            y = ny;
            ang -= a;
        }
    }
    // ang ≈ atan(|dy|/|dx|) ∈ [0, t/4]; place it in the right quadrant.
    let q = match (dx >= 0, dy >= 0) {
        (true, true) => ang,
        (false, true) => t / 2 - ang,
        (false, false) => t / 2 + ang,
        (true, false) => t - ang,
    };
    ((q % t) + t) % t
}

/// The pinned generator model: an integer sine LUT plus the spectral-draw config.
/// Pure data, no float; the same bundle the export tool commits.
pub struct SpectralModel {
    pub version: String,
    pub lut: Vec<i64>,
    pub lut_size: i64,
    pub lut_bits: u32,
    pub freq_max: i64,
    pub amp_bits: u32,
    pub color_offsets: [i64; 3],
    pub base_components: usize,
    pub step_components: usize,
    /// Concept word → `[r, g, b, freq_q8, orient, radial, mirror, gradient,
    /// angular, spiral]` — the semantic lexicon that makes the prompt's *meaning*
    /// shape the image, deterministically (colour + texture + orientation + shape
    /// grammar). Shorter entries are zero-padded; empty for a v1 model.
    pub lexicon: HashMap<String, [i64; 10]>,
}

/// The spectral *intent* a prompt folds into — colour, texture, orientation and
/// the shape-grammar weights. All integer; a neutral (unmatched) prompt is all
/// zero shape with `hue=[256;3], freq=256`.
struct Intent {
    hue: [i64; 3],   // per-channel weight, max channel = 256 (the tint)
    freq: i64,       // frequency multiplier, Q8
    orient: i64,     // -256 horizontal … +256 vertical
    radial: i64,     // 0..256 weight toward concentric-ring components
    mirror: i64,     // 0..256 weight toward bilaterally-folded components
    gradient: i64,   // -256..256 vertical brightness ramp (+ = bright top)
    angular: i64,    // 0..256 weight toward angular-ray components (star/sunburst)
    spiral: i64,     // 0..256 weight toward spiral components (swirl/vortex)
}

impl SpectralModel {
    /// Parse the committed model bundle (`generate_spectral_model.json`).
    pub fn from_value(v: &Value) -> Self {
        let co = v["color_offsets"].as_array().unwrap();
        let lexicon = v.get("lexicon").and_then(|l| l.as_object()).map(|o| {
            o.iter().filter_map(|(k, val)| {
                let a = val.as_array()?;
                let mut e = [0i64; 10];
                for (i, slot) in e.iter_mut().enumerate() {
                    *slot = a.get(i).and_then(|n| n.as_i64()).unwrap_or(0);
                }
                Some((k.clone(), e))
            }).collect()
        }).unwrap_or_default();
        Self {
            version: v["version"].as_str().unwrap_or("").to_string(),
            lut: v["sine_lut"].as_array().unwrap().iter().map(|n| n.as_i64().unwrap()).collect(),
            lut_size: v["lut_size"].as_i64().unwrap(),
            lut_bits: v["lut_bits"].as_u64().unwrap_or(14) as u32,
            freq_max: v["freq_max"].as_i64().unwrap(),
            amp_bits: v["amp_bits"].as_u64().unwrap() as u32,
            color_offsets: [co[0].as_i64().unwrap(), co[1].as_i64().unwrap(), co[2].as_i64().unwrap()],
            base_components: v["base_components"].as_u64().unwrap() as usize,
            step_components: v["step_components"].as_u64().unwrap() as usize,
            lexicon,
        }
    }

    /// Fold the prompt's recognized words (averaged) into an [`Intent`]. Unmatched
    /// prompts return the neutral intent, so an unknown prompt still generates.
    fn intent(&self, prompt: &str) -> Intent {
        let (mut h, mut f, mut o, mut r, mut m, mut gr, mut an, mut sp, mut n) =
            ([0i64; 3], 0, 0, 0, 0, 0, 0, 0, 0i64);
        let lower = prompt.to_lowercase();
        for tok in lower.split(|c: char| !c.is_ascii_alphanumeric()).filter(|t| !t.is_empty()) {
            if let Some(e) = self.lexicon.get(tok) {
                h[0] += e[0];
                h[1] += e[1];
                h[2] += e[2];
                f += e[3];
                o += e[4];
                r += e[5];
                m += e[6];
                gr += e[7];
                an += e[8];
                sp += e[9];
                n += 1;
            }
        }
        if n == 0 {
            return Intent {
                hue: [256, 256, 256], freq: 256, orient: 0,
                radial: 0, mirror: 0, gradient: 0, angular: 0, spiral: 0,
            };
        }
        let mx = *h.iter().max().unwrap().max(&1);
        Intent {
            hue: [h[0] * 256 / mx, h[1] * 256 / mx, h[2] * 256 / mx],
            freq: f / n,
            orient: (o / n).clamp(-256, 256),
            radial: (r / n).clamp(0, 256),
            mirror: (m / n).clamp(0, 256),
            gradient: (gr / n).clamp(-256, 256),
            angular: (an / n).clamp(0, 256),
            spiral: (sp / n).clamp(0, 256),
        }
    }
}

/// Generate an `h`×`w` RGB image (row-major **HWC** `u8`) deterministically from
/// `(model, prompt, seed, steps)`. Pure i64 — identical bytes on every machine.
///
/// `steps` scales the spectral richness: `K = base + steps·step` components are
/// drawn by [`splitmix64`] seeded from `fnv1a(prompt) ^ seed`. The prompt's
/// recognized words ([`SpectralModel::intent`]) shape the draw: a per-channel
/// **hue** tint, a **frequency** multiplier (calm↔busy), an **orientation** skew
/// (horizontal↔vertical), and a **shape grammar** — each component is a *plane*
/// wave, a *radial* ring (`sin(fr·r + φ)`, integer distance via `isqrt`), a
/// *mirror* fold (`sin(fx·|x−cx| + fy·|y−cy| + φ)`), an *angular* ray
/// (`sin(fa·θ + φ)`, integer angle θ via the CORDIC [`iatan2`]) or a *spiral*
/// (`sin(fa·θ + fr·r + φ)`), drawn from the `radial`/`mirror`/`angular`/`spiral`
/// weights — plus a vertical brightness `gradient`. The prompt+seed still fix the
/// specific draw. The field is normalized to `[0,255]` by its integer min/max,
/// then hue-tinted. Same inputs → exact same bytes; different prompts → different,
/// *thematically shaped & structured*, images.
pub fn generate(m: &SpectralModel, prompt: &str, seed: u64, steps: usize, h: usize, w: usize) -> Vec<u8> {
    let k = m.base_components + steps * m.step_components;
    let it = m.intent(prompt);
    // orientation skews how wide the horizontal vs vertical frequency draw is:
    // negative → suppress fx (horizontal bands), positive → suppress fy (vertical).
    let (fx_scale, fy_scale) = if it.orient < 0 {
        ((256 + it.orient).max(0), 256)
    } else {
        (256, (256 - it.orient).max(0))
    };
    let fmax = (m.freq_max * it.freq / 256).max(1);
    // deterministic seed: prompt folded into the explicit seed (a fixed mix const
    // keeps an empty prompt or zero seed from degenerating the stream).
    let mut rng = fnv1a(prompt.as_bytes()) ^ seed ^ 0xD1B5_4A32_D192_ED03;
    let amp_max = 1i64 << m.amp_bits;
    let span = 2 * fmax as u64 + 1;
    // component-type mix: a base of plane waves plus the shape-grammar weights;
    // each component draws its type from this distribution. (typ 0 plane, 1 radial,
    // 2 mirror, 3 angular, 4 spiral; stored as `(typ, fx, fy, amp, phase)`.) The
    // draw order — pick, fxr, fyr, amp, phase — is the SAME for every type (the
    // angular/spiral frequencies reuse fxr/fyr), so a prompt with no shape weights
    // produces the exact same stream as before (the pin is unchanged).
    let thr = [it.radial, it.mirror, it.angular, it.spiral];
    let plane_w = (256 - thr.iter().sum::<i64>()).max(48);
    let total = (plane_w + thr.iter().sum::<i64>()) as u64;
    let comps: Vec<(u8, i64, i64, i64, i64)> = (0..k)
        .map(|_| {
            let pick = (splitmix64(&mut rng) % total) as i64; // u64 modulo (as before), in [0, total)
            let mut typ = 0u8;
            if pick >= plane_w {
                let mut bound = plane_w;
                for (i, &wgt) in thr.iter().enumerate() {
                    bound += wgt;
                    if pick < bound {
                        typ = i as u8 + 1;
                        break;
                    }
                }
            }
            let fxr = (splitmix64(&mut rng) % span) as i64 - fmax;
            let fyr = (splitmix64(&mut rng) % span) as i64 - fmax;
            let amp = (splitmix64(&mut rng) % amp_max as u64) as i64 + 1;
            let phase = (splitmix64(&mut rng) % m.lut_size as u64) as i64;
            match typ {
                1 => (1, fxr.abs().max(2), 0, amp, phase),                   // radial: fr = |fxr|
                3 => (3, fxr.abs() % 11 + 2, 0, amp, phase),                 // angular: fa rays
                4 => (4, fxr.abs() % 9 + 2, fyr.abs().max(2), amp, phase),   // spiral: fa angular, fr radial
                _ => (typ, fxr * fx_scale / 256, fyr * fy_scale / 256, amp, phase), // plane/mirror
            }
        })
        .collect();

    let n = h * w;
    let t = m.lut_size;
    let (cx, cy) = (w as i64 / 2, h as i64 / 2);
    // radius (radial/spiral) and angle (angular/spiral) depend only on the pixel —
    // compute each at most once per pixel, and only if some component needs it.
    let need_r = comps.iter().any(|&(typ, ..)| typ == 1 || typ == 4);
    let need_th = comps.iter().any(|&(typ, ..)| typ == 3 || typ == 4);
    // vertical brightness ramp, scaled to sit alongside the sinusoid sum (not swamp it)
    let gscale = ((1i64 << m.lut_bits) * amp_max * k as i64) / (2048 * cy.max(1));
    let mut acc = vec![0i64; 3 * n];
    for (c, &coff) in m.color_offsets.iter().enumerate() {
        let plane = c * n;
        for y in 0..h {
            let yi = y as i64;
            let grad = it.gradient * (cy - yi) * gscale; // same per channel — a brightness gradient
            for x in 0..w {
                let xi = x as i64;
                let (dx, dy) = (xi - cx, yi - cy);
                let r = if need_r { ((dx * dx + dy * dy) as u64).isqrt() as i64 } else { 0 };
                let th = if need_th { iatan2(dy, dx, t) } else { 0 };
                let mut a = grad;
                for &(typ, fx, fy, amp, phase) in &comps {
                    let arg = match typ {
                        1 => fx * r + phase + coff,                            // radial:  fr·r
                        2 => fx * dx.abs() + fy * dy.abs() + phase + coff,     // mirror:  fold
                        3 => fx * th + phase + coff,                          // angular: fa·θ (rays)
                        4 => fx * th + fy * r + phase + coff,                 // spiral:  fa·θ + fr·r
                        _ => fx * xi + fy * yi + phase + coff,                // plane
                    };
                    let idx = ((arg % t) + t) % t;
                    a += amp * m.lut[idx as usize];
                }
                acc[plane + y * w + x] = a;
            }
        }
    }

    // integer min/max normalization → [0,255], round-half-up, then a per-channel
    // hue TINT: the prompt's dominant colour keeps full range, the others are
    // scaled toward a floor (so the colour reads without going muddy-black). A
    // neutral prompt has hue `[256;3]` → no tint (the colour-offset rainbow). Pure
    // integer → byte-exact.
    const FLOOR: i64 = 64; // non-dominant channels never drop below ~floor/256
    let eh = [
        FLOOR + it.hue[0] * (256 - FLOOR) / 256,
        FLOOR + it.hue[1] * (256 - FLOOR) / 256,
        FLOOR + it.hue[2] * (256 - FLOOR) / 256,
    ];
    let gmin = *acc.iter().min().unwrap();
    let gmax = *acc.iter().max().unwrap();
    let range = (gmax - gmin).max(1);
    let mut rgb = vec![0u8; n * 3];
    for c in 0..3 {
        for p in 0..n {
            let v = acc[c * n + p];
            let q = ((v - gmin) * 255 + range / 2) / range;
            rgb[p * 3 + c] = (q * eh[c] / 256).clamp(0, 255) as u8;
        }
    }
    rgb
}
