/*
 * Copyright (C) 2020-2021 Aloysius Indrayanto
 *
 * This file is part of the eCxx library, see LICENSE file for the license details.
 */

/*
 * ----------------------------------------------------------------------------------
 * The code in this file is based on:
 *     uart.h & uart.cpp - esp8266 UART HAL for Arduino
 * ----------------------------------------------------------------------------------
 * Copyright (C) 2014 Ivan Grokhotkov. All rights reserved.
 *
 * This file is part of the esp8266 core for Arduino environment.
 *
 * This library is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any later
 * version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along
 * with this library; if not, write to the Free Software Foundation, Inc., 51
 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 * ----------------------------------------------------------------------------------
 */


#ifndef INCLUDE_ECXX_NODEMCU_UART_IMPL
#error "this file cannot be compiled directly"
#endif
#undef INCLUDE_ECXX_NODEMCU_UART_IMPL


#include <uart_register.h>


namespace eCxx {


//
// A simple NodeMCU UART wrapper
//
uint8_t UART::_currentUARTDebugNo = UART::UART0;


UART::UART(uint8_t uartNo)
: _uartNo     (uartNo)
, _uartDebugNo(UARTN)
, _baudrate   (0)
, _uart0AltPin(false)
, _initialized(false)
, _rxOverrun  (false)
, _rxError    (false)
{}


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


void UART::begin(uint32_t baudrate, DataFormat dataFormat, bool invertUART0, bool useUART0AltPin)
{
    // Ensure that this UART has been uninitialized (in case it has been initialized before)
    end();

    // Clear the Rx buffer and flags
    _rxBuff.clear();

    _rxOverrun = false;
    _rxError   = false;

    // Save the current debugging UART and disable debugging via this UART
    _uartDebugNo = getDebug();

    if(_uartDebugNo == _uartNo) setDebug(UARTN);

    // Configure the UART's pin(s)
    switch(_uartNo) {

        // UART0
        case UART0:
            ETS_UART_INTR_DISABLE();
            ETS_UART_INTR_ATTACH(nullptr, nullptr);
            _uart0AltPin = useUART0AltPin;
            if( !_uart0AltPin ) {
                NodeMCU::pinMode(NodeMCU::UART0_RxDef, SPECIAL   );
                NodeMCU::pinMode(NodeMCU::UART0_TxDef, FUNCTION_0);
                IOSWAP &= ~_BV(IOSWAPU0);
            }
            else {
                NodeMCU::pinMode(NodeMCU::UART0_RxAlt, FUNCTION_4);
                NodeMCU::pinMode(NodeMCU::UART0_TxAlt, FUNCTION_4);
                IOSWAP |= _BV(IOSWAPU0);
            }
            break;

        // UART1 (Tx only)
        case UART1:
            NodeMCU::pinMode(NodeMCU::UART1_Tx, SPECIAL);
            break;

        // Invalid
        default:
            return;

    } // switch _uartNo

    // Set baudrate
    _baudrate    = baudrate;
    USD(_uartNo) = UART_CLK_FREQ / _baudrate;

    // Set data format
    switch(dataFormat) {
        case DataFormat::_8N1 : USC0(_uartNo) = UART_NB_BIT_8 | UART_PARITY_NONE | UART_NB_STOP_BIT_1; break;
        case DataFormat::_8N2 : USC0(_uartNo) = UART_NB_BIT_8 | UART_PARITY_NONE | UART_NB_STOP_BIT_2; break;
        case DataFormat::_8E1 : USC0(_uartNo) = UART_NB_BIT_8 | UART_PARITY_EVEN | UART_NB_STOP_BIT_1; break;
        case DataFormat::_8E2 : USC0(_uartNo) = UART_NB_BIT_8 | UART_PARITY_EVEN | UART_NB_STOP_BIT_2; break;
        case DataFormat::_8O1 : USC0(_uartNo) = UART_NB_BIT_8 | UART_PARITY_ODD  | UART_NB_STOP_BIT_1; break;
        case DataFormat::_8O2 : USC0(_uartNo) = UART_NB_BIT_8 | UART_PARITY_ODD  | UART_NB_STOP_BIT_2; break;
        default  /* _8N1 */   : USC0(_uartNo) = UART_NB_BIT_8 | UART_PARITY_NONE | UART_NB_STOP_BIT_1; break;
    }

    // Inverse the signal lines of UART0 if requested
    if(invertUART0 && _uartNo == UART0) {
        USC0(UART0) |= _BV(UCDTRI) | _BV(UCRTSI) | _BV(UCTXI) | _BV(UCDSRI) | _BV(UCCTSI) | _BV(UCRXI);
    }

    // Reset the UART
    USC0(_uartNo) |=  ( _BV(UCRXRST) | _BV(UCTXRST) );
    USC0(_uartNo) &= ~( _BV(UCRXRST) | _BV(UCTXRST) );
    USC1(_uartNo)  = 0;
    USIC(_uartNo)  = 0xFFFF;
    USIE(_uartNo)  = 0;

    // Initialize UART0 Rx interrupt
    if(_uartNo == UART0) {
        USC1(UART0) = (16 << UCFFT);
        USIC(UART0) = 0xFFFF;
        USIE(UART0) = _BV(UIFF) | _BV(UIOF) | _BV(UIFR) | _BV(UIPE) | _BV(UITO);
        ETS_UART_INTR_ATTACH(&UART::rx_isr, this);
        ETS_UART_INTR_ENABLE();
    }

    // Set flag
    _initialized = true;
}


void UART::end()
{
    // Simply return if this UART has never been initialized before
    if(!_initialized) return;

    // Ensure all outgoing data are transmitted
    flush();

    // Uninitialize UART0 Rx interrupt
    if(_uartNo == UART0) {
        ETS_UART_INTR_DISABLE();
        USC1(UART0) = 0;
        USIC(UART0) = 0xFFFF;
        USIE(UART0) = 0;
        ETS_UART_INTR_ATTACH(nullptr, nullptr);
    }

    // Restore the UART's pin(s)
    switch(_uartNo) {

        // UART0
        case UART0:
            if( !_uart0AltPin ) {
                NodeMCU::pinMode(NodeMCU::UART0_RxDef, INPUT);
                NodeMCU::pinMode(NodeMCU::UART0_TxDef, INPUT);
            }
            else {
                NodeMCU::pinMode(NodeMCU::UART0_RxAlt, INPUT);
                NodeMCU::pinMode(NodeMCU::UART0_TxAlt, INPUT);
            }
            break;

        // UART1 (Tx only)
        case UART1:
            NodeMCU::pinMode(NodeMCU::UART1_Tx, INPUT);
            break;

        // Invalid
        default:
            return;

    } // switch _uartNo


    // Restore the debugging UART
    setDebug(_uartDebugNo);

    // Reset flag
    _initialized = false;
}


bool UART::changeBaudrate(uint32_t baudrate)
{
    // Simply return if this UART has never been initialized before
    if(!_initialized) return false;

    // Set baudrate
    _baudrate    = baudrate;
    USD(_uartNo) = UART_CLK_FREQ / _baudrate;

    // Done
    return true;
}


bool UART::canRead() const
{
    // Simply return if this UART has never been initialized before
    if(!_initialized) return false;

    // Check if the Rx buffer is empty
    ETS_UART_INTR_DISABLE();

    const bool empty = _rxBuff.empty();

    ETS_UART_INTR_ENABLE();

    // Return 'true' if the Rx buffer is not empty or there is data in the hardware FIFO
    return (!empty) || ( ( USS(_uartNo) >> USRXC ) & 0xFF );
}


bool UART::read(uint8_t& value)
{
    // Simply return if this UART has never been initialized before
    if(!_initialized) return false;

    ETS_UART_INTR_DISABLE();

    // Try to read from the Rx buffer first
    bool result = _rxBuff.get(value);

    // If the Rx buffer is empty, try to read from the hardware FIFO
    if( !result && ( ( USS(_uartNo) >> USRXC ) & 0xFF ) ) {
        value  = USF(_uartNo);
        result = true;
    }

    ETS_UART_INTR_ENABLE();

    return result;
}


bool UART::peek(uint8_t& value) const
{
    // Simply return if this UART has never been initialized before
    if(!_initialized) return false;

    ETS_UART_INTR_DISABLE();

    // If the Rx buffer is empty, try to fill it first
    if( _rxBuff.empty() ) {
        while( ( USS(_uartNo) >> USRXC ) & 0xFF ) {
            // Get one byte
            const uint8_t c = USF(_uartNo);
            // Check if the buffer is full
            if( _rxBuff.full() ) {
                // Set flag
                _rxOverrun = true;
#ifdef ECXX_NODEMCU_UART_RX_DISCARD_OLDEST
                // Discard the oldest byte
                uint8_t dummy;
                _rxBuff.get(dummy);
#else
                // Discard the newest byte
                break;
#endif
            }
            // Put the one byte into the Rx buffer
            _rxBuff.put(c);
        }
    }

    // Peek from the Rx buffer
    const bool result = _rxBuff.peek(value);

    ETS_UART_INTR_ENABLE();

    return result;
}


bool UART::canWrite() const
{
    // Simply return if this UART has never been initialized before
    if(!_initialized) return false;

    // Check if the hardware FIFO still has some space left
    static constexpr uint8_t UART_TX_FIFO_SIZE = 0x80;

    return ( UART_TX_FIFO_SIZE - ( ( USS(_uartNo) >> USTXC ) & 0xFF ) ) != 0;
}


bool UART::write(uint8_t value)
{
    // Simply return if this UART has never been initialized before
    if(!_initialized) return false;

    // Write the value
    while( ( ( USS(_uartNo) >> USTXC ) & 0xFF ) >= 0x7F );

    USF(_uartNo) = value;

    return true;
}


void UART::flush()
{
    // Simply return if this UART has never been initialized before
    if(!_initialized) return;

    // Wait until the hardware FIFO is empty
    while( ( ( USS(_uartNo) >> USTXC ) & 0xFF ) > 0 ) Application::yield();

    const uint32_t dBit = ( ( ( USC0(_uartNo) % 16 ) >> 2 ) + 5 );
    const uint32_t pBit = (   ( USC0(_uartNo) %  4 )            );
    const uint32_t sBit = ( ( ( USC0(_uartNo)      ) >> 5 ) + 1 );

    Application::delayUS( (dBit + pBit + sBit + 1) * 1000000UL / _baudrate + 1);
}


bool UART::rxOverrun() const
{
    if(!_initialized || !_rxOverrun) return false;

    _rxOverrun = false;
    return true;
}


bool UART::rxError() const
{
    if(!_initialized || !_rxError) return false;

    _rxError = false;
    return true;
}


uint32_t UART::detectBaudrate(uint16_t timeoutMS)
{
    // Initialize the UART using the default baudrate first
    begin(115200);

    // Start baudrate detection
    USA(_uartNo) &= ~( ( UART_GLITCH_FILT << UART_GLITCH_FILT_S ) | UART_AUTOBAUD_EN );
    USA(_uartNo)  =  ( ( 0x08             << UART_GLITCH_FILT_S ) | UART_AUTOBAUD_EN );

    // Attempt to detect the baudrate until timeout
    const uint32_t start         = Application::millis();
          uint32_t foundBaudrate = 0;

    while(Application::millis() - start < timeoutMS) {
        // Detect the baudrate
        Application::yield();
        const int32_t divisor = uart_baudrate_detect(_uartNo, 1);
        if(!divisor) continue;
        const int32_t baudrate = UART_CLK_FREQ / divisor;
        // Find the nearest standard baudrate
        static constexpr int32_t defaultBaudrate[] = {
            300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 74880, 115200, 230400, 256000, 460800, 921600, 1843200, 3686400
        };
        for(size_t i = 1; i < eCxx::arraySize(defaultBaudrate); ++i) {
            if(baudrate <= defaultBaudrate[i]) {
                if(baudrate - defaultBaudrate[i - 1] < defaultBaudrate[i] - baudrate) --i;
                foundBaudrate = defaultBaudrate[i];
                break;
            }
        }
    }

    // Uninitialize the UART
    end();

    // Detection has failed
    return foundBaudrate;
}


void UART::setDebug(uint8_t uartNo)
{
    // Save the debugging UART number
    _currentUARTDebugNo = uartNo;

    // Configure the debugging UART
    switch(_currentUARTDebugNo) {
        case UART0:
            uart_buff_switch(HW_UART_0);
            system_set_os_print(1);
            ets_install_putc1(&uart0WriteChar);
            break;

        case UART1:
            uart_buff_switch(HW_UART_1);
            system_set_os_print(1);
            ets_install_putc1(&uart1WriteChar);
            break;

        default:
            uart_buff_switch(HW_UART_0);
            system_set_os_print(0);
            ets_install_putc1(&uartIgnoreChar);
            break;

    } // switch _currentUARTDebugNo
}


void UART::uartIgnoreChar(char)
{}


void UART::uart0WriteChar(char c)
{
    while( ( ( ( USS(UART0) >> USTXC ) & 0xFF ) >= 0x7F ) ) Application::yield();

    USF(UART0) = c;
}


void UART::uart1WriteChar(char c)
{
    while( ( ( ( USS(UART1) >> USTXC ) & 0xFF ) >= 0x7F ) ) Application::yield();

    USF(UART1) = c;
}


void UART::rx_isr(void* arg)
{
    // Get the UART wrapper instance
    UART* uart = static_cast<UART*>(arg);

    // Copy the byte(s)
    const uint32_t usis = USIS(uart->_uartNo);

    if( usis & _BV(UIFF) ) {
        while( ( USS(uart->_uartNo) >> USRXC ) & 0xFF ) {
            // Get one byte
            const uint8_t c = USF(uart->_uartNo);
            // Check if the buffer is full
            if( uart->_rxBuff.full() ) {
                // Set flag
                uart->_rxOverrun = true;
#ifdef ECXX_NODEMCU_UART_RX_DISCARD_OLDEST
                // Discard the oldest byte
                uint8_t dummy;
                uart->_rxBuff.get(dummy);
#else
                // Discard the newest byte
                break;
#endif
            }
            // Put the one byte into the Rx buffer
            uart->_rxBuff.put(c);
        }
    }

    // Check for receive overflow and error
    if( usis & ( _BV(UIOF)                         ) ) uart->_rxOverrun = true;
    if( usis & ( _BV(UIFR) | _BV(UIPE) | _BV(UITO) ) ) uart->_rxError   = true;

    USIC(uart->_uartNo) = usis;
}


} // namespace eCxx
