package fr.iut_fbleau.HexGame;

import fr.iut_fbleau.GameAPI.*;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;

/**
 * Représente le plateau du jeu de Hex.
 *
 * <h2>Rappel des conditions de victoire</h2>
 * <ul>
 *   <li>{@link Player#PLAYER1} gagne s'il existe un chemin de pions connectés
 *       reliant le bord gauche au bord droit.</li>
 *   <li>{@link Player#PLAYER2} gagne s'il existe un chemin de pions connectés
 *       reliant le bord haut au bord bas.</li>
 * </ul>
 *
 * <h2>Idée de l'algorithme de détection de victoire</h2>
 * On modélise le plateau comme un graphe :
 * <ul>
 *   <li>Chaque case est un sommet</li>
 *   <li>Deux cases sont connectées si elles sont voisines sur la grille hexagonale (6 voisins)</li>
 * </ul>
 *
 * Pour tester la victoire d'un joueur, on lance un parcours (DFS/BFS) :
 * <ol>
 *   <li>On part de toutes les cases du bord de départ qui contiennent un pion du joueur.</li>
 *   <li>On explore tous les pions du joueur connectés à ces cases.</li>
 *   <li>Si on atteint le bord opposé, il existe un chemin : le joueur gagne.</li>
 * </ol>
 *
 * Complexité : O(N²) au pire (on visite chaque case au plus une fois).
 */
public class HexBoard extends AbstractBoard {

    /** Taille du plateau : size x size. */
    private final int size;

    /**
     * Grille des cases.
     * Une case vaut :
     * <ul>
     *   <li>null : case vide</li>
     *   <li>PLAYER1 : pion du joueur 1</li>
     *   <li>PLAYER2 : pion du joueur 2</li>
     * </ul>
     */
    private final Player[][] cells;

    /**
     * Offsets des 6 voisins d'une case dans une grille hexagonale.
     *
     * Pour une case (r,c), les voisins potentiels sont :
     * (r-1,c), (r+1,c), (r,c-1), (r,c+1), (r-1,c+1), (r+1,c-1).
     */
    private static final int[][] NEIGHBORS = {
            {-1,  0}, {+1,  0},
            { 0, -1}, { 0, +1},
            {-1, +1}, {+1, -1}
    };

    /** Crée un plateau vide avec {@link Player#PLAYER1} qui commence. */
    public HexBoard(int size) {
        this(size, Player.PLAYER1);
    }

    /**
     * Constructeur interne, utile pour {@link #safeCopy()}.
     * @param size taille du plateau
     * @param current joueur courant
     */
    private HexBoard(int size, Player current) {
        super(current, new ArrayDeque<>());
        if (size <= 0) throw new IllegalArgumentException("size must be > 0");
        this.size = size;
        this.cells = new Player[size][size];
    }

    /** @return la taille du plateau. */
    public int getSize() {
        return size;
    }

    /**
     * Vérifie si (r,c) est dans le plateau.
     * @param r ligne (0..size-1)
     * @param c colonne (0..size-1)
     */
    private boolean inBounds(int r, int c) {
        return r >= 0 && r < size && c >= 0 && c < size;
    }

    /** @return le contenu d'une case (null si vide). */
    private Player getCell(int r, int c) {
        return cells[r][c];
    }

    /** Modifie une case (utilisé par doPly/undoPly). */
    private void setCell(int r, int c, Player p) {
        cells[r][c] = p;
    }

    /**
     * Teste la victoire de PLAYER1 (gauche -> droite).
     *
     * <h3>Détails de l'algorithme</h3>
     * <ol>
     *   <li>On initialise une structure "visited" pour ne pas revisiter les cases.</li>
     *   <li>On met dans une pile toutes les cases du bord gauche (colonne 0)
     *       qui contiennent un pion PLAYER1.</li>
     *   <li>On effectue un DFS :
     *       <ul>
     *         <li>on dépile une case</li>
     *         <li>si elle est sur la colonne size-1 : on a touché le bord droit -> victoire</li>
     *         <li>sinon, on empile tous ses voisins qui sont des pions PLAYER1 et pas encore visités</li>
     *       </ul>
     *   </li>
     * </ol>
     *
     * @return true si PLAYER1 a un chemin gauche->droite, false sinon
     */
    private boolean hasPlayer1Won() {
        boolean[][] visited = new boolean[size][size];
        Deque<int[]> stack = new ArrayDeque<>();

        // 1) points de départ : bord gauche
        for (int r = 0; r < size; r++) {
            if (getCell(r, 0) == Player.PLAYER1) {
                visited[r][0] = true;
                stack.push(new int[]{r, 0});
            }
        }

        // 2) DFS
        while (!stack.isEmpty()) {
            int[] cur = stack.pop();
            int cr = cur[0], cc = cur[1];

            // condition d'arrivée : bord droit
            if (cc == size - 1) return true;

            // explore les 6 voisins
            for (int[] d : NEIGHBORS) {
                int nr = cr + d[0], nc = cc + d[1];
                if (inBounds(nr, nc)
                        && !visited[nr][nc]
                        && getCell(nr, nc) == Player.PLAYER1) {
                    visited[nr][nc] = true;
                    stack.push(new int[]{nr, nc});
                }
            }
        }
        return false;
    }

    /**
     * Teste la victoire de PLAYER2 (haut -> bas).
     *
     * Même principe que {@link #hasPlayer1Won()} mais :
     * <ul>
     *   <li>Départ : bord haut (ligne 0)</li>
     *   <li>Arrivée : bord bas (ligne size-1)</li>
     * </ul>
     *
     * @return true si PLAYER2 a un chemin haut->bas, false sinon
     */
    private boolean hasPlayer2Won() {
        boolean[][] visited = new boolean[size][size];
        Deque<int[]> stack = new ArrayDeque<>();

        // points de départ : bord haut
        for (int c = 0; c < size; c++) {
            if (getCell(0, c) == Player.PLAYER2) {
                visited[0][c] = true;
                stack.push(new int[]{0, c});
            }
        }

        // DFS
        while (!stack.isEmpty()) {
            int[] cur = stack.pop();
            int cr = cur[0], cc = cur[1];

            // condition d'arrivée : bord bas
            if (cr == size - 1) return true;

            for (int[] d : NEIGHBORS) {
                int nr = cr + d[0], nc = cc + d[1];
                if (inBounds(nr, nc)
                        && !visited[nr][nc]
                        && getCell(nr, nc) == Player.PLAYER2) {
                    visited[nr][nc] = true;
                    stack.push(new int[]{nr, nc});
                }
            }
        }
        return false;
    }

    @Override
    public boolean isLegal(AbstractPly move) {
        if (!(move instanceof HexPly)) return false;
        HexPly hp = (HexPly) move;
        int r = hp.getRow(), c = hp.getCol();
        return inBounds(r, c)
                && getCell(r, c) == null
                && hp.getPlayer() == getCurrentPlayer();
    }

    /**
     * Teste si un coup est immédiatement gagnant.
     *
     * On joue le coup, on teste la victoire, puis on annule le coup.
     * Cela permet d'évaluer un coup sans modifier définitivement l'état du plateau.
     *
     * @param move coup à tester
     * @return true si après ce coup le joueur a gagné, false sinon
     */
    public boolean isWinningMove(AbstractPly move) {
        if (!isLegal(move)) return false;
        Player p = move.getPlayer();

        doPly(move);
        boolean winNow = (p == Player.PLAYER1) ? hasPlayer1Won() : hasPlayer2Won();
        undoPly();

        return winNow;
    }

    @Override
    public void doPly(AbstractPly move) {
        if (!(move instanceof HexPly)) {
            throw new IllegalArgumentException("Coup invalide: " + move);
        }
        if (!isLegal(move)) {
            throw new IllegalStateException("Coup illégal: " + move);
        }

        HexPly hp = (HexPly) move;
        setCell(hp.getRow(), hp.getCol(), hp.getPlayer());
        addPlyToHistory(move);
        setNextPlayer();
    }

    @Override
    public void undoPly() {
        AbstractPly last = removePlyFromHistory();
        HexPly hp = (HexPly) last;

        setCell(hp.getRow(), hp.getCol(), null);
        setNextPlayer();
    }

    @Override
    public boolean isGameOver() {
        return hasPlayer1Won() || hasPlayer2Won();
    }

    @Override
    public Result getResult() {
        if (!isGameOver()) return null;
        if (hasPlayer1Won()) return Result.WIN;   // du point de vue PLAYER1
        return Result.LOSS;
    }

    @Override
    public Iterator<AbstractPly> iterator() {
        Player me = getCurrentPlayer();
        List<AbstractPly> moves = new ArrayList<>();

        for (int r = 0; r < size; r++) {
            for (int c = 0; c < size; c++) {
                if (getCell(r, c) == null) {
                    moves.add(new HexPly(me, r, c));
                }
            }
        }
        return moves.iterator();
    }

    @Override
    public IBoard safeCopy() {
        HexBoard copy = new HexBoard(this.size, this.getCurrentPlayer());
        for (int r = 0; r < size; r++) {
            System.arraycopy(this.cells[r], 0, copy.cells[r], 0, size);
        }
        return copy;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for (int r = 0; r < size; r++) {
            for (int k = 0; k < r; k++) sb.append(" ");
            for (int c = 0; c < size; c++) {
                Player p = getCell(r, c);
                char ch = '.';
                if (p == Player.PLAYER1) ch = '1';
                else if (p == Player.PLAYER2) ch = '2';
                sb.append(ch).append(" ");
            }
            sb.append("\n");
        }
        sb.append("Current player: ").append(getCurrentPlayer()).append("\n");
        return sb.toString();
    }


    public Player getCellPlayer(int r, int c) {
    return cells[r][c];
    }

}
