/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2025 by Sho Tanimoto
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/
#include "audio.h"
#include "pcm_mixer.h"
#include "pcm_sink.h"
#include "playback.h"
#include "powermgmt.h"
#include "timefuncs.h"
#include "usb_core.h"
#include "usb_drv.h"
#include "panic.h"

#include "iap/audio.h"
#include "iap/libiap/iap.h"
#include "iap/libiap/platform.h"
#include "iap/macros.h"
#include "iap/platform-macros.h"
#include "iap/platform.h"
#include "usb_audio_def.h"
#include "usb_class_driver.h"
#include "usb_hid_def.h"
#include "usb_iap.h"

struct usb_class_driver_ep_allocation usb_iap_ep_allocs[2] = {
    /* uac input */
    {.type = USB_ENDPOINT_XFER_ISOC, .dir = DIR_IN, .optional = false, .mps = 1024},
    /* hid input */
    {.type = USB_ENDPOINT_XFER_INT, .dir = DIR_IN, .optional = false, .mps = 64},
};

/* interface 0 (audio control) */
static struct usb_interface_descriptor ipod_audio_control_desc = {
    .bLength            = USB_DT_INTERFACE_SIZE,
    .bDescriptorType    = USB_DT_INTERFACE,
    .bInterfaceNumber   = -1, /* dynamic */
    .bAlternateSetting  = 0,
    .bNumEndpoints      = 0,
    .bInterfaceClass    = USB_CLASS_AUDIO,
    .bInterfaceSubClass = USB_SUBCLASS_AUDIO_CONTROL,
};

static struct usb_ac_header ipod_audio_control_uac_header = {
    .bLength            = USB_AC_SIZEOF_HEADER(1),
    .bDescriptorType    = USB_DT_CS_INTERFACE,
    .bDescriptorSubType = USB_AC_HEADER,
    .bcdADC             = 0x0100, /* 1.00 */
    .wTotalLength       = -1,     /* dynamic */
    .bInCollection      = 1,
    .baInterfaceNr      = {-1}, /* dynamic */
};

static struct usb_ac_input_terminal ipod_audio_control_uac_input_terminal = {
    .bLength            = sizeof(struct usb_ac_input_terminal),
    .bDescriptorType    = USB_DT_CS_INTERFACE,
    .bDescriptorSubType = USB_AC_INPUT_TERMINAL,
    .bTerminalId        = 1,
    .wTerminalType      = USB_AC_INPUT_TERMINAL_MICROPHONE,
    .bAssocTerminal     = 2, /* ipod_audio_control_uac_output_terminal */
    .bNrChannels        = 2,
    .wChannelConfig     = USB_AC_CHANNELS_LEFT_RIGHT_FRONT,
};

static struct usb_ac_output_terminal ipod_audio_control_uac_output_terminal = {
    .bLength            = sizeof(struct usb_ac_output_terminal),
    .bDescriptorType    = USB_DT_CS_INTERFACE,
    .bDescriptorSubType = USB_AC_OUTPUT_TERMINAL,
    .bTerminalId        = 2,
    .wTerminalType      = USB_AC_TERMINAL_STREAMING,
    .bAssocTerminal     = 1, /* ipod_audio_control_uac_input_terminal */
    .bSourceId          = 1,
};

/* interface 1 (audio stream) */
/* interface 1 alt 0 */
static struct usb_interface_descriptor ipod_audio_stream_0_desc = {
    .bLength            = USB_DT_INTERFACE_SIZE,
    .bDescriptorType    = USB_DT_INTERFACE,
    .bInterfaceNumber   = -1, /* dynamic */
    .bAlternateSetting  = 0,
    .bNumEndpoints      = 0,
    .bInterfaceClass    = USB_CLASS_AUDIO,
    .bInterfaceSubClass = USB_SUBCLASS_AUDIO_STREAMING,
};

/* interface 1 alt 1 */
static struct usb_interface_descriptor ipod_audio_stream_1_desc = {
    .bLength            = USB_DT_INTERFACE_SIZE,
    .bDescriptorType    = USB_DT_INTERFACE,
    .bInterfaceNumber   = -1, /* dynamic */
    .bAlternateSetting  = 1,
    .bNumEndpoints      = 1,
    .bInterfaceClass    = USB_CLASS_AUDIO,
    .bInterfaceSubClass = USB_SUBCLASS_AUDIO_STREAMING,
};

static struct usb_as_interface ipod_audio_stream_1_uac_header = {
    .bLength            = sizeof(struct usb_as_interface),
    .bDescriptorType    = USB_DT_CS_INTERFACE,
    .bDescriptorSubType = USB_AS_GENERAL,
    .bTerminalLink      = 2, /* ipod_audio_control_uac_output_terminal */
    .bDelay             = 1,
    .wFormatTag         = USB_AS_FORMAT_TYPE_I_PCM,
};

/* TODO: remove unsupported freqs */
static struct usb_as_format_type_i_discrete ipod_audio_stream_1_uac_discrete = {
    .bLength            = USB_AS_SIZEOF_FORMAT_TYPE_I_DISCRETE(9),
    .bDescriptorType    = USB_DT_CS_INTERFACE,
    .bDescriptorSubType = USB_AS_FORMAT_TYPE,
    .bFormatType        = USB_AS_FORMAT_TYPE_I,
    .bNrChannels        = 2,
    .bSubframeSize      = 2, /* bBitResolution / 8 */
    .bBitResolution     = 16,
    .bSamFreqType       = 9,
    .tSamFreq           = {
        {0x40, 0x1F, 0x00}, /* 8000  */
        {0x11, 0x2B, 0x00}, /* 11025 */
        {0xE0, 0x2E, 0x00}, /* 12000 */
        {0x80, 0x3E, 0x00}, /* 16000 */
        {0x22, 0x56, 0x00}, /* 22050 */
        {0xC0, 0x5D, 0x00}, /* 24000 */
        {0x00, 0x7D, 0x00}, /* 32000 */
        {0x44, 0xAC, 0x00}, /* 44100 */
        {0x80, 0xBB, 0x00}, /* 48000 */
    },
};

/* interface 1 endpoint 0 */
static struct usb_as_iso_audio_endpoint ipod_audio_stream_1_endpoint = {
    .bLength          = USB_DT_ENDPOINT_AUDIO_SIZE,
    .bDescriptorType  = USB_DT_ENDPOINT,
    .bEndpointAddress = -1, /* dynamic */
    .bmAttributes     = USB_ENDPOINT_XFER_ISOC,
    .wMaxPacketSize   = AS_PACKET_SIZE,
    .bInterval        = -1, /* dynamic */
    .bRefresh         = 0,
    .bSynchAddress    = 0,
};

static struct usb_as_iso_ctrldata_endpoint ipod_audio_stream_1_endpoint_uac = {
    .bLength            = sizeof(struct usb_as_iso_ctrldata_endpoint),
    .bDescriptorType    = USB_DT_CS_ENDPOINT,
    .bDescriptorSubType = USB_AS_EP_GENERAL,
    .bmAttributes       = USB_AS_EP_CS_SAMPLING_FREQ_CTL,
    .bLockDelayUnits    = 0,
    .wLockDelay         = 0,
};

/* interface 2 (hid) */
static struct usb_interface_descriptor ipod_hid_desc = {
    .bLength            = USB_DT_INTERFACE_SIZE,
    .bDescriptorType    = USB_DT_INTERFACE,
    .bInterfaceNumber   = -1, /* dynamic */
    .bAlternateSetting  = 0,
    .bNumEndpoints      = 1,
    .bInterfaceClass    = USB_CLASS_HID,
    .bInterfaceSubClass = 0,
};

#define INPUT_REPORT(id, count) 0x09, 0x01,      /* Usage 0x01 */   \
                                0x85, id,        /* Report ID */    \
                                0x95, count,     /* Report Count */ \
                                0x82, 0x02, 0x01 /* Input 0x0102 (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Buffered Bytes) */

#define OUTPUT_REPORT(id, count) 0x09, 0x01,      /* Usage 0x01 */   \
                                 0x85, id,        /* Report ID */    \
                                 0x95, count,     /* Report Count */ \
                                 0x92, 0x02, 0x01 /* Output 0x0102 (...) */

#define INPUT_REPORT2(id, count1, count2) 0x09, 0x01,           /* Usage 0x01 */   \
                                          0x85, id,             /* Report ID */    \
                                          0x96, count1, count2, /* Report Count */ \
                                          0x82, 0x02, 0x01      /* Input 0x0102 (...) */

#define OUTPUT_REPORT2(id, count1, count2) 0x09, 0x01,           /* Usage 0x01 */   \
                                           0x85, id,             /* Report ID */    \
                                           0x96, count1, count2, /* Report Count */ \
                                           0x92, 0x02, 0x01      /* Output 0x0102 (...) */

// clang-format off
static const uint8_t ipod_hid_report_fs[] = {
    0x06, 0x00, 0xFF, /* Usage Page 0xFF00 (Vendor-defined) */
    0x09, 0x01,       /* Usage 0x01 */
    0xA1, 0x01,       /* Collection 0x01 (Application) */
    0x75, 0x08,       /* Report Size 0x08 */
    0x26, 0x80, 0x00, /* Logical Maximum 0x0081 (128) */
    0x15, 0x00,       /* Logical Minumum 0x0000 (0) */

    INPUT_REPORT(0x01, 0x0C),
    INPUT_REPORT(0x02, 0x0E),
    INPUT_REPORT(0x03, 0x14),
    INPUT_REPORT(0x04, 0x3F),

    OUTPUT_REPORT(0x05, 0x08),
    OUTPUT_REPORT(0x06, 0x0A),
    OUTPUT_REPORT(0x07, 0x0E),
    OUTPUT_REPORT(0x08, 0x14),
    OUTPUT_REPORT(0x09, 0x3F),

    0xC0, /* End Collection */
};

static const uint8_t ipod_hid_report_hs[] = {
    0x06, 0x00, 0xFF, /* Usage Page 0xFF00 (Vendor-defined) */
    0x09, 0x01,       /* Usage 0x01 */
    0xA1, 0x01,       /* Collection 0x01 (Application) */
    0x75, 0x08,       /* Report Size 0x08 */
    0x26, 0x80, 0x00, /* Logical Maximum 0x0081 (128) */
    0x15, 0x00,       /* Logical Minumum 0x0000 (0) */

    INPUT_REPORT(0x01, 0x05),
    INPUT_REPORT(0x02, 0x09),
    INPUT_REPORT(0x03, 0x0D),
    INPUT_REPORT(0x04, 0x11),
    INPUT_REPORT(0x05, 0x19),
    INPUT_REPORT(0x06, 0x31),
    INPUT_REPORT(0x07, 0x5F),
    INPUT_REPORT(0x08, 0xC1),
    INPUT_REPORT2(0x09, 0x01, 0x01),
    INPUT_REPORT2(0x0A, 0x81, 0x01),
    INPUT_REPORT2(0x0B, 0x01, 0x02),
    INPUT_REPORT2(0x0C, 0xFF, 0x02),

    OUTPUT_REPORT(0x0D, 0x05),
    OUTPUT_REPORT(0x0E, 0x09),
    OUTPUT_REPORT(0x1F, 0x0D),
    OUTPUT_REPORT(0x10, 0x11),
    OUTPUT_REPORT(0x11, 0x19),
    OUTPUT_REPORT(0x12, 0x31),
    OUTPUT_REPORT(0x13, 0x5F),
    OUTPUT_REPORT(0x14, 0xC1),
    OUTPUT_REPORT(0x15, 0xFF),

    0xC0, /* End Collection */
};
// clang-format on

static struct usb_hid_descriptor ipod_hid_hid_desc = {
    .bLength            = sizeof(struct usb_hid_descriptor),
    .bDescriptorType    = USB_DT_HID,
    .wBcdHID            = 0x0111, /* 1.11 */
    .bCountryCode       = 0,
    .bNumDescriptors    = 1,
    .bDescriptorType0   = USB_DT_REPORT,
    .wDescriptorLength0 = -1, /* dynamic */
};

/* interface 2 endpoint 0 */
static struct usb_endpoint_descriptor ipod_hid_endpoint = {
    .bLength          = USB_DT_ENDPOINT_SIZE,
    .bDescriptorType  = USB_DT_ENDPOINT,
    .bEndpointAddress = -1, /* dynamic */
    .bmAttributes     = USB_ENDPOINT_XFER_INT,
    .wMaxPacketSize   = -1, /* dynamic */
    .bInterval        = 1,
};

static struct {
    int interface;
} ctrl;

static struct {
    uint32_t sample_rate;
    int      interface;
    int8_t   alt;
} stream;

static struct {
    int interface;
} hid;

static struct mutex    iap_ctx_mutex;
static struct Platform platform;
static struct timeout  tick_tmo;
static bool            iap_ctx_mutex_initialized = false;

struct IAPContext* _iap_acquire_ctx(bool lock) {
    static struct IAPContext ctx;
    if(lock) {
        mutex_lock(&iap_ctx_mutex);
    }
    return &ctx;
}

void _iap_release_ctx() {
    mutex_unlock(&iap_ctx_mutex);
}

bool iap_initialized;

/* those notifications need pollling */
static enum charge_state_type last_charge_state;
static uint8_t                last_battery_level;
static int8_t                 last_minute;
static int8_t                 last_hold_switch_state;

enum Notify {
    Notify_Tick,
};

static int tick_callback(struct timeout* tmo) {
    (void)tmo;
    usb_signal_class_notify(USB_DRIVER_IAP, Notify_Tick);
    return HZ / 10;
}

static int usb_iap_set_first_interface(int interface) {
    ctrl.interface   = interface + 0;
    stream.interface = interface + 1;
    hid.interface    = interface + 2;
    return interface + 3;
}

#define PACK_DESC(desc) pack_data(&dest, &desc, ((struct usb_descriptor_header*)&desc)->bLength)

static int usb_iap_get_config_descriptor(unsigned char* dest, int max_packet_size) {
    (void)max_packet_size;

    unsigned char* orig_dest = dest;

    ipod_audio_control_desc.bInterfaceNumber = ctrl.interface;
    PACK_DESC(ipod_audio_control_desc);
    ipod_audio_control_uac_header.baInterfaceNr[0] = stream.interface;
    ipod_audio_control_uac_header.wTotalLength =
        sizeof(ipod_audio_control_uac_header) +
        sizeof(ipod_audio_control_uac_input_terminal) +
        sizeof(ipod_audio_control_uac_output_terminal);
    PACK_DESC(ipod_audio_control_uac_header);
    PACK_DESC(ipod_audio_control_uac_input_terminal);
    PACK_DESC(ipod_audio_control_uac_output_terminal);

    ipod_audio_stream_0_desc.bInterfaceNumber     = stream.interface;
    ipod_audio_stream_1_desc.bInterfaceNumber     = stream.interface;
    ipod_audio_stream_1_endpoint.bEndpointAddress = AS_EP_IN;
    ipod_audio_stream_1_endpoint.bInterval        = usb_drv_port_speed() ? 4 : 1;
    PACK_DESC(ipod_audio_stream_0_desc);
    PACK_DESC(ipod_audio_stream_1_desc);
    PACK_DESC(ipod_audio_stream_1_uac_header);
    PACK_DESC(ipod_audio_stream_1_uac_discrete);
    PACK_DESC(ipod_audio_stream_1_endpoint);
    PACK_DESC(ipod_audio_stream_1_endpoint_uac);

    ipod_hid_desc.bInterfaceNumber     = hid.interface;
    ipod_hid_endpoint.bEndpointAddress = HID_EP_IN;
    if(usb_drv_port_speed()) {
        ipod_hid_hid_desc.wDescriptorLength0 = sizeof(ipod_hid_report_hs);
        ipod_hid_endpoint.wMaxPacketSize     = 64;
    } else {
        ipod_hid_hid_desc.wDescriptorLength0 = sizeof(ipod_hid_report_fs);
        ipod_hid_endpoint.wMaxPacketSize     = 64;
    }
    PACK_DESC(ipod_hid_desc);
    PACK_DESC(ipod_hid_hid_desc);
    PACK_DESC(ipod_hid_endpoint);

    return dest - orig_dest;
}

static int usb_iap_init_connection(void) {
    stream.sample_rate     = 48000;
    last_charge_state      = -1;
    last_minute            = -1;
    last_hold_switch_state = -1;

    iap_debug_reset_timestamp();

    /* init audio sink */
    check_act(iap_audio_init(), return -1);

    /* init libiap */
    if(!iap_ctx_mutex_initialized) {
        iap_ctx_mutex_initialized = true;
        mutex_init(&iap_ctx_mutex);
    }

    struct IAPContext* ctx = _iap_acquire_ctx(true);

    const struct IAPOpts opts = {
        .usb_highspeed         = usb_drv_port_speed(),
        .ignore_hid_report_id  = iap_true,
        .artwork_single_report = iap_false,
        .enable_packet_dump    = iap_false,
    };
    check_act(iap_init_ctx(ctx, opts, &platform), goto cleanup_audio);
    _iap_release_ctx();

    /* prepare artwork */
    struct dim dim   = {IAP_ARTWORK_WIDTH, IAP_ARTWORK_HEIGHT};
    platform.aa_slot = playback_claim_aa_slot(&dim);
    if(platform.aa_slot < 0) {
        ERROR("failed to claim albumart slot");
    }
    platform.control_pending = false;

    /* register timer */
    timeout_register(&tick_tmo, tick_callback, HZ / 10, 0);

    iap_initialized = true;
    LOG("initialized");
    return 0;

cleanup_audio:
    _iap_release_ctx();
    iap_audio_deinit();
    return -1;
}

static int usb_iap_set_interface(int intf, int alt) {
    LOG("set interface interface=%d alt=%d", intf, alt);
    check_act(intf == stream.interface, return -1);
    if(alt == 0) {
        check_act(iap_audio_disable(), return -1);
    } else if(alt == 1) {
        check_act(iap_audio_enable(), return -1);
    } else {
        ERROR("invalid alt %d", alt);
        return -1;
    }
    return 0;
}

static int usb_iap_get_interface(int intf) {
    LOG("get interface interface=%d", intf);
    check_act(intf == stream.interface, return -1);
    return stream.alt;
}

static void usb_iap_init(void) {
    LOG("init");
}

static void usb_iap_disconnect(void) {
    iap_initialized = false;
    audio_pause();
    mixer_switch_sink(PCM_SINK_BUILTIN);
    timeout_cancel(&tick_tmo);
    if(platform.aa_slot >= 0) {
        playback_release_aa_slot(platform.aa_slot);
    }
    struct IAPContext* ctx = _iap_acquire_ctx(true);
    check_act(iap_deinit_ctx(ctx), );
    _iap_release_ctx();
    check_act(iap_audio_deinit(), );
    LOG("disconnected");
}

static void usb_iap_transfer_complete(int ep, int dir, int status, int length) {
    (void)length;

    if((ep | dir) == HID_EP_IN) {
        check_act(status == 0, return);
#if DEBUG_DUMP_TX
        LOG("ep=%d dir=%d state=%d length=%d", ep, dir, status, length);
#endif
        struct IAPContext* ctx = _iap_acquire_ctx(true);
        check_act(iap_notify_send_complete(ctx), );
        _iap_release_ctx();
    }
}

static bool usb_iap_fast_transfer_complete(int ep, int dir, int status, int length) {
    (void)status;
    (void)length;
    return (ep | dir) == AS_EP_IN;
}

static void respond_zero(struct usb_ctrlrequest* req, uint8_t* reqdata, size_t reqdata_size) {
    if(req->wLength > reqdata_size) {
        ERROR("required data too long %u > %u", req->wLength, reqdata_size);
        usb_core_control_response(USB_CONTROL_STALL, NULL, 0);
    } else {
        memset(reqdata, 0, req->wLength);
        usb_core_control_response(USB_CONTROL_ACK, reqdata, req->wLength);
    }
}

static bool control_request_if_std(struct usb_ctrlrequest* req, uint8_t* reqdata, size_t reqdata_size) {
    const void* src = NULL;
    size_t      size;
    switch(req->bRequest) {
    case USB_REQ_GET_DESCRIPTOR: {
        const uint8_t desc_type  = req->wValue >> 8;
        const uint8_t desc_index = req->wValue & 0xff;
        LOG("descriptor request type=%x index=%x", desc_type, desc_index);
        (void)desc_index;
        switch(desc_type) {
        case USB_DT_HID:
            src  = &ipod_hid_hid_desc;
            size = sizeof(ipod_hid_hid_desc);
            break;
        case USB_DT_REPORT:
            if(usb_drv_port_speed()) {
                src  = &ipod_hid_report_hs;
                size = sizeof(ipod_hid_report_hs);
            } else {
                src  = &ipod_hid_report_fs;
                size = sizeof(ipod_hid_report_fs);
            }
            break;
        }
        if(src != NULL) {
            if(size > reqdata_size) {
                LOG("descriptor size too large");
                usb_core_control_response(USB_CONTROL_STALL, NULL, 0);
            } else {
                size = MIN(size, req->wLength);
                memcpy(reqdata, src, size);
                usb_core_control_response(USB_CONTROL_ACK, reqdata, size);
            }
            return true;
        }
    } break;
    }
    return false;
}

static bool control_request_if_class(struct usb_ctrlrequest* req, uint8_t* reqdata, size_t reqdata_size) {
    const uint8_t recip_interface = req->wIndex & 0xff;
    if(recip_interface == hid.interface) {
        switch(req->bRequest) {
        case USB_HID_GET_REPORT:
            respond_zero(req, reqdata, reqdata_size);
            return true;
        case USB_HID_SET_REPORT: {
#if DEBUG_DUMP_RX == 1
            logf("==== acc: %u bytes ====", req->wLength);
            iap_platform_dump_hex(reqdata, req->wLength);
#endif

            struct IAPContext* ctx = _iap_acquire_ctx(true);
            const bool         ret = iap_feed_hid_report(ctx, reqdata, req->wLength);
            _iap_release_ctx();

            check_act(ret, return false);
            usb_core_control_response(USB_CONTROL_ACK, NULL, 0);
            return true;
        }
        case USB_HID_SET_IDLE:
            usb_core_control_response(USB_CONTROL_ACK, NULL, 0);
            return true;
        }
    }
    return false;
}

static bool control_request_if_endpoint(struct usb_ctrlrequest* req, uint8_t* reqdata, size_t reqdata_size) {
    LOG("ctrl to endpoint %x (stream=%x, hid=%x)", req->wIndex, AS_EP_IN, HID_EP_IN);
    if(req->wIndex == AS_EP_IN) {
        const uint8_t recip_entity     = req->wIndex >> 8;
        const uint8_t control_selector = req->wValue >> 8;
        (void)recip_entity;
        switch(req->bRequest) {
        case USB_AC_SET_CUR:
            LOG("audio ctrl to stream endpoint entity=0x%02X request=0x%02X length=%u", recip_entity, req->bRequest, req->wLength);
            switch(control_selector) {
            case USB_AS_EP_CS_SAMPLING_FREQ_CTL:
                check_act(req->wLength == 3, goto stall);
                stream.sample_rate = reqdata[0] | (reqdata[1] << 8) | (reqdata[2] << 16);
                LOG("audio stream sampling rate %lu", stream.sample_rate);
                check_act(iap_audio_set_sampr(stream.sample_rate), goto stall);
                break;
            }
            usb_core_control_response(USB_CONTROL_ACK, NULL, 0);
            return true;
        case USB_AC_GET_CUR:
            switch(control_selector) {
            case USB_AS_EP_CS_SAMPLING_FREQ_CTL:
                check_act(req->wLength == 3, goto stall);
                reqdata[2] = (stream.sample_rate >> 16) & 0xff;
                reqdata[1] = (stream.sample_rate >> 8) & 0xff;
                reqdata[0] = (stream.sample_rate & 0xff);
                usb_core_control_response(USB_CONTROL_ACK, reqdata, 3);
                return true;
            }
            /* fallthrough */
        case USB_AC_GET_MIN:
        case USB_AC_GET_MAX:
        case USB_AC_GET_RES:
            respond_zero(req, reqdata, reqdata_size);
            return true;
        stall:
            usb_core_control_response(USB_CONTROL_STALL, NULL, 0);
            return true;
        }
    }
    return false;
}

static bool usb_iap_control_request(struct usb_ctrlrequest* req, uint8_t* reqdata, size_t reqdata_size) {
    const uint8_t req_recipient = req->bRequestType & USB_RECIP_MASK;
    const uint8_t req_type      = req->bRequestType & USB_TYPE_MASK;
#if 0
    LOG("bRequestType=%x, bRequest=%x, wValue=%x, wIndex=%x, wLength=%x", req->bRequestType, req->bRequest, req->wValue, req->wIndex, req->wLength);
    LOG("recip=%x type=%x", req_recipient, req_type);
#endif
    if(req_recipient == USB_RECIP_INTERFACE && req_type == USB_TYPE_STANDARD) {
        return control_request_if_std(req, reqdata, reqdata_size);
    } else if(req_recipient == USB_RECIP_INTERFACE && req_type == USB_TYPE_CLASS) {
        return control_request_if_class(req, reqdata, reqdata_size);
    } else if(req_recipient == USB_RECIP_ENDPOINT && req_type == USB_TYPE_CLASS) {
        return control_request_if_endpoint(req, reqdata, reqdata_size);
    }
    return false;
}

static void usb_iap_notify_event(intptr_t data) {
    switch(data) {
    case Notify_Tick: {
        struct IAPContext* ctx = _iap_acquire_ctx(true);
        struct Platform*   plt = ctx->platform;
        if(plt->control_pending) {
            /* waiting for playback begins */
            _iap_release_ctx();
            return;
        }

        if(last_charge_state < 0 || last_charge_state != charge_state || last_battery_level != battery_level()) {
            last_charge_state  = charge_state;
            last_battery_level = battery_level();
            iap_notify_power_state(ctx, _iap_convert_charge_status(last_charge_state), _iap_convert_battery_level(last_battery_level));
        }

        struct tm* tm = get_time();
        if(last_minute == -1 || last_minute != tm->tm_min) {
            last_minute = tm->tm_min;
            struct IAPDateTime time;
            _iap_convert_datetime(get_time(), &time);
            iap_notify_time_setting(ctx, &time);
        }

#ifdef HAS_BUTTON_HOLD
        int8_t hold = button_hold() ? 1 : 0;
        if(hold != last_hold_switch_state) {
            last_hold_switch_state = hold;
            iap_notify_hold_switch_state(ctx, last_hold_switch_state);
        }
#endif

        check_act(iap_periodic_tick(ctx), );
        _iap_release_ctx();
    } break;
    }
}

struct usb_class_driver usb_cdrv_iap = {
    .needs_exclusive_storage = false,
    .needs_cpu_boost         = true,
    .config                  = 2,
    .ep_allocs_size          = ARRAYLEN(usb_iap_ep_allocs),
    .ep_allocs               = usb_iap_ep_allocs,
    .set_first_interface     = usb_iap_set_first_interface,
    .get_config_descriptor   = usb_iap_get_config_descriptor,
    .init_connection         = usb_iap_init_connection,
    .init                    = usb_iap_init,
    .disconnect              = usb_iap_disconnect,
    .transfer_complete       = usb_iap_transfer_complete,
    .fast_transfer_complete  = usb_iap_fast_transfer_complete,
    .control_request         = usb_iap_control_request,
    .set_interface           = usb_iap_set_interface,
    .get_interface           = usb_iap_get_interface,
    .notify_event            = usb_iap_notify_event,
};
