#pragma once

#include <cstdint>
#include <cstring>

#include "gpio.h"
#include "usbd_cdc.h"
#include "usbd_def.h"

extern "C" {
extern USBD_CDC_ItfTypeDef USBD_Interface_fops_FS;
extern USBD_HandleTypeDef hUsbDeviceFS;
}

namespace uart::detail {

template<size_t Size>
struct RingBuffer {
    volatile size_t head;
    volatile size_t tail;
    volatile uint8_t data[Size];
};

template<size_t Size>
struct Buffer {
    volatile size_t size;
    volatile uint8_t data[Size];
};

template<class cfg>
class VirtualComPort {
  public:
    static constexpr auto DATA_BITS = cfg::DATA_BITS;

    [[gnu::always_inline]] static void init()
    {
        USBD_Interface_fops_FS.Init = CdcInit;
        USBD_Interface_fops_FS.DeInit = CdcDeInit;
        USBD_Interface_fops_FS.Control = CdcControl;
        USBD_Interface_fops_FS.Receive = CdcReceive;
    }

    [[gnu::always_inline]] static bool rxByte(uint8_t& byte)
    {
        if(m_rxBuffer.head == m_rxBuffer.tail)
            return false;

        m_reading = true;
        const size_t newTail = (m_rxBuffer.tail + 1) % RX_BUFFER_SIZE;
        byte = m_rxBuffer.data[newTail];
        m_rxBuffer.tail = newTail;
        m_reading = false;
        return true;
    }

    [[gnu::always_inline]] static void txByte(const uint8_t& byte)
    {
        if(m_txBuffer.size == TX_BUFFER_SIZE) {
            flushTx();
        }
        m_txBuffer.data[m_txBuffer.size++] = byte;
    }

    [[gnu::always_inline]] static bool peek(uint8_t& byte)
    {
        if(m_rxBuffer.head == m_rxBuffer.tail)
            return false;

        m_reading = true;
        const size_t newTail = (m_rxBuffer.tail + 1) % RX_BUFFER_SIZE;
        byte = m_rxBuffer.data[newTail];
        m_reading = false;
        return true;
    }

    [[gnu::always_inline]] static void flushTx()
    {
        if(m_txBuffer.size == 0)
            return;

        constexpr auto usbReady = []() {
            USBD_CDC_HandleTypeDef* hcdc = static_cast<USBD_CDC_HandleTypeDef*>(hUsbDeviceFS.pClassData);
            return hcdc->TxState != 0;
        };

#ifdef INFO_LEDS
        HAL_GPIO_WritePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin, GPIO_PIN_SET);
        HAL_GPIO_WritePin(BLUE_LED_GPIO_Port, BLUE_LED_Pin, GPIO_PIN_RESET);
#endif

        while(usbReady())
            ;

        std::memcpy(const_cast<uint8_t*>(m_usbAsyncTxBuffer.data), const_cast<const uint8_t*>(m_txBuffer.data), m_txBuffer.size);
        m_usbAsyncTxBuffer.size = m_txBuffer.size;
        m_txBuffer.size = 0;

        USBD_CDC_SetTxBuffer(&hUsbDeviceFS, const_cast<uint8_t*>(m_usbAsyncTxBuffer.data), m_usbAsyncTxBuffer.size);
        USBD_CDC_TransmitPacket(&hUsbDeviceFS);

#ifdef INFO_LEDS
        HAL_GPIO_WritePin(BLUE_LED_GPIO_Port, BLUE_LED_Pin, GPIO_PIN_SET);
#endif
    }

  private:
    static constexpr auto TX_BUFFER_SIZE = CDC_DATA_FS_OUT_PACKET_SIZE;
    static constexpr auto RX_BUFFER_SIZE = CDC_DATA_FS_IN_PACKET_SIZE;

    static Buffer<TX_BUFFER_SIZE> m_txBuffer;
    static Buffer<TX_BUFFER_SIZE> m_usbAsyncTxBuffer;

    static RingBuffer<RX_BUFFER_SIZE> m_rxBuffer;
    static Buffer<RX_BUFFER_SIZE> m_usbAsyncRxBuffer;
    static volatile bool m_reading;

    static int8_t CdcInit()
    {
#ifdef INFO_LEDS
        HAL_GPIO_WritePin(RED_LED_GPIO_Port, RED_LED_Pin, GPIO_PIN_SET);
        HAL_GPIO_WritePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin, GPIO_PIN_RESET);
        HAL_GPIO_WritePin(BLUE_LED_GPIO_Port, BLUE_LED_Pin, GPIO_PIN_SET);
#endif

        USBD_CDC_SetTxBuffer(&hUsbDeviceFS, const_cast<uint8_t*>(m_txBuffer.data), 0);
        USBD_CDC_SetRxBuffer(&hUsbDeviceFS, const_cast<uint8_t*>(m_usbAsyncRxBuffer.data));
        return USBD_OK;
    }

    static int8_t CdcDeInit()
    {
#ifdef INFO_LEDS
        HAL_GPIO_WritePin(RED_LED_GPIO_Port, RED_LED_Pin, GPIO_PIN_RESET);
        HAL_GPIO_WritePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin, GPIO_PIN_SET);
        HAL_GPIO_WritePin(BLUE_LED_GPIO_Port, BLUE_LED_Pin, GPIO_PIN_SET);
#endif

        m_txBuffer.size = 0;
        m_usbAsyncTxBuffer.size = 0;
        m_rxBuffer.head = 0;
        m_rxBuffer.tail = 0;
        m_usbAsyncRxBuffer.size = 0;
        m_reading = false;
        return USBD_OK;
    }

    static int8_t CdcControl(uint8_t cmd, [[maybe_unused]] uint8_t* buf, [[maybe_unused]] uint16_t length)
    {
        switch(cmd) {
        case CDC_SEND_ENCAPSULATED_COMMAND:
            break;
        case CDC_GET_ENCAPSULATED_RESPONSE:
            break;
        case CDC_SET_COMM_FEATURE:
            break;
        case CDC_GET_COMM_FEATURE:
            break;
        case CDC_CLEAR_COMM_FEATURE:
            break;

        case CDC_SET_LINE_CODING:
            /*******************************************************************************/
            /* Line Coding Structure                                                       */
            /*-----------------------------------------------------------------------------*/
            /* Offset | Field       | Size | Value  | Description                          */
            /* 0      | dwDTERate   |   4  | Number |Data terminal rate, in bits per second*/
            /* 4      | bCharFormat |   1  | Number | Stop bits                            */
            /*                                        0 - 1 Stop bit                       */
            /*                                        1 - 1.5 Stop bits                    */
            /*                                        2 - 2 Stop bits                      */
            /* 5      | bParityType |  1   | Number | Parity                               */
            /*                                        0 - None                             */
            /*                                        1 - Odd                              */
            /*                                        2 - Even                             */
            /*                                        3 - Mark                             */
            /*                                        4 - Space                            */
            /* 6      | bDataBits  |   1   | Number Data bits (5, 6, 7, 8 or 16).          */
            /*******************************************************************************/
            break;

        case CDC_GET_LINE_CODING:
            break;

        case CDC_SET_CONTROL_LINE_STATE:
            break;

        case CDC_SEND_BREAK:
            break;

        default:
            break;
        }

        return USBD_OK;
    }

    static int8_t CdcReceive([[maybe_unused]] uint8_t* buf, uint32_t* length)
    {
#ifdef INFO_LEDS
        HAL_GPIO_WritePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin, GPIO_PIN_SET);
        HAL_GPIO_WritePin(RED_LED_GPIO_Port, RED_LED_Pin, GPIO_PIN_RESET);
#endif

        if(USBD_CDC_ReceivePacket(&hUsbDeviceFS) != USBD_OK)
            return USBD_FAIL;

        for(uint32_t i = 0; i < *length; ++i) {
            rxHandler(m_usbAsyncRxBuffer.data[i]);
        }

#ifdef INFO_LEDS
        HAL_GPIO_WritePin(RED_LED_GPIO_Port, RED_LED_Pin, GPIO_PIN_SET);
#endif
        return USBD_OK;
    }

    [[gnu::always_inline]] static inline void rxHandler(const volatile uint8_t& data)
    {
        const size_t newHead = (m_rxBuffer.head + 1) % RX_BUFFER_SIZE;

        // Overflow, but tail is being read
        if(newHead == m_rxBuffer.tail && m_reading) {
            // Throw away the data, because it cannot be received safely
            return;
        }
        // Overflow, overwrite oldest data
        else if(newHead == m_rxBuffer.tail) {
            const size_t newTail = (m_rxBuffer.tail + 1) % RX_BUFFER_SIZE;
            m_rxBuffer.tail = newTail;
        }

        m_rxBuffer.data[newHead] = data;
        m_rxBuffer.head = newHead;
    }
};

template<class cfg>
Buffer<VirtualComPort<cfg>::TX_BUFFER_SIZE> VirtualComPort<cfg>::m_txBuffer = {0, {0}};

template<class cfg>
Buffer<VirtualComPort<cfg>::TX_BUFFER_SIZE> VirtualComPort<cfg>::m_usbAsyncTxBuffer = {0, {0}};

template<class cfg>
RingBuffer<VirtualComPort<cfg>::RX_BUFFER_SIZE> VirtualComPort<cfg>::m_rxBuffer = {0, 0, {0}};

template<class cfg>
Buffer<VirtualComPort<cfg>::RX_BUFFER_SIZE> VirtualComPort<cfg>::m_usbAsyncRxBuffer = {0, {0}};

template<class cfg>
volatile bool VirtualComPort<cfg>::m_reading = false;

} // namespace uart::detail
