#
# Adapted from: # https://github.com/ethereum/pyethereum/blob/master/ethereum/transactions.py
#
from .. import rlp
from ..rlp.sedes import big_endian_int,binary
from .utils import (
		str_to_bytes,encode_hex,ascii_chr,big_endian_to_int,
		TT256,mk_contract_address,
		ecsign,ecrecover_to_pub,normalize_key )
from . import utils

class InvalidTransaction(Exception): pass
class opcodes(object):
	GTXCOST = 21000       # TX BASE GAS COST
	GTXDATAZERO = 4       # TX DATA ZERO BYTE GAS COST
	GTXDATANONZERO = 68   # TX DATA NON ZERO BYTE GAS COST

# in the yellow paper it is specified that s should be smaller than
# secpk1n (eq.205)
secpk1n = 115792089237316195423570985008687907852837564279074904382605163141518161494337
null_address = b'\xff' * 20

class Transaction(rlp.Serializable):

	"""
	A transaction is stored as:
	[nonce, gasprice, startgas, to, value, data, v, r, s]

	nonce is the number of transactions already sent by that account, encoded
	in binary form (eg.  0 -> '', 7 -> '\x07', 1000 -> '\x03\xd8').

	(v,r,s) is the raw Electrum-style signature of the transaction without the
	signature made with the private key corresponding to the sending account,
	with 0 <= v <= 3. From an Electrum-style signature (65 bytes) it is
	possible to extract the public key, and thereby the address, directly.

	A valid transaction is one where:
	(i) the signature is well-formed (ie. 0 <= v <= 3, 0 <= r < P, 0 <= s < N,
		0 <= r < P - N if v >= 2), and
	(ii) the sending account has enough funds to pay the fee and the value.
	"""

	fields = [
		('nonce', big_endian_int),
		('gasprice', big_endian_int),
		('startgas', big_endian_int),
		('to', utils.address),
		('value', big_endian_int),
		('data', binary),
		('v', big_endian_int),
		('r', big_endian_int),
		('s', big_endian_int),
	]

	_sender = None

	def __init__(self, nonce, gasprice, startgas, to, value, data, v=0, r=0, s=0):
		# self.data = None

		to = utils.normalize_address(to, allow_blank=True)

		super(
			Transaction,
			self).__init__(
			nonce,
			gasprice,
			startgas,
			to,
			value,
			data,
			v,
			r,
			s)

		if self.gasprice >= TT256 or self.startgas >= TT256 or \
				self.value >= TT256 or self.nonce >= TT256:
			raise InvalidTransaction("Values way too high!")

	@property
	def sender(self):
		if not self._sender:
			# Determine sender
			if self.r == 0 and self.s == 0:
				self._sender = null_address
			else:
				if self.v in (27, 28):
					vee = self.v
					sighash = utils.sha3(rlp.encode(unsigned_tx_from_tx(self), UnsignedTransaction))

				elif self.v >= 37:
					vee = self.v - self.network_id * 2 - 8
					assert vee in (27, 28)
					rlpdata = rlp.encode(rlp.infer_sedes(self).serialize(self)[:-3] + [self.network_id, '', ''])
					sighash = utils.sha3(rlpdata)
				else:
					raise InvalidTransaction("Invalid V value")
				if self.r >= secpk1n or self.s >= secpk1n or self.r == 0 or self.s == 0:
					raise InvalidTransaction("Invalid signature values!")
				pub = ecrecover_to_pub(sighash, vee, self.r, self.s)
				if pub == b'\x00' * 64:
					raise InvalidTransaction(
						"Invalid signature (zero privkey cannot sign)")
				self._sender = utils.sha3(pub)[-20:]
		return self._sender

	@property
	def network_id(self):
		if self.r == 0 and self.s == 0:
			return self.v
		elif self.v in (27, 28):
			return None
		else:
			return ((self.v - 1) // 2) - 17

	@sender.setter
	def sender(self, value):
		self._sender = value

	def sign(self, key, network_id=None):
		"""Sign this transaction with a private key.

		A potentially already existing signature would be overridden.
		"""
		if network_id is None:
			rawhash = utils.sha3(rlp.encode(unsigned_tx_from_tx(self), UnsignedTransaction))
		else:
			assert 1 <= network_id < 2**63 - 18
			rlpdata = rlp.encode(rlp.infer_sedes(self).serialize(self)[:-3] + [network_id, b'', b''])
			rawhash = utils.sha3(rlpdata)

		key = normalize_key(key)

		v, r, s = ecsign(rawhash, key)
		if network_id is not None:
			v += 8 + network_id * 2

		ret = self.copy(
			v=v, r=r, s=s
		)
		ret._sender = utils.privtoaddr(key)
		return ret

	@property
	def hash(self):
		return utils.sha3(rlp.encode(self))

	def to_dict(self):
		d = {}
		for name, _ in self.__class__._meta.fields:
			d[name] = getattr(self, name)
			if name in ('to', 'data'):
				d[name] = '0x' + encode_hex(d[name])
		d['sender'] = '0x' + encode_hex(self.sender)
		d['hash'] = '0x' + encode_hex(self.hash)
		return d

	@property
	def intrinsic_gas_used(self):
		num_zero_bytes = str_to_bytes(self.data).count(ascii_chr(0))
		num_non_zero_bytes = len(self.data) - num_zero_bytes
		return (opcodes.GTXCOST
				#         + (0 if self.to else opcodes.CREATE[3])
				+ opcodes.GTXDATAZERO * num_zero_bytes
				+ opcodes.GTXDATANONZERO * num_non_zero_bytes)

	@property
	def creates(self):
		"returns the address of a contract created by this tx"
		if self.to in (b'', '\0' * 20):
			return mk_contract_address(self.sender, self.nonce)

	def __eq__(self, other):
		return isinstance(other, self.__class__) and self.hash == other.hash

	def __lt__(self, other):
		return isinstance(other, self.__class__) and self.hash < other.hash

	def __hash__(self):
		return utils.big_endian_to_int(self.hash)

	def __ne__(self, other):
		return not self.__eq__(other)

	def __repr__(self):
		return '<Transaction(%s)>' % encode_hex(self.hash)[:4]

	def __structlog__(self):
		return encode_hex(self.hash)

	# This method should be called for block numbers >= HOMESTEAD_FORK_BLKNUM only.
	# The >= operator is replaced by > because the integer division N/2 always produces the value
	# which is by 0.5 less than the real N/2
	def check_low_s_metropolis(self):
		if self.s > secpk1n // 2:
			raise InvalidTransaction("Invalid signature S value!")

	def check_low_s_homestead(self):
		if self.s > secpk1n // 2 or self.s == 0:
			raise InvalidTransaction("Invalid signature S value!")


class UnsignedTransaction(rlp.Serializable):
	fields = [
		(field, sedes) for field, sedes in Transaction._meta.fields
		if field not in "vrs"
	]

def unsigned_tx_from_tx(tx):
	return UnsignedTransaction(
		nonce=tx.nonce,
		gasprice=tx.gasprice,
		startgas=tx.startgas,
		to=tx.to,
		value=tx.value,
		data=tx.data,
	)
