package gui;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import sudoku.structure.Block;
import sudoku.structure.Cell;
import sudoku.structure.Coordinate;
import sudoku.structure.MultiDoku;
import sudoku.structure.Sudoku;

public class RenderableMultidoku {

	private final MultiDoku doku;
	private final List<Cell> cells;
	private final List<Block> blocks;
	private final int width;
	private final int height;

	private RenderableMultidoku(MultiDoku doku, List<Block> blocks, List<Cell> cells, int width, int height) {
		this.cells = cells;
		this.blocks = blocks;
		this.width = width;
		this.height = height;
		this.doku = doku;
	}

	public int getWidth() {
		return width;
	}

	public int getHeight() {
		return height;
	}

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

	public Cell getCell(int x, int y) {
		return getCell(y * width + x);
	}

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

	private static record PositionConstraint(Sudoku sudoku1, Sudoku sudoku2, Coordinate offset) {
	}

	private static Coordinate getConstraint(Sudoku sudoku1, Sudoku sudoku2) {
		int blockWidth = sudoku1.getBlockWidth();
		int blockHeight = sudoku1.getSize() / blockWidth;
		for (int i = 0; i < sudoku1.getSize(); i++) {
			for (int j = 0; j < sudoku2.getSize(); j++) {
				Block block1 = sudoku1.getBlocks().get(i);
				Block block2 = sudoku2.getBlocks().get(j);
				if (block1 == block2) {
					int block1X = i % blockHeight;
					int block1Y = i / blockHeight;
					int block2X = j % blockHeight;
					int block2Y = j / blockHeight;
					return new Coordinate((block1X - block2X) * blockWidth, (block1Y - block2Y) * blockHeight);
				}
			}
		}
		return null;
	}

	private static boolean hasContraint(Sudoku sudoku1, Sudoku sudoku2, List<PositionConstraint> positionConstraints) {
		for (PositionConstraint constraint : positionConstraints) {
			if ((constraint.sudoku1 == sudoku1 && constraint.sudoku2 == sudoku2)
					|| (constraint.sudoku1 == sudoku2 && constraint.sudoku2 == sudoku1))
				return true;
		}
		return false;
	}

	private static Coordinate getMinSudokuOffset(Map<Sudoku, Coordinate> sudokusOffset) {
		Coordinate minCoordinate = null;
		for (Coordinate coordinate : sudokusOffset.values()) {
			if (minCoordinate == null)
				minCoordinate = coordinate;
			minCoordinate = new Coordinate(Math.min(minCoordinate.getX(), coordinate.getX()),
					Math.min(minCoordinate.getY(), coordinate.getY()));
		}
		return minCoordinate;
	}

	private static Coordinate getMaxSudokuCoordinate(Map<Sudoku, Coordinate> sudokusOffset) {
		int maxX = 0;
		int maxY = 0;
		Sudoku lastSudoku = null;
		for (var entry : sudokusOffset.entrySet()) {
			Coordinate coordinate = entry.getValue();
			if (coordinate.getX() > maxX)
			maxX = coordinate.getX();
			if (coordinate.getY() > maxY)
			maxY = coordinate.getY();
			lastSudoku = entry.getKey();
		}

		Coordinate maxCoordinate = new Coordinate(maxX, maxY);
		// tous les sudokus sont censés faire la même taille
		int sudokuSize = lastSudoku.getSize();

		return new Coordinate(maxCoordinate.getX() + sudokuSize, maxCoordinate.getY() + sudokuSize);
	}

	public static RenderableMultidoku fromMultidoku(MultiDoku doku) {
		if (doku.getNbSubGrids() == 1) {
			Sudoku sudoku = doku.getSubGrid(0);
			return new RenderableMultidoku(doku, sudoku.getBlocks(), sudoku.getCells(), sudoku.getSize(),
					sudoku.getSize());
		}
		Map<Sudoku, Coordinate> sudokusOffset = new HashMap<>();
		// coordinates in cell
		List<PositionConstraint> positionConstraints = new ArrayList<>();
		for (Sudoku sudoku1 : doku.getSubGrids()) {
			for (Sudoku sudoku2 : doku.getSubGrids()) {
				if (sudoku1 == sudoku2)
					continue;

				Coordinate constraint = getConstraint(sudoku1, sudoku2);

				if (constraint != null && !hasContraint(sudoku1, sudoku2, positionConstraints)) {
					positionConstraints.add(new PositionConstraint(sudoku1, sudoku2, constraint));
				}
			}
		}

		for (PositionConstraint constraint : positionConstraints) {
			if (!sudokusOffset.containsKey(constraint.sudoku1) && !sudokusOffset.containsKey(constraint.sudoku2)) {
				sudokusOffset.put(constraint.sudoku1, new Coordinate(0, 0));
				sudokusOffset.put(constraint.sudoku2, constraint.offset());
			}

			if (sudokusOffset.containsKey(constraint.sudoku1)) {
				sudokusOffset.put(constraint.sudoku2, constraint.offset.add(sudokusOffset.get(constraint.sudoku1)));
			} else {
				sudokusOffset.put(constraint.sudoku1, constraint.offset.add(sudokusOffset.get(constraint.sudoku2)));
			}

		}

		Coordinate minCoordinate = getMinSudokuOffset(sudokusOffset);

		for (var entry : sudokusOffset.entrySet()) {
			entry.setValue(entry.getValue().sub(minCoordinate));
		}

		Coordinate maxCoordinate = getMaxSudokuCoordinate(sudokusOffset);

		List<Block> blocks = new ArrayList<>();
		List<Cell> cells = new ArrayList<>(maxCoordinate.getX() * maxCoordinate.getY());
		for (int i = 0; i < maxCoordinate.getX() * maxCoordinate.getY(); i++) {
			cells.add(null);
		}

		for (var entry : sudokusOffset.entrySet()) {
			Sudoku sudoku = entry.getKey();
			Coordinate offset = entry.getValue();

			for (int x = 0; x < sudoku.getSize(); x++) {
				for (int y = 0; y < sudoku.getSize(); y++) {
					Cell cell = sudoku.getCell(x, y);
					int absoluteX = x + offset.getX();
					int absoluteY = y + offset.getY();
					cells.set(absoluteY * maxCoordinate.getX() + absoluteX, cell);
				}
			}

			for (Block block : sudoku.getBlocks()) {
				if (!blocks.contains(block)) {
					blocks.add(block);
				}
			}
		}

		return new RenderableMultidoku(doku, blocks, cells, maxCoordinate.getX(), maxCoordinate.getY());
	}

	public MultiDoku getDoku() {
		return doku;
	}

}
