//! Graph-format exporters for [`Network`](crate::Network).

use std::collections::HashMap;

use crate::interpreter::network::{Connection, Network, Node, NodeId, PortRef};

pub(crate) mod dot;
pub(crate) mod mermaid;
pub(crate) mod plantuml;

/// Escape a string for embedding in a double-quoted DOT/`PlantUML` string literal (both use
/// backslash-escaped `"`/`\`, and support literal `\n` for line breaks). Mermaid does **not**
/// share this convention; it has no backslash-escaping inside quoted labels, so `format::mermaid`
/// defines its own `escape` using Mermaid's HTML-entity form instead.
fn escape(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "\\n")
}

/// One four-space indentation step per nesting level.
fn indent(depth: usize) -> String {
    "    ".repeat(depth)
}

/// A specific port on a specific node, identified by node id and port name.
type PortKey = (NodeId, String);

fn key(port_ref: &PortRef) -> PortKey {
    (port_ref.node_id, port_ref.port_name.clone())
}

/// Shared edge-resolution logic for the graph formatters.
///
/// A group's boundary ports have no visual identity in any output format. Each declared
/// boundary port shows up in the network as a D19 proxy pair (see `README_DSL.md`): an
/// *internal* connection between the group's own port and the member port it forwards
/// to, plus whatever *external* connections cross the group's boundary through it. The
/// internal half carries no information a reader needs; the group's box already implies
/// its members connect somewhere inside it; so [`Edges::visible`] filters it out, and
/// [`Edges::resolve`] follows the proxy chain so each drawn edge terminates at the real
/// leaf port behind the boundary, not at the group's box.
struct Edges<'a> {
    connections: &'a [Connection],
    index: HashMap<NodeId, &'a Node>,
    paths: HashMap<NodeId, String>,
    /// Maps a group's boundary port to the internal endpoint it proxies to (one hop;
    /// [`Edges::resolve`] follows multi-level chains down to a leaf).
    proxies: HashMap<PortKey, PortKey>,
}

impl<'a> Edges<'a> {
    fn new(network: &'a Network) -> Self {
        let mut edges = Self {
            connections: &network.connections,
            index: network.node_index(),
            paths: network.node_paths(),
            proxies: HashMap::new(),
        };

        for conn in edges.connections {
            if edges.is_group_ancestor_of(conn.from.node_id, conn.to.node_id) {
                edges.proxies.insert(key(&conn.from), key(&conn.to));
            } else if edges.is_group_ancestor_of(conn.to.node_id, conn.from.node_id) {
                edges.proxies.insert(key(&conn.to), key(&conn.from));
            }
        }

        edges
    }

    /// The connections worth drawing: everything except the internal half of each
    /// boundary port's proxy pair (a connection whose one endpoint is a group and whose
    /// other endpoint is that group's own descendant).
    fn visible(&self) -> impl Iterator<Item = &'a Connection> {
        self.connections.iter().filter(|conn| {
            !self.is_group_ancestor_of(conn.from.node_id, conn.to.node_id)
                && !self.is_group_ancestor_of(conn.to.node_id, conn.from.node_id)
        })
    }

    /// Follow a group boundary port's proxy chain down to the real port it ultimately
    /// connects to inside the group: a leaf's port, or (defensively, for a hand-built
    /// `Network` that doesn't uphold "every declared port has an internal proxy
    /// connection") a group's own port with no recorded proxy, in which case resolution
    /// just stops there.
    fn resolve(&self, endpoint: &PortRef) -> PortKey {
        let mut current = key(endpoint);

        // Bounded by the network's actual nesting depth in practice; this guards a
        // malformed hand-built `Network` with a cyclic proxy chain from looping forever.
        for _ in 0..64 {
            if !self.is_group(current.0) {
                break;
            }
            match self.proxies.get(&current) {
                Some(next) => current = next.clone(),
                None => break,
            }
        }

        current
    }

    /// True when `node_id` is a composite (a group or the synthetic array root).
    fn is_group(&self, node_id: NodeId) -> bool {
        self.index
            .get(&node_id)
            .is_some_and(|n| !n.children.is_empty())
    }

    fn is_group_ancestor_of(&self, group_id: NodeId, other_id: NodeId) -> bool {
        self.is_group(group_id)
            && match (self.paths.get(&other_id), self.paths.get(&group_id)) {
                (Some(other_path), Some(group_path)) => {
                    other_path.starts_with(&format!("{group_path}/"))
                }
                _ => false,
            }
    }
}
