import * as splitterMachine from "@zag-js/splitter";
import { cn } from "#lib/utils.ts";
import { resizable as styles } from "./classes.ts";

export interface ResizablePanel {
  id: string;
  defaultSize?: number | string;
  minSize?: number | string;
  maxSize?: number | string;
}

export interface Input
  extends Omit<Marko.Input<"div">, "content" | "onResize"> {
  panels: ResizablePanel[];
  content?: Marko.Body<[ResizablePanel, number]>;
  orientation?: "horizontal" | "vertical";
  class?: string;
  // Zag's own callbacks also work and receive the details object.
  onResize?: (details: splitterMachine.ResizeDetails) => void;
  onResizeStart?: () => void;
  onResizeEnd?: (details: splitterMachine.ResizeEndDetails) => void;
  onCollapse?: (details: splitterMachine.ExpandCollapseDetails) => void;
  onExpand?: (details: splitterMachine.ExpandCollapseDetails) => void;
  // Additive sugar mirroring the `xxxChange` convention used elsewhere in
  // the registry (e.g. switch's `checkedChange`) for the values most
  // consumers actually want out of a resize/collapse/expand event.
  resizeChange?: (size: number[]) => void;
  resizeEndChange?: (size: number[]) => void;
  collapseChange?: (panelId: string, size: number) => void;
  expandChange?: (panelId: string, size: number) => void;
}

// This component's public API (`panels`, `orientation`) does not map 1:1
// onto the machine's Props shape, so every machine prop it exposes is
// supplied as an override attribute below, computed here and visibly —
// each one replaces whatever `from=input` would otherwise pick by name.
//
// The callbacks are overrides for a second reason: <zag>'s default rule
// derives the sugar name from the callback name (`on<X>Change` -> `xChange`)
// and only fires it when the details object carries key `x`. `onResize`/
// `onResizeEnd` are not `on<X>Change` at all, and their details key is
// `size`, so the default rule emits nothing — the sugar calls have to be
// written out.
<zag/api=() => splitterMachine from=input
  orientation=input.orientation ?? "horizontal"
  panels=input.panels.map((panel) => ({
    id: panel.id,
    minSize: panel.minSize,
    maxSize: panel.maxSize,
  }))
  defaultSize=input.panels.map((panel) => panel.defaultSize ?? 100 / input.panels.length)
  onResize(details: splitterMachine.ResizeDetails) {
    input.onResize?.(details);
    input.resizeChange?.(details.size);
  }
  onResizeStart() {
    input.onResizeStart?.();
  }
  onResizeEnd(details: splitterMachine.ResizeEndDetails) {
    input.onResizeEnd?.(details);
    input.resizeEndChange?.(details.size);
  }
  onCollapse(details: splitterMachine.ExpandCollapseDetails) {
    input.onCollapse?.(details);
    input.collapseChange?.(details.panelId, details.size);
  }
  onExpand(details: splitterMachine.ExpandCollapseDetails) {
    input.onExpand?.(details);
    input.expandChange?.(details.panelId, details.size);
  }
/>

// NOTE (intentional deviation from the standard
// `machineModule.splitProps(input)[1]` pattern): Input's `panels` field is
// `ResizablePanel[]` (id/defaultSize/minSize/maxSize), a superset of the
// machine's `PanelData[]` shape. `splitterMachine.splitProps(input)` would
// type-check (extra fields don't block interface-to-interface
// assignability), but the manual destructure keeps every
// component-owned field explicit alongside the machine ones. Every
// machine-owned field this component exposes (`panels`, `orientation`,
// `onResize`, `onResizeStart`, `onResizeEnd`, `onCollapse`, `onExpand`,
// `resizeChange`, `resizeEndChange`, `collapseChange`, `expandChange`) is
// named explicitly below instead. If a new machine-named field is ever
// added to Input ad hoc, it MUST be added to this destructure too, or it
// will leak onto the native <div> via `...rest`.
<const/{ panels, content, class: className, orientation, onResize, onResizeStart, onResizeEnd, onCollapse, onExpand, resizeChange, resizeEndChange, collapseChange, expandChange, ...rest }=input/>

<div ...rest ...api().getRootProps() data-slot="resizable-panel-group" class=cn(styles.panelGroup, className)>
  <for|panel, index| of=panels>
    <div ...api().getPanelProps({ id: panel.id }) data-slot="resizable-panel">
      <if=content>
        <${content}(panel, index)/>
      </if>
    </div>
    // Bind the next panel instead of indexing inside the template: under
    // `noUncheckedIndexedAccess` TS cannot carry the `index < length - 1`
    // guard into the subscript, so `panels[index + 1]` reads as possibly
    // undefined. `<if=nextPanel>` narrows it and keeps the same condition.
    <const/nextPanel=panels[index + 1]/>
    <if=nextPanel>
      <div
        ...api().getResizeTriggerProps({ id: `${panel.id}:${nextPanel.id}` })
        data-slot="resizable-handle"
        class=styles.handle
      >
        <div class=styles.handleIcon>
          <svg
            xmlns="http://www.w3.org/2000/svg"
            width="24"
            height="24"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
            class=styles.handleIconSvg
          >
            <circle cx="9" cy="12" r="1"/>
            <circle cx="9" cy="5" r="1"/>
            <circle cx="9" cy="19" r="1"/>
            <circle cx="15" cy="12" r="1"/>
            <circle cx="15" cy="5" r="1"/>
            <circle cx="15" cy="19" r="1"/>
          </svg>
        </div>
      </div>
    </if>
  </for>
</div>

<return=api>
