export type TransportClockSnapshot = {
  time: number;
  playing: boolean;
  rate: number;
  duration: number;
  source: "monotonic" | "audio";
};

export type AudioClockSource =
  | {
      el: HTMLMediaElement;
      compositionStart: number;
      mediaStart: number;
    }
  | {
      currentTimeSeconds: number;
    };

export class TransportClock {
  private _baseTime = 0;
  private _playStartMs: number | null = null;
  private _rate = 1;
  private _duration = Infinity;
  private _nowMs: () => number;
  private _audioSource: AudioClockSource | null = null;

  constructor(opts?: {
    initialTime?: number;
    rate?: number;
    duration?: number;
    nowMs?: () => number;
  }) {
    this._baseTime = opts?.initialTime ?? 0;
    this._rate = opts?.rate ?? 1;
    this._duration = opts?.duration ?? Infinity;
    this._nowMs = opts?.nowMs ?? (() => performance.now());
  }

  now(): number {
    if (this._playStartMs === null) return this._baseTime;

    // Audio-master: when an audio source is attached, derive time
    // from it. Drift is impossible because audio IS the clock.
    if (this._audioSource) {
      let audioTime: number | null = null;
      if ("currentTimeSeconds" in this._audioSource) {
        audioTime = this._audioSource.currentTimeSeconds;
      } else {
        const { el, compositionStart, mediaStart } = this._audioSource;
        if (!el.paused && Number.isFinite(el.currentTime)) {
          audioTime =
            ((el.currentTime - mediaStart) / (el.playbackRate > 0 ? el.playbackRate : 1)) *
              this._rate +
            compositionStart;
        }
      }
      if (audioTime !== null) {
        if (Number.isFinite(this._duration) && audioTime >= this._duration) {
          return this._duration;
        }
        return Math.max(0, audioTime);
      }
    }

    // Monotonic fallback
    const elapsed = (this._nowMs() - this._playStartMs) / 1000;
    const t = this._baseTime + elapsed * this._rate;
    if (Number.isFinite(this._duration) && t >= this._duration) {
      return this._duration;
    }
    return Math.max(0, t);
  }

  play(): boolean {
    if (this._playStartMs !== null) return false;
    if (Number.isFinite(this._duration) && this._baseTime >= this._duration) return false;
    this._playStartMs = this._nowMs();
    return true;
  }

  pause(): boolean {
    if (this._playStartMs === null) return false;
    this._baseTime = this.now();
    this._playStartMs = null;
    return true;
  }

  seek(timeSeconds: number): void {
    const clamped = Number.isFinite(this._duration)
      ? Math.max(0, Math.min(timeSeconds, this._duration))
      : Math.max(0, timeSeconds);
    this._baseTime = clamped;
    if (this._playStartMs !== null) {
      this._playStartMs = this._nowMs();
    }
  }

  isPlaying(): boolean {
    return this._playStartMs !== null;
  }

  setRate(rate: number): void {
    const safe = Number.isFinite(rate) && rate > 0 ? Math.max(0.1, Math.min(5, rate)) : 1;
    if (this._playStartMs !== null) {
      this._baseTime = this.now();
      this._playStartMs = this._nowMs();
    }
    this._rate = safe;
  }

  getRate(): number {
    return this._rate;
  }

  setDuration(duration: number): void {
    this._duration = Number.isFinite(duration) && duration > 0 ? duration : Infinity;
    if (this._baseTime > this._duration) {
      this._baseTime = this._duration;
    }
  }

  getDuration(): number {
    return this._duration;
  }

  attachAudioSource(source: AudioClockSource): void {
    this._audioSource = source;
  }

  detachAudioSource(): void {
    if (this._audioSource && this._playStartMs !== null) {
      this._baseTime = this.now();
      this._playStartMs = this._nowMs();
    }
    this._audioSource = null;
  }

  hasAudioSource(): boolean {
    return this._audioSource !== null;
  }

  getSource(): "monotonic" | "audio" {
    if (this._audioSource && this._playStartMs !== null) {
      if ("currentTimeSeconds" in this._audioSource) return "audio";
      const { el } = this._audioSource;
      if (!el.paused && Number.isFinite(el.currentTime)) return "audio";
    }
    return "monotonic";
  }

  snapshot(): TransportClockSnapshot {
    return {
      time: this.now(),
      playing: this.isPlaying(),
      rate: this._rate,
      duration: this._duration,
      source: this.getSource(),
    };
  }

  reachedEnd(): boolean {
    return Number.isFinite(this._duration) && this.now() >= this._duration;
  }
}
