#include "serial_io.h"
#include "blocking_io/blocking_fifo.h"
#include "cmsis_os2.h"

#include <stdint.h>
#include <stm32h7xx_hal.h>
#include <string.h>

static void serial_io_tx_cplt(UART_HandleTypeDef *huart);
static void serial_io_rx_cplt(UART_HandleTypeDef *huart);
static void serial_io_thread(void * arg);

#define SERIAL_IO_FIFO_LEN (16384)

static UART_HandleTypeDef uart;
static DMA_HandleTypeDef dma;
static osEventFlagsId_t flags;
static osThreadId_t th;
static BFifo fifo;
static uint8_t buf[SERIAL_IO_FIFO_LEN];
static char c;

#define SERIAL_IO_TX_CPLT_FLAG (0x01)

void serial_io_init() {
    // initialize FIFO
    bfifo_create(&fifo, buf, SERIAL_IO_FIFO_LEN);

    // initialize flags and thread
    flags = osEventFlagsNew(NULL);

    osThreadAttr_t attr;
    memset(&attr, 0, sizeof(osThreadAttr_t));
    attr.name = "serialio";
    attr.stack_size = 2048;
    th = osThreadNew(serial_io_thread, NULL, &attr);

    // Enable peripheral clocks
    __HAL_RCC_GPIOD_CLK_ENABLE();
    __HAL_RCC_USART3_CLK_ENABLE();
    __HAL_RCC_DMA1_CLK_ENABLE();

    // Initialize pins as USART3 Tx and Rx
    GPIO_InitTypeDef gpio;

    // TX
    gpio.Pin = GPIO_PIN_8;
    gpio.Mode = GPIO_MODE_AF_PP;
    gpio.Alternate = GPIO_AF7_USART3;
    gpio.Speed = GPIO_SPEED_HIGH;
    gpio.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOD, &gpio);

    // RX
    gpio.Pin = GPIO_PIN_9;
    gpio.Mode = GPIO_MODE_AF_OD;
    HAL_GPIO_Init(GPIOD, &gpio);

    // Initialize USART3
    uart.Instance = USART3;
    uart.Init.BaudRate = 115200;
    uart.Init.Mode = UART_MODE_TX_RX;
    uart.Init.Parity = UART_PARITY_NONE;
    uart.Init.WordLength = UART_WORDLENGTH_8B;
    uart.Init.StopBits = UART_STOPBITS_1;
    uart.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    uart.Init.OverSampling = UART_OVERSAMPLING_16;
    uart.Init.ClockPrescaler = UART_PRESCALER_DIV16;
    // huart.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
    HAL_UART_Init(&uart);

    HAL_UART_RegisterCallback(&uart, HAL_UART_TX_COMPLETE_CB_ID, serial_io_tx_cplt);
    HAL_UART_RegisterCallback(&uart, HAL_UART_RX_COMPLETE_CB_ID, serial_io_rx_cplt);

    HAL_UART_Receive_IT(&uart, (uint8_t *)&c, 1);

    HAL_NVIC_SetPriority(USART3_IRQn, 12, 0);
    HAL_NVIC_EnableIRQ(USART3_IRQn);

    // configure DMA
    dma.Instance = DMA1_Stream0;
    dma.Init.Request = DMA_REQUEST_USART3_TX;
    dma.Init.Direction = DMA_MEMORY_TO_PERIPH;
    dma.Init.PeriphInc = DMA_PINC_DISABLE;
    dma.Init.MemInc = DMA_MINC_ENABLE;
    dma.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    dma.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    dma.Init.Mode = DMA_NORMAL;
    dma.Init.Priority = DMA_PRIORITY_VERY_HIGH;
    dma.Init.FIFOMode = DMA_FIFO_THRESHOLD_FULL;
    dma.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;
    dma.Init.MemBurst = DMA_MBURST_SINGLE;
    dma.Init.PeriphBurst = DMA_PBURST_SINGLE;

    HAL_DMA_Init(&dma);

    __HAL_LINKDMA(&uart, hdmatx, dma);

    HAL_NVIC_SetPriority(DMA1_Stream0_IRQn, 12, 0);
    HAL_NVIC_EnableIRQ(DMA1_Stream0_IRQn);
}

void DMA1_Stream0_IRQHandler() {
    HAL_DMA_IRQHandler(&dma);
}

void USART3_IRQHandler() {
    HAL_UART_IRQHandler(&uart);
}

void serial_io_write(const char *str, uint32_t len) {
    bfifo_push(&fifo, (const uint8_t *)str, len);
}

//  ----------

#define SERIAL_IO_LINEBUF_LEN (256)
static uint8_t lineBuf[SERIAL_IO_LINEBUF_LEN];

static void serial_io_thread(void * arg) {
    while (true) {
        bfifo_wait_avail(&fifo);
        uint32_t size = bfifo_read(&fifo, lineBuf, SERIAL_IO_LINEBUF_LEN);
        bfifo_pop(&fifo, size, 0);
        HAL_UART_Transmit_DMA(&uart, lineBuf, size);
        osEventFlagsWait(flags, SERIAL_IO_TX_CPLT_FLAG, 0, osWaitForever);
    }
}

//  ----------

static void serial_io_tx_cplt(UART_HandleTypeDef *huart) {
    osEventFlagsSet(flags, SERIAL_IO_TX_CPLT_FLAG);
}

__weak void serial_io_rx_callback(const uint8_t *data, uint32_t size) {
    (void)data;
    (void)size;
}

static void serial_io_rx_cplt(UART_HandleTypeDef *huart) {
    char k = c;
    HAL_UART_Receive_IT(&uart, (uint8_t *)&c, 1);
    serial_io_rx_callback((const uint8_t *)&k, 1);
}