import styled from '@emotion/styled';
import {
  ForwardedRef,
  forwardRef,
  useCallback,
  useEffect,
  useRef,
  useState,
} from 'react';
import { useInterval } from 'usehooks-ts';

import { useContext } from '@/hooks/useContextSelector';
import useDebounceCallback from '@/hooks/useDebounceCallback';

import { getPlaintextLyrics } from '../edit2025/lyrics/getPlaintextLyrics';
import StudioContext from './StudioContext';
import {
  moveSelectionFocus,
  setBeatRangeSelection,
} from './actions/updateSelection';
import { inFlightDragKeys } from './hooks/useInFlightDrags';
import { BeatAlignedLyric } from './types';
import { getTimingEnd, getTimingStart } from './useStudioLyricsEditController';

const Wrapper = styled.div`
  position: relative;
  border-radius: inherit;
  transition: all 0.2s ease-in-out;
`;

const LyricsDisplay = styled.div<{ edited: boolean; fixAlignment?: boolean }>`
  position: relative;
  height: 100%;
  width: 100%;
  white-space: pre-wrap;
  overflow: auto;
  font-size: 16px;
  padding: 8px;
  color: ${(props) =>
    props.contentEditable
      ? 'white'
      : props.fixAlignment
        ? 'var(--color-border-primary)'
        : 'var(--color-border-secondary)'};

  border: 1px solid transparent;
  border-radius: 8px;

  span {
    transition: color 0.15s ease-in-out;
    position: relative;
    &.selected {
      &:before {
        content: '';
        z-index: 0;
        position: absolute;
        top: -4px;
        left: -2px;
        right: -2px;
        bottom: -4px;
        background-color: var(--color-background-tertiary);
        border-radius: 4px;
        pointer-events: none;
      }
      > span {
        position: relative;
        pointer-events: none;
        z-index: 1;
      }
      color: var(--color-foreground-primary);
    }

    &:not(.selected) {
      z-index: 2;
    }
  }

  &:focus {
    outline: none;
  }

  cursor: text;

  ${({ contentEditable }) =>
    contentEditable &&
    `
      border-color: var(--color-border-secondary);
    `}

  .grab-handle {
    display: inline-block;
    width: 3px;
    position: relative;
    background-color: var(--color-accent-brand);
    border-radius: 100px;
    &:hover,
    &:active {
      cursor: ew-resize;
      background-color: var(--color-foreground-primary);
    }
    &:before {
      content: ' ';
    }
    &:after {
      content: '';
      display: block;
      position: absolute;
      top: -4px;
      left: -4px;
      right: -4px;
      height: 11px;
      background-color: inherit;
      border-radius: 100px;
    }
    &.hidden {
      opacity: 0;
      width: 0;
      pointer-events: none;
      &:before {
        content: '';
      }
    }
  }
`;

const Placeholder = styled.div`
  position: absolute;
  top: 0;
  left: 8px;
  right: 8px;
  bottom: 8px;
  opacity: 0.35;
  display: none;
  &.active {
    display: block;
  }
  pointer-events: none;
`;

const getMouseTargetTiming = (e: MouseEvent) => {
  const targetElement = e.target as HTMLElement;
  const timingStart = parseFloat(
    targetElement.getAttribute('data-timing-start') as string
  );
  const timingEnd = parseFloat(
    targetElement.getAttribute('data-timing-end') as string
  );
  return {
    timingStart,
    timingEnd,
  };
};

export default forwardRef(function StudioLyricsDisplay(
  {
    selectedOnly,
    fixAlignment,
    editable,
  }: {
    selectedOnly?: boolean;
    fixAlignment?: boolean;
    editable?: boolean;
  },
  ref: ForwardedRef<HTMLDivElement>
) {
  const studioContext = useContext(StudioContext);

  const lyricsDisplayRef = useRef<HTMLDivElement>(null);
  const receiveLyricsDisplayRef = useCallback(
    (el: HTMLDivElement) => {
      lyricsDisplayRef.current = el;

      if (ref) {
        if (typeof ref === 'function') {
          ref(el);
        } else {
          ref.current = el;
        }
      }
    },
    [ref]
  );
  const placeholderRef = useRef<HTMLDivElement>(null);
  const draggingStartIndex = useRef<number | null>(null);
  const draggingEndIndex = useRef<number | null>(null);
  const [displayingExistingLyrics, setDisplayingExistingLyrics] = useState(
    !(editable && studioContext.lyricsEditController.editedLyrics !== null)
  );

  const lastDisplayedLyricsRef = useRef<
    [BeatAlignedLyric[], BeatAlignedLyric[], BeatAlignedLyric[]]
  >([[], [], []]);

  const selectingRef = useRef<boolean>(false);

  const scrollIntoViewDebounced = useDebounceCallback(() => {
    if (selectingRef.current) return;

    const beforeHandle = lyricsDisplayRef.current?.querySelector(
      '.grab-handle-before'
    );

    beforeHandle?.parentElement?.scrollTo({
      top: (beforeHandle as any).offsetTop - 30,
      behavior: 'smooth',
    });
  }, 100);

  const lastFixAlignmentRef = useRef<boolean>(fixAlignment);

  useInterval(() => {
    if (!lyricsDisplayRef.current) {
      return;
    }
    const lyrics = studioContext.lyricsEditController.getSplitLyrics();
    if (
      (lyrics !== lastDisplayedLyricsRef.current ||
        fixAlignment !== lastFixAlignmentRef.current) &&
      displayingExistingLyrics
    ) {
      const [preSelect, inSelect, postSelect] = lyrics;

      const startIsChanging =
        preSelect.length !== lastDisplayedLyricsRef.current[0].length;
      const endIsChanging =
        postSelect.length !== lastDisplayedLyricsRef.current[2].length;

      if (selectedOnly) {
        lyricsDisplayRef.current.innerHTML = inSelect
          .map(
            (l) =>
              `<span data-timing-start="${getTimingStart(l.timing)}" data-timing-end="${getTimingEnd(l.timing)}">${l.text}</span>`
          )
          .join('');
        if (startIsChanging && !endIsChanging) {
          if (!selectingRef.current) {
            lyricsDisplayRef.current.scrollTo({
              top: -30,
              behavior: 'instant',
            });
          }
        } else if (!startIsChanging && endIsChanging) {
          if (!selectingRef.current) {
            lyricsDisplayRef.current.scrollTo({
              top: lyricsDisplayRef.current.scrollHeight,
              behavior: 'instant',
            });
          }
        }
      } else {
        draggingStartIndex.current = preSelect.length;
        draggingEndIndex.current = preSelect.length + inSelect.length;

        lyricsDisplayRef.current.innerHTML = [
          ...preSelect.map(
            (l, i) =>
              `<span data-index="${i}"data-timing-start="${getTimingStart(l.timing)}" data-timing-end="${getTimingEnd(l.timing)}">${l.text}</span>`
          ),
          `<span class="grab-handle grab-handle-before${fixAlignment ? '' : ' hidden'}"></span>`,
          ...inSelect.map(
            (l, i) =>
              `<span data-index="${i + preSelect.length}" data-timing-start="${getTimingStart(l.timing)}" data-timing-end="${getTimingEnd(l.timing)}" class="selected"><span>${l.text}</span></span>`
          ),
          `<span class="grab-handle grab-handle-after${fixAlignment ? '' : ' hidden'}"></span>`,
          ...postSelect.map(
            (l, i) =>
              `<span data-index="${i + preSelect.length + inSelect.length}" data-timing-start="${getTimingStart(l.timing)}" data-timing-end="${getTimingEnd(l.timing)}">${l.text}</span>`
          ),
        ].join('');
      }

      lastDisplayedLyricsRef.current = lyrics;
    }
    lastFixAlignmentRef.current = fixAlignment;

    if (editable && lyricsDisplayRef.current.innerText.trim() === '') {
      placeholderRef.current?.classList.add('active');
    } else {
      placeholderRef.current?.classList.remove('active');
    }

    if (displayingExistingLyrics) {
      const currentBeats = studioContext.playbackController.getCurrentBeats();
      Array.from(lyricsDisplayRef.current?.children || []).forEach((child) => {
        const childAsSpan = child as any as HTMLSpanElement;
        const timingStart = childAsSpan.getAttribute('data-timing-start') as
          | number
          | null;
        const timingEnd = childAsSpan.getAttribute('data-timing-end') as
          | number
          | null;

        if (timingStart === null || timingEnd === null) {
          childAsSpan.style.color = '';
          return;
        }

        if (
          currentBeats >= timingStart &&
          currentBeats <= timingEnd &&
          studioContext.playbackController.playing
        ) {
          childAsSpan.style.color = 'var(--color-accent-brand)';
        } else {
          childAsSpan.style.color = '';
        }
      });
    }
  }, 50);

  useEffect(() => {
    if (selectingRef.current) return;
    scrollIntoViewDebounced();
  }, [studioContext.state.selection]);

  useEffect(() => {
    if (fixAlignment && !selectingRef.current) {
      scrollIntoViewDebounced();
    }
  }, [fixAlignment]);

  useEffect(() => {
    if (!editable || studioContext.lyricsEditController.editedLyrics === null) {
      setDisplayingExistingLyrics(true);
    }
  }, [editable, studioContext.lyricsEditController.editedLyrics]);

  useEffect(() => {
    if (
      !displayingExistingLyrics &&
      editable &&
      studioContext.lyricsEditController.editedLyrics !== null
    ) {
      lastDisplayedLyricsRef.current = [[], [], []];
    }
  }, [
    displayingExistingLyrics,
    editable,
    studioContext.lyricsEditController.editedLyrics,
  ]);

  useEffect(() => {
    if (
      lyricsDisplayRef.current &&
      document.activeElement !== lyricsDisplayRef.current &&
      editable &&
      studioContext.lyricsEditController.editedLyrics !== null
    ) {
      lyricsDisplayRef.current.innerText =
        studioContext.lyricsEditController.editedLyrics;
    }
  }, [editable, studioContext.lyricsEditController.editedLyrics]);

  return (
    <Wrapper className='lyrics-display'>
      <LyricsDisplay
        className='lyrics-display-inner'
        fixAlignment={fixAlignment}
        dir='auto'
        ref={receiveLyricsDisplayRef}
        edited={!displayingExistingLyrics}
        contentEditable={editable}
        style={editable ? undefined : { userSelect: 'none' }}
        onDoubleClick={() => {
          if (!editable) studioContext.startReplacingLyrics();
        }}
        onMouseDown={(e) => {
          if (
            (e.target as HTMLElement).classList.contains('grab-handle-after')
          ) {
            const onMouseMove = (e: MouseEvent) => {
              const targetElement = e.target as HTMLSpanElement;
              const index = parseInt(
                targetElement.getAttribute('data-index') as string
              );
              if (Number.isNaN(index)) {
                return;
              }
              const targetElementRect = targetElement.getBoundingClientRect();
              const indexOffset =
                e.clientX > targetElementRect.left + targetElementRect.width / 2
                  ? 1
                  : 0;
              studioContext.lyricsEditController.setEndIndexOverride(
                index + indexOffset
              );
            };
            const onMouseUp = () => {
              if (
                draggingStartIndex.current !== null &&
                draggingEndIndex.current !== null
              ) {
                studioContext.lyricsEditController.commitLyricsSelectionOverride();
              }
              window.removeEventListener('mousemove', onMouseMove);
              window.removeEventListener('mouseup', onMouseUp);
            };
            window.addEventListener('mousemove', onMouseMove);
            window.addEventListener('mouseup', onMouseUp);
          } else if (
            (e.target as HTMLElement).classList.contains('grab-handle-before')
          ) {
            const onMouseMove = (e: MouseEvent) => {
              const targetElement = e.target as HTMLSpanElement;
              const index = parseInt(
                targetElement.getAttribute('data-index') as string
              );
              if (Number.isNaN(index)) {
                return;
              }
              const targetElementRect = targetElement.getBoundingClientRect();
              const indexOffset =
                e.clientX > targetElementRect.left + targetElementRect.width / 2
                  ? 1
                  : 0;
              studioContext.lyricsEditController.setStartIndexOverride(
                index + indexOffset
              );
            };
            const onMouseUp = () => {
              if (
                draggingStartIndex.current !== null &&
                draggingEndIndex.current !== null
              ) {
                studioContext.lyricsEditController.commitLyricsSelectionOverride();
              }
              window.removeEventListener('mousemove', onMouseMove);
              window.removeEventListener('mouseup', onMouseUp);
            };
            window.addEventListener('mousemove', onMouseMove);
            window.addEventListener('mouseup', onMouseUp);
          } else if (!editable && !fixAlignment) {
            const targetElement = e.target as HTMLSpanElement;
            const mouseDownStart = parseFloat(
              targetElement.getAttribute('data-timing-start') as string
            );
            const mouseDownEnd = parseFloat(
              targetElement.getAttribute('data-timing-end') as string
            );
            if (Number.isNaN(mouseDownStart) || Number.isNaN(mouseDownEnd)) {
              return;
            }
            selectingRef.current = true;
            try {
              studioContext.setState(
                setBeatRangeSelection(mouseDownStart, mouseDownEnd)
              );
              const handleMove = (e: MouseEvent) => {
                if (
                  !(e.target as HTMLElement).closest('.lyrics-display-inner')
                ) {
                  return;
                }

                const { timingStart: mouseMoveStart, timingEnd: mouseMoveEnd } =
                  getMouseTargetTiming(e);
                if (
                  Number.isNaN(mouseMoveStart) ||
                  Number.isNaN(mouseMoveEnd)
                ) {
                  return;
                }

                const delta =
                  mouseMoveStart < mouseDownStart
                    ? mouseMoveStart - mouseDownStart
                    : mouseMoveEnd > mouseDownEnd
                      ? mouseMoveEnd - mouseDownEnd
                      : 0;

                studioContext.inFlightDrags.update(
                  inFlightDragKeys.selectionEdge('focus'),
                  delta
                );

                if (studioContext.lyricsEditController.replacingLyrics) {
                  studioContext.startReplacingLyrics(); // to re-zoom on selection
                }
              };

              const handleUp = () => {
                selectingRef.current = false;

                const selectionDelta = studioContext.inFlightDrags.get(
                  inFlightDragKeys.selectionEdge('focus')
                );

                if (selectionDelta) {
                  studioContext.setState(moveSelectionFocus(selectionDelta));
                }

                if (studioContext.lyricsEditController.replacingLyrics) {
                  studioContext.startReplacingLyrics(); // to re-zoom on selection
                }

                studioContext.inFlightDrags.finishAll();
                window.removeEventListener('mousemove', handleMove);
                window.removeEventListener('mouseup', handleUp);
              };
              window.addEventListener('mousemove', handleMove);
              window.addEventListener('mouseup', handleUp);
            } catch (e) {
              console.error(e);
              selectingRef.current = false;
            }
          }
        }}
        onPaste={(e) => {
          if (document.execCommand !== undefined) {
            e.preventDefault();
            const text = e.clipboardData.getData('text/plain');
            document.execCommand('insertText', false, text);
          }
        }}
        onFocus={() => {
          if (editable) {
            setDisplayingExistingLyrics(false);
          }
        }}
        onInput={(e) => {
          if (editable) {
            if (studioContext.lyricsEditController.editedLyrics === null) {
              studioContext.lyricsEditController.setEditedLyrics(
                e.currentTarget.innerText
              );
            }
          }
        }}
        onBlur={(e) => {
          e.currentTarget.innerHTML = e.currentTarget.innerText;
          if (
            editable &&
            e.currentTarget.innerText.trim() !==
              getPlaintextLyrics(
                studioContext.lyricsEditController.getSplitLyrics()[1]
              ).trim()
          ) {
            studioContext.lyricsEditController.setEditedLyrics(
              e.currentTarget.innerText
            );
          } else {
            setDisplayingExistingLyrics(true);
          }
        }}
      />
      {editable && displayingExistingLyrics && (
        <Placeholder ref={placeholderRef}>Enter lyrics to insert</Placeholder>
      )}
    </Wrapper>
  );
});
