"use strict";

/**
 * Javascript implementation of Conway's Game of Life.
 * See http://en.wikipedia.org/wiki/Conway's_Game_of_Life for more
 * information.
 */
function GameOfLife(context, settings) {
  // Force settings to be an object.
  settings = settings || {};
  // Save the settings for use everywhere.
  this.settings = settings;

  this.board = {};
  this.board.div_class = 'game-of-life-div';
  this.board.context = context;
  this.board.cellSize = 25;
  if (this.settings.cellSize) {
    this.board.cellSize = this.settings.cellSize;
  }
  this.settings.drawLines = this.settings.drawLines || false;

  // Do an initial resize.
  this.resize();

  if (this.settings.debug) {
    console.log("Created a new GameOfLife object.");
  }
};

/**
  * Determine the number of rows and columns of the board based on the size
  * of the canvas and cellSize.
  *
  * Please note that this will reset boards state.
  */
GameOfLife.prototype.resize = function() {
  this.board.rows = Math.floor(this.getViewportHeight() / this.board.cellSize);
  if (this.settings.maxRows && this.settings.maxRows > this.board.rows) {
    this.board.rows = this.settings.maxRows;
  }
  this.board.columns = Math.floor(this.getViewportWidth() / this.board.cellSize);
  if (this.settings.maxColumns && this.settings.maxColumns > this.board.columns) {
    this.board.columns = this.settings.maxColumns;
  }

  // Init the this.board and its next state with blank values.
  this.board.nextState = new Array(this.board.rows);
  for (var i = 0; i < this.board.rows; ++i) {
    this.board[i] = new Array(this.board.columns);
    this.board.nextState[i] = new Array(this.board.columns);
    for (var j = 0; j < this.board.columns; ++j) {
      this.board[i][j] = 0;
      this.board.nextState[i][j] = 0;
    }
  }

  if (this.settings.debug) {
    console.log("Resized the board.");
  }
};

/**
 * Get the height of the container that the gameboard
 * is contained within.
 */
GameOfLife.prototype.getViewportHeight = function() {
  if (this.board.context.canvas != null) {
    return this.board.context.canvas.height;
  }
  else {
    return $(this.board.context).height();
  }
};

/**
 * Get the width of the container that the gameboard
 * is contained within.
 */
GameOfLife.prototype.getViewportWidth = function() {
  if (this.board.context.canvas != null) {
    return this.board.context.canvas.width;
  }
  else {
    return $(this.board.context).width();
  }
};

/**
 * Draw an orange line from the start coordinates to end coordinates.
 */
GameOfLife.prototype.drawLine = function(xStart, yStart, xEnd, yEnd) {
  if (this.board.context.fillStyle) {
    this.board.context.fillStyle = 'orange';
    this.board.context.beginPath();
    this.board.context.moveTo(xStart, yStart);
    this.board.context.lineTo(xEnd, yEnd);
    this.board.context.closePath();
    this.board.context.stroke();
  }
};

/**
  * Fill the given cell for the row and column with a black box.
  *
  * @param row The row of the cell.
  * @column column The column of the cell.
  */
GameOfLife.prototype.fillCell = function(row, column) {
  this.settings.color = this.settings.color || "black";
  var x = column * this.board.cellSize;
  var y = row * this.board.cellSize;

  if (this.board.context.fillStyle) {
    this.board.context.fillStyle = this.settings.color;
    this.board.context.fillRect(x, y, this.board.cellSize, this.board.cellSize);
  }
  else {
    var rect_div = $('<div></div>');
    rect_div.css('position', 'absolute')
      .addClass('game-of-life-div')
      .css('bottom', y)
      .css('left', x)
      .css('z-index', 7)
      .css('width', this.board.cellSize)
      .css('height', this.board.cellSize)
      .css('background-color', this.settings.color)
      .css('boarder', '1px solid black');
    this.board.context.append(rect_div);
  }
};

/**
  * Fill all cells that contain the value 1.
  */
GameOfLife.prototype.fillCells = function() {
  for (var i = 0; i < this.board.rows; ++i) {
    if (this.settings.drawLines) {
      this.drawLine(0, i*this.board.cellSize, this.getViewportWidth(), i*this.board.cellSize);
    }

    for (var j = 0; j < this.board.columns; ++j) {
      if (this.board[i][j] == 1) {
        this.fillCell(i, j);
      }

      if (this.settings.drawLines && i == 0) {
        this.drawLine(j*this.board.cellSize, 0, j*this.board.cellSize, this.getViewportHeight());
      }
    }
  }

  if (this.settings.debug) {
    console.log("fillCells.");
  }
};

/**
  * Determine how many adjacent neighbors are alive for this cell.
  *
  * @param row The cell's row number.
  * @param column The cell's column number.
  *
  * @return The number of adjacent alive rows.
  */
GameOfLife.prototype.getNeighbourCount = function(row, column)
{
  var count = 0;
  var columnToRight = column - 1;
  var columnToLeft = column + 1;
  var rowAbove = row - 1;
  var rowBelow = row + 1;

  if (columnToRight < this.board.columns)
  {
    count = count + this.board[row][columnToRight];
  }
  if (columnToLeft >= 0)
  {
    count = count + this.board[row][columnToLeft];
  }
  // Count the rows above us that are alive.
  if (rowAbove >= 0)
  {
    count = count + this.board[rowAbove][column];
    if (columnToRight < this.board.columns)
    {
      count = count + this.board[rowAbove][columnToRight];
    }
    if (columnToLeft >= 0)
    {
      count = count + this.board[rowAbove][columnToLeft];
    }
  }
  // Count the cells below us that are alive.
  if (rowBelow < this.board.rows)
  {
    count = count + this.board[rowBelow][column];
    if (columnToRight < this.board.columns)
    {
      count = count + this.board[rowBelow][columnToRight];
    }
    if (columnToLeft >= 0)
    {
      count = count + this.board[rowBelow][columnToLeft];
    }
  }

  return count;
}

/**
  * Determine which cells live and die.
  */
GameOfLife.prototype.determineNextState = function()
{
  for (var i = 0; i < this.board.rows; ++i)
  {
    for (var j = 0; j < this.board.columns; ++j)
    {
      var numberOfNeighbours = this.getNeighbourCount(i, j);
      // Rule 4 of Life.
      if (this.board[i][j] == 0 && numberOfNeighbours  == 3)
      {
        this.board.nextState[i][j] = 1;
      }
      else if (this.board[i][j] == 1)
      {
        // Rule 1 and Rule 3 of Life.
        if (numberOfNeighbours < 2 || numberOfNeighbours > 3)
        {
          this.board.nextState[i][j] = 0;
        }
        // Rule 2 of Life.
        else
        {
          this.board.nextState[i][j] = 1;
        }
      }
    }
  }
}

/**
  * Copy all values in nextState to the this.board.
  */
GameOfLife.prototype.moveToNextState = function()
{
  for (var i = 0; i < this.board.rows; ++i)
  {
    for (var j = 0; j < this.board.columns; ++j)
    {
      this.board[i][j] = this.board.nextState[i][j];
    }
  }
}

/**
  * Clear the canvas of everything.
  */
GameOfLife.prototype.clear = function() {
  if (this.board.context.clearRect) {
    this.board.context.clearRect(0, 0, this.getViewportWidth(), this.getViewportHeight());
  }
  else {
    this.board.context.find('div.game-of-life-div').remove();
  }

  if (this.settings.debug) {
    console.log("Clear the board.");
  }
};

/**
  * Draw the this.board. Then update it for the next call to update.
  */
GameOfLife.prototype.update = function()
{
  this.clear();
  this.fillCells();
  this.determineNextState();
  this.moveToNextState();

  if (this.settings.debug)
  {
    console.log("updating the game of life.");
  }
}

/**
  * Start the setInterval to update the game board periodically based
  * on the upateInterval given by the user, or half a second if none
  * was given.
  */
GameOfLife.prototype.start = function()
{
  var self = this;

  var updateInterval = 500;
  if (this.settings.updateInterval)
  {
    updateInterval = this.settings.updateInterval;
  }

  this.intervalID = setInterval(function() {
    self.update();
  }, updateInterval);

  if (this.settings.debug)
  {
    console.log("Start the game of life.");
  }
}

/**
  * Clear the setInterval used in the start function. This will halt the board
  * updating.
  */
GameOfLife.prototype.stop = function ()
{
  clearInterval(this.intervalID);
  this.intervalID = undefined;

  if (this.settings.debug)
  {
    console.log("Stop the game of life.");
  }
}

/**
  * Toggle between start and stop based on whether or not we
  * know the interval id.
  */
GameOfLife.prototype.toggle = function () {
  if (this.intervalID)
  {
    this.stop();
  }
  else
  {
    this.start();
  }
}

/**
 * Check if the given parameters will fit in the current board's
 * size.
 *
 * @return True if the arguments will fit.
 */
GameOfLife.prototype.canFit = function(row, column, rowStretch, colStretch)
{
  if (row+rowStretch > this.board.rows ||
      row < 0 ||
      column+colStretch > this.board.columns ||
      column < 0)
  {
    return false;
  }

  return true;
}

/**
  * Create a small glider at the given coords.
  *
  * @param row The row to start the glider at.
  * @param column The column to start the glider at.
  *
  * @return True if the glider was created.
  */
GameOfLife.prototype.createGlider = function(row, column)
{
  // Check to make sure the glider will fit.
  if (!this.canFit(row, column, 2, 2))
  {
    return false;
  }

  this.board[row+1][column] = 1;
  this.board[row+2][column+1] = 1;
  this.board[row+2][column+2] = 1;
  this.board[row+1][column+2] = 1;
  this.board[row][column+2] = 1;

  if (this.settings.debug)
  {
    console.log("Create a glider at " + row + ", " + column);
  }

  return true;
}

/**
  * Create a small glider at the given coords.
  *
  * @param row The row to start the glider at.
  * @param column The column to start the glider at.
  *
  * @return True if the glider was created.
  */
GameOfLife.prototype.createAcorn = function(row, column)
{
  // Check to make sure the glider will fit.
  if (!this.canFit(row, column, 2, 6))
  {
    return false;
  }

  this.board[row+3][column] = 1;
  this.board[row+1][column+1] = 1;
  this.board[row+3][column+1] = 1;
  this.board[row+2][column+3] = 1;
  this.board[row+3][column+4] = 1;
  this.board[row+3][column+5] = 1;
  this.board[row+3][column+6] = 1;

  if (this.settings.debug)
  {
    console.log("Create a glider at " + row + ", " + column);
  }

  return true;
}

/**
  * Create a small glider at the given coords.
  *
  * @param row The row to start the glider at.
  * @param column The column to start the glider at.
  *
  * @return True if the glider was created.
  */
GameOfLife.prototype.createBeacon = function(row, column)
{
  // Check to make sure the glider will fit.
  if (!this.canFit(row, column, 3, 3))
  {
    return false;
  }

  this.board[row][column] = 1;
  this.board[row+1][column] = 1;
  this.board[row][column+1] = 1;
  this.board[row+1][column+1] = 1;
  this.board[row+2][column+2] = 1;
  this.board[row+3][column+2] = 1;
  this.board[row+2][column+3] = 1;
  this.board[row+3][column+3] = 1;

  if (this.settings.debug)
  {
    console.log("Create a glider at " + row + ", " + column);
  }

  return true;
}

/**
  * Create a small glider gun at the given coords.
  *
  * @param row The row to start the gun at.
  * @param column The column to start the gun at.
  *
  * @return True if the gun was created.
  */
GameOfLife.prototype.createGliderGun = function(row, column)
{
  // Check to make sure the glider will fit.
  if (!this.canFit(row, column, 8, 35))
  {
    return false;
  }

  // Left square.
  this.board[row+4][column] = 1;
  this.board[row+4][column+1] = 1;
  this.board[row+5][column] = 1;
  this.board[row+5][column+1] = 1;

  // Middle almost circle.
  this.board[row+4][column+10] = 1;
  this.board[row+5][column+10] = 1;
  this.board[row+6][column+10] = 1;
  this.board[row+3][column+11] = 1;
  this.board[row+7][column+11] = 1;
  this.board[row+2][column+12] = 1;
  this.board[row+8][column+12] = 1;
  this.board[row+2][column+13] = 1;
  this.board[row+8][column+13] = 1;
  this.board[row+5][column+14] = 1;
  this.board[row+3][column+15] = 1;
  this.board[row+4][column+16] = 1;
  this.board[row+5][column+16] = 1;
  this.board[row+6][column+16] = 1;
  this.board[row+7][column+15] = 1;
  this.board[row+5][column+17] = 1;

  // Arrow
  this.board[row+2][column+20] = 1;
  this.board[row+3][column+20] = 1;
  this.board[row+4][column+20] = 1;
  this.board[row+2][column+21] = 1;
  this.board[row+3][column+21] = 1;
  this.board[row+4][column+21] = 1;
  this.board[row+1][column+22] = 1;
  this.board[row+5][column+22] = 1;
  this.board[row][column+24] = 1;
  this.board[row+1][column+24] = 1;
  this.board[row+5][column+24] = 1;
  this.board[row+6][column+24] = 1;

  // Right square
  this.board[row+2][column+34] = 1;
  this.board[row+2][column+35] = 1;
  this.board[row+3][column+34] = 1;
  this.board[row+3][column+35] = 1;

  if (this.settings.debug)
  {
    console.log("Create glider gun at " + row + ", " + column);
  }

  return true;
}

/**
 * Create the F-pentomino pattern.
 */
GameOfLife.prototype.createF_Pentomino = function(row, column)
{
  if (!this.canFit(row, column, 2, 2))
  {
    return false;
  }

  this.board[row+1][column] = 1;
  this.board[row][column+1] = 1;
  this.board[row+1][column+1] = 1;
  this.board[row+2][column+1] = 1;
  this.board[row][column+2] = 1;

  return true;
}

/**
 * Create the 1 row gun listed on the wikipedia page.
 */
GameOfLife.prototype.create1RowGun = function(row, column)
{
  if (!this.canFit(row, column, 0, 38))
  {
    return false;
  }

  this.board[row][column] = 1;
  this.board[row][column+1] = 1;
  this.board[row][column+2] = 1;
  this.board[row][column+3] = 1;
  this.board[row][column+4] = 1;
  this.board[row][column+5] = 1;
  this.board[row][column+6] = 1;
  this.board[row][column+7] = 1;

  this.board[row][column+9] = 1;
  this.board[row][column+10] = 1;
  this.board[row][column+11] = 1;
  this.board[row][column+12] = 1;
  this.board[row][column+13] = 1;

  this.board[row][column+17] = 1;
  this.board[row][column+18] = 1;
  this.board[row][column+19] = 1;

  this.board[row][column+26] = 1;
  this.board[row][column+27] = 1;
  this.board[row][column+28] = 1;
  this.board[row][column+29] = 1;
  this.board[row][column+30] = 1;
  this.board[row][column+31] = 1;
  this.board[row][column+32] = 1;

  this.board[row][column+34] = 1;
  this.board[row][column+35] = 1;
  this.board[row][column+36] = 1;
  this.board[row][column+37] = 1;
  this.board[row][column+38] = 1;

  return true;
}

/**
 * Add a cell based on the x and y coordinates of the click. This
 * will also call fillCells.
 *
 * @param clickEvent The click event to obtain x and y coords from.
 */
GameOfLife.prototype.fillByClick = function(clickEvent) {
  var x = clickEvent.clientX;
  var y = clickEvent.clientY;

  var col = Math.floor(x / this.board.cellSize);
  var row = Math.floor(y / this.board.cellSize);

  this.board[row][col] = 1;
  this.fillCells();

  if (this.settings.debug)
  {
    console.log("Fill by click on " + x + ", " + y);
  }
}

GameOfLife.prototype.destroyByClick = function(click_event) {
  var x = clickEvent.clientX;
  var y = clickEvent.clientY;

  var col = Math.floor(x / this.board.cellSize);
  var row = Math.floor(y / this.board.cellSize);

  if (this.board[row][col] == 1) {
    this.board[row][col] = 0;
  }  
  this.fillCells();
};