package at.priv.graf.georg.sudokusolv;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;

public class SudokuBoard {

    private static final int maxSolutions = Main.maxSolutions;
    private final Box[][] box;
    private final String recursionPrefix;
    private int recursionLevel;

    public SudokuBoard(String path) throws IOException {
        recursionPrefix = " 0 ";
        box = new Box[9][9];
        byte[] inBytes = new FileInputStream(path).readAllBytes();
        int i; // Schleifenvariable
        int x = 0, y = 0;  // Koordinaten
        int wert;

        for (i = 0; i < inBytes.length; i++) {  // TODO switch
            if (inBytes[i] == ' ') continue;
            if (inBytes[i] == '\n') {
                y++;
                x = 0;
                continue;
            }
            if (inBytes[i] == '.') {
                box[y][x] = new Box();
                x++;
                continue;
            }
            wert = Integer.parseInt(Character.toString(inBytes[i]));
            box[y][x] = new Box(wert);
            x++;
        }
        checkValidity();
    }

    public SudokuBoard(SudokuBoard old, int recursionLevel) {
        this.recursionLevel = recursionLevel;
        recursionPrefix = String.format("%2d %s", recursionLevel, "   ".repeat(recursionLevel));
        this.box = new Box[9][9];
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                if (old.box[i][j].isFix()) this.box[i][j] = old.box[i][j];  // gleiches Objekt
                else this.box[i][j] = new Box();
            }
        }
        checkValidity();
    }


    public void checkValidity() {
        HashSet<Integer> nums;
        HashSet<Integer> constNums = new HashSet<>();
        for (int i = 1; i <= 9; i++) constNums.add(i);  // initialize constNums

        // Go over all the lines
        for (int i = 0; i < 9; i++) {
            nums = new HashSet<>(constNums);
            for (int j = 0; j < 9; j++) {
                if (box[i][j].isFix()) {
                    if (!nums.remove(box[i][j].getValue())) {
                        throw new IllegalStateException(String.format("checkValidity Line: Cannot remove %d at position %d/%d", box[i][j].getValue(), i, j));
                    }
                }
            }
        }
        // Now columns
        for (int j = 0; j < 9; j++) {
            nums = new HashSet<>(constNums);
            for (int i = 0; i < 9; i++) {
                if (box[i][j].isFix()) {
                    if (!nums.remove(box[i][j].getValue())) {
                        throw new IllegalStateException(String.format("checkValidity Column: Cannot remove %d at position %d/%d", box[i][j].getValue(), i, j));
                    }
                }
            }
        }
        // squares here
        for (int i = 0; i <= 6; i += 3) {
            for (int j = 0; j <= 6; j += 3) {
                nums = new HashSet<>(constNums);
                for (int k = i; k < i + 3; k++) {
                    for (int l = j; l < j + 3; l++) {
                        if (box[k][l].isFix()) {
                            if (!nums.remove(box[k][l].getValue())) {
                                throw new IllegalStateException(String.format("checkValidity Square: Cannot remove %d at position %d/%d", box[i][j].getValue(), i, j));
                            }
                        }
                    }
                }
            }
        }
    }


    public HashSet<SudokuBoard> solve() throws UnsolvableSudokuError {
        HashSet<SudokuBoard> rc = new HashSet<>();
        quicksolve();  // throws
        if (isSolved()) {
            rc.add(this);
            return rc;
        }
        // not yet fully solved
        Position tryPosition = getTryPosition();
        assert tryPosition != null;
        int x = tryPosition.x;
        int y = tryPosition.y;

        for (Integer wert : box[x][y].getPossibilitiesCopy()) {
            // If there are valid solutions, one item of this list will be part of it
            System.out.format(recursionPrefix + "START SOLV %s: _%d_ (Possible: %s)\n",
                    Position.str(x, y), wert, box[x][y].getPossibilities());
            SudokuBoard neu = new SudokuBoard(this, recursionLevel + 1);
            neu.box[x][y] = new Box(wert);
            try {
                rc.addAll(neu.solve());
                System.out.format(recursionPrefix + "SOLUTION!  %s: *%d* (POST SOLV)\n", Position.str(x, y), wert);
            } catch (UnsolvableSudokuError ex) {
                System.out.format(recursionPrefix + "POST  SOLV %s: _%d_ (Unsolvable, Was: %s)\n",
                        Position.str(x, y), wert, box[x][y].getPossibilities());
            }
            try {
                box[x][y].removePossibility(wert);
            } catch (IllegalBoxState e) {
                System.out.format(recursionPrefix + "DONE  SOLV %s: Exhausted last Possibility %s\n", Position.str(x, y), wert);
            }
            if (rc.size() >= maxSolutions) {
                System.out.format(recursionPrefix + "Too many solutions: %d\n", rc.size());
                return rc;
            }
        }
        if (rc.isEmpty())
            throw new UnsolvableSudokuError("No valid solution found here");
        return rc;
    }

    private Position getTryPosition() {
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                if (!box[i][j].isFix()) {
                    return new Position(i, j);
                }
            }
        }
        return null;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        return this.toString().equals(o.toString());
    }

    @Override
    public int hashCode() {
        return toString().hashCode();
    }

    @Override
    public String toString() {
        int x, y;
        StringBuilder rc = new StringBuilder("Sudoku Table:\n     ");
        // 97 ... 'a' 49 ... '1'
        for (x = 0; x < 9; x++) rc.append(String.format(" %3c", x + 49));
        rc.append("\n");
        // achtung x, y hier vertauscht!
        for (y = 0; y < 9; y++) {
            rc.append(String.format(" %3c:", y + 65));
            for (x = 0; x < 9; x++) {
                rc.append(String.format(" %3s", box[y][x].toString()));
            }
            rc.append("\n");
        }
        return rc.toString();
    }

    public void quicksolve() throws UnsolvableSudokuError {
        // return, if no new solutions possible with quicksolveFlat
        // runs until it does not get better
        int before, after;
        before = getOpen();
        while (true) {
            quicksolveFlat();
            after = getOpen();
            if (after == before) {
                return;  // this round didn't work, only way out here ;)
            }
            before = after;  // improvements this round, try one more!
        }
    }

    private void quicksolveFlat() throws UnsolvableSudokuError {
        checkValidity();
        Box currentBox;

        try {
            for (int i = 0; i < 9; i++) {
                for (int j = 0; j < 9; j++) {
                    currentBox = box[i][j];
                    if (currentBox.isFix()) continue;  // no need anymore

                    for (Integer removeMe : getOthersInColumn(i, j)) {
                        currentBox.removePossibility(removeMe);
                    }
                    for (Integer removeMe : getOthersInLine(i, j)) {
                        currentBox.removePossibility(removeMe);
                    }
                    for (Integer removeMe : getOthersInSquare(i, j)) {
                        currentBox.removePossibility(removeMe);
                    }
                }
            }
        } catch (IllegalBoxState e) {
            throw new UnsolvableSudokuError("Cannot quicksolveFlat");
        }
        checkValidity();
    }

    private ArrayList<Integer> getOthersInLine(int i, int j) {  // 3rd Line: y=2
        ArrayList<Integer> rc = new ArrayList<>();
        Box tmpBox;
        for (int x = 0; x < 9; x++) {
            if (x == i) continue;
            tmpBox = box[x][j];
            if (tmpBox.isFix()) rc.add(tmpBox.getValue());
        }
        return rc;
    }

    private ArrayList<Integer> getOthersInColumn(int i, int j) {
        ArrayList<Integer> rc = new ArrayList<>();
        Box tmpBox;
        for (int y = 0; y < 9; y++) {
            if (y == j) continue;
            tmpBox = box[i][y];
            if (tmpBox.isFix()) rc.add(tmpBox.getValue());
        }
        return rc;
    }

    private ArrayList<Integer> getOthersInSquare(int i, int j) {
        ArrayList<Integer> rc = new ArrayList<>();
        Box tmpBox;
        int xStart = i / 3 * 3;  // start-x 0..2:0, 3..5:3, 6..9:6
        int xEnd = xStart + 3;
        int yStart = j / 3 * 3;
        int yEnd = yStart + 3;
        for (int x = xStart; x < xEnd; x++) {
            for (int y = yStart; y < yEnd; y++) {
                if (x == i && y == j) continue;
                tmpBox = box[x][y];
                if (tmpBox.isFix()) rc.add(tmpBox.getValue());
            }
        }
        return rc;
    }

    public boolean isSolved() {
        return getOpen() == 0;
    }

    public int getOpen() {
        int rc = 0;
        for (int i = 0; i < 9; i++)
            for (int j = 0; j < 9; j++)
                if (!box[i][j].isFix()) rc++;
        return rc;
    }
}