import {
  getBeatsFromZero,
  getSecondsFromZero,
} from '@suno/studiokit/timeMapping';
import {
  getWarpEnabledAndPopulated,
  getWarpSecondsFromBeats,
} from '@suno/studiokit/warpUtils';

import { EditTiming } from '../edit2025/types';
import { StudioClip } from './types';

const iterateWarpedStudioClipLoops = (
  clip: StudioClip,
  callback: (
    loopStart: number,
    loopEnd: number,
    loopTimelineStart: number,
    loopTimelineEnd: number
  ) => void
) => {
  const readStart = clip.readStartBeats;
  const timelineStart = clip.startBeats;
  const timelineEnd = clip.endBeats;
  const loopStart = clip.loop.startBeats;
  const loopEnd = clip.loop.endBeats;

  let currentLoopStart = readStart;
  let currentLoopTimelineStart = timelineStart;
  let currentLoopTimelineEnd = Math.min(
    timelineEnd,
    timelineStart + (loopEnd - readStart)
  );
  let currentLoopEnd = Math.min(
    loopEnd,
    currentLoopStart + (currentLoopTimelineEnd - currentLoopTimelineStart)
  );

  while (currentLoopTimelineStart < timelineEnd) {
    callback(
      currentLoopStart,
      currentLoopEnd,
      currentLoopTimelineStart,
      currentLoopTimelineEnd
    );
    currentLoopTimelineStart = currentLoopTimelineEnd;
    currentLoopTimelineEnd = Math.min(
      timelineEnd,
      currentLoopTimelineStart + (loopEnd - loopStart)
    );
    currentLoopStart = loopStart;
    currentLoopEnd = Math.min(
      loopEnd,
      loopStart + (currentLoopTimelineEnd - currentLoopTimelineStart)
    );
    if (!clip.loop.enabled) return;
  }
};

const iterateUnwarpedStudioClipLoops = (
  clip: StudioClip,
  timing: EditTiming,
  callback: (
    loopStartSeconds: number,
    loopEndSeconds: number,
    loopTimelineStartBeats: number,
    loopTimelineEndBeats: number
  ) => void
) => {
  const readStartSeconds = getWarpSecondsFromBeats(
    clip.warp,
    clip.readStartBeats
  );
  const timelineStartBeats = clip.startBeats;
  const timelineEndBeats = clip.endBeats;
  const loopStartSeconds = getWarpSecondsFromBeats(
    clip.warp,
    clip.loop.startBeats
  );
  const loopEndSeconds = getWarpSecondsFromBeats(clip.warp, clip.loop.endBeats);

  const timelineStartSeconds = getSecondsFromZero(timelineStartBeats, timing);
  const timelineEndSeconds = getSecondsFromZero(timelineEndBeats, timing);

  let currentLoopStartSeconds = readStartSeconds;
  let currentLoopTimelineStartBeats = timelineStartBeats;
  let currentLoopTimelineStartSeconds = timelineStartSeconds;
  let currentLoopTimelineEndBeats = Math.min(
    timelineEndBeats,
    getBeatsFromZero(
      currentLoopTimelineStartSeconds + (loopEndSeconds - readStartSeconds),
      timing
    )
  );
  let currentLoopTimelineEndSeconds = Math.min(
    timelineEndSeconds,
    getSecondsFromZero(currentLoopTimelineEndBeats, timing)
  );

  let currentLoopEndSeconds = Math.min(
    loopEndSeconds,
    currentLoopStartSeconds + timelineEndSeconds - timelineStartSeconds
  );

  let safety = 0;
  while (currentLoopTimelineStartBeats < timelineEndBeats && safety++ < 10000) {
    callback(
      currentLoopStartSeconds,
      currentLoopEndSeconds,
      currentLoopTimelineStartBeats,
      currentLoopTimelineEndBeats
    );

    currentLoopTimelineStartBeats = currentLoopTimelineEndBeats;
    currentLoopTimelineStartSeconds = currentLoopTimelineEndSeconds;
    currentLoopStartSeconds = loopStartSeconds;

    const secondsToEndOfClip =
      timelineEndSeconds - currentLoopTimelineStartSeconds;
    currentLoopEndSeconds =
      secondsToEndOfClip === 0
        ? loopEndSeconds
        : Math.min(loopEndSeconds, loopStartSeconds + secondsToEndOfClip);

    currentLoopTimelineEndBeats = Math.min(
      timelineEndBeats,
      getBeatsFromZero(
        currentLoopTimelineStartSeconds +
          (currentLoopEndSeconds - currentLoopStartSeconds),
        timing
      )
    );

    currentLoopTimelineEndSeconds = Math.min(
      timelineEndSeconds,
      getSecondsFromZero(currentLoopTimelineEndBeats, timing)
    );

    if (secondsToEndOfClip < 1 / 44100) {
      // fixes an error that could occur if the loop boundary landed an infinitesimal distance from the end of the clip.
      // tbh, don't totally understand how this happened
      break;
    }
    if (!clip.loop.enabled) return;
  }
  if (safety >= 10000) {
    throw new Error('Infinite loop in iterateUnwarpedStudioClipLoops');
  }
};

export const iterateStudioClipLoops = (
  clip: StudioClip,
  timing: EditTiming,
  callback: (
    loopStart: number,
    loopEnd: number,
    loopTimelineStart: number,
    loopTimelineEnd: number
  ) => void
) => {
  if (getWarpEnabledAndPopulated(clip.warp)) {
    iterateWarpedStudioClipLoops(clip, callback);
  } else {
    iterateUnwarpedStudioClipLoops(clip, timing, callback);
  }
};
