// arbor-core — core types for Arbor diagrams.
//
// Idiomatic El: everything is a Map. Functions take/return maps; helpers are
// pure and small. The downstream vessels (parse, layout, render) consume the
// shapes defined here.
//
// Shape vocabulary:
//   ArborShape strings — "rect" "rounded" "cylinder" "diamond" "stadium" "primary"
//
// Edge-kind strings:
//   "solid" "dashed" "forbidden" "bidirectional"
//
// Node value:    { "id":Str, "label":Str, "shape":Str }
// Edge value:    { "from":Str, "to":Str, "label":Str, "kind":Str }
// Group value:   { "id":Str, "label":Str, "node_ids":[Str], "direction":Str }
// Graph value:   { "title":Str, "direction":Str, "nodes":[Node], "edges":[Edge], "groups":[Group] }
//
// Diagram-form (lowered) is the same shape but with NodeStyle/EdgeLine/Arrow
// resolved into renderer-friendly fields:
//   Node:  + "sublabel":Str, "style_fill":Str, "style_stroke":Str, "style_color":Str
//   Edge:  + "line":Str ("solid"/"dashed"/"dotted"/"thick"), "arrow":Str ("forward"/"backward"/"both"/"none")
//
// This file is the canonical definition of those shapes. Other vessels rely on
// these field names.

// ── NodeId sanitisation ──────────────────────────────────────────────────────
//
// Sanitise an arbitrary string into a Mermaid-safe identifier.
//   - any char not in [a-zA-Z0-9_] becomes '_'
//   - consecutive underscores collapse
//   - trailing underscores stripped
//   - if first char is a digit, prepend 'n'
//   - if empty, return "node"

fn is_alnum_underscore(ch: String) -> Bool {
    let code: Int = str_char_code(ch, 0)
    if code >= 48 {
        if code <= 57 { return true }
    }
    if code >= 65 {
        if code <= 90 { return true }
    }
    if code >= 97 {
        if code <= 122 { return true }
    }
    if code == 95 { return true }
    false
}

fn is_ascii_digit(ch: String) -> Bool {
    let code: Int = str_char_code(ch, 0)
    if code >= 48 {
        if code <= 57 { return true }
    }
    false
}

fn sanitize_id(s: String) -> String {
    let n: Int = str_len(s)
    if n == 0 { return "node" }

    // Pass 1: replace and collapse.
    let out = ""
    let prev_underscore = false
    let i = 0
    while i < n {
        let ch: String = str_char_at(s, i)
        if is_alnum_underscore(ch) {
            let out = out + ch
            let prev_underscore = false
        } else {
            if !prev_underscore {
                let out = out + "_"
            }
            let prev_underscore = true
        }
        let i = i + 1
    }

    // Pass 2: strip trailing underscores.
    let m: Int = str_len(out)
    let end = m
    let stripping = true
    while stripping {
        if end <= 0 {
            let stripping = false
        } else {
            let last: String = str_char_at(out, end - 1)
            if last == "_" {
                let end = end - 1
            } else {
                let stripping = false
            }
        }
    }
    let out = str_slice(out, 0, end)

    if str_len(out) == 0 { return "node" }

    // Pass 3: leading-digit guard.
    let first: String = str_char_at(out, 0)
    if is_ascii_digit(first) {
        let out = "n" + out
    }
    out
}

// ── Constructors ──────────────────────────────────────────────────────────────

fn make_node(id: String, label: String, shape: String) -> Map<String, Any> {
    { "id": id, "label": label, "shape": shape }
}

fn make_edge(src: String, dst: String, kind: String) -> Map<String, Any> {
    { "from": src, "to": dst, "label": "", "kind": kind }
}

fn make_edge_with_label(src: String, dst: String, kind: String, label: String) -> Map<String, Any> {
    { "from": src, "to": dst, "label": label, "kind": kind }
}

fn make_group(id: String, label: String) -> Map<String, Any> {
    let empty_ids: [String] = el_list_empty()
    { "id": id, "label": label, "node_ids": empty_ids, "direction": "" }
}

fn make_graph() -> Map<String, Any> {
    let empty_n: [Map<String, Any>] = el_list_empty()
    let empty_e: [Map<String, Any>] = el_list_empty()
    let empty_g: [Map<String, Any>] = el_list_empty()
    { "title": "", "direction": "top-down",
      "nodes": empty_n, "edges": empty_e, "groups": empty_g }
}

// ── Shape vocabulary ──────────────────────────────────────────────────────────
// Returns the canonical shape string for a token, or "" if unknown.

fn shape_from_token(tok: String) -> String {
    let t: String = str_trim(tok)
    if t == "rect" { return "rect" }
    if t == "rounded" { return "rounded" }
    if t == "cylinder" { return "cylinder" }
    if t == "diamond" { return "diamond" }
    if t == "stadium" { return "stadium" }
    if t == "primary" { return "primary" }
    ""
}

// Lower an Arbor shape into the renderer's NodeShape vocabulary.
fn shape_to_node_shape(shape: String) -> String {
    if shape == "rect" { return "rectangle" }
    if shape == "primary" { return "rectangle" }
    if shape == "rounded" { return "rounded_rect" }
    if shape == "cylinder" { return "cylinder" }
    if shape == "diamond" { return "diamond" }
    if shape == "stadium" { return "stadium" }
    "rectangle"
}

// ── Lowering: ArborGraph → DiagramGraph ──────────────────────────────────────
//
// Replaces every node with a diagram-form node carrying explicit style fields,
// and every edge with a diagram-form edge carrying line/arrow strings.

fn lower_node(n: Map<String, Any>) -> Map<String, Any> {
    let shape: String = n["shape"]
    let node_shape: String = shape_to_node_shape(shape)
    let fill = ""
    let stroke = ""
    let color = ""
    if shape == "primary" {
        let fill = "#0052A0"
        let stroke = "#0052A0"
        let color = "#ffffff"
    }
    { "id": n["id"], "label": n["label"], "sublabel": "",
      "shape": node_shape,
      "style_fill": fill, "style_stroke": stroke, "style_color": color }
}

fn lower_edge(e: Map<String, Any>) -> Map<String, Any> {
    let kind: String = e["kind"]
    let line = "solid"
    let arrow = "forward"
    if kind == "dashed" {
        let line = "dashed"
    }
    if kind == "bidirectional" {
        let arrow = "both"
    }
    // forbidden uses solid line + forward arrow; the renderer overlays the
    // circle-X marker based on a forbidden-set the caller threads through.
    { "from": e["from"], "to": e["to"], "label": e["label"],
      "line": line, "arrow": arrow }
}

fn lower_graph(g: Map<String, Any>) -> Map<String, Any> {
    let nodes: [Map<String, Any>] = g["nodes"]
    let edges: [Map<String, Any>] = g["edges"]
    let lowered_nodes: [Map<String, Any>] = el_list_empty()
    let i = 0
    let n: Int = el_list_len(nodes)
    while i < n {
        let lowered_nodes = native_list_append(lowered_nodes, lower_node(get(nodes, i)))
        let i = i + 1
    }
    let lowered_edges: [Map<String, Any>] = el_list_empty()
    let i = 0
    let m: Int = el_list_len(edges)
    while i < m {
        let lowered_edges = native_list_append(lowered_edges, lower_edge(get(edges, i)))
        let i = i + 1
    }
    { "title": g["title"], "direction": g["direction"],
      "nodes": lowered_nodes, "edges": lowered_edges, "groups": g["groups"] }
}

// Find a node by id within a (lowered or raw) graph. Returns an empty map
// when not found — callers check map_get(result, "id") for presence.
fn graph_find_node(graph: Map<String, Any>, id: String) -> Map<String, Any> {
    let nodes: [Map<String, Any>] = graph["nodes"]
    let n: Int = el_list_len(nodes)
    let i = 0
    while i < n {
        let node: Map<String, Any> = get(nodes, i)
        let nid: String = node["id"]
        if nid == id { return node }
        let i = i + 1
    }
    let empty: Map<String, Any> = el_map_new(0)
    empty
}

// ── Forbidden-edge set helpers ────────────────────────────────────────────────
// The lowered graph drops the "forbidden" kind (line/arrow have no slot for
// it). Callers preserve the set as a list of "from->to" strings.

fn forbidden_key(from: String, to: String) -> String {
    from + "->" + to
}

fn collect_forbidden(graph: Map<String, Any>) -> [String] {
    let edges: [Map<String, Any>] = graph["edges"]
    let n: Int = el_list_len(edges)
    let out: [String] = el_list_empty()
    let i = 0
    while i < n {
        let e: Map<String, Any> = get(edges, i)
        let kind: String = e["kind"]
        if kind == "forbidden" {
            let f: String = e["from"]
            let t: String = e["to"]
            let out = native_list_append(out, forbidden_key(f, t))
        }
        let i = i + 1
    }
    out
}

fn forbidden_contains(set: [String], src: String, dst: String) -> Bool {
    let key: String = forbidden_key(src, dst)
    let n: Int = el_list_len(set)
    let i = 0
    while i < n {
        let s: String = get(set, i)
        if s == key { return true }
        let i = i + 1
    }
    false
}

// ── Smoke test ────────────────────────────────────────────────────────────────
//
// State is kept in process-local k/v storage so we never mix Int + Call or
// Int + Ident in `+` (which the codegen heuristic emits as string concat
// on tagged-pointer values, segfaulting on Int operands).

fn fail(label: String, got: String, want: String) -> Int {
    println("FAIL " + label + " got=[" + got + "] want=[" + want + "]")
    state_set("failures", "1")
    0
}

fn check_eq(label: String, got: String, want: String) -> Int {
    if got == want {
        println("ok   " + label + " = " + got)
        return 1
    }
    fail(label, got, want)
}

check_eq("sanitize crates/nc-core",
    sanitize_id("crates/nc-core"), "crates_nc_core")

check_eq("sanitize package.json",
    sanitize_id("package.json"), "package_json")

check_eq("sanitize 42-module",
    sanitize_id("42-module"), "n42_module")

check_eq("sanitize empty", sanitize_id(""), "node")

check_eq("sanitize !!--@@", sanitize_id("!!--@@"), "node")

check_eq("shape_from_token rounded",
    shape_from_token("rounded"), "rounded")

check_eq("shape_to_node_shape primary",
    shape_to_node_shape("primary"), "rectangle")

// Lowering preserves a node id and adds style.
let n: Map<String, Any> = make_node("svc", "Service", "primary")
let ln: Map<String, Any> = lower_node(n)
check_eq("lower preserves id", ln["id"], "svc")
check_eq("lower applies primary fill", ln["style_fill"], "#0052A0")

// Edge lowering
let e: Map<String, Any> = make_edge("a", "b", "dashed")
let le: Map<String, Any> = lower_edge(e)
check_eq("lower edge dashed line", le["line"], "dashed")

let e2: Map<String, Any> = make_edge("a", "b", "bidirectional")
let le2: Map<String, Any> = lower_edge(e2)
check_eq("lower edge bidirectional arrow", le2["arrow"], "both")

println("")
let failures: String = state_get("failures")
if str_eq(failures, "1") {
    println("arbor-core: FAILED")
    exit_program(1)
} else {
    println("arbor-core: ok")
}
