/**
 * Pane layout state — the resizable three-pane shell (spec §01A, persisted per
 * §09D "Panel layout").
 *
 * Model: the two **side** panes (library, preview) carry explicit remembered
 * widths; the **center** editor flexes to fill the remainder. This is what makes
 * a drag "leave the rest of the layout unaffected" (§01A): dragging the left
 * handle trades width between library and editor, the right handle between
 * preview and editor — the opposite side pane never moves.
 *
 * Per-pane visibility / the toggle strip (the other half of §01A) is not here
 * yet; this slice is resize + persistence, pulled forward from M6 because it is
 * coupled to the Live Preview's container sizing.
 *
 * Pure clamp helpers live alongside the store so the resize math is unit-tested
 * without a DOM. Persistence is a startup subscriber (orchestration/panes), not
 * done here — the store stays browser-free.
 */

import { create } from 'zustand';

/** Which side pane a drag handle controls. */
export type PaneSide = 'library' | 'preview';

/** Every pane that can be shown/hidden via the toggle strip (spec §01A). */
export type PaneName = 'library' | 'editor' | 'preview';

/**
 * Minimum widths (px) enforced while resizing, so no pane collapses (§01A).
 * Tuned so each pane's toolbar stays uncrowded at its floor:
 * - library 216 — the search box + compact Sort trigger have room to breathe;
 *   below ~250 the "Create New Snippet" CTA collapses to its icon (container query).
 * - preview 300 — below ~280 the fit control + settings gear clip, and a chart in
 *   <300px is too cramped to read.
 * - editor 384 — gives the JSON real horizontal room (it wrapped hard at 320); the
 *   toolbar's secondary actions still collapse to icons below ~480.
 */
export const PANE_MIN = { library: 216, preview: 300, editor: 384 } as const;

/** Initial side-pane widths (px) on first run, before any persisted layout. */
export const PANE_DEFAULT = { library: 280, preview: 360 } as const;

/** Width (px) of one resize handle. */
export const HANDLE_WIDTH = 6;

/** Combined width (px) the two handles flanking the editor occupy. */
export const HANDLES_TOTAL = HANDLE_WIDTH * 2;

/**
 * The widest a side pane can be while the editor still meets its minimum.
 * `containerWidth` is the full panes-row width; `otherWidth` is the opposite
 * side pane's current width. Never reports below the pane's own minimum.
 */
export function maxSideWidth(side: PaneSide, containerWidth: number, otherWidth: number): number {
  return Math.max(PANE_MIN[side], containerWidth - otherWidth - HANDLES_TOTAL - PANE_MIN.editor);
}

/**
 * Clamp a desired side-pane width so it stays at least its own minimum and
 * leaves the editor at least its minimum. Pure — the single place the resize
 * constraint lives.
 */
export function clampSideWidth(
  side: PaneSide,
  desired: number,
  containerWidth: number,
  otherWidth: number,
): number {
  return Math.max(
    PANE_MIN[side],
    Math.min(desired, maxSideWidth(side, containerWidth, otherWidth)),
  );
}

/**
 * Normalize a side pane's width to the 0–100 position a window splitter reports
 * via `aria-valuenow` (WAI-ARIA APG → Window Splitter): 0 = pane at its minimum
 * size, 100 = pane at its maximum. Returns null when the container width isn't
 * known yet (pre-layout) or the range has collapsed, so the caller omits the
 * attribute rather than emitting NaN. The 0–100 scale is APG's "typical" choice
 * and announces as a percentage — more meaningful than a moving pixel count.
 */
export function sideWidthValue(
  side: PaneSide,
  width: number,
  containerWidth: number,
  otherWidth: number,
): number | null {
  if (containerWidth <= 0) return null;
  const min = PANE_MIN[side];
  const max = maxSideWidth(side, containerWidth, otherWidth);
  if (max <= min) return null;
  const pct = ((width - min) / (max - min)) * 100;
  return Math.round(Math.min(100, Math.max(0, pct)));
}

/**
 * Clamp the library's width when it and the preview share a span with no editor
 * between them — the editor-hidden split (spec §01A). `availForBoth` is the
 * combined width the two panes fill; each keeps at least its own minimum. Pure —
 * the single place the library↔preview split constraint lives, used by both the
 * split drag handle and the editor-reveal re-layout.
 */
export function splitLibraryWidth(desiredLibrary: number, availForBoth: number): number {
  const min = PANE_MIN.library;
  // Never let the library crowd the preview below its minimum; if the span is too
  // small for both, the library still keeps its own minimum.
  const max = Math.max(min, availForBoth - PANE_MIN.preview);
  return Math.max(min, Math.min(desiredLibrary, max));
}

/**
 * The library's 0–100 position in the editor-hidden split, for the separator's
 * `aria-valuenow` (WAI-ARIA APG → Window Splitter): 0 = library at its minimum,
 * 100 = library at its maximum (preview at its minimum). Reads the ratio straight
 * from the two stored widths, so it's independent of the container. Null when the
 * span has no range (too narrow for both minimums), so the caller omits the attr.
 */
export function splitValue(libraryWidth: number, previewWidth: number): number | null {
  const span = libraryWidth + previewWidth;
  const min = PANE_MIN.library;
  const max = span - PANE_MIN.preview;
  if (max <= min) return null;
  const pct = ((libraryWidth - min) / (max - min)) * 100;
  return Math.round(Math.min(100, Math.max(0, pct)));
}

/**
 * The width to remember for the editor at the moment it is hidden, so it re-shows
 * at the same width (spec §01A; the editor gains a remembered width of its own).
 * It's the span the editor currently occupies: the panes row minus the toggle
 * strip (`panesInner`), minus the visible side panes and the handle beside each.
 * Floored at the editor minimum.
 */
export function capturedEditorWidth(
  panesInner: number,
  libraryWidth: number,
  previewWidth: number,
  libraryVisible: boolean,
  previewVisible: boolean,
): number {
  const sides = (libraryVisible ? libraryWidth : 0) + (previewVisible ? previewWidth : 0);
  const handles = (Number(libraryVisible) + Number(previewVisible)) * HANDLE_WIDTH;
  return Math.max(PANE_MIN.editor, panesInner - sides - handles);
}

/**
 * The side-pane widths to apply when the editor is shown again: pin the editor to
 * its remembered width and keep the library:preview *ratio*, rescaled to whatever
 * space is left (spec §01A — the editor reclaims its slot; the side panes keep the
 * proportion set while it was hidden). When the window hasn't changed since hiding,
 * the remembered editor width makes `avail` equal the side panes' former combined
 * width, so an untouched split returns pixel-perfect. A zero/absent remembered
 * width (never hidden, or legacy state) means leave the widths be and let the
 * editor fill the remainder as the flex filler.
 */
export function shownSideWidths(
  panesInner: number,
  editorWidth: number,
  libraryWidth: number,
  previewWidth: number,
  libraryVisible: boolean,
  previewVisible: boolean,
): { libraryWidth: number; previewWidth: number } {
  const handles = (Number(libraryVisible) + Number(previewVisible)) * HANDLE_WIDTH;
  const avail = panesInner - editorWidth - handles;
  if (editorWidth <= 0 || avail <= 0) return { libraryWidth, previewWidth };
  if (libraryVisible && previewVisible) {
    const ratio = libraryWidth / (libraryWidth + previewWidth || 1);
    const library = splitLibraryWidth(Math.round(ratio * avail), avail);
    return { libraryWidth: library, previewWidth: avail - library };
  }
  // Only one side pane is visible: it takes the whole leftover span.
  if (libraryVisible) return { libraryWidth: Math.max(PANE_MIN.library, avail), previewWidth };
  if (previewVisible) return { libraryWidth, previewWidth: Math.max(PANE_MIN.preview, avail) };
  return { libraryWidth, previewWidth };
}

/** Per-pane visibility (spec §01A). Widths are kept *independently* of visibility,
 * so a hidden pane keeps its remembered width and re-shows at it (not a default). */
interface PaneVisibility {
  library?: boolean;
  editor?: boolean;
  preview?: boolean;
}

export interface PanesState {
  libraryWidth: number;
  previewWidth: number;
  /**
   * The editor's remembered width, captured when it is hidden so it re-shows at
   * the same size (spec §01A). Zero until the editor has been hidden at least once
   * — while the editor is shown it is the flex filler and this is unused.
   */
  editorWidth: number;
  /** Whether each pane is currently shown. All visible by default (§01A). */
  libraryVisible: boolean;
  editorVisible: boolean;
  previewVisible: boolean;
  /** Set a side pane's width (already clamped by the caller). */
  setWidth: (side: PaneSide, width: number) => void;
  /**
   * Set both side-pane widths at once — the editor-hidden split, where dragging
   * the library↔preview handle re-proportions the two together (already clamped by
   * the caller via `splitLibraryWidth`).
   */
  setSplit: (libraryWidth: number, previewWidth: number) => void;
  /**
   * Lay the workspace out at the onboarding default split — library 25% · editor
   * 25% · preview 50% of `containerWidth` — and show all three panes. Applied when
   * the user leaves the empty/onboarding state by creating their first snippet(s),
   * so the first chart opens with a generous preview rather than the generic
   * remembered widths (spec §02 → First-Run & Empty Workspace). Widths are clamped
   * to keep every pane at least its minimum.
   */
  applyOnboardingSplit: (containerWidth: number) => void;
  /**
   * Show/hide one pane. Hiding all panes is permitted (§01A) — the strip stays.
   * Toggling the **editor** is layout-aware: hiding remembers its current width and
   * showing pins it back, keeping the library:preview ratio — so `panesInner` (the
   * panes-row width minus the toggle strip) must be passed for the editor. It is
   * ignored for the side panes, which only flip visibility.
   */
  togglePane: (pane: PaneName, panesInner?: number) => void;
  /** Restore persisted widths + visibility on startup; missing values keep defaults. */
  hydrate: (
    layout: { libraryWidth?: number; previewWidth?: number; editorWidth?: number },
    visibility?: PaneVisibility,
  ) => void;
}

const VISIBLE_KEY = {
  library: 'libraryVisible',
  editor: 'editorVisible',
  preview: 'previewVisible',
} as const;

export const usePanesStore = create<PanesState>((set) => ({
  libraryWidth: PANE_DEFAULT.library,
  previewWidth: PANE_DEFAULT.preview,
  editorWidth: 0,
  libraryVisible: true,
  editorVisible: true,
  previewVisible: true,

  setWidth: (side, width) =>
    set(side === 'library' ? { libraryWidth: width } : { previewWidth: width }),

  setSplit: (libraryWidth, previewWidth) => set({ libraryWidth, previewWidth }),

  applyOnboardingSplit: (containerWidth) =>
    set(() => {
      // Clamp preview (the larger share) first, then library against it, so the
      // editor keeps at least its minimum wherever the window is wide enough.
      const previewWidth = clampSideWidth(
        'preview',
        Math.round(containerWidth * 0.5),
        containerWidth,
        Math.round(containerWidth * 0.25),
      );
      const libraryWidth = clampSideWidth(
        'library',
        Math.round(containerWidth * 0.25),
        containerWidth,
        previewWidth,
      );
      return {
        libraryWidth,
        previewWidth,
        libraryVisible: true,
        editorVisible: true,
        previewVisible: true,
      };
    }),

  togglePane: (pane, panesInner = 0) =>
    set((s) => {
      // Side panes only flip visibility; the editor (the flex filler) also carries
      // its width across the hide/show so it reclaims its slot (spec §01A).
      if (pane !== 'editor') return { [VISIBLE_KEY[pane]]: !s[VISIBLE_KEY[pane]] };
      if (s.editorVisible) {
        // Hiding: remember the editor's current width; the side panes keep theirs
        // and expand proportionally to fill the freed space (their flex render).
        return {
          editorVisible: false,
          editorWidth: capturedEditorWidth(
            panesInner,
            s.libraryWidth,
            s.previewWidth,
            s.libraryVisible,
            s.previewVisible,
          ),
        };
      }
      // Showing: pin the editor to its remembered width; re-split the rest between
      // the side panes at the ratio they were left at while it was hidden.
      return {
        editorVisible: true,
        ...shownSideWidths(
          panesInner,
          s.editorWidth,
          s.libraryWidth,
          s.previewWidth,
          s.libraryVisible,
          s.previewVisible,
        ),
      };
    }),

  hydrate: (layout, visibility) =>
    set({
      libraryWidth: layout.libraryWidth ?? PANE_DEFAULT.library,
      previewWidth: layout.previewWidth ?? PANE_DEFAULT.preview,
      editorWidth: layout.editorWidth ?? 0,
      libraryVisible: visibility?.library ?? true,
      editorVisible: visibility?.editor ?? true,
      previewVisible: visibility?.preview ?? true,
    }),
}));
