# JSDoc: Source: app/channel/chatHandler.js

# Source: app/channel/chatHandler.js

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

    //NPM imports
    const validator = require('validator')

    //local imports
    const commandPreprocessor = require('./commandPreprocessor');
    const loggerUtils = require('../../utils/loggerUtils');
    const linkUtils = require('../../utils/linkUtils');
    const emoteValidator = require('../../validators/emoteValidator');
    const chat = require('./chat');
    const {userModel} = require('../../schemas/user/userSchema');

    /**
     * Class containing global server-side chat relay logic
     */
    module.exports = class{
        /**
         * Instantiates a chatHandler object
         * @param {channelManager} server - Parent Server Object
         */
        constructor(server){
            //Set server
            this.server = server;
            //Initialize command preprocessor
            this.commandPreprocessor = new commandPreprocessor(server, this)
            //Max chat buffer size
            this.chatBufferSize = 50;
        }

        /**
         * Defines global server-side chat relay event listeners
         * @param {Socket} socket - Requesting Socket
         */
        defineListeners(socket){
            socket.on("chatMessage", (data) => {this.handleChat(socket, data)});
            socket.on("setFlair", (data) => {this.setFlair(socket, data)});
            socket.on("setHighLevel", (data) => {this.setHighLevel(socket, data)});
            socket.on("addPersonalEmote", (data) => {this.addPersonalEmote(socket, data)});
            socket.on("deletePersonalEmote", (data) => {this.deletePersonalEmote(socket, data)});
        }

        /**
         * Handles incoming chat messages from client connections
         * @param {Socket} socket - Socket we're receiving the request from
         * @param {Object} data - Event payload
         */
        handleChat(socket, data){
            this.commandPreprocessor.preprocess(socket, data);
        }

        /**
         * Handles incoming client request to change flair
         * @param {Socket} socket - Socket we're receiving the request from
         * @param {Object} data - Event payload
         */
        async setFlair(socket, data){
            var userDB = await userModel.findOne({user: socket.user.user});

            if(userDB){
                try{
                    //We can take this data raw since our schema checks it against existing flairs, and mongoose sanatizes queries
                    const flairDB = await userDB.setFlair(data.flair);

                    //Crawl through users active connections
                    this.server.crawlConnections(socket.user.user, (conn)=>{
                        //Update flair
                        conn.updateFlair(flairDB.name);
                    });
                }catch(err){
                    return loggerUtils.socketExceptionHandler(socket, err);
                }
            }
        }

        /**
         * Handles incoming client request to change high level
         * @param {Socket} socket - Socket we're receiving the request from
         * @param {Object} data - Event payload
         */
        async setHighLevel(socket, data){
            var userDB = await userModel.findOne({user: socket.user.user});

            if(userDB){
                try{
                    //Floor input to an integer and set high level
                    userDB.highLevel = Math.floor(data.highLevel);
                    //Save user DB Document
                    await userDB.save();

                    //GetConnects across all channels
                    const connections = this.server.getConnections(socket.user.user);

                    //For each connection
                    connections.forEach((conn) => {
                        conn.updateHighLevel(userDB.highLevel);
                    });
                }catch(err){
                    return loggerUtils.socketExceptionHandler(socket, err);
                }
            }
        }

        /**
         * Handles incoming client request to add a personal emote
         * @param {Socket} socket - Socket we're receiving the request from
         * @param {Object} data - Event payload
         */
        async addPersonalEmote(socket, data){
            //Sanatize and Validate input
            const name = emoteValidator.manualName(data.name);
            const link = emoteValidator.manualLink(data.link);

            //If we received good input
            if(link && name){
                //Generate marked link object
                var emote = await linkUtils.markLink(link);

                //If the link we have is an image or video
                if(emote.type == 'image' || emote.type == 'video'){
                    //Get user document from DB
                    const userDB = await userModel.findOne({user: socket.user.user})

                    //if we have a user in the DB
                    if(userDB != null){
                        //Convert marked link into emote object with 1 ez step for only $19.95
                        emote.name = name;

                        //add emote to user document emotes list
                        userDB.emotes.push(emote);

                        //Save user doc
                        await userDB.save();
                    }
                }
            }
        }

        /**
         * Handles incoming client request to delete a personal emote
         * @param {Socket} socket - Socket we're receiving the request from
         * @param {Object} data - Event payload
         */
        async deletePersonalEmote(socket, data){
            //Get user doc from DB based on socket
            const userDB = await userModel.findOne({user: socket.user.user});

            //if we found a user
            if(userDB != null){
                await userDB.deleteEmote(data.name);
            }
        }

        //Base chat functions
        /**
         * Creates a new chatObject and relays the resulting message to the given channel
         * @param {String} user - Originating user
         * @param {String} flair - Flair ID to mark chat with
         * @param {Number} highLevel - High Level to mark chat with
         * @param {String} msg - Message Text Content
         * @param {String} type - Message Type, used for client-side chat post-processing.
         * @param {String} chan - Channel to broadcast message within
         * @param {Array} links - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
         */
        relayChat(user, flair, highLevel, msg, type = 'chat', chan, links){
            this.relayChatObject(chan, new chat(user, flair, highLevel, msg, type, links));
        }

        /**
         * Relays an existing chat object to a channel
         * @param {String} chan - Channel to broadcast message within
         * @param {chat} chat - Chat Object representing the message to broadcast to the given channel
         */
        relayChatObject(chan, chat){
            //Send out chat
            this.server.io.in(chan).emit("chatMessage", chat);

            const channel = this.server.activeChannels.get(chan);

            //If chat buffer length is over mandated size
            if(channel.chatBuffer.buffer.length >= this.chatBufferSize){
                //Take out oldest chat
                channel.chatBuffer.shift();
            }

            //Add buffer to chat
            channel.chatBuffer.push(chat);
        }

        /**
         * Creates a new chatObject and relays the resulting message to the given socket
         * @param {Socket} socket - Socket we're sending a message to (sounds menacing, huh?)
         * @param {String} user - Originating user
         * @param {String} flair - Flair ID to mark chat with
         * @param {Number} highLevel - High Level to mark chat with
         * @param {String} msg - Message Text Content
         * @param {String} type - Message Type, used for client-side chat post-processing.
         * @param {String} chan - Channel to broadcast message within
         * @param {Array} links - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
         */
        relayPrivateChat(socket, user, flair, highLevel, msg, type, links){
            this.relayPrivateChatObject(socket , new chat(user, flair, highLevel, msg, type, links));
        }

        /**
         * Handles incoming client request to delete a personal emote
         * @param {Socket} socket - Socket we're receiving the request from
         * @param {Object} data - Event payload
         */
        relayPrivateChatObject(socket, chat){
            socket.emit("chatMessage", chat);
        }

        /**
         * Creates a new chatObject and relays the resulting message to the entire server
         * @param {String} user - Originating user
         * @param {String} flair - Flair ID to mark chat with
         * @param {Number} highLevel - High Level to mark chat with
         * @param {String} msg - Message Text Content
         * @param {String} type - Message Type, used for client-side chat post-processing.
         * @param {Array} links - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
         */
        relayGlobalChat(user, flair, highLevel, msg, type = 'chat', links){
            this.relayGlobalChatObject(new chat(user, flair, highLevel, msg, type, links));
        }

        /**
         * Relays an existing chat object to the entire server
         * @param {chat} chat - Chat Object representing the message to broadcast throughout the server
         */
        relayGlobalChatObject(chat){
            this.server.io.emit("chatMessage", chat);
        }

        //User Chat Functions
        /**
         * Relays a chat message from a user to the rest of the channel based on socket
         * @param {Socket} socket - Socket we're receiving the request from
         * @param {String} msg - Message Text Content
         * @param {String} type - Message Type, used for client-side chat post-processing.
         * @param {Array} links - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
         */
        relayUserChat(socket, msg, type, links){
            const user = this.server.getSocketInfo(socket);
            this.relayChat(user.user, user.flair, user.highLevel, msg, type, socket.chan, links);
        }

        //Toke Chat Functions
        /**
         * Broadcasts toke callout to the server
         * @param {String} msg - Message Text Content
         * @param {Array} links - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
         */
        relayTokeCallout(msg, links){
            this.relayGlobalChat("Tokebot", "", '∞', msg, "toke", links);
        }
        /**
         * Broadcasts toke callout to the server
         * @param {Socket} socket - Socket we're sending the whisper to
         * @param {String} msg - Message Text Content
         * @param {Array} links - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
         */
        relayTokeWhisper(socket, msg, links){
            this.relayPrivateChat(socket, "Tokebot", "", '∞', msg, "tokewhisper", links);
        }

        /**
         * Broadcasts toke whisper to the server
         * @param {String} msg - Message Text Content
         * @param {Array} links - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
         */
        relayGlobalTokeWhisper(msg, links){
            this.relayGlobalChat("Tokebot", "", '∞', msg, "tokewhisper", links);
        }

        //Announcement Functions
        /**
         * Broadcasts announcement to the server
         * @param {String} msg - Message Text Content
         * @param {Array} links - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
         */
        relayServerAnnouncement(msg, links){
            this.relayGlobalChat("Server", "", '∞', msg, "announcement", links);
        }

        /**
         * Broadcasts announcement to a given channel
         * @param {String} msg - Message Text Content
         * @param {Array} links - Array of URLs/Links to hand to the client-side chat post-processor to inject into the final message.
         */
        relayChannelAnnouncement(chan, msg, links){
            const activeChan = this.server.activeChannels.get(chan);

            //If channel isn't null
            if(activeChan != null){
                this.relayChat("Channel", "", '∞', msg, "announcement", chan, links);
            }
        }

        //Misc Functions
        /**
         * Clears chat for a given channel, targets specified user or entire channel if none found/specified.
         * @param {String} user - User chats to clear
         * @param {String} chan - Channel to broadcast message within
         */
        clearChat(chan, user){
            const activeChan = this.server.activeChannels.get(chan);

            //If channel isn't null
            if(activeChan != null){
                const target = activeChan.userList.get(user);

                //If no user was entered OR the user was found
                if(user == null || target != null){
                    this.server.io.in(chan).emit("clearChat", {user});
                }
            }
        }
    }