// SPDX-License-Identifier: Apache-2.0
#pragma once

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

#include <tactility/error.h>

#ifdef __cplusplus
extern "C" {
#endif

struct Device;
struct DeviceType;

// ---- USB device-mode (peripheral) controller ----

/**
 * Which USB device-mode class currently owns the single TinyUSB device-mode slot.
 * Only one class may be active at a time - see usb_device_controller_claim(). CDC is not part of
 * this enum: it's an addon that composites into whichever primary class is active (or stands
 * alone with none active) rather than a primary itself - see usb_cdc_device.h.
 */
enum UsbDeviceClass {
    USB_DEVICE_CLASS_NONE,
    USB_DEVICE_CLASS_MSC,
    USB_DEVICE_CLASS_HID_KEYBOARD,
    USB_DEVICE_CLASS_MIDI,
};

/**
 * A block of raw USB interface-descriptor bytes contributed by one class (MSC/HID/MIDI/CDC) to
 * the composite configuration descriptor the controller assembles at claim() time.
 *
 * Interface and endpoint numbers inside the descriptor bytes must already be renumbered by the
 * contributor to the base values the controller handed back via usb_device_controller_allocate_interfaces()
 * - the controller does not parse or patch the bytes, it only concatenates them behind the
 * config-descriptor header it writes itself.
 *
 * `hs_descriptor_bytes`/`hs_descriptor_bytes_len` are optional (leave both NULL/0 if this
 * contributor's bytes don't differ between full-speed and high-speed, e.g. HID/MIDI/CDC's
 * endpoint sizes are already speed-independent for their traffic) - only used when
 * TUD_OPT_HIGH_SPEED is set, mirroring the fs_/hs_configuration_descriptor split
 * tinyusb_config_t itself has (see MSC, whose bulk endpoint max-packet-size legitimately differs:
 * 64 bytes FS vs 512 bytes HS).
 */
struct UsbInterfaceContribution {
    const uint8_t* descriptor_bytes;   // one or more TUD_*_DESCRIPTOR blocks, back to back (full-speed)
    size_t descriptor_bytes_len;
    const uint8_t* hs_descriptor_bytes;    // optional high-speed variant; NULL to reuse descriptor_bytes
    size_t hs_descriptor_bytes_len;        // must equal descriptor_bytes_len (same interface/endpoint layout, only sizes differ)
    uint8_t interface_count;           // interfaces consumed (MSC/HID/CDC=1, MIDI=2)
    uint8_t in_endpoint_count;         // IN endpoints consumed (excluding EP0)
    uint8_t out_endpoint_count;        // OUT endpoints consumed
};

/**
 * Interface/endpoint numbers assigned to one contributor by the controller before it built its
 * descriptor bytes. Endpoint numbers are direction-local (IN and OUT each number from 1), as is
 * standard for USB - usb_device_controller_allocate_interfaces() hands out the next free *pair*
 * per direction, not a single shared counter.
 */
struct UsbInterfaceAllocation {
    uint8_t first_interface_number;
    uint8_t first_in_endpoint;   // e.g. 0x81, 0x82, ... or 0 if in_endpoint_count was 0
    uint8_t first_out_endpoint;  // e.g. 0x01, 0x02, ... or 0 if out_endpoint_count was 0
};

/**
 * A fully-described primary class descriptor plus device/string descriptor metadata, submitted
 * to claim(). CDC (if enabled) is appended by the controller itself - primary contributors never
 * see or reference CDC's interface numbers.
 */
struct UsbDeviceClaimConfig {
    // Actually a `tusb_desc_device_t*` (TinyUSB's device descriptor struct) - void* here so this
    // kernel header doesn't need to depend on TinyUSB's own headers; the controller and every
    // contributor already build against TinyUSB directly and cast accordingly. Mutable: the
    // controller patches the class triad (bDeviceClass/SubClass/Protocol) in place at claim()
    // time depending on whether CDC is composited in.
    void* device_descriptor;
    const char* const* string_descriptor;
    size_t string_descriptor_count;
    struct UsbInterfaceContribution primary; // MSC or HID or MIDI's contribution
};

/**
 * Shared owner of the TinyUSB device-mode (peripheral) stack. Exactly one primary USB device
 * class (mass storage, HID, MIDI) may be installed at a time - callers claim the slot before use
 * and release it when done. CDC is a separate, orthogonal addon (see usb_cdc_device.h) that the
 * controller composites into whichever primary is active, independent of the claim/release cycle.
 * This exists so multiple independent drivers (MSC, HID, MIDI, CDC) can share the single
 * underlying `tinyusb_driver_install()` call, composite descriptor assembly, and any
 * board-specific PHY routing without needing to know about each other.
 */
struct UsbDeviceControllerApi {
    /**
     * Starts a new interface/endpoint allocation session, resetting the numbering counters
     * allocate_interfaces() hands out. Call once, before building any descriptor bytes, as the
     * first step of a claim() attempt (i.e. before the primary class's own allocate_interfaces()
     * call) - claim() itself calls this again internally for CDC's allocation, so primary
     * contributors only need to call it for their own single call.
     *
     * There is no explicit abort/cancel: a session abandoned after begin_claim() (e.g. a
     * contributor's own allocate_interfaces() call fails and it returns early without calling
     * claim()) is simply reset by the next begin_claim() call, which unconditionally reinitializes
     * the allocation state regardless of whether the previous session ever finished.
     *
     * @param[in] device the USB device controller device
     * @retval ERROR_RESOURCE_BUSY if a different class already holds the slot
     * @retval ERROR_NONE on success
     */
    error_t (*begin_claim)(struct Device* device);

    /**
     * Ask the controller for the next free interface number and endpoint pair, before building
     * descriptor bytes. Call once per contributor (primary class, and CDC internally) per
     * begin_claim() session, in the order the resulting descriptor should list interfaces:
     * primary class first, then CDC (the controller enforces this order internally for CDC;
     * primary contributors just call this once for themselves, after begin_claim()).
     * @param[in] device the USB device controller device
     * @param[in] interface_count number of interfaces this contributor needs
     * @param[in] in_endpoint_count number of IN endpoints needed (0 if none)
     * @param[in] out_endpoint_count number of OUT endpoints needed (0 if none)
     * @param[out] out_allocation the assigned numbers
     * @retval ERROR_INVALID_STATE if called without a preceding begin_claim()
     * @retval ERROR_NONE on success
     */
    error_t (*allocate_interfaces)(struct Device* device, uint8_t interface_count,
                                    uint8_t in_endpoint_count, uint8_t out_endpoint_count,
                                    struct UsbInterfaceAllocation* out_allocation);

    /**
     * Claim the USB device-mode slot for the given primary class and install the composite
     * descriptor (primary + CDC, if the board's usbdevicecdc0 child is enabled). Must be called
     * after begin_claim() and the primary's own allocate_interfaces() call, using the same
     * device-mode session (no other claim()/begin_claim() calls in between).
     * @param[in] device the USB device controller device
     * @param[in] usb_class the class to claim the slot for
     * @param[in] config the primary class's descriptor contribution and metadata
     * @retval ERROR_RESOURCE_BUSY if a different class already holds the slot
     * @retval ERROR_NONE on success
     */
    error_t (*claim)(struct Device* device, enum UsbDeviceClass usb_class,
                      const struct UsbDeviceClaimConfig* config);

    /**
     * Release the USB device-mode slot. Stops the CDC console (if it was composited in),
     * disconnects from the host, uninstalls the TinyUSB driver, and restores any board-specific
     * PHY routing. Only the current holder may release.
     * @param[in] device the USB device controller device
     * @param[in] usb_class the class releasing the slot; must match the current holder
     * @retval ERROR_INVALID_STATE if usb_class does not hold the slot
     * @retval ERROR_NONE on success
     */
    error_t (*release)(struct Device* device, enum UsbDeviceClass usb_class);

    /**
     * @param[in] device the USB device controller device
     * @return the class currently holding the slot, or USB_DEVICE_CLASS_NONE if free
     */
    enum UsbDeviceClass (*get_active_class)(struct Device* device);

    /**
     * @param[in] device the USB device controller device
     * @return true if a usbdevicecdc0 child device is present and enabled on this board
     */
    bool (*is_cdc_enabled)(struct Device* device);
};

extern const struct DeviceType USB_DEVICE_CONTROLLER_TYPE;

/**
 * Find the first started USB device controller and take a reference on it.
 * @return the device with an outstanding reference, or NULL if none is available - caller must
 *         call device_put() exactly once when done, same as device_get_first_active_by_type().
 */
struct Device* usb_device_controller_get(void);

error_t usb_device_controller_begin_claim(struct Device* device);
error_t usb_device_controller_allocate_interfaces(struct Device* device, uint8_t interface_count,
                                                   uint8_t in_endpoint_count, uint8_t out_endpoint_count,
                                                   struct UsbInterfaceAllocation* out_allocation);
error_t usb_device_controller_claim(struct Device* device, enum UsbDeviceClass usb_class,
                                     const struct UsbDeviceClaimConfig* config);
error_t usb_device_controller_release(struct Device* device, enum UsbDeviceClass usb_class);
enum UsbDeviceClass usb_device_controller_get_active_class(struct Device* device);
bool usb_device_controller_is_cdc_enabled(struct Device* device);

#ifdef __cplusplus
}
#endif
