// beer.node.js - Matt Layher, 12/27/2012
// Core server for Beer Exchange clone.  Keeps all clients synchronized with its clock, and pushes
// beer and price updates to all attached clients simultaneously
//
// changelog:
// 2/1/13 MDL:
//  - minor fixes, beginning migration of PHP/MySQL helpers to node.js/sqlite
// 1/3/13 MDL:
//	- reorganization and creation of functions for common (or large) features
// 1/2/13 MDL:
//	- added triggers for firing events
//	- added micro HTTP server to advertise key for administrative interface
// 12/28/12 MDL:
//	- console control
//	- automatic algorithm
// 12/27/12 MDL:
//	- cache and push model for updates
//	- separate time period for crashes
//	- command line arguments
//	- multiple modes enabled
//	- authentication for price change and retrieval
// 12/26/12 MDL:
//	- initial code

// CONFIGURATION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

// Constants
// Debug
const DEBUG = true;
// Default time period (15 minutes)
const DEFAULT_TIME_START = 60 * 15;
// Default time period for market crash (5 minutes)
const DEFAULT_TIME_CRASH = 60 * 5;

// Default SQLite database
//const DEFAULT_DB_FILE = "beer.sqlite";

// Secret authorization key
const AUTH = "myfakekey";

// (Pseudo-constant) List of available modes
var MODES = { };
	MODES.random = "random";
	MODES.automatic = "automatic";
// Mode in use
const DEFAULT_MODE = "automatic";

// "Global" variables
// Defaults for time period, crash time period (can be overridden by arguments)
var TIME_START = DEFAULT_TIME_START;
var TIME_CRASH = DEFAULT_TIME_CRASH;

// Default database file (can be overriden by arguments)
//var DB_FILE = DEFAULT_DB_FILE;

// Global clock timer
var TIME;

// Global period counter
var PERIOD = 0;

// Defaults for running mode (can be overridden by arguments)
var MODE = MODES[DEFAULT_MODE];

// Enable verbose mode
var VERBOSE = false;

// Create global cache, used to push data to clients
var CACHE = { };

// Determine if enforcing a panic mode
var PANIC = false;

// Determine if stopping at end of period
var FORCE_STOP = false;

// Determine if crash occurs next period
var FORCE_CRASH = false;

// Determine if active crash
var ACTIVE_CRASH = false;

// STARTUP - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

// Import required libraries
var httpio = require('socket.io').listen(8080);
var qs = require('querystring');
var http = require('http');

// Import mhash for hashing
try
{
	var mhash = require('mhash').hash;
}
catch (e)
{
	console.log("[beer.node.js] - error: mhash not found, install using 'npm install mhash'");
	process.exit(-1);
}

/*
// Import sqlite3 for database
try
{
	var sqlite = require('sqlite3');
	var db = new sqlite.Database(DB_FILE);
}
catch (e)
{
	console.log("[beer.node.js] - error: sqlite3 not found, install using 'npm install sqlite3'");
	process.exit(-1);
}
*/

// Parse command line arguments
parse_argv();

// SOCKET SETUP - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

// On client connection, pass off to socket handler
httpio.on('connection', function(socket)
{
	socket_handler(socket);
});

// Configure socket.io to be silent
httpio.configure(function()
{
	httpio.set('log level', 0);
	httpio.set('match origin protocol', true);
});

// INITIALIZATION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

// Print startup messages
console.log("[beer.node.js] starting... [time: " + TIME_START + "s (" + (TIME_START / 60) + "m) | crash_time: " + TIME_CRASH + "s (" + (TIME_CRASH / 60) + "m)]");
console.log("[beer.node.js] mode: " + MODE);
if (VERBOSE)
{
	console.log("[beer.node.js] verbose mode enabled");
}

// Open stdin for console input
var stdin = process.openStdin();
stdin.on('data', function(chunk)
{
	stdin_handler(chunk);
});

// Generate authentication key
logger("[init] initializing");
var auth_key = mhash("sha256", AUTH);
logger("[init] authentication key generated");

// Start HTTP server to advertise key to local clients
http.createServer(function(req, res)
{
	res.writeHead(200, { 'Content-Type': 'text/plain' });
	res.end(auth_key);
}).listen(8081);
logger("[init] HTTP key server started");

// Generate default POST data
var POST_DATA = {
	key: auth_key,
	crash: FORCE_CRASH
}

// Update prices using randomization algorithm
post_handler("/beer/mode/" + MODES.random + ".php", "status", POST_DATA);
logger("[init] price randomization complete");

// Retrieve and cache most recent data on startup
post_handler("/beer/post/chart.php", "chart");
post_handler("/beer/post/ticker.php", "ticker");
logger("[init] data fetch into cache complete");

// End init
logger("[init] initialization complete");

// RUN - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

// Initialize timer
TIME = TIME_START;

// Start period counter
PERIOD++;

// Core timer countdown function
setInterval(function()
{
	// "Predict" market crash if one is forced
	if (TIME === 30 && FORCE_CRASH)
	{
		httpio.sockets.in('beer').emit('status', "Market crash predicted...");
	}
	// Perform price update (given a few extra seconds to ensure completion)
	if (TIME === 3)
	{
		// If crash requested, add triggering data
		POST_DATA.crash = FORCE_CRASH;

		// Reset force crash so we can't crash twice in a row
		if (ACTIVE_CRASH)
		{
			// Disable force crash	
			FORCE_CRASH = false;
			POST_DATA.crash = FORCE_CRASH;
		}
		
		// Perform specified price change algorithm
		post_handler("/beer/mode/" + MODE + ".php", "status", POST_DATA);
		logger("[update] price change complete");
	}
	// Fetch updates into cache
	if (TIME === 1)
	{
		// Get status from price update
		console.log("[update] [p: " + PERIOD + "] [m: " + MODE + "] status: " + CACHE.status);

		// Retrieve latest data for chart and ticker
		post_handler("/beer/post/chart.php", "chart");
		post_handler("/beer/post/ticker.php", "ticker");
		logger("[fetch] data fetch into cache complete");
	}
	// Push updates from cache
	if (TIME === 0)
	{
		// On force stop, exit here!
		if (FORCE_STOP)
		{
			console.log("[beer.node.js] period complete, stopping now!");
			process.exit(0);
		}

		// Reset crash status
		ACTIVE_CRASH = false;

		// Fire and increment trading period
		PERIOD++;
		httpio.sockets.in('beer').emit('period', PERIOD);

		// Trigger an update for all clients
		httpio.sockets.in('beer').emit('update', CACHE);

		// If crash occurred, send message and shorten timer
		if (CACHE.status === "crash")
		{
			// Trigger crash notification
			ACTIVE_CRASH = true;
			console.log("[push] market crash triggered!");
			httpio.sockets.in('beer').emit('crash', true);

			// Add 1s to account for slight delay
			TIME = parseInt(TIME_CRASH) + 1;
		}
		else
		{
			// Else, reset timer to default (add 1s to account for slight delay)
			TIME = parseInt(TIME_START) + 1;
		}

		// Reset update status
		logger("[push] update push complete, time reset to " + (TIME - 1) + "s");
	}

	// Decrement timer, enforce panic, repeat
	TIME--;
	httpio.sockets.in('beer').emit('panic', PANIC);
	httpio.sockets.in('beer').emit('timer', TIME);
}, 1000);

// FUNCTIONS - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

// Unified logging
function logger(string)
{
	// Log to file?
	
	// If verbose, output to console
	if (VERBOSE)
	{
		console.log(string);
	}
}

// Parse and set command line arguments
function parse_argv()
{
	if (DEBUG)
	{
		console.log("parse_argv()");
	}

	// Retrieve "argv" arguments from node.js
	var argv = process.argv.splice(2);

	// Iterate command line arguments
	for (var i = 0; i < argv.length; i++)
	{
		// Print help menu
		if (argv[i] === "-h" || argv[i] === "--help")
		{
			console.log("Usage: node beer.node.js [options] [arguments]\n");
			console.log("Options:");
			console.log("  -h, --help	  print this menu");
			console.log("  -m, --mode	  select operation mode [random, automatic]");
			console.log("  -t, --time     set time period");
			console.log(" -tc, --timec    set crash time period");
			console.log("  -v, --verbose  enable console logging");
			process.exit(0);
		}
		// Set operations mode
		else if (argv[i] === "-m" || argv[i] === "--mode")
		{
			// Use next argument to set mode, default if argument null
			if (argv[i+1])
			{
				// Skip to value
				i++;
				
				// Validate mode type
				if (argv[i] === MODES.random || argv[i] === MODES.automatic)
				{
					// Set specified mode
					MODE = MODES[argv[i]];
				}
				else
				{
					// Default on bad mode type
					MODE = MODES[DEFAULT_MODE]
				}
			}
			else
			{
				// Default to default mode
				MODE = DEFAULT_MODE;
			}
		}
		// Set default and crash time periods
		else if (argv[i] === "-t" || argv[i] === "-tc" || argv[i] === "--time" || argv[i] === "--timec")
		{
			// Store argument
			var arg = argv[i];
		
			// Use next argument to set time, default if argument null
			if (argv[i+1])
			{
				// Skip to value
				i++;

				// Validate as integer, ensure time is greater than 10 seconds
				if ((argv[i] == parseInt(argv[i])) && (parseInt(argv[i]) >= 10))
				{
					// Set specified time in seconds
					if (arg === "-t" || arg === "--time")
					{
						TIME_START = argv[i];
					}
					else
					{
						// Else, set specified crash time in seconds
						TIME_CRASH = argv[i];
					}
				}
				else
				{
					// If value doesn't validate, set both to defaults
					console.log("[beer.node.js] - error: time too short (< 10s) or invalid, using defaults");
					TIME_START = DEFAULT_TIME_START;
					TIME_CRASH = DEFAULT_TIME_CRASH;
				}
			}
		}
		// Enable verbose console logging
		else if (argv[i] === "-v" || argv[i] === "--verbose")
		{
			// Set verbose mode true
			VERBOSE = true;
		}
		else
		{
			// Else, invalid argument
			console.log("[beer.node.js] - error: invalid argument '" + argv[i] + "'");
			process.exit(-1);
		}
	}
}

// Send POST request to target, put result into cache key
function post_handler(handler, cache_key, data)
{
	if (DEBUG)
	{
		console.log("post_handler(" + handler + ", " + cache_key + ", " + data + ")");
	}

	// Generate POST data from input object
	var post_data = qs.stringify(data);

	// Set POST options and destination
	var post_options = {
		host: "servnerr.com",
		port: 80,
		path: handler,
		method: "POST",
		headers: {
			'Content-Type': 'application/x-www-form-urlencoded',
			'Content-Length': post_data.length
		}
	}

	// Initialize a POST request
	var post_req = http.request(post_options, function(response)
	{
		// Capture UTF-8 response
		response.setEncoding('utf8');
		var resp_str = '';

		// When data is received, add the data chunk
		response.on('data', function(chunk)
		{
			resp_str += chunk;
		});

		// On data complete, cache the received response
		response.on('end', function()
		{
			CACHE[cache_key] = resp_str;
		});

		// On data error, log error
		response.on('error', function()
		{
			console.log("[post] error occurred!");
			console.log("[post] response: " + resp_str);
		});
	});

	// Send POST request
	post_req.write(post_data);
	post_req.end();
}

// Handle all socket events fired
function 
(socket)
{
	// User handlers
	// INIT - used to initialize clients with all current data (always fire first!)
	socket.on('init', function(obj)
	{
		// Join them to the 'beer' room, so we can perform other functions
		socket.join('beer');
		logger("[info] new client connected");

		// Send client current timer
		socket.emit('timer', TIME);

		// Send client current period
		socket.emit('period', PERIOD);

		// Send client current cached data
		socket.emit('update', CACHE);

		// Trigger crash notification if one is active
		if (ACTIVE_CRASH)
		{
			socket.emit('crash', true);
		}
	});

	// Administrative handlers (require correct auth_key)
	// CRASH - used to toggle a forced market crash next period
	socket.on('crash', function(obj)
	{
		// Verify correct key sent to trigger
		if (obj.key === auth_key)
		{
			// Toggle crash status
			FORCE_CRASH = !FORCE_CRASH;
			if (FORCE_CRASH)
			{
				console.log("[admin] force crash enabled, will crash next period");
			}
			else
			{
				console.log("[admin] force crash disabled, will not crash next period");
			}
		}
		else
		{
			console.log("[admin] could not toggle force crash, invalid key");
		}
	});

	// HALT - used to immediately terminate the program
	socket.on('halt', function(obj)
	{
		// Verify correct key sent to trigger
		if (obj.key === auth_key)
		{
			console.log("[admin] halting!");
			process.exit(0);
		}
		else
		{
			console.log("[admin] could not halt, invalid key");
		}
	});

	// PANIC - used to toggle the panic mode
	socket.on('panic', function(obj)
	{
		// Verify correct key sent to trigger
		if (obj.key === auth_key)
		{
			// Toggle panic mode
			PANIC = !PANIC;
			if (PANIC)
			{
				console.log("[admin] panic mode enabled, all screens blacked out");
			}
			else
			{
				console.log("[admin] panic mode disabled, all screens resumed");
			}
		}
		else
		{
			console.log("[admin] could not toggle panic, invalid key");
		}
	});

	// STOP - used to toggle a forced stop after this period
	socket.on('stop', function(obj)
	{
		// Verify correct key sent to trigger
		if (obj.key === auth_key)
		{
			// Toggle force stop
			FORCE_STOP = !FORCE_STOP;
			if (FORCE_STOP)
			{
				console.log("[admin] force stop enabled, will stop next period");
			}
			else
			{
				console.log("[admin] force stop disabled, will not stop next period");
			}
		}
		else
		{
			console.log("[admin] could not force stop, invalid key");
		}
	});

	// UPDATE - used to drop timer to lowest possible interval, triggering a quick update
	socket.on('update', function(obj)
	{
		// Verify correct key sent to trigger
		if (obj.key === auth_key)
		{
			console.log("[admin] force update triggered, setting time to 5 seconds");
			TIME = 5;
		}
		else
		{
			console.log("[admin] could not trigger force update, invalid key");
		}
	});
}

// Handle stdin (console) data processing
function stdin_handler(chunk)
{
	// Split into array
	var chunk = chunk.toString();
	var input = chunk.substring(0, chunk.length -1).split(' ');
	var i = 0;

	// Clear console
	if (input[i] === "clear")
	{
		// Linux trick to clear console
		console.log('\033[2J');	
	}
	// Toggle forced crash
	else if (input[i] === "crash")
	{
		// Toggle crash status
		FORCE_CRASH = !FORCE_CRASH;
		if (FORCE_CRASH)
		{
			console.log("[console] force crash enabled, will crash next period");
		}
		else
		{
			console.log("[console] force crash disabled, will not crash next period");
		}
	}
	// Display a help menu
	else if (input[i] === "help")
	{
		console.log("[beer.node.js] - Matt Layher 2012-2013");
		console.log("commands:");
		console.log("  clear	clear this console");
		console.log("  crash	force a crash next period");
		console.log("   halt	immediately stop server");
		console.log("   mode	change operation mode");
		console.log("    msg	display a message on client status bar");
		console.log("  panic	black out all client displays");
		console.log("   stat	display current state of server variables");
		console.log("   stop	force server to stop next period");
		console.log("   time	display or set the current period time");
		console.log(" update	force a price update");
		console.log("verbose	display verbose console logging");
	}
	// Check for force halt
	else if (input[i] === "halt")
	{
		// Immediately halt and exit
		console.log("[console] halting!");
		process.exit(0);
	}
	// Check for mode change
	else if (input[i] === "mode")
	{
		// Check for argument
		if (input[i+1])
		{
			// Increment index
			i++;

			// Check for valid mode type (cannot swap to manual now)
			if (input[i] == MODES.random || input[i] == MODES.automatic)
			{
				// Set specified mode
				MODE = MODES[input[i]];
				console.log("[console] switching to mode: " + MODE);
			}
			else
			{
				console.log("[console] invalid mode specified, no change");
			}
		}
		else
		{
			// Display current mode
			console.log("[console] current mode: " + MODE);
		}
	}
	// Send a message to the status bar on clients
	else if (input[i] === "msg")
	{
		// Check for argument
		if (input[i+1])
		{
			// Increment index
			i++;

			// Iterate remaining arguments to process message
			var message = "";
			for (var j = i; j < input.length; j++)
			{
				message += input[j] + " ";
			}

			// Send message to clients
			httpio.sockets.in('beer').emit('status', message);
		}
		else
		{
			// Clear status on clients
			httpio.sockets.in('beer').emit('status', '');
		}
	}
	// Toggle a panic on the displays
	else if (input[i] === "panic")
	{
		// Toggle panic mode
		PANIC = !PANIC;
		if (PANIC)
		{
			console.log("[console] panic mode enabled, all screens blacked out");
		}
		else
		{
			console.log("[console] panic mode disabled, all screens resumed");
		}
	}
	// Display current program status
	else if (input[i] === "stat")
	{
		console.log("[beer.node.js] - Matt Layher, 2012-2013");
		console.log("[console]   TIME_START : " + TIME_START);
		console.log("[console]   TIME_CRASH : " + TIME_CRASH);
		console.log("[console]         MODE : " + MODE);
		console.log("[console]         TIME : " + TIME);
		console.log("[console]       PERIOD : " + PERIOD);
		console.log("[console]      VERBOSE : " + VERBOSE);
		console.log("[console]        PANIC : " + PANIC);
		console.log("[console]   FORCE_STOP : " + FORCE_STOP);
		console.log("[console]  FORCE_CRASH : " + FORCE_CRASH);
		console.log("[console] ACTIVE_CRASH : " + ACTIVE_CRASH);
	}
	// Toggle force stop
	else if (input[i] === "stop")
	{
		// Toggle force stop
		FORCE_STOP = !FORCE_STOP;
		if (FORCE_STOP)
		{
			console.log("[console] force stop enabled, will stop next period");
		}
		else
		{
			console.log("[console] force stop disabled, will not stop next period");
		}
	}
	// Display/modify time remaining in period
	else if (input[i] === "time")
	{
		// If further input exists...
		if (input[i+1])
		{
			i++;
			
			// Validate as integer, ensure time is greater than 10 seconds
			if ((input[i] == parseInt(argv[i])) && (parseInt(input[i]) >= 10))
			{
				TIME = input[i];
			}
			else
			{
				// Else, invalid time, display error
				console.log("[beer.node.js] - error: time too short (< 10s) or invalid");
			}
		}
		else
		{
			// Else, display current remaining time
			console.log("[console] " + TIME + " seconds remain");
		}
	}
	// Force an update (drop timer very low to finish clock cycle)
	else if (input[i] === "update")
	{
		console.log("[console] force update triggered, setting time to 5 seconds");
		TIME = 5;
	}
	// Toggle verbose mode
	else if (input[i] === "verbose")
	{
		// Toggle verbose mode
		VERBOSE = !VERBOSE;
		if (VERBOSE)
		{
			console.log("[console] verbose mode enabled");
		}
		else
		{
			console.log("[console] verbose mode disabled");
		}
	}
	else
	{
		// Print console help
		console.log("[console] invalid command");
	}
}

socket_handler