import cleanState from '@suno/studiokit/projectState/cleanState';
import { deduplicateWarpMarkers } from '@suno/studiokit/projectState/warpMarkersRegistry';
import { getSecondsBetween } from '@suno/studiokit/timeMapping';
import { getWarpBeatsFromSeconds } from '@suno/studiokit/warpUtils';
import { useAnimationFrame } from 'framer-motion';
import { observer } from 'mobx-react-lite';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useLocalStorage } from 'usehooks-ts';
import { v4 as uuidv4 } from 'uuid';

import { useStores } from '@/app/(root)/AppProviders';
import { FocusedObjectContext } from '@/app/(root)/create/v2/useFocusedObject';
import DragAndDropContext from '@/app/(root)/dragAndDrop/DragAndDropContext';
import { useStudioTimelineController } from '@/components/studio/useStudioTimelineController';
import getClipGeneratePayload from '@/hooks/getClipGeneratePayload';
import useClip, { fetchClip } from '@/hooks/useClip';
import {
  createContext,
  useContext,
  useContextSelector,
} from '@/hooks/useContextSelector';
import useDebounceCallback from '@/hooks/useDebounceCallback';
import useDismount from '@/hooks/useDismount';
import {
  GenerateParams,
  ModelTier,
  getClipModelTier,
} from '@/hooks/useGenerate';
import useUploadAndInitializeClip from '@/hooks/useUploadAndInitializeClip';
import { Clip } from '@/state/clipStore';
import { PlanFeature } from '@/state/sessionStore';
import { isFeatureEnabledForPlan } from '@/utils/session';

import { useCleanupDownbeatsFromLocalStorage } from '../edit2025/queryHooks/useEditTimingQuery';
import useStateHistory from '../edit2025/useStateHistory';
import CloneProjectModal from './CloneProjectModal';
import {
  DSPModuleContext,
  getFfmpegAudioBufferFromFile,
} from './DSPModuleContext';
import { StudioProjectManagementContext } from './StudioProjectManagementContext';
import addTakeLane from './actions/addTakeLane';
import arrangeClip from './actions/arrangeClip';
import closeUnselectedGaps from './actions/closeUnselectedGaps';
import { combineActions } from './actions/combineActions';
import retimeUnwarpedClips from './actions/retimeUnwarpedClips';
import setGapSize from './actions/setGapSize';
import updateClips from './actions/updateClips';
import updateQueuedGeneration from './actions/updateQueuedGeneration';
import { updateTrackOrTakeLane } from './actions/updateTracksAndTakeLanes';
import importedClearTimeFromList from './clearTimeFromClipList';
import createTakeLane from './createTakeLane';
import createTrack from './createTrack';
import importedCutClipCreationIntents from './cutClipCreationIntents';
import importedCutClips from './cutClips';
import { detectStateChanges } from './editLockProtection';
import getEditModeStateForClip from './getEditModeStateForClip';
import getMarkersFromDownbeats from './getMarkersFromDownbeats';
import { ClickDragMouseDownHandler } from './handleClickDrag';
import importedHandleClickDrag from './handleClickDrag';
import { getAlignedLyricsQueryKey } from './hooks/useAlignedLyrics';
import { useClips } from './hooks/useClips';
import { inFlightDragKeys, useInFlightDrags } from './hooks/useInFlightDrags';
import useKeyCommands from './hooks/useKeyCommands';
import { useWaveformPrecomputation } from './hooks/useWaveformPrecomputation';
import insertAfterFocusedTrack from './insertAfterFocusedTrack';
import { ClipInsertionSpec } from './insertion';
import { isValidStateChangeInPreview } from './isValidStateChangeInPreview';
import queryClient from './queryClient';
import resolveHeardState from './resolveHeardState';
import {
  getAllUsedClipIds,
  getDerivedTiming,
  getFocusedTrackClips,
  getSelectableTrackIds,
  getSelectedTracks,
  getSelectionEndBeats,
  getSelectionEndSeconds,
  getSelectionGapSize,
  getSelectionStartBeats,
  getSelectionStartSeconds,
  getSongEndBeats,
  getSongStartBeats,
  getStudioClips,
  getTakeLaneParentsByTrackId,
  getTrackIds,
} from './selectors';
import {
  AFTER_LAST_TRACK,
  ClipCreationIntent,
  InstrumentSpec,
  StudioClip,
  StudioProjectState,
  StudioResolvedSelection,
  StudioTakeLane,
  StudioWebGLCache,
} from './types';
import {
  audioDurationsByUploadId,
  clipIdsByUploadId,
  ffmpegBuffersByUploadId,
} from './uploadedClipCache';
import useAlignedLyricsByClipId from './useAlignedLyricsByClipId';
import useAwaitedDownbeatsResolver from './useAwaitedDownbeatsResolver';
import useClipCreationIntentResolver from './useClipCreationIntentResolver';
import { setResolvedFfmpegBuffer } from './useFfmpegBufferCache';
import { getMaximumKnownClipDuration } from './useFfmpegBufferCache';
import useFileDropReceiver from './useFileDropReceiver';
import useFocusedClip from './useFocusedClip';
import usePreviewController from './usePreviewController';
import useRecordingController from './useRecordingController';
import useStreamingClipResolver from './useStreamingClipResolver';
import useStudioLyricsEditController from './useStudioLyricsEditController';
import useStudioPlaybackController from './useStudioPlaybackController';
import useStudioStylesEditController from './useStudioStylesEditController';
import useUnheardTakeLanesController from './useUnheardTakeLanesController';

const IGNORED_UNDO_STATE_KEYS = ['selection'];

const isPushableStateChange = (
  prev: StudioProjectState,
  next: StudioProjectState
) => {
  return !Object.keys(prev)
    .filter((k) => !IGNORED_UNDO_STATE_KEYS.includes(k))
    .every((key) => {
      return (
        prev[key as keyof StudioProjectState] ===
        next[key as keyof StudioProjectState]
      );
    });
};

export const hasInfillTask = (clip: Clip) =>
  [
    'infill' as const,
    'fixed_infill' as const,
    'cover_infill' as const,
    'artist_infill' as const,
    'artist_cover_infill' as const,
  ].includes(clip.metadata.task as any);

export const hasExtendTask = (clip: Clip) =>
  [
    'extend',
    'infill_outro',
    'cover_extend',
    'artist_extend',
    'artist_cover_extend',
  ].includes(clip.metadata.task as any);

export const hasStemTask = (clip: Clip) =>
  ['stem', 'gen_stem'].includes(clip.metadata.task as any);

export const hasAddStemTask = (clip: Clip) =>
  hasStemTask(clip) && clip.metadata.stem_task === 'add';

export const hasStemConditionTask = (clip: Clip) =>
  clip.metadata.task === 'stem_condition';

const useStudioContext = (
  studioProjectId: string,
  loadedState: StudioProjectState,
  mode: 'edit' | 'studio'
) => {
  const {
    session,
    clips: clipsStore,
    project: projectStore,
    edit: editStore,
  } = useStores();

  const { module: dspModule } = useContext(DSPModuleContext);

  useCleanupDownbeatsFromLocalStorage();

  // Get lock status for read-only mode
  const isLocked = useContextSelector(
    StudioProjectManagementContext,
    (ctx) => ctx?.isLocked ?? false // Default to false (unlocked) if context not available
  );
  const cloneProject = useContextSelector(
    StudioProjectManagementContext,
    (ctx) => ctx?.cloneProject
  );
  const currentVersionId = useContextSelector(
    StudioProjectManagementContext,
    (ctx) => ctx?.currentVersionId
  );

  const setStudioPanelMode = useContextSelector(
    FocusedObjectContext,
    (ctx) => ctx?.setStudioPanelMode
  );

  // Read-only mode when project is locked
  const isReadOnlyMode = isLocked;

  // Modal state for clone project when edit attempts are blocked
  const [showCloneModal, setShowCloneModal] = useState(false);

  const editSessionId = editStore.editSessionId || 'MISSING_EDIT_SESSION_ID';

  const magnetMode = mode === 'edit';
  const oneTrackMode = mode === 'edit';
  const previewBeforeInsertMode = mode === 'edit';

  const initialFixedStateRef = useRef<StudioProjectState>(loadedState);
  const lastProjectIdRef = useRef<string | null>(null);
  const initialFixedState = useMemo(() => {
    if (lastProjectIdRef.current !== studioProjectId) {
      initialFixedStateRef.current = loadedState;
      lastProjectIdRef.current = studioProjectId;
    }
    return initialFixedStateRef.current;
  }, [loadedState, studioProjectId]);

  const {
    state: baseState,
    undo,
    redo,
    canUndo,
    canRedo,
    setState: setBaseState,
    clearHistory,
    rewriteHistory,
    setLockUndoStates,
    lockUndoStatesRef,
  } = useStateHistory<StudioProjectState>(
    initialFixedState,
    isPushableStateChange
  );
  const canUseAuk = isFeatureEnabledForPlan(session, PlanFeature.Auk);

  const lastStudioProjectIdRef = useRef(studioProjectId);
  useEffect(() => {
    if (lastStudioProjectIdRef.current !== studioProjectId) {
      clearHistory(loadedState);
      lastStudioProjectIdRef.current = studioProjectId;
    }
  }, [studioProjectId, loadedState]);

  const dragStateRef = useRef<{
    studioClip: StudioClip;
    trackId: string | null;
    active: boolean;
    insertionSpecs: ClipInsertionSpec[];
    targetInsertionSpec: ClipInsertionSpec | null;
  } | null>(null);

  const lastSavedStateRef = useRef<StudioProjectState>(baseState);
  const saveProjectAPICall = useContextSelector(
    StudioProjectManagementContext,
    (s) => s.saveProject
  );

  const saveProject = useCallback(
    (state: StudioProjectState) => {
      lastSavedStateRef.current = state;
      saveProjectAPICall(state);
    },
    [saveProjectAPICall]
  );

  const autoSaveCallbacks = useRef<(() => void)[]>([]);

  const runAutoSaveCallbacks = useCallback(() => {
    autoSaveCallbacks.current.forEach((callback) => callback());
  }, []);

  const onAutoSave = useCallback((callback: () => void) => {
    autoSaveCallbacks.current.push(callback);
    return () => {
      autoSaveCallbacks.current = autoSaveCallbacks.current.filter(
        (c) => c !== callback
      );
    };
  }, []);

  const currentAutoSaveProjectIdRef = useRef(studioProjectId);
  const currentProjectIdRef = useRef(studioProjectId);
  currentProjectIdRef.current = studioProjectId;

  const saveProjectRef = useRef(saveProject);
  saveProjectRef.current = saveProject;

  const autoSave = useCallback(
    (state: StudioProjectState) => {
      if (currentAutoSaveProjectIdRef.current !== currentProjectIdRef.current) {
        return;
      }
      saveProjectRef.current(state);
      runAutoSaveCallbacks();
    },
    [runAutoSaveCallbacks]
  );

  const autoSaveDebounced = useDebounceCallback(autoSave, 2500);

  useEffect(() => {
    currentAutoSaveProjectIdRef.current = currentProjectIdRef.current;
    setTimeout(() => {
      if (
        lockUndoStatesRef.current ||
        isReadOnlyMode || // Don't auto-save when viewing historical versions
        !isPushableStateChange(lastSavedStateRef.current, baseState)
      )
        return;
      currentAutoSaveProjectIdRef.current = currentProjectIdRef.current;
      autoSaveDebounced(baseState);
    });
    const onBeforeUnload = () => {
      // Don't save historical versions on page exit
      if (!isReadOnlyMode) {
        currentAutoSaveProjectIdRef.current = currentProjectIdRef.current;
        autoSave(baseState);
      }
    };
    window.addEventListener('beforeunload', onBeforeUnload);
    return () => {
      window.removeEventListener('beforeunload', onBeforeUnload);
    };
  }, [
    baseState,
    autoSave,
    autoSaveDebounced,
    runAutoSaveCallbacks,
    lockUndoStatesRef,
    isReadOnlyMode,
  ]);

  const previewController = usePreviewController(
    baseState,
    previewBeforeInsertMode
  );

  const displayedState = useMemo(() => {
    if (
      !previewController.previewPackage ||
      previewController.previewPackage.offTimeline
    ) {
      return baseState;
    } else {
      return previewController.previewPackage.state;
    }
  }, [previewController.previewPackage, baseState]);

  const heardState = useMemo(() => {
    if (!previewController.previewPackage) {
      return resolveHeardState(baseState);
    } else {
      return resolveHeardState(previewController.previewPackage.state);
    }
  }, [previewController.previewPackage, baseState]);

  const unheardTakeLanesController = useUnheardTakeLanesController(baseState);

  const baseStateRef = useRef(baseState);
  useEffect(() => {
    baseStateRef.current = baseState;
  }, [baseState]);

  const setState = useCallback(
    (
      update:
        | StudioProjectState
        | ((prev: StudioProjectState) => StudioProjectState)
    ) => {
      // Check for edit lock in read-only mode
      if (isReadOnlyMode) {
        // Compute what the new state would be (dry run)
        const currentState = baseStateRef.current;
        const newState =
          typeof update === 'function' ? update(currentState) : update;

        // Detect what actually changed
        const changes = detectStateChanges(currentState, newState);

        if (changes.hasEditChanges) {
          // Show clone modal to allow duplication
          setShowCloneModal(true);
          return; // Block the operation
        }
      }

      setBaseState((prev) => {
        // 1. Run the caller's updater
        let draft =
          typeof update === 'function'
            ? (update as (p: StudioProjectState) => StudioProjectState)(prev)
            : update;

        // 1.5 Exit early if we're making a change that's not kosher during preview mode
        if (previewController.previewingOnTimeline) {
          if (!isValidStateChangeInPreview(prev, draft)) {
            return prev;
          }
        }

        // 1.75 resize any unwarped studio clips
        const oldTiming = getDerivedTiming(prev);
        const newTiming = getDerivedTiming(draft);
        if (oldTiming !== newTiming) {
          draft = retimeUnwarpedClips(prev)(draft);
        }

        // 2. Run cleanState to sort clips etc
        let cleaned = cleanState(draft);

        // 3 Close any gaps in the user's selection, if we're in magnet mode
        if (magnetMode) {
          cleaned = closeUnselectedGaps(cleaned);
        }

        return cleaned;
      });
    },
    [
      setBaseState,
      previewController.previewingOnTimeline,
      isReadOnlyMode,
      magnetMode,
      setShowCloneModal,
    ]
  );

  // Move clone handler to be accessible by modal

  const stateRef = useRef<StudioProjectState>(displayedState);
  useEffect(() => {
    stateRef.current = displayedState;
  }, [displayedState]);

  const isInPreviewMode = Boolean(previewController.previewPackage);

  const playbackController = useStudioPlaybackController(
    heardState,
    mode !== 'studio',
    studioProjectId,
    isInPreviewMode
  );

  useEffect(() => {
    return previewController.receivePlaybackController(playbackController);
  }, [previewController, playbackController]);

  const arrangeFile = useCallback(
    (
      trackId: string | null,
      clipSettings: {
        startBeats: number;
        endBeats: number;
        readStartBeats?: number;
        downbeats?: [number, number][];
        markers?: Record<number, number>;
      },
      file: File,
      audioBuffer: AudioBuffer,
      toNewTakeLane?: boolean
    ) => {
      withQueuedGeneration({
        startBeats: clipSettings.startBeats,
        endBeats: clipSettings.endBeats,
        targetTrackId: trackId,
        fn: async (promoteClipCreationIntents, trackId) => {
          // 1. kick off the audio upload to get an id
          const receiveUploadIdRef = { current: (_uploadId: string) => void 0 };
          const uploadIdPromise = new Promise<string>((resolve) => {
            receiveUploadIdRef.current = (uploadId: string) => {
              resolve(uploadId);
            };
          });

          const clipPromise = uploadAndInitializeClip(
            file,
            'studio_file_upload', // audio_recording applies automatic trimming on the backend. not good.
            undefined,
            undefined,
            undefined,
            undefined,
            undefined,
            clipSettings.downbeats,
            receiveUploadIdRef.current
          );

          const uploadId = await uploadIdPromise;

          // 2. create FfmpegAudioBuffer and cache it (memory-efficient compressed storage)
          // Pass uploadId as UUID so the buffer can be looked up by the waveform renderer
          const ffmpegBuffer = await getFfmpegAudioBufferFromFile(
            dspModule,
            file,
            uploadId
          );
          ffmpegBuffersByUploadId[uploadId] = ffmpegBuffer;
          audioDurationsByUploadId[uploadId] = audioBuffer.duration;

          // 3. arrange the actual clip
          const warp = clipSettings.markers
            ? {
                enabled: true,
                markers: clipSettings.markers,
                speed: 1,
                awaitingAnalysis: false,
              }
            : clipSettings.downbeats
              ? {
                  enabled: true,
                  markers: deduplicateWarpMarkers(
                    getMarkersFromDownbeats(clipSettings.downbeats)
                  ),
                  speed: 1,
                  awaitingAnalysis: false,
                }
              : {
                  enabled: false,
                  markers: deduplicateWarpMarkers({ 0: 0 }),
                  speed: 1,
                  awaitingAnalysis: true,
                };
          const contentStartBeats = getWarpBeatsFromSeconds(warp, 0);
          const contentEndBeats = getWarpBeatsFromSeconds(
            warp,
            audioBuffer.duration
          );
          const parentTrack = getTakeLaneParentsByTrackId(stateRef.current)[
            trackId
          ];

          const clipToArrange = {
            streaming: false,
            name: file.name,
            color: parentTrack.color,
            amplitude: 1,
            transposition: 0,
            readStartBeats: clipSettings.readStartBeats ?? contentStartBeats,
            fadeInBeats: 0,
            fadeOutBeats: 0,
            startBeats: clipSettings.startBeats,
            endBeats: clipSettings.endBeats,
            loop: {
              startBeats: contentStartBeats,
              endBeats: contentEndBeats,
              enabled: false,
            },
            warp,
            id: uuidv4(),
            clipId: null,
            uploadId: uploadId,
            mute: false,
          };

          if (toNewTakeLane) {
            setState((state) => {
              const withTakeLane = addTakeLane(trackId)(state);
              return arrangeClip(
                withTakeLane.tracks.find((t) => t.id === trackId)!.takeLanes[0]
                  .id,
                clipToArrange
              )(withTakeLane);
            });
          } else {
            setState(arrangeClip(trackId, clipToArrange));
          }

          // 4. nuke the QG/CCI
          promoteClipCreationIntents(() => null);

          const { clipId } = await clipPromise;

          clipIdsByUploadId[uploadId] = clipId;

          // Register the clipId as an alias for the uploadId in the DSP context
          // so waveforms continue to work after the transition
          const aliasSuccess = dspModule.FfmpegAudioBuffer.addUuidAlias(
            uploadId,
            clipId
          );
          if (!aliasSuccess) {
            console.error(
              `Failed to alias uploadId ${uploadId} to clipId ${clipId}`
            );
          }

          // Also copy the buffer reference to the resolved cache so getStudioClipBuffer can find it
          const uploadedBuffer = ffmpegBuffersByUploadId[uploadId];
          if (uploadedBuffer) {
            setResolvedFfmpegBuffer(clipId, uploadedBuffer);
          }

          // 5. switch to clipId
          rewriteHistory((state) => {
            const clipsWithUploadId = getStudioClips(state)
              .filter((c) => c.uploadId === uploadId)
              .map((c) => c.id);
            if (clipsWithUploadId.length === 0) {
              return state;
            }
            return updateClips(clipsWithUploadId, (c) => ({
              ...c,
              uploadId: null,
              clipId: clipId,
            }))(state);
          });
        },
      });
    },
    []
  );

  const timelineController = useStudioTimelineController(
    mode,
    stateRef,
    playbackController.playing,
    playbackController.getCurrentBeats
  );

  // Handle spacebar play/pause
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (
        (e.target as HTMLElement)?.matches(
          'input, textarea, [contenteditable]'
        ) ||
        document.activeElement instanceof HTMLTextAreaElement ||
        document.activeElement instanceof HTMLInputElement
      )
        return;
      if (e.key === ' ') {
        if (previewController.previewingOffTimeline) {
          if (playbackController.playing) {
            playbackController.stop(true);
            previewController.stopPreviewing();
          } else {
            setTimeout(() => playbackController.play(), 0);
            previewController.stopPreviewing();
          }
        } else {
          if (playbackController.playing) {
            playbackController.stop();
          } else {
            playbackController.play();
          }
        }
      }
    };
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [
    playbackController.stop,
    playbackController.play,
    previewController.previewingOffTimeline,
    previewController.stopPreviewing,
    playbackController.playing,
  ]);

  const inFlightDrags = useInFlightDrags(timelineController.frameCountRef);
  const webGLCache = useRef<StudioWebGLCache>({
    renderers: new Map(),
    programs: new Map(),
  });

  const hasPreviewState = !!previewController.previewPackage;

  useEffect(() => {
    if (inFlightDrags.isAnyDragging || hasPreviewState) {
      setLockUndoStates(true);
    } else {
      setLockUndoStates(false);
    }
  }, [inFlightDrags.isAnyDragging, setLockUndoStates, hasPreviewState]);

  useEffect(() => {
    timelineController.frameCountRef.current++;
  }, [displayedState]);

  useAnimationFrame(() => {
    if (playbackController.playing) {
      timelineController.frameCountRef.current++;
    }
  });

  const [gridMultiplier, setGridMultiplierState] = useState(1);
  const setGridMultiplier = useCallback(
    (multiplier: number | ((prev: number) => number)) => {
      setGridMultiplierState(multiplier);
      timelineController.frameCountRef.current++;
    },
    []
  );
  const gridSizeRef = useRef(1);

  const { draggedFilePositionRef } = useFileDropReceiver(
    stateRef,
    gridSizeRef,
    arrangeFile,
    timelineController
  );

  const getMaxShift = useCallback(() => {
    return 12 / timelineController.pxPerBeatRef.current;
  }, [timelineController.pxPerBeatRef]);

  const withQueuedGeneration = useCallback(
    async (options: {
      startBeats: number | undefined;
      endBeats: number | undefined;
      targetTrackId: string | null;
      fn: (
        promoteClipCreationIntents: (
          updateFn: (c: ClipCreationIntent) => ClipCreationIntent | null,
          isRegeneration?: boolean
        ) => void,
        trackId: string
      ) => Promise<void>;
      newTrackName?: string;
      splitToTakeLanes?: boolean;
      instrument?: InstrumentSpec;
    }) => {
      const {
        startBeats,
        endBeats,
        targetTrackId,
        fn,
        newTrackName,
        splitToTakeLanes,
        instrument,
      } = options;

      const queuedGenerationId = uuidv4();

      const { trackId } = arrangeClipCreationIntent({
        clipCreationIntent: {
          startBeats,
          endBeats,
          possibleClipIds: [],
          queuedGenerationIds: [queuedGenerationId],
          name: 'Queued Generation',
          namePersists: false,
        },
        targetTrackId,
        newTrackName,
        instrument,
      });

      const unqueueGeneration = () => {
        setState(updateQueuedGeneration(queuedGenerationId, () => null));
      };

      const promoteClipCreationIntents = (
        updateFunction: (c: ClipCreationIntent) => ClipCreationIntent | null,
        isRegeneration?: boolean
      ) => {
        rewriteHistory((state) => {
          const promotedClipCreationIntentIds: string[] = [];
          let newState = updateQueuedGeneration(queuedGenerationId, (c) => {
            const result = updateFunction(c);
            if (!result) return result;

            if (
              result.possibleClipIds.length === 0 &&
              result.queuedGenerationIds.length === 0
            ) {
              return null;
            }

            if (
              result.possibleClipIds.length !== 0 &&
              result.queuedGenerationIds.includes(queuedGenerationId)
            ) {
              promotedClipCreationIntentIds.push(result.id);
              return {
                ...result,
                queuedGenerationIds: result.queuedGenerationIds.filter(
                  (id) => id !== queuedGenerationId
                ),
              };
            }

            return result;
          })(state);

          if (!splitToTakeLanes) return newState;

          const takeLanesToAddByTrackId: Record<string, StudioTakeLane[]> = {};

          newState = {
            ...newState,
            tracks: newState.tracks.map((t) => ({
              ...t,
              clipCreationIntents: t.clipCreationIntents.map((c) => {
                if (promotedClipCreationIntentIds.includes(c.id)) {
                  const possibleClipIds = c.possibleClipIds;
                  if (possibleClipIds.length === 0) {
                    return c;
                  }
                  if (possibleClipIds.length > 1) {
                    // If this is a new infill (not a regeneration), add the original clip as a take
                    // If we're regenerating existing alternatives, don't add it again
                    if (
                      !isRegeneration &&
                      c.startBeats != null &&
                      c.endBeats != null
                    ) {
                      const originalClipsInRange = t.clips.filter((clip) => {
                        const clipStart = clip.startBeats;
                        const clipEnd = clip.endBeats;
                        const selectionStart = c.startBeats!;
                        const selectionEnd = c.endBeats!;
                        // Check if clip overlaps with selection
                        return (
                          clipEnd > selectionStart && clipStart < selectionEnd
                        );
                      });

                      if (originalClipsInRange.length > 0) {
                        // Create a take lane with cuts of the original clips, trimmed to the selection
                        takeLanesToAddByTrackId[t.id] ||= [];
                        const originalTakeLane = {
                          id: uuidv4(),
                          name: 'Original',
                          height: t.height,
                          clipCreationIntents: [],
                          clips: originalClipsInRange.map((clip) => {
                            const clipStart = clip.startBeats;
                            const clipEnd = clip.endBeats;
                            const selectionStart = c.startBeats!;
                            const selectionEnd = c.endBeats!;

                            // Trim the clip to only the replaced part
                            const newStartBeats = Math.max(
                              clipStart,
                              selectionStart
                            );
                            const newEndBeats = Math.min(clipEnd, selectionEnd);

                            // Adjust readStartBeats to skip forward by the amount we trimmed from the start
                            const trimmedFromStart = newStartBeats - clipStart;
                            const newReadStartBeats =
                              clip.readStartBeats + trimmedFromStart;

                            return {
                              ...clip,
                              id: uuidv4(),
                              startBeats: newStartBeats,
                              endBeats: newEndBeats,
                              readStartBeats: newReadStartBeats,
                            };
                          }),
                        };
                        takeLanesToAddByTrackId[t.id].unshift(originalTakeLane);
                        unheardTakeLanesController.addUnheardTakeLane(
                          originalTakeLane.id
                        );
                      }
                    }

                    possibleClipIds.forEach((id, index) => {
                      takeLanesToAddByTrackId[t.id] ||= [];

                      // Count existing version take lanes (exclude "Original" and other non-version names)
                      const existingVersionCount = [
                        ...t.takeLanes,
                        ...takeLanesToAddByTrackId[t.id],
                      ].filter((lane) => lane.name.includes('Version')).length;

                      const newTakeLane = {
                        ...createTakeLane(existingVersionCount + 1),
                        clipCreationIntents: [
                          {
                            ...c,
                            id: uuidv4(),
                            possibleClipIds: [id],
                          },
                        ],
                      };
                      takeLanesToAddByTrackId[t.id].unshift(newTakeLane);
                      if (index !== 0) {
                        unheardTakeLanesController.addUnheardTakeLane(
                          newTakeLane.id
                        );
                      }
                    });
                  }
                  return {
                    ...c,
                    possibleClipIds: [possibleClipIds[0]],
                  };
                } else {
                  return c;
                }
              }),
            })),
          };

          newState = {
            ...newState,
            tracks: newState.tracks.map((t) =>
              takeLanesToAddByTrackId[t.id]?.length
                ? {
                    ...t,
                    takeLanes: [
                      ...Object.values(takeLanesToAddByTrackId[t.id]),
                      ...t.takeLanes,
                    ],
                  }
                : t
            ),
          };

          return newState;
        });
      };

      try {
        const timeout = setTimeout(() => {
          unqueueGeneration();
        }, 60 * 1000);
        await fn(promoteClipCreationIntents, trackId);
        clearTimeout(timeout);
      } finally {
        unqueueGeneration();
      }
    },
    [unheardTakeLanesController.addUnheardTakeLane]
  );

  const arrangeClipCreationIntent = useCallback(
    (options: {
      clipCreationIntent: Omit<
        ClipCreationIntent,
        'id' | 'startTrimmedBeats' | 'endTrimmedBeats'
      > & {
        startTrimmedBeats?: number;
        endTrimmedBeats?: number;
      };
      targetTrackId: string | null;
      newTrackName?: string;
      instrument?: InstrumentSpec;
    }) => {
      const {
        clipCreationIntent,
        targetTrackId: inputTrackId,
        newTrackName,
        instrument,
      } = options;
      const id = uuidv4();
      // Treat AFTER_LAST_TRACK as null (create new track)
      let trackIdToUpdate = inputTrackId;
      let createdTrack = null;
      if (trackIdToUpdate === AFTER_LAST_TRACK) {
        createdTrack = createTrack(false);
        if (instrument) {
          createdTrack.instrument = instrument;
        }
        if (newTrackName) {
          createdTrack.name = newTrackName;
        }
        trackIdToUpdate = createdTrack.id;
      }
      setState(
        combineActions(
          ...(!createdTrack
            ? []
            : [
                (state: StudioProjectState): StudioProjectState => ({
                  ...state,
                  tracks:
                    inputTrackId === AFTER_LAST_TRACK
                      ? [...state.tracks, createdTrack]
                      : insertAfterFocusedTrack(
                          state.tracks,
                          state.selection.focusedTrackId,
                          createdTrack
                        ),
                }),
              ]),
          updateTrackOrTakeLane(trackIdToUpdate!, (track) => {
            if (
              track.clipCreationIntents.find(
                (c) =>
                  c.startBeats === clipCreationIntent.startBeats &&
                  c.endBeats === clipCreationIntent.endBeats
              )
            ) {
              return {
                ...track,
                clipCreationIntents: track.clipCreationIntents.map((c) => {
                  if (
                    c.startBeats === clipCreationIntent.startBeats &&
                    c.endBeats === clipCreationIntent.endBeats
                  ) {
                    return {
                      ...c,
                      id,
                      possibleClipIds: [
                        ...c.possibleClipIds,
                        ...clipCreationIntent.possibleClipIds,
                      ],
                      queuedGenerationIds: [
                        ...c.queuedGenerationIds,
                        ...clipCreationIntent.queuedGenerationIds,
                      ],
                    };
                  } else {
                    return c;
                  }
                }),
              };
            }
            return {
              ...track,
              clipCreationIntents: [
                ...track.clipCreationIntents,
                {
                  startTrimmedBeats: 0,
                  endTrimmedBeats: 0,
                  ...clipCreationIntent,
                  id,
                },
              ],
            };
          }),
          (state) => ({
            ...state,
            selection: {
              ...state.selection,
              anchorBeats:
                clipCreationIntent.startBeats ?? state.selection.anchorBeats,
              focusBeats:
                clipCreationIntent.endBeats ?? state.selection.focusBeats,
              focusedTrackId: trackIdToUpdate!,
              trackIds: [trackIdToUpdate!],
            },
          })
        )
      );
      return { id, trackId: trackIdToUpdate! };
    },
    [setState]
  );

  const [loading, setLoading] = useState(false);
  const initializeStartedRef = useRef(false);
  const initializeStateForClip = useCallback(
    async (force: boolean = false) => {
      if (
        displayedState.editClipId &&
        (force || displayedState.tracks.length === 0)
      ) {
        if (!force && initializeStartedRef.current) return;
        setLoading(true);
        try {
          initializeStartedRef.current = true;

          const clip = await fetchClip(clipsStore, displayedState.editClipId);
          setModelTier(
            clip.major_model_version
              ? getClipModelTier(clip, false, canUseAuk)
              : ModelTier.V5
          );

          const newState = await getEditModeStateForClip(
            clipsStore,
            displayedState.editClipId,
            true
          );

          setState(newState);
        } finally {
          setLoading(false);
        }
      }
    },
    [displayedState.editClipId, setState, clearHistory, clipsStore, canUseAuk]
  );

  const restrictForEditMode = useMemo(
    () => !!displayedState.editClipId,
    [displayedState.editClipId]
  );

  const [sidebarPanelSizePx, setSidebarPanelSizePx] = useState<number>(50);
  const [sidebarSection, setSidebarSection] = useState<
    'clip-info' | 'create' | 'library'
  >('clip-info');
  const effectiveSidebarSection = useMemo(() => {
    if (sidebarPanelSizePx <= 50) {
      return null;
    }
    return sidebarSection;
  }, [sidebarPanelSizePx, sidebarSection]);

  const openAndSetSidebarSection = useCallback(
    (section: 'clip-info' | 'create' | 'library') => {
      setSidebarSection(section);
      setSidebarPanelSizePx((sidebarPanelSizePx) =>
        sidebarPanelSizePx <= 50 ? 350 : sidebarPanelSizePx
      );
    },
    []
  );

  const closeSidebar = useCallback(() => {
    setSidebarPanelSizePx(50);
  }, []);

  const getEffectiveSelection = useCallback((): StudioResolvedSelection => {
    const stateSelection = stateRef.current.selection;
    const timing = getDerivedTiming(stateRef.current);
    const inFlightDelta = inFlightDrags.get(inFlightDragKeys.selection());
    const inFlightStartDelta =
      inFlightDrags.get(inFlightDragKeys.selectionEdge('anchor')) ||
      inFlightDelta;
    const inFlightEndDelta =
      inFlightDrags.get(inFlightDragKeys.selectionEdge('focus')) ||
      inFlightDelta;

    const trackIdDragDelta = inFlightDrags.get(
      inFlightDragKeys.selectionTrackRange()
    );
    const selectableTrackIds = getSelectableTrackIds(stateRef.current);

    // Check if AFTER_LAST_TRACK is in the selection
    const hasAfterLastTrack =
      stateSelection.trackIds.includes(AFTER_LAST_TRACK);

    const firstStateTrackIdIndex = selectableTrackIds.findIndex((id) =>
      stateSelection.trackIds.includes(id)
    );
    const lastStateTrackIdIndex = selectableTrackIds.findLastIndex((id) =>
      stateSelection.trackIds.includes(id)
    );

    let trackIds = selectableTrackIds.slice(
      firstStateTrackIdIndex + Math.min(0, trackIdDragDelta),
      lastStateTrackIdIndex + Math.max(0, trackIdDragDelta) + 1
    );

    // Add AFTER_LAST_TRACK if it's in the state selection and drag delta would include it
    // Only add if not already in the sliced result to avoid duplicates
    if (hasAfterLastTrack && trackIdDragDelta >= 0) {
      if (!trackIds.includes(AFTER_LAST_TRACK)) {
        trackIds.push(AFTER_LAST_TRACK);
      }
    }

    const adjustedAnchor = stateSelection.anchorBeats + inFlightStartDelta;
    const adjustedFocus = stateSelection.focusBeats + inFlightEndDelta;

    const adjustedStart = Math.min(adjustedAnchor, adjustedFocus);
    const adjustedEnd = Math.max(adjustedAnchor, adjustedFocus);

    let startSeconds = getSecondsBetween(
      getSongStartBeats(stateRef.current),
      adjustedStart,
      timing
    );

    let endSeconds = getSecondsBetween(
      getSongStartBeats(stateRef.current),
      adjustedEnd,
      timing
    );

    if (mode === 'edit') {
      startSeconds = Math.max(0, startSeconds);
      endSeconds = Math.max(0, endSeconds);
    }

    return {
      startBeats: adjustedStart,
      endBeats: adjustedEnd,
      startSeconds,
      endSeconds,
      anchorBeats: adjustedAnchor,
      focusBeats: adjustedFocus,
      trackIds,
      focusedTrackId: stateSelection.focusedTrackId,
    };
  }, [inFlightDrags.get, mode]);

  const alignedLyricsByClipId = useAlignedLyricsByClipId(
    getAllUsedClipIds(displayedState),
    displayedState.lyricsCorrectionsByClipId
  );

  const lyricsEditController = useStudioLyricsEditController(
    stateRef,
    getEffectiveSelection,
    alignedLyricsByClipId
  );

  const { clips: clipsById } = useClips(getAllUsedClipIds(displayedState));

  // Precompute waveform aggregates for clips when they are added to the studio
  const { precomputeForClips } = useWaveformPrecomputation();
  const previousClipIds = useRef<Set<string>>(new Set());

  useEffect(() => {
    const currentClipIds = new Set(
      getAllUsedClipIds(displayedState).filter(
        (id) => clipsById[id]?.status === 'complete'
      )
    );
    const newClipIds = [...currentClipIds].filter(
      (id) => !previousClipIds.current.has(id)
    );

    if (newClipIds.length > 0) {
      console.log(
        `Precomputing waveform aggregates for new clips: ${newClipIds.join(', ')}`
      );
      precomputeForClips(newClipIds);
    }

    previousClipIds.current = new Set(currentClipIds);
  }, [displayedState, precomputeForClips, clipsById]);

  useAwaitedDownbeatsResolver(clipsById, displayedState, rewriteHistory);
  useStreamingClipResolver(displayedState, rewriteHistory);

  const stylesEditController = useStudioStylesEditController(
    clipsById,
    stateRef,
    getEffectiveSelection
  );

  const { clip: editClip } = useClip(displayedState.editClipId);

  const resetStyles = useCallback(async () => {
    if (!!editClip) {
      stylesEditController.setStyles(editClip.metadata?.tags || '');
      stylesEditController.setExcludeStyles(
        editClip.metadata?.negative_tags || ''
      );
    } else {
      const { styles, excludeStyles } =
        stylesEditController.getCurrentSelectionStyles();
      stylesEditController.setStyles(styles);
      stylesEditController.setExcludeStyles(excludeStyles);
    }
  }, [
    stylesEditController,
    !!editClip,
    editClip?.metadata?.tags,
    editClip?.metadata?.negative_tags,
  ]);

  const hasInitializedStyles = useRef(false);
  useEffect(() => {
    if (!!displayedState.editClipId && !hasInitializedStyles.current) {
      hasInitializedStyles.current = true;
      resetStyles();
    }
  }, [!!displayedState.editClipId, resetStyles]);

  const derivedTiming = getDerivedTiming(displayedState);

  const clearTimeFromClipList = useCallback(
    (startBeats: number, endBeats: number, clips: StudioClip[]) => {
      return importedClearTimeFromList(
        derivedTiming,
        startBeats,
        endBeats,
        clips
      );
    },
    [derivedTiming]
  );

  const cutClips = useCallback(
    (clips: StudioClip[], ...cutPointsBeats: number[]) => {
      return importedCutClips(derivedTiming, clips, ...cutPointsBeats);
    },
    [derivedTiming]
  );

  const cutClipCreationIntents = useCallback(
    (
      clipCreationIntents: ClipCreationIntent[],
      ...cutPointsBeats: number[]
    ) => {
      return importedCutClipCreationIntents(
        clipCreationIntents,
        ...cutPointsBeats
      );
    },
    []
  );

  useEffect(() => {
    lyricsEditController.clearLyricsSelectionOverride();
  }, [displayedState.selection]);

  const [replaceMode, setReplaceMode] = useLocalStorage<
    'smart_infill' | 'infill' | 'fixed_infill'
  >('replace-mode-may-27-2025', 'smart_infill');
  const [variationFreedom, setVariationFreedom] = useState<number>(0.5);

  const [endSelectionMode, setEndSelectionMode] = useLocalStorage<
    'replace' | 'extend'
  >(`end-selection-mode-jun-1-2025-v2`, 'replace');

  const hasEndSelection = useMemo(() => {
    const selectionStartBeats = getSelectionStartBeats(displayedState);
    const selectionEndBeats = getSelectionEndBeats(displayedState);
    const songStartBeats = getSongStartBeats(displayedState);
    const songEndBeats = getSongEndBeats(displayedState);
    return (
      selectionStartBeats <= songEndBeats &&
      selectionEndBeats >= songEndBeats &&
      selectionStartBeats > songStartBeats
    );
  }, [displayedState]);

  const focusedClip = useFocusedClip(displayedState);

  const { regenerateParams, generateMode } = useMemo(() => {
    let regenerateParams: GenerateParams | null = null;
    let generateMode: 'replace' | 'create' | 'extend' | null;
    const selectionStartBeats = getSelectionStartBeats(displayedState);
    const selectionEndBeats = getSelectionEndBeats(displayedState);
    const selectedTracks = getSelectedTracks(displayedState);

    const hasClipsBeforeSelection = selectedTracks.some((t) =>
      t.clips.some((c) => c.endBeats <= selectionStartBeats)
    );
    const hasClipsAfterSelection = selectedTracks.some((t) =>
      t.clips.some((c) => c.startBeats >= selectionEndBeats)
    );
    const hasClipsInSelection = selectedTracks.some((t) =>
      t.clips.some(
        (c) =>
          c.startBeats < selectionEndBeats && c.endBeats > selectionStartBeats
      )
    );

    if (
      focusedClip &&
      focusedClip.user_id === session.user.id &&
      focusedClip.id !== displayedState.editClipId &&
      hasInfillTask(focusedClip)
    ) {
      regenerateParams = getClipGeneratePayload(focusedClip);
      generateMode = 'replace';
    } else if (
      mode === 'edit' &&
      focusedClip &&
      focusedClip.user_id === session.user.id &&
      focusedClip.id !== displayedState.editClipId &&
      hasExtendTask(focusedClip) &&
      hasEndSelection
    ) {
      regenerateParams = getClipGeneratePayload(focusedClip);
      generateMode = 'extend';
    } else if (hasEndSelection) {
      generateMode = endSelectionMode;
    } else if (hasClipsInSelection) {
      generateMode = 'replace';
    } else if (hasClipsBeforeSelection && hasClipsAfterSelection) {
      if (
        displayedState.selection.anchorBeats !==
        displayedState.selection.focusBeats
      ) {
        generateMode = 'create';
      } else {
        // if our cursor happens to be between two clips with no space between them, behavior should be identical to a zero-length selection inside of a clip.
        generateMode = 'replace';
      }
    } else if (hasClipsBeforeSelection && mode === 'edit') {
      generateMode = 'extend';
    } else {
      generateMode = 'replace';
    }
    return { regenerateParams, generateMode };
  }, [
    displayedState,
    focusedClip,
    endSelectionMode,
    hasEndSelection,
    mode,
    session.user.id,
  ]);

  const selectionStartSeconds = getSelectionStartSeconds(displayedState);
  const selectionEndSeconds = getSelectionEndSeconds(displayedState);
  const focusedTrackClips = getFocusedTrackClips(displayedState);

  const getGenerateBlockingErrorMessage = useCallback(
    (
      currentSelectionStartSeconds: number = getSelectionStartSeconds(
        stateRef.current
      ),
      currentSelectionEndSeconds: number = getSelectionEndSeconds(
        stateRef.current
      ),
      currentFocusedTrackClips: StudioClip[] = getFocusedTrackClips(
        stateRef.current
      )
    ) => {
      const selectionDurationSeconds =
        currentSelectionEndSeconds - currentSelectionStartSeconds;

      if (mode !== 'edit' && currentFocusedTrackClips.length === 0) {
        return 'Use Instrument mode to add content to empty tracks.';
      }

      if (generateMode === 'extend') return null;

      if (generateMode === 'create') {
        return selectionDurationSeconds < 2
          ? `Selection must be at least 2 seconds long (currently ${selectionDurationSeconds.toFixed(1)} seconds)`
          : selectionDurationSeconds > 120
            ? `Selection must be at most 120 seconds long (currently ${selectionDurationSeconds.toFixed(0)} seconds)`
            : null;
      }

      if (selectionDurationSeconds <= 0.01) {
        return 'Select a range of time to replace';
      }

      if (replaceMode === 'fixed_infill') {
        return selectionDurationSeconds > 26
          ? 'Selection must be at most 26 seconds long in "Fixed" replace mode'
          : null;
      }
      if (replaceMode === 'infill') {
        return selectionDurationSeconds < 2
          ? `Selection must be at least 2 seconds long in "Classic" replace mode (currently ${selectionDurationSeconds.toFixed(1)} seconds)`
          : selectionDurationSeconds > 120
            ? `Selection must be at most 120 seconds long in "Classic" replace mode (currently ${selectionDurationSeconds.toFixed(0)} seconds)`
            : null;
      }
      if (replaceMode === 'smart_infill') {
        return selectionDurationSeconds > 120
          ? `Selection must be at most 120 seconds long (currently ${selectionDurationSeconds.toFixed(0)} seconds)`
          : null;
      }

      return null;
    },
    [generateMode, replaceMode]
  );

  const generateBlockingErrorMessage = useMemo(
    () =>
      getGenerateBlockingErrorMessage(
        selectionStartSeconds,
        selectionEndSeconds,
        focusedTrackClips
      ),
    [
      getGenerateBlockingErrorMessage,
      selectionStartSeconds,
      selectionEndSeconds,
      focusedTrackClips,
    ]
  );

  const setSelectionGapSize = useCallback(
    (newGapSize: number | ((prev: number) => number)) => {
      const newGapSizeValue =
        typeof newGapSize === 'function'
          ? newGapSize(getSelectionGapSize(stateRef.current))
          : newGapSize;
      setState(setGapSize(newGapSizeValue));
    },
    [setState]
  );

  const [modelTier, setModelTier] = useLocalStorage<ModelTier>(
    `model-tier-${studioProjectId}-may-27-2025`,
    canUseAuk ? ModelTier.V5 : ModelTier.V3_5
  );

  useEffect(() => {
    if (editClip && editClip.major_model_version) {
      setModelTier(getClipModelTier(editClip, false, canUseAuk));
    }
  }, [editClip, canUseAuk, setModelTier]);

  const [modelOverride, setModelOverride] = useLocalStorage<string | null>(
    `model-override-${studioProjectId}-jun-24-2025`,
    null
  );

  const displayedStateMatchesBaseState = displayedState === baseState;

  const commitPreview = useCallback(() => {
    if (!displayedStateMatchesBaseState) {
      setBaseState(stateRef.current);
      previewController.stopPreviewing();
    }
  }, [
    displayedStateMatchesBaseState,
    setState,
    previewController.stopPreviewing,
  ]);

  const [contextWindowBeforeBeatsState, setContextWindowBeforeBeats] =
    useState(32);
  const [contextWindowAfterBeatsState, setContextWindowAfterBeats] =
    useState(32);
  const setContextWindowBeats = useCallback(
    (beforeBeats: number, afterBeats: number = beforeBeats) => {
      setContextWindowBeforeBeats(beforeBeats);
      setContextWindowAfterBeats(afterBeats);
    },
    []
  );
  const [showContextWindowState, setShowContextWindow] = useLocalStorage(
    'show-context-window-may-26-2025',
    false
  );

  const showContextWindow =
    mode === 'studio' ||
    (showContextWindowState &&
      replaceMode === 'infill' &&
      ['replace', 'create'].includes(generateMode as any));
  const contextBeforeBeats = showContextWindow
    ? contextWindowBeforeBeatsState
    : 32;
  const contextAfterBeats = showContextWindow
    ? contextWindowAfterBeatsState
    : 32;
  const contextBeatsSetting = showContextWindow
    ? contextWindowBeforeBeatsState
    : 32;

  useDismount(() => {
    playbackController.stop();
  }, 250);

  const startReplacingLyrics = useCallback(() => {
    lyricsEditController.clearLyricsSelectionOverride();
    lyricsEditController.setReplacingLyrics(true);
    const { startBeats, endBeats } = getEffectiveSelection();
    timelineController.showBeats(startBeats - 1, endBeats + 1, true);
  }, [
    lyricsEditController.setReplacingLyrics,
    getEffectiveSelection,
    timelineController,
    lyricsEditController.clearLyricsSelectionOverride,
  ]);

  const stopReplacingLyrics = useCallback(() => {
    lyricsEditController.setReplacingLyrics(false);
    lyricsEditController.clearLyricsSelectionOverride();
  }, [
    lyricsEditController.setReplacingLyrics,
    lyricsEditController.clearLyricsSelectionOverride,
  ]);

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        lyricsEditController.setReplacingLyrics(false);
        if (
          generateMode === 'create' &&
          getSelectionGapSize(stateRef.current) > 0
        ) {
          setSelectionGapSize(0);
        }
      }
    };
    window.addEventListener('keydown', handleKeyDown);
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, [
    generateMode,
    lyricsEditController.setReplacingLyrics,
    setSelectionGapSize,
  ]);

  const [masterVolume, setMasterVolume] = useLocalStorage(
    'master-volume-jun-1-2025',
    1
  );

  const heardTrackIds = getTrackIds(heardState);

  useEffect(() => {
    if (oneTrackMode && heardTrackIds.length > 0) {
      heardTrackIds.forEach((trackId) => {
        playbackController.setTrackGain(trackId, masterVolume);
      });
    }
  }, [masterVolume, playbackController, oneTrackMode, heardTrackIds]);

  const reloadClipLyrics = useCallback(
    async (clipId: string) => {
      console.log('reloading clip');
      await fetchClip(
        clipsStore,
        clipId,
        undefined,
        undefined,
        undefined,
        true
      );
      console.log('clip fetched, fetching lyrics');
      await queryClient.invalidateQueries({
        queryKey: getAlignedLyricsQueryKey(clipId),
      });
      console.log('lyrics fetched');
    },
    [clipsStore]
  );
  const [hasMadeFirstSelection, setHasMadeFirstSelection] = useState(false);

  useEffect(() => {
    if (
      displayedState.selection.anchorBeats !==
      displayedState.selection.focusBeats
    ) {
      setHasMadeFirstSelection(true);
    }
  }, [displayedState.selection]);

  const previewOrSetState = useCallback(
    (
      id: string,
      previewState: (state: StudioProjectState) => StudioProjectState,
      forcePreview: boolean = false
    ) => {
      if (previewBeforeInsertMode || forcePreview) {
        previewController.startPreviewing(id, previewState);
      } else {
        setState(previewState);
      }
    },
    [previewBeforeInsertMode, previewController.startPreviewing, setState]
  );

  useClipCreationIntentResolver(
    // subtle bug alert: if we pass in `state` here we get an infinite loop if something is being previewed while a CCI resolves.
    // this is because the previewed state will not be updated by the unselected resolved CCI.
    baseState,
    rewriteHistory,
    setState,
    oneTrackMode && magnetMode,
    mode === 'edit', // TEMPORARY. disable when tempo alignment for inserts works better.
    mode === 'edit', // prevent streaming in edit mode
    previewOrSetState
  );

  const beatsToCanvasX = useCallback(
    (beats: number) => {
      return (
        beats * timelineController.pxPerBeatRef.current -
        timelineController.scrollXRef.current
      );
    },
    [timelineController.pxPerBeatRef, timelineController.scrollXRef]
  );

  const clientYToTrackId = useCallback(
    (clientY: number) => {
      const trackHeaderEntries = Object.entries(
        timelineController.trackHeadersRef.current
      );

      let lastTrackBottom = -Infinity;

      for (const [trackId, trackHeader] of trackHeaderEntries) {
        const rect = trackHeader.getBoundingClientRect();
        lastTrackBottom = Math.max(lastTrackBottom, rect.bottom);

        if (clientY >= rect.top && clientY <= rect.bottom) {
          return trackId;
        }
      }

      if (clientY >= lastTrackBottom) {
        return AFTER_LAST_TRACK;
      }

      return null;
    },
    [timelineController.trackHeadersRef]
  );

  const clientXToBeats = useCallback(
    (clientX: number) => {
      const wrapperRect = timelineController.getWrapperRect();
      const scrollX = timelineController.scrollXRef.current;
      const pxPerBeat = timelineController.pxPerBeatRef.current;
      const xScreenPx = clientX - wrapperRect.left;
      const xContentPx = xScreenPx + scrollX;
      const beats = xContentPx / pxPerBeat;

      return beats;
    },
    [
      timelineController.getWrapperRect,
      timelineController.scrollXRef,
      timelineController.pxPerBeatRef,
    ]
  );

  const handleClickDrag = useCallback(
    (onMouseDown: ClickDragMouseDownHandler) => {
      return importedHandleClickDrag(
        timelineController.scrollXRef,
        timelineController.getWrapperRect,
        clientXToBeats,
        clientYToTrackId,
        stateRef,
        onMouseDown
      );
    },
    [
      timelineController.scrollXRef,
      timelineController.getWrapperRect,
      clientXToBeats,
      clientYToTrackId,
      stateRef,
    ]
  );

  const focusStudioClipCallbacks = useRef<((clipId: string) => void)[]>([]);

  const onFocusStudioClip = useCallback(
    (callback: (clipId: string) => void) => {
      focusStudioClipCallbacks.current.push(callback);
      return () => {
        focusStudioClipCallbacks.current =
          focusStudioClipCallbacks.current.filter((c) => c !== callback);
      };
    },
    []
  );

  const focusStudioClip = useCallback((clipId: string) => {
    focusStudioClipCallbacks.current.forEach((callback) => callback(clipId));
  }, []);

  const dragAndDropContext = useContext(DragAndDropContext);

  const uploadAndInitializeClip = useUploadAndInitializeClip();

  const recordingController = useRecordingController(
    setState,
    displayedState,
    arrangeFile,
    playbackController
  );

  const getStudioClipContentDurationSeconds = useCallback(
    (studioClip: StudioClip): number => {
      if (studioClip.clipId) {
        const clip = clipsById[studioClip.clipId];
        if (clip) {
          return getMaximumKnownClipDuration(clip);
        }
      } else if (studioClip.uploadId) {
        const duration = audioDurationsByUploadId[studioClip.uploadId];
        if (duration !== undefined) {
          return duration;
        }
      }
      return getSecondsBetween(
        studioClip.startBeats,
        studioClip.endBeats,
        getDerivedTiming(stateRef.current)
      );
    },
    [clipsById]
  );

  // switch to `useComplainingMemo` to see which variables are causing re-renders
  return useMemo(
    () => ({
      clipsStore,
      restrictForEditMode,
      studioProjectId,
      projectId: projectStore.currentProjectId,
      state: displayedState,
      nonPreviewState: baseState,
      heardState,
      stateRef,
      gridSizeRef,
      setState,
      isReadOnlyMode,
      showCloneModal,
      setShowCloneModal,
      cloneProject,
      currentVersionId,
      timelineController,
      playbackController,

      webGLCache,
      inFlightDrags,
      dragStateRef,
      getMaxShift,
      withQueuedGeneration,
      arrangeClipCreationIntent,
      sidebarPanelSizePx,
      setSidebarPanelSizePx,
      openAndSetSidebarSection,
      effectiveSidebarSection,

      closeSidebar,
      stylesEditController,
      lyricsEditController,
      getEffectiveSelection,
      oneTrackMode,
      magnetMode,
      mode,
      cutClips,
      cutClipCreationIntents,
      clearTimeFromClipList,

      replaceMode,
      setReplaceMode,
      variationFreedom,
      setVariationFreedom,
      generateBlockingErrorMessage,
      getGenerateBlockingErrorMessage,
      initializeStateForClip,
      loading,
      setSelectionGapSize,
      undo,

      redo,
      canUndo,
      canRedo,
      modelTier,
      setModelTier,
      previewController,
      commitPreview,
      contextBeatsSetting,

      contextBeforeBeats,
      contextAfterBeats,
      setContextWindowBeats,
      setContextWindowBeforeBeats,
      setContextWindowAfterBeats,
      showContextWindow,
      setShowContextWindow,
      startReplacingLyrics,
      stopReplacingLyrics,
      generateMode,
      regenerateParams,
      setEndSelectionMode,

      hasEndSelection,
      masterVolume,
      setMasterVolume,
      reloadClipLyrics,
      hasMadeFirstSelection,
      editSessionId,
      resetStyles,
      baseState,
      previewBeforeInsertMode,

      previewOrSetState,
      modelOverride,
      setModelOverride,
      clipsById,
      beatsToCanvasX,
      clientYToTrackId,
      clientXToBeats,
      handleClickDrag,

      onFocusStudioClip,
      focusStudioClip,
      setStudioPanelMode,
      dragAndDropContext,
      setGridMultiplier,
      gridMultiplier,
      alignedLyricsByClipId,
      recordingController,
      draggedFilePositionRef,
      arrangeFile,

      unheardTakeLanesController,
      onAutoSave,
      getStudioClipContentDurationSeconds,
    }),
    [
      clipsStore,
      restrictForEditMode,
      studioProjectId,
      projectStore.currentProjectId,
      displayedState,
      heardState,
      baseState,
      stateRef,
      gridSizeRef,
      setState,
      isReadOnlyMode,
      showCloneModal,
      setShowCloneModal,
      cloneProject,
      currentVersionId,
      timelineController,
      playbackController,

      webGLCache,
      inFlightDrags,
      dragStateRef,
      getMaxShift,
      withQueuedGeneration,
      arrangeClipCreationIntent,
      sidebarPanelSizePx,
      setSidebarPanelSizePx,
      openAndSetSidebarSection,
      effectiveSidebarSection,

      closeSidebar,
      stylesEditController,
      lyricsEditController,
      getEffectiveSelection,
      oneTrackMode,
      magnetMode,
      mode,
      cutClips,
      cutClipCreationIntents,
      clearTimeFromClipList,

      replaceMode,
      setReplaceMode,
      variationFreedom,
      setVariationFreedom,
      generateBlockingErrorMessage,
      getGenerateBlockingErrorMessage,
      initializeStateForClip,
      loading,
      setSelectionGapSize,
      undo,

      redo,
      canUndo,
      canRedo,
      modelTier,
      setModelTier,
      previewController,
      commitPreview,
      contextBeatsSetting,

      contextBeforeBeats,
      contextAfterBeats,
      setContextWindowBeats,
      setContextWindowBeforeBeats,
      setContextWindowAfterBeats,
      showContextWindow,
      setShowContextWindow,
      startReplacingLyrics,
      stopReplacingLyrics,
      generateMode,
      regenerateParams,
      setEndSelectionMode,

      hasEndSelection,
      masterVolume,
      setMasterVolume,
      reloadClipLyrics,
      hasMadeFirstSelection,
      editSessionId,
      resetStyles,
      previewBeforeInsertMode,

      previewOrSetState,
      modelOverride,
      setModelOverride,
      clipsById,
      beatsToCanvasX,
      clientYToTrackId,
      clientXToBeats,
      handleClickDrag,

      onFocusStudioClip,
      focusStudioClip,
      setStudioPanelMode,
      dragAndDropContext,
      setGridMultiplier,
      gridMultiplier,
      alignedLyricsByClipId,
      recordingController,
      draggedFilePositionRef,
      arrangeFile,

      unheardTakeLanesController,
      onAutoSave,
      getStudioClipContentDurationSeconds,
    ]
  );
};

const StudioContext = createContext<ReturnType<typeof useStudioContext>>(
  undefined as never
);

const StudioContextProviderInner = observer(
  ({
    children,
    loadedState,
    studioProjectId,
    mode,
  }: {
    children: React.ReactNode;
    loadedState: StudioProjectState;
    studioProjectId: string;
    mode: 'edit' | 'studio';
  }) => {
    const studioContext = useStudioContext(studioProjectId, loadedState, mode);

    useKeyCommands(studioContext);

    // Clone project handler for the modal
    const handleCloneProject = useCallback(
      async (title?: string) => {
        const cloneProject = studioContext.cloneProject;
        const currentVersionId = studioContext.currentVersionId;

        if (!currentVersionId) {
          throw new Error('No project version ID available for cloning');
        }
        if (!cloneProject) {
          throw new Error('Clone project function not available');
        }
        return await cloneProject(currentVersionId, title);
      },
      [studioContext.cloneProject, studioContext.currentVersionId]
    );

    if (typeof window !== 'undefined') {
      (window as any).studioContext = studioContext;
      (window as any).currentState = studioContext.state;
      (window as any).heardState = studioContext.heardState;
      (window as any).setCurrentState = studioContext.setState;
    }

    return (
      <StudioContext.Provider value={studioContext}>
        {children}
        <CloneProjectModal
          isOpen={studioContext.showCloneModal}
          onClose={() => studioContext.setShowCloneModal(false)}
          onClone={handleCloneProject}
        />
      </StudioContext.Provider>
    );
  }
);

export const StudioContextProvider = observer(
  ({
    children,
    studioProject,
    mode,
  }: {
    children: React.ReactNode;
    studioProject: {
      id: string;
      title: string;
      state: StudioProjectState;
    };
    mode: 'edit' | 'studio';
  }) => {
    return (
      <StudioContextProviderInner
        loadedState={studioProject.state}
        studioProjectId={studioProject.id}
        mode={mode}
      >
        {children}
      </StudioContextProviderInner>
    );
  }
);

export type StudioContextType = ReturnType<typeof useStudioContext>;

export default StudioContext;
