'use client';

/* eslint jsx-a11y/click-events-have-key-events: warn */

/* eslint jsx-a11y/no-static-element-interactions: warn */
import clsx from 'clsx';
import Hls from 'hls.js';
import React, {
  useCallback,
  useEffect,
  useImperativeHandle,
  useRef,
  useState,
} from 'react';
import { twMerge } from 'tailwind-merge';

import PlaybackProgress, {
  PlaybackProgressProps,
} from '@/components/mediaPlayback/PlaybackProgress';
import {
  VideoPlayerInterface,
  VideoPlayerProps,
  appendSourceElements,
  elementSupportsNativeHls,
  isHlsVideoUrl,
} from '@/components/video/SimpleVideoPlayer';
import useMediaPlayback from '@/hooks/useMediaPlayback';
import usePolledValue from '@/hooks/usePolledValue';

import PlaybackProgressHoverOverlay from '../mediaPlayback/PlaybackProgressHoverOverlay';
import { HookLyricDisplay } from './constants';

export type HooksPlayerProps = Omit<VideoPlayerProps, 'url' | 'poster'> & {
  className?: string;
  playerClassName?: string;
  overlayClassName?: string;
  metadataClassName?: string;
  lyricsOverlayClassName?: string;
  playProgressClassName?: string;
  actionsClassName?: string;
  hookId?: string;
  clipId?: string;
  clipStartTime?: number;
  clipEndTime?: number;
  lyricsInMetadata?: boolean;
  lyricDisplayStyle?: HookLyricDisplay | `${HookLyricDisplay}`;
  lyricsOverlayContent?:
    | React.ReactNode
    | React.ComponentType<{
        className?: string;
        isPlaying?: boolean;
        currentTime?: number;
        lyricDisplayStyle?: HookLyricDisplay | `${HookLyricDisplay}`;
        hookId?: string;
        clipId?: string;
        clipStartTime?: number;
        clipEndTime?: number;
      }>;
  metadataContent?:
    | React.ReactNode
    | React.ComponentType<{
        className?: string;
        isPlaying?: boolean;
        children?: React.ReactNode;
      }>;
  actionsContent?:
    | React.ReactNode
    | React.ComponentType<{ className?: string; isPlaying?: boolean }>;
  videoUrl?: string | string[] | null;
  imageUrl?: string | null;
  duration?: number;
  preload?: 'none' | 'metadata' | 'auto';
  videoElement?: HTMLVideoElement | null;
  onVideoClick?: React.MouseEventHandler;
  onVideoSeekStart?: (time: number | null, prevTime: number | null) => void;
  onVideoSeekEnd?: (time: number | null, prevTime: number | null) => void;
  ref?: React.Ref<VideoPlayerInterface | null>;
};

export type Props = Omit<
  React.HTMLAttributes<HTMLDivElement>,
  keyof HooksPlayerProps
> &
  HooksPlayerProps;

const HooksPlayer: React.FC<Props> = (props) => {
  const {
    children,
    className,
    playerClassName: explicitPlayerClassName,
    overlayClassName,
    metadataClassName: explicitMetadataClassName,
    lyricsOverlayClassName: explicitLyricsOverlayClassName,
    playProgressClassName: explicitPlayProgressClassName,
    actionsClassName: explicitActionsClassName,
    hookId,
    clipId,
    clipStartTime,
    clipEndTime,
    lyricsInMetadata = true,
    lyricDisplayStyle = HookLyricDisplay.BottomLeft,
    lyricsOverlayContent,
    metadataContent,
    actionsContent,
    ref,
    videoUrl,
    imageUrl,
    duration,
    preload = 'metadata',
    videoElement = null,
    onVideoClick,
    onVideoSeekStart,
    onVideoSeekEnd,
    // video player props
    playing, // behavior works better with playback controlled imperatively
    loop = false,
    volume,
    muted = false,
    pip = false,
    onBuffering,
    onPlaybackError,
    onPlay,
    onPause,
    onEnded,
    onSeeking,
    onSeeked,
    onRateChange,
    onLoadStart,
    onLoadedMetadata,
    onLoadedData,
    onCanPlay,
    onCanPlayThrough,
    onProgress,
    onStalled,
    onSuspend,
    onAbort,
    onError,
    onEmptied,
    onTimeUpdate,
    onDurationChange,
    onVolumeChange,
    onWaiting,
    onPlaying,
    ...restProps
  } = props;

  const videoContainerRef = useRef<HTMLDivElement>(null);
  const videoElementRef = useRef(videoElement);

  // When the video element changes, reset it and update the ref
  useEffect(() => {
    if (videoElement) {
      videoElement.pause();

      videoElement.removeAttribute('src');
      videoElement.replaceChildren();

      videoElement.load();
      videoElement.currentTime = 0;

      videoElement.poster = '';
      videoElement.muted = true;
      videoElement.volume = 1;
      // While we want Hooks to loop, we don't want to use the loop prop because it
      // prevents the `ended` event from being fired, and then we can't log when
      // a hook ends/restarts. We handle looping manually in HooksFeedClient.tsx.
      // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended_event for more details.
      videoElement.loop = loop;
      videoElement.playsInline = true;
      videoElement.className = 'absolute inset-0 w-full h-full object-contain';
    }

    videoElementRef.current = videoElement;
  }, [videoElement, loop]);

  // Use media playback hook with shared element
  const mediaPlayback = useMediaPlayback({
    mediaElement: videoElement,
    onBuffering,
    onPlaybackError,
    onPlay,
    onPause,
    onEnded,
    onSeeking,
    onSeeked,
    onRateChange,
    onLoadStart,
    onLoadedMetadata,
    onLoadedData,
    onCanPlay,
    onCanPlayThrough,
    onProgress,
    onStalled,
    onSuspend,
    onAbort,
    onError,
    onEmptied,
    onTimeUpdate,
    onDurationChange,
    onVolumeChange,
    onWaiting,
    onPlaying,
  });

  useImperativeHandle(
    ref,
    () => ({
      ...mediaPlayback,
      getInternalPlayer: () => videoElementRef.current,
    }),
    [mediaPlayback]
  );

  const { seek, getCurrentTime, getDuration } = mediaPlayback;

  const [isPlaying, setIsPlaying] = useState(false);

  const playerClassName = twMerge(
    'absolute inset-0 w-full h-full',
    explicitPlayerClassName
  );
  const lyricsOverlayClassName = twMerge(
    clsx({
      'absolute inset-0 flex items-center justify-center p-4':
        !lyricsInMetadata,
      'drop-shadow-black-overlay-2 pr-16 max-w-160': lyricsInMetadata,
    }),
    explicitLyricsOverlayClassName
  );
  const metadataClassName = twMerge(
    'absolute inset-x-4 bottom-8',
    'flex flex-col gap-4',
    explicitMetadataClassName
  );
  const playProgressClassName = twMerge(
    clsx(
      'absolute inset-x-4 bottom-4',
      'transition-opacity duration-200 opacity-0 group-hover:opacity-100',
      'max-md:opacity-100',
      '[--button-width:0px] [--track-width:2px]',
      '[--track-progress-color:var(--color-background-fog-dense)]',
      '[--track-remaining-color:var(--color-background-fog-thick)]',
      {
        'opacity-0': duration == null,
      }
    ),
    explicitPlayProgressClassName
  );
  const actionsClassName = twMerge(
    'absolute right-4 bottom-26 flex flex-col gap-2',
    explicitActionsClassName
  );

  // Load video URL and set properties
  useEffect(() => {
    if (videoElement) {
      videoElement.poster = imageUrl ?? '';
      videoElement.muted = muted;
      videoElement.loop = loop;
      videoElement.playsInline = true;
      videoElement.disablePictureInPicture = !pip;
      if ('controlsList' in videoElement) {
        videoElement.controlsList = 'nodownload';
      }
      videoElement.preload = preload;
      if (typeof volume === 'number' && volume >= 0) {
        videoElement.volume = volume;
      }
    }
  }, [videoElement, imageUrl, muted, volume, loop, pip, preload]);

  // Load video URL and set properties
  useEffect(() => {
    if (videoElement) {
      // Reset sources
      videoElement.removeAttribute('src');
      videoElement.replaceChildren();

      // Normalize video URLs
      const videoUrls = Array.isArray(videoUrl)
        ? videoUrl
        : videoUrl
          ? [videoUrl]
          : [];
      const hlsVideoUrls = videoUrls.filter((url) => isHlsVideoUrl(url));

      let hls: Hls | null = null;
      if (
        // Use hls.js if we have HLS video URLs to play but the video element
        // doesn't support native HLS
        hlsVideoUrls.length &&
        !elementSupportsNativeHls(videoElement) &&
        Hls.isSupported()
      ) {
        // Use hls.js and load just the first HLS video
        hls = new Hls();
        hls.loadSource(hlsVideoUrls[0]);
        hls.attachMedia(videoElement);
      } else {
        const supportedVideoUrls = elementSupportsNativeHls(videoElement)
          ? videoUrls
          : videoUrls.filter((url) => !isHlsVideoUrl(url));
        if (supportedVideoUrls.length === 1) {
          // Use `src` attribute for single supported video URL
          videoElement.src = supportedVideoUrls[0];
        } else if (supportedVideoUrls.length > 1) {
          appendSourceElements(videoElement, supportedVideoUrls);
        } else {
          // If we don't have any explicitly supported video URLs, use the
          // original video URLs and cross your fingers
          appendSourceElements(videoElement, videoUrls);
        }
        // These new sources aren't gonna load themselves!
        videoElement.load();
      }

      // Track play state internally so we aren't relying on `props.playing`
      const handlePlay = () => {
        setIsPlaying(true);
      };
      const handlePause = () => {
        setIsPlaying(false);
      };

      videoElement.addEventListener('play', handlePlay);
      videoElement.addEventListener('pause', handlePause);

      videoElement.currentTime = 0;

      return () => {
        videoElement.removeEventListener('play', handlePlay);
        videoElement.removeEventListener('pause', handlePause);
        hls?.destroy();
      };
    }
  }, [videoElement, videoUrl]);

  const getCurrentProgress = useCallback(
    (): [currentTime: number, duration: number] => [
      getCurrentTime() ?? 0,
      getDuration() ?? duration ?? 0,
    ],
    [duration, getCurrentTime, getDuration]
  );

  const [[currentTime, mediaDuration], updateCurrentProgress] = usePolledValue(
    getCurrentProgress,
    {
      enabled: playing ?? isPlaying,
      pollingInterval: 16,
    }
  );

  const handleSeekStart = useCallback<
    NonNullable<PlaybackProgressProps['onSeekStart']>
  >(
    (time, prevTime) => {
      onVideoSeekStart?.(time, prevTime);
    },
    [onVideoSeekStart]
  );
  const handleSeekEnd = useCallback<
    NonNullable<PlaybackProgressProps['onSeekEnd']>
  >(
    (time, prevTime) => {
      onVideoSeekEnd?.(time, prevTime);
    },
    [onVideoSeekEnd]
  );
  const handleSeekTo = useCallback<
    NonNullable<PlaybackProgressProps['onSeekTo']>
  >(
    (time: number | null) => {
      if (time != null) {
        // seekTo does not seem to like certain fractions
        const seekTime = time < 1 ? 0 : time;
        seek(seekTime);
        updateCurrentProgress();
      }
    },
    [updateCurrentProgress, seek]
  );
  const handleSeekMove = handleSeekTo;

  // Append the video element to the container
  useEffect(() => {
    const containerElement = videoContainerRef.current;
    if (containerElement && videoElement) {
      containerElement.appendChild(videoElement);
      return () => {
        if (videoElement.parentElement === containerElement) {
          containerElement.removeChild(videoElement);
        }
      };
    }
  }, [videoElement]);

  const lyricsContent =
    typeof lyricsOverlayContent === 'function' ? (
      React.createElement(lyricsOverlayContent, {
        className: lyricsOverlayClassName,
        isPlaying: playing ?? isPlaying,
        currentTime,
        lyricDisplayStyle,
        hookId,
        clipId,
        clipStartTime,
        clipEndTime,
      })
    ) : lyricsOverlayContent ? (
      <div className={lyricsOverlayClassName}>{lyricsOverlayContent}</div>
    ) : null;

  return (
    <div
      className={twMerge('relative aspect-9/16 w-full', className)}
      {...restProps}
    >
      <div ref={videoContainerRef} className={playerClassName} />
      <div
        className={twMerge(
          'pointer-events-none absolute inset-0 *:pointer-events-auto',
          'before:absolute before:inset-x-0 before:bottom-0 before:block',
          'before:h-60',
          'before:bg-linear-to-t before:from-opacity-black-90',
          overlayClassName
        )}
      >
        <div
          className={clsx('absolute inset-0', {
            'cursor-pointer': !!onVideoClick,
          })}
          onClick={onVideoClick}
        />
        {/**
         * @TODO if we decouple this component from `currentTime` and do the
         * polling internally, we won't have so many rerenders of the entire
         * `HooksPlayer` component
         */}
        <PlaybackProgress
          className={playProgressClassName}
          showTime={false}
          currentTime={currentTime}
          duration={mediaDuration}
          onSeekStart={handleSeekStart}
          onSeekEnd={handleSeekEnd}
          onSeekMove={handleSeekMove}
          onSeekTo={handleSeekTo}
          renderTooltip={PlaybackProgressHoverOverlay}
        />
        {lyricsInMetadata ? null : lyricsContent}
        {typeof metadataContent === 'function' ? (
          React.createElement(
            metadataContent,
            {
              className: metadataClassName,
              isPlaying: playing ?? isPlaying,
            },
            lyricsInMetadata ? lyricsContent : null
          )
        ) : metadataContent ? (
          <div className={metadataClassName}>
            {lyricsInMetadata ? lyricsContent : null}
            {metadataContent}
          </div>
        ) : lyricsInMetadata ? (
          lyricsContent
        ) : null}
        {typeof actionsContent === 'function' ? (
          React.createElement(actionsContent, {
            className: actionsClassName,
            isPlaying: playing ?? isPlaying,
          })
        ) : actionsContent ? (
          <div className={actionsClassName}>{actionsContent}</div>
        ) : null}
      </div>
      {children}
    </div>
  );
};

export default HooksPlayer;
