// Audio / video playback over the host. Ported from worker/src/MediaPlayer.ts, with the listener
// boilerplate folded into the shared Emitter. VideoPlayer.texture (a 3D Texture) is wired in Phase 4
// when the Texture type exists — see gl/.
//
// The host no longer routes end-of-playback events back by id through a Registry: each player hands
// the host a `_finished` callback at creation time. The host holds the JS closure directly — valid
// on every backend (web JS, Android quickjs, iOS JavaScriptCore) — and invokes it when a play-through
// completes, so there is no id table to keep in sync.

import { Emitter } from "../core/events"
import { Texture } from "../gl/Texture"

type MediaEvents = {
  /** Fired at each loop boundary while `loop` is true. */
  loopReached: () => void
  /** Fired once when playback reaches the end while `loop` is false. */
  completed: () => void
}

abstract class MediaPlayer extends Emitter<MediaEvents> {
  protected _id = -1
  protected _src = ""
  private _playing = false
  private _loop = false
  private _volume = 1
  private _disposed = false

  // Every host-touching operation runs through this so a disposed player can never address a freed
  // (and possibly reused) host slot.
  private _assertAlive(): void {
    if (this._disposed) throw new Error("MediaPlayer has been disposed")
  }

  play(): void { this.playing = true }
  pause(): void { this.playing = false }

  get playing(): boolean { return this._playing }
  set playing(value: boolean) {
    this._assertAlive()
    this._playing = value
    _creatorUtils.updateMediaPlayerPlaying(this._id, value)
  }

  get volume(): number { return this._volume }
  set volume(value: number) {
    this._assertAlive()
    this._volume = value
    _creatorUtils.updateMediaPlayerVolume(this._id, value)
  }

  get loop(): boolean { return this._loop }
  set loop(value: boolean) {
    this._assertAlive()
    this._loop = value
    _creatorUtils.updateMediaPlayerLoop(this._id, value)
  }

  /** Current playback position, in seconds. Settable to seek. */
  get time(): number { this._assertAlive(); return _creatorUtils.getMediaPlayerTime(this._id) }
  set time(value: number) { this._assertAlive(); _creatorUtils.setMediaPlayerTime(this._id, value) }

  /** Total media length in seconds (0 until metadata has loaded). */
  get duration(): number { this._assertAlive(); return _creatorUtils.getMediaPlayerDuration(this._id) }

  /** True once {@link dispose} has been called; the player is unusable afterwards. */
  get disposed(): boolean { return this._disposed }

  /**
   * Release the underlying host media resource (decoder, network, buffers) immediately and detach
   * all listeners. Idempotent. Any further use of the player throws.
   */
  dispose(): void {
    if (this._disposed) return
    this._disposed = true
    this._playing = false
    this.clearListeners()
    _creatorUtils.removeMediaPlayer(this._id)
  }

  /** @internal — the host calls this when a play-through finishes. */
  protected _finished(): void {
    if (this._disposed) return
    if (this._loop) {
      this.dispatch("loopReached")
    } else {
      this._playing = false
      this.dispatch("completed")
    }
  }
}

export class AudioPlayer extends MediaPlayer {
  constructor(src?: string) {
    super()
    this._src = src ?? ""
    this._id = _creatorUtils.createAudioPlayer(this._src, () => this._finished())
  }
}

export class VideoPlayer extends MediaPlayer {
  constructor(src?: string) {
    super()
    this._src = src ?? ""
    this._id = _creatorUtils.createVideoPlayer(this._src, () => this._finished())
  }

  /** A 3D Texture that samples this video (e.g. for a Material.video()). */
  get texture(): Texture {
    return new Texture(0, 0, _creator.createMediaPlayerTexture(this._id))
  }
}
