/* jslint node: true */
'use strict';

const { MenuModule } = require('./menu_module.js');
const DropFile = require('./dropfile.js');
const Door = require('./door.js');
const theme = require('./theme.js');
const ansi = require('./ansi_term.js');
const { Errors } = require('./enig_error.js');
const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js');
const Log = require('./logger').log;
const Config = require('./config.js').get;

//  deps
const async = require('async');
const assert = require('assert');
const _ = require('lodash');
const paths = require('path');
const fs = require('graceful-fs');

const activeDoorNodeInstances = {};

exports.moduleInfo = {
    name: 'Abracadabra',
    desc: 'External BBS Door Module',
    author: 'NuSkooler',
};

/*
    Example configuration for LORD under DOSEMU:

    {
        config: {
            name: PimpWars
            dropFileType: DORINFO
            cmd: qemu-system-i386
            args: [
                "-localtime",
                "freedos.img",
                "-chardev",
                "socket,port={srvPort},nowait,host=localhost,id=s0",
                "-device",
                "isa-serial,chardev=s0"
            ]
            io: socket
        }
    }

    listen: socket | stdio

    {
        "config" : {
            "name"          : "LORD",
            "dropFileType"  : "DOOR",
            "cmd"           : "/usr/bin/dosemu",
            "args"          : [ "-quiet", "-f", "/etc/dosemu/dosemu.conf", "X:\\PW\\START.BAT {dropfile} {node}" ] ],
            "nodeMax"       : 32,
            "tooManyArt"    : "toomany-lord.ans"
        }
    }

    :TODO: See Mystic & others for other arg options that we may need to support
*/

exports.getModule = class AbracadabraModule extends MenuModule {
    constructor(options) {
        super(options);

        this.config = options.menuConfig.config;
        //  :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... }
        //  ..  and/or EnigAssert
        assert(_.isString(this.config.name, "Config 'name' is required"));
        assert(_.isString(this.config.cmd, "Config 'cmd' is required"));

        this.config.nodeMax = this.config.nodeMax || 0;
        this.config.args = this.config.args || [];
    }

    incrementActiveDoorNodeInstances() {
        if (activeDoorNodeInstances[this.config.name]) {
            activeDoorNodeInstances[this.config.name] += 1;
        } else {
            activeDoorNodeInstances[this.config.name] = 1;
        }
        this.activeDoorInstancesIncremented = true;
    }

    decrementActiveDoorNodeInstances() {
        if (true === this.activeDoorInstancesIncremented) {
            activeDoorNodeInstances[this.config.name] -= 1;
            this.activeDoorInstancesIncremented = false;
        }
    }

    initSequence() {
        const self = this;

        async.series(
            [
                function validateNodeCount(callback) {
                    if (
                        self.config.nodeMax > 0 &&
                        _.isNumber(activeDoorNodeInstances[self.config.name]) &&
                        activeDoorNodeInstances[self.config.name] + 1 >
                            self.config.nodeMax
                    ) {
                        self.client.log.info(
                            {
                                name: self.config.name,
                                activeCount: activeDoorNodeInstances[self.config.name],
                            },
                            `Too many active instances of door "${self.config.name}"`
                        );

                        if (_.isString(self.config.tooManyArt)) {
                            theme.displayThemeArt(
                                { client: self.client, name: self.config.tooManyArt },
                                function displayed() {
                                    self.pausePrompt(() => {
                                        return callback(
                                            Errors.AccessDenied(
                                                'Too many active instances'
                                            )
                                        );
                                    });
                                }
                            );
                        } else {
                            self.client.term.write(
                                '\nToo many active instances. Try again later.\n'
                            );

                            //  :TODO: Use MenuModule.pausePrompt()
                            self.pausePrompt(() => {
                                return callback(
                                    Errors.AccessDenied('Too many active instances')
                                );
                            });
                        }
                    } else {
                        self.incrementActiveDoorNodeInstances();
                        return callback(null);
                    }
                },
                function prepareDoor(callback) {
                    self.doorInstance = new Door(self.client);
                    return self.doorInstance.prepare(self.config.io || 'stdio', callback);
                },
                function generateDropfile(callback) {
                    if (
                        !self.config.dropFileType ||
                        self.config.dropFileType.toLowerCase() === 'none'
                    ) {
                        return callback(null);
                    }

                    self.dropFile = new DropFile(self.client, {
                        fileType: self.config.dropFileType,
                    });

                    return self.dropFile.createFile(callback);
                },
            ],
            function complete(err) {
                if (err) {
                    self.client.log.warn(
                        { error: err.toString() },
                        'Could not start door'
                    );
                    self.lastError = err;
                    self.prevMenu();
                } else {
                    self.finishedLoading();
                }
            }
        );
    }

    runDoor() {
        this.client.term.write(ansi.resetScreen());

        const exeInfo = {
            name: this.config.name,
            cmd: this.config.cmd,
            preCmd: this.config.preCmd,
            preCmdArgs: this.config.preCmdArgs,
            cwd: this.config.cwd || paths.dirname(this.config.cmd),
            args: this.config.args,
            io: this.config.io || 'stdio',
            encoding: this.config.encoding || 'cp437',
            node: this.client.node,
            env: this.config.env,
        };

        exeInfo.dropFileDir = DropFile.dropFileDirectory(
            Config().paths.dropFiles,
            this.client
        );
        exeInfo.userAreaDir = paths.join(
            exeInfo.dropFileDir,
            this.client.user.getSanitizedName(),
            this.config.name.toLowerCase()
        );

        if (this.dropFile) {
            exeInfo.dropFile = this.dropFile.fileName;
            exeInfo.dropFilePath = this.dropFile.fullPath;
        }

        this._makeDropDirs([exeInfo.dropFileDir, exeInfo.userAreaDir], err => {
            if (err) {
                Log.warn(
                    `Failed creating directory ${exeInfo.dropFilePath}: ${err.message}`
                );
            }

            const doorTracking = trackDoorRunBegin(this.client, this.config.name);

            this.doorInstance.run(exeInfo, err => {
                if (err) {
                    Log.error(`Error running "${this.config.name}": ${err.message}`);
                }

                trackDoorRunEnd(doorTracking);
                this.decrementActiveDoorNodeInstances();

                //  Clean up dropfile, if any
                if (exeInfo.dropFilePath) {
                    fs.unlink(exeInfo.dropFilePath, err => {
                        if (err) {
                            Log.warn(
                                { error: err, path: exeInfo.dropFilePath },
                                'Failed to remove drop file.'
                            );
                        }
                    });
                }

                //  client may have disconnected while process was active -
                //  we're done here if so.
                if (!this.client.term.output) {
                    return;
                }

                //
                //  Try to clean up various settings such as scroll regions that may
                //  have been set within the door
                //
                this.client.term.rawWrite(
                    ansi.normal() +
                        ansi.goto(
                            this.client.term.termHeight,
                            this.client.term.termWidth
                        ) +
                        ansi.setScrollRegion() +
                        ansi.goto(this.client.term.termHeight, 0) +
                        '\r\n\r\n'
                );

                this.autoNextMenu();
            });
        });
    }

    _makeDropDirs(dirs, cb) {
        async.forEach(
            dirs,
            (dir, nextDir) => {
                fs.mkdir(dir, { recursive: true }, nextDir);
            },
            cb
        );
    }

    leave() {
        super.leave();
        this.decrementActiveDoorNodeInstances();
    }

    finishedLoading() {
        this.runDoor();
    }
};
