<template>
  <div v-if="loading" class="flex justify-center py-10">
    <UIcon name="i-lucide-loader-circle" class="size-16 animate-spin text-toned" />
  </div>

  <div v-else class="space-y-4">
    <div
      ref="playerContainer"
      class="relative flex w-full overflow-hidden rounded-sm bg-black"
      :class="
        isFullscreen
          ? 'h-screen w-screen max-h-screen max-w-none items-center justify-center rounded-none'
          : 'min-h-72 max-h-[70vh] max-w-full items-center justify-center sm:min-h-88 sm:max-h-[72vh]'
      "
    >
      <button
        v-if="!active"
        type="button"
        class="group absolute inset-0 z-40 block overflow-hidden bg-black text-left"
        @click="activatePlayer"
      >
        <img
          v-if="poster"
          :src="uri(poster)"
          :alt="`${title || 'Untitled media'} preview`"
          class="block h-full w-full bg-black object-contain opacity-90 transition duration-200 group-hover:opacity-100"
          @error="handlePosterError"
        />
        <div
          v-else
          class="flex h-full w-full items-center justify-center bg-black/90 text-white/80"
        >
          <UIcon :name="isAudio ? 'i-lucide-disc-3' : 'i-lucide-film'" class="size-12" />
        </div>
        <div
          class="pointer-events-none absolute inset-0 bg-linear-to-t from-black/70 via-transparent to-black/20"
        />
        <div
          class="pointer-events-none absolute inset-x-0 bottom-0 flex items-center justify-between gap-4 px-4 py-4 sm:px-6"
        >
          <div class="min-w-0">
            <div class="text-xs uppercase tracking-[0.2em] text-white/70">Click to play</div>
            <div class="mt-1 truncate text-lg font-semibold text-white">
              {{ title || 'Untitled media' }}
            </div>
          </div>
          <div
            class="flex size-16 shrink-0 items-center justify-center rounded-full bg-white/12 text-white backdrop-blur ring-1 ring-white/25"
          >
            <UIcon name="i-lucide-play" class="ml-1 size-8" />
          </div>
        </div>
      </button>

      <video
        ref="videoElement"
        class="share-video-element block bg-black object-contain"
        :class="
          isFullscreen
            ? 'h-full w-full max-h-screen max-w-screen'
            : 'h-full min-h-72 w-full max-w-full max-h-[70vh] sm:min-h-88 sm:max-h-[72vh]'
        "
        playsinline
        webkit-playsinline
        preload="metadata"
        crossorigin="anonymous"
        :poster="poster ? uri(poster) : undefined"
        @error="handleMediaError"
        @loadeddata="handleVideoLoadedData"
        @loadedmetadata="handleVideoLoadedMetadata"
        @timeupdate="handleVideoTimeUpdate"
        @play="handleVideoPlay"
        @pause="handleVideoPause"
        @click="handleVideoClick"
        @dblclick="handleVideoDoubleClick"
        @pointermove="handlePointerMove"
        @resize="scheduleAssLayoutRefresh"
        @volumechange="handleMediaVolumeChange"
        @webkitbeginfullscreen="handleVideoWebkitBeginFullscreen"
        @webkitendfullscreen="handleVideoWebkitEndFullscreen"
      >
        <source
          v-for="source in sources"
          :key="source.src"
          :src="source.src"
          :type="source.type"
          @error="source.onerror"
        />
        <track
          v-if="nativeSubtitleTrack && subtitleEnabled"
          :key="nativeSubtitleTrack.url"
          kind="subtitles"
          :srclang="nativeSubtitleTrack.lang || 'und'"
          :label="nativeSubtitleTrack.name || 'Subtitles'"
          default
          :src="uri(nativeSubtitleTrack.url)"
        />
        Your browser does not support the video tag.
      </video>

      <button
        v-if="active && isTouchDevice"
        type="button"
        class="absolute inset-0 z-10"
        :aria-label="controlsVisible ? 'Hide controls' : 'Show controls'"
        @click="toggleControls"
      />

      <div
        v-if="usesAssSubtitleTrack"
        ref="assOverlayElement"
        class="pointer-events-none absolute inset-0 z-20 overflow-hidden"
        aria-hidden="true"
      />

      <div
        v-if="active"
        class="absolute inset-x-0 bottom-0 z-30 bg-linear-to-t from-black/36 via-black/8 to-transparent px-3 pb-3 pt-10 text-white transition-opacity duration-150"
        :class="controlsVisible ? 'opacity-100' : 'pointer-events-none opacity-0'"
        @pointermove="showControls"
      >
        <div class="rounded-sm border border-white/8 bg-black/8 p-2.5 shadow-lg backdrop-blur-sm">
          <div
            class="grid grid-cols-[minmax(0,1fr)_auto] gap-x-3 gap-y-2.5 sm:grid-cols-[auto_minmax(0,1fr)_auto] sm:items-center sm:gap-3"
          >
            <div class="order-2 min-w-0 sm:order-1 sm:flex sm:min-w-0 sm:items-center sm:gap-3">
              <div class="flex min-w-0 items-center gap-2">
                <UButton
                  color="neutral"
                  variant="soft"
                  size="sm"
                  class="opacity-65 transition-opacity hover:opacity-100 focus-visible:opacity-100"
                  :icon="paused ? 'i-lucide-play' : 'i-lucide-pause'"
                  :aria-label="paused ? 'Play video' : 'Pause video'"
                  @click="togglePlayback"
                />
                <div class="min-w-0 truncate whitespace-nowrap text-xs font-medium text-white/60">
                  {{ timeLabel }}
                </div>
              </div>
            </div>
            <div class="order-1 col-span-2 sm:order-2 sm:col-span-1 sm:min-w-0 sm:flex-1">
              <input
                :value="progress"
                type="range"
                min="0"
                max="1000"
                step="1"
                class="h-1.5 w-full accent-white opacity-55 transition-opacity hover:opacity-100"
                aria-label="Seek video"
                @input="handleSeekInput"
              />
            </div>
            <div class="order-3 flex items-center justify-end sm:order-3 sm:shrink-0">
              <div class="flex shrink-0 items-center gap-1.5 sm:gap-2">
                <UTooltip
                  v-if="!usingHls && hasVideo"
                  side="top"
                  text="Trouble playing? Switch to HLS stream."
                >
                  <UButton
                    color="neutral"
                    variant="soft"
                    size="sm"
                    class="opacity-65 transition-opacity hover:opacity-100 focus-visible:opacity-100"
                    icon="i-lucide-refresh-cw"
                    aria-label="Switch to HLS stream"
                    @click="forceSwitchToHls"
                  />
                </UTooltip>
                <UButton
                  v-if="hasSubtitles"
                  color="neutral"
                  variant="soft"
                  size="sm"
                  class="opacity-65 transition-opacity hover:opacity-100 focus-visible:opacity-100"
                  :icon="subtitleEnabled ? 'i-lucide-captions' : 'i-lucide-captions-off'"
                  :aria-label="subtitleEnabled ? 'Disable subtitles' : 'Enable subtitles'"
                  @click="subtitleEnabled = !subtitleEnabled"
                />
                <UButton
                  color="neutral"
                  variant="soft"
                  size="sm"
                  class="opacity-65 transition-opacity hover:opacity-100 focus-visible:opacity-100"
                  :icon="muted || mediaVol <= 0 ? 'i-lucide-volume-x' : 'i-lucide-volume-2'"
                  :aria-label="muted || mediaVol <= 0 ? 'Unmute video' : 'Mute video'"
                  @click="toggleMute"
                />
                <input
                  v-if="!isTouchDevice"
                  :value="Math.round(mediaVol * 100)"
                  type="range"
                  min="0"
                  max="100"
                  step="1"
                  class="w-16 accent-white opacity-55 transition-opacity hover:opacity-100 sm:w-18"
                  aria-label="Video volume"
                  @input="handleVolumeInput"
                />
                <UButton
                  color="neutral"
                  variant="soft"
                  size="sm"
                  class="opacity-65 transition-opacity hover:opacity-100 focus-visible:opacity-100"
                  :icon="isFullscreen ? 'i-lucide-minimize' : 'i-lucide-maximize'"
                  :aria-label="isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'"
                  @click="toggleFullscreen"
                />
                <UTooltip side="top" text="Show keyboard shortcuts with ? or /">
                  <UButton
                    color="neutral"
                    variant="soft"
                    size="sm"
                    class="opacity-65 transition-opacity hover:opacity-100 focus-visible:opacity-100"
                    icon="i-lucide-circle-help"
                    aria-label="Show keyboard shortcuts"
                    @click="showHelp = !showHelp"
                  />
                </UTooltip>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <div
      v-if="subtitleLoading || subtitleLoadError"
      class="flex flex-wrap items-center gap-3 text-sm"
    >
      <span v-if="subtitleLoading" class="text-toned">Looking for matching subtitles...</span>
      <span v-else-if="subtitleLoadError" class="text-warning">{{ subtitleLoadError }}</span>
    </div>

    <UModal
      v-model:open="showHelp"
      title="Keyboard Shortcuts"
      :portal="helpPortal"
      :ui="{ content: 'sm:max-w-3xl' }"
    >
      <template #body>
        <div class="grid gap-5 text-sm sm:grid-cols-2">
          <div class="space-y-3">
            <div class="font-semibold text-highlighted">Playback</div>
            <div class="flex items-center justify-between gap-4">
              <span>Play or pause</span>
              <span class="text-muted">Space, K</span>
            </div>
            <div class="flex items-center justify-between gap-4">
              <span>Back 10 seconds</span>
              <span class="text-muted">J</span>
            </div>
            <div class="flex items-center justify-between gap-4">
              <span>Forward 10 seconds</span>
              <span class="text-muted">L</span>
            </div>
            <div class="flex items-center justify-between gap-4">
              <span>Mute</span>
              <span class="text-muted">M</span>
            </div>
          </div>
          <div class="space-y-3">
            <div class="font-semibold text-highlighted">Navigation</div>
            <div class="flex items-center justify-between gap-4">
              <span>Back 5 seconds</span>
              <span class="text-muted">Left</span>
            </div>
            <div class="flex items-center justify-between gap-4">
              <span>Forward 5 seconds</span>
              <span class="text-muted">Right</span>
            </div>
            <div class="flex items-center justify-between gap-4">
              <span>Go to start or end</span>
              <span class="text-muted">Home, End</span>
            </div>
            <div class="flex items-center justify-between gap-4">
              <span>Jump through the timeline</span>
              <span class="text-muted">0-9</span>
            </div>
          </div>
          <div class="space-y-3">
            <div class="font-semibold text-highlighted">Volume & Speed</div>
            <div class="flex items-center justify-between gap-4">
              <span>Volume up or down</span>
              <span class="text-muted">Up, Down</span>
            </div>
            <div class="flex items-center justify-between gap-4">
              <span>Faster</span>
              <span class="text-muted">'</span>
            </div>
            <div class="flex items-center justify-between gap-4">
              <span>Slower</span>
              <span class="text-muted">;</span>
            </div>
            <div class="flex items-center justify-between gap-4">
              <span>Step frame by frame</span>
              <span class="text-muted">, .</span>
            </div>
          </div>
          <div class="space-y-3">
            <div class="font-semibold text-highlighted">Display</div>
            <div class="flex items-center justify-between gap-4">
              <span>Fullscreen</span>
              <span class="text-muted">F</span>
            </div>
            <div class="flex items-center justify-between gap-4">
              <span>Show or hide subtitles</span>
              <span class="text-muted">C</span>
            </div>
            <div class="flex items-center justify-between gap-4">
              <span>Open this help</span>
              <span class="text-muted">?, /</span>
            </div>
            <div class="flex items-center justify-between gap-4">
              <span>Close help or player</span>
              <span class="text-muted">Esc</span>
            </div>
          </div>
        </div>
      </template>
    </UModal>
  </div>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useStorage } from '@vueuse/core';
import Hls from 'hls.js';
import {
  disableOpacity,
  enableOpacity,
  formatPageTitle,
  getRemoteImage,
  makeDownload,
  request,
  uri,
} from '~/utils';
import { usePlayerShortcutHelp } from '~/composables/usePlayerShortcutHelp';
import { usePlayerShortcuts } from '~/composables/usePlayerShortcuts';
import { usePlayerSubtitles } from '~/composables/usePlayerSubtitles';
import {
  canRequestFullscreen,
  exitDocumentFullscreen,
  getFullscreenElement,
  requestElementFullscreen,
} from '~/utils/fullscreen';
import { clampMediaVolume } from '~/utils/keyboard';
import { clear, clampResumeTime, nearEnd, read, save } from '~/utils/media';
import { nextTapVisible } from '~/utils/playerControls';

import type { StoreItem } from '~/types/store';
import type { FileInfo, PlayerSourceElement } from '~/types/video';

const config = useYtpConfig();

const props = defineProps<{ item: StoreItem }>();
const emitter = defineEmits<{
  (e: 'closeModel'): void;
  (e: 'error', message: string): void;
  (e: 'playback-state-change', isPlaying: boolean): void;
}>();

const showShortcutHelp = usePlayerShortcutHelp();

const playerContainer = ref<HTMLElement | null>(null);
const videoElement = ref<HTMLVideoElement | null>(null);
const assOverlayElement = ref<HTMLElement | null>(null);
const playerInfo = ref<FileInfo | null>(null);
const sources = ref<Array<PlayerSourceElement>>([]);
const loading = ref(true);
const loadingError = ref('');
const active = ref(false);
const isFullscreen = ref(false);
const assLayoutVersion = ref(0);
const controlsVisible = ref(true);
const currentTime = ref(0);
const duration = ref(0);
const paused = ref(true);
const isTouchDevice = ref(false);
const title = ref('');
const artist = ref('');
const poster = ref('/images/placeholder.png');
const hasPoster = ref(false);
const isAudio = ref(false);
const hasVideo = ref(false);
const usingHls = ref(false);
const destroyed = ref(false);
const mediaVol = useStorage<number>('player_volume', 1);
const muted = useStorage<boolean>('player_muted', false);
const showHelp = computed({
  get: () => showShortcutHelp.value,
  set: (value: boolean) => {
    showShortcutHelp.value = value;
  },
});
const helpPortal = computed<boolean | HTMLElement>(() => {
  if (isFullscreen.value) {
    return playerContainer.value || false;
  }

  return true;
});

let assLayoutRefreshFrame = 0;
let controlsHideTimeout = 0;
let pendingVideoClickTimeout = 0;
let unbindMediaSession: null | (() => void) = null;
let hls: Hls | null = null;
let pendingSeek: number | null = null;
let pendingPlay = false;
let lastSaveAt = 0;

const isApple = /(iPhone|iPod|iPad).*AppleWebKit/i.test(navigator.userAgent);
const mediaFile = computed(() => props.item.filename || '');
const id = computed(() => props.item._id || '');
const subtitleManifestUrl = computed(() => currentPlaybackUrl('api/player/subtitles/manifest'));
const canPlay = computed(() => Boolean(mediaFile.value && !loadingError.value));
const shouldRender = computed(() => active.value && !loading.value);
const progress = computed(() => {
  if (!duration.value) return 0;
  return Math.round((currentTime.value / duration.value) * 1000);
});
const timeLabel = computed(() => {
  const currentLabel = formatDuration(Math.round(currentTime.value));
  const durationLabel = duration.value ? formatDuration(Math.round(duration.value)) : '--:--';
  return `${currentLabel} / ${durationLabel}`;
});
const {
  subtitleLoading,
  subtitleLoadError,
  subtitleEnabled,
  nativeSubtitleTrack,
  usesAssSubtitleTrack,
  hasSubtitles,
} = usePlayerSubtitles({
  manifestUrl: subtitleManifestUrl,
  isVideo: computed(() => !isAudio.value),
  canPlay,
  shouldRender,
  assLayoutVersion,
  video: videoElement,
  overlay: assOverlayElement,
});

useHead(() => (title.value ? { title: formatPageTitle(`Playing: ${title.value}`) } : {}));

watch(
  [mediaVol, muted],
  ([nextVol]) => {
    const normalizedVolume = clampMediaVolume(nextVol);
    if (normalizedVolume !== nextVol) {
      mediaVol.value = normalizedVolume;
      return;
    }

    applyMediaState(videoElement.value);
    syncVideoState();
  },
  { immediate: true },
);

watch(
  videoElement,
  (element, previousElement) => {
    if (previousElement && previousElement !== element) {
      previousElement.muted = true;
    }

    applyMediaState(element);
    syncVideoState();
  },
  { immediate: true },
);

watch(hasSubtitles, (enabled) => {
  if (!enabled) {
    subtitleEnabled.value = false;
  }
});

function formatDuration(totalSeconds: number): string {
  const hours = Math.floor(totalSeconds / 3600);
  const minutes = Math.floor((totalSeconds % 3600) / 60);
  const seconds = totalSeconds % 60;

  if (hours > 0) {
    return [hours, minutes, seconds].map((value) => String(value).padStart(2, '0')).join(':');
  }

  return [minutes, seconds].map((value) => String(value).padStart(2, '0')).join(':');
}

function currentPlaybackUrl(base: string, playlist: boolean = false): string {
  if (!props.item.filename) {
    return '';
  }

  if (base === 'm3u8') {
    return makeDownload(config, props.item, base, true);
  }

  return makeDownload(config, props.item, base, playlist);
}

function activatePlayer() {
  active.value = true;
  void nextTick(async () => {
    applyMediaState(videoElement.value);
    try {
      await videoElement.value?.play();
    } catch {}
    syncVideoState();
    showControls();
  });
}

function handleVideoLoadedData() {
  syncVideoState();
}

function handleVideoLoadedMetadata() {
  loadingError.value = '';
  syncVideoState();
  showControls();
  scheduleAssLayoutRefresh();
  if (videoElement.value) {
    updateMediaSessionPosition(videoElement.value);
  }

  if (pendingSeek !== null) {
    const time = pendingSeek;
    const play = pendingPlay;
    pendingSeek = null;
    pendingPlay = false;
    void restoreSwitch(time, play).finally(() => {
      syncVideoState();
      showControls();
    });
  } else {
    restoreStoredProgress();
  }
}

function handleVideoTimeUpdate() {
  syncVideoState();
  if (videoElement.value) {
    updateMediaSessionPosition(videoElement.value);
  }

  persistProgress(false);
}

function handleVideoPlay() {
  loadingError.value = '';
  syncVideoState();
  showControls();
  emitter('playback-state-change', true);
}

function handleVideoPause() {
  syncVideoState();
  clearControlsHideTimeout();
  controlsVisible.value = true;
  persistProgress(true);
  emitter('playback-state-change', false);
}

function handleVideoClick() {
  if (isTouchDevice.value) {
    toggleControls();
    return;
  }

  clearPendingVideoClickTimeout();
  pendingVideoClickTimeout = window.setTimeout(() => {
    pendingVideoClickTimeout = 0;
    if (controlsVisible.value && !videoElement.value?.paused) {
      clearControlsHideTimeout();
      controlsVisible.value = false;
    }
  }, 180);
}

function handleVideoDoubleClick() {
  clearPendingVideoClickTimeout();
  void toggleFullscreen();
}

function handleVideoWebkitBeginFullscreen() {
  scheduleAssLayoutRefresh();
}

function handleVideoWebkitEndFullscreen() {
  scheduleAssLayoutRefresh();
}

function handleMediaError(event: Event) {
  void src_error(event);
}

function handlePosterError(event: Event) {
  const target = event.target as HTMLImageElement | null;
  if (!target || poster.value === '/images/placeholder.png') {
    return;
  }

  const fallback = getRemoteImage(props.item, false);
  if (fallback && poster.value !== fallback) {
    poster.value = fallback;
    hasPoster.value = true;
    target.src = uri(fallback);
    return;
  }

  poster.value = '/images/placeholder.png';
  hasPoster.value = false;
  target.src = uri('/images/placeholder.png');
}

function handleMediaVolumeChange(event: Event) {
  const target = event.target as HTMLMediaElement | null;
  if (!target || typeof target.volume !== 'number') return;

  if (target.muted !== muted.value) {
    muted.value = target.muted;
  }

  const normalizedVolume = clampMediaVolume(target.volume);
  if (Math.abs(mediaVol.value - normalizedVolume) > 0.001) {
    mediaVol.value = normalizedVolume;
  }

  syncVideoState();
  updateMediaSessionPosition(target);
}

function restoreStoredProgress() {
  if (!id.value || !videoElement.value) {
    return;
  }

  const saved = read(id.value);
  if (saved <= 0) {
    return;
  }

  void seekTo(saved).finally(() => {
    syncVideoState();
  });
}

function readSwitchTime() {
  const video = videoElement.value;
  if (!video) {
    return 0;
  }

  return Number.isFinite(video.currentTime) && video.currentTime > 0 ? video.currentTime : 0;
}

function isPlaying() {
  const video = videoElement.value;
  return Boolean(video && !video.paused && !video.ended);
}

async function seekTo(time: number) {
  const video = videoElement.value;
  if (!video) {
    return;
  }

  const nextTime = clampResumeTime(video, time);
  if (nextTime > 0) {
    try {
      video.currentTime = nextTime;
    } catch {}
  }
}

async function restoreSwitch(time: number, play: boolean) {
  await seekTo(time);

  if (play) {
    try {
      await videoElement.value?.play();
    } catch {}
  }
}

function persistProgress(force: boolean) {
  const video = videoElement.value;
  if (!id.value || !video) {
    return;
  }

  if (nearEnd(video)) {
    clear(id.value);
    lastSaveAt = 0;
    return;
  }

  const time = Number.isFinite(video.currentTime) && video.currentTime > 0 ? video.currentTime : 0;
  if (time <= 0) {
    return;
  }

  const now = Date.now();
  if (!force && now - lastSaveAt < 1000) {
    return;
  }

  save(id.value, time);
  lastSaveAt = now;
}

function handlePointerMove(event: PointerEvent) {
  if (!playerContainer.value || isTouchDevice.value) {
    return;
  }

  const rect = playerContainer.value.getBoundingClientRect();
  const y = event.clientY - rect.top;
  const bottomZone = Math.min(Math.max(rect.height * 0.28, 96), 180);

  if (y >= rect.height - bottomZone) {
    showControls();
  }
}

function handleSeekInput(event: Event) {
  const target = event.target as HTMLInputElement | null;
  if (!target || !videoElement.value || !duration.value) return;

  const sliderValue = Number(target.value);
  if (!Number.isFinite(sliderValue)) return;

  videoElement.value.currentTime = (sliderValue / 1000) * duration.value;
  syncVideoState();
  showControls();
}

function handleVolumeInput(event: Event) {
  const target = event.target as HTMLInputElement | null;
  if (!target || !videoElement.value) return;

  const nextVol = clampMediaVolume(Number(target.value) / 100);
  mediaVol.value = nextVol;
  muted.value = nextVol <= 0;
  applyMediaState(videoElement.value);
  syncVideoState();
  showControls();
}

async function togglePlayback() {
  if (!videoElement.value) return;

  try {
    if (videoElement.value.paused) {
      await videoElement.value.play();
      syncVideoState();
      showControls();
      return;
    }

    videoElement.value.pause();
    syncVideoState();
  } catch {}
}

function toggleMute() {
  if (muted.value || mediaVol.value <= 0) {
    mediaVol.value = mediaVol.value > 0 ? clampMediaVolume(mediaVol.value) : 1;
    muted.value = false;
  } else {
    muted.value = true;
  }

  applyMediaState(videoElement.value);
  syncVideoState();
  showControls();
}

function applyMediaState(element: HTMLMediaElement | null) {
  if (!element) return;

  const normalizedVolume = clampMediaVolume(mediaVol.value);
  if (Math.abs(element.volume - normalizedVolume) > 0.001) {
    element.volume = normalizedVolume;
  }

  if (element.muted !== muted.value) {
    element.muted = muted.value;
  }
}

function syncVideoState() {
  const video = videoElement.value;
  if (!video) {
    currentTime.value = 0;
    duration.value = 0;
    paused.value = true;
    emitter('playback-state-change', false);
    return;
  }

  const nextDuration = Number.isFinite(video.duration) && video.duration > 0 ? video.duration : 0;
  const nextTime =
    Number.isFinite(video.currentTime) && video.currentTime > 0 ? video.currentTime : 0;

  duration.value = nextDuration;
  currentTime.value = nextTime;
  paused.value = video.paused;

  if (video.ended || nearEnd(video)) {
    clear(id.value);
    lastSaveAt = 0;
  }

  emitter('playback-state-change', !video.paused);
}

function scheduleAssLayoutRefresh() {
  if (!usesAssSubtitleTrack.value) return;

  if (assLayoutRefreshFrame) {
    window.cancelAnimationFrame(assLayoutRefreshFrame);
  }

  void nextTick(() => {
    assLayoutRefreshFrame = window.requestAnimationFrame(() => {
      assLayoutRefreshFrame = 0;
      assLayoutVersion.value += 1;
    });
  });
}

function showControls() {
  controlsVisible.value = true;
  clearControlsHideTimeout();

  if (videoElement.value?.paused) {
    return;
  }

  controlsHideTimeout = window.setTimeout(() => {
    controlsVisible.value = false;
  }, 2500);
}

function toggleControls() {
  const nextVisible = nextTapVisible({
    touch: isTouchDevice.value,
    paused: Boolean(videoElement.value?.paused),
    visible: controlsVisible.value,
  });

  if (nextVisible === controlsVisible.value) {
    return;
  }

  if (nextVisible) {
    showControls();
    return;
  }

  clearControlsHideTimeout();
  controlsVisible.value = false;
}

function clearControlsHideTimeout() {
  if (controlsHideTimeout) {
    window.clearTimeout(controlsHideTimeout);
    controlsHideTimeout = 0;
  }
}

function clearPendingVideoClickTimeout() {
  if (pendingVideoClickTimeout) {
    window.clearTimeout(pendingVideoClickTimeout);
    pendingVideoClickTimeout = 0;
  }
}

function syncFullscreenState() {
  const fullscreenElement = getFullscreenElement();
  isFullscreen.value = Boolean(
    fullscreenElement && playerContainer.value && fullscreenElement === playerContainer.value,
  );
  scheduleAssLayoutRefresh();
}

async function toggleFullscreen() {
  if (!playerContainer.value || !canRequestFullscreen(playerContainer.value)) return;

  try {
    if (isFullscreen.value) {
      await exitDocumentFullscreen();
    } else {
      await requestElementFullscreen(playerContainer.value);
    }
  } catch {}
}

function bindMediaSessionListeners(el: HTMLVideoElement) {
  const onLoadedMetadata = (event: Event) => updateMediaSessionPosition(event.currentTarget);
  const onTimeUpdate = (event: Event) => updateMediaSessionPosition(event.currentTarget);
  const onRateChange = (event: Event) => updateMediaSessionPosition(event.currentTarget);
  const onSeeked = (event: Event) => updateMediaSessionPosition(event.currentTarget);

  el.addEventListener('loadedmetadata', onLoadedMetadata);
  el.addEventListener('timeupdate', onTimeUpdate);
  el.addEventListener('ratechange', onRateChange);
  el.addEventListener('seeked', onSeeked);

  return () => {
    el.removeEventListener('loadedmetadata', onLoadedMetadata);
    el.removeEventListener('timeupdate', onTimeUpdate);
    el.removeEventListener('ratechange', onRateChange);
    el.removeEventListener('seeked', onSeeked);
  };
}

function updateMediaSessionPosition(target: EventTarget | null) {
  if (false === 'mediaSession' in navigator) {
    return;
  }
  const el = (target as HTMLVideoElement) ?? null;
  if (!el || destroyed.value) {
    return;
  }
  const duration = el.duration;
  if (false === Number.isFinite(duration) || duration <= 0) {
    return;
  }
  try {
    navigator.mediaSession.setPositionState({
      duration,
      playbackRate: el.playbackRate,
      position: el.currentTime,
    });
  } catch {}
}

async function restoreDefaultTextTrack() {
  const el = videoElement.value;
  if (!el) {
    return;
  }

  try {
    const tracksList = el.textTracks;
    if (!tracksList || tracksList.length === 0) {
      return;
    }

    const firstTrack = tracksList[0] as TextTrack | undefined;
    const needsReload = firstTrack && (!firstTrack.cues || firstTrack.cues.length === 0);

    if (needsReload) {
      const trackElements = el.querySelectorAll('track');

      trackElements.forEach((trackEl, idx) => {
        const parent = trackEl.parentNode;
        const clone = trackEl.cloneNode(true) as HTMLTrackElement;
        trackEl.remove();
        setTimeout(() => {
          if (parent) {
            parent.appendChild(clone);
            clone.addEventListener(
              'load',
              () => {
                const trackObj = clone.track;
                if (trackObj) {
                  trackObj.mode = idx === 0 && subtitleEnabled.value ? 'showing' : 'disabled';
                }
              },
              { once: true },
            );
          }
        }, idx * 10);
      });

      return;
    }

    for (let i = 0; i < tracksList.length; i += 1) {
      const track = tracksList[i] as TextTrack | undefined;
      if (track) {
        track.mode = 'disabled';
      }
    }

    await new Promise((resolve) => setTimeout(resolve, 50));

    for (let i = 0; i < tracksList.length; i += 1) {
      const track = tracksList[i] as TextTrack | undefined;
      if (!track) {
        continue;
      }
      track.mode = i === 0 && subtitleEnabled.value ? 'showing' : 'disabled';
    }
  } catch (error) {
    console.warn('Failed to restore subtitle track state', error);
  }
}

function applyMediaSessionMetadata() {
  if (false === 'mediaSession' in navigator) {
    return;
  }
  const metadata: MediaMetadataInit = { title: title.value };
  if (artist.value) {
    metadata.artist = artist.value;
  }
  if (poster.value) {
    metadata.artwork = [{ src: poster.value, sizes: '1920x1080', type: 'image/jpeg' }];
  }
  try {
    navigator.mediaSession.metadata = new MediaMetadata(metadata);
  } catch {}
}

async function loadPlayerInfo() {
  if (!props.item.filename) {
    loading.value = false;
    loadingError.value = 'No media file is available for playback.';
    emitter('error', loadingError.value);
    emitter('closeModel');
    return;
  }

  loading.value = true;
  loadingError.value = '';

  const req = await request(currentPlaybackUrl('api/file/info'));
  const response: FileInfo = await req.json();

  if (!req.ok) {
    loading.value = false;
    loadingError.value = response?.error || 'Failed to fetch video info. Unknown error';
    emitter('error', loadingError.value);
    emitter('closeModel');
    return;
  }

  playerInfo.value = response;

  poster.value = '/images/placeholder.png';
  hasPoster.value = false;

  if (props.item._id && props.item.filename) {
    poster.value = `/api/history/${encodeURIComponent(props.item._id)}/thumbnail`;
    hasPoster.value = true;
  } else if (response.sidecar?.image?.[0]?.file) {
    poster.value = makeDownload(config, { filename: response.sidecar.image[0].file });
    hasPoster.value = true;
  } else if (props.item.extras?.thumbnail) {
    poster.value = getRemoteImage(props.item);
    hasPoster.value = true;
  }

  hasVideo.value =
    Array.isArray(response.ffprobe?.video) &&
    response.ffprobe.video.some((stream) => stream.codec_type === 'video');

  if (!props.item.extras?.is_video && props.item.extras?.is_audio) {
    isAudio.value = true;
  } else if (hasVideo.value === false) {
    isAudio.value = true;
  }

  sources.value = [];
  if (isApple) {
    const allowedCodec = response.mimetype && response.mimetype.includes('video/mp4');
    const src = currentPlaybackUrl(allowedCodec ? 'api/download' : 'm3u8', !allowedCodec);
    sources.value.push({
      src,
      type: allowedCodec ? response.mimetype : 'application/x-mpegURL',
      onerror: (err: Event) => void src_error(err),
    });
    usingHls.value = !allowedCodec;
  } else {
    sources.value.push({
      src: currentPlaybackUrl('api/download'),
      type: response.mimetype,
      onerror: (err: Event) => void src_error(err),
    });
    usingHls.value = false;
  }

  if (props.item.extras?.channel) {
    artist.value = props.item.extras.channel;
  } else if (props.item.extras?.uploader) {
    artist.value = props.item.extras.uploader;
  }

  if (props.item.title) {
    title.value = props.item.title;
  } else if (response.title) {
    title.value = response.title;
  } else if (response.ffprobe?.metadata?.tags?.title) {
    title.value = response.ffprobe.metadata.tags.title;
  }

  if (isApple) {
    document.documentElement.style.setProperty('--webkit-text-track-display', 'block');
  }

  loading.value = false;
  await nextTick();

  if (videoElement.value) {
    unbindMediaSession = bindMediaSessionListeners(videoElement.value);
  }

  prepareVideoPlayer();
}

function prepareVideoPlayer() {
  if (loading.value) {
    return;
  }

  applyMediaSessionMetadata();

  if (!videoElement.value) {
    return;
  }

  applyMediaState(videoElement.value);
  restoreDefaultTextTrack();
}

async function src_error(event: Event) {
  if (hls) {
    return;
  }

  await nextTick();
  if (destroyed.value) {
    return;
  }

  console.warn('Source failed to load, attempting HLS fallback via hls.js...', event);
  attach_hls(currentPlaybackUrl('m3u8', true));
}

function attach_hls(
  link: string,
  time = pendingSeek ?? readSwitchTime(),
  play = pendingPlay || isPlaying(),
) {
  if (!videoElement.value) {
    return;
  }

  pendingSeek = time;
  pendingPlay = play;

  if (hls) {
    hls.destroy();
  }

  hls = new Hls({
    debug: false,
    enableWorker: true,
    lowLatencyMode: true,
    backBufferLength: 120,
    fragLoadingTimeOut: 200000,
  });

  hls.on(Hls.Events.MANIFEST_PARSED, () => applyMediaSessionMetadata());
  hls.on(Hls.Events.MANIFEST_PARSED, async () => {
    await new Promise((resolve) => setTimeout(resolve, 100));
    await restoreDefaultTextTrack();
  });

  hls.on(Hls.Events.MEDIA_ATTACHED, async () => {
    await new Promise((resolve) => setTimeout(resolve, 200));
    await restoreDefaultTextTrack();
  });

  hls.loadSource(link);
  hls.attachMedia(videoElement.value);
  usingHls.value = true;
}

function forceSwitchToHls() {
  if (usingHls.value) {
    return;
  }

  if (!hasVideo.value) {
    useNotification().error('Cannot switch to HLS: stream has no video track.');
    return;
  }

  attach_hls(currentPlaybackUrl('m3u8', true));
}

usePlayerShortcuts({
  enabled: computed(() => active.value && Boolean(videoElement.value)),
  media: videoElement,
  video: videoElement,
  adjustVolume: (delta) => {
    mediaVol.value = clampMediaVolume(mediaVol.value + delta);
    muted.value = mediaVol.value <= 0;
    applyMediaState(videoElement.value);
    syncVideoState();
    showControls();
  },
  canToggleSubs: hasSubtitles,
  helpOpen: showShortcutHelp,
  toggleSubtitles: () => {
    subtitleEnabled.value = !subtitleEnabled.value;
    void nextTick(() => restoreDefaultTextTrack());
  },
  toggleFullscreen,
  toggleMute,
  closePlayer: () => emitter('closeModel'),
});

watch(subtitleEnabled, () => {
  void nextTick(() => restoreDefaultTextTrack());
});

onMounted(async () => {
  disableOpacity();
  isTouchDevice.value = window.matchMedia('(pointer: coarse)').matches;
  document.addEventListener('fullscreenchange', syncFullscreenState);
  document.addEventListener('webkitfullscreenchange', syncFullscreenState as EventListener);
  window.addEventListener('resize', scheduleAssLayoutRefresh);
  window.addEventListener('orientationchange', scheduleAssLayoutRefresh);
  syncFullscreenState();
  await loadPlayerInfo();
});

onBeforeUnmount(() => {
  enableOpacity();
  document.removeEventListener('fullscreenchange', syncFullscreenState);
  document.removeEventListener('webkitfullscreenchange', syncFullscreenState as EventListener);
  window.removeEventListener('resize', scheduleAssLayoutRefresh);
  window.removeEventListener('orientationchange', scheduleAssLayoutRefresh);

  if (assLayoutRefreshFrame) {
    window.cancelAnimationFrame(assLayoutRefreshFrame);
  }

  clearControlsHideTimeout();
  clearPendingVideoClickTimeout();

  if (hls) {
    hls.destroy();
    hls = null;
  }

  if (unbindMediaSession) {
    unbindMediaSession();
    unbindMediaSession = null;
  }

  if (videoElement.value) {
    persistProgress(true);
    destroyed.value = true;
    try {
      videoElement.value.pause();
      videoElement.value
        .querySelectorAll('source')
        .forEach((source) => source.removeAttribute('src'));
      videoElement.value.load();
    } catch (error) {
      console.error(error);
    }
  }
});
</script>

<style scoped>
.share-video-element::-webkit-media-controls {
  display: none;
}

.share-video-element::-webkit-media-controls-fullscreen-button {
  display: none;
}
</style>
