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

import '../game/types.dart';
import 'memory_match_flame_game.dart';

class MemoryCardComponent extends PositionComponent
    with TapCallbacks, HasGameReference<MemoryMatchFlameGame> {
  MemoryCardComponent({
    required this.model,
    required this.frontAsset,
    required this.backAsset,
    required this.disabled,
  }) : super(anchor: Anchor.topLeft);

  CardModel model;
  final String frontAsset;
  final String backAsset;
  bool disabled;

  late final RectangleComponent _base;
  late final SpriteComponent _sprite;

  late final Sprite _frontSprite;
  late final Sprite _backSprite;

  bool _showBack = false;
  bool _isAnimatingFlip = false;
  bool _pendingMatchEffect = false;
  int _lastShakeToken = 0;
  bool _isEntering = false;
  double _entryElapsed = 0;
  double _entryDelay = 0;
  double _entryDuration = 0.42;
  double _entryStartAngle = 0;
  Vector2 _entryStartPosition = Vector2.zero();
  Vector2 _entryTargetPosition = Vector2.zero();

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

    _frontSprite = await game.loadSprite(frontAsset);
    _backSprite = await game.loadSprite(backAsset);

    _base = RectangleComponent(
      anchor: Anchor.topLeft,
      position: Vector2.zero(),
      size: Vector2.all(size.x > 0 ? size.x : 80),
      paint: Paint()..color = const Color.fromRGBO(8, 16, 30, 0.28),
    );
    add(_base);

    _sprite = SpriteComponent(
      sprite: _frontSprite,
      anchor: Anchor.topLeft,
      position: Vector2.zero(),
      size: Vector2.all((size.x > 0 ? size.x : 80) - 10),
    );
    add(_sprite);

    _applyLayout();
    _applyImmediateVisual();
  }

  @override
  void render(Canvas canvas) {
    final r = RRect.fromRectAndRadius(
      Rect.fromLTWH(0, 0, size.x, size.y),
      const Radius.circular(16),
    );

    canvas.save();
    canvas.clipRRect(r);
    super.render(canvas);
    canvas.restore();

    final borderPaint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2
      ..color = const Color.fromRGBO(120, 255, 255, 0.9);

    canvas.drawRRect(r, borderPaint);
  }

  @override
  bool containsLocalPoint(Vector2 point) {
    return point.x >= 0 &&
        point.y >= 0 &&
        point.x <= size.x &&
        point.y <= size.y;
  }

  @override
  void onMount() {
    super.onMount();
    _applyLayout();
  }

  @override
  void onGameResize(Vector2 gameSize) {
    super.onGameResize(gameSize);
    _applyLayout();
  }

  void updateCardSize(Vector2 newSize) {
    size = newSize;
    _applyLayout();
  }

  void resetEntranceVisual() {
    _isEntering = false;
    _entryElapsed = 0;
    position = position.clone();
    scale = Vector2.all(1.0);
    angle = 0;
  }

  void _applyLayout() {
    if (!isLoaded) return;

    _base.size = size.clone();
    _base.position = Vector2.zero();

    final padding = 5.0;
    final innerW = (size.x - padding * 2).clamp(1.0, double.infinity);
    final innerH = (size.y - padding * 2).clamp(1.0, double.infinity);

    _sprite.size = Vector2(innerW, innerH);
    _sprite.position = Vector2(padding, padding);
  }

  void _setSpriteOpacity(double opacity) {
    _sprite.paint = Paint()
      ..color = Color.fromRGBO(255, 255, 255, opacity.clamp(0.0, 1.0));
  }

  void updateFromModel(
    CardModel nextModel, {
    required bool disablePlay,
    required bool shouldShake,
    required int shakeToken,
  }) {
    final wasFlippedOrMatched = model.isFlipped || model.isMatched;
    final wasMatched = model.isMatched;

    model = nextModel;
    disabled = disablePlay || nextModel.isMatched;

    if (!isLoaded) {
      _showBack = model.isFlipped || model.isMatched;
      return;
    }

    final nowFlippedOrMatched = nextModel.isFlipped || nextModel.isMatched;
    final becameMatched = !wasMatched && nextModel.isMatched;

    if (nowFlippedOrMatched != wasFlippedOrMatched) {
      if (becameMatched) {
        _pendingMatchEffect = true;
      }
      _runFlip(nowFlippedOrMatched);
    } else {
      _applyImmediateVisual();
      if (becameMatched) {
        _runMatchEffect();
      }
    }

    if (shouldShake && shakeToken > _lastShakeToken) {
      _lastShakeToken = shakeToken;
      _runShakeEffect();
    }
  }

  void _applyImmediateVisual() {
    _showBack = model.isFlipped || model.isMatched;
    _updateSprite();
    _setSpriteOpacity(model.isMatched ? 0.68 : 1.0);
    scale = Vector2.all(1.0);
    angle = 0;
  }

  void _updateSprite() {
    if (!isLoaded) return;
    _sprite.sprite = _showBack ? _backSprite : _frontSprite;
  }

  void _runFlip(bool toBack) {
    if (_isAnimatingFlip) {
      _showBack = toBack;
      _applyImmediateVisual();
      return;
    }

    _isAnimatingFlip = true;

    final half = EffectController(duration: 0.11, curve: Curves.easeInOut);

    add(
      SequenceEffect(
        [
          ScaleEffect.to(Vector2(0.04, 1.0), half),
          _SwapCardFaceEffect(
            onSwap: () {
              _showBack = toBack;
              _updateSprite();
            },
          ),
          ScaleEffect.to(Vector2(1.0, 1.0), half),
        ],
        onComplete: () {
          _isAnimatingFlip = false;
          scale = Vector2.all(1.0);
          _setSpriteOpacity(model.isMatched ? 0.68 : 1.0);

          if (_pendingMatchEffect) {
            _pendingMatchEffect = false;
            _runMatchEffect();
          }
        },
      ),
    );
  }

  void _runMatchEffect() {
    add(
      SequenceEffect(
        [
          ScaleEffect.to(
            Vector2.all(1.08),
            EffectController(duration: 0.09, curve: Curves.easeOut),
          ),
          ScaleEffect.to(
            Vector2.all(1.0),
            EffectController(duration: 0.12, curve: Curves.easeIn),
          ),
        ],
        onComplete: () {
          scale = Vector2.all(1.0);
          _setSpriteOpacity(model.isMatched ? 0.68 : 1.0);
        },
      ),
    );
  }

  void _runShakeEffect() {
    add(
      SequenceEffect([
        MoveByEffect(Vector2(8, 0), EffectController(duration: 0.045)),
        MoveByEffect(Vector2(-16, 0), EffectController(duration: 0.045)),
        MoveByEffect(Vector2(16, 0), EffectController(duration: 0.045)),
        MoveByEffect(Vector2(-8, 0), EffectController(duration: 0.045)),
      ]),
    );
  }

  void playEntranceEffect({
    required Vector2 startPosition,
    required Vector2 targetPosition,
    required double delay,
  }) {
    _isEntering = true;
    _entryElapsed = 0;
    _entryDelay = delay;
    _entryDuration = 0.42;
    _entryStartPosition = startPosition.clone();
    _entryTargetPosition = targetPosition.clone();
    _entryStartAngle = startPosition.x <= targetPosition.x ? -0.24 : 0.24;

    position = startPosition.clone();
    scale = Vector2.all(0.72);
    angle = _entryStartAngle;
  }

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

    if (!_isEntering) return;

    _entryElapsed += dt;

    if (_entryElapsed < _entryDelay) return;

    final rawT = ((_entryElapsed - _entryDelay) / _entryDuration).clamp(0.0, 1.0);
    final moveT = Curves.easeOutCubic.transform(rawT);
    final scaleT = Curves.easeOutBack.transform(rawT);

    position = Vector2(
      _lerp(_entryStartPosition.x, _entryTargetPosition.x, moveT),
      _lerp(_entryStartPosition.y, _entryTargetPosition.y, moveT),
    );
    final currentScale = _lerp(0.72, 1.0, scaleT);
    scale = Vector2.all(currentScale);
    angle = _lerp(_entryStartAngle, 0, moveT);

    if (rawT >= 1.0) {
      _isEntering = false;
      position = _entryTargetPosition.clone();
      scale = Vector2.all(1.0);
      angle = 0;
    }
  }

  double _lerp(double a, double b, double t) {
    return a + (b - a) * t;
  }

  @override
  void onTapUp(TapUpEvent event) {
    super.onTapUp(event);
    if (disabled) return;
    game.onCardTapped(model.id);
  }
}

class _SwapCardFaceEffect extends ComponentEffect<PositionComponent> {
  _SwapCardFaceEffect({required this.onSwap})
    : super(EffectController(duration: 0.0001));

  final void Function() onSwap;
  bool _done = false;

  @override
  void apply(double progress) {
    if (_done) return;
    _done = true;
    onSwap();
  }

  double measure() => 0.0001;
}
