#!/usr/bin/env python2.7
# pybc_cointk: A generic blockchain peer with a Tk GUI.

import argparse, sys, os, itertools, time
from twisted.internet import reactor, tksupport
import Tkinter, tkMessageBox

import pybc
import pybc.coin

def parse_args(args):
    """
    Takes in the command-line arguments list (args), and returns a nice argparse
    result with fields for all the options.
    Borrows heavily from the argparse documentation examples:
    <http://docs.python.org/library/argparse.html>
    """
    
    # The command line arguments start with the program name, which we don't
    # want to treat as an argument for argparse. So we remove it.
    args = args[1:]
    
    # Construct the parser (which is stored in parser)
    # Module docstring lives in __doc__
    # See http://python-forum.com/pythonforum/viewtopic.php?f=3&t=36847
    # And a formatter class so our examples in the docstring look good. Isn't it
    # convenient how we already wrapped it to 80 characters?
    # See http://docs.python.org/library/argparse.html#formatter-class
    parser = argparse.ArgumentParser(description=__doc__, 
        formatter_class=argparse.RawDescriptionHelpFormatter)
    
    # Now add all the options to it
    parser.add_argument("blockstore",
        help="the name of a file to store blocks in")
    parser.add_argument("keystore",
        help="the name of a file to store blocks in")
    parser.add_argument("--port", type=int, default=8008, 
        help="the port to listen on")
    parser.add_argument("--peer_host", type=str, default=None, 
        help="the hostname of another peer to connect to")
    parser.add_argument("--peer_port", type=int, default=None, 
        help="the port of another peer to connect to")
    parser.add_argument("--generate", action="store_true",
        help="generate a block every so often")

        
    return parser.parse_args(args)
    

block_in_progress = None

def generate_block(peer, wallet):
    """
    Keep on generating blocks in the background.
    
    Put the blocks in the given peer's blockchain, and send the proceeds to the
    given wallet.
    
    Don't loop indefinitely, so that the Twisted main thread dying will stop
    us.
   
    
    """
    
    global block_in_progress
    
    if block_in_progress is not None:
        # Keep working on the block we were working on
        success = block_in_progress.do_some_work(peer.blockchain.algorithm)
        
        if success:
            # We found a block!
            print "Generated block!"
            
            # Dump the block
            print block_in_progress
            for transaction_bytes in pybc.unpack_transactions(
                block_in_progress.payload):
                
                print pybc.coin.Transaction.from_bytes(transaction_bytes)
            
            peer.send_block(block_in_progress)
            # Start again
            block_in_progress = None
        elif time.time() > block_in_progress.timestamp + 60:
            # This block is too old. Try a new one.
            print "Generating block is getting old! Restart generation!"
            block_in_progress = None
        elif (peer.blockchain.highest_block is not None and 
            peer.blockchain.highest_block.block_hash() != 
            block_in_progress.previous_hash):
            
            # This block is no longer based on the top of the chain
            print "New block from elsewhere! Restart generation!"
            block_in_progress = None
    else:
        # We need to start a new block
        print "Starting a block!"
        block_in_progress = peer.blockchain.make_block(wallet.get_address())
        
        # Might as well dump balance here too
        print "Receiving address: {}".format(pybc.bytes2string(
        wallet.get_address()))
        print "Current balance: {}".format(wallet.get_balance())
    
    # Tell the main thread to make us another thread.
    reactor.callFromThread(reactor.callInThread, generate_block, peer, wallet)

class PybcCoinGui(Tkinter.Tk, object):
    """
    Represents a GUI to control a PyBC Coin client. A new-style class.
    """
    
    def __init__(self, blockchain, wallet, peer):
        """
        Make a new GUI main window that uses the given blockchain, wallet, and
        peer to send and receive transactions.
        
        """
        
        # Save our parameters
        self.wallet = wallet
        self.blcokchain = blockchain
        self.peer = peer
        
        # Call the base window constructor        
        super(PybcCoinGui, self).__init__()
        
        # Add our window to Twisted.
        tksupport.install(self)
        
        # Set the title
        self.title("PyBC Coin")
        
        # Connect the close handler
        self.protocol("WM_DELETE_WINDOW", self.handle_close)
        
        # Add a balance label to the window
        self.balance_label = Tkinter.Label(self, text="Balance: ", 
            font=("Helvetica", 16))
        self.balance_label.pack(fill=Tkinter.X, expand=1)
        
        # Add a Receive button
        self.receive_button = Tkinter.Button(self, text="Receive Coins", 
            command=self.handle_receive)
        self.receive_button.pack(fill=Tkinter.X, expand=1)
            
        # Add a Send button
        self.send_button = Tkinter.Button(self, text="Send Coins",
            command=self.handle_send)
        self.send_button.pack(fill=Tkinter.X, expand=1)
        
        # Do a first tick before we display
        self.tick()        
        
    def tick(self):
        """
        Called once a second or so. Refresh the UI by polling the wallet,
        blockchain, and peer.
        
        """
        
        # Refresh the balance label
        self.update_balance()
        
        # Call the tick again
        reactor.callLater(1, self.tick)
        
    
    def update_balance(self):
        """
        Find our balance and update the label on-screen.
        
        """
        
        # Go get the balance, and put it in the label.
        self.balance_label["text"] = "Balance: {}".format(
            self.wallet.get_balance())
        
    def handle_close(self):
        """
        Handle the user closing the window.
        
        """
        
        print "Close"
        
        reactor.stop()
        
        # Close the window
        self.destroy()
        
        
        
        
    def handle_receive(self):
        """
        Handle the user clicking the receive coins button. Pops up a dialog with
        a receiving address.
        
        """
        
        # Make a ReceiveDialog with our address
        dialog = ReceiveDialog(self, pybc.bytes2string(
            self.wallet.get_address()))
        
        # Wait on it
        self.wait_window(dialog)
        
    def handle_send(self):
        """
        Handle the user clicking the send coins button. Pops up a dialog to take
        a destination address and an amount.
        
        """
        
        # Make a SendDialog with our wallet and peer
        dialog = SendDialog(self, self.wallet, self.peer)
        
        # Wait on it
        self.wait_window(dialog)
        

class ReceiveDialog(Tkinter.Toplevel, object):
    """
    A modal dialog displaying a receiving address.
    
    """
    
    def __init__(self, parent, address):
        """
        Make a dialog as a child of the given parent window to display the given
        receiving address, which myst be a printable string.
        
        """
        
        # Call the base window constructor        
        super(ReceiveDialog, self).__init__(parent)
        
        # Remember our address
        self.address = address
        
        # Do some magic Tk dialog things
        self.transient(parent)
        self.grab_set()
        self.geometry("+%d+%d" % (parent.winfo_rootx()+50,
                                  parent.winfo_rooty()+50))
        self.focus_set()
        
        # Add our window to Twisted.
        tksupport.install(self)
        
        # Set the title
        self.title("Receive Coins")
        
        # Connect the close handler
        self.protocol("WM_DELETE_WINDOW", self.handle_close)
        
        # Add a Label with the address.
        address_label = Tkinter.Label(self, text=address, 
            font=("Helvetica", 16))
        address_label.pack(side=Tkinter.LEFT)
        
        # Add a button to copy the address to the clipboard
        address_button = Tkinter.Button(self, text="Copy to Clipboard", 
            command=self.copy)
        address_button.pack(side=Tkinter.RIGHT)
        
     
    def copy(self):
        """
        Put the receiving address we're showing on the clipboard.
        
        """
        
        # Put the address as the only thing on the clipboard
        self.clipboard_clear()
        self.clipboard_append(self.address)
        
        # Tell the user we did what they asked.
        tkMessageBox.showinfo("Copied", "Address coppied to clipboard.")
        
        # Dismiss the dialog
        self.handle_close()
        
    def handle_close(self):
        """
        Handle the user closing the window.
        
        """
        
        # Close the window
        self.destroy()
        
        
class SendDialog(Tkinter.Toplevel, object):
    """
    A modal dialog for sending coins.
    
    """
    
    def __init__(self, parent, wallet, peer):
        """
        Make a dialog as a child of the given parent window to send coins from
        the given wallet, broadcasting transactions with the given peer.
        
        """
        
        # Call the base window constructor        
        super(SendDialog, self).__init__(parent)
        
        # Save the wallet
        self.wallet = wallet
        
        # Save the peer
        self.peer = peer
        
        # Do some magic Tk dialog things
        self.transient(parent)
        self.grab_set()
        self.geometry("+%d+%d" % (parent.winfo_rootx()+50,
                                  parent.winfo_rooty()+50))
        self.focus_set()
        
        # Add our window to Twisted.
        tksupport.install(self)
        
        # Set the title
        self.title("Send Coins")
        
        # Connect the close handler
        self.protocol("WM_DELETE_WINDOW", self.handle_close)
        
        # Add a Label asking for the address
        address_label = Tkinter.Label(self, text="Destination:")
        address_label.pack()
        
        # Add an Entry for the address.
        self.address_entry = Tkinter.Entry(self, font=("Helvetica", 16), 
            width=44)
        self.address_entry.pack()
        
        # Add a Label asking for the amount
        amount_label = Tkinter.Label(self, text="Amount:")
        amount_label.pack()
        
        # Add an Entry for the amount
        self.amount_entry = Tkinter.Entry(self, font=("Helvetica", 16), width=5)
        self.amount_entry.pack()
        
        # Add a fee label
        fee_label = Tkinter.Label(self, 
            text="A fee of 1 is automatically added to all transactions.")
        fee_label.pack()
        
        # Add a button to send the transaction
        send_button = Tkinter.Button(self, text="Send", command=self.send)
        send_button.pack()
        
     
    def send(self):
        """
        Send the transaction entered, and close the window.
        
        """
        
        try:
            # Where should the coins go, as an address bytestring?
            destination = pybc.string2bytes(self.address_entry.get())
        except:
            tkMessageBox.showerror("Error", "Invalid address: {}".format(
                self.address_entry.get()))
            return
        
        try:
            # Parse out how much we should send, as an int
            amount = int(self.amount_entry.get())
        except:
            # Complain we couldn't parse their int.
            tkMessageBox.showerror("Error", "Invalid amount: {}".format(
                self.amount_entry.get()))
            return
                
        if amount <= 0:
            # We can't send silly amounts
            tkMessageBox.showerror("Error", 
                "Must send a positive number of coins.")
            return
        
        # How much fee should we pay? TODO: make this dynamic or configurable
        fee = 1
        
        # How much do we need to send this transaction with its fee?
        total_input = amount + fee
        
        if total_input > self.wallet.get_balance():
            # We don't have enough to pay the transaction and the fee.
            tkMessageBox.showerror("Error",
                "Insufficient funds: {} needed".format(total_input))
            return
            
        # If we get here this is an actually sane transaction.
        # Make the transaction
        transaction = self.wallet.make_simple_transaction(amount, destination,
            fee=fee)
            
        if tkMessageBox.askyesno("Are You Sure?", 
            "Send {} to {} paying fee of {}?".format(amount, 
            pybc.bytes2string(destination), fee)):
            
            # The user wants to make the transaction. Send it.
            self.peer.send_transaction(transaction.to_bytes())
            
            tkMessageBox.showinfo("Sent", "Transaction sent.")
            
            # Close the window since we're done.
            self.handle_close()
        
    def handle_close(self):
        """
        Handle the user closing the window.
        
        """
        
        # Close the window
        self.destroy()
                
def main(args):
    """
    Parses command line arguments, and runs a blockchain peer.
    "args" specifies the program arguments, with args[0] being the executable
    name. The return value should be used as the program's exit code.
    
    """
    
    options = parse_args(args) # This holds the nicely-parsed options object
    
    print "Starting server on port {}".format(options.port)
    
    # Make a CoinBlockchain, using the specified blockchain file
    blockchain = pybc.coin.CoinBlockchain(options.blockstore)
    
    # Make a Wallet that uses the blockchain and our keystore
    wallet = pybc.coin.Wallet(blockchain, options.keystore)
    
    print "Receiving address: {}".format(pybc.bytes2string(
        wallet.get_address()))
    print "Current balance: {}".format(wallet.get_balance())
    
    # Now make a Peer.
    peer = pybc.Peer("PyBC-Coin", 1, blockchain, port=options.port)
    
    if options.peer_host is not None and options.peer_port is not None:
        # Point it at a user specified other peer.
        peer.connect(options.peer_host, options.peer_port)
        # Tell it it heard about the peer now
        peer.peer_seen(options.peer_host, options.peer_port, int(time.time()))
    
    if options.generate:
        # Schedule a block generation
        reactor.callFromThread(generate_block, peer, wallet)

    
    # Make the GUI
    gui = PybcCoinGui(blockchain, wallet, peer)
    
    # Run the reactor (and the Tk main loop)
    peer.run()
        
    return 0

if __name__ == "__main__" :
    sys.exit(main(sys.argv))

