//go:build linux

package usb

import (
	"fmt"
	"sync"

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

// Externally supplied file descriptors, keyed by bus ID.
//
// Android is the reason this exists. Apps there cannot open /dev/bus/usb:
// access goes through the framework, which shows a permission dialog and
// returns an already-open descriptor. A small Java shim obtains it and passes
// it to this process, which then drives the device through the same usbdevfs
// ioctls as anywhere else — the kernel interface is identical, only the way
// the descriptor is obtained differs.
var (
	adoptedMu  sync.Mutex
	adoptedFDs = make(map[string]int)
)

// AdoptDeviceFD registers an already-open usbdevfs file descriptor for a bus
// ID. The next OpenDevice for that bus ID takes it instead of opening a path.
//
// Ownership transfers: the descriptor is closed when the resulting handle is
// closed, or by ReleaseAdoptedFDs if it is never claimed.
func AdoptDeviceFD(busID string, fd int) error {
	if busID == "" {
		return fmt.Errorf("bus ID is required")
	}
	if fd < 0 {
		return fmt.Errorf("invalid file descriptor %d", fd)
	}

	// Reject a descriptor that is not actually usable, so the failure is
	// reported here rather than as a confusing ioctl error much later.
	if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); err != nil {
		return fmt.Errorf("file descriptor %d is not open: %w", fd, err)
	}

	adoptedMu.Lock()
	defer adoptedMu.Unlock()

	if old, exists := adoptedFDs[busID]; exists && old != fd {
		unix.Close(old)
	}
	adoptedFDs[busID] = fd
	return nil
}

// takeAdoptedFD removes and returns a registered descriptor, if any.
func takeAdoptedFD(busID string) (int, bool) {
	adoptedMu.Lock()
	defer adoptedMu.Unlock()

	fd, ok := adoptedFDs[busID]
	if ok {
		delete(adoptedFDs, busID)
	}
	return fd, ok
}

// HasAdoptedFD reports whether a descriptor is registered for a bus ID.
func HasAdoptedFD(busID string) bool {
	adoptedMu.Lock()
	defer adoptedMu.Unlock()
	_, ok := adoptedFDs[busID]
	return ok
}

// ReleaseAdoptedFDs closes every registered descriptor that was never claimed.
func ReleaseAdoptedFDs() {
	adoptedMu.Lock()
	defer adoptedMu.Unlock()

	for busID, fd := range adoptedFDs {
		unix.Close(fd)
		delete(adoptedFDs, busID)
	}
}
