/* eslint-disable @typescript-eslint/no-explicit-any */
import React, {
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import VideoJS, { VideoJsPlayer, VideoJsPlayerOptions } from "video.js";
import "videojs-vtt-thumbnails-freetube";
import "videojs-seek-buttons";
import "videojs-landscape-fullscreen";
import "./live";
import "./PlaylistButtons";
import "./source-selector";
import "./persist-volume";
import "./markers";
import "./big-buttons";
import cx from "classnames";

import * as GQL from "src/core/generated-graphql";
import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
import { ConfigurationContext } from "src/hooks/Config";
import {
  ConnectionState,
  InteractiveContext,
} from "src/hooks/Interactive/context";
import { SceneInteractiveStatus } from "src/hooks/Interactive/status";
import { languageMap } from "src/utils/caption";
import { VIDEO_PLAYER_ID } from "./util";

function handleHotkeys(player: VideoJsPlayer, event: VideoJS.KeyboardEvent) {
  function seekPercent(percent: number) {
    const duration = player.duration();
    const time = duration * percent;
    player.currentTime(time);
  }

  function seekPercentRelative(percent: number) {
    const duration = player.duration();
    const currentTime = player.currentTime();
    const time = currentTime + duration * percent;
    if (time > duration) return;
    player.currentTime(time);
  }

  if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
    return;
  }

  switch (event.which) {
    case 32: // space
    case 13: // enter
      if (player.paused()) player.play();
      else player.pause();
      break;
    case 77: // m
      player.muted(!player.muted());
      break;
    case 70: // f
      if (player.isFullscreen()) player.exitFullscreen();
      else player.requestFullscreen();
      break;
    case 39: // right arrow
      player.currentTime(Math.min(player.duration(), player.currentTime() + 5));
      break;
    case 37: // left arrow
      player.currentTime(Math.max(0, player.currentTime() - 5));
      break;
    case 38: // up arrow
      player.volume(player.volume() + 0.1);
      break;
    case 40: // down arrow
      player.volume(player.volume() - 0.1);
      break;
    case 48: // 0
      player.currentTime(0);
      break;
    case 49: // 1
      seekPercent(0.1);
      break;
    case 50: // 2
      seekPercent(0.2);
      break;
    case 51: // 3
      seekPercent(0.3);
      break;
    case 52: // 4
      seekPercent(0.4);
      break;
    case 53: // 5
      seekPercent(0.5);
      break;
    case 54: // 6
      seekPercent(0.6);
      break;
    case 55: // 7
      seekPercent(0.7);
      break;
    case 56: // 8
      seekPercent(0.8);
      break;
    case 57: // 9
      seekPercent(0.9);
      break;
    case 221: // ]
      seekPercentRelative(0.1);
      break;
    case 219: // [
      seekPercentRelative(-0.1);
      break;
  }
}

interface IScenePlayerProps {
  className?: string;
  scene: GQL.SceneDataFragment | undefined | null;
  timestamp: number;
  autoplay?: boolean;
  onComplete?: () => void;
  onNext?: () => void;
  onPrevious?: () => void;
}

export const ScenePlayer: React.FC<IScenePlayerProps> = ({
  className,
  autoplay,
  scene,
  timestamp,
  onComplete,
  onNext,
  onPrevious,
}) => {
  const { configuration } = useContext(ConfigurationContext);
  const config = configuration?.interface;
  const videoRef = useRef<HTMLVideoElement>(null);
  const playerRef = useRef<VideoJsPlayer | undefined>();
  const sceneId = useRef<string | undefined>();
  const skipButtonsRef = useRef<any>();

  const [time, setTime] = useState(0);

  const {
    interactive: interactiveClient,
    uploadScript,
    currentScript,
    initialised: interactiveInitialised,
    state: interactiveState,
  } = React.useContext(InteractiveContext);

  const [initialTimestamp] = useState(timestamp);
  const [ready, setReady] = useState(false);
  const started = useRef(false);
  const interactiveReady = useRef(false);

  const file = useMemo(
    () => ((scene?.files.length ?? 0) > 0 ? scene?.files[0] : undefined),
    [scene]
  );

  const maxLoopDuration = config?.maximumLoopDuration ?? 0;

  useEffect(() => {
    if (playerRef.current && timestamp >= 0) {
      const player = playerRef.current;
      player.play()?.then(() => {
        player.currentTime(timestamp);
      });
    }
  }, [timestamp]);

  useEffect(() => {
    const videoElement = videoRef.current;
    if (!videoElement) return;

    const options: VideoJsPlayerOptions = {
      controls: true,
      controlBar: {
        pictureInPictureToggle: false,
        volumePanel: {
          inline: false,
        },
        chaptersButton: false,
      },
      nativeControlsForTouch: false,
      playbackRates: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
      inactivityTimeout: 2000,
      preload: "none",
      userActions: {
        hotkeys: function (event) {
          const player = this as VideoJsPlayer;
          handleHotkeys(player, event);
        },
      },
    };

    const player = VideoJS(videoElement, options);

    const settings = (player as any).textTrackSettings;
    settings.setValues({
      backgroundColor: "#000",
      backgroundOpacity: "0.5",
    });
    settings.updateDisplay();

    (player as any).markers();
    (player as any).offset();
    (player as any).sourceSelector();
    (player as any).persistVolume();
    (player as any).bigButtons();

    player.focus();
    playerRef.current = player;
  }, []);

  useEffect(() => {
    if (scene?.interactive && interactiveInitialised) {
      interactiveReady.current = false;
      uploadScript(scene.paths.funscript || "").then(() => {
        interactiveReady.current = true;
      });
    }
  }, [
    uploadScript,
    interactiveInitialised,
    scene?.interactive,
    scene?.paths.funscript,
  ]);

  useEffect(() => {
    if (skipButtonsRef.current) {
      skipButtonsRef.current.setForwardHandler(onNext);
      skipButtonsRef.current.setBackwardHandler(onPrevious);
    }
  }, [onNext, onPrevious]);

  useEffect(() => {
    const player = playerRef.current;
    if (player) {
      player.seekButtons({
        forward: 10,
        back: 10,
      });

      skipButtonsRef.current = player.skipButtons() ?? undefined;

      player.focus();
    }

    // Video player destructor
    return () => {
      if (playerRef.current) {
        playerRef.current.dispose();
        playerRef.current = undefined;
      }
    };
  }, []);

  const start = useCallback(() => {
    const player = playerRef.current;
    if (player && scene) {
      started.current = true;

      player
        .play()
        ?.then(() => {
          if (initialTimestamp > 0) {
            player.currentTime(initialTimestamp);
          }
        })
        .catch(() => {
          if (scene.paths.screenshot) player.poster(scene.paths.screenshot);
        });
    }
  }, [scene, initialTimestamp]);

  useEffect(() => {
    let prevCaptionOffset = 0;

    function addCaptionOffset(player: VideoJsPlayer, offset: number) {
      const tracks = player.remoteTextTracks();
      for (let i = 0; i < tracks.length; i++) {
        const track = tracks[i];
        const { cues } = track;
        if (cues) {
          for (let j = 0; j < cues.length; j++) {
            const cue = cues[j];
            cue.startTime = cue.startTime + offset;
            cue.endTime = cue.endTime + offset;
          }
        }
      }
    }

    function removeCaptionOffset(player: VideoJsPlayer, offset: number) {
      const tracks = player.remoteTextTracks();
      for (let i = 0; i < tracks.length; i++) {
        const track = tracks[i];
        const { cues } = track;
        if (cues) {
          for (let j = 0; j < cues.length; j++) {
            const cue = cues[j];
            cue.startTime = cue.startTime + prevCaptionOffset - offset;
            cue.endTime = cue.endTime + prevCaptionOffset - offset;
          }
        }
      }
    }

    function handleOffset(player: VideoJsPlayer) {
      if (!scene || !file) return;

      const currentSrc = new URL(player.currentSrc());

      const isDirect =
        currentSrc.pathname.endsWith("/stream") ||
        currentSrc.pathname.endsWith("/stream.m3u8");

      const curTime = player.currentTime();
      if (!isDirect) {
        (player as any).setOffsetDuration(file.duration);
      } else {
        (player as any).clearOffsetDuration();
      }

      if (curTime != prevCaptionOffset) {
        if (!isDirect) {
          removeCaptionOffset(player, curTime);
          prevCaptionOffset = curTime;
        } else {
          if (prevCaptionOffset != 0) {
            addCaptionOffset(player, prevCaptionOffset);
            prevCaptionOffset = 0;
          }
        }
      }
    }

    function handleError(play: boolean) {
      const player = playerRef.current;
      if (!player) return;

      const currentFile = player.currentSource();
      if (currentFile) {
        // eslint-disable-next-line no-console
        console.log(`Source failed: ${currentFile.src}`);
        player.focus();
      }

      if (tryNextStream()) {
        // eslint-disable-next-line no-console
        console.log(`Trying next source in playlist: ${player.currentSrc()}`);
        player.load();
        if (play) {
          player.play();
        }
      } else {
        // eslint-disable-next-line no-console
        console.log("No more sources in playlist.");
      }
    }

    function tryNextStream() {
      const player = playerRef.current;
      if (!player) return;

      const sources = player.currentSources();

      if (sources.length > 1) {
        sources.shift();
        player.src(sources);
        return true;
      }

      return false;
    }

    function getDefaultLanguageCode() {
      var languageCode = window.navigator.language;

      if (languageCode.indexOf("-") !== -1) {
        languageCode = languageCode.split("-")[0];
      }

      if (languageCode.indexOf("_") !== -1) {
        languageCode = languageCode.split("_")[0];
      }

      return languageCode;
    }

    function loadCaptions(player: VideoJsPlayer) {
      if (!scene) return;

      if (scene.captions) {
        var languageCode = getDefaultLanguageCode();
        var hasDefault = false;

        for (let caption of scene.captions) {
          var lang = caption.language_code;
          var label = lang;
          if (languageMap.has(lang)) {
            label = languageMap.get(lang)!;
          }

          label = label + " (" + caption.caption_type + ")";
          var setAsDefault = !hasDefault && languageCode == lang;
          if (!hasDefault && setAsDefault) {
            hasDefault = true;
          }
          player.addRemoteTextTrack(
            {
              src:
                scene.paths.caption +
                "?lang=" +
                lang +
                "&type=" +
                caption.caption_type,
              kind: "captions",
              srclang: lang,
              label: label,
              default: setAsDefault,
            },
            true
          );
        }
      }
    }

    function loadstart(this: VideoJsPlayer) {
      // handle offset after loading so that we get the correct current source
      handleOffset(this);
    }

    function onPlay(this: VideoJsPlayer) {
      this.poster("");
      if (scene?.interactive && interactiveReady.current) {
        interactiveClient.play(this.currentTime());
      }
    }

    function pause() {
      interactiveClient.pause();
    }

    function timeupdate(this: VideoJsPlayer) {
      if (scene?.interactive && interactiveReady.current) {
        interactiveClient.ensurePlaying(this.currentTime());
      }
      setTime(this.currentTime());
    }

    function seeking(this: VideoJsPlayer) {
      this.play();
    }

    function error() {
      handleError(true);
    }

    // changing source (eg when seeking) resets the playback rate
    // so set the default in addition to the current rate
    function ratechange(this: VideoJsPlayer) {
      this.defaultPlaybackRate(this.playbackRate());
    }

    function loadedmetadata(this: VideoJsPlayer) {
      if (!this.videoWidth() && !this.videoHeight()) {
        // Occurs during preload when videos with supported audio/unsupported video are preloaded.
        // Treat this as a decoding error and try the next source without playing.
        // However on Safari we get an media event when m3u8 is loaded which needs to be ignored.
        const currentFile = this.currentSrc();
        if (currentFile != null && !currentFile.includes("m3u8")) {
          // const play = !player.paused();
          // handleError(play);
          this.error(MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
        }
      }
    }

    const player = playerRef.current;
    if (!player) return;

    // always initialise event handlers since these are destroyed when the
    // component is destroyed
    player.on("loadstart", loadstart);
    player.on("play", onPlay);
    player.on("pause", pause);
    player.on("timeupdate", timeupdate);
    player.on("seeking", seeking);
    player.on("error", error);
    player.on("ratechange", ratechange);
    player.on("loadedmetadata", loadedmetadata);

    // don't re-initialise the player unless the scene has changed
    if (!scene || !file || scene.id === sceneId.current) return;
    sceneId.current = scene.id;

    // always stop the interactive client on initialisation
    interactiveClient.pause();
    interactiveReady.current = false;

    const auto =
      autoplay || (config?.autostartVideo ?? false) || initialTimestamp > 0;
    if (!auto && scene.paths?.screenshot) player.poster(scene.paths.screenshot);
    else player.poster("");

    const isLandscape = file.height && file.width && file.width > file.height;

    if (isLandscape) {
      (player as any).landscapeFullscreen({
        fullscreen: {
          enterOnRotate: true,
          exitOnRotate: true,
          alwaysInLandscapeMode: true,
          iOS: false,
        },
      });
    }

    // clear the offset before loading anything new.
    // otherwise, the offset will be applied to the next file when
    // currentTime is called.
    (player as any).clearOffsetDuration();

    const tracks = player.remoteTextTracks();
    for (let i = 0; i < tracks.length; i++) {
      player.removeRemoteTextTrack(tracks[i] as any);
    }

    player.src(
      scene.sceneStreams.map((stream) => ({
        src: stream.url,
        type: stream.mime_type ?? undefined,
        label: stream.label ?? undefined,
      }))
    );

    if (scene.paths.chapters_vtt) {
      player.addRemoteTextTrack(
        {
          src: scene.paths.chapters_vtt,
          kind: "chapters",
          default: true,
        },
        true
      );
    }

    if (scene.captions?.length! > 0) {
      loadCaptions(player);
    }

    player.currentTime(0);

    const looping =
      !!file.duration &&
      maxLoopDuration !== 0 &&
      file.duration < maxLoopDuration;
    player.loop(looping);
    interactiveClient.setLooping(looping);

    player.load();
    player.focus();

    if ((player as any).vttThumbnails?.src)
      (player as any).vttThumbnails?.src(scene?.paths.vtt);
    else
      (player as any).vttThumbnails({
        src: scene?.paths.vtt,
        showTimestamp: true,
      });

    setReady(true);
    started.current = false;

    return () => {
      setReady(false);

      // stop the interactive client
      interactiveClient.pause();

      player.off("loadstart", loadstart);
      player.off("play", onPlay);
      player.off("pause", pause);
      player.off("timeupdate", timeupdate);
      player.off("seeking", seeking);
      player.off("error", error);
      player.off("ratechange", ratechange);
      player.off("loadedmetadata", loadedmetadata);
    };
  }, [
    scene,
    file,
    config?.autostartVideo,
    maxLoopDuration,
    initialTimestamp,
    autoplay,
    interactiveClient,
    start,
  ]);

  useEffect(() => {
    if (!ready || started.current) {
      return;
    }

    const auto =
      autoplay || (config?.autostartVideo ?? false) || initialTimestamp > 0;

    // check if we're waiting for the interactive client
    const interactiveWaiting =
      scene?.interactive &&
      interactiveClient.handyKey &&
      currentScript !== scene.paths.funscript;

    if (scene && auto && !interactiveWaiting) {
      start();
    }
  }, [
    config?.autostartVideo,
    initialTimestamp,
    scene,
    ready,
    interactiveClient,
    currentScript,
    autoplay,
    start,
  ]);

  useEffect(() => {
    // Attach handler for onComplete event
    const player = playerRef.current;
    if (!player) return;

    player.on("ended", () => {
      onComplete?.();
    });

    return () => player.off("ended");
  }, [onComplete]);

  const onScrubberScrolled = () => {
    playerRef.current?.pause();
  };
  const onScrubberSeek = (seconds: number) => {
    const player = playerRef.current;
    if (player) {
      player.play()?.then(() => {
        player.currentTime(seconds);
      });
    }
  };

  const isPortrait =
    scene && file && file.height && file.width && file.height > file.width;

  return (
    <div className={cx("VideoPlayer", { portrait: isPortrait })}>
      <div data-vjs-player className={cx("video-wrapper", className)}>
        <video
          playsInline
          ref={videoRef}
          id={VIDEO_PLAYER_ID}
          className="video-js vjs-big-play-centered"
        />
      </div>
      {scene?.interactive &&
        (interactiveState !== ConnectionState.Ready ||
          playerRef.current?.paused()) && <SceneInteractiveStatus />}
      {scene && file && (
        <ScenePlayerScrubber
          file={file}
          scene={scene}
          position={time}
          onSeek={onScrubberSeek}
          onScrolled={onScrubberScrolled}
        />
      )}
    </div>
  );
};

export default ScenePlayer;
