export type SoundDetectorOptions = {
  /**
   * Defines how often the detector should check whether a sound is present.
   * Defaults to 500ms.
   */
  detectionFrequencyInMs?: number;

  /**
   * Defines the audio level threshold before a "change" is emitted.
   * Defaults to 150. This value should be in the range of 0-255.
   */
  audioLevelThreshold?: number;

  /**
   * See https://developer.mozilla.org/en-US/docs/web/api/analysernode/fftsize
   *
   * Defaults to 128.
   */
  fftSize?: number;

  /**
   * Defines whether the provided audio stream should be stopped (destroyed)
   * when the sound detector is stopped.
   *
   * Defaults to `true`.
   */
  destroyStreamOnStop?: boolean;
};

export type SoundDetectorState = {
  isSoundDetected: boolean;
  /**
   * Represented as percentage (0-100) where 100% is defined by `audioLevelThreshold` property.
   * Decrease time between samples (to 50-100ms) with `detectionFrequencyInMs` property.
   */
  audioLevel: number;
};

export type SoundStateChangeHandler = (state: SoundDetectorState) => void;

/**
 * Creates a new sound detector.
 *
 * @param audioStream the audio stream to observe. Depending on the provided configuration, this stream might be destroyed when the sound detector is stopped.
 * @param onSoundDetectedStateChanged a callback which is called when the sound state changes.
 * @param options custom options for the sound detector.
 * @returns a clean-up function which once invoked stops the sound detector.
 */
export const createSoundDetector = (
  audioStream: MediaStream,
  onSoundDetectedStateChanged: SoundStateChangeHandler,
  options: SoundDetectorOptions = {},
) => {
  const {
    detectionFrequencyInMs = 500,
    audioLevelThreshold = 150,
    fftSize = 128,
    destroyStreamOnStop = true,
  } = options;

  const audioContext = new AudioContext();
  const analyser = audioContext.createAnalyser();
  analyser.fftSize = fftSize;

  const microphone = audioContext.createMediaStreamSource(audioStream);
  microphone.connect(analyser);

  const intervalId = setInterval(() => {
    const data = new Uint8Array(analyser.frequencyBinCount);
    analyser.getByteFrequencyData(data);

    const isSoundDetected = data.some((value) => value >= audioLevelThreshold);

    const averagedDataValue = data.reduce((pv, cv) => pv + cv, 0) / data.length;

    const percentage =
      averagedDataValue > audioLevelThreshold
        ? 100
        : Math.round((averagedDataValue / audioLevelThreshold) * 100);

    // When the track is disabled, it takes time for the buffer to empty
    // This check will ensure that we don't send anything if the track is disabled
    if (audioStream.getAudioTracks()[0]?.enabled) {
      onSoundDetectedStateChanged({ isSoundDetected, audioLevel: percentage });
    } else {
      onSoundDetectedStateChanged({ isSoundDetected: false, audioLevel: 0 });
    }
  }, detectionFrequencyInMs);

  return async function stop() {
    clearInterval(intervalId);

    // clean-up the AudioContext elements
    microphone.disconnect();
    analyser.disconnect();
    if (audioContext.state !== 'closed') {
      await audioContext.close();
    }

    // stop the stream
    if (destroyStreamOnStop) {
      audioStream.getTracks().forEach((track) => {
        track.stop();
        audioStream.removeTrack(track);
      });
    }
  };
};
