#include "include.h"
#include "usb_com.h"
#include "usb_hid.h"
#include "usb_hid_desc.h"

#if USB_HID_EN

#define TRACE_RATE_EN                       0
#define TRACE_RATE_PERIOD                   1000    // Unit: sof

#define HID_RECV_PKS_NUM                    5

#define HID_RESUME_PREVENT_TIME             1000    // Unit: ms

udh_t udh_0 AT(.usb_buf.usb);
epcb_t ep_hid_tx AT(.usb_buf.usb);
epcb_t ep_hid_rx AT(.usb_buf.usb);

u8 ep_hid_buf_out[HID_EP_OUT_MAX_SIZE + 4] AT(.usb_buf.hid);          // CRC
u8 ep_hid_buf_in[HID_EP_IN_MAX_SIZE] AT(.usb_buf.hid);

u32 tick_resume_prevent AT(.bss.usb_buf.hid);

#if TRACE_RATE_EN
static u32 statistic_sof_count;
static u32 statistic_tx_done_count;
#endif

static struct {
    uint8_t         ridx;
    uint8_t         widx;
    uint16_t        cnt;
    uint8_t         data[HID_EP_IN_MAX_SIZE * HID_RECV_PKS_NUM];
} hid_recv_cb AT(.usb_buf.hid);

#define usb_hid_is_valid()                  (udh_0.valid == true)
#define usb_hid_resume_prevent_en()         {tick_resume_prevent = tick_get();}
#define usb_hid_resume_prevent_dis()        {tick_resume_prevent = 0;}
#define usb_hid_resume_prevent_chk()        (!tick_resume_prevent || !tick_check_expire(tick_resume_prevent, HID_RESUME_PREVENT_TIME))

AT(.com_text.hid.mouse)
static bool hid_recv_queue_push(uint8_t *data)
{
    GLOBAL_INT_DISABLE();

    bool status   = false;
    uint8_t *wptr = &hid_recv_cb.data[hid_recv_cb.widx * HID_EP_IN_MAX_SIZE];

    if (hid_recv_cb.cnt < HID_RECV_PKS_NUM) {
        memcpy(wptr, data, HID_EP_IN_MAX_SIZE);
        hid_recv_cb.widx = (hid_recv_cb.widx + 1) % HID_RECV_PKS_NUM;
        hid_recv_cb.cnt++;
        status = true;
    }

    GLOBAL_INT_RESTORE();

    return (status);
}

AT(.com_text.hid.mouse)
static bool hid_recv_queue_pop(uint8_t *data)
{
    GLOBAL_INT_DISABLE();

    bool status   = false;
    uint8_t *rptr = &hid_recv_cb.data[hid_recv_cb.ridx * HID_EP_IN_MAX_SIZE];

    if (hid_recv_cb.cnt) {
        memcpy(data, rptr, HID_EP_IN_MAX_SIZE);
        hid_recv_cb.ridx = (hid_recv_cb.ridx + 1) % HID_RECV_PKS_NUM;
        hid_recv_cb.cnt--;
        status = true;
    }

    GLOBAL_INT_RESTORE();

    return (status);
}

AT(.com_text.hid.mouse)
static void hid_recv_queue_clear(void)
{
    GLOBAL_INT_DISABLE();
    hid_recv_cb.ridx = 0;
    hid_recv_cb.widx = 0;
    hid_recv_cb.cnt  = 0;
    GLOBAL_INT_RESTORE();
}

void ude_hid_setvalid(bool valid)
{
    udh_t *udh = &udh_0;
    udh->valid = valid;
}

AT(.usbdev.com)
void ude_hid_rx_process(void)
{
    u16 size;
    udh_t *udh = &udh_0;

    if (udh->valid) {
        size = usb_ep_get_rx_len(udh->int_out);

        usb_hid_receive_callback(udh->int_out->buf, size);

        usb_ep_clear_rx_fifo(udh->int_out);
    }
}

AT(.usbdev.com)
void ude_hid_tx_process(void)
{
    usb_hid_send_kick();

#if TRACE_RATE_EN
    if (statistic_sof_count < (TRACE_RATE_PERIOD + 1)) {
        statistic_tx_done_count++;
    }
#endif
}

AT(.com_text.isr.suspend)
void usb_hid_suspend_callback(bool suspend)
{
    if (suspend) {
        hid_recv_queue_clear();
        usb_hid_resume_prevent_en();
    } else {
        usb_hid_resume_prevent_dis();
    }
}

AT(.com_text.isr.sof)
void usb_sof_hid_isr(void)
{
#if TRACE_RATE_EN
    if (statistic_sof_count < (TRACE_RATE_PERIOD + 1)) {
        statistic_sof_count++;
    }
#endif
}

///USB hid endpoint reset
void usb_int_ep_reset(void)
{
    udh_t *udh = &udh_0;

    /* Any usb class can't always use ep0, so it't un-init when index is zere */

    if (udh->int_in->index) {
        usb_ep_reset(udh->int_in);
    }

    if (udh->int_out->index) {
        usb_ep_reset(udh->int_out);
    }
}

///USB HID 初始化
void udh_init(void)
{
    epcb_t *epcb;
    udh_t *udh = &udh_0;

    memset(&udh_0, 0, sizeof(udh_0));

    udh_0.int_in = &ep_hid_tx;
    udh_0.int_out = &ep_hid_rx;

    memset(udh->int_in, 0x00, sizeof(epcb_t));
    memset(udh->int_out, 0x00, sizeof(epcb_t));

    usb_ep_callback_register(ude_hid_tx_process, USB_HID_EP_IN_INDEX, EP_DIR_IN);
    usb_ep_callback_register(ude_hid_rx_process, USB_HID_EP_OUT_INDEX, EP_DIR_OUT);

    memset(ep_hid_buf_in,  0, sizeof(ep_hid_buf_in));
    epcb = udh->int_in;
    epcb->dir = EP_DIR_IN;
    epcb->index = USB_HID_EP_IN_INDEX;
    epcb->type = EP_TYPE_INTR;
    epcb->epsize = HID_EP_IN_MAX_SIZE;
    epcb->buf = ep_hid_buf_in;
    usb_ep_init(epcb);

    memset(ep_hid_buf_out,  0, sizeof(ep_hid_buf_out));
    epcb = udh->int_out;
    epcb->dir = EP_DIR_OUT;
    epcb->index = USB_HID_EP_OUT_INDEX;
    epcb->type = EP_TYPE_INTR;
    epcb->epsize = HID_EP_OUT_MAX_SIZE;
    epcb->buf = ep_hid_buf_out;
    usb_ep_init(epcb);

    ude_hid_setvalid(false);
}

/* START: API for upper layer */

WEAK void usb_hid_receive_callback(uint8_t *buf, uint32_t size)
{
    hid_rep_info_t *hid_rep_info = (hid_rep_info_t *)buf;

    printf("hid data out[%d]:\n", size);
    print_r(buf, size);

    if (hid_rep_info->report_id == HID_REP_ID_LED) {
        u8 caps_lock[1] = {(hid_rep_info->pdu.led.caps_lock)? 1: 0};
        wireless_send_ctrl_cmd(WIRELESS_CTRL_CMD_CODE_CAPS_LOCK_CHANGE, 1, caps_lock);
    }
}

AT(.com_text.usb.kick)
void usb_hid_send_kick(void)
{
    udh_t *udh = &udh_0;
    u32 size = 0;

    if (!udh->valid) {
        return;
    }

    if (usb_device_is_suspend()) {
        if (false == usb_hid_resume_prevent_chk()) {
            usb_hid_resume_prevent_dis();
            usb_device_resume();
        }
        return;
    }

    if (usb_ep_transfer(udh->int_in)) {
        usb_hid_send_prepare(udh->int_in->buf, &size);
        if (size) {
            usb_ep_start_transfer(udh->int_in, size);
        }
    }
}

AT(.com_text.usb.hid)
bool usb_hid_buffer_push(uint8_t *buf, uint32_t size)
{
    if (!usb_hid_is_valid() || usb_device_is_suspend()) {
        return false;
    }

    if ((size % HID_EP_IN_MAX_SIZE) != 0) {
        return false;
    }

    while (size && hid_recv_queue_push(buf)) {
        size -= HID_EP_IN_MAX_SIZE;
        buf += HID_EP_IN_MAX_SIZE;
    }

    // ASSERT: all data can push in queue
    if (size) {
    }

    return true;
}

AT(.com_text.usb.hid)
bool usb_hid_buffer_push_check(void)
{
    return (hid_recv_cb.cnt < HID_RECV_PKS_NUM);
}

AT(.com_text.usb.hid)
void usb_hid_send_prepare(uint8_t *buf, uint32_t *size)
{
    if (sys_cb.usb_is_active && hid_recv_queue_pop(buf)) {
        *size = HID_EP_IN_MAX_SIZE;
    } else {
        *size = 0;
    }
}

void usb_hid_init(void)
{
    memset(&hid_recv_cb, 0x00, sizeof(hid_recv_cb));
}

void usb_hid_deinit(void)
{
    ude_hid_setvalid(false);
}

#if TRACE_RATE_EN
AT(.com_text.test)
void usb_hid_statistic_flush(void)
{
    GLOBAL_INT_DISABLE();
    statistic_sof_count = 0;
    statistic_tx_done_count = 0;
    GLOBAL_INT_RESTORE();
}
#endif

void usb_hid_process(void)
{
#if TRACE_RATE_EN
    if (statistic_sof_count > TRACE_RATE_PERIOD)
    {
        printf("[USB]: %din/%dsof\n", statistic_tx_done_count, TRACE_RATE_PERIOD);
        usb_hid_statistic_flush();
    }
#endif
}

#endif // USB_HID_EN
