/*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 canopyUtils{
    constructor(){
        this.ajax = new canopyAjaxUtils();
        this.ux = new canopyUXUtils();
    }

    //somehow this isn't built in to JS's unescape functions...
    unescapeEntities(string){
        //Create a new DOMParser and tell it to parse string as HTML
        const outNode = new DOMParser().parseFromString(string, "text/html");
        //Grab text content and send that shit out
        return outNode.documentElement.textContent;
    }

    escapeRegex(string){
        /* I won't lie this line was whole-sale ganked from stack overflow like a fucking skid
        In my defense I only did it because browser-devs are taking fucking eons to implement RegExp.escape()
        This should be replaced once that function becomes available in mainline versions of firefox/chromium:
        https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape */
        return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }

    isSameDate(d0, d1){
        return d0.getYear() == d1.getYear() && d0.getMonth() == d1.getMonth() && d0.getDate() == d1.getDate();
    }

    dateWithinRange(min, max, input){
        return min.getTime() < input.getTime() && max.getTime() > input.getTime();
    }
 
}

class canopyUXUtils{
    constructor(){
    } 

    getInstanceName(){
        return document.querySelector("#instance-title").innerText;
    }

    async awaitNextFrame(){
        //return a new promise
        return new Promise((resolve)=>{
            //Before the next frame
             requestAnimationFrame(()=>{
                //fires before next-next frame (after next frame)
                requestAnimationFrame(()=>{
                    //resolve the promise
                    resolve();
                });
            });
        })
    }

    //Useful for pesky date/time input conversions
    localizeDate(date){
        //Apply timezone offset to base (utc) time
        return new Date(date.getTime() - (date.getTimezoneOffset() * 60000));
    }

    //Useful for pesky date/time input conversions
    normalizeDate(date){
        //Convert dates which store local time to utc to dates which store utc as utc
        return new Date(date.getTime() + (date.getTimezoneOffset() * 60000));
    }

    timeStringFromDate(date, displaySeconds = true){
                let outString = ''

                //If scale is over a minute then we don't need to display seconds
                const seconds = displaySeconds ? `:${('0' + date.getSeconds()).slice(-2)}` : ''

                //If we're counting AM
                if(date.getHours() < 12){
                    //Display as AM
                    outString = `${('0'+date.getHours()).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}${seconds}AM`
                //If we're cointing noon
                }else if(date.getHours() == 12){
                    //display as noon
                    outString = `${('0'+date.getHours()).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}${seconds}PM`
                //if we're counting pm
                }else{
                    //display as pm
                    outString = `${('0'+(date.getHours() - 12)).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}${seconds}PM`
                }

                return outString;
    }

    humieFriendlyDuration(seconds, compact = false){
        //If we have an invalid duration
        if(seconds <= 0){
            //bitch, moan, and complain!
            return('Invalid input duration');
        }

        //Create an empty array to hold the time strings
        const timeStrings = [];

        //Pull hours from time
        const hours = Math.floor(seconds / 3600);
        //Remove recorded hours
        seconds -= hours * 3600;

        //Pull minutes from time
        const minutes = Math.floor(seconds / 60);
        //Remove recorded minutes
        seconds -= minutes * 60;

        //If we're rendering compact, alarm-clock style duration
        if(compact){
            if(hours > 0){
                //Push hours, pad start with 0
                timeStrings.push(String(hours).padStart(2, '0'));
            }

            if(hours > 0 || minutes > 0){
                //Push minutes, pad start with 0
                timeStrings.push(String(minutes).padStart(2, '0'));
            }

            if(hours > 0 || minutes > 0 || seconds > 0){
                //Push seconds, pre-fix a 00: if hours and minutes are empty, round to nearest int and pad start with 0
                timeStrings.push(`${(hours == 0 && minutes == 0) ? '00:' : ''}${String(Math.round(seconds)).padStart(2, '0')}`);
            }

            return timeStrings.join(':');
        //If we're rendering out using our words
        }else{
            //If we have an hour
            if(hours == 1){
                //Add the string
                timeStrings.push('1 Hour');
            //If we have hours
            }else if(hours > 0){
                //Add the string
                timeStrings.push(`${hours} Hours`);
            }

            //If we have a minute
            if(minutes == 1){
                //Add the string
                timeStrings.push('1 Minute');
            //If we have minutes
            }else if(minutes > 0){
                //Add the string
                timeStrings.push(`${minutes} Minutes`);
            }

            //Add the 'and ' if we need it
            const secondsPrefix = timeStrings.length > 0 ? 'and ' : '';

            //If we have a second
            if(seconds == 1){
                //Add the string
                timeStrings.push(`${secondsPrefix}1 Second`);
            //If we have more than a second
            }else if(seconds > 1){
                //Add the string
                timeStrings.push(`${secondsPrefix}${Math.round(seconds)} Seconds`);
            } 

            //Join the time strings together
            return timeStrings.join(', ');
        }
    }

    //Update this and popup class to use nodes
    //and display multiple errors in one popup
    displayResponseError(body){
        try{
            const errors = body.errors;
            errors.forEach((err)=>{
                if(err.msg == "invalid csrf token"){
                    //reload CSRF token
                    utils.ajax.reloadCSRFToken();

                    new canopyUXUtils.popup(`<h3>CSRF Error:</h3><p><br>Bad CSRF token, try again!</p>`);
                }else{
                    //Capitalize the first letter of the type
                    const type = `${err.type[0].toUpperCase()}${err.type.slice(1)}`

                    //Display our error
                    new canopyUXUtils.popup(`<h3>${type} Error:</h3><p><br>${err.msg}</p>`);
                }
            });
        }catch(err){
            console.error("Display Error Body:");
            console.error(body);
            console.error("Display Error Error:");
            console.error(err);
        }
    }

    displayTooltip(event, content, ajaxTooltip, cb, soft = false, doc = document){
        //Create the tooltip
        const tooltip = new canopyUXUtils.tooltip(content, ajaxTooltip, ()=>{
            //If this is an ajax tooltip
            if(ajaxTooltip){
                //Call mouse move again after ajax load to re-calculate position within context of the new content
                tooltip.moveToMouse(event);
            }

            //If we have a callback function
            if(typeof cb == "function"){
                //Call async callback
                cb();
            }
        }, doc);

        //Move the tooltip with the mouse
        event.target.addEventListener('mousemove', tooltip.moveToMouse.bind(tooltip));

        //Do intial mouse move
        tooltip.moveToMouse(event);

        //remove the tooltip on mouseleave
        event.target.addEventListener('mouseleave', tooltip.remove.bind(tooltip));

        //Kill tooltip with parent
        doc.body.addEventListener('mousemove', killWithParent);

        if(soft){
            //remove the tooltip on context menu open
            event.target.addEventListener('mousedown', tooltip.remove.bind(tooltip));
            event.target.addEventListener('contextmenu', tooltip.remove.bind(tooltip));
        }

        function killWithParent(){
            //If the tooltip parent no longer exists
            if(!event.target.isConnected){
                //Kill the whole family :D
                tooltip.remove();
            }
        }
    }

    displayContextMenu(event, title, menuMap, doc){
        event.preventDefault();

        //Create context menu
        const contextMenu = new canopyUXUtils.contextMenu(title, menuMap, doc);

        //Move context menu to mouse
        contextMenu.moveToMouse(event);
    }


    //We should really move this over to uxutils along with newrow & newtable functions
    newTableCell(content, firstCol = false){
        //Create a new 'td' element
        const cell = document.createElement('td');

        //If it's not the first column, mention it!
        if(!firstCol){
            cell.classList.add("not-first-col");
        }

        //check for arrays
        if(content.forEach == null){
            //add single items
            addContent(content);
        }else{
            //Crawl through content array
            content.forEach((item)=>{
                //add each item
                addContent(item);
            });
        }

        //return the resulting cell
        return cell;


        function addContent(ct){
            //If we're adding as node
            if(ct.cloneNode != null){
                //append it like it's a node
                cell.appendChild(ct);
            }else{
                //otherwise use it as innerHTML
                cell.innerHTML = ct;
            }
        }

    }

    playSound(url){
        const audio = new Audio(url);
        audio.play();
    }

    newTableRow(cellContent){
        //Create an empty table row to hold the cells
        const entryRow = document.createElement('tr');

        entryRow.classList.add("gen-row");

        entryRow.appendChild(this.newTableCell(cellContent[0], true));

        cellContent.forEach((content, i)=>{
            if(i > 0){
                entryRow.append(this.newTableCell(content));
            }
        });
    
        return entryRow;
    }

    static tooltip = class{
        constructor(content, ajaxTooltip = false, cb, doc = document){
            //Define non-tooltip node values
            this.content = content;
            this.ajaxPopup = ajaxTooltip;
            this.cb = cb;
            this.id = Math.random();
            this.doc = doc;

            //create and append tooltip
            this.tooltip = document.createElement('div');
            this.tooltip.classList.add('tooltip');

            //Display tooltip even if it's not loaded to prevent removal of unloaded tooltips
            this.displayTooltip();

            //Fill the tooltip
            this.fillTooltipContent();
        }

        async fillTooltipContent(){
            if(this.ajaxPopup){
                this.tooltip.textContent = "Loading tooltip..."
                this.tooltip.innerHTML = await utils.ajax.getTooltip(this.content);
            }else{
                //If the content we received is a string
                if(typeof this.content == "string"){
                    //Use it
                    this.tooltip.innerHTML = this.content;
                //Otherwise
                }else{
                    //Append it as a node
                    this.tooltip.appendChild(this.content);
                }
            }

            if(this.cb){
                //Callbacks are kinda out of vogue, but there really isn't a good way to handle asynchronously constructed objects/classes
                this.cb();
            }
        }

        displayTooltip(){
            this.doc.body.appendChild(this.tooltip);
        }

        moveToPos(x,y){
            //If the distance between the left edge of the window - the window width is more than the width of our tooltip
            if((this.doc.defaultView.innerWidth - (this.doc.defaultView.innerWidth - x)) > this.tooltip.getBoundingClientRect().width){
                //Push it to the right edge of the cursor, where the hard edge typically is
                this.tooltip.style.right = `${this.doc.defaultView.innerWidth - x}px`;
                this.tooltip.style.left = '';
            //otherwise, if we're close to the edge
            }else{
                //push it away from the edge of the screen
                this.tooltip.style.right = ''
                this.tooltip.style.left = `${x}px`
            }


            //If the distance between the top edge of the window - the window height is more than the heigt of our tooltip
            if((this.doc.defaultView.innerHeight - (this.doc.defaultView.innerHeight - y)) > this.tooltip.getBoundingClientRect().height){
                //Push it above the mouse
                this.tooltip.style.bottom = `${this.doc.defaultView.innerHeight - y}px`;
                this.tooltip.style.top = '';
            //otherwise if we're close to the edge
            }else{
                //Push it below the mouse to avoid the top edge of the screen
                //-50px to account for a normal sized cursor
                this.tooltip.style.bottom = '';
                this.tooltip.style.top = `${y+15}px`;
            }

        }

        moveToMouse(event){
            this.moveToPos(event.clientX, event.clientY)
        }

        remove(){
            this.tooltip.dispatchEvent(new Event("close"));
            this.tooltip.remove();
        }
    }

    static contextMenu = class extends this.tooltip{
        constructor(title, menuMap, doc = document){
            //Call inherited tooltip constructor
            super('Loading Menu...', false, null, doc);
            //Set tooltip class
            this.tooltip.classList.add('context-menu');

            //Set title and menu map
            this.title = title;
            this.menuMap = menuMap;

            this.constructMenu();
        }

        constructMenu(){
            //Clear out tooltip
            this.tooltip.innerHTML = '';

            //Create menu title
            const menuTitle = document.createElement('h2');
            menuTitle.textContent = utils.unescapeEntities(this.title);

            //Append the title to the tooltip
            this.tooltip.append(menuTitle);

            for(let choice of this.menuMap){
                //Create button
                const button = document.createElement('button');
                button.textContent = utils.unescapeEntities(choice[0]);

                //Add event listeners
                button.addEventListener('click', choice[1]);

                //Append the button to the menu div
                this.tooltip.appendChild(button);
            }

            //Create event listener to remove tooltip whenever anything is clicked, inside or out of the menu
            //Little hacky but we have to do it a bit later to prevent the opening event from closing the menu
            setTimeout(()=>{this.doc.body.addEventListener('click', this.remove.bind(this));}, 1);
            setTimeout(()=>{this.doc.body.addEventListener('contextmenu', this.remove.bind(this));}, 1);
        }
    }

    static popup = class{
        constructor(content, ajaxPopup = false, cb, doc = document, keyClose = true){
            //Define non-popup node values
            this.content = content;
            this.ajaxPopup = ajaxPopup;
            this.cb = cb;
            this.doc = doc;
            this.keyClose = keyClose

            //define popup nodes
            this.createPopup();

            //fill popup nodes
            this.fillPopupContent();
        }

        createPopup(){
            //Check if another popup has already thrown a backer up
            if(this.doc.querySelector('.popup-backer') == null){
                //Create popup backer
                this.popupBacker = document.createElement('div');
                this.popupBacker.classList.add('popup-backer');
            }

            //Create popup
            this.popupDiv = document.createElement('div');
            this.popupDiv.classList.add('popup-div');

            //Create close icon
            this.closeIcon = document.createElement('i');
            this.closeIcon.classList.add('bi-x','popup-close-icon');
            this.closeIcon.addEventListener("click", this.closePopup.bind(this));

            //Create content div
            this.contentDiv = document.createElement('div');
            this.contentDiv.classList.add('popup-content-div');

            //Append popup innards
            this.popupDiv.appendChild(this.closeIcon);
            this.popupDiv.appendChild(this.contentDiv);

            //If we're closing on keydown
            if(this.keyClose){
                //Bit hacky but the only way to remove an event listener while keeping the function bound to this
                //Isn't javascript precious?
                this.keyClose = ((event)=>{
                    //If we hit enter or escape
                    if(event.key == "Enter" || event.key == "Escape"){
                        //Close the pop-up
                        this.closePopup();
                        //Remove this event listener
                        this.doc.removeEventListener('keydown', this.keyClose);
                    }
                }).bind(this);

                //Add event listener to close popup when enter is hit
                this.doc.addEventListener('keydown', this.keyClose);
            }
        }

        async fillPopupContent(){
            if(this.ajaxPopup){
                this.contentDiv.innerHTML = await utils.ajax.getPopup(this.content);
            }else{
                this.contentDiv.innerHTML = this.content;
                this.contentDiv.classList.add('popup-plaintext-content');
            }

            //display popup nodes
            this.displayPopup();

            if(this.cb){
                //Callbacks are kinda out of vogue, but there really isn't a good way to handle asynchronously constructed objects/classes
                this.cb();
            }
        }

        displayPopup(){
            //Blur active element that probably caused the popup
            this.doc.activeElement.blur();

            //display the popup
            this.doc.body.prepend(this.popupDiv);

            //if we created a popup backer
            if(this.popupBacker != null){
                //display the popup backer
                this.doc.body.prepend(this.popupBacker);
            }
        }

        closePopup(){
            this.popupDiv.dispatchEvent(new Event("close"));

            //Take out the popup
            this.popupDiv.remove();

            //Look for the backer instead of using the object property since the bitch mighta been created by someone else
            const foundBacker = this.doc.querySelector('.popup-backer');

            //if there aren't any more popups
            if(document.querySelector('.popup-div') == null && foundBacker != null){
                //Take out the backer
                foundBacker.remove();
            }
        }
    }

    static clickDragger = class{
        constructor(handle, element, leftHandle = true, parent, flex = true){
            //Pull needed nodes
            this.handle = document.querySelector(handle);
            this.element = document.querySelector(element);

            //True while dragging
            this.dragLock = false;

            //Come to the ~~dark~~ left side
            this.leftHandle = leftHandle

            //True when the user is actively breaking the screen by dragging too far off to the side
            this.breakingScreen = false;

            //we put a click dragger in yo click dragger so you could click and drag while you click and drag
            this.parent = parent;

            //Whether or not click dragger is in a flexbox
            this.flex = flex;

            //Create boolean to allow parent classes to suspend click dragging behavior
            this.enabled = true;

            //Setup our event listeners
            this.setupInput();
        } 

        setupInput(){
            this.handle.addEventListener("mousedown", this.startDrag.bind(this));
            this.element.parentElement.addEventListener("mouseup", this.endDrag.bind(this));
            this.element.parentElement.addEventListener("mousemove", this.drag.bind(this));
        }

        startDrag(event){
            //we are now dragging
            this.dragLock = true;

            //Keep user from selecting text or changing cursor as we drag
            window.document.body.style.userSelect = "none";
            window.document.body.style.cursor = "ew-resize";

            //Dispatch click drag start event through handle
            this.handle.dispatchEvent(new CustomEvent("clickdragstart"));
        }

        endDrag(event){
            //we're no longer dragging
            this.dragLock = false;
            //This is only relevant when dragging, keeping this on can cause issues after content changes
            this.breakingScreen = false;

            //Return cursor to normal, and allow user to select text again
            window.document.body.style.userSelect = "auto";
            window.document.body.style.cursor = "auto";

            //Dispatch click drag end event through handle
            this.handle.dispatchEvent(new CustomEvent("clickdragend"));
        }

        drag(event){
            if(this.dragLock && this.enabled){
                //If no buttons are being held down
                if(event.buttons == 0){
                    //end the drag
                    this.endDrag();
                }

                if(this.leftHandle){
                    //get difference between mouse and right edge of element
                    var difference = this.element.getBoundingClientRect().right - event.clientX;
                }else{
                    //get difference between mouse and left edge of element
                    var difference = event.clientX - this.element.getBoundingClientRect().left;
                }

                //check if we have a scrollbar because we're breaking shit, make sure to round actual width so we don't break when zoomed in
                var pageBreak = document.body.scrollWidth - Math.round(document.body.getBoundingClientRect().width);

                //if we're not breaking the page, or we're moving left
                if((!this.breakingScreen && pageBreak <= 0) || event.clientX < this.handle.getBoundingClientRect().left){
                    //Apply difference to width
                    if(this.flex){
                        this.element.style.flexBasis = `${this.calcWidth(difference)}vw`;
                    }
                    //I know it's kludgy to apply this along-side flex basis but it fixes some nasty bugs with nested draggers
                    //Don't @ me, it's not like i'm an actual web developer anyways  :P
                    this.element.style.width = `${this.calcWidth(difference)}vw`;
                   
                    //If we let go here, the width isn't breaking anything so there's nothing to fix.
                    this.breakingScreen = false;
                }else{
                    //call fixCutoff with standalone mode off, and a pre-calculated pageBreak
                    if(!this.breakingScreen){
                        this.fixCutoff(false, pageBreak);
                    }
                }

                //Dispatch click drag event through handle
                this.handle.dispatchEvent(new CustomEvent("clickdrag"));
            }
        }

        calcWidth(px){
            //I cannot fucking believe they added a clamp function to CSS before JS...
            const width = (px / window.innerWidth) * 100;

            //Little hacky but this ensures that if our click dragger tries to do something that it should absolutely never be doing
            //it'll be told to sit down, shut up, and fuck off.
            return width < 100 ? width : 20;
        }


        fixCutoff(standalone = true, pageBreak){
            //If the click dragger isn't enable
            if(!this.enabled){
                //FUCK OFF AND DIE
                return;
            }

            //If we have no pagebreak
            if(pageBreak == null){
                //If we have a document body
                if(document.body != null){
                    pageBreak = document.body.scrollWidth - document.body.getBoundingClientRect().width
                //Otherwise
                }else{
                    //Pretend nothing happened because we probably have bigger issues then a fucked up click-dragger cutoff
                    return;
                }
            }

            //Fix the page width
            if(this.flex){
                this.element.style.flexBasis = `${this.calcWidth(this.element.getBoundingClientRect().width + pageBreak)}vw`;
            }
            this.element.style.width = `${this.calcWidth(this.element.getBoundingClientRect().width + pageBreak)}vw`;

            //If we're calling this outside of drag() (regardless of draglock unless set otherwise)
            if(standalone){
                //call end drag to finish the job
                this.endDrag();
            }else{
                //We should let the rest of the object know we're breaking the screen
                this.breakingScreen = true;
            }

            //If we have a parent dragger
            if(this.parent != null){
                //Make sure to fix it's cutoff too
                this.parent.fixCutoff(standalone);
            }
        }
    }
}

class canopyAjaxUtils{

    constructor(){

    }

    //Account
    async register(user, pass, passConfirm, email, verification){
        var response = await fetch(`/api/account/register`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                //It's either this or find and bind all event listeners :P
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            body: JSON.stringify(email ? {user, pass, passConfirm, email, verification} : {user, pass, passConfirm, verification})
        });

        if(response.ok){
            location = "/";
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async migrate(user, oldPass, newPass, passConfirm, verification){
        var response = await fetch(`/api/account/migrate`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                //It's either this or find and bind all event listeners :P
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            body: JSON.stringify({user, oldPass, newPass, passConfirm, verification})
        });

        if(response.ok){
            location = "/";
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async login(user, pass, rememberMe, verification){
        const response = await fetch(`/api/account/login`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            body: JSON.stringify(verification ? {user, pass, rememberMe, verification} : {user, rememberMe, pass})
        });

        if(response.ok){
            location.reload();
        }else if(response.status == 429){
            location = `/login?user=${user}`;
        }else if(response.status == 301){
            /*
            * So this is gross but I don't know that theres a better way to do this
            * Reloading the page would mean either sending the pass to the server as a URL query string which is insecure
            * Or the server pre-loading it from the request, however sending passwords back to users seems like a bad idea too, even if it's just an echo
            * Using fetch API to  load the page assets in dynamically fucks up too, because register.js waits for DOM to load
            * 
            * We could try an iframe and inject the password into that, however that seems really fucking dirty
            * Sometimes it might just be better to make the user re-enter it...
            */
            location = `/migrate?user=${user}`;
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async logout(){
        var response = await fetch(`/api/account/logout`,{
            method: "POST",
            headers: {
                "x-csrf-token": utils.ajax.getCSRFToken()
            }
        });

        if(response.ok){
            location.reload();
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async updateProfile(update){
        const response = await fetch(`/api/account/update`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            body: JSON.stringify(update)
        });

        if(response.ok){
            return await response.json();
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async getRankEnum(){
        var response = await fetch(`/api/account/rankEnum`,{
            method: "GET"
        });

        if(response.ok){
            return (await response.json())
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async deleteAccount(pass){
        const response = await fetch(`/api/account/delete`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            body: JSON.stringify({pass})
        });

        if(response.ok){
            window.location.pathname = '/';
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async requestPasswordReset(user, verification){
        const response = await fetch(`/api/account/passwordResetRequest`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            body: JSON.stringify({user, verification})
        });

        //If we received a successful response
        if(response.ok){
            //Create pop-up
            const popup = new canopyUXUtils.popup("A password reset link has been sent to the email associated with the account requested assuming it has one!");
            //Go to home-page on pop-up closure
            popup.popupDiv.addEventListener("close", ()=>{window.location = '/'});
        //Otherwise
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async resetPassword(token, pass, confirmPass, verification){
        const response = await fetch(`/api/account/passwordReset`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            body: JSON.stringify({token, pass, confirmPass, verification})
        });

        //If we received a successful response
        if(response.ok){
            //Create pop-up
            const popup = new canopyUXUtils.popup("Your password has been reset, and all devices have been logged out of your account!");
            //Go to home-page on pop-up closure
            popup.popupDiv.addEventListener("close", ()=>{window.location = '/'});
        //Otherwise
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async requestEmailChange(email, pass){
        const response = await fetch(`/api/account/emailChangeRequest`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            body: JSON.stringify({email, pass})
        });

        //If we received a successful response
        if(response.ok){
            const popup = new canopyUXUtils.popup("A confirmation link has been sent to the new email address.");
        //Otherwise
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    //Channel
    async newChannel(name, description, thumbnail, verification){
        var response = await fetch(`/api/channel/register`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            body: JSON.stringify(thumbnail ? {name, description, thumbnail, verification} : {name, description, verification})
        });

        if(response.ok){
            location = `/c/${name}/settings`;
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async setChannelThumbnail(chanName, thumbnail){
        var response = await fetch(`/api/channel/thumbnail`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            body: JSON.stringify({chanName, thumbnail})
        });

        if(response.ok){
            return await response.json();
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async setChannelDescription(chanName, description){
        var response = await fetch(`/api/channel/description`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            body: JSON.stringify({chanName, description})
        });

        if(response.ok){
            return await response.json();
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async setChannelSetting(chanName, settingsMap){
        var response = await fetch(`/api/channel/settings`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            //Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
            body: JSON.stringify({chanName, settingsMap: Object.fromEntries(settingsMap)})
        });

        if(response.ok){
            return await response.json();
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async setChannelPermissions(chanName, permissionsMap){
        var response = await fetch(`/api/channel/permissions`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            //Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
            body: JSON.stringify({chanName, channelPermissionsMap: Object.fromEntries(permissionsMap)})
        });

        if(response.ok){
            return await response.json();
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async getChannelRank(chanName){
        var response = await fetch(`/api/channel/rank?chanName=${chanName}`,{
            method: "GET"
        });

        if(response.ok){
            return (await response.json())
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async setChannelRank(chanName, user, rank){
        var response = await fetch(`/api/channel/rank`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            body: JSON.stringify({chanName, user, rank})
        });

        if(response.ok){
            return await response.json();
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async getChanBans(chanName){
        var response = await fetch(`/api/channel/ban?chanName=${chanName}`,{
            method: "GET",
            headers: {
                "Content-Type": "application/json"
            }
        });

        if(response.ok){
            return await response.json();
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async chanBan(chanName, user, expirationDays, banAlts){
        var response = await fetch(`/api/channel/ban`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            body: JSON.stringify({chanName, user, expirationDays, banAlts})
        });

        if(response.ok){
            return await response.json();
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async chanUnban(chanName, user){
        var response = await fetch(`/api/channel/ban`,{
            method: "DELETE",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            body: JSON.stringify({chanName, user})
        });

        if(response.ok){
            return await response.json();
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async getChanTokes(chanName){
        var response = await fetch(`/api/channel/tokeCommand?chanName=${chanName}`,{
            method: "GET",
            headers: {
                "Content-Type": "application/json"
            }
        });

        if(response.ok){
            return await response.json();
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async addChanToke(chanName, command){
        var response = await fetch('/api/channel/tokeCommand',{
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()

            },
            body: JSON.stringify({chanName, command})
        });

        if(response.ok){
            return await response.json();
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async deleteChanToke(chanName, command){
        var response = await fetch('/api/channel/tokeCommand',{
            method: "DELETE",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            body: JSON.stringify({chanName, command})
        });

        if(response.ok){
            return await response.json();
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async getChanEmotes(chanName){
        var response = await fetch(`/api/channel/emote?chanName=${chanName}`,{
            method: "GET",
            headers: {
                "Content-Type": "application/json"
            }
        });

        if(response.ok){
            return await response.json();
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async addChanEmote(chanName, emoteName, link){
        var response = await fetch('/api/channel/emote',{
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            body: JSON.stringify({chanName, emoteName, link})
        });

        if(response.ok){
            return await response.json();
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async deleteChanEmote(chanName, emoteName){
        var response = await fetch('/api/channel/emote',{
            method: "DELETE",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            body: JSON.stringify({chanName, emoteName})
        });

        if(response.ok){
            return await response.json();
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    async deleteChannel(chanName, confirm){
        var response = await fetch(`/api/channel/delete`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-csrf-token": utils.ajax.getCSRFToken()
            },
            body: JSON.stringify({chanName, confirm})
        });

        if(response.ok){
            location = "/";
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

 
    //Popup
    async getPopup(popup){
        var response = await fetch(`/popup/${popup}.html`,{
            method: "GET"
        });

        if(response.ok){
            return (await response.text())
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    //Tooltip
    async getTooltip(tooltip){
        var response = await fetch(`/tooltip/${tooltip}`,{
            method: "GET"
        });

        if(response.ok){
            return (await response.text())
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

    //Syntatic sugar
    getCSRFToken(){
        return document.querySelector("[name='csrf-token']").content;
    }

    async reloadCSRFToken(){
        //Fetch a new token
        var response = await fetch('/api/refreshToken',{
            method: "GET"
        });

        if(response.ok){
            //Get data from fetch
            let data = await response.json()
            //Inject new token into the page
            document.querySelector("[name='csrf-token']").content = data.token;
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

}

const utils = new canopyUtils()