// ---------------------------------------------------------------------------
// customer-grid / icons.tsx
// Wave-8 items I18 + I20 — the React half of the grid's icon vocabulary.
//
// The GEOMETRY lives in iconShapes.ts, which this file and the glide canvas
// sprites both read. Two painters, one source: glide's headerIcons API takes a
// function returning SVG *source* (a header icon can never be a React
// component), so the shapes are kept as data and each painter gets its own thin
// renderer. Hand-copying paths into a template literal instead would guarantee
// the header and the Fields panel eventually disagree about what a "date" is.
//
// Components only in this file — the constants/sprite builders sit next door so
// fast refresh keeps working (oxlint react/only-export-components).
// ---------------------------------------------------------------------------

import type { ReactNode } from "react";
import type { DisplayMode, FieldType, FolderIcon, FolderTone } from "./types";
import { DEFAULT_FOLDER_SHAPE, DEFAULT_FOLDER_TONE } from "./types";
import type { IconShape } from "./iconShapes";
import type { ChartKindKey } from "./iconShapes";
import {
  CHART_KIND_SHAPES,
  CHART_KIND_TONE,
  FOLDER_SHAPE_PATHS,
  folderTonePaint,
  MODE_SHAPES,
  TYPE_SHAPES,
} from "./iconShapes";

/**
 * ⭐ WAVE 30 item 2 (R5) — `shapes` IS OPTIONAL, and the `?? []` is the whole point.
 *
 * Every caller reaches this through a TOTAL `Record<K, IconShape[]>`, so TypeScript promises the
 * lookup always hits. That promise is only as good as the key: `type` arrives over the WIRE, and
 * a field whose type is not in the union — a kind added server-side, a stored definition from
 * before a rename — makes `TYPE_SHAPES[type]` `undefined` at runtime with tsc none the wiser.
 * `undefined.map` is a TypeError, and until this wave a TypeError anywhere unmounted the entire
 * React tree: a blank white page, which is exactly what a user calls "it crashes".
 *
 * ⚠ RANKED SECOND AND SHIPPED ANYWAY (R5). Every field in `odoo_relational.order_fields` carries
 * a type present in `TYPE_SHAPES`, and the server's `UT_FIELD_TYPES` filters unknown types off
 * the wire — so this is LATENT, not the proven cause of owner item 2. The reporter was a user and
 * cannot be re-interviewed, so both candidate causes are fixed rather than one guessed at.
 *
 * An absent lookup now paints NO GLYPH, which is the honest degradation: the row still renders,
 * still carries its label, and the missing mark says "this type has no icon" rather than taking
 * the product down to say it.
 */
function Shapes({ shapes }: { shapes?: IconShape[] }) {
  return (
    <>
      {(shapes ?? []).map((s, i) =>
        s.fill ? (
          <path key={i} d={s.d} fill="currentColor" />
        ) : (
          <path
            key={i}
            d={s.d}
            fill="none"
            stroke="currentColor"
            strokeWidth={1.35}
            strokeLinecap="round"
            strokeLinejoin="round"
          />
        )
      )}
    </>
  );
}

/** The field-type mark for panels/menus (I20). Decorative by default — the row
 *  it sits in already names the field; a screen reader reading "currency icon"
 *  before every label is noise. Pass `title` where the icon teaches the
 *  vocabulary (the Fields panel) so hovering it names the type.
 *
 *  Wave-14 C-FLDSEL: `type` widened to carry the "measure" PSEUDO-KIND, which is not a
 *  `FieldType` (a measure column's real type is currency/int/pct — widening the union would
 *  let it leak into every switch that renders cells). It paints the CURRENCY mark, which is
 *  the decision the create-picker already shipped and its reason still holds: inventing a
 *  17th glyph for a pseudo-kind would put a mark on screen that no column header ever wears.
 *  Every caller that used to spell that mapping out at the call site now stops. */
export function FieldTypeIcon({
  type,
  size = 14,
  title,
}: {
  type: FieldType | "measure";
  size?: number;
  title?: string;
}) {
  return (
    <svg
      className="cg-type-icon"
      width={size}
      height={size}
      viewBox="0 0 16 16"
      aria-hidden={title ? undefined : true}
      role={title ? "img" : undefined}
      aria-label={title}
    >
      {title && <title>{title}</title>}
      <Shapes shapes={TYPE_SHAPES[type === "measure" ? "currency" : type]} />
    </svg>
  );
}

/** The display-mode mark for the View switcher (I18). */
export function ModeIcon({ mode, size = 14 }: { mode: DisplayMode; size?: number }) {
  return (
    <svg className="cg-mode-icon" width={size} height={size} viewBox="0 0 16 16" aria-hidden>
      <Shapes shapes={MODE_SHAPES[mode]} />
    </svg>
  );
}

/**
 * Wave-9 I14 — the SAME mode mark, in a pastel tone (the flyout's "pastel-coloured icons").
 *
 * A separate component rather than a `tone` prop on ModeIcon: every existing ModeIcon call
 * site inherits `currentColor` from the row it sits in, and quietly giving that a colour
 * would repaint the toolbar switcher and every radio row along with the flyout. The geometry
 * is still the one source — only the paint differs.
 */
export function ToneModeIcon({
  mode,
  tone,
  size = 15,
}: {
  mode: DisplayMode;
  tone: FolderTone;
  size?: number;
}) {
  const paint = folderTonePaint(tone);
  return (
    <svg
      className="cg-mode-icon cg-tone-icon"
      width={size}
      height={size}
      viewBox="0 0 16 16"
      aria-hidden
    >
      {/* R5: `?? []` for the same reason as `Shapes` — `mode` is a STORED display mode off
          the wire, so a view saved under a kind this build no longer knows makes the lookup
          undefined and `.map` a TypeError. */}
      {(MODE_SHAPES[mode] ?? []).map((s, i) =>
        s.fill ? (
          <path key={i} d={s.d} fill={paint.stroke} />
        ) : (
          <path
            key={i}
            d={s.d}
            // OUTLINE ONLY — the Airtable register (owner, 2026-07-29: "it doesn't have a
            // full color, only the linings"). This used to tint the first path with
            // `paint.fill`, which made every mark read as a solid pastel BLOCK with the
            // drawing hidden inside it: at 16px the grid's own lines were invisible against
            // their own fill. The tone now lives entirely in the STROKE, which is the `-d`
            // weight and is the only part that was ever carrying the shape.
            fill="none"
            stroke={paint.stroke}
            strokeWidth={1.35}
            strokeLinecap="round"
            strokeLinejoin="round"
          />
        )
      )}
    </svg>
  );
}

/**
 * Wave-9 I16 — the chart-kind mark, in its pastel tone. Same two-painter discipline as
 * everything else here: the geometry is in iconShapes.ts, this is only a renderer.
 */
export function ChartKindIcon({
  kind,
  size = 15,
}: {
  kind: ChartKindKey;
  size?: number;
}) {
  const paint = folderTonePaint(CHART_KIND_TONE[kind]);
  return (
    <svg
      className="cg-mode-icon cg-tone-icon"
      width={size}
      height={size}
      viewBox="0 0 16 16"
      aria-hidden
    >
      {/* R5: same guard, same reason — `kind` comes from a saved chart definition. */}
      {(CHART_KIND_SHAPES[kind] ?? []).map((s, i) => (
        <path
          key={i}
          d={s.d}
          // Same rule as ToneModeIcon: the FIRST path is the shape's body, so it takes the
          // pastel fill and the rest stay strokes over it. Bar/line are pure strokes, so
          // their first path has no interior to fill and this is a no-op for them.
          fill={i === 0 &&
            (kind === "area" || kind === "donut" || kind === "kpi" || kind === "table")
            ? paint.fill
            : "none"}
          stroke={paint.stroke}
          strokeWidth={1.35}
          strokeLinecap="round"
          strokeLinejoin="round"
        />
      ))}
    </svg>
  );
}

// ---------------------------------------------------------------------------
// MENU ACTION ICONS (wave-7 item W4, moved here by wave-14 item 19).
//
// These lived privately in ColumnMenu.tsx, which was fine while the column menu was the only
// menu with icons. Item 19 puts icons on the VIEW menu too, and the alternative to one home
// was a second hand-drawn padlock that would drift from the first — the same argument
// iconShapes.ts records for the canvas/React split, one layer up.
//
// Exported as a COMPONENT taking a name, not as a record of nodes: a record would be a
// non-component export from a components file, which is the `only-export-components` warning
// the whole icons.tsx/iconShapes.ts split exists to avoid.
// ---------------------------------------------------------------------------

const mi = { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none" } as const;
const miStroke = {
  stroke: "currentColor",
  strokeWidth: 1.3,
  strokeLinecap: "round" as const,
  strokeLinejoin: "round" as const,
};

const MENU_ICONS = {
  duplicate: (
    <>
      <rect x="5.5" y="5.5" width="8" height="8" rx="1.5" {...miStroke} />
      <path d="M10.5 2.5h-7a1 1 0 0 0-1 1v7" {...miStroke} />
    </>
  ),
  insertLeft: (
    <>
      <line x1="3" y1="2.5" x2="3" y2="13.5" {...miStroke} />
      <path d="M13.5 8H6.5M9.2 5.3 6.5 8l2.7 2.7" {...miStroke} />
    </>
  ),
  insertRight: (
    <>
      <line x1="13" y1="2.5" x2="13" y2="13.5" {...miStroke} />
      <path d="M2.5 8h7M6.8 5.3 9.5 8l-2.7 2.7" {...miStroke} />
    </>
  ),
  addEnd: <path d="M8 3v10M3 8h10" {...miStroke} />,
  rename: <path d="m9.8 3.2 3 3L6.2 12.8l-3.6.6.6-3.6 6.6-6.6Z" {...miStroke} />,
  description: <path d="M3 4.5h10M3 8h10M3 11.5h6" {...miStroke} />,
  permissions: (
    <>
      <rect x="3.5" y="7" width="9" height="6" rx="1" {...miStroke} />
      <path d="M5.5 7V5.2a2.5 2.5 0 0 1 5 0V7" {...miStroke} />
    </>
  ),
  /** The same padlock with an OPEN shackle — "unlock" as the picture of undoing the one above,
   *  rather than as a padlock with a slash, which every other `off` row already uses. */
  unlock: (
    <>
      <rect x="3.5" y="7" width="9" height="6" rx="1" {...miStroke} />
      <path d="M5.5 7V5.2a2.5 2.5 0 0 1 5 0" {...miStroke} />
    </>
  ),
  format: (
    <>
      <path d="M3 5h5.5M12.5 5H13M3 11h1M7 11h6" {...miStroke} />
      <circle cx="10.5" cy="5" r="1.6" {...miStroke} />
      <circle cx="4.9" cy="11" r="1.6" {...miStroke} />
    </>
  ),
  swap: (
    <path
      d="M3.5 6h9M10.2 3.7 12.5 6l-2.3 2.3M12.5 10.5h-9M5.8 8.2 3.5 10.5l2.3 2.3"
      {...miStroke}
    />
  ),
  sortAsc: (
    <>
      <path d="M4.5 3v10M4.5 13l-2-2M4.5 13l2-2" {...miStroke} />
      <path d="M8.5 4.5h2M8.5 8h3.5M8.5 11.5h5" {...miStroke} />
    </>
  ),
  sortDesc: (
    <>
      <path d="M4.5 3v10M4.5 13l-2-2M4.5 13l2-2" {...miStroke} />
      <path d="M8.5 4.5h5M8.5 8h3.5M8.5 11.5h2" {...miStroke} />
    </>
  ),
  off: (
    <>
      <circle cx="8" cy="8" r="5.5" {...miStroke} />
      <line x1="4.4" y1="11.6" x2="11.6" y2="4.4" {...miStroke} />
    </>
  ),
  filter: <path d="M2.5 3.5h11L9.5 8.5v3.8L6.5 14V8.5L2.5 3.5Z" {...miStroke} />,
  group: (
    <>
      <line x1="2.5" y1="4" x2="13.5" y2="4" {...miStroke} />
      <line x1="5.5" y1="8" x2="13.5" y2="8" {...miStroke} />
      <line x1="5.5" y1="12" x2="13.5" y2="12" {...miStroke} />
    </>
  ),
  pin: (
    <>
      <path d="M6 2.5h4l-.5 4L12 9v1H4V9l2.5-2.5L6 2.5Z" {...miStroke} />
      <line x1="8" y1="10" x2="8" y2="13.5" {...miStroke} />
    </>
  ),
  unpin: (
    <>
      <path d="M6 2.5h4l-.5 4L12 9v1H4V9l2.5-2.5L6 2.5Z" {...miStroke} />
      <line x1="8" y1="10" x2="8" y2="13.5" {...miStroke} />
      <line x1="3" y1="13" x2="13" y2="3" {...miStroke} />
    </>
  ),
  hide: (
    <>
      <path d="M2.5 8s2-3.5 5.5-3.5S13.5 8 13.5 8s-2 3.5-5.5 3.5S2.5 8 2.5 8Z" {...miStroke} />
      <circle cx="8" cy="8" r="1.5" {...miStroke} />
      <line x1="3.5" y1="12.5" x2="12.5" y2="3.5" {...miStroke} />
    </>
  ),
  trash: <path d="M3.5 4.5h9M6.5 4.5V3h3v1.5M5 4.5l.6 8.5h4.8l.6-8.5" {...miStroke} />,
  /** Item 19 — the view menu's own three. */
  download: (
    <>
      <path d="M8 2.5v7M5.3 6.8 8 9.5l2.7-2.7" {...miStroke} />
      <path d="M3 11v1.5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V11" {...miStroke} />
    </>
  ),
  cohortAdd: (
    <>
      <path d="M2.5 4h7M2.5 7.5h5M2.5 11h4" {...miStroke} />
      <path d="M11.5 8.5v5M9 11h5" {...miStroke} />
    </>
  ),
  /**
   * ⭐ W29 close-out — IMPORT's own mark, asked for by E and taken by the integrator while
   * W29-T78 had this menu open anyway.
   *
   * `download` MIRRORED: the same tray, the same stem, the arrowhead at the top instead of the
   * bottom — so Export and Import read as one gesture in two directions rather than as two
   * unrelated glyphs. It replaces a borrowed `cohortAdd` (a LIST with a plus), which named
   * "add to a cohort" on a row that imports a spreadsheet: a borrowed mark is a small lie that
   * a menu repeats every time it opens.
   */
  upload: (
    <>
      <path d="M8 9.5v-7M5.3 5.2 8 2.5l2.7 2.7" {...miStroke} />
      <path d="M3 11v1.5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V11" {...miStroke} />
    </>
  ),
} as const;

export type MenuIconName = keyof typeof MENU_ICONS;

/** One 16px menu glyph, by name. */
export function MenuIcon({ name }: { name: MenuIconName }) {
  return (
    <svg {...mi} aria-hidden>
      {MENU_ICONS[name]}
    </svg>
  );
}

/**
 * One menu action row: icon + label. The icon column is fixed-width so labels align.
 *
 * `icon` is a NAME by default; a node is accepted for the rows whose mark is not part of this
 * vocabulary (the view menu's "Lock as Kanban" wears the KANBAN mark, because the picture of
 * what gets frozen says more than a second padlock).
 */
export function MenuLabel({
  icon,
  text,
}: {
  icon: MenuIconName | ReactNode;
  text: string;
}) {
  return (
    <>
      <span className="cg-mi-ic" aria-hidden>
        {typeof icon === "string" ? <MenuIcon name={icon as MenuIconName} /> : icon}
      </span>
      <span className="cg-mi-text">{text}</span>
    </>
  );
}

/**
 * Wave-14 item 12 — the HIDE-FIELDS mark: an eye with a slash through it.
 *
 * The toolbar button that opens the panel wore `IconFields` (three vertical column rules),
 * which named the NOUN and left the verb to be guessed — the same complaint that renamed the
 * panel "Hide fields" in the first place. A struck-through eye says what the control does.
 *
 * It lives here rather than beside the toolbar's private `Icon*` set because it has two
 * consumers: the toolbar button, and the copy-configuration modal's "Hidden fields" row
 * (item 13), which must wear the SAME mark for the same concept. Paths are inline for the
 * same reason `LockMark`'s are: `iconShapes.ts` holds the geometry the glide CANVAS also
 * paints, and no header ever draws this one.
 */
export function EyeOffIcon({ size = 14 }: { size?: number }) {
  return (
    <svg
      className="cg-eyeoff-icon"
      width={size}
      height={size}
      viewBox="0 0 16 16"
      fill="none"
      aria-hidden
    >
      <path
        d="M2.5 8s2-3.5 5.5-3.5S13.5 8 13.5 8s-2 3.5-5.5 3.5S2.5 8 2.5 8Z"
        stroke="currentColor"
        strokeWidth={1.4}
        strokeLinecap="round"
        strokeLinejoin="round"
      />
      <path
        d="M9.6 8a1.6 1.6 0 1 1-3.2 0 1.6 1.6 0 0 1 3.2 0Z"
        stroke="currentColor"
        strokeWidth={1.4}
        strokeLinejoin="round"
      />
      <path
        d="M3.3 12.7 12.7 3.3"
        stroke="currentColor"
        strokeWidth={1.4}
        strokeLinecap="round"
      />
    </svg>
  );
}

/**
 * Wave-9 contract C3 (I12) — the "this view's mode is frozen" mark. Drawn from the same
 * stroke vocabulary as everything else here; deliberately small and muted, because it
 * annotates the row rather than competing with the mode icon that leads it.
 */
export function LockMark({ size = 11 }: { size?: number }) {
  return (
    <svg
      className="cg-lock-mark"
      width={size}
      height={size}
      viewBox="0 0 16 16"
      aria-hidden
    >
      <path
        d="M4.2 7.2h7.6v5.6H4.2z"
        fill="none"
        stroke="currentColor"
        strokeWidth={1.35}
        strokeLinejoin="round"
      />
      <path
        d="M5.9 7.2V5.5a2.1 2.1 0 0 1 4.2 0v1.7"
        fill="none"
        stroke="currentColor"
        strokeWidth={1.35}
        strokeLinecap="round"
      />
    </svg>
  );
}

/**
 * Wave-9 contract C5 (I15) — a folder's chosen mark. `icon` absent renders the DEFAULT
 * folder shape in grey, which is every pre-wave-9 folder and is exactly what I14 asks for
 * ("existing folders get the folder icon").
 */
export function FolderMark({
  icon,
  size = 14,
}: {
  icon?: FolderIcon;
  size?: number;
}) {
  const shape = icon?.shape ?? DEFAULT_FOLDER_SHAPE;
  const paint = folderTonePaint(icon?.tone ?? DEFAULT_FOLDER_TONE);
  return (
    <svg
      className="cg-folder-mark"
      width={size}
      height={size}
      viewBox="0 0 16 16"
      aria-hidden
    >
      {/* R5: same guard — a folder's shape is stored in nav prefs. */}
      {(FOLDER_SHAPE_PATHS[shape] ?? []).map((s, i) => (
        <path
          key={i}
          d={s.d}
          // Outline only, same ruling as ToneModeIcon — a filled folder sitting directly
          // above outlined view rows would make the rail read as two icon vocabularies.
          fill="none"
          stroke={paint.stroke}
          strokeWidth={1.35}
          strokeLinecap="round"
          strokeLinejoin="round"
        />
      ))}
    </svg>
  );
}
