const DEFAULT_SAMPLE_RATE = 44100;

export class AudioService {
  private context: AudioContext | null = null;
  private currentSource: AudioBufferSourceNode | null = null;
  private gainNode: GainNode | null = null;
  private startTime = 0;
  private currentOffset = 0;
  private isPlaying = false;
  private onPlaybackUpdate: ((position: number) => void) | null = null;
  private animationFrameId: number | null = null;

  private getContext(): AudioContext {
    if (!this.context) {
      this.context = new AudioContext({ sampleRate: DEFAULT_SAMPLE_RATE });
      this.gainNode = this.context.createGain();
      this.gainNode.connect(this.context.destination);
    }
    return this.context;
  }

  getSampleRate(): number {
    return DEFAULT_SAMPLE_RATE;
  }

  async initialize(): Promise<void> {
    const ctx = this.getContext();
    if (ctx.state === 'suspended') {
      await ctx.resume();
    }
  }

  setVolume(volume: number): void {
    if (this.gainNode) {
      this.gainNode.gain.value = Math.max(0, Math.min(1, volume));
    }
  }

  setPlaybackUpdateCallback(callback: ((position: number) => void) | null): void {
    this.onPlaybackUpdate = callback;
  }

  createAudioBuffer(stereoData: [Float32Array, Float32Array]): AudioBuffer {
    const ctx = this.getContext();
    const [leftChannel, rightChannel] = stereoData;
    const buffer = ctx.createBuffer(2, leftChannel.length, DEFAULT_SAMPLE_RATE);
    buffer.copyToChannel(new Float32Array(leftChannel), 0);
    buffer.copyToChannel(new Float32Array(rightChannel), 1);
    return buffer;
  }

  play(buffer: AudioBuffer, offset: number = 0): void {
    this.stop();

    const ctx = this.getContext();
    const source = ctx.createBufferSource();
    source.buffer = buffer;
    source.connect(this.gainNode!);

    this.startTime = ctx.currentTime;
    this.currentOffset = offset;
    this.isPlaying = true;
    this.currentSource = source;

    source.onended = () => {
      // Guard against ghost callbacks from old sources
      if (source !== this.currentSource) return;

      this.isPlaying = false;
      if (this.onPlaybackUpdate) {
        this.onPlaybackUpdate(-1);
      }
      if (this.animationFrameId !== null) {
        cancelAnimationFrame(this.animationFrameId);
        this.animationFrameId = null;
      }
    };

    source.start(0, offset);
    this.updatePlaybackPosition();
  }

  private updatePlaybackPosition(): void {
    if (!this.isPlaying || !this.context || !this.onPlaybackUpdate) {
      return;
    }

    const elapsed = this.context.currentTime - this.startTime;
    this.onPlaybackUpdate(elapsed + this.currentOffset);

    this.animationFrameId = requestAnimationFrame(() => this.updatePlaybackPosition());
  }

  stop(): void {
    if (this.currentSource) {
      try {
        this.currentSource.stop();
      } catch {
        // Already stopped
      }
      this.currentSource = null;
    }
    this.isPlaying = false;
    this.currentOffset = 0;
    if (this.animationFrameId !== null) {
      cancelAnimationFrame(this.animationFrameId);
      this.animationFrameId = null;
    }
    if (this.onPlaybackUpdate) {
      this.onPlaybackUpdate(-1);
    }
  }
}
