package sudoku.structure;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Random;

import sudoku.constraint.Constraint;
import sudoku.constraint.IConstraint;
import sudoku.io.SudokuSerializer;
import sudoku.solver.RandomSolver;
import sudoku.solver.Solver;

public class SudokuFactory {

	/**
	 * Générateur de nombre aléatoire.
	 */
	private static final Random random = new Random();

	/**
	 * Liste des contraintes par défaut d'un Multi- ou Sudoku.
	 * Comprend les contraintes de blocs, de lignes, et de colonnes.
	 */
	public static List<IConstraint> DEFAULT_CONSTRAINTS = SudokuFactory
			.fromConstraints(Arrays.asList(Constraint.Block, Constraint.Column,
					Constraint.Line));

	/**
	 * Créée des Cells et les met dans une liste de taille size.
	 * 
	 * @param size int, nombre de Cells à initialiser.
	 * @return List<Cell>, liste des Cells initialisées.
	 */
	private static List<Cell> initCells(int size) {
		List<Cell> cells = new ArrayList<>(size * size);
		for (int i = 0; i < size * size; i++) {
			cells.add(new Cell());
		}
		return cells;
	}

	/**
	 * Créée des Blocks de taille width par height à partir des cellules données, et
	 * les met dans une liste.
	 * 
	 * @param cells  List<Cell>, liste des Cells à découper en Blocks.
	 * @param width  int, largeur des Blocks à créer.
	 * @param height int, hauteur des Blocks à créer.
	 * @return List<Block>, liste des Blocks créés.
	 */
	private static List<Block> initRectangleBlocs(List<Cell> cells, int width, int height) {
		List<Block> blocs = new ArrayList<>();
		int size = width * height;
		for (int i = 0; i < size; i++) {
			Block newBlock = new Block();
			int blockX = i % height;
			int blockY = i / height;
			for (int y = 0; y < height; y++) {
				for (int x = 0; x < width; x++) {
					int index = ((y + blockY * height) * size + (x + blockX * width));
					Cell blockCell = cells.get(index);
					blockCell.setBlock(newBlock);
					// List<Block> blockContainers = new ArrayList<>();
					// blockContainers.add(newBlock);
					// blockCell.setBlockContainers(blockContainers);
					newBlock.addCell(blockCell);
				}
			}
			blocs.add(newBlock);
		}
		return blocs;
	}

	/**
	 * Créée un MultiDoku vide dont les Blocks sont de taille widthBlock par
	 * heightBlock.
	 * 
	 * @param widthBlock  int, largeur des Blocks.
	 * @param heightBlock int, hauteur des Blocks.
	 * @return MultiDoku, MultiDoku vide.
	 */
	public static MultiDoku createBasicEmptyRectangleDoku(int widthBlock, int heightBlock,
			List<IConstraint> constraints) {
		return new MultiDoku(Arrays.asList(createRectangleSudoku(widthBlock, heightBlock, constraints)));
	}

	/**
	 * Créée un MultiDoku vide dont les Blocks sont carrés de longueur size.
	 * 
	 * @param size int, taille des Blocks.
	 * @return MultiDoku, MultiDoku vide.
	 */
	public static MultiDoku createBasicEmptySquareDoku(int size, List<IConstraint> constraints) {
		return new MultiDoku(Arrays.asList(createSquareSudoku(size, constraints)));
	}

	/**
	 * Place des Cells immutables de valeurs fournies, aux Coordinate fournies dans
	 * le MultiDoku doku fourni.
	 * 
	 * @param doku           MultiDoku, MultiDoku à remplir.
	 * @param immutableCells Map<Coordinate, Integer>, association de Coordinate
	 *                       coordonnées et Integer valeurs, correspondant aux cases
	 *                       à remplir.
	 */
	public static void setImmutableCells(MultiDoku doku, Map<Coordinate, Integer> immutableCells) {
		immutableCells.forEach((coordinate, symbol) -> {
			for (Sudoku sudoku : doku.getSubGrids()) {
				Cell cell = sudoku.getCell(coordinate.getX(), coordinate.getY());
				if (cell != null) {
					cell.setSymbolIndex(symbol);
					cell.setImmutable();
				}
			}
		});
	}

	/**
	 * Créée un MultiDoku de difficulté difficulty à partir d'un MultiDoku fourni.
	 *
	 * @param doku           MultiDoku, MultiDoku dont on doit vider des Cells.
	 * @param nbCellsToEmpty int, nombre de cases à retirer.
	 * @return boolean, valant true si un MultiDoku de difficulté donnée peut être
	 *         créée, false sinon.
	 * @throws Exception si la difficulté n'est pas compatible avec la taille du
	 *                   MultiDoku.
	 */
	public static boolean newDokuFromFilledOne(MultiDoku doku, int nbCellsToEmpty, Solver solver) throws Exception {

		if (nbCellsToEmpty >= doku.getCells().size()) {
			throw new Exception();
		}

		if (nbCellsToEmpty == 0) {
			return true;
		}

		List<Cell> cellsThatCanBeEmptied = doku.getFilledCells();

		int index = random.nextInt(cellsThatCanBeEmptied.size());
		Cell cellToEmpty = cellsThatCanBeEmptied.get(index);

		cellToEmpty.empty();

		int nbDokuSultions = solver.countSolution(doku);
		if (nbDokuSultions == 1) {
            return newDokuFromFilledOne(doku, --nbCellsToEmpty, solver);
		}

		return false;
	}

	/**
	 * Créée un Sudoku vide dont les Blocks sont de taille widthBlock par
	 * heightBlock.
	 * 
	 * @param widthBlock  int, largeur des Blocks.
	 * @param heightBlock int, hauteur des Blocks.
	 * @return Sudoku, Sudoku vide.
	 */
	private static Sudoku createRectangleSudoku(int widthBlock, int heightBlock, List<IConstraint> constraints) {
		int symbolCount = widthBlock * heightBlock;
		List<Cell> cases = initCells(symbolCount);
		List<Block> blocs = initRectangleBlocs(cases, widthBlock, heightBlock);
		Sudoku s = new Sudoku(cases, blocs, constraints);
		for (Block block : s.getBlocks()) {
			block.getSudokus().add(s);
		}
		s.setBlockWidth(widthBlock);
		return s;
	}

	/**
	 * Créée un Sudoku vide dont les Blocks sont carrés de longueur size.
	 * 
	 * @param size int, taille des Blocks.
	 * @return Sudoku, Sudoku vide.
	 */
	private static Sudoku createSquareSudoku(int size, List<IConstraint> constraints) {
		return createRectangleSudoku(size, size, constraints);
	}

	/**
	 * Connecte deux Sudokus selon la décalage offset fourni.
	 * 
	 * @param sudoku1 Sudoku, premier sudoku à connecter.
	 * @param sudoku2 Sudoku, second sudoku à connecter.
	 * @param offset  Coordinate, décalage entre les deux Sudokus.
	 */
	private static void linkRectangleSudokus(Sudoku sudoku1, Sudoku sudoku2, Coordinate offset) {
		int blockWidth = sudoku1.getBlockWidth();
		int blockHeight = sudoku1.getSize() / blockWidth;
		for (int dx = 0; dx < blockHeight; dx++) {
			for (int dy = 0; dy < blockWidth; dy++) {
				int block1X = dx + offset.getX();
				int block1Y = dy + offset.getY();
				int block2X = dx;
				int block2Y = dy;

				if ((block1X < blockHeight) && (block1X >= 0) && (block1Y >= 0) && (block1Y < blockWidth)) {
					Block block1 = sudoku1.getBlocks().get(block1Y * blockHeight + block1X);
					Block block2 = sudoku2.getBlocks().get(block2Y * blockHeight + block2X);

					// on remplace le bloc
					sudoku2.getBlocks().set(block2Y * blockHeight + block2X, block1);
					block1.getSudokus().add(sudoku2);

					// on remplace les cellules
					for (int i = 0; i < block1.getCells().size(); i++) {
						Cell newCell = block1.getCells().get(i);
						Cell oldCell = block2.getCells().get(i);

						int oldCellIndex = sudoku2.getCells().indexOf(oldCell);
						sudoku2.getCells().set(oldCellIndex, newCell);
					}
				}
			}
		}
	}

	/**
	 * Créée un MultiDoku de Blocks carrés de taille size composé de cinq Sudokus,
	 * dont un central qui partage chacun de ses Blockss d'angle avec un autre
	 * Sudoku.
	 * 
	 * @param size int, largeur des Blocks unitraires des Sudokus à crééer.
	 * @return MultiDoku, MultiDoku de forme X.
	 */
	public static MultiDoku createBasicXShapedMultidoku(int size, List<IConstraint> constraints) {
		assert (size > 1);

		return createBasicXShapedMultidoku(size, size, constraints);
	}

	/**
	 * Créée un MultiDoku de Blocks rectangulaires de forme width par height composé
	 * de cinq Sudokus,
	 * dont un central qui partage chacun de ses Blocks d'angle avec un autre
	 * Sudoku.
	 *
	 * @param width  int, largeur des Blocks unitraires des Sudokus à crééer.
	 * @param height int, hauteur des Blocks unitraires des Sudokus à crééer.
	 * @return MultiDoku, MultiDoku de forme X.
	 */
	public static MultiDoku createBasicXShapedMultidoku(int width, int height, List<IConstraint> constraints) {
		assert (width > 1 && height > 1);

		/*
		 * 2 3
		 *  1
		 * 4 5
		 */

		Sudoku sudoku1 = createRectangleSudoku(width, height, constraints);
		Sudoku sudoku2 = createRectangleSudoku(width, height, constraints);
		Sudoku sudoku3 = createRectangleSudoku(width, height, constraints);
		Sudoku sudoku4 = createRectangleSudoku(width, height, constraints);
		Sudoku sudoku5 = createRectangleSudoku(width, height, constraints);

		linkRectangleSudokus(sudoku1, sudoku2, new Coordinate(1 - height, 1 - width));
		linkRectangleSudokus(sudoku1, sudoku3, new Coordinate(height - 1, 1 - width));
		linkRectangleSudokus(sudoku1, sudoku4, new Coordinate(1 - height, width - 1));
		linkRectangleSudokus(sudoku1, sudoku5, new Coordinate(height - 1, width - 1));

		return new MultiDoku(Arrays.asList(sudoku1, sudoku2, sudoku3, sudoku4, sudoku5));
	}

	/**
	 * TODO
	 * Créée un MultiDoku de Blocks carrés de taille size composé de cinq Sudokus,
	 * dont un central qui partage chacun de ses Blockss d'angle avec un autre
	 * Sudoku.
	 *
	 * @param size int, largeur des Blocks unitraires des Sudokus à crééer.
	 * @return MultiDoku, MultiDoku de forme X.
	 */
	public static MultiDoku createBasicPlusShapedMultidoku(int size, List<IConstraint> constraints) {
		assert (size > 1);

		return createBasicPlusShapedMultidoku(size, size, constraints);
	}

	/**
	 * TODO
	 * Créée un MultiDoku de Blocks rectangulaires de forme width par height composé
	 * de cinq Sudokus,
	 * dont un central qui partage chacun de ses Blocks d'angle avec un autre
	 * Sudoku.
	 *
	 * @param width  int, largeur des Blocks unitraires des Sudokus à crééer.
	 * @param height int, hauteur des Blocks unitraires des Sudokus à crééer.
	 * @return MultiDoku, MultiDoku de forme X.
	 */
	public static MultiDoku createBasicPlusShapedMultidoku(int width, int height, List<IConstraint> constraints) {
		assert (width > 1 && height > 1);

		/*
		 *   3
		 * 2 1 4
		 *   5
		 */

		Sudoku sudoku1 = createRectangleSudoku(width, height, constraints);
		Sudoku sudoku2 = createRectangleSudoku(width, height, constraints);
		Sudoku sudoku3 = createRectangleSudoku(width, height, constraints);
		Sudoku sudoku4 = createRectangleSudoku(width, height, constraints);
		Sudoku sudoku5 = createRectangleSudoku(width, height, constraints);

		linkRectangleSudokus(sudoku1, sudoku2, new Coordinate(1 - height, 0));
		linkRectangleSudokus(sudoku1, sudoku3, new Coordinate(0, 1 - width));
		linkRectangleSudokus(sudoku1, sudoku4, new Coordinate(height - 1, 0));
		linkRectangleSudokus(sudoku1, sudoku5, new Coordinate(0, width - 1));

		return new MultiDoku(Arrays.asList(sudoku1, sudoku2, sudoku3, sudoku4, sudoku5));
	}

	public static void fillDoku(MultiDoku doku, Difficulty difficulty) throws Exception {
		Solver solver = new RandomSolver();
		solver.solve(doku);
		int nbCellsToEmpty = (int) (difficulty.getFactor() * doku.getNbCells());
		boolean successfull = newDokuFromFilledOne(doku, nbCellsToEmpty, solver);
		doku.setFilledCellsImmutable();
	}

	public static MultiDoku fromfile(String filePath) {
		try {
			String content = Files.readString(Paths.get(filePath));
			MultiDoku doku = SudokuSerializer.deserializeSudoku(content);
			return doku;
		} catch (IOException e) {
			e.printStackTrace();
			return null;
		}
	}

	public static MultiDoku createBasicEmptyRandomBlockDoku(int blockSize, List<IConstraint> constraints) {
		int blockCellCount = blockSize * blockSize;
		List<Cell> cells = initCells(blockCellCount);
        List<Cell> homeLessCells = new ArrayList<>(cells);
		List<Block> blocks = new ArrayList<>();
		Random r = new Random();
		for (int i = 0; i < blockCellCount; i++) {
			Block b = new Block();
			for (int j = 0; j < blockCellCount; j++) {
				int cellIndex = r.nextInt(homeLessCells.size());
				Cell cell = homeLessCells.remove(cellIndex);
				b.addCell(cell);
				cell.setBlock(b);
			}
			blocks.add(b);
		}
		Sudoku sudoku = new Sudoku(cells, blocks, constraints);
		for (Block block : blocks) {
			block.getSudokus().add(sudoku);
		}
		return new MultiDoku(Arrays.asList(sudoku));
	}

	public static List<IConstraint> fromConstraints(List<Constraint> constraints) {
		List<IConstraint> iconstraints = new ArrayList<>();
		for (Constraint cons : constraints) {
			iconstraints.add(cons.getConstraint());
		}
		return iconstraints;
	}
}
