import 'dart:async';
import 'dart:math';

import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:freecell/audio/audio_manager.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

/// 单独一个 Screen，直接：
/// Navigator.push(context, MaterialPageRoute(builder: (_) => const FreeCellScreen()));
class FreeCellScreen extends StatelessWidget {
  const FreeCellScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: GameWidget(
          game: FreeCellGame(),
          overlayBuilderMap: {
            'settings': (context, game) {
              return SettingsOverlay(game: game as FreeCellGame);
            },
            'history': (context, game) {
              return HistoryOverlay(game: game as FreeCellGame);
            },
            'win': (context, game) {
              final freeCellGame = game as FreeCellGame;
              return Center(
                child: Container(
                  padding: const EdgeInsets.all(24),
                  decoration: BoxDecoration(
                    color: Colors.black.withValues(alpha: 0.75),
                    borderRadius: BorderRadius.circular(18),
                  ),
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      const Text(
                        'You Win!',
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: 32,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      const SizedBox(height: 12),
                      _ResultStatText(
                        label: 'Time',
                        value: freeCellGame.stats.formatSeconds(
                          freeCellGame.elapsedSeconds.floor(),
                        ),
                      ),
                      _ResultStatText(
                        label: 'Moves',
                        value: '${freeCellGame.history.length}',
                      ),
                      const SizedBox(height: 18),
                      ElevatedButton(
                        onPressed: freeCellGame.restartGame,
                        child: const Text('Restart Game'),
                      ),
                    ],
                  ),
                ),
              );
            },
            'lose': (context, game) {
              final freeCellGame = game as FreeCellGame;
              return Center(
                child: Container(
                  padding: const EdgeInsets.all(24),
                  decoration: BoxDecoration(
                    color: Colors.black.withValues(alpha: 0.75),
                    borderRadius: BorderRadius.circular(18),
                  ),
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      const Text(
                        'No Moves',
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: 32,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      const SizedBox(height: 12),
                      _ResultStatText(
                        label: 'Time',
                        value: freeCellGame.stats.formatSeconds(
                          freeCellGame.elapsedSeconds.floor(),
                        ),
                      ),
                      _ResultStatText(
                        label: 'Moves',
                        value: '${freeCellGame.history.length}',
                      ),
                      const SizedBox(height: 18),
                      ElevatedButton(
                        onPressed: freeCellGame.restartGame,
                        child: const Text('Restart Game'),
                      ),
                    ],
                  ),
                ),
              );
            },
          },
        ),
      ),
    );
  }
}

class _ResultStatText extends StatelessWidget {
  const _ResultStatText({required this.label, required this.value});

  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 2),
      child: Text(
        '$label: $value',
        style: const TextStyle(
          color: Colors.white,
          fontSize: 16,
          fontWeight: FontWeight.w700,
        ),
      ),
    );
  }
}

class SettingsOverlay extends StatefulWidget {
  const SettingsOverlay({required this.game, super.key});

  final FreeCellGame game;

  @override
  State<SettingsOverlay> createState() => _SettingsOverlayState();
}

class _SettingsOverlayState extends State<SettingsOverlay> {
  late bool bgmEnabled;
  late bool sfxEnabled;
  late int selectedBackgroundIndex;

  @override
  void initState() {
    super.initState();
    bgmEnabled = widget.game.bgmEnabled;
    sfxEnabled = widget.game.sfxEnabled;
    selectedBackgroundIndex = widget.game.selectedBackgroundIndex;
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        width: min(MediaQuery.of(context).size.width - 28, 390),
        padding: const EdgeInsets.fromLTRB(18, 16, 18, 18),
        decoration: BoxDecoration(
          color: const Color(0xFFF6EBCF),
          border: Border.all(color: const Color(0xFFC9B98F), width: 2),
          borderRadius: BorderRadius.circular(6),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withValues(alpha: 0.24),
              blurRadius: 18,
              offset: const Offset(0, 8),
            ),
          ],
        ),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text(
              'SETTING',
              style: TextStyle(
                color: Color(0xFF595347),
                fontSize: 18,
                fontWeight: FontWeight.w900,
                letterSpacing: 0,
              ),
            ),
            const SizedBox(height: 14),
            Row(
              children: [
                const Expanded(
                  child: Text(
                    'Background Music',
                    style: TextStyle(
                      color: Color(0xFF595347),
                      fontSize: 16,
                      fontWeight: FontWeight.w700,
                    ),
                  ),
                ),
                Switch(
                  value: bgmEnabled,
                  activeThumbColor: const Color(0xFF67B856),
                  activeTrackColor: const Color(0xFFB9E6A8),
                  onChanged: (value) {
                    widget.game.playSelectSound();
                    setState(() => bgmEnabled = value);
                    widget.game.setBgmEnabled(value);
                  },
                ),
              ],
            ),
            Row(
              children: [
                const Expanded(
                  child: Text(
                    'Sound Effects',
                    style: TextStyle(
                      color: Color(0xFF595347),
                      fontSize: 16,
                      fontWeight: FontWeight.w700,
                    ),
                  ),
                ),
                Switch(
                  value: sfxEnabled,
                  activeThumbColor: const Color(0xFF67B856),
                  activeTrackColor: const Color(0xFFB9E6A8),
                  onChanged: (value) {
                    widget.game.playSelectSound();
                    setState(() => sfxEnabled = value);
                    widget.game.setSfxEnabled(value);
                  },
                ),
              ],
            ),
            const SizedBox(height: 10),
            const Align(
              alignment: Alignment.centerLeft,
              child: Text(
                'Change Background',
                style: TextStyle(
                  color: Color(0xFF595347),
                  fontSize: 16,
                  fontWeight: FontWeight.w800,
                ),
              ),
            ),
            const SizedBox(height: 10),
            GridView.builder(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              itemCount: widget.game.backgroundAssets.length,
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 4,
                mainAxisSpacing: 10,
                crossAxisSpacing: 10,
                childAspectRatio: 1.35,
              ),
              itemBuilder: (context, index) {
                final selected = selectedBackgroundIndex == index;
                return GestureDetector(
                  onTap: () async {
                    widget.game.playSelectSound();
                    setState(() => selectedBackgroundIndex = index);
                    await widget.game.setBackgroundIndex(index);
                  },
                  child: AnimatedContainer(
                    duration: const Duration(milliseconds: 140),
                    decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(5),
                      border: Border.all(
                        color: selected
                            ? const Color(0xFF5FAE45)
                            : const Color(0xFFE1D1AA),
                        width: selected ? 3 : 1,
                      ),
                    ),
                    clipBehavior: Clip.antiAlias,
                    child: Image.asset(
                      'assets/images/${widget.game.backgroundAssets[index]}',
                      fit: BoxFit.cover,
                      errorBuilder: (context, error, stackTrace) {
                        return ColoredBox(
                          color: const Color(0xFF105A35),
                          child: Center(
                            child: Text(
                              '${index + 1}',
                              style: const TextStyle(
                                color: Colors.white,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                          ),
                        );
                      },
                    ),
                  ),
                );
              },
            ),
            const SizedBox(height: 18),
            SizedBox(
              width: 188,
              height: 44,
              child: ElevatedButton(
                style: ElevatedButton.styleFrom(
                  backgroundColor: const Color(0xFF6DBA57),
                  foregroundColor: Colors.white,
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(6),
                  ),
                  textStyle: const TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.w900,
                  ),
                ),
                onPressed: () {
                  widget.game.playSelectSound();
                  widget.game.closeSettings();
                },
                child: const Text('SAVE AND EXIT'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class HistoryOverlay extends StatelessWidget {
  const HistoryOverlay({required this.game, super.key});

  final FreeCellGame game;

  @override
  Widget build(BuildContext context) {
    final stats = game.stats;
    return Center(
      child: Container(
        width: min(MediaQuery.of(context).size.width - 28, 390),
        padding: const EdgeInsets.fromLTRB(20, 18, 20, 18),
        decoration: BoxDecoration(
          color: const Color(0xFFF6EBCF),
          border: Border.all(color: const Color(0xFFC9B98F), width: 2),
          borderRadius: BorderRadius.circular(6),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withValues(alpha: 0.24),
              blurRadius: 18,
              offset: const Offset(0, 8),
            ),
          ],
        ),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text(
              'HISTORY',
              style: TextStyle(
                color: Color(0xFF595347),
                fontSize: 18,
                fontWeight: FontWeight.w900,
                letterSpacing: 0,
              ),
            ),
            const SizedBox(height: 14),
            _HistoryRow(label: 'Wins', value: '${stats.winCount}'),
            _HistoryRow(label: 'Losses', value: '${stats.lossCount}'),
            _HistoryRow(label: 'Unfinished', value: '${stats.unfinishedCount}'),
            const Divider(height: 22, color: Color(0xFFC9B98F)),
            _HistoryRow(
              label: 'Fastest Win',
              value: stats.formatSeconds(stats.minWinSeconds),
            ),
            _HistoryRow(
              label: 'Slowest Win',
              value: stats.formatSeconds(stats.maxWinSeconds),
            ),
            _HistoryRow(
              label: 'Fewest Win Moves',
              value: stats.formatInt(stats.minWinMoves),
            ),
            _HistoryRow(
              label: 'Most Win Moves',
              value: stats.formatInt(stats.maxWinMoves),
            ),
            const SizedBox(height: 18),
            SizedBox(
              width: 150,
              height: 42,
              child: ElevatedButton(
                style: ElevatedButton.styleFrom(
                  backgroundColor: const Color(0xFF6DBA57),
                  foregroundColor: Colors.white,
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(6),
                  ),
                  textStyle: const TextStyle(
                    fontSize: 15,
                    fontWeight: FontWeight.w900,
                  ),
                ),
                onPressed: () {
                  game.playSelectSound();
                  game.closeHistory();
                },
                child: const Text('CLOSE'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _HistoryRow extends StatelessWidget {
  const _HistoryRow({required this.label, required this.value});

  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 5),
      child: Row(
        children: [
          Expanded(
            child: Text(
              label,
              style: const TextStyle(
                color: Color(0xFF595347),
                fontSize: 15,
                fontWeight: FontWeight.w700,
              ),
            ),
          ),
          Text(
            value,
            style: const TextStyle(
              color: Color(0xFF3D372D),
              fontSize: 15,
              fontWeight: FontWeight.w900,
            ),
          ),
        ],
      ),
    );
  }
}

class FreeCellStats {
  int winCount = 0;
  int lossCount = 0;
  int unfinishedCount = 0;
  int? minWinSeconds;
  int? maxWinSeconds;
  int? minWinMoves;
  int? maxWinMoves;

  void recordWin({required int seconds, required int moves}) {
    winCount++;
    minWinSeconds = _minNullable(minWinSeconds, seconds);
    maxWinSeconds = _maxNullable(maxWinSeconds, seconds);
    minWinMoves = _minNullable(minWinMoves, moves);
    maxWinMoves = _maxNullable(maxWinMoves, moves);
  }

  void recordLoss() {
    lossCount++;
  }

  void recordUnfinished() {
    unfinishedCount++;
  }

  String formatInt(int? value) => value == null ? '-' : '$value';

  String formatSeconds(int? value) {
    if (value == null) return '-';

    final minutes = value ~/ 60;
    final seconds = value % 60;
    return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
  }

  static int _minNullable(int? current, int value) {
    return current == null ? value : min(current, value);
  }

  static int _maxNullable(int? current, int value) {
    return current == null ? value : max(current, value);
  }
}

enum Suit { spade, heart, diamond, club }

enum PileType { freeCell, foundation, tableau }

class CardModel {
  final Suit suit;
  final int rank;

  const CardModel(this.suit, this.rank);

  bool get isRed => suit == Suit.heart || suit == Suit.diamond;

  String get rankText {
    return switch (rank) {
      1 => 'A',
      11 => 'J',
      12 => 'Q',
      13 => 'K',
      _ => '$rank',
    };
  }

  String get suitText {
    return switch (suit) {
      Suit.spade => '♠',
      Suit.heart => '♥',
      Suit.diamond => '♦',
      Suit.club => '♣',
    };
  }

  /// 预留扑克图片路径：assets/images/cards/AS.png、10H.png、KD.png 等。
  String get imageName {
    final s = switch (suit) {
      Suit.spade => 'S',
      Suit.heart => 'H',
      Suit.diamond => 'D',
      Suit.club => 'C',
    };
    return '$rankText$s.png';
  }
}

class CardPile {
  CardPile({required this.type, required this.index, required this.position});

  final PileType type;
  final int index;
  Vector2 position;
  final List<CardComponent> cards = [];
}

class MoveRecord {
  MoveRecord({
    required this.cards,
    required this.fromPile,
    required this.toPile,
  });

  final List<CardComponent> cards;
  final CardPile fromPile;
  final CardPile toPile;
}

class HintMove {
  const HintMove({required this.cards, required this.target});

  final List<CardComponent> cards;
  final CardPile target;
}

enum _SolveStatus { solved, unsolved, unknown }

class _SolverMove {
  const _SolverMove({
    required this.fromType,
    required this.fromIndex,
    required this.startIndex,
    required this.toType,
    required this.toIndex,
  });

  final PileType fromType;
  final int fromIndex;
  final int startIndex;
  final PileType toType;
  final int toIndex;
}

class _SolverState {
  _SolverState({
    required this.tableaus,
    required this.freeCells,
    required this.foundations,
  });

  final List<List<int>> tableaus;
  final List<int?> freeCells;
  final List<int> foundations;

  _SolverState clone() {
    return _SolverState(
      tableaus: [for (final pile in tableaus) List<int>.of(pile)],
      freeCells: List<int?>.of(freeCells),
      foundations: List<int>.of(foundations),
    );
  }

  int get foundationCount {
    return foundations.fold<int>(0, (sum, rank) => sum + rank);
  }

  int get emptyFreeCellCount {
    return freeCells.where((card) => card == null).length;
  }

  int get emptyTableauCount {
    return tableaus.where((pile) => pile.isEmpty).length;
  }

  bool get isWon => foundationCount == 52;
}

class _FreeCellSolver {
  _FreeCellSolver({
    required this.initial,
    this.maxDepth = 80,
    this.maxVisited = 50000,
  });

  final _SolverState initial;
  final int maxDepth;
  final int maxVisited;
  int _visitedCount = 0;
  bool _exhausted = false;

  _SolveStatus solve() {
    final visited = <String>{};
    final solved = _dfs(initial, maxDepth, visited);

    if (solved) return _SolveStatus.solved;
    if (_exhausted) return _SolveStatus.unknown;
    return _SolveStatus.unsolved;
  }

  bool _dfs(_SolverState state, int depthLeft, Set<String> visited) {
    if (state.isWon) return true;

    final moves = _moves(state);
    if (moves.isEmpty) return false;

    if (depthLeft <= 0 || _visitedCount >= maxVisited) {
      _exhausted = true;
      return false;
    }

    final key = _key(state);
    if (!visited.add(key)) return false;
    _visitedCount++;

    for (final move in moves) {
      final next = _apply(state, move);
      if (next.foundationCount > state.foundationCount) return true;
      if (next.emptyTableauCount > state.emptyTableauCount) return true;
      if (_dfs(next, depthLeft - 1, visited)) return true;
    }

    return false;
  }

  List<_SolverMove> _moves(_SolverState state) {
    final moves = <_SolverMove>[];

    for (int i = 0; i < state.freeCells.length; i++) {
      final card = state.freeCells[i];
      if (card == null) continue;

      if (_canMoveToFoundation(card, state)) {
        moves.add(
          _SolverMove(
            fromType: PileType.freeCell,
            fromIndex: i,
            startIndex: 0,
            toType: PileType.foundation,
            toIndex: _suitOf(card),
          ),
        );
      }

      for (int t = 0; t < state.tableaus.length; t++) {
        if (_canMoveToTableau(card, state.tableaus[t])) {
          moves.add(
            _SolverMove(
              fromType: PileType.freeCell,
              fromIndex: i,
              startIndex: 0,
              toType: PileType.tableau,
              toIndex: t,
            ),
          );
        }
      }
    }

    final emptyFreeCell = state.freeCells.indexWhere((card) => card == null);
    for (int from = 0; from < state.tableaus.length; from++) {
      final tableau = state.tableaus[from];
      if (tableau.isEmpty) continue;

      final top = tableau.last;
      if (_canMoveToFoundation(top, state)) {
        moves.add(
          _SolverMove(
            fromType: PileType.tableau,
            fromIndex: from,
            startIndex: tableau.length - 1,
            toType: PileType.foundation,
            toIndex: _suitOf(top),
          ),
        );
      }

      if (emptyFreeCell != -1) {
        moves.add(
          _SolverMove(
            fromType: PileType.tableau,
            fromIndex: from,
            startIndex: tableau.length - 1,
            toType: PileType.freeCell,
            toIndex: emptyFreeCell,
          ),
        );
      }

      final maxMovableStackSize = _maxMovableStackSize(state);
      for (int start = tableau.length - 1; start >= 0; start--) {
        final run = tableau.sublist(start);
        if (!_isOrderedRun(run)) break;
        if (run.length > maxMovableStackSize) continue;

        final first = run.first;
        for (int to = 0; to < state.tableaus.length; to++) {
          if (to == from) continue;
          final target = state.tableaus[to];
          if (target.isEmpty && start == 0) continue;
          if (!_canMoveToTableau(first, target)) continue;

          moves.add(
            _SolverMove(
              fromType: PileType.tableau,
              fromIndex: from,
              startIndex: start,
              toType: PileType.tableau,
              toIndex: to,
            ),
          );
        }
      }
    }

    moves.sort((a, b) => _moveScore(b).compareTo(_moveScore(a)));
    return moves;
  }

  int _maxMovableStackSize(_SolverState state) {
    return (state.emptyFreeCellCount + 1) * (1 << state.emptyTableauCount);
  }

  int _moveScore(_SolverMove move) {
    if (move.toType == PileType.foundation) return 100;
    if (move.fromType == PileType.freeCell && move.toType == PileType.tableau) {
      return 70;
    }
    if (move.toType == PileType.tableau) return 50;
    if (move.toType == PileType.freeCell) return 20;
    return 0;
  }

  _SolverState _apply(_SolverState state, _SolverMove move) {
    final next = state.clone();
    final moving = <int>[];

    switch (move.fromType) {
      case PileType.freeCell:
        final card = next.freeCells[move.fromIndex];
        if (card != null) {
          moving.add(card);
          next.freeCells[move.fromIndex] = null;
        }
        break;
      case PileType.tableau:
        final from = next.tableaus[move.fromIndex];
        moving.addAll(from.sublist(move.startIndex));
        from.removeRange(move.startIndex, from.length);
        break;
      case PileType.foundation:
        break;
    }

    if (moving.isEmpty) return next;

    switch (move.toType) {
      case PileType.freeCell:
        next.freeCells[move.toIndex] = moving.single;
        break;
      case PileType.foundation:
        final card = moving.single;
        next.foundations[_suitOf(card)] = _rankOf(card);
        break;
      case PileType.tableau:
        next.tableaus[move.toIndex].addAll(moving);
        break;
    }

    return next;
  }

  String _key(_SolverState state) {
    final foundation = state.foundations.join(',');
    final free = [for (final card in state.freeCells) ?card]..sort();
    final columns = [for (final tableau in state.tableaus) tableau.join('.')]
      ..sort();
    return '$foundation|${free.join('.')}|${columns.join('|')}';
  }

  bool _canMoveToFoundation(int card, _SolverState state) {
    return state.foundations[_suitOf(card)] + 1 == _rankOf(card);
  }

  bool _canMoveToTableau(int card, List<int> tableau) {
    if (tableau.isEmpty) return true;

    final top = tableau.last;
    return _isRed(card) != _isRed(top) && _rankOf(card) == _rankOf(top) - 1;
  }

  bool _isOrderedRun(List<int> cards) {
    if (cards.length <= 1) return true;

    for (int i = 0; i < cards.length - 1; i++) {
      final upper = cards[i];
      final lower = cards[i + 1];
      if (_isRed(upper) == _isRed(lower) ||
          _rankOf(lower) != _rankOf(upper) - 1) {
        return false;
      }
    }

    return true;
  }

  static int encode(CardModel model) {
    return model.suit.index * 13 + model.rank;
  }

  static int _suitOf(int card) => (card - 1) ~/ 13;

  static int _rankOf(int card) => ((card - 1) % 13) + 1;

  static bool _isRed(int card) {
    final suit = _suitOf(card);
    return suit == Suit.heart.index || suit == Suit.diamond.index;
  }
}

class FreeCellGame extends FlameGame {
  static const double cardWidth = 36;
  static const double cardHeight = 52;
  static const double gap = 8;
  static const double horizontalPadding = 8;
  static const double topY = 12;
  static const double tableauY = 148;
  static const double stackOffset = 27;
  static const double moveAnimationDuration = 0.24;
  static const String _bgmEnabledKey = 'freecell_bgm_enabled';
  static const String _sfxEnabledKey = 'freecell_sfx_enabled';
  static const String _backgroundIndexKey = 'freecell_background_index';
  static const String _statsWinCountKey = 'freecell_stats_win_count';
  static const String _statsLossCountKey = 'freecell_stats_loss_count';
  static const String _statsUnfinishedCountKey =
      'freecell_stats_unfinished_count';
  static const String _statsMinWinSecondsKey = 'freecell_stats_min_win_seconds';
  static const String _statsMaxWinSecondsKey = 'freecell_stats_max_win_seconds';
  static const String _statsMinWinMovesKey = 'freecell_stats_min_win_moves';
  static const String _statsMaxWinMovesKey = 'freecell_stats_max_win_moves';

  final List<CardPile> freeCells = [];
  final List<CardPile> foundations = [];
  final List<CardPile> tableaus = [];
  final List<MoveRecord> history = [];

  CardComponent? draggingCard;
  List<CardComponent> draggingCards = [];
  CardPile? dragStartPile;
  Vector2 dragStartPosition = Vector2.zero();
  double elapsedSeconds = 0;
  int _lastDisplayedSeconds = -1;

  late TextComponent titleText;
  late TextComponent timerText;
  late TextComponent movesText;
  late TextComponent hintText;
  late HudButtonComponent undoButton;
  late HudButtonComponent hintButton;
  late HudButtonComponent restartButton;
  late HudButtonComponent settingButton;
  late HudButtonComponent historyButton;
  late HintOverlayComponent hintOverlay;
  late RectangleComponent backgroundFill;
  SpriteComponent? backgroundSprite;
  bool _hudReady = false;
  List<CardComponent> hintedCards = [];
  CardPile? hintedTarget;
  double _hintSecondsLeft = 0;
  bool bgmEnabled = true;
  bool sfxEnabled = true;
  int selectedBackgroundIndex = 0;
  bool _winSfxPlayed = false;
  bool _lossSfxPlayed = false;
  bool _gameResultRecorded = false;
  int _dealGeneration = 0;
  final FreeCellStats stats = FreeCellStats();

  final List<String> backgroundAssets = const [
    'bg/freecell_bg.png',
    'bg/freecell_bg1.png',
    'bg/freecell_bg2.png',
    'bg/freecell_bg3.png',
  ];

  @override
  Color backgroundColor() => const Color(0xFF105A35);

  Future<void> setBackgroundIndex(int index) async {
    if (index < 0 || index >= backgroundAssets.length) return;

    selectedBackgroundIndex = index;
    unawaited(_saveSettings());
    try {
      final bgImage = await images.load(backgroundAssets[index]);
      final sprite = backgroundSprite;
      if (sprite == null) {
        backgroundSprite = SpriteComponent(
          sprite: Sprite(bgImage),
          size: size,
          priority: -100,
        );
        add(backgroundSprite!);
      } else {
        sprite.sprite = Sprite(bgImage);
        sprite.size = size;
      }
    } catch (_) {
      backgroundSprite?.removeFromParent();
      backgroundSprite = null;
    }
  }

  void setBgmEnabled(bool value) {
    bgmEnabled = value;
    unawaited(_saveSettings());
    unawaited(_applyBgmEnabled(value));
  }

  void setSfxEnabled(bool value) {
    sfxEnabled = value;
    unawaited(_saveSettings());
    unawaited(_applySfxEnabled(value));
  }

  void playSelectSound() {
    _playSfx(SfxKey.select);
  }

  void _playSfx(SfxKey key) {
    unawaited(_safePlaySfx(key));
  }

  Future<void> _setupAudio() async {
    try {
      await AudioManager.instance.init();
      await _applyBgmEnabled(bgmEnabled);
      await _applySfxEnabled(sfxEnabled);
    } catch (e) {
      debugPrint('FreeCell audio setup failed: $e');
    }
  }

  Future<void> _loadSettings() async {
    try {
      final prefs = await SharedPreferences.getInstance();
      bgmEnabled = prefs.getBool(_bgmEnabledKey) ?? bgmEnabled;
      sfxEnabled = prefs.getBool(_sfxEnabledKey) ?? sfxEnabled;

      final savedBackgroundIndex = prefs.getInt(_backgroundIndexKey);
      if (savedBackgroundIndex != null &&
          savedBackgroundIndex >= 0 &&
          savedBackgroundIndex < backgroundAssets.length) {
        selectedBackgroundIndex = savedBackgroundIndex;
      }

      stats.winCount = prefs.getInt(_statsWinCountKey) ?? stats.winCount;
      stats.lossCount = prefs.getInt(_statsLossCountKey) ?? stats.lossCount;
      stats.unfinishedCount =
          prefs.getInt(_statsUnfinishedCountKey) ?? stats.unfinishedCount;
      stats.minWinSeconds = prefs.getInt(_statsMinWinSecondsKey);
      stats.maxWinSeconds = prefs.getInt(_statsMaxWinSecondsKey);
      stats.minWinMoves = prefs.getInt(_statsMinWinMovesKey);
      stats.maxWinMoves = prefs.getInt(_statsMaxWinMovesKey);
    } catch (e) {
      debugPrint('FreeCell load settings failed: $e');
    }
  }

  Future<void> _saveSettings() async {
    try {
      final prefs = await SharedPreferences.getInstance();
      await prefs.setBool(_bgmEnabledKey, bgmEnabled);
      await prefs.setBool(_sfxEnabledKey, sfxEnabled);
      await prefs.setInt(_backgroundIndexKey, selectedBackgroundIndex);
      await prefs.setInt(_statsWinCountKey, stats.winCount);
      await prefs.setInt(_statsLossCountKey, stats.lossCount);
      await prefs.setInt(_statsUnfinishedCountKey, stats.unfinishedCount);
      await _setOptionalInt(prefs, _statsMinWinSecondsKey, stats.minWinSeconds);
      await _setOptionalInt(prefs, _statsMaxWinSecondsKey, stats.maxWinSeconds);
      await _setOptionalInt(prefs, _statsMinWinMovesKey, stats.minWinMoves);
      await _setOptionalInt(prefs, _statsMaxWinMovesKey, stats.maxWinMoves);
    } catch (e) {
      debugPrint('FreeCell save settings failed: $e');
    }
  }

  Future<void> _setOptionalInt(
    SharedPreferences prefs,
    String key,
    int? value,
  ) {
    if (value == null) {
      return prefs.remove(key);
    }

    return prefs.setInt(key, value);
  }

  Future<void> _applyBgmEnabled(bool enabled) async {
    try {
      await AudioManager.instance.setBgmMuted(!enabled);
      if (enabled) {
        await AudioManager.instance.playBgm();
      }
    } catch (e) {
      debugPrint('FreeCell bgm toggle failed: $e');
    }
  }

  Future<void> _applySfxEnabled(bool enabled) async {
    try {
      await AudioManager.instance.setSfxMuted(!enabled);
    } catch (e) {
      debugPrint('FreeCell sfx toggle failed: $e');
    }
  }

  Future<void> _safePlaySfx(SfxKey key) async {
    if (!sfxEnabled) return;

    try {
      await AudioManager.instance.playSfx(key);
    } catch (e) {
      debugPrint('FreeCell sfx failed: $e');
    }
  }

  void openSettings() {
    overlays.add('settings');
  }

  void closeSettings() {
    overlays.remove('settings');
  }

  void openHistory() {
    overlays.add('history');
  }

  void closeHistory() {
    overlays.remove('history');
  }

  void _recordWinStats() {
    if (_gameResultRecorded) return;

    stats.recordWin(seconds: elapsedSeconds.floor(), moves: history.length);
    _gameResultRecorded = true;
    unawaited(_saveSettings());
  }

  void _recordLossStats() {
    if (_gameResultRecorded) return;

    stats.recordLoss();
    _gameResultRecorded = true;
    unawaited(_saveSettings());
  }

  void _recordUnfinishedStats() {
    if (_gameResultRecorded) return;

    stats.recordUnfinished();
    _gameResultRecorded = true;
    unawaited(_saveSettings());
  }

  @override
  Future<void> onLoad() async {
    await super.onLoad();
    await _loadSettings();
    unawaited(_setupAudio());

    backgroundFill = RectangleComponent(
      size: size,
      paint: Paint()..color = const Color(0xFF105A35),
      priority: -101,
    );
    add(backgroundFill);
    await setBackgroundIndex(selectedBackgroundIndex);

    titleText = TextComponent(
      text: 'FREECELL',
      position: Vector2(size.x / 2, 8),
      anchor: Anchor.topCenter,
      priority: 1000,
      textRenderer: TextPaint(
        style: const TextStyle(
          color: Colors.white,
          fontSize: 18,
          fontWeight: FontWeight.bold,
        ),
      ),
    );

    timerText = TextComponent(
      text: '00:00',
      position: Vector2(10, 10),
      anchor: Anchor.topLeft,
      priority: 1000,
      textRenderer: TextPaint(
        style: const TextStyle(
          color: Colors.white,
          fontSize: 13,
          fontWeight: FontWeight.bold,
        ),
      ),
    );

    movesText = TextComponent(
      text: 'Moves 0',
      position: Vector2(size.x - 10, 10),
      anchor: Anchor.topRight,
      priority: 1000,
      textRenderer: TextPaint(
        style: const TextStyle(
          color: Colors.white,
          fontSize: 13,
          fontWeight: FontWeight.bold,
        ),
      ),
    );

    hintText = TextComponent(
      text: 'Drag top cards. Double tap to auto move.',
      position: Vector2(size.x / 2, size.y - 54),
      anchor: Anchor.center,
      priority: 1000,
      textRenderer: TextPaint(
        style: const TextStyle(color: Colors.white70, fontSize: 13),
      ),
    );

    add(titleText);
    add(timerText);
    add(movesText);
    add(hintText);
    undoButton = HudButtonComponent(
      label: 'UNDO',
      onPressed: (game) => game.undo(),
    );
    hintButton = HudButtonComponent(
      label: 'HINT',
      onPressed: (game) => game.showHint(),
    );
    restartButton = HudButtonComponent(
      label: 'RESTART',
      onPressed: (game) => game.restartGame(),
    );
    settingButton = HudButtonComponent(
      label: 'SETTING',
      onPressed: (game) => game.openSettings(),
    );
    historyButton = HudButtonComponent(
      label: 'HISTORY',
      onPressed: (game) => game.openHistory(),
    );
    hintOverlay = HintOverlayComponent();
    add(undoButton);
    add(hintButton);
    add(restartButton);
    add(settingButton);
    add(historyButton);
    add(hintOverlay);
    _hudReady = true;
    _positionHud();
    _refreshHud(force: true);

    _createPiles();
    restartGame(recordUnfinished: false);
  }

  @override
  void onRemove() {
    unawaited(AudioManager.instance.stopBgm());
    super.onRemove();
  }

  @override
  void update(double dt) {
    super.update(dt);

    if (!_hudReady) return;
    if (isWon || overlays.isActive('lose')) return;
    elapsedSeconds += dt;
    if (_hintSecondsLeft > 0) {
      _hintSecondsLeft -= dt;
      if (_hintSecondsLeft <= 0) {
        clearHint();
      }
    }
    _refreshHud();
  }

  @override
  void onGameResize(Vector2 size) {
    super.onGameResize(size);

    if (!_hudReady) return;
    backgroundFill.size = size;
    backgroundSprite?.size = size;
    _positionHud();
    _positionPiles();
    _layoutAll();
  }

  void _positionHud() {
    if (!_hudReady) return;

    titleText.position = Vector2(size.x / 2, 8);
    timerText.position = Vector2(10, 10);
    movesText.position = Vector2(size.x - 10, 10);
    hintText.position = Vector2(size.x / 2, size.y - 58);
    final buttonY = size.y - 30;
    const buttonStep = 70.0;
    undoButton.position = Vector2(size.x / 2 - buttonStep * 2, buttonY);
    hintButton.position = Vector2(size.x / 2 - buttonStep, buttonY);
    restartButton.position = Vector2(size.x / 2, buttonY);
    settingButton.position = Vector2(size.x / 2 + buttonStep, buttonY);
    historyButton.position = Vector2(size.x / 2 + buttonStep * 2, buttonY);
  }

  void _refreshHud({bool force = false}) {
    if (!_hudReady) return;

    final seconds = elapsedSeconds.floor();
    if (force || seconds != _lastDisplayedSeconds) {
      _lastDisplayedSeconds = seconds;
      final minutes = seconds ~/ 60;
      final restSeconds = seconds % 60;
      timerText.text =
          '${minutes.toString().padLeft(2, '0')}:${restSeconds.toString().padLeft(2, '0')}';
    }

    movesText.text = 'Moves ${history.length}';
    undoButton.enabled = history.isNotEmpty && !isGameOver;
    hintButton.enabled = _findLegalMoveForLoseCheck() != null && !isGameOver;
    restartButton.enabled = true;
    settingButton.enabled = true;
    historyButton.enabled = true;
  }

  double _columnGap(double safeWidth) {
    final availableGap =
        (safeWidth - horizontalPadding * 2 - cardWidth * 8) / 7;
    return availableGap.clamp(2.0, gap);
  }

  void _createPiles() {
    freeCells.clear();
    foundations.clear();
    tableaus.clear();

    for (int i = 0; i < 4; i++) {
      freeCells.add(
        CardPile(type: PileType.freeCell, index: i, position: Vector2.zero()),
      );
    }

    for (int i = 0; i < 4; i++) {
      foundations.add(
        CardPile(type: PileType.foundation, index: i, position: Vector2.zero()),
      );
    }

    for (int i = 0; i < 8; i++) {
      tableaus.add(
        CardPile(type: PileType.tableau, index: i, position: Vector2.zero()),
      );
    }

    _positionPiles();

    for (final pile in [...freeCells, ...foundations, ...tableaus]) {
      add(PileSlotComponent(pile));
    }
  }

  void _positionPiles() {
    if (freeCells.isEmpty || foundations.isEmpty || tableaus.isEmpty) return;

    final rowGap = _columnGap(size.x);
    final step = cardWidth + rowGap;
    final usedWidth = cardWidth * 8 + rowGap * 7;
    final startX = max(horizontalPadding, (size.x - usedWidth) / 2);
    final topPileY = topY + 24;

    for (int i = 0; i < freeCells.length; i++) {
      freeCells[i].position = Vector2(startX + i * step, topPileY);
    }

    for (int i = 0; i < foundations.length; i++) {
      foundations[i].position = Vector2(startX + (i + 4) * step, topPileY);
    }

    for (int i = 0; i < tableaus.length; i++) {
      tableaus[i].position = Vector2(startX + i * step, tableauY);
    }
  }

  Future<void> _dealCards({required int generation}) async {
    if (generation != _dealGeneration) return;

    final deck = <CardModel>[];
    for (final suit in Suit.values) {
      for (int rank = 1; rank <= 13; rank++) {
        deck.add(CardModel(suit, rank));
      }
    }
    deck.shuffle(Random());
    _playSfx(SfxKey.shuffle);

    for (int i = 0; i < deck.length; i++) {
      final pile = tableaus[i % tableaus.length];
      final card = CardComponent(deck[i], pile);
      pile.cards.add(card);
      add(card);
    }

    final dealtCards = <CardComponent>[];
    final dealOrigin = Vector2(size.x / 2 - cardWidth / 2, -cardHeight - 8);

    for (final pile in tableaus) {
      for (final card in pile.cards) {
        dealtCards.add(card);
        card.position = dealOrigin.clone();
      }
    }

    _layoutAll(skip: dealtCards.toSet());
    for (int i = 0; i < dealtCards.length; i++) {
      final card = dealtCards[i];
      card.animateTo(
        _restPositionFor(card),
        duration: 0.32,
        delay: i * 0.025,
        endPriority: _restPriorityFor(card),
      );
    }
    await Future<void>.delayed(const Duration(milliseconds: 1700));
    if (generation != _dealGeneration) return;
    _autoMoveAvailableToFoundations(record: false);
  }

  void restartGame({bool recordUnfinished = true}) {
    final generation = ++_dealGeneration;

    if (recordUnfinished) {
      _recordUnfinishedStats();
    }

    overlays.remove('win');
    overlays.remove('lose');
    overlays.remove('history');
    clearHint();
    _winSfxPlayed = false;
    _lossSfxPlayed = false;
    _gameResultRecorded = false;

    for (final card in children.whereType<CardComponent>().toList()) {
      card.removeFromParent();
    }

    for (final pile in [...freeCells, ...foundations, ...tableaus]) {
      pile.cards.clear();
    }

    history.clear();
    draggingCard = null;
    draggingCards = [];
    dragStartPile = null;
    elapsedSeconds = 0;
    _lastDisplayedSeconds = -1;
    _refreshHud(force: true);

    _dealCards(generation: generation);
  }

  bool isTopCard(CardComponent card) {
    return card.pile.cards.isNotEmpty && card.pile.cards.last == card;
  }

  bool _isOrderedPair(CardModel upper, CardModel lower) {
    return upper.isRed != lower.isRed && lower.rank == upper.rank - 1;
  }

  bool _isOrderedRun(List<CardComponent> cards) {
    if (cards.length <= 1) return true;

    for (int i = 0; i < cards.length - 1; i++) {
      if (!_isOrderedPair(cards[i].model, cards[i + 1].model)) {
        return false;
      }
    }

    return true;
  }

  List<CardComponent> _movableRunFrom(CardComponent card) {
    if (isGameOver) return const [];

    final pile = card.pile;
    final index = pile.cards.indexOf(card);
    if (index == -1) return const [];

    final run = pile.cards.sublist(index);
    if (run.any((card) => card.isAnimating)) return const [];

    if (pile.type != PileType.tableau) {
      return run.length == 1 ? run : const [];
    }

    return _isOrderedRun(run) ? run : const [];
  }

  void startDrag(CardComponent card) {
    if (isGameOver) return;

    final run = _movableRunFrom(card);
    if (run.isEmpty) return;
    _playSfx(SfxKey.select);

    draggingCard = card;
    draggingCards = run;
    dragStartPile = card.pile;
    dragStartPosition = card.position.clone();
    for (int i = 0; i < draggingCards.length; i++) {
      draggingCards[i].priority = 999 + i;
    }
  }

  void updateDrag(Vector2 delta) {
    if (isGameOver) return;

    for (final card in draggingCards) {
      card.position.add(delta);
    }
  }

  void endDrag() {
    final card = draggingCard;
    final fromPile = dragStartPile;
    if (card == null || fromPile == null || draggingCards.isEmpty) return;

    if (isGameOver) {
      _layoutAll();
      draggingCard = null;
      draggingCards = [];
      dragStartPile = null;
      return;
    }

    final target = _findTargetPile(card);
    if (target != null && canMoveCards(draggingCards, target)) {
      moveCards(draggingCards, target, record: true);
    } else {
      _layoutAll();
    }

    draggingCard = null;
    draggingCards = [];
    dragStartPile = null;
    checkGameState();
  }

  CardPile? _findTargetPile(CardComponent card) {
    final center = card.position + Vector2(cardWidth / 2, cardHeight / 2);
    final allPiles = [...freeCells, ...foundations, ...tableaus];

    CardPile? best;
    double bestDistance = double.infinity;

    for (final pile in allPiles) {
      if (pile == card.pile) continue;

      final pos = pile.cards.isEmpty
          ? pile.position
          : pile.cards.last.position.clone();
      final rect = Rect.fromLTWH(pos.x, pos.y, cardWidth, cardHeight);
      final expanded = rect.inflate(26);
      if (expanded.contains(Offset(center.x, center.y))) {
        final d = center.distanceTo(
          pos + Vector2(cardWidth / 2, cardHeight / 2),
        );
        if (d < bestDistance) {
          bestDistance = d;
          best = pile;
        }
      }
    }

    return best;
  }

  bool canMove(CardComponent card, CardPile target) {
    return canMoveCards([card], target);
  }

  bool canMoveCards(List<CardComponent> cards, CardPile target) {
    if (cards.isEmpty) return false;

    final firstCard = cards.first;
    if (target == firstCard.pile) return false;
    if (!_isOrderedRun(cards)) return false;

    if (cards.length > 1 && target.type != PileType.tableau) {
      return false;
    }

    switch (target.type) {
      case PileType.freeCell:
        return target.cards.isEmpty;

      case PileType.foundation:
        final card = cards.single;
        if (target.cards.isEmpty) {
          return card.model.rank == 1;
        }
        final top = target.cards.last.model;
        return top.suit == card.model.suit && card.model.rank == top.rank + 1;

      case PileType.tableau:
        if (target.cards.isEmpty) return true;
        final top = target.cards.last.model;
        return top.isRed != firstCard.model.isRed &&
            firstCard.model.rank == top.rank - 1;
    }
  }

  CardPile? _findFoundationTarget(CardComponent card) {
    for (final foundation in foundations) {
      if (foundation.cards.isEmpty) continue;

      final top = foundation.cards.last.model;
      if (top.suit == card.model.suit && card.model.rank == top.rank + 1) {
        return foundation;
      }
    }

    if (card.model.rank != 1) return null;

    for (final foundation in foundations) {
      if (foundation.cards.isEmpty) {
        return foundation;
      }
    }

    return null;
  }

  bool _canMoveModelToTableau(CardModel model, CardPile target) {
    if (target.cards.isEmpty) return true;

    final top = target.cards.last.model;
    return top.isRed != model.isRed && model.rank == top.rank - 1;
  }

  bool _wouldRevealUsefulCard(List<CardComponent> cards, CardPile target) {
    if (cards.isEmpty) return false;

    final source = cards.first.pile;
    if (source.type != PileType.tableau) return true;

    final index = source.cards.indexOf(cards.first);
    if (index <= 0) return false;

    final exposedCard = source.cards[index - 1];
    if (_findFoundationTarget(exposedCard) != null) return true;

    for (final tableau in tableaus) {
      if (tableau == source || tableau == target) continue;
      if (_canMoveModelToTableau(exposedCard.model, tableau)) {
        return true;
      }
    }

    return false;
  }

  void moveCards(
    List<CardComponent> cards,
    CardPile target, {
    required bool record,
    bool autoCheck = true,
    bool animate = true,
  }) {
    if (isGameOver) return;
    if (cards.isEmpty) return;
    clearHint();

    final movingCards = List<CardComponent>.of(cards);
    final from = movingCards.first.pile;

    for (final card in movingCards) {
      from.cards.remove(card);
    }
    target.cards.addAll(movingCards);
    for (final card in movingCards) {
      card.pile = target;
    }

    if (record) {
      history.add(
        MoveRecord(cards: movingCards, fromPile: from, toPile: target),
      );
      _playSfx(SfxKey.drop);
    }
    _refreshHud(force: true);

    if (animate) {
      _layoutAll(skip: movingCards.toSet());
      for (int i = 0; i < movingCards.length; i++) {
        final card = movingCards[i];
        card.animateTo(
          _restPositionFor(card),
          duration: moveAnimationDuration,
          delay: i * 0.035,
          endPriority: _restPriorityFor(card),
        );
      }
    } else {
      _layoutAll();
    }

    if (autoCheck) {
      _autoMoveAvailableToFoundations(record: record);
    }
  }

  void moveCard(
    CardComponent card,
    CardPile target, {
    required bool record,
    bool autoCheck = true,
    bool animate = true,
  }) {
    moveCards(
      [card],
      target,
      record: record,
      autoCheck: autoCheck,
      animate: animate,
    );
  }

  void _autoMoveAvailableToFoundations({required bool record}) {
    if (isGameOver) return;

    var moved = true;

    while (moved) {
      moved = false;
      final candidates = <CardComponent>[
        for (final freeCell in freeCells)
          if (freeCell.cards.isNotEmpty) freeCell.cards.last,
        for (final tableau in tableaus)
          if (tableau.cards.isNotEmpty) tableau.cards.last,
      ];

      for (final card in candidates) {
        if (!isTopCard(card)) continue;

        final foundation = _findFoundationTarget(card);
        if (foundation == null) continue;

        moveCard(card, foundation, record: record, autoCheck: false);
        moved = true;
        break;
      }
    }

    checkGameState();
  }

  void undo() {
    if (isGameOver) return;
    if (history.isEmpty) return;
    clearHint();
    final last = history.removeLast();
    for (final card in last.cards) {
      last.toPile.cards.remove(card);
    }
    last.fromPile.cards.addAll(last.cards);
    for (final card in last.cards) {
      card.pile = last.fromPile;
    }
    _refreshHud(force: true);
    _layoutAll();
    _playSfx(SfxKey.drop);
    checkGameState();
  }

  Vector2 _restPositionFor(CardComponent card) {
    if (card.pile.type == PileType.tableau) {
      final index = card.pile.cards.indexOf(card);
      return card.pile.position + Vector2(0, index * stackOffset);
    }

    return card.pile.position.clone();
  }

  Vector2 _dropPositionFor(CardPile pile) {
    if (pile.type == PileType.tableau) {
      return pile.position + Vector2(0, pile.cards.length * stackOffset);
    }

    return pile.position.clone();
  }

  int _restPriorityFor(CardComponent card) {
    final index = card.pile.cards.indexOf(card);
    return 10 + max(0, index);
  }

  void _layoutAll({Set<CardComponent> skip = const {}}) {
    for (final pile in [...freeCells, ...foundations]) {
      for (int i = 0; i < pile.cards.length; i++) {
        final card = pile.cards[i];
        if (skip.contains(card)) continue;
        card.position = pile.position.clone();
        card.priority = 10 + i;
      }
    }

    for (final pile in tableaus) {
      for (int i = 0; i < pile.cards.length; i++) {
        final card = pile.cards[i];
        if (skip.contains(card)) continue;
        card.position = pile.position + Vector2(0, i * stackOffset);
        card.priority = 10 + i;
      }
    }
  }

  void autoMove(CardComponent card) {
    if (isGameOver) return;

    clearHint();
    if (!isTopCard(card)) return;

    final foundation = _findFoundationTarget(card);
    if (foundation != null) {
      moveCard(card, foundation, record: true);
      checkGameState();
      return;
    }

    for (final freeCell in freeCells) {
      if (canMove(card, freeCell)) {
        moveCard(card, freeCell, record: true);
        return;
      }
    }
  }

  bool get isWon {
    final count = foundations.fold<int>(
      0,
      (sum, pile) => sum + pile.cards.length,
    );
    return count == 52;
  }

  bool get isGameOver {
    return overlays.isActive('win') || overlays.isActive('lose');
  }

  bool get hasLegalMoves {
    if (_findAnyLegalMove() != null) return true;
    return _solveCurrentBoard() != _SolveStatus.unsolved;
  }

  HintMove? _findLegalMoveForLoseCheck() {
    final hint = _findHintMove();
    if (hint != null) return hint;

    return _findAnyLegalMove();
  }

  HintMove? _findAnyLegalMove() {
    final targets = <CardPile>[...freeCells, ...foundations, ...tableaus];

    for (final freeCell in freeCells) {
      if (freeCell.cards.isEmpty) continue;

      final card = freeCell.cards.last;
      for (final target in targets) {
        if (target.type == PileType.freeCell) continue;
        if (canMove(card, target)) {
          return HintMove(cards: [card], target: target);
        }
      }
    }

    for (final tableau in tableaus) {
      if (tableau.cards.isEmpty) continue;

      for (int i = 0; i < tableau.cards.length; i++) {
        final run = tableau.cards.sublist(i);
        if (!_isOrderedRun(run)) continue;

        for (final target in targets) {
          if (canMoveCards(run, target)) {
            return HintMove(cards: run, target: target);
          }
        }
      }
    }

    return null;
  }

  HintMove? _findHintMove() {
    final occupiedTableaus = [
      for (final tableau in tableaus)
        if (tableau.cards.isNotEmpty) tableau,
    ];
    final emptyTableaus = [
      for (final tableau in tableaus)
        if (tableau.cards.isEmpty) tableau,
    ];

    for (final freeCell in freeCells) {
      if (freeCell.cards.isEmpty) continue;

      final card = freeCell.cards.last;
      final foundation = _findFoundationTarget(card);
      if (foundation != null) {
        return HintMove(cards: [card], target: foundation);
      }
    }

    for (final tableau in tableaus) {
      if (tableau.cards.isEmpty) continue;

      final card = tableau.cards.last;
      final foundation = _findFoundationTarget(card);
      if (foundation != null) {
        return HintMove(cards: [card], target: foundation);
      }
    }

    for (final freeCell in freeCells) {
      if (freeCell.cards.isEmpty) continue;

      final card = freeCell.cards.last;
      for (final target in occupiedTableaus) {
        if (canMove(card, target)) {
          return HintMove(cards: [card], target: target);
        }
      }

      for (final target in emptyTableaus) {
        if (canMove(card, target)) {
          return HintMove(cards: [card], target: target);
        }
      }
    }

    for (final tableau in tableaus) {
      for (int i = 0; i < tableau.cards.length; i++) {
        final run = tableau.cards.sublist(i);
        if (!_isOrderedRun(run)) continue;

        for (final target in occupiedTableaus) {
          if (canMoveCards(run, target) &&
              _wouldRevealUsefulCard(run, target)) {
            return HintMove(cards: run, target: target);
          }
        }

        for (final target in emptyTableaus) {
          if (canMoveCards(run, target) &&
              _wouldRevealUsefulCard(run, target)) {
            return HintMove(cards: run, target: target);
          }
        }
      }
    }

    return null;
  }

  _SolverState _solverStateFromCurrentBoard() {
    return _SolverState(
      tableaus: [
        for (final tableau in tableaus)
          [
            for (final card in tableau.cards)
              _FreeCellSolver.encode(card.model),
          ],
      ],
      freeCells: [
        for (final freeCell in freeCells)
          freeCell.cards.isEmpty
              ? null
              : _FreeCellSolver.encode(freeCell.cards.last.model),
      ],
      foundations: [
        for (final suit in Suit.values)
          foundations
              .where((pile) => pile.cards.isNotEmpty)
              .map((pile) => pile.cards.last.model)
              .where((card) => card.suit == suit)
              .fold<int>(0, (rank, card) => max(rank, card.rank)),
      ],
    );
  }

  _SolveStatus _solveCurrentBoard() {
    return _FreeCellSolver(
      initial: _solverStateFromCurrentBoard(),
      maxDepth: 80,
      maxVisited: 50000,
    ).solve();
  }

  void showHint() {
    if (isGameOver) return;

    final hint = _findLegalMoveForLoseCheck();
    if (hint == null) return;

    hintedCards = List<CardComponent>.of(hint.cards);
    hintedTarget = hint.target;
    _hintSecondsLeft = 3;
  }

  void clearHint() {
    hintedCards = [];
    hintedTarget = null;
    _hintSecondsLeft = 0;
  }

  void checkGameState() {
    overlays.remove('win');
    overlays.remove('lose');

    if (isWon) {
      _recordWinStats();
      if (!_winSfxPlayed) {
        _playSfx(SfxKey.win);
        _winSfxPlayed = true;
      }
      overlays.add('win');
    } else if (!hasLegalMoves) {
      _recordLossStats();
      if (!_lossSfxPlayed) {
        _playSfx(SfxKey.loss);
        _lossSfxPlayed = true;
      }
      overlays.add('lose');
    }
  }
}

class PileSlotComponent extends PositionComponent {
  PileSlotComponent(this.pile)
    : super(
        position: pile.position,
        size: Vector2(FreeCellGame.cardWidth, FreeCellGame.cardHeight),
        priority: -1,
      );

  final CardPile pile;

  @override
  void update(double dt) {
    super.update(dt);
    position = pile.position;
  }

  @override
  void render(Canvas canvas) {
    final rect = Rect.fromLTWH(0, 0, size.x, size.y);
    final paint = Paint()
      ..color = Colors.white.withValues(alpha: 0.15)
      ..style = PaintingStyle.fill;
    final border = Paint()
      ..color = Colors.white.withValues(alpha: 0.45)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2;

    canvas.drawRRect(
      RRect.fromRectAndRadius(rect, const Radius.circular(8)),
      paint,
    );
    canvas.drawRRect(
      RRect.fromRectAndRadius(rect, const Radius.circular(8)),
      border,
    );

    final label = switch (pile.type) {
      PileType.freeCell => 'FREE',
      PileType.foundation => 'A',
      PileType.tableau => '',
    };

    if (label.isNotEmpty) {
      final tp = TextPainter(
        text: TextSpan(
          text: label,
          style: TextStyle(
            color: Colors.white.withValues(alpha: 0.45),
            fontSize: 9,
            fontWeight: FontWeight.bold,
          ),
        ),
        textDirection: TextDirection.ltr,
      )..layout();
      tp.paint(
        canvas,
        Offset((size.x - tp.width) / 2, (size.y - tp.height) / 2),
      );
    }
  }
}

class HintOverlayComponent extends Component
    with HasGameReference<FreeCellGame> {
  @override
  int priority = 970;

  @override
  void render(Canvas canvas) {
    final target = game.hintedTarget;
    if (game.hintedCards.isEmpty || target == null) return;

    final glow = Paint()
      ..color = const Color(0xFFFFD54F).withValues(alpha: 0.35)
      ..style = PaintingStyle.fill;
    final stroke = Paint()
      ..color = const Color(0xFFFFD54F).withValues(alpha: 0.95)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2.5;

    for (final card in game.hintedCards) {
      final rect = Rect.fromLTWH(
        card.position.x - 3,
        card.position.y - 3,
        FreeCellGame.cardWidth + 6,
        FreeCellGame.cardHeight + 6,
      );
      canvas.drawRRect(
        RRect.fromRectAndRadius(rect, const Radius.circular(9)),
        glow,
      );
      canvas.drawRRect(
        RRect.fromRectAndRadius(rect, const Radius.circular(9)),
        stroke,
      );
    }

    final targetPosition = game._dropPositionFor(target);
    final rect = Rect.fromLTWH(
      targetPosition.x - 5,
      targetPosition.y - 5,
      FreeCellGame.cardWidth + 10,
      FreeCellGame.cardHeight + 10,
    );
    canvas.drawRRect(
      RRect.fromRectAndRadius(rect, const Radius.circular(10)),
      stroke,
    );
  }
}

class HudButtonComponent extends PositionComponent
    with TapCallbacks, HasGameReference<FreeCellGame> {
  HudButtonComponent({required this.label, required this.onPressed})
    : super(size: Vector2(64, 32), anchor: Anchor.center, priority: 1000);

  final String label;
  final void Function(FreeCellGame game) onPressed;
  bool enabled = false;

  @override
  void render(Canvas canvas) {
    final rect = Rect.fromLTWH(0, 0, size.x, size.y);
    final bg = Paint()
      ..color = Colors.black.withValues(alpha: enabled ? 0.34 : 0.16)
      ..style = PaintingStyle.fill;
    final border = Paint()
      ..color = Colors.white.withValues(alpha: enabled ? 0.72 : 0.28)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1.5;

    canvas.drawRRect(
      RRect.fromRectAndRadius(rect, const Radius.circular(8)),
      bg,
    );
    canvas.drawRRect(
      RRect.fromRectAndRadius(rect, const Radius.circular(8)),
      border,
    );

    final tp = TextPainter(
      text: TextSpan(
        text: label,
        style: TextStyle(
          color: Colors.white.withValues(alpha: enabled ? 0.9 : 0.38),
          fontSize: label.length > 6 ? 10.5 : 12,
          fontWeight: FontWeight.bold,
        ),
      ),
      textDirection: TextDirection.ltr,
    )..layout();
    tp.paint(canvas, Offset((size.x - tp.width) / 2, (size.y - tp.height) / 2));
  }

  @override
  void onTapDown(TapDownEvent event) {
    super.onTapDown(event);
    if (!enabled) return;
    game.playSelectSound();
    onPressed(game);
  }
}

class CardComponent extends PositionComponent
    with DragCallbacks, DoubleTapCallbacks, HasGameReference<FreeCellGame> {
  CardComponent(this.model, this.pile)
    : super(size: Vector2(FreeCellGame.cardWidth, FreeCellGame.cardHeight));

  final CardModel model;
  CardPile pile;
  Sprite? cardSprite;
  Vector2? _animationStart;
  Vector2? _animationTarget;
  double _animationElapsed = 0;
  double _animationDuration = 0;
  double _animationDelay = 0;
  int? _animationEndPriority;

  bool get isAnimating => _animationTarget != null;

  void animateTo(
    Vector2 target, {
    required double duration,
    double delay = 0,
    int? endPriority,
  }) {
    _animationStart = position.clone();
    _animationTarget = target.clone();
    _animationElapsed = 0;
    _animationDuration = max(0.01, duration);
    _animationDelay = max(0, delay);
    _animationEndPriority = endPriority;
    priority = max(priority, 980);
  }

  @override
  void update(double dt) {
    super.update(dt);

    final target = _animationTarget;
    final start = _animationStart;
    if (target == null || start == null) return;

    _animationElapsed += dt;
    if (_animationElapsed < _animationDelay) return;

    final rawProgress =
        ((_animationElapsed - _animationDelay) / _animationDuration).clamp(
          0.0,
          1.0,
        );
    final progress = Curves.easeOutCubic.transform(rawProgress);
    final offset = target - start;
    offset.scale(progress);
    position = start + offset;

    if (rawProgress >= 1) {
      position = target;
      if (_animationEndPriority != null) {
        priority = _animationEndPriority!;
      }
      _animationStart = null;
      _animationTarget = null;
      _animationEndPriority = null;
      _animationElapsed = 0;
      _animationDelay = 0;
    }
  }

  @override
  Future<void> onLoad() async {
    await super.onLoad();

    // 扑克图预留：assets/images/cards/AS.png、2S.png ... KC.png
    // 没放图片时，会自动使用代码绘制的简易扑克。
    try {
      final img = await game.images.load('cards/${model.imageName}');
      cardSprite = Sprite(img);
    } catch (_) {
      cardSprite = null;
    }
  }

  @override
  void render(Canvas canvas) {
    if (cardSprite != null) {
      cardSprite!.render(canvas, size: size);
      return;
    }

    final rect = Rect.fromLTWH(0, 0, size.x, size.y);
    final bg = Paint()..color = Colors.white;
    final border = Paint()
      ..color = Colors.black.withValues(alpha: 0.35)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1.5;

    canvas.drawRRect(
      RRect.fromRectAndRadius(rect, const Radius.circular(8)),
      bg,
    );
    canvas.drawRRect(
      RRect.fromRectAndRadius(rect, const Radius.circular(8)),
      border,
    );

    final color = model.isRed ? Colors.red : Colors.black;
    _drawText(
      canvas,
      '${model.rankText}${model.suitText}',
      8,
      7,
      18,
      color,
      FontWeight.bold,
    );
    _drawText(
      canvas,
      model.suitText,
      size.x / 2 - 14,
      size.y / 2 - 22,
      38,
      color,
      FontWeight.bold,
    );
  }

  void _drawText(
    Canvas canvas,
    String text,
    double x,
    double y,
    double fontSize,
    Color color,
    FontWeight weight,
  ) {
    final tp = TextPainter(
      text: TextSpan(
        text: text,
        style: TextStyle(color: color, fontSize: fontSize, fontWeight: weight),
      ),
      textDirection: TextDirection.ltr,
    )..layout();
    tp.paint(canvas, Offset(x, y));
  }

  @override
  void onDragStart(DragStartEvent event) {
    super.onDragStart(event);
    game.startDrag(this);
  }

  @override
  void onDragUpdate(DragUpdateEvent event) {
    super.onDragUpdate(event);
    if (game.draggingCard == this) {
      game.updateDrag(event.canvasDelta);
    }
  }

  @override
  void onDragEnd(DragEndEvent event) {
    super.onDragEnd(event);
    if (game.draggingCard == this) {
      game.endDrag();
    }
  }

  @override
  void onDoubleTapDown(DoubleTapDownEvent event) {
    super.onDoubleTapDown(event);
    game.autoMove(this);
  }
}
