/*
 * 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_SPECIFIC_SPI_H
#error "this file cannot be included directly"
#endif
#undef INCLUDE_ECXX_PLATFORM_SPECIFIC_SPI_H


#ifndef ECXX_PLATFORM_NODEMCU_SPI_H
#define ECXX_PLATFORM_NODEMCU_SPI_H


namespace eCxx {


//
// A simple NodeMCU SPI wrapper
//
// WARNING : All functions derived from BasicOutputWriter<T> that accept parameters with data types
//           'PGMCharPointer' and 'PGMCharPointerFar' expect the pointers to come from program
//           memory exclusively!
//
// NOTE    : Whenever possible, use the versions that accept near program pointers because those that
//           accept the far program pointers may have more overhead and restrictions.
//
/*
 * Using this implementation, NodeMCU in SPI-slave mode is register-based, half-duplex, and working in
 * a bulk of 4 or 32 bytes.
 *
 * Therefore for a master (any master) to send and receive data to a NodeMCU in SPI-slave mode, the master
 * must follow this procedure (for a bulk of four bytes):
 *
 *     # Send data
 *           + Assert the SS pin to low;
 *           + Send 0x01          via: transfer(dummy, 0x01 );
 *           + Send one byte data via: transfer(dummy, data1);
 *           + Send one byte data via: transfer(dummy, data2);
 *           + Send one byte data via: transfer(dummy, data3);
 *           + Send one byte data via: transfer(dummy, data4);
 *           + Deassert the SS pin to high.
 *
 *     # Receive data
 *           + Assert the SS pin to low;
 *           + Send 0x04          via: transfer(dummy, 0x04);
 *           + Read one byte data via: transfer(data1, 0x00);
 *           + Read one byte data via: transfer(data2, 0x00);
 *           + Read one byte data via: transfer(data3, 0x00);
 *           + Read one byte data via: transfer(data4, 0x00);
 *           + Deassert the SS pin to high.
 *
 *     WARNING : # If the NodeMCU is booting at a moment when the SPI-master deasserted the SS pin,
 *                 the NodeMCU will fail to boot.
 *               # The official Arduino SPISlave example, workaround this issue by pulsing the SS pin
 *                 before and after the transfer sequence (instead of asserting and deasserting it).
 *               # Another workaround is to use tri-state buffers on all the GPIOs. The eCxx NodeMCU
 *                 development board is using this method.
 *
 *     NOTE    : # You can also use a call to transfer(uint8_t*, 5) to send data.
 *               # You can also use a call to write() followed by transfer(uint8_t*, 4) to receive data.
 *               # To use the 32 bytes variant use the command [ 0x02, 0x00 ] for sending data and the
 *                 command [ 0x03, 0x00 ] for receiving data.
 *
 * The flow for a NodeMCU in SPI-slave mode is as follow:
 *
 *     # Receive data:
 *           + If there is any pending data to be read then canRead() will return true and
 *             the next steps can be executed;
 *           + Read four bytes data via: readFromMaster(uint8_t (&)[4]).
 *
 *     # Send data:
 *           + Sending data can only be done if canRead() return 'false';
 *           + Send four bytes data via: writeToMaster(const uint8_t (&)[4]).
 *
 *     NOTE : To use the 32 bytes variant use the functions readFromMaster(uint8_t (&)[32])
 *            and writeToMaster(const uint8_t (&)[32]).
 */
class SPI : public BasicOutputWriter<SPI> {
    public:
        using SPIMode  = eCxx::SPIMode;
        using DataMode = eCxx::SPIDataMode;

        static constexpr decltype(eCxx::DefaultSPIFrequency) DefaultSPIFrequency = eCxx::DefaultSPIFrequency;

        static constexpr uint8_t HSPI_STATUS_BUFFER_SIZE =  4;
        static constexpr uint8_t HSPI_DATA_BUFFER_SIZE   = 32;

    public:
        inline ~SPI() = default;

    public:
        void begin(SPIMode mode = SPIMode::Master);
        void end();

        // WARNING : SPI-master mode only!
        void beginTransaction(uint32_t frequency = DefaultSPIFrequency, BitOrder bitOrder = BitOrder::MSBFirst, DataMode dataMode = DataMode::Mode0);
        void endTransaction();

        bool transfer(uint8_t& recv, uint8_t send) const;
        bool transfer(uint8_t* rxtx, uint8_t len) const;

        inline bool write(uint8_t value) const;

        // WARNING : SPI-slave mode only!
        bool readFromMaster(uint8_t (&recv)[HSPI_STATUS_BUFFER_SIZE]);
        bool writeToMaster(const uint8_t (&send)[HSPI_STATUS_BUFFER_SIZE]);

        bool readFromMaster(uint8_t (&recv)[HSPI_DATA_BUFFER_SIZE]);
        bool writeToMaster(const uint8_t (&send)[HSPI_DATA_BUFFER_SIZE]);

        inline bool canRead() const;

        // WARNING : Non-transactional SPI-master mode only!
        void ntmBegin(uint32_t frequency = DefaultSPIFrequency, BitOrder bitOrder = BitOrder::MSBFirst, DataMode dataMode = DataMode::Mode0) const;

        void ntmSelectSlave() const;
        void ntmDeselectSlave() const;

        uint8_t ntmRead(uint8_t dummy) const;
        uint8_t ntmReadX00() const;
        uint8_t ntmReadXFF() const;
        void ntmWrite(uint8_t value) const;

    private:
        SPIMode _spiMode;

    private:
        union SPIDataBuffer {
            uint32_t q;
            uint8_t  b[HSPI_DATA_BUFFER_SIZE];
        };

        volatile SPIDataBuffer _buff;
        volatile uint8_t       _buffFlag;

        static void spiOnStatusRxCallback(void* arg, uint32_t status);
        static void spiOnDataRxCallback(void* arg, uint8_t (&data)[HSPI_DATA_BUFFER_SIZE]);

        static uint8_t _initCtr;
        static bool    _inTransaction;

    private:
        inline SPI() = default;

        friend inline SPI& spi_instance();
};


inline bool SPI::canRead() const
{ return !!_buffFlag; }


inline bool SPI::write(uint8_t value) const
{
    uint8_t dummy;
    return transfer(dummy, value);
}


} // namespace eCxx


#endif
