"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const Connector_1 = require("./connector/Connector");
const Channel_1 = require("./Channel");
const RosException_1 = require("./RosException");
const RStream_1 = require("./RStream");
const crypto = require("crypto");
const debug = require("debug");
const timers_1 = require("timers");
const events_1 = require("events");
const info = debug('routeros-api:api:info');
const error = debug('routeros-api:api:error');
/**
 * Creates a connection object with the credentials provided
 */
class RouterOSAPI extends events_1.EventEmitter {
    /**
     * Constructor, also sets the language of the thrown errors
     *
     * @param {Object} options
     */
    constructor(options) {
        super();
        /**
         * Connected flag
         */
        this.connected = false;
        /**
         * Connecting flag
         */
        this.connecting = false;
        /**
         * Closing flag
         */
        this.closing = false;
        /**
         * Counter for channels open
         */
        this.channelsOpen = 0;
        /**
         * Flag if the connection was held by the keepalive parameter
         * or keepaliveBy function
         */
        this.holdingConnectionWithKeepalive = false;
        this.registeredStreams = [];
        this.setOptions(options);
    }
    /**
     * Set connection options, affects before connecting
     *
     * @param options connection options
     */
    setOptions(options) {
        this.host = options.host;
        this.user = options.user;
        this.password = options.password;
        this.port = options.port || 8728;
        this.timeout = options.timeout || 10;
        this.tls = options.tls;
        this.keepalive = options.keepalive || false;
    }
    /**
     * Tries a connection to the routerboard with the provided credentials
     *
     * @returns {Promise}
     */
    connect() {
        if (this.connecting)
            return Promise.reject('ALRDYCONNECTING');
        if (this.connected)
            return Promise.resolve(this);
        info('Connecting on %s', this.host);
        this.connecting = true;
        this.connected = false;
        this.connector = new Connector_1.Connector({
            host: this.host,
            port: this.port,
            timeout: this.timeout,
            tls: this.tls,
        });
        return new Promise((resolve, reject) => {
            const endListener = (e) => {
                this.stopAllStreams();
                this.connected = false;
                this.connecting = false;
                if (e)
                    reject(e);
            };
            this.connector.once('error', endListener);
            this.connector.once('timeout', endListener);
            this.connector.once('close', () => {
                this.emit('close');
                endListener();
            });
            this.connector.once('connected', () => {
                this.login()
                    .then(() => {
                    this.connecting = false;
                    this.connected = true;
                    this.connector.removeListener('error', endListener);
                    this.connector.removeListener('timeout', endListener);
                    const connectedErrorListener = (e) => {
                        this.connected = false;
                        this.connecting = false;
                        this.emit('error', e);
                    };
                    this.connector.once('error', connectedErrorListener);
                    this.connector.once('timeout', connectedErrorListener);
                    if (this.keepalive)
                        this.keepaliveBy('#');
                    info('Logged in on %s', this.host);
                    resolve(this);
                })
                    .catch((e) => {
                    this.connecting = false;
                    this.connected = false;
                    reject(e);
                });
            });
            this.connector.connect();
        });
    }
    /**
     * Writes a command over the socket to the routerboard
     * on a new channel
     *
     * @param {string|Array} params
     * @param {Array<string|string[]>} moreParams
     * @returns {Promise}
     */
    write(params, ...moreParams) {
        params = this.concatParams(params, moreParams);
        let chann = this.openChannel();
        this.holdConnection();
        chann.once('close', () => {
            chann = null; // putting garbage collector to work :]
            this.decreaseChannelsOpen();
            this.releaseConnectionHold();
        });
        return chann.write(params);
    }
    /**
     * Writes a command over the socket to the routerboard
     * on a new channel and return an event of what happens
     * with the responses. Listen for 'data', 'done', 'trap' and 'close'
     * events.
     *
     * @param {string|Array} params
     * @param {Array<string|string[]>} moreParams
     * @returns {RStream}
     */
    writeStream(params, ...moreParams) {
        params = this.concatParams(params, moreParams);
        const stream = new RStream_1.RStream(this.openChannel(), params);
        stream.on('started', () => {
            this.holdConnection();
        });
        stream.on('stopped', () => {
            this.unregisterStream(stream);
            this.decreaseChannelsOpen();
            this.releaseConnectionHold();
        });
        stream.start();
        this.registerStream(stream);
        return stream;
    }
    /**
     * Returns a stream object for handling continuous data
     * flow.
     *
     * @param {string|Array} params
     * @param {function} callback
     * @returns {RStream}
     */
    stream(params = [], ...moreParams) {
        let callback = moreParams.pop();
        if (typeof callback !== 'function') {
            if (callback)
                moreParams.push(callback);
            callback = null;
        }
        params = this.concatParams(params, moreParams);
        const stream = new RStream_1.RStream(this.openChannel(), params, callback);
        stream.on('started', () => {
            this.holdConnection();
        });
        stream.on('stopped', () => {
            this.unregisterStream(stream);
            this.decreaseChannelsOpen();
            this.releaseConnectionHold();
            stream.removeAllListeners();
        });
        stream.start();
        stream.prepareDebounceEmptyData();
        this.registerStream(stream);
        return stream;
    }
    /**
     * Keep the connection alive by running a set of
     * commands provided instead of the random command
     *
     * @param {string|Array} params
     * @param {function} callback
     */
    keepaliveBy(params = [], ...moreParams) {
        this.holdingConnectionWithKeepalive = true;
        if (this.keptaliveby)
            timers_1.clearTimeout(this.keptaliveby);
        let callback = moreParams.pop();
        if (typeof callback !== 'function') {
            if (callback)
                moreParams.push(callback);
            callback = null;
        }
        params = this.concatParams(params, moreParams);
        const exec = () => {
            if (!this.closing) {
                if (this.keptaliveby)
                    timers_1.clearTimeout(this.keptaliveby);
                this.keptaliveby = setTimeout(() => {
                    this.write(params.slice())
                        .then((data) => {
                        if (typeof callback === 'function')
                            callback(null, data);
                        exec();
                    })
                        .catch((err) => {
                        if (typeof callback === 'function')
                            callback(err, null);
                        exec();
                    });
                }, (this.timeout * 1000) / 2);
            }
        };
        exec();
    }
    /**
     * Closes the connection.
     * It can be openned again without recreating
     * an object from this class.
     *
     * @returns {Promise}
     */
    close() {
        if (this.closing) {
            return Promise.reject(new RosException_1.RosException('ALRDYCLOSNG'));
        }
        if (!this.connected) {
            return Promise.resolve(this);
        }
        if (this.connectionHoldInterval) {
            timers_1.clearTimeout(this.connectionHoldInterval);
        }
        timers_1.clearTimeout(this.keptaliveby);
        this.stopAllStreams();
        return new Promise((resolve) => {
            this.closing = true;
            this.connector.once('close', () => {
                this.connector.destroy();
                this.connector = null;
                this.closing = false;
                this.connected = false;
                resolve(this);
            });
            this.connector.close();
        });
    }
    /**
     * Opens a new channel either for just writing or streaming
     *
     * @returns {Channel}
     */
    openChannel() {
        this.increaseChannelsOpen();
        return new Channel_1.Channel(this.connector);
    }
    increaseChannelsOpen() {
        this.channelsOpen++;
    }
    decreaseChannelsOpen() {
        this.channelsOpen--;
    }
    registerStream(stream) {
        this.registeredStreams.push(stream);
    }
    unregisterStream(stream) {
        this.registeredStreams = this.registeredStreams.filter((registeredStreams) => registeredStreams !== stream);
    }
    stopAllStreams() {
        for (const registeredStream of this.registeredStreams) {
            registeredStream.stop();
        }
    }
    /**
     * Holds the connection if keepalive wasn't set
     * so when a channel opens, ensure that we
     * receive a response before a timeout
     */
    holdConnection() {
        // If it's not the first connection to open
        // don't try to hold it again
        if (this.channelsOpen !== 1)
            return;
        if (this.connected && !this.holdingConnectionWithKeepalive) {
            if (this.connectionHoldInterval)
                timers_1.clearTimeout(this.connectionHoldInterval);
            const holdConnInterval = () => {
                this.connectionHoldInterval = setTimeout(() => {
                    let chann = new Channel_1.Channel(this.connector);
                    chann.on('close', () => {
                        chann = null;
                    });
                    chann
                        .write(['#'])
                        .then(() => {
                        holdConnInterval();
                    })
                        .catch(() => {
                        holdConnInterval();
                    });
                }, (this.timeout * 1000) / 2);
            };
            holdConnInterval();
        }
    }
    /**
     * Release the connection that was held
     * when waiting for responses from channels open
     */
    releaseConnectionHold() {
        // If there are channels still open
        // don't release the hold
        if (this.channelsOpen > 0)
            return;
        if (this.connectionHoldInterval)
            timers_1.clearTimeout(this.connectionHoldInterval);
    }
    /**
     * Login on the routerboard to provide
     * api functionalities, using the credentials
     * provided.
     *
     * @returns {Promise}
     */
    login() {
        this.connecting = true;
        info('Sending 6.43+ login to %s', this.host);
        return this.write('/login', [
            `=name=${this.user}`,
            `=password=${this.password}`,
        ])
            .then((data) => {
            if (data.length === 0) {
                info('6.43+ Credentials accepted on %s, we are connected', this.host);
                return Promise.resolve(this);
            }
            else if (data.length === 1) {
                info('Received challenge on %s, will send credentials. Data: %o', this.host, data);
                const challenge = Buffer.alloc(this.password.length + 17);
                const challengeOffset = this.password.length + 1;
                // Here we have 32 chars with hex encoded 16 bytes of challenge data
                const ret = data[0].ret;
                challenge.write(String.fromCharCode(0) + this.password);
                // To write 32 hec chars to buffer as bytes we need to write 16 bytes
                challenge.write(ret, challengeOffset, ret.length / 2, 'hex');
                const resp = '00' +
                    crypto
                        .createHash('MD5')
                        .update(challenge)
                        .digest('hex');
                return this.write('/login', [
                    '=name=' + this.user,
                    '=response=' + resp,
                ])
                    .then(() => {
                    info('Credentials accepted on %s, we are connected', this.host);
                    return Promise.resolve(this);
                })
                    .catch((err) => {
                    if (err.message === 'cannot log in' ||
                        err.message ===
                            'invalid user name or password (6)') {
                        err = new RosException_1.RosException('CANTLOGIN');
                    }
                    this.connector.destroy();
                    error("Couldn't loggin onto %s, Error: %O", this.host, err);
                    return Promise.reject(err);
                });
            }
            error('Unknown return from /login command on %s, data returned: %O', this.host, data);
            Promise.reject(new RosException_1.RosException('CANTLOGIN'));
        })
            .catch((err) => {
            if (err.message === 'cannot log in' ||
                err.message === 'invalid user name or password (6)') {
                err = new RosException_1.RosException('CANTLOGIN');
            }
            this.connector.destroy();
            error("Couldn't loggin onto %s, Error: %O", this.host, err);
            return Promise.reject(err);
        });
    }
    concatParams(firstParameter, parameters) {
        if (typeof firstParameter === 'string')
            firstParameter = [firstParameter];
        for (let parameter of parameters) {
            if (typeof parameter === 'string')
                parameter = [parameter];
            if (parameter.length > 0)
                firstParameter = firstParameter.concat(parameter);
        }
        return firstParameter;
    }
}
exports.RouterOSAPI = RouterOSAPI;
//# sourceMappingURL=data:application/json;base64, 