import { lruMemoize } from 'reselect';

import { StudioClip } from './projectState/fixClip';

const scaleMarkers = (
  markers: StudioClip['warp']['markers'],
  speed: number
): Record<string, number> => {
  return Object.fromEntries(
    Object.entries(markers).map(([seconds, beats]) => [
      seconds,
      Number(beats) / speed,
    ])
  );
};

export const getWarpEnabledAndPopulated = (
  warp: Pick<StudioClip['warp'], 'enabled' | 'markers'>
) => {
  return warp.enabled && Object.keys(warp.markers).length > 1;
};

export const getEffectiveMarkers = lruMemoize(
  (warp: StudioClip['warp']) => {
    if (!getWarpEnabledAndPopulated(warp)) return warp.markers;
    return scaleMarkers(warp.markers, warp.speed ?? 1);
  },
  { maxSize: 100000 }
);

export const getWarpSecondsFromBeats = (
  warp: StudioClip['warp'],
  inputBeats: number
) => {
  const effectiveMarkers = getEffectiveMarkers(warp);
  const entries = Object.entries(effectiveMarkers).map(
    ([seconds, beats]) => [Number(seconds), Number(beats)] as const
  );

  const beatSortedEntries = entries.sort((a, b) => a[1] - b[1]);

  let secondLastEntryBefore: [number, number] | null = null;
  let lastEntryBefore: [number, number] | null = null;
  let firstEntryAfter: [number, number] | null = null;
  let secondEntryAfter: [number, number] | null = null;

  for (const [seconds, beats] of beatSortedEntries) {
    if (beats <= inputBeats) {
      secondLastEntryBefore = lastEntryBefore;
      lastEntryBefore = [seconds, beats];
    } else if (!firstEntryAfter) {
      firstEntryAfter = [seconds, beats];
    } else {
      secondEntryAfter = [seconds, beats];
      break;
    }
  }

  if (lastEntryBefore && firstEntryAfter) {
    const secondsPerBeat =
      (firstEntryAfter[0] - lastEntryBefore[0]) /
      (firstEntryAfter[1] - lastEntryBefore[1]);
    return (
      lastEntryBefore[0] + (inputBeats - lastEntryBefore[1]) * secondsPerBeat
    );
  } else if (lastEntryBefore) {
    const secondsPerBeat = secondLastEntryBefore
      ? (lastEntryBefore[0] - secondLastEntryBefore[0]) /
        (lastEntryBefore[1] - secondLastEntryBefore[1])
      : 1;
    return (
      lastEntryBefore[0] + (inputBeats - lastEntryBefore[1]) * secondsPerBeat
    );
  } else if (firstEntryAfter) {
    const secondsPerBeat = secondEntryAfter
      ? (secondEntryAfter[0] - firstEntryAfter[0]) /
        (secondEntryAfter[1] - firstEntryAfter[1])
      : 1;
    return (
      firstEntryAfter[0] + (inputBeats - firstEntryAfter[1]) * secondsPerBeat
    );
  } else {
    return inputBeats;
  }
};

export const getWarpBeatsFromSeconds = (
  warp: StudioClip['warp'],
  inputSeconds: number
) => {
  const effectiveMarkers = getEffectiveMarkers(warp);
  const entries = Object.entries(effectiveMarkers).map(
    ([seconds, beats]) => [Number(seconds), Number(beats)] as const
  );

  const secondsSortedEntries = entries.sort((a, b) => a[0] - b[0]);

  let secondLastEntryBefore: [number, number] | null = null;
  let lastEntryBefore: [number, number] | null = null;
  let firstEntryAfter: [number, number] | null = null;
  let secondEntryAfter: [number, number] | null = null;

  for (const [seconds, beats] of secondsSortedEntries) {
    if (seconds <= inputSeconds) {
      secondLastEntryBefore = lastEntryBefore;
      lastEntryBefore = [seconds, beats];
    } else if (!firstEntryAfter) {
      firstEntryAfter = [seconds, beats];
    } else {
      secondEntryAfter = [seconds, beats];
      break;
    }
  }

  if (lastEntryBefore && firstEntryAfter) {
    const beatsPerSecond =
      (firstEntryAfter[1] - lastEntryBefore[1]) /
      (firstEntryAfter[0] - lastEntryBefore[0]);
    return (
      lastEntryBefore[1] + (inputSeconds - lastEntryBefore[0]) * beatsPerSecond
    );
  } else if (lastEntryBefore) {
    const beatsPerSecond = secondLastEntryBefore
      ? (lastEntryBefore[1] - secondLastEntryBefore[1]) /
        (lastEntryBefore[0] - secondLastEntryBefore[0])
      : 1;
    return (
      lastEntryBefore[1] + (inputSeconds - lastEntryBefore[0]) * beatsPerSecond
    );
  } else if (firstEntryAfter) {
    const beatsPerSecond = secondEntryAfter
      ? (secondEntryAfter[1] - firstEntryAfter[1]) /
        (secondEntryAfter[0] - firstEntryAfter[0])
      : 1;
    return (
      firstEntryAfter[1] + (inputSeconds - firstEntryAfter[0]) * beatsPerSecond
    );
  } else {
    return inputSeconds;
  }
};
