/// Terminal game loop that ties engine ticks, raw input, and CLI rendering together.
library;

import 'dart:async';
import 'dart:io';

import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_dart/wolf_3d_input.dart';
import 'package:wolf_3d_dart/wolf_3d_renderer.dart';

/// Runs the Wolf3D engine inside a terminal using CLI-specific renderers.
///
/// The loop owns raw-stdin handling, renderer switching, terminal size checks,
/// and frame pacing. It expects [engine.input] to be a [CliInput] instance so
/// raw key bytes can be queued directly into the engine input adapter.
class CliGameLoop {
  CliGameLoop({
    required this.engine,
    required this.onExit,
    this.persistence,
    this.initialSettings,
  }) : input = engine.input is CliInput
           ? engine.input as CliInput
           : throw ArgumentError.value(
               engine.input,
               'engine.input',
               'CliGameLoop requires a CliInput instance.',
             ),

       primaryRenderer = SixelRenderer(),
       secondaryRenderer = AsciiRenderer(
         mode: AsciiRendererMode.terminalAnsi,
       ) {
    _renderer = primaryRenderer;
  }

  final WolfEngine engine;
  final CliRendererBackend primaryRenderer;
  final CliRendererBackend secondaryRenderer;
  final CliInput input;
  final void Function(int code) onExit;
  final RendererSettingsPersistence? persistence;
  final WolfRendererSettings? initialSettings;

  final Stopwatch _stopwatch = Stopwatch();
  final Stream<List<int>> _stdinStream = stdin.asBroadcastStream();
  late CliRendererBackend _renderer;
  StreamSubscription<List<int>>? _stdinSubscription;
  Timer? _timer;
  bool _isRunning = false;
  bool _isSixelAvailable = false;
  Duration _lastTick = Duration.zero;

  /// Starts terminal probing, enters raw input mode, and begins the frame timer.
  Future<void> start() async {
    if (_isRunning) {
      return;
    }

    if (primaryRenderer is SixelRenderer) {
      final sixel = primaryRenderer as SixelRenderer;
      sixel.isSixelSupported = await SixelRenderer.checkTerminalSixelSupport(
        inputStream: _stdinStream,
      );
      _isSixelAvailable = sixel.isSixelSupported;
    } else {
      _isSixelAvailable = false;
    }

    final Set<WolfRendererMode> supportedModes = <WolfRendererMode>{
      WolfRendererMode.ascii,
      if (_isSixelAvailable) WolfRendererMode.sixel,
    };
    engine.setRendererCapabilities(
      WolfRendererCapabilities(
        supportedModes: supportedModes,
        supportsAsciiThemes: true,
        supportsFpsCounter: true,
      ),
    );

    if (initialSettings != null) {
      engine.updateRendererSettings(initialSettings!);
    } else if (_isSixelAvailable) {
      engine.updateRendererSettings(
        engine.rendererSettings.copyWith(mode: WolfRendererMode.sixel),
      );
    }

    _syncRendererFromEngine();

    if (stdin.hasTerminal) {
      try {
        stdin.echoMode = false;
        stdin.lineMode = false;
      } catch (_) {
        // Keep running without raw mode when stdin is not mutable.
      }
    }

    // Disable Sixel scrolling mode so frames overwrite in-place.
    stdout.write('\x1b[?80l\x1b[?25l\x1b[2J');

    _stdinSubscription = _stdinStream.listen(_handleInput);
    _stopwatch.start();
    _timer = Timer.periodic(const Duration(milliseconds: 33), _tick);
    _isRunning = true;
  }

  /// Stops the timer, unsubscribes from stdin, and restores terminal settings.
  void stop() {
    if (!_isRunning) {
      return;
    }

    _timer?.cancel();
    _timer = null;
    _stdinSubscription?.cancel();
    _stdinSubscription = null;

    if (_stopwatch.isRunning) {
      _stopwatch.stop();
    }

    if (stdin.hasTerminal) {
      try {
        stdin.echoMode = true;
        stdin.lineMode = true;
      } catch (_) {
        // Ignore cleanup failures if stdin is no longer a mutable TTY.
      }
    }

    if (stdout.hasTerminal) {
      // Restore scrolling Sixel mode and cursor visibility.
      stdout.write('\x1b[0m\x1b[?80h\x1b[?25h');
    }

    _isRunning = false;
  }

  void _handleInput(List<int> bytes) {
    // Keep q and Ctrl+C as hard exits; ESC is now menu-back input.
    if (bytes.contains(113) || bytes.contains(3)) {
      stop();
      onExit(0);
      return;
    }

    if (input.matchesRendererToggleShortcut(bytes)) {
      engine.cycleRendererMode();
      _syncRendererFromEngine();
      unawaited(persistence?.save(engine.rendererSettings));
      stdout.write('\x1b[2J\x1b[H');
      return;
    }

    if (input.matchesAsciiThemeCycleShortcut(bytes)) {
      engine.cycleAsciiTheme();
      _syncRendererFromEngine();
      unawaited(persistence?.save(engine.rendererSettings));
      return;
    }

    input.handleKey(bytes);
  }

  void _syncRendererFromEngine() {
    final CliRendererBackend previousRenderer = _renderer;
    final WolfRendererMode mode = engine.rendererSettings.mode;
    if (mode == WolfRendererMode.sixel && _isSixelAvailable) {
      _renderer = primaryRenderer;
    } else {
      _renderer = secondaryRenderer;
    }

    final AsciiTheme theme =
        engine.rendererSettings.asciiThemeId ==
            WolfRendererSettings.asciiThemeQuadrant
        ? AsciiThemes.quadrant
        : AsciiThemes.blocks;
    if (primaryRenderer is AsciiRenderer) {
      (primaryRenderer as AsciiRenderer).activeTheme = theme;
    }
    if (secondaryRenderer is AsciiRenderer) {
      (secondaryRenderer as AsciiRenderer).activeTheme = theme;
    }

    engine.showFpsCounter = engine.rendererSettings.fpsCounterEnabled;

    if (!identical(previousRenderer, _renderer) && stdout.hasTerminal) {
      // Clear stale frame content when switching backend output modes.
      stdout.write('\x1b[2J\x1b[H');
    }
  }

  void _tick(Timer timer) {
    if (!_isRunning) {
      return;
    }

    // Apply renderer changes made via in-game menus or settings updates.
    _syncRendererFromEngine();

    if (stdout.hasTerminal) {
      final int cols = stdout.terminalColumns;
      final int rows = stdout.terminalLines;
      if (!_renderer.prepareTerminalFrame(
        engine,
        columns: cols,
        rows: rows,
      )) {
        // Size warnings are rendered instead of running the simulation so the
        // game does not keep advancing while the user resizes the terminal.
        stdout.write('\x1b[2J\x1b[H');
        stdout.write(
          _renderer.buildTerminalSizeWarning(columns: cols, rows: rows),
        );

        _lastTick = _stopwatch.elapsed;
        return;
      }
    }

    final Duration currentTick = _stopwatch.elapsed;
    final Duration elapsed = currentTick - _lastTick;
    _lastTick = currentTick;

    stdout.write('\x1b[H');

    engine.tick(elapsed);
    stdout.write(_renderer.render(engine));
  }
}
