// The track's tunable surface, lifted out of TechnoEngine so it can be changed
// while the music is playing.
//
// Three things need this and none of them can use a `const` in a module body:
// the ear pass (a human turns a knob and hears it on the next beat), the night
// arc (arc.ts scales voice levels as the night builds), and the Phase-4 DJ role,
// whose control panel is specified as this exact surface (BUILD_PLAN §4).
//
// Defaults reproduce the Phase-1 engine's hardcoded values exactly, so lifting
// them changed no sound. Anything John retunes in the ear pass gets pasted back
// over DEFAULT_MIX via `formatMix()`.

export type VoiceName = 'kick' | 'hat' | 'bass' | 'pad';

export const VOICE_NAMES: readonly VoiceName[] = ['kick', 'hat', 'bass', 'pad'];

export interface KickParams {
  /** Beater pitch at the transient. */
  startHz: number;
  /** Where the drop lands — the body of the cone. */
  endHz: number;
  /** Seconds for the pitch drop. Longer reads as a bigger speaker. */
  dropS: number;
  attackS: number;
  decayS: number;
}

export interface HatParams {
  /** Highpass corner. Lower lets more of the noise body through. */
  hpHz: number;
  decayS: number;
}

export interface BassParams {
  rootHz: number;
  /** Per-note filter sweep floor and peak — the "wow" on each 16th. */
  filterLoHz: number;
  filterHiHz: number;
  q: number;
  decayS: number;
  /** Cents of detune between the two saws. The whole character lives here. */
  detuneCents: number;
}

export interface PadParams {
  lpHz: number;
  /** Length of the fade-in/out cycle, in bars. */
  bars: number;
}

export interface TrackMix {
  /** Post-everything output level. */
  master: number;
  /** Per-voice peak gains, pre-filter. */
  levels: Record<VoiceName, number>;
  /** Ear-pass solo/mute. A muted voice is still scheduled, just silent. */
  enabled: Record<VoiceName, boolean>;
  kick: KickParams;
  hat: HatParams;
  bass: BassParams;
  pad: PadParams;
  /** Filter resonance at each location — the door's peak is the "through a wall" cue. */
  doorQ: number;
  floorQ: number;
}

export const DEFAULT_MIX: TrackMix = {
  master: 0.8,
  levels: { kick: 0.9, hat: 0.25, bass: 0.11, pad: 0.05 },
  enabled: { kick: true, hat: true, bass: true, pad: true },
  kick: { startHz: 150, endHz: 50, dropS: 0.06, attackS: 0.008, decayS: 0.34 },
  hat: { hpHz: 7000, decayS: 0.04 },
  bass: { rootHz: 55, filterLoHz: 180, filterHiHz: 900, q: 8, decayS: 0.12, detuneCents: 8 },
  pad: { lpHz: 600, bars: 8 },
  doorQ: 6,
  floorQ: 0.7,
};

/** Bounds every knob, so a slider (or a typo in the console) cannot blow an ear. */
export const MIX_RANGES = {
  master: [0, 1.2],
  level: [0, 1.5],
  'kick.startHz': [60, 400],
  'kick.endHz': [25, 120],
  'kick.dropS': [0.01, 0.3],
  'kick.attackS': [0.001, 0.05],
  'kick.decayS': [0.05, 1],
  'hat.hpHz': [2000, 14000],
  'hat.decayS': [0.005, 0.2],
  'bass.rootHz': [30, 110],
  'bass.filterLoHz': [60, 600],
  'bass.filterHiHz': [300, 4000],
  'bass.q': [0.5, 20],
  'bass.decayS': [0.02, 0.5],
  'bass.detuneCents': [0, 40],
  'pad.lpHz': [200, 4000],
  'pad.bars': [1, 16],
  doorQ: [0.5, 20],
  floorQ: [0.1, 4],
} as const satisfies Record<string, readonly [number, number]>;

export const clampTo = (v: number, range: readonly [number, number]): number =>
  Number.isFinite(v) ? Math.max(range[0], Math.min(range[1], v)) : range[0];

/** Deep copy — callers mutate their own mix without touching DEFAULT_MIX. */
export function cloneMix(m: TrackMix): TrackMix {
  return {
    master: m.master,
    levels: { ...m.levels },
    enabled: { ...m.enabled },
    kick: { ...m.kick },
    hat: { ...m.hat },
    bass: { ...m.bass },
    pad: { ...m.pad },
    doorQ: m.doorQ,
    floorQ: m.floorQ,
  };
}

/**
 * Effective gain for a voice: its level, muted by `enabled`, scaled by whatever
 * the night arc says this voice should be doing right now.
 *
 * Kept here rather than in the engine so the ear-pass rig can show the same
 * number the scheduler will actually use.
 */
export function voiceGain(mix: TrackMix, voice: VoiceName, arcScale = 1): number {
  if (!mix.enabled[voice]) return 0;
  return Math.max(0, mix.levels[voice] * arcScale);
}

/** True when exactly one voice is unmuted — the ear pass's soloed state. */
export const soloedVoice = (mix: TrackMix): VoiceName | null => {
  const on = VOICE_NAMES.filter((v) => mix.enabled[v]);
  return on.length === 1 ? (on[0] ?? null) : null;
};

const n = (v: number): string => (Number.isInteger(v) ? String(v) : String(Number(v.toFixed(4))));

/**
 * Emit a mix as a pasteable `TrackMix` literal.
 *
 * The ear pass is a human saying "more click on the kick" and me turning that
 * into numbers; without a dump button those numbers die with the tab. This is
 * how a tuning session becomes a diff.
 */
export function formatMix(m: TrackMix): string {
  return `{
  master: ${n(m.master)},
  levels: { kick: ${n(m.levels.kick)}, hat: ${n(m.levels.hat)}, bass: ${n(m.levels.bass)}, pad: ${n(m.levels.pad)} },
  enabled: { kick: true, hat: true, bass: true, pad: true },
  kick: { startHz: ${n(m.kick.startHz)}, endHz: ${n(m.kick.endHz)}, dropS: ${n(m.kick.dropS)}, attackS: ${n(m.kick.attackS)}, decayS: ${n(m.kick.decayS)} },
  hat: { hpHz: ${n(m.hat.hpHz)}, decayS: ${n(m.hat.decayS)} },
  bass: { rootHz: ${n(m.bass.rootHz)}, filterLoHz: ${n(m.bass.filterLoHz)}, filterHiHz: ${n(m.bass.filterHiHz)}, q: ${n(m.bass.q)}, decayS: ${n(m.bass.decayS)}, detuneCents: ${n(m.bass.detuneCents)} },
  pad: { lpHz: ${n(m.pad.lpHz)}, bars: ${n(m.pad.bars)} },
  doorQ: ${n(m.doorQ)},
  floorQ: ${n(m.floorQ)},
}`;
}
