var web3 = require('ethereum.js');
var _ = require('lodash');
var os = require('os');
var shelljs = require('shelljs');
var debounce = require('debounce');

var Primus = require('primus'),
	Emitter = require('primus-emit'),
	Latency = require('primus-spark-latency'),
	Socket;

var ETH_VERSION,
	NET_VERSION,
	API_VERSION;
var INSTANCE_NAME = process.env.INSTANCE_NAME;

web3.setProvider(new web3.providers.HttpProvider('http://' + (process.env.RPC_HOST || 'localhost') + ':' + (process.env.RPC_PORT || '8080')));

Socket = Primus.createSocket({
	transformer: 'websockets',
	pathname: '/api',
	timeout: 10000,
	strategy: 'disconnect,online',
	plugin: {emitter: Emitter, sparkLatency: Latency}
});

if(process.env.NODE_ENV === 'production' && INSTANCE_NAME === "")
{
	INSTANCE_NAME = shelljs.exec('ec2metadata --instance-id', {silent: true}).output;
}

var socket = new Socket(process.env.WS_SERVER || 'ws://localhost:3000');
var WS_SECRET = process.env.WS_SECRET || "eth-net-stats-has-a-secret";

var PENDING_WORKS = true;
var MAX_BLOCKS_HISTORY = 36;
var UPDATE_INTERVAL = 5000;
var PING_INTERVAL = 2000;
var MINERS_LIMIT = 5;

function Node()
{
	var self = this;

	try {
		ETH_VERSION = web3.version.client;
		NET_VERSION = web3.version.network;
		API_VERSION = web3.version.api;
	}
	catch (err) {
		console.error("Couldn't get version");
	}

	this.info = {
		name: INSTANCE_NAME || (process.env.EC2_INSTANCE_ID || os.hostname()),
		node: ETH_VERSION,
		net: NET_VERSION,
		api: API_VERSION,
		os: os.platform(),
		os_v: os.release()
	};

	this.id = _.camelCase(this.info.name);

	console.info(this.info);

	this.stats = {
		active: false,
		listening: false,
		mining: false,
		peers: 0,
		pending: 0,
		gasPrice: 0,
		block: {},
		blocktimeAvg: 0,
		difficulty: [],
		txDensity: [],
		blockTimes: [],
		gasSpending: [],
		miners: [],
		uptime: 0,
		errors: []
	};
	this._lastStats = JSON.stringify(this.stats);

	this._tries = 0;
	this._down = 0;
	this._lastSent = 0;
	this._latency = 0;

	this.blocks = [];

	this._socket = null;
	this.pendingFilter = false;
	this.chainFilter = false;
	this.updateInterval = false;
	this.pingInterval = false;

	socket.on('open', function open() {
		socket.emit('hello', { id: self.id, info: self.info, secret: WS_SECRET });
		console.info('The connection has been opened.');
		console.info('Trying to login');
	})
	.on('end', function end() {
		self._socket = false;
		console.error('Socket connection closed');
	})
	.on('error', function error(err) {
		console.error("socket:", err);
	})
	.on('reconnecting', function reconnecting(opts) {
		console.warn('We are scheduling a reconnect operation', opts);
	})
	.on('node-pong', function(data) {
		var latency = (new Date()).getTime() - self._latency;
		socket.emit('latency', { id: self.id, latency: latency });
	})
	.on('data', function incoming(data) {
		console.info('Received some data', data);
	});

	socket.on('ready', function()
	{
		self._socket = true;
		self.sendUpdate(true);

		console.info('The connection has been established.');
	});

	this.init();

	return this;
}

Node.prototype.isActive = function()
{
	this._tries++;
	this.stats.errors = [];

	try {
		var peers = web3.toDecimal(web3.net.peerCount);

		if(peers !== null)
		{
			this.stats.peers = peers;
			this.stats.active = true;

			return true;
		}
	}
	catch (err) {
		this.stats.errors.push({
			code: '1',
			msg: err
		});
		console.error("peerCount:", err);
	}

	this.stats.active = false;
	this.stats.listening = false;
	this.stats.mining = false;
	this.stats.peers = 0;
	this._down++;

	return false;
}

Node.prototype.getBlock = function(number)
{
	var block = {
		number: 0,
		hash: '?',
		difficulty: 0,
		timestamp: 0,
		arrival: 0
	};

	if(typeof number === 'undefined'){
		try {
			number = web3.eth.blockNumber;

			if(number === this.stats.block.number)
				return this.stats.block;
		}
		catch (err) {
			this.stats.errors.push({
				code: '3',
				msg: err
			});
			console.error("blockNumber:", err);
		}
	}

	try {
		block = web3.eth.getBlock(number, true);
		block.arrival = (new Date()).getTime();
		block.propagation = block.arrival - (block.timestamp * 1000);

		if(block.hash != '?' && typeof block.difficulty !== 'undefined')
		{
			block.difficulty = web3.toDecimal(block.difficulty);
		}
	}
	catch (err) {
		this.stats.errors.push({
			code: '2',
			msg: err
		});
		console.error("getBlock:", err);
	}

	return block;
}

Node.prototype.getLatestBlocks = function()
{
	var bestBlock = this.stats.block.number;
	var maxIterations = MAX_BLOCKS_HISTORY;
	var minBlock = 0;

	if(this.blocks.length > 0)
	{
		maxIterations = Math.min(bestBlock - this.blocks[0].number, MAX_BLOCKS_HISTORY);
	}

	minBlock = Math.max(0, parseInt(bestBlock) - maxIterations);

	for (var i = minBlock; i < bestBlock; i++)
	{
		this.addBlockHistory(this.getBlock(i));
	};

	this.addBlockHistory(this.stats.block);

	this.stats.blockTimes = this.calculateBlockTimes();
	this.stats.blocktimeAvg = this.blockTimesAvg();
	this.stats.difficulty = this.difficultyChart();
	this.stats.txDensity = this.txDensityChart();
	this.stats.gasSpending = this.gasSpendingChart();
	this.stats.miners = this.minersChart();
}

Node.prototype.addBlockHistory = function(block)
{
	if(this.blocks.length === 0 || (block !== null && block.number !== this.blocks[0].number))
	{
		if(this.blocks.length === MAX_BLOCKS_HISTORY)
		{
			this.blocks.pop();
		}

		this.blocks.unshift(block);
	}
}

Node.prototype.calculateBlockTimes = function()
{
	var self = this;

	var blockTimes = _.map(this.blocks, function(block, key, list)
	{
		var diff = (key > 0 ? list[key - 1].timestamp : Math.floor(Date.now()/1000)) - block.timestamp;

		return diff;
	});

	blockTimes.shift();

	return blockTimes;
}

Node.prototype.blockTimesAvg = function()
{
	var sum = _.reduce(this.stats.blockTimes, function(memo, time) { return memo + time;}, 0);

	return sum/this.stats.blockTimes.length;
}

Node.prototype.difficultyChart = function()
{
	return difficulty = _.map(this.blocks, function(block)
	{
		return block.difficulty;
	});
}

Node.prototype.txDensityChart = function()
{
	return txDensity = _.map(this.blocks, function(block)
	{
		return block.transactions.length;
	});
}

Node.prototype.gasSpendingChart = function()
{
	return gasSpending = _.map(this.blocks, function(block)
	{
		return block.gasUsed;
	});
}

Node.prototype.minersChart = function()
{
	var miners = _.countBy(this.blocks, function(block)
	{
		return block.miner;
	});

	var minersArray = [];

	_.forEach(miners, function(cnt, miner) {
		minersArray.push({miner: miner, blocks: cnt});
	});

	var minersArray = _.sortBy(minersArray, 'blocks').reverse();

	return minersArray.slice(0, MINERS_LIMIT);
}

Node.prototype.uptime = function()
{
	this.stats.uptime = ((this._tries - this._down) / this._tries) * 100;
}

Node.prototype.getStats = function()
{
	if(this._socket)
		this._lastStats = JSON.stringify(this.stats);

	if(this.isActive())
	{
		this.stats.block = this.getBlock();

		// Get last MAX_BLOCKS_HISTORY blocks for calculations
		if(this.stats.block.number > 0)
			this.getLatestBlocks();

		if(PENDING_WORKS) {
			try {
				this.stats.pending = web3.eth.getBlockTransactionCount('pending');
			} catch (err) {
				PENDING_WORKS = false;
				console.error("getBlockTransactionCount('pending'):", err);
			}
		}

		this.stats.mining = web3.eth.mining;
		this.stats.gasPrice = web3.toBigNumber(web3.eth.gasPrice).toString(10);
		this.stats.listening = web3.net.listening;
	}

	this.uptime();
}

Node.prototype.changed = function()
{
	var changed = ! _.isEqual(this._lastStats, JSON.stringify(this.stats));

	if(this._tries - this._lastSent > 5)
	{
		this._lastSent = this._tries;

		return true;
	}

	return changed;
}

Node.prototype.prepareStats = function()
{
	return {
		id: this.id,
		stats: this.stats
	};
}

Node.prototype.sendUpdate = function(force)
{
	if(this.changed() || force)
		this.emit('update', this.prepareStats());
}

Node.prototype.update = function()
{
	this.getStats();

	this.sendUpdate();

	return this.stats;
};

Node.prototype.ping = function()
{
	this._latency = (new Date()).getTime();
	this.emit('node-ping', { id: this.id });
};

Node.prototype.setWatches = function()
{
	var self = this;

	this.pendingFilter = web3.eth.filter('pending');
	this.pendingFilter.watch( function(log) {
		if(PENDING_WORKS) {
			debounce(function() {
				self.update();
			}, 50);
		}
	});

	this.chainFilter = web3.eth.filter('latest');
	this.chainFilter.watch(function(log) {
		debounce(function() {
			self.update();
		}, 50);
	});

	this.updateInterval = setInterval(function(){
		self.update();
	}, UPDATE_INTERVAL);

	this.pingInterval = setInterval(function(){
		self.ping();
	}, PING_INTERVAL);
}

Node.prototype.emit = function(message, payload)
{
	if(this._socket){
		try {
			socket.emit(message, payload);
		}
		catch (err) {
			console.error("socket.emit:", err);
		}
	}
}

Node.prototype.init = function()
{
	this.update();
	this.setWatches();
}

Node.prototype.stop = function()
{
	if(this._socket)
		socket.end();


	if(this.updateInterval)
		clearInterval(this.updateInterval);

	if(this.pingInterval)
		clearInterval(this.pingInterval);

	web3.reset();
}

module.exports = Node;