import 'dart:math' as math;

import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';

import '../audio/audio_manager.dart';
import '../game/game_controller.dart';
import '../game/levels.dart';
import '../game/music.dart';
import '../game/types.dart';
import 'memory_card_component.dart';

class MemoryMatchFlameGame extends FlameGame {
  MemoryMatchFlameGame({
    required this.level,
    required this.onStateChanged,
    required this.onResult,
  });

  LevelConfig level;
  final VoidCallback onStateChanged;
  final void Function(GameStatus status, int score) onResult;

  late GameController controller;

  bool _ready = false;
  bool get isReady => _ready;

  final Map<String, MemoryCardComponent> _cardViews = {};
  double _countdownAccumulator = 0;

  late final SpriteComponent _background;
  late final PositionComponent _boardRoot;

  bool pausedByUser = false;
  bool showResult = false;
  bool mutedMusic = false;
  bool mutedSfx = false;
  int timeLeft = 0;
  int bestScore = 0;
  String resultStatus = 'lost';
  int finalScore = 0;
  bool _boardEntryAnimating = false;
  int _boardEntryToken = 0;
  final math.Random _entryRandom = math.Random();

  int _lastMismatchToken = 0;
  int _lastStreak = 0;
  GameStatus _lastStatus = GameStatus.playing;

  String get timeText {
    final m = (timeLeft ~/ 60).toString().padLeft(2, '0');
    final s = (timeLeft % 60).toString().padLeft(2, '0');
    return '$m:$s';
  }

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

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

    _background = SpriteComponent(
      sprite: await loadSprite('a5c6f3813d0d23b4042dd130542753cc.png'),
      size: size,
      position: Vector2.zero(),
      priority: -100,
    );
    add(_background);

    _boardRoot = PositionComponent(anchor: Anchor.topLeft);
    add(_boardRoot);

    controller = GameController(level: level, allFaceIds: allFaceIds)
      ..addListener(_onGameChanged);

    timeLeft = level.timeSec ?? 120;
    _lastStatus = controller.status;
    _lastMismatchToken = controller.mismatch.token;
    _lastStreak = controller.streak;

    _ready = true;

    await AudioManager.instance.playBgm();

    _syncBoard(rebuildAll: true, animateEntry: true);
    _startTicker();
  }

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

    if (!isLoaded) return;

    _background.size = canvasSize;
    _layoutBoard();
  }

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

    if (!_ready) return;
    if (showResult || pausedByUser || controller.status != GameStatus.playing) {
      return;
    }
    if (timeLeft <= 0) return;

    _countdownAccumulator += dt;

    while (_countdownAccumulator >= 1.0) {
      _countdownAccumulator -= 1.0;

      if (showResult ||
          pausedByUser ||
          controller.status != GameStatus.playing) {
        return;
      }
      if (timeLeft <= 0) return;

      timeLeft -= 1;
      onStateChanged();

      if (timeLeft == 0 &&
          !showResult &&
          controller.status == GameStatus.playing) {
        _finishAsLose();
        return;
      }
    }
  }

  void _startTicker() {
    _countdownAccumulator = 0;
  }

  Future<void> onCardTapped(String cardId) async {
    if (!_ready) return;
    if (showResult ||
        pausedByUser ||
        _boardEntryAnimating ||
        controller.status != GameStatus.playing) {
      return;
    }

    await AudioManager.instance.playSfx(SfxKey.click);
    controller.flip(cardId);
  }

  Future<void> _onGameChanged() async {
    if (!_ready) return;

    if (controller.mismatch.token > _lastMismatchToken) {
      _lastMismatchToken = controller.mismatch.token;
      await AudioManager.instance.playSfx(SfxKey.mismatch);
    }

    if (controller.streak > _lastStreak) {
      _lastStreak = controller.streak;
      await AudioManager.instance.playSfx(SfxKey.match);
    }
    if (controller.streak == 0) {
      _lastStreak = 0;
    }

    if (controller.status != _lastStatus) {
      _lastStatus = controller.status;
      if (controller.status == GameStatus.won) {
        await AudioManager.instance.playSfx(SfxKey.win);
      } else if (controller.status == GameStatus.lost) {
        await AudioManager.instance.playSfx(SfxKey.lose);
      }
    }

    if (!showResult &&
        !pausedByUser &&
        controller.status == GameStatus.playing &&
        level.maxMistakes != null &&
        controller.mistakes >= level.maxMistakes!) {
      _finishAsLose();
    }

    if (!showResult && controller.status == GameStatus.won) {
      final score = controller.score + timeLeft * 10;
      resultStatus = 'won';
      finalScore = score;
      bestScore = math.max(bestScore, score);
      showResult = true;
      paused = true;
      onResult(GameStatus.won, score);
    }

    _syncBoard();
    onStateChanged();
  }

  void _finishAsLose() {
    resultStatus = 'lost';
    finalScore = controller.score;
    bestScore = math.max(bestScore, controller.score);
    showResult = true;
    paused = true;
    onResult(GameStatus.lost, finalScore);
  }

  void _syncBoard({bool rebuildAll = false, bool animateEntry = false}) {
    if (!_ready) return;

    if (rebuildAll && animateEntry) {
      _boardEntryAnimating = true;
    }

    final disablePlay =
        showResult ||
        controller.status != GameStatus.playing ||
        pausedByUser ||
        _boardEntryAnimating;

    if (rebuildAll) {
      _boardRoot.removeAll(_boardRoot.children.toList());
      _cardViews.clear();

      for (final card in controller.cards) {
        final component = MemoryCardComponent(
          model: card,
          frontAsset: 'faces/b1c14934b04bf12e674abfffcb87cf6e.png',
          backAsset: faceMap[card.faceId]!,
          disabled: disablePlay || card.isMatched,
        );
        _cardViews[card.id] = component;
        _boardRoot.add(component);
      }

      _layoutBoard(animateEntry: animateEntry);
      return;
    }

    for (final card in controller.cards) {
      _cardViews[card.id]?.updateFromModel(
        card,
        disablePlay: disablePlay,
        shouldShake: controller.mismatch.ids.contains(card.id),
        shakeToken: controller.mismatch.token,
      );
    }
  }

  void _layoutBoard({bool animateEntry = false}) {
    if (_cardViews.isEmpty || size.x <= 0 || size.y <= 0) return;

    final topReserve = size.y * 0.30;
    final bottomReserve = size.y * 0.18;
    final horizontalPadding = 18.0;

    final rows = level.rowPattern.length;
    final widest = level.rowPattern.reduce((a, b) => a > b ? a : b);

    final isTabletWide = size.x >= 768;
    final gap = isTabletWide ? 10.0 : 12.0;

    final usableWidth = size.x - horizontalPadding * 2;
    final usableHeight = size.y - topReserve - bottomReserve;

    final cellByWidth = (usableWidth - gap * (widest - 1)) / widest;
    final cellByHeight = (usableHeight - gap * (rows - 1)) / rows;

    final cell = math
        .min(cellByWidth, cellByHeight)
        .clamp(38.0, isTabletWide ? 120.0 : 110.0);

    final gridWidth = widest * cell + (widest - 1) * gap;
    final gridHeight = rows * cell + (rows - 1) * gap;

    final boardLeft = (size.x - gridWidth) / 2;
    final boardTop = topReserve + (usableHeight - gridHeight) / 2;

    _boardRoot.position = Vector2(boardLeft, boardTop);
    _boardRoot.size = Vector2(gridWidth, gridHeight);

    int cardIndex = 0;
    final cardLayouts = <({
      MemoryCardComponent view,
      Vector2 target,
      Vector2 start,
      int row,
      int indexInRow,
    })>[];

    for (int row = 0; row < rows; row++) {
      final count = level.rowPattern[row];
      final rowWidth = count * cell + (count - 1) * gap;

      final startX = (gridWidth - rowWidth) / 2;
      final y = row * (cell + gap);

      for (int i = 0; i < count; i++) {
        final card = controller.cards[cardIndex++];
        final view = _cardViews[card.id]!;
        final target = Vector2(startX + i * (cell + gap), y);

        view.updateCardSize(Vector2.all(cell));
        cardLayouts.add((
          view: view,
          target: target,
          start: _randomEntryStartPosition(target, gridWidth, gridHeight, cell),
          row: row,
          indexInRow: i,
        ));
      }
    }

    if (animateEntry) {
      _playBoardEntryAnimation(cardLayouts);
      return;
    }

    for (final layout in cardLayouts) {
      layout.view.position = layout.target;
      layout.view.resetEntranceVisual();
    }
  }

  void _playBoardEntryAnimation(
    List<
      ({
        MemoryCardComponent view,
        Vector2 target,
        Vector2 start,
        int row,
        int indexInRow,
      })
    > cardLayouts,
  ) {
    _boardEntryToken += 1;
    final token = _boardEntryToken;

    const rowDelayStep = 0.12;
    const itemDelayStep = 0.035;
    const duration = 0.42;

    for (final layout in cardLayouts) {
      layout.view.playEntranceEffect(
        startPosition: layout.start,
        targetPosition: layout.target,
        delay: layout.row * rowDelayStep + layout.indexInRow * itemDelayStep,
      );
    }

    onStateChanged();

    final totalSeconds =
        (level.rowPattern.length - 1) * rowDelayStep +
        (widestRowCount() - 1) * itemDelayStep +
        duration +
        0.04;
    Future.delayed(
      Duration(milliseconds: (totalSeconds * 1000).round()),
      () {
        if (!_ready || token != _boardEntryToken) return;
        _boardEntryAnimating = false;
        _syncBoard();
        onStateChanged();
      }
    );
  }

  int widestRowCount() {
    return level.rowPattern.reduce((a, b) => a > b ? a : b);
  }

  Vector2 _randomEntryStartPosition(
    Vector2 target,
    double gridWidth,
    double gridHeight,
    double cell,
  ) {
    final side = _entryRandom.nextInt(4);
    final extraX = cell * (1.2 + _entryRandom.nextDouble() * 1.8);
    final extraY = cell * (1.0 + _entryRandom.nextDouble() * 2.2);

    switch (side) {
      case 0:
        return Vector2(
          -extraX,
          target.y - extraY - _entryRandom.nextDouble() * cell,
        );
      case 1:
        return Vector2(
          gridWidth + extraX,
          target.y - extraY - _entryRandom.nextDouble() * cell,
        );
      case 2:
        return Vector2(
          target.x + (_entryRandom.nextDouble() - 0.5) * cell * 1.8,
          -extraY,
        );
      default:
        return Vector2(
          target.x + (_entryRandom.nextDouble() - 0.5) * cell * 1.6,
          gridHeight + extraY,
        );
    }
  }

  Future<void> restartSameLevel() async {
    if (!_ready) return;

    showResult = false;
    paused = false;
    pausedByUser = false;
    timeLeft = level.timeSec ?? 120;
    _countdownAccumulator = 0;

    controller.reset();
    _lastStatus = controller.status;
    _lastMismatchToken = controller.mismatch.token;
    _lastStreak = controller.streak;

    _syncBoard(rebuildAll: true, animateEntry: true);
    _startTicker();
    onStateChanged();
  }

  Future<void> nextLevel(LevelConfig next) async {
    if (!_ready) return;

    level = next;
    showResult = false;
    paused = false;
    pausedByUser = false;
    _countdownAccumulator = 0;

    controller.removeListener(_onGameChanged);
    controller.dispose();

    controller = GameController(level: level, allFaceIds: allFaceIds)
      ..addListener(_onGameChanged);

    timeLeft = level.timeSec ?? 120;
    _lastStatus = controller.status;
    _lastMismatchToken = controller.mismatch.token;
    _lastStreak = controller.streak;

    _syncBoard(rebuildAll: true, animateEntry: true);
    _startTicker();
    onStateChanged();
  }

  Future<int> reviveWithReward() async {
    if (!_ready) return 0;

    final sec = controller.reviveWithReward(
      baseSeconds: 30,
      penaltyPerMove: 10,
      rollbackMistakes: controller.mistakes,
    );

    if (sec > 0) {
      timeLeft += sec;
      _countdownAccumulator = 0;
      showResult = false;
      paused = false;
      resultStatus = 'lost';
      finalScore = 0;
      _syncBoard();
      onStateChanged();
    }

    return sec;
  }

  Future<void> togglePause() async {
    if (!_ready) return;
    pausedByUser = !pausedByUser;
    paused = pausedByUser;
    onStateChanged();
  }

  Future<void> toggleMusic() async {
    mutedMusic = !mutedMusic;
    await AudioManager.instance.setBgmMuted(mutedMusic);
    onStateChanged();
  }

  void toggleSfx() {
    mutedSfx = !mutedSfx;
    AudioManager.instance.setSfxMuted(mutedSfx);
    onStateChanged();
  }

  @override
  void onRemove() {
    if (_ready) {
      controller.removeListener(_onGameChanged);
      controller.dispose();
    }
    super.onRemove();
  }
}
