import styled from '@emotion/styled';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin';
import { useAnimationFrame } from 'framer-motion';
import {
  $createPoint,
  $createRangeSelection,
  $getRoot,
  $getSelection,
  CAN_REDO_COMMAND,
  CAN_UNDO_COMMAND,
  COMMAND_PRIORITY_LOW,
  REDO_COMMAND,
  SELECTION_CHANGE_COMMAND,
  UNDO_COMMAND,
} from 'lexical';
import {
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { useInterval } from 'usehooks-ts';

import Button, { ButtonSize, ButtonVariant } from '@/components/button/Button';
import {
  ContextMenuItem,
  ContextMenuTrigger,
} from '@/components/contextMenu/ContextMenu';
import SpinnerSVG from '@/components/svg/SpinnerSVG';
import { Tooltip } from '@/components/tooltip/Tooltip';
import { EditRedoIcon, EditUndoIcon, MoreHorizontalIcon } from '@/icons';

import { NewLyrics, OldLyrics } from '../EditLyricsReplacement';
import EditPlaybackContext from '../EditPlaybackContext';
import SelectionContext from '../SelectionContext';
import { GlassModule, GlassModuleBody, GlassModuleHeader } from '../components';
import { getBestEditColor } from '../getSections';
import { AlignedLyric } from '../types';
import AlignedLyricsContext from './AlignedLyricsContext';
import getChangedAlignedLyrics from './getChangedAlignedLyrics';
import $getCharacterSpanRects from './getCharacterSpanRects';
import { getPlaintextLyrics } from './getPlaintextLyrics';
import $getSecondsSpanRects from './getSecondsSpanRects';
import $getSelectionStartEnd from './getSelectionStartEnd';
import {
  getLyricEndSeconds,
  getLyricStartSeconds,
} from './refineAlignedLyrics';
import splitLyrics from './splitLyrics';

const Wrapper = styled.div`
  position: relative;
  z-index: 2;
  font-size: 18px;
  height: 100%;
  overflow: auto;
  padding-bottom: 40px;
  [contenteditable] {
    transition: opacity 0.2s;
    padding: 14px;
    min-height: 100%;
    position: relative;
    z-index: 3;
    opacity: 0;
    &:focus {
      opacity: 1;
    }
    & + div {
      transition: opacity 0.2s;
      opacity: 1;
    }
  }
  [contenteditable]:focus {
    outline: none;
    & + div {
      opacity: 0;
    }
  }
`;

const Footer = styled.div<{ transparent: boolean }>`
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 4;
  height: 40px;
  backdrop-filter: blur(5px);
  background-color: #303030;
  transition: opacity 0.2s;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: ${({ transparent }) => (transparent ? 0 : 1)};
  box-shadow: 0 -8px 16px rgba(0, 0, 0, 0.3);
`;

const DisplayedTextOverlay = styled.div`
  white-space: pre-wrap;
  pointer-events: none;
  * {
    white-space: pre-wrap;
  }
  position: absolute;
  padding: 14px;
  z-index: 3;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  color: rgba(255, 255, 255, 0.5);
  .selection {
    color: white;
  }
  .section-heading {
    position: relative;
    display: inline-block;
    color: transparent;
    z-index: 2;
    > span {
      font-family: 'Input Sans', monospace;
      text-transform: uppercase;
      white-space: nowrap;
      font-size: 11px;
      display: flex;
      text-align: center;
      align-items: center;
      justify-content: center;
      position: absolute;
      top: -3px;
      bottom: 3px;
      left: -6px;
      right: -6px;
      border-radius: 4px;
      padding: 2px 2px 4px 2px;
      color: rgba(255, 255, 255, 0.5);
      z-index: 2;
    }
    &.selection > span {
      background-color: #db2e7b;
      color: white;
    }
  }
  span {
    transition: color 0.5s ease;
  }
  .playing {
    color: #db2e7b;
    transition: color 0s;
  }
`;

const BGRects = styled.div`
  position: absolute;
  z-index: 1;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  pointer-events: none;
  > * {
    position: absolute;
    border-radius: 4px;
    margin-left: -6px;
    margin-top: -6px;
    box-sizing: content-box;
    padding: 6px;
  }
  .dirty {
    background-color: #e64739;
    margin-left: -2px;
    padding: 6px 2px;
  }
  .selection {
    background-color: #303030;
  }
`;

const FGRects = styled(BGRects)`
  z-index: 3;
`;

const ButtonWrapper = styled.div`
  display: flex;
  gap: 5px;
`;

const LoadingOverlay = styled.div`
  position: absolute;
  top: -4px;
  left: -4px;
  right: -4px;
  bottom: -4px;
  background-color: #222;
  display: flex;
  flex-direction: column;
  gap: 5px;
  align-items: center;
  justify-content: center;
  z-index: 3;
`;

const UnpaddedGlassModuleBody = styled(GlassModuleBody)`
  padding: 0;
`;

const timeoutsBySpan = new Map<
  HTMLSpanElement,
  ReturnType<typeof setTimeout>
>();

const FALLBACK_PRE_EDIT_LYRICS = [{ text: '', timing: null, dirty: false }];

export default function AlignedLyricsEditor() {
  const { selectionStartSeconds, selectionEndSeconds } =
    useContext(SelectionContext);

  const wrapperRef = useRef<HTMLDivElement>(null);

  const {
    lyricsLoading,
    songLyrics,
    editPreviewLyrics,
    setUserLyrics,
    userLyrics,
    isFocused,
    setEditorLyrics,
    setSelectedLyricsTiming,
    replacementConfirmation,
  } = useContext(AlignedLyricsContext);

  const songLyricsOrFallback = useMemo(
    () => (songLyrics.length === 0 ? FALLBACK_PRE_EDIT_LYRICS : songLyrics),
    [songLyrics]
  );

  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    if (!isFocused) setSelectedLyricsTiming(null);
  }, [isFocused, setSelectedLyricsTiming]);

  const displayedLyrics = editPreviewLyrics || userLyrics;

  useEffect(() => {
    editor.update(
      () => {
        $getRoot().clear();
        $getRoot().selectStart();
        const selection = $getSelection();
        const text = getPlaintextLyrics(songLyricsOrFallback);
        selection?.insertRawText(text);
        $getRoot().selectStart();
        editor.getRootElement()?.blur();
      },
      {
        tag: 'skip-dom-selection',
      }
    );
  }, [songLyricsOrFallback]);

  const disabled = lyricsLoading || editPreviewLyrics !== null;
  const selectionStartSecondsRef = useRef(selectionStartSeconds);
  const selectionEndSecondsRef = useRef(selectionEndSeconds);

  useEffect(() => {
    selectionStartSecondsRef.current = selectionStartSeconds;
    selectionEndSecondsRef.current = selectionEndSeconds;
  }, [selectionStartSeconds, selectionEndSeconds]);

  useEffect(() => {
    if (!disabled) {
      editor.setEditable(true);
      const unregister = [
        editor.registerTextContentListener((text) => {
          if (songLyricsOrFallback.length) {
            const userLyrics = getChangedAlignedLyrics(
              songLyricsOrFallback,
              text
            );
            setUserLyrics(userLyrics);
          }
        }),
      ];
      return () => unregister.forEach((cb) => cb());
    } else {
      editor.setEditable(false);
    }
  }, [editor, disabled, setUserLyrics, songLyricsOrFallback]);

  const [canUndo, setCanUndo] = useState(false);
  const [canRedo, setCanRedo] = useState(false);

  useEffect(() => {
    if (!disabled) {
      const unregister = [
        editor.registerCommand(
          SELECTION_CHANGE_COMMAND,
          () => {
            editor.update(() => {
              const selection = $getSelection();
              let { start } = $getSelectionStartEnd();
              if (!start || !selection) return;
              if (start.type === 'element') {
                const textNode = start.getNode().getChildren()[start.offset];
                if (textNode) {
                  start = $createPoint(
                    textNode.getKey(),
                    textNode.getTextContentSize(),
                    'text'
                  );
                }
              }
              const selectionLength = selection.getTextContent().length;
              const preSelection = $createRangeSelection();
              preSelection.focus = start;
              let charsRemaining = preSelection.getTextContent().length;

              let selectionTimeStart = -1;
              let selectionTimeEnd = -1;
              let hasFoundStart = false;
              for (let i = 0; i < userLyrics.length; i++) {
                charsRemaining -= userLyrics[i].text.length;
                if (!hasFoundStart) {
                  selectionTimeStart = getLyricStartSeconds(
                    userLyrics[i],
                    selectionTimeStart
                  );
                }

                selectionTimeEnd = getLyricEndSeconds(
                  userLyrics[i],
                  selectionTimeEnd
                );

                if (!hasFoundStart && charsRemaining <= 0) {
                  hasFoundStart = true;
                  charsRemaining += selectionLength;
                }

                if (hasFoundStart && charsRemaining <= 0) {
                  break;
                }
              }
              setSelectedLyricsTiming([selectionTimeStart, selectionTimeEnd]);
            });
            return false;
          },
          COMMAND_PRIORITY_LOW
        ),
        editor.registerCommand(
          CAN_UNDO_COMMAND,
          (canUndo) => {
            setCanUndo(canUndo);
            return false;
          },
          COMMAND_PRIORITY_LOW
        ),
        editor.registerCommand(
          CAN_REDO_COMMAND,
          (canRedo) => {
            setCanRedo(canRedo);
            return false;
          },
          COMMAND_PRIORITY_LOW
        ),
      ];
      return () => unregister.forEach((cb) => cb());
    }
  }, [editor, disabled, userLyrics]);

  useEffect(() => {
    if (wrapperRef.current) {
      const handleKeyDown = (e: KeyboardEvent) => {
        if (
          e.key === ' ' &&
          !(e.target as HTMLElement)?.matches('[contenteditable]')
        ) {
          e.preventDefault();
        }
      };
      wrapperRef.current.addEventListener('keydown', handleKeyDown);
      return () => {
        wrapperRef.current?.removeEventListener('keydown', handleKeyDown);
      };
    }
  }, [wrapperRef]);

  const bgRectsRef = useRef<HTMLDivElement>(null);
  const fgRectsRef = useRef<HTMLDivElement>(null);
  const lyricsTextDisplayRef = useRef<HTMLDivElement>(null);
  const playingSpanRef = useRef<HTMLSpanElement | null>(null);
  const lyricSpansRef = useRef<
    { span: HTMLSpanElement; lyric: AlignedLyric }[]
  >([]);

  const drawLyricsBGRects = useCallback(() => {
    if (editPreviewLyrics) {
      if (bgRectsRef.current) bgRectsRef.current.innerHTML = '';
      return;
    }
    editor.read(() => {
      const selectionRects = editPreviewLyrics
        ? []
        : $getSecondsSpanRects(
            displayedLyrics,
            selectionStartSeconds,
            selectionEndSeconds
          );
      const dirtySpans: [number, number][] = [];
      let currentCharOffset = 0;

      displayedLyrics.forEach((lyric) => {
        if (lyric.dirty) {
          if (dirtySpans[dirtySpans.length - 1]?.[1] === currentCharOffset) {
            dirtySpans[dirtySpans.length - 1][1] += lyric.text.length;
          } else {
            dirtySpans.push([
              currentCharOffset,
              currentCharOffset + lyric.text.length,
            ]);
          }
        }
        currentCharOffset += lyric.text.length;
      });

      const dirtyRects = dirtySpans
        .map(([start, end]) => {
          return $getCharacterSpanRects(start, end);
        })
        .flat();

      bgRectsRef.current?.replaceChildren(
        ...selectionRects.map((rect) => {
          const element = document.createElement('div');
          element.style.top = `${rect.top}px`;
          element.style.left = `${rect.left}px`;
          element.style.width = `${rect.width}px`;
          element.style.height = `${rect.height}px`;
          element.setAttribute('class', 'selection');
          return element;
        }),
        ...dirtyRects.map((rect) => {
          const element = document.createElement('div');
          element.style.top = `${rect.top}px`;
          element.style.left = `${rect.left}px`;
          element.style.width = `${rect.width}px`;
          element.style.height = `${rect.height}px`;
          element.setAttribute('class', 'dirty');
          return element;
        })
      );
    });
  }, [
    editPreviewLyrics,
    selectionStartSeconds,
    selectionEndSeconds,
    displayedLyrics,
    editor,
  ]);

  useEffect(() => {
    drawLyricsBGRects();
    if (!lyricsTextDisplayRef.current) return;
    const resizeObserver = new ResizeObserver(() => {
      drawLyricsBGRects();
    });
    resizeObserver.observe(lyricsTextDisplayRef.current);
    return () => {
      resizeObserver.disconnect();
    };
  }, [drawLyricsBGRects]);

  const colorDisplayedLyrics = useCallback(() => {
    if (!lyricSpansRef.current) return;
    const selectedLyrics = splitLyrics(
      displayedLyrics,
      selectionStartSeconds,
      selectionEndSeconds
    )[1];
    // TODO: lyricSpansRef.current does not include un-timed lyrics.
    // this is a problem because untimed lyrics CAN be selected, but don't turn white currently.
    // should fix.
    lyricSpansRef.current.forEach(({ lyric, span }) => {
      if (selectedLyrics.includes(lyric) && !editPreviewLyrics) {
        span.classList.add('selection');
      } else {
        span.classList.remove('selection');
      }
    });
  }, [displayedLyrics, selectionStartSeconds, selectionEndSeconds]);

  useEffect(() => {
    if (!lyricsTextDisplayRef.current) return;
    const allSpans: HTMLSpanElement[] = [];
    playingSpanRef.current = null;
    lyricSpansRef.current = [];
    displayedLyrics.forEach((lyric) => {
      const span = document.createElement('span');
      if (lyric.timing) {
        lyricSpansRef.current.push({
          span,
          lyric,
        });
      }
      span.textContent = lyric.text;
      allSpans.push(span);
      if (lyric.timing?.type === 'point') {
        span.classList.add('section-heading');
        const styledChild = document.createElement('span');
        styledChild.textContent = lyric.text
          .split('][')
          .join(' ')
          .replaceAll('[', '')
          .replaceAll(']', '');
        styledChild.style.backgroundColor = getBestEditColor(lyric.text);
        styledChild.style.minWidth = `${span.textContent?.length}ch`;
        span.appendChild(styledChild);
      }
    });

    lyricsTextDisplayRef.current.replaceChildren(...allSpans);
    colorDisplayedLyrics();
    drawLyricsBGRects();
  }, [displayedLyrics]);

  useEffect(() => {
    colorDisplayedLyrics();
  }, [colorDisplayedLyrics]);

  const playback = useContext(EditPlaybackContext);
  const lastCurrentTimeRef = useRef(0);
  const hasSeenPlayingSpanRef = useRef(false);

  useInterval(
    useCallback(() => {
      const wrapper = wrapperRef.current;
      const lyricsTextDisplay = lyricsTextDisplayRef.current;
      if (!wrapper || !lyricsTextDisplay) return;
      const isRTL = wrapper.querySelector('[contenteditable] > p[dir="rtl"]');
      const displayDir = lyricsTextDisplay.getAttribute('dir');
      if (isRTL && displayDir !== 'rtl') {
        lyricsTextDisplay.setAttribute('dir', 'rtl');
      } else if (!isRTL && displayDir === 'rtl') {
        lyricsTextDisplay.setAttribute('dir', '');
      }
    }, []),
    250
  );

  useAnimationFrame(
    useCallback(() => {
      const currentTime = playback.getCurrentTime();
      if (currentTime === lastCurrentTimeRef.current) return;
      lastCurrentTimeRef.current = currentTime;
      let foundSpan = false;
      lyricSpansRef.current.forEach(({ lyric, span }) => {
        if (
          currentTime >= getLyricStartSeconds(lyric, Infinity) &&
          currentTime <= getLyricEndSeconds(lyric, Infinity)
        ) {
          foundSpan = true;
          if (playingSpanRef.current === span) return;
          if (playingSpanRef.current) {
            const playingSpan = playingSpanRef.current;
            timeoutsBySpan.set(
              playingSpan,
              setTimeout(() => playingSpan.classList.remove('playing'), 50)
            );
          }
          if (hasSeenPlayingSpanRef.current || playback.playing) {
            timeoutsBySpan.delete(span);
            span.classList.add('playing');
            playingSpanRef.current = span;
            hasSeenPlayingSpanRef.current = true;
          }
        }
      });
      if (!foundSpan) {
        if (playingSpanRef.current) {
          const playingSpan = playingSpanRef.current;
          timeoutsBySpan.set(
            playingSpan,
            setTimeout(() => playingSpan.classList.remove('playing'), 50)
          );
        }
      }
    }, [playback.getCurrentTime, playback.playing])
  );

  if (replacementConfirmation) {
    return (
      <GlassModule>
        <OldLyrics />
        <NewLyrics />
      </GlassModule>
    );
  }

  return (
    <GlassModule>
      <GlassModuleHeader>
        Lyrics
        <ButtonWrapper>
          <Tooltip label='Undo' placement='bottom'>
            <Button
              variant={ButtonVariant.Tertiary}
              size={ButtonSize.Mini}
              className='p-1.5'
              icon={EditUndoIcon}
              disabled={!canUndo}
              onMouseDown={(e) => {
                editor.dispatchCommand(UNDO_COMMAND, undefined);
                e.stopPropagation();
                e.preventDefault();
              }}
            />
          </Tooltip>
          <Tooltip label='Redo' placement='bottom'>
            <Button
              variant={ButtonVariant.Tertiary}
              size={ButtonSize.Mini}
              className='p-1.5'
              icon={EditRedoIcon}
              disabled={!canRedo}
              onMouseDown={(e) => {
                editor.dispatchCommand(REDO_COMMAND, undefined);
                e.stopPropagation();
                e.preventDefault();
              }}
            />
          </Tooltip>
          <ContextMenuTrigger
            title='Lyrics'
            placement='bottom-left'
            ButtonComponent={(props) => (
              <Button
                variant={ButtonVariant.Tertiary}
                size={ButtonSize.Mini}
                className='p-1 opacity-50 hover:opacity-100'
                icon={MoreHorizontalIcon}
                {...props}
              />
            )}
            ContentsComponent={() => (
              <ContextMenuItem
                onClick={() => {
                  setEditorLyrics(songLyricsOrFallback);
                }}
              >
                Reset Lyrics
              </ContextMenuItem>
            )}
          />
        </ButtonWrapper>
      </GlassModuleHeader>
      <UnpaddedGlassModuleBody>
        {lyricsLoading && (
          <LoadingOverlay>
            <SpinnerSVG className='h-5 w-5' />
            Aligning Lyrics...
          </LoadingOverlay>
        )}
        <Wrapper ref={wrapperRef}>
          <BGRects ref={bgRectsRef} />
          <PlainTextPlugin
            contentEditable={<ContentEditable />}
            ErrorBoundary={LexicalErrorBoundary}
          />
          <DisplayedTextOverlay ref={lyricsTextDisplayRef} />
          <FGRects ref={fgRectsRef} />
          <HistoryPlugin delay={250} />
        </Wrapper>
        <Footer transparent={!editPreviewLyrics}>
          Apply edit for more accurate lyrics.
        </Footer>
      </UnpaddedGlassModuleBody>
    </GlassModule>
  );
}
