import { uniq } from 'lodash-es';

import { StudioContextType } from '../StudioContext';
import moveClip from '../actionHelpers/moveClip';
import { combineActions } from '../actions/combineActions';
import { selectTrackIds } from '../actions/dragSelect';
import duplicateClips from '../actions/duplicateClips';
import focusTimeline from '../actions/focusTimeline';
import updateClips from '../actions/updateClips';
import { updateSelectedTracksAndTakeLanes } from '../actions/updateTracksAndTakeLanes';
import getClipSnap from '../getClipSnap';
import getOffsetTrackIds from '../getOffsetTrackIds';
import { selectionEntirelyContainsClip } from '../getOverlapType';
import { getTrimmedUnmutedTracks } from '../helpers/getTrimmedUnmutedTracks';
import { inFlightDragKeys } from '../hooks/useInFlightDrags';
import {
  getDerivedTiming,
  getSelectableTrackIds,
  getSelectionEndBeats,
  getSelectionStartBeats,
  getStudioClips,
  getTracksAndExpandedTakeLanes,
} from '../selectors';
import { AFTER_LAST_TRACK, StudioClip, StudioTrackCore } from '../types';

// when we start a clip-moving drag, we trim at the edges of the selection.
// this function applies that cut and returns the IDs of the clips that should be moved by the drag.
export const cutSelectedClipsAndGetIDs = (studioContext: StudioContextType) => {
  const result: string[] = [];
  const selectionStartBeats = getSelectionStartBeats(
    studioContext.stateRef.current
  );
  const selectionEndBeats = getSelectionEndBeats(
    studioContext.stateRef.current
  );

  const newState = updateSelectedTracksAndTakeLanes((t) => {
    return {
      ...t,
      clips: studioContext.cutClips(
        t.clips,
        selectionStartBeats,
        selectionEndBeats
      ),
    };
  })(studioContext.stateRef.current);

  getTracksAndExpandedTakeLanes(newState)
    .filter((t) => newState.selection.trackIds.includes(t.id))
    .forEach((t) => {
      t.clips
        .filter((c) =>
          selectionEntirelyContainsClip(
            c,
            selectionStartBeats,
            selectionEndBeats
          )
        )
        .forEach((c) => {
          result.push(c.id);
        });
    });
  studioContext.setState(newState);

  return result;
};

export const cutSelectedClipCreationIntentsAndGetIDs = (
  studioContext: StudioContextType
) => {
  const result: string[] = [];
  const selectionStartBeats = getSelectionStartBeats(
    studioContext.stateRef.current
  );
  const selectionEndBeats = getSelectionEndBeats(
    studioContext.stateRef.current
  );

  const newState = updateSelectedTracksAndTakeLanes((t) => ({
    ...t,
    clipCreationIntents: studioContext.cutClipCreationIntents(
      t.clipCreationIntents,
      selectionStartBeats,
      selectionEndBeats
    ),
  }))(studioContext.stateRef.current);

  newState.tracks
    .filter((t) => newState.selection.trackIds.includes(t.id))
    .forEach((t) => {
      t.clipCreationIntents
        .filter((c) => {
          const effectiveStart = c.startBeats ?? -Infinity;
          const effectiveEnd = c.endBeats ?? Infinity;
          return (
            effectiveStart >= selectionStartBeats &&
            effectiveStart < selectionEndBeats &&
            effectiveEnd >= selectionStartBeats &&
            effectiveEnd < selectionEndBeats
          );
        })
        .forEach((c) => {
          result.push(c.id);
        });
    });

  studioContext.setState(newState);

  return result;
};

export default function onClipDrag(
  studioContext: StudioContextType,
  track: StudioTrackCore,
  studioClip: StudioClip
) {
  return studioContext.handleClickDrag(
    ({ downBeats, downEvent, downTrackId }) => {
      const originalStartBeats = studioClip.startBeats;
      const originalEndBeats = studioClip.endBeats;

      const isShiftSelect = downEvent.shiftKey;
      let selectedClickedClip = false;
      let hasDragged = false;
      let draggedClipIds: string[] = [];

      if (
        downEvent.button !== 0 &&
        studioContext.state.selection.trackIds.includes(track.id) &&
        downBeats >= getSelectionStartBeats(studioContext.state) &&
        downBeats <= getSelectionEndBeats(studioContext.state)
      ) {
        return {};
      }

      const selectClickedClip = combineActions(
        focusTimeline,
        (state) => {
          if (isShiftSelect) {
            return {
              ...state,
              selection: {
                ...state.selection,
                anchorBeats: Math.min(
                  originalStartBeats,
                  getSelectionStartBeats(state)
                ),
                focusBeats: Math.max(
                  originalEndBeats,
                  getSelectionEndBeats(state)
                ),
              },
            };
          } else {
            return {
              ...state,
              selection: {
                ...state.selection,
                anchorBeats: originalStartBeats,
                focusBeats: originalEndBeats,
              },
            };
          }
        },
        selectTrackIds(track.id, track.id, track.id, isShiftSelect)
      );

      if (
        !(
          downBeats >= getSelectionStartBeats(studioContext.state) &&
          downBeats <= getSelectionEndBeats(studioContext.state)
        ) ||
        !studioContext.state.selection.trackIds.includes(track.id)
      ) {
        studioContext.setState(selectClickedClip);
        selectedClickedClip = true;
      } else {
        studioContext.setState(focusTimeline);
      }

      let otherClipBoundaries = uniq(
        getStudioClips(studioContext.state)
          .filter((c) => c.id !== studioClip.id)
          .flatMap((c) => [c.startBeats, c.endBeats])
          .filter(
            (b) =>
              b > studioContext.timelineController.getTimelineStartBeats() &&
              b < studioContext.timelineController.getTimelineEndBeats()
          )
      );

      let isOutsideWrapper = false;

      return {
        onMouseMove: ({ moveBeats, moveEvent, isDrag, moveTrackId }) => {
          const wrapperRect = studioContext.timelineController.getWrapperRect();
          if (
            moveEvent.clientX < wrapperRect.left ||
            moveEvent.clientX > wrapperRect.right ||
            moveEvent.clientY < wrapperRect.top ||
            moveEvent.clientY > wrapperRect.bottom
          ) {
            if (!isOutsideWrapper) {
              const timing = getDerivedTiming(studioContext.state);
              const trimmedUnmutedTracks = getTrimmedUnmutedTracks(
                studioContext.state
              );
              if (trimmedUnmutedTracks.length > 0) {
                studioContext.dragAndDropContext.onDragStart({
                  type: 'studio-timeline',
                  payload: {
                    startBeats: getSelectionStartBeats(studioContext.state),
                    endBeats: getSelectionEndBeats(studioContext.state),
                    tracks: trimmedUnmutedTracks,
                    timing: {
                      type: 'manual',
                      ...timing,
                    },
                  },
                });
                studioContext.inFlightDrags.suspendAll();
              }
              isOutsideWrapper = true;
            }
          } else {
            if (isOutsideWrapper) {
              studioContext.dragAndDropContext.onDragEnd();
              studioContext.inFlightDrags.resumeAll();
              isOutsideWrapper = false;
            }
          }

          const trackIndexDelta =
            moveTrackId && downTrackId
              ? getSelectableTrackIds(studioContext.state).indexOf(
                  moveTrackId
                ) -
                getSelectableTrackIds(studioContext.state).indexOf(downTrackId)
              : 0;

          const delta =
            moveEvent.metaKey || moveEvent.ctrlKey
              ? moveBeats - downBeats
              : getClipSnap(
                  studioClip.startBeats,
                  studioClip.endBeats,
                  moveBeats - downBeats,
                  otherClipBoundaries,
                  studioContext.gridSizeRef.current,
                  studioContext.getMaxShift(),
                  studioContext.timelineController.getTimelineStartBeats(),
                  studioContext.timelineController.getTimelineEndBeats()
                );

          if (isDrag && !isOutsideWrapper) {
            if (!hasDragged) {
              hasDragged = true;
              draggedClipIds = cutSelectedClipsAndGetIDs(studioContext);
              otherClipBoundaries = uniq(
                getStudioClips(studioContext.state)
                  .filter((c) => !draggedClipIds.includes(c.id))
                  .flatMap((c) => [c.startBeats, c.endBeats])
                  .filter(
                    (b) =>
                      b >
                        studioContext.timelineController.getTimelineStartBeats() &&
                      b < studioContext.timelineController.getTimelineEndBeats()
                  )
              );
            }

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

              studioContext.inFlightDrags.update(
                inFlightDragKeys.clipTrack(clipId),
                trackIndexDelta
              );
            });

            studioContext.inFlightDrags.update(
              inFlightDragKeys.selection(),
              delta
            );
          }
        },
        onMouseUp: ({ upBeats, upEvent, upTrackId, isDrag }) => {
          if (isOutsideWrapper) {
            studioContext.inFlightDrags.finishAll();
            return;
          }

          const trackIndexDelta =
            upTrackId && downTrackId
              ? getSelectableTrackIds(studioContext.state).indexOf(upTrackId) -
                getSelectableTrackIds(studioContext.state).indexOf(downTrackId)
              : 0;
          const delta =
            upEvent.metaKey || upEvent.ctrlKey
              ? upBeats - downBeats
              : getClipSnap(
                  studioClip.startBeats,
                  studioClip.endBeats,
                  upBeats - downBeats,
                  otherClipBoundaries,
                  studioContext.gridSizeRef.current,
                  studioContext.getMaxShift(),
                  studioContext.timelineController.getTimelineStartBeats(),
                  studioContext.timelineController.getTimelineEndBeats()
                );

          if (delta !== 0 || trackIndexDelta !== 0) {
            const offsetTrackIds = getOffsetTrackIds(
              getSelectableTrackIds(studioContext.state),
              trackIndexDelta
            );
            studioContext.setState(
              combineActions(focusTimeline, (state) => ({
                ...combineActions(
                  upEvent.altKey
                    ? duplicateClips(
                        draggedClipIds,
                        (clip) => moveClip(clip, delta),
                        trackIndexDelta
                      )
                    : updateClips(
                        draggedClipIds,
                        (clip) => moveClip(clip, delta),
                        trackIndexDelta
                      ),
                  (stateAfterClipUpdate) => ({
                    ...stateAfterClipUpdate,
                    selection: isShiftSelect
                      ? {
                          ...stateAfterClipUpdate.selection,
                          anchorBeats: !hasDragged
                            ? Math.min(
                                studioClip.startBeats,
                                getSelectionStartBeats(stateAfterClipUpdate)
                              )
                            : getSelectionStartBeats(stateAfterClipUpdate) +
                              delta,
                          focusBeats: !hasDragged
                            ? Math.max(
                                studioClip.endBeats,
                                getSelectionEndBeats(stateAfterClipUpdate)
                              )
                            : getSelectionEndBeats(stateAfterClipUpdate) +
                              delta,
                        }
                      : {
                          ...stateAfterClipUpdate.selection,
                          trackIds:
                            trackIndexDelta !== 0
                              ? stateAfterClipUpdate.selection.trackIds.map(
                                  (trackId) =>
                                    offsetTrackIds[trackId] === AFTER_LAST_TRACK
                                      ? stateAfterClipUpdate.tracks[
                                          stateAfterClipUpdate.tracks.length - 1
                                        ].id
                                      : offsetTrackIds[trackId]
                                )
                              : stateAfterClipUpdate.selection.trackIds,
                          anchorBeats: !hasDragged
                            ? studioClip.startBeats
                            : getSelectionStartBeats(stateAfterClipUpdate) +
                              delta,
                          focusBeats: !hasDragged
                            ? studioClip.endBeats
                            : getSelectionEndBeats(stateAfterClipUpdate) +
                              delta,
                        },
                  })
                )(state),
              }))
            );
          }
          if (!isDrag && !selectedClickedClip) {
            studioContext.setState(selectClickedClip);
          }

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