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

// http://avrbeginners.net/architecture/spi/spi.html
// http://maxembedded.com/2013/11/the-spi-of-the-avr
// https://ww1.microchip.com/downloads/en/AppNotes/Atmel-2585-Setup-and-Use-of-the-SPI_ApplicationNote_AVR151.pdf


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


#ifdef ECXX_AVR_HAVE_MULTIPLE_SPI_INTERFACES

    #define _spiPinSS   spiPinsSS  [_pinSet]
    #define _spiPinMOSI spiPinsMOSI[_pinSet]
    #define _spiPinMISO spiPinsMISO[_pinSet]
    #define _spiPinSCK  spiPinsSCK [_pinSet]

#else

    #define _spiPinSS   spiPinSS
    #define _spiPinMOSI spiPinMOSI
    #define _spiPinMISO spiPinMISO
    #define _spiPinSCK  spiPinSCK

    #if defined(SPCR0) && defined(SPDR0) && defined(SPSR0)
        #define _SPCR (&SPCR0)
        #define _SPDR (&SPDR0)
        #define _SPSR (&SPSR0)
    #else
        #define _SPCR (&SPCR)
        #define _SPDR (&SPDR)
        #define _SPSR (&SPSR)
    #endif

    #if defined(__AVR_ATmega324A__) || defined(__AVR_ATmega324P__ ) || defined(__AVR_ATmega324PA__)
        #define SPR0  SPR00
        #define SPR1  SPR10
        #define MSTR  MSTR0
        #define DORD  DORD0
        #define SPE   SPE0
        #define SPI2X SPI2X0
        #define SPIF  SPIF0
    #endif

#endif


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);
            // Enable SPI in master mode using the safest setting
            *_SPCR = _BV(SPE) | _BV(MSTR) | _BV(SPR1) | _BV(SPR0);
            // 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);
            // Enable SPI in slave mode using the safest setting
            *_SPCR = _BV(SPE);
            // 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
        *_SPCR &= ~_BV(SPE);
        // 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
    *_SPDR = send;

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

    while( !( *_SPSR & _BV(SPIF) ) ) {
        // 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 = *_SPDR;

    // Done
    return true;
}


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

        // Send the data
        *_SPDR = *rxtx;

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

        while( !( *_SPSR & _BV(SPIF) ) ) {
            // 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 = *_SPDR;

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

    } // while len

    // Done
    return true;
}


void SPI::ntmBegin(uint32_t frequency, BitOrder bitOrder, DataMode dataMode) const
{
    // Find the divider that resulted in frequency that is less than or equal to the requested frequency
    //
    // #   SPR1   SPR0   ~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;

    // Generate and set the SPCR bits
    //        7    6    5    4    3    2    1    0
    // SPCR : SPIE SPE  DORD MSTR CPOL CPHA SPR1 SPR0
    //                            Data_Mode Clock_Div
    uint8_t spcr = _BV(SPE)
                 | ( ( static_cast<uint8_t>(dataMode) & 0x03 ) << 2 )
                 | ( clockDivider >> 1 );

    if(_spiMode == SPIMode::Master   ) spcr |= _BV(MSTR);
    if(bitOrder == BitOrder::LSBFirst) spcr |= _BV(DORD);

    *_SPCR = spcr;

    // Set the SPSR bit
    if(clockDivider & 0x01) *_SPSR &= ~_BV(SPI2X); // The SPI2X is inverted in the table above
    else                    *_SPSR |=  _BV(SPI2X);
}


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
    *_SPDR = dummy;
    while( !( *_SPSR & _BV(SPIF) ) );
    return *_SPDR;
}


uint8_t SPI::ntmReadX00() const
{
    // Send dummy data, wait until complete, and return the received data
    *_SPDR = 0x00;
    while( !( *_SPSR & _BV(SPIF) ) );
    return *_SPDR;
}


uint8_t SPI::ntmReadXFF() const
{
    // Send dummy data, wait until complete, and return the received data
    *_SPDR = 0xFF;
    while( !( *_SPSR & _BV(SPIF) ) );
    return *_SPDR;
}


void SPI::ntmWrite(uint8_t value) const
{
    // Send the data and wait until complete
    *_SPDR = value;
    while( !( *_SPSR & _BV(SPIF) ) );
}


} // namespace eCxx
