import { getWarpBeatsFromSeconds } from '@suno/studiokit/warpUtils';
import { isEqual } from 'lodash-es';

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

import { hasExtendTask } from '../StudioContext';
import {
  getSelectionEndBeats,
  getSelectionStartBeats,
  getTracksById,
} from '../selectors';
import { StudioClip, StudioProjectState } from '../types';
import getStudioClipContentDurationSeconds from './getStudioClipContentDurationSeconds';
import { updateSelectedTracksAndTakeLanes } from './updateTracksAndTakeLanes';

export const canHealSelection = (state: StudioProjectState) => {
  const effectiveStartBeats = getSelectionStartBeats(state);
  const effectiveEndBeats = getSelectionEndBeats(state);
  const tracksById = getTracksById(state);

  const selectedTrackIds = state.selection.trackIds;

  return selectedTrackIds.some((trackId) => {
    const track = tracksById[trackId];
    if (!track) return false;
    return canHealClips(track.clips, effectiveStartBeats, effectiveEndBeats);
  });
};

const canHealClips = (
  clips: StudioClip[],
  startBeats: number,
  endBeats: number
) => {
  const lastClipEnd = clips.length ? clips[clips.length - 1].endBeats : 0;
  const firstSelectedClipIndex = clips.findIndex(
    (clip) => clip.endBeats >= startBeats && clip.startBeats <= endBeats
  );
  const lastSelectedClipIndex = clips.findLastIndex(
    (clip) => clip.endBeats >= startBeats && clip.startBeats <= endBeats
  );

  const firstSelectedClip = clips[firstSelectedClipIndex];
  const lastSelectedClip = clips[lastSelectedClipIndex];

  if (endBeats >= lastClipEnd) {
    return firstSelectedClipIndex !== -1;
  } else {
    return (
      firstSelectedClipIndex !== -1 &&
      lastSelectedClipIndex !== -1 &&
      firstSelectedClipIndex !== lastSelectedClipIndex &&
      (firstSelectedClip.clipId === lastSelectedClip.clipId ||
        lastSelectedClipIndex - firstSelectedClipIndex > 1)
    );
  }
};

export const healClipEdits = (
  clipsById: Record<string, Clip>,
  studioClips: StudioClip[],
  startBeats: number,
  endBeats: number
): StudioClip[] => {
  const result: StudioClip[] = [];

  const lastClipEnd = studioClips.length
    ? studioClips[studioClips.length - 1].endBeats
    : 0;

  if (!canHealClips(studioClips, startBeats, endBeats)) {
    return studioClips;
  }

  const firstSelectedClipIndex = studioClips.findIndex(
    (clip) => clip.endBeats >= startBeats && clip.startBeats <= endBeats
  );
  const lastSelectedClipIndex = studioClips.findLastIndex(
    (clip) => clip.endBeats >= startBeats && clip.startBeats <= endBeats
  );

  const firstSelectedClip = studioClips[firstSelectedClipIndex];
  const lastSelectedClip = studioClips[lastSelectedClipIndex];

  if (
    endBeats >= lastClipEnd &&
    lastSelectedClip.clipId &&
    clipsById[lastSelectedClip.clipId] &&
    hasExtendTask(clipsById[lastSelectedClip.clipId]!)
  ) {
    // we're cleaning up an extend at the end of the timeline. extend the second-last clip as far as it can go and drop the last clip.
    return studioClips.slice(0, lastSelectedClipIndex).map((c, i) => {
      if (i === lastSelectedClipIndex - 1) {
        const clipDuration = getStudioClipContentDurationSeconds(c, clipsById);
        return {
          ...c,
          endBeats: clipDuration
            ? c.startBeats -
              c.readStartBeats +
              getWarpBeatsFromSeconds(c.warp, clipDuration)
            : endBeats,
        };
      } else {
        return c;
      }
    });
  }

  if (!firstSelectedClip || !lastSelectedClip) {
    return studioClips;
  }

  const firstClipAbsoluteBeatZero =
    firstSelectedClip.startBeats - firstSelectedClip.readStartBeats;
  const lastClipAbsoluteBeatZero =
    lastSelectedClip.startBeats - lastSelectedClip.readStartBeats;
  const timeAddedInCurrentSelection =
    lastClipAbsoluteBeatZero - firstClipAbsoluteBeatZero;

  const entirelySelectedClipsDurationBeats =
    lastSelectedClip.startBeats - firstSelectedClip.endBeats;

  const firstClipDurationBeats =
    firstSelectedClip.endBeats - firstSelectedClip.startBeats;

  if (
    firstSelectedClip.clipId === lastSelectedClip.clipId &&
    timeAddedInCurrentSelection <= entirelySelectedClipsDurationBeats &&
    timeAddedInCurrentSelection > -firstClipDurationBeats &&
    isEqual(firstSelectedClip.warp, lastSelectedClip.warp)
  ) {
    let timeOffset = 0;
    if (
      firstSelectedClip.color === lastSelectedClip.color &&
      firstSelectedClip.name === lastSelectedClip.name
    ) {
      for (let i = 0; i < studioClips.length; i++) {
        if (i === firstSelectedClipIndex) {
          result.push({
            ...firstSelectedClip,
            endBeats: lastSelectedClip.endBeats - timeAddedInCurrentSelection,
          });
          timeOffset -= timeAddedInCurrentSelection;
          i = lastSelectedClipIndex;
        } else {
          result.push({
            ...studioClips[i],
            startBeats: studioClips[i].startBeats + timeOffset,
            endBeats: studioClips[i].endBeats + timeOffset,
          });
        }
      }
    } else {
      const rawNewCutPointBeats =
        firstSelectedClip.endBeats + entirelySelectedClipsDurationBeats / 2;
      let newCutPointBeats = snap(rawNewCutPointBeats, 1);
      if (
        newCutPointBeats < firstSelectedClip.endBeats ||
        newCutPointBeats > lastSelectedClip.startBeats
      ) {
        newCutPointBeats = rawNewCutPointBeats;
      }
      for (let i = 0; i < studioClips.length; i++) {
        if (i === firstSelectedClipIndex) {
          result.push({
            ...firstSelectedClip,
            endBeats: newCutPointBeats,
          });
          timeOffset -= timeAddedInCurrentSelection;
          result.push({
            ...lastSelectedClip,
            startBeats: newCutPointBeats,
            readStartBeats:
              lastSelectedClip.readStartBeats -
              timeOffset +
              (newCutPointBeats - lastSelectedClip.startBeats),
          });
          i = lastSelectedClipIndex;
        } else {
          result.push(studioClips[i]);
        }
      }
    }

    return result;
  } else {
    // case 2: different clips or too big a jump. just extend the left one to cover the space here

    const rawNewCutPointBeats =
      firstSelectedClip.endBeats + entirelySelectedClipsDurationBeats / 2;
    let newCutPointBeats = snap(rawNewCutPointBeats, 1);
    if (
      newCutPointBeats < firstSelectedClip.endBeats ||
      newCutPointBeats > lastSelectedClip.startBeats
    ) {
      newCutPointBeats = rawNewCutPointBeats;
    }
    for (let i = 0; i < studioClips.length; i++) {
      if (i === firstSelectedClipIndex) {
        result.push({
          ...firstSelectedClip,
          endBeats: newCutPointBeats,
        });
        result.push({
          ...lastSelectedClip,
          startBeats: newCutPointBeats,
          readStartBeats:
            lastSelectedClip.readStartBeats +
            newCutPointBeats -
            lastSelectedClip.startBeats,
        });
        i = lastSelectedClipIndex;
      } else {
        result.push(studioClips[i]);
      }
    }
    return result;
  }
};

export default function healEdits(
  clipsById: Record<string, Clip>,
  startBeats?: number,
  endBeats?: number
) {
  return (state: StudioProjectState): StudioProjectState => {
    const effectiveStartBeats = startBeats ?? getSelectionStartBeats(state);
    const effectiveEndBeats = endBeats ?? getSelectionEndBeats(state);

    return updateSelectedTracksAndTakeLanes((track) => {
      return {
        ...track,
        clips: healClipEdits(
          clipsById,
          track.clips,
          effectiveStartBeats,
          effectiveEndBeats
        ),
      };
    })(state);
  };
}
