import ctypes
import struct
from ctypes.wintypes import DWORD, BYTE, WCHAR

from .base import BaseManager

try:
    memoryview
except NameError:
    memoryview = buffer

# select constants
# error.h
NO_ERROR = 0
ERROR_INSUFFICIENT_BUFFER = 122
ERROR_INVALID_PARAMETER = 87
ERROR_NOT_SUPPORTED = 50

# mprapi.h
MAX_INTERFACE_NAME_LEN = 256

# iprtrmib.h
MAXLEN_PHYSADDR = 8
MAXLEN_IFDESCR = 256


class MIB_IFROW(ctypes.Structure):
    _fields_ = (
        ('name', WCHAR * MAX_INTERFACE_NAME_LEN),
        ('index', DWORD),
        ('type', DWORD),
        ('MTU', DWORD),
        ('speed', DWORD),
        ('physical_address_length', DWORD),
        ('physical_address_raw', BYTE * MAXLEN_PHYSADDR),
        ('admin_status', DWORD),
        ('operational_status', DWORD),
        ('last_change', DWORD),
        ('octets_received', DWORD),
        ('unicast_packets_received', DWORD),
        ('non_unicast_packets_received', DWORD),
        ('incoming_discards', DWORD),
        ('incoming_errors', DWORD),
        ('incoming_unknown_protocols', DWORD),
        ('octets_sent', DWORD),
        ('unicast_packets_sent', DWORD),
        ('non_unicast_packets_sent', DWORD),
        ('outgoing_discards', DWORD),
        ('outgoing_errors', DWORD),
        ('outgoing_queue_length', DWORD),
        ('description_length', DWORD),
        ('description_raw', ctypes.c_char * MAXLEN_IFDESCR),
    )

    def get_variable_length_property(self, name):
        val = getattr(self, name + '_raw')
        length = getattr(self, name + '_length')
        return memoryview(val).tobytes()[:length]

    def physical_address(self):
        return self.get_variable_length_property('physical_address')

    physical_address = property(physical_address)

    def description(self):
        return self.get_variable_length_property('description')

    description = property(description)


class MIB_IFTABLE(ctypes.Structure):
    _fields_ = (
        ('num_entries', DWORD),  # dwNumEntries
        ('entries', MIB_IFROW * 0),  # table
    )


class MIB_IPADDRROW(ctypes.Structure):
    _fields_ = (
        ('address', DWORD),
        ('index', DWORD),
        ('mask', DWORD),
        ('broadcast_address', DWORD),
        ('reassembly_size', DWORD),
        ('unused', ctypes.c_ushort),
        ('type', ctypes.c_ushort),
    )


class MIB_IPADDRTABLE(ctypes.Structure):
    _fields_ = (('num_entries', DWORD), ('entries', MIB_IPADDRROW * 0))


ip_helper = ctypes.windll.iphlpapi


class AllocatingTable:
    """
    Microsoft uses a consistent interface to retrieve arrays of structures of
    unknown length. This class abstracts that functionality so that it
    can be used to retrieve table entries using different API calls.
    """

    def __get_table_length(self):
        """
        Call the retrieval method with a null pointer and length of
        zero.  This should cause the 'insufficient buffer' error and
        return the size of the buffer needed.
        """
        length = DWORD()
        res = self.method(None, ctypes.byref(length), False)
        if res != ERROR_INSUFFICIENT_BUFFER:
            raise RuntimeError("Error getting table length (%d)" % res)
        return length.value

    def get_table(self):
        """
        Retrieve the complete table for the given method and structures.
        """
        buffer_length = self.__get_table_length()
        returned_buffer_length = DWORD(buffer_length)
        buffer = (ctypes.c_byte * buffer_length)()
        res = self.method(
            ctypes.byref(buffer), ctypes.byref(returned_buffer_length), False
        )
        if res != NO_ERROR:
            raise RuntimeError("Error retrieving table (%d)" % res)
        pointer_type = ctypes.POINTER(self.structure)
        return ctypes.cast(buffer, pointer_type).contents

    def get_entries(self):
        """
        Cast the result to an array of the size indicated by the container
        structure.
        """
        table = self.get_table()
        entries_array = self.row_structure * table.num_entries
        pointer_type = ctypes.POINTER(entries_array)
        return ctypes.cast(table.entries, pointer_type).contents

    entries = property(get_entries)


class InterfaceTable(AllocatingTable):
    method = ip_helper.GetIfTable
    structure = MIB_IFTABLE
    row_structure = MIB_IFROW


class AddressTable(AllocatingTable):
    method = ip_helper.GetIpAddrTable
    structure = MIB_IPADDRTABLE
    row_structure = MIB_IPADDRROW


class Manager(BaseManager):
    def get_host_mac_addresses(self):
        for entry in InterfaceTable().entries:
            yield entry.physical_address

    def get_host_ip_addresses(self):
        for entry in AddressTable().entries:
            yield struct.pack('L', entry.address)
