import React, {
  KeyboardEvent,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from "video.js";
import useScript from "src/hooks/useScript";
import "videojs-contrib-dash";
import "videojs-mobile-ui";
import "videojs-seek-buttons";
import { UAParser } from "ua-parser-js";
import "./live";
import "./PlaylistButtons";
import "./source-selector";
import "./persist-volume";
import MarkersPlugin, { type IMarker } from "./markers";
void MarkersPlugin;
import "./vtt-thumbnails";
import "./big-buttons";
import "./track-activity";
import "./vrmode";
import cx from "classnames";
import {
  useSceneSaveActivity,
  useSceneIncrementPlayCount,
} from "src/core/StashService";

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";

// @ts-ignore
import airplay from "@silvermine/videojs-airplay";
// @ts-ignore
import chromecast from "@silvermine/videojs-chromecast";
import abLoopPlugin from "videojs-abloop";
import ScreenUtils from "src/utils/screen";

// register videojs plugins
airplay(videojs);
chromecast(videojs);
abLoopPlugin(window, videojs);

function handleHotkeys(player: VideoJsPlayer, event: videojs.KeyboardEvent) {
  function seekStep(step: number) {
    const time = player.currentTime() + step;
    const duration = player.duration();
    if (time < 0) {
      player.currentTime(0);
    } else if (time < duration) {
      player.currentTime(time);
    } else {
      player.currentTime(duration);
    }
  }

  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);
  }

  function toggleABLooping() {
    const opts = player.abLoopPlugin.getOptions();
    if (!opts.start) {
      opts.start = player.currentTime();
    } else if (!opts.end) {
      opts.end = player.currentTime();
      opts.enabled = true;
    } else {
      opts.start = 0;
      opts.end = 0;
      opts.enabled = false;
    }
    player.abLoopPlugin.setOptions(opts);
  }

  let seekFactor = 10;
  if (event.shiftKey) {
    seekFactor = 5;
  } else if (event.ctrlKey || event.altKey) {
    seekFactor = 60;
  }
  switch (event.which) {
    case 39: // right arrow
      seekStep(seekFactor);
      break;
    case 37: // left arrow
      seekStep(-seekFactor);
      break;
  }

  // toggle player looping with shift+l
  if (event.shiftKey && event.which === 76) {
    player.loop(!player.loop());
    return;
  }

  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 76: // l
      toggleABLooping();
      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;
  }
}

type MarkerFragment = Pick<GQL.SceneMarker, "title" | "seconds"> & {
  primary_tag: Pick<GQL.Tag, "name">;
  tags: Array<Pick<GQL.Tag, "name">>;
};

function getMarkerTitle(marker: MarkerFragment) {
  if (marker.title) {
    return marker.title;
  }

  let ret = marker.primary_tag.name;
  if (marker.tags.length) {
    ret += `, ${marker.tags.map((t) => t.name).join(", ")}`;
  }

  return ret;
}

interface IScenePlayerProps {
  scene: GQL.SceneDataFragment;
  hideScrubberOverride: boolean;
  autoplay?: boolean;
  permitLoop?: boolean;
  initialTimestamp: number;
  sendSetTimestamp: (setTimestamp: (value: number) => void) => void;
  onComplete: () => void;
  onNext: () => void;
  onPrevious: () => void;
}

export const ScenePlayer: React.FC<IScenePlayerProps> = ({
  scene,
  hideScrubberOverride,
  autoplay,
  permitLoop = true,
  initialTimestamp: _initialTimestamp,
  sendSetTimestamp,
  onComplete,
  onNext,
  onPrevious,
}) => {
  const { configuration } = useContext(ConfigurationContext);
  const interfaceConfig = configuration?.interface;
  const uiConfig = configuration?.ui;
  const videoRef = useRef<HTMLDivElement>(null);
  const [_player, setPlayer] = useState<VideoJsPlayer>();
  const sceneId = useRef<string>();
  const [sceneSaveActivity] = useSceneSaveActivity();
  const [sceneIncrementPlayCount] = useSceneIncrementPlayCount();

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

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

  const [fullscreen, setFullscreen] = useState(false);
  const [showScrubber, setShowScrubber] = useState(false);

  const started = useRef(false);
  const auto = useRef(false);
  const interactiveReady = useRef(false);
  const minimumPlayPercent = uiConfig?.minimumPlayPercent ?? 0;
  const trackActivity = uiConfig?.trackActivity ?? true;
  const vrTag = uiConfig?.vrTag ?? undefined;

  useScript(
    "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1",
    uiConfig?.enableChromecast
  );

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

  const maxLoopDuration = interfaceConfig?.maximumLoopDuration ?? 0;
  const looping = useMemo(
    () =>
      !!file?.duration &&
      permitLoop &&
      maxLoopDuration !== 0 &&
      file.duration < maxLoopDuration,
    [file, permitLoop, maxLoopDuration]
  );

  const getPlayer = useCallback(() => {
    if (!_player) return null;
    if (_player.isDisposed()) return null;
    return _player;
  }, [_player]);

  useEffect(() => {
    if (hideScrubberOverride || fullscreen) {
      setShowScrubber(false);
      return;
    }

    const onResize = () => {
      const show = window.innerHeight >= 450 && !ScreenUtils.isMobile();
      setShowScrubber(show);
    };
    onResize();

    window.addEventListener("resize", onResize);

    return () => window.removeEventListener("resize", onResize);
  }, [hideScrubberOverride, fullscreen]);

  useEffect(() => {
    sendSetTimestamp((value: number) => {
      const player = getPlayer();
      if (player && value >= 0) {
        if (player.hasStarted() && player.paused()) {
          player.currentTime(value);
        } else {
          player.play()?.then(() => {
            player.currentTime(value);
          });
        }
      }
    });
  }, [sendSetTimestamp, getPlayer]);

  // Initialize VideoJS player
  useEffect(() => {
    const options: VideoJsPlayerOptions = {
      id: VIDEO_PLAYER_ID,
      controls: true,
      controlBar: {
        pictureInPictureToggle: false,
        volumePanel: {
          inline: false,
        },
        chaptersButton: false,
      },
      html5: {
        dash: {
          updateSettings: [
            {
              streaming: {
                buffer: {
                  bufferTimeAtTopQuality: 30,
                  bufferTimeAtTopQualityLongForm: 30,
                },
                gaps: {
                  jumpGaps: false,
                  jumpLargeGaps: false,
                },
              },
            },
          ],
        },
      },
      nativeControlsForTouch: false,
      playbackRates: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
      inactivityTimeout: 2000,
      preload: "none",
      playsinline: true,
      techOrder: ["chromecast", "html5"],
      userActions: {
        hotkeys: function (this: VideoJsPlayer, event) {
          handleHotkeys(this, event);
        },
      },
      plugins: {
        airPlay: {},
        chromecast: {},
        vttThumbnails: {
          showTimestamp: true,
        },
        markers: {},
        sourceSelector: {},
        persistVolume: {},
        bigButtons: {},
        seekButtons: {
          forward: 10,
          back: 10,
        },
        skipButtons: {},
        trackActivity: {},
        vrMenu: {},
        abLoopPlugin: {
          start: 0,
          end: false,
          enabled: false,
          loopIfBeforeStart: true,
          loopIfAfterEnd: true,
          pauseAfterLooping: false,
          pauseBeforeLooping: false,
          createButtons: uiConfig?.showAbLoopControls ?? false,
        },
      },
    };

    const videoEl = document.createElement("video-js");
    videoEl.setAttribute("data-vjs-player", "true");
    videoEl.setAttribute("crossorigin", "anonymous");
    videoEl.classList.add("vjs-big-play-centered");
    videoRef.current!.appendChild(videoEl);

    const vjs = videojs(videoEl, options);

    /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
    const settings = (vjs as any).textTrackSettings;
    settings.setValues({
      backgroundColor: "#000",
      backgroundOpacity: "0.5",
    });
    settings.updateDisplay();

    vjs.focus();
    setPlayer(vjs);

    // Video player destructor
    return () => {
      vjs.dispose();
      videoEl.remove();
      setPlayer(undefined);

      // reset sceneId to force reload sources
      sceneId.current = undefined;
    };
    // empty deps - only init once
    // showAbLoopControls is necessary to re-init the player when the config changes
  }, [uiConfig?.showAbLoopControls]);

  useEffect(() => {
    const player = getPlayer();
    if (!player) return;
    const skipButtons = player.skipButtons();
    skipButtons.setForwardHandler(onNext);
    skipButtons.setBackwardHandler(onPrevious);
  }, [getPlayer, onNext, onPrevious]);

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

  useEffect(() => {
    const player = getPlayer();
    if (!player) return;

    const vrMenu = player.vrMenu();

    let showButton = false;

    if (vrTag) {
      showButton = scene.tags.some((tag) => vrTag === tag.name);
    }

    vrMenu.setShowButton(showButton);
  }, [getPlayer, scene, vrTag]);

  // Player event handlers
  useEffect(() => {
    const player = getPlayer();
    if (!player) return;

    function canplay(this: VideoJsPlayer) {
      // if we're seeking before starting, don't set the initial timestamp
      // when starting from the beginning, there is a small delay before the event
      // is triggered, so we can't just check if the time is 0
      if (this.currentTime() >= 0.1) {
        return;
      }
    }

    function playing(this: VideoJsPlayer) {
      // This still runs even if autoplay failed on Safari,
      // only set flag if actually playing
      if (!started.current && !this.paused()) {
        started.current = true;
      }
    }

    function loadstart(this: VideoJsPlayer) {
      setReady(true);
    }

    function fullscreenchange(this: VideoJsPlayer) {
      setFullscreen(this.isFullscreen());
    }

    player.on("canplay", canplay);
    player.on("playing", playing);
    player.on("loadstart", loadstart);
    player.on("fullscreenchange", fullscreenchange);

    return () => {
      player.off("canplay", canplay);
      player.off("playing", playing);
      player.off("loadstart", loadstart);
      player.off("fullscreenchange", fullscreenchange);
    };
  }, [getPlayer]);

  useEffect(() => {
    const player = getPlayer();
    if (!player) return;

    function onplay(this: VideoJsPlayer) {
      if (scene.interactive && interactiveReady.current) {
        interactiveClient.play(this.currentTime());
      }
    }

    function pause(this: VideoJsPlayer) {
      interactiveClient.pause();
    }

    function seeking(this: VideoJsPlayer) {
      if (this.paused()) return;
      if (scene.interactive && interactiveReady.current) {
        interactiveClient.play(this.currentTime());
      }
    }

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

    player.on("play", onplay);
    player.on("pause", pause);
    player.on("seeking", seeking);
    player.on("timeupdate", timeupdate);

    return () => {
      player.off("play", onplay);
      player.off("pause", pause);
      player.off("seeking", seeking);
      player.off("timeupdate", timeupdate);
    };
  }, [getPlayer, interactiveClient, scene]);

  useEffect(() => {
    const player = getPlayer();
    if (!player) return;

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

    sceneId.current = scene.id;

    setReady(false);

    // reset on new scene
    player.trackActivity().reset();

    // always stop the interactive client on initialisation
    interactiveClient.pause();

    const isSafari = UAParser().browser.name?.includes("Safari");
    const isLandscape = file.height && file.width && file.width > file.height;
    const mobileUiOptions = {
      fullscreen: {
        enterOnRotate: true,
        exitOnRotate: true,
        lockOnRotate: true,
        lockToLandscapeOnEnter: uiConfig?.disableMobileMediaAutoRotateEnabled
          ? false
          : isLandscape,
      },
      touchControls: {
        disabled: true,
      },
    };
    if (!isSafari) {
      player.mobileUi(mobileUiOptions);
    }

    function isDirect(src: URL) {
      return (
        src.pathname.endsWith("/stream") ||
        src.pathname.endsWith("/stream.mpd") ||
        src.pathname.endsWith("/stream.m3u8")
      );
    }

    const { duration } = file;
    const sourceSelector = player.sourceSelector();
    sourceSelector.setSources(
      scene.sceneStreams
        .filter((stream) => {
          const src = new URL(stream.url);
          const isFileTranscode = !isDirect(src);

          return !(isFileTranscode && isSafari);
        })
        .map((stream) => {
          const src = new URL(stream.url);

          return {
            src: stream.url,
            type: stream.mime_type ?? undefined,
            label: stream.label ?? undefined,
            offset: !isDirect(src),
            duration,
          };
        })
    );

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

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

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

      return languageCode;
    }

    if (scene.captions && scene.captions.length > 0) {
      const languageCode = getDefaultLanguageCode();
      let hasDefault = false;

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

        label = label + " (" + caption.caption_type + ")";
        const setAsDefault = !hasDefault && languageCode == lang;
        if (setAsDefault) {
          hasDefault = true;
        }
        sourceSelector.addTextTrack(
          {
            src: `${scene.paths.caption}?lang=${lang}&type=${caption.caption_type}`,
            kind: "captions",
            srclang: lang,
            label: label,
            default: setAsDefault,
          },
          false
        );
      }
    }

    auto.current =
      autoplay ||
      (interfaceConfig?.autostartVideo ?? false) ||
      _initialTimestamp > 0;

    const alwaysStartFromBeginning =
      uiConfig?.alwaysStartFromBeginning ?? false;
    const resumeTime = scene.resume_time ?? 0;

    let startPosition = _initialTimestamp;
    if (
      !startPosition &&
      !alwaysStartFromBeginning &&
      file.duration > resumeTime
    ) {
      startPosition = resumeTime;
    }

    setTime(startPosition);

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

    player.ready(() => {
      player.vttThumbnails().src(scene.paths.vtt ?? null);

      if (startPosition) {
        player.currentTime(startPosition);
      }
    });

    started.current = false;

    return () => {
      // stop the interactive client
      interactiveClient.pause();
    };
  }, [
    getPlayer,
    file,
    scene,
    interactiveClient,
    autoplay,
    interfaceConfig?.autostartVideo,
    uiConfig?.alwaysStartFromBeginning,
    uiConfig?.disableMobileMediaAutoRotateEnabled,
    _initialTimestamp,
  ]);

  useEffect(() => {
    const player = getPlayer();
    if (!player) return;

    if (scene.paths.screenshot) {
      player.poster(scene.paths.screenshot);
    } else {
      player.poster("");
    }

    function loadMarkers() {
      const loadMarkersAsync = async () => {
        const markerData = scene.scene_markers.map((marker) => ({
          title: getMarkerTitle(marker),
          seconds: marker.seconds,
          end_seconds: marker.end_seconds ?? null,
          primaryTag: marker.primary_tag,
        }));

        const markers = player!.markers();
        markers.clearMarkers();

        const uniqueTagNames = markerData
          .map((marker) => marker.primaryTag.name)
          .filter((value, index, self) => self.indexOf(value) === index);

        // Wait for colors
        await markers.findColors(uniqueTagNames);

        const showRangeTags =
          !ScreenUtils.isMobile() && (uiConfig?.showRangeMarkers ?? true);
        const timestampMarkers: IMarker[] = [];
        const rangeMarkers: IMarker[] = [];

        if (!showRangeTags) {
          for (const marker of markerData) {
            timestampMarkers.push(marker);
          }
        } else {
          for (const marker of markerData) {
            if (marker.end_seconds === null) {
              timestampMarkers.push(marker);
            } else {
              rangeMarkers.push(marker);
            }
          }
        }

        // Add markers in chunks
        const CHUNK_SIZE = 10;
        for (let i = 0; i < timestampMarkers.length; i += CHUNK_SIZE) {
          const chunk = timestampMarkers.slice(i, i + CHUNK_SIZE);
          requestAnimationFrame(() => {
            chunk.forEach((m) => markers.addDotMarker(m));
          });
        }

        requestAnimationFrame(() => {
          markers.addRangeMarkers(rangeMarkers);
        });
      };

      // Call our async function
      void loadMarkersAsync();
    }
    // Ensure markers are added after player is fully ready and sources are loaded
    if (player.readyState() >= 1) {
      loadMarkers();
      return;
    } else {
      player.on("loadedmetadata", () => {
        loadMarkers();
      });
      return () => {
        player.off("loadedmetadata", loadMarkers);
      };
    }
  }, [getPlayer, scene, uiConfig]);

  useEffect(() => {
    const player = getPlayer();
    if (!player) return;

    async function saveActivity(resumeTime: number, playDuration: number) {
      if (!scene.id) return;

      await sceneSaveActivity({
        variables: {
          id: scene.id,
          playDuration,
          resume_time: resumeTime,
        },
      });
    }

    async function incrementPlayCount() {
      if (!scene.id) return;

      await sceneIncrementPlayCount({
        variables: {
          id: scene.id,
        },
      });
    }

    const activity = player.trackActivity();
    activity.saveActivity = saveActivity;
    activity.incrementPlayCount = incrementPlayCount;
    activity.minimumPlayPercent = minimumPlayPercent;
    activity.setEnabled(trackActivity);
  }, [
    getPlayer,
    scene,
    vrTag,
    trackActivity,
    minimumPlayPercent,
    sceneIncrementPlayCount,
    sceneSaveActivity,
  ]);

  useEffect(() => {
    const player = getPlayer();
    if (!player) return;

    player.loop(looping);
    interactiveClient.setLooping(looping);
  }, [getPlayer, interactiveClient, looping]);

  useEffect(() => {
    const player = getPlayer();
    if (!player || !ready || !auto.current) {
      return;
    }

    // check if we're waiting for the interactive client
    if (
      scene.interactive &&
      interactiveClient.handyKey &&
      currentScript !== scene.paths.funscript
    ) {
      return;
    }

    player.play();
    auto.current = false;
  }, [getPlayer, scene, ready, interactiveClient, currentScript]);

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

    player.on("ended", onComplete);

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

  function onScrubberScroll() {
    if (started.current) {
      getPlayer()?.pause();
    }
  }

  function onScrubberSeek(seconds: number) {
    if (started.current) {
      getPlayer()?.currentTime(seconds);
    } else {
      setTime(seconds);
    }
  }

  // Override spacebar to always pause/play
  function onKeyDown(this: HTMLDivElement, event: KeyboardEvent) {
    const player = getPlayer();
    if (!player) return;

    if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
      return;
    }
    if (event.key == " ") {
      event.preventDefault();
      event.stopPropagation();
      if (player.paused()) {
        player.play();
      } else {
        player.pause();
      }
    }
  }

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

  return (
    <div
      className={cx("VideoPlayer", { portrait: isPortrait, "no-file": !file })}
      onKeyDownCapture={onKeyDown}
    >
      <div className="video-wrapper" ref={videoRef} />
      {scene.interactive &&
        (interactiveState !== ConnectionState.Ready ||
          getPlayer()?.paused()) && <SceneInteractiveStatus />}
      {file && showScrubber && (
        <ScenePlayerScrubber
          file={file}
          scene={scene}
          time={time}
          onSeek={onScrubberSeek}
          onScroll={onScrubberScroll}
        />
      )}
    </div>
  );
};

export default ScenePlayer;
