import { StudioContextType } from '../StudioContext';
import moveClip from '../actionHelpers/moveClip';
import moveClipCreationIntent from '../actionHelpers/moveClipCreationIntent';
import assignMetadata from '../actions/assignMetadata';
import { combineActions } from '../actions/combineActions';
import duplicateClipCreationIntents from '../actions/duplicateClipCreationIntents';
import duplicateClips from '../actions/duplicateClips';
import focusTimeline from '../actions/focusTimeline';
import updateClipCreationIntents from '../actions/updateClipCreationIntents';
import updateClips from '../actions/updateClips';
import updateSelection from '../actions/updateSelection';
import { inFlightDragKeys } from '../hooks/useInFlightDrags';
import {
  getClipCreationIntentsByTrackId,
  getSelectionEndBeats,
  getSelectionStartBeats,
  getSongEndBeats,
  getSongStartBeats,
  getStudioClipsById,
  getStudioClipsByTrackId,
} from '../selectors';
import { StudioClip, StudioTrackCore } from '../types';
import {
  cutSelectedClipCreationIntentsAndGetIDs,
  cutSelectedClipsAndGetIDs,
} from './handleClipDrag';

const EPSILON = 0.00001;

const getClipDuration = (studioClip: StudioClip) => {
  return studioClip.endBeats - studioClip.startBeats;
};

// figures out how far the dragged clip will move based on the positions of other clips on the track.
// if you are dragging right, this is determined based on where the end of the clip will end up.
// if you are dragging left, this is determined based on where the start of the clip will end up.
const getDraggedClipDelta = (
  studioContext: StudioContextType,
  track: StudioTrackCore,
  studioClip: StudioClip,
  dragDeltaBeats: number
) => {
  let delta = 0;
  const targetBeats =
    dragDeltaBeats <= 0
      ? studioClip.startBeats + dragDeltaBeats
      : studioClip.endBeats + dragDeltaBeats;
  const overlappingClip = getStudioClipsByTrackId(studioContext.state)[
    track.id
  ]?.find(
    (c) =>
      c.id !== studioClip.id &&
      c.startBeats <= targetBeats &&
      c.endBeats > targetBeats
  );

  if (overlappingClip) {
    const afterClip =
      targetBeats >
      overlappingClip.startBeats +
        (overlappingClip.endBeats - overlappingClip.startBeats) / 2;

    if (afterClip) {
      if (dragDeltaBeats < 0) {
        delta = overlappingClip.endBeats - studioClip.startBeats;
      } else {
        delta = overlappingClip.endBeats - studioClip.endBeats;
      }
    } else {
      if (dragDeltaBeats < 0) {
        delta = overlappingClip.startBeats - studioClip.startBeats;
      } else {
        delta = overlappingClip.startBeats - studioClip.endBeats;
      }
    }
  } else {
    const songStartBeats = getSongStartBeats(studioContext.state);
    const songEndBeats = getSongEndBeats(studioContext.state);
    delta = Math.max(
      songStartBeats - studioClip.startBeats,
      Math.min(songEndBeats - studioClip.endBeats, dragDeltaBeats)
    );
  }
  return delta;
};

// gets the IDs of clips that are being repositioned as a result of the current drag.
// should return a list of IDs of all clips between the dragged clip's original position and its intended new position.
const getSlidClipIds = (
  studioContext: StudioContextType,
  track: StudioTrackCore,
  studioClip: StudioClip,
  delta: number
) => {
  const dropStart = studioClip.startBeats + delta;
  const dropEnd = studioClip.endBeats + delta;

  const slidClipIds = getStudioClipsByTrackId(studioContext.state)
    [track.id]?.filter(
      (c) =>
        c.id !== studioClip.id &&
        ((c.startBeats >= dropStart - EPSILON &&
          c.startBeats < studioClip.startBeats) ||
          (c.endBeats <= dropEnd + EPSILON && c.endBeats > studioClip.endBeats))
    )
    .map((c) => c.id);

  return slidClipIds;
};

// gets the IDs of clipCreationIntents that are being repositioned as a result of the current drag.
const getSlidClipCreationIntentIds = (
  studioContext: StudioContextType,
  track: StudioTrackCore,
  studioClip: StudioClip,
  delta: number
) => {
  const dropStart = studioClip.startBeats + delta;
  const dropEnd = studioClip.endBeats + delta;

  const slidClipCreationIntentIds = getClipCreationIntentsByTrackId(
    studioContext.state
  )
    [track.id].filter((c) => {
      // Check if clip's start position is in the affected range
      const isStartInRange =
        c.startBeats !== undefined &&
        c.startBeats >= dropStart - EPSILON &&
        c.startBeats < studioClip.startBeats;

      // Check if clip's end position is in the affected range
      const isEndInRange =
        c.endBeats !== undefined &&
        c.endBeats <= dropEnd + EPSILON &&
        c.endBeats > studioClip.endBeats;

      // Include clip if either start or end positions are in range
      return isStartInRange || isEndInRange;
    })
    .map((c) => c.id);

  return slidClipCreationIntentIds;
};

// figures out how far the dragged clip & other clips will each move as a result of the current drag.
const getDragSpec = (
  studioContext: StudioContextType,
  track: StudioTrackCore,
  studioClip: StudioClip,
  dragDelta: number
) => {
  const delta = getDraggedClipDelta(
    studioContext,
    track,
    studioClip,
    dragDelta
  );
  const slideDistance = getClipDuration(studioClip) * (delta > 0 ? -1 : 1);
  const slidClipIds = getSlidClipIds(studioContext, track, studioClip, delta);
  const slidClipCreationIntentIds = getSlidClipCreationIntentIds(
    studioContext,
    track,
    studioClip,
    delta
  );

  return {
    delta,
    slideDistance,
    slidClipIds,
    slidClipCreationIntentIds,
  };
};

export default function onMagnetClipDrag(
  studioContext: StudioContextType,
  track: StudioTrackCore,
  studioClip: StudioClip,
  onSelectionChange: () => void
) {
  return studioContext.handleClickDrag(({ downBeats, downEvent }) => {
    const originalStartBeats = downEvent.shiftKey
      ? Math.min(
          getSelectionStartBeats(studioContext.state),
          studioClip.startBeats
        )
      : studioClip.startBeats;
    const originalEndBeats = downEvent.shiftKey
      ? Math.max(getSelectionEndBeats(studioContext.state), studioClip.endBeats)
      : studioClip.endBeats;

    let hasDragged = false;
    let draggedClipIds: string[] = [];
    let draggedClipCreationIntentIds: string[] = [];

    studioContext.setState(
      combineActions(
        focusTimeline,
        updateSelection((prev) => ({
          ...prev,
          anchorBeats: originalStartBeats,
          focusBeats: originalEndBeats,
          focusedTrackId: track.id,
          trackIds: [track.id],
        }))
      )
    );
    onSelectionChange();

    let lastSlidClipIds: string[] = [];

    return {
      onMouseMove: ({ moveBeats, isDrag }) => {
        if (isDrag) {
          const { delta, slideDistance, slidClipIds } = getDragSpec(
            studioContext,
            track,
            studioClip,
            moveBeats - downBeats
          );

          // clear out in-flight drags for clips that may not be slid any more.
          // e.g. if we drag left 500px, and then right 500px, a clip that was slid before may no longer be slid.
          lastSlidClipIds.forEach((clipId) => {
            studioContext.inFlightDrags.finish(inFlightDragKeys.clip(clipId));
          });

          slidClipIds.forEach((clipId) => {
            studioContext.inFlightDrags.update(
              inFlightDragKeys.clip(clipId),
              slideDistance
            );
          });

          lastSlidClipIds = slidClipIds;

          if (!hasDragged) {
            hasDragged = true;
            draggedClipIds = cutSelectedClipsAndGetIDs(studioContext);
            draggedClipCreationIntentIds =
              cutSelectedClipCreationIntentsAndGetIDs(studioContext);

            console.log(
              'draggedClipIds',
              draggedClipIds,
              'draggedClipCreationIntentIds',
              draggedClipCreationIntentIds
            );
          }

          draggedClipIds.forEach((clipId) => {
            studioContext.inFlightDrags.update(
              inFlightDragKeys.clip(clipId),
              delta
            );

            // Add real-time audio update for each dragged clip
            const clipNow = getStudioClipsById(studioContext.stateRef.current)[
              clipId
            ];
            if (clipNow) {
              studioContext.playbackController.updateClipTimeline(
                clipId,
                clipNow.startBeats + delta,
                clipNow.readStartBeats,
                clipNow.endBeats + delta
              );
            }
          });

          studioContext.inFlightDrags.update(
            inFlightDragKeys.selection(),
            delta
          );
        }
      },
      onMouseUp: ({ upBeats, upEvent, isDrag }) => {
        if (isDrag) {
          const {
            delta,
            slideDistance,
            slidClipIds,
            slidClipCreationIntentIds,
          } = getDragSpec(
            studioContext,
            track,
            studioClip,
            upBeats - downBeats
          );

          studioContext.setState(
            combineActions(
              focusTimeline,
              upEvent.altKey
                ? duplicateClips([...slidClipIds, ...draggedClipIds], (clip) =>
                    moveClip(
                      clip,
                      draggedClipIds.includes(clip.id) ? delta : slideDistance
                    )
                  )
                : updateClips([...slidClipIds, ...draggedClipIds], (clip) =>
                    moveClip(
                      clip,
                      draggedClipIds.includes(clip.id) ? delta : slideDistance
                    )
                  ),
              upEvent.altKey
                ? duplicateClipCreationIntents(
                    [
                      ...slidClipCreationIntentIds,
                      ...draggedClipCreationIntentIds,
                    ],
                    (clipCreationIntent) =>
                      moveClipCreationIntent(
                        clipCreationIntent,
                        draggedClipCreationIntentIds.includes(
                          clipCreationIntent.id
                        )
                          ? delta
                          : slideDistance
                      )
                  )
                : updateClipCreationIntents(
                    [
                      ...slidClipCreationIntentIds,
                      ...draggedClipCreationIntentIds,
                    ],
                    (clipCreationIntent) =>
                      moveClipCreationIntent(
                        clipCreationIntent,
                        draggedClipCreationIntentIds.includes(
                          clipCreationIntent.id
                        )
                          ? delta
                          : slideDistance
                      )
                  ),
              (state) => ({
                ...state,
                selection: {
                  ...state.selection,
                  anchorBeats: getSelectionStartBeats(state) + delta,
                  focusBeats: getSelectionEndBeats(state) + delta,
                },
              }),
              ...(slidClipIds.length > 0
                ? [
                    assignMetadata({
                      usedReorder: true,
                    }),
                  ]
                : [])
            )
          );
        }

        studioContext.inFlightDrags.finishAll();
      },
    };
  });
}
