import 'dart:developer';
import 'dart:math' as math;

import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
import 'package:wolf_3d_dart/wolf_3d_input.dart';

/// The core orchestration class for the Wolfenstein 3D engine.
///
/// [WolfEngine] manages the game loop, level transitions, entity updates,
/// and collision detection. It serves as the bridge between raw data,
/// input systems, and the world state.
class WolfEngine {
  WolfEngine({
    WolfensteinData? data,
    List<WolfensteinData>? availableGames,
    this.startingEpisode,
    required this.onGameWon,
    required this.input,
    required this.frameBuffer,
    this.difficulty,
    this.menuBackgroundRgb = 0x890000,
    this.menuPanelRgb = 0x590002,
    this.onMenuExit,
    this.onQuit,
    this.onGameSelected,
    this.onEpisodeSelected,
    this.onRendererSettingsChanged,
    WolfRendererCapabilities? rendererCapabilities,
    WolfRendererSettings? rendererSettings,
    EngineAudio? engineAudio,
  }) : rendererCapabilities =
           rendererCapabilities ??
           const WolfRendererCapabilities(
             supportedModes: <WolfRendererMode>{
               WolfRendererMode.software,
             },
             supportsFpsCounter: true,
           ),
       rendererSettings = rendererSettings ?? const WolfRendererSettings(),
       assert(
         data != null || (availableGames != null && availableGames.isNotEmpty),
         'Provide either data or a non-empty availableGames list.',
       ),
       _availableGames = availableGames ?? <WolfensteinData>[data!],
       audio = engineAudio ?? CliSilentAudio(),
       doorManager = DoorManager(
         onPlaySound: (effect) => engineAudio?.playSoundEffect(effect),
       ),
       pushwallManager = PushwallManager(
         onPlaySound: (effect) => engineAudio?.playSoundEffect(effect),
       ) {
    if (_availableGames.isEmpty) {
      throw StateError('WolfEngine requires at least one game data set.');
    }
    menuManager.menuBackgroundRgb = menuBackgroundRgb;
    _normalizeRendererSettings();
    _syncRendererMenuModel();
  }

  /// Total milliseconds elapsed since the engine was initialized.
  int _timeAliveMs = 0;

  /// The discovered game data sets available for selection.
  final List<WolfensteinData> _availableGames;

  /// Available game data sets for menu rendering and selection.
  List<WolfensteinData> get availableGames =>
      List.unmodifiable(_availableGames);

  int _currentGameIndex = 0;

  /// The currently active game data set.
  WolfensteinData get data => _availableGames[_currentGameIndex];

  /// Desired menu background color in 24-bit RGB.
  final int menuBackgroundRgb;

  /// Desired menu panel color in 24-bit RGB.
  final int menuPanelRgb;

  /// The active difficulty level, affecting enemy spawning and behavior.
  Difficulty? difficulty;

  /// Menu state owner for difficulty-selection navigation and edge detection.
  final MenuManager menuManager = MenuManager();

  /// Elapsed engine lifetime in milliseconds.
  int get timeAliveMs => _timeAliveMs;

  /// Exponential moving average of rendered frames per second.
  double _smoothedFps = 0.0;

  /// Current smoothed FPS, suitable for lightweight on-screen diagnostics.
  double get fps => _smoothedFps;

  /// Whether renderers should draw the FPS counter overlay.
  bool showFpsCounter = false;

  /// The episode index where the game session begins.
  final int? startingEpisode;

  /// Handles music and sound effect playback.
  late final EngineAudio audio;

  /// Callback triggered when the final level of an episode is completed.
  final void Function() onGameWon;

  /// Callback triggered when backing out of the top-level menu.
  final void Function()? onMenuExit;

  /// Callback triggered when the player explicitly selects QUIT.
  final void Function()? onQuit;

  /// Callback triggered whenever the active game changes from menu flow.
  final void Function(WolfensteinData game)? onGameSelected;

  /// Callback triggered when episode selection changes; `null` means cleared.
  final void Function(int? episodeIndex)? onEpisodeSelected;

  /// Callback triggered whenever renderer settings are updated by the engine.
  final void Function(WolfRendererSettings settings)? onRendererSettingsChanged;

  /// Host-reported mode/effect capabilities that drive menu visibility.
  WolfRendererCapabilities rendererCapabilities;

  /// Engine-owned renderer settings shared between menus and host shortcuts.
  WolfRendererSettings rendererSettings;

  // --- State Managers ---

  /// Manages the state and animation of doors throughout the level.
  final DoorManager doorManager;

  /// Polls and processes raw user input into actionable engine commands.
  final Wolf3dInput input;

  /// The shared render target used by all renderer frontends.
  FrameBuffer frameBuffer;

  /// Handles the detection and movement of secret "Pushwalls".
  final PushwallManager pushwallManager;

  // --- World State ---

  /// The player's current position, stats, and inventory.
  ///
  /// This starts with a safe placeholder so menu-mode rendering/input can
  /// access player fields (for example damage flash state) before a map is
  /// loaded. `_loadLevel()` replaces it with the true map spawn.
  Player player = Player(x: 1.5, y: 1.5, angle: 0.0);

  /// The mutable 64x64 grid representing the current world.
  /// This grid is modified in real-time by doors and pushwalls.
  late SpriteMap currentLevel;

  /// The static level data source used for reloading or reference.
  late WolfLevel activeLevel;

  /// The static object layer (spawns, markers, triggers) for the active level.
  late SpriteMap _objectLevel;

  /// All dynamic entities currently in the level (Enemies, Pickups).
  List<Entity> entities = [];

  /// Per-tile area numbers derived from map plane 0 (or fallback fill).
  late SpriteMap _areaGrid;

  /// True for areas currently connected to the player's area.
  List<bool> _areasByPlayer = [];

  /// Total number of known area indices in the active level.
  int _areaCount = 0;

  /// Last tile processed for patrol marker routing, keyed by enemy debug id.
  final Map<int, ({int x, int y})> _lastPatrolTileByEnemy = {};

  int _currentEpisodeIndex = 0;
  bool _isMenuOverlayVisible = false;
  bool _hasActiveSession = false;

  bool _isPlayerMovingFast = false;
  int _currentLevelIndex = 0;

  /// Stores the previous level index when entering a secret floor,
  /// allowing the player to return to the correct spot.
  int? _returnLevelIndex;

  bool isInitialized = false;

  /// Tracks the last time a sound wave was emitted to prevent BFS spam.
  int _lastAcousticAlertTime = 0;

  /// Initializes the engine, sets the starting episode, and loads the first level.
  void init() {
    _currentGameIndex = 0;
    audio.activeGame = data;
    onGameSelected?.call(data);

    _currentEpisodeIndex = startingEpisode ?? 0;
    _currentLevelIndex = 0;

    menuManager.beginSelectionFlow(
      gameCount: _availableGames.length,
      initialGameIndex: _currentGameIndex,
      initialEpisodeIndex: _currentEpisodeIndex,
      initialDifficulty: difficulty,
      hasResumableGame: false,
      initialGameIsRetail: data.version == GameVersion.retail,
    );

    if (_availableGames.length == 1) {
      menuManager.setSelectedGameIndex(0, 1);
      onEpisodeSelected?.call(null);
    }

    if (difficulty != null) {
      _loadLevel(preservePlayerState: false);
      _hasActiveSession = true;
    }

    isInitialized = true;
  }

  /// Whether a menu overlay is currently blocking gameplay updates.
  bool get isMenuOpen => difficulty == null || _isMenuOverlayVisible;

  /// Whether the current gameplay session can be resumed from the main menu.
  bool get canResumeGame => _hasActiveSession;

  /// Replaces the shared framebuffer when dimensions change.
  void setFrameBuffer(int width, int height) {
    if (width <= 0 || height <= 0) {
      throw ArgumentError('FrameBuffer dimensions must be greater than zero.');
    }
    if (frameBuffer.width == width && frameBuffer.height == height) {
      return;
    }
    frameBuffer = FrameBuffer(width, height);
    log("[DEBUG] FrameBuffer resized to ${width}x$height");
  }

  /// Replaces host capability metadata and refreshes menu visibility.
  void setRendererCapabilities(WolfRendererCapabilities capabilities) {
    rendererCapabilities = capabilities;
    _normalizeRendererSettings();
    _syncRendererMenuModel();
  }

  /// Applies [settings], notifies host, and refreshes renderer menu visibility.
  void updateRendererSettings(WolfRendererSettings settings) {
    rendererSettings = settings;
    _normalizeRendererSettings();
    showFpsCounter = rendererSettings.fpsCounterEnabled;
    _syncRendererMenuModel();
    onRendererSettingsChanged?.call(rendererSettings);
  }

  /// Cycles to the next available renderer mode supported by the host.
  void cycleRendererMode() {
    final List<WolfRendererMode> available = rendererCapabilities.supportedModes
        .toList(growable: false);
    if (available.isEmpty) {
      return;
    }

    final int current = available.indexOf(rendererSettings.mode);
    final int next = current == -1 ? 0 : (current + 1) % available.length;
    updateRendererSettings(rendererSettings.copyWith(mode: available[next]));
  }

  /// Cycles the ASCII character set when the host supports ASCII rendering.
  void cycleAsciiTheme() {
    if (!rendererCapabilities.supportsAsciiThemes) {
      return;
    }
    updateRendererSettings(rendererSettings.cycleAsciiTheme());
  }

  /// Toggles CRT/post-processing effects for hardware renderer hosts.
  void toggleHardwareEffects() {
    if (!rendererCapabilities.supportsHardwareEffects) {
      return;
    }
    updateRendererSettings(
      rendererSettings.copyWith(
        hardwareEffectsEnabled: !rendererSettings.hardwareEffectsEnabled,
      ),
    );
  }

  /// Toggles the shared FPS counter setting.
  void toggleFpsCounter() {
    if (!rendererCapabilities.supportsFpsCounter) {
      return;
    }
    updateRendererSettings(
      rendererSettings.copyWith(
        fpsCounterEnabled: !rendererSettings.fpsCounterEnabled,
      ),
    );
  }

  void _normalizeRendererSettings() {
    final Set<WolfRendererMode> supported = rendererCapabilities.supportedModes;
    if (supported.isEmpty) {
      rendererSettings = rendererSettings.copyWith(
        mode: WolfRendererMode.software,
      );
    } else if (!supported.contains(rendererSettings.mode)) {
      rendererSettings = rendererSettings.copyWith(mode: supported.first);
    }

    if (!WolfRendererSettings.asciiThemeIds.contains(
      rendererSettings.asciiThemeId,
    )) {
      rendererSettings = rendererSettings.copyWith(
        asciiThemeId: WolfRendererSettings.asciiThemeBlocks,
      );
    }

    if (!rendererCapabilities.supportsHardwareEffects &&
        rendererSettings.hardwareEffectsEnabled) {
      rendererSettings = rendererSettings.copyWith(
        hardwareEffectsEnabled: false,
      );
    }

    if (!rendererCapabilities.supportsFpsCounter &&
        rendererSettings.fpsCounterEnabled) {
      rendererSettings = rendererSettings.copyWith(fpsCounterEnabled: false);
    }
    showFpsCounter = rendererSettings.fpsCounterEnabled;
  }

  void _syncRendererMenuModel() {
    final WolfRendererMode activeMode = rendererSettings.mode;
    final List<WolfMenuRendererOptionEntry> options =
        <WolfMenuRendererOptionEntry>[];

    if (activeMode == WolfRendererMode.ascii &&
        rendererCapabilities.supportsAsciiThemes) {
      options.add(
        WolfMenuRendererOptionEntry(
          id: WolfRendererOptionId.asciiTheme,
          label: WolfRendererSettings.asciiThemeLabel(
            rendererSettings.asciiThemeId,
          ),
          isChecked: true,
        ),
      );
    }

    if (activeMode == WolfRendererMode.hardware &&
        rendererCapabilities.supportsHardwareEffects) {
      options.add(
        WolfMenuRendererOptionEntry(
          id: WolfRendererOptionId.hardwareEffects,
          label: 'EFFECTS',
          isChecked: rendererSettings.hardwareEffectsEnabled,
        ),
      );
    }

    if (rendererCapabilities.supportsFpsCounter) {
      options.add(
        WolfMenuRendererOptionEntry(
          id: WolfRendererOptionId.fpsCounter,
          label: 'FPS COUNTER',
          isChecked: rendererSettings.fpsCounterEnabled,
        ),
      );
    }

    final List<WolfMenuRendererEntry> rendererEntries =
        <WolfMenuRendererEntry>[];

    void addRenderer(WolfRendererMode mode) {
      if (!rendererCapabilities.supportsMode(mode)) {
        return;
      }
      rendererEntries.add(
        WolfMenuRendererEntry(
          mode: mode,
          label: mode.label,
          hasOptions: false,
          isEnabled: true,
          isChecked: rendererSettings.mode == mode,
        ),
      );
    }

    addRenderer(WolfRendererMode.hardware);
    addRenderer(WolfRendererMode.software);
    addRenderer(WolfRendererMode.sixel);
    addRenderer(WolfRendererMode.ascii);

    menuManager.setChangeViewEntries(rendererEntries);

    menuManager.setRendererOptionEntries(
      title: 'CUSTOMIZE ${activeMode.label}',
      entries: options,
    );
  }

  /// The primary heartbeat of the engine.
  ///
  /// Updates all world subsystems based on the [elapsed] time.
  /// This should be called once per frame by the host application.
  void tick(Duration delta) {
    if (!isInitialized) return;

    // Trust the incoming delta time natively
    _timeAliveMs += delta.inMilliseconds;
    if (delta.inMicroseconds > 0) {
      final double instantaneousFps = 1000000.0 / delta.inMicroseconds;
      if (_smoothedFps <= 0.0) {
        _smoothedFps = instantaneousFps;
      } else {
        _smoothedFps = (_smoothedFps * 0.88) + (instantaneousFps * 0.12);
      }
    }

    // 1. Process User Input
    input.update();
    final currentInput = input.currentInput;

    if (difficulty != null && !_isMenuOverlayVisible && currentInput.isBack) {
      _openPauseMenu();
      menuManager.absorbInputState(currentInput);
      return;
    }

    if (isMenuOpen) {
      menuManager.tickTransition(delta.inMilliseconds);
      _tickMenu(currentInput);
      return;
    }

    final inputResult = _processInputs(delta, currentInput);

    // 2. Update Environment
    doorManager.update(delta);
    pushwallManager.update(delta, currentLevel);

    // 3. Update Physics & Movement
    player.tick(delta);
    player.angle += inputResult.dAngle;

    if (player.angle < 0) player.angle += 2 * math.pi;
    if (player.angle >= 2 * math.pi) player.angle -= 2 * math.pi;

    final Coordinate2D validatedPos = _calculateValidatedPosition(
      player.position,
      inputResult.movement,
    );

    // RUN key does not exist yet in current input model. Keep canonical hit
    // chance in "walking" mode until true run-state input is implemented.
    _isPlayerMovingFast = false;

    player.x = validatedPos.x;
    player.y = validatedPos.y;

    _refreshPlayerConnectedAreas();

    // 4. Update Dynamic World
    _updateEntities(delta);

    player.updateWeapon(
      currentTime: _timeAliveMs,
      entities: entities,
      isWalkable: isWalkable,
    );
  }

  void _tickMenu(EngineInput input) {
    if (menuManager.isTransitioning) {
      menuManager.absorbInputState(input);
      return;
    }

    switch (menuManager.activeMenu) {
      case WolfMenuScreen.introSplash:
        menuManager.updateIntroSplash(input);
        break;
      case WolfMenuScreen.mainMenu:
        _tickMainMenu(input);
        break;
      case WolfMenuScreen.gameSelect:
        _tickGameSelectionMenu(input);
        break;
      case WolfMenuScreen.episodeSelect:
        _tickEpisodeSelectionMenu(input);
        break;
      case WolfMenuScreen.difficultySelect:
        _tickDifficultySelectionMenu(input);
        break;
      case WolfMenuScreen.changeView:
        _tickChangeViewMenu(input);
        break;
      case WolfMenuScreen.rendererOptions:
        _tickRendererOptionsMenu(input);
        break;
    }
  }

  void _tickMainMenu(EngineInput input) {
    final menuResult = menuManager.updateMainMenu(input);

    if (menuResult.goBack) {
      if (_isMenuOverlayVisible && _hasActiveSession) {
        _resumeGame();
      } else if (menuManager.canGoBackToGameSelection) {
        menuManager.startTransition(WolfMenuScreen.gameSelect);
      } else {
        _exitTopLevelMenu();
      }
      return;
    }

    switch (menuResult.selected) {
      case WolfMenuMainAction.newGame:
        _beginNewGameMenuFlow();
        break;
      case WolfMenuMainAction.endGame:
        _endCurrentGame();
        break;
      case WolfMenuMainAction.backToGame:
        _resumeGame();
        break;
      case WolfMenuMainAction.quit:
        _quitProgram();
        break;
      case WolfMenuMainAction.backToDemo:
        _exitTopLevelMenu();
        break;
      case WolfMenuMainAction.changeView:
        _syncRendererMenuModel();
        menuManager.showChangeViewMenu();
        break;
      case WolfMenuMainAction.sound:
      case WolfMenuMainAction.control:
      case WolfMenuMainAction.loadGame:
      case WolfMenuMainAction.saveGame:
      case WolfMenuMainAction.readThis:
      case WolfMenuMainAction.viewScores:
      case null:
        break;
    }
  }

  void _tickGameSelectionMenu(EngineInput input) {
    final menuResult = menuManager.updateGameSelection(
      input,
      gameCount: _availableGames.length,
    );

    if (menuResult.goBack) {
      menuManager.startTransition(WolfMenuScreen.mainMenu);
      return;
    }

    if (menuResult.selectedIndex != null) {
      _currentGameIndex = menuResult.selectedIndex!;
      audio.activeGame = data;
      onGameSelected?.call(data);
      _currentEpisodeIndex = 0;
      onEpisodeSelected?.call(null);
      menuManager.clearEpisodeSelection();
      menuManager.beginIntroSplash(
        includeRetailWarning: data.version == GameVersion.retail,
      );
    }
  }

  void _tickEpisodeSelectionMenu(EngineInput input) {
    final menuResult = menuManager.updateEpisodeSelection(
      input,
      episodeCount: data.episodes.length,
    );

    if (menuResult.goBack) {
      onEpisodeSelected?.call(null);
      menuManager.clearEpisodeSelection();
      menuManager.startTransition(WolfMenuScreen.mainMenu);
      return;
    }

    if (menuResult.selectedIndex != null) {
      _currentEpisodeIndex = menuResult.selectedIndex!;
      onEpisodeSelected?.call(_currentEpisodeIndex);
      menuManager.startTransition(WolfMenuScreen.difficultySelect);
    }
  }

  void _tickDifficultySelectionMenu(EngineInput input) {
    final menuResult = menuManager.updateDifficultySelection(input);
    if (menuResult.goBack) {
      menuManager.startTransition(WolfMenuScreen.episodeSelect);
      return;
    }

    if (menuResult.selected != null) {
      _startNewGameSession(menuResult.selected!);
    }
  }

  void _tickChangeViewMenu(EngineInput input) {
    final menuResult = menuManager.updateChangeViewMenu(input);
    if (menuResult.goBack) {
      menuManager.showMainMenu(hasResumableGame: _hasActiveSession);
      return;
    }

    if (menuResult.selectedMode != null) {
      updateRendererSettings(
        rendererSettings.copyWith(mode: menuResult.selectedMode),
      );
    }

    switch (menuResult.selectedOption) {
      case WolfRendererOptionId.asciiTheme:
        cycleAsciiTheme();
        break;
      case WolfRendererOptionId.hardwareEffects:
        toggleHardwareEffects();
        break;
      case WolfRendererOptionId.fpsCounter:
        toggleFpsCounter();
        break;
      case null:
        break;
    }
  }

  void _tickRendererOptionsMenu(EngineInput input) {
    final menuResult = menuManager.updateRendererOptionsMenu(input);
    if (menuResult.goBack) {
      _syncRendererMenuModel();
      menuManager.showChangeViewMenu();
      return;
    }

    switch (menuResult.selectedOption) {
      case WolfRendererOptionId.asciiTheme:
        cycleAsciiTheme();
        break;
      case WolfRendererOptionId.hardwareEffects:
        toggleHardwareEffects();
        break;
      case WolfRendererOptionId.fpsCounter:
        toggleFpsCounter();
        break;
      case null:
        break;
    }
  }

  void _beginNewGameMenuFlow() {
    onEpisodeSelected?.call(null);
    menuManager.clearEpisodeSelection();
    menuManager.startTransition(WolfMenuScreen.episodeSelect);
  }

  void _openPauseMenu() {
    if (!_hasActiveSession) {
      return;
    }
    _isMenuOverlayVisible = true;
    menuManager.showMainMenu(hasResumableGame: true);
  }

  void _resumeGame() {
    _isMenuOverlayVisible = false;
    menuManager.absorbInputState(input.currentInput);
  }

  void _startNewGameSession(Difficulty selectedDifficulty) {
    difficulty = selectedDifficulty;
    _currentLevelIndex = 0;
    _returnLevelIndex = null;
    _isMenuOverlayVisible = false;
    _loadLevel(preservePlayerState: false);
    _hasActiveSession = true;
  }

  void _endCurrentGame() {
    difficulty = null;
    _isMenuOverlayVisible = false;
    _hasActiveSession = false;
    _returnLevelIndex = null;
    onEpisodeSelected?.call(null);
    menuManager.showMainMenu(hasResumableGame: false);
  }

  void _exitTopLevelMenu() {
    if (onMenuExit != null) {
      onMenuExit!.call();
      return;
    }
    onGameWon();
  }

  void _quitProgram() {
    if (onQuit != null) {
      onQuit!.call();
      return;
    }
    _exitTopLevelMenu();
  }

  /// Wipes the current world state and builds a new floor from map data.
  void _loadLevel({required bool preservePlayerState}) {
    entities.clear();
    _lastPatrolTileByEnemy.clear();

    final episode = data.episodes[_currentEpisodeIndex];
    activeLevel = episode.levels[_currentLevelIndex];

    // Create a mutable copy of the wall grid so pushwalls can modify it
    currentLevel = List.generate(64, (y) => List.from(activeLevel.wallGrid[y]));
    _areaGrid = List.generate(64, (y) => List.from(activeLevel.areaGrid[y]));
    _objectLevel = activeLevel.objectGrid;

    doorManager.initDoors(currentLevel);
    pushwallManager.initPushwalls(currentLevel, _objectLevel);
    audio.playLevelMusic(activeLevel.music);

    // Spawn Player and Entities from the Object Grid
    bool playerSpawned = false;
    for (int y = 0; y < 64; y++) {
      for (int x = 0; x < 64; x++) {
        int objId = _objectLevel[y][x];

        // Map IDs 19-22 are Reserved for Player Starts
        if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) {
          playerSpawned = true;
          if (preservePlayerState) {
            player
              ..x = x + 0.5
              ..y = y + 0.5
              ..angle = MapObject.getAngle(objId);
          } else {
            player = Player(
              x: x + 0.5,
              y: y + 0.5,
              angle: MapObject.getAngle(objId),
            );
          }
        } else {
          Entity? newEntity = EntityRegistry.spawn(
            objId,
            x + 0.5,
            y + 0.5,
            difficulty!,
            data.sprites.length,
            isSharewareMode: data.version == GameVersion.shareware,
          );
          if (newEntity != null) entities.add(newEntity);
        }
      }
    }

    if (!playerSpawned && !preservePlayerState) {
      player = Player(x: 1.5, y: 1.5, angle: 0.0);
    }

    // Sanitize the level grid to ensure only valid walls/doors remain
    for (int y = 0; y < 64; y++) {
      for (int x = 0; x < 64; x++) {
        int id = currentLevel[y][x];
        if (!((id >= 1 && id <= 63) || (id >= 90 && id <= 101))) {
          currentLevel[y][x] = 0;
        }
      }
    }

    _buildFallbackAreasIfNeeded();
    _refreshPlayerConnectedAreas();

    _bumpPlayerIfStuck();
  }

  /// Handles floor transitions, including the "Level 10" secret floor logic.
  void _onLevelCompleted({bool isSecretExit = false}) {
    audio.playSoundEffect(SoundEffect.levelComplete);
    audio.stopMusic();
    player
      ..hasGoldKey = false
      ..hasSilverKey = false;
    final currentEpisode = data.episodes[_currentEpisodeIndex];

    if (isSecretExit) {
      // Secret exits jump to map index 9 (Level 10)
      _returnLevelIndex = _currentLevelIndex + 1;
      _currentLevelIndex = 9;
    } else {
      // Returning from Level 10 or moving to the next sequential floor
      if (_currentLevelIndex == 9 && _returnLevelIndex != null) {
        _currentLevelIndex = _returnLevelIndex!;
        _returnLevelIndex = null;
      } else {
        _currentLevelIndex++;
      }
    }

    if (_currentLevelIndex >= currentEpisode.levels.length ||
        _currentLevelIndex > 9) {
      onGameWon();
    } else {
      _loadLevel(preservePlayerState: true);
    }
  }

  /// Translates [EngineInput] into movement vectors and rotation.
  ({Coordinate2D movement, double dAngle}) _processInputs(
    Duration delta,
    EngineInput input,
  ) {
    // Standardize movement to 60 FPS (16.66ms per frame)
    final double timeScale = delta.inMilliseconds / 16.666;

    // Apply the timeScale multiplier to ensure consistent speed at any framerate
    final double moveSpeed = 0.10 * timeScale;
    final double turnSpeed = 0.08 * timeScale;

    Coordinate2D movement = const Coordinate2D(0, 0);
    double dAngle = 0.0;

    if (input.requestedWeapon != null) {
      player.requestWeaponSwitch(input.requestedWeapon!);
    }

    if (input.isFiring) {
      final bool shotFired = player.fire(_timeAliveMs);

      if (shotFired) {
        _playPlayerWeaponSound();
      }

      if (shotFired && _timeAliveMs - _lastAcousticAlertTime > 400) {
        _propagateGunfire();
        _lastAcousticAlertTime = _timeAliveMs;
      }
    } else {
      player.releaseTrigger();
    }

    if (input.isTurningLeft) dAngle -= turnSpeed;
    if (input.isTurningRight) dAngle += turnSpeed;

    Coordinate2D forwardVec = Coordinate2D(
      math.cos(player.angle),
      math.sin(player.angle),
    );
    if (input.isMovingForward) movement += forwardVec * moveSpeed;
    if (input.isMovingBackward) movement -= forwardVec * moveSpeed;

    if (input.isInteracting) {
      int targetX = (player.x + math.cos(player.angle)).toInt();
      int targetY = (player.y + math.sin(player.angle)).toInt();

      if (targetX >= 0 && targetX < 64 && targetY >= 0 && targetY < 64) {
        int wallId = currentLevel[targetY][targetX];
        if (wallId == MapObject.normalElevatorSwitch) {
          _onLevelCompleted(isSecretExit: false);
          return (movement: const Coordinate2D(0, 0), dAngle: 0.0);
        } else if (wallId == MapObject.secretElevatorSwitch) {
          _onLevelCompleted(isSecretExit: true);
          return (movement: const Coordinate2D(0, 0), dAngle: 0.0);
        }
      }
      doorManager.handleInteraction(player.x, player.y, player.angle);
      pushwallManager.handleInteraction(
        player.x,
        player.y,
        player.angle,
        currentLevel,
      );
    }

    return (movement: movement, dAngle: dAngle);
  }

  /// Performs axis-aligned collision detection with a wall-margin buffer.
  Coordinate2D _calculateValidatedPosition(
    Coordinate2D currentPos,
    Coordinate2D movement,
  ) {
    const double margin = 0.3; // Prevents clipping through wall edges
    double newX = currentPos.x;
    double newY = currentPos.y;

    Coordinate2D target = currentPos + movement;

    // Check X-axis Movement
    if (movement.x != 0) {
      int checkX = (movement.x > 0)
          ? (target.x + margin).toInt()
          : (target.x - margin).toInt();
      if (isWalkable(checkX, currentPos.y.toInt())) newX = target.x;
    }

    // Check Y-axis Movement
    if (movement.y != 0) {
      int checkY = (movement.y > 0)
          ? (target.y + margin).toInt()
          : (target.y - margin).toInt();
      if (isWalkable(newX.toInt(), checkY)) newY = target.y;
    }

    return Coordinate2D(newX, newY);
  }

  /// Updates all [Enemy] and [Collectible] entities in the world.
  void _updateEntities(Duration elapsed) {
    List<Entity> itemsToRemove = [];
    List<Entity> itemsToAdd = [];

    for (Entity entity in entities) {
      if (entity is Enemy) {
        if (entity.state == EntityState.dead &&
            entity.isDying &&
            !entity.hasPlayedDeathSound) {
          audio.playSoundEffect(entity.deathSound);
          entity.hasPlayedDeathSound = true;
        }

        // --- ANIMATION TRANSITION FIX (SAFE VERSION) ---
        // We check if the enemy is in the 'dead' state and currently 'isDying'.
        // Inside WolfEngine._updateEntities
        if (entity.state == EntityState.dead && entity.isDying) {
          // We use a try-catch because HansGrosse throws an UnimplementedError on 'type'.
          try {
            final range = entity.type.animations.dying;
            final totalAnimTime = range.length * 150;

            if (_timeAliveMs - entity.lastActionTime >= totalAnimTime) {
              // Transition from 'Falling' to 'Corpse'.
              entity.isDying = false;
            }
          } catch (_) {
            // Bosses manage their 'isDying' flag manually in their update() methods.
          }
        }

        // Standard AI Update cycle
        if (!entity.isAlerted && entity.state == EntityState.patrolling) {
          _applyPatrolPathMarker(entity);
        }

        final intent = entity.update(
          elapsedMs: _timeAliveMs,
          elapsedDeltaMs: elapsed.inMilliseconds,
          playerPosition: player.position,
          playerAngle: player.angle,
          isPlayerRunning: _isPlayerMovingFast,
          isWalkable: isWalkable,
          areaAt: _areaAt,
          isAreaConnectedToPlayer: _isAreaConnectedToPlayer,
          tryOpenDoor: doorManager.tryOpenDoor,
          onDamagePlayer: (int damage) {
            final difficultyMode = difficulty ?? Difficulty.medium;
            player.takeDamage(difficultyMode.scaleIncomingEnemyDamage(damage));
          },
          onPlaySound: audio.playSoundEffect,
        );

        // Scale the enemy's movement intent to prevent super-speed on 90Hz/120Hz displays
        double timeScale = elapsed.inMilliseconds / 16.666;

        Coordinate2D scaledMovement = intent.movement * timeScale;
        Coordinate2D safeMovement = _clampMovement(scaledMovement);
        final validatedPosition = _calculateValidatedPosition(
          entity.position,
          safeMovement,
        );

        entity.angle = intent.newAngle;

        // Final sanity check: only move if the destination is on-map
        if (validatedPosition.x >= 0 &&
            validatedPosition.x < 64 &&
            validatedPosition.y >= 0 &&
            validatedPosition.y < 64) {
          entity.x = validatedPosition.x;
          entity.y = validatedPosition.y;
        }

        // Handle Item Drops
        if (entity.state == EntityState.dead &&
            entity.isDying &&
            !entity.hasDroppedItem) {
          final dx = player.x - entity.x;
          final dy = player.y - entity.y;
          final distance = math.sqrt(dx * dx + dy * dy);
          final bearingToPlayer = math.atan2(dy, dx);

          log(
            '[DEBUG] Enemy #${entity.debugId} (${entity.type.name}) death context '
            'enemyPos=(${entity.x.toStringAsFixed(2)}, ${entity.y.toStringAsFixed(2)}) '
            'enemyTile=(${entity.x.toInt()}, ${entity.y.toInt()}) '
            'playerPos=(${player.x.toStringAsFixed(2)}, ${player.y.toStringAsFixed(2)}) '
            'playerTile=(${player.x.toInt()}, ${player.y.toInt()}) '
            'delta=(${dx.toStringAsFixed(2)}, ${dy.toStringAsFixed(2)}) '
            'dist=${distance.toStringAsFixed(3)} '
            'bearing=${bearingToPlayer.toStringAsFixed(3)}',
          );

          entity.hasDroppedItem = true;

          Entity? droppedItem;
          if (entity is Dog) {
            droppedItem = null;
          } else if (entity is SS) {
            droppedItem = !player.hasMachineGun
                ? WeaponCollectible(
                    x: entity.x,
                    y: entity.y,
                    mapId: MapObject.machineGun,
                  )
                : SmallAmmoCollectible(x: entity.x, y: entity.y);
          } else if (entity is Guard || entity is Officer || entity is Mutant) {
            droppedItem = SmallAmmoCollectible(x: entity.x, y: entity.y);
          }

          if (droppedItem != null) {
            itemsToAdd.add(droppedItem);
          }
        }
      } else if (entity is Collectible) {
        if (player.position.distanceTo(entity.position) < 0.5) {
          final pickupSoundEffect = player.tryPickup(entity);
          if (pickupSoundEffect != null) {
            audio.playSoundEffect(pickupSoundEffect);
            itemsToRemove.add(entity);
          }
        }
      }
    }

    if (itemsToRemove.isNotEmpty) {
      entities.removeWhere((e) => itemsToRemove.contains(e));
    }
    if (itemsToAdd.isNotEmpty) entities.addAll(itemsToAdd);
  }

  /// Canonical-style gunfire alert propagation: enemies in areas connected to
  /// the player area hear the shot (unless they are ambush-only placements).
  void _propagateGunfire() {
    for (final entity in entities) {
      if (entity is! Enemy ||
          entity.isAlerted ||
          entity.state == EntityState.dead) {
        continue;
      }

      final int area = _areaAt(
        entity.position.x.toInt(),
        entity.position.y.toInt(),
      );
      if (area < 0 || !_isAreaConnectedToPlayer(area)) {
        continue;
      }

      // Ambush enemies are intentionally deaf to noise until they see the player.
      if (entity.state == EntityState.ambushing) {
        continue;
      }

      entity.isAlerted = true;
      audio.playSoundEffect(entity.alertSound);
      log(
        '[DEBUG] Enemy #${entity.debugId} (${entity.type.name}) '
        'alerted by gunfire in area $area',
      );

      if (entity.state == EntityState.idle) {
        entity.state = EntityState.patrolling;
        entity.lastActionTime = _timeAliveMs;
      }
    }
  }

  int _areaAt(int x, int y) {
    if (x < 0 || x >= 64 || y < 0 || y >= 64) return -1;
    return _areaGrid[y][x];
  }

  bool _isAreaConnectedToPlayer(int area) {
    return area >= 0 && area < _areasByPlayer.length && _areasByPlayer[area];
  }

  void _buildFallbackAreasIfNeeded() {
    int maxArea = -1;
    for (int y = 0; y < 64; y++) {
      for (int x = 0; x < 64; x++) {
        if (_areaGrid[y][x] > maxArea) {
          maxArea = _areaGrid[y][x];
        }
      }
    }

    if (maxArea >= 0) {
      _areaCount = maxArea + 1;
      _areasByPlayer = List<bool>.filled(_areaCount, false);
      return;
    }

    // Compatibility fallback for maps without canonical area data: infer area
    // sectors by flood-filling contiguous walkable/door tiles.
    int nextArea = 0;
    for (int y = 0; y < 64; y++) {
      for (int x = 0; x < 64; x++) {
        if (_areaGrid[y][x] >= 0 || !_isAreaFillPassable(x, y)) continue;

        final queue = <({int x, int y})>[(x: x, y: y)];
        _areaGrid[y][x] = nextArea;

        for (int i = 0; i < queue.length; i++) {
          final tile = queue[i];
          final neighbors = <({int x, int y})>[
            (x: tile.x + 1, y: tile.y),
            (x: tile.x - 1, y: tile.y),
            (x: tile.x, y: tile.y + 1),
            (x: tile.x, y: tile.y - 1),
          ];

          for (final n in neighbors) {
            if (n.x < 0 || n.x >= 64 || n.y < 0 || n.y >= 64) continue;
            if (_areaGrid[n.y][n.x] >= 0 || !_isAreaFillPassable(n.x, n.y)) {
              continue;
            }
            _areaGrid[n.y][n.x] = nextArea;
            queue.add(n);
          }
        }
        nextArea++;
      }
    }

    _areaCount = nextArea;
    _areasByPlayer = List<bool>.filled(_areaCount, false);
  }

  bool _isAreaFillPassable(int x, int y) {
    final id = currentLevel[y][x];
    return id == 0 || (id >= 90 && id <= 101);
  }

  bool _isAreaTraversalPassable(int x, int y) {
    if (x < 0 || x >= 64 || y < 0 || y >= 64) return false;
    final id = currentLevel[y][x];
    if (id == 0) return true;
    if (id >= 90 && id <= 101) {
      // For area connectivity, opening/closing doors already bridge sectors.
      final door = doorManager.doors[((y & 0xFFFF) << 16) | (x & 0xFFFF)];
      return door != null && door.state != DoorState.closed;
    }
    return false;
  }

  void _refreshPlayerConnectedAreas() {
    if (_areaCount <= 0) return;

    for (int i = 0; i < _areasByPlayer.length; i++) {
      _areasByPlayer[i] = false;
    }

    final int startX = player.x.toInt();
    final int startY = player.y.toInt();
    final int playerArea = _areaAt(startX, startY);
    if (playerArea < 0) return;

    final visited = List<List<bool>>.generate(
      64,
      (_) => List<bool>.filled(64, false),
    );
    final queue = <({int x, int y})>[(x: startX, y: startY)];
    visited[startY][startX] = true;

    for (int i = 0; i < queue.length; i++) {
      final tile = queue[i];
      final area = _areaAt(tile.x, tile.y);
      if (area >= 0 && area < _areasByPlayer.length) {
        _areasByPlayer[area] = true;
      }

      final neighbors = <({int x, int y})>[
        (x: tile.x + 1, y: tile.y),
        (x: tile.x - 1, y: tile.y),
        (x: tile.x, y: tile.y + 1),
        (x: tile.x, y: tile.y - 1),
      ];

      for (final n in neighbors) {
        if (n.x < 0 || n.x >= 64 || n.y < 0 || n.y >= 64) continue;
        if (visited[n.y][n.x] || !_isAreaTraversalPassable(n.x, n.y)) continue;
        visited[n.y][n.x] = true;
        queue.add(n);
      }
    }
  }

  void _applyPatrolPathMarker(Enemy enemy) {
    final tileX = enemy.x.toInt();
    final tileY = enemy.y.toInt();
    if (tileX < 0 || tileX >= 64 || tileY < 0 || tileY >= 64) return;

    final previousTile = _lastPatrolTileByEnemy[enemy.debugId];
    if (previousTile != null &&
        previousTile.x == tileX &&
        previousTile.y == tileY) {
      return;
    }
    _lastPatrolTileByEnemy[enemy.debugId] = (x: tileX, y: tileY);

    final markerId = _objectLevel[tileY][tileX];
    final markerAngle = MapObject.patrolAngleForMarker(markerId);
    if (enemy is Dog) {
      log(
        '[DEBUG] Dog #${enemy.debugId} patrol tile entry '
        '($tileX, $tileY) marker=$markerId',
      );
    }
    if (markerAngle == null) return;

    final normalizedDiff =
        ((enemy.angle - markerAngle + math.pi) % (2 * math.pi)) - math.pi;
    if (normalizedDiff.abs() > 0.001) {
      enemy.angle = markerAngle;
      log(
        '[DEBUG] Enemy #${enemy.debugId} (${enemy.type.name}) '
        'patrol marker $markerId applied at ($tileX, $tileY)',
      );
    }
  }

  /// Returns true if a tile is empty or contains a door that is sufficiently open.
  bool isWalkable(int x, int y) {
    // 1. Boundary Guard: Prevent range errors by checking if coordinates are on the map
    if (x < 0 || x >= 64 || y < 0 || y >= 64) {
      return false; // Out of bounds is never walkable
    }

    if (currentLevel[y][x] == 0) return true;
    if (currentLevel[y][x] >= 90) return doorManager.isDoorOpenEnough(x, y);
    return false;
  }

  void _playPlayerWeaponSound() {
    final sfxId = switch (player.currentWeapon.type) {
      WeaponType.knife => SoundEffect.knifeAttack,
      WeaponType.pistol => SoundEffect.pistolFire,
      WeaponType.machineGun => SoundEffect.machineGunFire,
      WeaponType.chainGun => SoundEffect.chainGunFire,
    };

    audio.playSoundEffect(sfxId);
  }

  /// Clamps movement to a maximum of 0.2 tiles per step to prevent wall-clipping
  /// and map-boundary jumps during low framerate/high delta spikes.
  Coordinate2D _clampMovement(Coordinate2D intent) {
    const double maxStep = 0.2;
    double length = math.sqrt(intent.x * intent.x + intent.y * intent.y);

    if (length > maxStep) {
      double scale = maxStep / length;
      return Coordinate2D(intent.x * scale, intent.y * scale);
    }
    return intent;
  }

  /// Teleports the player to the nearest empty tile if they spawn inside a wall.
  void _bumpPlayerIfStuck() {
    int pX = player.x.toInt();
    int pY = player.y.toInt();

    if (pY < 0 ||
        pY >= currentLevel.length ||
        pX < 0 ||
        pX >= currentLevel[0].length ||
        currentLevel[pY][pX] > 0) {
      double shortestDist = double.infinity;
      Coordinate2D nearestSafeSpot = Coordinate2D(1.5, 1.5);

      for (int y = 0; y < currentLevel.length; y++) {
        for (int x = 0; x < currentLevel[y].length; x++) {
          if (currentLevel[y][x] == 0) {
            Coordinate2D safeSpot = Coordinate2D(x + 0.5, y + 0.5);
            double dist = safeSpot.distanceTo(player.position);
            if (dist < shortestDist) {
              shortestDist = dist;
              nearestSafeSpot = safeSpot;
            }
          }
        }
      }
      player.x = nearestSafeSpot.x;
      player.y = nearestSafeSpot.y;
    }
  }
}
