<!-- 创作视频播放器，负责鉴权媒体加载、播放控制和全屏预览。 -->
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
import { FullScreen } from "@element-plus/icons-vue";
import { Speaker, SpeakerOff } from "@/icons";
import { creativeApi } from "@/api/creative";
import MediaLoadingState from "@/components/creative/MediaLoadingState.vue";
import { useToastStore } from "@/stores/toast";

const props = withDefaults(
  defineProps<{
    src: string;
    mediaId?: string;
    startTime?: number;
    aspectRatio?: string;
    seekable?: boolean;
    transparentControls?: boolean;
  }>(),
  { startTime: 0, aspectRatio: "16:9", seekable: true, transparentControls: false },
);
const toast = useToastStore();
const emit = defineEmits<{
  play: [time: number];
  pause: [time: number];
  seek: [time: number];
  "seek-end": [time: number];
  volume: [muted: boolean];
  ended: [];
  "fullscreen-open": [];
}>();
const video = ref<HTMLVideoElement>();
const dialogVideo = ref<HTMLVideoElement>();
const playing = ref(false);
const dialogPlaying = ref(false);
const muted = ref(true);
const progress = ref(0);
const duration = ref(0);
const fullscreen = ref(false);
const dialogProgress = ref(0);
const dialogDuration = ref(0);
const playableSrc = ref("");
const loading = ref(true);
const failed = ref(false);
const dialogLoading = ref(true);
const dialogFailed = ref(false);
let mediaObjectUrl = "";
let loadSequence = 0;
const dialogClass = computed(() => ({ portrait: props.aspectRatio === "9:16" }));

watch(
  [() => props.src, () => props.mediaId],
  async () => {
    const sequence = ++loadSequence;
    const previousObjectUrl = mediaObjectUrl;
    progress.value = 0;
    duration.value = 0;
    loading.value = true;
    failed.value = false;
    dialogLoading.value = true;
    dialogFailed.value = false;
    if (props.mediaId) {
      try {
        const blob = await creativeApi.media(props.mediaId);
        if (sequence !== loadSequence) return;
        mediaObjectUrl = URL.createObjectURL(blob);
        playableSrc.value = mediaObjectUrl;
      } catch {
        if (sequence !== loadSequence) return;
        mediaObjectUrl = "";
        playableSrc.value = props.src;
        toast.error("媒体读取失败");
      }
    } else {
      mediaObjectUrl = "";
      playableSrc.value = props.src;
    }
    await nextTick();
    if (sequence !== loadSequence) return;
    video.value?.load();
    dialogVideo.value?.load();
    if (previousObjectUrl && previousObjectUrl !== mediaObjectUrl) {
      URL.revokeObjectURL(previousObjectUrl);
    }
  },
  { immediate: true },
);
onBeforeUnmount(() => {
  loadSequence++;
  if (mediaObjectUrl) URL.revokeObjectURL(mediaObjectUrl);
});

/** 根据当前播放状态切换视频播放和暂停。 */
function toggle() {
  if (!video.value) return;
  if (video.value.paused) {
    void video.value.play();
  } else {
    video.value.pause();
  }
}
/** 同步播放状态并通知父组件。 */
function onPlay() {
  playing.value = true;
  emit("play", video.value?.currentTime || 0);
}
/** 同步暂停状态并通知父组件。 */
function onPause() {
  playing.value = false;
  emit("pause", video.value?.currentTime || 0);
}
/** 视频播放结束时复位状态并通知父组件。 */
function onEnded() {
  playing.value = false;
  emit("ended");
}
/** 媒体元数据加载完成后同步时长和当前时间。 */
function onLoadedMetadata() {
  duration.value = video.value?.duration || 0;
  if (video.value) video.value.currentTime = props.startTime;
  progress.value = props.startTime;
}
/** 媒体数据可用时清除加载状态。 */
function onLoadedData() {
  loading.value = false;
  failed.value = false;
}
/** 媒体加载失败时记录错误状态并通知父组件。 */
function onMediaError() {
  if (!playableSrc.value) return;
  loading.value = false;
  failed.value = true;
}
/** 根据主播放器进度控件更新播放位置。 */
function seek(event: Event) {
  if (!video.value) return;
  const value = Number((event.target as HTMLInputElement).value);
  video.value.currentTime = value;
  progress.value = value;
  emit("seek", value);
}
/** 结束拖动并通知父组件恢复时间同步。 */
function finishSeek() {
  emit("seek-end", video.value?.currentTime || 0);
}
/** 根据全屏预览进度控件更新播放位置。 */
function dialogSeek(event: Event) {
  if (!dialogVideo.value) return;
  const value = Number((event.target as HTMLInputElement).value);
  dialogVideo.value.currentTime = value;
  dialogProgress.value = value;
}
/** 按给定秒数偏移逐帧调整当前播放位置。 */
function frame(delta: number) {
  if (!video.value) return;
  video.value.pause();
  video.value.currentTime = Math.max(0, video.value.currentTime + delta);
  progress.value = video.value.currentTime;
  emit("seek", video.value.currentTime);
}
/** 切换全屏预览视频的播放状态。 */
function toggleDialog() {
  if (!dialogVideo.value) return;
  dialogVideo.value.paused ? void dialogVideo.value.play() : dialogVideo.value.pause();
}
/** 切换全屏预览视频的静音状态。 */
function toggleDialogMute() {
  muted.value = !muted.value;
  if (dialogVideo.value) dialogVideo.value.muted = muted.value;
  emit("volume", muted.value);
}
/** 切换主播放器静音状态并通知父组件。 */
function toggleMute() {
  muted.value = !muted.value;
  if (video.value) video.value.muted = muted.value;
  if (dialogVideo.value) dialogVideo.value.muted = muted.value;
  emit("volume", muted.value);
}
/** 打开全屏预览，并保持当前播放位置和静音状态。 */
function openFullscreen() {
  video.value?.pause();
  playing.value = false;
  fullscreen.value = true;
  dialogLoading.value = true;
  dialogFailed.value = false;
  emit("fullscreen-open");
  void nextTick(() => {
    if (!dialogVideo.value || !video.value) return;
    dialogVideo.value.currentTime = video.value.currentTime;
    dialogVideo.value.muted = muted.value;
    dialogProgress.value = dialogVideo.value.currentTime;
  });
}
/** 关闭全屏预览并暂停预览播放器。 */
function closeFullscreen() {
  dialogVideo.value?.pause();
  dialogPlaying.value = false;
}
/** 将秒数格式化为播放器时间文案。 */
function formatTime(value: number) {
  if (!Number.isFinite(value)) return "00:00";
  return `${String(Math.floor(value / 60)).padStart(2, "0")}:${String(Math.floor(value) % 60).padStart(2, "0")}`;
}
/** 将当前视频帧导出为 PNG 图片。 */
function exportFrame() {
  const currentVideo = video.value;
  if (
    !currentVideo ||
    currentVideo.readyState < HTMLMediaElement.HAVE_CURRENT_DATA ||
    !currentVideo.videoWidth ||
    !currentVideo.videoHeight
  ) {
    toast.warning("视频尚未加载完成");
    return;
  }
  const canvas = document.createElement("canvas");
  canvas.width = currentVideo.videoWidth;
  canvas.height = currentVideo.videoHeight;
  const context = canvas.getContext("2d");
  if (!context) {
    toast.error("当前浏览器无法导出静帧");
    return;
  }
  try {
    context.drawImage(currentVideo, 0, 0, canvas.width, canvas.height);
    canvas.toBlob((blob) => {
      if (!blob) {
        toast.error("静帧导出失败");
        return;
      }
      const objectUrl = URL.createObjectURL(blob);
      const link = document.createElement("a");
      link.href = objectUrl;
      link.download = "frame.png";
      document.body.appendChild(link);
      link.click();
      link.remove();
      window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
    }, "image/png");
  } catch {
    toast.error("静帧导出失败");
  }
}
defineExpose({
  openFullscreen,
  previousFrame: () => frame(-1 / 25),
  nextFrame: () => frame(1 / 25),
  exportFrame,
  pause: () => video.value?.pause(),
  play: () => video.value?.play(),
  currentTime: () => video.value?.currentTime || 0,
  setCurrentTime: (time: number) => {
    if (video.value) {
      video.value.currentTime = time;
      progress.value = time;
    }
  },
  setMuted: (value: boolean) => {
    muted.value = value;
    if (video.value) video.value.muted = value;
    if (dialogVideo.value) dialogVideo.value.muted = value;
  },
  isMuted: () => muted.value,
});
</script>

<template>
  <div class="video-player">
    <video
      ref="video"
      :src="playableSrc || undefined"
      preload="metadata"
      playsinline
      disablepictureinpicture
      disableremoteplayback
      controlslist="nodownload noplaybackrate noremoteplayback"
      :muted="muted"
      @timeupdate="progress = video?.currentTime || 0"
      @loadedmetadata="onLoadedMetadata"
      @loadeddata="onLoadedData"
      @error="onMediaError"
      @play="onPlay"
      @pause="onPause"
      @ended="onEnded"
      @click="toggle"
    />
    <MediaLoadingState
      v-if="loading || failed"
      :failed="failed"
      label="视频加载中"
      error-label="视频加载失败"
    />
    <div class="video-controls" :class="{ 'video-controls-transparent': props.transparentControls }">
      <input
        type="range"
        :disabled="!props.seekable"
        min="0"
        :max="duration"
        step="0.01"
        :value="progress"
        @input="seek"
        @change="finishSeek"
      /><span class="video-time">{{ formatTime(progress) }} / {{ formatTime(duration) }}</span
      ><button
        type="button"
        class="sound-button"
        :title="muted ? '取消静音' : '静音'"
        @click="toggleMute"
      >
        <SpeakerOff
          v-if="muted"
          :size="17"
          :stroke-width="1.8"
        /><Speaker
          v-else
          :size="17"
          :stroke-width="1.8"
        /></button
      ><el-icon
        title="全屏预览"
        @click="openFullscreen"
        ><FullScreen
      /></el-icon>
    </div>
    <el-dialog
      v-model="fullscreen"
      title="视频预览"
      :width="props.aspectRatio === '9:16' ? 'min(440px, calc(100vw - 28px))' : 'min(900px, calc(100vw - 28px))'"
      class="video-preview-dialog"
      append-to-body
      align-center
      @closed="closeFullscreen"
    >
      <div
        class="dialog-player"
        :class="dialogClass"
      >
        <video
          ref="dialogVideo"
          :src="playableSrc || undefined"
          preload="metadata"
          playsinline
          disablepictureinpicture
          disableremoteplayback
          controlslist="nodownload noplaybackrate noremoteplayback"
          :muted="muted"
          @timeupdate="dialogProgress = dialogVideo?.currentTime || 0"
          @loadedmetadata="
            dialogDuration = dialogVideo?.duration || 0;
            dialogProgress = video?.currentTime || 0;
          "
          @loadeddata="
            dialogLoading = false;
            dialogFailed = false;
          "
          @error="
            dialogLoading = false;
            dialogFailed = true;
          "
          @play="dialogPlaying = true"
          @pause="dialogPlaying = false"
          @click="toggleDialog"
        />
        <MediaLoadingState
          v-if="dialogLoading || dialogFailed"
          :failed="dialogFailed"
          label="视频加载中"
          error-label="视频加载失败"
        />
        <div class="dialog-controls">
          <input
            type="range"
            min="0"
            :max="dialogDuration"
            step="0.01"
            :value="dialogProgress"
            @input="dialogSeek"
          /><span>{{ formatTime(dialogProgress) }} / {{ formatTime(dialogDuration) }}</span
          ><button
            type="button"
            @click="toggleDialogMute"
          >
            <SpeakerOff
              v-if="muted"
              :size="17"
            /><Speaker
              v-else
              :size="17"
            />
          </button>
        </div>
      </div>
    </el-dialog>
  </div>
</template>

<style scoped>
.video-player {
  position: relative;
  width: 100%;
  height: 100%;
  background: var(--web-surface-soft);
}
.video-player > video {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
}
.video-player video::-webkit-media-controls-picture-in-picture-button,
.dialog-player video::-webkit-media-controls-picture-in-picture-button {
  display: none !important;
}
.video-controls {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 38px;
  padding: 0 10px;
  display: flex;
  align-items: center;
  gap: 9px;
  background: var(--web-video-controls);
  color: var(--web-text);
}
.video-controls-transparent {
  background: transparent;
  color: var(--web-text-secondary);
}
.video-controls-transparent .video-time {
  color: var(--web-text-secondary);
}
.video-controls-transparent .sound-button,
.video-controls-transparent .el-icon {
  color: var(--web-text-secondary);
}
.video-controls-transparent .sound-button:hover,
.video-controls-transparent .el-icon:hover {
  color: var(--web-text-secondary);
}
.video-controls input {
  min-width: 0;
  flex: 1;
}
.video-controls input:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}
.video-time {
  flex: 0 0 auto;
  color: var(--web-text-secondary);
  font:
    10px Arial,
    sans-serif;
  white-space: nowrap;
}
.video-controls .el-icon {
  cursor: pointer;
}
.sound-button,
.dialog-controls button {
  width: 22px;
  height: 22px;
  padding: 0;
  display: grid;
  place-items: center;
  color: var(--web-media-control-text);
  border: 0;
  background: none;
  cursor: pointer;
}
.sound-button:hover,
.dialog-controls button:hover {
  color: var(--web-media-control-text);
}
.dialog-player {
  position: relative;
  width: 100%;
  aspect-ratio: 16/9;
  background: var(--web-video-bg);
}
.dialog-player.portrait {
  width: min(100%, 400px);
  margin: 0 auto;
  aspect-ratio: 9/16;
}
.dialog-player > video {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
  background: var(--web-video-bg);
}
.dialog-controls {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 42px;
  padding: 0 10px;
  display: flex;
  align-items: center;
  gap: 9px;
  background: var(--web-video-dialog-controls);
  color: var(--web-text);
}
.dialog-controls input {
  min-width: 0;
  flex: 1;
}
.dialog-controls span {
  font:
    11px Arial,
    sans-serif;
  white-space: nowrap;
  color: var(--web-media-control-text);
}
</style>
<style>
.video-preview-dialog.el-dialog {
  background: var(--web-surface-soft);
  border: 1px solid var(--web-line-strong);
}

.video-preview-dialog.el-dialog .el-dialog__header {
  border-bottom: 1px solid var(--web-line-strong);
}

.video-preview-dialog.el-dialog .el-dialog__title,
.video-preview-dialog.el-dialog .el-dialog__headerbtn .el-dialog__close {
  color: var(--web-text);
}

.video-preview-dialog.el-dialog .el-dialog__body {
  padding: 14px;
  background: var(--web-surface-soft);
}
</style>
