import { deduplicateWarpMarkers } from '@suno/studiokit/projectState/warpMarkersRegistry';
import {
  getWarpBeatsFromSeconds,
  getWarpSecondsFromBeats,
} from '@suno/studiokit/warpUtils';
import { noop } from 'lodash-es';
import { useCallback, useEffect } from 'react';

import { useStreamingDownbeats } from '@/hooks/useStreamingDownbeats';
import { useApiClient } from '@/lib/apiClient';
import { Clip } from '@/state/clipStore';

import updateClips from './actions/updateClips';
import getMarkersFromDownbeats from './getMarkersFromDownbeats';
import {
  getClipIdsReadyForDownbeatAnalysis,
  getStudioClipsByClipId,
} from './selectors';
import { StudioProjectState } from './types';

const secondsPrependedByClipId = new Map<string, number>();

export default function useAwaitedDownbeatsResolver(
  clipsById: Record<string, Clip>,
  state: StudioProjectState,
  rewriteHistory: (
    transformState: (state: StudioProjectState) => StudioProjectState
  ) => void
) {
  (window as any).getWarpSecondsFromBeats = getWarpSecondsFromBeats;
  (window as any).getWarpBeatsFromSeconds = getWarpBeatsFromSeconds;

  const clipIdsToAnalyze = getClipIdsReadyForDownbeatAnalysis({
    state,
    clipsById,
  });
  const { streamDownbeatsForClip } = useStreamingDownbeats();
  const apiClient = useApiClient();

  const streamClipDownbeats = useCallback(
    (clipId: string) => {
      return streamDownbeatsForClip(
        clipId,
        // we are going to get updates to the full downbeats array over time.
        // future updates may correct earlier updates.
        // the start of the clip may wiggle around if we're accumulating missing seconds for aligned content,
        // or if it's awaiting content alignment.
        ({ downbeats, final, secondsMissingFromStart }) => {
          if (!downbeats || !downbeats.length) return noop;
          const secondsToPrepend =
            secondsMissingFromStart -
            ((secondsPrependedByClipId as any).get(clipId) ?? 0);
          secondsPrependedByClipId.set(clipId, secondsMissingFromStart);
          rewriteHistory((state) => {
            const studioClips = getStudioClipsByClipId(state)[clipId];
            if (!studioClips || studioClips.length === 0) return state;

            return updateClips(
              studioClips.map((c) => c.id),
              (c) => {
                if (!c.warp.awaitingAnalysis) return c;

                const newWarp = {
                  ...c.warp,
                  markers: deduplicateWarpMarkers(
                    getMarkersFromDownbeats(downbeats)
                  ),
                  awaitingAnalysis: !final,
                };

                const readStartSeconds = getWarpSecondsFromBeats(
                  c.warp,
                  c.readStartBeats
                );

                const loopStartSeconds = getWarpSecondsFromBeats(
                  c.warp,
                  c.loop.startBeats
                );

                const loopEndSeconds = getWarpSecondsFromBeats(
                  c.warp,
                  c.loop.endBeats
                );

                let readStartBeats = getWarpBeatsFromSeconds(
                  newWarp,
                  readStartSeconds - secondsToPrepend
                );

                const loopStartBeats = getWarpBeatsFromSeconds(
                  newWarp,
                  loopStartSeconds
                );

                const loopEndBeats = getWarpBeatsFromSeconds(
                  newWarp,
                  loopEndSeconds
                );

                const clip = c.clipId && clipsById[c.clipId];
                const finalDuration = clip && clip.metadata.duration;
                let endBeats = c.endBeats;
                let finalLoopEndBeats = c.loop.endBeats;

                let awaitingContentAlignment = c.awaitingContentAlignment;
                if (c.awaitingContentAlignment) {
                  if (Math.abs(readStartSeconds) < 0.00001) {
                    readStartBeats = Math.ceil(readStartBeats);
                  } else {
                    readStartBeats = Math.round(readStartBeats);
                  }
                }

                if (final && finalDuration) {
                  const readEndBeats = getWarpBeatsFromSeconds(
                    newWarp,
                    finalDuration
                  );
                  endBeats = Math.min(
                    c.endBeats,
                    c.startBeats + (readEndBeats - readStartBeats)
                  );
                  finalLoopEndBeats = Math.min(loopEndBeats, readEndBeats);
                }

                if (final) {
                  awaitingContentAlignment = false;
                }

                return {
                  ...c,
                  awaitingContentAlignment,
                  warp: newWarp,
                  readStartBeats: readStartBeats,
                  loop: {
                    ...c.loop,
                    startBeats: loopStartBeats,
                    endBeats: finalLoopEndBeats,
                  },
                  endBeats: endBeats,
                };
              }
            )(state);
          });
        }
      );
    },
    [apiClient, rewriteHistory, clipsById]
  );

  useEffect(() => {
    const cancelFunctions = clipIdsToAnalyze.map(streamClipDownbeats);
    return () => {
      cancelFunctions.forEach((cancelFunction) => cancelFunction());
    };
  }, [clipIdsToAnalyze, streamClipDownbeats]);
}
