//! The corpus of the resynchronising scheme `wai-mark:tent64s` (extensions/synthesis.md §7.9,
//! §13), in the same directory as `tent64`'s and sharing its frames: `scheme-tent64s.json`,
//! `vectors-tent64s.json` and `battery-tent64s.json`, and the frames they name. Frames a crop,
//! a rotation or a capture of a screen makes are not stored: a vector names the stored frames
//! and the attack, and every implementation derives them.

use super::super::battery::{apply, attack_named, periodic, CLIP_ATTACKS, IMAGE_ATTACKS, SYNC_ATTACKS};
use super::*;
use crate::synthesis::mark::sync;

/// The corpus's clip under this scheme is the same clip, so its frames are shared.
fn marking_s(c: &Value, payload: [u8; 8], embedding: Value) -> Value {
    sign(
        json!({
            "kind": "wai-marking",
            "subject": c["subject"]["content_hash"],
            "origin": hex(&link_hash(c)),
            "scheme": sync::SCHEME,
            "scheme_digest": hex(&sync::digest()),
            "payload": hex(&payload),
            "embedding": embedding,
            "issued_at": ISSUED,
        }),
        &key(MARKER),
    )
}

fn detection_s(subject: &[u8; 32], payload: Option<[u8; 8]>, read: Value) -> Value {
    sign(
        json!({
            "kind": "wai-detection",
            "subject": hex(subject),
            "scheme": sync::SCHEME,
            "scheme_digest": hex(&sync::digest()),
            "payload": payload.map(|p| hex(&p)),
            "read": read,
            "issued_at": ISSUED,
        }),
        &key(DETECTOR),
    )
}

/// What the detector read, with stage 2's finding.
pub(super) fn detected_s_json(d: Option<sync::Detected>) -> Value {
    d.map_or(Value::Null, |d| {
        json!({
            "payload": hex(&d.payload),
            "stage": d.stage,
            "orientation": d.orientation,
            "errors": d.errors,
            "resync": d.resync.map(|r| json!({
                "lattice": r.lattice,
                "bins": [r.bins.0, r.bins.1],
                "phase": [r.phase.0, r.phase.1],
                "offset": [r.offset.0, r.offset.1],
                "candidate": r.candidate,
                "rank": r.rank,
                "agreement": r.agreement,
            })),
        })
    })
}

/// Stage 2's trace (§7.9.4 steps 5 to 9): every candidate kept and the hypotheses decoded.
pub(super) fn trace_json(t: Option<sync::Trace>) -> Value {
    t.map_or(Value::Null, |t| {
        json!({
            "candidates": t.candidates.iter().map(|(v, r)| json!({
                "vectors": v,
                "lattice": r.lattice,
                "bins": [r.bins.0, r.bins.1],
                "phase": [r.phase.0, r.phase.1],
            })).collect::<Vec<_>>(),
            "decoded": t.decoded.iter().map(|&(a, ti, o, x0, y0)| json!([a, ti, o, x0, y0])).collect::<Vec<_>>(),
        })
    })
}

/// A derivation a case or a vector names: stored frames, their size and count, and an attack
/// at a seed (or `periodic`).
#[derive(Clone)]
struct Derive {
    base: Vec<u8>,
    w: usize,
    h: usize,
    n: usize,
    attack: &'static str,
    seed: u32,
}

impl Derive {
    fn frames(&self) -> Frames {
        let f = Frames { data: self.base.clone(), w: self.w, h: self.h, n: self.n };
        if self.attack == "periodic" {
            return periodic(&f);
        }
        apply(attack_named(self.attack).expect("an attack the battery names"), &f, self.seed)
    }

    fn json(&self) -> Value {
        json!({"attack": self.attack, "seed": self.seed, "width": self.w, "height": self.h, "frames": self.n})
    }
}

/// A case and the derivations of its decoded frames, by object.
struct SyncVector {
    name: &'static str,
    about: &'static str,
    case: Case,
    derive: Vec<([u8; 32], Derive)>,
    intent: Intent,
}

fn derived_case_json(v: &SyncVector, frames: &mut BTreeMap<String, Vec<u8>>) -> Map<String, Value> {
    let mut m = case_json(&v.case, frames);
    // Derived frames are named by their base, never stored.
    let mut decoded = Vec::new();
    for (h, b) in &v.case.decoded {
        match v.derive.iter().find(|(o, _)| o == h) {
            Some((_, d)) => {
                frames.remove(&frame_name(b));
                frames.insert(frame_name(&d.base), d.base.clone());
                decoded.push(json!({"object": hex(h), "frames": hex(&sha(&d.base)), "derive": d.json()}));
            }
            None => decoded.push(json!({"object": hex(h), "frames": hex(&sha(b))})),
        }
    }
    m.insert("decoded".into(), json!(decoded));
    m
}

/// The scheme's cases (§13): the marking proved and the detection reproduced in place; a crop
/// and a rotation of the marked still and a capture of a screen, read again by stage 2; and the
/// controls a resynchronising detector needs, a crop beyond its reach and a lattice forged into
/// unmarked content, each reported as carrying a mark.
fn sync_vectors(x: &World, still_s: &[u8], clip_s: &[u8]) -> Vec<SyncVector> {
    let (w, h) = STILL;
    let (cw, chh, n) = CLIP;
    let sp = payload_of(&x.still);
    let still_mark = marking_s(&x.still, sp, embedding(1, w, h, &x.p.still, still_s));
    let proved = Policy { dual_layer_required: true, second_layer_proof: true, ..Default::default() };
    let rot_env = envelope("image", "replicate", "wai.image.png", &label("rotated copy"));
    let rot_claim = claim(&rot_env, "image", "synthesized", "convert", "wai.derive.rotate", "trainedAlgorithmicMedia", json!([]), Value::Null, json!([input_of(&x.still)]), EDITOR);
    let screen_env = envelope("image", "replicate", "wai.image.png", &label("captured screen"));
    let clip_crop_env = envelope("video", "replicate", "wai.video.int_motion", &label("cropped clip"));
    let on_still = |attack: &'static str, seed: u32| Derive { base: still_s.to_vec(), w, h, n: 1, attack, seed };
    let read_of = |d: &Derive| -> (Value, Vec<u8>) {
        let f = d.frames();
        (read(f.n, f.w, f.h, &f.data), f.data)
    };
    let mut out = Vec::new();
    out.push(SyncVector {
        name: "still_proved_s",
        about: "the generated still marked by wai-mark:tent64s: the marking recomputed from the generator's unmarked frame, the detection run again agrees, read by stage 1 (§7.9.3, §7.9.4, §7.6)",
        case: Case {
            claims: vec![text(&x.still)],
            markings: vec![text(&still_mark)],
            detections: vec![text(&detection_s(&ch(&x.still_env), Some(sp), read(1, w, h, still_s)))],
            objects: vec![x.still_env.clone()],
            unmarked: held(&[(&x.still_env, &x.p.still)]),
            decoded: held(&[(&x.still_env, still_s)]),
            policy: proved,
            ..Default::default()
        },
        derive: vec![],
        intent: Intent::Accept,
    });
    for (name, about, attack, env, claim_v) in [
        (
            "crop_agrees_s",
            "a crop of 10% from the left of the marked still, a conversion of it: stage 2 finds the tile lattice again and reads the still's payload, which the crop's claim inherits (§7.9.4, §7.3 step 4)",
            "crop-left-10%",
            &x.crop_env,
            Some(&x.crop_claim),
        ),
        (
            "rotation_agrees_s",
            "a rotation of the marked still by 2 degrees, a conversion of it: stage 2 finds the rotated lattice and reads the still's payload, which the rotation's claim inherits (§7.9.4, §7.3 step 4)",
            "rotate-2",
            &rot_env,
            Some(&rot_claim),
        ),
        (
            "screen_read_s",
            "a simulated capture of a screen showing the marked still (resampled by 4/5, turned 2 degrees clockwise, the display curve and noise): stage 2 reads the payload; with no claim held for the capture the detection is not checked (§7.9.6, §7.3 step 4)",
            "screen-b",
            &screen_env,
            None,
        ),
    ] {
        let d = on_still(attack, 0);
        let (rd, data) = read_of(&d);
        let mut claims = vec![text(&x.still)];
        if let Some(c) = claim_v {
            claims.push(text(c));
        }
        out.push(SyncVector {
            name,
            about,
            case: Case {
                claims,
                detections: vec![text(&detection_s(&ch(env), Some(sp), rd))],
                decoded: held(&[(env, &data)]),
                policy: Policy { second_layer_proof: true, ..Default::default() },
                ..Default::default()
            },
            derive: vec![(ch(env), d)],
            intent: Intent::Accept,
        });
    }
    let dclip = Derive { base: clip_s.to_vec(), w: cw, h: chh, n, attack: "crop-left-10%", seed: 0 };
    let (rd, data) = read_of(&dclip);
    out.push(SyncVector {
        name: "clip_crop_s",
        about: "the marked clip cropped by 10% from the left: stage 2 folds its three frames together and reads the clip's payload; with no claim held for the crop the detection is not checked (§7.9.4)",
        case: Case {
            claims: vec![text(&x.clip)],
            detections: vec![text(&detection_s(&ch(&clip_crop_env), Some(payload_of(&x.clip)), rd))],
            decoded: held(&[(&clip_crop_env, &data)]),
            policy: Policy { second_layer_proof: true, ..Default::default() },
            ..Default::default()
        },
        derive: vec![(ch(&clip_crop_env), dclip)],
        intent: Intent::Accept,
    });
    let beyond = on_still("crop-left-40%", 0);
    let (rd, data) = read_of(&beyond);
    out.push(SyncVector {
        name: "beyond_range_s",
        about: "a detection that reports the still's mark in a crop of 40% from the left, beyond the lattice stage 2 searches: run again, the detector reads no mark, and the detection is refused (§7.9.4, §7.6)",
        case: Case {
            claims: vec![text(&x.still), text(&x.crop_claim)],
            detections: vec![text(&detection_s(&ch(&x.crop_env), Some(sp), rd))],
            decoded: held(&[(&x.crop_env, &data)]),
            ..Default::default()
        },
        derive: vec![(ch(&x.crop_env), beyond)],
        intent: Intent::Refuse(At::Detection(0), Reason::DetectionIrreproducible),
    });
    let forged = Derive { base: x.p.still.clone(), w, h, n: 1, attack: "periodic", seed: 0 };
    let (rd, data) = read_of(&forged);
    out.push(SyncVector {
        name: "forged_lattice_s",
        about: "unmarked content forged to repeat with the period of the tiles (the unmarked still tiled by its top-left quarter), and a detection that reports a mark in it: stage 2 finds the forged lattice, no hypothesis decodes, and the detection is refused (§7.9.4, §7.9.5)",
        case: Case {
            claims: vec![text(&x.relabel)],
            detections: vec![text(&detection_s(&ch(&x.relabel_env), Some(payload_of(&x.relabel)), rd))],
            decoded: held(&[(&x.relabel_env, &data)]),
            ..Default::default()
        },
        derive: vec![(ch(&x.relabel_env), forged)],
        intent: Intent::Refuse(At::Detection(0), Reason::DetectionIrreproducible),
    });
    out.push(SyncVector {
        name: "scheme_mismatch_s",
        about: "a marking of wai-mark:tent64s under the digest of wai-mark:tent64 (§7.4)",
        case: Case {
            claims: vec![text(&x.still)],
            markings: vec![text(&resign(with(still_mark.clone(), &["scheme_digest"], json!(hex(&mark::digest()))), MARKER))],
            ..Default::default()
        },
        derive: vec![],
        intent: Intent::Refuse(At::Marking(0), Reason::SchemeMismatch),
    });
    let other = marking_s(&x.still, sp, embedding(1, w, h, &x.p.still, &x.still_m));
    out.push(SyncVector {
        name: "other_scheme_frames_s",
        about: "a marking of wai-mark:tent64s whose marked frames are the still marked by wai-mark:tent64: the two schemes lay the bits out differently, and the recomputation is not those frames (§7.6, §7.9.2)",
        case: Case {
            claims: vec![text(&x.still)],
            markings: vec![text(&other)],
            objects: vec![x.still_env.clone()],
            unmarked: held(&[(&x.still_env, &x.p.still)]),
            decoded: held(&[(&x.still_env, &x.still_m)]),
            ..Default::default()
        },
        derive: vec![],
        intent: Intent::Refuse(At::Marking(0), Reason::MarkRecomputeMismatch),
    });
    out
}

/// The scheme's files: its definition, its vectors, its measurement, and the frames its
/// vectors store (added to `frames`).
pub(super) fn generate(x: &World, frames: &mut BTreeMap<String, Vec<u8>>) -> Vec<(String, Vec<u8>)> {
    let (w, h) = STILL;
    let (cw, chh, n) = CLIP;
    let tl = sync::tile_layout();
    let still_s = sync::embed(&x.p.still, w, h, 1, &payload_of(&x.still)).0;
    let clip_s = sync::embed(&x.p.clip, cw, chh, n, &payload_of(&x.clip)).0;

    let mut embeds = Vec::new();
    for (name, about, unmarked, fw, fh, fn_, payload) in [
        ("still", "the corpus's spectral still under its claim's payload", &x.p.still, w, h, 1, payload_of(&x.still)),
        ("clip", "the corpus's clip, three frames, under its claim's payload", &x.p.clip, cw, chh, n, payload_of(&x.clip)),
    ] {
        let (marked, reports) = sync::embed(unmarked, fw, fh, fn_, &payload);
        frames.insert(frame_name(unmarked), unmarked.clone());
        frames.insert(frame_name(&marked), marked.clone());
        embeds.push(json!({
            "name": name,
            "about": about,
            "frames": fn_,
            "width": fw,
            "height": fh,
            "unmarked": hex(&sha(unmarked)),
            "payload": hex(&payload),
            "marked": hex(&sha(&marked)),
            "amplitudes": reports.iter().map(|r| r.amplitudes.iter().map(|a| format!("{a:x}")).collect::<String>()).collect::<Vec<_>>(),
            "short": reports.iter().map(|r| r.short).collect::<Vec<_>>(),
            "sse": sse(unmarked, &marked),
        }));
    }

    // Detections on stored frames, and on frames derived from them.
    let mut detects = Vec::new();
    let stored: Vec<(&str, &Vec<u8>, usize, usize, usize)> = vec![
        ("still marked", &still_s, w, h, 1),
        ("still unmarked", &x.p.still, w, h, 1),
        ("still marked by tent64", &x.still_m, w, h, 1),
        ("clip marked", &clip_s, cw, chh, n),
        ("photo unmarked", &x.p.photo, w, h, 1),
    ];
    for (name, data, fw, fh, fn_) in stored {
        frames.insert(frame_name(data), data.clone());
        detects.push(json!({
            "name": name,
            "frames": fn_,
            "width": fw,
            "height": fh,
            "samples": hex(&sha(data)),
            "detected": detected_s_json(sync::detect(data, fw, fh, fn_)),
            "stage2": trace_json(sync::stage2_trace(data, fw, fh, fn_)),
        }));
    }
    for (name, base, fw, fh, fn_, attack) in [
        ("clip marked, cropped 10% from the left", &clip_s, cw, chh, n, "crop-left-10%"),
        ("clip marked, turned 1 degree", &clip_s, cw, chh, n, "rotate-1"),
        ("still marked, cropped 40% from the left", &still_s, w, h, 1, "crop-left-40%"),
        ("still unmarked, tiled by its quarter", &x.p.still, w, h, 1, "periodic"),
    ] {
        let d = Derive { base: base.clone(), w: fw, h: fh, n: fn_, attack, seed: 0 };
        let f = d.frames();
        detects.push(json!({
            "name": name,
            "base": hex(&sha(base)),
            "derive": d.json(),
            "width": f.w,
            "height": f.h,
            "frames": f.n,
            "samples": hex(&sha(&f.data)),
            "detected": detected_s_json(sync::detect(&f.data, f.w, f.h, f.n)),
            "stage2": trace_json(sync::stage2_trace(&f.data, f.w, f.h, f.n)),
        }));
    }

    // The battery's sample: every picture attack on the marked and the unmarked still.
    let mut sample = Vec::new();
    for (which, data) in [("marked", &still_s), ("unmarked", &x.p.still)] {
        let f = Frames::one(data.clone(), w, h);
        for (ai, (an, a)) in IMAGE_ATTACKS.iter().chain(SYNC_ATTACKS.iter()).enumerate() {
            let y = apply(*a, &f, ai as u32);
            sample.push(json!({
                "frames": hex(&sha(data)),
                "which": which,
                "attack": an,
                "seed": ai,
                "width": y.w,
                "height": y.h,
                "result_samples": hex(&sha(&y.data)),
                "detected": detected_s_json(sync::detect(&y.data, y.w, y.h, y.n)),
            }));
        }
    }

    // The cases.
    let vs = sync_vectors(x, &still_s, &clip_s);
    let mut cases = Vec::new();
    for v in &vs {
        let got = crate::synthesis::verify(&v.case);
        match (&v.intent, &got) {
            (Intent::Accept, Outcome::Accept(_)) => {}
            (Intent::Refuse(at, r), Outcome::Refuse { at: a, reason }) if at == a && r == reason => {}
            _ => panic!("case {} does not reach the outcome it was written to have: {got:?}", v.name),
        }
        let mut m = Map::new();
        m.insert("name".into(), json!(v.name));
        m.insert("about".into(), json!(v.about));
        m.extend(derived_case_json(v, frames));
        m.extend(outcome_json(&got));
        cases.push(Value::Object(m));
    }

    let attempts = mark::ORIENTATIONS as u32 + sync::DECODES as u32;
    let doc = json!({
        "about": "the conformance vectors of the resynchronising watermark scheme of extensions/synthesis.md §7.9, written by wai_synthesis_mark_vectors gen",
        "scheme": {
            "term": sync::SCHEME,
            "digest": hex(&sync::digest()),
            "definition": "scheme-tent64s.json",
            "shuffle_first": tl.perm[..16].to_vec(),
            "shuffle_last": tl.perm[sync::POSITIONS - 16..].to_vec(),
            "signs_first": tl.sign[..16].to_vec(),
            "mask": bits(&tl.mask),
            "false_positive_log2": false_positive_log2_for(attempts),
        },
        "embeds": embeds,
        "detects": detects,
        "battery_sample": sample,
        "cases": cases,
    });

    let attacks: Vec<_> = IMAGE_ATTACKS.iter().chain(SYNC_ATTACKS.iter()).copied().collect();
    let images = measure_with(Registered::Tent64s, &[("spectral", spectral_set()), ("leaves", leaves_set())], &attacks);
    let video = measure_with(Registered::Tent64s, &[("clips", clips())], &CLIP_ATTACKS);
    let battery = json!({
        "about": "the measurement of extensions/synthesis.md §7.9.7, written by wai_synthesis_mark_vectors gen",
        "sets": [
            {"name": "spectral", "pictures": SET_SIZE, "width": SET_SIDE, "height": SET_SIDE, "source": "wai.generate.spectral's generator with its committed model, the prompts of synthesis_mark::PROMPTS, seed the index, 4 steps"},
            {"name": "leaves", "pictures": SET_SIZE, "width": SET_SIDE, "height": SET_SIDE, "source": "synthesis_mark::leaves, seeds 0 to 15"},
            {"name": "clips", "pictures": 4, "width": CLIP_SIDE, "height": CLIP_SIDE, "frames": CLIP_FRAMES, "source": "synthesis_mark::clips: a window panning over two dead-leaves and two spectral pictures"},
        ],
        "image_attacks": attacks.iter().map(|(n, _)| n).collect::<Vec<_>>(),
        "clip_attacks": CLIP_ATTACKS.iter().map(|(n, _)| n).collect::<Vec<_>>(),
        "pictures": images.pictures.iter().chain(&video.pictures).collect::<Vec<_>>(),
        "table": images.table.iter().chain(&video.table).collect::<Vec<_>>(),
        "unmarked": images.unmarked.iter().chain(&video.unmarked).collect::<Vec<_>>(),
        "words": images.words_json(),
        "false_positive_log2": false_positive_log2_for(attempts),
        "claims": {"pictures": images.claims, "clips": video.claims},
    });
    vec![
        ("scheme-tent64s.json".into(), sync::definition_text().into_bytes()),
        ("vectors-tent64s.json".into(), (serde_json::to_string_pretty(&doc).expect("JSON") + "\n").into_bytes()),
        ("battery-tent64s.json".into(), (serde_json::to_string_pretty(&battery).expect("JSON") + "\n").into_bytes()),
    ]
}

/// Check the scheme's files: every embedding recomputes, every detection reads what it states,
/// every case gets its outcome.
pub(super) fn verify(files: &BTreeMap<String, Vec<u8>>) -> Vec<String> {
    let mut fails = Vec::new();
    let Some(doc) = files.get("vectors-tent64s.json").and_then(|s| serde_json::from_slice::<Value>(s).ok()) else {
        return vec!["vectors-tent64s.json does not read".into()];
    };
    if files.get("scheme-tent64s.json").map(|b| sha(b)) != Some(sync::digest()) {
        fails.push("scheme-tent64s.json is not the definition whose digest the scheme has".into());
    }
    let frames: BTreeMap<String, Vec<u8>> = files.iter().filter(|(k, _)| k.starts_with("frames/")).map(|(k, v)| (k.clone(), v.clone())).collect();
    let get = |h: &Value| frames.get(&format!("frames/{}.rgb", h.as_str().unwrap_or_default())).cloned();
    let dims = |v: &Value, k: &str| v[k].as_u64().unwrap_or(0) as usize;
    for e in doc["embeds"].as_array().into_iter().flatten() {
        let (Some(u), Some(p)) = (get(&e["unmarked"]), e["payload"].as_str().and_then(unhex)) else {
            fails.push(format!("tent64s embed {}: does not read", e["name"]));
            continue;
        };
        let marked = sync::embed(&u, dims(e, "width"), dims(e, "height"), dims(e, "frames"), &p.try_into().unwrap_or([0; 8])).0;
        if hex(&sha(&marked)) != e["marked"].as_str().unwrap_or_default() {
            fails.push(format!("tent64s embed {}: the marked frames differ", e["name"]));
        }
    }
    for d in doc["detects"].as_array().into_iter().flatten() {
        let data = match d.get("derive") {
            None => get(&d["samples"]),
            Some(dv) => derived(&json!({"frames": d["base"], "derive": dv}), &frames),
        };
        let Some(s) = data else {
            fails.push(format!("tent64s detect {}: no frames", d["name"]));
            continue;
        };
        if hex(&sha(&s)) != d["samples"].as_str().unwrap_or_default() {
            fails.push(format!("tent64s detect {}: the derived frames differ", d["name"]));
        }
        if detected_s_json(sync::detect(&s, dims(d, "width"), dims(d, "height"), dims(d, "frames"))) != d["detected"] {
            fails.push(format!("tent64s detect {}: reads otherwise", d["name"]));
        }
        if trace_json(sync::stage2_trace(&s, dims(d, "width"), dims(d, "height"), dims(d, "frames"))) != d["stage2"] {
            fails.push(format!("tent64s detect {}: stage 2 runs otherwise", d["name"]));
        }
    }
    for v in doc["cases"].as_array().into_iter().flatten() {
        let name = v["name"].as_str().unwrap_or("?");
        let Some(c) = case_from_json(v, &frames) else {
            fails.push(format!("{name}: the case does not read"));
            continue;
        };
        let got = Value::Object(outcome_json(&crate::synthesis::verify(&c)));
        let mut want = Map::new();
        for k in ["outcome", "reason", "at", "statements"] {
            if let Some(x) = v.get(k) {
                want.insert(k.into(), x.clone());
            }
        }
        if got != Value::Object(want) {
            fails.push(format!("{name}: {got}"));
        }
    }
    fails
}
