//! Conformance corpus for exploration (`live-conformance/exploration/`, extensions/exploration.md §14).
//!
//!   wai_exploration_vectors gen     <corpus-dir>   write vectors.json from fixed seeds
//!   wai_exploration_vectors verify  <corpus-dir>   every file regenerates byte for byte, no other
//!                                                  file but README.md is there, and every case
//!                                                  gets the outcome it states
//!   wai_exploration_vectors measure [trials]       the archive against plain seed sampling and
//!                                                  uniform genome sampling at equal compute
//!
//! The generator and this verifier are one implementation. The corpus's second
//! implementation is `live-conformance/exploration_verify.py`, written from the extension's
//! text and sharing no code with this binary.

use std::collections::BTreeMap;
use std::path::Path;
use wai::exploration::{self, corpus, Axis, Objective, Request};
use wai::generate::SpectralModel;

fn main() {
    let a: Vec<String> = std::env::args().collect();
    match (a.get(1).map(String::as_str), a.get(2)) {
        (Some("gen"), Some(d)) => {
            if let Err(e) = std::fs::create_dir_all(d) {
                eprintln!("create {d}: {e}");
                std::process::exit(1);
            }
            for (name, body) in corpus::generate() {
                let p = Path::new(d).join(&name);
                if let Err(e) = std::fs::write(&p, body) {
                    eprintln!("write {}: {e}", p.display());
                    std::process::exit(1);
                }
            }
            println!("wrote {d}");
        }
        (Some("verify"), Some(d)) => std::process::exit(verify(Path::new(d))),
        (Some("measure"), n) => measure(n.and_then(|s| s.parse().ok()).unwrap_or(512)),
        _ => {
            eprintln!("usage: wai_exploration_vectors gen|verify <corpus-dir> | measure [trials]");
            std::process::exit(2);
        }
    }
}

fn verify(dir: &Path) -> i32 {
    let mut files = BTreeMap::new();
    let entries = match std::fs::read_dir(dir) {
        Ok(e) => e,
        Err(e) => {
            eprintln!("read {}: {e}", dir.display());
            return 1;
        }
    };
    for e in entries.flatten() {
        let name = e.file_name().to_string_lossy().into_owned();
        if name == "README.md" {
            continue;
        }
        match std::fs::read_to_string(e.path()) {
            Ok(s) => {
                files.insert(name, s);
            }
            Err(err) => {
                eprintln!("read {name}: {err}");
                return 1;
            }
        }
    }
    let generated: BTreeMap<String, String> = corpus::generate().into_iter().collect();
    let same = files == generated;
    println!("{} every file regenerates byte for byte from its seeds, and no other file is there", if same { "PASS" } else { "FAIL" });
    let fails = corpus::verify(&files);
    for f in &fails {
        println!("FAIL {f}");
    }
    println!("{} every case gets the outcome the corpus states", if fails.is_empty() { "PASS" } else { "FAIL" });
    (!same || !fails.is_empty()) as i32
}

/// The measurement the pull request reports: for each prompt, the cells of one grid that plain
/// seed sampling (seeds 0, 1, 2, … at the request's steps, the prompt as written), uniform sampling
/// of the archive's genome space (a random seed, steps 1 to 16 and any modifier mask) and the
/// archive's schedule reach with the same number of generations, each of the last two summed over
/// three search seeds; and the mean pairwise descriptor distance of one representative per cell
/// reached. Informative: it changes no conformance outcome.
fn measure(trials: u64) {
    let p = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/vectors/generate/generate_spectral_model.json");
    let bytes = std::fs::read(&p).expect("the committed model");
    let m = SpectralModel::from_value(&serde_json::from_slice(&bytes).expect("JSON"));
    let modifiers: Vec<String> = ["bright", "dark", "calm", "busy", "warm", "cool", "spiral", "mirror"].map(String::from).to_vec();
    let grid = vec![
        Axis { descriptor: 0, lo: 128, hi: 896, bins: 8 },
        Axis { descriptor: 1, lo: 128, hi: 896, bins: 8 },
        Axis { descriptor: 2, lo: 0, hi: 512, bins: 8 },
    ];
    let prompts = [
        "a quiet forest at night",
        "ocean spiral vortex at dusk",
        "a red rose",
        "neon city grid",
        "golden sunrise over the desert",
        "portrait of a cat",
    ];
    const RUNS: u64 = 3;
    println!("{trials} generations each, 64x64, grid luma x chroma x detail (8 x 8 x 8 = 512 cells), modifiers {modifiers:?}");
    println!("cells reached, each column summed over {RUNS} search seeds (plain seed sampling has none: it is the same each time)");
    println!("| prompt | seeds 0..{} | uniform genome | archive | mean pair distance: seeds | uniform | archive |", trials - 1);
    println!("|---|---|---|---|---|---|---|");
    let mut tot = [0u64; 3];
    for (i, prompt) in prompts.iter().enumerate() {
        let mut cells = [0u64; 3];
        let mut means = [0u64; 3];
        for run in 0..RUNS {
            let r = Request {
                model: exploration::digest(&bytes),
                prompt: prompt.to_string(),
                modifiers: modifiers.clone(),
                width: 64,
                height: 64,
                steps: 4,
                grid: grid.clone(),
                seed: 1 + i as u64 + 100 * run,
                trials,
                init: (trials / 8).max(1),
                objective: Objective::None,
            };
            let reach = [exploration::reach_seeds(&m, &r), exploration::reach_random(&m, &r), exploration::reach_archive(&m, &r)];
            for (k, x) in reach.iter().enumerate() {
                cells[k] += x.occupied;
                means[k] += x.spread.as_ref().map_or(0, |s| s.sum / s.pairs);
            }
        }
        println!("| {prompt} | {} | {} | {} | {} | {} | {} |", cells[0], cells[1], cells[2], means[0] / RUNS, means[1] / RUNS, means[2] / RUNS);
        for (t, c) in tot.iter_mut().zip(cells) {
            *t += c;
        }
    }
    println!("| all | {} | {} | {} | | | |", tot[0], tot[1], tot[2]);
}
