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


#include <eCxx/eCxx_Internal.h>
#include <eCxx/Platform/MAX72XXMatrixDriver.h>


namespace eCxx {


//
// Constants
//
static constexpr uint8_t OP_NOP         = 0x00; // From MAX7219/MAX7221 datasheet
static constexpr uint8_t OP_Digit0      = 0x01; // ---
static constexpr uint8_t OP_DecodeMode  = 0x09; // ---
static constexpr uint8_t OP_Intensity   = 0x0A; // ---
static constexpr uint8_t OP_ScanLimit   = 0x0B; // ---
static constexpr uint8_t OP_Shutdown    = 0x0C; // ---
static constexpr uint8_t OP_DisplayTest = 0x0F; // ---


//
// Helper functions
//
static inline void sendPulse(Application::_PinFDW& pin) __ECXX_ATTR_ALWAYS_INLINE;
static inline void sendPulse(Application::_PinFDW& pin)
{
#ifdef __XTENSA__
    __asm__ __volatile__ ( " nop\n\t nop\n\t nop\n\t " );
    pin(true );
    __asm__ __volatile__ ( " nop\n\t nop\n\t nop\n\t nop\n\t nop\n\t nop\n\t nop\n\t " );
    pin(false);
#else
    pin(true );
    pin(false);
#endif
}


static inline void sendOneBit(Application::_PinFDW& data, Application::_PinFDW& clock, bool bit) __ECXX_ATTR_ALWAYS_INLINE;
static inline void sendOneBit(Application::_PinFDW& data, Application::_PinFDW& clock, bool bit)
{
    data(bit);
    sendPulse(clock);
}


static void sendDigitOpcode(Application::_PinFDW& data, Application::_PinFDW& clock, uint8_t digitNumber)
{
    // Generate the opcode
    const uint8_t opcode = OP_Digit0 + digitNumber;

    // Send the opcode
    sendOneBit(data, clock, opcode & 0b10000000);
    sendOneBit(data, clock, opcode & 0b01000000);
    sendOneBit(data, clock, opcode & 0b00100000);
    sendOneBit(data, clock, opcode & 0b00010000);
    sendOneBit(data, clock, opcode & 0b00001000);
    sendOneBit(data, clock, opcode & 0b00000100);
    sendOneBit(data, clock, opcode & 0b00000010);
    sendOneBit(data, clock, opcode & 0b00000001);
}


static void sendDigitData(Application::_PinFDW& data, Application::_PinFDW& clock, uint8_t digitData)
{
    // Send the opcode
    sendOneBit(data, clock, digitData & 0b10000000);
    sendOneBit(data, clock, digitData & 0b01000000);
    sendOneBit(data, clock, digitData & 0b00100000);
    sendOneBit(data, clock, digitData & 0b00010000);
    sendOneBit(data, clock, digitData & 0b00001000);
    sendOneBit(data, clock, digitData & 0b00000100);
    sendOneBit(data, clock, digitData & 0b00000010);
    sendOneBit(data, clock, digitData & 0b00000001);
}


static void sendControls(Application::_PinFDW& data, Application::_PinFDW& clock, Application::_PinFDW& latch, uint8_t opcode, uint8_t param, uint16_t repeatCount)
{
    // Loop for every chained LED matrix module(s)
    for(uint16_t i = 0; i < repeatCount; ++i) {
        // Send the opcode
        sendOneBit(data, clock, opcode & 0b10000000);
        sendOneBit(data, clock, opcode & 0b01000000);
        sendOneBit(data, clock, opcode & 0b00100000);
        sendOneBit(data, clock, opcode & 0b00010000);
        sendOneBit(data, clock, opcode & 0b00001000);
        sendOneBit(data, clock, opcode & 0b00000100);
        sendOneBit(data, clock, opcode & 0b00000010);
        sendOneBit(data, clock, opcode & 0b00000001);
        // Send the parameter
        sendOneBit(data, clock, param  & 0b10000000);
        sendOneBit(data, clock, param  & 0b01000000);
        sendOneBit(data, clock, param  & 0b00100000);
        sendOneBit(data, clock, param  & 0b00010000);
        sendOneBit(data, clock, param  & 0b00001000);
        sendOneBit(data, clock, param  & 0b00000100);
        sendOneBit(data, clock, param  & 0b00000010);
        sendOneBit(data, clock, param  & 0b00000001);
    }

    // Pulse the latch pin
    sendPulse(latch);
}


static void ctlSetIntensity(Application::_PinFDW& data, Application::_PinFDW& clock, Application::_PinFDW& latch, uint8_t intensity, uint16_t repeatCount)
{
    if(intensity > MAX72XXMatrixDriver::MaxIntensity) intensity = MAX72XXMatrixDriver::MaxIntensity;

    sendControls(data, clock, latch, OP_Intensity, intensity, repeatCount);
}


static void ctlDisplayClear(Application::_PinFDW& data, Application::_PinFDW& clock, Application::_PinFDW& latch, uint16_t repeatCount)
{
    // Loop as many as the number of rows
    for(uint8_t r = 0; r < MAX72XXMatrixDriver::RowPerModule; ++r) {
        // Loop for every chained LED matrix module(s)
        for(uint16_t m = 0; m < repeatCount; ++m) {
            sendDigitOpcode(data, clock, r   );
            sendDigitData  (data, clock, 0x00);
        }
        // Pulse the latch pin
        sendPulse(latch);
    }
}


static void ctlDisplayOff(Application::_PinFDW& data, Application::_PinFDW& clock, Application::_PinFDW& latch, uint16_t repeatCount)
{
    ctlDisplayClear(data, clock, latch, repeatCount);
    sendControls(data, clock, latch, OP_Shutdown, 0, repeatCount);
}


static void ctlDisplayOn(Application::_PinFDW& data, Application::_PinFDW& clock, Application::_PinFDW& latch, uint16_t repeatCount)
{
    ctlDisplayClear(data, clock, latch, repeatCount);
    sendControls(data, clock, latch, OP_Shutdown, 1, repeatCount);
}


//
// A simple MAX7219/MAX7221-based LED matrix driver
//
MAX72XXMatrixDriver::MAX72XXMatrixDriver()
: _pinData        (0)
, _pinClock       (0)
, _pinLatch       (0)
, _rawByteBuffer  (nullptr)
, _rawByteSizeLine(0)
{}


MAX72XXMatrixDriver::~MAX72XXMatrixDriver()
{ if(_rawByteSizeLine) end(); }


bool MAX72XXMatrixDriver::begin(uint8_t pinData, uint8_t pinClock, uint8_t pinLatch, uint8_t numCol, uint8_t numRow, const uint8_t* rawByteBuffer)
{
    // Check if it is already initialized
    if(_rawByteSizeLine) return false;

    // Check the parameters
    if( !numCol || (numCol % MAX72XXMatrixDriver::ColPerModule) ||
        !numRow || (numRow % MAX72XXMatrixDriver::RowPerModule)
    ) return false;

    // Save the settings
    _pinData         = pinData;
    _pinClock        = pinClock;
    _pinLatch        = pinLatch;

    _numMod          = static_cast<uint16_t>(numCol / MAX72XXMatrixDriver::ColPerModule)
                     * static_cast<uint16_t>(numRow / MAX72XXMatrixDriver::RowPerModule);

    _rawByteBuffer   = rawByteBuffer;
    _rawByteSizeLine = _numMod * 8;

    _ringCnt         = RingCntMin;

    // Set pin modes to output and set them to low
    Application::setPinMode(_pinData,  Application::PinMode::Output);
    Application::setPinMode(_pinClock, Application::PinMode::Output);
    Application::setPinMode(_pinLatch, Application::PinMode::Output);

    Application::digitalWrite(_pinData,  false);
    Application::digitalWrite(_pinClock, false);
    Application::digitalWrite(_pinLatch, false);

    // Prepare for fast-digital-write
    Application::_preparePinFDW(_fdwData,  _pinData );
    Application::_preparePinFDW(_fdwClock, _pinClock);
    Application::_preparePinFDW(_fdwLatch, _pinLatch);

    // Initialize the chained LED matrix module(s)
    sendControls(_fdwData, _fdwClock, _fdwLatch, OP_DisplayTest, 0,                _numMod);
    sendControls(_fdwData, _fdwClock, _fdwLatch, OP_DecodeMode,  0,                _numMod);
    sendControls(_fdwData, _fdwClock, _fdwLatch, OP_ScanLimit,   RowPerModule - 1, _numMod);

    ctlSetIntensity(_fdwData, _fdwClock, _fdwLatch, DefIntensity, _numMod);
    ctlDisplayOn   (_fdwData, _fdwClock, _fdwLatch,               _numMod);

    // Done
    return true;
}


void MAX72XXMatrixDriver::end()
{
    // Check if it is not yet initialized
    if(!_rawByteSizeLine) return;

    // Turn off display
    ctlDisplayOff(_fdwData, _fdwClock, _fdwLatch, _numMod);

    // Set pin modes to input
    Application::setPinMode(_pinData,  Application::PinMode::Input);
    Application::setPinMode(_pinClock, Application::PinMode::Input);
    Application::setPinMode(_pinLatch, Application::PinMode::Input);

    // Clear the settings
    _rawByteBuffer   = nullptr;
    _rawByteSizeLine = 0;
}


void MAX72XXMatrixDriver::setRawByteBuffer(const uint8_t* rawByteBuffer)
{
    // Check if it is not yet initialized
    if(!_rawByteSizeLine) return;

    // Save the new raw byte buffer pointer
    _rawByteBuffer = rawByteBuffer;
}


void MAX72XXMatrixDriver::setIntensity(uint8_t intensity)
{
    // Check if it is not yet initialized
    if(!_rawByteSizeLine) return;

    // Set intensity
    ctlSetIntensity(_fdwData, _fdwClock, _fdwLatch, intensity, _numMod);
}


void MAX72XXMatrixDriver::update()
{
    // Check if it is not yet initialized
    if(!_rawByteSizeLine) return;

    // Send the buffer data
    const uint8_t* buff = _rawByteBuffer;

    // Loop as many as the number of rows
    for(uint8_t r = 0; r < MAX72XXMatrixDriver::RowPerModule; ++r) {
        // Loop for every chained LED matrix module(s)
        for(uint16_t m = 0; m < _numMod; ++m) {
            // Send the row number
            sendDigitOpcode(_fdwData, _fdwClock, r);
            // Send the data bits
            for(uint8_t b = 0; b < MAX72XXMatrixDriver::ColPerModule; ++b) {
                sendOneBit(_fdwData, _fdwClock, *buff++);
            }
        }
        // Pulse the latch pin
        sendPulse(_fdwLatch);
    }

    // Reset the state variable used by updateExt()
    _ringCnt = RingCntMin;
}


void MAX72XXMatrixDriver::updateExt()
{
    // Check if it is not yet initialized
    if(!_rawByteSizeLine) return;

    // Send the buffer data
    const uint8_t* buff = _rawByteBuffer;

    // Loop as many as the number of rows
    for(uint8_t r = 0; r < MAX72XXMatrixDriver::RowPerModule; ++r) {
        // Loop for every chained LED matrix module(s)
        for(uint16_t m = 0; m < _numMod; ++m) {
            // Send the row number
            sendDigitOpcode(_fdwData, _fdwClock, r);
            // Send the data bits
            for(uint8_t b = 0; b < MAX72XXMatrixDriver::ColPerModule; ++b) {
                const uint8_t val = *buff++;
                     if(val == 0         ) sendOneBit(_fdwData, _fdwClock, false          ); // LED off
                else if(val == 255       ) sendOneBit(_fdwData, _fdwClock, true           ); // LED on
                else if(val >  RingCntMax) sendOneBit(_fdwData, _fdwClock, true           ); // LED on
                else                       sendOneBit(_fdwData, _fdwClock, val == _ringCnt); // LED conditionally on
            }
        }
        // Pulse the latch pin
        sendPulse(_fdwLatch);
    }

    // Update the extended-condition counter
    ++_ringCnt;
    if(_ringCnt > RingCntMax) _ringCnt = RingCntMin;
}


template <typename PointerT>
inline void MAX72XXMatrixDriver::_packedUpdated(PointerT ptr)
{
    // Check if it is not yet initialized
    if(!_rawByteSizeLine) return;

    // Loop as many as the number of rows
    for(uint8_t r = 0; r < MAX72XXMatrixDriver::RowPerModule; ++r) {
        // Loop for every chained LED matrix module(s)
        for(uint16_t m = 0; m < _numMod; ++m) {
            // Send the row number
            sendDigitOpcode(_fdwData, _fdwClock, r);
            // Send the data bits
            sendDigitData( _fdwData, _fdwClock, PGMReadCharPtrInc<uint8_t, PointerT>::_(ptr) );
        }
        // Pulse the latch pin
        sendPulse(_fdwLatch);
    }

    // Reset the state variable used by updateExt()
    _ringCnt = RingCntMin;
}


void MAX72XXMatrixDriver::packedUpdated(const uint8_t* rawByteBuffer)
{ _packedUpdated(rawByteBuffer ? rawByteBuffer : _rawByteBuffer); }


void MAX72XXMatrixDriver::packedUpdated(PGMCharPointer pgmPtr)
{ _packedUpdated(pgmPtr); }


void MAX72XXMatrixDriver::packedUpdated(PGMCharPointerFar pgmPtr)
{ _packedUpdated(pgmPtr); }


} // namespace eCxx
