//! Conformance corpus and measurement of the second marking layer's registered scheme
//! (`live-conformance/synthesis-mark/`, extensions/synthesis.md §7.5 and §13).
//!
//!   wai_synthesis_mark_vectors gen     <corpus-dir>        write the corpus from fixed seeds
//!   wai_synthesis_mark_vectors verify  <corpus-dir>        every file regenerates byte for
//!                                                          byte, no other file but README.md is
//!                                                          there, and every vector and case
//!                                                          gets what it states
//!   wai_synthesis_mark_vectors measure <png-dir> <out> [tent64s]
//!                                                          run the battery of §7.5.7 (with
//!                                                          `tent64s`, of §7.9.6) over every
//!                                                          8-bit RGB PNG of a directory and
//!                                                          write the measurement, each picture
//!                                                          named by its file's SHA-256
//!
//! The generator and this verifier are one implementation. The corpus's second
//! implementation is `live-conformance/synthesis_mark_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::synthesis::{hex, mark, Registered};
use wai::synthesis_mark::battery::IMAGE_ATTACKS;
use wai::synthesis_mark::{corpus, Frames};

fn main() {
    let a: Vec<String> = std::env::args().collect();
    match (a.get(1).map(String::as_str), a.get(2), a.get(3)) {
        (Some("gen"), Some(d), None) => {
            for (name, body) in corpus::generate() {
                let p = Path::new(d).join(&name);
                if let Some(dir) = p.parent()
                    && let Err(e) = std::fs::create_dir_all(dir)
                {
                    eprintln!("create {}: {e}", dir.display());
                    std::process::exit(1);
                }
                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), None) => std::process::exit(verify(Path::new(d))),
        (Some("measure"), Some(d), Some(out)) => {
            let scheme = match a.get(4).map(String::as_str) {
                None | Some("tent64") => Registered::Tent64,
                Some("tent64s") => Registered::Tent64s,
                Some(x) => {
                    eprintln!("no scheme named {x}: tent64 or tent64s");
                    std::process::exit(2);
                }
            };
            std::process::exit(measure(Path::new(d), Path::new(out), scheme))
        }
        _ => {
            eprintln!("usage: wai_synthesis_mark_vectors gen|verify <corpus-dir> | measure <png-dir> <out.json>");
            std::process::exit(2);
        }
    }
}

/// Every file under `dir`, by its path relative to `dir`, but the top-level README.md.
fn read_tree(dir: &Path) -> std::io::Result<BTreeMap<String, Vec<u8>>> {
    let mut out = BTreeMap::new();
    let mut stack = vec![dir.to_path_buf()];
    while let Some(d) = stack.pop() {
        for e in std::fs::read_dir(&d)? {
            let e = e?;
            let p = e.path();
            if p.is_dir() {
                stack.push(p);
                continue;
            }
            let rel = p.strip_prefix(dir).expect("under the directory").to_string_lossy().replace('\\', "/");
            if rel != "README.md" {
                out.insert(rel, std::fs::read(&p)?);
            }
        }
    }
    Ok(out)
}

fn verify(dir: &Path) -> i32 {
    let files = match read_tree(dir) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("read {}: {e}", dir.display());
            return 1;
        }
    };
    let generated: BTreeMap<String, Vec<u8>> = corpus::generate().into_iter().collect();
    let same = files == generated;
    if !same {
        for k in generated.keys().filter(|k| files.get(*k) != generated.get(*k)) {
            println!("  differs or missing: {k}");
        }
        for k in files.keys().filter(|k| !generated.contains_key(*k)) {
            println!("  not generated: {k}");
        }
    }
    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 embedding, detection and case gets what the corpus states", if fails.is_empty() { "PASS" } else { "FAIL" });
    (!same || !fails.is_empty()) as i32
}

fn measure(dir: &Path, out: &Path, scheme: Registered) -> i32 {
    let mut names: Vec<_> = match std::fs::read_dir(dir) {
        Ok(r) => r.flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == "png")).collect(),
        Err(e) => {
            eprintln!("read {}: {e}", dir.display());
            return 1;
        }
    };
    names.sort();
    let mut pics = Vec::new();
    let mut files = Vec::new();
    for p in &names {
        let bytes = match std::fs::read(p) {
            Ok(b) => b,
            Err(e) => {
                eprintln!("read {}: {e}", p.display());
                return 1;
            }
        };
        let (w, h, rgb) = match wai::png_still::decode(&bytes) {
            Ok(x) => x,
            Err(e) => {
                eprintln!("{}: {e}", p.display());
                return 1;
            }
        };
        let digest = hex(&mark::sha256(&bytes));
        files.push(serde_json::json!({"file": p.file_name().map(|n| n.to_string_lossy().into_owned()), "sha256": digest}));
        pics.push((digest[..16].to_string(), Frames::one(rgb, w as usize, h as usize)));
    }
    let attacks: Vec<_> = match scheme {
        Registered::Tent64 => IMAGE_ATTACKS.to_vec(),
        Registered::Tent64s => IMAGE_ATTACKS.iter().chain(wai::synthesis_mark::battery::SYNC_ATTACKS.iter()).copied().collect(),
    };
    let m = corpus::measure_with(scheme, &[("photographs", pics)], &attacks);
    let doc = serde_json::json!({
        "about": "the battery of extensions/synthesis.md §7.5.7 over the PNG files named, written by wai_synthesis_mark_vectors measure",
        "files": files,
        "pictures": m.pictures,
        "table": m.table,
        "unmarked": m.unmarked,
        "words": m.words_json(),
        "claims": m.claims,
    });
    if let Err(e) = std::fs::write(out, serde_json::to_string_pretty(&doc).expect("JSON") + "\n") {
        eprintln!("write {}: {e}", out.display());
        return 1;
    }
    println!("wrote {}", out.display());
    0
}
