//! `cargo xtask libraries`: writes the libraries RustFig makes itself (plan
//! D13) as `.fig` files under `libraries/`, from code, so the parts stay
//! consistent and can be changed in one place. Today: `libraries/Feynman/`,
//! `libraries/QuantumCircuits/`, `libraries/OpticsLab/` and
//! `libraries/PhysicsCircuits/`.
//! The output is deterministic (the same bytes on every run); a test checks
//! that the committed files equal what the generator writes.
//!
//! Conventions: Fig units (1200 per inch), the 1/4-inch grid (300 units; the
//! arrows' mid points on the 1/8-inch grid, 150), every part one compound
//! with a description as its comment (the library search reads it), xfig's
//! standard attributes (thickness 2, black, depth 50; vertices and blobs in
//! front at depth 40).

mod circuits;
mod optics;

use rustfig_format::{write, FileFormat};
use rustfig_geom::Point;
use rustfig_model::*;
use std::f64::consts::PI;
use std::path::Path;

/// The grid parts snap to: 1/8 inch (line ends are on the 1/4-inch grid).
#[cfg_attr(not(test), allow(dead_code))] // used by the tests
pub const GRID: f64 = 150.0;

/// One library figure: file name (without `.fig`), the compound, and the
/// points where lines join (all on the grid).
#[cfg_attr(not(test), allow(dead_code))] // `ends` is checked by the tests
pub struct Part {
    pub name: String,
    pub object: Object,
    pub ends: Vec<Point>,
}

fn p(x: f64, y: f64) -> Point {
    Point::new(x.round(), y.round())
}

fn style(line: LineKind, thickness: i32, depth: i32) -> Style {
    Style {
        line,
        thickness,
        depth,
        style_val: match line {
            LineKind::Dashed => 6.0,
            LineKind::Dotted => 4.0,
            _ => 0.0,
        },
        ..Style::default()
    }
}

fn arrow_head(size: f64) -> Arrow {
    Arrow {
        kind: 1,
        style: 1,
        thickness: 1.0,
        width: size * 0.6,
        height: size,
    }
}

fn compound(objects: Vec<Object>, comments: &[&str]) -> Object {
    let mut o = Object::new(ObjectKind::Compound(Compound { objects }));
    o.comments = comments.iter().map(|s| s.to_string()).collect();
    o
}

fn polyline(s: Style, pts: Vec<Point>, fwd: Option<Arrow>) -> Object {
    let mut l = Polyline::new(PolyKind::Polyline, s, pts);
    l.forward_arrow = fwd;
    Object::new(ObjectKind::Polyline(l))
}

fn spline(s: Style, pts: Vec<Point>) -> Object {
    let mut sp = Spline::new(SplineKind::Interpolated, false, s, pts);
    let n = sp.shapes.len();
    for (i, sh) in sp.shapes.iter_mut().enumerate() {
        // xfig's interpolated splines: -1 inside, 0 at the open ends.
        *sh = if i == 0 || i + 1 == n { 0.0 } else { -1.0 };
    }
    Object::new(ObjectKind::Spline(sp))
}

fn arc_obj(s: Style, a: Point, m: Point, b: Point, fwd: Option<Arrow>) -> Option<Object> {
    let mut arc = Arc::through(s, a, m, b, false)?;
    arc.forward_arrow = fwd;
    Some(Object::new(ObjectKind::Arc(arc)))
}

/// The curve a line runs along: a straight segment, or the half circle on
/// the chord `a`-`b`, bulging to the left of the direction of travel.
#[derive(Clone, Copy)]
enum Route {
    Line(Point, Point),
    Arc(Point, Point),
}

impl Route {
    fn ends(self) -> (Point, Point) {
        match self {
            Route::Line(a, b) | Route::Arc(a, b) => (a, b),
        }
    }
    fn length(self) -> f64 {
        let (a, b) = self.ends();
        let c = (b.x - a.x).hypot(b.y - a.y);
        match self {
            Route::Line(..) => c,
            Route::Arc(..) => c * PI / 2.0,
        }
    }
    /// Position and unit normal at `s` in [0, 1] (arc length fraction).
    fn at(self, s: f64) -> (Point, (f64, f64)) {
        let (a, b) = self.ends();
        let c = (b.x - a.x).hypot(b.y - a.y).max(1e-9);
        let (ux, uy) = ((b.x - a.x) / c, (b.y - a.y) / c);
        let n = (uy, -ux);
        match self {
            Route::Line(..) => (Point::new(a.x + (b.x - a.x) * s, a.y + (b.y - a.y) * s), n),
            Route::Arc(..) => {
                let (cx, cy, r) = ((a.x + b.x) / 2.0, (a.y + b.y) / 2.0, c / 2.0);
                let th = PI * s;
                let (c0, s0) = (th.cos(), th.sin());
                let x = cx - r * c0 * ux + r * s0 * n.0;
                let y = cy - r * c0 * uy + r * s0 * n.1;
                (Point::new(x, y), (-c0 * ux + s0 * n.0, -c0 * uy + s0 * n.1))
            }
        }
    }
    fn offset(self, s: f64, d: f64) -> Point {
        let (q, n) = self.at(s);
        p(q.x + n.0 * d, q.y + n.1 * d)
    }
    fn at_rounded(self, s: f64) -> Point {
        let q = self.at(s).0;
        p(q.x, q.y)
    }
    /// Number of wave periods for a wavelength of about 1/4 inch.
    fn periods(self) -> usize {
        ((self.length() / 300.0).round() as usize).max(1)
    }
}

const THICK: i32 = 2;
const AMP: f64 = 75.0;

/// How far an arrow head on a line reaches past the point where the line
/// goes on: the head is as wide as the line `d` before its tip
/// (`d = line width × head length / head width`), so the continuing line
/// starts under the head instead of poking out of its tip.
fn tip_overshoot(thickness: i32, head: &Arrow) -> f64 {
    if head.width <= 0.0 {
        return 0.0;
    }
    rustfig_geom::stroke_width(f64::from(thickness)) * head.height / head.width
}

/// Fermion line with the arrow in the middle: two pieces, one ending in the
/// arrow head, whose tip lies a little past the middle so that the arrow
/// sits on the line ([`tip_overshoot`]). `reverse` points the arrow back
/// (anti-fermion).
fn fermion(path: Route, reverse: bool) -> Vec<Object> {
    let s = style(LineKind::Solid, THICK, 50);
    let (a, b) = path.ends();
    let arrow = arrow_head(150.0);
    // The tip's place along the route, as a fraction of its length.
    let ds = tip_overshoot(THICK, &arrow) / path.length().max(1.0);
    let tip = if reverse { 0.5 - ds } else { 0.5 + ds };
    let head = Some(arrow);
    match path {
        Route::Line(..) => {
            let (m, t) = (path.at_rounded(0.5), path.at_rounded(tip));
            if reverse {
                vec![polyline(s, vec![a, m], None), polyline(s, vec![b, t], head)]
            } else {
                vec![polyline(s, vec![a, t], head), polyline(s, vec![m, b], None)]
            }
        }
        Route::Arc(..) => {
            let top = path.at_rounded(0.5);
            let halves = if reverse {
                [
                    arc_obj(s, b, path.at_rounded((1.0 + tip) / 2.0), path.at_rounded(tip), head),
                    arc_obj(s, a, path.at_rounded(0.25), top, None),
                ]
            } else {
                [
                    arc_obj(s, a, path.at_rounded(tip / 2.0), path.at_rounded(tip), head),
                    arc_obj(s, top, path.at_rounded(0.75), b, None),
                ]
            };
            halves.into_iter().flatten().collect()
        }
    }
}

/// Dashed (scalar), dotted (ghost) or thin solid line.
fn plain(path: Route, line: LineKind, thickness: i32) -> Vec<Object> {
    let s = style(line, thickness, 50);
    let (a, b) = path.ends();
    match path {
        Route::Line(..) => vec![polyline(s, vec![a, b], None)],
        Route::Arc(..) => arc_obj(s, a, path.at_rounded(0.5), b, None).into_iter().collect(),
    }
}

/// Sine wave (photon, W/Z boson, graviton) along the path: points at the
/// quarter periods of an interpolated spline, so the curve is smooth and the
/// wiggles are evenly spaced.
fn wave(path: Route, thickness: i32) -> Object {
    let n = path.periods();
    let steps = 8 * n;
    let pts: Vec<Point> = (0..=steps)
        .map(|i| {
            let s = i as f64 / steps as f64;
            path.offset(s, AMP * (2.0 * PI * n as f64 * s).sin())
        })
        .collect();
    spline(style(LineKind::Solid, thickness, 50), pts)
}

/// Curly gluon line: a prolate cycloid (loops) along the path, 16 points per
/// loop, starting and ending on the path.
fn gluon(path: Route) -> Object {
    let n = path.periods();
    let l = path.length();
    let a = l / n as f64 / (2.0 * PI);
    let b = 2.0 * a;
    let steps = 16 * n;
    let pts: Vec<Point> = (0..=steps)
        .map(|i| {
            let tau = 2.0 * PI * n as f64 * i as f64 / steps as f64;
            let u = a * tau + b * (1.0 - tau.cos());
            let v = -b * tau.sin();
            path.offset((u / l).clamp(0.0, 1.0), v)
        })
        .collect();
    spline(style(LineKind::Solid, THICK, 50), pts)
}

fn dot(c: Point, r: f64) -> Object {
    let mut s = style(LineKind::Solid, 1, 40);
    s.fill_color = ColorRef::BLACK;
    s.area_fill = 20;
    circle(c, r, s)
}

fn circle(c: Point, r: f64, s: Style) -> Object {
    Object::new(ObjectKind::Ellipse(Ellipse {
        kind: EllipseKind::CircleRadius,
        style: s,
        direction: 1,
        angle: 0.0,
        center: c,
        radii: p(r, r),
        start: c,
        end: p(c.x + r, c.y),
    }))
}

fn momentum(a: Point, b: Point) -> Object {
    polyline(
        style(LineKind::Solid, 1, 50),
        vec![a, b],
        Some(Arrow {
            kind: 1,
            style: 1,
            thickness: 1.0,
            width: 75.0,
            height: 120.0,
        }),
    )
}

/// A LaTeX label (text with the TeX flag) for the examples.
fn label(x: f64, y: f64, tex: &str) -> Object {
    Object::new(ObjectKind::Text(Text {
        justify: Justify::Center,
        color: ColorRef::BLACK,
        depth: 30,
        pen_style: -1,
        font: 0,
        size: 12.0,
        angle: 0.0,
        flags: text_flags::SPECIAL,
        height: 135.0,
        length: 120.0 * tex.chars().count() as f64,
        origin: p(x, y),
        text: tex.to_string(),
        local_font: None,
    }))
}

fn part(name: &str, objects: Vec<Object>, comments: &[&str], ends: Vec<Point>) -> Part {
    Part {
        name: name.to_string(),
        object: compound(objects, comments),
        ends,
    }
}

/// The line parts: horizontal (1 inch, four periods), in one orientation
/// (the user's decision, 2026-10-25: turning is one click in xfig), and the
/// half-circle arcs for loops.
fn line_parts(out: &mut Vec<Part>) {
    let h = Route::Line(p(300.0, 900.0), p(1500.0, 900.0));
    {
        let (path, size) = (h, "horizontal, 1 inch");
        let ends = vec![path.ends().0, path.ends().1];
        let mk = |name: &str, what: &str, how: &str, objs: Vec<Object>| {
            let c1 = format!("Feynman diagram: {what}, {size}");
            let c2 =
                format!("{how} Ends on the 1/4-inch grid; join parts end to end. Keywords: feynman particle physics");
            part(name, objs, &[&c1, &c2], ends.clone())
        };
        out.push(mk(
            "fermion",
            "fermion line (electron, quark, muon)",
            "Arrow along the line.",
            fermion(path, false),
        ));
        out.push(mk(
            "anti_fermion",
            "anti-fermion line (positron, antiquark)",
            "Arrow against the line.",
            fermion(path, true),
        ));
        out.push(mk(
            "scalar",
            "scalar line (Higgs, squark), dashed",
            "Dashed.",
            plain(path, LineKind::Dashed, THICK),
        ));
        out.push(mk(
            "ghost",
            "ghost line, dotted",
            "Dotted.",
            plain(path, LineKind::Dotted, THICK),
        ));
        out.push(mk(
            "photon",
            "photon line (gamma), wavy",
            "Sine wave of 1/4-inch wavelength.",
            vec![wave(path, THICK)],
        ));
        out.push(mk(
            "boson_wz",
            "W or Z boson line, heavy wavy",
            "Thicker sine wave.",
            vec![wave(path, 3)],
        ));
        out.push(mk(
            "gluon",
            "gluon line (g), curly",
            "Loops 1/4 inch apart.",
            vec![gluon(path)],
        ));
        let mut g = vec![wave(path, THICK)];
        g.extend(plain(path, LineKind::Solid, 1));
        out.push(mk(
            "graviton",
            "graviton line, wavy with a straight line through it",
            "Double line.",
            g,
        ));
    }
    let arc = Route::Arc(p(300.0, 900.0), p(1500.0, 900.0));
    let ends = vec![arc.ends().0, arc.ends().1];
    let mk = |name: &str, what: &str, objs: Vec<Object>| {
        let c1 = format!("Feynman diagram: {what}, half circle on a chord of 1 inch (loops, self-energy)");
        part(
            name,
            objs,
            &[
                &c1,
                "Ends on the 1/4-inch grid. Keywords: feynman loop particle physics",
            ],
            ends.clone(),
        )
    };
    out.push(mk("fermion_arc", "fermion line", fermion(arc, false)));
    out.push(mk("anti_fermion_arc", "anti-fermion line", fermion(arc, true)));
    out.push(mk("scalar_arc", "scalar line", plain(arc, LineKind::Dashed, THICK)));
    out.push(mk("photon_arc", "photon line", vec![wave(arc, THICK)]));
    out.push(mk("gluon_arc", "gluon line", vec![gluon(arc)]));
}

fn other_parts(out: &mut Vec<Part>) {
    let c = p(300.0, 300.0);
    out.push(part(
        "vertex_dot",
        vec![dot(c, 45.0)],
        &[
            "Feynman diagram: vertex dot (interaction point, counterterm)",
            "Place on the point where lines meet. Keywords: feynman vertex",
        ],
        vec![c],
    ));
    let c = p(600.0, 600.0);
    let mut s = style(LineKind::Solid, THICK, 40);
    s.fill_color = ColorRef::WHITE;
    s.area_fill = 45;
    out.push(part(
        "blob_hatched",
        vec![circle(c, 300.0, s)],
        &[
            "Feynman diagram: hatched blob (effective vertex, form factor, hadronic part)",
            "Circle of 1/2 inch diameter, hatched at 45 degrees. Keywords: feynman blob",
        ],
        vec![c],
    ));
    let mut s = style(LineKind::Solid, THICK, 40);
    s.fill_color = ColorRef::WHITE;
    s.area_fill = 14;
    out.push(part(
        "blob_grey",
        vec![circle(c, 300.0, s)],
        &[
            "Feynman diagram: grey blob (effective vertex, form factor, hadronic part)",
            "Circle of 1/2 inch diameter, grey fill. Keywords: feynman blob",
        ],
        vec![c],
    ));
    for (name, dir, a, b) in [
        ("momentum_right", "to the right", p(300.0, 300.0), p(900.0, 300.0)),
        ("momentum_left", "to the left", p(900.0, 300.0), p(300.0, 300.0)),
        ("momentum_up", "up", p(300.0, 900.0), p(300.0, 300.0)),
        ("momentum_down", "down", p(300.0, 300.0), p(300.0, 900.0)),
    ] {
        let c1 = format!("Feynman diagram: momentum arrow {dir}");
        out.push(part(
            name,
            vec![momentum(a, b)],
            &[
                &c1,
                "Short arrow of 1/2 inch to place beside a line; add the label k or p as LaTeX text. Keywords: feynman momentum",
            ],
            vec![a, b],
        ));
    }
}

/// The complete example diagrams, built from the same generators as the parts.
fn examples(out: &mut Vec<Part>) {
    let line = Route::Line;
    let mut ex = |name: &str, what: &str, objs: Vec<Object>| {
        let c1 = format!("Feynman diagram example: {what}");
        out.push(part(
            name,
            objs,
            &[
                &c1,
                "Made from the library's parts; edit the labels (LaTeX text). Keywords: feynman example",
            ],
            Vec::new(),
        ));
    };

    // e+ e- -> mu+ mu-, s channel.
    let (v1, v2) = (p(1200.0, 1500.0), p(2400.0, 1500.0));
    let mut o = Vec::new();
    o.extend(fermion(line(p(300.0, 2400.0), v1), false));
    o.extend(fermion(line(p(300.0, 600.0), v1), true));
    o.push(wave(line(v1, v2), THICK));
    o.extend(fermion(line(v2, p(3300.0, 2400.0)), false));
    o.extend(fermion(line(v2, p(3300.0, 600.0)), true));
    o.push(dot(v1, 45.0));
    o.push(dot(v2, 45.0));
    o.push(label(300.0, 2700.0, "$e^-$"));
    o.push(label(300.0, 450.0, "$e^+$"));
    o.push(label(3300.0, 2700.0, "$\\mu^-$"));
    o.push(label(3300.0, 450.0, "$\\mu^+$"));
    o.push(label(1800.0, 1200.0, "$\\gamma^*$"));
    ex(
        "example_ee_to_mumu",
        "e+ e- annihilation into mu+ mu- (s channel, one virtual photon)",
        o,
    );

    // Compton scattering, s channel.
    let (v1, v2) = (p(1200.0, 1800.0), p(2400.0, 1800.0));
    let mut o = Vec::new();
    o.extend(fermion(line(p(300.0, 1800.0), v1), false));
    o.extend(fermion(line(v1, v2), false));
    o.extend(fermion(line(v2, p(3300.0, 1800.0)), false));
    o.push(wave(line(p(300.0, 900.0), v1), THICK));
    o.push(wave(line(v2, p(3300.0, 900.0)), THICK));
    o.push(dot(v1, 45.0));
    o.push(dot(v2, 45.0));
    o.push(label(300.0, 2100.0, "$e^-$"));
    o.push(label(3300.0, 2100.0, "$e^-$"));
    o.push(label(300.0, 750.0, "$\\gamma$"));
    o.push(label(3300.0, 750.0, "$\\gamma$"));
    o.push(label(1800.0, 2100.0, "$p+k$"));
    ex(
        "example_compton",
        "Compton scattering e- gamma to e- gamma (s channel)",
        o,
    );

    // One-loop fermion self-energy.
    let (v1, v2) = (p(1200.0, 1500.0), p(2400.0, 1500.0));
    let mut o = Vec::new();
    o.extend(fermion(line(p(300.0, 1500.0), v1), false));
    o.extend(fermion(line(v1, v2), false));
    o.push(wave(Route::Arc(v1, v2), THICK));
    o.extend(fermion(line(v2, p(3300.0, 1500.0)), false));
    o.push(dot(v1, 45.0));
    o.push(dot(v2, 45.0));
    o.push(label(300.0, 1800.0, "$p$"));
    o.push(label(3300.0, 1800.0, "$p$"));
    o.push(label(1800.0, 1800.0, "$p-k$"));
    o.push(label(1800.0, 600.0, "$k$"));
    ex(
        "example_self_energy",
        "one-loop fermion self-energy (photon loop on a fermion line)",
        o,
    );

    // Gluon exchange between two quarks (t channel).
    let (v1, v2) = (p(1200.0, 900.0), p(1200.0, 2100.0));
    let mut o = Vec::new();
    for v in [v1, v2] {
        o.extend(fermion(line(p(300.0, v.y), v), false));
        o.extend(fermion(line(v, p(2100.0, v.y)), false));
        o.push(dot(v, 45.0));
    }
    o.push(gluon(line(v1, v2)));
    o.push(label(300.0, 750.0, "$q$"));
    o.push(label(2100.0, 750.0, "$q$"));
    o.push(label(300.0, 2400.0, "$q$"));
    o.push(label(2100.0, 2400.0, "$q$"));
    o.push(label(1500.0, 1500.0, "$g$"));
    ex(
        "example_gluon_exchange",
        "quark-quark scattering by one gluon exchange (t channel)",
        o,
    );
}

/// Every figure of the Feynman library, in file-name order.
pub fn feynman() -> Vec<Part> {
    let mut v = Vec::new();
    line_parts(&mut v);
    other_parts(&mut v);
    examples(&mut v);
    v.sort_by(|a, b| a.name.cmp(&b.name));
    v
}

// ---------------------------------------------------------------------------
// QuantumCircuits: wires, gates, controls, measurement and example circuits
// in the look of Qcircuit / quantikz. Qubit wires are horizontal lines 1 inch
// long (x 300..1500, ends on the 1/4-inch grid); wires of multi-wire parts are
// 1/2 inch apart (y 600, 1200, 1800), so parts stack on the 1/2-inch grid.
// ---------------------------------------------------------------------------

/// Half the side of a single-qubit gate box.
const GATE: f64 = 180.0;
/// Half the distance between the two lines of a classical wire.
const CGAP: f64 = 30.0;

fn solid(thickness: i32, depth: i32) -> Style {
    style(LineKind::Solid, thickness, depth)
}

fn white_box(a: Point, b: Point) -> Object {
    let mut s = solid(THICK, 40);
    s.fill_color = ColorRef::WHITE;
    s.area_fill = 20;
    Object::new(ObjectKind::Polyline(Polyline::rect(s, a, b)))
}

/// Gate box centred at (`x`, `y`), `half_w` wide on each side, with a LaTeX
/// label (none for the empty gate).
fn gate_box(x: f64, y: f64, half_w: f64, tex: &str) -> Vec<Object> {
    let mut v = vec![white_box(p(x - half_w, y - GATE), p(x + half_w, y + GATE))];
    if !tex.is_empty() {
        v.push(label(x, y + 60.0, tex));
    }
    v
}

fn qwire(x0: f64, x1: f64, y: f64) -> Object {
    polyline(solid(THICK, 50), vec![p(x0, y), p(x1, y)], None)
}

/// Classical wire: two parallel lines.
fn cwire(x0: f64, x1: f64, y: f64) -> Vec<Object> {
    let s = solid(1, 50);
    vec![
        polyline(s, vec![p(x0, y - CGAP), p(x1, y - CGAP)], None),
        polyline(s, vec![p(x0, y + CGAP), p(x1, y + CGAP)], None),
    ]
}

/// Classical control line (vertical, double).
fn cvline(x: f64, y0: f64, y1: f64) -> Vec<Object> {
    let s = solid(1, 50);
    vec![
        polyline(s, vec![p(x - CGAP, y0), p(x - CGAP, y1)], None),
        polyline(s, vec![p(x + CGAP, y0), p(x + CGAP, y1)], None),
    ]
}

fn vline(x: f64, y0: f64, y1: f64) -> Object {
    polyline(solid(THICK, 50), vec![p(x, y0), p(x, y1)], None)
}

fn ctrl(x: f64, y: f64) -> Object {
    dot(p(x, y), 60.0)
}

fn ctrl_open(x: f64, y: f64) -> Object {
    let mut s = solid(THICK, 40);
    s.fill_color = ColorRef::WHITE;
    s.area_fill = 20;
    circle(p(x, y), 75.0, s)
}

/// Target: circle with a cross (the CNOT's plus in a circle).
fn target(x: f64, y: f64) -> Vec<Object> {
    let mut s = solid(THICK, 40);
    s.fill_color = ColorRef::WHITE;
    s.area_fill = 20;
    let r = 120.0;
    vec![
        circle(p(x, y), r, s),
        polyline(solid(THICK, 39), vec![p(x - r, y), p(x + r, y)], None),
        polyline(solid(THICK, 39), vec![p(x, y - r), p(x, y + r)], None),
    ]
}

/// SWAP end: a cross.
fn swap_x(x: f64, y: f64) -> Vec<Object> {
    let d = 90.0;
    let s = solid(THICK, 40);
    vec![
        polyline(s, vec![p(x - d, y - d), p(x + d, y + d)], None),
        polyline(s, vec![p(x - d, y + d), p(x + d, y - d)], None),
    ]
}

/// Measurement meter: gate box with an arc and a needle.
fn meter(x: f64, y: f64) -> Vec<Object> {
    let mut v = gate_box(x, y, GATE, "");
    if let Some(a) = arc_obj(
        solid(1, 39),
        p(x - 120.0, y + 60.0),
        p(x, y - 60.0),
        p(x + 120.0, y + 60.0),
        None,
    ) {
        v.push(a);
    }
    v.push(polyline(
        solid(1, 39),
        vec![p(x, y + 90.0), p(x + 90.0, y - 60.0)],
        Some(Arrow {
            kind: 1,
            style: 1,
            thickness: 1.0,
            width: 45.0,
            height: 75.0,
        }),
    ));
    v
}

fn barrier(x: f64, y0: f64, y1: f64) -> Object {
    polyline(style(LineKind::Dashed, 1, 45), vec![p(x, y0), p(x, y1)], None)
}

fn qc_part(name: &str, what: &str, how: &str, objs: Vec<Object>, ends: Vec<Point>) -> Part {
    let c1 = format!("Quantum circuit: {what}");
    let c2 = format!("{how} Keywords: quantum circuit qubit gate qcircuit quantikz");
    part(name, objs, &[&c1, &c2], ends)
}

const Y0: f64 = 600.0;
const Y1: f64 = 1200.0;
const Y2: f64 = 1800.0;

fn qc_parts(out: &mut Vec<Part>) {
    let wends = |ys: &[f64]| -> Vec<Point> { ys.iter().flat_map(|&y| [p(300.0, y), p(1500.0, y)]).collect() };
    let w1 = wends(&[Y0]);
    let w2 = wends(&[Y0, Y1]);
    let w3 = wends(&[Y0, Y1, Y2]);
    let with_wire = |mut objs: Vec<Object>, ys: &[f64]| -> Vec<Object> {
        let mut v: Vec<Object> = ys.iter().map(|&y| qwire(300.0, 1500.0, y)).collect();
        v.append(&mut objs);
        v
    };
    out.push(qc_part(
        "wire",
        "qubit wire, 1 inch",
        "Horizontal line; ends on the 1/4-inch grid. Wires of a circuit are 1/2 inch apart.",
        vec![qwire(300.0, 1500.0, Y0)],
        w1.clone(),
    ));
    out.push(qc_part(
        "wire_classical",
        "classical wire (double line), 1 inch",
        "Carries a measurement result; ends on the 1/4-inch grid.",
        cwire(300.0, 1500.0, Y0),
        w1.clone(),
    ));
    for (name, tex, what) in [
        ("gate_h", "$H$", "Hadamard gate H"),
        ("gate_x", "$X$", "Pauli X gate (NOT)"),
        ("gate_y", "$Y$", "Pauli Y gate"),
        ("gate_z", "$Z$", "Pauli Z gate"),
        ("gate_s", "$S$", "phase gate S"),
        ("gate_t", "$T$", "T gate (pi/8 gate)"),
        ("gate_empty", "", "empty gate box for any single-qubit gate"),
    ] {
        out.push(qc_part(
            name,
            &format!("{what}, single-qubit gate on a wire"),
            "Square box centred on the wire. The label is LaTeX text with the TeX flag; edit it with the Text mode.",
            with_wire(gate_box(900.0, Y0, GATE, tex), &[Y0]),
            w1.clone(),
        ));
    }
    out.push(qc_part(
        "gate_rx",
        "rotation gate R_x(theta), single-qubit gate on a wire",
        "Wide box centred on the wire; LaTeX text with the TeX flag, change x and the angle as needed.",
        with_wire(gate_box(900.0, Y0, 330.0, "$R_x(\\theta)$"), &[Y0]),
        w1.clone(),
    ));
    out.push(qc_part(
        "measure",
        "measurement (meter: box with arc and needle) on a wire",
        "Projective measurement in the computational basis; continue with a classical wire.",
        with_wire(meter(900.0, Y0), &[Y0]),
        w1.clone(),
    ));
    out.push(qc_part(
        "control_dot",
        "control (filled dot) on a wire",
        "Draw a vertical line to the target.",
        with_wire(vec![ctrl(900.0, Y0)], &[Y0]),
        w1.clone(),
    ));
    out.push(qc_part(
        "control_open",
        "open control (hollow circle) on a wire, acts when the control qubit is 0",
        "Draw a vertical line to the target.",
        with_wire(vec![ctrl_open(900.0, Y0)], &[Y0]),
        w1.clone(),
    ));
    out.push(qc_part(
        "target",
        "target (circle with a cross) on a wire, the target of a CNOT",
        "Draw a vertical line to the control.",
        with_wire(target(900.0, Y0), &[Y0]),
        w1.clone(),
    ));
    let mut v = vec![vline(900.0, Y0, Y1), ctrl(900.0, Y0)];
    v.extend(target(900.0, Y1));
    out.push(qc_part(
        "cnot",
        "CNOT (controlled NOT), control above, target below, two wires",
        "Complete part on two wires 1/2 inch apart. Flip it vertically for the other direction.",
        with_wire(v, &[Y0, Y1]),
        w2.clone(),
    ));
    let v = vec![vline(900.0, Y0, Y1), ctrl(900.0, Y0), ctrl(900.0, Y1)];
    out.push(qc_part(
        "cz",
        "CZ (controlled Z, two filled dots), two wires",
        "Complete part on two wires 1/2 inch apart; symmetric in the two qubits.",
        with_wire(v, &[Y0, Y1]),
        w2.clone(),
    ));
    let mut v = vec![vline(900.0, Y0, Y1)];
    v.extend(swap_x(900.0, Y0));
    v.extend(swap_x(900.0, Y1));
    out.push(qc_part(
        "swap",
        "SWAP gate (two crosses joined by a line), two wires",
        "Complete part on two wires 1/2 inch apart.",
        with_wire(v, &[Y0, Y1]),
        w2.clone(),
    ));
    let mut v = gate_box(900.0, (Y0 + Y1) / 2.0, GATE, "$U$");
    v[0] = white_box(p(720.0, Y0 - 150.0), p(1080.0, Y1 + 150.0));
    out.push(qc_part(
        "gate_multi2",
        "multi-qubit gate box spanning 2 wires",
        "Tall box over two wires; label in the middle (LaTeX text with the TeX flag).",
        with_wire(v, &[Y0, Y1]),
        w2.clone(),
    ));
    let mut v = gate_box(900.0, Y1, GATE, "$U$");
    v[0] = white_box(p(720.0, Y0 - 150.0), p(1080.0, Y2 + 150.0));
    out.push(qc_part(
        "gate_multi3",
        "multi-qubit gate box spanning 3 wires",
        "Tall box over three wires; label in the middle (LaTeX text with the TeX flag).",
        with_wire(v, &[Y0, Y1, Y2]),
        w3.clone(),
    ));
    for (name, tex, what) in [
        ("init_0", "$|0\\rangle$", "initial state |0> at the left end of a wire"),
        ("init_1", "$|1\\rangle$", "initial state |1> at the left end of a wire"),
        (
            "ket_in",
            "$|\\psi\\rangle$",
            "input ket |psi> at the left end of a wire",
        ),
    ] {
        out.push(qc_part(
            name,
            what,
            "Label left of the wire (LaTeX text with the TeX flag); the wire starts on the grid.",
            vec![label(360.0, Y0 + 60.0, tex), qwire(600.0, 1500.0, Y0)],
            vec![p(600.0, Y0), p(1500.0, Y0)],
        ));
    }
    out.push(qc_part(
        "ket_out",
        "output ket |psi'> at the right end of a wire",
        "Label right of the wire (LaTeX text with the TeX flag).",
        vec![label(1440.0, Y0 + 60.0, "$|\\psi'\\rangle$"), qwire(300.0, 1200.0, Y0)],
        vec![p(300.0, Y0), p(1200.0, Y0)],
    ));
    out.push(qc_part(
        "barrier",
        "barrier (dashed vertical line) across two wires",
        "Separates stages of a circuit; scale it to span more wires.",
        vec![barrier(900.0, Y0 - 150.0, Y1 + 150.0)],
        Vec::new(),
    ));
    out.push(qc_part(
        "control_classical",
        "classical control line (double vertical line) between two wires",
        "Runs from a measurement down to a gate that is controlled by the result.",
        cvline(900.0, Y0, Y1),
        vec![p(900.0, Y0), p(900.0, Y1)],
    ));
}

/// A circuit under construction.
struct Circuit {
    objs: Vec<Object>,
}

impl Circuit {
    fn new() -> Circuit {
        Circuit { objs: Vec::new() }
    }
    fn q(&mut self, x0: f64, x1: f64, y: f64) {
        self.objs.push(qwire(x0, x1, y));
    }
    fn c(&mut self, x0: f64, x1: f64, y: f64) {
        self.objs.extend(cwire(x0, x1, y));
    }
    fn gate(&mut self, x: f64, y: f64, tex: &str) {
        self.objs.extend(gate_box(x, y, GATE, tex));
    }
    fn cnot(&mut self, x: f64, c: f64, t: f64) {
        self.objs.push(vline(x, c, t));
        self.objs.push(ctrl(x, c));
        self.objs.extend(target(x, t));
    }
    fn ctrl_gate(&mut self, x: f64, c: f64, t: f64, tex: &str) {
        self.objs.push(vline(x, c, t));
        self.objs.push(ctrl(x, c));
        self.gate(x, t, tex);
    }
    fn meter(&mut self, x: f64, y: f64) {
        self.objs.extend(meter(x, y));
    }
    fn text(&mut self, x: f64, y: f64, tex: &str) {
        self.objs.push(label(x, y + 60.0, tex));
    }
}

fn qc_examples(out: &mut Vec<Part>) {
    let mut ex = |name: &str, what: &str, c: Circuit| {
        let c1 = format!("Quantum circuit example: {what}");
        out.push(part(
            name,
            c.objs,
            &[
                &c1,
                "Made from the library's parts; edit the labels (LaTeX text). Keywords: quantum circuit example qubit gate",
            ],
            Vec::new(),
        ));
    };

    // Bell state: H, CNOT, measurement of both qubits.
    let mut c = Circuit::new();
    c.q(600.0, 2100.0, Y0);
    c.q(600.0, 2100.0, Y1);
    c.c(2100.0, 2700.0, Y0);
    c.c(2100.0, 2700.0, Y1);
    c.text(330.0, Y0, "$|0\\rangle$");
    c.text(330.0, Y1, "$|0\\rangle$");
    c.gate(900.0, Y0, "$H$");
    c.cnot(1500.0, Y0, Y1);
    c.meter(2100.0, Y0);
    c.meter(2100.0, Y1);
    ex(
        "example_bell",
        "Bell-state preparation (Hadamard, CNOT) and measurement of both qubits",
        c,
    );

    // GHZ on three qubits.
    let mut c = Circuit::new();
    for y in [Y0, Y1, Y2] {
        c.q(600.0, 2400.0, y);
        c.text(330.0, y, "$|0\\rangle$");
    }
    c.gate(900.0, Y0, "$H$");
    c.cnot(1500.0, Y0, Y1);
    c.cnot(2100.0, Y1, Y2);
    ex(
        "example_ghz",
        "GHZ-state preparation on 3 qubits (Hadamard, two CNOTs)",
        c,
    );

    // Teleportation of |psi> from the top qubit to the bottom one.
    let mut c = Circuit::new();
    c.q(600.0, 3600.0, Y0);
    c.c(3600.0, 4800.0, Y0);
    c.q(600.0, 3600.0, Y1);
    c.c(3600.0, 4200.0, Y1);
    c.q(600.0, 5100.0, Y2);
    c.text(330.0, Y0, "$|\\psi\\rangle$");
    c.text(330.0, Y1, "$|0\\rangle$");
    c.text(330.0, Y2, "$|0\\rangle$");
    c.gate(900.0, Y1, "$H$");
    c.cnot(1500.0, Y1, Y2);
    c.objs.push(barrier(1950.0, Y0 - 150.0, Y2 + 150.0));
    c.cnot(2400.0, Y0, Y1);
    c.gate(3000.0, Y0, "$H$");
    c.meter(3600.0, Y0);
    c.meter(3600.0, Y1);
    c.objs.extend(cvline(4200.0, Y1, Y2 - GATE));
    c.gate(4200.0, Y2, "$X$");
    c.objs.extend(cvline(4800.0, Y0, Y2 - GATE));
    c.gate(4800.0, Y2, "$Z$");
    c.text(5460.0, Y2, "$|\\psi\\rangle$");
    ex(
        "example_teleportation",
        "quantum teleportation of |psi> (Bell pair, Bell measurement, classical wires, controlled X and Z)",
        c,
    );

    // QFT on 3 qubits.
    let mut c = Circuit::new();
    for (i, y) in [Y0, Y1, Y2].into_iter().enumerate() {
        c.q(600.0, 4800.0, y);
        c.text(330.0, y, &format!("$|x_{i}\\rangle$"));
        c.text(5100.0, y, &format!("$|y_{i}\\rangle$"));
    }
    c.gate(900.0, Y0, "$H$");
    c.ctrl_gate(1500.0, Y1, Y0, "$R_2$");
    c.ctrl_gate(2100.0, Y2, Y0, "$R_3$");
    c.gate(2700.0, Y1, "$H$");
    c.ctrl_gate(3300.0, Y2, Y1, "$R_2$");
    c.gate(3900.0, Y2, "$H$");
    c.objs.push(vline(4500.0, Y0, Y2));
    c.objs.extend(swap_x(4500.0, Y0));
    c.objs.extend(swap_x(4500.0, Y2));
    ex(
        "example_qft",
        "quantum Fourier transform on 3 qubits (Hadamards, controlled phase rotations, SWAP)",
        c,
    );
}

/// Every figure of the QuantumCircuits library, in file-name order.
pub fn quantum_circuits() -> Vec<Part> {
    let mut v = Vec::new();
    qc_parts(&mut v);
    qc_examples(&mut v);
    v.sort_by(|a, b| a.name.cmp(&b.name));
    v
}

pub use circuits::physics_circuits;
pub use optics::optics_lab;

/// The `.fig` text of a part.
pub fn render(part: &Part) -> Result<String, String> {
    let mut doc = Document::new();
    doc.settings.units = Units::Inches;
    doc.objects.push(std::sync::Arc::new(part.object.clone()));
    doc.assign_ids();
    write(&doc, FileFormat::Fig).map_err(|e| format!("{}: {e:?}", part.name))
}

/// Write the generated libraries: `libraries/Feynman/*.fig` and
/// `libraries/QuantumCircuits/*.fig` `libraries/OpticsLab/*.fig` and `libraries/PhysicsCircuits/*.fig`.
pub fn run(root: &Path) -> Result<(), String> {
    for (name, parts) in [
        ("Feynman", feynman()),
        ("QuantumCircuits", quantum_circuits()),
        ("OpticsLab", optics_lab()),
        ("PhysicsCircuits", physics_circuits()),
    ] {
        let dir = root.join("libraries").join(name);
        std::fs::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
        for part in &parts {
            let path = dir.join(format!("{}.fig", part.name));
            std::fs::write(&path, render(part)?).map_err(|e| format!("{}: {e}", path.display()))?;
        }
        println!("libraries: wrote {} figures to {}", parts.len(), dir.display());
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn generator_is_deterministic() {
        let a: Vec<String> = feynman().iter().map(|p| render(p).unwrap()).collect();
        let b: Vec<String> = feynman().iter().map(|p| render(p).unwrap()).collect();
        assert_eq!(a, b);
        assert!(a.len() >= 20);
    }

    #[test]
    fn names_are_unique_and_plain() {
        let parts = feynman();
        let mut names: Vec<&str> = parts.iter().map(|p| p.name.as_str()).collect();
        names.sort_unstable();
        names.dedup();
        assert_eq!(names.len(), parts.len());
        for n in names {
            assert!(n.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'), "{n}");
        }
    }

    #[test]
    fn every_figure_reads_back_as_one_compound_with_a_comment() {
        for part in feynman() {
            let text = render(&part).unwrap();
            let doc = rustfig_format::read_any(text.as_bytes()).unwrap();
            assert_eq!(doc.objects.len(), 1, "{}", part.name);
            let o = &doc.objects[0];
            assert!(matches!(o.kind, ObjectKind::Compound(_)), "{}", part.name);
            assert!(
                o.comments.first().is_some_and(|c| c.starts_with("Feynman diagram")),
                "{}",
                part.name
            );
        }
    }

    #[test]
    fn part_ends_lie_on_the_grid() {
        for part in feynman() {
            for e in &part.ends {
                assert!(
                    (e.x / GRID).fract() == 0.0 && (e.y / GRID).fract() == 0.0,
                    "{} {e:?}",
                    part.name
                );
            }
        }
    }

    #[test]
    fn committed_files_match_the_generator() {
        let dir = crate::root().join("libraries").join("Feynman");
        for part in feynman() {
            let path = dir.join(format!("{}.fig", part.name));
            let on_disk = std::fs::read_to_string(&path).unwrap_or_default();
            assert_eq!(
                on_disk.replace("\r\n", "\n"),
                render(&part).unwrap(),
                "{} is stale: run cargo xtask libraries",
                path.display()
            );
        }
    }

    #[test]
    fn quantum_circuits_are_deterministic_and_plain() {
        let a: Vec<String> = quantum_circuits().iter().map(|p| render(p).unwrap()).collect();
        let b: Vec<String> = quantum_circuits().iter().map(|p| render(p).unwrap()).collect();
        assert_eq!(a, b);
        assert!(a.len() > 25);
        let parts = quantum_circuits();
        let mut names: Vec<&str> = parts.iter().map(|p| p.name.as_str()).collect();
        names.sort_unstable();
        names.dedup();
        assert_eq!(names.len(), parts.len());
        assert!(names
            .iter()
            .all(|n| n.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')));
    }

    #[test]
    fn quantum_figures_read_back_as_one_compound_with_a_comment() {
        for part in quantum_circuits() {
            let text = render(&part).unwrap();
            let doc = rustfig_format::read_any(text.as_bytes()).unwrap();
            assert_eq!(doc.objects.len(), 1, "{}", part.name);
            let o = &doc.objects[0];
            assert!(matches!(o.kind, ObjectKind::Compound(_)), "{}", part.name);
            assert!(
                o.comments.first().is_some_and(|c| c.starts_with("Quantum circuit")),
                "{}",
                part.name
            );
            assert!(o.comments.iter().any(|c| c.contains("Keywords: quantum circuit")));
        }
    }

    #[test]
    fn quantum_part_ends_lie_on_the_quarter_inch_grid() {
        for part in quantum_circuits() {
            for e in &part.ends {
                assert!(
                    (e.x / 300.0).fract() == 0.0 && (e.y / 300.0).fract() == 0.0,
                    "{} {e:?}",
                    part.name
                );
            }
        }
    }

    #[test]
    fn committed_quantum_files_match_the_generator() {
        let dir = crate::root().join("libraries").join("QuantumCircuits");
        for part in quantum_circuits() {
            let path = dir.join(format!("{}.fig", part.name));
            let on_disk = std::fs::read_to_string(&path).unwrap_or_default();
            assert_eq!(
                on_disk.replace("\r\n", "\n"),
                render(&part).unwrap(),
                "{} is stale: run cargo xtask libraries",
                path.display()
            );
        }
    }

    #[test]
    fn optics_lab_is_deterministic_and_plain() {
        let a: Vec<String> = optics_lab().iter().map(|p| render(p).unwrap()).collect();
        let b: Vec<String> = optics_lab().iter().map(|p| render(p).unwrap()).collect();
        assert_eq!(a, b);
        assert!(a.len() > 40);
        let parts = optics_lab();
        let mut names: Vec<&str> = parts.iter().map(|p| p.name.as_str()).collect();
        names.sort_unstable();
        names.dedup();
        assert_eq!(names.len(), parts.len());
        assert!(names
            .iter()
            .all(|n| n.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')));
    }

    #[test]
    fn optics_lab_figures_read_back_as_one_compound_with_a_comment() {
        for part in optics_lab() {
            let text = render(&part).unwrap();
            let doc = rustfig_format::read_any(text.as_bytes()).unwrap();
            assert_eq!(doc.objects.len(), 1, "{}", part.name);
            let o = &doc.objects[0];
            assert!(matches!(o.kind, ObjectKind::Compound(_)), "{}", part.name);
            assert!(
                o.comments.first().is_some_and(|c| c.starts_with("Optics lab")),
                "{}",
                part.name
            );
            assert!(o.comments.iter().any(|c| c.contains("Keywords: optics")));
        }
    }

    #[test]
    fn optics_lab_beam_ports_lie_on_the_quarter_inch_grid() {
        for part in optics_lab() {
            for e in &part.ends {
                assert!(
                    (e.x / 300.0).fract() == 0.0 && (e.y / 300.0).fract() == 0.0,
                    "{} {e:?}",
                    part.name
                );
            }
        }
    }

    #[test]
    fn committed_optics_lab_files_match_the_generator() {
        let dir = crate::root().join("libraries").join("OpticsLab");
        for part in optics_lab() {
            let path = dir.join(format!("{}.fig", part.name));
            let on_disk = std::fs::read_to_string(&path).unwrap_or_default();
            assert_eq!(
                on_disk.replace("\r\n", "\n"),
                render(&part).unwrap(),
                "{} is stale: run cargo xtask libraries",
                path.display()
            );
        }
    }

    #[test]
    fn physics_circuits_are_deterministic_and_plain() {
        let a: Vec<String> = physics_circuits().iter().map(|p| render(p).unwrap()).collect();
        let b: Vec<String> = physics_circuits().iter().map(|p| render(p).unwrap()).collect();
        assert_eq!(a, b);
        assert!(a.len() >= 30);
        let parts = physics_circuits();
        let mut names: Vec<&str> = parts.iter().map(|p| p.name.as_str()).collect();
        names.sort_unstable();
        names.dedup();
        assert_eq!(names.len(), parts.len());
        assert!(names
            .iter()
            .all(|n| n.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')));
    }

    #[test]
    fn physics_circuits_read_back_as_one_compound_with_a_comment() {
        for part in physics_circuits() {
            let text = render(&part).unwrap();
            let doc = rustfig_format::read_any(text.as_bytes()).unwrap();
            assert_eq!(doc.objects.len(), 1, "{}", part.name);
            let o = &doc.objects[0];
            assert!(matches!(o.kind, ObjectKind::Compound(_)), "{}", part.name);
            assert!(
                o.comments.first().is_some_and(|c| c.starts_with("Physics circuits")),
                "{}",
                part.name
            );
            assert!(o.comments.iter().any(|c| c.contains("Keywords: circuit")));
        }
    }

    #[test]
    fn physics_circuit_pins_lie_on_the_quarter_inch_grid() {
        for part in physics_circuits() {
            for e in &part.ends {
                assert!(
                    (e.x / 300.0).fract() == 0.0 && (e.y / 300.0).fract() == 0.0,
                    "{} {e:?}",
                    part.name
                );
            }
        }
    }

    #[test]
    fn two_terminal_parts_have_pins_one_inch_apart_and_one_orientation() {
        let parts = physics_circuits();
        for name in circuits::TWO_TERMINAL {
            let part = parts.iter().find(|p| p.name == name).unwrap();
            assert_eq!(part.ends.len(), 2, "{name}");
            let (a, b) = (part.ends[0], part.ends[1]);
            assert_eq!(a.y, b.y, "{name}");
            assert_eq!((a.x - b.x).abs(), 1200.0, "{name}");
        }
        // One orientation per part: no turned copies in the library.
        assert!(parts.iter().all(|p| !p.name.ends_with("_h") && !p.name.ends_with("_v")));
    }

    #[test]
    fn drawn_leads_reach_the_pins() {
        for part in physics_circuits() {
            if part.ends.is_empty() {
                continue;
            }
            let ObjectKind::Compound(c) = &part.object.kind else {
                panic!("{}", part.name);
            };
            let pts: Vec<Point> = c
                .objects
                .iter()
                .filter_map(|o| match &o.kind {
                    ObjectKind::Polyline(l) => Some(l.points.clone()),
                    ObjectKind::Ellipse(e) => Some(vec![e.center]),
                    _ => None,
                })
                .flatten()
                .collect();
            for e in &part.ends {
                assert!(
                    pts.iter().any(|q| (q.x - e.x).abs() < 1.5 && (q.y - e.y).abs() < 1.5),
                    "{}: nothing is drawn at pin {e:?}",
                    part.name
                );
            }
        }
    }

    #[test]
    fn committed_physics_circuit_files_match_the_generator() {
        let dir = crate::root().join("libraries").join("PhysicsCircuits");
        for part in physics_circuits() {
            let path = dir.join(format!("{}.fig", part.name));
            let on_disk = std::fs::read_to_string(&path).unwrap_or_default();
            assert_eq!(
                on_disk.replace("\r\n", "\n"),
                render(&part).unwrap(),
                "{} is stale: run cargo xtask libraries",
                path.display()
            );
        }
    }
}
