"""Top-down tree layout with subtree-width packing (parity plan Q2-2).

Pure geometry — no Qt imports — so the packing math is testable headless
(``tests/test_netweaver_graph_layout.py``). The node-network dialog measures
each node's pixel size with Qt font metrics and hands the sizes here.

The input is the occurrence list produced by the ``get_node_network`` bridge
verb: ``nodes[i]["id"] == i`` and ``children`` hold forward ids, so the
structure is a strict tree (a node reached through two parents appears twice
with distinct ids — exactly how the ArcGIS diagram draws it).

Mirrors the ArcGIS viewer's Telerik ``TreeLayoutType.TreeDown``: children are
packed left→right under their parent, each parent is centered over its
children's span, and rows sit at depth-cumulative heights.
"""

from __future__ import annotations

from typing import Dict, Iterable, List, Sequence, Tuple

# The ArcGIS viewer uses HorizontalSeparation=28 / VerticalSeparation=56.
DEFAULT_H_GAP = 28.0
DEFAULT_V_GAP = 56.0


def subtree_ids(nodes: Sequence[dict], root_id: int) -> List[int]:
    """Depth-first ids of the subtree rooted at *root_id* (root first)."""
    order: List[int] = []

    def walk(node_id: int) -> None:
        order.append(node_id)
        for child_id in nodes[node_id].get("children") or []:
            walk(child_id)

    walk(root_id)
    return order


def layout_tree(
    nodes: Sequence[dict],
    sizes: Dict[int, Tuple[float, float]],
    *,
    root_id: int = 0,
    h_gap: float = DEFAULT_H_GAP,
    v_gap: float = DEFAULT_V_GAP,
) -> Dict[int, Tuple[float, float]]:
    """Return ``{id: (x, y)}`` top-left positions for *root_id*'s subtree.

    * siblings never overlap horizontally (separated by ``h_gap``);
    * a parent is centered over the horizontal span of its children;
    * each depth level is one row; a row's height is the tallest node in it,
      and rows are separated by ``v_gap``.
    """

    def children_of(node_id: int) -> List[int]:
        return list(nodes[node_id].get("children") or [])

    # ---- depth rows -----------------------------------------------------
    depth_of: Dict[int, int] = {root_id: 0}
    order = subtree_ids(nodes, root_id)
    for node_id in order:
        for child_id in children_of(node_id):
            depth_of[child_id] = depth_of[node_id] + 1

    row_heights: Dict[int, float] = {}
    for node_id in order:
        depth = depth_of[node_id]
        height = sizes[node_id][1]
        row_heights[depth] = max(row_heights.get(depth, 0.0), height)

    row_y: Dict[int, float] = {}
    y_cursor = 0.0
    for depth in sorted(row_heights):
        row_y[depth] = y_cursor
        y_cursor += row_heights[depth] + v_gap

    # ---- subtree-width packing ------------------------------------------
    span_of: Dict[int, float] = {}

    def span(node_id: int) -> float:
        cached = span_of.get(node_id)
        if cached is not None:
            return cached
        own_width = sizes[node_id][0]
        kids = children_of(node_id)
        if kids:
            kids_width = sum(span(child_id) for child_id in kids)
            kids_width += h_gap * (len(kids) - 1)
            result = max(own_width, kids_width)
        else:
            result = own_width
        span_of[node_id] = result
        return result

    positions: Dict[int, Tuple[float, float]] = {}

    def place(node_id: int, left: float) -> None:
        node_span = span(node_id)
        own_width, own_height = sizes[node_id]
        depth = depth_of[node_id]
        # Center the node itself within its subtree span, and center the node
        # vertically within its row (rows are as tall as their tallest node).
        x = left + (node_span - own_width) / 2.0
        y = row_y[depth] + (row_heights[depth] - own_height) / 2.0
        positions[node_id] = (x, y)

        kids = children_of(node_id)
        if not kids:
            return
        kids_width = sum(span(child_id) for child_id in kids) + h_gap * (len(kids) - 1)
        child_left = left + (node_span - kids_width) / 2.0
        for child_id in kids:
            place(child_id, child_left)
            child_left += span(child_id) + h_gap

    place(root_id, 0.0)
    return positions


def edge_endpoints(
    positions: Dict[int, Tuple[float, float]],
    sizes: Dict[int, Tuple[float, float]],
    parent_id: int,
    child_id: int,
) -> Tuple[float, float, float, float]:
    """(x1, y1, x2, y2) from the parent's bottom-center to the child's
    top-center — the connector line for a top-down tree."""
    px, py = positions[parent_id]
    pw, ph = sizes[parent_id]
    cx, cy = positions[child_id]
    cw, _ch = sizes[child_id]
    return (px + pw / 2.0, py + ph, cx + cw / 2.0, cy)
