/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2019 Ha Thach (tinyusb.org)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * This file is part of the TinyUSB stack.
 */

/*
 * Copyright (c) 2021 hpmicro
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *
 */

/*---------------------------------------------------------------------*
 * Include
 *---------------------------------------------------------------------*/
#include "tusb_option.h"

#if TUSB_OPT_DEVICE_ENABLED && CFG_TUSB_MCU == OPT_MCU_HPM

#include "board.h"
#include "hpm_usb_device.h"
#include "common/tusb_common.h"
#include "device/dcd.h"

/*---------------------------------------------------------------------*
 * Macro Enum Declaration
 *---------------------------------------------------------------------*/
/* USBSTS, USBINTR */
enum {
    intr_usb         = HPM_BITSMASK(1, 0),
    intr_error       = HPM_BITSMASK(1, 1),
    intr_port_change = HPM_BITSMASK(1, 2),
    intr_reset       = HPM_BITSMASK(1, 6),
    intr_sof         = HPM_BITSMASK(1, 7),
    intr_suspend     = HPM_BITSMASK(1, 8),
    intr_nak         = HPM_BITSMASK(1, 16)
};

/*---------------------------------------------------------------------*
 * Macro Typedef Declaration
 *---------------------------------------------------------------------*/
typedef struct {
    USB_Type* regs;         /* register base*/
    const uint32_t irqnum;  /* IRQ number */
    const uint8_t ep_count; /* Max bi-directional Endpoints */
} dcd_controller_t;

/*---------------------------------------------------------------------*
 * Variable Definitions
 *---------------------------------------------------------------------*/
static const dcd_controller_t _dcd_controller[] =
{
    { .regs = (USB_Type*) HPM_USB0_BASE, .irqnum = IRQn_USB0, .ep_count = USB_SOC_DCD_MAX_ENDPOINT_COUNT },
    { .regs = (USB_Type*) HPM_USB1_BASE, .irqnum = IRQn_USB1, .ep_count = USB_SOC_DCD_MAX_ENDPOINT_COUNT }
};

ATTR_PLACE_AT_NONCACHEABLE static usb_device_handle_t usb_device_handle[USB_SOC_MAX_COUNT];

ATTR_PLACE_AT_NONCACHEABLE_WITH_ALIGNMENT(USB_SOC_DCD_DATA_RAM_ADDRESS_ALIGNMENT) static dcd_data_t _dcd_data;

/*---------------------------------------------------------------------*
 * Device API
 *---------------------------------------------------------------------*/

/* USB bus reset */
static void bus_reset(uint8_t rhport)
{
    usb_device_bus_reset(&usb_device_handle[rhport], CFG_TUD_ENDPOINT0_SIZE);
}

/* Initialize controller to device mode */
void dcd_init(uint8_t rhport)
{
    uint32_t int_mask;

    usb_device_handle[rhport].regs     = _dcd_controller[rhport].regs;
    usb_device_handle[rhport].dcd_data =  &_dcd_data;

    int_mask = (USB_USBINTR_UE_MASK | USB_USBINTR_UEE_MASK | USB_USBINTR_PCE_MASK | USB_USBINTR_URE_MASK

    #if (defined(USB_DEVICE_CONFIG_LOW_POWER_MODE) && (USB_DEVICE_CONFIG_LOW_POWER_MODE > 0U))
                    | USB_USBINTR_SLE_MASK
    #endif /* USB_DEVICE_CONFIG_LOW_POWER_MODE */
                    );

    usb_device_init(&usb_device_handle[rhport], int_mask);
}

/* De-initialize controller */
void dcd_deinit(uint8_t rhport)
{
    usb_device_deinit(&usb_device_handle[rhport]);
}

/* Enable device interrupt */
void dcd_int_enable(uint8_t rhport)
{
    intc_m_enable_irq(_dcd_controller[rhport].irqnum);
}

/* Disable device interrupt */
void dcd_int_disable(uint8_t rhport)
{
    intc_m_disable_irq(_dcd_controller[rhport].irqnum);
}

/* Receive Set Address request, mcu port must also include status IN response */
void dcd_set_address(uint8_t rhport, uint8_t dev_addr)
{
    usb_device_set_address(&usb_device_handle[rhport], dev_addr);
}

/* Wake up host */
void dcd_remote_wakeup(uint8_t rhport)
{
    usb_device_remote_wakeup(&usb_device_handle[rhport]);
}

/* Connect by enabling internal pull-up resistor on D+/D- */
void dcd_connect(uint8_t rhport)
{
    usb_device_connect(&usb_device_handle[rhport]);
}

/* Disconnect by disabling internal pull-up resistor on D+/D- */
void dcd_disconnect(uint8_t rhport)
{
    usb_device_disconnect(&usb_device_handle[rhport]);
}

/*---------------------------------------------------------------------*
 * Endpoint API
 *---------------------------------------------------------------------*/
/* Configure endpoint's registers according to descriptor */
bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc)
{
    usb_endpoint_config_t ep_cfg;

    ep_cfg.xfer            = p_endpoint_desc->bmAttributes.xfer;
    ep_cfg.ep_addr         = p_endpoint_desc->bEndpointAddress;
    ep_cfg.max_packet_size = p_endpoint_desc->wMaxPacketSize;

    return usb_device_edpt_open(&usb_device_handle[rhport], &ep_cfg);
}

/* Submit a transfer, When complete dcd_event_xfer_complete() is invoked to notify the stack */
bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t * buffer, uint16_t total_bytes)
{
    return usb_device_edpt_xfer(&usb_device_handle[rhport], ep_addr, buffer, total_bytes);
}

/* Stall endpoint */
void dcd_edpt_stall(uint8_t rhport, uint8_t ep_addr)
{
    usb_device_edpt_stall(&usb_device_handle[rhport], ep_addr);
}

/* clear stall, data toggle is also reset to DATA0 */
void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr)
{
    usb_device_edpt_clear_stall(&usb_device_handle[rhport], ep_addr);
}

/* close a specified endpoint */
void dcd_edpt_close(uint8_t rhport, uint8_t ep_addr)
{
    usb_device_edpt_close(&usb_device_handle[rhport], ep_addr);
}

/* Close all non-control endpoints, cancel all pending transfers if any */
void dcd_edpt_close_all(uint8_t rhport)
{
    usb_device_edpt_close_all(&usb_device_handle[rhport]);
}
/*---------------------------------------------------------------------*
 * Internal API
 *---------------------------------------------------------------------*/
/* Index to bit position in register */
static inline uint8_t ep_idx2bit(uint8_t ep_idx)
{
    return ep_idx / 2 + ((ep_idx % 2) ? 16 : 0);
}

/*---------------------------------------------------------------------*
 * ISR
 *---------------------------------------------------------------------*/
void dcd_int_handler(uint8_t rhport)
{
    uint32_t int_status;
    uint32_t speed;
    usb_device_handle_t *handle = &usb_device_handle[rhport];

    /* Acknowledge handled interrupt */
    int_status = usb_device_status_flags(handle);
    int_status &= usb_device_interrupts(handle);
    usb_device_clear_status_flags(handle, int_status);

    /* disabled interrupt sources */
    if (int_status == 0) {
        return;
    }

    if (int_status & intr_reset) {
        bus_reset(rhport);
        speed = usb_device_get_port_speed(handle);
        dcd_event_bus_reset(rhport, (tusb_speed_t)speed, true);
    }

    if (int_status & intr_suspend) {
         if (usb_device_get_suspend_status(handle)) {
            /* Note: Host may delay more than 3 ms before and/or after bus reset before doing enumeration. */
            if (usb_device_get_address(handle)) {
                dcd_event_bus_signal(rhport, DCD_EVENT_SUSPEND, true);
            }
        }
    }

    if (int_status & intr_port_change) {
        if (!usb_device_get_port_ccs(handle)) {
            dcd_event_t event = {.rhport = rhport, .event_id = DCD_EVENT_UNPLUGGED};
            dcd_event_handler(&event, true);
        } else {
            dcd_event_t event = {.rhport = rhport, .event_id = DCD_EVENT_PLUGGED};
            if (usb_device_get_port_reset_status(handle) == 0) {
                 uint32_t speed = usb_device_get_port_speed(handle);
                 event.plugged.speed = speed;
                 dcd_event_handler(&event, true);
            }
        }
    }

    if (int_status & intr_usb) {
        uint32_t const edpt_complete = usb_device_get_edpt_complete_status(handle);
        usb_device_clear_edpt_complete_status(handle, edpt_complete);

        uint32_t edpt_setup_status = usb_device_get_setup_status(handle);
        if (edpt_setup_status) {
            /*------------- Set up Received -------------*/
            usb_device_clear_setup_status(handle, edpt_setup_status);
            dcd_qhd_t *qhd0 = usb_device_qhd_get(&usb_device_handle[rhport], 0);
            dcd_event_setup_received(rhport, (uint8_t*) &qhd0->setup_request, true);
        }

        if (edpt_complete) {
            for(uint8_t ep_idx = 0; ep_idx < USB_SOS_DCD_MAX_QHD_COUNT; ep_idx++) {
                if (tu_bit_test(edpt_complete, ep_idx2bit(ep_idx))) {
                    /* Failed QTD also get ENDPTCOMPLETE set */
                    dcd_qtd_t *p_qtd = usb_device_qtd_get(&usb_device_handle[rhport], ep_idx);

                    uint8_t result = p_qtd->halted ? XFER_RESULT_STALLED :
                    (p_qtd->xact_err ||p_qtd->buffer_err) ? XFER_RESULT_FAILED : XFER_RESULT_SUCCESS;

                    uint8_t const ep_addr = (ep_idx/2) | ( (ep_idx & 0x01) ? TUSB_DIR_IN_MASK : 0);
                    /* only number of bytes in the IOC qtd */
                    dcd_event_xfer_complete(rhport, ep_addr, p_qtd->expected_bytes - p_qtd->total_bytes, result, true);
                }
            }
        }
    }

    if (int_status & intr_sof) {
        dcd_event_bus_signal(rhport, DCD_EVENT_SOF, true);
    }

    if (int_status & intr_nak) {}
    if (int_status & intr_error) TU_ASSERT(false, );
}

#endif
