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

import '../../../shared/utils/app_logger.dart';

const _tag = 'FrameJank';


@immutable
class FrameDiag {

  final double fps;


  final double lastBuildMs;


  final double lastRasterMs;


  final int jankCount;

  const FrameDiag({
    required this.fps,
    required this.lastBuildMs,
    required this.lastRasterMs,
    required this.jankCount,
  });

  static const zero = FrameDiag(
    fps: 0,
    lastBuildMs: 0,
    lastRasterMs: 0,
    jankCount: 0,
  );
}


class FrameJankMonitor {
  FrameJankMonitor({this.jankThreshold = const Duration(milliseconds: 32)});


  final Duration jankThreshold;

  final ValueNotifier<FrameDiag> diag = ValueNotifier<FrameDiag>(
    FrameDiag.zero,
  );

  TimingsCallback? _callback;
  int _jankCount = 0;
  int _windowFrames = 0;
  final Stopwatch _windowClock = Stopwatch()..start();
  double _fps = 0;

  void start() {
    if (kReleaseMode || _callback != null) return;
    final cb = _onTimings;
    _callback = cb;
    SchedulerBinding.instance.addTimingsCallback(cb);
  }

  void _onTimings(List<FrameTiming> timings) {
    if (timings.isEmpty) return;
    for (final t in timings) {
      if (t.buildDuration >= jankThreshold ||
          t.rasterDuration >= jankThreshold) {
        _jankCount++;
        AppLogger.warn(
          _tag,
          'janky frame build=${_ms(t.buildDuration)} '
          'raster=${_ms(t.rasterDuration)} total=${_ms(t.totalSpan)}',
        );
      }
    }

    _windowFrames += timings.length;
    final elapsedMs = _windowClock.elapsedMilliseconds;
    if (elapsedMs >= 500) {
      _fps = _windowFrames * 1000 / elapsedMs;
      _windowFrames = 0;
      _windowClock.reset();
    }

    final last = timings.last;
    diag.value = FrameDiag(
      fps: _fps,
      lastBuildMs: last.buildDuration.inMicroseconds / 1000,
      lastRasterMs: last.rasterDuration.inMicroseconds / 1000,
      jankCount: _jankCount,
    );
  }

  void dispose() {
    final cb = _callback;
    if (cb != null) {
      SchedulerBinding.instance.removeTimingsCallback(cb);
      _callback = null;
    }
    diag.dispose();
  }

  static String _ms(Duration d) =>
      '${(d.inMicroseconds / 1000).toStringAsFixed(1)}ms';
}
