export type OnVolumeChange = (volume: number) => void;
export type OnDataAvailable = (data: Blob) => void | undefined;

export interface SilenceAwareRecorderOptions {
  deviceId?: string;
  minDecibels?: number;
  onDataAvailable?: OnDataAvailable;
  onVolumeChange?: OnVolumeChange;

  setDeviceId?: (deviceId: string) => void;
  silenceDetectionEnabled?: boolean;

  silenceDuration?: number;

  silentThreshold?: number;
  stopRecorderOnSilence?: boolean;
  timeSlice?: number;
}

class SilenceAwareRecorder {
  private readonly silenceDetectionEnabled: boolean;

  private readonly timeSlice: number;

  private audioContext: AudioContext | null;

  private mediaStreamSource: MediaStreamAudioSourceNode | null;

  private analyser: AnalyserNode | null;

  private mediaRecorder: MediaRecorder | null;

  private silenceTimeout: ReturnType<typeof setTimeout> | null;

  private readonly silenceThreshold: number;

  private readonly silenceDuration: number;

  private readonly minDecibels: number;

  private readonly onVolumeChange?: OnVolumeChange;

  private readonly onDataAvailable?: OnDataAvailable;

  private isSilence: boolean;

  private hasSoundStarted: boolean;

  public deviceId: string | null;

  public isRecording: boolean;

  private readonly stopRecorderOnSilence: boolean;

  private animationFrameId: number | null;

  constructor({
    onVolumeChange,
    onDataAvailable,
    silenceDuration = 2500,
    silentThreshold = -50,
    minDecibels = -100,
    deviceId = 'default',
    timeSlice = 250,
    silenceDetectionEnabled = true,
    stopRecorderOnSilence = false,
  }: SilenceAwareRecorderOptions) {
    this.silenceDetectionEnabled = silenceDetectionEnabled;
    this.stopRecorderOnSilence = stopRecorderOnSilence;
    this.timeSlice = timeSlice;
    this.audioContext = null;
    this.mediaStreamSource = null;
    this.analyser = null;
    this.mediaRecorder = null;
    this.silenceTimeout = null;
    this.silenceThreshold = silentThreshold;
    this.silenceDuration = silenceDuration;
    this.minDecibels = minDecibels;
    this.onVolumeChange = onVolumeChange;
    this.onDataAvailable = onDataAvailable;
    this.isSilence = false;
    this.hasSoundStarted = false;
    this.deviceId = deviceId;
    this.isRecording = false;
    this.animationFrameId = null;
  }

  async startRecording(): Promise<void> {
    if (this.isRecording) {
      return;
    }

    try {
      const stream = await this.getAudioStream();
      this.setupAudioContext(stream);
      this.setupMediaRecorder(stream);
      this.isRecording = true;
      this.checkForSilence();
    } catch (err) {
      console.error('Error getting audio stream:', err);
    }
  }

  private async getAudioStream(): Promise<MediaStream> {
    // eslint-disable-next-line no-undef
    const constraints: MediaStreamConstraints = {
      audio: this.deviceId ? { deviceId: { exact: this.deviceId } } : true,
      video: false,
    };

    return navigator.mediaDevices.getUserMedia(constraints);
  }

  private setupAudioContext(stream: MediaStream): void {
    this.audioContext = new AudioContext();
    this.mediaStreamSource = this.audioContext.createMediaStreamSource(stream);
    this.analyser = this.audioContext.createAnalyser();
    this.analyser.minDecibels = this.minDecibels;
    this.mediaStreamSource.connect(this.analyser);
  }

  private setupMediaRecorder(stream: MediaStream): void {
    this.mediaRecorder = new MediaRecorder(stream);

    this.mediaRecorder.ondataavailable = (event) => {
      if (event.data.size > 0 && !this.isSilence) {
        this.onDataAvailable?.(event.data);
      }
    };

    this.mediaRecorder.start(this.timeSlice);
  }

  async getAvailableDevices(): Promise<MediaDeviceInfo[]> {
    return navigator.mediaDevices.enumerateDevices();
  }

  setDevice(deviceId: string): void {
    if (this.deviceId !== deviceId) {
      this.deviceId = deviceId;
      if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
        // If the recording is running, stop it before switching devices
        this.stopRecording();
      }
    }
  }

  stopRecording(): void {
    if (!this.isRecording) {
      return;
    }

    if (this.mediaRecorder && this.hasSoundStarted && this.mediaRecorder.state === 'recording') {
      this.mediaRecorder.requestData();
      setTimeout(() => {
        this.cleanUp();
      }, 100); // adjust this delay as necessary
    } else {
      this.cleanUp();
    }

    if (this.silenceTimeout) {
      clearTimeout(this.silenceTimeout);
      this.silenceTimeout = null;
    }
  }

  private cleanUp(): void {
    if (this.mediaRecorder?.state === 'recording') {
      this.mediaRecorder?.stop();
      cancelAnimationFrame(this.animationFrameId!);
    }
    this.mediaRecorder?.stream?.getTracks().forEach((track) => track.stop());
    this.audioContext?.close();
    this.hasSoundStarted = false;
    this.isRecording = false;
  }

  private checkForSilence(): void {
    if (!this.mediaRecorder) {
      throw new Error('MediaRecorder is not available');
    }

    if (!this.analyser) {
      throw new Error('Analyser is not available');
    }

    const bufferLength = this.analyser.fftSize;
    const amplitudeArray = new Float32Array(bufferLength || 0);
    this.analyser.getFloatTimeDomainData(amplitudeArray);

    const volume = this.computeVolume(amplitudeArray);

    this.onVolumeChange?.(volume);

    if (this.silenceDetectionEnabled) {
      if (volume < this.silenceThreshold) {
        if (!this.silenceTimeout) {
          this.silenceTimeout = setTimeout(() => {
            if (this.stopRecorderOnSilence) {
              this.mediaRecorder?.stop();
            }
            this.isSilence = true;
            this.silenceTimeout = null;
          }, this.silenceDuration);
        }
      } else {
        if (this.silenceTimeout) {
          clearTimeout(this.silenceTimeout);
          this.silenceTimeout = null;
        }
        if (this.isSilence) {
          if (this.stopRecorderOnSilence) {
            this.mediaRecorder.start(this.timeSlice);
          }
          this.isSilence = false;
        }
        if (!this.hasSoundStarted) {
          this.hasSoundStarted = true;
        }
      }
    }

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

  private computeVolume(amplitudeArray: Float32Array): number {
    const values = amplitudeArray.reduce((sum, value) => sum + value * value, 0);
    const average = Math.sqrt(values / amplitudeArray.length); // calculate rms
    const volume = 20 * Math.log10(average); // convert to dB
    return volume;
  }
}

export default SilenceAwareRecorder;
