import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import { RootState, AppDispatch } from "../../store";

//************************
// Interface definitions *
//************************

interface MusicPlayerState {
    muted: boolean;
    playing: PlayState;
    title: MusicPlayerTitle;
}

interface MusicPlayerTitle {
    source: string;
    artist: string;
    name: string;
    album: string;
    length: number;
}

enum PlayState {
    Playing = "Playing",
    Paused = "Paused",
    Loading = "Loading",
}

//*********************
// Music player logic *
//*********************

class MusicPlayer {
    private context: AudioContext;
    private source: HTMLAudioElement;
    private sourceNode: MediaElementAudioSourceNode;
    private volume: GainNode;

    constructor() {
        this.context = new AudioContext();
        this.source = new Audio();
        this.sourceNode = this.context.createMediaElementSource(this.source);
        this.volume = this.context.createGain();

        this.sourceNode.connect(this.volume);
        this.volume.connect(this.context.destination);
    }

    get audioContext() {
        return this.context;
    }

    get audioNode() {
        return this.sourceNode;
    }

    set src(source: string) {
        this.source.src = source;
    }

    togglePlay = async (
        _: null,
        { getState }: { getState: () => RootState }
    ): Promise<PlayState> => {
        const playing = getState().musicPlayer.playing;

        switch (playing) {
            case PlayState.Playing:
                this.source.pause();
                return PlayState.Paused;
            case PlayState.Paused:
            case PlayState.Loading:
                // Chrome's extra cookie, it refuses to play if we
                // don't resume after the first user interaction.
                await this.context.resume();
                return this.source.play().then(() => PlayState.Playing);
        }
    };
}

const player = new MusicPlayer();

//*************************
// Redux state management *
//*************************

const initialState: MusicPlayerState = {
    muted: false,
    playing: PlayState.Paused,
    title: {
        source: "",
        artist: "",
        name: "",
        album: "",
        length: 0,
    },
};

export const musicPlayerSlice = createSlice({
    name: "musicPlayer",
    initialState,

    reducers: {
        setSource: (state, action: PayloadAction<MusicPlayerTitle>) => {
            state.title = action.payload;
            player.src = state.title.source;
        },
    },

    extraReducers: (builder) => {
        builder
            .addCase(togglePlay.pending, (state) => {
                // If we are currently paused or loading, then this is
                // actually an async call, otherwise we just
                // synchronously pause the music.
                if (state.playing !== PlayState.Playing) {
                    state.playing = PlayState.Loading;
                }
            })
            .addCase(togglePlay.fulfilled, (state, { payload }) => {
                state.playing = payload;
            })
            .addCase(togglePlay.rejected, (state, { error }) => {
                if (error.message !== undefined) {
                    console.error(`Could not play music: ${error.message}`);
                }

                state.playing = PlayState.Paused;
            });
    },
});

export const togglePlay = createAsyncThunk<
    PlayState,
    null,
    { dispatch: AppDispatch; state: RootState }
>("musicPlayer/togglePlay", player.togglePlay, {
    condition: (_, { getState }) => {
        const playing = getState().musicPlayer.playing;

        if (playing == PlayState.Loading) {
            // Block updates when we're loading
            return false;
        }
    },
});

export const { setSource } = musicPlayerSlice.actions;
export { PlayState, player as musicPlayer };
export type { MusicPlayerState };
export default musicPlayerSlice.reducer;
