/* Conways Game of Life
  -----------------------
  iioEngine version 1.4.1
  -----------------------
*/
ConwaysGameOfLife = function( app, settings ){

  // Game Rules
  // -------------------------------------------------
  // Any live cell with fewer than two live neighbors dies
  // Any live cell with two or three live neighbors lives on to 
  //    the next generation.
  // Any live cell with more than three live neighbors dies
  // Any dead cell with exactly three live neighbors becomes a
  //    live cell

  // Input Controls
  // -------------------------------------------------
  // Create New Cell: click anywhere
  // Pause: 'p' or 'space' keys
  // Step Forward: 'right arrow' if game is paused
  // Play Again After all Cells Die: 'space'

  // Settings
  // -------------------------------------------------
  // game settings
  var settings = settings || {};
  var maxCellsInRow = 18;
  var generation = 0;
  var simulationSpeed = 10;
  var currentCellGeneration = [];
  var toroidalGrid = true;
  var loopOnStart = true;
  var gridLines = false;
  var gameColor = new iio.Color(81, 255, 0, 1);

  // grid settings
  var grid;
  var gridLastRow;
  var gridLastColumn;
  var gridWidth = app.width;
  var gridHeight = app.height;
  var largestSide = gridWidth > gridHeight ? gridHeight : gridWidth;
  var gridResolution = largestSide / maxCellsInRow;

  // app settings
  app.set({ color: 'black' });

  // Optional Initial Configurations (cool patterns)
  // -------------------------------------------------
  var createGlider = function(c, r) {
    spawnPattern([
      [c, r],
      [c + 2, r],
      [c + 1, r + 1],
      [c + 2, r + 1],
      [c + 1, r + 1],
      [c + 1, r + 2]
    ]);
  }

  var createPulsar = function(c, r) {
    var subPattern = function(offset) {
      return [
        [c + 2, r + offset], [c + offset, r + 2],
        [c + 3, r + offset], [c + offset, r + 3],
        [c + 4, r + offset], [c + offset, r + 4],
        [c + 8, r + offset], [c + offset, r + 8],
        [c + 9, r + offset], [c + offset, r + 9],
        [c + 10, r + offset], [c + offset, r + 10]
      ];
    };
    spawnPattern(subPattern(0));
    spawnPattern(subPattern(5));
    spawnPattern(subPattern(7));
    spawnPattern(subPattern(12));
  }

  // Initialization
  // -------------------------------------------------
  var init = function() {
    grid = createGrid();
    gridLastRow = Math.floor(grid.R - 1);
    gridLastColumn = Math.floor(grid.C - 1);

    // create optional starting patterns from settings
    if (settings.pulsar) {
      createPulsar(
        Math.floor(grid.C / 2) - 6,
        Math.floor(grid.R / 2) - 6
      );
    } else if (settings.glider) {
      createGlider(0, 0);
    }

    // start game
    if (loopOnStart)
      if (currentCellGeneration.length > 0)
        app.loop(simulationSpeed);
    else
      app.draw();
  }

  // Input Handling
  // -------------------------------------------------
  var onGridClick = function( grid, event, clickPos, cell ) {
    spawnInCurrentGeneration(cell);
    gameColor = iio.Color.random();
    app.draw();
  }

  app.onKeyUp = function(e, k) {
    // step one generation if app is paused
    if (k === 'right arrow') {
      if (app.paused)
        nextGeneration();
    }
    // handle pausing and restarts
    else if (k === 'space' || k === 'p') {
      if (app.paused) {
        app.unpause();
      } else {
        if (app.looping)
          app.unpause();
        else
          app.loop(simulationSpeed)
      }
    }
  }

  // Screen Resizes
  // -------------------------------------------------
  this.onResize = function(){
    // implement for dynamic screen sizes
  };

  // Object Management
  // -------------------------------------------------
  var createGrid = function() {
    return app.add( new iio.Grid({
      pos: app.center,
      width: gridWidth,
      height: gridHeight,
      color: gridLines ? 'white' : undefined,
      lineWidth: gridLines ? 2 : undefined,
      C: gridWidth / gridResolution,
      R: gridHeight / gridResolution,
      onClick: onGridClick,
      updateCellGeneration: updateCellGeneration,
      cellWillBeAlive: cellWillBeAlive
    }));
  }

  var spawn = function(cell) {
    if (cell.alive) return;
    cell.color = gameColor;
    cell.alive = true;
    return cell;
  };

  var spawnPattern = function(coordinates) {
    var vectors = createVectors(coordinates);
    var adjusted = adjustOutOfBoundsCoordinates(vectors);
    for (var i = 0; i < adjusted.length; i++) {
      spawnInCurrentGeneration(
        grid.cells[adjusted[i].x][adjusted[i].y]
      );
    }
  }

  var spawnInCurrentGeneration = function(cell) {
    var newCell = spawn(cell);
    if (newCell)
      currentCellGeneration.push(newCell);
  }

  // Main Loop
  // -------------------------------------------------
  this.onUpdate = function() {
    nextGeneration();
    if (currentCellGeneration.length === 0)
      return false;
  }

  var nextGeneration = function() {
    generation++;

    var allCellsAndNeighborsMap = {}
    currentCellGeneration.forEach(function(cell) {
      if (!allCellsAndNeighborsMap['' + cell.c]) {
        allCellsAndNeighborsMap['' + cell.c] = {};
      }
      allCellsAndNeighborsMap['' + cell.c]['' + cell.r] = true;

      var neighborCoordinates = getNeighborCoordinates(cell);
      neighborCoordinates =
        adjustOutOfBoundsCoordinates(neighborCoordinates);

      neighborCoordinates.forEach(function(vector) {
        if (!allCellsAndNeighborsMap['' + vector.x]) {
          allCellsAndNeighborsMap['' + vector.x] = {};
        }
        allCellsAndNeighborsMap['' + vector.x]['' + vector.y] = true;
      });
    });

    var cellCoordinates = 
      calculateNextGenerationCoordinates(allCellsAndNeighborsMap);
    currentCellGeneration =
      grid.updateCellGeneration(cellCoordinates);

    app.draw();
  };

  // Coordinate Helpers
  // -------------------------------------------------
  var createVectors = function(coordinates) {
    var vs = []
    for (var i = 0; i < coordinates.length; i++)
      vs.push(new iio.Vector(coordinates[i][0], coordinates[i][1]));
    return vs;
  }

  var getNeighborCoordinates = function(cell) {
    return createVectors([
      [cell.c - 1, cell.r - 1],
      [cell.c, cell.r - 1],
      [cell.c + 1, cell.r - 1],
      [cell.c - 1, cell.r],
      [cell.c + 1, cell.r],
      [cell.c - 1, cell.r + 1],
      [cell.c, cell.r + 1],
      [cell.c + 1, cell.r + 1]
    ]);
  }

  var adjustOutOfBoundsCoordinates = function(coordinates) {
    var adjusted = [];
    for (var i = 0; i < coordinates.length; i++) {
      var x = coordinates[i].x
      var y = coordinates[i].y

      if (toroidalGrid) {
        if (x < 0) x = gridLastColumn;
        if (y < 0) y = gridLastRow;
        if (x > gridLastColumn) x = 0
        if (y > gridLastRow) y = 0
      } else {
        if (x < 0) continue;
        if (y < 0) continue;
        if (x > gridLastColumn) continue;
        if (y > gridLastRow) continue;
      }
      adjusted.push(new iio.Vector(x, y))
    }
    return adjusted;
  }

  var calculateNextGenerationCoordinates = function(map) {
    var nextGenerationCoordinates = [];
    for (var column in map) {
      for (var row in map[column]) {
        var vector = new iio.Vector(
          parseInt(column, 10),
          parseInt(row, 10)
        );
        var cell = grid.cells[vector.x][vector.y];
        if (grid.cellWillBeAlive(cell))
          nextGenerationCoordinates.push(
            new iio.Vector(cell.c, cell.r)
          );
      }
    }
    return nextGenerationCoordinates;
  }

  // Custom Grid Functions
  // -------------------------------------------------
  var updateCellGeneration = function(coordinates) {
    this.clear(true);
    var nextCellGeneration = [];
    for (var i = 0; i < coordinates.length; i++) {
      var newCell =
        spawn(this.cells[coordinates[i].x][coordinates[i].y]);
      if (newCell)
        nextCellGeneration.push(newCell);
    }
    return nextCellGeneration;
  }

  var cellWillBeAlive = function(cell) {
    var lifeSum = 0;
    var neighborCoordinates = getNeighborCoordinates(cell)
    neighborCoordinates =
      adjustOutOfBoundsCoordinates(neighborCoordinates);

    if (cell.alive)
      lifeSum++;

    for (var i = 0; i < neighborCoordinates.length; i++) {
      var neighborCell =
        this.cells[neighborCoordinates[i].x]
                  [neighborCoordinates[i].y]
      if (neighborCell.alive)
        lifeSum++;   
    }

    if (lifeSum === 3)
      return true;
    if (lifeSum === 4 && cell.alive)
      return true;
    return false;
  }

  // start game
  // -------------------------------------------------
  init();
}
iio.start([ ConwaysGameOfLife, { pulsar: true } ]);