import Hls from 'hls.js';
import React, {
  useCallback,
  useEffect,
  useImperativeHandle,
  useRef,
  useState,
} from 'react';

import useMediaPlayback, {
  UseMediaPlaybackOptions,
  UseMediaPlaybackReturn,
} from '@/hooks/useMediaPlayback';

export interface VideoPlayerInterface extends UseMediaPlaybackReturn {
  getInternalPlayer: () => HTMLVideoElement | null;
}

export type VideoPlayerProps = {
  url?: string | string[];
  playing?: boolean;
  pip?: boolean;
  volume?: number;
  ref?: React.Ref<VideoPlayerInterface | null>;
} & Pick<
  React.VideoHTMLAttributes<HTMLVideoElement>,
  'muted' | 'loop' | 'poster' | 'autoPlay' | 'preload'
> &
  Omit<UseMediaPlaybackOptions, 'mediaElement' | 'mediaUrl'>;

/**
 * Determines whether the supplied video URL requires HLS support
 */
export function isHlsVideoUrl(videoUrl: string) {
  return videoUrl.includes('.m3u8');
}

/**
 * Determines whether the video element has native HLS support
 */
export function elementSupportsNativeHls(videoElement: HTMLVideoElement) {
  return (
    videoElement.canPlayType('application/vnd.apple.mpegURL') ||
    videoElement.canPlayType('application/x-mpegURL')
  );
}

/**
 * Appends `source` elements to a video element
 */
export function appendSourceElements(
  videoElement: HTMLVideoElement,
  videoUrls: string[]
) {
  videoUrls.forEach((url) => {
    const source = document.createElement('source');
    videoElement.appendChild(source);
    source.src = url;
    if (url.includes('.m3u8')) {
      source.type = videoElement.canPlayType('application/vnd.apple.mpegURL')
        ? 'application/vnd.apple.mpegURL'
        : 'application/x-mpegURL';
    } else {
      source.type = 'video/mp4';
    }
  });
}

export type Props = Omit<
  React.VideoHTMLAttributes<HTMLVideoElement>,
  keyof VideoPlayerProps | 'src' | 'children'
> &
  VideoPlayerProps;

const SimpleVideoPlayer: React.FC<Props> = (props) => {
  const {
    url,
    playing,
    pip = true,
    volume,
    preload,
    ref: propRef,
    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 [video, setVideo] = useState<HTMLVideoElement | null>(null);
  const videoRef = useRef(video);
  useEffect(() => {
    videoRef.current = video;
  }, [video]);

  // Maintain state to help determine whether and when the video should be loaded
  const stateRef = useRef<{
    hls: Hls | null;
    needsToLoad: boolean;
    autoLoad: boolean;
  }>({
    hls: null,
    needsToLoad: true,
    autoLoad: preload !== 'none',
  });

  // Attach the media, which we may want to defer for bandwidth reasons
  const loadVideo = useCallback(() => {
    if (videoRef.current && stateRef.current.needsToLoad) {
      if (stateRef.current.hls) {
        stateRef.current.hls.attachMedia(videoRef.current);
      } else {
        videoRef.current.load();
      }
      stateRef.current.needsToLoad = false;
    }
  }, []);

  const [canPlay, setCanPlay] = useState(false);
  const handleCanPlay = useCallback(
    (e: Event) => {
      setCanPlay(true);
      onCanPlay?.(e);
    },
    [onCanPlay]
  );

  const handlePlaying = useCallback(
    (e: Event) => {
      // Ensure that the video is loaded
      loadVideo();
      // Subsequent video changes will automatically load
      stateRef.current.autoLoad = true;
      onPlaying?.(e);
    },
    [onPlaying, loadVideo]
  );

  const mediaPlayback = useMediaPlayback({
    mediaElement: video,
    onBuffering,
    onPlaybackError,
    onPlay,
    onPause,
    onEnded,
    onSeeking,
    onSeeked,
    onRateChange,
    onLoadStart,
    onLoadedMetadata,
    onLoadedData,
    onCanPlay: handleCanPlay,
    onCanPlayThrough,
    onProgress,
    onStalled,
    onSuspend,
    onAbort,
    onError,
    onEmptied,
    onTimeUpdate,
    onDurationChange,
    onVolumeChange,
    onWaiting,
    onPlaying: handlePlaying,
  });

  const { setVolume, play, pause } = mediaPlayback;

  useImperativeHandle(
    propRef,
    () => ({
      ...mediaPlayback,
      getInternalPlayer: () => videoRef.current,
      load(mediaUrl: string) {
        if (mediaUrl && stateRef.current.hls && videoRef.current) {
          stateRef.current.hls.loadSource(mediaUrl);
          stateRef.current.hls.attachMedia(videoRef.current);
        } else {
          mediaPlayback.load(mediaUrl);
        }
      },
    }),
    [mediaPlayback]
  );

  useEffect(() => {
    if (video) {
      video.disablePictureInPicture = !pip;
    }
  }, [video, pip]);

  useEffect(() => {
    if (typeof volume === 'number') {
      setVolume(volume);
    }
  }, [volume, setVolume]);

  useEffect(() => {
    async function playOrPause() {
      if (playing && canPlay) {
        play();
      } else if (playing === false) {
        pause();
      }
    }
    if (video) {
      playOrPause();
    }
  }, [video, playing, play, pause, canPlay]);

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

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

      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(video) &&
        Hls.isSupported()
      ) {
        // Use hls.js and load just the first HLS video
        hls = new Hls();
        hls.loadSource(hlsVideoUrls[0]);
      } else {
        const supportedUrls = elementSupportsNativeHls(video)
          ? urls
          : urls.filter((src) => !isHlsVideoUrl(src));
        if (supportedUrls.length === 1) {
          // Use `src` attribute for single supported video URL
          video.src = supportedUrls[0];
        } else if (supportedUrls.length > 1) {
          appendSourceElements(video, supportedUrls);
        } else {
          // If we don't have any explicitly supported video URLs, use the
          // original video URLs and cross your fingers
          appendSourceElements(video, urls);
        }
      }

      stateRef.current.hls = hls;

      // Load the video if we auto-loading is enabled
      if (stateRef.current.autoLoad) {
        loadVideo();
      } else {
        stateRef.current.needsToLoad = true;
      }

      video.currentTime = 0;

      return () => {
        hls?.destroy();
      };
    }
  }, [video, url, loadVideo]);

  // If `preload` changes to something that should load the video, do it
  useEffect(() => {
    if (preload !== 'none') {
      loadVideo();
    }
  }, [preload, loadVideo]);

  return (
    <video
      ref={setVideo}
      controls={false}
      playsInline={true}
      muted={true}
      preload={preload}
      {...restProps}
    />
  );
};

export default SimpleVideoPlayer;
