import {
  getBeatsFromZero,
  getSecondsFromZero,
} from '@suno/studiokit/timeMapping';
import {
  createContext,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';

import { useStores } from '@/app/(root)/AppProviders';
import { toast } from '@/components/toast/Toast';
import snap from '@/utils/snap';

import { EditMode } from './EditModeContext';
import { EditTiming } from './types';

export type EditCategory = 'end-fixed' | 'fluid' | 'no-selection';

export const getEditCategory = (editMode: EditMode): EditCategory => {
  if (editMode === 'extend') {
    return 'end-fixed';
  }
  if (editMode === 'fadeOut') {
    return 'end-fixed';
  }
  if (editMode === 'none' || editMode === 'fixLyrics' || editMode === 'stems') {
    return 'no-selection';
  }
  return 'fluid';
};

export const DEFAULT_SELECTION_LENGTH = 15;

const MIN_FIXED_INFILL_SELECTION_LENGTH = 0.2;
const MIN_SELECTION_LENGTH = 3;
const MAX_SELECTION_LENGTH = Infinity;
const MIN_NON_SELECTION_LENGTH = 6;

export const useSelectionContext = (
  editMode: EditMode,
  songEndSeconds: number,
  timing: EditTiming
) => {
  const { session } = useStores();
  const minSelectionLength = session.flags?.['diffusion-infill']
    ? MIN_FIXED_INFILL_SELECTION_LENGTH
    : MIN_SELECTION_LENGTH;

  const [selectionState, setSelectionState] = useState<[number, number]>(() => [
    0,
    Math.min(songEndSeconds, DEFAULT_SELECTION_LENGTH),
  ]);

  const [hasChangedSelection, setHasChangedSelection] = useState(false);

  const hasSetInitialLengthRef = useRef(false);

  useEffect(() => {
    if (songEndSeconds && hasSetInitialLengthRef && selectionState[0] > 0) {
      setHasChangedSelection(true);
    }
  }, [selectionState, songEndSeconds]);

  useEffect(() => {
    if (songEndSeconds && !hasSetInitialLengthRef.current) {
      hasSetInitialLengthRef.current = true;
      setSelectionState([
        0,
        Math.min(songEndSeconds, DEFAULT_SELECTION_LENGTH),
      ]);
    }
  }, [songEndSeconds]);

  const selectionStartSeconds = useMemo(
    () => selectionState[0],
    [selectionState]
  );

  const selectionEndSeconds = useMemo(
    () =>
      getEditCategory(editMode) === 'fluid'
        ? selectionState[1]
        : songEndSeconds,
    [editMode, selectionState, songEndSeconds]
  );

  const hasInformedAboutMinSelectionLength = useRef<boolean>(false);
  const informAboutMinSelectionLength = useCallback(() => {
    if (hasInformedAboutMinSelectionLength.current) return;
    hasInformedAboutMinSelectionLength.current = true;
    setTimeout(() =>
      toast({
        title: 'Minimum duration reached',
        description: `"Replace" requires at least ${minSelectionLength} seconds of audio.`,
        status: 'warning',
        duration: 4000,
        isClosable: true,
      })
    );
  }, [minSelectionLength]);

  const hasInformedAboutMaxSelectionLength = useRef<boolean>(false);
  const informAboutMaxSelectionLength = useCallback(() => {
    if (hasInformedAboutMaxSelectionLength.current) return;
    hasInformedAboutMaxSelectionLength.current = true;
    setTimeout(() =>
      toast({
        title: 'Maximum duration reached',
        description: `"Replace" is limited to selections of ${MAX_SELECTION_LENGTH}s or less.`,
        status: 'warning',
        duration: 4000,
        isClosable: true,
      })
    );
  }, []);

  const hasInformedAboutMinNonSelectionLength = useRef<boolean>(false);
  const informAboutMinNonSelectionLength = useCallback(() => {
    if (hasInformedAboutMinNonSelectionLength.current) return;
    hasInformedAboutMinNonSelectionLength.current = true;
    setTimeout(() =>
      toast({
        title: 'Maximum duration reached',
        description: 'Some content must be un-selected for Replace or Extend.',
        status: 'warning',
        duration: 4000,
        isClosable: true,
      })
    );
  }, []);

  const setSelection = useCallback(
    (
      selection:
        | [number, number]
        | ((selection: [number, number]) => [number, number]),
      fromExplicitUserInput: boolean = false
    ) => {
      setSelectionState((prev) => {
        const nextSelection =
          typeof selection === 'function' ? selection(prev) : selection;
        const a = nextSelection[0];
        const b = nextSelection[1];
        let newSelectionStartSeconds = Math.max(
          0,
          Math.min(songEndSeconds, a, b)
        );
        let newSelectionEndSeconds = Math.min(
          songEndSeconds,
          Math.max(0, a, b)
        );

        if (
          editMode === 'replace' &&
          songEndSeconds > minSelectionLength &&
          newSelectionEndSeconds - newSelectionStartSeconds < minSelectionLength
        ) {
          if (fromExplicitUserInput) {
            informAboutMinSelectionLength();
          }
          const secondsToAdd =
            minSelectionLength -
            (newSelectionEndSeconds - newSelectionStartSeconds);
          newSelectionStartSeconds =
            newSelectionStartSeconds - secondsToAdd / 2;
          newSelectionEndSeconds = newSelectionEndSeconds + secondsToAdd / 2;
          if (newSelectionEndSeconds > songEndSeconds) {
            const overflow = newSelectionEndSeconds - songEndSeconds;
            newSelectionStartSeconds -= overflow;
            newSelectionEndSeconds -= overflow;
          }
          if (newSelectionStartSeconds < 0) {
            newSelectionEndSeconds -= newSelectionStartSeconds;
            newSelectionStartSeconds = 0;
          }
        }

        if (
          Number.isFinite(MAX_SELECTION_LENGTH) &&
          editMode === 'replace' &&
          newSelectionEndSeconds - newSelectionStartSeconds >
            MAX_SELECTION_LENGTH
        ) {
          const overflow =
            newSelectionEndSeconds -
            newSelectionStartSeconds -
            MAX_SELECTION_LENGTH;
          newSelectionStartSeconds += overflow / 2;
          newSelectionEndSeconds -= overflow / 2;
          if (fromExplicitUserInput) {
            informAboutMaxSelectionLength();
          }
        }

        if (
          editMode === 'replace' &&
          songEndSeconds > MIN_NON_SELECTION_LENGTH + minSelectionLength &&
          newSelectionEndSeconds - newSelectionStartSeconds >
            songEndSeconds - MIN_NON_SELECTION_LENGTH
        ) {
          const overflow =
            newSelectionEndSeconds -
            newSelectionStartSeconds -
            (songEndSeconds - MIN_NON_SELECTION_LENGTH);
          newSelectionStartSeconds += overflow / 2;
          newSelectionEndSeconds -= overflow / 2;
          if (fromExplicitUserInput) {
            informAboutMinNonSelectionLength();
          }
        } else if (
          editMode === 'extend' &&
          songEndSeconds > MIN_NON_SELECTION_LENGTH + minSelectionLength &&
          newSelectionStartSeconds < MIN_NON_SELECTION_LENGTH
        ) {
          newSelectionStartSeconds = MIN_NON_SELECTION_LENGTH;
          if (fromExplicitUserInput) {
            informAboutMinNonSelectionLength();
          }
        }

        if (editMode === 'remove' && !fromExplicitUserInput) {
          const selectionStartBeats = snap(
            getBeatsFromZero(newSelectionStartSeconds, timing),
            4
          );
          const selectionEndBeats = snap(
            Math.max(
              getBeatsFromZero(newSelectionEndSeconds, timing),
              selectionStartBeats
            ),
            4
          );
          newSelectionStartSeconds = getSecondsFromZero(
            selectionStartBeats,
            timing
          );
          newSelectionEndSeconds = getSecondsFromZero(
            selectionEndBeats,
            timing
          );
        }

        return [newSelectionStartSeconds, newSelectionEndSeconds];
      });
    },
    [
      selectionState,
      songEndSeconds,
      editMode,
      informAboutMinSelectionLength,
      minSelectionLength,
    ]
  );

  useEffect(() => {
    setSelection(selectionState);
  }, [editMode]);

  const setSelectionStartSeconds = useCallback(
    (seconds: number, fromExplicitUserInput: boolean = false) => {
      setSelection((prev) => [seconds, prev[1]], fromExplicitUserInput);
    },
    [setSelection]
  );

  const setSelectionEndSeconds = useCallback(
    (seconds: number, fromExplicitUserInput: boolean = false) => {
      setSelection((prev) => [prev[0], seconds], fromExplicitUserInput);
    },
    [setSelection]
  );

  useEffect(() => {
    if (
      ['extend', 'fadeOut'].includes(editMode) &&
      !hasChangedSelection &&
      songEndSeconds &&
      hasSetInitialLengthRef.current
    ) {
      setSelection([songEndSeconds - DEFAULT_SELECTION_LENGTH, songEndSeconds]);
    }
  }, [editMode, songEndSeconds, hasChangedSelection]);

  return useMemo(
    () => ({
      selectionStartSeconds,
      selectionEndSeconds,
      setSelection,
      setSelectionStartSeconds,
      setSelectionEndSeconds,
    }),
    [
      selectionStartSeconds,
      selectionEndSeconds,
      setSelectionStartSeconds,
      setSelectionEndSeconds,
      setSelection,
    ]
  );
};

const SelectionContext = createContext<ReturnType<typeof useSelectionContext>>(
  undefined as never
);

export default SelectionContext;
