import copy
import utils
from block import Block
from blockchain import Blockchain
from vote import Vote

class Node:
	''' This class represents a protocol node.
		Each node is numbered and has a secret-public keys pair, to sign messages.
		Nodes hold a set of Blockchains(some of which are not notarized)
		and a set of unconfirmed pending transactions.
		All nodes have syncronized clocks, using GST approach. '''

	def __init__(self, id, clock, password, init_block):
		self.id = id
		self.clock = clock	# Clock syncronization to be implemented.
		self.password = password
		self.private_key, self.public_key = utils.generate_keys(self.password)
		self.canonical_blockchain = Blockchain(init_block)
		self.node_blockchains = []
		self.unconfirmed_transactions = []

	def __repr__(self):
		return "Node=[id={0}]".format(self.id)

	def output(self):
		''' A nodes output is the finalized (canonical) blockchain they hold. '''
	
		return self.canonical_blockchain

	def receive_transaction(self, transaction):
		''' Node retreives a transaction and append it to the unconfirmed transactions list.
			Additional validity rules must be defined by the protocol for its blockchain data structure. '''

		self.unconfirmed_transactions.append(transaction)

	def broadcast_transaction(self, nodes, transaction):
		''' Node broadcast a transaction to provided nodes list. '''
		
		for node in nodes:
			node.receive_transaction(transaction)

	def find_longest_notarized_chain(self):
		''' Finds the longest fully notarized blockchain the node holds.'''
	
		longest_notarized_chain = self.canonical_blockchain
		length = 0
		for blockchain in self.node_blockchains:
			if blockchain.is_notarized() and len(blockchain.blocks) > length:
				longest_notarized_chain = blockchain
				length = len(blockchain.blocks)
		return longest_notarized_chain
		
	def get_unproposed_transactions(self):
		''' Node retrieves all unconfiremd transactions not proposed in previous blocks. '''
		unproposed_transactions = self.unconfirmed_transactions
		for blockchain in self.node_blockchains:
			for block in blockchain.blocks:
				for transaction in block.txs:
						if transaction in unproposed_transactions:
							unproposed_transactions.remove(transaction)
		return unproposed_transactions

	def propose_block(self, epoch, nodes):
		''' Node generates a block for that epoch, containing all uncorfirmed transactions.
			Block extends the longest notarized blockchain the node holds.
			Node signs the block, and broadcasts it to rest nodes. '''
	
		longest_notarized_chain = self.find_longest_notarized_chain()
		unproposed_transactions = self.get_unproposed_transactions()
		proposed_block = copy.deepcopy(Block(
			hash(longest_notarized_chain.blocks[-1]), epoch, unproposed_transactions))
		signed_proposed_block = copy.deepcopy(
			utils.sign_message(
				self.password,
				self.private_key,
				proposed_block))
		for node in nodes:
			node.receive_proposed_block(self.public_key, copy.deepcopy(
				proposed_block), copy.deepcopy(signed_proposed_block), nodes)

	def find_extended_blockchain(self, block):
		''' For a provided block, node searches for any blockchain that it extends.
			If a fork blockchain is not found, block is tested against the canonical blockchain. '''
	
		for blockchain in self.node_blockchains:
			if block.h == hash(
					blockchain.blocks[-1]) and block.e > blockchain.blocks[-1].e:
				return blockchain
		if block.h == hash(
				self.canonical_blockchain.blocks[-1]) and block.e > self.canonical_blockchain.blocks[-1].e:
			return self.canonical_blockchain
		return None

	def find_block(self, vote_block):
		''' Node searches it the blockchains it holds for provided block. '''
	
		for blockchain in self.node_blockchains:
			for block in reversed(blockchain.blocks):
				if vote_block == block:
					return block
		for block in reversed(self.canonical_blockchain.blocks):
			if vote_block == block:
				return block
		return None

	def extends_notarized_blockchain(self, blockchain):
		''' Node verifies if provided blockchain is notarized excluding the last block. '''
		
		for block in blockchain.blocks[:-1]:
			if not block.notarized:
				return False
		return True

	def vote_block(self, block, nodes):
		''' Given a block, node finds which blockchain it extends.
			If block extends the canonical blockchain, a new fork blockchain is created.
			Node votes on the block, only if it extends the longest notarized chain it has seen. '''
	
		blockchain = self.find_extended_blockchain(block)
		if not blockchain or blockchain is self.canonical_blockchain:
			blockchain = Blockchain(copy.deepcopy(block))
			self.node_blockchains.append(blockchain)
		else:
			blockchain.add_block(copy.deepcopy(block))

		if self.extends_notarized_blockchain(blockchain):
			signed_block = utils.sign_message(
				self.password, self.private_key, block)
			vote = Vote(signed_block, block, self.id)
			for node in nodes:
				node.receive_vote(self.public_key, vote, nodes)

	def receive_proposed_block(
			self,
			leader_public_key,
			round_block,
			signed_round_block,
			nodes):
		''' Node receives the proposed block, verifies its sender(epoch leader), and proceeds with voting on it. '''
		
		assert(
			utils.verify_signature(
				leader_public_key,
				round_block,
				signed_round_block))
		self.vote_block(round_block, nodes)

	def check_blockchain_finalization(self, block):
		''' For the provided block, node checks if the blockchain it extends can be finalized.
			Consensus finalization logic: If node has observed the notarization of 3 consecutive
			blocks in a fork chain, it finalizes (appends to canonical blockchain) all blocks up to the middle block.
			When a block gets finalized, the transactions it contains are removed from nodes unconfirmed transactions list.
			When fork chain blocks are finalized, rest fork chains not starting by those blocks are removed. '''
		
		if block in self.canonical_blockchain.blocks:
			blockchain = self.canonical_blockchain
		else:
			for node_blockchain in self.node_blockchains:
				if block in node_blockchain.blocks:
					blockchain = node_blockchain
		if blockchain and len(blockchain) > 2:
			if blockchain.blocks[-3].notarized and blockchain.blocks[-2].notarized:
				for block in blockchain.blocks[:-1]:
					block.finalized = True
					self.canonical_blockchain.blocks.append(block)
					for transaction in block.txs:
						if transaction in self.unconfirmed_transactions:
							self.unconfirmed_transactions.remove(transaction)
				for node_blockchain in self.node_blockchains:
					if node_blockchain.blocks[-len(blockchain.blocks[:-1]):] != blockchain.blocks[:-1]:
						self.node_blockchains.remove(node_blockchain)
					else:
						del node_blockchain[-len(blockchain.blocks[:-1]):]

	def receive_vote(self, node_public_key, vote, nodes):
		''' Node receives a vote for a block.
			First, sender is verified using their public key.
			Block is searched in nodes blockchains.
			If the vote wasn't received before, it is appended to block votes list.
			When a node sees 2n/3 votes for a block it notarizes it.			
			Finally, we check if the notarization of the block can finalize parent blocks
			in its blockchain. '''
	
		assert(utils.verify_signature(node_public_key, vote.block, vote.vote))
		vote_block = self.find_block(vote.block)
		if not vote_block:
			self.vote_block(copy.deepcopy(vote.block), nodes)
			return
		if vote not in vote_block.votes:
			vote_block.votes.append(vote)
		if not vote_block.notarized and len(vote_block.votes) > (2 * len(nodes) / 3):
			vote_block.notarized = True
			self.check_blockchain_finalization(vote_block)
