import {
  InitialConfigType,
  LexicalComposer,
} from '@lexical/react/LexicalComposer';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import {
  $getRoot,
  $getSelection,
  BLUR_COMMAND,
  COMMAND_PRIORITY_LOW,
  FOCUS_COMMAND,
  LexicalEditor,
} from 'lexical';
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
} from 'react';

import useClip from '@/hooks/useClip';

import PreviewClipContext from '../PreviewClipContext';
import SelectionContext from '../SelectionContext';
import getSections from '../getSections';
import useEditorLyricsQuery from '../queryHooks/useEditorLyricsQuery';
import { AlignedLyric, SongSection } from '../types';
import AlignedLyricNode from './AlignedLyricNode';
import { getPlaintextLyrics } from './getPlaintextLyrics';
import splitLyrics from './splitLyrics';
import useEditPreviewLyrics from './useEditPreviewLyrics';

const EMPTY_ARRAY = [] as AlignedLyric[];

export type ReplacementConfirmation = {
  originalLyricsSplit: [
    { lyric: AlignedLyric; index: number }[],
    { lyric: AlignedLyric; index: number }[],
    { lyric: AlignedLyric; index: number }[],
  ];
  replacementLyrics: string;
};

export const useAlignedLyricsContext = (
  clipId: string,
  editor: LexicalEditor
) => {
  const { clip } = useClip(clipId);
  const { previewClip } = useContext(PreviewClipContext);
  const { selectionStartSeconds, selectionEndSeconds } =
    useContext(SelectionContext);

  const clipLyrics = useEditorLyricsQuery(clipId);
  const previewClipLyrics = useEditorLyricsQuery(previewClip?.id || null);
  const [selectedLyricsTiming, setSelectedLyricsTiming] = useState<
    [number, number] | null
  >(null);

  const [userLyrics, setUserLyrics] = useState<AlignedLyric[]>(
    clipLyrics.query.data || EMPTY_ARRAY
  );

  const editPreviewLyrics = useEditPreviewLyrics(clip, previewClip);

  const [replacementConfirmation, setReplacementConfirmation] =
    useState<ReplacementConfirmation | null>(null);

  const startReplacementConfirmation = useCallback(
    (
      preEditAlignedLyrics: AlignedLyric[],
      postEditAlignedLyrics: AlignedLyric[],
      selectionStart: number,
      selectionEnd: number
    ) => {
      let index = 0;
      const originalLyricsSplit = splitLyrics(
        preEditAlignedLyrics,
        selectionStart,
        selectionEnd
      ).map((lyrics) =>
        lyrics.map((lyric) => ({ lyric, index: index++ }))
      ) as ReplacementConfirmation['originalLyricsSplit'];

      const replacementLyrics = getPlaintextLyrics(
        splitLyrics(postEditAlignedLyrics, selectionStart, selectionEnd)[1]
      );

      setReplacementConfirmation({
        originalLyricsSplit,
        replacementLyrics,
      });
    },
    []
  );

  const updateReplacementConfirmation = useCallback(
    (
      preEditAlignedLyrics: AlignedLyric[],
      selectionStart: number,
      selectionEnd: number
    ) => {
      let index = 0;
      const originalLyricsSplit = splitLyrics(
        preEditAlignedLyrics,
        selectionStart,
        selectionEnd
      ).map((lyrics) =>
        lyrics.map((lyric) => ({ lyric, index: index++ }))
      ) as ReplacementConfirmation['originalLyricsSplit'];

      setReplacementConfirmation((prev) =>
        prev
          ? {
              ...prev,
              originalLyricsSplit,
            }
          : prev
      );
    },
    []
  );

  const setEditorLyrics = useCallback(
    (alignedLyrics: AlignedLyric[]) => {
      setUserLyrics(alignedLyrics);
      editor.update(
        () => {
          $getRoot().clear();
          $getRoot().selectStart();
          const selection = $getSelection();
          selection?.insertRawText(getPlaintextLyrics(alignedLyrics));
          $getRoot().selectStart();
        },
        {
          tag: 'skip-dom-selection',
        }
      );
    },
    [editor]
  );

  const [isFocused, setIsFocused] = useState(false);

  useEffect(
    () =>
      editor.registerCommand(
        BLUR_COMMAND,
        () => {
          setIsFocused(false);
          return false;
        },
        COMMAND_PRIORITY_LOW
      ),
    []
  );

  useEffect(
    () =>
      editor.registerCommand(
        FOCUS_COMMAND,
        () => {
          setIsFocused(true);
          return false;
        },
        COMMAND_PRIORITY_LOW
      ),
    []
  );

  const songLyrics = clipLyrics.query.data || EMPTY_ARRAY;

  useEffect(() => {
    if (songLyrics) {
      setUserLyrics(songLyrics);
    }
  }, [songLyrics]);

  const displayedLyrics = editPreviewLyrics || userLyrics;
  const lyricsLoading =
    clipLyrics.query.isLoading || previewClipLyrics.query.isLoading;

  const exportExtendLyrics = useCallback(
    (extendFromSeconds: number) => {
      const splitUserLyrics = splitLyrics(
        userLyrics,
        extendFromSeconds,
        Infinity
      );
      const splitSongLyrics = splitLyrics(
        songLyrics,
        extendFromSeconds,
        Infinity
      );
      const postExtendPlaintextLyricsAfterEdit = getPlaintextLyrics([
        ...splitUserLyrics[1],
        ...splitUserLyrics[2],
      ]);
      const postExtendPlaintextLyricsBeforeEdit = getPlaintextLyrics([
        ...splitSongLyrics[1],
        ...splitSongLyrics[2],
      ]);
      const preExtendPlaintextLyricsAfterEdit = getPlaintextLyrics(
        splitSongLyrics[0]
      );
      const fullPlaintextLyricsAfterEdit = getPlaintextLyrics([
        ...splitSongLyrics[0],
        ...splitUserLyrics[1],
        ...splitUserLyrics[2],
      ]);
      return {
        postExtendPlaintextLyricsAfterEdit,
        postExtendPlaintextLyricsBeforeEdit,
        preExtendPlaintextLyricsAfterEdit,
        fullPlaintextLyricsAfterEdit,
      };
    },
    [userLyrics, songLyrics]
  );

  const exportReplaceLyrics = useCallback(
    (
      contextStartSeconds: number | ((selectionStartSeconds: number) => number),
      contextEndSeconds: number | ((selectionEndSeconds: number) => number)
    ) => {
      const splitUserLyrics = splitLyrics(
        userLyrics,
        selectionStartSeconds,
        selectionEndSeconds
      );
      const splitSongLyrics = splitLyrics(
        songLyrics,
        selectionStartSeconds,
        selectionEndSeconds
      );

      const postEditLyrics: AlignedLyric[] = replacementConfirmation
        ? [
            ...replacementConfirmation.originalLyricsSplit[0].map(
              (l) => l.lyric
            ),
            {
              text: replacementConfirmation.replacementLyrics,
              dirty: true,
              timing: {
                type: 'point',
                seconds: (selectionStartSeconds + selectionEndSeconds) / 2,
              },
            },
            ...replacementConfirmation.originalLyricsSplit[2].map(
              (l) => l.lyric
            ),
          ]
        : [...splitSongLyrics[0], ...splitUserLyrics[1], ...splitSongLyrics[2]];

      const fullPlaintextLyricsAfterEdit = getPlaintextLyrics(postEditLyrics);

      const contextLyricsAfterEdit = splitLyrics(
        postEditLyrics,
        typeof contextStartSeconds === 'function'
          ? contextStartSeconds(selectionStartSeconds)
          : contextStartSeconds,
        typeof contextEndSeconds === 'function'
          ? contextEndSeconds(selectionEndSeconds)
          : contextEndSeconds
      )[1];

      const contextPlaintextLyricsAfterEdit = getPlaintextLyrics(
        contextLyricsAfterEdit
      );

      const selectedPlaintextLyricsAfterEdit = replacementConfirmation
        ? replacementConfirmation.replacementLyrics
        : getPlaintextLyrics(splitUserLyrics[1]);

      const selectedPlaintextLyricsBeforeEdit = getPlaintextLyrics(
        splitSongLyrics[1]
      );

      return {
        selectedPlaintextLyricsAfterEdit,
        selectedPlaintextLyricsBeforeEdit,
        contextPlaintextLyricsAfterEdit,
        fullPlaintextLyricsAfterEdit,
      };
    },
    [
      userLyrics,
      songLyrics,
      selectionStartSeconds,
      selectionEndSeconds,
      replacementConfirmation,
    ]
  );

  useEffect(() => {
    (window as any).exportReplaceLyrics = () =>
      exportReplaceLyrics(selectionStartSeconds, selectionEndSeconds);
    (window as any).exportExtendLyrics = () =>
      exportExtendLyrics(selectionStartSeconds);
  }, [
    exportReplaceLyrics,
    exportExtendLyrics,
    selectionStartSeconds,
    selectionEndSeconds,
  ]);

  const sections = useMemo(
    () =>
      lyricsLoading
        ? {
            0: {
              name: 'Loading Song Structure...',
              lyrics: [],
              color: '#666666',
            },
          }
        : getSections(displayedLyrics),
    [displayedLyrics, lyricsLoading]
  );
  const sortedSectionEntries = useMemo(() => {
    const result = Object.entries(sections)
      .map(
        ([seconds, section]) =>
          [Number(seconds), section] as [number, SongSection]
      )
      .sort(([a], [b]) => a - b);
    return result;
  }, [sections]);

  return useMemo(
    () => ({
      lyricsLoading,
      setEditorLyrics,
      isFocused,
      userLyrics,
      setUserLyrics,
      displayedLyrics,
      songLyrics,
      editPreviewLyrics,
      selectedLyricsTiming,
      setSelectedLyricsTiming,
      refetchClipLyrics: clipLyrics.query.refetch,
      exportReplaceLyrics,
      exportExtendLyrics,
      replacementConfirmation,
      setReplacementConfirmation,
      startReplacementConfirmation,
      updateReplacementConfirmation,
      skipAugmentation: clipLyrics.skipAugmentation,
      setSkipAugmentation: clipLyrics.setSkipAugmentation,
      sortedSectionEntries,
    }),
    [
      lyricsLoading,
      setEditorLyrics,
      isFocused,
      userLyrics,
      displayedLyrics,
      setUserLyrics,
      songLyrics,
      editPreviewLyrics,
      selectedLyricsTiming,
      setSelectedLyricsTiming,
      clipLyrics.query.refetch,
      exportReplaceLyrics,
      exportExtendLyrics,
      replacementConfirmation,
      setReplacementConfirmation,
      startReplacementConfirmation,
      updateReplacementConfirmation,
      clipLyrics.skipAugmentation,
      clipLyrics.setSkipAugmentation,
      sortedSectionEntries,
    ]
  );
};

const config: InitialConfigType = {
  namespace: 'AlignedLyricsEditor',
  theme: {},
  onError: (e) => {
    console.error(e);
  },
  nodes: [AlignedLyricNode],
};

const InnerAlignedLyricsProvider = ({
  children,
  clipId,
}: {
  children: React.ReactNode[] | React.ReactNode;
  clipId: string;
}) => {
  const [editor] = useLexicalComposerContext();
  const alignedLyricsEditorContext = useAlignedLyricsContext(clipId, editor);
  return (
    <AlignedLyricsContext.Provider value={alignedLyricsEditorContext}>
      {children}
    </AlignedLyricsContext.Provider>
  );
};

export const AlignedLyricsProvider = ({
  children,
  clipId,
}: {
  children: React.ReactNode[] | React.ReactNode;
  clipId: string;
}) => {
  return (
    <LexicalComposer initialConfig={config}>
      <InnerAlignedLyricsProvider clipId={clipId}>
        {children}
      </InnerAlignedLyricsProvider>
    </LexicalComposer>
  );
};

const AlignedLyricsContext = createContext<
  ReturnType<typeof useAlignedLyricsContext>
>(undefined as never);

export default AlignedLyricsContext;
