import { loadSDK, type Player, type PlayerPlugin, type RequiredPartial, type Source } from '@oplayer/core'
import type {
  BitrateInfo,
  MediaPlayerClass,
  MediaPlayerSettingClass,
  ProtectionDataSet,
  QualityChangeRenderedEvent
} from 'dashjs'

const PLUGIN_NAME = 'oplayer-plugin-dash'

export type Matcher = (video: HTMLVideoElement, source: Source) => boolean

export interface DashPluginOptions {
  library?: string
  matcher?: Matcher

  /**
   * default auto
   */
  defaultQuality?: (levels: BitrateInfo[]) => number

  /**
   * default browser language
   */
  defaultAudio?: (tracks: dashjs.MediaInfo[]) => number

  /**
   * default browser language
   */
  defaultSubtitle?: (tracks: dashjs.MediaInfo[]) => number

  /**
   * config for dashjs
   *
   * @type {MediaPlayerSettingClass}
   */
  config?: MediaPlayerSettingClass

  drm?: ProtectionDataSet

  // qualityLabelBuilder?: (instance: MediaPlayerClass) => {
  //   name: string
  //   default: boolean
  //   value: any
  // }[]

  /**
   * enable quality control for the stream, does not apply to the native (iPhone) clients.
   * @default: true
   */
  qualityControl?: boolean
  /**
   *  control how the stream quality is switched. default: immediate
   *  @value immediate: Trigger an immediate quality level switch to new quality level. This will abort the current fragment request if any, flush the whole buffer, and fetch fragment matching with current position and requested quality level.
   *  @value smooth: Trigger a quality level switch for next fragment. This could eventually flush already buffered next fragment.
   */
  qualitySwitch?: 'immediate' | 'smooth'
  /**
   * @default: false
   */
  withBitrate?: boolean
  /**
   * @default: true
   */
  audioControl?: boolean
  /**
   * @default: true
   */
  textControl?: boolean
}

const defaultMatcher: Matcher = (_, source) =>
  source.format === 'dash' ||
  source.format === 'mpd' ||
  ((source.format === 'auto' || typeof source.format === 'undefined') && /.mpd(#|\?|$)/i.test(source.src))

class DashPlugin implements PlayerPlugin {
  key = 'dash'
  name = PLUGIN_NAME
  version = __VERSION__

  static library: typeof import('dashjs')

  player!: Player

  instance?: MediaPlayerClass

  options: RequiredPartial<DashPluginOptions, 'config' | 'library' | 'drm'> = {
    textControl: true,
    audioControl: true,
    qualityControl: true,
    withBitrate: false,
    qualitySwitch: 'immediate',
    matcher: defaultMatcher,
    defaultQuality: () => -1,
    defaultAudio: () => -1,
    defaultSubtitle: () => -1
  }

  constructor(options?: DashPluginOptions) {
    Object.assign(this.options, options)
  }

  apply(player: Player) {
    this.player = player
    return this
  }

  async load({ $video }: Player, source: Source) {
    const { matcher, library } = this.options

    if (!matcher($video, source)) return false

    if (!DashPlugin.library) {
      DashPlugin.library =
        globalThis.dashjs || (library ? await loadSDK(library, 'dashjs') : (await import('dashjs')).default)
    }

    if (!DashPlugin.library.supportsMediaSource()) return false

    this.instance = DashPlugin.library.MediaPlayer().create()

    const { player, instance } = this
    const { drm, config } = this.options

    if (config) instance.updateSettings(config)
    if (drm) instance.setProtectionData(drm)
    instance.initialize($video, source.src, $video.autoplay)

    instance.on(DashPlugin.library.MediaPlayer.events.ERROR, function (event: any) {
      const err = event.event || event.error
      const message = event.event ? event.event.message || event.type : undefined
      player.emit('error', { pluginName: PLUGIN_NAME, message, ...err })
    })

    if (player.context.ui?.setting) {
      // @ts-ignore
      if (instance.getBitrateInfoListFor) {
        generateSetting(player, instance, this.options)
      } else {
        console.warn('https://github.com/shiyiya/oplayer/issues/155')
      }
    }

    return this
  }

  destroy() {
    if (this.instance) {
      const { player, instance } = this
      if (player.context.ui?.setting) removeSetting(player)
      instance.destroy()
    }
  }
}

function getSettingsByType(instance: MediaPlayerClass, type: 'video', withBitrate?: boolean) {
  const bitrateInfoList = instance.getBitrateInfoListFor(type)
  const isAuto = Boolean(instance.getSettings().streaming?.abr?.autoSwitchBitrate?.video)
  const videoQuality = instance.getQualityFor('video')
  if (bitrateInfoList.length > 1) {
    return bitrateInfoList
      .toSorted((a, b) => b.bitrate - a.bitrate)
      .map((it) => {
        let name = it.height + 'p'

        if (withBitrate) {
          const kb = it.bitrate / 1000
          const useMb = kb > 1000
          const number = useMb ? (kb / 1000).toFixed(2) : Math.floor(kb)
          name += ` (${number}${useMb ? 'm' : 'k'}bps)`
        }

        return {
          name,
          default: isAuto ? false : videoQuality == it.qualityIndex,
          value: it.qualityIndex
        }
      })
  }

  return []
}

const generateSetting = (player: Player, instance: MediaPlayerClass, options: DashPlugin['options']) => {
  instance.on(DashPlugin.library.MediaPlayer.events.STREAM_INITIALIZED, function () {
    if (options.qualityControl) {
      const quality = instance.getBitrateInfoListFor('video')
      if (quality.length < 2) return

      const defaultLevel = options.defaultQuality(quality)
      if (defaultLevel != -1) instance.setQualityFor('video', defaultLevel)

      settingUpdater({
        name: 'Quality',
        icon: player.context.ui.icons.quality,
        settings: () =>
          [
            {
              name: player.locales.get('Auto'),
              default: Boolean(instance.getSettings().streaming?.abr?.autoSwitchBitrate?.video),
              value: -1
            }
          ].concat(getSettingsByType(instance, 'video', options.withBitrate)),
        onChange({ value }) {
          instance.updateSettings({
            streaming: { abr: { autoSwitchBitrate: { video: value == -1 } } }
          })
          if (value != -1) {
            instance.setQualityFor('video', value, options.qualitySwitch == 'immediate')
          }
        }
      })

      instance.on(
        DashPlugin.library.MediaPlayer.events.QUALITY_CHANGE_RENDERED,
        function qualityMenuUpdater(data: QualityChangeRenderedEvent) {
          if (data.mediaType !== 'video' || !instance.getSettings().streaming?.abr?.autoSwitchBitrate?.video)
            return

          const height = instance.getBitrateInfoListFor('video')[data.newQuality]?.height
          const levelName = player.locales.get('Auto') + (height ? ` (${height}p)` : '')
          player.context.ui?.setting.updateLabel(`${PLUGIN_NAME}-Quality`, levelName)
        }
      )
    }

    if (options.audioControl) {
      const audioTracks = instance.getTracksFor('audio')
      if (audioTracks.length < 2) return

      let defaultAudio: number | undefined = options.defaultAudio(audioTracks)
      if (defaultAudio == -1) {
        defaultAudio = audioTracks.find(({ lang }) => {
          return lang === navigator.language || lang === navigator.language.split('-')[0]
        })?.id as unknown as number
      }

      if (defaultAudio != -1 && defaultAudio != undefined) {
        instance.setCurrentTrack(audioTracks.find((t) => (t.id as unknown as number) == defaultAudio)!)
      }

      const currentAudio = instance.getCurrentTrackFor('audio')

      settingUpdater({
        name: 'Language',
        icon: player.context.ui.icons.lang,
        settings() {
          return audioTracks.map((it) => ({
            name: it.lang || 'unknown',
            default: currentAudio?.index != null && currentAudio.index == it.index,
            value: it
          }))
        },
        onChange({ value }) {
          instance.setCurrentTrack(value)
        }
      })
    }

    if (options.textControl) {
      const textTracks = instance.getTracksFor('text')
      if (textTracks.length < 1) return

      let defaultSubtitle: number | undefined = options.defaultSubtitle(textTracks)
      if (defaultSubtitle == -1) {
        defaultSubtitle = textTracks.find(({ lang }) => {
          return lang === navigator.language || lang === navigator.language.split('-')[0]
        })?.id as unknown as number
      }

      if (defaultSubtitle != -1 && defaultSubtitle != undefined) {
        instance.enableText(true)
        instance.setTextTrack(defaultSubtitle)
      }

      const currentTrack = instance.getCurrentTrackFor('text')

      settingUpdater({
        name: 'Subtitle',
        icon: player.context.ui.icons.subtitle,
        settings() {
          return [
            {
              name: player.locales.get('Off'),
              default: !instance.isTextEnabled(),
              value: -1 as any
            }
          ].concat(
            textTracks.map((it) => ({
              name: it.lang || 'unknown',
              default: currentTrack?.index != null && currentTrack.index == it.index,
              value: it.index
            }))
          )
        },
        onChange({ value }) {
          instance.enableText(value != -1)
          if (value != -1) instance.setTextTrack(value)
        }
      })
    }
  })

  function settingUpdater(arg: {
    icon: string
    name: string
    settings: () => {
      name: string
      default: boolean
      value: any
    }[]
    onChange: (it: { value: any }) => void
  }) {
    const settings = arg.settings()
    const { name, icon, onChange } = arg

    player.context.ui.setting.unregister(`${PLUGIN_NAME}-${name}`)
    player.context.ui.setting.register({
      name: player.locales.get(name),
      icon,
      onChange,
      type: 'selector',
      key: `${PLUGIN_NAME}-${name}`,
      children: settings
    })
  }
}

const removeSetting = (player: Player) => {
  ;['Quality', 'Language', 'Subtitle'].forEach((it) =>
    player.context.ui.setting.unregister(`${PLUGIN_NAME}-${it}`)
  )
}

export default function create(options?: DashPluginOptions) {
  return new DashPlugin(options)
}
