
/* Controllers */

netStatsApp.controller('StatsCtrl', function($scope, $filter, socket, _, toastr) {

	var MAX_BINS = 40;

	// Main Stats init
	// ---------------

	$scope.frontierHash = '?';
	$scope.nodesTotal = 0;
	$scope.nodesActive = 0;
	$scope.bestBlock = 0;
	$scope.lastBlock = 0;
	$scope.lastDifficulty = 0;
	$scope.upTimeTotal = 0;
	$scope.avgBlockTime = 0;
	$scope.blockPropagationAvg = 0;
	$scope.avgHashrate = 0;
	$scope.uncleCount = 0;
	$scope.bestStats = {};

	$scope.lastBlocksTime = _.fill(Array(MAX_BINS), 2);
	$scope.difficultyChart = _.fill(Array(MAX_BINS), 2);
	$scope.transactionDensity = _.fill(Array(MAX_BINS), 2);
	$scope.gasSpending = _.fill(Array(MAX_BINS), 2);
	$scope.miners = [];


	$scope.nodes = [];
	$scope.map = [];
	$scope.blockPropagationChart = [];
	$scope.uncleCountChart = _.fill(Array(MAX_BINS), 2);
	$scope.coinbases = [];

	$scope.latency = 0;

	$scope.currentApiVersion = "0.0.16";

	$scope.predicate = ['-pinned', '-stats.active', '-stats.block.number', 'stats.block.propagation'];
	$scope.reverse = false;
	$scope.pinned = [];

	$scope.prefixPredicate = ['-pinned', '-stats.active'];
	$scope.originalPredicate = ['-stats.block.number', 'stats.block.propagation'];

	$scope.orderTable = function(predicate, reverse)
	{
		if(!_.isEqual(predicate, $scope.originalPredicate))
		{
			$scope.reverse = reverse;
			$scope.originalPredicate = predicate;
			$scope.predicate = _.union($scope.prefixPredicate, predicate);
		}
		else
		{
			$scope.reverse = !$scope.reverse;

			if($scope.reverse === true){
				_.forEach(predicate, function (value, key) {
					predicate[key] = (value[0] === '-' ? value.replace('-', '') : '-' + value);
				});
			}

			$scope.predicate = _.union($scope.prefixPredicate, predicate);
		}
	}

	$scope.pinNode = function(id)
	{
		index = findIndex({id: id});

		if( !_.isUndefined($scope.nodes[index]) )
		{
			$scope.nodes[index].pinned = !$scope.nodes[index].pinned;

			if($scope.nodes[index].pinned)
			{
				$scope.pinned.push(id);
			}
			else
			{
				$scope.pinned.splice($scope.pinned.indexOf(id), 1);
			}
		}
	}

	var timeout = setInterval(function ()
	{
		$scope.$apply();
	}, 300);

	$scope.getNumber = function (num) {
		return new Array(num);
	}

	// Socket listeners
	// ----------------

	socket.on('open', function open() {
		socket.emit('ready');
		console.log('The connection has been opened.');
	})
	.on('end', function end() {
		console.log('Socket connection ended.')
	})
	.on('error', function error(err) {
		console.log(err);
	})
	.on('reconnecting', function reconnecting(opts) {
		console.log('We are scheduling a reconnect operation', opts);
	})
	.on('data', function incoming(data) {
		$scope.$apply(socketAction(data.action, data.data));
	});

	socket.on('init', function(data)
	{
		$scope.$apply(socketAction("init", data.nodes));
	});

	socket.on('client-latency', function(data)
	{
		$scope.latency = data.latency;
	})

	function socketAction(action, data)
	{
		// console.log('Action: ', action);
		// console.log('Data: ', data);

		switch(action)
		{
			case "init":
				$scope.nodes = data;

				_.forEach($scope.nodes, function (node, index) {
					// Init hashrate
					if( _.isUndefined(node.stats.hashrate) )
						node.stats.hashrate = 0;

					// Init latency
					latencyFilter(node);

					// Init history
					if( _.isUndefined(data.history) )
					{
						data.history = new Array(40);
						_.fill(data.history, -1);
					}

					// Init or recover pin
					node.pinned = ($scope.pinned.indexOf(node.id) >= 0 ? true : false);
				});

				if( $scope.nodes.length > 0 )
				{
					toastr['success']("Got nodes list", "Got nodes!");

					updateActiveNodes();
				}

				break;

			case "add":
				var index = findIndex({id: data.id});

				if( addNewNode(data) )
					toastr['success']("New node "+ $scope.nodes[findIndex({id: data.id})].info.name +" connected!", "New node!");
				else
					toastr['info']("Node "+ $scope.nodes[index].info.name +" reconnected!", "Node is back!");

				break;

			// TODO: Remove when everybody updates api client to 0.0.12
			case "update":
				var index = findIndex({id: data.id});

				if( index >= 0 && !_.isUndefined($scope.nodes[index]) && !_.isUndefined($scope.nodes[index].stats) )
				{
					if( !_.isUndefined($scope.nodes[index].stats.latency) )
						data.stats.latency = $scope.nodes[index].stats.latency;

					if( _.isUndefined(data.stats.hashrate) )
						data.stats.hashrate = 0;

					if( $scope.nodes[index].stats.block.number < data.stats.block.number )
					{
						var best = _.max($scope.nodes, function (node) {
							return parseInt(node.stats.block.number);
						}).stats.block;

						if (data.stats.block.number > best.number) {
							data.stats.block.arrived = _.now();
						} else {
							data.stats.block.arrived = best.arrived;
						}

						$scope.nodes[index].history = data.history;
					}

					$scope.nodes[index].stats = data.stats;

					if( !_.isUndefined(data.stats.latency) && _.get($scope.nodes[index], 'stats.latency', 0) !== data.stats.latency )
					{
						$scope.nodes[index].stats.latency = data.stats.latency;

						latencyFilter($scope.nodes[index]);
					}

					updateBestBlock();
				}

				break;

			case "block":
				var index = findIndex({id: data.id});

				if( index >= 0 && !_.isUndefined($scope.nodes[index]) && !_.isUndefined($scope.nodes[index].stats) )
				{
					if( $scope.nodes[index].stats.block.number < data.block.number )
					{
						var best = _.max($scope.nodes, function (node) {
							return parseInt(node.stats.block.number);
						}).stats.block;

						if (data.block.number > best.number) {
							data.block.arrived = _.now();
						} else {
							data.block.arrived = best.arrived;
						}

						$scope.nodes[index].history = data.history;
					}

					$scope.nodes[index].stats.block = data.block;
					$scope.nodes[index].stats.propagationAvg = data.propagationAvg;

					updateBestBlock();
				}

				break;

			case "pending":
				var index = findIndex({id: data.id});

				if( !_.isUndefined(data.id) && index >= 0 )
				{
					var node = $scope.nodes[index];

					if( !_.isUndefined(node) && !_.isUndefined(node.stats.pending) && !_.isUndefined(data.pending) )
						$scope.nodes[index].stats.pending = data.pending;
				}

				break;

			case "stats":
				var index = findIndex({id: data.id});

				if( !_.isUndefined(data.id) && index >= 0 )
				{
					var node = $scope.nodes[index];

					if( !_.isUndefined(node) && !_.isUndefined(node.stats) )
					{
						$scope.nodes[index].stats.active = data.stats.active;
						$scope.nodes[index].stats.mining = data.stats.mining;
						$scope.nodes[index].stats.hashrate = data.stats.hashrate;
						$scope.nodes[index].stats.peers = data.stats.peers;
						$scope.nodes[index].stats.gasPrice = data.stats.gasPrice;
						$scope.nodes[index].stats.uptime = data.stats.uptime;

						if( !_.isUndefined(data.stats.latency) && _.get($scope.nodes[index], 'stats.latency', 0) !== data.stats.latency )
						{
							$scope.nodes[index].stats.latency = data.stats.latency;

							latencyFilter($scope.nodes[index]);
						}

						updateActiveNodes();
					}
				}

				break;

			case "info":
				var index = findIndex({id: data.id});

				if( index >= 0 )
				{
					$scope.nodes[index].info = data.info;

					if( _.isUndefined($scope.nodes[index].pinned) )
						$scope.nodes[index].pinned = false;

					// Init latency
					latencyFilter($scope.nodes[index]);

					updateActiveNodes();
				}

				break;

			case "blockPropagationChart":
				$scope.blockPropagationChart = data.histogram;
				$scope.blockPropagationAvg = data.avg;

				break;

			case "uncleCount":
				$scope.uncleCount = data[0] + data[1];
				data.reverse();
				$scope.uncleCountChart = data;

				break;

			case "charts":
				if( !_.isEqual($scope.avgBlockTime, data.avgBlocktime) )
					$scope.avgBlockTime = data.avgBlocktime;

				if( !_.isEqual($scope.avgHashrate, data.avgHashrate) )
					$scope.avgHashrate = data.avgHashrate;

				if( !_.isEqual($scope.lastBlocksTime, data.blocktime) && data.blocktime.length >= MAX_BINS )
					$scope.lastBlocksTime = data.blocktime;

				data.uncleCount.reverse();

				if( !_.isEqual($scope.uncleCountChart, data.uncleCount) && data.uncleCount.length >= MAX_BINS ) {
					$scope.uncleCount = data.uncleCount[data.uncleCount.length-2] + data.uncleCount[data.uncleCount.length-1];
					$scope.uncleCountChart = data.uncleCount;
				}

				break;

			case "inactive":
				var index = findIndex({id: data.id});

				if( index >= 0 )
				{
					if( !_.isUndefined(data.stats) )
						$scope.nodes[index].stats = data.stats;

					toastr['error']("Node "+ $scope.nodes[index].info.name +" went away!", "Node connection was lost!");

					updateActiveNodes();
				}

				break;

			case "latency":
				if( !_.isUndefined(data.id) && !_.isUndefined(data.latency) )
				{
					var index = findIndex({id: data.id});

					if( index >= 0 )
					{
						var node = $scope.nodes[index];

						if( !_.isUndefined(node) && !_.isUndefined(node.stats) && !_.isUndefined(node.stats.latency) && node.stats.latency !== data.latency )
						{
							node.stats.latency = data.latency;
							latencyFilter(node);
						}
					}
				}

				break;

			case "client-ping":
				socket.emit('client-pong', {
					serverTime: data.serverTime,
					clientTime: _.now()
				});

				break;
		}
	}

	function findIndex(search)
	{
		return _.findIndex($scope.nodes, search);
	}

	function addNewNode(data)
	{
		var index = findIndex({id: data.id});

		if( _.isUndefined(data.history) )
		{
			data.history = new Array(40);
			_.fill(data.history, -1);
		}

		if( index < 0 )
		{
			if( !_.isUndefined(data.stats) && _.isUndefined(data.stats.hashrate) )
			{
				data.stats.hashrate = 0;
			}

			data.pinned = false;

			$scope.nodes.push(data);

			return true;
		}

		data.pinned = ( !_.isUndefined($scope.nodes[index].pinned) ? $scope.nodes[index].pinned : false);

		if( !_.isUndefined($scope.nodes[index].history) )
		{
			data.history = $scope.nodes[index].history;
		}

		$scope.nodes[index] = data;

		updateActiveNodes();

		return false;
	}

	function updateActiveNodes()
	{
		updateBestBlock();

		$scope.nodesTotal = $scope.nodes.length;

		$scope.nodesActive = _.filter($scope.nodes, function (node) {
			forkFilter(node);
			return node.stats.active == true;
		}).length;
	}

	function updateBestBlock()
	{
		if( $scope.nodes.length )
		{
			var chains = {};
			var maxScore = 0;

			_($scope.nodes)
				.map(function (item)
				{
					maxScore += (item.trusted ? 50 : 1);

					if( _.isUndefined(chains[item.stats.block.number]) )
						chains[item.stats.block.number] = [];

					if( _.isUndefined(chains[item.stats.block.number][item.stats.block.fork]) )
						chains[item.stats.block.number][item.stats.block.fork] = {
							fork: item.stats.block.fork,
							count: 0,
							trusted: 0,
							score: 0
						};

					if(item.stats.block.trusted)
						chains[item.stats.block.number][item.stats.block.fork].trusted++;
					else
						chains[item.stats.block.number][item.stats.block.fork].count++;

					chains[item.stats.block.number][item.stats.block.fork].score = chains[item.stats.block.number][item.stats.block.fork].trusted * 50 + chains[item.stats.block.number][item.stats.block.fork].count;
				})
				.value();

			$scope.maxScore = maxScore;
			$scope.chains = _.reduce(chains, function (result, item, key)
			{
				result[key] = _.max(item, 'score');
				return result;
			}, {});

			var bestBlock = _.max($scope.nodes, function (node)
			{
				// if( $scope.chains[node.stats.block.number].fork === node.stats.block.fork && $scope.chains[node.stats.block.number].score / $scope.maxScore >= 0.5 )
				// {
					return parseInt(node.stats.block.number);
				// }

				// return 0;
			}).stats.block.number;

			if( bestBlock !== $scope.bestBlock )
			{
				$scope.bestBlock = bestBlock;
				$scope.bestStats = _.max($scope.nodes, function (node) {
					return parseInt(node.stats.block.number);
				}).stats;

				$scope.lastBlock = $scope.bestStats.block.arrived;
				$scope.lastDifficulty = $scope.bestStats.block.difficulty;
			}
		}
	}

	function forkFilter(node)
	{
		if( _.isUndefined(node.readable) )
			node.readable = {};

		node.readable.forkClass = 'hidden';
		node.readable.forkMessage = '';

		return true;

		if( $scope.chains[node.stats.block.number].fork === node.stats.block.fork && $scope.chains[node.stats.block.number].score / $scope.maxScore >= 0.5 )
		{
			node.readable.forkClass = 'hidden';
			node.readable.forkMessage = '';

			return true;
		}

		if( $scope.chains[node.stats.block.number].fork !== node.stats.block.fork )
		{
			node.readable.forkClass = 'text-danger';
			node.readable.forkMessage = 'Wrong chain.<br/>This chain is a fork.';

			return false;
		}

		if( $scope.chains[node.stats.block.number].score / $scope.maxScore < 0.5)
		{
			node.readable.forkClass = 'text-warning';
			node.readable.forkMessage = 'May not be main chain.<br/>Waiting for more confirmations.';

			return false;
		}
	}

	function latencyFilter(node)
	{
		if( _.isUndefined(node.readable) )
			node.readable = {};

		if( _.isUndefined(node.stats) ) {
			node.readable.latencyClass = 'text-danger';
			node.readable.latency = 'offline';
		}

		if (node.stats.active === false)
		{
			node.readable.latencyClass = 'text-danger';
			node.readable.latency = 'offline';
		}
		else
		{
			if (node.stats.latency <= 100)
				node.readable.latencyClass = 'text-success';

			if (node.stats.latency > 100 && node.stats.latency <= 1000)
				node.readable.latencyClass = 'text-warning';

			if (node.stats.latency > 1000)
				node.readable.latencyClass = 'text-danger';

			node.readable.latency = node.stats.latency + ' ms';
		}
	}
});