/*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 representing the settings panel
 * @extends panelObj
 */
class pmPanel extends panelObj{
    /**
     * Instantiates a new Panel Object
     * @param {channel} client - Parent client Management Object
     * @param {Document} panelDocument - Panel Document
     */
    constructor(client, panelDocument, startSesh){
        super(client, "Private Messaging", "/panel/pm", panelDocument);

        /**
         * String to hold name of currently active sesh
         */
        this.activeSesh = "";

        /**
         * Random UUID to identify the panel with the pmHandler
         */
        this.uuid = crypto.randomUUID();

        /**
         * PM TX Sound
         */
        this.txSound = '/nonfree/imsend.ogg';

        /**
         * Message Buffer Scroll Top on last scroll
         */
        this.lastPos = 0;

        /**
         * Height of Message Buffer on last scroll
         */
        this.lastHeight = 0;

        /**
         * Width of Message Buffer on last scroll
         */
        this.lastWidth = 0;

        /**
         * Whether or not auto-scroll is enabled
         */
        this.autoScroll = true;

        /**
         * re-occuring auto-scroll call
         * cope for the fact that postprocessedMessage objects in renderMessage aren't throwing load events
         */
        this.scrollInterval = setInterval(this.handleAutoScroll.bind(this), 200);

        //Tell PMHandler to start tracking this panel
        this.client.pmHandler.panelList.set(this.uuid, null);

        //Define network related listeners
        this.defineListeners();

        //If a start sesh was provided
        if(startSesh != null && startSesh != ""){
            //Send message out to server
            this.client.pmSocket.emit("pm", {
                recipients: startSesh.split(" "),
                msg: ""
            });
        }
    }

    closer(){
        //Tell PMHandler to stop tracking this panel
        this.client.pmHandler.panelList.delete(this.uuid);

        //Clear the scroll interval
        clearInterval(this.scrollInterval);

        //Run derived closer
        super.closer();
    }

    async docSwitch(){
        //Call derived method
        super.docSwitch();

        this.startSeshButton = this.panelDocument.querySelector('#pm-panel-start-sesh');
        this.seshList = this.panelDocument.querySelector('#pm-panel-sesh-list');
        this.seshBuffer = this.panelDocument.querySelector('#pm-panel-sesh-buffer');
        this.seshPrompt = this.panelDocument.querySelector('#pm-panel-message-prompt');
        this.seshSendButton = this.panelDocument.querySelector('#pm-panel-send-button');

        //reset auto-scroll
        this.autoScroll = true;

        this.setupInput();

        this.renderSeshList();

        //If we have an active sesh
        if(this.activeSesh != null && this.activeSesh != ""){
            //Render messages
            this.renderMessages();
        }
    }

    /**
     * Defines network related event listeners
     */
    defineListeners(){
        this.client.pmSocket.on("message", this.handlePM.bind(this));
        this.client.pmSocket.on("sent", this.handlePM.bind(this));
    }

    /**
     * Defines input-related event handlers
     */
    setupInput(){
        this.startSeshButton.addEventListener('click', this.startSesh.bind(this));
        this.seshPrompt.addEventListener("keydown", this.send.bind(this));
        this.seshSendButton.addEventListener("click", this.send.bind(this));
        this.seshBuffer.addEventListener('scroll', this.scrollHandler.bind(this));
        this.ownerDoc.defaultView.addEventListener('resize', this.handleAutoScroll.bind(this));
    }

    startSesh(event){
        new startSeshPopup(event, this.client, this.renderSeshList.bind(this), this.ownerDoc);
    }

    handlePM(data){
        const nameObj = pmHandler.genSeshName(data);

        //If this message is for the active sesh
        if(nameObj.name == this.activeSesh){
            //Render out the newest message
            this.renderMessage(data);
        }else{
            //Re-render out the sesh list
            this.renderSeshList();
        }
    }

    /**
     * sends private message from sesh prompt to server
     * @param {Event} event - Event passed down from Event Handler
     */
    send(event){
        if((!event || !event.key || event.key == "Enter") && this.seshPrompt.value && this.activeSesh != ''){
            //Pull current sesh from sesh list
            const sesh = this.client.pmHandler.seshList.get(this.activeSesh);

            //Preprocess message from prompt
            const preprocessedMessage = this.client.chatBox.commandPreprocessor.preprocess(this.seshPrompt.value);

            //If preprocessedMessage had it's send flag thrown as false
            if(preprocessedMessage != false){
                //Stick recipients into the pre-processed message
                preprocessedMessage.recipients = sesh.recipients;

                //Send message out to server
                this.client.pmSocket.emit("pm", preprocessedMessage);

                if(localStorage.getItem('txPMSound') == 'true'){
                    utils.ux.playSound(this.txSound);
                }
            }

                //Clear our prompt
                this.seshPrompt.value = "";
        }
    }

    /**
     * Render out current sesh array to sesh list UI
     */
    renderSeshList(){
        //If we don't have a sesh list
        if(this.seshList == null){
            //Fuck off, you're not even done building the object yet.
            return;
        }

        //Clear out the sesh list
        this.seshList.innerHTML = "";

        //Assemble temporary array from client PM Handler sesh list
        const seshList = Array.from(this.client.pmHandler.seshList);

        //If we have existing sessions and no active sesh
        if(this.activeSesh == "" && seshList[0] != null){
            //Enable UI elements
            this.seshPrompt.disabled = false;
            this.seshSendButton.disabled = false;

            //Render out messages
            this.renderMessages();

            //Set the first one as active
            this.activeSesh = seshList[0][1].id;

            //Tell PMHandler what sesh we have open for notification reasons
            this.client.pmHandler.readSesh(this.uuid, this.activeSesh);
        }

        //For each session tracked by the pmHandler
        for(const seshEntry of seshList){
            this.renderSeshListEntry(seshEntry[1]);
        }
    }

    /**
     * Renders out a given messaging sesh to the sesh list UI
     */
    renderSeshListEntry(sesh){
        //Create container div
        const entryDiv = document.createElement('div');
        //Set conatiner div classes
        entryDiv.classList.add('pm-panel-sesh-list-entry','interactive');
        //Set dataset sesh name
        entryDiv.dataset.id = sesh.id;

        //If the current entry is the active sesh
        if(sesh.id == this.activeSesh){
            //mark it as such
            entryDiv.classList.add('positive');
        //If it contains something unread
        }else if(sesh.unread){
            entryDiv.classList.add('positive-afterglow');
        }

        //Create sesh label
        const seshLabel = document.createElement('p');
        //Create human-readable label out of members array
        seshLabel.textContent = utils.unescapeEntities(sesh.id);

        //append sesh label to entry div
        entryDiv.appendChild(seshLabel);

        //Append entry div to sesh list
        this.seshList.appendChild(entryDiv);

        //Add input-related event listener for entry div
        entryDiv.addEventListener('click', this.selectSesh.bind(this));
    }

    selectSesh(event){
        //Attempt to pull previously active sesh item from list
        const wasActive = this.panelDocument.querySelector('.positive');

        //If there was an active sesh
        if(wasActive != null){
            //Remove active sesh class from old item
            wasActive.classList.remove('positive');
        }

        //Pull new active sesh name from target dataset
        this.activeSesh = event.target.dataset.id;

        //Set new sesh as active sesh
        event.target.classList.add('positive');

        //Tell PMHandler what sesh we have open for notification reasons
        this.client.pmHandler.readSesh(this.uuid, this.activeSesh);

        //Reset auto scroll to scroll newly selected sesh down to the bottom
        this.autoScroll = true;

        //Re-render message buffer
        this.renderMessages();

        //Re-Render Sesh List
        this.renderSeshList();
    }

    renderMessages(){
        //Empty out the sesh buffer
        this.seshBuffer.innerHTML = "";

        //Pull sesh from pmHandler
        const sesh = this.client.pmHandler.seshList.get(this.activeSesh);

        //If the sesh is real
        if(sesh != null){
            //for each message in the sesh
            for(const message of sesh.messages){
                //Render out messages to the buffer
                this.renderMessage(message);
            }
        }
    }

    /**
     * Renders message out to PM Panel Message Buffer
     * @param {Object} message - Message to render
     */
    async renderMessage(message){
        //If we have an empty message
        if(message.msg == null || message.msg == ''){
            //BAIL!!
            return;
        }

        //Run postprocessing functions on chat message
        const postprocessedMessage = client.chatBox.chatPostprocessor.postprocess(message, true);

        //Append message to buffer
        this.seshBuffer.appendChild(postprocessedMessage);

        //Auto-scroll buffer on content load
        this.handleAutoScroll();
    }

    /**
     * Handles scrolling within the message buffer
     * @param {Event} event - Event passed down from Event Handler
     */
    scrollHandler(event){
        //If we're just starting out
        if(this.lastPos == 0){
            //Set last pos for the first time
            this.lastPos = this.seshBuffer.scrollTop;
        }

        //Calculate scroll delta
        const deltaY = this.seshBuffer.scrollTop - this.lastPos;

        //Grab visible bounding rect so we don't have to do it again (can't use offset because someone might zoom in :P)
        const bufferRect = this.seshBuffer.getBoundingClientRect();
        const bufferHeight = Math.round(bufferRect.height);
        const bufferWidth = Math.round(bufferRect.width);

        //If last height was unset
        if(this.lastHeight == 0){
            //Set it based on buffer Height
            this.lastHeight = bufferHeight;
        }

        //if last width is unset
        if(this.lastWidth == 0){
            //Set it based on buffer width
            this.lastWidth = bufferWidth;
        }

        //If we're scrolling up
        if(deltaY < 0){
            //If we have room to scroll, and we didn't resize
            if(this.seshBuffer.scrollHeight > bufferHeight && (this.lastWidth == bufferWidth && this.lastHeight == bufferHeight)){
                //Disable auto scrolling
                this.autoScroll = false;
            //We probably resized
            }else{
                this.handleAutoScroll();
            }
        //Otherwise if the difference between the message buffers scroll height and offset height is equal to the scroll top
        //(Because it is scrolled all the way down)
        }else if((this.seshBuffer.scrollHeight - bufferHeight) == this.seshBuffer.scrollTop){
            this.autoScroll = true;
        }

        //Set last post/size for next the run
        this.lastPos = this.seshBuffer.scrollTop;
        this.lastHeight = bufferHeight;
        this.lastWidth = bufferWidth;
    }

    /**
    //  * Auto-scrolls sesh chat buffer when new chats are entered.
     */
    handleAutoScroll(){
        //If autoscroll is enabled
        if(this.autoScroll){
            //Set seshBuffer scrollTop to the difference between scrollHeight and buffer height (scroll to the bottom)
            this.seshBuffer.scrollTop = this.seshBuffer.scrollHeight - Math.round(this.seshBuffer.getBoundingClientRect().height);
        }
    }
}

/**
 * Class representing pop-up dialogue to start a private messaging sesh
 */
class startSeshPopup{
    /**
     * Instantiates a new schedule media Pop-up
     * @param {Event} event - Event passed down from Event Listener
     * @param {channel} client - Parent Client Management Object
     * @param {String} url - URL/link to media to queue
     * @param {String} title - Title of media to queue
     * @param {Function} cb - Callback function, passed upon pop-up creation
     * @param {Document} doc - Current owner documnet of the panel, so we know where to drop our pop-up
     */
    constructor(event, client, cb, doc){
        /**
         * Parent Client Management Object
         */
        this.client = client;
        
        /**
         * Callback function, passed upon pop-up creation
         */
        this.cb = cb;

        //Create media popup and call async constructor when done
        //unfortunately we cant call constructors asyncronously, and we cant call back to this from super, so we can't extend this as it stands :(
        /**
         * canopyUXUtils.popup() object
         */
        this.popup = new canopyUXUtils.popup('/startChatSesh', true, this.asyncConstructor.bind(this), doc);
    }

    /**
     * Continuation of object construction, called after child popup object construction
     */
    asyncConstructor(){
        //Grab required UI elements
        this.startSeshButton = this.popup.contentDiv.querySelector('#pm-sesh-popup-button');
        this.usernamePrompt = this.popup.contentDiv.querySelector('#pm-sesh-popup-prompt');

        //Setup input
        this.setupInput();
    }

    /**
     * Defines input-related Event Handlers
     */
    setupInput(){
        //Setup input
        this.startSeshButton.addEventListener('click', this.startSesh.bind(this));
        this.popup.popupDiv.addEventListener('keydown', this.startSesh.bind(this));
    }

    /**
     * Handles sending request to schedule item to the queue
     * @param {Event} event - Event passed down from Event Listener
     */
    startSesh(event){
        //If we clicked or hit enter
        if(event.key == null || event.key == "Enter"){
            //Send message out to server
            this.client.pmSocket.emit("pm", {
                recipients: this.usernamePrompt.value.split(" "),
                msg: ""
            });

            //If we have a function
            if(typeof this.cb == "function"){
                //Call any callbacks we where given
                this.cb();
            }

            //Close the popup
            this.popup.closePopup();
        }
    }
}