package sudoku.structure;

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

import sudoku.constraint.Constraint;
import sudoku.constraint.IConstraint;

/**
 * @class Sudoku
 * @brief Représent un Sudoku
 */
public class Sudoku {

	/**
	 * Liste des Block contenus dans le Sudoku.
	 */
	private final List<Block> blocks;
	/**
	 * Liste des Cells contenus dans le Sudoku.
	 */
	private List<Cell> cells = new ArrayList<>();
	/**
	 * Liste des contraintes (TODO) du Sudoku.
	 */
	private final List<IConstraint> constraints;
	/**
	 * Largeur des Blocks s'ils sont rectangulaires, valant 0 si ce n'est pas le
	 * cas.
	 */
	private int blockWidth;

	public Sudoku(List<Cell> cells, List<Block> blocks, List<IConstraint> constraints) {
		this.cells = cells;
		this.blocks = blocks;
		this.constraints = constraints;
	}

	/**
	 * Transforme un index de Cell en Coordinate.
	 *
	 * @param index int, index d'une Cell.
	 * @return Coordinate, correspondante à l'index donné.
	 */
	public Coordinate toCoords(int index) {
		return new Coordinate(index % getSize(), index / getSize());
	}

	/**
	 * Transforme des coordonées d'une Cell en index.
	 * 
	 * @param x int, abscisse.
	 * @param y int, ordonnée.
	 * @return int, index correspondant.
	 */
	public int toIndex(int x, int y) {
		return y * getSize() + x;
	}

	/**
	 * Vérifie que des coordonnées correspondent bien à une Cell dans le Sudoku.
	 * 
	 * @return boolean, valant true si les coordonnées sont dans les bornes du
	 *         Sudoku, false sinon.
	 */
	public boolean isValidCoords(int x, int y) {
		int index = toIndex(x, y);
		return isValidCoords(index);
	}

	/**
	 * Vérifie que l'index correspond bien à une Cell dans le Sudoku.
	 * 
	 * @return boolean, valant true si l'index est dans les bornes du Sudoku, false
	 *         sinon.
	 */
	public boolean isValidCoords(int index) {
		return index < getSize() * getSize();
	}

	/**
	 * Teste si on peut placer la value dans la Cell aux coordonnées x, y d'après
	 * les contraintes du Sudoku.
	 * 
	 * @param x     int, abscisse de la Cell voulue.
	 * @param y     int, ordonnée de la Cell voulue.
	 * @param value int, index du symbole qu'on veut placer.
	 * @return boolean, true si on peut la placer et false sinon.
	 */
	public boolean canBePlaced(int x, int y, int value) {
		for (IConstraint constraint : this.constraints) {
			if (!constraint.canBePlaced(this, x, y, value)) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Vide la Cell dotn les coordonnées sont renseignées de son symbole.
	 * 
	 * @param x int, abscisse de la Cell voulue.
	 * @param y int, coordonnée de la Cell voulue.
	 */

	public void clearCell(int x, int y) {
		assert (isValidCoords(x, y));
		Cell cell = getCell(x, y);
		cell.setSymbolIndex(Cell.NOSYMBOL);
	}

	/**
	 * Vide toutes les Cell du Sudoku.
	 */
	public void clear() {
		for (int i = 0; i < getSize() * getSize(); i++) {
			Cell cell = getCell(i);
			if (cell.isMutable())
				cell.setSymbolIndex(Cell.NOSYMBOL);
		}
	}

	public int getBlockWidth() {
		return blockWidth;
	}

	/**
	 * Place le symbole d'index value dans la Cell de coordonnées précisées.
	 * 
	 * @param x     int, abscisse de la Cell voulue.
	 * @param y     int, coordonnée de la Cell voulue.
	 * @param value int, index du symbole à placer.
	 * @return Cell, la Cell qui a été modifiée.
	 */
	public Cell setCellSymbol(int x, int y, int value) {
		assert (isValidCoords(x, y));
		for (IConstraint constraint : this.constraints) {
			if (!constraint.canBePlaced(this, x, y, value)) {
				return null;
			}
		}
		Cell cell = getCell(x, y);
		cell.setSymbolIndex(value);
		return cell;
	}

	/**
	 * Place les symboles d'index contenus dans values dans les cases du Sudoku.
	 * 
	 * @param values List<Integer>, liste des index des symboles à placer.
	 * @return boolean, vaut true si les symboles ont été placés, false sinon.
	 */
	public boolean setCellsSymbol(List<Integer> values) {
		if (values.size() > this.cells.size()) {
			return false;
		}
		for (int i = 0; i < values.size(); i++) {
			int x = i % this.blocks.size();
			int y = (i - x) / this.blocks.size();
			int value = values.get(i);
			this.setCellSymbol(x, y, value);
		}
		return true;
	}

	/**
	 * Place les symboles d'index contenus dans values dans les cases du Sudoku et
	 * rend ces cases immuables.
	 * 
	 * @param values List<Integer>, liste des index des symboles à placer.
	 * @return boolean, vaut true si les symboles ont été placés, false sinon.
	 */
	public boolean setImmutableCellsSymbol(List<Integer> values) {
		if (values.size() > this.cells.size()) {
			return false;
		}
		for (int i = 0; i < values.size(); i++) {
			int x = i % this.blocks.size();
			int y = (i - x) / this.blocks.size();
			int value = values.get(i);
			if (value != Cell.NOSYMBOL) {
				Cell cellPlaced = this.setCellSymbol(x, y, value);
				if (cellPlaced == null) {
					continue;
				}
				cellPlaced.setImmutable();
			}
		}
		return true;
	}

	public Cell getCell(int x, int y) {
		int index = toIndex(x, y);
		assert (isValidCoords(x, y));
		try {
			return this.cells.get(index);
		} catch (IndexOutOfBoundsException e) {
			return null;
		}
	}

	public Cell getCell(int i) {
		return this.cells.get(i);
	}

	public List<IConstraint> getConstraints() {
		return constraints;
	}

	public int getSize() {
		return this.blocks.size();
	}

	public List<Cell> getCells() {
		return this.cells;
	}

	public List<Block> getBlocks() {
		return this.blocks;
	}

	/**
	 * Vérifie si une Cell appartient au Sudoku.
	 * 
	 * @param cell Cell, cellule dont on veut vérifier l'appartenance au Sudoku.
	 * @return boolean, vaut true si la Cell appartient au Sudoku.
	 */
	public boolean contains(Cell cell) {
		return this.cells.contains(cell);
	}

	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append("Sudoku {");
		for (int i = 0; i < getSize(); i++) {
			sb.append("\n\t");
			for (int j = 0; j < getSize(); j++) {
				Cell cell = getCell(i, j);
				sb.append(cell.toString()).append(" ");
			}
		}
		sb.append("\n}");
		return sb.toString();
	}

	/**
	 * Renvoie la 1re Cell vide du Sudoku.
	 * 
	 * @return Cell, une Cell vide, ou null s'il n'y en a pas.
	 */
	public Cell getFirstEmptyCell() {
		for (Cell cell : this.cells) {
			if (cell.isEmpty()) {
				return cell;
			}
		}
		return null;
	}

	/**
	 * Vérifie si le Sudoku est résolue, soit complet et cohérent avec ses
	 * contraintes.
	 * 
	 * @return boolean, valant true si le Sudoku est résolu, false sinon.
	 */
	public boolean isSolved() {
		boolean isComplete = isComplete();
		boolean isValid = isValid();
		return isComplete && isValid;
	}

	/**
	 * Vérifie que le Sudoku est complet, soit qu'il n'y ait aucune case vide.
	 * 
	 * @return boolean, true si le Sudoku est complet, false sinon.
	 */
	private boolean isComplete() {
		return getFirstEmptyCell() == null;
	}

	/**
	 * Vérifie si le Sudoku est valide, soit qu'il est cohérent avec ses
	 * contraintes.
	 * 
	 * @return bollean, true si le Sudoku est valide, false sinon
	 */
	private boolean isValid() {
		for (int i = 0; i < cells.size(); i++) {
			Cell cell = getCell(i);
			if (cell.isEmpty())
				continue;

			Coordinate coordinate = toCoords(i);

			int symbolPlaced = cell.empty();
			if (!canBePlaced(coordinate.getX(), coordinate.getY(), symbolPlaced)) {
				cell.setSymbolIndex(symbolPlaced);
				return false;
			}
			cell.setSymbolIndex(symbolPlaced);
		}
		return true;
	}

	public void setBlockWidth(int blockWidth) {
		this.blockWidth = blockWidth;
	}

	public boolean hasConstraint(Constraint constraint) {
		return this.constraints.contains(constraint.getConstraint());
	}

}
