import { useCallback, useMemo, useRef } from 'react';

import DragAndDropContext, {
  DraggedClip,
  DraggedObject,
} from '@/app/(root)/dragAndDrop/DragAndDropContext';
import Droppable, { DropOverlay } from '@/app/(root)/dragAndDrop/Droppable';
import useClip from '@/hooks/useClip';
import { useContextSelector } from '@/hooks/useContextSelector';
import { useClipDownbeats } from '@/hooks/useStreamingDownbeats';
import { snapWithEvent } from '@/utils/snap';

import StudioContext from './StudioContext';
import createKnownTimingStudioClipSync from './createKnownTimingStudioClipSync';
import createPartiallyKnownTimingStudioClipSync from './createPartiallyKnownTimingStudioClipSync';
import createUnknownTimingStudioClipSync from './createUnknownTimingStudioClipSync';
import { ClipInsertionSpec, applyInsertionSpec } from './insertion';
import { getFallbackBPS } from './selectors';

export default function StudioTimelineDroppable({
  children,
}: {
  children: React.ReactNode;
}) {
  const fallbackBPS = useContextSelector(StudioContext, (context) =>
    getFallbackBPS(context.state)
  );
  const draggedClipId = useContextSelector(DragAndDropContext, (context) => {
    if (context.draggedObject?.type !== 'clip') return undefined;
    return (context.draggedObject as DraggedClip).payload.clipId;
  });

  const { clip: draggedClip } = useClip(draggedClipId);
  const downbeatsResult = useClipDownbeats(draggedClipId);

  const baseStudioClip = useMemo(() => {
    if (!draggedClip) return;
    const downbeats = downbeatsResult?.downbeats || [];
    const final = downbeatsResult?.final;

    if (final) {
      return createKnownTimingStudioClipSync(draggedClip, downbeats);
    } else if (downbeats.length) {
      return createPartiallyKnownTimingStudioClipSync(draggedClip, downbeats);
    } else {
      return {
        ...createUnknownTimingStudioClipSync(draggedClip, fallbackBPS),
        awaitingContentAlignment: true,
      };
    }
  }, [draggedClip, downbeatsResult, fallbackBPS]);

  const frameCountRef = useContextSelector(
    StudioContext,
    (context) => context.timelineController.frameCountRef
  );

  const clientYToTrackId = useContextSelector(
    StudioContext,
    (context) => context.clientYToTrackId
  );

  const clientXToBeats = useContextSelector(
    StudioContext,
    (context) => context.clientXToBeats
  );

  const dragStateRef = useContextSelector(
    StudioContext,
    (context) => context.dragStateRef
  );

  const currentDragCoordinatesRef = useRef<{
    clientX: number;
    clientY: number;
  } | null>(null);

  const gridSizeRef = useContextSelector(
    StudioContext,
    (context) => context.gridSizeRef
  );

  const previewOrSetState = useContextSelector(
    StudioContext,
    (context) => context.previewOrSetState
  );

  const lastBaseStudioClipIdRef = useRef<string | null>(null);

  const handleDragMove = useCallback(
    (
      _draggedObject: DraggedObject<'clip'>,
      event: React.MouseEvent<HTMLDivElement>
    ) => {
      if (!draggedClip || !baseStudioClip) return;

      if (currentDragCoordinatesRef.current) {
        const { clientX, clientY } = currentDragCoordinatesRef.current;
        if (
          clientX === event.clientX &&
          clientY === event.clientY &&
          lastBaseStudioClipIdRef.current === baseStudioClip.id
        )
          return;
      }

      lastBaseStudioClipIdRef.current = baseStudioClip.id;

      currentDragCoordinatesRef.current = {
        clientX: event.clientX,
        clientY: event.clientY,
      };

      const targetTrackId = clientYToTrackId(event.clientY);
      const rawTargetBeats = clientXToBeats(event.clientX);
      const targetBeats = snapWithEvent(
        event.nativeEvent,
        rawTargetBeats,
        gridSizeRef.current
      );
      let targetAdjustedForGridAlignment = targetBeats;
      if (baseStudioClip.readStartBeats < 0) {
        targetAdjustedForGridAlignment =
          targetBeats + baseStudioClip.readStartBeats;
      }
      const movedStudioClip = {
        ...baseStudioClip,
        startBeats: targetAdjustedForGridAlignment,
        endBeats:
          targetAdjustedForGridAlignment +
          baseStudioClip.endBeats -
          baseStudioClip.startBeats,
      };
      const targetInsertionSpec: ClipInsertionSpec = {
        replacementStartBeats: movedStudioClip.startBeats,
        replacementEndBeats: movedStudioClip.endBeats,
        trackId: targetTrackId,
        studioClip: movedStudioClip,
        clip: draggedClip,
      };
      dragStateRef.current = {
        studioClip: movedStudioClip,
        trackId: targetTrackId,
        active: true,
        insertionSpecs: [],
        targetInsertionSpec,
      };
      frameCountRef.current++;
    },
    [
      clientYToTrackId,
      clientXToBeats,
      gridSizeRef,
      frameCountRef,
      dragStateRef,
      draggedClip,
      baseStudioClip,
    ]
  );

  const handleDragLeave = useCallback(() => {
    dragStateRef.current = null;
    frameCountRef.current++;
  }, [dragStateRef, frameCountRef]);

  const handleDrop = useCallback(() => {
    if (dragStateRef.current && dragStateRef.current.targetInsertionSpec) {
      previewOrSetState(
        dragStateRef.current.targetInsertionSpec.clip.id,
        applyInsertionSpec(dragStateRef.current.targetInsertionSpec, true)
      );
    }
    dragStateRef.current = null;
    frameCountRef.current++;
  }, [dragStateRef, frameCountRef, previewOrSetState]);

  return (
    <Droppable
      accept={['clip']}
      onDrop={handleDrop}
      onDragMove={handleDragMove}
      onDragLeave={handleDragLeave}
      overlay={<DropOverlay>Drop Here</DropOverlay>}
    >
      {children}
    </Droppable>
  );
}
