package legion.muyue.best2048;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * Représente la logique et l'état d'une partie du jeu 2048.
 * Gère le plateau de jeu, le score, la génération de nouvelles tuiles,
 * le traitement des mouvements du joueur, la détection des conditions de victoire et de fin de partie,
 * ainsi que la sérialisation et la désérialisation de base de l'état du jeu.
 * La taille du plateau est définie par {@link #BOARD_SIZE}.
 */
public class Game {

    /**
     * La taille (nombre de lignes et de colonnes) du plateau de jeu.
     * Fixé à 4 pour un jeu 2048 standard.
     */
    private static final int BOARD_SIZE = 4;

    /**
     * Le plateau de jeu, représenté par une grille 2D d'entiers.
     * Chaque entier correspond à la valeur d'une tuile (0 pour une case vide).
     */
    private int[][] board;

    /**
     * Générateur de nombres aléatoires utilisé pour placer de nouvelles tuiles
     * et déterminer leur valeur initiale (2, 4, 8, etc.).
     */
    private final Random randomNumberGenerator;

    /**
     * Le score actuel de la partie en cours. Augmente lors de la fusion de tuiles.
     */
    private int currentScore = 0;

    /**
     * Le meilleur score enregistré. Peut être chargé ou défini de l'extérieur.
     * Note : La persistance de ce score n'est pas gérée par cette classe.
     */
    private int highestScore = 0;

    /**
     * Indicateur de victoire. Passe à {@code true} lorsqu'une tuile >= 2048 est créée.
     * Le jeu peut continuer après la victoire.
     */
    private boolean gameWon = false;

    /**
     * Indicateur de fin de partie. Passe à {@code true} lorsqu'aucun mouvement valide n'est plus possible.
     */
    private boolean gameOver = false;

    /**
     * Constructeur par défaut pour démarrer une nouvelle partie.
     * Initialise un plateau vide, met le score à zéro, et ajoute deux tuiles initiales aléatoires.
     */
    public Game() {
        this.randomNumberGenerator = new Random();
        initializeNewBoard();
    }

    /**
     * Constructeur pour créer une instance de jeu à partir d'un état existant.
     * Utile pour charger une partie sauvegardée ou pour les tests.
     * Vérifie si les conditions de victoire ou de fin de partie sont déjà remplies.
     *
     * @param board Le plateau de jeu existant. Doit être non null et de taille {@code BOARD_SIZE}x{@code BOARD_SIZE}.
     * @param score Le score associé à l'état du plateau fourni.
     * @throws IllegalArgumentException si le plateau fourni est null ou n'a pas les dimensions correctes.
     */
    public Game(@NonNull int[][] board, int score) {
        if (board == null || board.length != BOARD_SIZE || board[0].length != BOARD_SIZE) {
            throw new IllegalArgumentException("Le plateau fourni n'a pas les dimensions attendues (" + BOARD_SIZE + "x" + BOARD_SIZE + ").");
        }
        // Crée une copie défensive du tableau pour éviter les modifications externes
        this.board = new int[BOARD_SIZE][BOARD_SIZE];
        for (int i = 0; i < BOARD_SIZE; i++) {
            System.arraycopy(board[i], 0, this.board[i], 0, BOARD_SIZE);
        }
        this.currentScore = score;
        this.randomNumberGenerator = new Random();
        // Recalcule l'état win/over basé sur le plateau fourni
        checkWinCondition();
        checkGameOverCondition();
    }

    /**
     * Récupère la valeur de la tuile à la position spécifiée.
     *
     * @param row    L'indice de la ligne (0 à BOARD_SIZE - 1).
     * @param column L'indice de la colonne (0 à BOARD_SIZE - 1).
     * @return La valeur de la tuile à la position (row, column), ou 0 si les indices sont invalides ou la case est vide.
     */
    public int getCellValue(int row, int column) {
        if (isIndexValid(row, column)) {
            return this.board[row][column];
        }
        return 0; // Retourne 0 si l'index est hors limites
    }

    /**
     * Définit la valeur d'une cellule spécifique sur le plateau.
     * Principalement destiné aux tests unitaires pour configurer des scénarios spécifiques.
     * Utiliser avec prudence car cela modifie directement l'état interne du jeu.
     *
     * @param row   L'indice de la ligne (0 à BOARD_SIZE - 1).
     * @param col   L'indice de la colonne (0 à BOARD_SIZE - 1).
     * @param value La nouvelle valeur pour la cellule.
     */
    @VisibleForTesting
    void setCellValue(int row, int col, int value) {
        if (isIndexValid(row, col)) {
            this.board[row][col] = value;
        }
    }

    /**
     * Récupère le score actuel de la partie.
     *
     * @return Le score actuel.
     */
    public int getCurrentScore() {
        return currentScore;
    }

    /**
     * Récupère le meilleur score enregistré (peut nécessiter une gestion externe).
     *
     * @return Le meilleur score connu.
     */
    public int getHighestScore() {
        return highestScore;
    }

    /**
     * Définit le meilleur score. Utile pour charger un meilleur score sauvegardé.
     *
     * @param highScore Le meilleur score à définir.
     */
    public void setHighestScore(int highScore) {
        this.highestScore = highScore;
    }

    /**
     * Vérifie si la condition de victoire (atteindre une tuile >= 2048) a été remplie.
     *
     * @return {@code true} si le jeu est gagné, {@code false} sinon.
     */
    public boolean isGameWon() {
        return gameWon;
    }

    /**
     * Vérifie si la condition de fin de partie (aucun mouvement possible) a été remplie.
     *
     * @return {@code true} si le jeu est terminé, {@code false} sinon.
     */
    public boolean isGameOver() {
        return gameOver;
    }

    /**
     * Définit l'état de victoire du jeu. Méthode privée utilisée en interne.
     * @param won Nouvel état de victoire.
     */
    private void setGameWon(boolean won) {
        this.gameWon = won;
    }

    /**
     * Définit l'état de fin de partie du jeu. Méthode privée utilisée en interne.
     * @param over Nouvel état de fin de partie.
     */
    private void setGameOver(boolean over) {
        this.gameOver = over;
    }

    /**
     * Retourne une copie du plateau de jeu actuel.
     * Utiliser cette méthode pour obtenir l'état du plateau sans risquer de
     * modifier l'état interne du jeu par inadvertance.
     *
     * @return Une copie 2D du tableau {@code board}. Ne sera jamais null.
     */
    @NonNull
    public int[][] getBoard() {
        int[][] copy = new int[BOARD_SIZE][BOARD_SIZE];
        for (int i = 0; i < BOARD_SIZE; i++) {
            // Utilise arraycopy pour une copie efficace de chaque ligne
            System.arraycopy(this.board[i], 0, copy[i], 0, BOARD_SIZE);
        }
        return copy;
    }

    /**
     * Initialise ou réinitialise le plateau pour une nouvelle partie.
     * Met toutes les cellules à 0, réinitialise le score et les indicateurs
     * de victoire/fin de partie, puis ajoute deux nouvelles tuiles.
     */
    private void initializeNewBoard() {
        this.board = new int[BOARD_SIZE][BOARD_SIZE]; // Crée un nouveau tableau vide
        this.currentScore = 0;
        this.gameWon = false;
        this.gameOver = false;
        addNewTile(); // Ajoute la première tuile
        addNewTile(); // Ajoute la deuxième tuile
    }

    /**
     * Ajoute une nouvelle tuile aléatoire (2, 4, 8, ...) sur une case vide aléatoire du plateau.
     * Si aucune case n'est vide, cette méthode n'a aucun effet.
     * La valeur de la nouvelle tuile est déterminée par {@link #generateRandomTileValue()}.
     */
    public void addNewTile() {
        List<int[]> emptyCells = findEmptyCells();
        if (!emptyCells.isEmpty()) {
            // Choisit une cellule vide au hasard
            int[] randomCell = emptyCells.get(randomNumberGenerator.nextInt(emptyCells.size()));
            // Génère la valeur de la nouvelle tuile (2, 4, 8, ...)
            int value = generateRandomTileValue();
            // Place la nouvelle tuile sur le plateau
            setCellValue(randomCell[0], randomCell[1], value);
        }
        // Après ajout, revérifie si le jeu est terminé (au cas où l'ajout bloquerait tout)
        checkGameOverCondition();
    }

    /**
     * Trouve toutes les cellules vides (valeur 0) sur le plateau.
     *
     * @return Une liste de tableaux d'entiers {@code [row, col]}, où chaque tableau représente
     * les coordonnées d'une cellule vide. Retourne une liste vide s'il n'y a pas de cellules vides. Ne sera jamais null.
     */
    @NonNull
    private List<int[]> findEmptyCells() {
        List<int[]> emptyCells = new ArrayList<>();
        for (int row = 0; row < BOARD_SIZE; row++) {
            for (int col = 0; col < BOARD_SIZE; col++) {
                if (board[row][col] == 0) {
                    emptyCells.add(new int[]{row, col});
                }
            }
        }
        return emptyCells;
    }

    /**
     * Génère la valeur pour une nouvelle tuile ajoutée au plateau.
     * Cette implémentation spécifique a des probabilités pour 2, 4, 8, 16, 32, 64, 128, 256.
     * ~85.4% de chance pour 2, ~12% pour 4, ~2% pour 8, ~0.5% pour 16, etc.
     *
     * @return La valeur de la nouvelle tuile (2, 4, 8, ... , 256).
     */
    private int generateRandomTileValue() {
        // Note: Les probabilités ici diffèrent du 2048 standard (90% 2, 10% 4).
        int randomValue = randomNumberGenerator.nextInt(10000); // Base 10000 pour les pourcentages
        if (randomValue < 8540) return 2;  // ~85.4%
        if (randomValue < 9740) return 4;  // ~12.0% (9740 - 8540)
        if (randomValue < 9940) return 8;  // ~ 2.0% (9940 - 9740)
        if (randomValue < 9990) return 16; // ~ 0.5% (9990 - 9940)
        if (randomValue < 9995) return 32; // ~0.05% (9995 - 9990)
        if (randomValue < 9998) return 64; // ~0.03% (9998 - 9995)
        if (randomValue < 9999) return 128;// ~0.01% (9999 - 9998)
        return 256; // ~0.01%
    }

    /**
     * Tente d'effectuer un mouvement vers le haut.
     * Déplace et fusionne les tuiles vers le haut.
     *
     * @return {@code true} si le plateau a été modifié par le mouvement, {@code false} sinon.
     */
    public boolean pushUp() {
        return processMove(MoveDirection.UP);
    }

    /**
     * Tente d'effectuer un mouvement vers le bas.
     * Déplace et fusionne les tuiles vers le bas.
     *
     * @return {@code true} si le plateau a été modifié par le mouvement, {@code false} sinon.
     */
    public boolean pushDown() {
        return processMove(MoveDirection.DOWN);
    }

    /**
     * Tente d'effectuer un mouvement vers la gauche.
     * Déplace et fusionne les tuiles vers la gauche.
     *
     * @return {@code true} si le plateau a été modifié par le mouvement, {@code false} sinon.
     */
    public boolean pushLeft() {
        return processMove(MoveDirection.LEFT);
    }

    /**
     * Tente d'effectuer un mouvement vers la droite.
     * Déplace et fusionne les tuiles vers la droite.
     *
     * @return {@code true} si le plateau a été modifié par le mouvement, {@code false} sinon.
     */
    public boolean pushRight() {
        return processMove(MoveDirection.RIGHT);
    }

    /**
     * Énumération interne représentant les quatre directions de mouvement possibles.
     */
    private enum MoveDirection { UP, DOWN, LEFT, RIGHT }

    /**
     * Traite un mouvement dans la direction spécifiée.
     * Parcourt le plateau ligne par ligne ou colonne par colonne, déplace les tuiles,
     * effectue les fusions, met à jour le score, et vérifie les conditions de victoire/fin de partie.
     *
     * @param direction La direction {@link MoveDirection} du mouvement à traiter.
     * @return {@code true} si au moins une tuile a bougé ou fusionné, {@code false} sinon.
     */
    private boolean processMove(MoveDirection direction) {
        boolean boardChanged = false; // Indicateur si le plateau a été modifié

        // Itère sur les lignes (pour GAUCHE/DROITE) ou les colonnes (pour HAUT/BAS)
        for (int i = 0; i < BOARD_SIZE; i++) {
            // Tableau pour suivre si une cellule de la ligne/colonne cible a déjà fusionné dans ce mouvement
            boolean[] hasMerged = new boolean[BOARD_SIZE];

            // Détermine l'ordre de parcours des cellules dans la ligne/colonne
            // Pour HAUT et GAUCHE : on part du début (index 1) vers la fin
            // Pour BAS et DROITE : on part de l'avant-dernière (index BOARD_SIZE - 2) vers le début
            int start = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? BOARD_SIZE - 2 : 1;
            int end = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? -1 : BOARD_SIZE;
            int step = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? -1 : 1;

            // Parcourt les cellules de la ligne/colonne
            for (int j = start; j != end; j += step) {
                int row, col; // Coordonnées de la cellule actuelle (j)
                if (direction == MoveDirection.UP || direction == MoveDirection.DOWN) {
                    // Pour HAUT/BAS, 'i' est la colonne, 'j' est la ligne
                    row = j; col = i;
                } else {
                    // Pour GAUCHE/DROITE, 'i' est la ligne, 'j' est la colonne
                    row = i; col = j;
                }

                int currentValue = getCellValue(row, col); // Valeur de la tuile actuelle

                // Si la cellule n'est pas vide
                if (currentValue != 0) {
                    int currentRow = row; // Position initiale de la tuile
                    int currentCol = col;
                    int targetRow = currentRow; // Position cible après déplacement (sans fusion)
                    int targetCol = currentCol;

                    // Calcule la position de la case voisine dans la direction du mouvement
                    int nextRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0);
                    int nextCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 1 : 0);

                    // Tant que la case voisine est valide et vide, on déplace la cible
                    while (isIndexValid(nextRow, nextCol) && getCellValue(nextRow, nextCol) == 0) {
                        targetRow = nextRow;
                        targetCol = nextCol;
                        // Calcule la case suivante pour continuer le déplacement
                        nextRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0);
                        nextCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 1 : 0);
                    }

                    // nextRow/nextCol contient maintenant la première case non vide rencontrée ou une case invalide
                    int mergeTargetRow = nextRow; // Case potentielle pour la fusion
                    int mergeTargetCol = nextCol;
                    // Index utilisé pour vérifier si la case cible a déjà fusionné
                    int mergeIndex = (direction == MoveDirection.UP || direction == MoveDirection.DOWN) ? mergeTargetRow : mergeTargetCol;

                    boolean merged = false; // Indicateur si une fusion a eu lieu pour cette tuile
                    // Vérifie si une fusion est possible
                    if (isIndexValid(mergeTargetRow, mergeTargetCol) &&                  // La case cible est valide
                            getCellValue(mergeTargetRow, mergeTargetCol) == currentValue && // Elle a la même valeur
                            !hasMerged[mergeIndex])                                         // Elle n'a pas déjà fusionné ce tour-ci
                    {
                        // --- Fusion ---
                        int newValue = currentValue * 2; // Nouvelle valeur après fusion
                        setCellValue(mergeTargetRow, mergeTargetCol, newValue); // Met à jour la case cible
                        setCellValue(currentRow, currentCol, 0);            // Vide la case d'origine
                        currentScore += newValue;                           // Augmente le score
                        hasMerged[mergeIndex] = true;                       // Marque la case cible comme ayant fusionné
                        boardChanged = true;                                // Le plateau a changé
                        merged = true;                                      // La tuile a fusionné

                        // Vérifie la condition de victoire après la fusion
                        if (newValue >= 2048) { // Utiliser une constante WINNING_TILE serait mieux
                            setGameWon(true);
                        }
                    }

                    // Si aucune fusion n'a eu lieu mais que la tuile doit bouger
                    if (!merged && (targetRow != currentRow || targetCol != currentCol)) {
                        // --- Déplacement simple ---
                        setCellValue(targetRow, targetCol, currentValue); // Déplace la valeur vers la case cible
                        setCellValue(currentRow, currentCol, 0);        // Vide la case d'origine
                        boardChanged = true;                            // Le plateau a changé
                    }
                } // Fin if (currentValue != 0)
            } // Fin boucle for interne (j)
        } // Fin boucle for externe (i)

        // Après avoir traité toutes les lignes/colonnes, revérifie les conditions
        // (la condition de victoire peut avoir été atteinte pendant la fusion)
        checkWinCondition();
        // La condition de fin de partie doit être vérifiée après le mouvement
        checkGameOverCondition();

        // Retourne si le plateau a été modifié
        return boardChanged;
    }

    /**
     * Vérifie si les coordonnées fournies (ligne, colonne) sont valides
     * par rapport à la taille du plateau ({@link #BOARD_SIZE}).
     *
     * @param row L'indice de la ligne à vérifier.
     * @param col L'indice de la colonne à vérifier.
     * @return {@code true} si 0 <= row < BOARD_SIZE et 0 <= col < BOARD_SIZE, {@code false} sinon.
     */
    private boolean isIndexValid(int row, int col) {
        return row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE;
    }

    /**
     * Fournit une représentation textuelle simple de l'état du jeu (plateau + score).
     * Le format est une chaîne de valeurs séparées par des virgules :
     * toutes les valeurs du plateau (ligne par ligne), suivies du score actuel.
     * Utilisé pour la sérialisation simple via {@link #deserialize(String)}.
     *
     * @return Une chaîne représentant l'état du jeu. Ne sera jamais null.
     */
    @NonNull
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for (int row = 0; row < BOARD_SIZE; row++) {
            for (int col = 0; col < BOARD_SIZE; col++) {
                sb.append(board[row][col]).append(",");
            }
        }
        // Ajoute le score à la fin, sans virgule après
        sb.append(currentScore);
        return sb.toString();
    }

    /**
     * Crée une instance de {@link Game} à partir d'une chaîne sérialisée.
     * La chaîne doit correspondre au format généré par {@link #toString()}.
     *
     * @param serializedState La chaîne contenant l'état du jeu sérialisé. Peut être null ou vide.
     * @return Une nouvelle instance de {@link Game} initialisée avec l'état désérialisé,
     * ou {@code null} si la chaîne est invalide, vide, null, ou ne correspond pas au format attendu.
     */
    @Nullable
    public static Game deserialize(@Nullable String serializedState) {
        if (serializedState == null || serializedState.isEmpty()) {
            return null; // Chaîne vide ou nulle
        }
        String[] values = serializedState.split(",");
        // Vérifie si le nombre de valeurs correspond à (taille*taille + 1 score)
        if (values.length != (BOARD_SIZE * BOARD_SIZE + 1)) {
            return null; // Nombre incorrect de valeurs
        }

        int[][] newBoard = new int[BOARD_SIZE][BOARD_SIZE];
        int index = 0;
        try {
            // Remplit le nouveau plateau
            for (int row = 0; row < BOARD_SIZE; row++) {
                for (int col = 0; col < BOARD_SIZE; col++) {
                    newBoard[row][col] = Integer.parseInt(values[index++]);
                }
            }
            // Récupère le score
            int score = Integer.parseInt(values[index]); // La dernière valeur est le score
            // Crée et retourne le nouvel objet Game en utilisant le constructeur approprié
            return new Game(newBoard, score);
        } catch (ArrayIndexOutOfBoundsException | IllegalArgumentException e) {
            return null; // Échec de la désérialisation
        }
    }

    /**
     * Vérifie si la condition de victoire (une tuile >= 2048) est atteinte.
     * Met à jour le flag {@link #gameWon} si nécessaire.
     * Ne fait rien si le jeu est déjà marqué comme gagné.
     */
    private void checkWinCondition() {
        // Si déjà gagné, pas besoin de revérifier
        if (gameWon) {
            return;
        }
        // Parcourt le plateau à la recherche d'une tuile >= 2048
        for (int r = 0; r < BOARD_SIZE; r++) {
            for (int c = 0; c < BOARD_SIZE; c++) {
                // Utiliser getCellValue pour la cohérence (bien que l'accès direct soit possible ici)
                if (getCellValue(r, c) >= 2048) { // Utiliser une constante WINNING_TILE = 2048
                    setGameWon(true);
                    // Pas besoin de continuer à chercher une fois la condition atteinte
                    return;
                }
            }
        }
        // Si on arrive ici, aucune tuile >= 2048 n'a été trouvée
    }

    /**
     * Vérifie si la condition de fin de partie est atteinte (plus aucun mouvement possible).
     * Un jeu est terminé s'il n'y a plus de cellules vides ET aucune paire de tuiles
     * adjacentes identiques (horizontalement ou verticalement).
     * Met à jour le flag {@link #gameOver} si nécessaire.
     * Ne fait rien si le jeu est déjà marqué comme terminé.
     */
    private void checkGameOverCondition() {
        // Si déjà terminé, pas besoin de revérifier
        if (gameOver) {
            return;
        }

        // S'il y a au moins une cellule vide, le jeu n'est pas terminé
        if (hasEmptyCell()) {
            setGameOver(false); // Assure que le flag est bien à false
            return;
        }

        // S'il n'y a pas de cellules vides, vérifie les fusions possibles
        // Parcourt toutes les cellules
        for (int r = 0; r < BOARD_SIZE; r++) {
            for (int c = 0; c < BOARD_SIZE; c++) {
                int current = getCellValue(r, c);
                // Vérifie la fusion possible vers le haut (si pas sur la première ligne)
                if (r > 0 && getCellValue(r - 1, c) == current) {
                    setGameOver(false); return; // Mouvement possible trouvé
                }
                // Vérifie la fusion possible vers le bas (si pas sur la dernière ligne)
                if (r < BOARD_SIZE - 1 && getCellValue(r + 1, c) == current) {
                    setGameOver(false); return; // Mouvement possible trouvé
                }
                // Vérifie la fusion possible vers la gauche (si pas sur la première colonne)
                if (c > 0 && getCellValue(r, c - 1) == current) {
                    setGameOver(false); return; // Mouvement possible trouvé
                }
                // Vérifie la fusion possible vers la droite (si pas sur la dernière colonne)
                if (c < BOARD_SIZE - 1 && getCellValue(r, c + 1) == current) {
                    setGameOver(false); return; // Mouvement possible trouvé
                }
            }
        }

        // Si on arrive ici, il n'y a pas de cellules vides ET aucune fusion possible
        setGameOver(true);
    }

    /**
     * Vérifie rapidement s'il existe au moins une cellule vide sur le plateau.
     *
     * @return {@code true} s'il y a au moins une cellule avec la valeur 0, {@code false} sinon.
     */
    private boolean hasEmptyCell() {
        for (int r = 0; r < BOARD_SIZE; r++) {
            for (int c = 0; c < BOARD_SIZE; c++) {
                if (getCellValue(r, c) == 0) {
                    return true; // Trouvé une cellule vide
                }
            }
        }
        return false; // Aucune cellule vide trouvée
    }

    /**
     * Calcule et retourne la valeur de la plus haute tuile actuellement sur le plateau.
     *
     * @return La valeur maximale parmi toutes les tuiles du plateau. Retourne 0 si le plateau est vide.
     */
    public int getHighestTileValue() {
        int maxTile = 0;
        for (int r = 0; r < BOARD_SIZE; r++) {
            for (int c = 0; c < BOARD_SIZE; c++) {
                if (board[r][c] > maxTile) {
                    maxTile = board[r][c];
                }
            }
        }
        return maxTile;
    }
}