import 'dart:math';

import 'package:flutter_custom_toolbox/flutter_toolbox.dart';

import 'package:sudoku/models/game/cell.dart';
import 'package:sudoku/models/game/cell_location.dart';
import 'package:sudoku/models/game/types.dart';

class Board {
  Board({
    required this.cells,
  });

  BoardCells cells = const [];

  factory Board.createEmpty() {
    return Board(
      cells: [],
    );
  }

  factory Board.createNew({
    required BoardCells cells,
  }) {
    return Board(
      cells: cells,
    );
  }

  factory Board.createFromTemplate({
    required String template,
    required bool isSymetric,
  }) {
    // Create board cells from size, with "empty" cells
    BoardCells createEmptyBoard(final int boardSize) {
      final BoardCells cells = [];
      for (int rowIndex = 0; rowIndex < boardSize; rowIndex++) {
        final List<Cell> row = [];
        for (int colIndex = 0; colIndex < boardSize; colIndex++) {
          row.add(Cell(
            location: CellLocation.go(rowIndex, colIndex),
            value: 0,
            isFixed: false,
          ));
        }
        cells.add(row);
      }

      return cells;
    }

    BoardCells cells = [];
    final int boardSize = int.parse(pow(template.length, 1 / 2).toStringAsFixed(0));

    const String stringValues = '0123456789ABCDEFG';

    int index = 0;
    for (int rowIndex = 0; rowIndex < boardSize; rowIndex++) {
      final List<Cell> row = [];
      for (int colIndex = 0; colIndex < boardSize; colIndex++) {
        final String stringValue = template[index++];
        final int value = stringValues.indexOf(stringValue);
        row.add(Cell(
          location: CellLocation.go(rowIndex, colIndex),
          value: value,
          isFixed: (value != 0),
        ));
      }
      cells.add(row);
    }

    const List<String> allowedFlip = ['none', 'horizontal', 'vertical'];
    List<String> allowedRotate = ['none', 'left', 'right'];

    // Forbid rotation if blocks are not symetric
    if (!isSymetric) {
      allowedRotate = ['none'];
    }

    final Random rand = Random();
    final String flip = allowedFlip[rand.nextInt(allowedFlip.length)];
    final String rotate = allowedRotate[rand.nextInt(allowedRotate.length)];

    printlog('flip board: $flip');
    printlog('rotate board: $rotate');

    switch (flip) {
      case 'horizontal':
        {
          final BoardCells transformedBoard = createEmptyBoard(boardSize);
          for (int rowIndex = 0; rowIndex < boardSize; rowIndex++) {
            for (int colIndex = 0; colIndex < boardSize; colIndex++) {
              transformedBoard[rowIndex][colIndex] = Cell(
                location: CellLocation.go(rowIndex, colIndex),
                value: cells[boardSize - rowIndex - 1][colIndex].value,
                isFixed: false,
              );
            }
          }
          cells = transformedBoard;
        }
        break;
      case 'vertical':
        {
          final BoardCells transformedBoard = createEmptyBoard(boardSize);
          for (int rowIndex = 0; rowIndex < boardSize; rowIndex++) {
            for (int colIndex = 0; colIndex < boardSize; colIndex++) {
              transformedBoard[rowIndex][colIndex] = Cell(
                location: CellLocation.go(rowIndex, colIndex),
                value: cells[rowIndex][boardSize - colIndex - 1].value,
                isFixed: false,
              );
            }
          }
          cells = transformedBoard;
        }
        break;
    }

    switch (rotate) {
      case 'left':
        {
          final BoardCells transformedBoard = createEmptyBoard(boardSize);
          for (int rowIndex = 0; rowIndex < boardSize; rowIndex++) {
            for (int colIndex = 0; colIndex < boardSize; colIndex++) {
              transformedBoard[rowIndex][colIndex] = Cell(
                location: CellLocation.go(rowIndex, colIndex),
                value: cells[colIndex][boardSize - rowIndex - 1].value,
                isFixed: false,
              );
            }
          }
          cells = transformedBoard;
        }
        break;
      case 'right':
        {
          final BoardCells transformedBoard = createEmptyBoard(boardSize);
          for (int rowIndex = 0; rowIndex < boardSize; rowIndex++) {
            for (int colIndex = 0; colIndex < boardSize; colIndex++) {
              transformedBoard[rowIndex][colIndex] = Cell(
                location: CellLocation.go(rowIndex, colIndex),
                value: cells[boardSize - colIndex - 1][rowIndex].value,
                isFixed: false,
              );
            }
          }
          cells = transformedBoard;
        }
        break;
    }

    // Fix cells fixed states
    for (int rowIndex = 0; rowIndex < boardSize; rowIndex++) {
      for (int colIndex = 0; colIndex < boardSize; colIndex++) {
        cells[rowIndex][colIndex] = Cell(
          location: CellLocation.go(rowIndex, colIndex),
          value: cells[rowIndex][colIndex].value,
          isFixed: (cells[rowIndex][colIndex].value != 0) ? true : false,
        );
      }
    }

    return Board.createNew(
      cells: cells,
    );
  }

  Cell get(CellLocation location) {
    if (location.row < cells.length) {
      if (location.col < cells[location.row].length) {
        return cells[location.row][location.col];
      }
    }

    return Cell.none;
  }

  void set(CellLocation location, Cell cell) {
    cells[location.row][location.col] = cell;
  }

  BoardCells copyCells() {
    final BoardCells copiedGrid = [];
    for (int rowIndex = 0; rowIndex < cells.length; rowIndex++) {
      final List<Cell> row = [];
      for (int colIndex = 0; colIndex < cells[rowIndex].length; colIndex++) {
        row.add(Cell(
          location: CellLocation.go(rowIndex, colIndex),
          value: cells[rowIndex][colIndex].value,
          isFixed: false,
        ));
      }
      copiedGrid.add(row);
    }

    return copiedGrid;
  }

  BoardCells getSolvedGrid() {
    final Board tmpBoard = Board(cells: copyCells());

    do {
      final List<List<int>> cellsWithUniqueAvailableValue =
          tmpBoard.getEmptyCellsWithUniqueAvailableValue();
      if (cellsWithUniqueAvailableValue.isEmpty) {
        break;
      }

      for (int i = 0; i < cellsWithUniqueAvailableValue.length; i++) {
        final int row = cellsWithUniqueAvailableValue[i][0];
        final int col = cellsWithUniqueAvailableValue[i][1];
        final int value = cellsWithUniqueAvailableValue[i][2];

        tmpBoard.cells[row][col] = Cell(
          location: CellLocation.go(row, col),
          value: value,
          isFixed: tmpBoard.cells[row][col].isFixed,
        );
      }
    } while (true);

    return tmpBoard.cells;
  }

  List<List<int>> getEmptyCellsWithUniqueAvailableValue() {
    List<List<int>> candidateCells = [];

    final int boardSize = cells.length;

    for (int row = 0; row < boardSize; row++) {
      for (int col = 0; col < boardSize; col++) {
        if (cells[row][col].value == 0) {
          int allowedValuesCount = 0;
          int candidateValue = 0;
          for (int value = 1; value <= boardSize; value++) {
            if (isValueAllowed(CellLocation.go(row, col), value)) {
              candidateValue = value;
              allowedValuesCount++;
            }
          }
          if (allowedValuesCount == 1) {
            candidateCells.add([row, col, candidateValue]);
          }
        }
      }
    }

    return candidateCells;
  }

  bool isValueAllowed(CellLocation? candidateLocation, int candidateValue) {
    if ((candidateLocation == null) || (candidateValue == 0)) {
      return true;
    }

    final int boardSize = cells.length;

    // check lines does not contains a value twice
    for (int row = 0; row < boardSize; row++) {
      final List<int> values = [];
      for (int col = 0; col < boardSize; col++) {
        int value = cells[row][col].value;
        if (row == candidateLocation.row && col == candidateLocation.col) {
          value = candidateValue;
        }
        if (value != 0) {
          values.add(value);
        }
      }
      final List<int> distinctValues = values.toSet().toList();
      if (values.length != distinctValues.length) {
        return false;
      }
    }

    // check columns does not contains a value twice
    for (int col = 0; col < boardSize; col++) {
      final List<int> values = [];
      for (int row = 0; row < boardSize; row++) {
        int value = cells[row][col].value;
        if (row == candidateLocation.row && col == candidateLocation.col) {
          value = candidateValue;
        }
        if (value != 0) {
          values.add(value);
        }
      }
      final List<int> distinctValues = values.toSet().toList();
      if (values.length != distinctValues.length) {
        return false;
      }
    }

    // check blocks does not contains a value twice
    final int blockSizeVertical = sqrt(cells.length).toInt();
    final int blockSizeHorizontal = cells.length ~/ blockSizeVertical;

    final int horizontalBlocksCount = blockSizeVertical;
    final int verticalBlocksCount = blockSizeHorizontal;
    for (int blockRow = 0; blockRow < verticalBlocksCount; blockRow++) {
      for (int blockCol = 0; blockCol < horizontalBlocksCount; blockCol++) {
        final List<int> values = [];

        for (int rowInBlock = 0; rowInBlock < blockSizeVertical; rowInBlock++) {
          for (int colInBlock = 0; colInBlock < blockSizeHorizontal; colInBlock++) {
            final int row = (blockRow * blockSizeVertical) + rowInBlock;
            final int col = (blockCol * blockSizeHorizontal) + colInBlock;
            int value = cells[row][col].value;
            if (row == candidateLocation.row && col == candidateLocation.col) {
              value = candidateValue;
            }
            if (value != 0) {
              values.add(value);
            }
          }
        }

        final List<int> distinctValues = values.toSet().toList();
        if (values.length != distinctValues.length) {
          return false;
        }
      }
    }

    return true;
  }

  void dump() {
    printlog('');
    printlog('$Board:');
    printlog('  cells: $cells');
    printlog('');
  }

  @override
  String toString() {
    return '$Board(${toJson()})';
  }

  Map<String, dynamic>? toJson() {
    return <String, dynamic>{
      'cells': cells,
    };
  }
}