import { AutomationPoint, DerivedTiming } from './derivedTiming';

// note: assumes all bpsAutomation values have curve = 0
const reduceBpsAutomation = (
  bpsAutomation: AutomationPoint[],
  thresholdSeconds: number
) => {
  if (bpsAutomation.length === 0) return bpsAutomation;
  if (thresholdSeconds === 0) return bpsAutomation;

  /*
    TODO: this needs to be re-thought.

    Current implementation can make changes to previous automation points which cause others in between to have more offset than the given thresholdSeconds.

    Fundamentally we're trying to simplify a line with many segments with limited error, there is probably an off-the-shelf algorithm for that.
  */

  const result: AutomationPoint[] = [{ ...bpsAutomation[0] }];
  let secondsSinceLastOutputPoint = 0;
  let beatsSinceLastOutputPoint = 0;
  for (let i = 1; i < bpsAutomation.length; i++) {
    const inputPoint = bpsAutomation[i];
    const lastInputPoint = bpsAutomation[i - 1];

    const lastOutputPoint = result[result.length - 1];

    const beatsSinceLastInputPoint = inputPoint.beats - lastInputPoint.beats;
    const secondsSinceLastInputPoint =
      beatsSinceLastInputPoint / lastInputPoint.value;

    beatsSinceLastOutputPoint += beatsSinceLastInputPoint;
    secondsSinceLastOutputPoint += secondsSinceLastInputPoint;

    const correctBPS = beatsSinceLastOutputPoint / secondsSinceLastOutputPoint;
    const expectedSeconds = beatsSinceLastOutputPoint / lastOutputPoint.value;

    if (
      Math.abs(secondsSinceLastOutputPoint - expectedSeconds) >
        thresholdSeconds ||
      i === bpsAutomation.length - 1
    ) {
      lastOutputPoint.value = correctBPS;
      result.push({ ...inputPoint });

      secondsSinceLastOutputPoint = 0;
      beatsSinceLastOutputPoint = 0;
    }
  }
  return result;
};

export const FALLBACK_TIMING = {
  bps: 1,
  bpsAutomation: [],
  firstBeatSeconds: 0,
};

export const FALLBACK_TIMING_120BPM = {
  ...FALLBACK_TIMING,
  bps: 2,
};

export default function getTimingFromDownbeats(
  downbeats: [number, number][],
  reductionThreshold: number = 0.0
): DerivedTiming {
  if (downbeats.length <= 1) {
    return FALLBACK_TIMING;
  }

  const firstBeatSeconds = downbeats[0][0] || 0;
  const lastBeatSeconds = downbeats[downbeats.length - 1][0] || 1;
  const lastBeatBeats = downbeats.length - 1;
  const averageBPS = lastBeatBeats / (lastBeatSeconds - firstBeatSeconds);
  const firstDownbeatSeconds =
    downbeats.find((db) => db[1] === 1)?.[0] || firstBeatSeconds;

  const beatsBeforeFirstDownbeat = downbeats.findIndex((db) => db[1] === 1);

  const bpsAutomation: AutomationPoint[] = [];
  let lastBps: number = NaN;
  for (let beat = 0; beat < downbeats.length - 1; beat++) {
    const seconds = downbeats[beat][0];
    const nextBeat = beat + 1;
    const nextSeconds = downbeats[nextBeat][0];

    const bps = 1 / (nextSeconds - seconds);
    if (lastBps !== bps) {
      bpsAutomation.push({
        beats: beat - beatsBeforeFirstDownbeat,
        value: bps,
        curve: 0,
      });
      lastBps = bps;
    }
  }

  return {
    bps: averageBPS,
    bpsAutomation: reduceBpsAutomation(bpsAutomation, reductionThreshold),
    firstBeatSeconds: firstDownbeatSeconds,
  };
}
