import controlP5.*;

ControlP5 cp5;
boolean isPaused = false;
int simulationSpeed = 10;

ArrayList<PShader> shaders = new ArrayList<PShader>();
int currentShaderIndex = 0;

int cols, rows;
int cellSize = 10;
boolean[][] grid;
boolean[][] nextGrid;

float camAngleX = PI / 4;
float camAngleY = PI / 4;
float camDistance = 500;
PVector lastMouse;

void settings() {
  size(800, 800, P3D);
}

void setup() {
  ortho();
  
  cp5 = new ControlP5(this);

  cp5.addToggle("isPaused")
     .setPosition(20, 20)
     .setSize(50, 20)
     .setLabel("Pause")
     .setValue(false);
  
  cp5.addSlider("simulationSpeed")
     .setPosition(20, 50)
     .setSize(200, 20)
     .setRange(1, 60)
     .setValue(10)
     .setLabel("Simulation Speed");
  
  cp5.addButton("resetGrid")
     .setPosition(20, 80)
     .setSize(80, 30)
     .setLabel("Reset Grid");
  
  cp5.addButton("nextShader")
     .setPosition(20, 120)
     .setSize(80, 30)
     .setLabel("Next Shader");
  
  // Load shaders
  PShader shaderOriginal = loadShader("cell_shader.frag", "cell_shader.vert");
  PShader shaderWave = loadShader("shader_wave.frag", "cell_shader.vert");
  PShader shaderNoise = loadShader("shader_noise.frag", "cell_shader.vert");
  
  shaders.add(shaderOriginal);
  shaders.add(shaderWave);
  shaders.add(shaderNoise);
  
  colorMode(HSB, 255);

  cols = width / cellSize;
  rows = height / cellSize;

  grid = new boolean[cols][rows];
  nextGrid = new boolean[cols][rows];

  resetGrid();
  
  lastMouse = new PVector(mouseX, mouseY);
}

void draw() {
  background(0);
  
  // Update simulation based on speed and pause state
  if (!isPaused && frameCount % int(60 / simulationSpeed) == 0) {
    computeNextGeneration();
  }
  
  PShader currentShader = shaders.get(currentShaderIndex);
  currentShader.set("u_time", millis() / 1000.0);
  shader(currentShader);
  
  noStroke();
  
  pushMatrix();
  translate(width / 2, height / 2, -camDistance);
  rotateX(camAngleX);
  rotateY(camAngleY);
  translate(-cols * cellSize / 2, -rows * cellSize / 2);
  
  for (int x = 0; x < cols; x++) {
    for (int y = 0; y < rows; y++) {
      if (grid[x][y]) {
        pushMatrix();
        translate(x * cellSize, y * cellSize, 0);
        
        float hue = map(y, 0, rows, 0, 255);
        fill(hue, 255, 255);
        
        box(cellSize);
        popMatrix();
      }
    }
  }
  popMatrix();
  
  resetShader();
}

void computeNextGeneration() {
  for (int x = 0; x < cols; x++) {
    for (int y = 0; y < rows; y++) {
      int neighbors = countAliveNeighbors(x, y);
      boolean alive = grid[x][y];
      
      if (alive) {
        nextGrid[x][y] = neighbors == 2 || neighbors == 3;
      } else {
        nextGrid[x][y] = neighbors == 3;
      }
    }
  }
  
  // Swap grids
  boolean[][] temp = grid;
  grid = nextGrid;
  nextGrid = temp;
}

int countAliveNeighbors(int x, int y) {
  int count = 0;
  for (int i = -1; i <= 1; i++) {
    for (int j = -1; j <= 1; j++) {
      if (i == 0 && j == 0) continue;
      int col = (x + i + cols) % cols;
      int row = (y + j + rows) % rows;
      if (grid[col][row]) count++;
    }
  }
  return count;
}

void resetGrid() {
  for (int x = 0; x < cols; x++) {
    for (int y = 0; y < rows; y++) {
      grid[x][y] = random(1) > 0.5;
    }
  }
}

void nextShader() {
  currentShaderIndex = (currentShaderIndex + 1) % shaders.size();
}

void mouseDragged() {
  if (mouseButton == LEFT) {
    float dx = mouseX - lastMouse.x;
    float dy = mouseY - lastMouse.y;
    camAngleY += dx * 0.01;
    camAngleX -= dy * 0.01;
  }
  lastMouse.set(mouseX, mouseY);
}

void mouseWheel(MouseEvent event) {
  float e = event.getCount();
  camDistance += e * 20;
}

void mousePressed() {
  if (mouseY > 150 && mouseButton == LEFT) { // Avoid ControlP5 area
    int mouseXAdjusted = mouseX - width / 2 + cols * cellSize / 2;
    int mouseYAdjusted = mouseY - height / 2 + rows * cellSize / 2;
    
    int x = int((mouseXAdjusted) / cellSize);
    int y = int((mouseYAdjusted) / cellSize);
    
    if (x >= 0 && x < cols && y >= 0 && y < rows) {
      grid[x][y] = !grid[x][y];
    }
  }
}


add a heighty map for visualisation, more beautiful shaders.