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

const useAudioPlayback = () => {
  const [volume, setVolumeState] = useState(0.8);
  const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(null);
  const [duration, setDuration] = useState(0);
  const [loading, setLoading] = useState(true);
  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 mediaElementSourceRef = useRef<MediaElementAudioSourceNode | null>(null);
  const spectrumBufferRef = useRef<Uint8Array | 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 (!audioElement) return 0;
    if (!playingRef.current) return lastStartedFromRef.current;

    const now = Date.now();

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

  const play = useCallback(
    (startTime: number = seekTime) => {
      if (!audioElement) return;
      if (onStopRef.current) onStopRef.current();

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

      source.connect(analyser);
      source.connect(gain);
      gain.connect(audioContext.destination);
      audioElement.load();
      audioElement.currentTime = startTime;
      audioElement.play();
      audioElement.onended = () => {
        audioElement.currentTime = 0;
        setSeekTime(0);
        playingRef.current = false;
        setPlaying(false);
        onStopRef.current?.();
      };

      audioElement.onplaying = () => {
        setPlaying(true);
        playingRef.current = true;
        lastReportedAtRef.current = Date.now();
        lastReportedTimeRef.current = audioElement.currentTime;
      };

      audioElement.ontimeupdate = () => {
        lastReportedAtRef.current = Date.now();
        lastReportedTimeRef.current = audioElement.currentTime;
      };

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

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

      onStopRef.current = () => {
        audioElement.pause();
        source.disconnect();
        onStopRef.current = null;
      };
    },
    [audioElement, seekTime, volume, muted]
  );

  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 = getCurrentTime();
      andSeekToStart && setSeekTime(lastStartedFromRef.current);
      playingRef.current = false;
      setPlaying(false);
    },
    [seek, getCurrentTime]
  );

  const load = useCallback(
    async (url: string) => {
      stop();
      const audioElement = new Audio(url);
      audioElement.crossOrigin = 'anonymous';
      setAudioElement(audioElement);
      audioElement.addEventListener('loadedmetadata', () => {
        setDuration(audioElement.duration);
      });
      audioElement.addEventListener('canplaythrough', () => {
        setLoading(false);
      });
      audioElement.load();
    },
    [stop]
  );

  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,
    loading,
    getSpectrum,
    volume,
    setVolume,
    muted,
    setMuted,
  };
};

export const AudioPlaybackContext = createContext<ReturnType<typeof useAudioPlayback>>(undefined as never);

export default useAudioPlayback;
