import { useCallback, useEffect, useRef, useState } from 'react';
import audioContext from './audioContext';

const useAudioBufferPlayback = () => {
  const [volume, setVolumeState] = useState(0.8);
  const [audioBuffer, setAudioBuffer] = useState<AudioBuffer | null>(null);
  const [duration, setDuration] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [seekTime, setSeekTime] = useState(0);
  const [muted, setMuted] = useState(false);
  const lastReportedTimeRef = useRef(0);
  const lastReportedAtRef = useRef(0);
  const lastStartedFromRef = useRef(0);
  const onStopRef = useRef<null | (() => void)>(null);
  const playingRef = useRef<boolean>(false);
  const analyserNodeRef = useRef<AnalyserNode | null>(null);
  const gainNodeRef = useRef<GainNode | null>(null);
  const spectrumBufferRef = useRef<Uint8Array | null>(null);
  const endTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  const getSpectrum = useCallback(() => {
    if (!analyserNodeRef.current) return null;

    const bufferLength = analyserNodeRef.current.frequencyBinCount;

    const dataArray = spectrumBufferRef.current || new Uint8Array(bufferLength);
    spectrumBufferRef.current = dataArray;

    analyserNodeRef.current.getByteFrequencyData(dataArray);

    return dataArray;
  }, []);

  const getCurrentTime = useCallback(() => {
    if (!playingRef.current) return lastStartedFromRef.current;

    const now = Date.now();

    return lastReportedTimeRef.current + (now - lastReportedAtRef.current) / 1000;
  }, []);

  const play = useCallback(
    (startTime: number = seekTime, endTime: number = Infinity) => {
      endTimeoutRef.current && clearTimeout(endTimeoutRef.current);
      if (!audioBuffer) return;

      const bufferSourceNode = audioContext.createBufferSource();
      bufferSourceNode.buffer = audioBuffer;

      if (onStopRef.current) onStopRef.current();

      const gain = gainNodeRef.current || audioContext.createGain();
      gain.gain.value = muted ? 0 : volume;
      const analyser = analyserNodeRef.current || audioContext.createAnalyser();
      analyser.smoothingTimeConstant = 0.85;

      analyserNodeRef.current = analyser;
      gainNodeRef.current = gain;

      bufferSourceNode.connect(analyser);
      bufferSourceNode.connect(gain);
      gain.connect(audioContext.destination);

      bufferSourceNode.start(0, startTime);
      setPlaying(true);
      playingRef.current = true;
      lastReportedAtRef.current = Date.now();
      lastReportedTimeRef.current = startTime;
      endTimeoutRef.current = setTimeout(
        () => {
          setPlaying(false);
          playingRef.current = false;
          onStopRef.current?.();
          bufferSourceNode.stop();
          bufferSourceNode.disconnect();
        },
        1000 * (Math.min(audioBuffer.duration, endTime) - startTime)
      );

      lastStartedFromRef.current = startTime;
      lastReportedTimeRef.current = startTime;
      lastReportedAtRef.current = Date.now();

      if (audioContext.state !== 'running') {
        audioContext.resume();
      }

      onStopRef.current = () => {
        bufferSourceNode.disconnect();
        onStopRef.current = null;
      };
    },
    [seekTime, volume, muted, audioBuffer]
  );

  const seek = useCallback(
    async (time: number) => {
      setSeekTime(time);
      lastStartedFromRef.current = time;
      lastReportedTimeRef.current = time;
      lastReportedAtRef.current = Date.now();
      if (playing) {
        play(time);
      }
    },
    [playing]
  );

  const stop = useCallback(
    (andSeekToStart = true) => {
      if (onStopRef.current) {
        onStopRef.current();
      }
      lastStartedFromRef.current = andSeekToStart ? 0 : getCurrentTime();
      andSeekToStart && setSeekTime(0);
      playingRef.current = false;
      setPlaying(false);
    },
    [seek, getCurrentTime]
  );

  const load = useCallback(async (audioBuffer: AudioBuffer) => {
    setDuration(audioBuffer.duration);
    setAudioBuffer(audioBuffer);
  }, []);

  const setVolume = useCallback((volume: number, andCommit: boolean) => {
    if (andCommit) {
      setVolumeState(volume);
    }
    if (!gainNodeRef.current) return;
    gainNodeRef.current.gain.value = volume;
  }, []);

  useEffect(() => {
    if (!gainNodeRef.current) return;
    if (muted) {
      gainNodeRef.current.gain.value = 0;
    } else {
      gainNodeRef.current.gain.value = volume;
    }
  }, [muted]);

  return {
    load,
    duration,
    playing,
    seek,
    play,
    stop,
    getCurrentTime,
    getSpectrum,
    volume,
    setVolume,
    muted,
    setMuted,
  };
};

export default useAudioBufferPlayback;
