/**
 * La classe <code>Grid</code> est utilisée pour générer la grille de blocs
 *  
 * @version 0.1
 * @author Adil HAMMERSCHMIDT & Lucas GRANDJEAN
 */

import java.io.*;
import java.awt.*;
import javax.swing.*;
import java.util.Random;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;

public class Grid extends JPanel {

    private Random random = new Random();
    private int col = 15;
    private int row = 10;
    private char charTab[][] = new char[10][15];
    private Block blockTab[][] = new Block[10][15];
    private String[] connectedCellList = null;
    private final ArrayList<String> cellNeigbors = new ArrayList<>();
    private HUD gameHUD;
    private int score = 0;

    /**
     *Constructeur de la Grid : Génère une grille aléatoire.
     *@param HUD Nom de l'HUD
     */
    public Grid(HUD gameHUD)  {
        this.gameHUD = gameHUD;
        this.setLayout(new GridLayout(row,col,0,0));
        this.setBackground(Color.WHITE);
        this.initGrid();
    }

    /**
     *TODO : Surchage du constructeur avec le fichier
     *
     *@param s Nom du fichier
     *@param HUD Nom de l'HUD
     */
    public Grid(String s, HUD gameHUD) {
        this.gameHUD = gameHUD;
        this.setLayout(new GridLayout(row,col,0,0));
        try {

            File grid_file = new File(s);
            FileInputStream flux = new FileInputStream(grid_file);

            this.initGrid(flux);

            try {
                flux.close();
            }

            catch (IOException e) {
                System.err.println("Impossible de fermer le fichier " + s + " !");
            }
        }

        catch (FileNotFoundException e) {
            System.err.println("Impossible d'ouvrir le fichier " + s + " !");
        }
    }
    /**
     * Met en place la grille
     *
     */
    public void initGrid() {
        GridMouseHandler gridMouseHandler = new GridMouseHandler();

        int i,j;
        for (i=0; i<row; i++) {
            for (j=0; j<col; j++) {
            int randomBlock=this.random.nextInt(3);
            try {
                switch(randomBlock) {
                    case 0:
                        charTab[i][j]='R';
                        break;
                    case 1:
                        charTab[i][j]='V';
                        break;
                    case 2:
                        charTab[i][j]='B';
                        break;
                    default:
                        throw new OutOfRangeBlock();
                }
                this.blockTab[i][j] = new Block(j, i, charTab[i][j]);
                this.blockTab[i][j].addMouseListener(gridMouseHandler);
                this.blockTab[i][j].setBackground(Color.WHITE);
                this.add(this.blockTab[i][j]);

            }  catch(OutOfRangeBlock e) {
                System.out.println(e);
            }

            }
        }

    }

    /**
     * Surchage de la fonction initGrid pour fonctionner avec les fichiers
     *
     *@param f Flux du fichier
     */
    public void initGrid(FileInputStream f) {
        GridMouseHandler gridMouseHandler = new GridMouseHandler();
        int i,j;
        char c;
        try {
            for (i=0; i<row; i++) {
                for (j=0; j<col; j++) {
                    c = (char)f.read();
                    if (c == '\r') {
                        c = (char)f.read();
                        c = (char)f.read();
                    }

                    charTab[i][j]=c;
                    this.blockTab[i][j] = new Block(j, i, charTab[i][j]);
                    this.blockTab[i][j].addMouseListener(gridMouseHandler);
                    this.blockTab[i][j].setBackground(Color.WHITE);
                    this.add(this.blockTab[i][j]);
    
                }
            }

        }

        catch(IOException e) {
            System.err.println("Impossible de lire le fichier !");
        }
    }

    /**
     * Cette méthode explore avec récursivité une matrice, partant
     * de la ligne et colonne spécifiée en argument, processe ses valeurs
     * puis regarde s'il possède un voisin horizontalement, verticalement et diagonalement (selon les options)
     * qui possède les mêmes valeurs. Chaques celulles voisines détectées rappellent la fonction,
     * jusqu'à ce qu'il n'y ai plus de voisins disponibles.
     * Toutes les celulles voisines ont les valeurs des lignes/colonnes 
     * storés dans une Array String (délimité par un ;).
     *
     * @param matrix (2D Array Int) La matrice pour check les voisins
     * 
     * @param matrix_X (int) Index de la ligne de base
     * 
     * @param matrix_Y (int) Index de la colonne de base
     * 
     * @return (String[] Array) Une Array de String contenant des sets délimités par un ";"
     * avec les valeurs des index de la Ligne;Colonne pour les cellules voisines détectées.
     * (Cellule de base inclue au début)
     */

    public String[] getMatrixNeighCells(char[][] matrix, int matrix_X, int matrix_Y) {
        /* Dans une matrice on peut avoir jusqu'à 8 voisins qui contiennent la même valeur.
        On va exclure les diagonales. */      
        int[][] directions = {
                    {0, -1},    // Haut
                    {-1, 0},    // Gauche
                    {1, 0},     // Droite
                    {0, 1},     // Bas
                };


        // Cellule de base
        String baseCell = String.valueOf((matrix_X)) + ";" + String.valueOf((matrix_Y));
        if (!this.cellNeigbors.contains(baseCell)) {
            this.cellNeigbors.add(baseCell);
        }

        for (int[] d : directions) {
            int dx = d[0];  // Ligne
            int dy = d[1];  // Colonne
            if ((matrix_X + dx) >= 0 && (matrix_X + dx) < (matrix.length)
                    && (matrix_Y + dy) >= 0 && (matrix_Y + dy) < (matrix[0].length)
                    && matrix[matrix_X + dx][matrix_Y + dy] == matrix[matrix_X][matrix_Y]
                    && matrix[matrix_X][matrix_Y] != '1') {
                // Pour les celulles avec des voisins à la même valeur
                String neighbor = String.valueOf((matrix_X + dx)) + ";" + String.valueOf((matrix_Y + dy));
                if (!this.cellNeigbors.contains(neighbor)) {
                    this.cellNeigbors.add(neighbor);
                    // Récursivité pour les voisins
                    String[] tmp = getMatrixNeighCells(matrix, matrix_X + dx, matrix_Y + dy);
                    if (tmp != null) {
                        for (String str : tmp) {
                            if (!this.cellNeigbors.contains(str)) {
                                this.cellNeigbors.add(str);
                            }
                        }
                    }
                }
            }
        }
        
        // On return la liste s'il y'a au moins une cellule voisine aux valeurs adjacente à celle de base
        if (this.cellNeigbors.size() >= 2) {
            return this.cellNeigbors.toArray(new String[this.cellNeigbors.size()]);
        }
        return null;    
    }

    /**
     * Permet de vérifier s'il y'a des lignes vides en dessous de chaques boules.
     * S'il y'en a, on abaisse d'un niveau la boule et on fait une récursion.
     *@param j Numéro de colonne
     */
    public void reorganizeCol(int j) {
        for(int i = 0; i < row-1; i++) {
            if(blockTab[i][j].getStatus() == 1 && blockTab[i+1][j].getStatus() == 0) {
                 blockTab[i+1][j].setColor(blockTab[i][j].getColor());
                 blockTab[i+1][j].setStatus(1);
                 blockTab[i][j].setStatus(0);
                 blockTab[i+1][j].setBackground(Color.WHITE);
                 blockTab[i][j].setBackground(Color.WHITE);
                 charTab[i][j]='1';
                 charTab[i+1][j]=blockTab[i+1][j].getColor();
                 reorganizeCol(j);
            }
        }
        this.validate();
        this.repaint();
    }

    /**
     * Permet de vérifier si une des colonnes est vide. Si oui, on décale et on relance.
     *@param i Numéro de col
     */
    public void reorganizeRow(int i) {
        int nbCount = 0;
        for(int j = 0; j < row; j++) {
            if(blockTab[j][i].getStatus() == 0) {
                nbCount++;
            }
            if(nbCount == row) {
                for(int k = 0; k < row; k++) {
                        blockTab[k][i].setColor(blockTab[k][i+1].getColor());
                        if(blockTab[k][i+1].getStatus() == 1) {
                            blockTab[k][i].setStatus(1);
                            blockTab[k][i+1].setStatus(0);
                            charTab[k][i]=blockTab[k][i+1].getColor();
                        }
                        blockTab[k][i].setBackground(Color.WHITE);
                        blockTab[k][i+1].setBackground(Color.WHITE);
                        charTab[k][i+1]='1';
                }
                if(i != col-2) {
                    reorganizeRow(i+1);
                }
            }

         }
        this.validate();
        this.repaint();

    }

    /**
     * Getter du tableau de la grille
     *
     *@return tableau de la grille
     */
    public char[][] getGrid() {
        return this.charTab;
    }

    /**
    * Un setter avec incrémentation du score
    * On change le label du score.
    */
    public void addScore(int score) {
        this.score += score;
        gameHUD.setScoreLabel(this.score);
    }

    /**
    * Getter du score
    * @return le score
    */
    public int getScore() {
        return this.score;
    }

    /**
    * On itère chaque blocs pour savoir s'il leur reste des voisins
    * Si non, on fait gagner le joueur
    * @return un bool, true marquant la victoire du joueur.
    */

    public void checkWin() {
        int moveLeft = 0;
        for (int i=0; i< blockTab.length; i++) {
	        for (int j=0; j< blockTab[i].length; j++) {
                cellNeigbors.clear();
			    connectedCellList = getMatrixNeighCells(getGrid(), i,j);
                if (connectedCellList != null && connectedCellList.length > 1) moveLeft++;
	        }
        }
        if(moveLeft <= 0) endGame();
    }

    /**
     * Créé l'écran de fin de partie
     */
    public void endGame() {
        FinalScreen fs = new FinalScreen(score);
        JFrame topFrame = (JFrame) SwingUtilities.getWindowAncestor(this);
        topFrame.dispose();
    }

    /**
     * Mouse Event
     */
    public class GridMouseHandler extends MouseAdapter {

        /**
        * Si le joueur passe la souris sur un des Blocks,
        * on vérifie les cellules voisines du block ponté
        * puis on change le backGround des cellules voisines
        */
        @Override
        public void mouseEntered(MouseEvent evt) {
            Block source = (Block) evt.getSource();
            for (int i = 0; i < blockTab.length; i++) {
                for (int j = 0; j < blockTab[i].length; j++) {
                    if (blockTab[i][j] == source) {
                        cellNeigbors.clear();
                        connectedCellList = getMatrixNeighCells(getGrid(), i, j);

                        if (connectedCellList != null && connectedCellList.length > 1) {
                            for (String cells : connectedCellList) {
                                int x = Integer.valueOf(cells.split(";")[0]);
                                int y = Integer.valueOf(cells.split(";")[1]);
                                blockTab[x][y].setBackground(Color.YELLOW);
                            }
                        }
                    }
                }
            }
        }

        /**
        * Si le joueur quitte le block avec le pointeur de souris,
        * on remet le background à sa couleur de base
        */
        @Override
        public void mouseExited(MouseEvent evt) {
            if (connectedCellList != null) {
                for (String cells : connectedCellList) {
                    int x = Integer.valueOf(cells.split(";")[0]);
                    int y = Integer.valueOf(cells.split(";")[1]);
                    blockTab[x][y].setBackground(Color.WHITE);
                }
            }
        }

        /**
        * Si le joueur clique et relâche la souris,
        * on désactive le block (en changeant le status),
        * on remet le fond, on lui assigne une couleur hors du champs
        * (afin de désactiver le surlignage), et on réorganise les
        * lignes et colonnes du grid.
        * On cherche aussi si le joueur a gagné en appelant CheckWin()
        */
        @Override
        public void mouseReleased(MouseEvent evt) {
            if (connectedCellList != null) {
                int ccListSize = connectedCellList.length;
                for (String cells : connectedCellList) {
                    int x = Integer.valueOf(cells.split(";")[0]);
                    int y = Integer.valueOf(cells.split(";")[1]);
                    charTab[x][y]='1';
                    blockTab[x][y].setStatus(0);
                    blockTab[x][y].setBackground(Color.WHITE);
                }
                addScore((ccListSize-2) * (ccListSize-2));
                for(int j = 0; j < col; j++) {
                    reorganizeCol(j);
                }
                for(int i = (col-2) ; i >= 0; i--) {
                    reorganizeRow(i);
                }
            }
            checkWin();
            mouseEntered(evt);
        }
    }

}