const life = (() => {
  const SIZE = 42; // Size of (square) board
  const INTERVAL = 300; // Frequency of screen updates
  const THRESHOLD = 33; // % chance a cell will be seeded with life

  // Printable representations of cells
  let LIVE = "L";
  let DEAD = " ";

  // ---------------------------------------------------------------------
  // The rules
  /*
    -  Any live cell with fewer than two live neighbours dies, as if caused by underpopulation.
    -  Any live cell with two or three live neighbours lives on to the next generation.
    -  Any live cell with more than three live neighbours dies, as if by overpopulation.
    -  Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.  
  */

  const FEW = 2;
  const MANY = 3;
  const PLENTY = 3;

  const isLive = c => c === LIVE;
  const isUnderPopulated = n => n < FEW;
  const isOverPopulated = n => n > MANY;
  const canReproduce = n => n === PLENTY;
  const willContinue = n => !(isUnderPopulated(n)) && !(isOverPopulated(n));

  // ---------------------------------------------------------------------

  const getRandomInt = (max, min=0) => {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min)) + min; //The maximum is exclusive and the minimum is inclusive
  }

  // New boards are rows of DEAD cells, optionally seeded with LIVE cells.
  const newRow = () => Array(SIZE).fill(DEAD);
  const newBoard = shouldSeed => {
    let board = newRow().map(newRow);
    if (shouldSeed) {
      board = board.map(row => row.map(_ => getRandomInt(100) < THRESHOLD ? LIVE : DEAD))
    }
    return board;
  }

  // When counting live neighbors, make sure to stay within array bounds.
  const isWithinBounds = v => v >= 0 && v < SIZE;
  const areWithinBounds = (x, y) => isWithinBounds(x) && isWithinBounds(y);

  // Given a coordinate pair, return an array of valid neighbor coordinate pairs.
  const neighborCoordinates = (x, y) => [
    [x-1, y-1], [x, y-1], [x+1, y-1],
    [x-1, y],             [x+1, y],
    [x-1, y+1], [x, y+1], [x+1, y+1],  
  ].filter(xyArr => areWithinBounds(...xyArr));

  // Functions to produce board containing coordinate pairs for each cell.
  const coordsForRow = (r, x=0) => r.map((_, y) => [x, y]);
  const coordsForBoard = b => b.map(coordsForRow);

  // Functions to produce board containing array of valid neighbor coordinate pairs.
  const neighborCoordinatesForRow = (r, x=0) => coordsForRow(r, x).map(xyArr => neighborCoordinates(...xyArr));
  const neighborCoordinatesForBoard = b => b.map(neighborCoordinatesForRow);

  // Retrieve value of cell at specified coordinates.
  const cellAtCoorinate = (board, x, y) => board[x][y];

  // Given a board and an array of neighbor coordinates, retrieve the neighbor cells.
  const neighborCellsForCoordinateArray = (board, arrayOfNeighborCoors) => {
    return arrayOfNeighborCoors.map(neighborCoordsForCell => {
      return neighborCoordsForCell.map(coordsArray => {
        return coordsArray.map(xyArray => cellAtCoorinate(board, ...xyArray))
      })
    })
  };

  // Given a board and an array of neighbor coordinates, return a board with the live neighbor count
  // for each cell.
  const boardAsNumberOfNeighbors = (board, arrayOfNeighborCoords) => {
    return neighborCellsForCoordinateArray(board, arrayOfNeighborCoords).map(neighborCellsForRow => {
      return neighborCellsForRow.map(neighborCellsForCell => {
        return neighborCellsForCell
          .filter(isLive)
          .reduce((total, _) => total + 1, 0)
      });
    });
  };

  // Given a live neighbor count and a cell, calculate the cell's next state.
  const numberToLiveDead = (number, cell) => {
    if (isLive(cell)) {
      if (isUnderPopulated(number)) {
        return DEAD;
      } else if (isOverPopulated(number)) {
        return DEAD;
      } else if (willContinue(number)) {
        return LIVE;
      }
    } else if (canReproduce(number)){
      return LIVE;
    } else {
      return DEAD;
    }
  };

  // Given rows or boards of cells and neighbor counts, calculate next states.
  const numberRowAsLiveDeadCells = (rowOfNumbers, rowOfCells) => rowOfNumbers.map((n, i) => numberToLiveDead(n, rowOfCells[i]));
  const numberBoardAsLiveDeadCells = (boardOfNumbers, boardOfCells) => boardOfNumbers.map((r, i) => numberRowAsLiveDeadCells(r, boardOfCells[i]));

  // Functions for printing to the console.
  const printRow = r => r.join(' ');  // A little horizontal space looks better
  const printBoard = b => {
    const boardAsString = b.map(printRow).join('\n');
    console.log(boardAsString);
    return boardAsString;
  };

  // The game loop!
  const main = board => {
    // Given a board, calculate all the valid neighbor coordinates.
    const coords = neighborCoordinatesForBoard(board);
    
    let neighbors;         // The game board as number of live neighbors per cell.
    let generation = 0;    // What generation we're on.
    let curr = '';         // Current generation as a string.
    let prev = '';         // For checking if this generation is same as current - 1.
    let prevMinusOne = ''; // For checking if this generation is same as current - 2.
    
    let tick = setInterval(() => {
      neighbors = boardAsNumberOfNeighbors(board, coords);  // Calculate live neighbor counts.
      board = numberBoardAsLiveDeadCells(neighbors, board); // Calculate next state of board.
      console.clear();
      prevMinusOne = prev.slice();  // Copy string representation of current - 2.
      prev = curr.slice();          // Copy string representation of current - 1.
      curr = printBoard(board);     // Print board, saving string representation.
      console.log(`Generation ${generation}`);
      generation++;

      // If the current generation is identical to one of the previous two,
      // then we've reached the end of the simulation.
      if (curr === prev || curr === prevMinusOne) {
        clearInterval(tick);
      }
    }, INTERVAL);
  };




  if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
    // This must be node!
    module.exports = () => {
      main(newBoard(true)); // Start game with new board, seeded with some live cells.      
    }
  } else {
    LIVE = true;
    DEAD = false;
    
    return {
      newBoard,
      neighborCoordinatesForBoard,
      boardAsNumberOfNeighbors,
      isLive,
      isUnderPopulated,
      isOverPopulated,
      willContinue,
      canReproduce,
      SIZE
    }
  }
})();
