<template>
    <div>
        <div class="info">Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a></div>
        <div class="content" id="app">
            <div v-if="hasLoaded" style="width: 100%">
                <div class="current-song-wrapper">
                    <span class="material-symbols-outlined fancy-view-song-art" v-if="!playingSong.hasCoverArt">music_note</span>
                    <img v-else-if="playingSong.hasCoverArt && playingSong.coverArtOrigin === 'api'" :src="playingSong.coverArtURL" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
                    <img v-else :src="'/getSongCover?filename=' + playingSong.filename" class="fancy-view-song-art" id="current-image">
                    <div class="current-song">
                        <progress max="1000" id="progress" :value="progressBar"></progress>
                        <h1>{{ playingSong.title }}</h1>
                        <p class="dancing-style" v-if="playingSong.dancingStyle">{{ playingSong.dancingStyle }}</p>
                        <p>{{ playingSong.artist }}</p>
                    </div>
                </div>
                <div class="mode-selector-wrapper">
                    <select v-model="visualizationSettings" @change="setVisualization()">
                        <option value="mic">Microphone (Mic access required)</option>
                        <option value="bpm">BPM (might not be 100% accurate)</option>
                        <option value="off">No visualization except background</option>
                    </select>
                </div>
                <div class="song-list-wrapper">
                    <div v-for="song in songQueue" class="song-list">
                        <span class="material-symbols-outlined song-image" v-if="!song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying )">music_note</span>
                        <img v-else-if="song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying ) && song.coverArtOrigin === 'api'" :src="song.coverArtURL" class="song-image">
                        <img v-else-if="song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying ) && song.coverArtOrigin !== 'api'" :src="'/getSongCover?filename=' + song.filename" class="song-image">
                        <div v-if="playingSong.filename === song.filename && isPlaying" class="playing-symbols">
                            <div class="playing-symbols-wrapper">
                                <div class="playing-bar" id="bar-1"></div>
                                <div class="playing-bar" id="bar-2"></div>
                                <div class="playing-bar" id="bar-3"></div>
                            </div>
                        </div>
                        <span class="material-symbols-outlined pause-icon" v-if="!isPlaying && playingSong.filename === song.filename">pause</span>
                        <div class="song-details-wrapper">
                            <h3>{{ song.title }}</h3>
                            <p>{{ song.artist }}</p>
                        </div>
                        <div class="time-until">
                            {{ getTimeUntil( song ) }}
                        </div>
                    </div>
                    <!-- <img :src="" alt=""> -->
                </div>
            </div>
            <div v-else>
                <h1>Loading...</h1>
            </div>
            <div class="background" id="background">
                <div class="beat"></div>
                <div class="beat-manual"></div>
            </div>
        </div>
        <!-- TODO: Get ColorThief either from CDN or preferably as NPM module -->
        <!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.0/color-thief.umd.js"></script> -->
    </div>
</template>


<script setup lang="ts">
    import type { Song } from '@/scripts/song';
    import { computed, ref, type Ref } from 'vue';
    import { ColorThief } from 'colorthief';

    const hasLoaded = ref( false );
    const songs: Ref<Song[]> = ref( [] );
    const playingSong = ref( 0 );
    const isPlaying = ref( false );
    const pos = ref( 0 );
    const colourPalette: string[] = [];
    const progressBar = ref( 0 );
    const timeTracker = ref( 0 );
    const visualizationSettings = ref( 'mic' );
    const micAnalyzer = ref( 0 );
    const beatDetected = ref( false );
    const colorThief = new ColorThief();
    const songQueue = computed( () => {
        let ret = [];
        let pos = 0;
        for ( let song in songs.value ) {
            if ( pos >= playingSong.value ) {
                ret.push( songs.value[ song ] );
            }
            pos += 1;
        }
        return ret;
    } );
    const getTimeUntil = computed( () => {
        return ( song ) => {
            let timeRemaining = 0;
            for ( let i = this.queuePos; i < Object.keys( this.songs ).length - 1; i++ ) {
                if ( this.songs[ i ] == song ) {
                    break;
                }
                timeRemaining += parseInt( this.songs[ i ].duration );
            }
            if ( isPlaying.value ) {
                if ( timeRemaining === 0 ) {
                    return 'Currently playing';
                } else {
                    return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 )  + 'min';
                }
            } else {
                if ( timeRemaining === 0 ) {
                    return 'Plays next';
                } else {
                    return 'Playing less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 )  + 'min after starting to play';
                }
            }
        }
    } );
    methods: {
        startTimeTracker () {
            this.timeTracker = setInterval( () => {
                this.pos = ( new Date().getTime() - this.playingSong.startTime ) / 1000 + this.oldPos;
                this.progressBar = ( this.pos / this.playingSong.duration ) * 1000;
                if ( isNaN( this.progressBar ) ) {
                    this.progressBar = 0;
                }
            }, 100 );
        },
        stopTimeTracker () {
            clearInterval( this.timeTracker );
            this.oldPos = this.pos;
        },
        getImageData() {
            return new Promise( ( resolve, reject ) => {
                if ( this.playingSong.hasCoverArt ) {
                    setTimeout( () => {
                        const img = document.getElementById( 'current-image' );
                        if ( img.complete ) {
                            resolve( this.colorThief.getPalette( img ) );
                        } else {
                            img.addEventListener( 'load', () => {
                                resolve( this.colorThief.getPalette( img ) );
                            } );
                        }
                    }, 500 );
                } else {
                    reject( 'no image' );
                }
            } );
        },
        connect() {
            this.colorThief = new ColorThief();
            let source = new EventSource( '/clientDisplayNotifier', { withCredentials: true } );
            source.onmessage = ( e ) => {
                let data;
                try {
                    data = JSON.parse( e.data );
                } catch ( err ) {
                    data = { 'type': e.data };
                }
                if ( data.type === 'basics' ) {
                    this.isPlaying = data.data.isPlaying ?? false;
                    this.playingSong = data.data.playingSong ?? {};
                    this.songs = data.data.songQueue ?? [];
                    this.pos = data.data.pos ?? 0;
                    this.oldPos = data.data.pos ?? 0;
                    this.progressBar = this.pos / this.playingSong.duration * 1000;
                    this.queuePos = data.data.queuePos ?? 0;
                    this.getImageData().then( palette => {
                        this.colourPalette = palette;
                        this.handleBackground();
                    } ).catch( () => {
                        this.colourPalette = [ { 'r': 255, 'g': 0, 'b': 0 }, { 'r': 0, 'g': 255, 'b': 0 }, { 'r': 0, 'g': 0, 'b': 255 } ];
                        this.handleBackground();
                    } );
                } else if ( data.type === 'pos' ) {
                    this.pos = data.data;
                    this.oldPos = data.data;
                    this.progressBar = data.data / this.playingSong.duration * 1000;
                } else if ( data.type === 'isPlaying' ) {
                    this.isPlaying = data.data;
                    this.handleBackground();
                } else if ( data.type === 'songQueue' ) {
                    this.songs = data.data;
                } else if ( data.type === 'playingSong' ) {
                    this.playingSong = data.data;
                    this.getImageData().then( palette => {
                        this.colourPalette = palette;
                        this.handleBackground();
                    } ).catch( () => {
                        this.colourPalette = [ [ 255, 0, 0 ], [ 0, 255, 0 ], [ 0, 0, 255 ] ];
                        this.handleBackground();
                    } );
                } else if ( data.type === 'queuePos' ) {
                    this.queuePos = data.data;
                }
            };

            source.onopen = () => {
                this.isReconnecting = false;
                this.hasLoaded = true;
            };

            let self = this;
                
            source.addEventListener( 'error', function( e ) {
                if ( e.eventPhase == EventSource.CLOSED ) source.close();

                if ( e.target.readyState == EventSource.CLOSED ) {
                    console.log( 'disconnected' );
                }

                // TODO: Notify about disconnect
                setTimeout( () => {
                    if ( !self.isReconnecting ) {
                        self.isReconnecting = true;
                        self.tryReconnect();
                    }
                }, 1000 );
            }, false );
        },
        tryReconnect() {
            const int = setInterval( () => {
                if ( !this.isReconnecting ) {
                    clearInterval( int );
                } else {
                    connectToSSESource();
                }
            }, 1000 );
        },
        handleBackground() {
            let colourDetails = [];
            let colours = [];
            let differentEnough = true;
            if ( this.colourPalette[ 0 ] ) {
                for ( let i in this.colourPalette ) {
                    for ( let colour in colourDetails ) {
                        const colourDiff = ( Math.abs( colourDetails[ colour ][ 0 ] - this.colourPalette[ i ][ 0 ] ) / 255
                            + Math.abs( colourDetails[ colour ][ 1 ] - this.colourPalette[ i ][ 1 ] ) / 255
                            + Math.abs( colourDetails[ colour ][ 2 ] - this.colourPalette[ i ][ 2 ] ) / 255 ) / 3 * 100;
                        if ( colourDiff > 15 ) {
                            differentEnough = true;
                        }
                    }
                    if ( differentEnough ) {
                        colourDetails.push( this.colourPalette[ i ] );
                        colours.push( 'rgb(' + this.colourPalette[ i ][ 0 ] + ',' + this.colourPalette[ i ][ 1 ] + ',' + this.colourPalette[ i ][ 2 ] + ')' );
                    }
                    differentEnough = false;
                }
            }
            let outColours = 'conic-gradient(';
            if ( colours.length < 3 ) {
                for ( let i = 0; i < 3; i++ ) {
                    if ( colours[ i ] ) {
                        outColours += colours[ i ] + ',';
                    } else {
                        if ( i === 0 ) {
                            outColours += 'blue,';
                        } else if ( i === 1 ) {
                            outColours += 'green,';
                        } else if ( i === 2 ) {
                            outColours += 'red,';
                        }
                    }
                }
            } else if ( colours.length < 11 ) {
                for ( let i in colours ) {
                    outColours += colours[ i ] + ',';
                }
            } else {
                for ( let i = 0; i < 10; i++ ) {
                    outColours += colours[ i ] + ',';
                }
            }
            outColours += colours[ 0 ] ?? 'blue' + ')';

            $( '#background' ).css( 'background', outColours );
            this.setVisualization();
        },
        setVisualization () {
            if ( Object.keys( this.playingSong ).length > 0 ) {
                if ( this.visualizationSettings === 'bpm' ) {
                    if ( this.playingSong.bpm && this.isPlaying ) {
                        $( '.beat' ).show();
                        $( '.beat' ).css( 'animation-duration', 60 / this.playingSong.bpm );
                        $( '.beat' ).css( 'animation-delay', this.pos % ( 60 / this.playingSong.bpm  * this.pos ) + this.playingSong.bpmOffset - ( 60 / this.playingSong.bpm  * this.pos / 2 ) );
                    } else {
                        $( '.beat' ).hide();
                    }
                    try {
                        clearInterval( this.micAnalyzer );
                    } catch ( err ) {}
                } else if ( this.visualizationSettings === 'off' ) {
                    $( '.beat' ).hide();
                    try {
                        clearInterval( this.micAnalyzer );
                    } catch ( err ) {}
                } else if ( this.visualizationSettings === 'mic' ) {
                    $( '.beat-manual' ).hide();
                    try {
                        clearInterval( this.micAnalyzer );
                    } catch ( err ) {}
                    this.micAudioHandler();
                }
            } else {
                console.log( 'not playing yet' );
            }
        },
        micAudioHandler () {
            const audioContext = new ( window.AudioContext || window.webkitAudioContext )();
            const analyser = audioContext.createAnalyser();
            analyser.fftSize = 256;
            const bufferLength = analyser.frequencyBinCount;
            const dataArray = new Uint8Array( bufferLength );

            navigator.mediaDevices.getUserMedia( { audio: true } ).then( ( stream ) => {
                const mic = audioContext.createMediaStreamSource( stream );
                mic.connect( analyser );
                analyser.getByteFrequencyData( dataArray );
                let prevSpectrum = null;
                let threshold = 10; // Adjust as needed
                this.beatDetected = false;
                this.micAnalyzer = setInterval( () => {
                    analyser.getByteFrequencyData( dataArray );
                    // Convert the frequency data to a numeric array
                    const currentSpectrum = Array.from( dataArray );

                    if ( prevSpectrum ) {
                        // Calculate the spectral flux
                        const flux = this.calculateSpectralFlux( prevSpectrum, currentSpectrum );

                        if ( flux > threshold && !this.beatDetected ) {
                            // Beat detected
                            this.beatDetected = true;
                            this.animateBeat();
                        }
                    }
                    prevSpectrum = currentSpectrum;
                }, 20 );
            } );
        },
        animateBeat () {
            $( '.beat-manual' ).stop();
            const duration = Math.ceil( 60 / ( this.playingSong.bpm ?? 180 ) * 500 ) - 50;
            $( '.beat-manual' ).fadeIn( 50 );
            setTimeout( () => {
                $( '.beat-manual' ).fadeOut( duration );
                setTimeout( () => {
                    $( '.beat-manual' ).stop();
                    this.beatDetected = false;
                }, duration );
            }, 50 );
        },
        calculateSpectralFlux( prevSpectrum, currentSpectrum ) {
            let flux = 0;
        
            for ( let i = 0; i < prevSpectrum.length; i++ ) {
                const diff = currentSpectrum[ i ] - prevSpectrum[ i ];
                flux += Math.max( 0, diff );
            }
        
            return flux;
        },
        notifier() {
            if ( parseInt( this.lastDispatch ) + 5000 < new Date().getTime() ) {

            }
            Notification.requestPermission();

            console.warn( '[ notifier ]: Status is now enabled \n\n-> Any leaving or tampering with the website will send a notification to the host' );
            // Detect if window is currently in focus
            window.onblur = () => {
                this.sendNotification( 'blur' );
            }

            // Detect if browser window becomes hidden (also with blur event)
            document.onvisibilitychange = () => {
                if ( document.visibilityState === 'hidden' ) {
                    this.sendNotification( 'visibility' );
                }
            };
        },
        sendNotification( notification ) {
            let fetchOptions = {
                method: 'post',
                body: JSON.stringify( { 'type': notification } ),
                headers: {
                    'Content-Type': 'application/json',
                    'charset': 'utf-8'
                },
            };
            fetch( '/clientStatusUpdate', fetchOptions ).catch( err => {
                console.error( err );
            } );

            new Notification( 'YOU ARE UNDER SURVEILLANCE', { 
                body: 'Please return to the original webpage immediately!',
                requireInteraction: true,
             } )
        }
    },
    mounted() {
        this.connect();
        this.notifier();
        // if ( this.visualizationSettings === 'mic' ) {
        //     this.micAudioHandler();
        // }
    },
    watch: {
        isPlaying( value ) {
            if ( value ) {
                this.startTimeTracker();
            } else {
                this.stopTimeTracker();
            }
        }
    }
} ).mount( '#app' );

</script>