#include "usbd_cdc_if.h"
#include "stm32f7xx_hal.h"
#include "ota.h"

extern USBD_HandleTypeDef hUsbDevice;
volatile uint8_t cdc_streaming = 1;  /* auto-stream */
static volatile uint8_t cdc_port_open = 0;  /* set when host asserts DTR */
volatile uint8_t  cdc_arm_request = 0;    /* set by A command */
volatile uint8_t  cdc_disarm_request = 0; /* set by D command */
volatile uint8_t  cdc_recal_request = 0;  /* set by G command — gyro recalibration */
volatile uint8_t  cdc_imu_cal_request = 0; /* set by O command — mount offset calibration (Issue #680) */
volatile uint32_t cdc_rx_count = 0;       /* total CDC packets received from host */

volatile uint8_t  cdc_estop_request = 0;
volatile uint8_t  cdc_estop_clear_request = 0;

/*
 * PID tuning command buffer.
 * CDC_Receive (USB IRQ) copies multi-char commands here.
 * Main loop polls cdc_cmd_ready, parses, and clears.
 * Commands: P<kp>  I<ki>  D<kd>  T<setpoint>  M<max_speed>  ?
 */
volatile uint8_t  cdc_cmd_ready = 0;
volatile char     cdc_cmd_buf[32];

/*
 * Jetson command buffer (bidirectional protocol).
 * 'H'\n        — heartbeat, ISR updates jetson_hb_tick only (no buf copy needed).
 * 'C'<s>,<t>\n — drive command: ISR copies to buf, main loop parses with sscanf.
 * jetson_hb_tick is also refreshed on every C command.
 */
volatile uint8_t  jetson_cmd_ready = 0;
volatile char     jetson_cmd_buf[32];
volatile uint32_t jetson_hb_tick = 0;  /* HAL_GetTick() of last H or C */

/*
 * USB TX/RX buffers grouped into a single 512-byte aligned struct so that
 * one MPU region (configured in usbd_conf.c) can mark them non-cacheable.
 * Size must be a power-of-2 >= total size for MPU RASR SIZE encoding.
 */
static struct {
    uint8_t tx[256];
    uint8_t rx[256];
} __attribute__((aligned(512))) usb_nc_buf;

#define UserTxBuffer usb_nc_buf.tx
#define UserRxBuffer usb_nc_buf.rx

/* Exported so usbd_conf.c USB_NC_MPU_Config() can set the region base */
void * const usb_nc_buf_base = &usb_nc_buf;

/*
 * Betaflight-proven DFU reboot:
 * 1. Write magic to RTC backup register (persists across soft reset)
 * 2. NVIC_SystemReset() — clean hardware reset
 * 3. Early startup checks magic, clears it, jumps to system bootloader
 *
 * Magic is written to BKP15R (OTA_DFU_BKP_IDX) — not BKP0R — so that
 * BKP0R–BKP6R are available for BNO055 calibration offsets (PR #150).
 * The magic check in checkForBootloader() reads the same BKP15R register.
 */
static void request_bootloader(void) {
    /* Betaflight-proven: write magic, disable IRQs, reset.
     * checkForBootloader() runs on next boot before anything else. */
    __HAL_RCC_PWR_CLK_ENABLE();
    HAL_PWR_EnableBkUpAccess();
    __HAL_RCC_RTC_ENABLE();

    /* Write magic to BKP15R via OTA module constants (avoids BNO055 BKP0–6) */
    (&RTC->BKP0R)[OTA_DFU_BKP_IDX] = OTA_DFU_MAGIC;

    __disable_irq();
    NVIC_SystemReset();
}

/*
 * Call this VERY early in main(), before HAL_Init().
 * Checks RTC backup register for magic value left by request_bootloader().
 * If found: clear magic, jump to STM32F7 system bootloader at 0x1FF00000.
 */
void checkForBootloader(void) {
    /*
     * Betaflight-proven bootloader jump for STM32F7.
     * Called VERY early, before HAL_Init/caches/clocks.
     * At this point only RCC PWR is needed to read RTC backup regs.
     */

    /* Enable backup domain access to read RTC backup register */
    __HAL_RCC_PWR_CLK_ENABLE();
    HAL_PWR_EnableBkUpAccess();
    __HAL_RCC_RTC_ENABLE();

    uint32_t magic = (&RTC->BKP0R)[OTA_DFU_BKP_IDX];  /* read BKP15R */

    if (magic != OTA_DFU_MAGIC) {
        return; /* Normal boot */
    }

    /* Clear magic so next boot is normal */
    (&RTC->BKP0R)[OTA_DFU_BKP_IDX] = 0;

    /* Jump to STM32F7 system bootloader at 0x1FF00000.
     * Exactly as Betaflight does it — no cache/VTOR/MEMRMP games needed
     * because we run before any of that is configured. */

    __HAL_RCC_SYSCFG_CLK_ENABLE();

    __set_MSP(*(uint32_t *)0x1FF00000);
    ((void (*)(void))(*(uint32_t *)0x1FF00004))();

    while (1);
}

static int8_t CDC_Init(void) {
    USBD_CDC_SetTxBuffer(&hUsbDevice, UserTxBuffer, 0);
    USBD_CDC_SetRxBuffer(&hUsbDevice, UserRxBuffer);
    USBD_CDC_ReceivePacket(&hUsbDevice);

    /* Reset TxState so CDC_Transmit works after host (re)connects.
     * Without this, if transmits happen before host opens port,
     * TxState stays BUSY forever since host never ACKs. */
    USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef *)hUsbDevice.pClassData;
    if (hcdc) hcdc->TxState = 0;

    return USBD_OK;
}

static int8_t CDC_DeInit(void) { return USBD_OK; }

static int8_t CDC_Control(uint8_t cmd, uint8_t *pbuf, uint16_t length) {
    (void)pbuf; (void)length;
    if (cmd == 0x22) {  /* CDC_SET_CONTROL_LINE_STATE — host opened port */
        cdc_port_open = 1;
        cdc_streaming = 1;
    }
    return USBD_OK;
}

static int8_t CDC_Receive(uint8_t *buf, uint32_t *len) {
    if (*len < 1) goto done;

    switch (buf[0]) {
        case 'S': cdc_streaming = !cdc_streaming; break;
        case 'A': cdc_arm_request = 1;            break;
        case 'D': cdc_disarm_request = 1;         break;
        case 'G': cdc_recal_request = 1;          break; /* gyro recalibration */
        case 'O': cdc_imu_cal_request = 1;        break; /* mount offset cal (Issue #680) */
        case 'R': request_bootloader();            break; /* never returns */

        case 'E': cdc_estop_request = 1;       break;
        case 'F': cdc_estop_request = 2;       break;
        case 'Z': cdc_estop_clear_request = 1; break;

        /*
         * PID tuning: P<kp>  I<ki>  D<kd>  T<setpoint>  M<max_speed>  ?
         * Copy to cmd buffer; main loop parses float (avoids sscanf in IRQ).
         */
        case 'P': case 'I': case 'K': case 'T': case 'M': case '?': {
            uint32_t copy_len = *len < 31 ? *len : 31;
            for (uint32_t i = 0; i < copy_len; i++) cdc_cmd_buf[i] = (char)buf[i];
            cdc_cmd_buf[copy_len] = '\0';
            cdc_cmd_ready = 1;
            break;
        }

        /* Jetson heartbeat — just refresh the tick, no buffer copy needed */
        case 'H':
            jetson_hb_tick = HAL_GetTick();
            break;

        /* Jetson drive command: C<speed>,<steer>\n
         * Copy to buffer; main loop parses ints (keeps sscanf out of ISR). */
        case 'C': {
            uint32_t copy_len = *len < 31 ? *len : 31;
            for (uint32_t i = 0; i < copy_len; i++) jetson_cmd_buf[i] = (char)buf[i];
            jetson_cmd_buf[copy_len] = '\0';
            jetson_hb_tick = HAL_GetTick(); /* C command also refreshes heartbeat */
            jetson_cmd_ready = 1;
            break;
        }

        default: break;
    }

done:
    cdc_rx_count++;
    USBD_CDC_SetRxBuffer(&hUsbDevice, UserRxBuffer);
    USBD_CDC_ReceivePacket(&hUsbDevice);
    return USBD_OK;
}

USBD_CDC_ItfTypeDef USBD_CDC_fops = { CDC_Init, CDC_DeInit, CDC_Control, CDC_Receive };

uint8_t CDC_Transmit(uint8_t *buf, uint16_t len) {
    USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef *)hUsbDevice.pClassData;
    if (hcdc == NULL) return USBD_FAIL;
    if (hcdc->TxState != 0) {
        /* If stuck busy (no host ACK), force reset after a while */
        static uint32_t busy_count = 0;
        if (++busy_count > 100) { hcdc->TxState = 0; busy_count = 0; }
        return USBD_BUSY;
    }
    /* Always copy into the static UserTxBuffer so the USB hardware reads
     * from a known fixed SRAM address — never from the caller's stack.
     * The USB TXFE IRQ fires asynchronously; a stack buffer could be
     * overwritten by the time the FIFO is loaded. */
    if (len > sizeof(UserTxBuffer)) len = sizeof(UserTxBuffer);
    memcpy(UserTxBuffer, buf, len);
    USBD_CDC_SetTxBuffer(&hUsbDevice, UserTxBuffer, len);
    return USBD_CDC_TransmitPacket(&hUsbDevice);
}
