import {
  getWarpBeatsFromSeconds,
  getWarpSecondsFromBeats,
} from '@suno/studiokit/warpUtils';
import { isEqual } from 'lodash-es';
import {
  RefObject,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';

import { getPlaintextLyrics } from '../edit2025/lyrics/getPlaintextLyrics';
import splitLyrics from '../edit2025/lyrics/splitLyrics';
import { AlignedLyric, EditTiming } from '../edit2025/types';
import clearTimeFromClipList from './clearTimeFromClipList';
import getAbsolutePoint from './getAbsolutePoint';
import getClipRelativePoint from './getClipRelativePoint';
import { getAlignedLyricsByTrackId } from './selectors';
import splitBeatAlignedLyrics from './splitBeatAlignedLyrics';
import {
  BeatAlignedLyric,
  LyricsEditSpec,
  StudioClip,
  StudioResolvedSelection,
} from './types';
import { StudioProjectState } from './types';

export const getLyricsBetween = (
  timing: EditTiming,
  clipList: StudioClip[],
  alignedLyricsByClipId: Record<string, AlignedLyric[]>,
  startBeats: number,
  endBeats: number
): BeatAlignedLyric[] => {
  const trimmedSelectedClips = clearTimeFromClipList(
    timing,
    -Infinity,
    startBeats,
    clearTimeFromClipList(timing, endBeats, Infinity, clipList)
  );

  const alignedLyricsPerClip = trimmedSelectedClips.map((c) => {
    if (!c.clipId) return [];

    const lyrics = alignedLyricsByClipId[c.clipId];

    if (!lyrics) return [];

    const clipStartSeconds = getWarpSecondsFromBeats(c.warp, c.readStartBeats);
    const clipEndSeconds = getWarpSecondsFromBeats(
      c.warp,
      getClipRelativePoint(timing, c, c.endBeats, true)[0]
    );

    const secondAlignedLyrics = splitLyrics(
      lyrics,
      clipStartSeconds,
      clipEndSeconds,
      false,
      true
    )[1];

    const beatAlignedLyricsForClip = secondAlignedLyrics.map((l) => ({
      ...l,
      timing:
        l.timing?.type === 'point'
          ? {
              type: 'point',
              beats: getAbsolutePoint(
                timing,
                c,
                getWarpBeatsFromSeconds(c.warp, l.timing.seconds),
                true
              ),
            }
          : l.timing?.type === 'range'
            ? {
                type: 'range',
                startBeats: Math.max(
                  c.startBeats,
                  getAbsolutePoint(
                    timing,
                    c,
                    getWarpBeatsFromSeconds(c.warp, l.timing.startSeconds),
                    true
                  )
                ),
                endBeats: Math.min(
                  c.endBeats,
                  getAbsolutePoint(
                    timing,
                    c,
                    getWarpBeatsFromSeconds(c.warp, l.timing.endSeconds),
                    true
                  )
                ),
              }
            : null,
    })) as BeatAlignedLyric[];

    return beatAlignedLyricsForClip;
  });

  const alignedLyrics: BeatAlignedLyric[] = [];

  for (let i = 0; i < alignedLyricsPerClip.length; i++) {
    alignedLyrics.push(...alignedLyricsPerClip[i]);
  }

  if (alignedLyrics.every((l) => !l.text.trim())) {
    return [];
  }

  return alignedLyrics;
};

export const getTimingStart = (timing: BeatAlignedLyric['timing']) => {
  if (!timing) return null;
  return timing.type === 'range' ? timing.startBeats : timing.beats;
};

export const getTimingEnd = (timing: BeatAlignedLyric['timing']) => {
  if (!timing) return null;
  return timing.type === 'range' ? timing.endBeats : timing.beats;
};

const EMPTY_ARRAY: BeatAlignedLyric[] = [];

export default function useStudioLyricsEditController(
  stateRef: RefObject<StudioProjectState>,
  getEffectiveSelection: () => StudioResolvedSelection,
  alignedLyricsByClipId: Record<string, AlignedLyric[]>
) {
  const [editedLyrics, setEditedLyrics] = useState<string | null>(null);
  const [hasChangedLyrics, setHasChangedLyrics] = useState<boolean>(false);

  const [lyricsSelectionOverride, setLyricsSelectionOverride] = useState<{
    startIndex: number;
    endIndex: number;
  } | null>(null);

  const lyricsSelectionOverrideRef = useRef(lyricsSelectionOverride);

  const lastStateRef = useRef(stateRef.current);
  const lastEffectiveSelectionRef = useRef<StudioResolvedSelection | null>(
    null
  );
  const lastLyricsSelectionOverrideRef = useRef(lyricsSelectionOverride);
  const lastSplitLyricsRef = useRef<
    [BeatAlignedLyric[], BeatAlignedLyric[], BeatAlignedLyric[]]
  >([[], [], []]);

  const getSplitLyrics = useCallback<
    (
      state: StudioProjectState,
      effectiveSelection: StudioResolvedSelection,
      alignedLyricsByTrackId: Record<string, BeatAlignedLyric[]>
    ) => [BeatAlignedLyric[], BeatAlignedLyric[], BeatAlignedLyric[]]
  >((state, effectiveSelection, alignedLyricsByTrackId) => {
    if (!state.selection.focusedTrackId)
      return [[], [], []] as [
        BeatAlignedLyric[],
        BeatAlignedLyric[],
        BeatAlignedLyric[],
      ];
    const allLyrics =
      alignedLyricsByTrackId[state.selection.focusedTrackId] ?? EMPTY_ARRAY;

    if (lyricsSelectionOverrideRef.current) {
      return [
        allLyrics.slice(0, lyricsSelectionOverrideRef.current.startIndex),
        allLyrics.slice(
          lyricsSelectionOverrideRef.current.startIndex,
          lyricsSelectionOverrideRef.current.endIndex
        ),
        allLyrics.slice(lyricsSelectionOverrideRef.current.endIndex),
      ] as [BeatAlignedLyric[], BeatAlignedLyric[], BeatAlignedLyric[]];
    } else {
      return splitBeatAlignedLyrics(
        allLyrics,
        effectiveSelection.startBeats,
        effectiveSelection.endBeats
      ) as [BeatAlignedLyric[], BeatAlignedLyric[], BeatAlignedLyric[]];
    }
  }, []);

  const lastAlignedLyricsByClipIdRef = useRef<Record<string, AlignedLyric[]>>(
    {}
  );
  const getCachedSplitLyrics = useCallback(() => {
    const state = stateRef.current;
    const effectiveSelection = getEffectiveSelection();
    if (
      lastStateRef.current === state &&
      isEqual(lastEffectiveSelectionRef.current, effectiveSelection) &&
      isEqual(
        lastLyricsSelectionOverrideRef.current,
        lyricsSelectionOverrideRef.current
      ) &&
      lastAlignedLyricsByClipIdRef.current === alignedLyricsByClipId
    ) {
      return lastSplitLyricsRef.current;
    }
    lastAlignedLyricsByClipIdRef.current = alignedLyricsByClipId;
    lastStateRef.current = state;
    lastEffectiveSelectionRef.current = effectiveSelection;
    lastLyricsSelectionOverrideRef.current = lyricsSelectionOverrideRef.current;
    lastSplitLyricsRef.current = getSplitLyrics(
      state,
      effectiveSelection,
      getAlignedLyricsByTrackId({
        state,
        alignedLyricsByClipId,
      })
    );
    return lastSplitLyricsRef.current;
  }, [getSplitLyrics, alignedLyricsByClipId]);

  const setStartIndexOverride = useCallback(
    (startIndex: number) => {
      const splitLyrics = getCachedSplitLyrics();
      const endIndex = splitLyrics[0].length + splitLyrics[1].length;
      lyricsSelectionOverrideRef.current = {
        startIndex: Math.min(endIndex, startIndex),
        endIndex,
      };
    },
    [getCachedSplitLyrics]
  );

  const setEndIndexOverride = useCallback(
    (endIndex: number) => {
      const splitLyrics = getCachedSplitLyrics();
      const startIndex = splitLyrics[0].length;
      lyricsSelectionOverrideRef.current = {
        startIndex,
        endIndex: Math.max(startIndex, endIndex),
      };
    },
    [getCachedSplitLyrics]
  );

  const commitLyricsSelectionOverride = useCallback(() => {
    setLyricsSelectionOverride(lyricsSelectionOverrideRef.current);
  }, [setLyricsSelectionOverride]);

  const clearLyricsSelectionOverride = useCallback(() => {
    setLyricsSelectionOverride(null);
    lyricsSelectionOverrideRef.current = null;
  }, [setLyricsSelectionOverride]);

  const [replacingLyrics, setReplacingLyrics] = useState<boolean>(false);

  const getLyricsEditSpec = useCallback((): LyricsEditSpec => {
    const splitLyrics = getCachedSplitLyrics();
    const originalPlaintextLyrics = getPlaintextLyrics(splitLyrics[1]);
    const addingLyrics =
      splitLyrics[0].length === 0 &&
      splitLyrics[1].length === 0 &&
      splitLyrics[2].length === 0;
    return {
      beatAlignedLyricsBefore: splitLyrics[0],
      originalPlaintextLyrics,
      editedPlaintextLyrics:
        replacingLyrics || addingLyrics
          ? (editedLyrics ?? originalPlaintextLyrics)
          : originalPlaintextLyrics,
      beatAlignedLyricsAfter: splitLyrics[2],
    };
  }, [getCachedSplitLyrics, editedLyrics, replacingLyrics]);

  const getFullSongPlaintextLyrics = useCallback(() => {
    const splitLyrics = getCachedSplitLyrics();
    return getPlaintextLyrics(splitLyrics.flat());
  }, [getCachedSplitLyrics]);

  const resetEditedLyrics = useCallback(() => {
    setEditedLyrics(null);
    setHasChangedLyrics(false);
  }, [getCachedSplitLyrics]);

  const canResetLyrics = useMemo(() => {
    return editedLyrics !== null;
  }, [editedLyrics, getCachedSplitLyrics]);

  useEffect(() => {
    if (replacingLyrics) {
      setHasChangedLyrics(
        editedLyrics !== null &&
          editedLyrics !== getPlaintextLyrics(getCachedSplitLyrics()[1])
      );
    }
  }, [editedLyrics, getCachedSplitLyrics, replacingLyrics]);

  return useMemo(
    () => ({
      getSplitLyrics: getCachedSplitLyrics,
      setStartIndexOverride,
      setEndIndexOverride,
      commitLyricsSelectionOverride,
      clearLyricsSelectionOverride,
      editedLyrics,
      setEditedLyrics,
      getLyricsEditSpec,
      getFullSongPlaintextLyrics,
      replacingLyrics,
      setReplacingLyrics,
      resetEditedLyrics,
      hasChangedLyrics,
      canResetLyrics,
    }),
    [
      getCachedSplitLyrics,
      lyricsSelectionOverride,
      lyricsSelectionOverrideRef,
      setStartIndexOverride,
      setEndIndexOverride,
      commitLyricsSelectionOverride,
      clearLyricsSelectionOverride,
      editedLyrics,
      setEditedLyrics,
      getLyricsEditSpec,
      getFullSongPlaintextLyrics,
      replacingLyrics,
      setReplacingLyrics,
      resetEditedLyrics,
      hasChangedLyrics,
      canResetLyrics,
    ]
  );
}
