import styled from '@emotion/styled';
import { noop } from 'lodash-es';
import React, {
  memo,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { useIntersectionObserver } from 'usehooks-ts';

import { FocusedObjectContext } from '@/app/(root)/create/v2/useFocusedObject';
import {
  AnyDraggedObject,
  DraggedClip,
  DraggedMultiClip,
} from '@/app/(root)/dragAndDrop/DragAndDropContext';
import Draggable, {
  useClipDraggedObject,
} from '@/app/(root)/dragAndDrop/Draggable';
import useClip from '@/hooks/useClip';
import { useContextSelector } from '@/hooks/useContextSelector';
import { useClipDownbeats } from '@/hooks/useStreamingDownbeats';
import { Clip } from '@/state/clipStore';

import { Tooltip } from '../tooltip/Tooltip';
import StudioContext from './StudioContext';

// If the height of this wrapper increases when we render a StudioBrowserSong in it
// after an intersection check passes, the next intersection check will fail, leading
// to rapid cycling between the placeholder and the song row. The min-height of the
// wrapper must be at least the height of its children. Callers of StudioSongRowWrapper
// are responsible for passing an appropriate childrenHeight (which becomes minHeight here).
const SongWrapper = styled.div<{
  isLoading: boolean;
  preventDrag?: boolean;
  minHeight?: number;
}>`
  width: 100%;
  user-select: none;
  position: relative;
  min-height: ${({ minHeight }) => `${minHeight ?? 56}px`};
  border-radius: 10px;
  overflow: hidden;
  transition: opacity 0.15s ease-in-out;
`;

interface StateAndVisibilityCheckWrapperProps {
  clipId: string;
  children: React.ReactNode;
  bypassIntersectionCheck?: boolean;
  bypassPlaybackAutoScroll?: boolean;
  childrenHeight?: number;
}

const StudioBrowserSong = memo(function StudioBrowserSong({
  clipId,
  clip,
  children,
}: {
  clipId: string;
  clip: Clip | null;
  children: React.ReactNode;
}) {
  const clipStatus = clip?.status;
  const isStreamingOrComplete = ['streaming', 'complete'].includes(
    clipStatus ?? ''
  );

  const draggedObject = useClipDraggedObject(clipId);
  const isInCurrentDraggedObject = useCallback(
    (draggedObject: AnyDraggedObject) => {
      if (draggedObject.type === 'clip') {
        return (draggedObject as DraggedClip).payload.clipId === clipId;
      }
      if (draggedObject.type === 'multi-clip') {
        return (draggedObject as DraggedMultiClip).payload.clipIds.includes(
          clipId
        );
      }
      return false;
    },
    [clipId]
  );

  // warmup
  const [mouseOverWarmupEnabled, setMouseOverWarmupEnabled] = useState(false);
  const [mouseIsOver, setMouseIsOver] = useState(false);
  const [startDownbeatWarmup, setStartDownbeatWarmup] = useState(false);

  useEffect(() => {
    const timeout = setTimeout(() => {
      setMouseOverWarmupEnabled(true);
    }, 250);
    return () => clearTimeout(timeout);
  }, [setMouseOverWarmupEnabled]);

  useEffect(() => {
    if (mouseIsOver && mouseOverWarmupEnabled) {
      const timeout = setTimeout(() => {
        setStartDownbeatWarmup(mouseIsOver);
      }, 100);
      return () => clearTimeout(timeout);
    }
  }, [mouseIsOver, mouseOverWarmupEnabled]);

  useClipDownbeats(
    startDownbeatWarmup && isStreamingOrComplete ? clipId : null
  );

  const setFocusedObject = useContextSelector(
    FocusedObjectContext,
    (context) => context?.setFocusedObject ?? noop
  );

  const isLoading = !isStreamingOrComplete;

  return (
    <>
      {clip ? (
        <div
          className='relative'
          onMouseEnter={() => setMouseIsOver(true)}
          onMouseLeave={() => setMouseIsOver(false)}
        >
          <Tooltip
            openDelay={500}
            label='Drag to timeline to add to your project'
            placement='right'
          >
            <Draggable
              draggedObject={isLoading ? null : draggedObject}
              isInCurrentDraggedObject={isInCurrentDraggedObject}
              onMouseDown={() => {
                setStartDownbeatWarmup(true);
                setFocusedObject({ type: 'clip', clipId });
              }}
            >
              {children}
            </Draggable>
          </Tooltip>

          {/* Loading/Error state overlays */}
          {isLoading && (
            /* Loading state - centered spinner with greyed out background */
            <div className='pointer-events-auto absolute inset-0 z-10 flex items-center justify-center rounded-[10px] bg-background-primary/50' />
          )}
        </div>
      ) : (
        <div className='flex h-full items-center justify-center' />
      )}
    </>
  );
});

const StateAndVisibilityCheckWrapper = memo(
  function StateAndVisibilityCheckWrapper({
    clipId,
    children,
    bypassIntersectionCheck,
    bypassPlaybackAutoScroll,
    childrenHeight,
  }: StateAndVisibilityCheckWrapperProps) {
    const previewId = useContextSelector(
      StudioContext,
      (context) => context.previewController.previewPackage?.id
    );

    const { isIntersecting, ref } = useIntersectionObserver({
      initialIsIntersecting: bypassIntersectionCheck ?? false,
      threshold: 0.1,
    });

    const wrapperRef = useRef<HTMLDivElement>(null);
    const receiveRef = useCallback((el: HTMLDivElement) => {
      wrapperRef.current = el;
      ref(el);
    }, []);

    const { clip, isLoading: clipPending } = useClip(clipId);

    const isPreviewing = useMemo(() => {
      return previewId === clipId;
    }, [previewId, clipId]);

    const wasPreviewing = useRef(false);

    useEffect(() => {
      if (bypassPlaybackAutoScroll) return;
      if (
        !wasPreviewing.current &&
        isPreviewing &&
        !isIntersecting &&
        wrapperRef.current
      ) {
        wrapperRef.current.scrollIntoView({ behavior: 'smooth' });
      }
      wasPreviewing.current = isPreviewing;
    }, [isPreviewing, isIntersecting, bypassPlaybackAutoScroll]);

    const loading = clipPending;

    return (
      <SongWrapper
        ref={receiveRef}
        isLoading={loading}
        minHeight={childrenHeight}
      >
        {(isIntersecting || bypassIntersectionCheck) && (
          <StudioBrowserSong clipId={clipId} clip={clip}>
            {children}
          </StudioBrowserSong>
        )}
      </SongWrapper>
    );
  }
);

const StudioSongRowWrapper = memo(function StudioSongRowWrapper({
  children,
  clipId,
  bypassIntersectionCheck,
  bypassPlaybackAutoScroll,
  childrenHeight,
}: {
  children: React.ReactNode;
  clipId: string;
  bypassIntersectionCheck?: boolean;
  bypassPlaybackAutoScroll?: boolean;
  childrenHeight?: number;
}) {
  const isStudioMode = useContextSelector(
    StudioContext,
    (context) => context?.mode === 'studio'
  );
  if (!isStudioMode) {
    return children;
  } else {
    return (
      <StateAndVisibilityCheckWrapper
        clipId={clipId}
        bypassIntersectionCheck={bypassIntersectionCheck}
        bypassPlaybackAutoScroll={bypassPlaybackAutoScroll}
        childrenHeight={childrenHeight}
      >
        {children}
      </StateAndVisibilityCheckWrapper>
    );
  }
});

export default StudioSongRowWrapper;
