import {TYPING_ANIMATION_DELAY_MS} from './StreamingInterface';
import {getURLParams} from './URLParams';
import audioBuffertoWav from 'audiobuffer-to-wav';
import './StreamingInterface.css';

type StartEndTime = {
  start: number;
  end: number;
};

type StartEndTimeWithAudio = StartEndTime & {
  float32Audio: Float32Array;
};

type Text = {
  time: number;
  chars: number;
};

type DebugTimings = {
  receivedAudio: StartEndTime[];
  playedAudio: StartEndTimeWithAudio[];
  receivedText: Text[];
  renderedText: StartEndTime[];
  sentAudio: StartEndTimeWithAudio[];
  startRenderTextTime: number | null;
  startRecordingTime: number | null;
  receivedAudioSampleRate: number | null;
};

function getInitialTimings(): DebugTimings {
  return {
    receivedAudio: [],
    playedAudio: [],
    receivedText: [],
    renderedText: [],
    sentAudio: [],
    startRenderTextTime: null,
    startRecordingTime: null,
    receivedAudioSampleRate: null,
  };
}

function downloadAudioBuffer(audioBuffer: AudioBuffer, fileName: string): void {
  const wav = audioBuffertoWav(audioBuffer);
  const wavBlob = new Blob([new DataView(wav)], {
    type: 'audio/wav',
  });
  const url = URL.createObjectURL(wavBlob);
  const anchor = document.createElement('a');
  anchor.href = url;
  anchor.target = '_blank';
  anchor.download = fileName;
  anchor.click();
}

// Uncomment for debugging without download
// function playAudioBuffer(audioBuffer: AudioBuffer): void {
//   const audioContext = new AudioContext();
//   const source = audioContext.createBufferSource();

//   source.buffer = audioBuffer;
//   source.connect(audioContext.destination);
//   source.start();
// }

// Accumulate timings and audio / text translation samples for debugging and exporting
class DebugTimingsManager {
  timings: DebugTimings = getInitialTimings();

  start(): void {
    this.timings = getInitialTimings();
    this.timings.startRecordingTime = new Date().getTime();
  }

  sentAudio(event: AudioProcessingEvent): void {
    const end = new Date().getTime();
    const start = end - event.inputBuffer.duration * 1000;
    // Copy or else buffer seems to be re-used
    const float32Audio = new Float32Array(event.inputBuffer.getChannelData(0));
    this.timings.sentAudio.push({
      start,
      end,
      float32Audio,
    });
  }

  receivedText(text: string): void {
    this.timings.receivedText.push({
      time: new Date().getTime(),
      chars: text.length,
    });
  }

  startRenderText(): void {
    if (this.timings.startRenderTextTime == null) {
      this.timings.startRenderTextTime = new Date().getTime();
    }
  }

  endRenderText(): void {
    if (this.timings.startRenderTextTime == null) {
      console.warn(
        'Wrong timings of start / end rendering text. startRenderText is null',
      );
      return;
    }

    this.timings.renderedText.push({
      start: this.timings.startRenderTextTime as number,
      end: new Date().getTime(),
    });
    this.timings.startRenderTextTime = null;
  }

  receivedAudio(duration: number): void {
    const start = new Date().getTime();
    this.timings.receivedAudio.push({
      start,
      end: start + duration * 1000,
    });
  }

  playedAudio(start: number, end: number, buffer: AudioBuffer | null): void {
    if (buffer != null) {
      if (this.timings.receivedAudioSampleRate == null) {
        this.timings.receivedAudioSampleRate = buffer.sampleRate;
      }
      if (this.timings.receivedAudioSampleRate != buffer.sampleRate) {
        console.error(
          'Sample rates of received audio are unequal, will fail to reconstruct debug audio',
          this.timings.receivedAudioSampleRate,
          buffer.sampleRate,
        );
      }
    }
    this.timings.playedAudio.push({
      start,
      end,
      float32Audio:
        buffer == null
          ? new Float32Array()
          : new Float32Array(buffer.getChannelData(0)),
    });
  }

  getChartData() {
    const columns = [
      {type: 'string', id: 'Series'},
      {type: 'date', id: 'Start'},
      {type: 'date', id: 'End'},
    ];
    return [
      columns,
      ...this.timings.sentAudio.map((sentAudio) => [
        'Sent Audio',
        new Date(sentAudio.start),
        new Date(sentAudio.end),
      ]),
      ...this.timings.receivedAudio.map((receivedAudio) => [
        'Received Audio',
        new Date(receivedAudio.start),
        new Date(receivedAudio.end),
      ]),
      ...this.timings.playedAudio.map((playedAudio) => [
        'Played Audio',
        new Date(playedAudio.start),
        new Date(playedAudio.end),
      ]),
      // Best estimate duration by multiplying length with animation duration for each letter
      ...this.timings.receivedText.map((receivedText) => [
        'Received Text',
        new Date(receivedText.time),
        new Date(
          receivedText.time + receivedText.chars * TYPING_ANIMATION_DELAY_MS,
        ),
      ]),
      ...this.timings.renderedText.map((renderedText) => [
        'Rendered Text',
        new Date(renderedText.start),
        new Date(renderedText.end),
      ]),
    ];
  }

  downloadInputAudio() {
    const audioContext = new AudioContext();
    const totalLength = this.timings.sentAudio.reduce((acc, cur) => {
      return acc + cur?.float32Audio?.length ?? 0;
    }, 0);
    if (totalLength === 0) {
      return;
    }

    const incomingArrayBuffer = audioContext.createBuffer(
      1, // 1 channel
      totalLength,
      audioContext.sampleRate,
    );

    const buffer = incomingArrayBuffer.getChannelData(0);
    let i = 0;
    this.timings.sentAudio.forEach((sentAudio) => {
      sentAudio.float32Audio.forEach((bytes) => {
        buffer[i++] = bytes;
      });
    });

    // Play for debugging
    // playAudioBuffer(incomingArrayBuffer);
    downloadAudioBuffer(incomingArrayBuffer, `input_audio.wav`);
  }

  downloadOutputAudio() {
    const playedAudio = this.timings.playedAudio;
    const sampleRate = this.timings.receivedAudioSampleRate;
    if (
      playedAudio.length === 0 ||
      this.timings.startRecordingTime == null ||
      sampleRate == null
    ) {
      return null;
    }

    let previousEndTime = this.timings.startRecordingTime;
    const audioArray: number[] = [];
    playedAudio.forEach((audio) => {
      const delta = (audio.start - previousEndTime) / 1000;
      for (let i = 0; i < delta * sampleRate; i++) {
        audioArray.push(0.0);
      }
      audio.float32Audio.forEach((bytes) => audioArray.push(bytes));
      previousEndTime = audio.end;
    });
    const audioContext = new AudioContext();
    const incomingArrayBuffer = audioContext.createBuffer(
      1, // 1 channel
      audioArray.length,
      sampleRate,
    );

    incomingArrayBuffer.copyToChannel(
      new Float32Array(audioArray),
      0, // first channel
    );

    // Play for debugging
    // playAudioBuffer(incomingArrayBuffer);
    downloadAudioBuffer(incomingArrayBuffer, 'output_audio.wav');
  }
}

const debugSingleton = new DebugTimingsManager();
export default function debug(): DebugTimingsManager | null {
  const debugParam = getURLParams().debug;
  return debugParam ? debugSingleton : null;
}