import primes,hashlib

class PublicKey:
    """
    A public key with modulo *N* and encrypting exponent *e*, and a human readable *name*.
    """
    def __init__(self,N,e,name=""):
        assert isinstance(N,(int,long)), "N must be an integer"
        assert isinstance(e,(int,long)), "e must be an integer"
        assert isinstance(name,str), "name must be a str"
        self.N = N
        self.e = e
        self.name = name

    # to check equality, we exclude the name
    def __eq__(self,other): return self.N == other.N and self.e == other.e
    def __ne__(self,other): return not self==other

    # string representation
    def __repr__(self): return "N = %d\ne = %d" % (self.N,self.e)

class PrivateKey:
    """
    A private key with modulo *N*, encryption exponent *e* and decryption exponent *d*, and a human readable *name*
    """
    def __init__(self,N,e,d,name=""):
        assert isinstance(N,(int,long)), "N must be an integer"
        assert isinstance(e,(int,long)), "e must be an integer"
        assert isinstance(d,(int,long)), "d must be an integer"
        assert isinstance(name,str), "name must be a str"
        self.N = N
        self.e = e
        self.d = d
        self.name = name

    # to check equality
    def __eq__(self,other): return self.__dict__ == other.__dict__
    def __ne__(self,other): return not self==other

    # get only the public part of the key
    def pubkey(self): 
        """
        Return only the public key part
        """
        return PublicKey(self.N,self.e,self.name)

    # string representation
    def __repr__(self): return "N = %d\ne = %d\nd = %d" % (self.N,self.e,self.d)

class TxIn:
    """
    A transaction input is
        - *pubkey*: public key of the party sending funds
        - *hash_prev*: hash of previous transaction
        - *output_num*: the output number in previous transaction
    """
    def __init__(self,pubkey=None,hash_prev="0",output_num=0):
        assert isinstance(hash_prev,str), "hash_prev must be a string"
        assert isinstance(output_num,int) and output_num >= 0, "output_num must be integer >=0"
        if pubkey is None:
            self.pubkey = PublicKey(0,0)
        else:
            assert isinstance(pubkey,PublicKey), "must give public key object"
            self.pubkey = pubkey
        self.hash_prev = hash_prev
        self.output_num = output_num

    # to check equality
    def __eq__(self,other): return self.__dict__ == other.__dict__
    def __ne__(self,other): return not self==other
        
    # string representation
    def __repr__(self):
        return  ( "%r\nhash_prev = %s\noutput_num = %d" 
                % (self.pubkey, self.hash_prev, self.output_num))

class TxOut:
    """
    A transaction output is
        - *pubkey*: public key of party receiving funds
        - *amount*: how much funds to transfer
    """
    def __init__(self,pubkey,amount):
        assert isinstance(pubkey,PublicKey), "must give public key object"
        assert isinstance(amount,int) and amount >=0, "amount must be integer >=0"
        self.pubkey = pubkey
        self.amount = amount

    # define addition to make it simple to add out values!
    def __add__(self,other):
        return self.amount + other
    def __radd__(self,other):
        return other + self.amount

    # to check equality
    def __eq__(self,other): return self.__dict__ == other.__dict__
    def __ne__(self,other): return not self==other

    # string representation
    def __repr__(self): return "%s\namount = %d" % (repr(self.pubkey),self.amount)

class Transaction:
    """
    A transaction is
     - *inputs*: list of transaction inputs :py:class:`TxIn`
     - *outputs*: list of transaction outputs :py:class:`TxOut`
     - *note*: a description of the transaction :py:class:`str`
    """
    def __init__(self,inputs,outputs,note=""):
        assert isinstance(inputs[0],TxIn), "inputs must be list of TxIn objects"
        assert isinstance(outputs[0],TxOut), "outputs must be list of TxOut objects"
        assert isinstance(note,str), "note must be a string"
        self.inputs = inputs
        self.outputs = outputs
        self.note = note

    # to check equality
    def __eq__(self,other): return self.__dict__ == other.__dict__
    def __ne__(self,other): return not self==other

    def __contains__(self,txin):
        """
        checks whether input txin is already recorded in this transaction,
        i.e. if it is already spent
        """
        return txin in self.inputs

    # string representation
    def __repr__(self):
        s = "nin = %d\nnout = %d" % (len(self.inputs),len(self.outputs))
        s= s+"\n#### inputs ####\n"
        s= s+"\n".join(map(repr,self.inputs))
        s= s+"\n#### outputs ####\n"
        s= s+"\n".join(map(repr,self.outputs))
        s= s+"\n#### note ####\n"+self.note
        return s

    # get hash for this transaction
    def hash(self):
        """
        a hexadecimal hash of the transaction
        """
        return hashlib.md5(repr(self)).hexdigest()



def tx_send_funds(wallet,origin,destination,amount,note):
    """
    makes a suitable transaction to go from origin to destination
    """
    spent = 0
    inputs = []
    avail = sum(wallet.values())
    if  avail < amount:
        raise AssertionError("Insufficient funds: asked %d, but only have %d"%(amount,avail))
    for key,value in wallet.iteritems():
        if spent < amount:
           inputs.append(TxIn(origin,key[0],key[1]))
           spent = spent + value
    outputs = [ TxOut(destination,amount) ]
    if spent > amount:
        # get change
        outputs.append( TxOut(origin,spent-amount) )

    # remove all used transactions from wallet
    for txin in inputs: del wallet[(txin.hash_prev,txin.output_num)]

    return Transaction(inputs,outputs,note)

class Block:
    """
    A block of transactions is:
        - *transactions*: a list of (transactions,signatures)
        - *hash_prev*: the hash of the previous block
        - *nonce*: the nonce
        - *difficulty*: the current difficulty
    """
    def __init__(self,hash_prev,transactions,nonce,difficulty):
        assert isinstance(transactions[0][0],Transaction), "transactions must be list of [Transaction,signed_hash]"
        assert isinstance(transactions[0][1],(int,long)), "transactions must be list of [Transaction,signed_hash]"
        assert isinstance(hash_prev,str), "hash_prev must be a string"
        assert isinstance(nonce,int), "nonce must be an integer"
        assert isinstance(difficulty,int), "difficulty must be integer"
        self.transactions = transactions
        self.hash_prev = hash_prev
        self.nonce = nonce
        self.difficulty = difficulty

    # to check equality
    def __eq__(self,other): return self.__dict__ == other.__dict__
    def __ne__(self,other): return not self==other

    # string representation
    def __repr__(self):
        s = "hash_prev = %s" % self.hash_prev
        s = s + "\nnonce = %d" % self.nonce
        s = s + "\ndifficulty = %d" % self.difficulty
        s = s + "\nnum_transactions = %d" % len(self.transactions)
        for i in range(0,len(self.transactions)):
            s = s + "\n#### transaction %d ####\n" % i
            s = s + repr(self.transactions[i][0]) + "\nsig = %d" % self.transactions[i][1]
        return s

    def hash(self):
        """
        hexadecimal hash for this block
        """
        return hashlib.md5(repr(self)).hexdigest()

    def check_nonce(self):
        """
        Check nonce

        The difficulty is how many zeros the (hexadecimal) :py:meth:`hash` starts with
        """
        h=self.hash()
        return h[0:self.difficulty] == "0"*self.difficulty

def tx_sig(tx,privkey):
    """
    Sign a transaction

    Note: all the input transactions are assumed to be
    from the same public key, so we just sign using one private key
    which is assumed to be that of the only owner of all inputs
    In bitcoin transactions, there is a "script" describing which keys
    to use to verify that the transaction is legit

    """
    assert isinstance(tx,Transaction)
    assert isinstance(privkey,PrivateKey)
    # Your code
    assert False # REMOVE THIS LINE OF CODE
