import { uniq } from 'lodash-es';

import { CanvasRegion } from '@/components/edit2025/canvasRenderer/CanvasRenderer';
import logWebUserEvent from '@/logging/logWebUserEvent';
import { snapWithEvent } from '@/utils/snap';

import { StudioContextType } from '../StudioContext';
import { combineActions } from '../actions/combineActions';
import dragSelect, { assignAnchorAndFocus } from '../actions/dragSelect';
import focusTimeline from '../actions/focusTimeline';
import { countTracksBetween } from '../getTrackIdsBetween';
import { inFlightDragKeys } from '../hooks/useInFlightDrags';
import {
  getSelectionEndBeats,
  getSelectionEndSeconds,
  getSelectionStartBeats,
  getSelectionStartSeconds,
  getSongEndBeats,
  getSongStartBeats,
  getStudioClips,
} from '../selectors';

const ENABLE_CLIP_BOUNDARY_SNAP = false;

// Clamp given beats to the song bounds in edit mode.
// In edit mode, clamps to [songStartBeats, songEndBeats].
// In non-edit modes, returns the input beats unchanged.
export const clampToSongBounds = (
  studioContext: StudioContextType,
  beats: number
): number => {
  if (studioContext.mode !== 'edit') return beats;

  const songStartBeats = getSongStartBeats(studioContext.state);
  const songEndBeats = getSongEndBeats(studioContext.state);
  return Math.max(songStartBeats, Math.min(songEndBeats, beats));
};

export const updateSelectionMouseDown = (studioContext: StudioContextType) => {
  const clipBoundaries = ENABLE_CLIP_BOUNDARY_SNAP
    ? uniq(
        getStudioClips(studioContext.state)
          .flatMap((c) => [c.startBeats, c.endBeats])
          .filter(
            (b) =>
              b > studioContext.timelineController.getTimelineStartBeats() &&
              b < studioContext.timelineController.getTimelineEndBeats()
          )
      )
    : [];

  return studioContext.handleClickDrag(
    ({ downBeats, downTrackId, downEvent }) => {
      studioContext.setEndSelectionMode('replace');

      if (
        downEvent.button === 2 &&
        downBeats >= getSelectionStartBeats(studioContext.state) &&
        downBeats <= getSelectionEndBeats(studioContext.state)
      ) {
        return {};
      }

      const snappedDownBeats = snapWithEvent(
        downEvent,
        downBeats,
        studioContext.gridSizeRef.current,
        null,
        studioContext.getMaxShift(),
        clipBoundaries
      );

      // Initial click placement constraint: Ensures the selection can't start outside clip boundaries
      let constrainedDownBeats = snappedDownBeats;
      if (studioContext.mode === 'edit') {
        constrainedDownBeats = clampToSongBounds(
          studioContext,
          snappedDownBeats
        );
      }

      if (downTrackId) {
        studioContext.inFlightDrags.update(
          inFlightDragKeys.selectionEdge('focus'),
          0
        );
        studioContext.setState(
          combineActions(
            focusTimeline,
            assignAnchorAndFocus(constrainedDownBeats),
            dragSelect(
              constrainedDownBeats,
              constrainedDownBeats,
              downTrackId,
              downTrackId,
              downTrackId,
              downEvent.shiftKey
            )
          )
        );
      } else {
        studioContext.playbackController.seek(constrainedDownBeats);
      }
      return {
        onMouseMove: ({ moveBeats, isDrag, moveTrackId, moveEvent }) => {
          const trackDelta =
            downTrackId && moveTrackId
              ? countTracksBetween(
                  downTrackId,
                  moveTrackId,
                  studioContext.state
                )
              : 0;

          if (isDrag) {
            const snappedMoveBeats = snapWithEvent(
              moveEvent,
              moveBeats,
              studioContext.gridSizeRef.current,
              null,
              studioContext.getMaxShift(),
              clipBoundaries
            );

            const constrainedMoveBeats = clampToSongBounds(
              studioContext,
              snappedMoveBeats
            );
            if (downTrackId) {
              const delta = constrainedMoveBeats - constrainedDownBeats;

              studioContext.inFlightDrags.update(
                inFlightDragKeys.selectionEdge('focus'),
                delta
              );
              studioContext.inFlightDrags.update(
                inFlightDragKeys.selectionTrackRange(),
                trackDelta
              );
            } else {
              studioContext.playbackController.seek(constrainedMoveBeats);
            }
          }
        },
        onMouseUp: ({ upBeats, upTrackId, isDrag, upEvent }) => {
          if (isDrag) {
            const snappedUpBeats = snapWithEvent(
              upEvent,
              upBeats,
              studioContext.gridSizeRef.current,
              null,
              studioContext.getMaxShift(),
              clipBoundaries
            );

            const constrainedUpBeats = clampToSongBounds(
              studioContext,
              snappedUpBeats
            );

            if (downTrackId) {
              const delta = constrainedUpBeats - constrainedDownBeats;

              studioContext.setState(
                combineActions(
                  focusTimeline,
                  dragSelect(
                    constrainedDownBeats,
                    constrainedDownBeats + delta,
                    downTrackId,
                    upTrackId,
                    upTrackId,
                    upEvent.shiftKey
                  )
                )
              );

              logWebUserEvent({
                actionName: 'EditV3Selection',
                context: {
                  editSessionId: studioContext.editSessionId,
                  editingClipId:
                    studioContext.state.editClipId || 'MISSING_EDIT_CLIP_ID',
                  startBeats: getSelectionStartBeats(
                    studioContext.stateRef.current
                  ),
                  startSeconds: getSelectionStartSeconds(
                    studioContext.stateRef.current
                  ),
                  endBeats: getSelectionEndBeats(
                    studioContext.stateRef.current
                  ),
                  endSeconds: getSelectionEndSeconds(
                    studioContext.stateRef.current
                  ),
                  trigger: 'waveform-drag',
                },
              });
            } else {
              studioContext.playbackController.seek(constrainedUpBeats);
            }
          }

          // we always set focus to make the selection active on a mousedown
          studioContext.inFlightDrags.finishAll();
        },
      };
    }
  );
};

export default function makeStudioBaseRegion(
  studioContext: StudioContextType
): CanvasRegion {
  const wrapperRect = studioContext.timelineController.getWrapperRect();
  return {
    touchTarget: {
      bounds: {
        left: 0,
        right: wrapperRect.width,
        top: 0,
        bottom: wrapperRect.height,
      },
      hoverCursor: 'text',
      onMouseDown: updateSelectionMouseDown(studioContext),
    },
  };
}
