import {
  getWarpBeatsFromSeconds,
  getWarpSecondsFromBeats,
} from '@suno/studiokit/warpUtils';

import { Clip } from '@/state/clipStore';
import modulo from '@/utils/modulo';
import snap from '@/utils/snap';

import { EditTiming } from '../edit2025/types';
import { hasExtendTask } from './StudioContext';
import describeClipTiming from './describeClipTiming';
import getClipRelativePoint from './getClipRelativePoint';
import { StudioClip } from './types';

export default function describeClipAdjustments(
  timing: EditTiming,
  studioClip: StudioClip,
  clip: Clip
): {
  contentDurationSeconds: number;
  contentStartSeconds: number;
  contentEndSeconds: number;
  arrangementDurationSeconds: number;
  arrangedStartSeconds: number;
  arrangedEndSeconds: number;
  startDeltaSeconds: number;
  endDeltaSeconds: number;
  clip: Clip;
} {
  if (!clip.metadata.duration) {
    throw new Error('Clip has no duration');
  }

  const clipTiming = describeClipTiming(clip) as {
    contentStartSeconds: number;
    contentDurationSeconds: number;
    contentEndSeconds: number;
  };

  if (!clipTiming.contentDurationSeconds || !clipTiming.contentEndSeconds) {
    throw new Error('Clip has no content duration');
  }

  const arrangedStartSeconds = getWarpSecondsFromBeats(
    studioClip.warp,
    studioClip.readStartBeats
  );

  const readEndBeats = getClipRelativePoint(
    timing,
    studioClip,
    studioClip.endBeats,
    true
  )[0];

  const arrangedEndSeconds = getWarpSecondsFromBeats(
    studioClip.warp,
    readEndBeats
  );

  if (hasExtendTask(clip)) {
    const historyEntry = clip.metadata.history?.slice(-1)[0] as any;
    const extendStartSeconds = historyEntry.continue_at;

    const startDeltaSeconds = arrangedStartSeconds - extendStartSeconds;
    const endDeltaSeconds =
      arrangedEndSeconds - clipTiming.contentDurationSeconds;

    return {
      ...clipTiming,
      arrangementDurationSeconds: arrangedEndSeconds - arrangedStartSeconds,
      arrangedStartSeconds,
      arrangedEndSeconds,
      startDeltaSeconds,
      endDeltaSeconds,
      clip,
    };
  } else {
    return {
      ...clipTiming,
      arrangementDurationSeconds: arrangedEndSeconds - arrangedStartSeconds,
      arrangedStartSeconds,
      arrangedEndSeconds,
      startDeltaSeconds: arrangedStartSeconds - clipTiming.contentStartSeconds,
      endDeltaSeconds: arrangedEndSeconds - clipTiming.contentEndSeconds,
      clip,
    };
  }
}

const MIN_DURATION_BEATS = 3.5;

export const describeClipReplacement = (
  timing: EditTiming,
  oldStudioClip: StudioClip,
  oldClip: Clip,
  newStudioClip: StudioClip,
  newClip: Clip
) => {
  const currentClipAdjustments = describeClipAdjustments(
    timing,
    oldStudioClip,
    oldClip
  );

  const newClipAdjustments = describeClipAdjustments(
    timing,
    newStudioClip,
    newClip
  );

  const minimumReadStartBeats = getWarpBeatsFromSeconds(newStudioClip.warp, 0);

  // we need to trim such that this many seconds are trimmed from the start of the new clip
  const startDeltaSeconds =
    currentClipAdjustments.startDeltaSeconds -
    newClipAdjustments.startDeltaSeconds;

  // we need to trim such that this many seconds are appended to the end of the new clip
  const endDeltaSeconds =
    currentClipAdjustments.endDeltaSeconds - newClipAdjustments.endDeltaSeconds;

  const targetStartSeconds =
    newClipAdjustments.arrangedStartSeconds + startDeltaSeconds;
  const targetEndSeconds =
    newClipAdjustments.arrangedEndSeconds + endDeltaSeconds;

  const targetStartBeats = getWarpBeatsFromSeconds(
    newStudioClip.warp,
    targetStartSeconds
  );
  const startTrimBeats = targetStartBeats - newStudioClip.readStartBeats;

  const targetEndBeats = getWarpBeatsFromSeconds(
    newStudioClip.warp,
    targetEndSeconds
  );

  const readEndBeats = getClipRelativePoint(
    timing,
    newStudioClip,
    newStudioClip.endBeats,
    true
  )[0];
  const endTrimBeats = targetEndBeats - readEndBeats;

  const movementBeats = oldStudioClip.startBeats - newStudioClip.startBeats;

  const resultingStudioClip = {
    ...newStudioClip,
    startBeats: newStudioClip.startBeats + movementBeats,
    endBeats:
      newStudioClip.endBeats + movementBeats + endTrimBeats - startTrimBeats,
    readStartBeats: getClipRelativePoint(
      timing,
      newStudioClip,
      newStudioClip.startBeats + startTrimBeats,
      true
    )[0],
  };

  const contentStartBeats = getWarpBeatsFromSeconds(
    resultingStudioClip.warp,
    0
  );

  const contentEndBeats = getWarpBeatsFromSeconds(
    resultingStudioClip.warp,
    newClip.metadata.duration!
  );

  let safety = 1000;
  while (
    resultingStudioClip.endBeats <
      resultingStudioClip.startBeats +
        Math.min(
          oldStudioClip.endBeats - oldStudioClip.startBeats,
          MIN_DURATION_BEATS
        ) &&
    safety-- > 0
  ) {
    resultingStudioClip.endBeats += 1;
  }

  safety = 1000;
  while (
    resultingStudioClip.readStartBeats < contentStartBeats &&
    safety-- > 0
  ) {
    resultingStudioClip.readStartBeats += 1;
  }
  if (hasExtendTask(newClip)) {
    safety = 1000;
    while (
      resultingStudioClip.readStartBeats +
        resultingStudioClip.endBeats -
        resultingStudioClip.startBeats >
        contentEndBeats &&
      safety-- > 0
    ) {
      resultingStudioClip.endBeats =
        resultingStudioClip.startBeats +
        (contentEndBeats - resultingStudioClip.readStartBeats);
    }
  } else {
    safety = 1000;
    while (
      resultingStudioClip.readStartBeats +
        resultingStudioClip.endBeats -
        resultingStudioClip.startBeats >
        contentEndBeats &&
      resultingStudioClip.readStartBeats - 1 >= minimumReadStartBeats &&
      safety-- > 0
    ) {
      resultingStudioClip.readStartBeats -= 1;
    }
  }

  const resultingClipDurationBeats =
    resultingStudioClip.endBeats - resultingStudioClip.startBeats;

  const originalClipDurationBeats =
    oldStudioClip.endBeats - oldStudioClip.startBeats;

  const durationChangeBeats =
    resultingClipDurationBeats - originalClipDurationBeats;

  const endShiftBeats = oldStudioClip.endBeats - resultingStudioClip.endBeats;
  const snappedEndShiftBeats = snap(endShiftBeats, 1);

  const oldClipReadStartAlignmentBeats = modulo(
    oldStudioClip.readStartBeats,
    1
  );
  const newClipReadStartAlignmentBeats = modulo(
    resultingStudioClip.readStartBeats,
    1
  );
  let readStartAlignmentBeats =
    oldClipReadStartAlignmentBeats - newClipReadStartAlignmentBeats;

  if (readStartAlignmentBeats > 0.5) {
    readStartAlignmentBeats -= 1;
  } else if (readStartAlignmentBeats < -0.5) {
    readStartAlignmentBeats += 1;
  }

  let snappedReadStartBeats = getClipRelativePoint(
    timing,
    resultingStudioClip,
    resultingStudioClip.startBeats + readStartAlignmentBeats,
    true
  )[0];

  let endShiftAlignmentBeats = endShiftBeats - snappedEndShiftBeats;

  safety = 1000;
  while (snappedReadStartBeats < minimumReadStartBeats && safety-- > 0) {
    snappedReadStartBeats += 1;
    endShiftAlignmentBeats -= 1;
  }

  const resultingSnappedStudioClip = {
    ...resultingStudioClip,
    endBeats: resultingStudioClip.endBeats + endShiftAlignmentBeats,
    readStartBeats: snappedReadStartBeats,
  };

  const resultingSnappedClipDurationBeats =
    resultingSnappedStudioClip.endBeats - resultingSnappedStudioClip.startBeats;

  const snappedDurationChangeBeats =
    resultingSnappedClipDurationBeats - originalClipDurationBeats;

  return {
    startTrimBeats,
    endTrimBeats,
    movementBeats,
    durationChangeBeats,
    resultingStudioClip,

    snappedDurationChangeBeats,
    resultingSnappedStudioClip,
  };
};
