//! Splits-tree layout model.
//!
//! The workspace is a binary tree of splits. Internal nodes are HSplit or
//! VSplit with a ratio; leaves are terminal panes. This is the same model
//! tmux / i3 / Zellij use — dragging a gutter mutates one parent ratio,
//! both sibling subtrees reflow automatically.

export type NodeId = string;

/** 'h' = side-by-side (a on left, b on right). 'v' = stacked (a on top, b below). */
export type Orientation = "h" | "v";

export interface LeafNode {
  kind: "leaf";
  id: NodeId;
  /** WSL distro the pane was spawned against. */
  distro?: string;
  /** Working directory the pane was started in. Not currently used at spawn time but preserved for future. */
  cwd?: string;
  /** Optional user label shown in the pane toolbar. */
  label?: string;
  /**
   * If true, keystrokes typed in this pane are mirrored to every other
   * leaf with `broadcast === true`. Toggle via the 📡 button in the
   * pane toolbar.
   */
  broadcast?: boolean;
}

export interface SplitNode {
  kind: "split";
  id: NodeId;
  orientation: Orientation;
  /** Size of `a` divided by total. Kept in (0, 1) — clamped on drag. */
  ratio: number;
  a: TreeNode;
  b: TreeNode;
}

export type TreeNode = LeafNode | SplitNode;

function newId(): NodeId {
  return (
    globalThis.crypto?.randomUUID?.() ??
    Math.random().toString(36).slice(2, 12)
  );
}

export function newLeaf(props: Partial<Omit<LeafNode, "kind" | "id">> = {}): LeafNode {
  return { kind: "leaf", id: newId(), ...props };
}

export function newSplit(
  orientation: Orientation,
  a: TreeNode,
  b: TreeNode,
  ratio = 0.5,
): SplitNode {
  return { kind: "split", id: newId(), orientation, ratio, a, b };
}

/** Walk the tree, replacing the node with the given id via `produce`. Returns a new tree. */
export function replaceById(
  root: TreeNode,
  targetId: NodeId,
  produce: (node: TreeNode) => TreeNode,
): TreeNode {
  if (root.id === targetId) return produce(root);
  if (root.kind === "leaf") return root;
  const a = replaceById(root.a, targetId, produce);
  const b = replaceById(root.b, targetId, produce);
  if (a === root.a && b === root.b) return root;
  return { ...root, a, b };
}

/** Split the leaf with the given id. New pane goes on side `b`. */
export function splitLeaf(
  root: TreeNode,
  leafId: NodeId,
  orientation: Orientation,
  newLeafProps: Partial<Omit<LeafNode, "kind" | "id">> = {},
): TreeNode {
  return replaceById(root, leafId, (node) => {
    if (node.kind !== "leaf") return node;
    return newSplit(orientation, node, newLeaf(newLeafProps));
  });
}

/**
 * Remove the leaf with the given id. The other child of its parent split
 * takes the parent's place in the tree. Returns null if the closed leaf
 * was the entire tree (caller should create a fresh leaf).
 */
export function closeLeaf(root: TreeNode, leafId: NodeId): TreeNode | null {
  if (root.id === leafId) return null;
  if (root.kind === "leaf") return root;
  // If a direct child is the target leaf, collapse this split to the sibling.
  if (root.a.kind === "leaf" && root.a.id === leafId) return root.b;
  if (root.b.kind === "leaf" && root.b.id === leafId) return root.a;
  // Recurse.
  const newA = closeLeaf(root.a, leafId);
  const newB = closeLeaf(root.b, leafId);
  // If either side collapsed to null somehow (target was a split, shouldn't
  // happen with current callers but defensive), fall back to the other side.
  if (newA === null) return newB ?? root;
  if (newB === null) return newA;
  if (newA === root.a && newB === root.b) return root;
  return { ...root, a: newA, b: newB };
}

/** Find a leaf by id. Returns null if not found. */
export function findLeaf(root: TreeNode, leafId: NodeId): LeafNode | null {
  if (root.kind === "leaf") return root.id === leafId ? root : null;
  return findLeaf(root.a, leafId) ?? findLeaf(root.b, leafId);
}

/**
 * Swap the distro on a leaf. The leaf gets a **new id** so the rendering
 * layer's `{#key node.id}` block remounts XtermPane — the old PTY is killed
 * and a fresh one spawns with the new distro.
 */
export function changeDistro(
  root: TreeNode,
  leafId: NodeId,
  distro: string,
): TreeNode {
  return replaceById(root, leafId, (node) => {
    if (node.kind !== "leaf") return node;
    return { ...node, id: newId(), distro };
  });
}

/** Set or clear a leaf's label. Does NOT remount (label is metadata only). */
export function changeLabel(
  root: TreeNode,
  leafId: NodeId,
  label: string | undefined,
): TreeNode {
  return replaceById(root, leafId, (node) => {
    if (node.kind !== "leaf") return node;
    const trimmed = label?.trim();
    return { ...node, label: trimmed ? trimmed : undefined };
  });
}

// ---- preset layouts --------------------------------------------------------

type LeafDefaults = Partial<Omit<LeafNode, "kind" | "id">>;

export function presetSingle(d: LeafDefaults = {}): TreeNode {
  return newLeaf(d);
}

export function presetTwoColumns(d: LeafDefaults = {}): TreeNode {
  return newSplit("h", newLeaf(d), newLeaf(d));
}

export function presetThreeColumns(d: LeafDefaults = {}): TreeNode {
  // Even thirds: outer split at 1/3, inner split at 1/2.
  return newSplit(
    "h",
    newLeaf(d),
    newSplit("h", newLeaf(d), newLeaf(d), 0.5),
    1 / 3,
  );
}

export function presetTwoRows(d: LeafDefaults = {}): TreeNode {
  return newSplit("v", newLeaf(d), newLeaf(d));
}

export function presetTwoByTwo(d: LeafDefaults = {}): TreeNode {
  const row = () => newSplit("h", newLeaf(d), newLeaf(d));
  return newSplit("v", row(), row());
}

/** Number of leaves in the tree. */
export function leafCount(root: TreeNode): number {
  if (root.kind === "leaf") return 1;
  return leafCount(root.a) + leafCount(root.b);
}

/** Iterate all leaves in left-to-right order. */
export function* walkLeaves(root: TreeNode): Generator<LeafNode> {
  if (root.kind === "leaf") {
    yield root;
  } else {
    yield* walkLeaves(root.a);
    yield* walkLeaves(root.b);
  }
}

/** Toggle a leaf's broadcast flag. Metadata-only — does NOT swap the id, so the pane is not respawned. */
export function toggleBroadcast(root: TreeNode, leafId: NodeId): TreeNode {
  return replaceById(root, leafId, (node) => {
    if (node.kind !== "leaf") return node;
    return { ...node, broadcast: !node.broadcast };
  });
}

export function serialize(root: TreeNode): string {
  return JSON.stringify(root);
}

/** Parse JSON back to a tree. Returns null on invalid input. */
export function deserialize(json: string): TreeNode | null {
  try {
    const parsed = JSON.parse(json);
    if (!isTreeNode(parsed)) return null;
    return parsed;
  } catch {
    return null;
  }
}

function isTreeNode(x: unknown): x is TreeNode {
  if (typeof x !== "object" || x === null) return false;
  const o = x as Record<string, unknown>;
  if (typeof o.id !== "string") return false;
  if (o.kind === "leaf") return true;
  if (o.kind === "split") {
    return (
      (o.orientation === "h" || o.orientation === "v") &&
      typeof o.ratio === "number" &&
      isTreeNode(o.a) &&
      isTreeNode(o.b)
    );
  }
  return false;
}
