'''
Reference
Methods, Structures and Documentation adapted from.
https://msdn.microsoft.com/en-us/library/windows/desktop \
/ms705945%28v=vs.85%29.aspx
'''

from ctypes import *
from ctypes.wintypes import *
from sys import exit as sys_exit
from plyer.compat import PY2, xrange


def customresize(array, new_size):
    return (
        array._type_ * new_size
    ).from_address(
        addressof(array)
    )


wlanapi = windll.LoadLibrary('wlanapi.dll')


class GUID(Structure):
    _fields_ = [
        ('Data1', c_ulong),
        ('Data2', c_ushort),
        ('Data3', c_ushort),
        ('Data4', c_ubyte * 8),
    ]


# The WLAN_INTERFACE_STATE enumerated type indicates the state of an interface.
WLAN_INTERFACE_STATE = c_uint
(wlan_interface_state_not_ready,
 wlan_interface_state_connected,
 wlan_interface_state_ad_hoc_network_formed,
 wlan_interface_state_disconnecting,
 wlan_interface_state_disconnected,
 wlan_interface_state_associating,
 wlan_interface_state_discovering,
 wlan_interface_state_authenticating) = map(WLAN_INTERFACE_STATE,
                                            xrange(0, 8))


class WLAN_INTERFACE_INFO(Structure):
    '''
    The WLAN_INTERFACE_STATE enumerated type indicates the state of an
    interface.
    '''
    _fields_ = [
        ("InterfaceGuid", GUID),
        ("strInterfaceDescription", c_wchar * 256),
        ("isState", WLAN_INTERFACE_STATE)
    ]


class WLAN_INTERFACE_INFO_LIST(Structure):
    '''
    The WLAN_INTERFACE_INFO_LIST structure contains an array of NIC interface
    information.
    '''
    _fields_ = [
        ("NumberOfItems", DWORD),
        ("Index", DWORD),
        ("InterfaceInfo", WLAN_INTERFACE_INFO * 1)
    ]


DOT11_MAC_ADDRESS = c_ubyte * 6
WLAN_MAX_PHY_TYPE_NUMBER = 0x8
DOT11_SSID_MAX_LENGTH = 32
WLAN_REASON_CODE = DWORD

DOT11_BSS_TYPE = c_uint
(dot11_BSS_type_infrastructure,
 dot11_BSS_type_independent,
 dot11_BSS_type_any) = map(DOT11_BSS_TYPE, xrange(1, 4))

# The DOT11_PHY_TYPE enumeration defines an 802.11 PHY and media type.
DOT11_PHY_TYPE = c_uint
dot11_phy_type_unknown = 0
dot11_phy_type_any = 0
dot11_phy_type_fhss = 1
dot11_phy_type_dsss = 2
dot11_phy_type_irbaseband = 3
dot11_phy_type_ofdm = 4
dot11_phy_type_hrdsss = 5
dot11_phy_type_erp = 6
dot11_phy_type_ht = 7
dot11_phy_type_IHV_start = 0x80000000
dot11_phy_type_IHV_end = 0xffffffff

# The DOT11_AUTH_ALGORITHM enumerated type defines a wireless
# LAN authentication algorithm.
DOT11_AUTH_ALGORITHM = c_uint
DOT11_AUTH_ALGO_80211_OPEN = 1
DOT11_AUTH_ALGO_80211_SHARED_KEY = 2
DOT11_AUTH_ALGO_WPA = 3
DOT11_AUTH_ALGO_WPA_PSK = 4
DOT11_AUTH_ALGO_WPA_NONE = 5
DOT11_AUTH_ALGO_RSNA = 6
DOT11_AUTH_ALGO_RSNA_PSK = 7
DOT11_AUTH_ALGO_IHV_START = 0x80000000
DOT11_AUTH_ALGO_IHV_END = 0xffffffff

# The DOT11_CIPHER_ALGORITHM enumerated type defines a cipher
# algorithm for data encryption and decryption.
DOT11_CIPHER_ALGORITHM = c_uint
DOT11_CIPHER_ALGO_NONE = 0x00
DOT11_CIPHER_ALGO_WEP40 = 0x01
DOT11_CIPHER_ALGO_TKIP = 0x02
DOT11_CIPHER_ALGO_CCMP = 0x04
DOT11_CIPHER_ALGO_WEP104 = 0x05
DOT11_CIPHER_ALGO_WPA_USE_GROUP = 0x100
DOT11_CIPHER_ALGO_RSN_USE_GROUP = 0x100
DOT11_CIPHER_ALGO_WEP = 0x101
DOT11_CIPHER_ALGO_IHV_START = 0x80000000
DOT11_CIPHER_ALGO_IHV_END = 0xffffffff


class DOT11_SSID(Structure):
    '''
    A DOT11_SSID structure contains the SSID of an interface
    '''
    _fields_ = [
        ("SSIDLength", c_ulong),
        ("SSID", c_char * DOT11_SSID_MAX_LENGTH)
    ]


# Enumerated type to define the code of connection.
WLAN_CONNECTION_MODE = c_uint
(wlan_connection_mode_profile,
 wlan_connection_mode_temporary_profile,
 wlan_connection_mode_discovery_secure,
 wlan_connection_mode_discovery_unsecure,
 wlan_connection_mode_auto,
 wlan_connection_mode_invalid) = map(WLAN_CONNECTION_MODE, xrange(0, 6))


class NDIS_OBJECT_HEADER(Structure):
    '''
    This Structure packages the object type, version, and size information
    that is required in many NDIS (Netword Driver interface Specification)
    Structures.
    '''
    _fields_ = [
        ("Type", c_char),
        ("Revision", c_char),
        ("Size", c_ushort)]


class DOT11_BSSID_LIST(Structure):
    '''
    The DOT11_BSSID_LIST structure contains a list of basic service set (BSS)
    identifiers.
    '''
    _fields_ = [
        ("Header", NDIS_OBJECT_HEADER),
        ("uNumOfEntries", ULONG),
        ("uTotalNumOfEntries", ULONG),
        ("BSSIDs", DOT11_MAC_ADDRESS * 1)
    ]


class WLAN_CONNECTION_PARAMETERS(Structure):
    '''
    The WLAN_CONNECTION_PARAMETERS structure specifies the parameters used when
    using the WlanConnect function.
    '''
    _fields_ = [
        ("wlanConnectionMode", WLAN_CONNECTION_MODE),
        ("strProfile", LPCWSTR),
        ("pDot11Ssid", POINTER(DOT11_SSID)),
        ("pDesiredBssidList", POINTER(DOT11_BSSID_LIST)),
        ("dot11BssType", DOT11_BSS_TYPE),
        ("dwFlags", DWORD)]


# The `WlanConnect` attempts to connect to a specific network.
WlanConnect = wlanapi.WlanConnect
WlanConnect.argtypes = (HANDLE,
                        POINTER(GUID),
                        POINTER(WLAN_CONNECTION_PARAMETERS),
                        c_void_p)
WlanConnect.restype = DWORD

# The `WlanDisconnect` method disconnects an interface from its
# current network.
WlanDisconnect = wlanapi.WlanDisconnect
WlanDisconnect.argtypes = (HANDLE,
                           POINTER(GUID),
                           c_void_p)
WlanDisconnect.restype = DWORD

# Opens a connection to the server.
WlanOpenHandle = wlanapi.WlanOpenHandle
WlanOpenHandle.argtypes = (DWORD, c_void_p, POINTER(DWORD), POINTER(HANDLE))
WlanOpenHandle.restype = DWORD

# The WlanCloseHandle method closes the connection to the server.
WlanCloseHandle = wlanapi.WlanCloseHandle
WlanCloseHandle.argtypes = (HANDLE, c_void_p)
WlanCloseHandle.restype = DWORD


class WLAN_AVAILABLE_NETWORK(Structure):
    '''
    The WLAN_INTERFACE_INFO structure contains information about a wireless
    LAN interface.
    '''
    _fields_ = [
        ("ProfileName", c_wchar * 256),
        ("dot11Ssid", DOT11_SSID),
        ("dot11BssType", DOT11_BSS_TYPE),
        ("NumberOfBssids", c_ulong),
        ("NetworkConnectable", c_bool),
        ("wlanNotConnectableReason", WLAN_REASON_CODE),
        ("NumberOfPhyTypes", c_ulong),
        ("dot11PhyTypes", DOT11_PHY_TYPE * WLAN_MAX_PHY_TYPE_NUMBER),
        ("MorePhyTypes", c_bool),
        ("wlanSignalQuality", c_ulong),
        ("SecurityEnabled", c_bool),
        ("dot11DefaultAuthAlgorithm", DOT11_AUTH_ALGORITHM),
        ("dot11DefaultCipherAlgorithm", DOT11_CIPHER_ALGORITHM),
        ("Flags", DWORD),
        ("Reserved", DWORD)]


class WLAN_AVAILABLE_NETWORK_LIST(Structure):
    '''
    The WLAN_INTERFACE_INFO_LIST structure contains an array of NIC
    interface information.
    '''
    _fields_ = [
        ("NumberOfItems", DWORD),
        ("Index", DWORD),
        ("Network", WLAN_AVAILABLE_NETWORK * 1)]


# The WlanEnumInterfaces function enumerates all of the wireless LAN interfaces
# currently enabled on the local computer.
WlanEnumInterfaces = wlanapi.WlanEnumInterfaces
WlanEnumInterfaces.argtypes = (HANDLE,
                               c_void_p,
                               POINTER(POINTER(WLAN_INTERFACE_INFO_LIST)))
WlanEnumInterfaces.restype = DWORD

# The WlanGetAvailableNetworkList function retrieves the list of available
# networks on a wireless LAN interface.
WlanGetAvailableNetworkList = wlanapi.WlanGetAvailableNetworkList
WlanGetAvailableNetworkList.argtypes = (HANDLE,
                                        POINTER(GUID),
                                        DWORD,
                                        c_void_p,
                                        POINTER(POINTER(
                                            WLAN_AVAILABLE_NETWORK_LIST)))
WlanGetAvailableNetworkList.restype = DWORD

# The WlanFreeMemory function frees memory. Any memory returned from Native
# Wifi functions must be freed.
WlanFreeMemory = wlanapi.WlanFreeMemory
WlanFreeMemory.argtypes = [c_void_p]

wireless_interfaces = None
available = None
_dict = {}

# Private methods.


def _connect(network, parameters):
    '''
    Attempts to connect to a specific network.
    '''
    global _dict  # pylint: disable=global-statement
    wireless_interface = _dict[network]

    wcp = WLAN_CONNECTION_PARAMETERS()
    connection_mode = parameters['connection_mode']
    wcp.wlanConnectionMode = WLAN_CONNECTION_MODE(connection_mode)

    if connection_mode == 0 or connection_mode == 1:
        wcp.strProfile = LPCWSTR(connection_params["profile"])
    else:
        cnxp.strProfile = None

    dot11Ssid = DOT11_SSID()
    try:
        dot11Ssid.SSID = parameters["ssid"]
        dot11Ssid.SSIDLength = len(parameters["ssid"])
    except KeyError:
        dot11Ssid.SSID = network
        dot11Ssid.SSIDLength = len(network)
    wcp.pDot11Ssid = pointer(dot11Ssid)

    dot11bssid = DOT11_BSSID_LIST()
    bssid = parameters["bssidList"]
    dot11bssid.Header = bssid['Header']
    dot11bssid.uNumOfEntries = bssid['uNumOfEntries']
    dot11bssid.uTotalNumOfEntries = bssid['uTotalNumOfEntries']
    dot11bssid.BSSIDs = bssid['BSSIDs']

    wcp.pDesiredBssidList = pointer(bssidList)

    bssType = parameters["bssType"]
    wcp.dot11BssType = DOT11_BSS_TYPE(bssType)

    wcp.dwFlags = DWORD(parameters["flags"])

    NegotiatedVersion = DWORD()
    ClientHandle = HANDLE()

    wlan = WlanOpenHandle(1,
                          None,
                          byref(NegotiatedVersion),
                          byref(ClientHandle))
    if wlan:
        sys_exit(FormatError(wlan))
    pInterfaceList = pointer(WLAN_INTERFACE_INFO_LIST())
    wlan = WlanEnumInterfaces(ClientHandle, None, byref(pInterfaceList))
    if wlan:
        sys_exit(FormatError(wlan))

    try:
        wlan = WlanConnect(ClientHandle,
                           wireless_interface,
                           wcp,
                           None)
        if wlan:
            sys_exit(FormatError(wlan))
        WlanCloseHandle(ClientHandle, None)
    finally:
        WlanFreeMemory(pInterfaceList)


def _disconnect():
    '''
    To disconnect an interface form the current network.
    '''
    NegotiatedVersion = DWORD()
    ClientHandle = HANDLE()

    wlan = WlanOpenHandle(
        1,
        None,
        byref(NegotiatedVersion),
        byref(ClientHandle)
    )
    if wlan:
        sys_exit(FormatError(wlan))

    pInterfaceList = pointer(WLAN_INTERFACE_INFO_LIST())

    wlan = WlanEnumInterfaces(ClientHandle, None, byref(pInterfaceList))
    if wlan:
        sys_exit(FormatError(wlan))

    result = None
    try:
        ifaces = customresize(
            pInterfaceList.contents.InterfaceInfo,
            pInterfaceList.contents.NumberOfItems
        )

        # find each available network for each interface
        for iface in ifaces:
            wlan = WlanDisconnect(
                ClientHandle,
                byref(iface.InterfaceGuid),
                None
            )
            if wlan:
                sys_exit(FormatError(wlan))
            WlanCloseHandle(ClientHandle, None)

    finally:
        WlanFreeMemory(pInterfaceList)
        result = get_available_wifi()

    return result


def _start_scanning():
    '''
    Private method for scanning and returns the available devices.
    '''
    global available  # pylint: disable=global-statement
    global wireless_interfaces  # pylint: disable=global-statement
    NegotiatedVersion = DWORD()
    ClientHandle = HANDLE()

    wlan = WlanOpenHandle(
        1,
        None,
        byref(NegotiatedVersion),
        byref(ClientHandle)
    )

    if wlan:
        sys_exit(FormatError(wlan))

    # find all wireless network interfaces
    pInterfaceList = pointer(WLAN_INTERFACE_INFO_LIST())
    wlan = WlanEnumInterfaces(ClientHandle, None, byref(pInterfaceList))
    if wlan:
        sys_exit(FormatError(wlan))

    result = None
    try:
        ifaces = customresize(
            pInterfaceList.contents.InterfaceInfo,
            pInterfaceList.contents.NumberOfItems
        )

        # find each available network for each interface
        wireless_interfaces = ifaces
        for iface in ifaces:
            pAvailableNetworkList = pointer(WLAN_AVAILABLE_NETWORK_LIST())
            wlan = WlanGetAvailableNetworkList(
                ClientHandle,
                byref(iface.InterfaceGuid),
                0,
                None,
                byref(pAvailableNetworkList)
            )

            if wlan:
                sys_exit(FormatError(wlan))

            try:
                avail_net_list = pAvailableNetworkList.contents
                networks = customresize(
                    avail_net_list.Network,
                    avail_net_list.NumberOfItems
                )

                # Assigning the value of networks to the global variable
                # `available`, so it could be used in other methods.
                available = networks
                _make_dict()
                wlan = WlanDisconnect(
                    ClientHandle,
                    byref(iface.InterfaceGuid),
                    None
                )

                if wlan:
                    sys_exit(FormatError(wlan))
                WlanCloseHandle(ClientHandle, None)

            finally:
                WlanFreeMemory(pAvailableNetworkList)

    finally:
        WlanFreeMemory(pInterfaceList)
        result = get_available_wifi()

    return result


def _get_network_info(name):
    '''
    returns the list of the network selected in a dict.
    '''
    global available  # pylint: disable=global-statement
    global _dict  # pylint: disable=global-statement

    net = _dict[name]
    dot11BssType = net.dot11BssType
    dot11DefaultAuthAlgorithm = net.dot11DefaultAuthAlgorithm
    dot11DefaultCipherAlgorithm = net.dot11DefaultCipherAlgorithm
    dot11PhyTypes = net.dot11PhyTypes[0]
    dot11Ssid = net.dot11Ssid
    wlanNotConnectableReason = net.wlanNotConnectableReason
    wlanSignalQuality = net.wlanSignalQuality
    return {"dot11BssType": dot11BssType,
            "dot11DefaultAuthAlgorithm": dot11DefaultAuthAlgorithm,
            "dot11DefaultCipherAlgorithm": dot11DefaultCipherAlgorithm,
            "dot11PhyTypes": dot11PhyTypes,
            "SSID": dot11Ssid.SSID,
            "SSIDLength": dot11Ssid.SSIDLength,
            "wlanNotConnectableReason": wlanNotConnectableReason,
            "wlanSignalQuality": wlanSignalQuality}


def _make_dict():
    '''
    Prepares a dict so it could store network information.
    '''
    global available  # pylint: disable=global-statement
    global _dict  # pylint: disable=global-statement
    _dict = {}
    for network in available:
        # if bytes, dict['name'] throws an error on py3 if not b'name'
        if PY2:
            _dict[unicode(network.dot11Ssid.SSID)] = network
        else:
            _dict[network.dot11Ssid.SSID.decode('utf-8')] = network


def _get_available_wifi():
    '''
    returns the available wifi networks.
    '''
    global _dict  # pylint: disable=global-statement
    return _dict


def _is_enabled():
    '''
    Reason for returning true is explained in widi facade.
    /plyer/facades/wifi.py
    '''
    return True

# public methods.


def is_enabled():
    '''
    calls private method `_is_enabled` and returns the result.
    '''
    return _is_enabled()


def connect(network, parameters):
    '''
    Connect to a network with following parameters.
    '''
    _connect(network=network, parameters=parameters)


def disconnect():
    '''
    Disconnect from a network.
    '''
    _disconnect()


def start_scanning():
    '''
    Start scanning for available wifi networks available.
    '''
    return _start_scanning()


def get_network_info(name):
    '''
    return the wifi network info.
    '''
    return _get_network_info(name=name)


def get_available_wifi():
    '''
    return the available wifi networks available
    '''
    return _get_available_wifi()
