#define FASTLED_INTERRUPT_RETRY_COUNT 0
#include "FastLED.h"
#include "Palettes.h"

#define LED_TYPE WS2811
#define COLOR_ORDER GRB
#define NUM_STRIPS 4
#define NUM_LEDS_PER_STRIP 100
#define NUM_LEDS NUM_LEDS_PER_STRIP * NUM_STRIPS
CRGB leds[NUM_STRIPS * NUM_LEDS_PER_STRIP];
const uint8_t MatrixWidth = 20;        // Width of the 2D LED matrix
const uint8_t MatrixHeight = 20;       // Height of the 2D LED matrix
// ----- Game of Life Parameters
uint8_t BRIGHTNESS = 64;              // Initial brightness (0-255)
int generation = 0;                  // Track the current generation
int PaletteIndex;					// Index of the current color palette
int ChangeCount = 0;                 // Number of cell changes in the current generation
int previousChangeCount = 0;         // Number of cell changes in the previous generation
int minorChanges = 0;                // Counts consecutive generations with minor changes
uint8_t density = 60;                 // Percentage chance a cell is initially alive
uint8_t fading_step = 8;             // Step size for fading out

// Equilibrium Detection Constants
#define MAX_MINOR_CHANGES 16 // Max changes allowed before considering the pattern stable
#define FADE_OUT_STEPS 50   // Steps for fading out LEDs when stable
class Cell {
public:
    bool alive = 1;                 // Current state (1 = alive, 0 = dead)
    bool prev = 1;                  // Previous state (used to calculate next state)
    uint8_t color_index = 0;        // Index into the color palette for this cell
};
Cell world[MatrixWidth][MatrixHeight];	// 2D array of cells representing the entire matrix

void randomFillWorld() {	//Initialize World Randomly
    for (int x = 0; x < MatrixWidth; x++) {      // Iterate through each column
        for (int y = 0; y < MatrixHeight; y++) {   // Iterate through each row
            if (random(100) < density) {
                world[x][y].alive = 1;        // Cell is alive
                world[x][y].color_index = 0;  // Reset its color index
            } else {
                world[x][y].alive = 0;        // Cell is dead
            }
            world[x][y].prev = world[x][y].alive; // Set the previous state to the current state
        }
    }
}

int neighbours(int x, int y) {	// Count Live Neighbors
    // Calculates the number of live neighbors around a cell at (x, y)
    // Wraps around edges using modulo operator (%)
    return  (world[(x + 1) % MatrixWidth][y].prev) +             // Right neighbor
            (world[x][(y + 1) % MatrixHeight].prev) +            // Bottom neighbor
            (world[(x + MatrixWidth - 1) % MatrixWidth][y].prev) +   // Left neighbor
            (world[x][(y + MatrixHeight - 1) % MatrixHeight].prev) + // Top neighbor
            // Diagonal neighbors:
            (world[(x + 1) % MatrixWidth][(y + 1) % MatrixHeight].prev) +
            (world[(x + MatrixWidth - 1) % MatrixWidth][(y + 1) % MatrixHeight].prev) +
            (world[(x + MatrixWidth - 1) % MatrixWidth][(y + MatrixHeight - 1) % MatrixHeight].prev) +
            (world[(x + 1) % MatrixWidth][(y + MatrixHeight - 1) % MatrixHeight].prev);
}

uint16_t XY( uint8_t x, uint8_t y)	// Function: Map 2D Coordinates to 1D LED Index
{
	uint16_t i;
	if (y==1 || y==3 || y==6 || y==8 || y==11 || y==13 || y==16 || y==18) {
		i = y*20+(19-x);	// Reverse the x-coordinate for specific rows (wiring adjustment)
	}
	else {
		i = y*20+x;	// Normal mapping for other rows
	}
	return i;
}

void chooseNewPalette() {
	PaletteIndex = random(gGradientPaletteCount);	// Get a random index within the range of available palettes
	currentPalette = gGradientPalettes[PaletteIndex];	// Assign the palette at the random index to currentPalette
}

void setup() {
	Serial.begin(115200);	// Start serial communication for debugging (optional)
	randomSeed(analogRead(ESP.getCycleCount())); // Seed random number generator
	// tell FastLED about the LED strip configuration
	FastLED.addLeds<LED_TYPE, 4, COLOR_ORDER>(leds, 0, NUM_LEDS_PER_STRIP).setCorrection(TypicalSMD5050);
	FastLED.addLeds<LED_TYPE, 5, COLOR_ORDER>(leds, NUM_LEDS_PER_STRIP, NUM_LEDS_PER_STRIP).setCorrection(TypicalSMD5050);
	FastLED.addLeds<LED_TYPE, 12, COLOR_ORDER>(leds, 2*NUM_LEDS_PER_STRIP, NUM_LEDS_PER_STRIP).setCorrection(TypicalSMD5050);
	FastLED.addLeds<LED_TYPE, 13, COLOR_ORDER>(leds, 3*NUM_LEDS_PER_STRIP, NUM_LEDS_PER_STRIP).setCorrection(TypicalSMD5050);
	FastLED.setBrightness(BRIGHTNESS);
	Serial.println("FastLed Setup done");
}

void loop() {
	// Initialize if it's the first generation
	if (generation == 0) {
		fill_solid((CRGB*)leds, NUM_LEDS, CRGB::Black); // Clear all LEDs to black
		randomFillWorld(); // Fill with random pattern
		chooseNewPalette();// Choose initial color palette
	}

	// Display current generation
	for (int i = 0; i < MatrixWidth; i++) {
		for (int j = 0; j < MatrixHeight; j++) {
			if (world[i][j].alive == 1) {	// If the cell is alive
				leds[XY(i, j)] = ColorFromPalette(currentPalette, world[i][j].color_index);	// Set its color from the palette
			}
		}
	}

	// Fade out dead cells (if any)
	for (int k = 0; k < BRIGHTNESS; k = k + fading_step) {	// Gradually increase the fading intensity
		for (int i = 0; i < MatrixWidth; i++) {
			for (int j = 0; j < MatrixHeight; j++) {
				if ( world[i][j].alive == 0){			// If the cell is dead
					leds[XY(i, j)].fadeToBlackBy(k);	// Gradually fade it to black
				}
			}
		}
		FastLED.show();	// Update the LEDs to show the fading effect
	}

	// Birth and death cycle - Conway's Game of Life, density 50, fading_step 6
	for (int x = 0; x < MatrixWidth; x++) {
		for (int y = 0; y < MatrixHeight; y++) {
			// Default is for cell to stay the same
			if (world[x][y].prev == 0){	//If the cell was dead in the previous step increase its color index
				world[x][y].color_index += 1;
			}
			int ncount = neighbours(x, y);	//Count live neighbors for the cell
			if ((ncount == 3) && world[x][y].prev == 0 ) {
				// A new cell is born
				world[x][y].alive = 1;
				world[x][y].color_index += 2;	// Boost the color index for new cells
			}
			else if (((ncount < 2) || (ncount > 3)) && world[x][y].prev == 1) {
				// Cell dies
				world[x][y].alive = 0;	// Update for next generation
			}
		}
	}

	// Check for equilibrium (allow for minor changes)
	int changeCount = 0;	// Reset the change counter for the current generation
	for (int x = 0; x < MatrixWidth; x++) {
		for (int y = 0; y < MatrixHeight; y++) {
			if (world[x][y].prev != world[x][y].alive) {	// If the cell's state changed...
				changeCount++;								// ...increment the change counter
			}
		}
	}

	// Check for consecutive minor changes
	if (changeCount == previousChangeCount) {
		minorChanges++;	// Increment the counter for consecutive minor changes
	}
	else {
		previousChangeCount = changeCount;	// Update previousChangeCount with the current change count
		minorChanges = 0;					 // Reset the minorChanges counter
	}

	// Copy next generation into place
	for (int x = 0; x < MatrixWidth; x++) {
		for (int y = 0; y < MatrixHeight; y++) {
			world[x][y].prev = world[x][y].alive;
		}
	}

	// Reset (with fade-out) if equilibrium reached, or increment generation
	if (minorChanges >= MAX_MINOR_CHANGES) {
		// Fade out LEDs to black
		for (int step = 0; step < FADE_OUT_STEPS; step++) {	// Loop through fade-out steps
			for (int i = 0; i < NUM_LEDS; i++) {	// Iterate through all LEDs
				leds[i].nscale8(255 - (255 * step / FADE_OUT_STEPS));
			}
			FastLED.show();	// Gradually reduce brightness
			delay(10); // Adjust for fade speed
		}
		// Reset and start new generation with a new palette
		minorChanges = 0;
		generation = 0;
		//Serial.print("======="); Serial.println(PaletteIndex);
	} else {
		generation++;
	}
}
