/*-----------------------------------------------------

Plattform for Raspberry Pi Pico and other RP2040 boards
by SirSydom <com@sirsydom.de> 2021-2022

made to work with arduino-pico - "Raspberry Pi Pico Arduino core, for all RP2040 boards"
by Earl E. Philhower III https://github.com/earlephilhower/arduino-pico


RTTI must be set to enabled in the board options

Uses direct flash reading/writing.
Size ist defined by KNX_FLASH_SIZE (default 4k) - must be a multiple of 4096.
Offset in Flash is defined by KNX_FLASH_OFFSET (default 1,5MiB / 0x180000) - must be a multiple of 4096.

EEPROM Emulation from arduino-pico core (max 4k) can be use by defining USE_RP2040_EEPROM_EMULATION

A RAM-buffered Flash can be use by defining USE_RP2040_LARGE_EEPROM_EMULATION

For usage of KNX-IP you have to define either
- KNX_IP_LAN (use the arduino-pico core's w5500 lwip stack)
- KNX_IP_WIFI (use the arduino-pico core's PiPicoW lwip stack)

----------------------------------------------------*/

#include "rp2040_arduino_platform.h"

#ifdef ARDUINO_ARCH_RP2040
#include "knx/bits.h"

#include <Arduino.h>

// Pi Pico specific libs
#include <EEPROM.h>            // EEPROM emulation in flash, part of Earl E Philhowers Pi Pico Arduino support
#include <hardware/flash.h>    // from Pico SDK
#include <hardware/watchdog.h> // from Pico SDK
#include <pico/unique_id.h>    // from Pico SDK

#ifdef USE_KNX_DMA_UART
#include <hardware/dma.h>
// constexpr uint32_t uartDmaTransferCount = 0b1111111111;
constexpr uint32_t uartDmaTransferCount = UINT32_MAX;
constexpr uint8_t uartDmaBufferExp = 8u; // 2**BufferExp
constexpr uint16_t uartDmaBufferSize = (1u << uartDmaBufferExp);
int8_t uartDmaChannel = -1;
volatile uint8_t __attribute__((aligned(uartDmaBufferSize))) uartDmaBuffer[uartDmaBufferSize] = {};
volatile uint32_t uartDmaReadCount = 0;
volatile uint16_t uartDmaRestartCount = 0;
volatile uint32_t uartDmaWriteCount2 = 0;
volatile uint32_t uartDmaAvail = 0;

// Returns the number of bytes read since the DMA transfer start
inline uint32_t uartDmaWriteCount()
{
    uartDmaWriteCount2 = uartDmaTransferCount - dma_channel_hw_addr(uartDmaChannel)->transfer_count;
    return uartDmaWriteCount2;
}

// Returns the current write position in the DMA buffer
inline uint16_t uartDmaWriteBufferPosition()
{
    return uartDmaWriteCount() % uartDmaBufferSize;
}

// Returns the current read position in the DMA buffer
inline uint16_t uartDmaReadBufferPosition()
{
    return uartDmaReadCount % uartDmaBufferSize;
}

// Returns the current reading position as a pointer
inline uint8_t* uartDmaReadAddr()
{
    return ((uint8_t*)uartDmaBuffer + uartDmaReadBufferPosition());
}

// Restarts the transfer after completion.
void __time_critical_func(uartDmaRestart)()
{
    // println("Restart");
    uartDmaRestartCount = uartDmaWriteBufferPosition() - uartDmaReadBufferPosition();

    // if uartDmaRestartCount == 0, everything has been processed and the read count can be set to 0 again with the restart.
    if (uartDmaRestartCount == 0)
    {
        uartDmaReadCount = 0;
    }

    asm volatile("" ::: "memory");
    dma_hw->ints0 = 1u << uartDmaChannel; // clear DMA IRQ0 flag
    asm volatile("" ::: "memory");
    dma_channel_set_write_addr(uartDmaChannel, uartDmaBuffer, true);
}
#endif

#define FLASHPTR ((uint8_t*)XIP_BASE + KNX_FLASH_OFFSET)

#ifndef USE_RP2040_EEPROM_EMULATION
    #if KNX_FLASH_SIZE % 4096
        #error "KNX_FLASH_SIZE must be multiple of 4096"
    #endif

    #if KNX_FLASH_OFFSET % 4096
        #error "KNX_FLASH_OFFSET must be multiple of 4096"
    #endif
#endif

#ifdef KNX_IP_LAN
    extern Wiznet5500lwIP KNX_NETIF;
#elif defined(KNX_IP_WIFI)
#elif defined(KNX_IP_GENERIC)

#endif

RP2040ArduinoPlatform::RP2040ArduinoPlatform()
#if !defined(KNX_NO_DEFAULT_UART) && !defined(USE_KNX_DMA_UART)
    : ArduinoPlatform(&KNX_SERIAL)
#endif
{
#ifdef KNX_UART_RX_PIN
    _rxPin = KNX_UART_RX_PIN;
#endif
#ifdef KNX_UART_TX_PIN
    _txPin = KNX_UART_TX_PIN;
#endif
#ifndef USE_RP2040_EEPROM_EMULATION
    _memoryType = Flash;
#endif
}

RP2040ArduinoPlatform::RP2040ArduinoPlatform(HardwareSerial* s)
    : ArduinoPlatform(s)
{
#ifndef USE_RP2040_EEPROM_EMULATION
    _memoryType = Flash;
#endif
}

void RP2040ArduinoPlatform::knxUartPins(pin_size_t rxPin, pin_size_t txPin)
{
    _rxPin = rxPin;
    _txPin = txPin;
}

bool RP2040ArduinoPlatform::overflowUart()
{
#ifdef USE_KNX_DMA_UART
    // during dma restart
    bool ret;
    const uint32_t writeCount = uartDmaWriteCount();

    if (uartDmaRestartCount > 0)
        ret = writeCount >= (uartDmaBufferSize - uartDmaRestartCount - 1);
    else
        ret = (writeCount - uartDmaReadCount) > uartDmaBufferSize;

    // if (ret)
    // {
    //     println(uartDmaWriteBufferPosition());
    //     println(uartDmaReadBufferPosition());
    //     println(uartDmaWriteCount());
    //     println(uartDmaReadCount);
    //     println(uartDmaRestartCount);
    //     printHex("BUF: ", (const uint8_t *)uartDmaBuffer, uartDmaBufferSize);
    //     println("OVERFLOW");
    //     while (true)
    //         ;
    // }
    return ret;
#else
    SerialUART* serial = dynamic_cast<SerialUART*>(_knxSerial);
    return serial->overflow();
#endif
}

void RP2040ArduinoPlatform::setupUart()
{
#ifdef USE_KNX_DMA_UART

    if (uartDmaChannel == -1)
    {
        // configure uart0
        gpio_set_function(_rxPin, GPIO_FUNC_UART);
        gpio_set_function(_txPin, GPIO_FUNC_UART);
        uart_init(KNX_DMA_UART, 19200);
        uart_set_hw_flow(KNX_DMA_UART, false, false);
        uart_set_format(KNX_DMA_UART, 8, 1, UART_PARITY_EVEN);
        uart_set_fifo_enabled(KNX_DMA_UART, false);

        // configure uart0
        uartDmaChannel = dma_claim_unused_channel(true); // get free channel for dma
        dma_channel_config dmaConfig = dma_channel_get_default_config(uartDmaChannel);
        channel_config_set_transfer_data_size(&dmaConfig, DMA_SIZE_8);
        channel_config_set_read_increment(&dmaConfig, false);
        channel_config_set_write_increment(&dmaConfig, true);
        channel_config_set_high_priority(&dmaConfig, true);
        channel_config_set_ring(&dmaConfig, true, uartDmaBufferExp);
        channel_config_set_dreq(&dmaConfig, KNX_DMA_UART_DREQ);
        dma_channel_set_read_addr(uartDmaChannel, &uart_get_hw(uart0)->dr, false);
        dma_channel_set_write_addr(uartDmaChannel, uartDmaBuffer, false);
        dma_channel_set_trans_count(uartDmaChannel, uartDmaTransferCount, false);
        dma_channel_set_config(uartDmaChannel, &dmaConfig, true);
        dma_channel_set_irq1_enabled(uartDmaChannel, true);
        // irq_add_shared_handler(KNX_DMA_IRQ, uartDmaRestart, PICO_SHARED_IRQ_HANDLER_HIGHEST_ORDER_PRIORITY);
        irq_set_exclusive_handler(KNX_DMA_IRQ, uartDmaRestart);
        irq_set_enabled(KNX_DMA_IRQ, true);
    }

#else
    SerialUART* serial = dynamic_cast<SerialUART*>(_knxSerial);

    if (serial)
    {
        if (_rxPin != UART_PIN_NOT_DEFINED)
            serial->setRX(_rxPin);

        if (_txPin != UART_PIN_NOT_DEFINED)
            serial->setTX(_txPin);

        serial->setPollingMode();
        serial->setFIFOSize(64);
    }

    _knxSerial->begin(19200, SERIAL_8E1);

    while (!_knxSerial)
        ;

#endif
}

#ifdef USE_KNX_DMA_UART
int RP2040ArduinoPlatform::uartAvailable()
{
    if (uartDmaChannel == -1)
        return 0;

    if (uartDmaRestartCount > 0)
    {
        return uartDmaRestartCount;
    }
    else
    {
        uint32_t tc = dma_channel_hw_addr(uartDmaChannel)->transfer_count;
        uartDmaAvail = tc;
        int test = uartDmaTransferCount - tc - uartDmaReadCount;
        return test;
    }
}

int RP2040ArduinoPlatform::readUart()
{
    if (!uartAvailable())
        return -1;

    int ret = uartDmaReadAddr()[0];
    // print("< ");
    // println(ret, HEX);
    uartDmaReadCount++;

    if (uartDmaRestartCount > 0)
    {
        // process previouse buffer
        uartDmaRestartCount--;

        // last char, then reset read count to start at new writer position
        if (uartDmaRestartCount == 0)
            uartDmaReadCount = 0;
    }

    return ret;
}

size_t RP2040ArduinoPlatform::writeUart(const uint8_t data)
{
    if (uartDmaChannel == -1)
        return 0;

    // print("> ");
    // println(data, HEX);
    while (!uart_is_writable(uart0))
        ;

    uart_putc_raw(uart0, data);
    return 1;
}

void RP2040ArduinoPlatform::closeUart()
{
    if (uartDmaChannel >= 0)
    {
        dma_channel_cleanup(uartDmaChannel);
        irq_set_enabled(DMA_IRQ_0, false);
        uart_deinit(uart0);
        uartDmaChannel = -1;
        uartDmaReadCount = 0;
        uartDmaRestartCount = 0;
    }
}
#endif

uint32_t RP2040ArduinoPlatform::uniqueSerialNumber()
{
    pico_unique_board_id_t id; // 64Bit unique serial number from the QSPI flash

    noInterrupts();
    rp2040.idleOtherCore();

    flash_get_unique_id(id.id); // pico_get_unique_board_id(&id);

    rp2040.resumeOtherCore();
    interrupts();

    // use lower 4 byte and convert to unit32_t
    uint32_t uid = ((uint32_t)(id.id[4]) << 24) | ((uint32_t)(id.id[5]) << 16) | ((uint32_t)(id.id[6]) << 8) | (uint32_t)(id.id[7]);

    return uid;
}

void RP2040ArduinoPlatform::restart()
{
    println("restart");
    watchdog_reboot(0, 0, 0);
}

#ifdef USE_RP2040_EEPROM_EMULATION

#pragma warning "Using EEPROM Simulation"

#ifdef USE_RP2040_LARGE_EEPROM_EMULATION

uint8_t* RP2040ArduinoPlatform::getEepromBuffer(uint32_t size)
{
    if (size % 4096)
    {
        println("KNX_FLASH_SIZE must be a multiple of 4096");
        fatalError();
    }

    if (!_rambuff_initialized)
    {
        memcpy(_rambuff, FLASHPTR, KNX_FLASH_SIZE);
        _rambuff_initialized = true;
    }

    return _rambuff;
}

void RP2040ArduinoPlatform::commitToEeprom()
{
    noInterrupts();
    rp2040.idleOtherCore();

    // ToDo: write block-by-block to prevent writing of untouched blocks
    if (memcmp(_rambuff, FLASHPTR, KNX_FLASH_SIZE))
    {
        flash_range_erase(KNX_FLASH_OFFSET, KNX_FLASH_SIZE);
        flash_range_program(KNX_FLASH_OFFSET, _rambuff, KNX_FLASH_SIZE);
    }

    rp2040.resumeOtherCore();
    interrupts();
}

#else

uint8_t* RP2040ArduinoPlatform::getEepromBuffer(uint32_t size)
{
    if (size > 4096)
    {
        println("KNX_FLASH_SIZE to big for EEPROM emulation (max. 4kB)");
        fatalError();
    }

    uint8_t* eepromptr = EEPROM.getDataPtr();

    if (eepromptr == nullptr)
    {
        EEPROM.begin(4096);
        eepromptr = EEPROM.getDataPtr();
    }

    return eepromptr;
}

void RP2040ArduinoPlatform::commitToEeprom()
{
    EEPROM.commit();
}

#endif

#else

size_t RP2040ArduinoPlatform::flashEraseBlockSize()
{
    return 16; // 16 pages x 256byte/page = 4096byte
}

size_t RP2040ArduinoPlatform::flashPageSize()
{
    return 256;
}

uint8_t* RP2040ArduinoPlatform::userFlashStart()
{
    return (uint8_t*)XIP_BASE + KNX_FLASH_OFFSET;
}

size_t RP2040ArduinoPlatform::userFlashSizeEraseBlocks()
{
    if (KNX_FLASH_SIZE <= 0)
        return 0;
    else
        return ((KNX_FLASH_SIZE - 1) / (flashPageSize() * flashEraseBlockSize())) + 1;
}

void RP2040ArduinoPlatform::flashErase(uint16_t eraseBlockNum)
{
    noInterrupts();
    rp2040.idleOtherCore();

    flash_range_erase(KNX_FLASH_OFFSET + eraseBlockNum * flashPageSize() * flashEraseBlockSize(), flashPageSize() * flashEraseBlockSize());

    rp2040.resumeOtherCore();
    interrupts();
}

void RP2040ArduinoPlatform::flashWritePage(uint16_t pageNumber, uint8_t* data)
{
    noInterrupts();
    rp2040.idleOtherCore();

    flash_range_program(KNX_FLASH_OFFSET + pageNumber * flashPageSize(), data, flashPageSize());

    rp2040.resumeOtherCore();
    interrupts();
}

void RP2040ArduinoPlatform::writeBufferedEraseBlock()
{
    if (_bufferedEraseblockNumber > -1 && _bufferedEraseblockDirty)
    {
        noInterrupts();
        rp2040.idleOtherCore();

        flash_range_erase(KNX_FLASH_OFFSET + _bufferedEraseblockNumber * flashPageSize() * flashEraseBlockSize(), flashPageSize() * flashEraseBlockSize());
        flash_range_program(KNX_FLASH_OFFSET + _bufferedEraseblockNumber * flashPageSize() * flashEraseBlockSize(), _eraseblockBuffer, flashPageSize() * flashEraseBlockSize());

        rp2040.resumeOtherCore();
        interrupts();

        _bufferedEraseblockDirty = false;
    }
}
#endif

#if defined(KNX_NETIF)
uint32_t RP2040ArduinoPlatform::currentIpAddress()
{
    return KNX_NETIF.localIP();
}
uint32_t RP2040ArduinoPlatform::currentSubnetMask()
{
    return KNX_NETIF.subnetMask();
}
uint32_t RP2040ArduinoPlatform::currentDefaultGateway()
{
    return KNX_NETIF.gatewayIP();
}
void RP2040ArduinoPlatform::macAddress(uint8_t* addr)
{
    KNX_NETIF.macAddress(addr);
}

// multicast
void RP2040ArduinoPlatform::setupMultiCast(uint32_t addr, uint16_t port)
{
    mcastaddr = IPAddress(htonl(addr));
    _port = port;
    uint8_t result = _udp.beginMulticast(mcastaddr, port);
    (void)result;

#ifdef KNX_IP_GENERIC
    // if(!_unicast_socket_setup)
    //     _unicast_socket_setup = UDP_UNICAST.begin(3671);
#endif

    // print("Setup Mcast addr: ");
    // print(mcastaddr.toString().c_str());
    // print(" on port: ");
    // print(port);
    // print(" result ");
    // println(result);
}

void RP2040ArduinoPlatform::closeMultiCast()
{
    _udp.stop();
}

bool RP2040ArduinoPlatform::sendBytesMultiCast(uint8_t* buffer, uint16_t len)
{
    // printHex("<- ",buffer, len);

    // ToDo: check if Ethernet is able to receive, return false if not
    _udp.beginPacket(mcastaddr, _port);
    _udp.write(buffer, len);
    _udp.endPacket();
    return true;
}

int RP2040ArduinoPlatform::readBytesMultiCast(uint8_t* buffer, uint16_t maxLen, uint32_t& src_addr, uint16_t& src_port)
{
    int len = _udp.parsePacket();

    if (len == 0)
        return 0;

    if (len > maxLen)
    {
        println("Unexpected UDP data packet length - drop packet");

        for (size_t i = 0; i < len; i++)
            _udp.read();

        return 0;
    }

    _udp.read(buffer, len);
    _remoteIP = _udp.remoteIP();
    _remotePort = _udp.remotePort();
    src_addr = htonl(_remoteIP);
    src_port = _remotePort;

    // print("Remote IP: ");
    // print(_udp.remoteIP().toString().c_str());
    // printHex("-> ", buffer, len);

    return len;
}

// unicast
bool RP2040ArduinoPlatform::sendBytesUniCast(uint32_t addr, uint16_t port, uint8_t* buffer, uint16_t len)
{
    IPAddress ucastaddr(htonl(addr));

    if (!addr)
        ucastaddr = _remoteIP;

    if (!port)
        port = _remotePort;

    // print("sendBytesUniCast to:");
    // println(ucastaddr.toString().c_str());

#ifdef KNX_IP_GENERIC

    if (!_unicast_socket_setup)
        _unicast_socket_setup = UDP_UNICAST.begin(3671);

#endif

    if (UDP_UNICAST.beginPacket(ucastaddr, port) == 1)
    {
        UDP_UNICAST.write(buffer, len);

        if (UDP_UNICAST.endPacket() == 0)
            println("sendBytesUniCast endPacket fail");
    }
    else
        println("sendBytesUniCast beginPacket fail");

    return true;
}
#endif

#endif
