import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.paint.Color;
import javafx.animation.AnimationTimer;

/**
 * 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.
 * 
 * The program's window shows a Life board with some control buttons beneath the board.
 * The user can create a board configuration 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.  When the program first starts, the board contains a simple configuration
 * of five living cells (the "R pentomino") that will give a long animation before settling
 * down to static patterns and simple repeaters. 
 * 
 * The board in this program is represented by an object of type MosaicCanvas, which is
 * a custom subclass of Canvas.  The program requires MosaicCanvas.java.
 */
public class Life extends Application {

    public static void main(String[] args) {
        launch(args);
    }
    //---------------------------------------------------------------------------------------

    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 MosaicCanvas lifeBoard;  // Displays the game to the user.  White squares are alive; black squares are dead.

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

    private Button  stopGoButton;  // Button for starting and stopping the running of the game.
    private Button  nextButton;    // Button for computing just the next generation.
    private Button  randomButton;  // Button for filling the board randomly with each cell having a 25% chance of  being alive.
    private Button  clearButton;   // Button for clearing the board, that is setting all the cells to "dead".
    private Button  quitButton;    // Button for ending the program.
    
    private CheckBox fastCheckbox; // When checked, the animation runs at full speed, with a new frame
                                   // generated in each call to the AnimationTimer's handle() method.
                                   // (This should be 60 frames per second.)  When not checked, there will
                                   // be at least 1/10 second between frames, giving about 6 frames per second.

    private boolean animationIsRunning;   // set to true when the timer is started, false when it is paused
    
    
    /**
     * Create a life game board, initially empty, and add it and some buttons to
     * the GUI.  Set up event handling for the buttons.
     * The number of cells on each side of the grid is GRID_SIZE.
     */
    public void start(Stage stage) {
        
        /* Create and configure the board, including setting up mouse event listeners */
        
        int cellSize = 800/GRID_SIZE; // Aim for about a 800-by-800 pixel board.
        lifeBoard = new MosaicCanvas(GRID_SIZE,GRID_SIZE,cellSize,cellSize);
        if (cellSize < 5)
            lifeBoard.setGroutingColor(null);  // Don't show grouting if cells are too small.
        lifeBoard.setUse3D(false);
        lifeBoard.setOnMousePressed( e -> mousePressed(e) );
        lifeBoard.setOnMouseDragged( e -> mouseDragged(e) );
        lifeBoard.setStyle("-fx-border-color:darkgray; -fx-border-width:3px");
        
        /* Create the buttons and checkbox.  Add action event listeners to the buttons. */
        
        clearButton = new Button("Clear");
        stopGoButton = new Button("Start");
        quitButton = new Button("Quit");
        nextButton = new Button("One Step");
        randomButton = new Button("Random Fill");
        stopGoButton.setOnAction( e -> doStopGo() );
        quitButton.setOnAction( e -> System.exit(0) );
        randomButton.setOnAction( e -> doRandom() );
        nextButton.setOnAction( e -> {
            doFrame();
            showBoard();
        });
        clearButton.setOnAction( e -> {
            alive = new boolean[GRID_SIZE][GRID_SIZE];
            showBoard();
        });
        fastCheckbox = new CheckBox("Fast");
        
        /* Create, but do not start, the animation timers.  The user has to press "Start" tp start it. */
        
        timer = new AnimationTimer() {
            final double oneTenthSecond = 1e8; // 1e8 nanoseconds = 1/10 second
            long previousTime;  // Time when a new frame was last generated.
            public void handle(long time) {
                if ( (time-previousTime) > 0.975*oneTenthSecond  || fastCheckbox.isSelected()) {
                    doFrame();
                    showBoard();
                    previousTime = time;
                }
            }
        };
        
        /* Build the scene graph. */

        HBox bottom = new HBox(20, 
                stopGoButton, fastCheckbox, nextButton, randomButton, clearButton, quitButton );
        bottom.setStyle("-fx-padding:8px; -fx-border-color:darkgray; -fx-border-width: 3px 0 0 0");
        bottom.setAlignment(Pos.CENTER);
        
        BorderPane root = new BorderPane();
        root.setCenter(lifeBoard);
        root.setBottom(bottom);
        
        /* Create the array that holds the state for every cell on the board.  Set some cells
         * to true for the "R pentomino" initial configuration, and draw the initial board. */ 
        
        alive = new boolean[GRID_SIZE][GRID_SIZE];
        alive[49][49] = true;
        alive[50][49] = true;
        alive[51][49] = true;
        alive[49][50] = true;
        alive[50][48] = true;
        showBoard();
        
        /* Set up the scene and stage, and show the window. */
        
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.setResizable(false);
        stage.setTitle("Conway's Game of Life");
        stage.show();
        
    } // end start();


    /**
     * 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() {
        lifeBoard.setAutopaint(false);  // For efficiency, prevent redrawing of individual squares.
                                        // Failure to turn off autopaint would SEVERLY slow
                                        // down the program!
        for (int r = 0; r < GRID_SIZE; r++) {
            for (int c = 0; c < GRID_SIZE; c++) {
                if (alive[r][c])
                    lifeBoard.setColor(r,c,Color.WHITE);  // alive sells are white
                else
                    lifeBoard.setColor(r,c,null);  // Shows the background color, black.
            }
        }
        lifeBoard.setAutopaint(true);  // Redraws the whole board, and turns on drawing of individual squares.
    }


    /**
     * This method is called for the button that is used to start and stop the array.
     * If the animation is running, it is paused.  If it is not running, it is started.
     * The text on the Start/Stop button is changed and some buttons are disabled and
     * enabled, depending on whether the animation is running or not.
     */
    private void doStopGo() {

        if (animationIsRunning) {  // If the game is currently running, stop it.
            timer.stop();  // This stops the game by turning off the timer that drives the game.
            clearButton.setDisable(false);  // Some buttons are disabled while the game is running.
            randomButton.setDisable(false);
            nextButton.setDisable(false);
            stopGoButton.setText("Start");  // Change text of button to "Start", since it can be used to start the game again.
            animationIsRunning = false;
        }
        else {  // If the game is not currently running, start it.
            timer.start();  // This starts the game by turning the timer that will drive the game.
            clearButton.setDisable(true);  // Buttons that modify the board are disabled while the game is running.
            randomButton.setDisable(true);
            nextButton.setDisable(true);
            stopGoButton.setText("Stop"); // Change text of button to "Stop", since it can be used to stop the game.
            animationIsRunning = true;
        }
    }


    /**
     * This method is called when the user clicks the "Random" button.  It fills the
     * alive array with random values and redraws the board.
     */
    private void doRandom() {
        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();
    }



    /**
     * This method is called when the user presses a mouse button on the canvas.
     * The square containing the mouse comes to life or, if the right-mouse button is down, dies.
     */
    private void mousePressed(MouseEvent e) {
        if (animationIsRunning)
            return;
        int row = lifeBoard.yCoordToRowNumber(e.getY());
        int col = lifeBoard.yCoordToRowNumber(e.getX());
        if (row >= 0 && row < lifeBoard.getRowCount() && col >= 0 && col < lifeBoard.getColumnCount()) {
            if (e.getButton() == MouseButton.SECONDARY) {
                lifeBoard.setColor(row,col,null);
                alive[row][col] = false;
            }
            else {
                lifeBoard.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.
     * Dragging the mouse into a square has the same effect as clicking in that square.
     */
    private void mouseDragged(MouseEvent e) {
        mousePressed(e);  // 
    }

}
