import { DerivedTiming } from './derivedTiming';
import getTimingFromDownbeats, {
  FALLBACK_TIMING,
} from './getTimingFromDownbeats';
import { StudioClip } from './projectState/fixClip';
import { StudioProjectState } from './projectState/fixStudioProjectState';
import { AutomationPoint, StudioTiming } from './projectState/fixTiming';
import { StudioTrack } from './projectState/fixTrack';
import { getSecondsBetween, getValueAtBeats } from './timeMapping';
import { getEffectiveMarkers } from './warpUtils';

let lastTimingInput: StudioTiming | null = null;
let lastTimingInputTrack: StudioTrack | null = null;
let lastTimingOutput: DerivedTiming | null = null;

const getClipTiming = (clip: StudioClip) => {
  const warpMarkers = getEffectiveMarkers(clip.warp);
  const downbeats = Object.entries(warpMarkers).map(([seconds, beats]) => [
    Number(seconds),
    beats + 1,
  ]) as [number, number][];
  downbeats.sort((a, b) => a[0] - b[0]);
  return getTimingFromDownbeats(downbeats, 0);
};

export default function deriveTiming(
  state: Pick<StudioProjectState, 'timing' | 'tracks'>
): DerivedTiming {
  const { timing, tracks } = state;
  if (timing.type === 'follow-track') {
    const track = tracks.find((t) => t.id === timing.trackId);
    if (
      timing === lastTimingInput &&
      track === lastTimingInputTrack &&
      !!lastTimingOutput
    ) {
      return lastTimingOutput;
    }
    if (!track)
      return {
        ...FALLBACK_TIMING,
        bps: timing.fallbackBPS,
      };
    if (!track.clips.length)
      return {
        ...FALLBACK_TIMING,
        bps: timing.fallbackBPS,
      };
    const timedClips = track.clips.map((c) => ({
      clip: c,
      timing: getClipTiming(c),
    }));

    const sortedClips = timedClips.sort(
      (a, b) => a.clip.startBeats - b.clip.startBeats
    );

    const bpsAutomation: AutomationPoint[] = [];

    for (let i = 0; i < sortedClips.length; i++) {
      const nextClipStart = sortedClips[i + 1]?.clip.startBeats || Infinity;
      const { clip, timing } = sortedClips[i];
      // next clip start could overlap with clip end if there's a crossfade.
      // in this case, switch to the next clip's timing halfway through the fade.
      const clipDuration =
        Math.min((nextClipStart + clip.endBeats) / 2, clip.endBeats) -
        clip.startBeats;

      const readStartBeats = clip.readStartBeats;
      const readEndBeats = readStartBeats + clipDuration;
      const clipStartBPS = getValueAtBeats(
        readStartBeats,
        timing.bpsAutomation
      )[1];
      const clipEndBPS = getValueAtBeats(readEndBeats, timing.bpsAutomation)[0];

      if (clipStartBPS) {
        if (
          !bpsAutomation[bpsAutomation.length - 1] ||
          bpsAutomation[bpsAutomation.length - 1]?.beats <
            clip.startBeats - 0.00001
        ) {
          bpsAutomation.push({
            beats: clip.startBeats,
            value: clipStartBPS,
            curve: 0,
          });
        } else {
          bpsAutomation[bpsAutomation.length - 1].value = clipStartBPS;
        }
      }
      for (let j = 0; j < timing.bpsAutomation.length; j++) {
        const automationPoint = timing.bpsAutomation[j];
        if (
          automationPoint.beats >= readStartBeats &&
          automationPoint.beats < readEndBeats
        ) {
          const targetBeats =
            automationPoint.beats + (clip.startBeats - readStartBeats);
          if (bpsAutomation[bpsAutomation.length - 1]?.beats !== targetBeats) {
            bpsAutomation.push({
              ...automationPoint,
              beats: targetBeats,
            });
          } else {
            bpsAutomation[bpsAutomation.length - 1].value =
              automationPoint.value;
          }
        }
      }

      if (
        clipEndBPS &&
        bpsAutomation[bpsAutomation.length - 1]?.beats < clip.endBeats - 0.00001
      ) {
        bpsAutomation.push({
          beats: clip.endBeats,
          value: clipEndBPS,
          curve: 0,
        });
      }
    }

    const result = {
      bpsAutomation,
      firstBeatSeconds: 0,
      bps: timing.fallbackBPS,
    };

    const lastClipEnd = sortedClips[sortedClips.length - 1].clip.endBeats;
    const firstClipStart = sortedClips[0].clip.startBeats;

    const durationInBeats = lastClipEnd - firstClipStart;
    const durationInSeconds = getSecondsBetween(
      firstClipStart,
      lastClipEnd,
      result
    );

    // Only calculate average BPS if we have non-zero duration
    // Otherwise keep the fallback BPS to avoid division by zero
    if (durationInBeats > 0 && durationInSeconds > 0) {
      const averageBPS = durationInBeats / durationInSeconds;
      if (isFinite(averageBPS) && averageBPS > 0) {
        result.bps = averageBPS;
      }
    }

    lastTimingInput = timing;
    lastTimingInputTrack = track ?? null;
    lastTimingOutput = result;
    return result;
  } else {
    if (timing === lastTimingInput && !!lastTimingOutput) {
      return lastTimingOutput;
    }
    const result = {
      bps: timing.bps,
      bpsAutomation: timing.bpsAutomation,
      firstBeatSeconds: 0,
    } as DerivedTiming;
    if (timing.lockBPS) {
      result.bpsAutomation = [];
    }
    lastTimingInput = timing;
    lastTimingOutput = result;
    return result;
  }
}
