import { v4 as uuidv4 } from 'uuid';

import createTrack from '../createTrack';
import getOffsetTrackIds from '../getOffsetTrackIds';
import insertClipsToList from '../insertClipsToList';
import {
  getDerivedTiming,
  getSelectableTrackIds,
  getSelectedClips,
  getTracksAndExpandedTakeLanes,
  getTracksAndTakeLanes,
} from '../selectors';
import {
  AFTER_LAST_TRACK,
  StudioClip,
  StudioProjectState,
  StudioTrack,
} from '../types';

export const isValidClip = (clip: StudioClip) => {
  if (clip.endBeats <= clip.startBeats) {
    return false;
  }

  return true;
};

export const updateSelectedClips =
  (updateFn: (clip: StudioClip) => StudioClip) =>
  (state: StudioProjectState) => {
    return updateClips(
      getSelectedClips(state).map((c) => c.id),
      updateFn
    )(state);
  };
/**
 * Creates a state updater function that updates a specific clip within tracks.
 * This is a common pattern used throughout the codebase to update a clip's properties.
 *
 * @param clipId The ID of the clip to update
 * @param updateFn A function that receives the current clip and returns the updated clip
 * @returns A function that can be used with setState to update the clip
 */

export default function updateClips(
  clipIds: string[],
  updateFn: (clip: StudioClip) => StudioClip,
  trackIndexDelta: number = 0,
  isDuplicate: boolean = false
) {
  return (state: StudioProjectState): StudioProjectState => {
    // NOTE: dangerous assumption is being made that if you're moving clips between tracks, you don't care about updates to unexpanded take lanes.
    const tracksAndTakeLanes =
      trackIndexDelta === 0
        ? getTracksAndTakeLanes(state)
        : getTracksAndExpandedTakeLanes(state);
    const tracksAndTakeLaneIds =
      trackIndexDelta === 0
        ? tracksAndTakeLanes.map((t) => t.id)
        : getSelectableTrackIds(state);
    const offsetTrackIds = getOffsetTrackIds(
      tracksAndTakeLaneIds,
      trackIndexDelta
    );
    const clipsRemovedFromTracks: { [key: string]: StudioClip[] } = {};
    const clipsAddedToTracks: { [key: string]: StudioClip[] } = {};
    let createdTrack: StudioTrack | null = null;

    tracksAndTakeLanes.forEach((track) => {
      const clipsToUpdate = track.clips.filter((c) => clipIds.includes(c.id));
      if (!clipsToUpdate.length) return;
      clipsRemovedFromTracks[track.id] = clipsToUpdate;

      let destinationTrackId = offsetTrackIds[track.id];
      if (destinationTrackId === AFTER_LAST_TRACK) {
        if (!createdTrack) {
          createdTrack = createTrack();
        }
        destinationTrackId = createdTrack.id;
      }
      clipsAddedToTracks[destinationTrackId] ||= [];
      clipsAddedToTracks[destinationTrackId].push(
        ...clipsToUpdate.map(updateFn).filter(isValidClip)
      );
    });

    return {
      ...state,
      ...(isDuplicate
        ? { metadata: { ...state.metadata, usedDuplicate: true } }
        : {}),
      tracks: [
        ...state.tracks.map((track) => {
          let anyTakeLaneChanged = false;
          const newTakeLanes = track.takeLanes.map((takeLane) => {
            const removedClips = clipsRemovedFromTracks[takeLane.id];
            const addedClips = clipsAddedToTracks[takeLane.id];
            if (!removedClips && !addedClips) {
              return takeLane;
            }

            let clips = takeLane.clips;

            if (removedClips) {
              clips = clips.filter(
                (c) => !removedClips.find((c2) => c2.id === c.id)
              );
              anyTakeLaneChanged = true;
            }

            if (addedClips) {
              clips = insertClipsToList(
                getDerivedTiming(state),
                addedClips,
                clips
              );
              anyTakeLaneChanged = true;
            }

            return {
              ...takeLane,
              clips,
            };
          });

          const removedClips = clipsRemovedFromTracks[track.id];
          const addedClips = clipsAddedToTracks[track.id];
          if (!removedClips && !addedClips && !anyTakeLaneChanged) {
            return track;
          }

          let clips = track.clips;

          if (removedClips) {
            if (isDuplicate) {
              clips = clips.map((c) =>
                removedClips.find((c2) => c2.id === c.id)
                  ? { ...c, id: uuidv4() } // assign a new ID to the clip on the original track.
                  : c
              );
            } else {
              clips = clips.filter(
                (c) => !removedClips.find((c2) => c2.id === c.id)
              );
            }
          }

          if (addedClips) {
            clips = insertClipsToList(
              getDerivedTiming(state),
              addedClips,
              clips
            );
          }

          return {
            ...track,
            clips,
            takeLanes: anyTakeLaneChanged ? newTakeLanes : track.takeLanes,
          };
        }),
        ...(createdTrack
          ? [
              {
                ...(createdTrack as StudioTrack),
                clips: clipsAddedToTracks[(createdTrack as StudioTrack).id],
              },
            ]
          : []),
      ],
    };
  };
}
