/**
 * public/grid-layout.ts — desktop split-grid ("watch board") layout logic + toggle.
 *
 * When several sessions are open on a large screen, the user can split #term into
 * a 2×2 grid (or 1×2 side-by-side) so multiple LIVE terminals are visible at once
 * — a monitoring/multitasking convenience for vibe-coding several Claude sessions.
 *
 * This module owns the PURE layout logic (capacity, which tabs are visible, the
 * matchMedia desktop gate, persistence) plus the toolbar toggle control. The DOM
 * of the panes themselves stays in tabs.ts (TabApp) — this keeps tabs.ts from
 * growing past its size budget and makes the layout math unit-testable in isolation.
 *
 * v1 layouts: 'single' (today's behavior) · 'split-2' (side-by-side) · 'grid-4' (2×2).
 * The design is deliberately extensible: adding 'row-3'/'grid-6' in v2 is a
 * CAPACITY entry + a LAYOUT_META entry + CSS, with no change to the state model.
 */

/** A screen layout for the terminal area. Ordered by pane capacity. */
export type GridLayout = 'single' | 'split-2' | 'row-3' | 'grid-4' | 'grid-6'

/** All layouts, in the order the toggle presents them (by capacity). */
export const GRID_LAYOUTS: readonly GridLayout[] = [
  'single',
  'split-2',
  'row-3',
  'grid-4',
  'grid-6',
]

const CAPACITY: Readonly<Record<GridLayout, number>> = {
  single: 1,
  'split-2': 2,
  'row-3': 3,
  'grid-4': 4,
  'grid-6': 6,
}

/** How many panes a layout shows at once. */
export function layoutCapacity(layout: GridLayout): number {
  return CAPACITY[layout] ?? 1
}

/**
 * The tab indices visible under `layout`: the first `min(tabCount, capacity)` in
 * tab order. Membership is controlled by reordering tabs (v1) — drag a tab into
 * the first N and it joins the board.
 */
export function visibleIndices(tabCount: number, layout: GridLayout): number[] {
  const n = Math.min(Math.max(0, tabCount), layoutCapacity(layout))
  return Array.from({ length: n }, (_, i) => i)
}

/**
 * Whether tab `idx` is on-screen. In 'single' that's only the focused pane; in a
 * grid it's any of the first-N visible tabs. Used for both rendering and to
 * suppress OS notifications for panes the user can already see.
 */
export function isVisibleIndex(
  idx: number,
  activeIndex: number,
  tabCount: number,
  layout: GridLayout,
): boolean {
  if (layout === 'single') return idx === activeIndex
  return idx >= 0 && idx < Math.min(Math.max(0, tabCount), layoutCapacity(layout))
}

/**
 * Match the split-grid focus-cycle keybinding (Ctrl+` forward, Ctrl+Shift+`
 * reverse). Returns the cycle direction, or null if the event isn't the binding.
 * Extracted from the DOM listener so it's unit-testable in isolation.
 */
export function matchFocusCycleKey(e: KeyboardEvent): 1 | -1 | null {
  if (!e.ctrlKey || e.altKey || e.metaKey) return null
  if (e.key !== '`' && e.code !== 'Backquote') return null
  return e.shiftKey ? -1 : 1
}

/** Minimum viewport width (px) at which multi-pane layouts are offered. Four
 *  terminals need real width; below this the board falls back to 'single'. */
export const GRID_MIN_WIDTH = 1024

/** True when the screen is wide enough for split layouts (desktop/large only). */
export function gridAllowed(): boolean {
  if (typeof window === 'undefined') return false
  if (typeof window.matchMedia === 'function') {
    return window.matchMedia(`(min-width: ${GRID_MIN_WIDTH}px)`).matches
  }
  return (window.innerWidth ?? 0) >= GRID_MIN_WIDTH
}

const GRID_LAYOUT_KEY = 'web-terminal:grid-layout'

function isGridLayout(v: unknown): v is GridLayout {
  return (
    v === 'single' || v === 'split-2' || v === 'row-3' || v === 'grid-4' || v === 'grid-6'
  )
}

/** Load the persisted layout. Forced to 'single' when the screen is too narrow
 *  (a stored grid choice must not resurrect on a phone). */
export function loadGridLayout(): GridLayout {
  if (!gridAllowed()) return 'single'
  try {
    const stored = localStorage.getItem(GRID_LAYOUT_KEY)
    if (isGridLayout(stored)) return stored
  } catch {
    // localStorage unavailable — use default
  }
  return 'single'
}

/** Persist the chosen layout (mirrors the other web-terminal:* prefs). */
export function saveGridLayout(layout: GridLayout): void {
  try {
    localStorage.setItem(GRID_LAYOUT_KEY, layout)
  } catch {
    // localStorage unavailable — run without persistence
  }
}

export interface GridToggleHooks {
  getLayout: () => GridLayout
  setLayout: (layout: GridLayout) => void
}

export interface GridToggle {
  /** Re-sync the control's pressed state + visibility (after setLayout / resize). */
  refresh: () => void
  dispose: () => void
}

/** Per-layout label/title for the toggle. The glyph is drawn in CSS from the
 *  cell count (see makeGlyph), not from a font, so it renders identically everywhere. */
const LAYOUT_META: Readonly<Record<GridLayout, { title: string; cells: number }>> = {
  single: { title: 'Single pane', cells: 1 },
  'split-2': { title: 'Side by side — compare two', cells: 2 },
  'row-3': { title: '1×3 row — three across', cells: 3 },
  'grid-4': { title: '2×2 watch board', cells: 4 },
  'grid-6': { title: '2×3 grid — six sessions', cells: 6 },
}

/** A small CSS-drawn layout glyph (N cells), reliable across platforms. */
function makeGlyph(layout: GridLayout): HTMLElement {
  const g = document.createElement('span')
  g.className = `grid-glyph gl-${layout}`
  g.setAttribute('aria-hidden', 'true')
  for (let i = 0; i < LAYOUT_META[layout].cells; i++) g.appendChild(document.createElement('i'))
  return g
}

/**
 * Mount the layout segmented control into the toolbar. Desktop-only: it hides
 * below GRID_MIN_WIDTH, and if the window narrows past the threshold while a grid
 * is active it forces the layout back to 'single' (the caller re-renders).
 */
export function mountGridToggle(toolbar: HTMLElement, hooks: GridToggleHooks): GridToggle {
  const seg = document.createElement('div')
  seg.className = 'grid-toggle'
  seg.setAttribute('role', 'group')
  seg.setAttribute('aria-label', 'Terminal layout')

  const buttons = GRID_LAYOUTS.map((layout) => {
    const btn = document.createElement('button')
    btn.className = 'grid-toggle-btn'
    btn.dataset.layout = layout
    btn.title = LAYOUT_META[layout].title
    btn.setAttribute('aria-label', LAYOUT_META[layout].title)
    btn.appendChild(makeGlyph(layout))
    btn.addEventListener('click', () => hooks.setLayout(layout))
    seg.appendChild(btn)
    return btn
  })

  const refresh = (): void => {
    seg.style.display = gridAllowed() ? 'inline-flex' : 'none'
    const current = hooks.getLayout()
    for (const btn of buttons) {
      btn.setAttribute('aria-pressed', String(btn.dataset.layout === current))
    }
  }

  const mql =
    typeof window !== 'undefined' && typeof window.matchMedia === 'function'
      ? window.matchMedia(`(min-width: ${GRID_MIN_WIDTH}px)`)
      : null
  const onChange = (): void => {
    if (!gridAllowed() && hooks.getLayout() !== 'single') hooks.setLayout('single')
    refresh()
  }
  mql?.addEventListener('change', onChange)

  toolbar.appendChild(seg)
  refresh()

  return {
    refresh,
    dispose: (): void => {
      mql?.removeEventListener('change', onChange)
      seg.remove()
    },
  }
}

/* ── Resizable splitters (v3) ────────────────────────────────────────── */

/** The (columns, rows) track counts a layout lays its panes out in. */
export function layoutTracks(layout: GridLayout): { cols: number; rows: number } {
  switch (layout) {
    case 'single':
      return { cols: 1, rows: 1 }
    case 'split-2':
      return { cols: 2, rows: 1 }
    case 'row-3':
      return { cols: 3, rows: 1 }
    case 'grid-4':
      return { cols: 2, rows: 2 }
    case 'grid-6':
      return { cols: 3, rows: 2 }
  }
}

/** Per-axis track fractions (relative fr units) for one layout. */
export interface TrackSplit {
  cols: number[]
  rows: number[]
}

/** Persisted custom track fractions, per layout (missing → equal tracks). */
export type GridSplits = Partial<Record<GridLayout, TrackSplit>>

/** Equal-fraction split for a layout (every track = 1fr). */
export function defaultSplit(layout: GridLayout): TrackSplit {
  const { cols, rows } = layoutTracks(layout)
  return { cols: Array<number>(cols).fill(1), rows: Array<number>(rows).fill(1) }
}

/** Smallest fraction a track may shrink to while its neighbor grows. */
export const MIN_TRACK_FRACTION = 0.3

/**
 * Move the boundary between track `gutterIndex` and `gutterIndex+1` by `delta`
 * (in fr units), trading the delta between the two adjacent tracks and clamping
 * both to MIN_TRACK_FRACTION. Pure — returns a new array (the input untouched).
 */
export function adjustSplit(
  fractions: readonly number[],
  gutterIndex: number,
  delta: number,
  min = MIN_TRACK_FRACTION,
): number[] {
  if (gutterIndex < 0 || gutterIndex >= fractions.length - 1) return fractions.slice()
  let a = (fractions[gutterIndex] ?? 1) + delta
  let b = (fractions[gutterIndex + 1] ?? 1) - delta
  if (a < min) {
    b -= min - a
    a = min
  }
  if (b < min) {
    a -= min - b
    b = min
  }
  if (a < min || b < min) return fractions.slice() // can't satisfy both
  const next = fractions.slice()
  next[gutterIndex] = a
  next[gutterIndex + 1] = b
  return next
}

/** Render fractions as a grid-template value (e.g. "1fr 1.4fr"). */
export function tracksToTemplate(fractions: readonly number[]): string {
  return fractions.map((f) => `${f}fr`).join(' ')
}

const GRID_SPLITS_KEY = 'web-terminal:grid-splits'

/** Load persisted splits (best-effort; shape validated per-layout at use). */
export function loadGridSplits(): GridSplits {
  try {
    const raw = localStorage.getItem(GRID_SPLITS_KEY)
    if (raw === null) return {}
    const parsed: unknown = JSON.parse(raw)
    if (parsed !== null && typeof parsed === 'object') return parsed as GridSplits
  } catch {
    // localStorage / JSON unavailable — use equal tracks
  }
  return {}
}

/** Persist the custom splits map. */
export function saveGridSplits(splits: GridSplits): void {
  try {
    localStorage.setItem(GRID_SPLITS_KEY, JSON.stringify(splits))
  } catch {
    // localStorage unavailable — run without persistence
  }
}

/** The valid split for a layout: the stored one if its shape matches the layout's
 *  track counts and all fractions are positive, else the equal default. */
export function splitForLayout(splits: GridSplits, layout: GridLayout): TrackSplit {
  const { cols, rows } = layoutTracks(layout)
  const stored = splits[layout]
  const ok =
    stored !== undefined &&
    stored !== null && // a null entry ({"grid-4":null}) is valid JSON but not a TrackSplit
    typeof stored === 'object' &&
    Array.isArray(stored.cols) &&
    stored.cols.length === cols &&
    Array.isArray(stored.rows) &&
    stored.rows.length === rows &&
    [...stored.cols, ...stored.rows].every((n) => typeof n === 'number' && n > 0)
  return ok ? stored : defaultSplit(layout)
}
