//! SVG rendering of a graph + parcel set.

use std::collections::HashMap;

use glam::DVec2;
use svg::node::element::path::Data;
use svg::node::element::{Group, Line, Path as SvgPath, Rectangle};
use svg::Document;

use crate::network::RoadGraph;
use crate::parcel::{EdgeKind, ParcelId, ParcelSet};

/// Knobs for the SVG renderer.
#[derive(Debug, Clone)]
pub struct RenderOptions {
    /// Pixels per meter.
    pub pixels_per_meter: f64,
    /// Padding around the scene, meters.
    pub margin_m: f64,
    /// Whether to draw vertex dots on roads.
    pub draw_nodes: bool,
    /// Per-parcel fill overrides — keyed by [`ParcelId`], values are
    /// SVG-compatible color strings. Used by the post-edit figures
    /// to paint each parcel by its [`EditOutcome`] category.
    pub parcel_fills: HashMap<ParcelId, String>,
}

impl Default for RenderOptions {
    fn default() -> Self {
        Self {
            pixels_per_meter: 4.0,
            margin_m: 5.0,
            draw_nodes: true,
            parcel_fills: HashMap::new(),
        }
    }
}

/// Render the whole scene to an SVG `String`.
#[must_use]
pub fn render_scene(graph: &RoadGraph, parcels: &ParcelSet, opts: &RenderOptions) -> String {
    let bbox = compute_bbox(graph, parcels);
    let (min, max) = bbox;
    let m = opts.margin_m;
    let world_w = (max.x - min.x) + 2.0 * m;
    let world_h = (max.y - min.y) + 2.0 * m;
    let px_w = world_w * opts.pixels_per_meter;
    let px_h = world_h * opts.pixels_per_meter;

    // World y is up, SVG y is down. Flip and translate via viewBox so
    // we can write coordinates in world units throughout.
    let view_min_x = min.x - m;
    let view_min_y = min.y - m;
    let view_w = world_w;
    let view_h = world_h;

    // We use an inverted y by setting viewBox to a flipped rectangle,
    // then a transform on the root group that applies (1, -1) and a
    // y-translation.
    let view_box = format!("{view_min_x} {view_min_y} {view_w} {view_h}");

    let bg = Rectangle::new()
        .set("x", view_min_x)
        .set("y", view_min_y)
        .set("width", view_w)
        .set("height", view_h)
        .set("fill", "#ffffff");

    // Group with y-flip to put world +y up.
    let flip = Group::new().set(
        "transform",
        format!("translate(0, {}) scale(1, -1)", 2.0 * view_min_y + view_h),
    );

    // Draw roads first (so frontage edges of parcels paint on top of
    // the road stroke and stay visible), then parcels, then nodes.
    let roads_layer = roads_group(graph);
    let parcels_layer = parcels_group(parcels, &opts.parcel_fills);
    let nodes_layer = if opts.draw_nodes {
        nodes_group(graph)
    } else {
        Group::new()
    };

    let flip = flip.add(roads_layer).add(parcels_layer).add(nodes_layer);

    Document::new()
        .set("viewBox", view_box)
        .set("width", px_w)
        .set("height", px_h)
        .set("xmlns", "http://www.w3.org/2000/svg")
        .add(bg)
        .add(flip)
        .to_string()
}

fn compute_bbox(graph: &RoadGraph, parcels: &ParcelSet) -> (DVec2, DVec2) {
    let mut min = DVec2::splat(f64::INFINITY);
    let mut max = DVec2::splat(f64::NEG_INFINITY);
    let mut update = |p: DVec2| {
        if p.x < min.x {
            min.x = p.x;
        }
        if p.y < min.y {
            min.y = p.y;
        }
        if p.x > max.x {
            max.x = p.x;
        }
        if p.y > max.y {
            max.y = p.y;
        }
    };
    for (_, _, _) in graph.road_endpoints() {
        // road_endpoints gives us ids; iterate nodes instead.
    }
    for nid in graph_node_ids(graph) {
        if let Some(p) = graph.node_position(nid) {
            update(p);
        }
    }
    for (_, parcel) in parcels.iter() {
        for &v in parcel.vertices() {
            update(v);
        }
    }
    if !min.x.is_finite() {
        // No content — fall back to a unit square.
        return (DVec2::ZERO, DVec2::splat(1.0));
    }
    (min, max)
}

fn graph_node_ids(graph: &RoadGraph) -> Vec<crate::network::NodeId> {
    let mut seen = Vec::new();
    for (_, a, b) in graph.road_endpoints() {
        seen.push(a);
        seen.push(b);
    }
    seen.sort_by_key(|n| format!("{n:?}"));
    seen.dedup();
    seen
}

fn parcels_group(parcels: &ParcelSet, overrides: &HashMap<ParcelId, String>) -> Group {
    let mut group = Group::new().set("id", "parcels");
    for (id, parcel) in parcels.iter() {
        let verts = parcel.vertices();
        if verts.is_empty() {
            continue;
        }
        let mut data = Data::new().move_to((verts[0].x, verts[0].y));
        for v in &verts[1..] {
            data = data.line_to((v.x, v.y));
        }
        data = data.close();
        let (fill_color, fill_opacity) = match overrides.get(&id) {
            Some(c) => (c.clone(), 0.55_f64),
            None => ("#fff7c2".to_string(), 0.3_f64),
        };
        let fill = SvgPath::new()
            .set("d", data)
            .set("fill", fill_color)
            .set("fill-opacity", fill_opacity)
            .set("stroke", "none");
        group = group.add(fill);

        let _ = id;
        // Stroke each edge by its kind.
        for (i, kind) in parcel.edge_kinds().iter().enumerate() {
            let a = verts[i];
            let b = verts[(i + 1) % verts.len()];
            let (color, dashed, width) = match kind {
                EdgeKind::Frontage => ("#1f5fb4", false, 0.9_f64),
                EdgeKind::Side => ("#6b6b6b", false, 0.4_f64),
                EdgeKind::Back => ("#bdbdbd", true, 0.4_f64),
            };
            let mut line = Line::new()
                .set("x1", a.x)
                .set("y1", a.y)
                .set("x2", b.x)
                .set("y2", b.y)
                .set("stroke", color)
                .set("stroke-width", width);
            if dashed {
                line = line.set("stroke-dasharray", "1.6,1.2");
            }
            group = group.add(line);
        }
    }
    group
}

fn roads_group(graph: &RoadGraph) -> Group {
    let mut group = Group::new().set("id", "roads");
    for (_, a, b) in graph.road_endpoints() {
        let pa = graph.node_position(a);
        let pb = graph.node_position(b);
        if let (Some(pa), Some(pb)) = (pa, pb) {
            let line = Line::new()
                .set("x1", pa.x)
                .set("y1", pa.y)
                .set("x2", pb.x)
                .set("y2", pb.y)
                .set("stroke", "#000000")
                .set("stroke-width", 0.6_f64)
                .set("stroke-linecap", "round");
            group = group.add(line);
        }
    }
    group
}

fn nodes_group(graph: &RoadGraph) -> Group {
    use svg::node::element::Circle;
    let mut group = Group::new().set("id", "nodes");
    for nid in graph_node_ids(graph) {
        if let Some(p) = graph.node_position(nid) {
            let c = Circle::new()
                .set("cx", p.x)
                .set("cy", p.y)
                .set("r", 0.7_f64)
                .set("fill", "#000000");
            group = group.add(c);
        }
    }
    group
}
