/*Canopy - The next generation of stoner streaming software
Copyright (C) 2024  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();
    }
}

class canopyUXUtils{
    constructor(){
    } 

    //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)=>{
                new canopyUXUtils.popup(`<h3>Server Error:</h3><p><br>${err.msg}</p>`);
            });
        }catch(err){
            console.error("Display Error Error:");
            console.error(body);
            console.error("Display Error Error:");
            console.error(err);
        }
    }


    //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;
            }
        }

    }

    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 popup = class{
        constructor(content, ajaxPopup = false, cb){
            //Define non-popup node values
            this.content = content;
            this.ajaxPopup = ajaxPopup;
            this.cb = cb;
            //define popup nodes
            this.createPopup();

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

        createPopup(){
            this.popupBacker = document.createElement('div');
            this.popupBacker.classList.add('popup-backer');

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

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

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

            this.popupDiv.appendChild(this.closeIcon);
            this.popupDiv.appendChild(this.contentDiv);
        }

        async fillPopupContent(){
            if(this.ajaxPopup){
                this.contentDiv.innerHTML = await utils.ajax.getPopup(this.content);
            }else{
                this.contentDiv.innerHTML = this.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(){
            document.body.prepend(this.popupDiv);
            document.body.prepend(this.popupBacker);
        }

        closePopup(){
            this.popupDiv.remove();
            this.popupBacker.remove();
        }
    }

    static clickDragger = class{
        constructor(handle, element, leftHandle = true, parent){
            //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;

            //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";
        }

        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";
        }

        drag(event){

            if(this.dragLock){
                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
                var pageBreak = document.body.scrollWidth - 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
                    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);
                    }
                }
            }
        }

        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 = document.body.scrollWidth - document.body.getBoundingClientRect().width){
            //Fix the page width
            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(){

    } 

    async register(user, pass, passConfirm, email){
        var response = await fetch(`/api/account/register`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify(email ? {user, pass, passConfirm, email} : {user, pass, passConfirm})
        });

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

    async login(user, pass){
        var response = await fetch(`/api/account/login`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify({user, pass})
        });

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

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

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

    //We need to fix this one to use displayResponseError function
    async updateProfile(update){
        const response = await fetch(`/api/account/update`,{
            method: "POST",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify(update)
        });

        if(response.status == 200){
            return await response.json();
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }

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

        if(response.status == 200){
            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"
            },
            body: JSON.stringify({pass})
        });

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

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

        if(response.status == 200){
            location = "/";
        }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"
            },
            //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.status == 200){
            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"
            },
            //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.status == 200){
            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.status == 200){
            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"
            },
            //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, user, rank})
        });

        if(response.status == 200){
            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"
            },
            body: JSON.stringify({chanName, confirm})
        });

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

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

        if(response.status == 200){
            return (await response.text())
        }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.status == 200){
            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"
            },
            body: JSON.stringify({chanName, user, expirationDays, banAlts})
        });

        if(response.status == 200){
            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"
            },
            body: JSON.stringify({chanName, user})
        });

        if(response.status == 200){
            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.status == 200){
            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"
            },
            body: JSON.stringify({chanName, command})
        });

        if(response.status == 200){
            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"
            },
            body: JSON.stringify({chanName, command})
        });

        if(response.status == 200){
            return await response.json();
        }else{
            utils.ux.displayResponseError(await response.json());
        }
    }
}

const utils = new canopyUtils()