core/communication.js

const Formatter = require('./formatter');
const Storage = require('./storage');
const Type = require('./type');

const { channels, adminRole } = require('../config.json');
const BotConfig = require('./botConfig');
const definitions = require('../commands/definitions.json');



/**
 * Get information depending the client communication.
 * Checkers to keep communication on the wanted channels
 * 
 * @author Maksim Sandybekov
 * @date 29.05.20202
 * @version 1.0
 * 
 * @property {Object} message The discord.js communication object to be used.
 * @property {Object} storage The active storage
 * @property {Object} defaults Responses for default cases invalid channel, ...
 * @property {*} category Configuration of categories to be used for communication.
 * @property {*} member Configuration of channels to be used by members
 * @property {*} admin Configuration of channels to be used by admins
 */
class Communication {


    /**
     * @constructor
     * @param {*} message The discord.js message object which was received.
     */
    constructor(message) {
        this.message = message;
        this.storage = Storage.getInstance();
        this.defaults = definitions._defaults_ || {};
        this.setChannelConfigs();
        this.defaultConfig = BotConfig.getInstance;
    }


    /**
     * Check if the communication over given channel name and category is allowed, for member/admin users.
     * Category configuration allows for string or array of strings.
     * Member configuration allows for string, array of strings or array of objects with {"name": "string", "category": "string"}
     * Admin configuration allows for string, array of strings or array of objects with {"name": "string", "category": "string"}
     * 
     * @todo Check for admin role instead if no channel/category is configured
     * @param {boolean} admin If the communication is for admins only
     * @return {boolean} Whether the user is able to communicate over given channel/category/rights
     */
    isAllowed(admin = false) {

        let channelName = this.getChannel();
        let categoryName = this.getCategory();

        let config = admin ? this.admin : this.member;

        // No configuration for amdin/member given  check if category is configured
        if (!config) {
            return this.__categoriesMatch(categoryName);

        }
        
        // Check if channels and categories match
        return this.__channelsMatch(channelName, categoryName, admin) && this.__categoriesMatch(categoryName);;
    }


    /**
     * Compare the currently configured category against the 
     * 
     * @private
     * @param {string} categoryName 
     * @return {boolean} 
     */
    __categoriesMatch(categoryName) {

        categoryName = categoryName.toLowerCase();
        let category = this.category;
        if (Type.isArray(category)) {

            let found = false;
            for (let i = 0; i < category.length; i++) {

                if (category[i] === categoryName) {
                    found = true;
                    break;
                }
            }

            console.log("Categories Match: " + found);

            return found;
        }

        // If no category is configured communication on any category is allowed
        return (Type.isString(category) && category.toLowerCase() == categoryName) || !category;
    }


    /**
     * Compare the provided channel name with the configured channel names. Return the result of the comparison.
     * 
     * @private 
     * @param {string} channelName 
     * @param {boolean} admin Whether to check for configured admin channels or member channels
     * @return {boolean} Whether the provided channelName matches with a configured channel
     */
    __channelsMatch(channelName, categoryName = "", admin = false) {

        categoryName = categoryName.toLowerCase();

        // Configured multiple channels, check all
        let config = admin ? this.admin : this.member;
        if (Type.isArray(config)) {

            let found = false;
            for (let i = 0; i < config.length; i++) {

                // Array entries are strings
                let element = config[i];
                if (Type.isString(element) && element == channelName && this.__categoriesMatch(categoryName)) {
                    found = true;
                    break; 
                }

                // Array entries are objects of form {name: string, category: string}
                let nameExists = element.name && Type.isString(element.name);
                let categoryExists = element.category && Type.isString(element.category);
                if (Type.isObject(element) && nameExists 
                    && element.name == channelName 
                    && categoryExists 
                    && element.category.toLowerCase() == categoryName) {
                    found = true;
                    break;
                }
            }

            console.log("Channels match: " + found);
            return found;
        }

        return Type.isString(config) && config == channelName;
    }


    /**
     * Retrieved the response from the _default_ section inside the definition file and 
     * replace template sequences of form {<number>} by given additional parameters.
     * 
     * @example
     * getDefaults("wrongChannel", userId, channelName);
     * 
     * @example
     * getDefaults("directMessage", userId);
     * 
     * @param {string} name Retrieve key to be used for the definition file. 
     * @return {string} Reponse with template sequences replaced.
     */
    getDefaults(name) {

        let response = this.defaults[name];
        let prevArguments = Object.keys(arguments).sort().slice(1).map(index => arguments[index]);
        return Formatter.format(response, ...prevArguments);
    }


    /**
     * Check if message received was direct a message.
     * 
     * @example
     * isDirect();
     * 
     * @param {*} message  A discord.js message object
     * @return {boolean} Whether or not the message received was a direct message.
     */
    isDirect() {
        return !this.message.member; 

    }


    /**
     * Get a response message giving a reason why communication over channel used by the user is not possible.
     * 
     * @param {Object} message The received discord.js message object
     * @param {boolean} admin Indicator to check for admin or member channel
     * @return {string} response message why communication is not possible, if possible "" is returned
     */
    getReason(admin = false) {


        // Message was direct message
        let userId = this.getUserId();
        if (this.isDirect()) {
            return this.getDefaults("directMessage", userId);
        }

        // Category for communication does not exist
        let comInfo = this.__aggregateInfo();
        if (!comInfo.categoryExists && comInfo.channelExists && !comInfo.categoryExists) {
            return this.getDefaults("categoryNonExistent", userId);

        }

        // Check the configuration, channel not setup under right category
        if (!comInfo.channelUnderCategory  && comInfo.categoryExists && !comInfo.channelExists) {
            return this.getDefaults("channelNonExistent", userId);

        }

        // Contacted from right category but wrong channel
        let onCategory = this.__categoriesMatch(this.getCategory());
        let onChannel = this.__channelsMatch(this.getChannel());
        if (onCategory && !onChannel) {
            return this.getDefaults("wrongChannel", userId);
        }
        

        // Neither default nor configured channel do exist
        return this.getDefaults("channelConfig", this.message.member.id);
    }


    /**
     * Perform check to get information about the lack of configuration of the server.
     * Checking if configured channels/categories are existent.
     * 
     * @private
     * @param {boolean} admin Whether to use the configuration for an admin channel or not
     * @return {Object}  
     */
    __aggregateInfo(admin = false) {

        let info = {
            categoryConfigured: this.category ? true : false,
            channelConfigured: this.getChannelConfig(admin) ? true : false,
            categoryExists: false,
            channelExists: false,
            channelUnderCategory: false
        };

        let callback = channel => {
            // category exists 

            if (channel.type != "text") {
                return;
            }

            let categoryExists = this.__categoriesMatch(channel.parent.name);
            let channelExists = this.__channelsMatch(channel.name, channel.parent.name);

            // Set category exists single time if category found
            if (!info["categoryExists"] && categoryExists) {
                info["categoryExists"] = true;
            }

            // Set channel exists single time if channel found
            if (!info["channelExists"] && channelExists) {
                info["channelExists"] = true;
            }

            // A channel under one of th econfigured 
            if (!info["channelUnderCategory"] && categoryExists && channelExists) {
                info["channelUnderCategory"] = true;
            }
        };

        // Search for configured channel/category combination
        this.message.guild.channels.cache.each(callback);
        return info;
    }



    // ------------------------------
    // Getter/-Setter
    // ------------------------------

    /**
     * Retrieve and return the id of the user.
     * 
     * @return {number} 
     */
    getUserId() {
        return this.message.member? this.message.member.id : this.message.author.id;
    }


    /**
     * Retrieve and return the guild from the message object.
     * 
     * @return {number}
     */
    getGuildId() {
        return this.message.guild? this.message.guild.id : null;
    }


    /**
     * Retrieve and return the channeln name of the current message.
     * 
     * @return {string}
     */
    getChannel() {
        return this.message.channel && this.message.channel.name ? this.message.channel.name : "";
    }

    
    /**
     * Retrieve and return the category name of the current message.
     * 
     * @return {string} 
     */
    getCategory() {
        return this.message.channel && this.message.channel.parent ? this.message.channel.parent.name : "";
    }


    /**
     * Retrieve the current channel configuration.
     * 
     * @param {boolean} admin 
     * @return {*}
     */
    getChannelConfig(admin = false) {
        return admin ? this.member : this.admin;
    }


    /**
     * Retrieve the current category configuration.
     * @return {*}
     */
    getCategoryConfig() {
        return this.category;
    }


    /**
     * Setup server specific channel configuration for bot communication.
     * 
     */
    setChannelConfigs() {

        // Potentially direct messsage, message not from guild
        if (!this.message.guild) {
            return null;
        }

        let guildId = this.message.guild.id;

        // Set category configuration
        let category = this.storage.get("config." + guildId + ".channels.category");
        if (category) {
            this.category = category;

        } else {
            this.setDefaultCategory();

        }

        // Set member configuration
        let member = this.storage.get("config." + guildId + ".channels.member");
        if (member) {
            this.member = member;

        } else {
            this.setDefaultMember();

        }

        // Set admin configuration
        let admin = this.storage.get("config." + guildId + ".channels.admin");
        if (admin) {
            this.admin = admin;

        } else {
            this.setDefaultAdmin();

        }
    }


    /**
     * Set default channel configuration for member channels.
     */
    setDefaultMember() {
        this.member = channels && channels.member ? channels.member : "";
    }


    /**
     * Set default channel configurations for admin channels.
     */
    setDefaultAdmin() {
        this.admin = channels && channels.admin ? channels.admin : "";
    }


    /**
     * Set default channel configuration for the category of channels.
     */
    setDefaultCategory() {
        this.category = channels && channels.category ? channels.category : "";
    }
}


module.exports = Communication;