/*Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2026 Rainbownapkin and the TTN Community

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.*/

/**
 * Class containing base code for the Canopy channel client.
 */
class channel{
    /**
     * Instantiates a new channel object
     */
    constructor(){
        //Establish connetion to the server via socket.io
        this.connect();
        //Define socket listeners
        this.defineListeners();

        /**
         * Returns true once the ytEmbed API has loaded in from google (eww)
         */
        this.ytEmbedAPILoaded = false;

        /**
         * Current connected channels name
         */
        this.channelName = window.location.pathname.split('/c/')[1].split('/')[0];

        /**
         * Child Video Player object
         */
        this.player = new player(this);

        /**
         * Child Chat Box Object
         */
        this.chatBox = new chatBox(this);

        /**
         * Child User List Object
         */
        this.userList = new userList(this);

        /**
         * Child PM Handler
         */
        this.pmHandler = new pmHandler(this);
        
        /**
         * Child Canopy Panel Object
         */
        this.cPanel = new cPanel(this);

        //Set defaults for any unset settings and run any required process steps for the current config
        this.setDefaults(false, true);

        //Freak out any weirdos who take a peek in the dev console for shits n gigs
        console.log("👁️👄👁️ ℬℴ𝓊𝓃𝒿ℴ𝓊𝓇.");
        //Preach the good word about software which is Free as in Freedom
        console.log(`Did you know Canopy, the software that runs '${utils.ux.getInstanceName()}' is software that is free as in freedom AND free weed?`);
        console.log("This means you can read/modify/redistribute the code, run your own server, and even contribute back!");
        console.log("https://git.ourfore.st/rainbownapkin/canopy");
    }

    /**
     * Handles initial client connection
     */
    connect(){
        const clientOptions = {
                extraHeaders: {
                //Include CSRF token
                'x-csrf-token': utils.ajax.getCSRFToken()
            }
        };

        this.socket = io(clientOptions);
        this.queueBroadcastSocket = io("/queue-broadcast", clientOptions);
        this.pmSocket = io("/pm", clientOptions);
    }

    /**
     * Defines network-related listeners
     */
    defineListeners(){
        this.socket.on("connect", () => {
            document.title = `${this.channelName} - Connected`
        });

        this.socket.on("kick", async (data) => {
            if(data.reason == "Invalid CSRF Token!"){
                //Warn the user
                new canopyUXUtils.popup('Invalid CSRF Token detected, reloading client...');

                //Just reload the fucker
                setTimeout(()=>{location.reload();}, 1000);
            }else{
                new canopyUXUtils.popup(`You have been ${data.type} from the channel for the following reason:<br>${data.reason}`);
            }
        });

        this.socket.on("clientMetadata", this.handleClientInfo.bind(this));

        this.socket.on("error", utils.ux.displayResponseError);

        this.socket.on("lock", (data) => {
            this.queueLock = data.locked;
        });

        this.queueBroadcastSocket.on("queue", (data) => {
            this.queue = new Map(data.queue);
        });
    }

    /**
     * Handles initial client-metadata ingestion from server upon connection
     * @param {Object} data - Data glob from server
     */
    handleClientInfo(data){
        //Ingest user data
        this.user = data.user;

        //Re-hydrate permission maps
        this.user.permMap.site = new Map(data.user.permMap.site);
        this.user.permMap.chan = new Map(data.user.permMap.chan);

        //If we can read the schedule
        if(client.user.permMap.chan.get('readSchedule')){
            //Display the queue icon
            this.chatBox.adminIcon.style.display = "";
        }

        //Tell the chatbox to handle client info 
        //should it have its own event listener instead? Guess it's a stylistic choice :P
        this.chatBox.handleClientInfo(data);

        //Store queue lock status
        this.queueLock = data.queueLock;
    }

    /**
     * Processes and applies default config on any unset settings
     * @param {Boolean} force - Whether or not to forcefully reset already set settings
     * @param {Boolean} processConfig - Whether or not to run the Process Config function once complete
     */
    setDefaults(force = false, processConfig = false){
        //Iterate through default config 
        for(let [key, value] of channel.defaultConfig){
            //If the setting is unset or function was called forcefully
            if(force || localStorage.getItem(key) == null){
                //Set item from default map
                localStorage.setItem(key, value);
            }

            //If we're running process steps for the config
            if(processConfig){
                //Process the current config value
                this.processConfig(key, localStorage.getItem(key));
            }
        }
    }

    /**
     * Run once every config change to ensure settings are properly set
     * @param {String} key - Setting to change
     * @param {*} value - Value to set setting to
     */
    processConfig(key, value){
        //Unfortunately we can't scope constants to switch-cases so this is the best we got if we wanna re-use the name
        let nowPlaying;

        //Switch/case by config key
        switch(key){
            case 'ytPlayerType':
                const embedScript = document.querySelector(".yt-embed-api");
                //If the user is running the embedded player and we don't have en embed script loaded
                if(value == 'embed' && embedScript == null){
                    //Find our footer
                    const footer = document.querySelector('footer');

                    //Create new script tag
                    const ytEmbedAPI = document.createElement('script');
                    //Link the iframe api from youtube
                    ytEmbedAPI.src = "https://www.youtube.com/iframe_api";
                    //set the iframe api script id
                    ytEmbedAPI.classList.add('yt-embed-api');

                    //Append the script tag to the top of the footer to give everything else access
                    footer.prepend(ytEmbedAPI);
                //If we're not using the embed player but the script is loaded
                }else if(embedScript != null){
                    //Pull all scripts since the main one might have pulled others
                    const scripts = document.querySelectorAll('script');

                    //Iterate through all script tags on the page
                    for(let script of scripts){
                        //If the script came from youtube
                        if(script.src.match(/youtube\.com|youtu\.be/)){
                            //Rip it out
                            script.remove();
                        }
                    }
                }

                //If the player or mediaHandler isn't loaded
                if(this.player == null || this.player.mediaHandler == null){
                    //We're fuggin done here
                    return;
                }

                //Get current video
                nowPlaying = this.player.mediaHandler.nowPlaying;

                //If we're playing a youtube video
                if(nowPlaying != null && nowPlaying.type == 'yt'){
                    //Restart the video
                    this.player.hardReload();
                }

                //Stop while we're ahead
                return;

            case 'IACDN':
                //If the player or mediaHandler isn't loaded
                if(this.player == null || this.player.mediaHandler == null){
                    //We're fuggin done here
                    return;
                }

                //Get current video
                nowPlaying = this.player.mediaHandler.nowPlaying;

                //If we're playing a video from Internet Archive
                if(nowPlaying != null && nowPlaying.type == 'ia'){
                    console.log("RELOAD");
                    //Hard reload the media, forcing media handler re-creation
                    this.player.hardReload();
                }

                return;
            case 'syncTolerance':
                //If the player isn't loaded
                if(this.player == null){
                    //We're fuckin' done here
                    return;
                }

                //Set syncronization tolerance
                this.player.syncTolerance = value;
                return;
            case 'liveSyncTolerance':
                //If the player isn't loaded
                if(this.player == null){
                    //We're fuckin' done here
                    return;
                }

                //Set syncronization tolerance
                this.player.streamSyncTolerance = value;
                return;
            case 'syncDelta':
                //If the player isn't loaded
                if(this.player == null){
                    //We're fuckin' done here
                    return;
                }

                //Set syncronization delta
                this.player.syncDelta = value;
                return;
            case 'chatWidthMin':
                //If the chat isn't loaded
                if(this.chatBox == null){
                    //We're fuckin' done here
                    return;
                }

                //Set Chat Box Width minimum while Locked to Aspect-Ratio
                this.chatBox.chatWidthMinimum = value / 100;
                return;
            case 'disablePortrait':
                //If the chat isn't loaded
                if(this.chatBox == null){
                    //We're fuckin' done here
                    return;
                }

                //Toggle portrait mode
                this.chatBox.togglePortrait();
                return;
            case 'userlistHidden':
                //If the userlist class isn't loaded in yet
                if(this.userList == null){
                    //We're fuckin' done here
                    return;
                }

                //Pass value down to UI toggle, making sure to allow for string conversion
                this.userList.toggleUI(!(value == true || value == "true"));
                return;
            case 'cinemaMode':
                //If the userlist class isn't loaded in yet
                if(this.player == null){
                    //We're fuckin' done here
                    return;
                }

                //Pass value down to UI toggle, making sure to allow for string conversion
                this.player.toggleCinemaMode(value == true || value == "true");
                return;
        }
    }

    /**
     * Default channel config
     */
    static defaultConfig = new Map([
        ["ytPlayerType","raw"],
        ["IACDN",""],
        ["syncTolerance",0.4],
        ["liveSyncTolerance", 2],
        ["syncDelta", 6],
        ["chatWidthMin", 20],
        ["userlistHidden", false],
        ["cinemaMode", false],
        ["rxPMSound", 'unread'],
        ["txPMSound", false],
        ["newSeshSound", true],
        ["endSeshSound", true],
        ["disablePortrait", false]
    ]);
}

/**
 * Youtube iframe-embed API entry point
 */
function onYouTubeIframeAPIReady(){
    //Set embed api to true
    client.ytEmbedAPILoaded = true;

    //If the player is ready and has a mediaHandler loaded
    if(client.player != null && client.player.mediaHandler != null){
        //Get currently playing item
        const nowPlaying = client.player.mediaHandler.nowPlaying;

        //If we're playing a youtube video and the official embeds are enabled
        if(nowPlaying.type == 'yt' && localStorage.getItem('ytPlayerType') == "embed"){
            //Restart the video now that the embed api has loaded
            client.player.start({media: nowPlaying});
        }
    }
}

const client = new channel();