import { useImperativeHandle, forwardRef, useRef, useState, useCallback, useEffect } from 'react';
import isElectron from 'is-electron';
import type { ReactPlayerProps } from 'react-player';
import ReactPlayer from 'react-player/lazy';
import type { Song } from '/@/renderer/api/types';
import {
    crossfadeHandler,
    gaplessHandler,
} from '/@/renderer/components/audio-player/utils/list-handlers';
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
import type { CrossfadeStyle } from '/@/renderer/types';
import { PlaybackStyle, PlayerStatus } from '/@/renderer/types';
import { useSpeed } from '/@/renderer/store';
import { toast } from '/@/renderer/components/toast';

interface AudioPlayerProps extends ReactPlayerProps {
    crossfadeDuration: number;
    crossfadeStyle: CrossfadeStyle;
    currentPlayer: 1 | 2;
    playbackStyle: PlaybackStyle;
    player1: Song;
    player2: Song;
    status: PlayerStatus;
    volume: number;
}

export type AudioPlayerProgress = {
    loaded: number;
    loadedSeconds: number;
    played: number;
    playedSeconds: number;
};

const getDuration = (ref: any) => {
    return ref.current?.player?.player?.player?.duration;
};

type WebAudio = {
    context: AudioContext;
    gain: GainNode;
};

// Credits: http://stackoverflow.com/questions/12150729/ddg
// This is used so that the player will always have an <audio> element. This means that
// player1Source and player2Source are connected BEFORE the user presses play for
// the first time. This workaround is important for Safari, which seems to require the
// source to be connected PRIOR to resuming audio context
const EMPTY_SOURCE =
    'data:audio/wav;base64,UklGRjIAAABXQVZFZm10IBIAAAABAAEAQB8AAEAfAAABAAgAAABmYWN0BAAAAAAAAABkYXRhAAAAAA==';

export const AudioPlayer = forwardRef(
    (
        {
            status,
            playbackStyle,
            crossfadeStyle,
            crossfadeDuration,
            currentPlayer,
            autoNext,
            player1,
            player2,
            muted,
            volume,
        }: AudioPlayerProps,
        ref: any,
    ) => {
        const player1Ref = useRef<ReactPlayer>(null);
        const player2Ref = useRef<ReactPlayer>(null);
        const [isTransitioning, setIsTransitioning] = useState(false);
        const audioDeviceId = useSettingsStore((state) => state.playback.audioDeviceId);
        const playback = useSettingsStore((state) => state.playback.mpvProperties);
        const { resetSampleRate } = useSettingsStoreActions();
        const playbackSpeed = useSpeed();

        const [webAudio, setWebAudio] = useState<WebAudio | null>(null);
        const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(
            null,
        );
        const [player2Source, setPlayer2Source] = useState<MediaElementAudioSourceNode | null>(
            null,
        );

        const calculateReplayGain = useCallback(
            (song: Song): number => {
                if (playback.replayGainMode === 'no') {
                    return 1;
                }

                let gain: number | undefined;
                let peak: number | undefined;

                if (playback.replayGainMode === 'track') {
                    gain = song.gain?.track ?? song.gain?.album;
                    peak = song.peak?.track ?? song.peak?.album;
                } else {
                    gain = song.gain?.album ?? song.gain?.track;
                    peak = song.peak?.album ?? song.peak?.track;
                }

                if (gain === undefined) {
                    gain = playback.replayGainFallbackDB;

                    if (!gain) {
                        return 1;
                    }
                }

                if (peak === undefined) {
                    peak = 1;
                }

                const preAmp = playback.replayGainPreampDB ?? 0;

                // https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification&section=19
                // Normalized to max gain
                const expectedGain = 10 ** ((gain + preAmp) / 20);

                if (playback.replayGainClip) {
                    return Math.min(expectedGain, 1 / peak);
                }
                return expectedGain;
            },
            [
                playback.replayGainClip,
                playback.replayGainFallbackDB,
                playback.replayGainMode,
                playback.replayGainPreampDB,
            ],
        );

        useEffect(() => {
            if ('AudioContext' in window) {
                let context: AudioContext;

                try {
                    context = new AudioContext({
                        latencyHint: 'playback',
                        sampleRate: playback.audioSampleRateHz || undefined,
                    });
                } catch (error) {
                    // In practice, this should never be hit because the UI should validate
                    // the range. However, the actual supported range is not guaranteed
                    toast.error({ message: (error as Error).message });
                    context = new AudioContext({ latencyHint: 'playback' });
                    resetSampleRate();
                }

                const gain = context.createGain();
                gain.connect(context.destination);

                setWebAudio({ context, gain });

                return () => {
                    return context.close();
                };
            }
            return () => {};
            // Intentionally ignore the sample rate dependency, as it makes things really messy
            // eslint-disable-next-line react-hooks/exhaustive-deps
        }, []);

        useImperativeHandle(ref, () => ({
            get player1() {
                return player1Ref?.current;
            },
            get player2() {
                return player2Ref?.current;
            },
        }));

        const handleOnEnded = () => {
            autoNext();
            setIsTransitioning(false);
        };

        useEffect(() => {
            if (status === PlayerStatus.PLAYING) {
                if (currentPlayer === 1) {
                    // calling play() is not necessarily a safe option (https://developer.chrome.com/blog/play-request-was-interrupted)
                    // In practice, this failure is only likely to happen when using the 0-second wav:
                    // play() + play() in rapid succession will cause problems as the frist one ends the track.
                    player1Ref.current
                        ?.getInternalPlayer()
                        ?.play()
                        .catch(() => {});
                } else {
                    player2Ref.current
                        ?.getInternalPlayer()
                        ?.play()
                        .catch(() => {});
                }
            } else {
                player1Ref.current?.getInternalPlayer()?.pause();
                player2Ref.current?.getInternalPlayer()?.pause();
            }
        }, [currentPlayer, status]);

        const handleCrossfade1 = useCallback(
            (e: AudioPlayerProgress) => {
                return crossfadeHandler({
                    currentPlayer,
                    currentPlayerRef: player1Ref,
                    currentTime: e.playedSeconds,
                    duration: getDuration(player1Ref),
                    fadeDuration: crossfadeDuration,
                    fadeType: crossfadeStyle,
                    isTransitioning,
                    nextPlayerRef: player2Ref,
                    player: 1,
                    setIsTransitioning,
                    volume,
                });
            },
            [crossfadeDuration, crossfadeStyle, currentPlayer, isTransitioning, volume],
        );

        const handleCrossfade2 = useCallback(
            (e: AudioPlayerProgress) => {
                return crossfadeHandler({
                    currentPlayer,
                    currentPlayerRef: player2Ref,
                    currentTime: e.playedSeconds,
                    duration: getDuration(player2Ref),
                    fadeDuration: crossfadeDuration,
                    fadeType: crossfadeStyle,
                    isTransitioning,
                    nextPlayerRef: player1Ref,
                    player: 2,
                    setIsTransitioning,
                    volume,
                });
            },
            [crossfadeDuration, crossfadeStyle, currentPlayer, isTransitioning, volume],
        );

        const handleGapless1 = useCallback(
            (e: AudioPlayerProgress) => {
                return gaplessHandler({
                    currentTime: e.playedSeconds,
                    duration: getDuration(player1Ref),
                    isFlac: player1?.container === 'flac',
                    isTransitioning,
                    nextPlayerRef: player2Ref,
                    setIsTransitioning,
                });
            },
            [isTransitioning, player1?.container],
        );

        const handleGapless2 = useCallback(
            (e: AudioPlayerProgress) => {
                return gaplessHandler({
                    currentTime: e.playedSeconds,
                    duration: getDuration(player2Ref),
                    isFlac: player2?.container === 'flac',
                    isTransitioning,
                    nextPlayerRef: player1Ref,
                    setIsTransitioning,
                });
            },
            [isTransitioning, player2?.container],
        );

        useEffect(() => {
            if (isElectron()) {
                if (audioDeviceId) {
                    player1Ref.current?.getInternalPlayer()?.setSinkId(audioDeviceId);
                    player2Ref.current?.getInternalPlayer()?.setSinkId(audioDeviceId);
                } else {
                    player1Ref.current?.getInternalPlayer()?.setSinkId('');
                    player2Ref.current?.getInternalPlayer()?.setSinkId('');
                }
            }
        }, [audioDeviceId]);

        useEffect(() => {
            if (webAudio && player1Source && player1 && currentPlayer === 1) {
                const newVolume = calculateReplayGain(player1) * volume;
                webAudio.gain.gain.setValueAtTime(newVolume, 0);
            }
        }, [calculateReplayGain, currentPlayer, player1, player1Source, volume, webAudio]);

        useEffect(() => {
            if (webAudio && player2Source && player2 && currentPlayer === 2) {
                const newVolume = calculateReplayGain(player2) * volume;
                webAudio.gain.gain.setValueAtTime(newVolume, 0);
            }
        }, [calculateReplayGain, currentPlayer, player2, player2Source, volume, webAudio]);

        const handlePlayer1Start = useCallback(
            async (player: ReactPlayer) => {
                if (!webAudio) return;
                if (player1Source) {
                    // This should fire once, only if the source is real (meaning we
                    // saw the dummy source) and the context is not ready
                    if (webAudio.context.state !== 'running') {
                        await webAudio.context.resume();
                    }
                    return;
                }

                const internal = player.getInternalPlayer() as HTMLMediaElement | undefined;
                if (internal) {
                    const { context, gain } = webAudio;
                    const source = context.createMediaElementSource(internal);
                    source.connect(gain);
                    setPlayer1Source(source);
                }
            },
            [player1Source, webAudio],
        );

        const handlePlayer2Start = useCallback(
            async (player: ReactPlayer) => {
                if (!webAudio) return;
                if (player2Source) {
                    if (webAudio.context.state !== 'running') {
                        await webAudio.context.resume();
                    }
                    return;
                }

                const internal = player.getInternalPlayer() as HTMLMediaElement | undefined;
                if (internal) {
                    const { context, gain } = webAudio;
                    const source = context.createMediaElementSource(internal);
                    source.connect(gain);
                    setPlayer2Source(source);
                }
            },
            [player2Source, webAudio],
        );

        // Bugfix for Safari: rather than use the `<audio>` volume (which doesn't work),
        // use the GainNode to scale the volume. In this case, for compatibility with
        // other browsers, set the `<audio>` volume to 1
        return (
            <>
                <ReactPlayer
                    ref={player1Ref}
                    config={{
                        file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
                    }}
                    height={0}
                    muted={muted}
                    playbackRate={playbackSpeed}
                    playing={currentPlayer === 1 && status === PlayerStatus.PLAYING}
                    progressInterval={isTransitioning ? 10 : 250}
                    url={player1?.streamUrl || EMPTY_SOURCE}
                    volume={webAudio ? 1 : volume}
                    width={0}
                    // If there is no stream url, we do not need to handle when the audio finishes
                    onEnded={player1?.streamUrl ? handleOnEnded : undefined}
                    onProgress={
                        playbackStyle === PlaybackStyle.GAPLESS ? handleGapless1 : handleCrossfade1
                    }
                    onReady={handlePlayer1Start}
                />
                <ReactPlayer
                    ref={player2Ref}
                    config={{
                        file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
                    }}
                    height={0}
                    muted={muted}
                    playbackRate={playbackSpeed}
                    playing={currentPlayer === 2 && status === PlayerStatus.PLAYING}
                    progressInterval={isTransitioning ? 10 : 250}
                    url={player2?.streamUrl || EMPTY_SOURCE}
                    volume={webAudio ? 1 : volume}
                    width={0}
                    onEnded={player2?.streamUrl ? handleOnEnded : undefined}
                    onProgress={
                        playbackStyle === PlaybackStyle.GAPLESS ? handleGapless2 : handleCrossfade2
                    }
                    onReady={handlePlayer2Start}
                />
            </>
        );
    },
);
