# Original work Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Modified work Copyright 2020 Yubico AB. All Rights Reserved.
# This file, with modifications, is licensed under the above Apache License.

from __future__ import annotations

import ctypes
import logging
import platform
import sys
from typing import cast

from .base import FIDO_USAGE, FIDO_USAGE_PAGE, CtapHidConnection, HidDescriptor

# Only typecheck this file on Windows
assert sys.platform == "win32"  # noqa: S101
from ctypes import WinError, wintypes  # noqa: E402

logger = logging.getLogger(__name__)


# Load relevant DLLs
LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800
hid = ctypes.WinDLL("hid", winmode=LOAD_LIBRARY_SEARCH_SYSTEM32)
setupapi = ctypes.WinDLL("setupapi", winmode=LOAD_LIBRARY_SEARCH_SYSTEM32)
kernel32 = ctypes.WinDLL("kernel32", winmode=LOAD_LIBRARY_SEARCH_SYSTEM32)


# Various structs that are used in the Windows APIs we call
class GUID(ctypes.Structure):
    _fields_ = [
        ("Data1", ctypes.c_ulong),
        ("Data2", ctypes.c_ushort),
        ("Data3", ctypes.c_ushort),
        ("Data4", ctypes.c_ubyte * 8),
    ]


# On Windows, SetupAPI.h packs structures differently in 64bit and
# 32bit mode.  In 64-bit mode, the structures are packed on 8 byte
# boundaries, while in 32-bit mode, they are packed on 1 byte boundaries.
# This is important to get right for some API calls that fill out these
# structures.
if platform.architecture()[0] == "64bit":
    SETUPAPI_PACK = 8
elif platform.architecture()[0] == "32bit":
    SETUPAPI_PACK = 1
else:
    raise OSError(f"Unknown architecture: {platform.architecture()[0]}")


class DeviceInterfaceData(ctypes.Structure):
    _fields_ = [
        ("cbSize", wintypes.DWORD),
        ("InterfaceClassGuid", GUID),
        ("Flags", wintypes.DWORD),
        ("Reserved", ctypes.POINTER(ctypes.c_ulong)),
    ]
    _pack_ = SETUPAPI_PACK


class DeviceInterfaceDetailData(ctypes.Structure):
    _fields_ = [("cbSize", wintypes.DWORD), ("DevicePath", ctypes.c_byte * 1)]
    _pack_ = SETUPAPI_PACK


class HidAttributes(ctypes.Structure):
    _fields_ = [
        ("Size", ctypes.c_ulong),
        ("VendorID", ctypes.c_ushort),
        ("ProductID", ctypes.c_ushort),
        ("VersionNumber", ctypes.c_ushort),
    ]


class HidCapabilities(ctypes.Structure):
    _fields_ = [
        ("Usage", ctypes.c_ushort),
        ("UsagePage", ctypes.c_ushort),
        ("InputReportByteLength", ctypes.c_ushort),
        ("OutputReportByteLength", ctypes.c_ushort),
        ("FeatureReportByteLength", ctypes.c_ushort),
        ("Reserved", ctypes.c_ushort * 17),
        ("NotUsed", ctypes.c_ushort * 10),
    ]


# Various void* aliases for readability.
HDEVINFO = ctypes.c_void_p
HANDLE = ctypes.c_void_p
PHIDP_PREPARSED_DATA = ctypes.c_void_p  # pylint: disable=invalid-name

# This is a HANDLE.
# INVALID_HANDLE_VALUE = 0xFFFFFFFF
INVALID_HANDLE_VALUE = (1 << 8 * ctypes.sizeof(ctypes.c_void_p)) - 1

# Status codes
FILE_SHARE_READ = 0x00000001
FILE_SHARE_WRITE = 0x00000002
OPEN_EXISTING = 0x03
NTSTATUS = ctypes.c_long
HIDP_STATUS_SUCCESS = 0x00110000

# CreateFile Flags
GENERIC_WRITE = 0x40000000
GENERIC_READ = 0x80000000

DIGCF_DEVICEINTERFACE = 0x10
DIGCF_PRESENT = 0x02

# Function signatures
hid.HidD_GetHidGuid.restype = None
hid.HidD_GetHidGuid.argtypes = [ctypes.POINTER(GUID)]
hid.HidD_GetAttributes.restype = wintypes.BOOLEAN
hid.HidD_GetAttributes.argtypes = [HANDLE, ctypes.POINTER(HidAttributes)]
hid.HidD_GetPreparsedData.restype = wintypes.BOOLEAN
hid.HidD_GetPreparsedData.argtypes = [HANDLE, ctypes.POINTER(PHIDP_PREPARSED_DATA)]
hid.HidD_FreePreparsedData.restype = wintypes.BOOLEAN
hid.HidD_FreePreparsedData.argtypes = [PHIDP_PREPARSED_DATA]
hid.HidD_GetProductString.restype = wintypes.BOOLEAN
hid.HidD_GetProductString.argtypes = [HANDLE, ctypes.c_void_p, ctypes.c_ulong]
hid.HidD_GetSerialNumberString.restype = wintypes.BOOLEAN
hid.HidD_GetSerialNumberString.argtypes = [HANDLE, ctypes.c_void_p, ctypes.c_ulong]
hid.HidP_GetCaps.restype = NTSTATUS
hid.HidP_GetCaps.argtypes = [PHIDP_PREPARSED_DATA, ctypes.POINTER(HidCapabilities)]


hid.HidD_GetFeature.restype = wintypes.BOOL
hid.HidD_GetFeature.argtypes = [HANDLE, ctypes.c_void_p, ctypes.c_ulong]
hid.HidD_SetFeature.restype = wintypes.BOOL
hid.HidD_SetFeature.argtypes = [HANDLE, ctypes.c_void_p, ctypes.c_ulong]

setupapi.SetupDiGetClassDevsA.argtypes = [
    ctypes.POINTER(GUID),
    ctypes.c_char_p,
    wintypes.HWND,
    wintypes.DWORD,
]
setupapi.SetupDiGetClassDevsA.restype = HDEVINFO
setupapi.SetupDiEnumDeviceInterfaces.restype = wintypes.BOOL
setupapi.SetupDiEnumDeviceInterfaces.argtypes = [
    HDEVINFO,
    ctypes.c_void_p,
    ctypes.POINTER(GUID),
    wintypes.DWORD,
    ctypes.POINTER(DeviceInterfaceData),
]
setupapi.SetupDiGetDeviceInterfaceDetailA.restype = wintypes.BOOL
setupapi.SetupDiGetDeviceInterfaceDetailA.argtypes = [
    HDEVINFO,
    ctypes.POINTER(DeviceInterfaceData),
    ctypes.POINTER(DeviceInterfaceDetailData),
    wintypes.DWORD,
    ctypes.POINTER(wintypes.DWORD),
    ctypes.c_void_p,
]
setupapi.SetupDiDestroyDeviceInfoList.restype = wintypes.BOOL
setupapi.SetupDiDestroyDeviceInfoList.argtypes = [
    HDEVINFO,
]

kernel32.CreateFileA.restype = HANDLE
kernel32.CreateFileA.argtypes = [
    ctypes.c_char_p,
    wintypes.DWORD,
    wintypes.DWORD,
    ctypes.c_void_p,
    wintypes.DWORD,
    wintypes.DWORD,
    HANDLE,
]
kernel32.CloseHandle.restype = wintypes.BOOL
kernel32.CloseHandle.argtypes = [HANDLE]


class WinCtapHidConnection(CtapHidConnection):
    def __init__(self, descriptor):
        self.descriptor = descriptor
        self.handle = kernel32.CreateFileA(
            descriptor.path,
            GENERIC_WRITE | GENERIC_READ,
            FILE_SHARE_READ | FILE_SHARE_WRITE,
            None,
            OPEN_EXISTING,
            0,
            None,
        )
        if self.handle == INVALID_HANDLE_VALUE:
            raise WinError()

    def close(self):
        kernel32.CloseHandle(self.handle)

    def write_packet(self, data):
        out = b"\0" + data  # Prepend report ID
        num_written = wintypes.DWORD()
        ret = kernel32.WriteFile(
            self.handle, out, len(out), ctypes.byref(num_written), None
        )
        if not ret:
            raise WinError()
        if num_written.value != len(out):
            raise OSError(
                "Failed to write complete packet.  "
                + "Expected %d, but got %d" % (len(out), num_written.value)
            )

    def read_packet(self):
        buf = ctypes.create_string_buffer(self.descriptor.report_size_in + 1)
        num_read = wintypes.DWORD()
        ret = kernel32.ReadFile(
            self.handle, buf, len(buf), ctypes.byref(num_read), None
        )
        if not ret:
            raise WinError()

        if num_read.value != self.descriptor.report_size_in + 1:
            raise OSError("Failed to read full length report from device.")

        return buf.raw[1:]  # Strip report ID


def get_vid_pid(device):
    attributes = HidAttributes()
    result = hid.HidD_GetAttributes(device, ctypes.byref(attributes))
    if not result:
        raise WinError()

    return attributes.VendorID, attributes.ProductID


def get_product_name(device):
    buf = ctypes.create_unicode_buffer(128)

    result = hid.HidD_GetProductString(device, buf, ctypes.c_ulong(ctypes.sizeof(buf)))
    if not result:
        return None

    return buf.value


def get_serial(device):
    buf = ctypes.create_unicode_buffer(128)

    result = hid.HidD_GetSerialNumberString(
        device, buf, ctypes.c_ulong(ctypes.sizeof(buf))
    )
    if not result:
        return None

    return buf.value


def get_descriptor(path):
    device = kernel32.CreateFileA(
        path,
        0,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        None,
        OPEN_EXISTING,
        0,
        None,
    )
    if device == INVALID_HANDLE_VALUE:
        raise WinError()
    try:
        preparsed_data = PHIDP_PREPARSED_DATA(0)
        ret = hid.HidD_GetPreparsedData(device, ctypes.byref(preparsed_data))
        if not ret:
            raise WinError()

        try:
            caps = HidCapabilities()
            ret = hid.HidP_GetCaps(preparsed_data, ctypes.byref(caps))

            if ret != HIDP_STATUS_SUCCESS:
                raise WinError()

            if caps.UsagePage == FIDO_USAGE_PAGE and caps.Usage == FIDO_USAGE:
                vid, pid = get_vid_pid(device)
                product_name = get_product_name(device)
                serial = get_serial(device)
                # Sizes here include 1-byte report ID, which we need to remove.
                size_in = caps.InputReportByteLength - 1
                size_out = caps.OutputReportByteLength - 1
                return HidDescriptor(
                    path, vid, pid, size_in, size_out, product_name, serial
                )
            raise ValueError("Not a CTAP device")

        finally:
            hid.HidD_FreePreparsedData(preparsed_data)
    finally:
        kernel32.CloseHandle(device)


def open_connection(descriptor):
    return WinCtapHidConnection(descriptor)


_SKIP = cast(HidDescriptor, object())
_descriptor_cache: dict[bytes, HidDescriptor] = {}


def list_descriptors():
    stale = set(_descriptor_cache)
    descriptors = []

    hid_guid = GUID()
    hid.HidD_GetHidGuid(ctypes.byref(hid_guid))

    collection = setupapi.SetupDiGetClassDevsA(
        ctypes.byref(hid_guid), None, None, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT
    )
    try:
        index = 0
        interface_info = DeviceInterfaceData()
        interface_info.cbSize = ctypes.sizeof(DeviceInterfaceData)

        while True:
            result = setupapi.SetupDiEnumDeviceInterfaces(
                collection,
                0,
                ctypes.byref(hid_guid),
                index,
                ctypes.byref(interface_info),
            )
            index += 1
            if not result:
                break

            dw_detail_len = wintypes.DWORD()
            result = setupapi.SetupDiGetDeviceInterfaceDetailA(
                collection,
                ctypes.byref(interface_info),
                None,
                0,
                ctypes.byref(dw_detail_len),
                None,
            )
            if result:
                raise WinError()

            detail_len = dw_detail_len.value
            if detail_len == 0:
                # skip this device, some kind of error
                continue

            buf = ctypes.create_string_buffer(detail_len)
            interface_detail = DeviceInterfaceDetailData.from_buffer(buf)
            interface_detail.cbSize = ctypes.sizeof(DeviceInterfaceDetailData)

            result = setupapi.SetupDiGetDeviceInterfaceDetailA(
                collection,
                ctypes.byref(interface_info),
                ctypes.byref(interface_detail),
                detail_len,
                None,
                None,
            )
            if not result:
                raise WinError()

            path = ctypes.string_at(interface_detail.DevicePath)
            stale.discard(path)

            # Check if path already cached
            desc = _descriptor_cache.get(path)
            if desc:
                if desc is not _SKIP:
                    descriptors.append(desc)
                continue

            try:
                descriptor = get_descriptor(path)
                _descriptor_cache[path] = descriptor
                descriptors.append(descriptor)
                continue
            except ValueError:
                pass  # Not a CTAP device
            except Exception:
                logger.debug(
                    "Failed reading HID descriptor for %s", path, exc_info=True
                )
            _descriptor_cache[path] = _SKIP
    finally:
        setupapi.SetupDiDestroyDeviceInfoList(collection)

    # Remove entries from the cache that were not seen
    for path in stale:
        del _descriptor_cache[path]

    return descriptors
