<script setup lang="ts">
import type { PropType } from 'vue'
import { encode } from '@monosky/base64'
import { object, string, number, type InferType } from 'yup';

interface Subtitle {
  start: string;
  end: string;
  text: string;
  active?: boolean
}

const props = defineProps({
  course: {
    type: Object as PropType<resp.gen.CourseGenItem>,
    required: true,
  },
})

const dayjs = useDayjs()
const toast = useToast()
const loginState = useLoginState()

const isDrawerActive = ref(false)
const isLoading = ref(true)
const isSaving = ref(false)
const rawSrt = ref<string | null>(null)
const subtitles = ref<Subtitle[]>([])
const modified = ref(false)
const isExporting = ref(false)

const videoElement = ref<HTMLVideoElement | null>(null)

const subtitleStyleSchema = object({
  color: string().required(),
  fontSize: number().required(),
  effect: string().required(),
  bottomOffset: number().required(),
})
type subtitleStyleSchema = InferType<typeof subtitleStyleSchema>

const subtitleStyleState = reactive<subtitleStyleSchema>({
  color: '#fff',
  effect: 'shadow',
  fontSize: 24,
  bottomOffset: 12,
})

const loadSrt = async () => {
  isLoading.value = true
  try {
    // const response = await fetch(props.course.subtitle_url)
    const response = await fetch(await fetchCourseSubtitleUrl(props.course))
    const text = await response.text()
    rawSrt.value = text
    parseSrt(text)
  } catch (err) {
    toast.add({
      title: '加载字幕失败',
      description: `${err}` || '未知错误',
      color: 'red',
    })
  } finally {
    isLoading.value = false
  }
}

const parseSrt = (srt: string) => {
  const lines = srt.split(/\r?\n/)
  const regex = /(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})/
  let subtitle: Subtitle | null = null

  lines.forEach(line => {
    if (/^\d+$/.test(line.trim())) return

    const match = line.match(regex)
    if (match) {
      if (subtitle) {
        subtitles.value.push(subtitle)
      }
      subtitle = {
        start: match[1],
        end: match[2],
        text: '',
      }
    } else if (subtitle) {
      subtitle.text += (line.trim() ? line : '')
    }
  })

  if (subtitle) {
    subtitles.value.push(subtitle)
  }
}

const generateSrt = () => {
  return subtitles.value.map((subtitle, index) => {
    return `${index + 1}\n${subtitle.start} --> ${subtitle.end}\n${subtitle.text}\n`
  }).join('\n')
}

const formatTime = (time: string) => {
  const parts = time.split(',')
  const timeParts = parts[0].split(':')
  return {
    hours: parseInt(timeParts[0]),
    minutes: parseInt(timeParts[1]),
    seconds: parseInt(timeParts[2]),
    milliseconds: parseInt(parts[1]),
  }
}

const formatTimeToDayjs = (time: string) => {
  const parts = time.split(',')
  const timeParts = parts[0].split(':')
  return dayjs().hour(parseInt(timeParts[0])).minute(parseInt(timeParts[1])).second(parseInt(timeParts[2])).millisecond(parseInt(parts[1]))
}

const syncSubtitles = () => {
  if (!videoElement.value) return

  const currentTime = videoElement.value.currentTime * 1000 // convert to milliseconds

  subtitles.value.forEach(subtitle => {
    const start = formatTime(subtitle.start)
    const end = formatTime(subtitle.end)

    const startTime = (start.hours * 3600 + start.minutes * 60 + start.seconds) * 1000 + start.milliseconds
    const endTime = (end.hours * 3600 + end.minutes * 60 + end.seconds) * 1000 + end.milliseconds

    subtitle.active = currentTime >= startTime && currentTime <= endTime
    // scroll active subtitle into view
    if (subtitle.active) {
      const element = document.getElementById(`subtitle-${subtitles.value.indexOf(subtitle)}`)!
      const parent = element?.parentElement
      // scroll element to the center of parent
      parent?.scrollTo({
        top: element.offsetTop,
      })
    }
  })
}

const onSubtitleInputClick = (subtitle: Subtitle) => {
  if (!videoElement.value) return
  if (!subtitle.active) {
    videoElement.value.currentTime = formatTime(subtitle.start).hours * 3600 +
      formatTime(subtitle.start).minutes * 60 +
      formatTime(subtitle.start).seconds + 1
  }
  videoElement.value.pause()
}

const saveNewSubtitle = () => {
  isSaving.value = true
  const encodedSubtitle = encode(generateSrt())
  useFetchWrapped<
    req.gen.CourseSubtitleCreate & AuthedRequest,
    BaseResponse<resp.gen.CourseSubtitleCreate>
  >('App.Digital_VideoSubtitle.CreateFile', {
    token: loginState.token!,
    user_id: loginState.user.id,
    sub_type: 1,
    sub_content: encodedSubtitle,
    task_id: props.course?.task_id,
  }).then(_ => {
    modified.value = false
    toast.add({
      color: 'green',
      title: '字幕已保存',
      description: '修改后的字幕文件已保存',
    })
  }).finally(() => {
    isSaving.value = false
  })
}

const exportVideo = () => {
  isExporting.value = true
  useVideoSubtitleEmbedding(props.course.video_url, props.course.subtitle_url, {
    color: subtitleStyleState.color,
    fontSize: subtitleStyleState.fontSize,
    textShadow: subtitleStyleState.effect === 'shadow' ? {
      offsetX: 2,
      offsetY: 2,
      blur: 4,
      color: "rgba(0, 0, 0, 0.25)",
    } : {
      offsetX: 0,
      offsetY: 0,
      blur: 0,
      color: 'transparent',
    },
    strokeStyle: subtitleStyleState.effect === 'stroke' ? '#000 2px' : 'none',
    bottomOffset: subtitleStyleState.bottomOffset,
  }).then(blobUrl => {
    const { download } = useDownload(blobUrl, 'combined_video.mp4')
    download()
  }).finally(() => {
    isExporting.value = false
  })
}

onMounted(() => {
  if (rawSrt.value) {
    parseSrt(rawSrt.value)
  }
})

defineExpose({
  open() {
    isDrawerActive.value = true
    if (!rawSrt.value) loadSrt()
  },
  close() {
    isDrawerActive.value = false
  },
})
</script>

<template>
  <div>
    <USlideover v-model="isDrawerActive" :prevent-close="modified" :ui="{ width: 'max-w-xl' }">
      <UCard class="flex flex-col flex-1 overflow-hidden" :ui="{
        body: { base: 'overflow-auto flex-1' },
        ring: '',
        divide: 'divide-y divide-gray-100 dark:divide-gray-800'
      }">
        <template #header>
          <UButton color="gray" variant="ghost" size="sm" icon="tabler:x"
            class="flex sm:hidden absolute end-5 top-5 z-10" square padded @click="isDrawerActive = false" />
          <div class="flex flex-col">
            <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
              字幕编辑器
            </h3>
            <h3 class="text-xs font-semibold text-blue-500" v-if="course.title">
              {{ course.title }}
            </h3>
          </div>
        </template>

        <div v-if="isLoading" class="flex justify-center items-center text-primary">
          <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
            <defs>
              <filter id="svgSpinnersGooeyBalls20">
                <feGaussianBlur in="SourceGraphic" result="y" stdDeviation="1" />
                <feColorMatrix in="y" result="z" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7" />
                <feBlend in="SourceGraphic" in2="z" />
              </filter>
            </defs>
            <g filter="url(#svgSpinnersGooeyBalls20)">
              <circle cx="5" cy="12" r="4" fill="currentColor">
                <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57"
                  repeatCount="indefinite" values="5;8;5" />
              </circle>
              <circle cx="19" cy="12" r="4" fill="currentColor">
                <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57"
                  repeatCount="indefinite" values="19;16;19" />
              </circle>
              <animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate"
                values="0 12 12;360 12 12" />
            </g>
          </svg>
        </div>
        <div v-else class="flex flex-col h-full gap-2 overflow-hidden overscroll-y-none overshadow">
          <div class="relative overflow-hidden min-h-12">
            <div class="absolute w-fit mx-auto inset-x-0 font-sans font-bold subtitle" :class="{
              'stroke': subtitleStyleState.effect === 'stroke',
            }" :style="{
              lineHeight: '1',
              color: subtitleStyleState.color,
              fontSize: subtitleStyleState.fontSize / 1.5 + 'px',
              bottom: subtitleStyleState.bottomOffset / 1.5 + 'px',
              textShadow: subtitleStyleState.effect === 'shadow' ? '2px 2px 4px rgba(0, 0, 0, 0.25)' : undefined
            }">
              {{ subtitles.find(sub => sub.active)?.text }}
            </div>
            <video controls ref="videoElement" class="rounded" style="-webkit-user-drag: none;" :src="course.video_url"
              @timeupdate="syncSubtitles" />
          </div>
          <UAccordion :items="[{ label: '字幕选项' }]" color="gray" size="lg">
            <template #item>
              <div class="border dark:border-neutral-700 rounded-lg space-y-4 p-4 pb-6">
                <div class="w-full flex flex-col justify-center">
                  <div class="rounded-md w-full aspect-video relative overflow-hidden">
                    <img class="object-cover w-full h-full rounded-md"
                      src="https://static-xsh.oss-cn-chengdu.aliyuncs.com/file/2024-08-04/9ed1e5c0133824f0bcf79d1ad9e9ecbb.png" />
                    <span class="absolute font-sans font-bold bottom-0 left-1/2 transform -translate-x-1/2 subtitle"
                      :class="{
                        'stroke': subtitleStyleState.effect === 'stroke',
                      }" :style="{
                        lineHeight: '1',
                        color: subtitleStyleState.color,
                        fontSize: subtitleStyleState.fontSize / 1.5 + 'px',
                        bottom: subtitleStyleState.bottomOffset / 1.5 + 'px',
                        textShadow: subtitleStyleState.effect === 'shadow' ? '2px 2px 4px rgba(0, 0, 0, 0.25)' : undefined,
                      }">
                      字幕样式预览
                    </span>
                  </div>
                  <span class="text-sm italic opacity-50">字幕预览仅供参考，以实际渲染效果为准</span>
                </div>
                <UForm :schema="subtitleStyleSchema" :state="subtitleStyleState" class="flex flex-col gap-4">
                  <div class="flex gap-4">
                    <UFormGroup label="字幕颜色" name="fontColor" class="w-full" size="xs">
                      <USelectMenu :options="[{
                        label: '黑色',
                        value: '#000',
                      }, {
                        label: '白色',
                        value: '#fff',
                      }]" option-attribute="label" value-attribute="value" v-model="subtitleStyleState.color" />
                    </UFormGroup>
                    <UFormGroup label="字幕效果" name="effect" class="w-full" size="xs">
                      <USelectMenu :options="[{
                        label: '阴影',
                        value: 'shadow',
                      }, {
                        label: '描边',
                        value: 'stroke',
                      }]" option-attribute="label" value-attribute="value" v-model="subtitleStyleState.effect" />
                    </UFormGroup>
                  </div>
                  <UFormGroup :label="`字幕大小 ${subtitleStyleState.fontSize}px`" name="fontSize" size="xs">
                    <URange :max="64" :min="20" :step="2" size="sm" v-model="subtitleStyleState.fontSize" />
                  </UFormGroup>
                  <UFormGroup :label="`字幕偏移量 ${subtitleStyleState.bottomOffset}px`" name="offset" size="xs">
                    <URange :max="30" :min="0" :step="1" size="sm" v-model="subtitleStyleState.bottomOffset" />
                  </UFormGroup>
                </UForm>
              </div>
            </template>
          </UAccordion>
          <ul class="flex-1 px-0.5 pb-[100%] overflow-y-auto space-y-0.5 scroll-smooth relative">
            <li v-for="(subtitle, index) in subtitles" :key="index" :id="'subtitle-' + index">
              <div :class="{ 'text-primary': subtitle.active }">
                <span class="text-xs font-medium opacity-60">
                  {{ formatTimeToDayjs(subtitle.start).format('HH:mm:ss') }}
                  -
                  {{ formatTimeToDayjs(subtitle.end).format('HH:mm:ss') }}
                  <span class="opacity-50">
                    [{{ formatTimeToDayjs(subtitle.end).diff(formatTimeToDayjs(subtitle.start), 'second') }}s]
                  </span>
                </span>
                <UInput v-model="subtitle.text" class="w-full" placeholder="请输入字幕内容" :name="'subtitle-' + index"
                  :autofocus="false" :color="subtitle.active ? 'primary' : undefined"
                  @click="onSubtitleInputClick(subtitle)" @input="() => { if (!modified) modified = true }">
                  <template #trailing>
                    <UIcon v-show="subtitle.active" name="tabler:keyframe-align-vertical-filled" />
                  </template>
                </UInput>
              </div>
            </li>
          </ul>
        </div>

        <template #footer>
          <div class="flex justify-end items-center gap-2">
            <span v-if="modified" class="text-sm text-yellow-500 font-medium">已更改但未保存</span>
            <UButton :loading="isExporting" variant="soft" icon="i-tabler-file-export" @click="exportVideo">
              导出视频
            </UButton>
            <UButton :disabled="isExporting || !modified" :loading="isSaving" icon="i-tabler-device-floppy"
              @click="saveNewSubtitle">
              保存{{ isSaving ? '中' : '' }}
            </UButton>
          </div>
        </template>
      </UCard>
    </USlideover>
  </div>
</template>

<style scoped>
.overshadow {
  @apply relative;
}

.overshadow:after {
  content: "";
  inset: 80% 0 0;
  position: absolute;
  @apply bg-gradient-to-b from-transparent to-white dark:to-neutral-950 pointer-events-none;
}

.subtitle.stroke {
  text-shadow: 1px 1px 0 #000,
    -1px -1px 0 #000,
    1px -1px 0 #000,
    -1px 1px 0 #000;
}

.subtitle.shadow {
  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.25);
}
</style>