import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

import '../../game/engine/game_engine.dart';
import '../../game/engine/game_event.dart';
import '../../game/models/grid.dart';
import '../../game/models/piece.dart';
import '../theme/palette.dart';
import 'board_geometry.dart';

enum _FxType { spark, popup, combo, confetti, settle }

class _Fx {
  _Fx(this.type, this.start, {this.pos = Offset.zero, this.data});

  final _FxType type;
  final Duration start;
  final Offset pos;
  final Object? data;

  static const durations = {
    _FxType.spark: Duration(milliseconds: 600),
    _FxType.popup: Duration(milliseconds: 900),
    _FxType.combo: Duration(milliseconds: 1000),
    _FxType.confetti: Duration(milliseconds: 1800),
    _FxType.settle: Duration(milliseconds: 140),
  };

  double progress(Duration now) {
    final d = durations[type]!;
    final p = (now - start).inMicroseconds / d.inMicroseconds;
    return p.clamp(0.0, 1.0);
  }

  bool done(Duration now) => progress(now) >= 1;
}

/// Transient game-feel effects above the board: clear sparks, rising score
/// popups, combo banners, win confetti, placed-piece settle. The game screen
/// reports events; effects expire on their own and the ticker stops when the
/// list drains, so widget tests settle normally.
class EffectsOverlay extends StatefulWidget {
  const EffectsOverlay({super.key});

  @override
  State<EffectsOverlay> createState() => EffectsOverlayState();
}

class EffectsOverlayState extends State<EffectsOverlay>
    with SingleTickerProviderStateMixin {
  late final Ticker _ticker;
  final List<_Fx> _fx = [];
  Duration _now = Duration.zero;

  @override
  void initState() {
    super.initState();
    _ticker = createTicker(_tick);
  }

  @visibleForTesting
  Ticker get ticker => _ticker;

  @visibleForTesting
  Duration get now => _now;

  void _tick(Duration elapsed) {
    setState(() {
      _now = elapsed;
      _fx.removeWhere((e) => e.done(elapsed));
      if (_fx.isEmpty) {
        _ticker.stop();
        // Ticker elapsed restarts from zero after stop(); re-anchor so
        // effects added later don't inherit a stale clock.
        _now = Duration.zero;
      }
    });
  }

  void _add(_Fx fx) {
    _fx.add(fx);
    if (!_ticker.isActive) _ticker.start();
  }

  /// [boardRect] is the board's rect in this overlay's coordinates.
  void onPlacement(PlacementResult placement, {required Rect boardRect}) {
    final geo = BoardGeometry(boardSize: boardRect.width);
    final origin = boardRect.topLeft;

    for (final event in placement.events) {
      if (event is PiecePlaced) {
        _add(_Fx(_FxType.settle, _now,
            pos: origin +
                Offset(event.x * geo.cellSize, event.y * geo.cellSize),
            data: (event.piece, geo.cellSize)));
      }
    }

    final cleared = <Offset>[];
    for (final y in placement.clearedRows) {
      for (var x = 0; x < GridState.size; x++) {
        cleared.add(origin + geo.cellRect(x, y).center);
      }
    }
    for (final x in placement.clearedCols) {
      for (var y = 0; y < GridState.size; y++) {
        cleared.add(origin + geo.cellRect(x, y).center);
      }
    }
    for (final c in cleared) {
      _add(_Fx(_FxType.spark, _now, pos: c, data: geo.cellSize));
    }

    if (placement.pointsGained > 0 && placement.linesCleared > 0) {
      final at = cleared.isEmpty
          ? boardRect.center
          : cleared[cleared.length ~/ 2];
      _add(_Fx(_FxType.popup, _now,
          pos: at, data: '+${placement.pointsGained}'));
    }

    if (placement.comboStreak >= 2) {
      _add(_Fx(_FxType.combo, _now,
          pos: boardRect.center, data: placement.comboStreak));
    }
  }

  void onWin(Size screenSize) {
    for (var i = 0; i < 36; i++) {
      _add(_Fx(_FxType.confetti, _now,
          pos: Offset(screenSize.width * hash(i, 1), -12), data: i));
    }
  }

  /// Deterministic pseudo-random in [0, 1) from an index.
  static double hash(int i, double salt) {
    final v = math.sin(i * 12.9898 + salt) * 43758.5453;
    return v - v.floorToDouble();
  }

  @override
  void dispose() {
    _ticker.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return IgnorePointer(
      child: CustomPaint(
        size: Size.infinite,
        painter: _FxPainter(List.of(_fx), _now),
      ),
    );
  }
}

class _FxPainter extends CustomPainter {
  const _FxPainter(this.fx, this.now);

  final List<_Fx> fx;
  final Duration now;

  @override
  void paint(Canvas canvas, Size size) {
    for (final e in fx) {
      final t = e.progress(now);
      switch (e.type) {
        case _FxType.spark:
          _spark(canvas, e, t);
        case _FxType.popup:
          _popup(canvas, e, t);
        case _FxType.combo:
          _combo(canvas, e, t);
        case _FxType.confetti:
          _confetti(canvas, e, t, size);
        case _FxType.settle:
          _settle(canvas, e, t);
      }
    }
  }

  void _spark(Canvas canvas, _Fx e, double t) {
    final cell = e.data as double;
    for (var i = 0; i < 6; i++) {
      final angle =
          i * math.pi / 3 + EffectsOverlayState.hash(i, 7) * 0.8;
      final dist = cell * (0.3 + 1.2 * Curves.easeOut.transform(t));
      final pos = e.pos + Offset(math.cos(angle), math.sin(angle)) * dist;
      canvas.drawCircle(
        pos,
        cell * 0.09 * (1 - t),
        Paint()..color = Colors.white.withValues(alpha: (1 - t) * 0.9),
      );
    }
  }

  void _popup(Canvas canvas, _Fx e, double t) {
    final rise = 44 * Curves.easeOut.transform(t);
    _text(canvas, e.data as String, e.pos - Offset(0, rise),
        fontSize: 22, color: Colors.white.withValues(alpha: 1 - t * t));
  }

  void _combo(Canvas canvas, _Fx e, double t) {
    final streak = e.data as int;
    final color = streak >= 6
        ? const Color(0xFFB980FF)
        : streak >= 4
            ? const Color(0xFFFF8A4D)
            : const Color(0xFFFFD166);
    final scale =
        t < 0.25 ? Curves.easeOutBack.transform(t / 0.25) : 1.0;
    final alpha = t > 0.7 ? (1 - t) / 0.3 : 1.0;
    canvas.save();
    canvas.translate(e.pos.dx, e.pos.dy - 30);
    canvas.scale(scale);
    _text(canvas, 'COMBO ×$streak', Offset.zero,
        fontSize: streak >= 4 ? 40 : 34,
        color: color.withValues(alpha: alpha.clamp(0.0, 1.0)));
    canvas.restore();
  }

  void _confetti(Canvas canvas, _Fx e, double t, Size size) {
    final i = e.data as int;
    final colors = GamePalette.tileColors;
    final x = e.pos.dx + 28 * math.sin(t * 4 * math.pi + i);
    final y = t * (size.height + 40);
    canvas.save();
    canvas.translate(x, y);
    canvas.rotate(
        t * 6 * math.pi * (EffectsOverlayState.hash(i, 3) - 0.5));
    canvas.drawRect(
      Rect.fromCenter(center: Offset.zero, width: 9, height: 5),
      Paint()
        ..color = colors[i % colors.length]
            .withValues(alpha: (1 - t * 0.6).clamp(0.0, 1.0)),
    );
    canvas.restore();
  }

  void _settle(Canvas canvas, _Fx e, double t) {
    final (piece, cellSize) = e.data as (Piece, double);
    final scale = 1.08 - 0.08 * Curves.easeOut.transform(t);
    final alpha = 0.35 * (1 - t);
    for (final (dx, dy) in piece.offsets) {
      final rect = Rect.fromLTWH(
        e.pos.dx + dx * cellSize,
        e.pos.dy + dy * cellSize,
        cellSize,
        cellSize,
      );
      final scaled = Rect.fromCenter(
        center: rect.center,
        width: rect.width * scale,
        height: rect.height * scale,
      ).deflate(cellSize * 0.05);
      canvas.drawRRect(
        RRect.fromRectAndRadius(scaled, Radius.circular(cellSize * 0.18)),
        Paint()..color = Colors.white.withValues(alpha: alpha),
      );
    }
  }

  void _text(Canvas canvas, String s, Offset center,
      {required double fontSize, required Color color}) {
    final painter = TextPainter(
      text: TextSpan(
        text: s,
        style: TextStyle(
          fontSize: fontSize,
          fontWeight: FontWeight.w900,
          color: color,
          shadows: const [Shadow(blurRadius: 14, color: Colors.black87)],
        ),
      ),
      textDirection: TextDirection.ltr,
    )..layout();
    painter.paint(
        canvas, center - Offset(painter.width / 2, painter.height / 2));
  }

  @override
  bool shouldRepaint(_FxPainter old) => true;
}
