import { loadSDK, PartialRequired, type Player, type PlayerPlugin, type Source } from '@oplayer/core'
//@ts-ignore
import type shaka from 'shaka-player'

export type Matcher = (source: Source) => boolean

export interface ShakaPluginOptions {
  library?: string

  matcher?: Matcher
  /**
   *shaka config
   * @type {object}
   */
  config?: any

  requestFilter?: shaka.extern.RequestFilter

  /**
   *default: 'menu'
   */
  qualityControlType?: 'menu' | 'setting'

  qualityControl?: boolean

  audioControl?: boolean

  textControl?: boolean
}

const defaultMatcher: Matcher = (source) => {
  if (source.format && ['m3u8', 'mpd', 'shaka'].includes(source.format)) {
    return true
  }
  return (
    (source.format === 'auto' || typeof source.format === 'undefined') &&
    /(m3u8|mpd|shaka)(#|\?|$)/i.test(source.src)
  )
}

class ShakaPlugin implements PlayerPlugin {
  key = 'shaka'
  name = 'oplayer-plugin-shaka'
  version = __VERSION__

  static library: typeof shaka

  player!: Player

  instance?: shaka.Player & { eventManager: shaka.util.EventManager }

  options: PartialRequired<ShakaPluginOptions, 'matcher'> = {
    matcher: defaultMatcher,
    qualityControl: true,
    audioControl: true,
    textControl: true,
    qualityControlType: 'menu'
  }

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

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

  async load(player: Player, source: Source) {
    if (!this.options.matcher(source)) return false

    const { library, config, requestFilter, qualityControl, audioControl, textControl, qualityControlType } =
      this.options

    if (!ShakaPlugin.library) {
      ShakaPlugin.library =
        (globalThis as any).shaka ||
        (library
          ? await loadSDK(library, 'shaka')
          : (await import('shaka-player/dist/shaka-player.compiled')).default)

      ShakaPlugin.library.polyfill.installAll()
    }

    const ShakaPlayer = ShakaPlugin.library.Player

    if (!ShakaPlayer.isBrowserSupported()) return false

    this.instance = new ShakaPlayer() as unknown as shaka.Player & {
      eventManager: shaka.util.EventManager
      timer: any
    }
    await this.instance.attach(player.$video)

    if (config) {
      this.instance.configure(config)
    }

    if (requestFilter) {
      this.instance.getNetworkingEngine()?.registerRequestFilter(requestFilter)
    }

    const eventManager = (this.instance.eventManager = new ShakaPlugin.library.util.EventManager())

    eventManager.listen(this.instance, 'loading', (event) => {
      player.emit('loading', event)
    })

    eventManager.listen(this.instance, 'loaded', (event) => {
      player.emit('loaded', event)
    })

    eventManager.listen(this.instance, 'error', (event) => {
      player.emit('error', { pluginName: ShakaPlugin.name, ...event })
    })

    eventManager.listenOnce(player.$video, 'seeking', () => {
      // ignore first seeking ?
      setTimeout(() => {
        player.emit('seeked')
      })
    })

    try {
      await this.instance.load(source.src)
    } catch (error: any) {
      player.emit('error', { pluginName: ShakaPlugin.name, ...error })
    }

    if (player.options.isLive) {
      eventManager.listenOnce(player.$video, 'loadedmetadata', () => {
        player.$video.currentTime = this.seekRange.end
      })

      const button = player.$root.querySelector('[aria-label="time"')?.parentElement
      const dot = button?.firstElementChild as HTMLSpanElement | undefined

      if (button && dot) {
        eventManager.listen(button, 'click', () => {
          player.$video.currentTime = this.seekRange.end
        })

        const backText = player.locales.get('Back to Live')
        const updateIsLive = () => {
          const timeBehindLiveEdge = this.seekRange.end - player.$video.currentTime
          // var streamPosition = Date.now() / 1000 - timeBehindLiveEdge

          if (timeBehindLiveEdge > 5) {
            dot.style.backgroundColor = '#ccc'
            button.ariaLabel = backText
          } else {
            dot.style.cssText = ''
            button.removeAttribute('aria-label')
          }
        }

        this.instance.eventManager.listen(player.$video, 'timeupdate', updateIsLive)
      }

      //TODO: revert
      // Object.defineProperty(player, 'duration', {
      //   get: () => {
      //     if (this.instance) return this._duration
      //     return player.$video.duration
      //   }
      // })
      Object.defineProperty(player.$video, 'duration', {
        get: () => {
          if (this.instance) return this._duration
          return player.$video.duration
        }
      })
      Object.defineProperty(player, 'currentTime', {
        get: () => {
          if (this.instance) return this.getCurrentTime()
          else return player.$video.currentTime
        }
      })
      Object.defineProperty(player, 'seek', {
        value: (v: number) => {
          if (this.instance) player.$video.currentTime = this.seekRange.start + v
          else player.$video.currentTime = v
        }
      })
    }

    if (player.context.ui) {
      if (qualityControl) {
        this.setupQuality(player, this.instance, qualityControlType)
        // eventManager.listen(this.instance, 'variantchanged', () => {})
        // eventManager.listen(this.instance, 'trackschanged', () => {})
      }

      if (audioControl) {
        this.setupAudioSelection(player, this.instance)
        // eventManager.listen(this.instance, 'audiotrackschanged', () => {})
      }

      if (textControl) {
        this.setupTextSelection(player, this.instance)
        // eventManager.listen(this.instance, 'texttrackvisibility', () => {})
        // eventManager.listen(this.instance, 'textchanged', (e) => {})
        // eventManager.listen(this.instance, 'trackschanged', () => {})
      }
    }

    return this
  }

  getCurrentTime() {
    if (!this.instance) return 0
    const mediaElement = this.instance.getMediaElement()
    return mediaElement ? mediaElement.currentTime - this.seekRange.start : 0
  }

  get seekRange() {
    if (!this.instance) return { start: 0, end: 0 }
    return this.instance.seekRange()
  }

  get _duration() {
    if (!this.instance) return 0

    return this.seekRange.end - this.seekRange.start
  }

  async destroy() {
    ;['Quality', 'Language', 'Subtitle'].forEach((it) =>
      this.player.context.ui.setting.unregister(`${ShakaPlugin.name}-${it}`)
    )
    this.player.context.ui.menu.unregister(`${ShakaPlugin.name}-${'Quality'}`)
    this.instance?.eventManager.removeAll()
    await this.instance?.unload()
    await this.instance?.destroy()
    this.instance = undefined
  }

  setupQuality = (
    player: Player,
    instance: shaka.Player,
    qualityControlType: ShakaPluginOptions['qualityControlType']
  ) => {
    // https://github.com/shaka-project/shaka-player/blob/1f336dd319ad23a6feb785f2ab05a8bc5fc8e2a2/ui/resolution_selection.js#L90
    let tracks: shaka.extern.Track[] = []

    if (instance.getLoadMode() != ShakaPlugin.library.Player.LoadMode.SRC_EQUALS) {
      tracks = instance.getVariantTracks()
    }

    const selectedTrack = tracks.find((track) => track.active)

    if (selectedTrack) {
      tracks = tracks.filter((track) => {
        if (track.language != selectedTrack.language) {
          return false
        }
        if (
          track.channelsCount &&
          selectedTrack.channelsCount &&
          track.channelsCount != selectedTrack.channelsCount
        ) {
          return false
        }
        if (JSON.stringify(track.audioRoles) != JSON.stringify(selectedTrack.audioRoles)) {
          return false
        }
        return true
      })
    }

    if (instance.isAudioOnly()) {
      tracks = tracks.filter((track, idx) => {
        return tracks.findIndex((t) => t.bandwidth == track.bandwidth) == idx
      })
    } else {
      const audiosIds = [...new Set(tracks.map((t) => t.audioId))].filter((t) => t !== null)

      if (audiosIds.length > 1) {
        tracks = tracks.filter((track, idx) => {
          const otherIdx = tracks.findIndex((t) => {
            const ret =
              t.height == track.height &&
              t.videoBandwidth == track.videoBandwidth &&
              t.frameRate == track.frameRate &&
              t.hdr == track.hdr &&
              t.videoLayout == track.videoLayout
            return ret
          })
          return otherIdx == idx
        })
      } else {
        tracks = tracks.filter((track, idx) => {
          const otherIdx = tracks.findIndex((t) => {
            const ret =
              t.height == track.height &&
              t.bandwidth == track.bandwidth &&
              t.frameRate == track.frameRate &&
              t.hdr == track.hdr &&
              t.videoLayout == track.videoLayout

            return ret
          })
          return otherIdx == idx
        })
      }
    }

    if (!(tracks.length > 1)) return

    if (instance.isAudioOnly()) {
      tracks.sort((t1, t2) => {
        return t2.bandwidth - t1.bandwidth
      })
    } else {
      tracks.sort((t1, t2) => {
        if (t2.height == t1.height || t1.height == null || t2.height == null) {
          return t2.bandwidth - t1.bandwidth
        }
        return t2.height - t1.height
      })
    }

    const abrEnabled = instance.getConfiguration().abr.enabled

    const settings = tracks.map((t) => {
      return {
        name:
          !instance.isAudioOnly() && t.height && t.width
            ? this.getResolutionLabel_(t, tracks)
            : t.bandwidth
              ? Math.round(t.bandwidth / 1000) + ' kbits/s'
              : 'Unknown',
        default: !abrEnabled && t == selectedTrack,
        value: t
      }
    })

    const ctrl = qualityControlType == 'menu' ? player.context.ui.menu : player.context.ui.setting
    const autoText = player.locales.get('Auto')

    ctrl.unregister(`${ShakaPlugin.name}-Quality`)
    ctrl.register({
      icon: qualityControlType == 'setting' ? player.context.ui.icons.quality : undefined,
      name:
        qualityControlType == 'setting'
          ? 'Quality'
          : !abrEnabled && selectedTrack
            ? this.getResolutionLabel_(selectedTrack, [])
            : autoText,
      type: 'selector',
      key: `${ShakaPlugin.name}-Quality`,
      children: [
        {
          name: player.locales.get('Auto'),
          default: abrEnabled,
          value: -1
        }
      ].concat(settings as any),
      onChange: ({ value }: { value: shaka.extern.Track | -1 }, dom: HTMLButtonElement) => {
        const isAuto = value == -1
        instance.configure({ abr: { enabled: isAuto } })

        if (!isAuto) {
          dom.textContent = this.getResolutionLabel_(value, [])
          instance.selectVariantTrack(value, /* clearBuffer */ true)
        } else {
          dom.textContent = autoText
          // setupQuality(player, instance)
        }
      }
    })
  }

  setupAudioSelection = (player: Player, instance: shaka.Player) => {
    const audioTracks = instance.getAudioTracks()

    if (!(audioTracks.length > 1)) return

    const levels = audioTracks
      .sort((a, b) => {
        return a.language.localeCompare(b.language)
      })
      .map((level) => {
        return {
          //@ts-expect-error
          name: `${level.language} ${ShakaPlugin.library.util.MimeUtils.getNormalizedCodec?.(level.codecs) || level.codecs}`,
          default: level.active,
          value: level
        }
      })
    this.settingUpdater({
      player,
      name: 'Language',
      icon: player.context.ui.icons.lang,
      settings: levels,
      onChange({ value }) {
        instance.selectAudioTrack(value)
      }
    })
  }

  setupTextSelection = (player: Player, instance: shaka.Player) => {
    const tracks = instance.getTextTracks()

    if (!(tracks.length > 1)) return

    const isTextTrackVisible = instance.isTextTrackVisible()

    const levels = [
      {
        name: player.locales.get('Off'),
        default: !isTextTrackVisible,
        value: -1
      }
    ].concat(
      tracks
        .sort((a, b) => {
          return a.language.localeCompare(b.language)
        })
        .map((level) => {
          return {
            name: level.language,
            default: isTextTrackVisible && level.active,
            value: level
          }
        }) as any
    )

    this.settingUpdater({
      player,
      name: 'Subtitle',
      icon: player.context.ui.icons.lang,
      settings: levels,
      onChange({ value }) {
        if (value != -1) instance.selectTextTrack(value)
        instance.setTextTrackVisibility(value != -1)
      }
    })
  }

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

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

  getResolutionLabel_(track: shaka.extern.Track, tracks: shaka.extern.Track[]) {
    const trackHeight = track.height || 0
    const trackWidth = track.width || 0
    let height = trackHeight
    const aspectRatio = trackWidth / trackHeight
    if (aspectRatio > 16 / 9) {
      height = Math.round((trackWidth * 9) / 16)
    }
    let text = height + 'p'
    if (height == 2160) {
      text = '4K'
    }
    const frameRates = new Set()
    for (const item of tracks) {
      if (item.frameRate) {
        frameRates.add(Math.round(item.frameRate))
      }
    }
    if (frameRates.size > 1) {
      const frameRate = track.frameRate
      if (frameRate && (frameRate >= 50 || frameRate <= 20)) {
        text += Math.round(frameRate)
      }
    }
    if (track.hdr == 'PQ' || track.hdr == 'HLG') {
      text += ' (HDR)'
    }
    if (track.videoLayout == 'CH-STEREO') {
      text += ' (3D)'
    }

    const hasDuplicateResolution = tracks.some((otherTrack) => {
      return otherTrack != track && otherTrack.height == track.height
    })

    if (hasDuplicateResolution && this.options.qualityControlType == 'setting') {
      const hasDuplicateBandwidth = tracks.some((otherTrack) => {
        return (
          otherTrack != track &&
          otherTrack.height == track.height &&
          (otherTrack.videoBandwidth || otherTrack.bandwidth) == (track.videoBandwidth || track.bandwidth)
        )
      })
      if (!hasDuplicateBandwidth) {
        const bandwidth = track.videoBandwidth || track.bandwidth
        text += ' (' + Math.round(bandwidth / 1000) + ' kbits/s)'
      }
    }
    return text
  }
}

export default function create(options?: ShakaPluginOptions) {
  return new ShakaPlugin(options)
}
