#
# Copyright (c) 2019 Nordic Semiconductor ASA
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
#   1. Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
#   2. Redistributions in binary form must reproduce the above copyright notice, this
#   list of conditions and the following disclaimer in the documentation and/or
#   other materials provided with the distribution.
#
#   3. Neither the name of Nordic Semiconductor ASA nor the names of other
#   contributors to this software may be used to endorse or promote products
#   derived from this software without specific prior written permission.
#
#   4. This software must only be used in or with a processor manufactured by Nordic
#   Semiconductor ASA, or in or with a processor manufactured by a third party that
#   is used in combination with a processor manufactured by Nordic Semiconductor.
#
#   5. Any software provided in binary or object form under this license must not be
#   reverse engineered, decompiled, modified and/or disassembled.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

import ctypes
import sys
from typing import TYPE_CHECKING, Any, Optional

from ..enumerated_device import EnumeratedDevice
from ..lister_backend import AbstractLister

if sys.platform == "win32":
    import winreg
elif TYPE_CHECKING:
    import fake_winreg as winreg

if sys.platform == "win32" or TYPE_CHECKING:
    from .constants import DEVPKEY, DIGCF_DEVICEINTERFACE, DIGCF_PRESENT
    from .structures import _GUID, GUID, DeviceInfoData, ValidHandle, ctypesInternalGUID

if sys.platform == "win32":
    setup_api = ctypes.windll.setupapi

    SetupDiGetClassDevs = setup_api.SetupDiGetClassDevsW
    SetupDiGetClassDevs.argtypes = [
        ctypes.POINTER(_GUID),
        ctypes.c_wchar_p,
        ctypes.c_void_p,
        ctypes.c_uint32,
    ]
    SetupDiGetClassDevs.restype = ctypes.c_void_p
    SetupDiGetClassDevs.errcheck = ValidHandle

    SetupDiEnumDeviceInfo = setup_api.SetupDiEnumDeviceInfo
    SetupDiEnumDeviceInfo.argtypes = [
        ctypes.c_void_p,
        ctypes.c_uint32,
        ctypes.POINTER(DeviceInfoData),
    ]
    SetupDiEnumDeviceInfo.restype = ctypes.c_bool

    SetupDiGetDeviceInstanceId = setup_api.SetupDiGetDeviceInstanceIdW
    SetupDiGetDeviceInstanceId.argtypes = [
        ctypes.c_void_p,
        ctypes.POINTER(DeviceInfoData),
        ctypes.c_wchar_p,
        ctypes.c_uint32,
        ctypes.POINTER(ctypes.c_uint32),
    ]
    SetupDiGetDeviceInstanceId.restype = ctypes.c_bool

    SetupDiGetDeviceProperty = setup_api.SetupDiGetDevicePropertyW
    SetupDiGetDeviceProperty.argtypes = [
        ctypes.c_void_p,
        ctypes.c_void_p,
        ctypes.c_void_p,
        ctypes.c_void_p,
        ctypes.c_void_p,
        ctypes.c_uint,
        ctypes.c_void_p,
        ctypes.c_uint,
    ]
    SetupDiGetDeviceProperty.restype = ctypes.c_bool

#  constants
DICS_FLAG_GLOBAL = 1
DIREG_DEV = 1
INVALID_HANDLE_VALUE = -1
MAX_BUFSIZE = 1000


def get_serial_serial_no(
    vendor_id: str, product_id: str, h_dev_info: Any, device_info_data: "DeviceInfoData"
) -> Optional[str]:
    prop_type = ctypes.c_ulong()
    required_size = ctypes.c_ulong()

    instance_id_buffer = ctypes.create_unicode_buffer(MAX_BUFSIZE)
    SetupDiGetDeviceProperty(  # type: ignore[name-defined]
        h_dev_info,
        ctypes.byref(device_info_data),
        ctypes.byref(DEVPKEY.Device.ContainerId),
        ctypes.byref(prop_type),
        instance_id_buffer,
        MAX_BUFSIZE,
        ctypes.byref(required_size),
        0,
    )

    wanted_GUID = GUID(ctypesInternalGUID(instance_id_buffer))

    device_address = ctypes.c_int32()
    setup_api.SetupDiGetDevicePropertyW(  # type: ignore[name-defined]
        h_dev_info,
        ctypes.byref(device_info_data),
        ctypes.byref(DEVPKEY.Device.DeviceAddress),
        ctypes.byref(prop_type),
        ctypes.byref(device_address),
        ctypes.sizeof(ctypes.c_int32),
        ctypes.byref(required_size),
        0,
    )

    hkey_path = "SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_{}&PID_{}".format(
        vendor_id, product_id
    )
    try:
        vendor_product_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path)
    except EnvironmentError:
        return None

    serial_numbers_count = winreg.QueryInfoKey(vendor_product_hkey)[0]

    for serial_number_idx in range(serial_numbers_count):
        try:
            serial_number: str = winreg.EnumKey(vendor_product_hkey, serial_number_idx)
        except EnvironmentError:
            continue

        hkey_path = "SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_{}&PID_{}\\{}".format(
            vendor_id, product_id, serial_number
        )

        try:
            device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path)
        except EnvironmentError:
            continue

        try:
            queried_container_id = winreg.QueryValueEx(device_hkey, "ContainerID")[0]
        except EnvironmentError:
            winreg.CloseKey(device_hkey)
            continue

        try:
            queried_address = winreg.QueryValueEx(device_hkey, "Address")[0]
        except EnvironmentError:
            winreg.CloseKey(device_hkey)
            continue

        winreg.CloseKey(device_hkey)

        if (
            queried_container_id.lower() == str(wanted_GUID).lower()
            and queried_address == device_address.value
        ):
            winreg.CloseKey(vendor_product_hkey)
            return serial_number

    winreg.CloseKey(vendor_product_hkey)
    return None


def com_port_is_open(port: str) -> bool:
    hkey_path = "HARDWARE\\DEVICEMAP\\SERIALCOMM"
    try:
        device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path)
    except EnvironmentError:
        #  Unable to check enumerated serialports. Assume open.
        return True
    next_key = 0
    while True:
        try:
            value = winreg.EnumValue(device_hkey, next_key)[1]
            next_key += 1
            if port == value:
                winreg.CloseKey(device_hkey)
                return True
        except WindowsError:  # type: ignore[name-defined]
            break
    winreg.CloseKey(device_hkey)
    return False


def list_all_com_ports(
    vendor_id: str, product_id: str, serial_number: str
) -> list[str]:
    ports: list[str] = []

    hkey_path = "SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_{}&PID_{}\\{}".format(
        vendor_id, product_id, serial_number
    )

    try:
        device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path)
    except EnvironmentError:
        return ports

    try:
        parent_id = winreg.QueryValueEx(device_hkey, "ParentIdPrefix")[0]
    except EnvironmentError:
        winreg.CloseKey(device_hkey)
        return ports

    winreg.CloseKey(device_hkey)

    hkey_path = "SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_{}&PID_{}\\{}\\Device Parameters".format(
        vendor_id, product_id, serial_number
    )
    try:
        device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path)
        try:
            COM_port = winreg.QueryValueEx(device_hkey, "PortName")[0]
            ports.append(COM_port)
        except EnvironmentError:
            #  No COM port for root device.
            pass
        winreg.CloseKey(device_hkey)
    except EnvironmentError:
        #  Root device has no device parameters
        pass

    iface_id = 0
    while True:
        hkey_path = (
            "SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_{vid_val}&PID_{pid_val}&"
            "MI_{mi_val}\\{parent_val}&{parent_iface}\\Device Parameters".format(
                vid_val=vendor_id,
                pid_val=product_id,
                mi_val=str(iface_id).zfill(2),
                parent_val=parent_id,
                parent_iface=str(iface_id).zfill(4),
            )
        )
        iface_id += 1
        try:
            device_hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, hkey_path)
        except EnvironmentError:
            break

        try:
            port_name = winreg.QueryValueEx(device_hkey, "PortName")[0]
        except EnvironmentError:
            winreg.CloseKey(device_hkey)
            continue

        winreg.CloseKey(device_hkey)
        if com_port_is_open(port_name):
            ports.append(port_name)

    return ports


class Win32Lister(AbstractLister):
    def __init__(self) -> None:
        self.GUID_DEVINTERFACE_USB_DEVICE = GUID(
            "{A5DCBF10-6530-11D2-901F-00C04FB951ED}"
        )

    def enumerate(self) -> list[EnumeratedDevice]:
        enumerated_devices: list[EnumeratedDevice] = []
        h_dev_info = SetupDiGetClassDevs(  # type: ignore[name-defined]
            ctypes.byref(self.GUID_DEVINTERFACE_USB_DEVICE._guid),
            None,
            0,
            DIGCF_PRESENT | DIGCF_DEVICEINTERFACE,
        )
        dev_info_data = DeviceInfoData()
        if h_dev_info == -1:
            return enumerated_devices

        next_enum = 0
        while SetupDiEnumDeviceInfo(h_dev_info, next_enum, ctypes.byref(dev_info_data)):  # type: ignore[name-defined]
            next_enum += 1

            sz_buffer = ctypes.create_unicode_buffer(MAX_BUFSIZE)
            dw_size = ctypes.c_ulong()
            res = SetupDiGetDeviceInstanceId(  # type: ignore[name-defined]
                h_dev_info,
                ctypes.byref(dev_info_data),
                sz_buffer,
                MAX_BUFSIZE,
                ctypes.byref(dw_size),
            )
            if not res:
                #  failed to fetch pid vid
                continue
            vendor_id = sz_buffer.value[8:12]
            product_id = sz_buffer.value[17:21]

            serial_number = get_serial_serial_no(
                vendor_id, product_id, h_dev_info, dev_info_data
            )
            if not serial_number:
                continue

            COM_ports = list_all_com_ports(vendor_id, product_id, serial_number)

            if len(COM_ports) > 0:
                device = EnumeratedDevice(
                    vendor_id, product_id, serial_number, COM_ports
                )
                enumerated_devices.append(device)

        return enumerated_devices
