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


#ifndef ECXX_PLATFORM_RAW_MEMORY_CARD_H
#define ECXX_PLATFORM_RAW_MEMORY_CARD_H


#include <eCxx/Platform/Common/SPI.h>


namespace eCxx {


//
// A simple raw-mode memory card reader and writer; it should support SD, SDHC, SDXC, MMC, and TF cards
//
// NOTE : It uses the SPI communication sub-systems.
//
class RawMemoryCard : public Uncopyable {
    public:
        enum class RMCResultCode : uint8_t {
            NoError            =  0, // No error
            NotInitialized     =  1, // The memory card has not been initialized
            ResetFailed        =  2, // Reset (go idle state) failed
            ReadCSDFailed      =  3, // Read CSD register failed
            ReadCIDFailed      =  4, // Read CID register failed
            ReadOCRFailed      =  5, // Read OCR register failed
            ReadSDStatusFailed =  6, // Read SD status failed
            ReadSBlockFailed   =  7, // Read single block failed
            ReadMBlockFailed   =  8, // Read multiple block failed
            WriteSBlockFailed  =  9, // Write single block failed
            WriteMBlockFailed  = 10, // Write multiple block failed
            StopMBlockFailed   = 11, // Failed ending multiple block read/write
            InvalidOperation   = 12, // Invalid operation
            InvalidParameter   = 13, // Invalid parameter
            UnsupportedCard    = 14  // Unsupported card
        };

        // Card type
        static constexpr uint8_t CT_MMC   = 0x01;
        static constexpr uint8_t CT_SD1   = 0x02;
        static constexpr uint8_t CT_SD2   = 0x04;
        static constexpr uint8_t CT_BLOCK = 0x80;

        // Sector size in bytes
        static constexpr uint16_t SectorSize = 512; // NOTE : 512-byte sector size should be supported by all cards

        // SPI clock frequency (the frequencies specified here seem to work for most memory cards)
        static constexpr uint32_t SPIFrequency_Init =  250000UL; // Low  frequency used for initialization
        static constexpr uint32_t SPIFrequency_Work = 8000000UL; // High frequency used for data transfer

    public:
        RawMemoryCard();
        ~RawMemoryCard();

        RMCResultCode begin();
        void end();

        inline bool initialized() const;

        RMCResultCode getSectorSize(uint16_t& ret) const;
        RMCResultCode getSectorCount(uint32_t& ret) const;
        RMCResultCode getEraseBlockSize(uint32_t& ret) const;

        RMCResultCode getCardType(uint8_t &ret) const;
        RMCResultCode getCSD(uint8_t (&ret)[16]) const;
        RMCResultCode getCID(uint8_t (&ret)[16]) const;
        RMCResultCode getOCR(uint8_t (&ret)[4]) const;
        RMCResultCode getStatus(uint8_t (&ret)[64]) const;

        // Read is always done in one sector size
        //     # If the buffer size is smaller than the sector size, the remaining bytes
        //       read from the card will be discarded.
        //     # If the buffer size is larger than the sector size, the remaining bytes
        //       within the buffer will be filled with zeroes.
        //     # The given buffer cannot be nullptr and the size cannot be zero.
        RMCResultCode readOneBlock(uint32_t sector, uint8_t* buff, uint16_t buffSize) const;

        // Write is always done in one sector size
        //     # If the buffer size is smaller than the sector size, the remaining bytes
        //       within the card will be written with zeroes.
        //     # If the buffer size is larger than the sector size, the remaining bytes
        //       within the buffer will be ignored.
        //     # The given buffer cannot be nullptr and the size cannot be zero.
        RMCResultCode writeOneBlock(uint32_t sector, const uint8_t* buff, uint16_t buffSize);

        // Streaming read is always done in a multiple of sector size
        //     # Mixing streaming read with streaming write (or any other operation) is not
        //       supported; doing so will result in undefined behavior!
        //     # If the process is ended before all the bytes are read, the remaining bytes
        //       read from the card will be discarded.
        //     # There is no need to explicitly end the process if (SectorSize * blockCount)
        //       bytes have been read. In this case, calling streamingReadEnd() will actually
        //       return an error.
        //     # The buffer size must be less than or equal to one sector size or nothing
        //       will be read; in this case, streamingReadGet() will be return 0xFFFF.
        //     # On success, streamingReadGet() will return the actual number of bytes read.
        //     # If the given buffer is a nullptr, all the bytes read from the card will be
        //       discarded.
        RMCResultCode streamingReadBegin(uint32_t sector, uint8_t blockCount) const;
        RMCResultCode streamingReadEnd() const;
        uint16_t streamingReadGet(uint8_t* buff, uint16_t buffSize) const;

        // Streaming write is always done in a multiple of sector size
        //     # Mixing streaming write with streaming read (or any other operation) is not
        //       supported; doing so will result in undefined behavior!
        //     # If the process is ended before all the bytes are written, the remaining bytes
        //       within the card will be written with zeroes.
        //     # There is no need to explicitly end the process if (SectorSize * blockCount)
        //       bytes have been written. In this case, calling streamingWriteEnd() will actually
        //       return an error.
        //     # The buffer size must be less than or equal to one sector size or nothing will
        //       be written; in this case, streamingWritePut() will be return 0xFFFF.
        //     # On success, streamingWritePut() will return the actual number of bytes written.
        //     # If the given buffer is a nullptr, zeroes will be written.
        RMCResultCode streamingWriteBegin(uint32_t sector, uint8_t blockCount);
        RMCResultCode streamingWriteEnd();
        uint16_t streamingWritePut(const uint8_t* buff, uint16_t buffSize);

        // Check if a streaming read or write operation is in progress
        inline bool streamingOperationInProgress() const
        { return !!_sBlockTotal; }

    public:
        // These two functions use a fair amount of program (flash) memory; therefore,
        // it is recommended not to use them unless absolutely necessary
        static PGMCharPointer resultCodeStr(RMCResultCode rc);
        static PGMCharPointer cardTypeStr(uint8_t ct);

    private:
        // Card type
        uint8_t _cardType;

        // State variables for streaming read and write
        mutable uint8_t  _sBlockTotal;   // Total   block
        mutable uint8_t  _sBlockCounter; // Current block counter
        mutable uint16_t _sByteCounter;  // Current byte  counter of the current block
};


inline bool RawMemoryCard::initialized() const
{ return !! _cardType; }


} // namespace eCxx


#endif
