import {
  getBeatsFromZero,
  getSecondsFromZero,
} from '@suno/studiokit/timeMapping';

import snap from '@/utils/snap';

import { EditMode } from '../EditModeContext';
import { useEditPlaybackContext } from '../EditPlaybackContext';
import { useEditTimelineContext } from '../EditTimelineContext';
import { usePreviewClipContext } from '../PreviewClipContext';
import { useSelectionContext } from '../SelectionContext';
import { useStemsContext } from '../StemsContext';
import { useAlignedLyricsContext } from '../lyrics/AlignedLyricsContext';
import { EditTiming } from '../types';

export type EditCanvasRenderContextConstructorType = {
  editTimelineContext: ReturnType<typeof useEditTimelineContext>;
  selectionContext: ReturnType<typeof useSelectionContext>;
  previewClipContext: ReturnType<typeof usePreviewClipContext>;
  stemsContext: ReturnType<typeof useStemsContext>;
  alignedLyricsContext: ReturnType<typeof useAlignedLyricsContext>;
  playbackContext: ReturnType<typeof useEditPlaybackContext>;
  timing: EditTiming;

  timelineWrapperRect: DOMRect;
  stemRows: HTMLElement[];
  timelineWrapper: HTMLElement;
  waveformHeader: HTMLElement;
  sectionHeader: HTMLElement;
  sectionDivider?: HTMLElement;
  clipId: string;
  editMode: EditMode;
  editSessionId: string | null;
};

export const constructEditCanvasRenderContext = ({
  editTimelineContext,
  selectionContext,
  previewClipContext,
  stemsContext,
  alignedLyricsContext,
  playbackContext,
  timing,

  timelineWrapperRect,
  timelineWrapper,
  waveformHeader,
  sectionHeader,
  sectionDivider,
  stemRows,
  clipId,
  editMode,
  editSessionId,
}: EditCanvasRenderContextConstructorType) => {
  const { scrollXRef, pxPerBeatRef, frameCountRef } = editTimelineContext;
  const {
    setSelection,
    selectionStartSeconds,
    selectionEndSeconds,
    setSelectionStartSeconds,
    setSelectionEndSeconds,
  } = selectionContext;

  const {
    selectedLyricsTiming,
    isFocused: alignedLyricsFocused,
    sortedSectionEntries,
  } = alignedLyricsContext;

  const {
    songEndSeconds,
    arrangedClips,
    previewEdit,
    setPreviewEdit,
    applyPreviewEdit,
  } = previewClipContext;

  const timelineStartBeats = scrollXRef.current / pxPerBeatRef.current;
  const timelineEndBeats =
    timelineStartBeats + timelineWrapperRect.width / pxPerBeatRef.current;
  const songStartBeats = getBeatsFromZero(0, timing);
  const songEndBeats = getBeatsFromZero(songEndSeconds, timing);

  const alignWithElement = (element: HTMLElement) => {
    const elementRect = element.getBoundingClientRect();
    return [elementRect.top - timelineWrapperRect.top, elementRect.height];
  };

  const beatsToCanvasPx = (
    beats: number,
    pxPerBeat = pxPerBeatRef.current,
    scrollX = scrollXRef.current
  ) => beats * pxPerBeat - scrollX;

  const canvasPxToBeats = (
    canvasPx: number,
    pxPerBeat = pxPerBeatRef.current,
    scrollX = scrollXRef.current
  ) => (canvasPx + scrollX) / pxPerBeat;

  const handleClickDrag = (
    onMouseDown: (downParams: {
      downEvent: MouseEvent;
      downSeconds: number;
      downBeats: number;
      downCanvasRelativePx: number;
    }) => {
      onMouseMove?: (moveParams: {
        moveEvent: MouseEvent;
        moveSeconds: number;
        moveBeats: number;
        moveCanvasRelativePx: number;
        isDrag: boolean;
      }) => void;
      onMouseUp?: (upParams: {
        upEvent: MouseEvent;
        upSeconds: number;
        upBeats: number;
        upCanvasRelativePx: number;
        isDrag: boolean;
      }) => void;
    }
  ) => {
    return (downEvent: MouseEvent) => {
      const downCanvasRelativePx = downEvent.clientX - timelineWrapperRect.left;
      const downBeats = canvasPxToBeats(downCanvasRelativePx);
      const downSeconds = getSecondsFromZero(downBeats, timing);
      let isDrag = false;

      const { onMouseMove, onMouseUp } = onMouseDown({
        downEvent,
        downSeconds,
        downBeats,
        downCanvasRelativePx,
      });

      if (!onMouseMove && !onMouseUp) return;

      const handleMouseMove = (moveEvent: MouseEvent) => {
        const moveCanvasRelativePx =
          moveEvent.clientX - timelineWrapper.getBoundingClientRect().left;
        const moveBeats = canvasPxToBeats(
          moveCanvasRelativePx,
          pxPerBeatRef.current,
          scrollXRef.current
        );
        const moveSeconds = getSecondsFromZero(moveBeats, timing);
        if (Math.abs(moveCanvasRelativePx - downCanvasRelativePx) > 5) {
          isDrag = true;
        }

        onMouseMove?.({
          moveEvent,
          moveSeconds,
          moveBeats,
          moveCanvasRelativePx,
          isDrag,
        });
      };

      const handleMouseUp = (upEvent: MouseEvent) => {
        const upCanvasRelativePx =
          upEvent.clientX - timelineWrapper.getBoundingClientRect().left;
        const upBeats = canvasPxToBeats(
          upCanvasRelativePx,
          pxPerBeatRef.current,
          scrollXRef.current
        );
        const upSeconds = getSecondsFromZero(upBeats, timing);
        onMouseUp?.({
          upEvent,
          upSeconds,
          upBeats,
          upCanvasRelativePx,
          isDrag,
        });
        window.removeEventListener('mousemove', handleMouseMove);
        window.removeEventListener('mouseup', handleMouseUp);
      };

      window.addEventListener('mousemove', handleMouseMove);
      window.addEventListener('mouseup', handleMouseUp);
    };
  };

  const [sectionY, sectionHeight] = alignWithElement(sectionHeader);
  const [waveformY, waveformHeight] = alignWithElement(waveformHeader);
  const stemRowAlignments = stemRows.map(alignWithElement);

  const selectionStartBeats = getBeatsFromZero(selectionStartSeconds, timing);
  const selectionEndBeats = getBeatsFromZero(selectionEndSeconds, timing);

  const selectionStartX = beatsToCanvasPx(selectionStartBeats);
  const selectionEndX = beatsToCanvasPx(selectionEndBeats);

  const selectionY = sectionY;
  const selectionHeight = sectionHeight + waveformHeight + 1;

  const snapToBeat = (
    seconds: number,
    snapSize: number = 1,
    method: 'floor' | 'ceil' | null = null,
    maxShift: number = 10 / pxPerBeatRef.current
  ) =>
    getSecondsFromZero(
      snap(getBeatsFromZero(seconds, timing), snapSize, method, maxShift),
      timing
    );

  const [sectionDividerTop, sectionDividerHeight] = sectionDivider
    ? alignWithElement(sectionDivider)
    : [0, 0];
  const sectionDividerBottom = sectionDividerTop + sectionDividerHeight;

  return {
    sortedSectionEntries,
    timing,
    songEndSeconds,
    songEndBeats,
    songStartBeats,
    timelineStartBeats,
    timelineEndBeats,
    pxPerBeat: pxPerBeatRef.current,
    scrollX: scrollXRef.current,
    pxPerBeatRef,
    scrollXRef,
    timelineWrapperRect,
    timelineWrapper,
    beatsToCanvasPx,
    canvasPxToBeats,
    setSelection,
    playbackContext,
    handleClickDrag,
    previewEdit,
    sectionY,
    sectionHeight,
    waveformY,
    waveformHeight,
    stemRowAlignments,
    arrangedClips,
    clipId,
    setPreviewEdit,
    applyPreviewEdit,
    frameCountRef,
    editMode,
    editSessionId,
    selectionStartBeats,
    selectionEndBeats,
    selectionStartSeconds,
    selectionEndSeconds,
    selectionStartX,
    selectionEndX,
    selectionY,
    selectionHeight,
    setSelectionStartSeconds,
    setSelectionEndSeconds,
    selectedLyricsTiming,
    alignedLyricsFocused,
    snapToBeat,
    stemsContext,
    sectionDividerBottom,
  };
};

export type EditCanvasRenderContext = ReturnType<
  typeof constructEditCanvasRenderContext
>;
