#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>

#include "api/HardwareSerial.h"

#include "drv_fpioa.h"
#include "drv_uart.h"

namespace arduino {

HardwareSerial Serial(0);
HardwareSerial Serial1(1);
HardwareSerial Serial2(2);
HardwareSerial Serial3(3);
HardwareSerial Serial4(4);

HardwareSerial SerialCDC(SERIAL_USB_CDC);

HardwareSerial::HardwareSerial(int uart_idx)
    : _uart_idx(uart_idx)
    , _uart_inst(nullptr)
    , _rxPin(-1)
    , _txPin(-1)
    , _ctsPin(-1)
    , _rtsPin(-1)
{
    if (SERIAL_USB_CDC != _uart_idx && (_uart_idx < 0 || _uart_idx >= KD_HARD_UART_MAX_NUM)) {
        _uart_idx = -1; // mark invalid, guard in begin()
    }
}

HardwareSerial::~HardwareSerial() { end(); }

static inline struct uart_configure parse_serial_config(uint32_t serial_cfg, uint32_t baud)
{
    struct uart_configure uc;
    memset(&uc, 0, sizeof(uc));

    uc.baud_rate = baud;
    uc.bufsz     = 0; // use default
    uc.bit_order = BIT_ORDER_LSB; // default
    uc.invert    = NRZ_NORMAL; // default

    // Data bits mapping
    switch (serial_cfg & SERIAL_DATA_MASK) {
    case SERIAL_DATA_5:
        uc.data_bits = DATA_BITS_5;
        break;
    case SERIAL_DATA_6:
        uc.data_bits = DATA_BITS_6;
        break;
    case SERIAL_DATA_7:
        uc.data_bits = DATA_BITS_7;
        break;
    case SERIAL_DATA_8:
        uc.data_bits = DATA_BITS_8;
        break;
    default:
        uc.data_bits = DATA_BITS_8;
        break; // default if unspecified
    }

    // Stop bits mapping
    switch (serial_cfg & SERIAL_STOP_BIT_MASK) {
    case SERIAL_STOP_BIT_1:
        uc.stop_bits = STOP_BITS_1;
        break;
    case SERIAL_STOP_BIT_2:
        uc.stop_bits = STOP_BITS_2;
        break;
    default:
        uc.stop_bits = STOP_BITS_1;
        break; // 1.5 not supported in driver
    }

    // Parity mapping
    switch (serial_cfg & SERIAL_PARITY_MASK) {
    case SERIAL_PARITY_NONE:
        uc.parity = PARITY_NONE;
        break;
    case SERIAL_PARITY_ODD:
        uc.parity = PARITY_ODD;
        break;
    case SERIAL_PARITY_EVEN:
        uc.parity = PARITY_EVEN;
        break;
    default:
        uc.parity = PARITY_NONE;
        break;
    }

    return uc;
}

void HardwareSerial::begin(unsigned long baud, uint32_t config, int8_t rxPin, int8_t txPin)
{
    if (SERIAL_USB_CDC != _uart_idx && (_uart_idx < 0 || _uart_idx >= KD_HARD_UART_MAX_NUM)) {
        printf("Invalid UART index %d\n", _uart_idx);
        return;
    }

#define UART_TXD_FUNC(id)                                                                                                      \
    ((id) == 0       ? UART0_TXD                                                                                               \
         : (id) == 1 ? UART1_TXD                                                                                               \
         : (id) == 2 ? UART2_TXD                                                                                               \
         : (id) == 3 ? UART3_TXD                                                                                               \
         : (id) == 4 ? UART4_TXD                                                                                               \
                     : FUNC_MAX)

#define UART_RXD_FUNC(id)                                                                                                      \
    ((id) == 0       ? UART0_RXD                                                                                               \
         : (id) == 1 ? UART1_RXD                                                                                               \
         : (id) == 2 ? UART2_RXD                                                                                               \
         : (id) == 3 ? UART3_RXD                                                                                               \
         : (id) == 4 ? UART4_RXD                                                                                               \
                     : FUNC_MAX)

    fpioa_func_t func_tx = UART_TXD_FUNC(_uart_idx);
    fpioa_func_t func_rx = UART_RXD_FUNC(_uart_idx);

#undef UART_TXD_FUNC
#undef UART_RXD_FUNC

    fpioa_err_t           err     = FPIOA_OK;
    struct uart_configure new_cfg = parse_serial_config(config, baud);

    if (SERIAL_USB_CDC == _uart_idx) {
        if (!_uart_inst) {
            if (0x00 != drv_uart_inst_create_usb("/dev/ttyUSB", &_uart_inst)) {
                printf("Failed to create UART%d instance\n", _uart_idx);
                return;
            }
        }
    } else {
        if (FPIOA_OK != (err = drv_fpioa_validate_pin(rxPin, func_rx))) {
            printf("Invalid RX pin for UART%d\n", _uart_idx);
            return;
        }

        if (FPIOA_OK != (err = drv_fpioa_validate_pin(txPin, func_tx))) {
            printf("Invalid TX pin for UART%d\n", _uart_idx);
            return;
        }
        if (!_uart_inst) {
            if (0x00 != drv_uart_inst_create(_uart_idx, &_uart_inst)) {
                printf("Failed to create UART%d instance\n", _uart_idx);
                return;
            }
        }
        if (0x00 != drv_uart_set_config(_uart_inst, &new_cfg)) {
            printf("Failed to set UART%d config\n", _uart_idx);
            return;
        }
    }
}

void HardwareSerial::end() { drv_uart_inst_destroy(&_uart_inst); }

bool HardwareSerial::can_write(void)
{
    if (!_uart_inst) {
        printf("UART%d not begun\n", _uart_idx);
        return false;
    }
    return drv_uart_is_dtr_asserted(_uart_inst);
}

size_t HardwareSerial::read(uint8_t* buffer, size_t size)
{
    if (!_uart_inst) {
        printf("UART%d not begun\n", _uart_idx);
        return 0;
    }

    return drv_uart_read(_uart_inst, buffer, size);
}

size_t HardwareSerial::write(const uint8_t* buffer, size_t size)
{
    if (!_uart_inst) {
        printf("UART%d not begun\n", _uart_idx);
        return 0;
    }

    if (SERIAL_USB_CDC == _uart_idx) {
        if (!drv_uart_is_dtr_asserted(_uart_inst)) {
            printf("DTR not asserted for USB CDC UART%d\n", _uart_idx);
            return 0;
        }
    }

    return drv_uart_write(_uart_inst, buffer, size);
}

int HardwareSerial::available(void)
{
    if (!_uart_inst) {
        printf("UART%d not begun\n", _uart_idx);
        return 0;
    }

    return drv_uart_recv_available(_uart_inst);
}

void HardwareSerial::flush(bool txOnly) { printf("TODO: flush\n"); }

uint32_t HardwareSerial::baudRate()
{
    struct uart_configure curr_cfg;

    if (!_uart_inst) {
        printf("UART%d not begun\n", _uart_idx);
        return 0;
    }

    if (0x00 != drv_uart_get_config(_uart_inst, &curr_cfg)) {
        printf("Failed to get UART%d config\n", _uart_idx);
        return 0;
    }

    return curr_cfg.baud_rate;
}

void HardwareSerial::updateBaudRate(unsigned long baud)
{
    struct uart_configure curr_cfg;

    if (!_uart_inst) {
        printf("UART%d not begun\n", _uart_idx);
        return;
    }

    if (0x00 != drv_uart_get_config(_uart_inst, &curr_cfg)) {
        printf("Failed to get UART%d config\n", _uart_idx);
        return;
    }

    curr_cfg.baud_rate = baud;
    if (0x00 != drv_uart_set_config(_uart_inst, &curr_cfg)) {
        printf("Failed to set UART%d config\n", _uart_idx);
        return;
    }
}

size_t HardwareSerial::setRxBufferSize(size_t new_size)
{
    if (_uart_inst) {
        printf("UART%d is begun\n", _uart_idx);
        return 0;
    }

    drv_uart_configure_buffer_size(_uart_idx, new_size);

    return new_size;
}

HardwareSerial::operator bool() const { return !!_uart_inst; }

} // namespace arduino
