/**
 * @fileOverview Pretty Debug Javascript
 * @author Shadly Salahuddin
 * @link mailto:shadlyd15@gmail.com
 * @version 2.0.2
 */

const fs	=	require('fs');
const util	=	require('util');
const options = require('./defaultOptions');
const ansiColors = require('./ansiColors');

function _updateOptions(obj, options = {}) {
    for (var key in options) {
        if (typeof options[key] === "object") {
            _updateOptions(obj[key], options[key]);   
        } else {
            obj[key] = options[key];
        }
    }
}

function _checkUniqueStream(targetArray, targetValue){ 
    for(let i = 0; i < targetArray.length; i++){
    	if(targetArray[i] === targetValue) return false;    	
    }
    return true;
}

function _paintText(text, color, resetColor = ''){
	return color + text + resetColor;
}

function _renderTextSegment(text, color, resetColor = ''){
	if(text === undefined) return '';
	return _paintText('[' + text + ']', options.enableColor?color:'', options.enableColor?resetColor:'');
}

function _getCurrentDateTime(){
	if(options['dateTime']['show'] !== true) return undefined;
	return new Date().toLocaleTimeString('en-US', options['dateTime']['format']);
}

function _renderMessage(tag, functionName, fileLocation, message, color){
	return	_renderTextSegment(_getCurrentDateTime(), options['dateTime']['color'], ' ')
			+ _renderTextSegment(functionName, options['funcName']['color'], ' ')
			+ _renderTextSegment(fileLocation, options['fileLocation']['color'], ' ')
			+ _renderTextSegment(tag, color, ' : ')
			+ _renderTextSegment(message, color, ansiColors.reset);
}

function _getFunctionCallLocation(){
	var err = new Error();
	Error.captureStackTrace(err);
	let fileLocation;
	let functionName;

	if(options.fileLocation.show === true){
		const regexFile = /\((.*)\)$/;
		const matchFile = regexFile.exec(err.stack.split(/\r\n|\n/, 4)[3]);
		if((matchFile != null) && (matchFile.length > 1)){
			fileLocation = matchFile[1].replace(/^.*[\\\/]/, '');
		}
	}
	if(options.funcName.show === true){
		if(fileLocation){
			functionName = err.stack
							.split('\n', 4)[3]
							.replace(/^\s+at\s+(.+?)\s.+/g, '$1' );
	    } else{
	    	functionName = 'Callback';
	    }
	}
	return {
		functionName : functionName,
		fileLocation : fileLocation
	}
}

function _bytesToMb(kilo){
	return Math.round(kilo / 1024 / 1024 * 100) / 100;
}

function _setWaterMark(watermark, currentValue){
	watermark.now = currentValue;
	if(watermark.peak <= currentValue){
		watermark.peak = currentValue;
		watermark.time 	= new Date().toLocaleTimeString('en-US', options['memoryWatermark']['dateTime']['format']);
	}
}

function _checkAlarmPolicy(policy, currentValue){
	for (var key in policy) {
		if (policy[key]['lowerLimit'] > currentValue[key]){ 
			return true;
		}
		if (policy[key]['upperLimit'] < currentValue[key]){
			return true;
		}
	}
}

function _generateFunction(tag){
	return function(){
		if((options.enable != true) || options[tag]['level'] > options['debugLevel']) return;
		const currentLocationInfo = _getFunctionCallLocation();
		_printToAllStreams(_renderMessage(	options[tag]['tag'],
											currentLocationInfo['functionName'], 
											currentLocationInfo['fileLocation'], 
											util.format.apply(this, arguments),
											options[tag].color)
		);
	};
}

function _generateTextFromObj(obj, callback){
	let text = '';
	for(let key in obj){
		if(obj[key]){
			text = text + callback(key) ;
		}
	}
	return text;
}

function _printStream(stream, message){
	if(stream){
		stream.write(message + '\n');
	}
};

function _printToAllStreams(message){
	debugStreams.forEach(function(stream){
		_printStream(stream, message);
	});
};


/**
 * @module pretty-debug
 * @desc A highly configurable & lightweight debug library that prints debug messages beautifully. 
 * It works in Node.js and Web browser environments with very low memory footprint. 
 */

let debugStreams = [process.stdout];

module.exports = {
  /** Different ANSI color to decorate different segments
	* @type {Object.<string>}
	* @property {string} reset		- Text Color Reset
	* @property {string} black 		- Text Color Block
	* @property {string} red 		- Text Color Red
	* @property {string} green 		- Text Color Green
	* @property {string} yellow 	- Text Color Yellow
	* @property {string} blue 		- Text Color Blue
	* @property {string} magenta 	- Text Color Magenta
	* @property {string} cyan 		- Text Color Cyan
	* @property {string} white 		- Text Color Cyan
	* @property {string} bgBlack 	- Background Color Black
	* @property {string} bgRed 		- Background Color Red
	* @property {string} bgGreen 	- Background Color Green
	* @property {string} bgYellow 	- Background Color Yellow
	* @property {string} bgBlue 	- Background Color Blue
	* @property {string} bgMagenta 	- Background Color Magenta
	* @property {string} bgCyan 	- Background Color Cyan
	* @property {string} bgWhite 	- Background Color White
	*/
	color : ansiColors,

  /**
	* Overwrites default options and debug text formats
	* @param {Object.<userOptions>} userOptions - Options User specific options
	*/
	setOptions: function setOptions(userOptions){
		_updateOptions(options, userOptions);
	},

  /**
	* Generates policy for different memory monitors
	* @param {number} lowerLimit - Lower limit
	* @param {number} upperLimit - Upper limit
	* @return {Object.<policy>}
	*/
	generatePolicy: function generatePolicy(lower = 0, upper = 100){
		return {
			lowerLimit : `${lower}`,
			upperLimit : `${upper}`
		};
	},

  /**
	* Attaches stream to pipe debug output
	* @param {Object.<stream>} stream - Stream to attach
	*/
	attachStream: function attachStream(stream){
		if(stream && _checkUniqueStream(debugStreams, stream)){
			debugStreams.push(stream);
			this.alert('New Debug Stream Attached');
		}
	},

  /**
	* Detaches stream from debug output
	* @param {Object.<stream>} stream - Stream to detach
	*/
	detachStream: function detachStream(stream){
		let filteredStreams = debugStreams.filter(function(value){
		    return ( value != stream );
		});
		debugStreams = filteredStreams;
		this.info('Debug Stream Detached');
	},

  /**
	* Prints log messages in level 6
	* @function
	* @param {...*} var_args Variadic Argument
	* @example
	* debug.log("Just a simple log message");
	* @example
	* const policy = { upperLimit : 70, lowerLimit : 20 };
	* debug.log(policy);
	*/
	log			: _generateFunction('log'),

  /**
	* Prints info messages in level 5
	* @function
	* @param {...*} var_args Variadic Argument
	* @example
	* debug.info("Here is an info message.");
	* @example
	* const birthTimestamp = { date : "15/12/1993", time : "12:05 AM" };
	* debug.info(birthTimestamp);
	*/
	info		: _generateFunction('info'),

  /**
	* Prints alert messages in level 4
	* @function
	* @param {...*} var_args Variadic Argument
	* @example
	* debug.alert("An alert message!");
	* @example
	* const birthTimestamp = { date : "15/12/1993", time : "12:05 AM" };
	* debug.alert(birthTimestamp);
	*/
	alert		: _generateFunction('alert'),
  
  /**
	* Prints warn messages in level 3
	* @function
	* @param {...*} var_args Variadic Argument
	* @example
	* debug.warn("This is your last warning.");
	* @example
	* const resource = { RAM : 10, CPU : 60 };
	* debug.warn(resource);
	*/
	warn		: _generateFunction('warn'),

  /**
	* Prints error messages in level 2
	* @function
	* @param {...*} var_args Variadic Argument
	* @example
	* debug.error("Error in recording server");
	* @example
	* const errorMessage = { message : "Some Error", errorCode : 4 };
	* debug.error(errorMessage);
	*/
	error		: _generateFunction('error'),

  /**
	* Prints critical messages in level 1
	* @function
	* @param {...*} var_args Variadic Argument
	* @example
	* debug.critical("API server critical error");
	* @example
	* const criticalMessage = { message : "Some Critical Error" };
	* debug.critical(criticalMessage);
	*/
	critical	: _generateFunction('critical'),

  /**
	* Prints RAM usage by Node.js
	* @param {Object.<stream>} stream - Stream Object
	* @param {function} callback - Callback function to invoke when alarm triggers
	* @example
	* debug.nodeMemoryMonitor({
	*		heapTotal: { upperLimit : 100 }
	* 	}, function(){
	*		debug.critical('Memory Usage Alarm : Total heap usage is above 100 MB');
	*	}
	* );
	*/
	nodeMemoryMonitor: function nodeMemoryMonitor(alarmPolicy = {}, callback = null){
		if(options.enable != true) return;
		const nodeMemInfo = process.memoryUsage();

		let message = _renderMessage(	'MEMORY',
										'Node', 
										undefined, 
										_generateTextFromObj(options.nodeMemoryMonitor.fields, function(key){
											return `| ${key} : ${_bytesToMb(nodeMemInfo[key])} MB |`;
										}),
										options['nodeMemoryMonitor']['color']);

		_setWaterMark(options.memoryWatermark.fields['Node'], _bytesToMb(nodeMemInfo.heapUsed));
		_printToAllStreams(message);

		if(_checkAlarmPolicy(alarmPolicy, nodeMemInfo) && callback) callback();
	},

  /**
	* Prints RAM usage by operating system
	* @param {Object.<alarmPolicy>} alarmPolicy - Policy object to trigger alarm
	* @param {function} callback - Callback function to invoke when alarm triggers	
	* @example
	* debug.sysMemoryMonitor({
	*		MemTotal: { upperLimit : 700 }
	* 	}, function(){
	*		debug.critical('Memory Usage Alarm : Total system memory usage is above 700 MB');
	*	}
	* );
	*/
	sysMemoryMonitor: function sysMemoryMonitor(alarmPolicy = {}, callback = null){
		if(options.enable != true) return;
		fs.readFile('/proc/meminfo', function (err, data){
			if (err) throw err;
			var info = {};
			data.toString().split(/\n/g).forEach(function(line){
			   line = line.split(':');
			   if (line.length < 2){
			       return;
			   }	
			   info[line[0]] = Math.round(parseInt(line[1].trim(), 10) / 1024);
			});

			let message = _renderMessage(	'MEMORY',
											'System', 
											undefined,
											_generateTextFromObj(options.sysMemoryMonitor.fields, function(key){
												return `| ${key} : ${info[key]} MB |`;
											}),
											options['sysMemoryMonitor'].color);

			_setWaterMark(options.memoryWatermark.fields['Swap'], info['SwapTotal'] - info['SwapFree']);
			_setWaterMark(options.memoryWatermark.fields['RAM'], info['MemTotal'] - info['MemAvailable']);

			_printToAllStreams(message);

			if(_checkAlarmPolicy(alarmPolicy, info) && callback) callback();
		});
	},

  /**
	* Prints highest RAM usage in application. Scheduled healthcheck is needed to set watermark.
	* @example memoryWatermark();
	*/
	memoryWatermark: function memoryWatermark(){
		if(options.enable != true) return;
		let message = _renderMessage(	'Watermark',
										undefined, 
										undefined,
										_generateTextFromObj(options.memoryWatermark.fields, function(key){
											return `| ${key} : ${options.memoryWatermark.fields[key].peak} MB @ ${options.memoryWatermark.fields[key].time} |`;
										}),
										options['memoryWatermark'].color);

		_printToAllStreams(message);
	},

  /**
	* Set Schedule to perform healthcheck
	* @param {function} inputFunc - Healthcheck function
	* @param {numbers} timeInMinutes - Interval in minutes
	* @example
	* debug.scheduleHealthCheck(function(){
	* 	debug.memoryWatermark();
	* 	debug.sysMemoryMonitor();
	* 	debug.nodeMemoryMonitor({
	* 		heapTotal: { upperLimit : 5 }
	* 	}, function(){
	* 		debug.critical('Memory Usage Alarm : Total heap usage is above 5 MB');
	* 	});
	* }, 10);
	*/
	scheduleHealthCheck: function scheduleHealthCheck(inputFunc, timeInMinutes){
		if(options.enable != true) return;
		setTimeout(function(){
			if(options['enableGC'] && global.gc){
				global.gc();
			}
			if(inputFunc){
				inputFunc();
				scheduleHealthCheck(inputFunc, timeInMinutes);
			}
		}, timeInMinutes * 60 * 1000);
	}
};