import 'dart:async';

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

import '../../common.dart';
import '../../consts.dart';
import '../../models/platform_model.dart';
import '../../models/state_model.dart';

import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
    if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart';

/// Startup probe: renders one frame through a 1x1 external texture and
/// records in `texture-render-health` whether the engine consumed it, so a
/// broken environment is detected before the first session goes black.
class TextureRenderProbe extends StatefulWidget {
  const TextureRenderProbe({Key? key}) : super(key: key);

  @override
  State<TextureRenderProbe> createState() => _TextureRenderProbeState();
}

class _TextureRenderProbeState extends State<TextureRenderProbe> {
  static bool _ranThisLaunch = false;
  final _renderer = TextureRgbaRenderer();
  int _textureId = -1;
  int _textureKey = -1;
  int _ptr = 0;
  Timer? _timer;
  int _ticks = 0;
  bool _sawTimings = false;
  bool _wasEffectiveOn = false;
  DateTime? _lastTimings;
  DateTime? _firstPush;

  @override
  void initState() {
    super.initState();
    if (_ranThisLaunch || isWeb || !isDesktop) return;
    _ranThisLaunch = true;
    if (bind.isIncomingOnly()) return;
    // An old plugin without the consumed counter cannot be judged, and a
    // recorded raster-stall means compositing a texture may hang this window.
    if (!bind.mainTextureRenderProbeSupported()) return;
    if (bind
        .mainGetLocalOption(key: kOptionTextureRenderHealth)
        .startsWith('failed-raster-stall')) {
      return;
    }
    _wasEffectiveOn = bind.mainGetUseTextureRender();
    // Only probe after the window has really rendered a frame: a hidden
    // window (silent/tray start) must not record a false failure.
    SchedulerBinding.instance.addTimingsCallback(_onTimings);
    Future.delayed(const Duration(seconds: 5), () {
      if (!_sawTimings) {
        SchedulerBinding.instance.removeTimingsCallback(_onTimings);
        _finish(null);
      }
    });
  }

  void _onTimings(List<FrameTiming> timings) {
    _lastTimings = DateTime.now();
    if (_sawTimings) return;
    _sawTimings = true;
    _start();
  }

  void _start() async {
    if (!mounted) return;
    _textureKey = bind.getNextTextureKey();
    final id = await _renderer.createTexture(_textureKey);
    if (!mounted || id == -1) {
      _finish(!mounted ? null : false);
      return;
    }
    _ptr = await _renderer.getTexturePtr(_textureKey);
    if (!mounted || _ptr <= 0) {
      _finish(!mounted ? null : false);
      return;
    }
    setState(() => _textureId = id);
    _timer = Timer.periodic(const Duration(milliseconds: 100), (_) {
      _ticks += 1;
      _firstPush ??= DateTime.now();
      bind.mainPushTextureProbeFrame(ptr: _ptr);
      final consumed = bind.mainGetTextureProbeConsumed(ptr: _ptr) > 0;
      // "Consumed" advances inside the plugin callback, before the GL/Metal
      // upload; only a frame timing after the push proves a completed frame.
      final frameCompleted = consumed &&
          _lastTimings != null &&
          _lastTimings!.isAfter(_firstPush!);
      if (frameCompleted) {
        _finish(true);
      } else if (_ticks >= 10) {
        if (consumed) {
          _finish(null);
          return;
        }
        // Only a window that is visibly compositing can prove a failure.
        final lifecycle = SchedulerBinding.instance.lifecycleState;
        final active =
            lifecycle == null || lifecycle == AppLifecycleState.resumed;
        final timingsFresh = _lastTimings != null &&
            DateTime.now().difference(_lastTimings!) <
                const Duration(milliseconds: 1500);
        _finish(!stateGlobal.isMinimized && active && timingsFresh
            ? false
            : null);
      }
    });
  }

  void _finish(bool? ok) {
    _timer?.cancel();
    _timer = null;
    SchedulerBinding.instance.removeTimingsCallback(_onTimings);
    if (ok != null) {
      final old = bind.mainGetLocalOption(key: kOptionTextureRenderHealth);
      if (ok) {
        // This rgba probe disproves only the rgba black-texture class: gpu
        // failures and raster stalls clear via the option toggle alone.
        final clearable = old.isEmpty ||
            old.startsWith('failed-probe') ||
            old.startsWith('failed-watchdog-rgba');
        if (clearable) {
          bind.mainSetLocalOption(key: kOptionTextureRenderHealth, value: 'ok');
        }
      } else if (!old.startsWith('failed')) {
        debugPrint('texture render probe failed, disabling texture rendering');
        bind.mainSetLocalOption(
            key: kOptionTextureRenderHealth, value: 'failed-probe');
        if (_wasEffectiveOn) {
          showToast(translate('texture-render-fallback-tip'));
        }
      }
    }
    if (_textureKey != -1) {
      _renderer.closeTexture(_textureKey);
      _textureKey = -1;
    }
    _ptr = 0;
    if (mounted && _textureId != -1) {
      setState(() => _textureId = -1);
    } else {
      _textureId = -1;
    }
  }

  @override
  void dispose() {
    _timer?.cancel();
    SchedulerBinding.instance.removeTimingsCallback(_onTimings);
    if (_textureKey != -1) {
      _renderer.closeTexture(_textureKey);
      _textureKey = -1;
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (_textureId == -1) return const SizedBox.shrink();
    // Must actually composite for the engine to sample the texture; the
    // pushed pixel is fully transparent.
    return IgnorePointer(
      child: SizedBox(
          width: 1, height: 1, child: Texture(textureId: _textureId)),
    );
  }
}
