import 'dart:async';
import 'dart:ui' as ui;

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:lottie/lottie.dart';

import '../../core/media/rlottie/rlottie.dart';

class LottieLoadGovernor {
  LottieLoadGovernor._() {
    _budgetMs = _resolveBudgetMs();
    _avgMs = _budgetMs;
    SchedulerBinding.instance.addTimingsCallback(_onTimings);
  }

  static final LottieLoadGovernor instance = LottieLoadGovernor._();

  final ValueNotifier<bool> throttled = ValueNotifier(false);
  double _budgetMs = 1000 / 60;
  double _avgMs = 1000 / 60;

  static double _resolveBudgetMs() {
    final displays = ui.PlatformDispatcher.instance.displays;
    var hz = displays.isEmpty ? 60.0 : displays.first.refreshRate;
    if (!hz.isFinite || hz < 30) hz = 60;
    return 1000 / hz;
  }

  void _onTimings(List<FrameTiming> timings) {
    for (final t in timings) {
      final build = t.buildDuration.inMicroseconds;
      final raster = t.rasterDuration.inMicroseconds;
      final ms = (build > raster ? build : raster) / 1000.0;
      _avgMs = _avgMs * 0.6 + ms * 0.4;
    }
    final enterMs = _budgetMs * 1.5;
    final exitMs = _budgetMs * 0.8;
    if (!throttled.value && _avgMs > enterMs) {
      throttled.value = true;
    } else if (throttled.value && _avgMs < exitMs) {
      throttled.value = false;
    }
  }
}

class LottieScrollScope extends InheritedWidget {
  final ValueListenable<bool> isScrolling;

  const LottieScrollScope({
    super.key,
    required this.isScrolling,
    required super.child,
  });

  static ValueListenable<bool>? of(BuildContext context) => context
      .dependOnInheritedWidgetOfExactType<LottieScrollScope>()
      ?.isScrolling;

  @override
  bool updateShouldNotify(LottieScrollScope oldWidget) =>
      !identical(oldWidget.isScrolling, isScrolling);
}

class LottieHoldScope extends InheritedWidget {
  final ValueListenable<bool> isHeld;

  const LottieHoldScope({
    super.key,
    required this.isHeld,
    required super.child,
  });

  static ValueListenable<bool>? of(BuildContext context) => context
      .dependOnInheritedWidgetOfExactType<LottieHoldScope>()
      ?.isHeld;

  @override
  bool updateShouldNotify(LottieHoldScope oldWidget) =>
      !identical(oldWidget.isHeld, isHeld);
}

class LottiePlayer extends StatefulWidget {
  final String lottieUrl;
  final String? fallbackUrl;
  final double? size;
  final int? memCacheWidth;
  final bool shimmer;
  final bool eager;
  final bool animate;
  final bool repeat;
  final VoidCallback? onCompleted;

  const LottiePlayer({
    super.key,
    required this.lottieUrl,
    this.fallbackUrl,
    this.size,
    this.memCacheWidth,
    this.shimmer = true,
    this.eager = false,
    this.animate = true,
    this.repeat = true,
    this.onCompleted,
  });

  @override
  State<LottiePlayer> createState() => _LottiePlayerState();
}

class _LottiePlayerState extends State<LottiePlayer>
    with SingleTickerProviderStateMixin {
  static const int _leadFrames = 6;
  static const double _slowSpeed = 0.5;
  static const double _rampMs = 200.0;

  final ValueNotifier<int> _frameIndex = ValueNotifier(0);
  late final Ticker _ticker;
  late final bool _native;
  RlottieClip? _clip;
  ValueListenable<bool>? _scrollState;
  ValueListenable<bool>? _holdState;
  int? _px;
  bool _started = false;
  bool _showedFrames = false;
  bool _completed = false;
  Timer? _deferTimer;
  Timer? _fallbackCompletionTimer;

  static const Duration _maxLoadDefer = Duration(milliseconds: 700);

  double _speed = 1.0;
  double _targetSpeed = 1.0;
  double _playheadMs = 0.0;
  double? _lastElapsedMs;

  bool get _isScrolling => _scrollState?.value ?? false;
  bool get _isHeld => _holdState?.value ?? false;
  bool get _canLoad =>
      !_isScrolling &&
      !_isHeld &&
      (widget.eager || !LottieLoadGovernor.instance.throttled.value);

  @override
  void initState() {
    super.initState();
    _native = RlottieEngine.instance.available;
    _ticker = createTicker(_onTick);
    LottieLoadGovernor.instance.throttled.addListener(_onGateChanged);
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final state = LottieScrollScope.of(context);
    if (!identical(state, _scrollState)) {
      _scrollState?.removeListener(_onGateChanged);
      _scrollState = state;
      _scrollState?.addListener(_onGateChanged);
    }
    final hold = LottieHoldScope.of(context);
    if (!identical(hold, _holdState)) {
      _holdState?.removeListener(_onGateChanged);
      _holdState = hold;
      _holdState?.addListener(_onGateChanged);
    }
  }

  @override
  void didUpdateWidget(LottiePlayer oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.lottieUrl != widget.lottieUrl) {
      _ticker.stop();
      _releaseClip();
      _deferTimer?.cancel();
      _deferTimer = null;
      _started = false;
      _showedFrames = false;
      _playheadMs = 0.0;
      _speed = 1.0;
      _targetSpeed = _isScrolling ? _slowSpeed : 1.0;
      _lastElapsedMs = null;
      if (_frameIndex.value != 0) _frameIndex.value = 0;
      _completed = false;
      _fallbackCompletionTimer?.cancel();
      _fallbackCompletionTimer = null;
    } else if (oldWidget.animate != widget.animate ||
        oldWidget.repeat != widget.repeat) {
      _resetPlayback();
      final clip = _clip;
      if (clip != null) _maybeStartTicker(clip);
    }
  }

  @override
  void dispose() {
    _deferTimer?.cancel();
    _fallbackCompletionTimer?.cancel();
    LottieLoadGovernor.instance.throttled.removeListener(_onGateChanged);
    _scrollState?.removeListener(_onGateChanged);
    _holdState?.removeListener(_onGateChanged);
    _ticker.dispose();
    _releaseClip();
    _frameIndex.dispose();
    super.dispose();
  }

  void _releaseClip() {
    final clip = _clip;
    if (clip != null) {
      clip.ready.removeListener(_onReady);
      RlottieEngine.instance.release(clip);
      _clip = null;
    }
  }

  void _onTick(Duration elapsed) {
    final clip = _clip;
    if (clip == null || clip.frameCount <= 1) return;
    final periodMs = clip.durationMs;
    if (periodMs <= 0) return;

    final nowMs = elapsed.inMicroseconds / 1000.0;
    final last = _lastElapsedMs;
    _lastElapsedMs = nowMs;
    if (last == null) return;
    var dt = nowMs - last;
    if (dt < 0) dt = 0;
    if (dt > 64) dt = 64;

    if (_speed != _targetSpeed) {
      final step = dt / _rampMs * (1.0 - _slowSpeed);
      final diff = _targetSpeed - _speed;
      _speed = diff.abs() <= step ? _targetSpeed : _speed + step * diff.sign;
    }

    final nextPlayhead = _playheadMs + dt * _speed;
    if (!widget.repeat && nextPlayhead >= periodMs) {
      _playheadMs = periodMs.toDouble();
      if (_frameIndex.value != clip.frameCount - 1) {
        _frameIndex.value = clip.frameCount - 1;
      }
      _completePlayback();
      return;
    }

    _playheadMs = widget.repeat ? nextPlayhead % periodMs : nextPlayhead;
    final t = _playheadMs / periodMs;
    final index = (t * (clip.frameCount - 1)).round().clamp(
      0,
      clip.frameCount - 1,
    );
    if (index != _frameIndex.value) _frameIndex.value = index;
  }

  void _onGateChanged() {
    if (!mounted) return;
    _targetSpeed = _isScrolling ? _slowSpeed : 1.0;
    final clip = _clip;
    if (clip != null) {
      _maybeStartTicker(clip);
    } else if (_canLoad && !_started) {
      _startLoad();
    }
  }

  void _onReady() {
    if (!mounted) return;
    final clip = _clip;
    if (clip != null) _maybeStartTicker(clip);
    if (mounted) setState(() {});
  }

  void _maybeStartTicker(RlottieClip clip) {
    if (!widget.animate || _completed) return;
    if (clip.frameCount <= 1) {
      if (!widget.repeat) _completePlayback();
      return;
    }
    final lead = clip.frameCount < _leadFrames ? clip.frameCount : _leadFrames;
    if (clip.ready.value >= lead && !_ticker.isActive) {
      _lastElapsedMs = null;
      _ticker.start();
    }
  }

  void _resetPlayback() {
    _ticker.stop();
    _fallbackCompletionTimer?.cancel();
    _fallbackCompletionTimer = null;
    _completed = false;
    _playheadMs = 0;
    _lastElapsedMs = null;
    if (_frameIndex.value != 0) _frameIndex.value = 0;
  }

  void _completePlayback() {
    if (_completed) return;
    _completed = true;
    _ticker.stop();
    _fallbackCompletionTimer?.cancel();
    _fallbackCompletionTimer = null;
    final callback = widget.onCompleted;
    if (callback == null) return;
    SchedulerBinding.instance.addPostFrameCallback((_) {
      if (mounted) callback();
    });
  }

  void _scheduleFallbackCompletion(Duration duration) {
    if (!widget.animate || widget.repeat || _completed) return;
    _fallbackCompletionTimer ??= Timer(duration, _completePlayback);
  }

  void _ensure(double box) {
    if (_clip != null) return;
    final dpr = MediaQuery.devicePixelRatioOf(context);
    final raw = (box * dpr.clamp(1.0, 2.0)).clamp(96.0, 384.0);
    _px = (raw / 32).ceil() * 32;
    if (_started) return;
    if (_canLoad) {
      _startLoad();
    } else if (!_isScrolling && !_isHeld) {
      _deferTimer ??= Timer(_maxLoadDefer, _forceDeferredLoad);
    }
  }

  void _forceDeferredLoad() {
    _deferTimer = null;
    if (mounted && !_started && _clip == null && !_isScrolling && !_isHeld) {
      _startLoad();
    }
  }

  void _startLoad() {
    final px = _px;
    if (_started || px == null) return;
    _deferTimer?.cancel();
    _deferTimer = null;
    _started = true;
    RlottieEngine.instance.acquire(widget.lottieUrl, px).then((clip) {
      if (clip == null) return;
      if (!mounted) {
        RlottieEngine.instance.release(clip);
        return;
      }
      clip.ready.addListener(_onReady);
      setState(() => _clip = clip);
      _maybeStartTicker(clip);
    });
  }

  @override
  Widget build(BuildContext context) {
    if (!_native) return _nativeFallback();
    return LayoutBuilder(
      builder: (context, constraints) {
        final box =
            widget.size ??
            (constraints.hasBoundedWidth
                ? constraints.biggest.shortestSide
                : 96.0);
        _ensure(box);
        final clip = _clip;
        if (clip == null ||
            clip.ready.value == 0 ||
            (_isScrolling && !_showedFrames)) {
          return _staticFallback(box);
        }
        _showedFrames = true;
        return ValueListenableBuilder<int>(
          valueListenable: _frameIndex,
          builder: (_, index, _) => RawImage(
            image: clip.frameAt(index),
            width: box,
            height: box,
            fit: BoxFit.contain,
          ),
        );
      },
    );
  }

  Widget _nativeFallback() {
    return Lottie.network(
      widget.lottieUrl,
      width: widget.size,
      height: widget.size,
      fit: BoxFit.contain,
      frameRate: FrameRate.max,
      animate: widget.animate,
      repeat: widget.repeat,
      onLoaded: (composition) =>
          _scheduleFallbackCompletion(composition.duration),
      errorBuilder: (context, _, _) => _staticFallback(widget.size ?? 96.0),
    );
  }

  Widget _staticFallback(double box) {
    final url = widget.fallbackUrl ?? '';
    if (url.isEmpty) {
      return widget.shimmer
          ? LottieShimmer(size: box)
          : SizedBox(width: box, height: box);
    }
    return CachedNetworkImage(
      imageUrl: url,
      width: box,
      height: box,
      fit: BoxFit.contain,
      memCacheWidth: widget.memCacheWidth,
      fadeInDuration: const Duration(milliseconds: 120),
      placeholder: (_, _) => LottieShimmer(size: box),
      errorWidget: (_, _, _) => SizedBox(width: box, height: box),
    );
  }
}

class LottieImage extends StatelessWidget {
  final String? url;
  final String? lottieUrl;
  final double? size;
  final int? memCacheWidth;
  final bool shimmer;
  final bool eager;
  final bool animate;
  final bool repeat;
  final VoidCallback? onCompleted;

  const LottieImage({
    super.key,
    this.url,
    this.lottieUrl,
    this.size,
    this.memCacheWidth,
    this.shimmer = true,
    this.eager = false,
    this.animate = true,
    this.repeat = true,
    this.onCompleted,
  });

  @override
  Widget build(BuildContext context) {
    if (lottieUrl != null && lottieUrl!.isNotEmpty) {
      return LottiePlayer(
        lottieUrl: lottieUrl!,
        fallbackUrl: url,
        size: size,
        memCacheWidth: memCacheWidth,
        shimmer: shimmer,
        eager: eager,
        animate: animate,
        repeat: repeat,
        onCompleted: onCompleted,
      );
    }
    return _static();
  }

  Widget _static() {
    final src = url ?? '';
    if (src.isEmpty) return SizedBox(width: size, height: size);
    return CachedNetworkImage(
      imageUrl: src,
      width: size,
      height: size,
      fit: BoxFit.contain,
      memCacheWidth: memCacheWidth,
      fadeInDuration: const Duration(milliseconds: 120),
      placeholder: (_, _) => shimmer
          ? LottieShimmer(size: size)
          : SizedBox(width: size, height: size),
      errorWidget: (_, _, _) => SizedBox(width: size, height: size),
    );
  }
}

class LottieShimmer extends StatefulWidget {
  final double? size;

  const LottieShimmer({super.key, this.size});

  @override
  State<LottieShimmer> createState() => _LottieShimmerState();
}

class _LottieShimmerState extends State<LottieShimmer>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 950),
  )..repeat(reverse: true);

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

  @override
  Widget build(BuildContext context) {
    final base = Theme.of(context).colorScheme.onSurfaceVariant;
    final box = widget.size;
    final inset = box == null ? 2.0 : box * 0.06;
    final radius = box == null ? 8.0 : (box * 0.2).clamp(6.0, 26.0);
    return SizedBox(
      width: box,
      height: box,
      child: Padding(
        padding: EdgeInsets.all(inset),
        child: AnimatedBuilder(
          animation: _controller,
          builder: (context, _) => DecoratedBox(
            decoration: BoxDecoration(
              color: base.withValues(alpha: 0.12 + 0.16 * _controller.value),
              borderRadius: BorderRadius.circular(radius),
            ),
            child: const SizedBox.expand(),
          ),
        ),
      ),
    );
  }
}
