/*
 * 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:
 *     USB API for Teensy USB Development Board
 *     http://www.pjrc.com/teensy/teensyduino.html
 * ----------------------------------------------------------------------------------
 * Copyright (C) 2011 PJRC.COM, LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */


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


namespace eCxx {


//
// Constants
//
#define TRANSMIT_FLUSH_TIMEOUT  3
#define TRANSMIT_TIMEOUT       15


//
// A simple AVR virtual UART wrapper
//
VirtualUART::VirtualUART(uint8_t ifNumber)
: _ifNumber(ifNumber)
{}


void VirtualUART::begin(uint32_t baudrate, DataFormat dataFormat)
{
    // UART settings are ignored
    eCxx::ignoreUnused(baudrate  );
    eCxx::ignoreUnused(dataFormat);

    // End any previous initialization
    end();

    // Wait until the USB is initialized
    while( !usbReady() ) {
        Application::delayMS(250);
    }

    // Discard possible garbage in the RX buffer
    discardRXBuffer();

    // Initialize variables
    _prevTimeout = false;
    _peekBuff    = -1;
}


void VirtualUART::end()
{
    flush();
    discardRXBuffer();
}


bool VirtualUART::canRead() const
{
    if( !usbReady() ) return false;

    if(_peekBuff >= 0) return true;

    {
        AtomicGuard g;

        UENUM = CDC_RXD_ENDPOINT[_ifNumber];

        const uint8_t n = UEBCLX;

        if(!n) {
            const uint8_t i = UEINTX;
            if( i & _BV(RXOUTI) && !( i & _BV(RWAL) ) ) UEINTX = 0b01101011;
            return false;
        }
    }

    return true;
}


bool VirtualUART::read(uint8_t& value)
{
    if( !usbReady() ) return false;

    if(_peekBuff >= 0) {
        value     = _peekBuff;
        _peekBuff = -1;
        return true;
    }

    {
        AtomicGuard g;

        UENUM = CDC_RXD_ENDPOINT[_ifNumber];

        // Check if there is data available
        for(;;) {
            const uint8_t intBits = UEINTX;
            if( intBits & _BV(RWAL) ) {
                break;
            }
            else {
                // No data in buffer
                if( intBits & _BV(RXOUTI) ) {
                    UEINTX = 0b01101011;
                    continue;
                }
                return false;
            }
            wdt_reset();
        }

        // Take one byte out of the buffer
        value = UEDATX;

        // If the buffer is drained, release it
        if( !( UEINTX & _BV(RWAL) ) ) UEINTX = 0b01101011;
    }

    return true;
}


bool VirtualUART::peek(uint8_t& value) const
{
    if(_peekBuff < 0) {
        VirtualUART* vuart = const_cast<VirtualUART*>(this);

        uint8_t c;
        if( !vuart->read(c) ) return false;

        vuart->_peekBuff = c;
    }

    value = _peekBuff;

    return true;
}


bool VirtualUART::canWrite() const
{
    if( !usbReady() ) return false;

    {
        AtomicGuard g;

        UENUM = CDC_RXD_ENDPOINT[_ifNumber];

        return (CDC_ACM_BULKSIZE - UEBCLX) > 0;
    }
}


bool VirtualUART::write(uint8_t value)
{
    if( !usbReady() ) return false;

    {
        AtomicGuard g;

        UENUM = CDC_TXD_ENDPOINT[_ifNumber];

        // Check for previous timeout
        if(_prevTimeout) {
            if( !( UEINTX & _BV(RWAL) ) ) return false;
            _prevTimeout = false;
        }

        // Wait for the FIFO to be ready to accept data
        const uint8_t timeout = UDFNUML + TRANSMIT_TIMEOUT;

        for(;;) {
            // Break if ready to transmit
            if( UEINTX & _BV(RWAL) ) break;
            // Check for timeout
            if(UDFNUML == timeout) {
                _prevTimeout = true;
                return false;
            }
            // Has the USB gone offline?
            if( !usbReady() ) return false;
            // Repeat the check
            UENUM = CDC_TXD_ENDPOINT[_ifNumber];
            wdt_reset();
        }

        // Write the byte into the FIFO
        UEDATX = value;

        // If this operation has completed a packet, transmit it now
        if( !( UEINTX & _BV(RWAL) ) ) UEINTX = 0b00111010;

        txdFlushTimer[_ifNumber] = TRANSMIT_FLUSH_TIMEOUT;
    }

    return true;
}


void VirtualUART::flush()
{
    if( !usbReady() ) return;

    {
        AtomicGuard g;

        if(!txdFlushTimer[_ifNumber]) return;

        UENUM  = CDC_TXD_ENDPOINT[_ifNumber];
        UEINTX = 0b00111010;

        txdFlushTimer[_ifNumber] = 0;
    }
}


uint16_t VirtualUART::read(void* buffer_, uint16_t size, uint16_t timeoutMS)
{
    if( !usbReady() ) return 0;

    uint8_t* buffer = static_cast<uint8_t*>(buffer_);
    uint16_t count  = 0;

    if(_peekBuff >= 0) {
        *buffer++ = _peekBuff;
        _peekBuff = -1;
        --size;
        if(!size) return 1;
        ++count;
    }

    const uint32_t startMillis = Application::millis();

    do {

        // Has the USB gone offline?
        if( !usbReady() ) return count;

        uint8_t readSize = 0;

        {
            AtomicGuard g;

            // Check if there is no more data in buffer
            UENUM = CDC_RXD_ENDPOINT[_ifNumber];

            if( !(UEINTX & _BV(RXOUTI) ) ) break;

            // Get the number of bytes in the buffer
            readSize = UEBCLX;
            if(readSize > size) readSize = size;

            // Read the bytes
            for(uint8_t i = 0; i < readSize; ++i) *buffer++ = UEDATX;

            // If the buffer is drained, release it
            if( !( UEINTX & _BV(RWAL) ) ) UEINTX = 0b01101011;
        }

        // Adjust the number of processed bytes
        count += readSize;
        size  -= readSize;

        // Check if done
        if(!size) return count;

    } while( Application::millis() - startMillis < timeoutMS );

    return count;
}


uint16_t VirtualUART::write(const void* buffer_, uint16_t size)
{
    if( !usbReady() ) return 0;

    const uint8_t* buffer = static_cast<const uint8_t*>(buffer_);
          uint16_t count  = 0;

    {
        AtomicGuard g;

        UENUM = CDC_TXD_ENDPOINT[_ifNumber];

        // Check for previous timeout
        if(_prevTimeout) {
            if( !( UEINTX & _BV(RWAL) ) ) return count;
            _prevTimeout = false;
        }

        // Each iteration of this loop transmits a packet
        while(size) {

            // Wait for the FIFO to be ready to accept data
            const uint8_t timeout = UDFNUML + TRANSMIT_TIMEOUT;

            for(;;) {
                // Break if ready to transmit
                if( UEINTX & _BV(RWAL) ) break;
                // Check for timeout
                if(UDFNUML == timeout) {
                    _prevTimeout = true;
                    return count;
                }
                // Has the USB gone offline?
                if( !usbReady() ) return count;
                // Repeat the check
                UENUM = CDC_TXD_ENDPOINT[_ifNumber];
                wdt_reset();
            }

            // Compute how many bytes will fit into the next packet
            uint8_t writeSize = CDC_ACM_BULKSIZE - UEBCLX;
            if(writeSize > size) writeSize = size;

            // Write the bytes into the FIFO
            for(uint8_t i = 0; i < writeSize; ++i) UEDATX = *buffer++;

            // If this operation has completed a packet, transmit it now
            if( !( UEINTX & _BV(RWAL) ) ) UEINTX = 0b00111010;

            txdFlushTimer[_ifNumber] = TRANSMIT_FLUSH_TIMEOUT;

            // Adjust the number of processed bytes
            size  -= writeSize;
            count += writeSize;

        } // while size

    }

    return count;
}


#ifdef ECXX_AVR_VUART_DUMP_SETTINGS

void VirtualUART::_dumpSettings()
{
    // Get the settings
    uint32_t baud   = getVUARTBaudrate   (_ifNumber);
    uint8_t  rtsdtr = getVUARTRtsDtrState(_ifNumber);
    uint32_t windxs = getVUARTWIndexes   (_ifNumber);

    // For converting to string and sending the string to the host PC
    char  buff[32] = { 0 };
    char* ptr      = buff;

    // RTS and DTR
    do {
        const char digit = (rtsdtr % 10) + '0';
        rtsdtr /= 10;
        *ptr++ = digit;
    } while(rtsdtr != 0);
    *ptr++ = '=';
    *ptr++ = 's';
    *ptr++ = 'l';
    *ptr++ = 'c';
    *ptr++ = ' ';

    // Baudrate
    do {
        const char digit = (baud % 10) + '0';
        baud /= 10;
        *ptr++ = digit;
    } while(baud != 0);
    *ptr++ = '=';
    *ptr++ = 'r';
    *ptr++ = 'b';
    *ptr++ = ' ';

    // W-Indexes
    uint8_t wic = 0;
    do {
        const char  digit = (windxs % 16);
        const char xdigit = (digit < 10) ? (digit + '0') : (digit - 10 + 'A');
        windxs /= 16;
        *ptr++ = xdigit;
        ++wic;
    } while(windxs != 0);
    for(uint8_t i = 0; i < (8 - wic); ++i) *ptr++ = '0';
    *ptr++ = '=';
    *ptr++ = 'i';
    *ptr++ = 'w';

    // Write it to the host PC
    --ptr;
    while(ptr >= buff) {
        write(*ptr);
        --ptr;
    }
    write('\n');
}

#endif


void VirtualUART::discardRXBuffer()
{
    if( !usbReady() ) return;

    {
        AtomicGuard g;

        UENUM = CDC_RXD_ENDPOINT[_ifNumber];
        while( (UEINTX & _BV(RWAL) ) ) UEINTX = 0b01101011;
        wdt_reset();

        _peekBuff = -1;
    }
}


} // namespace eCxx
