package sudoku.io;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONObject;

import sudoku.constraint.Constraint;
import sudoku.constraint.IConstraint;
import sudoku.structure.Block;
import sudoku.structure.Cell;
import sudoku.structure.MultiDoku;
import sudoku.structure.Sudoku;

public class SudokuSerializer {

	public static JSONObject serializeSudoku(final MultiDoku multidoku) {
		List<Cell> cellIds = new ArrayList<>();
		List<Block> blockIds = new ArrayList<>();

		JSONObject jsonRoot = new JSONObject();
		JSONArray jsonCells = new JSONArray();
		JSONArray jsonBlocks = new JSONArray();
		JSONArray jsonSudokus = new JSONArray(multidoku.getNbSubGrids());

		for (int i = 0; i < multidoku.getNbSubGrids(); i++) {
			Sudoku sudoku = multidoku.getSubGrid(i);

			// init cells
			for (Cell cell : sudoku.getCells()) {
				if (cellIds.contains(cell))
					continue;

				cellIds.add(cell);

				Block block = cell.getBlock();
				if (!blockIds.contains(block)) {
					blockIds.add(block);
				}

				int blockID = blockIds.indexOf(block);
				assert (blockID >= 0);
				int symbolIndex = cell.getSymbolIndex();

				JSONObject cellJsonObject = new JSONObject();
				cellJsonObject.put("blockID", blockID);
				cellJsonObject.put("symbolIndex", symbolIndex);
				if (!cell.isMutable()) {
					cellJsonObject.put("immutable", true);
				}
				jsonCells.put(cellJsonObject);
			}

		}

		// init blocks

		for (Block blockId : blockIds) {
			JSONObject blockJsonObject = new JSONObject();
			JSONArray cellsJsonArray = new JSONArray();
			for (Cell cell : blockId.getCells()) {
				int cellID = cellIds.indexOf(cell);
				assert (cellID >= 0);
				cellsJsonArray.put(cellID);
			}
			blockJsonObject.put("cellIDs", cellsJsonArray);
			jsonBlocks.put(blockJsonObject);
		}

		for (int i = 0; i < multidoku.getNbSubGrids(); i++) {
			// serialise sub grid
			JSONObject jsonSudoku = new JSONObject();
			JSONArray cellsJsonArray = new JSONArray();
			JSONArray blocksJsonArray = new JSONArray();
			JSONArray constraintsJsonArray = new JSONArray();

			Sudoku sudoku = multidoku.getSubGrid(i);

			// serialize cells

			for (Cell cell : sudoku.getCells()) {
				int cellID = cellIds.indexOf(cell);
				assert (cellID >= 0);
				cellsJsonArray.put(cellID);
			}

			// serialize blocks

			for (Block block : sudoku.getBlocks()) {
				int blockID = blockIds.indexOf(block);
				assert (blockID >= 0);
				blocksJsonArray.put(blockID);
			}

			// serialize constraints

			for (IConstraint cons : sudoku.getConstraints()) {
				boolean constraintSerialized = false;
				for (Constraint enumCons : Constraint.values()) {
					if (cons.getClass().isAssignableFrom(enumCons.getConstraint().getClass())) {
						constraintSerialized = true;
						constraintsJsonArray.put(enumCons.ordinal());
					}
				}
				if (!constraintSerialized) {
					System.out.println("La contrainte " + cons.getClass() + " n'a pas pu être sérialisé !");
				}
			}

			jsonSudoku.put("constraints", constraintsJsonArray);
			jsonSudoku.put("cells", cellsJsonArray);
			jsonSudoku.put("blocks", blocksJsonArray);
			jsonSudoku.put("blockWidth", sudoku.getBlockWidth());
			jsonSudokus.put(i, jsonSudoku);
		}

		jsonRoot.put("multidoku", jsonSudokus);
		jsonRoot.put("cells", jsonCells);
		jsonRoot.put("blocks", jsonBlocks);

		return jsonRoot;
	}

	/**
	 * Save a serialized MultiDoku in a JSON file.
	 * 
	 * @param doku MultiDoku, MultiDoku to save.
	 * @return String, the path of the save.
	 */
	public static String saveMultiDoku(final MultiDoku doku) {
		JSONObject jsonRoot = serializeSudoku(doku);

		File f = new File("save", "save.json");
		int i = 0;
		while (f.exists()) {
			String newName = "save-" + ++i + ".json";
			f = new File("save", newName);
		}
		try (FileWriter file = new FileWriter(f)) {
			file.write(jsonRoot.toString(3));
		} catch (IOException e) {
			e.printStackTrace();
		}
		return f.getAbsolutePath();
	}

	public static String saveMultiDoku(final MultiDoku doku, final int saveToOverwrite) {
		File f;
		if (saveToOverwrite == 0) {
			f = new File("save", "save.json");
		} else {
			f = new File("save", "save-" + saveToOverwrite + ".json");
		}
		if (!f.exists()) {
			return saveMultiDoku(doku);
		} else {
			try (FileWriter file = new FileWriter(f)) {
				file.write(serializeSudoku(doku).toString(3));
			} catch (IOException e) {
				e.printStackTrace();
			}
			return f.getAbsolutePath();
		}
	}

	/**
	 * Get a MultiDoku from a pre-existing json save file.
	 * 
	 * @param numberSave int, number of the save file to open.
	 * @return MultiDoku, MultoDoku contained in the file.
	 * @throws Exception when the given save file does not exist.
	 */
	public static MultiDoku getSavedMultiDoku(int numberSave) throws Exception {
		String fileName;
		if (numberSave < 1) {
			fileName = "save.json";
		} else {
			fileName = "save-" + numberSave + ".json";
		}

		File f = new File("save", fileName);
		String fileContent;

		if (!f.exists()) {
			throw new Exception("This save does not exist.");
		} else {
			fileContent = new String(Files.readAllBytes(Paths.get("save/" + fileName)));
			return deserializeSudoku(fileContent);
		}
	}

	public static MultiDoku deserializeSudoku(final String json) {
		JSONObject jsonRoot = new JSONObject(json);
		return deserializeSudoku(jsonRoot);
	}

	public static MultiDoku deserializeSudoku(final JSONObject jsonObject) {

		List<Cell> cells = new ArrayList<>();
		List<Block> blocks = new ArrayList<>();
		List<Sudoku> sudokus = new ArrayList<>();

		// init cells
		JSONArray cellsJson = jsonObject.getJSONArray("cells");
		for (int i = 0; i < cellsJson.length(); i++) {
			JSONObject entry = cellsJson.getJSONObject(i);
			int symbolIndex = entry.getInt("symbolIndex");
			if (entry.has("immutable")) {
				cells.add(new Cell(symbolIndex, false));
			} else {
				cells.add(new Cell(symbolIndex));
			}
		}

		// init blocks
		JSONArray blocksJson = jsonObject.getJSONArray("blocks");
		for (int i = 0; i < blocksJson.length(); i++) {
			JSONObject entry = blocksJson.getJSONObject(i);
			JSONArray cellIds = entry.getJSONArray("cellIDs");
			List<Cell> blockCells = new ArrayList<>();
			Block newBlock = new Block(blockCells);
			for (int j = 0; j < cellIds.length(); j++) {
				int cellID = cellIds.getInt(j);
				Cell blockCell = cells.get(cellID);
				blockCell.setBlock(newBlock);
				blockCells.add(blockCell);
			}
			blocks.add(newBlock);
		}

		JSONArray multidokuJsonObject = jsonObject.getJSONArray("multidoku");
		for (int i = 0; i < multidokuJsonObject.length(); i++) {
			JSONObject sudokuJsonObject = multidokuJsonObject.getJSONObject(i);
			JSONArray sudokuCellsJsonArray = sudokuJsonObject.getJSONArray("cells");
			JSONArray sudokuBlocksJsonArray = sudokuJsonObject.getJSONArray("blocks");
			JSONArray sudokuConstraintsJsonArray = sudokuJsonObject.getJSONArray("constraints");

			List<Cell> sudokuCells = new ArrayList<>();
			List<Block> sudokuBlocks = new ArrayList<>();
			List<IConstraint> sudokuConstraints = new ArrayList<>();

			for (int j = 0; j < sudokuCellsJsonArray.length(); j++) {
				int cellID = sudokuCellsJsonArray.getInt(j);
				sudokuCells.add(cells.get(cellID));
			}

			for (int j = 0; j < sudokuBlocksJsonArray.length(); j++) {
				int blockID = sudokuBlocksJsonArray.getInt(j);
				sudokuBlocks.add(blocks.get(blockID));
			}

			for (int j = 0; j < sudokuConstraintsJsonArray.length(); j++) {
				int constraintID = sudokuConstraintsJsonArray.getInt(j);
				sudokuConstraints.add(Constraint.values()[constraintID].getConstraint());
			}

			Sudoku s = new Sudoku(sudokuCells, sudokuBlocks, sudokuConstraints);
			s.setBlockWidth(sudokuJsonObject.getInt("blockWidth"));
			for (Block block : s.getBlocks()) {
				block.getSudokus().add(s);
			}
			sudokus.add(s);
		}

		return new MultiDoku(sudokus);
	}

}
