/**
 * Base module that all modules extend
 */

let { logger } = require('../logging');
let message = require('../message');
let { isFunction, getObjectKeysToPrototype } = require('../utility');
let { getConfig } = require('../config');

class AbstractModule {
    /*
        Name of the module used in help documentation and logging.
    */
    name = "AbstractModule";

    /*
        Short description of the module functionality.
    */
    description = "Base Module That All Other Modules Extend";

    /*
        A helpful multiline string that defines the module usage
    */
    helpAndUsage = `Example: !abstract_module <command>
        !abstract_module <boo> : scares people`

    /*
        The exported command used to invoke the module directly.
    */
    command = "abstract_module";

    /*
        The default method to call when a command word is not recognized.
    */
    defaultCommand = null;

    /*
        The module should be hidden from help and command dialogs.
    */
    hidden = false;

    /*
        This module should receive all messages, regardless of whether 
        the module was directly referenced with a command.
     */
    canHandleIndirectMessages = false;

    /*
        Indicates if the module requires a readable config file.
    */
    needConfig = false;

    /* internal */

    /*
        The loaded config file, if it exists.
    */
    _config = null;

    constructor(name, description, command) {
        this.name = name;
        this.description = description;
        this.command = command;
        this._recognizedCommands = [];
        this._recognizedCommandMap = new Map();
    }

    addRecognizedCommand(command, methodName) {
        this._recognizedCommands.push(command);
        this._recognizedCommandMap.set(command, methodName);
    }

    getRecognizedCommands() {
        return this._recognizedCommandMap.keys();
    }

    getConfigFilePath() {
        return process.env.NODE_PATH + '/data/' + this.name.toLowerCase().replace(' ', '_') + '-config.json';
    }

    getConfigSensitiveFields() {
        return [];
    }

    /**
     * Default functionality for receiving and processing a message.
     * 
     * Override this if the module needs to do more complicated message processing.
     */
    handleMessage(event, room, callback) {
        logger.debug("[%s] [%s] [%s]", this.name, room.name, event.event.content.body);

        let messageBody = event.event.content.body;
        let bodyParts = messageBody.split(' ');
        let trigger = bodyParts[0];
        let command = bodyParts[1];
        var args = [];
        if (bodyParts.length > 2) {
            args = bodyParts.slice(2);
        }

        logger.debug("Attempting to call %s with %s", command, args);
        let responseMessage = this.processMessage(command, ...args);

        callback(
            room,
            responseMessage
        );
    }

    /*
        Call the command method with the args
    */
    processMessage(command, ...args) {
        if (this._recognizedCommands.includes(command)) {
            logger.debug("Calling %s with %s", this._recognizedCommandMap.get(command), args);
            return this[this._recognizedCommandMap.get(command)](...args);
        } else {
            if (this.defaultCommand != null) {
                logger.debug("Attempting to use default command %s", this.defaultCommand);
                try {
                    let newArgs = [command].concat(...args);
                    logger.debug("Calling %s with %s", this._recognizedCommandMap.get(this.defaultCommand), newArgs);
                    return this[this._recognizedCommandMap.get(this.defaultCommand)](...newArgs);
                } catch (e) {
                    logger.error("Error while calling default command %s %s", this.defaultCommand, e);
                    return this.cmd_help();
                }
            } else {
                logger.debug("Unrecognized command %s", command);
                return this.cmd_help();
            }
        }
    }

    /* Basic cmd methods */

    /*
        return basic help information,.
    */
    cmd_help(...args) {
        return message.createBasic(this.helpAndUsage);
    }
}

let abstractModulePrototype = Object.getPrototypeOf(new AbstractModule('', '', ''));

/*
Initialization of a module.
*/
function init(mod) {
    logger.debug("Initializing module %s", mod.name);
    if (mod.needConfig) {
        logger.debug("Loading config file %s", mod.getConfigFilePath());
        try {
            mod._config = getConfig(mod.getConfigFilePath(), mod.getConfigSensitiveFields());
        } catch (e) {
            logger.error("Module %s needs a valid config file at %s", mod.name, mod.getConfigFilePath());
            process.exit(1);
        }
    }
    logger.debug("Detecting command methods.");
    let commandMethods = getObjectKeysToPrototype(mod, abstractModulePrototype, (key) => {
        return key.startsWith('cmd_') && isFunction(mod[key]);
    })
    // let commandMethods = objectKeys.filter();
    logger.debug("Identified command methods: %s", commandMethods);
    commandMethods.forEach((commandMethodName) => {
        let command = commandMethodName.substring(4);
        mod.addRecognizedCommand(command, commandMethodName);
    })
    logger.debug("Bound command methods for %s as %s", mod.name, mod.getRecognizedCommands());
}

exports.AbstractModule = AbstractModule
exports.initModule = init;