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


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


namespace eCxx {


//
// A simple AVR SPI wrapper
//
uint8_t SPI::_initCtr       = 0;
bool    SPI::_inTransaction = false;


void SPI::begin(SPIMode mode)
{
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
    {
        // Check if it is already initialized
        if(_initCtr) {
            ++_initCtr;
            return;
        }
        _initCtr = 1;
        // Copy the SPI mode and clear the in-transaction flag
        _spiMode       = mode;
        _inTransaction = false;
        // Master mode
        if(_spiMode == SPIMode::Master) {
            // Initialize the SS pin
            Application::setPinMode(spiPinSS, Application::PinMode::Output);
            // Deselect the slave
            Application::digitalWrite(spiPinSS, true);
            // Setup port multiplexer
            PORTMUX.TWISPIROUTEA |= ECXX_SPI_PORT_MUX;
            // Enable SPI in master mode using the safest setting
            SPI0.CTRLB = (SPI_MODE_0_gc | SPI_SSD_bm   );
            SPI0.CTRLA = (SPI_ENABLE_bm | SPI_MASTER_bm);
            // Initialize the MOSI, MISO, and SCK pins
            Application::setPinMode(spiPinMOSI, Application::PinMode::Output     );
            Application::setPinMode(spiPinMISO, Application::PinMode::InputPullUp);
            Application::setPinMode(spiPinSCK,  Application::PinMode::Output     );
        }
        // Slave mode
        else {
            // Initialize the SS pin
            Application::setPinMode(spiPinSS, Application::PinMode::InputPullUp);
            PORTMUX.TWISPIROUTEA |= ECXX_SPI_PORT_MUX;
            // Enable SPI in slave mode using the safest setting
            SPI0.CTRLB = SPI_MODE_0_gc;
            SPI0.CTRLA = SPI_ENABLE_bm;
            // Initialize the MOSI, MISO, and SCK pins
            Application::setPinMode(spiPinMOSI, Application::PinMode::InputPullUp);
            Application::setPinMode(spiPinMISO, Application::PinMode::Input      );
            Application::setPinMode(spiPinSCK,  Application::PinMode::InputPullUp);
        }
    }
}


void SPI::end()
{
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
    {
        // Check if it has never been initialized
        if(!_initCtr) return;
        // Do not proceed if the counter has not reached zero
        --_initCtr;
        if(_initCtr) return;
        // End transaction as needed
        if(_inTransaction) endTransaction();
        // Disable SPI
        SPI0.CTRLA &= ~SPI_ENABLE_bm;
        // Restore the SS, MOSI, MISO, and SCK pins
        Application::setPinMode(spiPinSS,   Application::PinMode::Input);
        Application::setPinMode(spiPinMOSI, Application::PinMode::Input);
        Application::setPinMode(spiPinMISO, Application::PinMode::Input);
        Application::setPinMode(spiPinSCK,  Application::PinMode::Input);
    }
}


void SPI::beginTransaction(uint32_t frequency, BitOrder bitOrder, DataMode dataMode)
{
    // Check if it is actually already inside a transaction
    if(_inTransaction) return;
    _inTransaction = true;

    // Save SREG and disable interrupt
    _saveSREG = SREG;
    cli();

    // Set SPI configuration
    ntmBegin(frequency, bitOrder, dataMode);

    // Master mode - select the slave
    if(_spiMode == SPIMode::Master) {
        Application::digitalWrite(spiPinSS, false);
    }

    // Slave mode - set MISO to output mode
    else {
        Application::setPinMode(spiPinMISO, Application::PinMode::Output);
    }
}


void SPI::endTransaction()
{
    // Check if it actually is inside a transaction
    if(!_inTransaction) return;
    _inTransaction = false;

    // Master mode - deselect the slave
    if(_spiMode == SPIMode::Master) {
        Application::digitalWrite(spiPinSS, true);
    }

    // Slave mode - set MISO to input mode
    else {
        Application::setPinMode(spiPinMISO, Application::PinMode::Input);
    }

    // Restore SREG
    SREG = _saveSREG;
}


bool SPI::transfer(uint8_t& recv, uint8_t send) const
{
    // Send the data
    SPI0.DATA = send;

    // Wait until complete
    uint32_t timeoutDelay = Application::CommunicationTimeoutCycles;

    while( !(SPI0.INTFLAGS & SPI_RXCIF_bm) ) {
        // Yield first
        Application::yield();
        // Check timeout only in slave mode because the master is the one that generates the clock
        if( _spiMode == SPIMode::Slave && --timeoutDelay == 0 ) return false;
    }

    // Return the read data
    recv = SPI0.DATA;

    // Done
    return true;
}


bool SPI::transfer(uint8_t* rxtx, uint8_t len) const
{
    while(len) {

        // Send the data
        SPI0.DATA = *rxtx;

        // Wait until complete
        uint32_t timeoutDelay = Application::CommunicationTimeoutCycles;

        while( !(SPI0.INTFLAGS & SPI_RXCIF_bm) ) {
            // Yield first
            Application::yield();
            // Check timeout only in slave mode because the master is the one that generates the clock
            if( _spiMode == SPIMode::Slave && --timeoutDelay == 0 ) return false;
        }

        // Store the received data
        *rxtx = SPI0.DATA;

        // Adjust the variables
        ++rxtx;
        --len;

    } // while len

    // Done
    return true;
}


void SPI::ntmBegin(uint32_t frequency, BitOrder bitOrder, DataMode dataMode) const
{
    // Master mode
    if(_spiMode == SPIMode::Master) {
        // Find the divider that resulted in frequency that is less than or equal to the requested frequency
        //
        // #   PRESC[1:0]   ~SPI2X   Frequency
        // 0    0     0     0       F_CPU /   2
        // 1    0     0     1       F_CPU /   4
        // 2    0     1     0       F_CPU /   8
        // 3    0     1     1       F_CPU /  16
        // 4    1     0     0       F_CPU /  32
        // 5    1     0     1       F_CPU /  64
        // 6    1     1     0       F_CPU /  64
        // 7    1     1     1       F_CPU / 128
        //
        uint32_t clockSetting = Application::getCPUFrequency() / 2;
        uint8_t  clockDivider = 0;

        while(clockDivider < 6 && frequency < clockSetting) {
            clockSetting /= 2;
            ++clockDivider;
        }

        if(clockDivider == 6) clockDivider = 7;

        // Extract the fields
        const uint8_t spi2X    = (clockDivider & 0x01) ? 0x00 : SPI_CLK2X_bm; // The SPI2X is inverted in the table above
        const uint8_t spiPresc = (clockDivider >>   1);

        // Set SPI settings
        SPI0.CTRLB = ( static_cast<uint8_t>(dataMode) & 0x03 )
                   | SPI_SSD_bm;

        SPI0.CTRLA = SPI_ENABLE_bm
                   | (spiPresc << SPI_PRESC_gp)
                   |  spi2X
                   | SPI_MASTER_bm
                   | ( (bitOrder == BitOrder::LSBFirst) ? SPI_DORD_bm : 0x00 );
    }

    // Slave mode
    else {
        SPI0.CTRLB = ( static_cast<uint8_t>(dataMode) & 0x03 );

        SPI0.CTRLA = SPI_ENABLE_bm
                   | ( (bitOrder == BitOrder::LSBFirst) ? SPI_DORD_bm : 0 );
    }
}


void SPI::ntmSelectSlave() const
{ Application::digitalWrite(spiPinSS, false); }


void SPI::ntmDeselectSlave() const
{ Application::digitalWrite(spiPinSS, true); }


uint8_t SPI::ntmRead(uint8_t dummy) const
{
    // Send dummy data, wait until complete, and return the received data
    SPI0.DATA = dummy;
    while( !(SPI0.INTFLAGS & SPI_RXCIF_bm) );
    return SPI0.DATA;
}


uint8_t SPI::ntmReadX00() const
{
    // Send dummy data, wait until complete, and return the received data
    SPI0.DATA = 0x00;
    while( !(SPI0.INTFLAGS & SPI_RXCIF_bm) );
    return SPI0.DATA;
}


uint8_t SPI::ntmReadXFF() const
{
    // Send dummy data, wait until complete, and return the received data
    SPI0.DATA = 0xFF;
    while( !(SPI0.INTFLAGS & SPI_RXCIF_bm) );
    return SPI0.DATA;
}


void SPI::ntmWrite(uint8_t value) const
{
    // Send the data and wait until complete
    SPI0.DATA = value;
    while( !(SPI0.INTFLAGS & SPI_RXCIF_bm) );
}


} // namespace eCxx
