// High Resolution Performance Tracker for Node.JS
// Copyright (c) 2014 - 2022 Joseph Huckaby
// Released under the MIT License

var Class = require("pixl-class");

module.exports = Class.create({
	
	perf: null,
	counters: null,
	scale: 1000, // milliseconds
	precision: 1000, // 3 digits
	totalKey: 'total', // where to store the total
	minMax: false, // track min/avg/max per metric
	
	__events: false,
	
	__construct: function() {
		// class constructor
		this.reset();
	},
	
	reset: function() {
		// reset everything
		this.perf = {};
		this.counters = {};
	},
	
	setScale: function(scale) { 
		// set scale for time measurements
		// 1000000000 == nanoseconds
		// 1000000 == microseconds
		// 1000 == milliseconds
		// 1 == seconds
		this.scale = scale; 
	},
	
	setPrecision: function(precision) {
		// set precision for measurements
		// 1 == integers only
		// 10 == 1 digit after the decimal
		// 100 == 2 digits after the decimal
		// 1000 == 3 digits after the decimal
		this.precision = precision; 
	},
	
	calcElapsed: function(start) {
		// calculate elapsed time using process.hrtime() (nanoseconds)
		// then convert to our scale and precision
		var diff = process.hrtime( start );
		var nano = diff[0] * 1e9 + diff[1];
		
		// apply scale transform
		var value = nano / (1000000000 / this.scale);
		
		return value;
	},
	
	begin: function(id) {
		// begin tracking metric
		// ID defaults to 't' for total
		if (!id) id = this.totalKey;
		var now = process.hrtime();
		
		// only allow 't' begin to be called once per object (unless it is reset)
		if ((id == this.totalKey) && this.perf[id]) return;
		
		// set start time
		if (!this.perf[id]) this.perf[id] = { elapsed: 0 };
		this.perf[id].start = now;
		if (this.perf[id].end) delete this.perf[id].end;
		
		return new PerfMetric(this, id, now);
	},
	
	end: function(id, start) {
		// mark end of metric
		// ID defaults to 't' for total
		if (!id) id = this.totalKey;
		var now = process.hrtime();
		
		if (!this.perf[id] && !start) return;
		if (!this.perf[id]) this.perf[id] = { elapsed: 0 };
		var obj = this.perf[id];
		
		if (start) obj.start = start;
		obj.end = now;
		if (!obj.start) obj.start = obj.end;
		
		var elapsed = Array.isArray(obj.start) ? 
			this.calcElapsed( obj.start ) : 0;
		
		if (id == this.totalKey) {
			// end of all tracking
			// set elapsed instead of incrementing, to prevent bugs with calling end() twice.
			obj.elapsed = elapsed;
		}
		else {
			// stopped tracking single metric
			// increment elapsed to allow for multiple trackings on same metric
			obj.elapsed += elapsed;
		}
		
		if (this.minMax) {
			if (!obj.count || (elapsed < obj.min)) obj.min = elapsed;
			if (!obj.count || (elapsed > obj.max)) obj.max = elapsed;
			if (!obj.count) obj.count = 0;
			obj.count++;
		}
		
		return this.formatValue(elapsed);
	},
	
	count: function(id, amount) {
		// increment (or decrement) simple counter, unrelated to time measurement
		if (typeof(amount) == 'undefined') amount = 1;
		if (!(id in this.counters)) this.counters[id] = amount;
		else this.counters[id] += amount;
	},
	
	metrics: function() {
		// get all perf metrics and counters in simple object format
		var out = {};
		
		// make sure total metric is ended
		this.end();
		
		// generate object containing only elapsed times of each
		for (var id in this.perf) {
			if (this.perf[id].end) {
				out[id] = this.elapsed(id, true);
			}
		}
		
		return {
			scale: this.scale,
			perf: out,
			counters: this.counters
		};
	},
	
	json: function() {
		// return a JSON string with perf metrics and counters separated out
		return JSON.stringify( this.metrics() );
	},
	
	summarize: function(prefix) {
		// Summarize performance metrics in query string format
		var pairs = [];
		var metrics = this.metrics();
		if (!prefix) prefix = '';
		
		// start with scale
		pairs.push( 'scale=' + this.scale );
		
		// make sure total is always right after scale
		pairs.push( 'total=' + metrics.perf.total );
		delete metrics.perf.total;
		
		// build summary string of other metrics
		for (var id in metrics.perf) {
			pairs.push( prefix + id + '=' + metrics.perf[id] );
		}
		
		// add counters if applicable, prefix each with c_
		for (var id in metrics.counters) {
			var disp_id = id.match(/^c_/) ? id : ('c_'+id);
			pairs.push( disp_id + '=' + metrics.counters[id] );
		}
		
		return pairs.join('&');
	},
	
	elapsed: function(id, disp) {
		// get elapsed seconds from given metric
		if (!id) id = this.totalKey;
		if (!this.perf[id]) return 0;
		
		var obj = this.perf[id];
		var elapsed = obj.elapsed || 0;
		
		if (!elapsed && obj.start && !obj.end) {
			// perf is still in progress -- return current elapsed
			elapsed = this.calcElapsed( obj.start );
		}
		
		return disp ? this.formatValue(elapsed) : elapsed;
	},
	
	get: function() {
		// Get raw perf object
		return this.perf;
	},
	
	getCounters: function() {
		// Get raw counters object
		return this.counters;
	},
	
	formatValue: function(value) {
		// format value according to our precision
		return Math.floor(value * this.precision) / this.precision;
	},
	
	getMinMaxMetrics: function() {
		// get min/max/avg/count/total for each named metric (omits total)
		// special 'minMax' mode must be enabled
		if (!this.minMax) return {};
		var metrics = {};
		
		for (var id in this.perf) {
			var obj = this.perf[id];
			if (obj.end && (id != this.totalKey)) {
				if (!obj.elapsed) obj.elapsed = 0;
				metrics[id] = {
					min: this.formatValue( obj.min || 0 ),
					max: this.formatValue( obj.max || 0 ),
					total: this.formatValue( obj.elapsed ),
					count: obj.count || 0,
					avg: this.formatValue( obj.elapsed / (obj.count || 1) )
				};
			}
		}
		
		return metrics;
	},
	
	import: function(perf, prefix) {
		// import perf metrics from another object (and adjust scale to match)
		// can be a pixl-perf instance, or an object from calling metrics()
		if (!prefix) prefix = '';
		
		if (perf.perf) {
			for (var key in perf.perf) {
				if (key != this.totalKey) {
					var pkey = prefix + key;
					if (!this.perf[pkey]) this.perf[pkey] = {};
					if (!this.perf[pkey].end) this.perf[pkey].end = 1;
					if (!this.perf[pkey].elapsed) this.perf[pkey].elapsed = 0;
					var elapsed = (typeof(perf.perf[key]) == 'number') ? perf.perf[key] : perf.perf[key].elapsed;
					this.perf[pkey].elapsed += (elapsed / (perf.scale / this.scale)) || 0;
					
					if (this.minMax && perf.minMax) {
						// both source and dest have minMax, so import entire min/max/count
						var adj_min = perf.perf[key].min / (perf.scale / this.scale);
						if (!this.perf[pkey].count || (adj_min < this.perf[pkey].min)) this.perf[pkey].min = adj_min;
						
						var adj_max = perf.perf[key].max / (perf.scale / this.scale);
						if (!this.perf[pkey].count || (adj_max > this.perf[pkey].max)) this.perf[pkey].max = adj_max;
						
						if (!this.perf[pkey].count) this.perf[pkey].count = 0;
						this.perf[pkey].count += perf.perf[key].count || 0;
					} // minMax
					else if (this.minMax) {
						// source has no minMax, but dest does, so just import their elapsed as one measurement
						var adj_elapsed = (elapsed / (perf.scale / this.scale)) || 0;
						if (!this.perf[pkey].count || (adj_elapsed < this.perf[pkey].min)) this.perf[pkey].min = adj_elapsed;
						if (!this.perf[pkey].count || (adj_elapsed > this.perf[pkey].max)) this.perf[pkey].max = adj_elapsed;
						if (!this.perf[pkey].count) this.perf[pkey].count = 0;
						this.perf[pkey].count++;
					}
				} // not totalKey
			} // foreach perf
		} // perf.perf
		
		if (perf.counters) {
			for (var key in perf.counters) {
				var pkey = prefix + key;
				this.count( pkey, perf.counters[key] );
			}
		}
	}
	
});

// A PerfMetric promise is returned from each call to begin(),
// so the user can track multiple simultaneous metrics with the same key.

var PerfMetric = Class.create({
	
	__events: false,
	
	perf: null,
	id: '',
	start: 0,
	
	__construct: function(perf, id, start) {
		// class constructor
		this.perf = perf;
		this.id = id;
		this.start = start;
	},
	
	end: function() {
		// end tracking
		return this.perf.end(this.id, this.start);
	}
	
});
