/**
 * Theme Builder structured-control field primitives (docs/chart-theming-scope.md
 * §5).
 *
 * The structured-control panels are forms of scalar controls over the draft
 * config — a labelled color, number, enum, or text per config key. This is the
 * small shared vocabulary they're built from, so the panels read declaratively
 * and look identical:
 *
 *  - `Accordion` — the single-expand sections a panel groups its controls into.
 *  - `ColorRow` / `NumberRow` / `SelectRow` / `TextRow` / `WeightRow` — one
 *    labelled control each.
 *
 * Each control reports a value or `undefined` (cleared); the panel writes it
 * through `mutateDraftConfig` + `setConfigValue`, where `undefined` deletes the
 * key for a minimal diff. The Color panel keeps its own swatch/gradient controls
 * for the color families. Leaf coercion (`asString`/`asNumber`/`asBoolean`) plus
 * `enumValue`/`countSet` live in core `theme-controls.ts` with the path get/set.
 */

import { useRef, useState, type ReactNode } from 'react';
import { Button } from './Button';
import { ColorField } from './ColorField';
import { SelectControl, type SelectControlOption } from './SelectControl';
import styles from './ThemeFields.module.css';

/**
 * A labelled color. When unset, the swatch shows `fallback` (the rendered
 * default) with a "Default" note rather than a blank chip; once set, a Clear
 * deletes the key.
 */
export function ColorRow({
  label,
  name,
  value,
  fallback,
  onChange,
  onClear,
}: {
  label: string;
  /** Accessible name when `label` is too generic to stand alone (e.g. "Size"). */
  name?: string;
  value: string | undefined;
  fallback: string;
  onChange: (hex: string) => void;
  onClear?: () => void;
}) {
  return (
    <div className={styles.field}>
      <span className={styles.fieldLabel}>{label}</span>
      <div className={styles.control}>
        <ColorField value={value ?? fallback} label={name ?? label} onChange={onChange} hex />
        {value !== undefined ? (
          onClear && (
            <Button variant="ghost" onClick={onClear}>
              Clear
            </Button>
          )
        ) : (
          <span className={styles.defaultNote}>Default</span>
        )}
      </div>
    </div>
  );
}

/**
 * A labelled number. Local text state so a transient entry (a lone "-", a
 * half-typed value) isn't rejected mid-keystroke — commits a finished number,
 * deletes the key when emptied, reverts to the committed value on blur. Resyncs
 * to an outside change (another control, a JSON edit) the same way ColorField's
 * hex field does.
 */
export function NumberRow({
  label,
  name,
  value,
  onChange,
  min,
  max,
  step,
  unit,
}: {
  label: string;
  /** Accessible name when `label` is too generic to stand alone (e.g. "Size"). */
  name?: string;
  value: number | undefined;
  onChange: (value: number | undefined) => void;
  min?: number;
  max?: number;
  step?: number;
  unit?: string;
}) {
  const [text, setText] = useState(value === undefined ? '' : String(value));
  const [synced, setSynced] = useState(value);
  if (value !== synced) {
    setSynced(value);
    const typed = text.trim() === '' ? undefined : Number(text);
    if (typed !== value) setText(value === undefined ? '' : String(value));
  }

  return (
    <div className={styles.field}>
      <span className={styles.fieldLabel}>{label}</span>
      <div className={styles.control}>
        <input
          type="number"
          className={styles.number}
          aria-label={name ?? label}
          value={text}
          min={min}
          max={max}
          step={step}
          onChange={(e) => {
            const raw = e.target.value;
            setText(raw);
            const t = raw.trim();
            if (t === '') onChange(undefined);
            else if (t !== '-' && Number.isFinite(Number(t))) onChange(Number(t));
          }}
          onBlur={() => setText(value === undefined ? '' : String(value))}
        />
        {unit && <span className={styles.unit}>{unit}</span>}
      </div>
    </div>
  );
}

/**
 * Progressive-disclosure accordion: single-expand (one section open at a time, or
 * none), grouping a panel's controls by sub-domain. The Theme Builder
 * standardizes on it across every panel for consistency; flat `role="group"`
 * headings remain the documented alternative for groups read in full (arch 10 →
 * Organizing a large control surface).
 *
 * APG accordion: each header is a `<button>` inside a heading, toggling a
 * `role="region"` panel via `aria-expanded`/`aria-controls`; Up/Down move between
 * headers, Home/End jump to the ends. An optional `badge` (count of set
 * properties) makes overrides scannable while collapsed (NN/g recognition).
 */
export interface AccordionSection {
  id: string;
  title: string;
  /** Count of set properties in this section, shown collapsed as a recognition cue. */
  badge?: number;
  /** One-line description, shown at the top of the open panel. */
  hint?: string;
  children: ReactNode;
}

export function Accordion({
  sections,
  idPrefix,
  defaultOpenId,
}: {
  sections: AccordionSection[];
  /** Namespaces the header/panel ids so multiple accordions can't collide. */
  idPrefix: string;
  /** Section open on first render; defaults to the first. */
  defaultOpenId?: string;
}) {
  const [openId, setOpenId] = useState<string | null>(defaultOpenId ?? sections[0]?.id ?? null);
  const containerRef = useRef<HTMLDivElement>(null);

  const focusHeader = (index: number) => {
    const headers =
      containerRef.current?.querySelectorAll<HTMLButtonElement>('[data-accordion-header]');
    if (!headers || headers.length === 0) return;
    const n = headers.length;
    headers[((index % n) + n) % n].focus();
  };

  const onHeaderKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>, i: number) => {
    const move =
      e.key === 'ArrowDown'
        ? i + 1
        : e.key === 'ArrowUp'
          ? i - 1
          : e.key === 'Home'
            ? 0
            : e.key === 'End'
              ? sections.length - 1
              : null;
    if (move === null) return;
    e.preventDefault();
    focusHeader(move);
  };

  return (
    <div className={styles.accordion} ref={containerRef}>
      {sections.map((section, i) => {
        const open = section.id === openId;
        const headerId = `${idPrefix}-acc-h-${section.id}`;
        const panelId = `${idPrefix}-acc-p-${section.id}`;
        return (
          <div key={section.id} className={styles.accSection}>
            <h4 className={styles.accHeading}>
              <button
                type="button"
                data-accordion-header
                id={headerId}
                className={styles.accHeader}
                aria-expanded={open}
                aria-controls={panelId}
                onClick={() => setOpenId(open ? null : section.id)}
                onKeyDown={(e) => onHeaderKeyDown(e, i)}
              >
                <span className={styles.accCaret} aria-hidden="true">
                  {open ? '▾' : '▸'}
                </span>
                <span className={styles.accTitle}>{section.title}</span>
                {section.badge ? (
                  <span className={styles.accBadge} aria-label={`${section.badge} set`}>
                    {section.badge}
                  </span>
                ) : null}
              </button>
            </h4>
            <div
              id={panelId}
              role="region"
              aria-labelledby={headerId}
              className={styles.accPanel}
              hidden={!open}
            >
              {section.hint && <p className={styles.hint}>{section.hint}</p>}
              {section.children}
            </div>
          </div>
        );
      })}
    </div>
  );
}

/**
 * A labelled enum, on the app's SelectControl. The caller maps the config value
 * to/from option strings; an unset config key is conventionally the `''` option
 * ("Theme default"), so the trigger always shows a current choice.
 */
export function SelectRow<V extends string>({
  id,
  label,
  name,
  options,
  value,
  onSelect,
}: {
  id: string;
  label: string;
  /** Accessible name when `label` is too generic to stand alone (e.g. "Weight"). */
  name?: string;
  options: ReadonlyArray<SelectControlOption<V>>;
  value: V;
  onSelect: (value: V) => void;
}) {
  return (
    <div className={styles.field}>
      <span className={styles.fieldLabel}>{label}</span>
      <SelectControl
        id={id}
        label={name ?? label}
        options={options}
        value={value}
        onSelect={onSelect}
      />
    </div>
  );
}

/**
 * A labelled free-text field — for config leaves with no closed option set (d3
 * number/time format strings, the count title). Bound straight to the value
 * (strings round-trip cleanly, so no transient-entry buffering like NumberRow);
 * an empty field clears the key.
 */
export function TextRow({
  label,
  name,
  value,
  placeholder,
  onChange,
}: {
  label: string;
  name?: string;
  value: string | undefined;
  placeholder?: string;
  onChange: (value: string | undefined) => void;
}) {
  return (
    <div className={styles.field}>
      <span className={styles.fieldLabel}>{label}</span>
      <div className={styles.control}>
        <input
          type="text"
          className={styles.text}
          aria-label={name ?? label}
          value={value ?? ''}
          placeholder={placeholder}
          spellCheck={false}
          onChange={(e) => onChange(e.target.value === '' ? undefined : e.target.value)}
        />
      </div>
    </div>
  );
}

/**
 * Font weight as a tri-state-ish select shared by the Type / Title / Headers
 * panels: "Theme default" plus the nine numeric weights. A static face
 * faux-renders weights it doesn't carry; a variable font's weight axis is fully
 * selectable. A config weight outside the nine (a hand-edit) reads back as
 * "Theme default" rather than a blank trigger.
 */
const WEIGHT_OPTIONS: SelectControlOption<string>[] = [
  { value: '', label: 'Theme default' },
  { value: '100', label: 'Thin' },
  { value: '200', label: 'Extra Light' },
  { value: '300', label: 'Light' },
  { value: '400', label: 'Normal' },
  { value: '500', label: 'Medium' },
  { value: '600', label: 'Semibold' },
  { value: '700', label: 'Bold' },
  { value: '800', label: 'Extra Bold' },
  { value: '900', label: 'Black' },
];

function weightOption(v: unknown): string {
  if (typeof v === 'number') {
    const s = String(v);
    return WEIGHT_OPTIONS.some((o) => o.value === s) ? s : '';
  }
  if (v === 'normal') return '400';
  if (v === 'bold') return '700';
  return '';
}

export function WeightRow({
  id,
  label,
  name,
  value,
  onChange,
}: {
  id: string;
  label: string;
  name?: string;
  value: unknown;
  onChange: (weight: number | undefined) => void;
}) {
  return (
    <SelectRow
      id={id}
      label={label}
      name={name}
      options={WEIGHT_OPTIONS}
      value={weightOption(value)}
      onSelect={(w) => onChange(w === '' ? undefined : Number(w))}
    />
  );
}
