'use strict';

/* eslint no-unused-vars: 0 */

var __ = require('underscore');

/**
 * Parsed response
 * @typedef {object} PingResponse
 * @param {string} inputHost - The input IP address or HOST
 * @param {string} host - The input IP address or HOST
 * @param {string} numeric_host - Target IP address
 * @param {boolean} alive - True for existed host
 * @param {string} output - Raw stdout from system ping
 * @param {number} time - Time (float) in ms for first successful ping response
 * @param {string} min - Minimum time for collection records
 * @param {string} max - Maximum time for collection records
 * @param {string} avg - Average time for collection records
 * @param {number} packetLoss - Packet Losses in percent (number)
 * @param {string} stddev - Standard deviation time for collected records
 */

/**
 * @constructor
 *
 * @param {string} addr - Hostname or ip addres
 * @param {PingConfig} config - Config object in probe()
 */
function parser(addr, config) {
    // Initial state is 0
    this._state = 0;

    // Initial cache value
    this._response = {
        inputHost: addr,
        host: 'unknown',
        alive: false,
        output: 'unknown',
        time: 'unknown',
        times: [],
        min: 'unknown',
        max: 'unknown',
        avg: 'unknown',
        stddev: 'unknown',
        packetLoss: 'unknown',
    };

    // Initial times storage for ping time
    this._times = [];

    // Initial lines storage for ping output
    this._lines = [];

    // strip string regexp
    this._stripRegex = /[ ]*\r?\n?$/g;

    // Ping Config
    this._pingConfig = config || {};
}

/**
 * Enum for parser states
 * @readonly
 * @enum {number}
 */
parser.prototype.STATES = {
    INIT: 0,
    HEADER: 1,
    BODY: 2,
    FOOTER: 3,
    END: 4,
};

/**
 * Change state of this parser
 * @param {number} state - parser.STATES
 * @return {this} - This instance
 */
parser.prototype._changeState = function (state) {
    var states = __.values(this.STATES);
    if (states.indexOf(state) < 0) {
        throw new Error('Unknown state');
    }

    this._state = state;

    return this;
};

/**
 * Process output's header
 * @param {string} line - A line from system ping
 */
parser.prototype._processHeader = function (line) {
    throw new Error('Subclass should implement this method');
};

/**
 * Process output's body
 * @param {string} line - A line from system ping
 */
parser.prototype._processBody = function (line) {
    throw new Error('Subclass should implement this method');
};

/**
 * Process output's footer
 * @param {string} line - A line from system ping
 */
parser.prototype._processFooter = function (line) {
    throw new Error('Subclass should implement this method');
};

/**
 * Process a line from system ping
 * @param {string} line - A line from system ping
 * @return {this} - This instance
 */
parser.prototype.eat = function (line) {
    var headerStates = [
        this.STATES.INIT,
        this.STATES.HEADER,
    ];

    // Store lines
    this._lines.push(line);

    // Strip all space \r\n at the end
    var _line = line.replace(this._stripRegex, '');

    if (_line.length === 0) {
        // Do nothing if this is an empty line
    } else if (headerStates.indexOf(this._state) >= 0) {
        this._processHeader(_line);
    } else if (this._state === this.STATES.BODY) {
        this._processBody(_line);
    } else if (this._state === this.STATES.FOOTER) {
        this._processFooter(_line);
    } else if (this._state === this.STATES.END) {
        // Do nothing
    } else {
        throw new Error('Unknown state');
    }

    return this;
};

/**
 * Get results after parsing certain lines from system ping
 * @return {PingResponse} - Response from parsing ping output
 */
parser.prototype.getResult = function () {
    var ret = __.extend({}, this._response);

    // Concat output
    ret.output = this._lines.join('\n');

    // Determine alive
    ret.alive = this._times.length > 0;

    // Update time at first successful line
    if (ret.alive) {
        this._response.time = this._times[0];
        ret.time = this._response.time;
        this._response.times = this._times;
        ret.times = this._response.times;
    }

    // Get stddev
    if (
        ret.stddev === 'unknown' && ret.alive
    ) {
        var numberOfSamples = this._times.length;

        var sumOfAllSquareDifferences = __.reduce(
            this._times,
            function (memory, time) {
                var differenceFromMean = time - ret.avg;
                var squaredDifference =
                    differenceFromMean * differenceFromMean;
                return memory + squaredDifference;
            },
            0
        );
        var variances = sumOfAllSquareDifferences / numberOfSamples;

        ret.stddev = Math.round(
            Math.sqrt(variances) * 1000
        ) / 1000;
    }

    // Fix min, avg, max, stddev up to 3 decimal points
    __.each(['min', 'avg', 'max', 'stddev', 'packetLoss'], function (key) {
        var v = ret[key];
        if (__.isNumber(v)) {
            ret[key] = v.toFixed(3);
        }
    });

    return ret;
};

module.exports = parser;