#!/usr/bin/env python
#
# This is a basic single-node example of how to use the Entangled
# DHT. It creates a Node and (optionally) joins an existing DHT.  It
# then does a Kademlia store and find, and then it deletes the stored
# value (non-Kademlia method).
#
# No tuple space functionality is demonstrated by this script.
#
# To test it properly, start a multi-node Kademlia DHT with the "create_network.py"
# script and point this node to that, e.g.:
# $python create_network.py 10 127.0.0.1
#
# $python basic_example.py 5000 127.0.0.1 4000
#
# This library is free software, distributed under the terms of
# the GNU Lesser General Public License Version 3, or any later version.
# See the COPYING file included in this archive
#

# Thanks to Paul Cannon for IP-address resolution functions (taken from aspn.activestate.com)


import binascii
import random
import twisted.internet.reactor
from lbrynet.dht.node import Node
from lbrynet.core.cryptoutils import get_lbry_hash_obj

# The Entangled DHT node; instantiated in the main() method
node = None

# The key to use for this example when storing/retrieving data
h = get_lbry_hash_obj()
h.update("key")
KEY = h.digest()
# The value to store
VALUE = random.randint(10000, 20000)

lbryid = KEY


def storeValue(key, value):
    """ Stores the specified value in the DHT using the specified key """
    global node
    print '\nStoring value; Key: %s, Value: %s' % (key, value)
    # Store the value in the DHT. This method returns a Twisted
    # Deferred result, which we then add callbacks to
    deferredResult = node.announceHaveHash(key, value)
    # Add our callback; this method is called when the operation completes...
    deferredResult.addCallback(storeValueCallback)
    # ...and for error handling, add an "error callback" as well.
    #
    # For this example script, I use a generic error handler; usually
    # you would need something more specific
    deferredResult.addErrback(genericErrorCallback)


def storeValueCallback(*args, **kwargs):
    """ Callback function that is invoked when the storeValue() operation succeeds """
    print 'Value has been stored in the DHT'
    # Now that the value has been stored, schedule that the value is read again after 2.5 seconds
    print 'Scheduling retrieval in 2.5 seconds'
    twisted.internet.reactor.callLater(2.5, getValue)


def genericErrorCallback(failure):
    """ Callback function that is invoked if an error occurs during any of the DHT operations """
    print 'An error has occurred:', failure.getErrorMessage()
    twisted.internet.reactor.callLater(0, stop)


def getValue():
    """ Retrieves the value of the specified key (KEY) from the DHT """
    global node, KEY
    # Get the value for the specified key (immediately returns a Twisted deferred result)
    print ('\nRetrieving value from DHT for key "%s"' %
           binascii.unhexlify("f7d9dc4de674eaa2c5a022eb95bc0d33ec2e75c6"))
    deferredResult = node.iterativeFindValue(
        binascii.unhexlify("f7d9dc4de674eaa2c5a022eb95bc0d33ec2e75c6"))
    # Add a callback to this result; this will be called as soon as the operation has completed
    deferredResult.addCallback(getValueCallback)
    # As before, add the generic error callback
    deferredResult.addErrback(genericErrorCallback)


def getValueCallback(result):
    """ Callback function that is invoked when the getValue() operation succeeds """
    # Check if the key was found (result is a dict of format {key:
    # value}) or not (in which case a list of "closest" Kademlia
    # contacts would be returned instead")
    print "Got the value"
    print result

    # Either way, schedule a "delete" operation for the key
    print 'Scheduling shutdown in 2.5 seconds'
    twisted.internet.reactor.callLater(2.5, stop)


def stop():
    """ Stops the Twisted reactor, and thus the script """
    print '\nStopping Kademlia node and terminating script'
    twisted.internet.reactor.stop()


if __name__ == '__main__':
    import sys

    if len(sys.argv) < 2:
        print 'Usage:\n%s UDP_PORT [KNOWN_NODE_IP  KNOWN_NODE_PORT]' % sys.argv[0]
        print 'or:\n%s UDP_PORT [FILE_WITH_KNOWN_NODES]' % sys.argv[0]
        print
        print 'If a file is specified, it should containg one IP address and UDP port'
        print 'per line, seperated by a space.'
        sys.exit(1)
    try:
        int(sys.argv[1])
    except ValueError:
        print '\nUDP_PORT must be an integer value.\n'
        print 'Usage:\n%s UDP_PORT  [KNOWN_NODE_IP  KNOWN_NODE_PORT]' % sys.argv[0]
        print 'or:\n%s UDP_PORT  [FILE_WITH_KNOWN_NODES]' % sys.argv[0]
        print
        print 'If a file is specified, it should contain one IP address and UDP port'
        print 'per line, seperated by a space.'
        sys.exit(1)

    if len(sys.argv) == 4:
        knownNodes = [(sys.argv[2], int(sys.argv[3]))]
    elif len(sys.argv) == 3:
        knownNodes = []
        f = open(sys.argv[2], 'r')
        lines = f.readlines()
        f.close()
        for line in lines:
            ipAddress, udpPort = line.split()
            knownNodes.append((ipAddress, int(udpPort)))
    else:
        knownNodes = None
        print '\nNOTE: You have not specified any remote DHT node(s) to connect to'
        print 'It will thus not be aware of any existing DHT, but will still function as'
        print ' a self-contained DHT (until another node contacts it).'
        print 'Run this script without any arguments for info.\n'

    # Set up SQLite-based data store (you could use an in-memory store instead, for example)
    #
    # Create the Entangled node. It extends the functionality of a
    # basic Kademlia node (but is fully backwards-compatible with a
    # Kademlia-only network)
    #
    # If you wish to have a pure Kademlia network, use the
    # entangled.kademlia.node.Node class instead
    print 'Creating Node'
    node = Node(udpPort=int(sys.argv[1]), node_id=lbryid)

    # Schedule the node to join the Kademlia/Entangled DHT
    node.joinNetwork(knownNodes)
    # Schedule the "storeValue() call to be invoked after 2.5 seconds,
    # using KEY and VALUE as arguments
    twisted.internet.reactor.callLater(2.5, getValue)
    # Start the Twisted reactor - this fires up all networking, and
    # allows the scheduled join operation to take place
    print 'Twisted reactor started (script will commence in 2.5 seconds)'
    twisted.internet.reactor.run()