"""
Universal Node Locator (UNL.) Allows nodes to direct connect and helps to debug issues in doing so.
"""

from .lib import *
import time
import struct
import random
import binascii
import base64
import binascii
from threading import Thread, Lock

def is_valid_unl(value):
    unl = UNL(value=value)
    ret = unl.deconstruct()
    if ret == None:
        return 0
    else:
        return 1

class UNL():
    def __init__(self, net=None, dht_node=None, value=None):
        self.version = 1
        self.net = net
        self.nat_type_lookup = {
            "m": "random",
            "g": "preserving",
            "e": "reuse"
        }

        self.forwarding_type_lookup = {
            "f": "forwarded",
            "m": "manual",
            "U": "UPnP",
            "N": "NATPMP"
        }

        self.node_type_lookup = {
            "p": "passive",
            "a": "active",
            "s": "simultaneous"
        }

        self.dht_node = dht_node
        if value != None:
            self.value = value
        else:
            self.value = self.construct()

        # Table of in progress UNLs.
        self.pending_unls = []

        # Sim opens are queued to occur sequentially.
        self.pending_sim_open = []

        # Simple mutex.
        self.mutex = Lock()

    def __eq__(self, other):
        # Compare UNLs. Used to check if a UNL is us.
        our_unl = self.deconstruct(self.value)
        their_unl = self.deconstruct(other.value)

        # Different WAN IPs.
        if our_unl["wan_ip"] != their_unl["wan_ip"]:
            return False

        # Different LAN IPs.
        if our_unl["lan_ip"] != their_unl["lan_ip"]:
            return False

        # Different passive ports.
        if our_unl["listen_port"] != their_unl["listen_port"]:
            return False

        # They are the same.
        return True

    # Operator !=
    def __ne__(self, other):
        return not self == other

    def connect_handler(self, their_unl, events, force_master):
        # Figure out who should make the connection.
        our_unl = self.value.encode("ascii")
        their_unl = their_unl.encode("ascii")
        int_our_unl = int(binascii.hexlify(our_unl), 16)
        int_their_unl = int(binascii.hexlify(their_unl), 16)
        if int_our_unl > int_their_unl:
            master = 1
        else:
            master = 0

        """
        Master defines who connects if either side can. It's used to eliminate having multiple connections with the same host.
        """
        if force_master:
            master = 1

        # Deconstruct binary UNLs into dicts.
        our_unl = self.deconstruct(our_unl)
        their_unl = self.deconstruct(their_unl)

        if our_unl == None:
            raise Exception("Unable to deconstruct our UNL.")
        if their_unl == None:
            raise Exception("Unable to deconstruct their UNL.")

        # This means the nodes are behind the same router.
        if our_unl["wan_ip"] == their_unl["wan_ip"]:
            # Connect to LAN IP.
            our_unl["wan_ip"] = our_unl["lan_ip"]
            their_unl["wan_ip"] = their_unl["lan_ip"]

            # Already behind NAT so no forwarding needed.
            our_unl["node_type"] = "passive"
            their_unl["node_type"] = "passive"

        # Aquire mutex.
        self.mutex.acquire()

        # Wait for other UNLs to finish.
        while their_unl in self.pending_unls:
            # This is an undifferentiated duplicate.
            if events == None:
                return

            time.sleep(1)

        # Wait for any other hole punches to finish.
        if their_unl["node_type"] == "simultaneous" and our_unl["node_type"] != "passive":
            self.pending_sim_open.append(their_unl["value"])
            print("Waiting for queued sim open")
            while len(self.pending_sim_open):
                if self.pending_sim_open[0] == their_unl["value"]:
                    break

                time.sleep(1)
            print("Ok, it's our turn now.")

        # Set pending UNL.
        self.pending_unls.append(their_unl)

        # Release mutex.
        self.mutex.release()

        # Are we asking them to connect to us?
        reverse_query = 0

        # Are they already connected?
        con = self.net.con_by_ip(their_unl["wan_ip"])
        if con == None:
            # Valid node types.
            for node_type in ["passive", "simultaneous"]:
                # Matches for this node type.
                nodes = []
                if our_unl["node_type"] == node_type:
                    nodes.append(our_unl)

                if their_unl["node_type"] == node_type:
                    nodes.append(their_unl)

                # Try the next node type.
                if len(nodes):
                    # We only want one connection.
                    if len(nodes) == 2:
                        if not master:
                            # They will connect to us.
                            nodes.remove(their_unl)
                        else:
                            # We will connect to them.
                            nodes.remove(our_unl)

                    # Don't connect to ourself.
                    node = nodes[0]
                    if node == their_unl:
                        con = self.net.add_node(their_unl["wan_ip"], their_unl["listen_port"], their_unl["node_type"], timeout=60)
                        if con != None:
                            break
                    else:
                        # Tell them to connect to us.
                        if self.dht_node != None and force_master:
                            con_request = "REVERSE_CONNECT:%s" % (self.value)
                            node_id = their_unl["node_id"]
                            if int(binascii.hexlify(node_id), 16):
                                reverse_query = 1
                                self.dht_node.send_direct_message(node_id, con_request)

                        # They will connect to us.
                        found_con = 0
                        for i in range(0, 60):
                            con = self.net.con_by_ip(their_unl["wan_ip"])
                            if con != None:
                                found_con = 1
                                break

                            time.sleep(1)

                        if found_con:
                            break

        # Undo pending connect state.
        self.pending_unls.remove(their_unl)

        # Undo pending sim open.
        if len(self.pending_sim_open):
            if self.pending_sim_open[0] == their_unl["value"]:
                self.pending_sim_open = self.pending_sim_open[1:]

        # Only execute events if this function was called manually.
        if events != None:
            # Success.
            if con != None:
                # Confirm we sent the reverse connect request.
                if reverse_query:
                    msg = con.recv_line(timeout=2)
                    if "REVERSE_QUERY:" in msg:
                        reverse_accept = "REVERSE_ORIGIN:" + self.value
                        con.send_line(reverse_accept)
                    else:
                        con.close()
                        del events["success"]
                        con = None

                if "success" in events:
                    events["success"](con)

            # Failure.
            if con == None:
                if "failure" in events:
                    events["failure"](con)

    def connect(self, their_unl, events, force_master=1):
        """
        A new thread is spawned because many of the connection techniques rely on sleep to determine connection outcome or to synchronise hole punching techniques. If the sleep is in its own thread it won't block main execution.
        """
        parms = (their_unl, events, force_master)
        t = Thread(target=self.connect_handler, args=parms)
        t.start()

    def deconstruct(self, unl=None):
        if unl == None:
            unl = self.value

        try:
            if sys.version_info >= (3,0,0):
                # for Python 3
                if isinstance(unl, bytes):
                    unl = unl.decode('ascii')  # or  s = str(s)[2:-1]
            else:
                # for Python 2
                if isinstance(unl, str):
                    unl = unicode(unl)

            # Separate checksum.
            value = unl
            unl = base64.b64decode(unl)
            checksum_size = 4
            checksum = unl[-checksum_size:]
            unl = unl[:-checksum_size]

            # Check checksum.
            expected_checksum = hashlib.sha256(hashlib.sha256(unl).digest()).digest()
            expected_checksum = expected_checksum[0:4]
            if checksum != expected_checksum:
                raise Exception("Invalid checksum -- UNL is probably corrupt.")

            # Separate the other fields.
            version, node_id, node_type, nat_type, forwarding_type, passive_port, wan_ip, lan_ip, timestamp, nonce = struct.unpack("<B20sBBBHIIQI", unl)
            node_type = chr(node_type)
            node_type = self.node_type_lookup[node_type]
            nat_type = chr(nat_type)
            nat_type = self.nat_type_lookup[nat_type]
            forwarding_type = chr(forwarding_type)
            forwarding_type = self.forwarding_type_lookup[forwarding_type]
            wan_ip = int2ip(wan_ip)
            lan_ip = int2ip(lan_ip)

            # Return meaningful fields.
            ret = {
                "value": value,
                "version": version,
                "node_id": node_id,
                "node_type": node_type,
                "nat_type": nat_type,
                "forwarding_type": forwarding_type,
                "listen_port": passive_port,
                "wan_ip": wan_ip,
                "lan_ip": lan_ip,
                "timestamp": timestamp,
                "nonce": nonce
            }

            return ret
        except Exception as e:
            print(e)
            return None

    def construct(self, details={}):
        # Sanity check.
        if self.net == None:
            raise Exception("Missing Net object for UNL.construct")

        # Translate bind address.
        wan_ip = get_wan_ip()
        if "wan_ip" in details:
            wan_ip = details["wan_ip"]

        # Lan IP.
        unspecific_bind = ["0.0.0.0", "127.0.0.1", "localhost"]
        if self.net.passive_bind in unspecific_bind:
            lan_ip = get_lan_ip(self.net.interface)
        else:
            lan_ip = self.net.passive_bind
        if "lan_ip" in details:
            lan_ip = details["lan_ip"]

        # Node id.
        if self.dht_node == None:
            node_id = b"\0"
        else:
            node_id = self.dht_node.get_id()
        if "node_id" in details:
            node_id = details["node_id"]

        # Version.
        version = self.version
        if "version" in details:
            version = details["version"]

        # Node type.
        node_type = self.net.node_type
        if "node_type" in details:
            node_type = details["node_type"]

        # Nat type.
        nat_type = self.net.nat_type
        if "nat_type" in details:
            nat_type = details["nat_type"]

        # Forwarding type.
        forwarding_type = self.net.forwarding_type
        if "forwarding_type" in details:
            forwarding_type = details["forwarding_type"]

        # Listen port.
        listen_port = self.net.passive_port
        if "listen_port" in details:
            listen_port = details["listen_port"]

        # Timestamp.
        timestamp = time.time()
        if "timestamp" in details:
            timestamp = details["timestamp"]

        # Nonce.
        nonce = random.randrange(0, 2 ** (4 * 8))
        if "nonce" in details:
            nonce = details["nonce"]

        # Generate UNL.
        unl = struct.pack("<B20sBBBHIIQI", \
            version,
            node_id,
            ord(node_type[0]),
            ord(nat_type[-1]),
            ord(forwarding_type[0]),
            listen_port,
            ip2int(wan_ip),
            ip2int(lan_ip),
            int(timestamp),
            nonce
        )

        # Build checksum and make base64.
        checksum = hashlib.sha256(hashlib.sha256(unl).digest()).digest()
        checksum = checksum[0:4]
        unl = unl + checksum
        unl = base64.b64encode(unl)
        unl = unl.decode("utf-8")
        
        return unl

if __name__ == "__main__":
    # global direct_net
    # unl = UNL(direct_net)
    # print(unl.construct())

    # print(unl.deconstruct('x'))

    # print(unl.deconstruct(unl.construct()))
    pass


