
import java.awt.*;
import java.awt.event.*;

import javax.swing.*;


/**
 * This program is a very simple implementation of John H. Conway's famous "Game of Life".
 * In this game, the user sets up a board that contains a grid of cells, where each cell can be 
 * either "living" or "dead".  Once the board is set up and the game is started, it runs itself.
 * The board goes through a sequence of "generations."  In each generation, every cell can
 * change its state from living to dead or vice versa, depending on the number of neighbors
 * that it has.  The rules are:
 * 
 *     1.  If a cell is dead, and if it has exactly 3 living neighbors, then the cell comes
 *         to life; if the number of neighbors is less than or greater than 3, then the dead 
 *         cell remains dead.  (That is, three living neighbors give birth to a new cell.)
 *         
 *     2.  If a cell is alive, and if it has exactly 2 or 3 living neighbors, then the cell
 *         remains alive; otherwise, it dies.  (If a cell has 0 or 1 neighbors, it dies of
 *         loneliness; if it has 4 or more neighbors, it dies of overcrowding.)
 *         
 * It is important that all these changes happen simultaneously in each generation.  When
 * counting neighbors, the 8 cells that are next to a given cell horizontally, vertically,
 * and diagonally are considered.  Ideally, the board would be infinite.  On a finite board,
 * special consideration must be given to cells that lie along the boundary.  In this program, 
 * the approach is to consider the left edge to be next to the right edge and the top edge
 * to be next to the bottom edge.  This effectively turns the board into a "torus" (the shape
 * of the surface of a doughnut), which is finite but has no boundary.
 * 
 * This class has a main() routine, so it can be run as a stand-alone application.  The main
 * program simply opens a window that shows a Life board with some control buttons along the
 * bottom.  The user creates the initial board by clicking and dragging on the board to create
 * living cells.  Clicking and dragging while holding down the right mouse button will change
 * living cells back to dead.  There is also a button that will set the state of each cell to
 * be a random value.
 */
public class Life extends JPanel implements ActionListener, MouseListener, MouseMotionListener {

    /**
     * Main program opens a window whose content pane is a JPanel belonging to class Life.
     */
    public static void main(String[] args) {
        JFrame f = new JFrame("Life");
        JPanel p = new Life();
        f.setContentPane(p);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.pack();
        f.setLocation(100,50);
        f.setVisible(true);
    }

    private final int GRID_SIZE = 100;  // Number of squares along each side of the board
                                        // (Should probably not be less than 10 or more than 200,)

    private boolean[][] alive;   // Represents the board.  alive[r][c] is true if the cell in row r, column c is alive.
    
    private MosaicPanel display;  // Displays the game to the user..  White squares are alive; black squares are dead.

    private Timer timer;  // Drives the game when the user presses the "Start" button.

    private JButton  stopGoButton;  // Button for starting and stopping the running of the game.
    private JButton  nextButton;    // Button for computing just the next generation.
    private JButton  randomButton;  // Button for filling the board randomly with each cell having a 25% chance of  being alive.
    private JButton  clearButton;   // Button for clearing the board, that is setting all the cells to "dead".
    private JButton  quitButton;    // Button for ending the program.

    
    
    /**
     * Create a life game board, initially empty.  The number of cells on each side of the grid is GRID_SIZE.
     */
    public Life() {
        alive = new boolean[GRID_SIZE][GRID_SIZE];
        setLayout(new BorderLayout(3,3));
        setBackground(Color.GRAY);
        setBorder(BorderFactory.createLineBorder(Color.GRAY,3));
        int cellSize = 600/GRID_SIZE; // Aim for about a 600-by-600 pixel board.
        display = new MosaicPanel(GRID_SIZE,GRID_SIZE,cellSize,cellSize);
        if (cellSize < 5)
            display.setGroutingColor(null);
        display.setUse3D(false);
        add(display,BorderLayout.CENTER);
        JPanel bottom = new JPanel();
        add(bottom,BorderLayout.SOUTH);
        clearButton = new JButton("Clear");
        stopGoButton = new JButton("Start");
        quitButton = new JButton("Quit");
        nextButton = new JButton("One Step");
        randomButton = new JButton("Random Fill");
        bottom.add(stopGoButton);
        bottom.add(nextButton);
        bottom.add(randomButton);
        bottom.add(clearButton);
        bottom.add(quitButton);
        stopGoButton.addActionListener(this);
        clearButton.addActionListener(this);
        quitButton.addActionListener(this);
        randomButton.addActionListener(this);
        nextButton.addActionListener(this);
        display.addMouseListener(this);
        display.addMouseMotionListener(this);
        timer = new Timer(50,this);
    }

    
    /**
     * Compute the next generation of cells.  The "alive" array is modified to reflect the
     * state of each cell in the new generation.  (Note that this method does not actually
     * draw the new board; it only sets the values in the "alive" array.  The board is
     * redrawn in the showBoard() method.)
     */
    private void doFrame() { // Compute the new state of the Life board.
        boolean[][] newboard = new boolean[GRID_SIZE][GRID_SIZE];
        for ( int r = 0; r < GRID_SIZE; r++ ) {
            int above, below; // rows considered above and below row number r
            int left, right;  // columns considered left and right of column c
            above = r > 0 ? r-1 : GRID_SIZE-1;
            below = r < GRID_SIZE-1 ? r+1 : 0;
            for ( int c = 0; c < GRID_SIZE; c++ ) {
                left =  c > 0 ? c-1 : GRID_SIZE-1;
                right = c < GRID_SIZE-1 ? c+1 : 0;
                int n = 0; // number of alive cells in the 8 neighboring cells
                if (alive[above][left])
                    n++;
                if (alive[above][c])
                    n++;
                if (alive[above][right])
                    n++;
                if (alive[r][left])
                    n++;
                if (alive[r][right])
                    n++;
                if (alive[below][left])
                    n++;
                if (alive[below][c])
                    n++;
                if (alive[below][right])
                    n++;
                if (n == 3 || (alive[r][c] && n == 2))
                    newboard[r][c] = true;
                else
                    newboard[r][c] = false;
            }
        }
        alive = newboard;
    }


    /**
     *  Sets the color of every square in the display to show whether the corresponding
     *  cell on the Life board is alive or dead. 
     */
    private void showBoard() {
        display.setAutopaint(false);  // For efficiency, prevent redrawing of individual squares.
        for (int r = 0; r < GRID_SIZE; r++) {
            for (int c = 0; c < GRID_SIZE; c++) {
                if (alive[r][c])
                    display.setColor(r,c,Color.WHITE);
                else
                    display.setColor(r,c,null);  // Shows the background color, black.
            }
        }
        display.setAutopaint(true);  // Redraw the whole board, and turn on drawing of individual squares.
    }


    /**
     * Respond to an ActionEvent from one of the control buttons or from the timer.
     */
    public void actionPerformed(ActionEvent e) {
        Object src = e.getSource();  // The object that caused the event.
        if (src == quitButton) { // End the program.
            System.exit(0);
        }
        else if (src == clearButton) {  // Clear the board.
            alive = new boolean[GRID_SIZE][GRID_SIZE];
            display.clear();
        }
        else if (src == nextButton) {  // Compute and display the next generation.
            doFrame();
            showBoard();
        }
        else if (src == stopGoButton) {  // Start or stop the game, depending on whether or not it is currenty running.
            if (timer.isRunning()) {  // If the game is currently running, stop it.
                timer.stop();  // This stops the game by turning off the timer that drives the game.
                clearButton.setEnabled(true);  // Some buttons are disabled while the game is running.
                randomButton.setEnabled(true);
                nextButton.setEnabled(true);
                stopGoButton.setText("Start");  // Change text of button to "Start", since it can be used to start the game again.
            }
            else {  // If the game is not currently running, start it.
                timer.start();  // This starts the game by turning the timee that will drive the game.
                clearButton.setEnabled(false);  // Buttons that modify the board are disabled while the game is running.
                randomButton.setEnabled(false);
                nextButton.setEnabled(false);
                stopGoButton.setText("Stop"); // Change text of button to "Stop", since it can be used to stop the game.
            }
        }
        else if (src == randomButton) { // Fill the board randomly.
            for (int r = 0; r < GRID_SIZE; r++) {
                for (int c = 0; c < GRID_SIZE; c++)
                    alive[r][c] = (Math.random() < 0.25);  // 25% probability that the cell is alive.
            }
            showBoard();
        }
        else if (src == timer) {  // Each time the timer fires, a new frame is computed and displayed.
            doFrame();
            showBoard();
        }
    }


    /**
     * The square containing the mouse comes to life or, if the right-mouse button is down, dies.
     */
    public void mousePressed(MouseEvent e) {
        if (timer.isRunning())
            return;
        int row = display.yCoordToRowNumber(e.getY());
        int col = display.yCoordToRowNumber(e.getX());
        if (row >= 0 && row < display.getRowCount() && col >= 0 && col < display.getColumnCount()) {
            if (e.isMetaDown() || e.isControlDown()) {
                display.setColor(row,col,null);
                alive[row][col] = false;
            }
            else {
                display.setColor(row,col,Color.WHITE);
                alive[row][col] = true;
            }
        }
    }


    /**
     * The square containing the mouse comes to life or, if the right-mouse button is down, dies.
     */
    public void mouseDragged(MouseEvent e) {
        mousePressed(e);  // Dragging the mouse into a square has the same effect as clicking in that square.
    }

    public void mouseClicked(MouseEvent e) { }  // Other methods required by the MouseListener and MouseMotionListener interfaces.
    public void mouseEntered(MouseEvent e) { }
    public void mouseExited(MouseEvent e) { }
    public void mouseReleased(MouseEvent e) { }
    public void mouseMoved(MouseEvent e) { }


}
