import { VOICE_NAMES, type VoiceName } from './mix';

// The night's shape, as gain scalars per voice.
//
// A club does not play the same track at 9 PM and at 1 AM, and a room that
// sounds identical for six hours is the main reason a long shift reads as flat.
// So the mix opens thin — kick and a bit of hat, the sound of a room hoping
// people turn up — the bassline arrives once there is a crowd to carry it, the
// pads land at peak, and Last Drinks strips it back so the lights coming up
// feel earned rather than abrupt.
//
// Pure keyframe interpolation: no audio types here, so it is testable in node
// and reusable by the Phase-4 DJ role, which drives this same surface by hand.

/** clockMin is minutes since 21:00; the night ends at 360 (3 AM). */
export const NIGHT_END_MIN = 360;

export type ArcScales = Record<VoiceName, number>;

export interface ArcKeyframe {
  /** Minutes since 21:00. */
  at: number;
  label: string;
  scales: ArcScales;
}

/**
 * Keyframes, ascending by `at`. Deliberately sparse — the interpolation between
 * them is what makes the build feel gradual rather than stepped.
 */
export const ARC: readonly ArcKeyframe[] = [
  // 9 PM. Doors open to an empty room. Kick and a whisper of hat.
  { at: 0, label: 'EMPTY', scales: { kick: 0.8, hat: 0.4, bass: 0, pad: 0 } },
  // 10 PM. First real queue. The bassline turns up with them.
  { at: 60, label: 'FILLING', scales: { kick: 0.95, hat: 0.75, bass: 0.5, pad: 0.15 } },
  // 11 PM. Room has weight.
  { at: 120, label: 'BUILDING', scales: { kick: 1, hat: 0.9, bass: 0.85, pad: 0.5 } },
  // Midnight through 2 AM. Everything, all of it.
  { at: 180, label: 'PEAK', scales: { kick: 1, hat: 1, bass: 1, pad: 1 } },
  { at: 300, label: 'PEAK', scales: { kick: 1, hat: 1, bass: 1, pad: 1 } },
  // 2:30. Last drinks. Pull the low end first — that is what empties a floor.
  { at: 330, label: 'LAST DRINKS', scales: { kick: 0.9, hat: 0.7, bass: 0.55, pad: 0.85 } },
  // 3 AM. Pads over a dying kick while the lights come up.
  { at: NIGHT_END_MIN, label: 'LIGHTS UP', scales: { kick: 0.45, hat: 0.2, bass: 0.12, pad: 0.6 } },
];

const lerp = (a: number, b: number, t: number): number => a + (b - a) * t;

/**
 * Voice scalars at a given clock minute. Clamps outside the night rather than
 * extrapolating — a paused or overrunning clock should hold the edge, not
 * wander into negative gain.
 */
export function arcAt(clockMin: number): ArcScales {
  const t = Number.isFinite(clockMin) ? clockMin : 0;

  const first = ARC[0];
  const last = ARC[ARC.length - 1];
  /* istanbul ignore next -- ARC is a non-empty literal */
  if (!first || !last) return { kick: 1, hat: 1, bass: 1, pad: 1 };

  if (t <= first.at) return { ...first.scales };
  if (t >= last.at) return { ...last.scales };

  for (let i = 1; i < ARC.length; i++) {
    const prev = ARC[i - 1];
    const next = ARC[i];
    if (!prev || !next || t > next.at) continue;
    const span = next.at - prev.at;
    const k = span <= 0 ? 1 : (t - prev.at) / span;
    const out = {} as ArcScales;
    for (const v of VOICE_NAMES) out[v] = lerp(prev.scales[v], next.scales[v], k);
    return out;
  }

  /* istanbul ignore next -- the loop always returns for t inside the range */
  return { ...last.scales };
}

/** Nearest keyframe label at or before this minute — for HUD readouts. */
export function arcLabel(clockMin: number): string {
  const t = Number.isFinite(clockMin) ? clockMin : 0;
  let label = ARC[0]?.label ?? '';
  for (const k of ARC) {
    if (t >= k.at) label = k.label;
  }
  return label;
}

/**
 * One number for "how hard is the room going", 0..1 — the mean of the voice
 * scalars. Handy for anything that wants to follow the night's energy without
 * caring which voice is doing it (lighting, crowd density, the DJ role's meter).
 */
export function arcIntensity(clockMin: number): number {
  const s = arcAt(clockMin);
  return VOICE_NAMES.reduce((sum, v) => sum + s[v], 0) / VOICE_NAMES.length;
}
