import javafx.scene.input.KeyCode;

import java.io.*;
import java.util.Arrays;
import java.util.Scanner;
import java.util.Random;
import java.util.ArrayList;

/**
 * Game of life class.
 *
 * @author rasmus
 * @version 4.2
 */
public class GameOfLifeMain {
    /**
     * The Is periodic.
     */
    static boolean isPeriodic = false;
    /**
     * The Auto run.
     */
    static boolean autoRun = false;
    /**
     * The Key pressed.
     */
    static boolean keyPressed = false;
    /**
     * The Board size.
     */
    static int boardSize;
    /**
     * The Text position.
     */
    static double[] textPosition;
    /**
     * The Jump to iteration.
     */
    static int jumpToIteration = -1; // When it's < 0, it is to be ignored.

    /**
     * The entry point of application.
     *
     * @param args the input arguments
     * @throws IOException the io exception
     */
    public static void main(String[] args) throws IOException {
        StdDraw.setCanvasSize(1000, 1050);
        Scanner console  = new Scanner(System.in);
        System.out.print("Please enter the size of the board (enter -1 to input file that should be imported): ");
        boardSize = console.nextInt();
        GameOfLife gameOfLife = new GameOfLife(Math.max(boardSize, 1));
        if (boardSize < 0) {
            System.out.print("Please enter path of file: ");
            String filePath = console.next();
            File file = new File(filePath);
            gameOfLife = importFromFile(file);
            boardSize = gameOfLife.size;
        }
        textPosition = new double[] {10.0 / 1000 * boardSize,(Math.ceil(boardSize * 1.05)-boardSize)/2+boardSize};
        StdDraw.setPenRadius(1.3 / (boardSize + 1));
        StdDraw.show(0);
        StdDraw.setXscale(-1, boardSize);
        StdDraw.setYscale(-1, (int) Math.ceil(boardSize*1.05));
        StdDraw.clear();
        gameOfLife.draw();
        StdDraw.textLeft(textPosition[0], textPosition[1], "Iteration nr.: " +  gameOfLife.iteration + "   Period is not found yet.");
        StdDraw.show(0);
        while (true) {
            if (!keyPressed && jumpToIteration < 0) {
                if (StdDraw.isKeyPressed(39)) { // Right arrow key is pressed. Go forward 1 iteration
                    runIteration(gameOfLife);
                    keyPressed = true;
                } else if (StdDraw.isKeyPressed(37) && gameOfLife.iteration > 0) { // Left arrow key is pressed. Go back 1 iteration
                    gameOfLife.previousState();
                    StdDraw.clear();
                    gameOfLife.draw();
                    StdDraw.textLeft(textPosition[0], textPosition[1], "Iteration nr.: " +  gameOfLife.iteration +
                            (isPeriodic ? "   Period is: " + gameOfLife.period : "   Period is not found yet"));
                    StdDraw.show(0);
                    keyPressed = true;
                } else if (StdDraw.isKeyPressed(32)) { // If space is pressed. Autoruns or pauses game
                    autoRun = !autoRun;
                    keyPressed = true;
                } else if (StdDraw.isKeyPressed(73)) { // If "i" is pressed. Goes to inputted iteration.
                    /*
                    When a iteration nr. is given to be jumped to. The simulation will run in the background not updating the board
                    until it reaches that iteration, in which case it will pause running and update board again.
                    */
                    System.out.print("Enter iteration to jump to: ");
                    jumpToIteration = Integer.parseInt(console.nextLine());
                    StdDraw.setPenRadius(50);
                    StdDraw.text((double) (boardSize + 1) /2, (double) ((int) Math.ceil(boardSize * 1.05) + 1) /2, "RUNNING SIMULATION. WAIT");
                    StdDraw.show(0);
                    StdDraw.setPenRadius(1.3 / (boardSize + 1));
                    autoRun = true;
                    keyPressed = true;
                } else if (StdDraw.isKeyPressed(83)) { // If "s" is pressed. Saves board
                    System.out.print("Enter filename (and path): ");
                    String savedFile = console.next();
                    gameOfLife.exportStateToFile(savedFile);
                }
            } else if (!StdDraw.isKeyPressed(39) && !StdDraw.isKeyPressed(37) && !StdDraw.isKeyPressed(32) && !StdDraw.isKeyPressed(73)) {
                // This is to make sure, that when a key is pressed, it is only acted upon once until it is pressed again.
                keyPressed = false;
            }
            if (autoRun) {
                runIteration(gameOfLife);
            }
        }
    }

    /**
     * Run iteration.
     *
     * @param gameOfLife the game of life
     */
    public static void runIteration(GameOfLife gameOfLife) {
        gameOfLife.nextState();
        int periodic = (isPeriodic ? -1 : gameOfLife.checkPeriodic()); // Only check if board is periodic if it hasnt already been found to be.
        if  (periodic >= 0) {
            int period = gameOfLife.iteration - periodic;
            System.out.println("The simulation is periodic. The period is: " + period);
            isPeriodic = true;
            autoRun = gameOfLife.iteration <= jumpToIteration; // Game should autoRun if iteration nr. is below iteration nr. it should run until.
        }
        if (gameOfLife.iteration >= jumpToIteration) {
            StdDraw.clear();
            gameOfLife.draw();
            StdDraw.textLeft(textPosition[0], textPosition[1], "Iteration nr.: " + gameOfLife.iteration +
                    (isPeriodic ? "        Period is: " + gameOfLife.period : "        Period is not found yet"));
            StdDraw.show(0);
            if (gameOfLife.iteration == jumpToIteration) {
                jumpToIteration = -1;
                autoRun = false;
            }
        }
    }


    /**
     * Import from file game of life.
     *
     * @param file the file
     * @return the game of life
     * @throws FileNotFoundException the file not found exception
     */
    public static GameOfLife importFromFile(File file) throws FileNotFoundException {
        /*
        Imports a simulation-state from a file with example file format:
            0 1 0 0
            1 0 0 0
            1 1 1 1
            0 0 1 1
        First goes through first line of file (and defines length) and the rest of the lines. Returns a GameOfLife object with imported board.
        */
        Scanner importedFile = new Scanner(file);
        String firstLine = importedFile.nextLine();
        String[] firstLineSplit =  firstLine.split(" ");
        int length = firstLineSplit.length;
        int[][] grid = new int[length][length];
        for (int i = 0; i < length; i++) {
            grid[0][i] = Integer.parseInt(firstLineSplit[i]);
        }
        int rowNum = 1;
        while (importedFile.hasNextLine()) {
            String Line  = importedFile.nextLine();
            String[] lineSplit = Line.split(" ");
            for (int i = 0; i < length; i++) {
                grid[rowNum][i] = Integer.parseInt(lineSplit[i]);
            }
            rowNum++;
        }
        importedFile.close();
        return new GameOfLife(grid);
    }
}

/**
 * The type Game of life.
 */
class GameOfLife {
    private int[][] state;
    /**
     * The Size.
     */
    int size;
    /**
     * The Iteration.
     */
    int iteration = 0;
    /**
     * The Period.
     */
    int period = -1;
    /**
     * The First periodic state.
     */
    int firstPeriodicState; // The index of the state when the simulations periodic nature begins.
    /**
     * The Previous boards list.
     */
    ArrayList<int[][]> previousBoardsList = new ArrayList<>(); // A list containing all the previous arrays of this.state.
    /**
     * The Previous boards list hash.
     */
    ArrayList<Integer> previousBoardsListHash = new ArrayList<>(); // A list containing all the previous array-hashes of this.state. Both lists used for checking if periodic

    /**
     * Instantiates a new Game of life.
     *
     * @param n the n
     */
    public GameOfLife(int n) {
        /*
        Create a random board of 1's and 0's based on array size n * n
         */
        Random rand = new Random();
        this.size = n;
        this.state = new int[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                this.state[i][j] = rand.nextInt(2);
            }
        }
        this.previousBoardsList.add(this.state);
        this.previousBoardsListHash.add(Arrays.deepHashCode(this.state));
    }

    /**
     * Instantiates a new Game of life.
     *
     * @param initialState the initial state
     */
    public GameOfLife(int[][] initialState) {
        this.state = initialState;
        this.size = initialState.length;
        this.previousBoardsList.add(this.state);
        this.previousBoardsListHash.add(Arrays.deepHashCode(this.state));
    }

    public String toString() {
        return Arrays.deepToString(this.state);
    }

    /**
     * Draw.
     */
    public void draw() {
        for (int i = 0; i < state.length; i++) {
            for (int j = 0; j < state[i].length; j++) {
                if (this.state[i][j] == 1) {
                    StdDraw.point(j, this.size-i-1);
                }
            }
        }
    }

    /**
     * Next state.
     */
    public void nextState() {
        /*
        Goes through the current state-array and builds/calculates next state. Also adds current state and state-hash to lists
         */
        int[][] newState = new int[this.size][this.size];
        for  (int i = 0; i < state.length; i++) {
            for (int j = 0; j < state[i].length; j++) {
                int aliveNeighbors = this.aliveNeighbours(i, j);
                if (this.state[i][j] == 1 && (aliveNeighbors < 2 || aliveNeighbors > 3)) { // If current cell is alive, and it should die of boredom or overcrowding
                    newState[i][j] = 0;
                } else if (this.state[i][j] == 0 && aliveNeighbors == 3) { // Dead cell with 3 neighbors returns to life
                    newState[i][j] = 1;
                } else {
                    newState[i][j] = this.state[i][j];
                }
            }
        }
        this.previousBoardsList.add(this.state);
        this.previousBoardsListHash.add(Arrays.deepHashCode(this.state));
        this.state = newState;
        this.iteration++;
    }

    /**
     * Previous state.
     */
    public void previousState() {
        /*
        Makes current state the previous one, and removes it from the lists
         */
        this.state = this.previousBoardsList.getLast();
        this.previousBoardsList.removeLast();
        this.previousBoardsListHash.removeLast();
        this.iteration--;
    }

    /**
     * Alive neighbours int.
     *
     * @param x the x
     * @param y the y
     * @return the int
     */
    public int aliveNeighbours(int x, int y) {
        /*
        Returns the amount of alive neighbors. It does this by adding the values of the surrounding squares.
        In easier to read format, its built like this:
        If not at the top of the array:
            Add together:
                Value of above left square, if not at the left edge of array, else 0
                Value of square above
                Value of above right square, if not at top right edge of array, else 0
        Else: 0
        And then the sum of that plus the same for left and right squares and square below.
         */
        return (y > 0 ? (x > 0 ? this.state[x-1][y-1] : 0) + this.state[x][y-1] + (x < this.size - 1 ? this.state[x+1][y-1] : 0) : 0) + // Neighbors above
                (x > 0 ? this.state[x-1][y] : 0) + (x < this.size - 1 ? this.state[x+1][y] : 0) + // Neighbors on same row
                (y < this.size - 1 ? (x > 0 ? this.state[x-1][y+1] : 0) + this.state[x][y+1] + (x < this.size - 1 ? this.state[x+1][y+1] : 0) : 0); // Neighbors below
    }

    /**
     * Check periodic int.
     *
     * @return the int
     */
    public int checkPeriodic() {
        /*
        Uses the indexOf array-function to search for matches in the list of array-state-hashes, returns > 0 if theres a match.
        If there's a match, compares the arrays using Array-function deepEquals to make sure arrays match as only using
        hashes can lead to false positives.
        */
        int matches = this.previousBoardsListHash.indexOf(Arrays.deepHashCode(this.state));
        if (matches >= 0 && Arrays.deepEquals(this.previousBoardsList.get(matches), this.state)) {
            this.period = this.iteration - matches;
            this.firstPeriodicState = matches;
            return matches;
        }
        return -1;
    }

    /**
     * Export state to file.
     *
     * @param filename the filename
     * @throws IOException the io exception
     */
    public void exportStateToFile(String filename) throws IOException {
        /*
        Goes through this.state array and saves it to a file. Example format its saved as:
            0 1 0 0
            1 0 0 0
            1 1 1 1
            0 0 1 1
        */
        FileWriter fileName = new FileWriter(filename);
        PrintWriter writer = new PrintWriter(fileName);
        for (int i = 0; i < this.state.length; i++) {
            for (int j = 0; j < this.state[i].length; j++) {
                writer.print(this.state[i][j] + (j < this.state[i].length - 1 ? " " : ""));
            }
            writer.println();
        }
        writer.close();
    }
}