// Node grouping & collapsing — shared model + geometry used by all three visual
// node editors (LogicEditor, ControlLogicEditor, SyntheticGraphEditor).
//
// A group is purely a cosmetic, editor-side annotation: it references member
// node ids, carries an optional label, and can be collapsed to hide its members
// behind a compact box. Groups never change how a graph evaluates — the backends
// store and round-trip them but ignore them when compiling/running the graph.

export interface NodeGroup {
  id: string;
  label: string;
  members: string[]; // ids of member nodes
  collapsed: boolean;
}

export interface Rect { x: number; y: number; w: number; h: number; }

export function genGroupId(): string {
  return 'grp_' + Math.random().toString(36).slice(2, 9);
}

// The group (if any) that a node belongs to. A node lives in at most one group.
export function groupContaining(
  groups: NodeGroup[] | undefined,
  nodeId: string,
): NodeGroup | undefined {
  return (groups ?? []).find(g => g.members.includes(nodeId));
}

// Drop ids that no longer exist (deleted nodes) and discard groups left empty.
export function pruneGroups(
  groups: NodeGroup[] | undefined,
  existingIds: Set<string>,
): NodeGroup[] {
  return (groups ?? [])
    .map(g => ({ ...g, members: g.members.filter(id => existingIds.has(id)) }))
    .filter(g => g.members.length > 0);
}

// Axis-aligned bounding box around the member node rects, expanded by `pad` on
// every side and by `header` extra pixels on top (room for the title bar).
// Returns null when no member resolves to a rect.
export function groupBounds(
  members: string[],
  rectOf: (id: string) => Rect | null,
  pad: number,
  header: number,
): Rect | null {
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
  for (const id of members) {
    const r = rectOf(id);
    if (!r) continue;
    if (r.x < minX) minX = r.x;
    if (r.y < minY) minY = r.y;
    if (r.x + r.w > maxX) maxX = r.x + r.w;
    if (r.y + r.h > maxY) maxY = r.y + r.h;
  }
  if (!isFinite(minX)) return null;
  return {
    x: minX - pad,
    y: minY - pad - header,
    w: (maxX - minX) + pad * 2,
    h: (maxY - minY) + pad * 2 + header,
  };
}

// The compact box shown in place of a collapsed group, anchored at the group's
// top-left corner.
export function collapsedRect(bounds: Rect, width: number, height: number): Rect {
  return { x: bounds.x, y: bounds.y, w: width, h: height };
}
