// src/media/AudioManager.ts

import { Audio } from 'expo-av';
import { EventEmitter } from 'events';
import { Vector3 } from 'three';

export interface AudioSource {
  id: string;
  sound: Audio.Sound;
  position: Vector3;
  maxDistance?: number;
  refDistance?: number;
  rolloffFactor?: number;
}

export interface AudioOptions {
  maxDistance?: number; // Distance at which the sound is completely inaudible
  refDistance?: number; // Distance at which the sound starts to attenuate
  rolloffFactor?: number; // How quickly the sound attenuates with distance
  loop?: boolean;
  volume?: number;
}

export class AudioManager extends EventEmitter {
  private audioSources: Map<string, AudioSource> = new Map();
  private listenerPosition: Vector3 = new Vector3();
  private isInitialized: boolean = false;

  constructor() {
    super();
  }

  /**
   * Initializes the audio system
   */
  public async initialize(): Promise<void> {
    try {
      await Audio.setAudioModeAsync({
        playsInSilentModeIOS: true,
        staysActiveInBackground: true,
        shouldDuckAndroid: true
      });
      this.isInitialized = true;
      this.emit('initialized');
    } catch (error) {
      this.emit('error', error);
      throw error;
    }
  }

  /**
   * Loads and creates a spatial audio source
   * @param id Unique identifier for the audio source
   * @param uri URI of the audio file to load
   * @param position Initial 3D position of the audio source
   * @param options Audio options
   */
  public async createAudioSource(
    id: string,
    uri: string,
    position: Vector3,
    options: AudioOptions = {}
  ): Promise<void> {
    if (!this.isInitialized) {
      throw new Error('AudioManager must be initialized first');
    }

    try {
      const { sound } = await Audio.Sound.createAsync(
        { uri },
        {
          shouldPlay: false,
          volume: options.volume ?? 1.0,
          isLooping: options.loop ?? false,
        },
        undefined,
        true
      );

      const audioSource: AudioSource = {
        id,
        sound,
        position: position.clone(),
        maxDistance: options.maxDistance ?? 50,
        refDistance: options.refDistance ?? 1,
        rolloffFactor: options.rolloffFactor ?? 1
      };

      this.audioSources.set(id, audioSource);
      this.updateAudioSourceVolume(audioSource);
      this.emit('audio-source-created', id);
    } catch (error) {
      this.emit('error', error);
      throw error;
    }
  }

  /**
   * Updates the position of an audio source
   * @param id Audio source identifier
   * @param position New position
   */
  public updateSourcePosition(id: string, position: Vector3): void {
    const source = this.audioSources.get(id);
    if (!source) {
      throw new Error(`Audio source ${id} not found`);
    }

    source.position.copy(position);
    this.updateAudioSourceVolume(source);
  }

  /**
   * Updates the listener's position (usually the camera/user position)
   * @param position New listener position
   */
  public updateListenerPosition(position: Vector3): void {
    this.listenerPosition.copy(position);
    this.audioSources.forEach(source => this.updateAudioSourceVolume(source));
  }

  /**
   * Plays an audio source
   * @param id Audio source identifier
   */
  public async playSource(id: string): Promise<void> {
    const source = this.audioSources.get(id);
    if (!source) {
      throw new Error(`Audio source ${id} not found`);
    }

    try {
      await source.sound.playAsync();
    } catch (error) {
      this.emit('error', error);
      throw error;
    }
  }

  /**
   * Stops an audio source
   * @param id Audio source identifier
   */
  public async stopSource(id: string): Promise<void> {
    const source = this.audioSources.get(id);
    if (!source) {
      throw new Error(`Audio source ${id} not found`);
    }

    try {
      await source.sound.stopAsync();
    } catch (error) {
      this.emit('error', error);
      throw error;
    }
  }

  /**
   * Removes an audio source and cleans up its resources
   * @param id Audio source identifier
   */
  public async removeSource(id: string): Promise<void> {
    const source = this.audioSources.get(id);
    if (!source) {
      return;
    }

    try {
      await source.sound.unloadAsync();
      this.audioSources.delete(id);
      this.emit('audio-source-removed', id);
    } catch (error) {
      this.emit('error', error);
      throw error;
    }
  }

  /**
   * Updates the volume of an audio source based on its distance from the listener
   * @param source Audio source to update
   */
  private updateAudioSourceVolume(source: AudioSource): void {
    const distance = this.listenerPosition.distanceTo(source.position);
    const refDistance = source.refDistance ?? 1;
    const maxDistance = source.maxDistance ?? 50;
    const rolloffFactor = source.rolloffFactor ?? 1;

    // Calculate volume using inverse distance model with rolloff
    let volume = 1.0;
    if (distance > refDistance) {
      volume = refDistance / (refDistance + rolloffFactor * (distance - refDistance));
    }
    if (distance >= maxDistance) {
      volume = 0;
    }

    source.sound.setVolumeAsync(volume);
  }

  /**
   * Cleans up all audio resources
   */
  public async cleanup(): Promise<void> {
    const promises = Array.from(this.audioSources.keys()).map(id => this.removeSource(id));
    await Promise.all(promises);
    this.emit('cleanup-complete');
  }
}