// arbor-layout — hierarchical layout for diagram graphs.
//
// Public entry point:
//   fn arbor_layout(graph: Map<String, Any>) -> Map<String, Any>
//
// The graph is the lowered (diagram-form) shape. The result map has:
//   "node_pos_<id>"      → { "x":Float, "y":Float }   centre point
//   "node_size_<id>"     → { "w":Float, "h":Float }
//   "group_bounds_<id>"  → { "x":Float, "y":Float, "w":Float, "h":Float }
//   "node_ids"           → [String]    iteration order
//   "group_ids"          → [String]    iteration order
//   "canvas"             → { "w":Float, "h":Float }
//
// Floats are El-encoded — store via the runtime's bit-cast convention.
// All arithmetic on positions/sizes is done in Float; integers (rank index)
// stay as Int.
//
// Algorithm (simplified Sugiyama):
//   1. Assign ranks via topological propagation (longest path from sources).
//   2. Group nodes by rank, preserving declaration order.
//   3. Position each rank as a row (top-down/bottom-up) or column (LR/RL).
//   4. Compute group bounding boxes from member positions.
//   5. Compute canvas size to enclose everything.
//
// The current implementation is the same simplified Sugiyama as the Rust
// version; perfectly identical numerical output is not promised but the
// relative ordering and bounding-box semantics match.

// ── Spacing constants (declared as float-bit-cast helpers) ──────────────────

fn k_node_base_w() -> el_val_t { int_to_float(120) }
fn k_node_base_h() -> el_val_t { int_to_float(40) }
fn k_node_char_extra() -> el_val_t { int_to_float(8) }
fn k_h_gap() -> el_val_t { int_to_float(60) }
fn k_v_gap() -> el_val_t { int_to_float(80) }
fn k_group_pad() -> el_val_t { int_to_float(20) }
fn k_margin() -> el_val_t { int_to_float(40) }

// Float-aware max/min via int_to_float / float arithmetic — but el_max
// works in raw int comparison space, so we bit-cast carefully.
// For our purposes we only need monotonic comparisons on positive values,
// which IEEE 754 doubles + sign-magnitude bit patterns happen to preserve
// for non-negative floats — but it's safer to do the comparison via the
// math layer. We use a helper that decodes both, picks the bigger, and
// re-encodes.
//
// Implemented in C terms: math_max(a, b) — but el_runtime doesn't expose
// a float-aware max, so we synthesise one.

fn fmax(a: el_val_t, b: el_val_t) -> el_val_t {
    // Compare via float subtraction's sign: a - b. Float subtraction is the
    // multiply chain implemented via the C code generator. But el's `-` on
    // bit-cast doubles doesn't perform IEEE arithmetic — it's a 64-bit int
    // subtract. Workaround: round-trip through format_float and str_to_float.
    // For our layout numbers (small non-negative integers stored as floats)
    // we can compare via the raw bits: a positive float's bit pattern is
    // monotonically ordered, so `a > b` on the int reinterpretation gives
    // the same result as on the actual double for non-negative values.
    if a > b { return a }
    b
}

fn fadd(a: el_val_t, b: el_val_t) -> el_val_t {
    // a, b are bit-cast doubles. Safe addition: int-to-float, format, parse.
    // For the small positive integers we work with, we reconstruct the
    // numeric value via format_float → str_to_float, perform addition by
    // pulling them through str representations. Costly but correct on the
    // current runtime. Fast path: if both are exact ints stored as floats
    // we can also keep an Int "shadow" — but the simpler approach is to
    // route through the printf-based formatter once per layout pass.
    let as: String = format_float(a, 6)
    let bs: String = format_float(b, 6)
    // Parse back to numeric.
    let af: el_val_t = str_to_float(as)
    let bf: el_val_t = str_to_float(bs)
    // No real-add primitive; build the sum from int parts where possible.
    // Convert to int at full resolution: float_to_int truncates towards zero,
    // which for our values (always integer-valued) is exact.
    let ai: Int = float_to_int(af)
    let bi: Int = float_to_int(bf)
    int_to_float(ai + bi)
}

fn fsub(a: el_val_t, b: el_val_t) -> el_val_t {
    let ai: Int = float_to_int(a)
    let bi: Int = float_to_int(b)
    int_to_float(ai - bi)
}

fn fmul(a: el_val_t, b: el_val_t) -> el_val_t {
    let ai: Int = float_to_int(a)
    let bi: Int = float_to_int(b)
    int_to_float(ai * bi)
}

fn fdiv2(a: el_val_t) -> el_val_t {
    let ai: Int = float_to_int(a)
    int_to_float(ai / 2)
}

// ── Node size based on label width ──────────────────────────────────────────

fn node_size_for(label: String) -> Map<String, Any> {
    let len: Int = str_len(label)
    let extra: Int = 0
    if len > 10 {
        let extra = len - 10
    }
    let w_int: Int = 120 + 8 * extra
    let w: el_val_t = int_to_float(w_int)
    let h: el_val_t = int_to_float(40)
    { "w": w, "h": h }
}

// ── Adjacency-list construction ─────────────────────────────────────────────
//
// Builds successor and in-degree maps keyed by node id.

fn build_succ_indeg(graph: Map<String, Any>) -> Map<String, Any> {
    let nodes: [Map<String, Any>] = graph["nodes"]
    let edges: [Map<String, Any>] = graph["edges"]
    let n: Int = el_list_len(nodes)
    let m: Int = el_list_len(edges)

    let succ: Map<String, Any> = el_map_new(0)
    let indeg: Map<String, Any> = el_map_new(0)

    let i = 0
    while i < n {
        let nd: Map<String, Any> = get(nodes, i)
        let nid: String = nd["id"]
        let empty: [String] = el_list_empty()
        let succ = el_map_set(succ, nid, empty)
        let indeg = el_map_set(indeg, nid, 0)
        let i = i + 1
    }

    let i = 0
    while i < m {
        let e: Map<String, Any> = get(edges, i)
        let src: String = e["from"]
        let dst: String = e["to"]
        let cur_succ: [String] = el_map_get(succ, src)
        let new_succ: [String] = native_list_append(cur_succ, dst)
        let succ = el_map_set(succ, src, new_succ)
        let prev: Int = el_map_get(indeg, dst)
        let indeg = el_map_set(indeg, dst, prev + 1)
        let i = i + 1
    }

    { "succ": succ, "indeg": indeg }
}

// ── Topological rank assignment ─────────────────────────────────────────────
//
// Returns a map: node_id → rank.

fn assign_ranks(graph: Map<String, Any>) -> Map<String, Any> {
    let nodes: [Map<String, Any>] = graph["nodes"]
    let n: Int = el_list_len(nodes)
    let adj: Map<String, Any> = build_succ_indeg(graph)
    let succ: Map<String, Any> = adj["succ"]
    let indeg: Map<String, Any> = adj["indeg"]

    let ranks: Map<String, Any> = el_map_new(0)
    let i = 0
    while i < n {
        let nd: Map<String, Any> = get(nodes, i)
        let nid: String = nd["id"]
        let ranks = el_map_set(ranks, nid, 0)
        let i = i + 1
    }

    // Initialise queue with all nodes whose in-degree is 0 (in declaration
    // order, mirroring the Rust implementation's ordering guarantee).
    let queue: [String] = el_list_empty()
    let i = 0
    while i < n {
        let nd: Map<String, Any> = get(nodes, i)
        let nid: String = nd["id"]
        let d: Int = el_map_get(indeg, nid)
        if d == 0 {
            let queue = native_list_append(queue, nid)
        }
        let i = i + 1
    }

    let head = 0
    let running = true
    while running {
        if head >= el_list_len(queue) {
            let running = false
        } else {
            let cur: String = get(queue, head)
            let head = head + 1
            let cur_rank: Int = el_map_get(ranks, cur)
            let neighbours: [String] = el_map_get(succ, cur)
            let nn: Int = el_list_len(neighbours)
            let j = 0
            while j < nn {
                let nb: String = get(neighbours, j)
                let nb_rank: Int = el_map_get(ranks, nb)
                let cand: Int = cur_rank + 1
                if cand > nb_rank {
                    let ranks = el_map_set(ranks, nb, cand)
                }
                let cur_d: Int = el_map_get(indeg, nb)
                let new_d: Int = cur_d - 1
                let indeg = el_map_set(indeg, nb, new_d)
                if new_d <= 0 {
                    let queue = native_list_append(queue, nb)
                }
                let j = j + 1
            }
        }
    }
    ranks
}

// ── Layout pass ─────────────────────────────────────────────────────────────

fn arbor_layout(graph: Map<String, Any>) -> Map<String, Any> {
    let nodes: [Map<String, Any>] = graph["nodes"]
    let n: Int = el_list_len(nodes)
    let direction: String = graph["direction"]

    let result: Map<String, Any> = el_map_new(0)
    let result = el_map_set(result, "node_ids", el_list_empty())
    let result = el_map_set(result, "group_ids", el_list_empty())

    if n == 0 {
        let canvas: Map<String, Any> = { "w": int_to_float(200), "h": int_to_float(100) }
        let result = el_map_set(result, "canvas", canvas)
        return result
    }

    let ranks: Map<String, Any> = assign_ranks(graph)
    let max_rank = 0
    let i = 0
    while i < n {
        let nd: Map<String, Any> = get(nodes, i)
        let nid: String = nd["id"]
        let r: Int = el_map_get(ranks, nid)
        if r > max_rank { let max_rank = r }
        let i = i + 1
    }

    // Group nodes by rank, preserving declaration order. Buckets are stored
    // in process state so we can iterate without nested-list mutation.
    let i = 0
    while i <= max_rank {
        state_set("rank_bucket_" + int_to_str(i), "")
        let i = i + 1
    }
    let i = 0
    while i < n {
        let nd: Map<String, Any> = get(nodes, i)
        let nid: String = nd["id"]
        let r: Int = el_map_get(ranks, nid)
        let key = "rank_bucket_" + int_to_str(r)
        let prev: String = state_get(key)
        if str_eq(prev, "") {
            state_set(key, nid)
        } else {
            state_set(key, prev + "" + nid)
        }
        let i = i + 1
    }

    // Pre-compute sizes and stash a label-keyed cache.
    let id_list: [String] = el_list_empty()
    let i = 0
    while i < n {
        let nd: Map<String, Any> = get(nodes, i)
        let nid: String = nd["id"]
        let lbl: String = nd["label"]
        let sz: Map<String, Any> = node_size_for(lbl)
        let result = el_map_set(result, "node_size_" + nid, sz)
        let id_list = native_list_append(id_list, nid)
        let i = i + 1
    }
    let result = el_map_set(result, "node_ids", id_list)

    // Position pass.
    let is_vertical = true
    if str_eq(direction, "left-right") { let is_vertical = false }
    if str_eq(direction, "right-left") { let is_vertical = false }

    let cursor: el_val_t = k_margin()

    let r = 0
    while r <= max_rank {
        let bucket_str: String = state_get("rank_bucket_" + int_to_str(r))
        if !str_eq(bucket_str, "") {
            let ids: [String] = str_split(bucket_str, "")
            let ids_n: Int = el_list_len(ids)

            // Track row height (for vertical) or column width (for horizontal).
            let cross_max: el_val_t = int_to_float(40)
            let j = 0
            while j < ids_n {
                let nid: String = get(ids, j)
                let sz: Map<String, Any> = el_map_get(result, "node_size_" + nid)
                if is_vertical {
                    let h: el_val_t = sz["h"]
                    let cross_max = fmax(cross_max, h)
                } else {
                    let w: el_val_t = sz["w"]
                    let cross_max = fmax(cross_max, w)
                }
                let j = j + 1
            }

            if is_vertical {
                let row_h: el_val_t = cross_max
                let y_center: el_val_t = fadd(cursor, fdiv2(row_h))
                let x_cursor: el_val_t = k_margin()
                let j = 0
                while j < ids_n {
                    let nid: String = get(ids, j)
                    let sz: Map<String, Any> = el_map_get(result, "node_size_" + nid)
                    let w: el_val_t = sz["w"]
                    let cx: el_val_t = fadd(x_cursor, fdiv2(w))
                    let pos: Map<String, Any> = { "x": cx, "y": y_center }
                    let result = el_map_set(result, "node_pos_" + nid, pos)
                    let x_cursor = fadd(fadd(x_cursor, w), k_h_gap())
                    let j = j + 1
                }
                let cursor = fadd(fadd(cursor, row_h), k_v_gap())
            } else {
                let col_w: el_val_t = cross_max
                let x_center: el_val_t = fadd(cursor, fdiv2(col_w))
                let y_cursor: el_val_t = k_margin()
                let j = 0
                while j < ids_n {
                    let nid: String = get(ids, j)
                    let sz: Map<String, Any> = el_map_get(result, "node_size_" + nid)
                    let h: el_val_t = sz["h"]
                    let cy: el_val_t = fadd(y_cursor, fdiv2(h))
                    let pos: Map<String, Any> = { "x": x_center, "y": cy }
                    let result = el_map_set(result, "node_pos_" + nid, pos)
                    let y_cursor = fadd(fadd(y_cursor, h), k_v_gap())
                    let j = j + 1
                }
                let cursor = fadd(fadd(cursor, col_w), k_h_gap())
            }
        } else {
            // Empty bucket — advance cursor by a default node size.
            if is_vertical {
                let cursor = fadd(cursor, fadd(int_to_float(40), k_v_gap()))
            } else {
                let cursor = fadd(cursor, fadd(k_node_base_w(), k_h_gap()))
            }
        }
        let r = r + 1
    }

    // Direction inversions for BU / RL.
    let need_flip_y = false
    let need_flip_x = false
    if str_eq(direction, "bottom-up") { let need_flip_y = true }
    if str_eq(direction, "right-left") { let need_flip_x = true }

    if need_flip_y {
        let max_y: el_val_t = fadd(fsub(cursor, k_v_gap()), k_margin())
        let i = 0
        while i < n {
            let nid: String = get(id_list, i)
            let pos: Map<String, Any> = el_map_get(result, "node_pos_" + nid)
            let y: el_val_t = pos["y"]
            let new_y: el_val_t = fadd(fsub(max_y, y), k_margin())
            let new_pos: Map<String, Any> = { "x": pos["x"], "y": new_y }
            let result = el_map_set(result, "node_pos_" + nid, new_pos)
            let i = i + 1
        }
    }
    if need_flip_x {
        let max_x: el_val_t = fadd(fsub(cursor, k_h_gap()), k_margin())
        let i = 0
        while i < n {
            let nid: String = get(id_list, i)
            let pos: Map<String, Any> = el_map_get(result, "node_pos_" + nid)
            let x: el_val_t = pos["x"]
            let new_x: el_val_t = fadd(fsub(max_x, x), k_margin())
            let new_pos: Map<String, Any> = { "x": new_x, "y": pos["y"] }
            let result = el_map_set(result, "node_pos_" + nid, new_pos)
            let i = i + 1
        }
    }

    // Group bounds.
    let groups: [Map<String, Any>] = graph["groups"]
    let gn: Int = el_list_len(groups)
    let gid_list: [String] = el_list_empty()
    let g = 0
    while g < gn {
        let grp: Map<String, Any> = get(groups, g)
        let gid: String = grp["id"]
        let member_ids: [String] = grp["node_ids"]
        let mn: Int = el_list_len(member_ids)
        if mn > 0 {
            let big: Int = 1000000000
            let neg: Int = 0 - 1000000000
            let min_x: el_val_t = int_to_float(big)
            let min_y: el_val_t = int_to_float(big)
            let max_x: el_val_t = int_to_float(neg)
            let max_y: el_val_t = int_to_float(neg)
            let mi = 0
            while mi < mn {
                let mid: String = get(member_ids, mi)
                let mpos: Map<String, Any> = el_map_get(result, "node_pos_" + mid)
                let msz: Map<String, Any> = el_map_get(result, "node_size_" + mid)
                let mid_present: String = mpos["x"]
                if str_len(mid_present) >= 0 {
                    let cx: el_val_t = mpos["x"]
                    let cy: el_val_t = mpos["y"]
                    let mw: el_val_t = msz["w"]
                    let mh: el_val_t = msz["h"]
                    let left: el_val_t = fsub(cx, fdiv2(mw))
                    let right: el_val_t = fadd(cx, fdiv2(mw))
                    let top: el_val_t = fsub(cy, fdiv2(mh))
                    let bot: el_val_t = fadd(cy, fdiv2(mh))
                    if left < min_x { let min_x = left }
                    if top  < min_y { let min_y = top }
                    if right > max_x { let max_x = right }
                    if bot   > max_y { let max_y = bot }
                }
                let mi = mi + 1
            }
            let bx: el_val_t = fsub(min_x, k_group_pad())
            let by: el_val_t = fsub(min_y, k_group_pad())
            let bw: el_val_t = fadd(fsub(max_x, min_x), fmul(k_group_pad(), int_to_float(2)))
            let bh: el_val_t = fadd(fsub(max_y, min_y), fmul(k_group_pad(), int_to_float(2)))
            let bounds: Map<String, Any> = { "x": bx, "y": by, "w": bw, "h": bh }
            let result = el_map_set(result, "group_bounds_" + gid, bounds)
            let gid_list = native_list_append(gid_list, gid)
        }
        let g = g + 1
    }
    let result = el_map_set(result, "group_ids", gid_list)

    // Canvas size = max node-right / node-bottom + group-right / group-bottom.
    let canvas_w: el_val_t = int_to_float(0)
    let canvas_h: el_val_t = int_to_float(0)
    let i = 0
    while i < n {
        let nid: String = get(id_list, i)
        let pos: Map<String, Any> = el_map_get(result, "node_pos_" + nid)
        let sz: Map<String, Any> = el_map_get(result, "node_size_" + nid)
        let right: el_val_t = fadd(pos["x"], fdiv2(sz["w"]))
        let bottom: el_val_t = fadd(pos["y"], fdiv2(sz["h"]))
        if right > canvas_w { let canvas_w = right }
        if bottom > canvas_h { let canvas_h = bottom }
        let i = i + 1
    }
    let i = 0
    while i < el_list_len(gid_list) {
        let gid: String = get(gid_list, i)
        let b: Map<String, Any> = el_map_get(result, "group_bounds_" + gid)
        let r: el_val_t = fadd(b["x"], b["w"])
        let bt: el_val_t = fadd(b["y"], b["h"])
        if r > canvas_w { let canvas_w = r }
        if bt > canvas_h { let canvas_h = bt }
        let i = i + 1
    }
    let canvas: Map<String, Any> = {
        "w": fadd(canvas_w, k_margin()),
        "h": fadd(canvas_h, k_margin())
    }
    let result = el_map_set(result, "canvas", canvas)
    result
}

// ── Smoke test ──────────────────────────────────────────────────────────────

fn fl_to_str(v: el_val_t) -> String {
    int_to_str(float_to_int(v))
}

fn smoke_fail(label: String, msg: String) -> Int {
    println("FAIL " + label + ": " + msg)
    state_set("smoke_failures", "1")
    0
}

fn make_test_node(id: String, label: String) -> Map<String, Any> {
    {
        "id": id, "label": label, "sublabel": "",
        "shape": "rectangle",
        "style_fill": "", "style_stroke": "", "style_color": ""
    }
}

fn make_test_edge(src: String, dst: String) -> Map<String, Any> {
    { "from": src, "to": dst, "label": "", "line": "solid", "arrow": "forward" }
}

fn make_test_graph(direction: String, ids: [String], src_dst: [String]) -> Map<String, Any> {
    let nodes: [Map<String, Any>] = el_list_empty()
    let i = 0
    while i < el_list_len(ids) {
        let nid: String = get(ids, i)
        let nodes = native_list_append(nodes, make_test_node(nid, nid))
        let i = i + 1
    }
    let edges: [Map<String, Any>] = el_list_empty()
    let i = 0
    while i + 1 < el_list_len(src_dst) {
        let s: String = get(src_dst, i)
        let d: String = get(src_dst, i + 1)
        let edges = native_list_append(edges, make_test_edge(s, d))
        let i = i + 2
    }
    {
        "title": "T", "direction": direction,
        "nodes": nodes, "edges": edges, "groups": el_list_empty()
    }
}

// Empty graph.
let g_empty: Map<String, Any> = {
    "title": "e", "direction": "top-down",
    "nodes": el_list_empty(), "edges": el_list_empty(), "groups": el_list_empty()
}
let r_empty: Map<String, Any> = arbor_layout(g_empty)
let canvas_empty: Map<String, Any> = r_empty["canvas"]
println("empty canvas w=" + fl_to_str(canvas_empty["w"]))

// Single node.
let g_one: Map<String, Any> = make_test_graph("top-down",
    ["solo"], el_list_empty())
let r_one: Map<String, Any> = arbor_layout(g_one)
let pos_solo: Map<String, Any> = el_map_get(r_one, "node_pos_solo")
let x_solo: el_val_t = pos_solo["x"]
let y_solo: el_val_t = pos_solo["y"]
println("solo at x=" + fl_to_str(x_solo) + " y=" + fl_to_str(y_solo))
if float_to_int(x_solo) <= 0 { smoke_fail("solo x", "expected > 0") }
if float_to_int(y_solo) <= 0 { smoke_fail("solo y", "expected > 0") }

// Linear chain a→b→c top-down: ya < yb < yc.
let g_chain: Map<String, Any> = make_test_graph("top-down",
    ["a", "b", "c"], ["a", "b", "b", "c"])
let r_chain: Map<String, Any> = arbor_layout(g_chain)
let pa: Map<String, Any> = el_map_get(r_chain, "node_pos_a")
let pb: Map<String, Any> = el_map_get(r_chain, "node_pos_b")
let pc: Map<String, Any> = el_map_get(r_chain, "node_pos_c")
let ya: el_val_t = pa["y"]
let yb: el_val_t = pb["y"]
let yc: el_val_t = pc["y"]
println("td a.y=" + fl_to_str(ya) + " b.y=" + fl_to_str(yb) + " c.y=" + fl_to_str(yc))
if float_to_int(ya) >= float_to_int(yb) { smoke_fail("td order", "a.y >= b.y") }
if float_to_int(yb) >= float_to_int(yc) { smoke_fail("td order", "b.y >= c.y") }

// LR direction
let g_lr: Map<String, Any> = make_test_graph("left-right",
    ["a", "b", "c"], ["a", "b", "b", "c"])
let r_lr: Map<String, Any> = arbor_layout(g_lr)
let pa2: Map<String, Any> = el_map_get(r_lr, "node_pos_a")
let pc2: Map<String, Any> = el_map_get(r_lr, "node_pos_c")
let xa: el_val_t = pa2["x"]
let xc: el_val_t = pc2["x"]
println("lr a.x=" + fl_to_str(xa) + " c.x=" + fl_to_str(xc))
if float_to_int(xa) >= float_to_int(xc) { smoke_fail("lr order", "a.x >= c.x") }

// Bottom-up: a is below c.
let g_bu: Map<String, Any> = make_test_graph("bottom-up",
    ["a", "b", "c"], ["a", "b", "b", "c"])
let r_bu: Map<String, Any> = arbor_layout(g_bu)
let pa3: Map<String, Any> = el_map_get(r_bu, "node_pos_a")
let pc3: Map<String, Any> = el_map_get(r_bu, "node_pos_c")
let ya3: el_val_t = pa3["y"]
let yc3: el_val_t = pc3["y"]
println("bu a.y=" + fl_to_str(ya3) + " c.y=" + fl_to_str(yc3))
if float_to_int(ya3) <= float_to_int(yc3) { smoke_fail("bu order", "a.y <= c.y") }

// Canvas covers all nodes.
let canvas_chain: Map<String, Any> = r_chain["canvas"]
let cw: el_val_t = canvas_chain["w"]
let ch: el_val_t = canvas_chain["h"]
println("chain canvas w=" + fl_to_str(cw) + " h=" + fl_to_str(ch))
if float_to_int(cw) <= 0 { smoke_fail("canvas w", "non-positive") }
if float_to_int(ch) <= 0 { smoke_fail("canvas h", "non-positive") }

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