import 'dart:math';

import 'package:flutter_custom_toolbox/flutter_toolbox.dart';

import 'package:jeweled/config/color_theme.dart';
import 'package:jeweled/config/default_global_settings.dart';
import 'package:jeweled/models/game/board.dart';
import 'package:jeweled/models/game/cell.dart';
import 'package:jeweled/models/game/cell_location.dart';
import 'package:jeweled/models/settings/settings_game.dart';
import 'package:jeweled/models/settings/settings_global.dart';

class Game {
  Game({
    // Settings
    required this.gameSettings,
    required this.globalSettings,

    // State
    this.isRunning = false,
    this.isStarted = false,
    this.isFinished = false,
    this.animationInProgress = false,

    // Base data
    required this.board,

    // Game data
    required this.shuffledColors,
    this.availableBlocksCount = 0,
    this.score = 0,
    this.movesCount = 0,
  });

  // Settings
  final GameSettings gameSettings;
  final GlobalSettings globalSettings;

  // State
  bool isRunning;
  bool isStarted;
  bool isFinished;
  bool animationInProgress;

  // Base data
  final Board board;

  // Game data
  List<int> shuffledColors;
  int availableBlocksCount;
  int score;
  int movesCount;

  factory Game.createEmpty() {
    return Game(
      // Settings
      gameSettings: GameSettings.createDefault(),
      globalSettings: GlobalSettings.createDefault(),
      // Base data
      board: Board.createEmpty(),
      // Game data
      shuffledColors: shuffleColors(DefaultGlobalSettings.defaultColorsThemeValue),
    );
  }

  factory Game.createNew({
    GameSettings? gameSettings,
    GlobalSettings? globalSettings,
  }) {
    final GameSettings newGameSettings = gameSettings ?? GameSettings.createDefault();
    final GlobalSettings newGlobalSettings = globalSettings ?? GlobalSettings.createDefault();

    return Game(
      // Settings
      gameSettings: newGameSettings,
      globalSettings: newGlobalSettings,
      // State
      isRunning: true,
      // Base data
      board: Board.createRandom(newGameSettings),
      // Game data
      shuffledColors: shuffleColors(newGlobalSettings.colorsTheme),
    );
  }

  bool get canBeResumed => isStarted && !isFinished;

  bool get gameWon => isRunning && isStarted && isFinished;

  static List<int> shuffleColors(final String colorsTheme) {
    List<int> values =
        List<int>.generate(ColorTheme.getColorsCount(colorsTheme), (i) => i + 1);
    values.shuffle();

    return values;
  }

  void shuffleColorsAgain(final String colorsTheme) {
    shuffledColors = shuffleColors(colorsTheme);
  }

  Cell getCell(CellLocation cellLocation) {
    return board.cells[cellLocation.row][cellLocation.col];
  }

  int? getCellValue(CellLocation cellLocation) {
    return getCell(cellLocation).value;
  }

  int? getCellValueShuffled(CellLocation cellLocation) {
    final int? value = getCell(cellLocation).value;
    return value != null ? shuffledColors[value - 1] : null;
  }

  void updateCellValue(CellLocation locationToUpdate, int? value) {
    board.cells[locationToUpdate.row][locationToUpdate.col].value = value;
  }

  void increaseMovesCount() {
    movesCount += 1;
  }

  void increaseScore(int? count) {
    score += (count ?? 0);
  }

  void updateAvailableBlocksCount() {
    availableBlocksCount = getAvailableBlocks(this).length;
  }

  List<CellLocation> getSiblingCells(
    final CellLocation referenceCellLocation,
    List<CellLocation> siblingCells,
  ) {
    final int boardSizeHorizontal = gameSettings.boardSizeValue;
    final int boardSizeVertical = gameSettings.boardSizeValue;

    final int? referenceValue = getCellValue(referenceCellLocation);

    for (int deltaRow = -1; deltaRow <= 1; deltaRow++) {
      for (int deltaCol = -1; deltaCol <= 1; deltaCol++) {
        if (deltaCol == 0 || deltaRow == 0) {
          final int candidateRow = referenceCellLocation.row + deltaRow;
          final int candidateCol = referenceCellLocation.col + deltaCol;

          if ((candidateRow >= 0 && candidateRow < boardSizeVertical) &&
              (candidateCol >= 0 && candidateCol < boardSizeHorizontal)) {
            final candidateLocation = CellLocation.go(candidateRow, candidateCol);

            if (getCellValue(candidateLocation) == referenceValue) {
              bool alreadyFound = false;
              for (int index = 0; index < siblingCells.length; index++) {
                if ((siblingCells[index].row == candidateRow) &&
                    (siblingCells[index].col == candidateCol)) {
                  alreadyFound = true;
                }
              }
              if (!alreadyFound) {
                siblingCells.add(candidateLocation);
                siblingCells = getSiblingCells(candidateLocation, siblingCells);
              }
            }
          }
        }
      }
    }

    return siblingCells;
  }

  List<List<CellLocation>> getAvailableBlocks(final Game game) {
    final int boardSizeHorizontal = game.gameSettings.boardSizeValue;
    final int boardSizeVertical = game.gameSettings.boardSizeValue;

    final List<List<CellLocation>> blocks = [];

    for (int row = 0; row < boardSizeVertical; row++) {
      for (int col = 0; col < boardSizeHorizontal; col++) {
        final CellLocation cellLocation = CellLocation.go(row, col);
        if (game.getCellValue(cellLocation) != null) {
          // if current cell not already in a found block
          bool alreadyFound = false;

          for (List<CellLocation> foundBlock in blocks) {
            for (CellLocation foundBlockCell in foundBlock) {
              if ((foundBlockCell.row == row) && (foundBlockCell.col == col)) {
                alreadyFound = true;
              }
            }
          }
          if (!alreadyFound) {
            final List<CellLocation> block = game.getSiblingCells(cellLocation, []);
            if (block.length >= 3) {
              blocks.add(block);
            }
          }
        }
      }
    }

    return blocks;
  }

  bool hasAtLeastOneAvailableBlock() {
    final int boardSizeHorizontal = gameSettings.boardSizeValue;
    final int boardSizeVertical = gameSettings.boardSizeValue;

    for (int row = 0; row < boardSizeVertical; row++) {
      for (int col = 0; col < boardSizeHorizontal; col++) {
        final CellLocation cellLocation = CellLocation.go(row, col);
        if (getCellValue(cellLocation) != null) {
          final List<CellLocation> block = getSiblingCells(cellLocation, []);
          if (block.length >= 3) {
            // found one block => ok, not locked
            return true;
          }
        }
      }
    }

    printlog('Board is locked!');
    return false;
  }

  bool isInBoard(CellLocation cell) {
    final int boardSize = gameSettings.boardSizeValue;

    if (cell.row > 0 && cell.row < boardSize && cell.col > 0 && cell.col < boardSize) {
      return true;
    }
    return false;
  }

  int getFillValue(CellLocation referenceCellLocation) {
    final int row = referenceCellLocation.row;
    final int col = referenceCellLocation.col;

    // build a list of values to pick one
    final List<int> values = [];

    // All eligible values (twice)
    final int maxValue = gameSettings.colorsCountValue;
    for (int i = 1; i <= maxValue; i++) {
      values.add(i);
      values.add(i);
    }

    // Add values of current col (twice)
    for (int r = 0; r <= gameSettings.boardSizeValue; r++) {
      if (isInBoard(CellLocation.go(r, col))) {
        final int? value = getCellValue(CellLocation.go(r, col));
        if (value != null) {
          values.add(value);
          values.add(value);
        }
      }
    }

    // Add values of sibling cols (twice for top rows)
    for (int deltaCol = -1; deltaCol <= 1; deltaCol++) {
      final int c = col + deltaCol;
      for (int r = 0; r < gameSettings.boardSizeValue; r++) {
        if (isInBoard(CellLocation.go(r, c))) {
          final int? value = getCellValue(CellLocation.go(r, c));
          if (value != null) {
            values.add(value);
            if (row < gameSettings.boardSizeValue / 3) {
              values.add(value);
            }
          }
        }
      }
    }

    // Add values of sibling cells
    for (int deltaCol = -2; deltaCol <= 2; deltaCol++) {
      final int c = col + deltaCol;
      for (int deltaRow = -2; deltaRow <= 2; deltaRow++) {
        final int r = row + deltaRow;
        if (isInBoard(CellLocation.go(r, c))) {
          final int? value = getCellValue(CellLocation.go(r, c));
          if (value != null) {
            values.add(value);
          }
        }
      }
    }

    // Pick random value from "ponderated" list
    return values[Random().nextInt(values.length)];
  }

  void dump() {
    printlog('');
    printlog('## Current game dump:');
    printlog('');
    printlog('$Game:');
    printlog('  Settings');
    gameSettings.dump();
    globalSettings.dump();
    printlog('  State');
    printlog('    isRunning: $isRunning');
    printlog('    isStarted: $isStarted');
    printlog('    isFinished: $isFinished');
    printlog('    animationInProgress: $animationInProgress');
    printlog('  Base data');
    board.dump();
    printlog('  Game data');
    printlog('    shuffledColors: $shuffledColors');
    printlog('    availableBlocksCount: $availableBlocksCount');
    printlog('    score: $score');
    printlog('    movesCount: $movesCount');
    printlog('');
  }

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

  Map<String, dynamic>? toJson() {
    return <String, dynamic>{
      // Settings
      'gameSettings': gameSettings.toJson(),
      'globalSettings': globalSettings.toJson(),
      // State
      'isRunning': isRunning,
      'isStarted': isStarted,
      'isFinished': isFinished,
      'animationInProgress': animationInProgress,
      // Base data
      'board': board.toJson(),
      // Game data
      'shuffledColors': shuffledColors,
      'availableBlocksCount': availableBlocksCount,
      'score': score,
      'movesCount': movesCount,
    };
  }
}