import { useEffect, useState } from "react";

import * as Tone from "tone";

import { InferenceResult } from "../types";

interface AudioPlayerProps {
  paused: boolean;
  inferenceResults: InferenceResult[];
  nowPlayingCallback: (result: InferenceResult, playerTime: number) => void;
  playerIsBehindCallback: (isBehind: boolean) => void;
  useCompressor: boolean;
}

/**
 * Audio player with Tone.js
 *
 * TODO(hayk): Look into https://github.com/E-Kuerschner/useAudioPlayer as an alternative.
 */
export default function AudioPlayer({
  paused,
  inferenceResults,
  nowPlayingCallback,
  playerIsBehindCallback,
  useCompressor,
}: AudioPlayerProps) {
  const [tonePlayer, setTonePlayer] = useState<Tone.Player>(null);

  const [numClipsPlayed, setNumClipsPlayed] = useState(0);
  const [prevNumClipsPlayed, setPrevNumClipsPlayed] = useState(0);

  const [resultCounter, setResultCounter] = useState(0);

  // On load, create a player synced to the tone transport
  useEffect(() => {
    if (tonePlayer) {
      return;
    }

    if (inferenceResults.length === 0) {
      return;
    }

    const result = inferenceResults[0];

    const player = new Tone.Player(result.audio, () => {
      player.loop = true;
      player.sync().start(0);

      // Set up a callback to increment numClipsPlayed at the edge of each clip
      const bufferLength = player.sampleTime * player.buffer.length;

      // TODO(hayk): Set this callback up to vary each time using duration_s
      Tone.Transport.scheduleRepeat((time) => {
        setNumClipsPlayed((n) => n + 1);
      }, bufferLength);

      setTonePlayer(player);

      // Make further load callbacks do nothing.
      player.buffer.onload = () => {};
    });

    if (useCompressor) {
      const compressor = new Tone.Compressor(-30, 3).toDestination();
      player.connect(compressor);
    } else {
      player.toDestination();
    }
  }, [tonePlayer, inferenceResults, useCompressor]);

  // On play/pause button, play/pause the audio with the tone transport
  useEffect(() => {
    if (!paused) {
      if (Tone.context.state == "suspended") {
        Tone.context.resume();
      }

      if (tonePlayer) {
        Tone.Transport.start();
      }
    } else {
      if (tonePlayer) {
        Tone.Transport.pause();
      }
    }
  }, [paused, tonePlayer]);

  // If there is a new clip, switch to it
  useEffect(() => {
    if (numClipsPlayed == prevNumClipsPlayed) {
      return;
    }

    const maxResultCounter = Math.max(
      ...inferenceResults.map((r) => r.counter)
    );

    if (maxResultCounter < resultCounter) {
      console.info(
        "No new result available, looping previous clip",
        resultCounter,
        maxResultCounter
      );
      playerIsBehindCallback(true);
      return;
    }

    playerIsBehindCallback(false);

    const result = inferenceResults.find(
      (r: InferenceResult) => r.counter == resultCounter
    );

    setResultCounter((c) => c + 1);

    tonePlayer.load(result.audio).then(() => {
      // Re-jigger the transport so it stops playing old buffers. It seems like this doesn't
      // introduce a gap, but watch out for that.
      Tone.Transport.pause();
      if (!paused) {
        Tone.Transport.start();
      }

      const playerTime = Tone.Transport.seconds;
      nowPlayingCallback(result, playerTime);
    });

    setPrevNumClipsPlayed(numClipsPlayed);
  }, [
    numClipsPlayed,
    prevNumClipsPlayed,
    resultCounter,
    inferenceResults,
    paused,
    nowPlayingCallback,
    playerIsBehindCallback,
    tonePlayer,
  ]);

  return null;
}
