//go:build windows

package usb

import (
	"fmt"
	"log"
	"strings"
	"unsafe"

	"golang.org/x/sys/windows"
)

var (
	modsetupapi = windows.NewLazySystemDLL("setupapi.dll")

	procSetupDiGetClassDevsW             = modsetupapi.NewProc("SetupDiGetClassDevsW")
	procSetupDiEnumDeviceInterfaces      = modsetupapi.NewProc("SetupDiEnumDeviceInterfaces")
	procSetupDiGetDeviceInterfaceDetailW = modsetupapi.NewProc("SetupDiGetDeviceInterfaceDetailW")
	procSetupDiDestroyDeviceInfoList     = modsetupapi.NewProc("SetupDiDestroyDeviceInfoList")
)

const (
	digcfPresent         = 0x00000002
	digcfDeviceInterface = 0x00000010
)

type spDeviceInterfaceData struct {
	CbSize             uint32
	InterfaceClassGuid windows.GUID
	Flags              uint32
	Reserved           uintptr
}

// Enumerate lists USB devices reachable through the usbshare filter driver,
// plus any device registered from outside this process.
//
// Only devices with the filter attached appear: Windows has no equivalent of
// walking /sys/bus/usb, and without the filter there is no way to drive a
// device from user mode anyway, so listing the others would only offer
// devices that cannot actually be shared.
func Enumerate() ([]Device, error) {
	devices, err := enumerateFiltered()
	if err != nil {
		if external := ExternalDevices(); len(external) > 0 {
			return external, nil
		}
		return nil, err
	}

	return mergeExternal(devices), nil
}

func enumerateFiltered() ([]Device, error) {
	handle, _, _ := procSetupDiGetClassDevsW.Call(
		uintptr(unsafe.Pointer(&guidDevInterfaceUsbShare)),
		0, 0,
		uintptr(digcfPresent|digcfDeviceInterface),
	)
	if handle == uintptr(windows.InvalidHandle) {
		return nil, fmt.Errorf("no USB devices with the usbshare filter found " +
			"(install driver/windows/usbshare.inf and attach it to the devices you want to share)")
	}
	defer procSetupDiDestroyDeviceInfoList.Call(handle)

	var devices []Device

	for index := uint32(0); ; index++ {
		var ifaceData spDeviceInterfaceData
		ifaceData.CbSize = uint32(unsafe.Sizeof(ifaceData))

		ret, _, _ := procSetupDiEnumDeviceInterfaces.Call(
			handle, 0,
			uintptr(unsafe.Pointer(&guidDevInterfaceUsbShare)),
			uintptr(index),
			uintptr(unsafe.Pointer(&ifaceData)),
		)
		if ret == 0 {
			break // no more interfaces
		}

		devicePath, err := interfaceDetailPath(handle, &ifaceData)
		if err != nil {
			continue
		}

		dev, err := describeFilteredDevice(devicePath)
		if err != nil {
			log.Printf("[usb] skipping %s: %v", devicePath, err)
			continue
		}

		devices = append(devices, *dev)
	}

	return devices, nil
}

// interfaceDetailPath resolves an interface to the device path used to open it.
func interfaceDetailPath(handle uintptr, ifaceData *spDeviceInterfaceData) (string, error) {
	// First call determines the size.
	var required uint32
	procSetupDiGetDeviceInterfaceDetailW.Call(
		handle,
		uintptr(unsafe.Pointer(ifaceData)),
		0, 0,
		uintptr(unsafe.Pointer(&required)),
		0,
	)
	if required == 0 {
		return "", fmt.Errorf("could not determine the interface detail size")
	}

	buf := make([]byte, required)

	// SP_DEVICE_INTERFACE_DETAIL_DATA_W starts with cbSize, which must be set
	// to the size of the fixed part — 8 on 64-bit, counting the alignment of
	// the WCHAR array that follows — not the size of the whole buffer.
	*(*uint32)(unsafe.Pointer(&buf[0])) = 8

	ret, _, err := procSetupDiGetDeviceInterfaceDetailW.Call(
		handle,
		uintptr(unsafe.Pointer(ifaceData)),
		uintptr(unsafe.Pointer(&buf[0])),
		uintptr(required),
		uintptr(unsafe.Pointer(&required)),
		0,
	)
	if ret == 0 {
		return "", fmt.Errorf("reading interface detail: %w", err)
	}

	// The path is a null-terminated WCHAR string starting after cbSize.
	pathPtr := (*uint16)(unsafe.Pointer(&buf[4]))
	return windows.UTF16PtrToString(pathPtr), nil
}

// describeFilteredDevice opens a device briefly to read its descriptors.
//
// Claiming it here means the class driver stops seeing it for the duration.
// Enumeration therefore releases immediately: holding the claim would make
// merely listing devices disrupt whatever is using them.
func describeFilteredDevice(devicePath string) (*Device, error) {
	handle, err := OpenDriverDevice(devicePath)
	if err != nil {
		return nil, err
	}
	defer handle.Close()

	descriptors, err := handle.Descriptors()
	if err != nil {
		return nil, fmt.Errorf("reading descriptors: %w", err)
	}

	parsed, err := ParseDescriptors(descriptors)
	if err != nil {
		return nil, fmt.Errorf("parsing descriptors: %w", err)
	}

	info := handle.Info()

	cfg := parsed.FindConfig(info.ConfigurationValue)
	if cfg == nil {
		cfg = &parsed.Configs[0]
	}

	dev := &Device{
		BusID:          busIDFromPath(devicePath),
		BusNum:         0,
		DevNum:         uint32(info.PortNumber),
		Speed:          translateWindowsSpeed(info.Speed),
		VendorID:       parsed.VendorID,
		ProductID:      parsed.ProductID,
		BcdDevice:      parsed.BcdDevice,
		DeviceClass:    parsed.DeviceClass,
		DeviceSubClass: parsed.DeviceSubClass,
		DeviceProtocol: parsed.DeviceProtocol,
		ConfigValue:    cfg.Value,
		NumConfigs:     parsed.NumConfigs,
		DevPath:        devicePath,
		Interfaces:     cfg.ActiveInterfaces(),
		Endpoints:      cfg.AllEndpoints(),
	}

	return dev, nil
}

// busIDFromPath derives a stable identifier from a Windows device path.
//
// Paths look like \\?\usb#vid_046d&pid_c52b#5&1a2b3c4d&0&2#{guid}. The
// instance part is stable for as long as the device stays in the same port,
// which is what peers need: they request devices by this ID.
func busIDFromPath(devicePath string) string {
	trimmed := strings.TrimPrefix(devicePath, `\\?\`)
	if idx := strings.LastIndex(trimmed, "#{"); idx > 0 {
		trimmed = trimmed[:idx]
	}

	// '#' separates the parts; '&' appears inside them. Neither is a problem
	// for transport, but a shorter, more readable ID helps in the UI.
	parts := strings.Split(trimmed, "#")
	if len(parts) >= 3 {
		return strings.ReplaceAll(parts[2], "&", "-")
	}
	return strings.ReplaceAll(trimmed, "#", "-")
}

// translateWindowsSpeed maps USB_DEVICE_SPEED onto the USB/IP speed codes.
func translateWindowsSpeed(speed uint32) uint32 {
	switch speed {
	case 0: // UsbLowSpeed
		return 1
	case 1: // UsbFullSpeed
		return 2
	case 2: // UsbHighSpeed
		return 3
	case 3: // UsbSuperSpeed
		return 5
	default:
		return 0
	}
}
