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


#include <eCxx/eCxx_ColorSpace.h>
#include <eCxx/eCxx_LEDDriver.h>

#include "AVR_Common.h"


//
// Check the RAM size
//
#if RAM_SIZE < 1024
#warning "this test application is meant for AVR MCU with at least 1 kB of RAM"
#endif


//
// Pin numbers
//
static constexpr eCxx::uint8_t PIN_DATA  = 5;
static constexpr eCxx::uint8_t PIN_CLOCK = 6;
static constexpr eCxx::uint8_t PIN_LATCH = 7;


//
// Instantiate the MAX72XX LED matrix driver
//
static eCxx::MAX72XXMatrixDriver max72xx;


//
// Animation-related variables
//

// Create a 32x16 LED matrix for John Conway's Game of Life and the needed line buffers
static constexpr eCxx::uint8_t XPixelSize = 32;
static constexpr eCxx::uint8_t YPixelSize = 16;

typedef eCxx::ColorFX2D<eCxx::WColor, XPixelSize, YPixelSize, eCxx::CBP::InterRowMajor, eCxx::CBP::RawBuffer, 0, 0, 8> CFXGameBuffer;
typedef eCxx::ColorFX1D<eCxx::WColor, XPixelSize,                                       eCxx::CBP::RawBuffer         > CFXLineBuffer;

static CFXGameBuffer cfx;
static CFXLineBuffer line0, line1, line2;

// Colors for dead and live cell
static const eCxx::WColor& colDead = eCxx::WColor(0);
static const eCxx::WColor& colLive = eCxx::WColor(1);

// Initial cells' state (based on Tumbler from https://bitstorm.org/gameoflife)
static const eCxx::uint8_t PROGMEM initialData[XPixelSize * YPixelSize] = {
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,1,1,0,1,1,0,0,0,0,0, 0,0,0,0,1,1,0,0,0,1,1,0,0,0,0,0,
    0,0,0,0,0,0,1,1,0,1,1,0,0,0,0,0, 0,0,0,0,1,0,1,0,1,0,1,0,0,0,0,0,
    0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0, 0,0,0,0,1,0,1,0,1,0,1,0,0,0,0,0,
    0,0,0,0,0,1,0,1,0,1,0,1,0,0,0,0, 0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,
    0,0,0,0,0,1,0,1,0,1,0,1,0,0,0,0, 0,0,0,0,0,1,1,0,1,1,0,0,0,0,0,0,
    0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,0, 0,0,0,0,0,1,1,0,1,1,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};


//
// The init function
//
void eCxx_user_init()
{
    // Initialize application
    initBasicAVRApp();

    // Initialize the driver and reduce the intensity
    max72xx.begin( PIN_DATA, PIN_CLOCK, PIN_LATCH, XPixelSize, YPixelSize, cfx.colorBufferData() );
    max72xx.setIntensity( eCxx::MAX72XXMatrixDriver::DefIntensity / 2 );

    // Initialize the game buffer
    cfx.setXY<eCxx::WColor>( eCxx::stdPGMCharPointer(initialData) );

    // Update the LEDs for the first time
    max72xx.update();
}


//
// The task function
//
void eCxx_user_task()
{
    // Process event and delay
    static eCxx::uint32_t prvMillis = 0;
           eCxx::uint32_t curMillis = App::processEvent();
    if(curMillis - prvMillis < 250) return;
    else                            prvMillis = curMillis;

    //
    // Update the cells
    //

    // Shifting line buffers
    CFXLineBuffer* lbuff[3] = { &line0, &line1, &line2 };

    // Helper functions
    static auto getCellState = [](const CFXLineBuffer& lb, eCxx::int8_t x) -> eCxx::uint8_t {
        // Wrap the X coordinates
        x %= XPixelSize;
        if(x < 0) x += XPixelSize;
        // Get the color value
        return lb.get(x).w();
    };

    static auto getLiveNeighborCount = [](const CFXLineBuffer* const lbs[3], eCxx::int8_t x) -> eCxx::uint8_t {
        // Accumulate the number of living neighboring cells
        return getCellState( *lbs[0], x - 1 ) + getCellState( *lbs[0], x ) + getCellState( *lbs[0], x + 1 ) +
               getCellState( *lbs[1], x - 1 ) +                              getCellState( *lbs[1], x + 1 ) +
               getCellState( *lbs[2], x - 1 ) + getCellState( *lbs[2], x ) + getCellState( *lbs[2], x + 1 );
    };

    static auto getNextLiveState = [](const CFXLineBuffer* const lbs[3], eCxx::int8_t x) -> bool {
        // Get the cells' and neighboring cells' state
        const bool          alive     = getCellState        ( *lbs[1], x );
        const eCxx::uint8_t neighbors = getLiveNeighborCount(  lbs,    x );
        // Rule from https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
        return (neighbors == 3) || (neighbors == 2 && alive);
    };

    // Walk through the Y coordinates
    for(eCxx::uint8_t y = 0; y < YPixelSize; ++y) {
        // Initialize the line buffers when (Y == 0)
        if(!y) {
            lbuff[0]->clear();
            cfx.getHStrip( 0, 0, *lbuff[1] );
            cfx.getHStrip( 0, 1, *lbuff[2] );
        }
        // Otherwise, update the line buffers
        else {
            // Shift up the line buffers
            eCxx::swap( lbuff[0], lbuff[1] );
            eCxx::swap( lbuff[1], lbuff[2] );
            // Read data for the 3rd line buffer
            if(y + 1 < YPixelSize) cfx.getHStrip( 0, y + 1, *lbuff[2] );
            else                   lbuff[2]->clear();
        }
        // Walk through the X coordinates
        for(eCxx::uint8_t x = 0; x < XPixelSize; ++x) {
            // Determine the next state
            const bool willLive = getNextLiveState(lbuff, x);
            // Set the cell's state
            cfx.setXY(x, y, willLive ? colLive : colDead);
        }
    }

    // Update the LEDs
    max72xx.update();
}
