Source: AudioPlayer.js

import { addSignals } from './Signals.js'

/**
 * Class representing an audio player with playback control capabilities.
 * Supports playing, pausing, resuming, and stopping audio files with volume control
 * and playback speed adjustment.
 */
class AudioPlayer {
  /**
   * Creates an instance of AudioPlayer.
   * Initializes the player with default settings and sets up signal handling for events.
   */
  constructor() {
    this.audio = null;
    this.isPlaying = false;
    this.isPaused = false;
    this.isMuted = false;
    this.previousVolume = 1.0;
    this.playStartTime = null;
    this.playDuration = 0;
    addSignals(AudioPlayer, 'started', 'ended');
  }

  /**
   * Plays an audio file with optional playback speed adjustment.
   * If audio is paused, it will resume playback instead of starting a new file.
   * 
   * @param {string} audioFile - The path or URL to the audio file.
   * @param {number} [speed=1.0] - Playback speed multiplier (1.0 is normal speed).
   * @returns {Promise<void>} Resolves when the audio playback completes.
   */
  async play(audioFile, speed = 1.0) {
    if (!this.isPlaying && !this.isPaused) {
      this.audio = new Audio(audioFile);
      this.audio.playbackRate = speed;
      this.audio.volume = this.previousVolume;
      this.isPlaying = true;
      this.isPaused = false;
      this.playStartTime = Date.now();
      this.playDuration = 0;

      // Setup play handler
      this.audio.onplay = () => {
        this.setMute(this.isMuted);
        this.emit('started');
      };

      // Setup ended handler
      this.audio.onended = () => {
        this.isPlaying = false;
        this.updatePlayDuration();
        this.emit('ended');
      };

      try {
        await this.audio.play();
        return new Promise((resolve) => {
          const originalOnEnded = this.audio.onended;
          this.audio.onended = () => {
            originalOnEnded.call(this);  // Call the original handler
            resolve();
          };
        });
      } catch (error) {
        console.error("Error playing audio:", error);
        this.isPlaying = false;
        throw error;
      }
    } else if (this.isPaused) {
      await this.continue();
    }
  }

  /**
   * Pauses the currently playing audio.
   * Updates play duration when pausing.
   */
  pause() {
    if (!this.isPaused && this.audio) {
      this.audio.pause();
      this.isPaused = true;
      this.updatePlayDuration();
    }
  }

  /**
   * Resumes playback of a paused audio file.
   * 
   * @returns {Promise<void>} Resolves when the resumed audio playback completes.
   */
  async continue() {
    if (this.isPaused && this.audio) {
      this.isPaused = false;
      this.playStartTime = Date.now();

      // Setup play handler
      this.audio.onplay = () => {
        this.setMute(this.isMuted);
        this.emit('started');
      };

      try {
        await this.audio.play();
        return new Promise((resolve) => {
          const originalOnEnded = this.audio.onended;
          this.audio.onended = () => {
            originalOnEnded.call(this);  // Call the original handler
            resolve();
          };
        });
      } catch (error) {
        console.error("Error continuing audio:", error);
        this.isPaused = true;
        throw error;
      }
    } else {
      console.log("No paused audio to continue.");
    }
  }

  /**
   * Stops the current audio playback and resets all player states.
   * Removes event listeners and updates final play duration.
   */
  stop() {
    if (this.audio) {
      this.audio.pause();
      this.audio.currentTime = 0;
      this.audio.onplay = null;   // Clean up play handler
      this.audio.onended = null;  // Clean up ended handler
      this.isPlaying = false;
      this.isPaused = false;
      this.updatePlayDuration();
    }
  }

  /**
   * Updates the total play duration based on the current session.
   * Called internally when playback is paused, stopped, or ends.
   * @private
   */
  updatePlayDuration() {
    if (this.playStartTime) {
      const now = Date.now();
      this.playDuration += now - this.playStartTime;
      this.playStartTime = null;
    }
  }

  /**
   * Returns the total play duration in milliseconds.
   * 
   * @returns {number} Total play duration in milliseconds.
   */
  getPlayDuration() {
    return this.playDuration;
  }

  /**
   * Sets the audio volume level.
   * 
   * @param {number} volume - Volume level between 0.0 and 1.0.
   */
  setVolume(volume) {
    if (this.audio) {
      if (volume >= 0 && volume <= 1) {
        this.audio.volume = volume;
        this.previousVolume = volume;
      } else {
        console.log("Volume must be between 0.0 and 1.0");
      }
    } else {
      console.log("No audio loaded.");
    }
  }

  /**
   * Creates a delay in the execution flow.
   * 
   * @param {number} ms - Number of milliseconds to wait.
   * @returns {Promise<void>} Resolves after the specified delay.
   */
  async silence(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  /**
   * Set the mute state of the audio player.
   * Stores the previous volume level when muting and restores it when unmuting.
   * @param {boolean} b Whether to mute the audio playback
   */
  setMute(b) {
    this.isMuted = b;
    if (this.audio) {
      if (!this.isMuted) {
        this.audio.volume = this.previousVolume;
      } else {
        this.previousVolume = this.audio.volume;
        this.audio.volume = 0;
      }
    }
  }

  /**
   * Emits an event of the specified type
   * @param {string} type - The event type to emit
   */
  emit(type) {
    if (this[`${type}Signal`]) {
      this[`${type}Signal`].emit();
    }
  }
}

export { AudioPlayer }