/* eslint-disable import-x/extensions */
// OKLCH color-scale generator. Palettes are generated in OKLCH so a single
// lightness ramp is perceptually uniform across every hue (HSL lightness is not
// — yellow at HSL L=56 reads far brighter than blue at the same L). Chroma is
// specified *relative to the sRGB gamut*: each step requests a fraction of the
// maximum chroma sRGB can render at that step's lightness and hue. The fraction
// curve is shared by every palette and tiered by usage (muted surface tints,
// medium highlight mids, near-vivid interactive and text steps), so all hues are
// equally saturated relative to what's possible — no per-palette chroma table.

import Color from "colorjs.io";
import type { PaletteSpec } from "./paletteSpecs.ts";
import type { Mode, ScaleNum } from "./tokenScaleMap.ts";

export type ColorScale = Record<ScaleNum, string>;

// step:                     1     2     3     4     5     6     7     8     9    10    11
const lightnessRamps = {
  grayscale: {
    dark: [0.18, 0.24, 0.28, 0.32, 0.36, 0.4, 0.45, 0.795, 0.865, 0.96, 1],
    // only diff is on the first 2 values
    light: [1, 0.98, 0.948, 0.89, 0.85, 0.79, 0.61, 0.54, 0.48, 0.42, 0.27],
  },
  accent: {
    dark: [0.18, 0.22, 0.26, 0.3, 0.34, 0.4, 0.5, 0.7, 0.82, 0.865, 0.955],
    light: [
      0.988, 0.968, 0.948, 0.89, 0.85, 0.79, 0.62, 0.54, 0.48, 0.42, 0.27,
    ],
  },
  brightAccent: {
    dark: [0.28, 0.32, 0.4, 0.45, 0.5, 0.53, 0.56, 0.8, 0.86, 0.9, 0.955],
    light: [0.988, 0.968, 0.948, 0.928, 0.89, 0.82, 0.8, 0.6, 0.56, 0.44, 0.27],
  },
} as const;

// Fraction of the max in-gamut chroma requested at each step. Usage tiers
// mirror the old HSL generator's 56/82/96 saturation bands: steps 1-3 pale
// surfaces, 4-6 highlight/message tints, 7-11 interactive fills and text.
const relativeChromaCurve: Record<Mode, number[]> = {
  dark: [0.6, 0.68, 0.72, 0.76, 0.78, 0.84, 0.88, 0.88, 0.85, 0.82, 0.86],
  light: [0.5, 0.52, 0.55, 0.6, 0.75, 0.78, 0.92, 0.97, 0.97, 0.97, 0.95],
};

// Largest OKLCH chroma still inside the sRGB gamut at this lightness/hue.
export const maxSrgbChroma = (lightness: number, hue: number): number => {
  let low = 0;
  let high = 0.5;
  for (let i = 0; i < 20; i++) {
    const mid = (low + high) / 2;
    if (new Color("oklch", [lightness, mid, hue]).to("srgb").inGamut()) {
      low = mid;
    } else {
      high = mid;
    }
  }
  return low;
};

const toHex = (lightness: number, chroma: number, hue: number): string => {
  const color = new Color("oklch", [lightness, chroma, hue]).to("srgb");
  const hex = color.toGamut({ method: "css" }).toString({ format: "hex" });
  // colorjs collapses to 3-digit shorthand (#050); expand so appended alpha
  // suffixes (e.g. selection = step + "40") stay valid 8-digit hex.
  const full =
    hex.length === 4 ? hex.replace(/^#(.)(.)(.)$/, "#$1$1$2$2$3$3") : hex;
  return full.toUpperCase();
};

export const createColorScale = (spec: PaletteSpec, mode: Mode): ColorScale => {
  const hueHi = spec.hueHi ?? spec.hue;
  const hueLo = spec.hueLo ?? spec.hue;
  const intensity = spec.intensity ?? 1;
  const ramp = lightnessRamps[spec.type][mode];
  const steps = ramp.map((lightness, index) => {
    const hue = hueLo + (hueHi - hueLo) * lightness;
    const chroma =
      relativeChromaCurve[mode][index]! *
      intensity *
      maxSrgbChroma(lightness, hue);
    return toHex(lightness, chroma, hue);
  });
  return Object.fromEntries(
    steps.map((hex, index) => [index + 1, hex]),
  ) as unknown as ColorScale;
};
