import type { CallAudioChunkMeta } from "@/core/net/voice-call-transport";

export class VoiceCallPlayer {
  private context: AudioContext | null = null;
  private sources = new Set<AudioBufferSourceNode>();
  private decodeChain: Promise<void> = Promise.resolve();
  private scheduledUntil = 0;
  private pendingDecodes = 0;
  private generation = 0;
  onPlaying: (() => void) | null = null;
  onIdle: (() => void) | null = null;
  onGap: ((milliseconds: number) => void) | null = null;
  onError: ((error: Error) => void) | null = null;

  get isPlaying(): boolean {
    return this.sources.size > 0 || this.pendingDecodes > 0;
  }

  async prepare(): Promise<void> {
    const context = this.getContext();
    if (context.state === "suspended") await context.resume();
  }

  enqueue(_meta: CallAudioChunkMeta, bytes: ArrayBuffer): void {
    const generation = this.generation;
    this.pendingDecodes += 1;
    this.decodeChain = this.decodeChain
      .then(async () => {
        const context = this.getContext();
        if (context.state === "suspended") await context.resume();
        const audioBuffer = await context.decodeAudioData(bytes.slice(0));
        if (generation !== this.generation) return;
        this.schedule(context, audioBuffer);
      })
      .catch((error: unknown) => {
        this.onError?.(error instanceof Error ? error : new Error(String(error)));
      })
      .finally(() => {
        this.pendingDecodes = Math.max(0, this.pendingDecodes - 1);
        this.notifyIdleIfNeeded();
      });
  }

  stop(): void {
    this.generation += 1;
    for (const source of this.sources) {
      source.onended = null;
      try {
        source.stop();
      } catch {
        // 已自然结束的 source 无需重复停止。
      }
      source.disconnect();
    }
    this.sources.clear();
    this.scheduledUntil = 0;
    this.onIdle?.();
  }

  dispose(): void {
    this.stop();
    const context = this.context;
    this.context = null;
    if (context && context.state !== "closed") void context.close();
  }

  private getContext(): AudioContext {
    if (!this.context || this.context.state === "closed") {
      this.context = new AudioContext({ latencyHint: "interactive" });
    }
    return this.context;
  }

  private schedule(context: AudioContext, buffer: AudioBuffer): void {
    const leadSeconds = 0.025;
    const earliestStart = context.currentTime + leadSeconds;
    const hadPreviousSegment = this.scheduledUntil > 0;
    const startAt = Math.max(earliestStart, this.scheduledUntil);
    if (hadPreviousSegment) {
      this.onGap?.(Math.max(0, (startAt - this.scheduledUntil) * 1000));
    }
    const source = context.createBufferSource();
    source.buffer = buffer;
    source.connect(context.destination);
    this.sources.add(source);
    this.scheduledUntil = startAt + buffer.duration;
    source.onended = () => {
      source.disconnect();
      this.sources.delete(source);
      this.notifyIdleIfNeeded();
    };
    source.start(startAt);
    this.onPlaying?.();
  }

  private notifyIdleIfNeeded(): void {
    if (this.sources.size || this.pendingDecodes) return;
    this.scheduledUntil = 0;
    this.onIdle?.();
  }
}
