// Package usbgadget provides a high-level interface to manage USB gadgets
// THIS PACKAGE IS FOR INTERNAL USE ONLY AND ITS API MAY CHANGE WITHOUT NOTICE
package usbgadget

import (
	"context"
	"os"
	"path"
	"sync"
	"time"

	"github.com/jetkvm/kvm/internal/logging"
	"github.com/rs/zerolog"
)

// Devices is a struct that represents the USB devices that can be enabled on a USB gadget.
type Devices struct {
	AbsoluteMouse bool `json:"absolute_mouse"`
	RelativeMouse bool `json:"relative_mouse"`
	Keyboard      bool `json:"keyboard"`
	MassStorage   bool `json:"mass_storage"`
	Audio         bool `json:"audio"`
}

// Config is a struct that represents the customizations for a USB gadget.
// TODO: rename to something else that won't confuse with the USB gadget configuration
type Config struct {
	VendorId     string `json:"vendor_id"`
	ProductId    string `json:"product_id"`
	SerialNumber string `json:"serial_number"`
	Manufacturer string `json:"manufacturer"`
	Product      string `json:"product"`

	strictMode bool // when it's enabled, all warnings will be converted to errors
	isEmpty    bool
}

var defaultUsbGadgetDevices = Devices{
	AbsoluteMouse: true,
	RelativeMouse: true,
	Keyboard:      true,
	MassStorage:   true,
}

type KeysDownState struct {
	Modifier byte      `json:"modifier"`
	Keys     ByteSlice `json:"keys"`
}

// UsbGadget is a struct that represents a USB gadget.
type UsbGadget struct {
	name          string
	udc           string
	kvmGadgetPath string
	configC1Path  string

	configMap    map[string]gadgetConfigItem
	customConfig Config

	configLock sync.Mutex

	keyboardHidFile *os.File
	keyboardLock    sync.Mutex
	absMouseHidFile *os.File
	absMouseLock    sync.Mutex
	relMouseHidFile *os.File
	relMouseLock    sync.Mutex

	keyboardState byte          // keyboard latched state (NumLock, CapsLock, ScrollLock, Compose, Kana)
	keysDownState KeysDownState // keyboard dynamic state (modifier keys and pressed keys)

	keyboardStateLock   sync.Mutex
	keyboardStateCtx    context.Context
	keyboardStateCancel context.CancelFunc

	enabledDevices Devices

	strictMode bool // only intended for testing for now

	absMouseAccumulatedWheelY float64

	lastUserInput time.Time

	tx     *UsbGadgetTransaction
	txLock sync.Mutex

	onKeyboardStateChange *func(state KeyboardState)
	onKeysDownChange      *func(state KeysDownState)

	log *zerolog.Logger

	logSuppressionCounter map[string]int
	logSuppressionLock    sync.Mutex
}

const configFSPath = "/sys/kernel/config"
const gadgetPath = "/sys/kernel/config/usb_gadget"

var defaultLogger = logging.GetSubsystemLogger("usbgadget")

// NewUsbGadget creates a new UsbGadget.
func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *UsbGadget {
	return newUsbGadget(name, defaultGadgetConfig, enabledDevices, config, logger)
}

// CloseHidFiles closes all open HID files
func (u *UsbGadget) CloseHidFiles() {
	u.log.Debug().Msg("closing HID files")

	// Close keyboard HID file
	if u.keyboardHidFile != nil {
		if err := u.keyboardHidFile.Close(); err != nil {
			u.log.Debug().Err(err).Msg("failed to close keyboard HID file")
		}
		u.keyboardHidFile = nil
	}

	// Close absolute mouse HID file
	if u.absMouseHidFile != nil {
		if err := u.absMouseHidFile.Close(); err != nil {
			u.log.Debug().Err(err).Msg("failed to close absolute mouse HID file")
		}
		u.absMouseHidFile = nil
	}

	// Close relative mouse HID file
	if u.relMouseHidFile != nil {
		if err := u.relMouseHidFile.Close(); err != nil {
			u.log.Debug().Err(err).Msg("failed to close relative mouse HID file")
		}
		u.relMouseHidFile = nil
	}
}

// PreOpenHidFiles opens all HID files to reduce input latency
func (u *UsbGadget) PreOpenHidFiles() {
	// Add a small delay to allow USB gadget reconfiguration to complete
	// This prevents "no such device or address" errors when trying to open HID files
	time.Sleep(100 * time.Millisecond)

	if u.enabledDevices.Keyboard {
		if err := u.openKeyboardHidFile(); err != nil {
			u.log.Debug().Err(err).Msg("failed to pre-open keyboard HID file")
		}
	}
	if u.enabledDevices.AbsoluteMouse {
		if u.absMouseHidFile == nil {
			var err error
			u.absMouseHidFile, err = os.OpenFile("/dev/hidg1", os.O_RDWR, 0666)
			if err != nil {
				u.log.Debug().Err(err).Msg("failed to pre-open absolute mouse HID file")
			}
		}
	}
	if u.enabledDevices.RelativeMouse {
		if u.relMouseHidFile == nil {
			var err error
			u.relMouseHidFile, err = os.OpenFile("/dev/hidg2", os.O_RDWR, 0666)
			if err != nil {
				u.log.Debug().Err(err).Msg("failed to pre-open relative mouse HID file")
			}
		}
	}
}

func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *UsbGadget {
	if logger == nil {
		logger = defaultLogger
	}

	if enabledDevices == nil {
		enabledDevices = &defaultUsbGadgetDevices
	}

	if config == nil {
		config = &Config{isEmpty: true}
	}

	keyboardCtx, keyboardCancel := context.WithCancel(context.Background())

	g := &UsbGadget{
		name:                name,
		kvmGadgetPath:       path.Join(gadgetPath, name),
		configC1Path:        path.Join(gadgetPath, name, "configs/c.1"),
		configMap:           configMap,
		customConfig:        *config,
		configLock:          sync.Mutex{},
		keyboardLock:        sync.Mutex{},
		absMouseLock:        sync.Mutex{},
		relMouseLock:        sync.Mutex{},
		txLock:              sync.Mutex{},
		keyboardStateCtx:    keyboardCtx,
		keyboardStateCancel: keyboardCancel,
		keyboardState:       0,
		keysDownState:       KeysDownState{Modifier: 0, Keys: []byte{0, 0, 0, 0, 0, 0}}, // must be initialized to hidKeyBufferSize (6) zero bytes
		enabledDevices:      *enabledDevices,
		lastUserInput:       time.Now(),
		log:                 logger,

		strictMode: config.strictMode,

		logSuppressionCounter: make(map[string]int),

		absMouseAccumulatedWheelY: 0,
	}
	if err := g.Init(); err != nil {
		logger.Error().Err(err).Msg("failed to init USB gadget")
		return nil
	}

	return g
}
