// #define ENABLE_DBG_LOG

#include <string.h>
#include "cmsis_os2.h"
#include "kmdw_power_manager.h"

#include "kmdw_console.h"
#include "kdrv_power.h"
#include "kdrv_scu_ext.h"
#include "kdp_system.h"

#include "usbd_hal.h"

#include "buffer_object.h"

#include "kmdw_fifoq_manager.h"
#include "kdp2_usb_companion.h"
#include "kdp2_ipc_cmd.h"

#ifdef ENABLE_DBG_LOG
#define dbg_log(__format__, ...) kmdw_printf("[kp companion]"__format__, ##__VA_ARGS__)
#else
#define dbg_log(__format__, ...)
#endif

#define FLAG_WAIT_USB_CONNECTION 0x1

static osThreadId_t result_thread_id = NULL;
static osThreadId_t image_thread_id = NULL;

static bool _do_reset_queue = false;
static bool _enable_inf_droppable = false;

static bool allocate_memory_for_inference_queue(uint32_t image_count, uint32_t image_size, uint32_t result_count, uint32_t result_size)
{
    if (true == kmdw_fifoq_manager_get_fifoq_allocated())
        return false; // already inited

    uint32_t total_need_size = (image_count * image_size) + (result_count * result_size);

    kmdw_printf("allocating memory for fifoq: image %d x %d, result %d x %d, total %u bytes\n", image_count, image_size, result_count, result_size, total_need_size);

    uint32_t buf_addr = kmdw_ddr_reserve(total_need_size);
    if (buf_addr > 0)
    {
        dbg_log("allocated fifoq buffers OK\n");

        kmdw_fifoq_manager_store_fifoq_config(image_count, image_size, result_count, result_size);

        osStatus_t sts;

        // queue image and result buffers into queues correspondingly
        for (uint32_t i = 0; i < image_count; i++)
        {
            sts = kmdw_fifoq_manager_image_put_free_buffer(buf_addr, (int)image_size, 0);
            if (sts != osOK)
            {
                dbg_log("kmdw_fifoq_manager_image_put_free_buffer error = %d\n", sts);
            }

            buf_addr += image_size;
        }
        for (uint32_t i = 0; i < result_count; i++)
        {
            sts = kmdw_fifoq_manager_result_put_free_buffer(buf_addr, (int)result_size, 0);
            if (sts != osOK)
            {
                dbg_log("kmdw_fifoq_manager_image_put_free_buffer error = %d\n", sts);
            }

            buf_addr += result_size;
        }

        return true;
    }
    else
    {
        kmdw_printf("error ! not enough memory for inference queue buffers\n");
        return false;
    }
}

// usb link status notify
void usb_link_status_callback(usbd_hal_link_status_t link_status)
{
    switch (link_status)
    {
    case USBD_STATUS_DISCONNECTED:
        kmdw_printf("USB is disconnected\n");
        break;

    case USBD_STATUS_CONFIGURED:
        kmdw_printf("USB is connected\n");
        osThreadFlagsSet(image_thread_id, FLAG_WAIT_USB_CONNECTION);
        break;
    }
}

// vendor-specific control transfer setup packet notify
static bool usb_user_control_callback(usbd_hal_setup_packet_t *setup)
{
    bool ret = false;

    dbg_log("control bRequest = 0x%x\n", setup->bRequest);

    switch (setup->bRequest)
    {
    case KDP2_CONTROL_REBOOT:
    {
        dbg_log("control reboot\n");
        kdrv_power_sw_reset();
        break;
    }
    case KDP2_CONTROL_SHUTDOWN:
    {
        dbg_log("control shutdown\n");
        kmdw_power_manager_shutdown();
        break;
    }
    case KDP2_CONTROL_FIFOQ_RESET:
    {
        dbg_log("control fifoq reset\n");

        if (true == kmdw_fifoq_manager_get_fifoq_allocated())
        {
            _do_reset_queue = true;

            // also reset any inf defaults
            _enable_inf_droppable = false;

            usbd_hal_terminate_all_endpoint();
        }

        ret = true;
        break;
    }
    case KDP2_CONTROL_FIFOQ_CONFIGURE:
    {
        if (true == kmdw_fifoq_manager_get_fifoq_allocated())
            break; // already inited

        uint16_t arg1_image = setup->wValue;
        uint16_t arg2_result = setup->wIndex;

        uint32_t image_count = (arg1_image & 0x7) + 1;                         // lower 3 bits for number of image, 1~8
        uint32_t image_size = (10 * 1024) * (uint32_t)((arg1_image >> 3) + 1); // higher 13 bits for image buffer size in 10KB, 10KB~80MB

        uint32_t result_count = (arg2_result & 0x7) + 1;                         // lower 3 bits for number of image, 1~8
        uint32_t result_size = (10 * 1024) * (uint32_t)((arg2_result >> 3) + 1); // higher 13 bits for image buffer size in 10KB, 10KB~80MB

        ret = allocate_memory_for_inference_queue(image_count, image_size, result_count, result_size);

        break;
    }
    case KDP2_CONTROL_FIFOQ_ENABLE_DROPPABLE:
    {
        _enable_inf_droppable = (setup->wValue == 1);
        ret = true;
        break;
    }
    case KDP2_CONTROL_DDR_HEAP_BOUNDARY_ADJUST:
    {
        uint32_t arg1 = (uint32_t)setup->wValue;
        uint32_t arg2 = (uint32_t)setup->wIndex;
        uint32_t boundary_addr = (arg1 << 16) | arg2;

        ret = (0 == kmdw_ddr_set_ddr_boundary(boundary_addr));
        break;
    }
    case KDP2_CONTROL_REBOOT_SYSTEM:
    {
        dbg_log("control reboot system\n");
        kdrv_power_sw_reset();
        break;
    }

    default:
        ret = false;
        break;
    }

    return ret;
}

static void process_plus_command(uint32_t cmd_buf)
{
    kp_inference_header_stamp_t *header_stamp = (kp_inference_header_stamp_t *)cmd_buf;

    if (header_stamp->magic_type == KDP2_MAGIC_TYPE_COMMAND)
    {
        dbg_log("handle kdp2 command = 0x%x\n", header_stamp->job_id);
        // handle kdp2 commands ...
        kdp2_cmd_handle_kp_command(cmd_buf);
    }
    else if ((header_stamp->magic_type & 0xFFFF) == KDP_MSG_HDR_CMD) // very speical case for old arch. fw update
    {
        // handle legendary kdp commands, should be as few as possible
        dbg_log("handle legendary kdp command = 0x%x\n", header_stamp->job_id);
        kdp2_cmd_handle_legend_kdp_command(cmd_buf);
    }
    else
    {
        dbg_log("[%s] error ! received un-recognized buffer beginning with incorrect magic_type 0x%x\n", __FUNCTION__, header_stamp->magic_type);
    }
}

enum
{
    RECV_TYPE_INF_IMAGE = 1,
    RECV_TYPE_COMMAND = 2,
    RECV_TYPE_UNKNOWN = 3,
};

void kdp2_usb_companion_image_thread(void *arg)
{
    dbg_log("[%s] starting ..\n", __FUNCTION__);

    uint32_t temp_cmd_buffer = 0;
    uint32_t temp_cmd_buffer_size = 0;

    kmdw_ddr_get_system_reserve(&temp_cmd_buffer, &temp_cmd_buffer_size);

    // this is due to "static thread allocation", refer to task_handler.h
    image_thread_id = osThreadGetId();
    if (image_thread_id == NULL)
        kmdw_printf("%s creation failed !\n", __FUNCTION__);

    // wait until usb connection is established
    osThreadFlagsWait(FLAG_WAIT_USB_CONNECTION, osFlagsWaitAny, osWaitForever);

    // run infinitely
    while (1)
    {
        uint32_t buf_addr; // contains a inference image or a command
        int buf_size;      // buffer size should bigger than inference image size

        // take a free buffer to receive a inf image or a command
        if (true == kmdw_fifoq_manager_get_fifoq_allocated()) {
            osStatus_t sts = kmdw_fifoq_manager_image_get_free_buffer(&buf_addr, &buf_size, osWaitForever, _enable_inf_droppable);

            while (_enable_inf_droppable && osErrorResource == sts)
            {
                sts = kmdw_fifoq_manager_image_get_free_buffer(&buf_addr, &buf_size, osWaitForever, _enable_inf_droppable);
            }
        } else {
            buf_addr = temp_cmd_buffer;
            buf_size = temp_cmd_buffer_size;
            dbg_log("[%s] got system reserve buffer: 0x%X size %d\n", __FUNCTION__, buf_addr, buf_size);
        }

        dbg_log("[%s] free queue --> buf 0x%X size %d\n", __FUNCTION__, buf_addr, buf_size);

        uint32_t total_recv_len = 0;
        uint32_t total_wanted_len = 0;
        int recv_type = RECV_TYPE_UNKNOWN;
        uint32_t total_image_count = 0;
        uint32_t image_index = 0;

        // loop done when receiving size-matched data
        while (1)
        {
            uint32_t txLen = buf_size - total_recv_len;
            kdrv_status_t usb_sts = usbd_hal_bulk_receive(KDP2_USB_ENDPOINT_DATA_OUT, (uint32_t *)(buf_addr + total_recv_len), &txLen, osWaitForever);
            if (usb_sts != KDRV_STATUS_OK) // KDRV_STATUS_USBD_TRANSFER_TERMINATED or KDRV_STATUS_USBD_TRANSFER_DISCONNECTED
            {
                dbg_log("[%s] bulk receive is terminated, sts %d\n", __FUNCTION__, usb_sts);

                // guess this is good time to clear/reset queue
                if (_do_reset_queue)
                {
                    dbg_log("[%s] do reset fifo queue !!!\n", __FUNCTION__);

                    _do_reset_queue = false;

                    // abandon all unprocessed data
                    kmdw_fifoq_manager_clean_queues();

                    // sending enpoint may still hold a buffer, terminate it
                    usbd_hal_terminate_all_endpoint();

                    recv_type = RECV_TYPE_UNKNOWN;

                    break;
                }
            }

            if (total_recv_len == 0) // buffer should begin with header stamp 'magic_type'
            {
                kp_inference_header_stamp_t *header_stamp = (kp_inference_header_stamp_t *)buf_addr;

                if (header_stamp->magic_type == KDP2_MAGIC_TYPE_INFERENCE)
                {
                    recv_type = RECV_TYPE_INF_IMAGE; // indicate this buffer contains inf image
                    total_wanted_len = header_stamp->total_size;
                    total_image_count = header_stamp->total_image;
                    image_index = header_stamp->image_index;

                    if ((buf_addr == temp_cmd_buffer) && (true == kmdw_fifoq_manager_get_fifoq_allocated())) {
                        osStatus_t sts = kmdw_fifoq_manager_image_get_free_buffer(&buf_addr, &buf_size, osWaitForever, _enable_inf_droppable);

                        while (_enable_inf_droppable && osErrorResource == sts) {
                            sts = kmdw_fifoq_manager_image_get_free_buffer(&buf_addr, &buf_size, osWaitForever, _enable_inf_droppable);
                        }

                        memcpy((void *)buf_addr, (void *)temp_cmd_buffer, txLen);

                        dbg_log("[%s] swap data from system reserve buffer to fifoq buffer: 0x%X\n", __FUNCTION__, buf_addr);
                    }

                    if (header_stamp->total_size > buf_size) {
                        // FIXME, serious error, make host SW pending (timeout)
                        // reboot and configure bigger buffer !!!!
                        kmdw_printf("[%s] error !! inf image size (%d) is bigger than buffer size (%d)\n", __FUNCTION__, total_wanted_len, buf_size);
                        return; // a way to inform host SW ?
                    }
                }
                else
                {
                    recv_type = RECV_TYPE_COMMAND; // indicate this buffer contains a command (if no failuire)
                    total_wanted_len = txLen;      // assume this is a command (or not if failure)
                }
            }
            else
            {
                // extra check for inference image data
                kp_inference_header_stamp_t *header_stamp = (kp_inference_header_stamp_t *)(buf_addr + total_recv_len);
                if (header_stamp->magic_type == KDP2_MAGIC_TYPE_COMMAND ||
                    header_stamp->magic_type == KDP2_MAGIC_TYPE_COMMAND)
                {
                    // this should not happen
                    kmdw_printf("[%s] error !! receiving inf image corrupted ! start over.\n", __FUNCTION__);
                    recv_type = RECV_TYPE_UNKNOWN;
                }
            }

            total_recv_len += txLen;

            dbg_log("[%s] host -- usb --> buf 0x%x len %d (total %d)\n", __FUNCTION__, (void *)buf_addr, txLen, total_recv_len);

            if (total_recv_len == total_wanted_len)
                break; // recv done
            else if (total_recv_len > total_wanted_len)
            {
                kmdw_printf("[%s] warning !! actual received size (%d) is bigger than expected size (%d)\n", __FUNCTION__, total_recv_len, total_wanted_len);
                break;
            }
        }

        if (recv_type == RECV_TYPE_INF_IMAGE)
        {
            dbg_log("[%s] buf 0x%x -- > inference queue\n", __FUNCTION__, (void *)buf_addr);
            kmdw_fifoq_manager_image_enqueue(total_image_count, image_index, buf_addr, buf_size, osWaitForever, false);
        }
        else if (recv_type == RECV_TYPE_COMMAND)
        {
            dbg_log("[%s] buf 0x%x -- > command handler\n", __FUNCTION__, (void *)buf_addr);
            process_plus_command(buf_addr);
            // as the buffer is not put into inference queue, return it back to free buffer queue
            if (temp_cmd_buffer != buf_addr)
                kmdw_fifoq_manager_image_put_free_buffer(buf_addr, buf_size, osWaitForever);
        }
        else
        {
            // invalid data !! ignore it !
            dbg_log("[%s] XXXXXX buf 0x%x -- > free buffer queue\n", __FUNCTION__, (void *)buf_addr);
            if (temp_cmd_buffer != buf_addr)
                kmdw_fifoq_manager_image_put_free_buffer(buf_addr, buf_size, osWaitForever);
        }
    }
}

void kdp2_usb_companion_result_thread(void *arg)
{
    bool bRunning_dbg = false;

    dbg_log("[%s] starting ..\n", __FUNCTION__);

    result_thread_id = osThreadGetId();
    if (result_thread_id == NULL)
        kmdw_printf("%s creation failed !\n", __FUNCTION__);

    while (1)
    {
        uint32_t buf_addr;
        int buf_size;

        // get result data from queue blocking wait
        kmdw_fifoq_manager_result_dequeue(&buf_addr, &buf_size, osWaitForever);

        kp_inference_header_stamp_t *header_stamp = (kp_inference_header_stamp_t *)buf_addr;

        dbg_log("[%s] buf 0x%x len %d -- usb --> host\n", __FUNCTION__, (void *)buf_addr, header_stamp->total_size);

        // kp inference debug code
        if(bRunning_dbg && header_stamp->magic_type != KDP2_MAGIC_TYPE_CHECKPOINT_DATA)
        {
            // send a signal notify the end of dbg process
            kp_inference_header_stamp_t dbg_ending;
            dbg_ending.magic_type = KDP2_MAGIC_TYPE_CHECKPOINT_DATA;
            dbg_ending.total_size = sizeof(kp_inference_header_stamp_t);
            dbg_ending.job_id = 0;
            dbg_ending.status_code = KP_SUCCESS;
            usbd_hal_bulk_send(KDP2_USB_ENDPOINT_DATA_IN, (void *)&dbg_ending, dbg_ending.total_size, osWaitForever);
            bRunning_dbg = false;
        }

        // send result to the host, blocking wait
        kdrv_status_t usb_sts = usbd_hal_bulk_send(KDP2_USB_ENDPOINT_DATA_IN, (void *)buf_addr, header_stamp->total_size, osWaitForever);

        if (usb_sts != KDRV_STATUS_OK) // KDRV_STATUS_USBD_TRANSFER_TERMINATED or KDRV_STATUS_USBD_TRANSFER_DISCONNECTED
        {
            dbg_log("[%s] bulk send is terminated, sts %d\n", __FUNCTION__, usb_sts);
        }

        // kp inference debug code
        if(header_stamp->magic_type == KDP2_MAGIC_TYPE_CHECKPOINT_DATA)
        {
            bRunning_dbg = true;
            header_stamp->status_code = 1; // to let ncpu go on
            continue;
        }

#ifdef AUTOTEST /* CI_PACK_REMOVE_START */
        if(header_stamp->magic_type == KDP2_MAGIC_TYPE_JSON)
        {
            // FIXME: figure out a better way to find offset from kdp2_ipc_json_result_t
            uint32_t offset = *(uint32_t *)(buf_addr + sizeof(kp_inference_header_stamp_t) + 8);
            buf_addr -= offset;
        }
#endif /* CI_PACK_REMOVE_END */

        // return free buf back to queue
        kmdw_fifoq_manager_result_put_free_buffer(buf_addr, buf_size, osWaitForever);
    }
}

////////////////////////////////////////////////////////////
// for usb-boot, flash-boot, jtag-boot, kdp2 loader

#define JTAG_MAGIC_ADDRESS 0x10100000
#define CONTROL_BYPASS_ADDRESS 0x10100004
#define KDP2_BOOT_CONFIG_ADDRESS 0x10100100
#define RECOVERY_MARK_POS (SdRAM_MEM_BASE + SdRAM_MEM_SIZE - 64)

#define JTAG_MAGIC_VALUE 0xFEDCBA01
#define BOOT_FROM_FLASH 0xA

typedef struct
{
    uint32_t boot_type;     // 0xA = flash-boot, others = usb-boot
    uint8_t loader_ver[4];  // fw loader version numbers
    uint8_t scpu_fw_ver[4]; // SCPU fw version numbers
    uint8_t ncpu_fw_ver[4]; // NCPU fw version numbers
} kdp2_boot_config_t;

////////////////////////////////////////////////////////////

int kdp2_usb_companion_init()
{
    // retrieve real serial number here from efuse
    // then convert it to hex string format

    uint32_t uid = 0;

    uid = kdp_sys_get_kn_number();

    int32_t sidx = 0;
    uint8_t kn_num_string[32] = {0};
    for (int i = 7; i >= 0; i--)
    {
        uint32_t hex = (uid >> i * 4) & 0xF;
        kn_num_string[sidx] = (hex < 10) ? '0' + hex : 'A' + (hex - 10);
        sidx += 2;
    }

    // Companion Mode
    uint16_t bcdDevice = KP_KDP2_FW_COMPANION_MODE;

    if (*((uint32_t *)JTAG_MAGIC_ADDRESS) == JTAG_MAGIC_VALUE)
    {
        kmdw_printf("FW is running in JTAG mode\n");
        bcdDevice |= KP_KDP2_FW_JTAG_TYPE;
    }
    else
    {
        kdp2_boot_config_t *bConfig = (kdp2_boot_config_t *)KDP2_BOOT_CONFIG_ADDRESS;
        if (bConfig->boot_type == BOOT_FROM_FLASH)
        {
            kmdw_printf("KDP2 FW is running in flash-boot mode\n");
            bcdDevice |= KP_KDP2_FW_FLASH_TYPE;

            kmdw_printf("boot ncpu fw from flash\n");
            SCU_EXTREG_CM4_NCPU_CTRL_SET_wakeup(1); // run ncpu
        }
        else
        {
            kmdw_printf("KDP2 FW is running in usb-boot mode\n");
            bcdDevice |= KP_KDP2_FW_USB_TYPE;
        }
    }

    // this is about recovery mode
    *(uint32_t *)RECOVERY_MARK_POS = 0;

    usbd_hal_initialize(kn_num_string, bcdDevice, usb_link_status_callback, usb_user_control_callback);
    usbd_hal_set_enable(true);

    // wow ! fifoq can also handle command
    kdp2_cmd_handler_initialize();

#ifdef FIFIOQ_LOG_VIA_USB
    kdp2_usb_log_initialize();
#endif

    return 0;
}
