"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const stream_1 = require("stream");
const events_1 = require("events");
const utils = __importStar(require("../utils"));
const nestedCounters_1 = require("../utils/nestedCounters");
const network_1 = require("../network");
const lib_types_1 = require("@shardeum-foundation/lib-types");
class Statistics extends events_1.EventEmitter {
    constructor(baseDir, config, { counters = [], watchers = {}, timers = [], manualStats = [], fifoStats = [], ringOverrides = {}, fifoOverrides = {}, }, context) {
        super();
        this.intervalDuration = config.interval || 1;
        this.intervalDuration = this.intervalDuration * 1000;
        this.context = context;
        this.counterDefs = counters;
        this.watcherDefs = watchers;
        this.timerDefs = timers;
        this.manualStatDefs = manualStats;
        this.fifoStatDefs = fifoStats;
        this.ringOverrides = ringOverrides;
        this.fifoOverrides = fifoOverrides;
        this.countedEventMap = new Map();
        this.initialize();
        this.interval = null;
        this.snapshotWriteFns = [];
        this.stream = null;
        this.streamIsPushable = false;
        if (config.save) {
            // Pipe stream to file
            const file = path_1.default.join(baseDir, 'statistics.tsv');
            const fileWriteStream = fs_1.default.createWriteStream(file);
            const statsReadStream = this.getStream();
            statsReadStream.pipe(fileWriteStream);
        }
        this.context.network.registerExternalGet('tx-stats', (req, res) => {
            try {
                // todo: reject if request is not coming from node operator dashboard
                let stats = {
                    txInjected: 0,
                    txApplied: 0,
                    txRejected: 0,
                    txProcessed: 0,
                    txExpired: 0,
                };
                stats.txInjected += this.getPreviousElement('txInjected') || 0;
                stats.txApplied += this.getPreviousElement('txApplied') || 0;
                stats.txRejected += this.getPreviousElement('txRejected') || 0;
                stats.txExpired += this.getPreviousElement('txExpired') || 0;
                stats.txProcessed += this.getPreviousElement('txProcessed') || 0;
                return res.json(stats);
            }
            catch (e) {
                console.log(`Error getting stats: ${lib_types_1.Utils.safeStringify(e)}`);
            }
        });
    }
    initialize() {
        this.counters = this._initializeCounters(this.counterDefs);
        this.watchers = this._initializeWatchers(this.watcherDefs, this.context);
        this.timers = this._initializeTimers(this.timerDefs);
        this.manualStats = this._initializeManualStats(this.manualStatDefs, this.ringOverrides);
        this.fifoStats = this._initializeFifoStats(this.fifoStatDefs, this.fifoOverrides);
    }
    getStream() {
        this.stream = new stream_1.Readable();
        this.stream._read = () => {
            this.streamIsPushable = true;
        };
        return this.stream;
    }
    writeOnSnapshot(writeFn, context) {
        this.snapshotWriteFns.push(writeFn.bind(context));
    }
    startSnapshots() {
        const tabSeperatedHeaders = 'Name\tValue\tTime\n';
        this._pushToStream(tabSeperatedHeaders);
        if (!this.interval)
            this.interval = setInterval(this._takeSnapshot.bind(this), this.intervalDuration);
    }
    stopSnapshots() {
        clearInterval(this.interval);
        this.interval = null;
    }
    // Increments the given CounterRing's count
    incrementCounter(counterName) {
        const counter = this.counters[counterName];
        if (!counter)
            throw new Error(`Counter '${counterName}' is undefined.`);
        counter.increment();
        nestedCounters_1.nestedCountersInstance.countEvent('statistics', counterName);
    }
    /**
     * Stores a new CountedEvent in a map
     * @param category
     * @param name
     * @param count
     * @param message
     * @returns
     */
    countEvent(category, name, count, message) {
        const newEvent = {
            eventCategory: category,
            eventName: name,
            eventCount: count,
            eventMessages: [message],
            eventTimestamps: [(0, network_1.shardusGetTime)()],
        };
        const categoryExists = this.countedEventMap.has(category);
        if (!categoryExists) {
            this.countedEventMap.set(category, new Map().set(name, newEvent));
            return;
        }
        const eventCategory = this.countedEventMap.get(category);
        const nameExists = eventCategory.has(name);
        if (!nameExists) {
            eventCategory.set(name, newEvent);
            return;
        }
        const currentCountedEvent = eventCategory.get(name);
        currentCountedEvent.eventCount += count;
        currentCountedEvent.eventMessages.push(message);
        currentCountedEvent.eventTimestamps.push((0, network_1.shardusGetTime)());
    }
    /**
     * Get an array of all the CountedEvents
     * @returns
     */
    getAllCountedEvents() {
        const countedEvents = [];
        for (let eventCategories of this.countedEventMap.values()) {
            for (let countedEvent of eventCategories.values()) {
                countedEvents.push(countedEvent);
            }
        }
        return countedEvents;
    }
    /**
     * Resets the internal CountedEventMap
     */
    resetCountedEvents() {
        this.countedEventMap = new Map();
    }
    setManualStat(manualStatName, value) {
        const ring = this.manualStats[manualStatName];
        if (!ring)
            throw new Error(`manualStat '${manualStatName}' is undefined.`);
        ring.manualSetValue(value);
        //nestedCountersInstance.countEvent('statistics', manualStatName)
    }
    setFifoStat(fifoStatName, value) {
        const fifoStat = this.fifoStats[fifoStatName];
        if (!fifoStat)
            throw new Error(`fifoStat '${fifoStatName}' is undefined.`);
        fifoStat.save(value);
    }
    // Returns the current count of the given CounterRing
    getCurrentCount(counterName) {
        const counter = this.counters[counterName];
        if (!counter)
            throw new Error(`Counter '${counterName}' is undefined.`);
        return counter.count;
    }
    // Returns the current total of the given CounterRing
    getCounterTotal(counterName) {
        const counter = this.counters[counterName];
        if (!counter)
            throw new Error(`Counter '${counterName}' is undefined.`);
        return counter.total;
    }
    // Returns the result of the given WatcherRings watchFn
    getWatcherValue(watcherName) {
        const watcher = this.watchers[watcherName];
        if (!watcher)
            throw new Error(`Watcher '${watcherName}' is undefined.`);
        return watcher.watchFn();
    }
    // Starts an entry for the given id in the given TimerRing
    startTimer(timerName, id) {
        const timer = this.timers[timerName];
        if (!timer)
            throw new Error(`Timer '${timerName}' is undefined.`);
        timer.start(id);
    }
    // Stops an entry for the given id in the given TimerRing
    stopTimer(timerName, id) {
        const timer = this.timers[timerName];
        if (!timer)
            throw new Error(`Timer '${timerName}' is undefined.`);
        timer.stop(id);
    }
    // Returns the current average of all elements in the given WatcherRing, CounterRing, or TimerRing
    getAverage(name) {
        const fifoHolder = this.fifoStats[name];
        if (fifoHolder) {
            return fifoHolder.average();
        }
        const ringHolder = this.counters[name] || this.watchers[name] || this.timers[name] || this.manualStats[name];
        if (!ringHolder.ring)
            throw new Error(`Ring holder '${name}' is undefined.`);
        return ringHolder.ring.average();
    }
    getMultiStatReport(name) {
        const fifoHolder = this.fifoStats[name];
        if (fifoHolder) {
            return fifoHolder.multiStats();
        }
        const ringHolder = this.counters[name] || this.watchers[name] || this.timers[name] || this.manualStats[name];
        if (!ringHolder.ring)
            throw new Error(`Ring holder '${name}' is undefined.`);
        return ringHolder.ring.multiStats();
    }
    getMax(name) {
        const fifoHolder = this.fifoStats[name];
        if (fifoHolder) {
            return fifoHolder.max();
        }
        const ringHolder = this.counters[name] || this.watchers[name] || this.timers[name] || this.manualStats[name];
        if (!ringHolder.ring)
            throw new Error(`Ring holder '${name}' is undefined.`);
        return ringHolder.ring.max();
    }
    clearRing(name) {
        const ringHolder = this.counters[name] || this.watchers[name] || this.timers[name] || this.manualStats[name];
        if (!ringHolder.ring)
            throw new Error(`Ring holder '${name}' is undefined.`);
        return ringHolder.ring.clear();
    }
    // Returns the value of the last element of the given WatcherRing, CounterRing, or TimerRing
    getPreviousElement(name) {
        const ringHolder = this.counters[name] || this.watchers[name] || this.timers[name];
        if (!ringHolder.ring)
            throw new Error(`Ring holder '${name}' is undefined.`);
        return ringHolder.ring.previous();
    }
    _initializeCounters(counterDefs = []) {
        const counters = {};
        for (const name of counterDefs) {
            counters[name] = new CounterRing(60);
        }
        return counters;
    }
    _initializeWatchers(watcherDefs = {}, context) {
        const watchers = {};
        for (const name in watcherDefs) {
            const watchFn = watcherDefs[name];
            watchers[name] = new WatcherRing(60, watchFn, context);
        }
        return watchers;
    }
    _initializeTimers(timerDefs = []) {
        const timers = {};
        for (const name of timerDefs) {
            timers[name] = new TimerRing(60);
        }
        return timers;
    }
    _initializeManualStats(counterDefs = [], ringOverrides = {}) {
        const manualStats = {};
        for (const name of counterDefs) {
            let count = 10;
            if (ringOverrides[name] != null) {
                count = ringOverrides[name];
            }
            manualStats[name] = new ManualRing(count); //should it be a config
        }
        return manualStats;
    }
    _initializeFifoStats(fifoStatsDefs = [], statsOverrides = {}) {
        const fifoStats = {};
        for (const name of fifoStatsDefs) {
            let count = 240;
            if (statsOverrides[name] != null) {
                count = statsOverrides[name];
            }
            fifoStats[name] = new FifoStats(count); //should it be a config
        }
        return fifoStats;
    }
    _takeSnapshot() {
        const time = new Date().toISOString();
        let tabSeperatedValues = '';
        for (const counter in this.counters) {
            this.counters[counter].snapshot();
            tabSeperatedValues += `${counter}-average\t${this.getAverage(counter)}\t${time}\n`;
            tabSeperatedValues += `${counter}-total\t${this.getCounterTotal(counter)}\t${time}\n`;
        }
        for (const watcher in this.watchers) {
            this.watchers[watcher].snapshot();
            tabSeperatedValues += `${watcher}-average\t${this.getAverage(watcher)}\t${time}\n`;
            tabSeperatedValues += `${watcher}-value\t${this.getWatcherValue(watcher)}\t${time}\n`;
        }
        for (const timer in this.timers) {
            this.timers[timer].snapshot();
            tabSeperatedValues += `${timer}-average\t${this.getAverage(timer) / 1000}\t${time}\n`;
        }
        for (const writeFn of this.snapshotWriteFns) {
            tabSeperatedValues += writeFn();
        }
        this._pushToStream(tabSeperatedValues);
        this.emit('snapshot');
    }
    _pushToStream(data) {
        if (this.stream && this.streamIsPushable) {
            this.streamIsPushable = this.stream.push(data);
        }
    }
}
class Ring {
    constructor(length) {
        this.elements = new Array(length);
        this.index = 0;
        this.length = length;
    }
    save(value) {
        this.elements[this.index] = value;
        this.index = ++this.index % this.elements.length;
    }
    average() {
        let sum = 0;
        let total = 0;
        for (const element of this.elements) {
            if (_exists(element)) {
                sum += Number(element);
                total++;
            }
        }
        return total > 0 ? sum / total : 0;
    }
    multiStats() {
        let sum = 0;
        let total = 0;
        let min = Number.MAX_VALUE;
        let max = Number.MIN_VALUE;
        let allVals = [];
        for (const element of this.elements) {
            if (_exists(element)) {
                let val = Number(element);
                sum += val;
                total++;
                if (val < min) {
                    min = val;
                }
                if (val > max) {
                    max = val;
                }
                allVals.push(val);
            }
        }
        let avg = total > 0 ? sum / total : 0;
        return { min, max, avg, allVals, sum };
    }
    max() {
        let maxVal = Number.MIN_VALUE;
        for (const element of this.elements) {
            if (_exists(element)) {
                const val = Number(element);
                if (val > maxVal) {
                    maxVal = val;
                }
            }
        }
        return maxVal !== Number.MIN_VALUE ? maxVal : null;
    }
    previous() {
        const prevIndex = (this.index < 1 ? this.elements.length : this.index) - 1;
        return this.elements[prevIndex] || 0;
    }
    clear() {
        this.elements = new Array(this.length);
        this.index = 0;
    }
}
class CounterRing {
    constructor(length) {
        this.count = 0;
        this.total = 0;
        this.ring = new Ring(length);
    }
    increment() {
        ++this.count;
        ++this.total;
    }
    snapshot() {
        this.ring.save(this.count);
        this.count = 0;
    }
}
class WatcherRing {
    constructor(length, watchFn, context) {
        this.watchFn = watchFn.bind(context);
        this.ring = new Ring(length);
    }
    snapshot() {
        const value = this.watchFn();
        this.ring.save(value);
    }
}
class TimerRing {
    constructor(length) {
        this.ids = {};
        this.ring = new Ring(length);
    }
    start(id) {
        if (!this.ids[id]) {
            this.ids[id] = (0, network_1.shardusGetTime)();
        }
    }
    stop(id) {
        const entry = this.ids[id];
        if (entry) {
            delete this.ids[id];
        }
    }
    snapshot() {
        // Calc median duration of all entries in ids
        const durations = [];
        for (const id in this.ids) {
            const startTime = this.ids[id];
            // console.log('START_TIME ', startTime, 'ID', id)
            const duration = (0, network_1.shardusGetTime)() - startTime;
            utils.insertSorted(durations, duration, (a, b) => a - b);
        }
        const median = utils.computeMedian(durations, false);
        // Save median
        this.ring.save(median);
    }
}
class FifoStats {
    constructor(limit = 240) {
        this.items = [];
        this.length = limit;
    }
    // Add an item to the front of the queue
    save(item) {
        this.items.unshift(item);
        // Check if length has been exceeded
        if (this.items.length > this.length) {
            // Remove the oldest item (the last in the array)
            this.items.pop();
        }
    }
    // return average of all elements in the queue
    average() {
        let sum = 0;
        let total = 0;
        for (const element of this.items) {
            if (_exists(element)) {
                sum += Number(element);
                total++;
            }
        }
        return total > 0 ? sum / total : 0;
    }
    max() {
        let maxVal = Number.MIN_VALUE;
        for (const element of this.items) {
            if (_exists(element)) {
                const val = Number(element);
                if (val > maxVal) {
                    maxVal = val;
                }
            }
        }
        return maxVal !== Number.MIN_VALUE ? maxVal : null;
    }
    multiStats() {
        let sum = 0;
        let total = 0;
        let min = Number.MAX_VALUE;
        let max = Number.MIN_VALUE;
        let allVals = [];
        for (const item of this.items) {
            if (_exists(item)) {
                let val = Number(item);
                sum += val;
                total++;
                if (val < min) {
                    min = val;
                }
                if (val > max) {
                    max = val;
                }
                allVals.push(val);
            }
        }
        let avg = total > 0 ? sum / total : 0;
        return { min, max, avg, allVals, sum };
    }
}
class ManualRing {
    constructor(length) {
        this.ring = new Ring(length);
    }
    manualSetValue(value) {
        this.ring.save(value);
    }
    snapshot() { }
}
/**
 * Check for a variable that is not undefined or null
 * @param thing The parameter to check
 */
function _exists(thing) {
    return typeof thing !== 'undefined' && thing !== null;
}
exports.default = Statistics;
//# sourceMappingURL=index.js.map