// Pure look-ahead scheduling math for TechnoEngine.
//
// The engine runs a 25ms timer; on each wake it asks this module which beats fall
// inside the next `lookaheadMs` of audio time and schedules oscillators for them.
// Keeping the arithmetic here means the beat grid is unit-testable without an
// AudioContext (tests run in node — there is no WebAudio there).

export interface BeatGrid {
  bpm: number;
  /** AudioContext time (ms) at which beat 0 sounds. */
  originMs: number;
}

export interface ScheduledBeat {
  beatIndex: number;
  /** Absolute AudioContext time (ms) the beat sounds at. */
  timeMs: number;
}

export const beatIntervalMs = (bpm: number): number => 60_000 / bpm;

/** Absolute audio time of a given beat index. */
export const beatTimeMs = (grid: BeatGrid, beatIndex: number): number =>
  grid.originMs + beatIndex * beatIntervalMs(grid.bpm);

/**
 * Every beat with index >= fromBeat whose time falls at or before
 * `nowMs + lookaheadMs`. Returned in ascending order; empty when nothing is due.
 *
 * The engine advances its own cursor past the last returned index, so a beat is
 * never scheduled twice however ragged the timer wakeups are.
 */
export function dueBeats(
  grid: BeatGrid,
  fromBeat: number,
  nowMs: number,
  lookaheadMs: number,
): ScheduledBeat[] {
  const horizon = nowMs + lookaheadMs;
  const out: ScheduledBeat[] = [];
  // Guard against a pathological horizon (tab suspended for minutes) producing a
  // scheduling storm — we cap the burst and let the cursor catch up next wake.
  const MAX_BURST = 64;
  for (let i = fromBeat; out.length < MAX_BURST; i++) {
    const timeMs = beatTimeMs(grid, i);
    if (timeMs > horizon) break;
    out.push({ beatIndex: i, timeMs });
  }
  return out;
}

/** 16th-note subdivision times within a beat, for hats and the bassline. */
export function subdivisions(grid: BeatGrid, beatIndex: number, n: number): number[] {
  const step = beatIntervalMs(grid.bpm) / n;
  const base = beatTimeMs(grid, beatIndex);
  return Array.from({ length: n }, (_, i) => base + i * step);
}

/** Bar/beat position of a beat index in 4/4. */
export const barOf = (beatIndex: number): number => Math.floor(beatIndex / 4);
export const beatInBar = (beatIndex: number): number => ((beatIndex % 4) + 4) % 4;
export const isDownbeat = (beatIndex: number): boolean => beatInBar(beatIndex) === 0;

/**
 * Where a 16th-note step sits in the 2-bar (32-step) bassline pattern.
 * Exposed so tests can assert the pattern wraps rather than drifting.
 */
export const patternStep = (beatIndex: number, sixteenth: number, steps = 32): number =>
  (((beatIndex * 4 + sixteenth) % steps) + steps) % steps;
