import {
  AudioFrame,
  AudioMixer,
  AudioSource,
  LocalAudioTrack,
  TrackPublishOptions
} from "@livekit/rtc-node";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { audioFramesFromFile, loopAudioFramesFromFile } from "../audio.js";
import { log } from "../log.js";
import { Future, Task, cancelAndWait } from "../utils.js";
import { AgentSessionEventTypes } from "./events.js";
const TASK_TIMEOUT_MS = 500;
var BuiltinAudioClip = /* @__PURE__ */ ((BuiltinAudioClip2) => {
  BuiltinAudioClip2["HOLD_MUSIC"] = "hold_music.ogg";
  BuiltinAudioClip2["OFFICE_AMBIENCE"] = "office-ambience.ogg";
  BuiltinAudioClip2["KEYBOARD_TYPING"] = "keyboard-typing.ogg";
  BuiltinAudioClip2["KEYBOARD_TYPING2"] = "keyboard-typing2.ogg";
  return BuiltinAudioClip2;
})(BuiltinAudioClip || {});
function isBuiltinAudioClip(source) {
  return typeof source === "string" && Object.values(BuiltinAudioClip).includes(source);
}
function getBuiltinAudioPath(clip) {
  const resourcesPath = join(dirname(fileURLToPath(import.meta.url)), "../../resources");
  return join(resourcesPath, clip);
}
const AUDIO_SOURCE_BUFFER_MS = 400;
const STREAM_TIMEOUT_MS = 2e3;
const BACKGROUND_AUDIO_TRACK_NAME = "background_audio";
class PlayHandle {
  doneFuture = new Future();
  stopFuture = new Future();
  done() {
    return this.doneFuture.done;
  }
  stop() {
    if (this.done()) return;
    if (!this.stopFuture.done) {
      this.stopFuture.resolve();
    }
    this._markPlayoutDone();
  }
  async waitForPlayout() {
    return this.doneFuture.await;
  }
  _markPlayoutDone() {
    if (!this.doneFuture.done) {
      this.doneFuture.resolve();
    }
  }
}
class BackgroundAudioPlayer {
  ambientSound;
  thinkingSound;
  streamTimeoutMs;
  playTasks = [];
  audioSource = new AudioSource(48e3, 1, AUDIO_SOURCE_BUFFER_MS);
  audioMixer;
  mixerTask;
  room;
  agentSession;
  publication;
  trackPublishOptions;
  ambientHandle;
  thinkingHandle;
  closed = true;
  // TODO (Brian): add lock
  #logger = log();
  constructor(options) {
    const { ambientSound, thinkingSound, streamTimeoutMs = STREAM_TIMEOUT_MS } = options || {};
    this.ambientSound = ambientSound;
    this.thinkingSound = thinkingSound;
    this.streamTimeoutMs = streamTimeoutMs;
    this.audioMixer = new AudioMixer(48e3, 1, {
      blocksize: 4800,
      // 100ms at 48kHz
      capacity: 1,
      streamTimeoutMs: this.streamTimeoutMs
    });
  }
  /**
   * Select a sound from a list of background sound based on probability weights
   * Return undefined if no sound is selected (when sum of probabilities < 1.0).
   */
  selectSoundFromList(sounds) {
    const totalProbability = sounds.reduce((sum, sound) => sum + (sound.probability ?? 1), 0);
    if (totalProbability <= 0) {
      return void 0;
    }
    if (totalProbability < 1 && Math.random() > totalProbability) {
      return void 0;
    }
    const normalizeFactor = totalProbability <= 1 ? 1 : totalProbability;
    const r = Math.random() * Math.min(totalProbability, 1);
    let cumulative = 0;
    for (const sound of sounds) {
      const prob = sound.probability ?? 1;
      if (prob <= 0) {
        continue;
      }
      const normProb = prob / normalizeFactor;
      cumulative += normProb;
      if (r <= cumulative) {
        return sound;
      }
    }
    return sounds[sounds.length - 1];
  }
  normalizeSoundSource(source) {
    if (source === void 0) {
      return void 0;
    }
    if (typeof source === "string") {
      return {
        source: this.normalizeBuiltinAudio(source),
        volume: 1
      };
    }
    if (Array.isArray(source)) {
      const selected = this.selectSoundFromList(source);
      if (selected === void 0) {
        return void 0;
      }
      return {
        source: selected.source,
        volume: selected.volume ?? 1
      };
    }
    if (typeof source === "object" && "source" in source) {
      return {
        source: this.normalizeBuiltinAudio(source.source),
        volume: source.volume ?? 1
      };
    }
    return { source, volume: 1 };
  }
  normalizeBuiltinAudio(source) {
    if (isBuiltinAudioClip(source)) {
      return getBuiltinAudioPath(source);
    }
    return source;
  }
  play(audio, loop = false) {
    const normalized = this.normalizeSoundSource(audio);
    if (normalized === void 0) {
      const handle = new PlayHandle();
      handle._markPlayoutDone();
      return handle;
    }
    const { source, volume } = normalized;
    const playHandle = new PlayHandle();
    const task = Task.from(async ({ signal }) => {
      await this.playTask({ playHandle, sound: source, volume, loop, signal });
    });
    task.addDoneCallback(() => {
      playHandle._markPlayoutDone();
      this.playTasks.splice(this.playTasks.indexOf(task), 1);
    });
    this.playTasks.push(task);
    return playHandle;
  }
  /**
   * Start the background audio system, publishing the audio track
   * and beginning playback of any configured ambient sound.
   *
   * If `ambientSound` is provided (and contains file paths), they will loop
   * automatically. If `ambientSound` contains AsyncIterators, they are assumed
   * to be already infinite or looped.
   *
   * @param options - Options for starting background audio playback
   */
  async start(options) {
    var _a;
    const { room, agentSession, trackPublishOptions } = options;
    this.room = room;
    this.agentSession = agentSession;
    this.trackPublishOptions = trackPublishOptions;
    this.closed = false;
    await this.publishTrack();
    this.mixerTask = Task.from(async () => {
      try {
        await this.runMixerTask();
      } catch (err) {
        if (this.closed) return;
        throw err;
      }
    });
    (_a = this.agentSession) == null ? void 0 : _a.on(AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged);
    if (!this.ambientSound) return;
    const normalized = this.normalizeSoundSource(this.ambientSound);
    if (!normalized) return;
    const { source, volume } = normalized;
    const selectedSound = { source, volume, probability: 1 };
    this.ambientHandle = this.play(selectedSound, typeof source === "string");
  }
  /**
   * Close and cleanup the background audio system
   */
  async close() {
    var _a, _b, _c;
    this.closed = true;
    await cancelAndWait(this.playTasks, TASK_TIMEOUT_MS);
    await this.audioMixer.aclose();
    await this.audioSource.close();
    if (this.mixerTask) {
      await this.mixerTask.cancelAndWait(TASK_TIMEOUT_MS);
    }
    (_a = this.agentSession) == null ? void 0 : _a.off(AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged);
    const current = this.findCurrentPublication();
    if (current && current.sid) {
      await ((_c = (_b = this.room) == null ? void 0 : _b.localParticipant) == null ? void 0 : _c.unpublishTrack(current.sid));
    }
  }
  findCurrentPublication() {
    var _a, _b;
    const pubs = (_b = (_a = this.room) == null ? void 0 : _a.localParticipant) == null ? void 0 : _b.trackPublications;
    if (!pubs) return void 0;
    for (const pub of pubs.values()) {
      if (pub.name === BACKGROUND_AUDIO_TRACK_NAME) return pub;
    }
    return void 0;
  }
  /**
   * Get the current track publication
   */
  getPublication() {
    return this.findCurrentPublication() ?? this.publication;
  }
  async publishTrack() {
    var _a;
    if (this.publication !== void 0) {
      return;
    }
    const track = LocalAudioTrack.createAudioTrack(BACKGROUND_AUDIO_TRACK_NAME, this.audioSource);
    if (((_a = this.room) == null ? void 0 : _a.localParticipant) === void 0) {
      throw new Error("Local participant not available");
    }
    const publication = await this.room.localParticipant.publishTrack(
      track,
      this.trackPublishOptions ?? new TrackPublishOptions()
    );
    this.publication = publication;
    this.#logger.debug(`Background audio track published: ${this.publication.sid}`);
  }
  async runMixerTask() {
    for await (const frame of this.audioMixer) {
      await this.audioSource.captureFrame(frame);
    }
  }
  onAgentStateChanged = (ev) => {
    var _a;
    if (!this.thinkingSound) {
      return;
    }
    if (ev.newState === "thinking") {
      if (this.thinkingHandle && !this.thinkingHandle.done()) {
        return;
      }
      const normalized = this.normalizeSoundSource(this.thinkingSound);
      if (normalized) {
        const { source, volume } = normalized;
        const selectedSound = { source, volume, probability: 1 };
        this.thinkingHandle = this.play(selectedSound, typeof source === "string");
      }
    } else {
      (_a = this.thinkingHandle) == null ? void 0 : _a.stop();
    }
  };
  // Note: Python uses numpy, TS uses typed arrays for equivalent logic
  applyVolumeToFrame(frame, volume) {
    const int16Data = new Int16Array(
      frame.data.buffer,
      frame.data.byteOffset,
      frame.data.byteLength / 2
    );
    const float32Data = new Float32Array(int16Data.length);
    for (let i = 0; i < int16Data.length; i++) {
      float32Data[i] = int16Data[i];
    }
    const volumeFactor = 10 ** Math.log10(volume);
    for (let i = 0; i < float32Data.length; i++) {
      float32Data[i] *= volumeFactor;
    }
    const outputData = new Int16Array(float32Data.length);
    for (let i = 0; i < float32Data.length; i++) {
      const clipped = Math.max(-32768, Math.min(32767, float32Data[i]));
      outputData[i] = Math.round(clipped);
    }
    return new AudioFrame(outputData, frame.sampleRate, frame.channels, frame.samplesPerChannel);
  }
  async playTask({
    playHandle,
    sound,
    volume,
    loop,
    signal
  }) {
    if (isBuiltinAudioClip(sound)) {
      sound = getBuiltinAudioPath(sound);
    }
    let audioStream;
    if (typeof sound === "string") {
      audioStream = loop ? loopAudioFramesFromFile(sound, { abortSignal: signal }) : audioFramesFromFile(sound, { abortSignal: signal });
    } else {
      audioStream = sound;
    }
    const applyVolume = this.applyVolumeToFrame.bind(this);
    async function* genWrapper() {
      for await (const frame of audioStream) {
        if (signal.aborted || playHandle.done()) break;
        yield volume !== 1 ? applyVolume(frame, volume) : frame;
      }
      playHandle._markPlayoutDone();
    }
    const gen = genWrapper();
    try {
      this.audioMixer.addStream(gen);
      await playHandle.waitForPlayout();
    } finally {
      this.audioMixer.removeStream(gen);
      playHandle._markPlayoutDone();
      if (playHandle.done()) {
        await gen.return(void 0);
      }
    }
  }
}
export {
  BackgroundAudioPlayer,
  BuiltinAudioClip,
  PlayHandle,
  getBuiltinAudioPath,
  isBuiltinAudioClip
};
//# sourceMappingURL=background_audio.js.map