import '../../core/rng.dart';
import '../models/grid.dart';
import '../models/piece.dart';
import '../models/piece_library.dart';
import 'line_clear.dart';
import 'placement.dart';

/// Whether some ordering of [tray] can be fully played out (with line clears
/// resolved between placements). Depth-3 backtracking on an 8x8 is cheap.
bool isTrayPlayable(GridState grid, List<Piece> tray) {
  if (tray.isEmpty) return true;
  final triedIds = <String>{};
  for (var i = 0; i < tray.length; i++) {
    final piece = tray[i];
    if (!triedIds.add(piece.id)) continue;
    final rest = [...tray]..removeAt(i);
    for (var y = 0; y < GridState.size; y++) {
      for (var x = 0; x < GridState.size; x++) {
        if (!canPlace(grid, piece, x, y)) continue;
        final after = detectAndClear(place(grid, piece, x, y)).grid;
        if (isTrayPlayable(after, rest)) return true;
      }
    }
  }
  return false;
}

/// Weighted-bag tray dealer with pity bias on tight boards and a solvability
/// nudge: the game never deals an instantly dead tray when a playable deal
/// exists, but bad play can still lose.
class PieceGenerator {
  PieceGenerator(this._rng, {List<Piece>? pool})
      : _pool = pool ?? PieceLibrary.all;

  static const int traySize = 3;
  static const int _maxRedraws = 5;
  static const double _pityThreshold = 0.6;

  final SeededRng _rng;
  final List<Piece> _pool;

  List<Piece> nextTray(GridState grid) {
    for (var attempt = 0; attempt < _maxRedraws; attempt++) {
      final tray = _draw(grid);
      if (isTrayPlayable(grid, tray)) return tray;
    }
    return _fallback(grid);
  }

  List<Piece> _draw(GridState grid) {
    final fill = grid.fillRatio;
    final candidates = List.of(_pool);
    final tray = <Piece>[];
    while (tray.length < traySize) {
      final pick = _weightedPick(candidates, fill);
      tray.add(pick);
      candidates.removeWhere((p) => p.id == pick.id);
    }
    return tray;
  }

  /// Above 60% board fill, small pieces get progressively more likely so the
  /// late game feels fair instead of dealing unplaceable monsters.
  double _effectiveWeight(Piece piece, double fill) {
    var w = piece.weight;
    if (fill > _pityThreshold && piece.size <= 3) {
      w *= 1 + 2 * (fill - _pityThreshold);
    }
    return w;
  }

  Piece _weightedPick(List<Piece> candidates, double fill) {
    var total = 0.0;
    for (final p in candidates) {
      total += _effectiveWeight(p, fill);
    }
    var r = _rng.nextDouble() * total;
    for (final p in candidates) {
      r -= _effectiveWeight(p, fill);
      if (r < 0) return p;
    }
    return candidates.last;
  }

  /// After repeated unplayable draws: the largest piece that still fits plus
  /// the smallest fillers. Consumes no RNG, so retries stay deterministic.
  List<Piece> _fallback(GridState grid) {
    final bySizeDesc = [..._pool]
      ..sort((a, b) {
        final s = b.size.compareTo(a.size);
        return s != 0 ? s : a.id.compareTo(b.id);
      });
    Piece? largestFitting;
    for (final p in bySizeDesc) {
      if (_fitsAnywhere(grid, p)) {
        largestFitting = p;
        break;
      }
    }
    final tray = <Piece>[?largestFitting];
    for (final p in bySizeDesc.reversed) {
      if (tray.length == traySize) break;
      if (tray.any((t) => t.id == p.id)) continue;
      tray.add(p);
    }
    return tray;
  }

  bool _fitsAnywhere(GridState grid, Piece piece) {
    for (var y = 0; y < GridState.size; y++) {
      for (var x = 0; x < GridState.size; x++) {
        if (canPlace(grid, piece, x, y)) return true;
      }
    }
    return false;
  }
}
