//! Graphviz DOT formatter for `Network`.

use std::fmt::Write as _;

use super::{Edges, escape, indent};
use crate::PortDirection;
use crate::interpreter::network::{Network, Node, NodeId, WalkEvent};

/// Render a `Network` as a Graphviz DOT digraph.
///
/// Leaf nodes become `record`-shaped nodes: input ports in a left-hand cell group,
/// output/inout ports in a right-hand cell group, and a `name: type` cell between —
/// a small chip with pins on either side, rather than an undifferentiated row of
/// fields. This is the one thing DOT does better than Mermaid (`format::mermaid`) and
/// is worth keeping: a real per-port anchor (`node_<id>:<port>`) that an edge attaches
/// to exactly.
///
/// Every composite — the root group included — becomes a `subgraph cluster_<id>`
/// block for visual containment, so the encompassing group's own box is visible and
/// nesting depth reads directly off the diagram. A group's boundary ports have no
/// visual identity of their own: each drawn connection is resolved through the D19
/// proxy chain to the real leaf port it ultimately reaches (see [`Edges`]) and
/// rendered as an ordinary `node_<id>:<port> -> node_<id>:<port>` edge.
pub(crate) fn render(network: &Network) -> String {
    let mut out = String::from("digraph network {\n");

    if network.root.is_some() {
        out.push_str("    rankdir=LR;\n    node [shape=record];\n\n");
        render_nodes(&mut out, network);
        render_edges(&mut out, network);
    }

    out.push_str("}\n");
    out
}

fn render_nodes(out: &mut String, network: &Network) {
    let mut depth = 1;

    for event in network.walk() {
        match event {
            WalkEvent::Enter(node) => {
                let pad = indent(depth);
                writeln!(out, "{pad}subgraph cluster_{} {{", node.id).unwrap();
                writeln!(out, "{pad}    label=\"{}\";", escape(&node.name)).unwrap();
                depth += 1;
            }
            WalkEvent::Leaf(node) => {
                writeln!(out, "{}{}", indent(depth), record(node)).unwrap();
            }
            WalkEvent::Leave(_) => {
                depth -= 1;
                writeln!(out, "{}}}", indent(depth)).unwrap();
            }
        }
    }
}

fn record(node: &Node) -> String {
    let (in_ports, out_ports): (Vec<_>, Vec<_>) = node
        .ports
        .iter()
        .partition(|p| p.direction == PortDirection::In);

    let mut fields = Vec::new();
    if !in_ports.is_empty() {
        fields.push(format!("{{{}}}", port_cells(&in_ports)));
    }
    fields.push(format!(
        "{}: {}",
        escape(&node.name),
        escape(&node.type_name)
    ));
    if !out_ports.is_empty() {
        fields.push(format!("{{{}}}", port_cells(&out_ports)));
    }

    format!("node_{} [label=\"{{{}}}\"];", node.id, fields.join(" | "))
}

fn port_cells(ports: &[&crate::Port]) -> String {
    ports
        .iter()
        .map(|p| {
            let name = escape(&p.name);
            format!("<{name}>{name}")
        })
        .collect::<Vec<_>>()
        .join(" | ")
}

fn render_edges(out: &mut String, network: &Network) {
    let edges = Edges::new(network);

    let mut wrote_any = false;
    for conn in edges.visible() {
        if !wrote_any {
            out.push('\n');
            wrote_any = true;
        }
        let (from_id, from_port) = edges.resolve(&conn.from);
        let (to_id, to_port) = edges.resolve(&conn.to);
        writeln!(
            out,
            "    {} -> {};",
            anchor(from_id, &from_port, &edges),
            anchor(to_id, &to_port, &edges)
        )
        .unwrap();
    }
}

/// DOT anchor a resolved `(NodeId, port name)` pair renders as: a real port anchor on
/// its record for a leaf (`node_<id>:<port>`); a bare reference to its `cluster_<id>`
/// for a group (defensive fallback only; see [`Edges::resolve`]; since a group never
/// gets a record of its own to anchor a port on).
fn anchor(node_id: NodeId, port_name: &str, edges: &Edges<'_>) -> String {
    if edges.is_group(node_id) {
        format!("cluster_{node_id}")
    } else {
        format!("node_{node_id}:{port_name}")
    }
}
