import 'dart:math' as math;

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({
    required this.data,
    required this.difficulty,
    required this.startingEpisode,
    required this.onGameWon,
    required this.audio,
    required this.input,
  }) : doorManager = DoorManager(
         onPlaySound: (sfxId) => audio.playSoundEffect(sfxId),
       );

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

  /// The static game data (textures, sounds, maps) parsed from original files.
  final WolfensteinData data;

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

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

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

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

  // --- 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;

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

  // --- World State ---

  /// The player's current position, stats, and inventory.
  late Player player;

  /// 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;

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

  int _currentEpisodeIndex = 0;
  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() {
    _currentEpisodeIndex = startingEpisode;
    _currentLevelIndex = 0;
    _loadLevel();
    isInitialized = true;
  }

  /// 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;

    // 1. Process User Input
    input.update();
    final currentInput = input.currentInput;
    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,
    );

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

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

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

  /// Wipes the current world state and builds a new floor from map data.
  void _loadLevel() {
    entities.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]));
    final SpriteMap objectLevel = activeLevel.objectGrid;

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

    // Spawn Player and Entities from the Object Grid
    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) {
          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);
        }
      }
    }

    // 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;
        }
      }
    }

    _bumpPlayerIfStuck();
  }

  /// Handles floor transitions, including the "Level 10" secret floor logic.
  void _onLevelCompleted({bool isSecretExit = false}) {
    audio.stopMusic();
    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();
    }
  }

  /// 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
    double moveSpeed = 0.14 * timeScale;
    double turnSpeed = 0.10 * timeScale;

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

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

    if (input.isFiring) {
      player.fire(_timeAliveMs);

      if (_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) {
        // --- 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
        final intent = entity.update(
          elapsedMs: _timeAliveMs,
          elapsedDeltaMs: elapsed.inMilliseconds,
          playerPosition: player.position,
          isWalkable: isWalkable,
          tryOpenDoor: doorManager.tryOpenDoor,
          onDamagePlayer: (int damage) => player.takeDamage(damage),
        );

        // 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);

        entity.angle = intent.newAngle;

        // Final sanity check: only move if the destination is on-map
        double nextX = entity.x + safeMovement.x;
        double nextY = entity.y + safeMovement.y;

        if (nextX >= 0 && nextX < 64 && nextY >= 0 && nextY < 64) {
          entity.x = nextX;
          entity.y = nextY;
        }

        // Handle Item Drops
        if (entity.state == EntityState.dead &&
            entity.isDying &&
            !entity.hasDroppedItem) {
          entity.hasDroppedItem = true;
          Entity? droppedAmmo = EntityRegistry.spawn(
            MapObject.ammoClip,
            entity.x,
            entity.y,
            difficulty,
            data.sprites.length,
          );
          if (droppedAmmo != null) itemsToAdd.add(droppedAmmo);
        }
      } else if (entity is Collectible) {
        if (player.position.distanceTo(entity.position) < 0.5) {
          if (player.tryPickup(entity)) {
            itemsToRemove.add(entity);
          }
        }
      }
    }

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

  /// Propagates weapon noise through corridors and open doors using a Breadth-First Search.
  void _propagateGunfire() {
    int maxAcousticRange = 20; // How many tiles the sound wave travels
    int startX = player.x.toInt();
    int startY = player.y.toInt();

    // Track visited tiles using a 1D index to prevent infinite loops
    Set<int> visited = {startY * 64 + startX};

    List<({int x, int y})> queue = [(x: startX, y: startY)];

    int distance = 0;

    while (queue.isNotEmpty && distance < maxAcousticRange) {
      List<({int x, int y})> nextQueue = [];

      for (var tile in queue) {
        // 1. Alert any enemies standing on this specific tile
        for (var entity in entities) {
          if (entity is Enemy &&
              !entity.isAlerted &&
              entity.state != EntityState.dead) {
            if (entity.position.x.toInt() == tile.x &&
                entity.position.y.toInt() == tile.y) {
              entity.isAlerted = true;

              // Wake them up!
              if (entity.state == EntityState.idle ||
                  entity.state == EntityState.ambush) {
                entity.state = EntityState.patrolling;
                entity.lastActionTime = _timeAliveMs;
              }
            }
          }
        }

        // 2. Expand the sound wave outward to North, East, South, West
        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 (var n in neighbors) {
          // Keep it within the 64x64 grid limits
          if (n.x >= 0 && n.x < 64 && n.y >= 0 && n.y < 64) {
            int idx = n.y * 64 + n.x;
            if (!visited.contains(idx)) {
              visited.add(idx);

              // Sound only travels through walkable tiles (air and OPEN doors).
              if (isWalkable(n.x, n.y)) {
                nextQueue.add(n);
              }
            }
          }
        }
      }
      queue = nextQueue;
      distance++;
    }
  }

  /// 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;
  }

  /// 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;
    }
  }
}
