import 'dart:math';

import 'package:flutter_custom_toolbox/flutter_toolbox.dart';

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

typedef BoardCells = List<List<Cell>>;

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,
    );
  }

  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,
    };
  }
}