//! SVG visualization (spec §8). Feature-gated behind `viz`.
//!
//! `render_scene` writes a single graph + parcel set to an SVG using
//! the color conventions of §8.2. `generate_all_figures` regenerates
//! every figure listed in the journal.

pub mod svg_render;

use std::path::Path;

pub use svg_render::{render_scene, RenderOptions};

/// Regenerate every figure listed in spec §8.1, writing into
/// `out_dir`.
///
/// # Errors
///
/// Propagates IO errors (`io::Error`) and subdivision errors. The
/// IO error path is wrapped into a `Box<dyn Error>` to keep the
/// signature simple.
pub fn generate_all_figures(out_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
    use crate::{subdivide_all, RoadGraph, SubdivisionParams};
    use glam::DVec2;

    std::fs::create_dir_all(out_dir)?;

    // fig_01_grid_block: rectangular block subdivided.
    {
        let mut g = RoadGraph::new();
        let a = g.add_node(DVec2::new(0.0, 0.0));
        let b = g.add_node(DVec2::new(200.0, 0.0));
        let c = g.add_node(DVec2::new(200.0, 100.0));
        let d = g.add_node(DVec2::new(0.0, 100.0));
        g.add_road(a, b)?;
        g.add_road(b, c)?;
        g.add_road(c, d)?;
        g.add_road(d, a)?;
        g.rebuild_topology()?;
        let params = SubdivisionParams::default();
        let parcels = subdivide_all(&g, &params)?;
        let svg = render_scene(&g, &parcels, &RenderOptions::default());
        std::fs::write(out_dir.join("fig_01_grid_block.svg"), svg)?;
    }

    // fig_02_curved_road: parcels on a polyline-curved frontage. We
    // approximate "curved" by a 12-segment arc.
    {
        let mut g = RoadGraph::new();
        let arc_radius = 200.0_f64;
        let arc_segments = 8;
        let arc_half_angle = std::f64::consts::FRAC_PI_4;
        // Arc on the bottom: from (-r sin θ, -r cos θ) to (r sin θ, -r cos θ),
        // centered at (0, 0). Then a back rectangle to close the block.
        let mut arc_nodes = Vec::new();
        for i in 0..=arc_segments {
            let t = -arc_half_angle + (i as f64) * (2.0 * arc_half_angle) / (arc_segments as f64);
            let p = DVec2::new(arc_radius * t.sin(), -arc_radius * t.cos() + arc_radius);
            arc_nodes.push(g.add_node(p));
        }
        let depth = 50.0;
        let last = *arc_nodes.last().expect("non-empty");
        let first = arc_nodes[0];
        let pos_last = g.node_position(last).expect("present");
        let pos_first = g.node_position(first).expect("present");
        let back_right = g.add_node(pos_last + DVec2::new(0.0, depth));
        let back_left = g.add_node(pos_first + DVec2::new(0.0, depth));
        for i in 0..arc_segments {
            g.add_road(arc_nodes[i], arc_nodes[i + 1])?;
        }
        g.add_road(last, back_right)?;
        g.add_road(back_right, back_left)?;
        g.add_road(back_left, first)?;
        if g.rebuild_topology().is_ok() {
            let params = SubdivisionParams::default();
            if let Ok(parcels) = subdivide_all(&g, &params) {
                let svg = render_scene(&g, &parcels, &RenderOptions::default());
                std::fs::write(out_dir.join("fig_02_curved_road.svg"), svg)?;
            }
        }
    }

    // fig_04_y_intersection: three roads at 120°, surrounded by a
    // triangular outer perimeter so the three blocks are bounded.
    {
        use std::f64::consts::TAU;
        let mut g = RoadGraph::new();
        let center = g.add_node(DVec2::new(0.0, 0.0));
        let r = 90.0_f64;
        let third = TAU / 3.0;
        let p1 = g.add_node(DVec2::new(r * 0.0_f64.cos(), r * 0.0_f64.sin()));
        let p2 = g.add_node(DVec2::new(r * third.cos(), r * third.sin()));
        let p3 = g.add_node(DVec2::new(r * (2.0 * third).cos(), r * (2.0 * third).sin()));
        g.add_road(center, p1)?;
        g.add_road(center, p2)?;
        g.add_road(center, p3)?;
        g.add_road(p1, p2)?;
        g.add_road(p2, p3)?;
        g.add_road(p3, p1)?;
        g.rebuild_topology()?;
        let params = SubdivisionParams::default();
        let parcels = subdivide_all(&g, &params)?;
        let svg = render_scene(&g, &parcels, &RenderOptions::default());
        std::fs::write(out_dir.join("fig_04_y_intersection.svg"), svg)?;
    }

    // fig_06a + fig_06b: a 200×100 rectangle, before and after a
    // node move that produces a mix of edit outcomes. Post-edit
    // parcels are colored by their `EditOutcome` category.
    {
        use crate::{apply_road_edit, RoadEdit};
        use std::collections::HashMap;
        let mut g = RoadGraph::new();
        let a = g.add_node(DVec2::new(0.0, 0.0));
        let b = g.add_node(DVec2::new(200.0, 0.0));
        let c = g.add_node(DVec2::new(200.0, 100.0));
        let d = g.add_node(DVec2::new(0.0, 100.0));
        g.add_road(a, b)?;
        g.add_road(b, c)?;
        g.add_road(c, d)?;
        g.add_road(d, a)?;
        g.rebuild_topology()?;
        let params = SubdivisionParams::default();
        let mut parcels = subdivide_all(&g, &params)?;
        let svg_before = render_scene(&g, &parcels, &RenderOptions::default());
        std::fs::write(out_dir.join("fig_06a_road_edit_before.svg"), svg_before)?;

        // Move node B (the bottom-right corner) right by 8 m. That's
        // big enough to drag the bottom-edge parcels' frontages but
        // small enough that none of them get condemned.
        let pos_b = g.node_position(b).unwrap_or(DVec2::ZERO);
        let outcome = apply_road_edit(
            &mut parcels,
            &mut g,
            RoadEdit::MoveNode {
                node: b,
                to: pos_b + DVec2::new(8.0, 0.0),
            },
            &params,
        )?;

        let mut fills: HashMap<crate::ParcelId, String> = HashMap::new();
        for &pid in &outcome.deformed {
            fills.insert(pid, "#9fd99a".to_string()); // green
        }
        for &pid in &outcome.regenerated {
            fills.insert(pid, "#ffc070".to_string()); // orange
        }
        for &pid in &outcome.created {
            fills.insert(pid, "#7fc8ff".to_string()); // blue (created)
        }
        // Condemned parcels are gone; we paint the empty space red
        // by overlaying the rendering — but with the lite parcel
        // model we just note that condemned ids no longer exist in
        // `parcels`, so the rendering naturally shows nothing in
        // their place.
        let mut opts = RenderOptions::default();
        opts.parcel_fills = fills;
        let svg_after = render_scene(&g, &parcels, &opts);
        std::fs::write(out_dir.join("fig_06b_road_edit_after.svg"), svg_after)?;
    }

    // fig_03_cul_de_sac: a road approaches a polygonal bulb. Bulb
    // interior gets parcels facing each segment of the bulb; with the
    // cleanup pass these tile cleanly even though they're rectangle-
    // approximations rather than true pie slices.
    {
        use std::f64::consts::TAU;
        let mut g = RoadGraph::new();
        let bulb_radius = 60.0_f64;
        let bulb_segments = 12;
        let mut bulb_nodes = Vec::new();
        for i in 0..bulb_segments {
            let a = (i as f64 / bulb_segments as f64) * TAU;
            let p = DVec2::new(bulb_radius * a.cos(), bulb_radius * a.sin());
            bulb_nodes.push(g.add_node(p));
        }
        let approach_start = g.add_node(DVec2::new(0.0, -200.0));
        let bottom_idx = bulb_segments * 3 / 4;
        g.add_road(approach_start, bulb_nodes[bottom_idx])?;
        for i in 0..bulb_segments {
            let next = (i + 1) % bulb_segments;
            g.add_road(bulb_nodes[i], bulb_nodes[next])?;
        }
        g.rebuild_topology()?;
        let params = SubdivisionParams::default();
        let parcels = subdivide_all(&g, &params)?;
        let svg = render_scene(&g, &parcels, &RenderOptions::default());
        std::fs::write(out_dir.join("fig_03_cul_de_sac.svg"), svg)?;
    }

    // fig_05_acute_corner: a triangular block with one acute interior
    // angle (~15°). Demonstrates the bisector-clip + polygon-diff
    // cleanup at sharp corners.
    {
        let mut g = RoadGraph::new();
        let apex = g.add_node(DVec2::new(0.0, 0.0));
        let p1 = g.add_node(DVec2::new(200.0, 0.0));
        let angle = 15.0_f64.to_radians();
        let p2 = g.add_node(DVec2::new(200.0 * angle.cos(), 200.0 * angle.sin()));
        g.add_road(apex, p1)?;
        g.add_road(apex, p2)?;
        g.add_road(p1, p2)?;
        g.rebuild_topology()?;
        let params = SubdivisionParams::default();
        let parcels = subdivide_all(&g, &params)?;
        let svg = render_scene(&g, &parcels, &RenderOptions::default());
        std::fs::write(out_dir.join("fig_05_acute_corner.svg"), svg)?;
    }

    // plot_subdivision_perf: a small bar chart of µs/parcel measured
    // on a few scene sizes. Uses subdivide_all_with_stats so the
    // measurement reflects actual phase timings rather than a separate
    // microbenchmark harness.
    {
        use crate::subdivide_all_with_stats;
        let scenes = [(1_usize, 200.0_f64, 100.0_f64), (5, 1000.0, 100.0), (25, 1000.0, 500.0)];
        let mut bars: Vec<(String, f64)> = Vec::new();
        for &(per_side, w, h) in &scenes {
            let mut g = RoadGraph::new();
            for i in 0..per_side {
                for j in 0..per_side {
                    let x0 = (i as f64) * (w / per_side as f64) + 1.0;
                    let y0 = (j as f64) * (h / per_side as f64) + 1.0;
                    let dx = w / per_side as f64 - 2.0;
                    let dy = h / per_side as f64 - 2.0;
                    let a = g.add_node(DVec2::new(x0, y0));
                    let b = g.add_node(DVec2::new(x0 + dx, y0));
                    let c = g.add_node(DVec2::new(x0 + dx, y0 + dy));
                    let d = g.add_node(DVec2::new(x0, y0 + dy));
                    let _ = g.add_road(a, b);
                    let _ = g.add_road(b, c);
                    let _ = g.add_road(c, d);
                    let _ = g.add_road(d, a);
                }
            }
            g.rebuild_topology()?;
            let params = SubdivisionParams::default();
            // Warm up to ride out first-call cache misses.
            for _ in 0..3 {
                let _ = subdivide_all_with_stats(&g, &params);
            }
            let (_, stats) = subdivide_all_with_stats(&g, &params)?;
            let label = format!("{}x{} blocks", per_side, per_side);
            bars.push((label, stats.time_per_parcel_us()));
        }
        let svg = render_bar_chart("µs / parcel", &bars);
        std::fs::write(out_dir.join("plot_subdivision_perf.svg"), svg)?;
    }

    // plot_parcel_area_hist: histogram of parcel areas from a stress
    // scene. Bucket by 50-m² bins. We vary block dimensions across the
    // grid so the histogram actually shows a distribution rather than
    // a single delta at uniform-block-area.
    {
        use rand::SeedableRng;
        use rand::Rng;
        let per_side = 12_usize;
        let mut g = RoadGraph::new();
        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(42);
        let mut x_lines = vec![0.0_f64];
        for _ in 0..per_side {
            let dx = 60.0 + rng.gen::<f64>() * 90.0;
            let next = *x_lines.last().unwrap() + dx;
            x_lines.push(next);
        }
        let mut y_lines = vec![0.0_f64];
        for _ in 0..per_side {
            let dy = 60.0 + rng.gen::<f64>() * 90.0;
            let next = *y_lines.last().unwrap() + dy;
            y_lines.push(next);
        }
        for i in 0..per_side {
            for j in 0..per_side {
                let x0 = x_lines[i] + 1.0;
                let y0 = y_lines[j] + 1.0;
                let x1 = x_lines[i + 1] - 1.0;
                let y1 = y_lines[j + 1] - 1.0;
                let a = g.add_node(DVec2::new(x0, y0));
                let b = g.add_node(DVec2::new(x1, y0));
                let c = g.add_node(DVec2::new(x1, y1));
                let d = g.add_node(DVec2::new(x0, y1));
                let _ = g.add_road(a, b);
                let _ = g.add_road(b, c);
                let _ = g.add_road(c, d);
                let _ = g.add_road(d, a);
            }
        }
        g.rebuild_topology()?;
        let mut params = SubdivisionParams::default();
        params.depth_variance = 8.0;
        params.frontage_variance = 6.0;
        let parcels = subdivide_all(&g, &params)?;
        let mut areas: Vec<f64> = parcels.iter().map(|(_, p)| p.area()).collect();
        areas.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
        let bin_size = 50.0;
        let bin_count = 20;
        let mut hist = vec![0_usize; bin_count];
        for a in &areas {
            let idx = ((a / bin_size) as usize).min(bin_count - 1);
            hist[idx] += 1;
        }
        let bars: Vec<(String, f64)> = hist
            .iter()
            .enumerate()
            .map(|(i, &c)| {
                let lo = (i as f64) * bin_size;
                let hi = lo + bin_size;
                (format!("{:.0}-{:.0} m²", lo, hi), c as f64)
            })
            .collect();
        let svg = render_bar_chart(
            &format!("Parcel area histogram ({} parcels)", areas.len()),
            &bars,
        );
        std::fs::write(out_dir.join("plot_parcel_area_hist.svg"), svg)?;
    }

    // fig_07_regularity_slider: a Y intersection at ρ ∈ {0, 0.5, 1.0}.
    // We use the Y instead of the rectangle because the rectangle's
    // parcels are already axis-aligned rectangles and OBB regularization
    // is a no-op for them. The Y's bisector-clipped parcels at acute
    // corners are 5+-vertex shapes that visibly snap toward rectangles
    // as ρ increases.
    {
        use std::f64::consts::TAU;
        let mut g = RoadGraph::new();
        let center = g.add_node(DVec2::new(0.0, 0.0));
        let r = 100.0_f64;
        let third = TAU / 3.0;
        let p1 = g.add_node(DVec2::new(r * 0.0_f64.cos(), r * 0.0_f64.sin()));
        let p2 = g.add_node(DVec2::new(r * third.cos(), r * third.sin()));
        let p3 = g.add_node(DVec2::new(r * (2.0 * third).cos(), r * (2.0 * third).sin()));
        g.add_road(center, p1)?;
        g.add_road(center, p2)?;
        g.add_road(center, p3)?;
        g.add_road(p1, p2)?;
        g.add_road(p2, p3)?;
        g.add_road(p3, p1)?;
        g.rebuild_topology()?;
        for (rho, suffix) in [(0.0, "rho_0_0"), (0.5, "rho_0_5"), (1.0, "rho_1_0")] {
            let params = SubdivisionParams {
                regularity: rho,
                ..SubdivisionParams::default()
            };
            let parcels = subdivide_all(&g, &params)?;
            let svg = render_scene(&g, &parcels, &RenderOptions::default());
            std::fs::write(
                out_dir.join(format!("fig_07_regularity_slider_{suffix}.svg")),
                svg,
            )?;
        }
    }

    Ok(())
}

/// Render a small horizontal bar chart to SVG. Each bar shows
/// `(label, value)`; the label runs to the left of the bar, the
/// value is annotated at the right of the bar.
fn render_bar_chart(title: &str, bars: &[(String, f64)]) -> String {
    let max_value: f64 = bars
        .iter()
        .map(|(_, v)| *v)
        .fold(f64::MIN, f64::max)
        .max(1e-9);
    let bar_h = 24.0_f64;
    let bar_gap = 6.0_f64;
    let label_width = 130.0_f64;
    let chart_width = 360.0_f64;
    let value_padding = 60.0_f64;
    let total_width = label_width + chart_width + value_padding + 20.0;
    let total_height =
        40.0 + (bars.len() as f64) * (bar_h + bar_gap) + 20.0;
    let mut svg = String::new();
    svg.push_str(&format!(
        "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"{}\" viewBox=\"0 0 {} {}\">",
        total_width, total_height, total_width, total_height
    ));
    svg.push_str(&format!(
        "<text x=\"10\" y=\"24\" font-family=\"sans-serif\" font-size=\"14\" font-weight=\"bold\">{}</text>",
        xml_escape(title)
    ));
    for (i, (label, value)) in bars.iter().enumerate() {
        let y = 40.0 + (i as f64) * (bar_h + bar_gap);
        let bar_len = (value / max_value) * chart_width;
        svg.push_str(&format!(
            "<text x=\"{}\" y=\"{}\" font-family=\"sans-serif\" font-size=\"11\" text-anchor=\"end\">{}</text>",
            label_width - 4.0,
            y + bar_h * 0.7,
            xml_escape(label)
        ));
        svg.push_str(&format!(
            "<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" fill=\"#3a78c2\" />",
            label_width, y, bar_len.max(0.5), bar_h
        ));
        svg.push_str(&format!(
            "<text x=\"{}\" y=\"{}\" font-family=\"sans-serif\" font-size=\"11\">{:.2}</text>",
            label_width + bar_len + 6.0,
            y + bar_h * 0.7,
            value
        ));
    }
    svg.push_str("</svg>");
    svg
}

fn xml_escape(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
}
