import socket
import struct

import pynng


class SockAddr:
    """
    Python wrapper for struct nng_sockaddr.

    """

    type_to_str = {
        pynng.lib.NNG_AF_UNSPEC: "Unspecified",
        pynng.lib.NNG_AF_INPROC: "inproc",
        pynng.lib.NNG_AF_IPC: "ipc",
        pynng.lib.NNG_AF_INET: "inet",
        pynng.lib.NNG_AF_INET6: "inetv6",
        pynng.lib.NNG_AF_ZT: "zerotier",
    }

    def __init__(self, ffi_sock_addr):
        self._sock_addr = ffi_sock_addr

    @property
    def sock_addr(self):
        return self._sock_addr[0]

    @property
    def family(self):
        """Return the integer representing the socket address family"""
        return self.sock_addr.s_family

    @property
    def family_as_str(self):
        """The string representation of the address family."""
        return self.type_to_str[self.family]

    def __repr__(self):
        return "nng_sockaddr {{.family = {}}}".format(self.family)


class InprocAddr(SockAddr):
    def __init__(self, ffi_sock_addr):
        super().__init__(ffi_sock_addr)
        # union member
        self._mem = self.sock_addr.s_inproc

    @property
    def name_bytes(self):
        name = pynng.ffi.string(self._mem.sa_name)
        return name

    @property
    def name(self):
        return self.name_bytes.decode()

    def __str__(self):
        return self.name


class IPCAddr(SockAddr):
    def __init__(self, ffi_sock_addr):
        super().__init__(ffi_sock_addr)
        # union member
        self._mem = self.sock_addr.s_ipc

    @property
    def path_bytes(self):
        path = pynng.ffi.string(self._mem.sa_path)
        return path

    @property
    def path(self):
        return self.path_bytes.decode()

    def __str__(self):
        return self.path


class InAddr(SockAddr):
    def __init__(self, ffi_sock_addr):
        super().__init__(ffi_sock_addr)
        # union member
        self._mem = self.sock_addr.s_in

    @property
    def port(self):
        """Port, big-endian style"""
        return self._mem.sa_port

    @property
    def addr(self):
        """IP address as big-endian 32-bit number"""
        return self._mem.sa_addr

    def __str__(self):
        as_bytes = struct.pack("I", self.addr)
        ip = socket.inet_ntop(socket.AF_INET, as_bytes)
        port = socket.ntohs(self.port)
        return "{}:{}".format(ip, port)


class In6Addr(SockAddr):
    def __init__(self, ffi_sock_addr):
        super().__init__(ffi_sock_addr)
        # union member
        self._mem = self.sock_addr.s_in6

    @property
    def port(self):
        """Port, big-endian style"""
        return self._mem.sa_port

    @property
    def addr(self):
        """IP address as big-endian bytes"""
        return bytes(self._mem.sa_addr)

    def __str__(self):
        # TODO: not a good string repr at all
        ip = socket.inet_ntop(socket.AF_INET6, self.addr)
        port = socket.ntohs(self.port)
        return "[{}]:{}".format(ip, port)


class ZTAddr(SockAddr):
    def __init__(self, ffi_sock_addr):
        super().__init__(ffi_sock_addr)
        # union member
        self._mem = self.sock_addr.s_zt

    @property
    def nwid(self):
        return self._mem.as_nwid

    @property
    def nodeid(self):
        return self._mem.as_nodeid

    @property
    def port(self):
        return self._mem.as_port


def _nng_sockaddr(sa):
    # ensures the correct class gets instantiated based on s_family
    lookup = {
        pynng.lib.NNG_AF_INPROC: InprocAddr,
        pynng.lib.NNG_AF_IPC: IPCAddr,
        pynng.lib.NNG_AF_INET: InAddr,
        pynng.lib.NNG_AF_INET6: In6Addr,
        pynng.lib.NNG_AF_ZT: ZTAddr,
    }
    # fall through to SockAddr, e.g. if it's unspecified
    cls = lookup.get(sa[0].s_family, SockAddr)
    return cls(sa)
