import { interpolateCividis, interpolateViridis, schemeSet2 } from 'd3-scale-chromatic';

/**
 * Colour palette resolver shared across every chart in the lib.
 *
 * Per ADR-0023, palettes are chosen from a *fixed* colour-blind-safe
 * set so no contributor can accidentally ship a chart with a non-
 * accessible scheme. Two families:
 *
 *  - **Sequential** — for quantitative orderings (a continuous value
 *    encoded along one ramp). Default: Viridis (perceptually
 *    uniform, colour-blind safe, prints OK in greyscale). Cividis
 *    is the dark-mode-aware sibling.
 *  - **Categorical** — for nominal categories (e.g. event outcomes).
 *    Default: ColorBrewer Set2 — 8 hues, colour-blind safe per
 *    ColorBrewer's review.
 *
 * Consumers can NOT override these from outside the lib — an ESLint
 * rule (added in this PR) blocks direct imports of
 * `d3-scale-chromatic` outside this file so the lib stays the only
 * source of palettes.
 *
 * In addition to the two scale families, the lib exposes two
 * curated colour constants:
 *
 *  - {@link DEFAULT_BAR_FILL} — single fixed fill used by
 *    `<lib-bar-chart>` when no per-bar encoding is requested.
 *    Picked to be visually neutral against both light and dark
 *    surfaces and colour-blind-safe (Wong 2011 "blue" deuteranopia-
 *    tested).
 *  - {@link semanticStatusColors} — small map of intent-bearing
 *    colours (`success / warning / error / info / neutral`)
 *    consumers can opt into via a `colorMap` on the donut chart
 *    when the categorical axis carries *meaning* rather than just
 *    distinct labels. Values are tuned for AA contrast on white
 *    surfaces and remain distinguishable under deuteranopia /
 *    protanopia simulation (red and orange differ in lightness, not
 *    just hue).
 */

export type ColorScheme = 'sequential' | 'categorical';

/**
 * Default fill colour for bars when no per-bar encoding applies.
 * Softer brand-aligned blue — AA contrast against white surfaces,
 * still readable on a dark surface. Curated alongside
 * {@link semanticStatusColors} so the chart palette reads as one
 * coherent set rather than a Tailwind grab-bag.
 */
export const DEFAULT_BAR_FILL = '#4075e7';

/**
 * Intent-bearing colours for the small set of statuses APF charts
 * deal with. Consumers cannot author their own colours from outside
 * the lib (per ADR-0023), but they can pick from this curated map
 * by name — keeps the colour-blind-safety guarantee while letting
 * the donut chart encode meaning (success = green, denied =
 * orange). Values are softer than the Tailwind defaults — chart
 * surfaces benefit from a lower-saturation palette so adjacent
 * slices/bars don't visually fight when they share a tile.
 */
export const semanticStatusColors = {
  success: '#46ac6b', // muted green
  warning: '#f38043', // muted orange
  error: '#eb5252', // muted red
  info: '#4075e7', // muted blue (same as DEFAULT_BAR_FILL)
  neutral: '#6b7280', // gray
} as const;

export type SemanticStatus = keyof typeof semanticStatusColors;

/**
 * Build a function that maps a numeric input in [0, 1] to a hex
 * colour from the sequential ramp. Use this for value-driven fills
 * (bar height encoded as colour, heatmap density, …).
 *
 * `mode` is the active theme — `dark` swaps Viridis for Cividis,
 * which retains its colour-blind safety while being visually
 * lighter against a dark surface.
 */
export function sequentialScale(mode: 'light' | 'dark'): (t: number) => string {
  return mode === 'dark' ? interpolateCividis : interpolateViridis;
}

/**
 * Return the categorical palette as a flat string array. Consumers
 * cycle the array; if they exceed the length (≥ 8 categories), they
 * should reconsider whether categorical is the right encoding (per
 * the chart literature, ≥ 8 categorical colours stop being
 * distinguishable to most viewers, colour-blind or not).
 */
export function categoricalPalette(): readonly string[] {
  return schemeSet2 as readonly string[];
}

/**
 * Helper: resolve a colour scheme + count to an array of strings.
 * Used by the chart components to feed `Plot.scale({color: {range:
 * […]}})` without having to know which underlying palette is
 * active.
 */
export function paletteFor(
  scheme: ColorScheme,
  mode: 'light' | 'dark',
  count: number,
): readonly string[] {
  if (scheme === 'categorical') {
    return categoricalPalette().slice(0, count);
  }
  const interpolate = sequentialScale(mode);
  // Sample the ramp at `count` evenly-spaced points. count === 1
  // picks the midpoint so a single-series sequential chart doesn't
  // render the darkest extreme.
  if (count <= 1) {
    return [interpolate(0.5)];
  }
  const step = 1 / (count - 1);
  return Array.from({ length: count }, (_, i) => interpolate(i * step));
}
