package sudoku.solver;

import java.util.List;
import java.util.Random;
import java.util.concurrent.CancellationException;
import java.util.logging.Level;
import java.util.logging.Logger;

import sudoku.io.SudokuPrinter;
import sudoku.structure.Cell;
import sudoku.structure.MultiDoku;
import sudoku.structure.Sudoku;

public class Solver {

    /**
     * Log du Solver, qui garde trace des actions réalisées.
     */
    private static final Logger logger = Logger.getLogger("SolverLogger");

    /**
     * Résout, si possible, le multidoku passé en paramètre
     * en testant toutes les possibilités, de manière aléatoire, avec un algorithme
     * de backtracking.
     *
     * @param doku Multidoku, à résoudre
     * @param rand Random, pour tester aléatoirement les symboles
     * @return boolean, true s'il est résolu ou false s'il ne l'est pas.
     */
    public static boolean solveRandom(MultiDoku doku, Random rand) {
        if (Thread.interrupted())
            throw new CancellationException("User wants to stop the solver");

        Sudoku sudoku = doku.getSubGrid(0);
        logger.log(Level.FINE,
                '\n' + SudokuPrinter.toStringRectangleSudoku(sudoku,
                        sudoku.getBlockWidth() == 0 ? sudoku.getSize() : sudoku.getBlockWidth(),
                        sudoku.getBlockWidth() == 0 ? sudoku.getSize() : sudoku.getSize() / sudoku.getBlockWidth()));

        if (doku.isSolved()) {
            return true;
        }

        Cell cellToFill = doku.getFirstEmptyCell();
        if (cellToFill == null) {
            return false;
        }

        List<Integer> possibleSymbols = cellToFill.getPossibleSymbols();

        while (!possibleSymbols.isEmpty()) {
            int nextPossibleSymbolIndex = rand.nextInt(possibleSymbols.size());
            int nextSymbol = possibleSymbols.get(nextPossibleSymbolIndex);

            cellToFill.setSymbolIndex(nextSymbol);
            if (Solver.solveRandom(doku, rand)) {
                return true;
            }
            cellToFill.setSymbolIndex(Cell.NOSYMBOL);
            possibleSymbols.remove(nextPossibleSymbolIndex);

        }
        return false;
    }

    /**
     * Compte le nombre de solutions possibles au MultiDoku passé en paramètres.
     * @param oldDoku MultiDoku, MultiDoku dont on veut le nombre de solutions.
     * @return int, nombre de solutions possibles.
     */
    public static int countSolution(MultiDoku doku) {
        int result = 0;


        if (doku.isSolved()) {
            return 1;
        }

        Cell cellToFill = doku.getFirstEmptyCell();
        assert(cellToFill != null);

        List<Integer> possibleSymbols = cellToFill.getPossibleSymbols();

        for (int symbol : possibleSymbols) {
            doku.getStateManager().pushState();
            cellToFill.setSymbolIndex(symbol);
            if (Solver.solve(doku)) {
                result++;
            }
            doku.getStateManager().popState();
        }

        return result;
    }

    /**
     * Résout le MultiDoku passé en paramètre, avec backtracking.
     * @param doku MultiDoku, MultiDoku à résoudre.
     * @return boolean, valant true si le MultiDoku est résolu, false sinon.
     */
    public static boolean solve(MultiDoku doku) {
        if (Thread.interrupted())
            throw new CancellationException("User wants to stop the solver");

        if (doku.isSolved()) {
            return true;
        }

        Cell cellToFill = doku.getFirstEmptyCell();
        if (cellToFill == null) {
            return false;
        }

        List<Integer> possibleSymbols = cellToFill.getPossibleSymbols();
        if (possibleSymbols.isEmpty()) {
            return false;
        }

        for (int symbol : possibleSymbols) {

            cellToFill.setSymbolIndex(symbol);
            if (Solver.solve(doku)) {
                return true;
            } else {
                cellToFill.setSymbolIndex(Cell.NOSYMBOL);
            }

        }
        return false;
    }

    /**
     * Résout le MultiDoku passé en paramètre, sans backtracking.
     * @param doku MultiDoku, MultiDoku à résoudre.
     * @return boolean, valant true si le MultiDoku est résolu, false sinon.
     */
    public static boolean humanSolve(MultiDoku doku) {
        if (Thread.interrupted())
            throw new CancellationException("User wants to stop the solver");

        List<Cell> cellsToFill = doku.getEmptyCells();

        while (!cellsToFill.isEmpty()) {

            Sudoku sudoku = doku.getSubGrid(0);
            logger.log(Level.FINE,
                    '\n' + SudokuPrinter.toStringRectangleSudoku(sudoku,
                            sudoku.getBlockWidth() == 0 ? sudoku.getSize() : sudoku.getBlockWidth(),
                            sudoku.getBlockWidth() == 0 ? sudoku.getSize() : sudoku.getSize() / sudoku.getBlockWidth()));

            boolean blocked = true;
            for (Cell cellToFill : cellsToFill) {

                List<Integer> possibleSymbols = cellToFill.getPossibleSymbols();
                if (possibleSymbols.size() != 1) {
                    continue;
                }

                cellToFill.setSymbolIndex(possibleSymbols.getFirst());
                cellsToFill.remove(cellToFill);
                blocked = false;
                break;
            }

            if (blocked) {
                break;
            }
        }

        return doku.isSolved();
    }
}
