/// Shared Flutter renderer shell for driving the Wolf3D engine from a widget tree.
library;

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';

typedef PresentFrameAction = void Function(VoidCallback onComplete);

/// Base widget for renderers that present frames from a [WolfEngine].
abstract class BaseWolfRenderer extends StatefulWidget {
  /// Engine instance that owns world state and the shared framebuffer.
  final WolfEngine engine;

  /// Optional key handler invoked by the focused renderer shell.
  final void Function(KeyEvent event)? onKeyEvent;

  /// Creates a renderer bound to [engine].
  const BaseWolfRenderer({
    required this.engine,
    this.onKeyEvent,
    super.key,
  });
}

/// Base [State] implementation that provides a ticker-driven render loop.
abstract class BaseWolfRendererState<T extends BaseWolfRenderer>
    extends State<T>
    with SingleTickerProviderStateMixin {
  /// Per-frame ticker used to advance the engine and request renders.
  late final Ticker gameLoop;

  /// Focus node used by the enclosing [KeyboardListener].
  final FocusNode focusNode = FocusNode();

  Duration _lastTick = Duration.zero;
  bool _isPresenting = false;
  PresentFrameAction? _queuedPresentAction;

  int _tickWindowCount = 0;
  int _presentWindowCount = 0;
  int _coalescedPresentWindowCount = 0;
  int _tickWindowMicrosTotal = 0;
  int _renderWindowMicrosTotal = 0;
  int _presentWindowMicrosTotal = 0;

  static const int _perfLogWindowFrames = 180;

  @override
  void initState() {
    super.initState();
    gameLoop = createTicker(_tick)..start();
    focusNode.requestFocus();
  }

  void _tick(Duration elapsed) {
    if (!widget.engine.isInitialized) return;

    if (_lastTick == Duration.zero) {
      _lastTick = elapsed;
      return;
    }

    final Duration delta = elapsed - _lastTick;
    _lastTick = elapsed;

    final Stopwatch tickStopwatch = Stopwatch()..start();
    widget.engine.tick(delta);
    tickStopwatch.stop();
    _tickWindowMicrosTotal += tickStopwatch.elapsedMicroseconds;
    _tickWindowCount++;

    performRender();

    _maybeLogPerfWindow();
  }

  @override
  void dispose() {
    gameLoop.dispose();
    focusNode.dispose();
    super.dispose();
  }

  /// Renders the latest engine state into the concrete renderer's output type.
  void performRender();

  /// Schedules presentation work while coalescing to the latest requested frame.
  @protected
  void scheduleLatestPresent(PresentFrameAction action) {
    if (_isPresenting) {
      _queuedPresentAction = action;
      _coalescedPresentWindowCount++;
      return;
    }

    _runPresentAction(action);
  }

  /// Records CPU-side rendering stage duration in microseconds.
  @protected
  void recordRenderStageMicros(int microseconds) {
    _renderWindowMicrosTotal += microseconds;
  }

  void _runPresentAction(PresentFrameAction action) {
    _isPresenting = true;
    final Stopwatch presentStopwatch = Stopwatch()..start();
    bool isCompleted = false;

    void onComplete() {
      if (isCompleted) {
        return;
      }
      isCompleted = true;
      presentStopwatch.stop();
      _presentWindowMicrosTotal += presentStopwatch.elapsedMicroseconds;
      _presentWindowCount++;
      _isPresenting = false;

      if (!mounted) {
        return;
      }

      final PresentFrameAction? nextAction = _queuedPresentAction;
      _queuedPresentAction = null;
      if (nextAction != null) {
        scheduleMicrotask(() => _runPresentAction(nextAction));
      }
    }

    action(onComplete);
  }

  void _maybeLogPerfWindow() {
    if (!kDebugMode || _tickWindowCount < _perfLogWindowFrames) {
      return;
    }

    final double avgTickMs = _tickWindowMicrosTotal / _tickWindowCount / 1000.0;
    final double avgRenderMs =
        _renderWindowMicrosTotal / _tickWindowCount / 1000.0;
    final double avgPresentMs = _presentWindowCount > 0
        ? _presentWindowMicrosTotal / _presentWindowCount / 1000.0
        : 0.0;
    debugPrint(
      'Renderer perf window ($_tickWindowCount frames): '
      'avg tick ${avgTickMs.toStringAsFixed(2)}ms, '
      'avg render ${avgRenderMs.toStringAsFixed(2)}ms, '
      'avg present ${avgPresentMs.toStringAsFixed(2)}ms, '
      'coalesced $_coalescedPresentWindowCount',
    );

    _tickWindowCount = 0;
    _presentWindowCount = 0;
    _coalescedPresentWindowCount = 0;
    _tickWindowMicrosTotal = 0;
    _renderWindowMicrosTotal = 0;
    _presentWindowMicrosTotal = 0;
  }

  /// Builds the visible viewport widget for the latest rendered frame.
  Widget buildViewport(BuildContext context);

  /// Background color used by the surrounding scaffold.
  Color get scaffoldColor;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: scaffoldColor,
      body: KeyboardListener(
        focusNode: focusNode,
        autofocus: true,
        onKeyEvent: (event) {
          widget.onKeyEvent?.call(event);
        },
        child: Center(
          child: buildViewport(context),
        ),
      ),
    );
  }
}
