import 'dart:math' as math;

import 'package:wolf_3d_dart/src/entities/entities/enemies/dog.dart';
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart';
import 'package:wolf_3d_dart/src/entities/entities/enemies/guard.dart';
import 'package:wolf_3d_dart/src/entities/entities/enemies/mutant.dart';
import 'package:wolf_3d_dart/src/entities/entities/enemies/officer.dart';
import 'package:wolf_3d_dart/src/entities/entities/enemies/ss.dart';
import 'package:wolf_3d_dart/src/entities/entity.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';

/// The base class for all computer-controlled opponents in the game.
///
/// This class encapsulates the shared AI behaviors, including movement physics,
/// difficulty-based spawning, and the signature Wolfenstein 3D sight-detection system.
abstract class Enemy extends Entity {
  Enemy({
    required super.x,
    required super.y,
    required super.spriteIndex,
    super.angle,
    super.state,
    super.mapId,
    super.lastActionTime,
  });

  /// The current "Tic" count remaining for the active animation frame.
  int _ticCount = 0;

  /// Accumulates milliseconds to determine when a Tic has passed (14.28ms per Tic).
  double _ticAccumulator = 0;

  /// The current animation frame index (used to progress through states).
  int currentFrame = 0;

  /// The amount of damage the enemy can take before dying.
  int health = 25;

  /// The potential damage dealt to the player per successful attack.
  int damage = 10;

  /// Set to true when the enemy enters the [EntityState.dead] transition.
  bool isDying = false;

  /// Returns the metadata for this specific enemy type (Guard, SS, etc.).
  ///
  /// This allows the engine to access [EnemyAnimationMap] data without
  /// knowing the specific subclass.
  EnemyType get type;

  /// Ensures enemies drop only one item (like ammo or a key) upon death.
  bool hasDroppedItem = false;

  /// When true, the enemy has spotted the player and is actively pursuing or attacking.
  bool isAlerted = false;

  /// Used to simulate a "reaction delay" before an alerted enemy begins to move.
  /// This replaces the `ob->temp2` variable found in the original `WL_ACT2.C`.
  int reactionTimeMs = 0;

  /// Processes elapsed time and returns true if the enemy's animation frame has completed.
  ///
  /// Movement is applied continuously during the frame, but state changes (like deciding
  /// to shoot) only happen when [processTics] returns true.
  bool processTics(int elapsedDeltaMs, {required double moveSpeed}) {
    // 1 Tic = 1/70th of a second (~14.28ms)
    _ticAccumulator += elapsedDeltaMs;

    // Calculate how many Tics passed this frame
    int ticsPassed = (_ticAccumulator / 14.28).floor();

    if (ticsPassed > 0) {
      _ticAccumulator -= (ticsPassed * 14.28);
      _ticCount -= ticsPassed;
    }

    return _ticCount <= 0;
  }

  /// Sets the duration for the next animation frame.
  void setTics(int tics) {
    _ticCount = tics;
  }

  /// Reduces health and handles state transitions for pain or death.
  ///
  /// Alerts the enemy automatically upon taking damage. There is a 50% chance
  /// to trigger a "pain" flinch animation if the enemy survives the hit.
  void takeDamage(int amount, int currentTime) {
    if (state == EntityState.dead) return;

    health -= amount;
    lastActionTime = currentTime;
    isAlerted = true;

    if (health <= 0) {
      state = EntityState.dead;
      isDying = true;
    } else if (math.Random().nextDouble() < 0.5) {
      state = EntityState.pain;
      setTics(5); // Add this so they actually pause to flinch
    } else {
      state = EntityState.patrolling;
      setTics(10); // Add this to reset their movement rhythm
    }
  }

  /// Handles the standard 3-phase attack progression (Aim -> Shoot -> Cooldown).
  ///
  /// Calculates hit chance and damage based on the [distance] to the player.
  void handleAttackState({
    required int elapsedDeltaMs,
    required void Function(int damage) onDamagePlayer,
    required int shootTics,
    required int cooldownTics,
    required int postAttackTics,
    required double distance,
  }) {
    if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
      currentFrame++;

      if (currentFrame == 1) {
        // Phase 1: Bang! Calculate hit chance based on distance.
        // Drops by ~5% per tile distance, capped at a minimum 10% chance to hit.
        double hitChance = (1.0 - (distance * 0.05)).clamp(0.1, 1.0);

        if (math.Random().nextDouble() <= hitChance) {
          // Hit! Roll for damage between 1 and the enemy's max damage stat.
          int actualDamage = math.Random().nextInt(damage) + 1;
          onDamagePlayer(actualDamage);
        }

        setTics(shootTics);
      } else if (currentFrame == 2) {
        // Phase 2: Cooldown. Gun lowered.
        setTics(cooldownTics);
      } else {
        // Phase 3: Attack complete. Return to patrol logic.
        state = EntityState.patrolling;
        currentFrame = 0;
        setTics(postAttackTics);
      }
    }
  }

  /// Periodically checks if the enemy should spot the player.
  ///
  /// Includes a randomized delay based on [baseReactionMs] and [reactionVarianceMs]
  /// to prevent all enemies in a room from reacting on the exact same frame.
  void checkWakeUp({
    required int elapsedMs,
    required Coordinate2D playerPosition,
    required bool Function(int x, int y) isWalkable,
    int baseReactionMs = 200,
    int reactionVarianceMs = 600,
  }) {
    if (!isAlerted && hasLineOfSight(playerPosition, isWalkable)) {
      if (reactionTimeMs == 0) {
        // First frame of spotting: calculate how long until they "wake up"
        reactionTimeMs =
            elapsedMs +
            baseReactionMs +
            math.Random().nextInt(reactionVarianceMs);
      } else if (elapsedMs >= reactionTimeMs) {
        // Reaction delay has passed
        isAlerted = true;

        if (state == EntityState.idle || state == EntityState.ambush) {
          state = EntityState.patrolling;
          setTics(10);
        }

        lastActionTime = elapsedMs;
        reactionTimeMs = 0;
      }
    }
  }

  /// Determines if there is a clear, unobstructed path between the enemy and the player.
  ///
  /// This logic matches the sight rules of the original engine:
  /// 1. **Proximity**: If the player is within ~1.2 tiles, sight is automatic.
  /// 2. **Field of View**: The player must be within a 180-degree arc in front of the enemy.
  /// 3. **Raycast**: Uses an Integer Bresenham traversal to check for wall obstructions.
  bool hasLineOfSight(
    Coordinate2D playerPosition,
    bool Function(int x, int y) isWalkable,
  ) {
    // 1. Proximity Check (Matches 'MINSIGHT' in original C)
    if (position.distanceTo(playerPosition) < 1.2) {
      return true;
    }

    // 2. FOV Check (Matches original 180-degree sight angles)
    double angleToPlayer = position.angleTo(playerPosition);
    double diff = angle - angleToPlayer;

    // Normalize angle difference to (-pi, pi]
    while (diff <= -math.pi) {
      diff += 2 * math.pi;
    }
    while (diff > math.pi) {
      diff -= 2 * math.pi;
    }

    // If the player is behind the enemy (more than 90 degrees left or right), return false
    if (diff.abs() > math.pi / 2) return false;

    // 3. Map Check (Integer Bresenham Traversal)
    int currentX = position.x.toInt();
    int currentY = position.y.toInt();
    int targetX = playerPosition.x.toInt();
    int targetY = playerPosition.y.toInt();

    int dx = (targetX - currentX).abs();
    int dy = -(targetY - currentY).abs();
    int sx = currentX < targetX ? 1 : -1;
    int sy = currentY < targetY ? 1 : -1;
    int err = dx + dy;

    while (true) {
      if (!isWalkable(currentX, currentY)) return false; // Hit a wall
      if (currentX == targetX && currentY == targetY) break; // Reached player

      int e2 = 2 * err;
      if (e2 >= dy) {
        err += dy;
        currentX += sx;
      }
      if (e2 <= dx) {
        err += dx;
        currentY += sy;
      }
    }
    return true;
  }

  /// Resolves movement by checking for collisions and attempting to interact with doors.
  ///
  /// The logic performs separate X and Y collision checks to allow "sliding" along
  /// walls. If a movement is blocked, it calls [tryOpenDoor] to simulate the
  /// enemy's ability to navigate through the level.
  Coordinate2D getValidMovement({
    required Coordinate2D intendedMovement,
    required Coordinate2D playerPosition,
    required bool Function(int x, int y) isWalkable,
    required void Function(int x, int y) tryOpenDoor,
  }) {
    final double distToPlayer = position.distanceTo(playerPosition);
    const double minDistance = 0.9;

    if (distToPlayer < minDistance) {
      // If already too close, only allow movement if it increases distance
      Coordinate2D nextPos = position + intendedMovement;
      if (nextPos.distanceTo(playerPosition) < distToPlayer) {
        return const Coordinate2D(0, 0);
      }
    }

    double newX = position.x + intendedMovement.x;
    double newY = position.y + intendedMovement.y;

    int currentTileX = position.x.toInt();
    int currentTileY = position.y.toInt();
    int targetTileX = newX.toInt();
    int targetTileY = newY.toInt();

    bool movedX = currentTileX != targetTileX;
    bool movedY = currentTileY != targetTileY;

    // 1. Check Diagonal Movement
    if (movedX && movedY) {
      bool canMoveX = isWalkable(targetTileX, currentTileY);
      bool canMoveY = isWalkable(currentTileX, targetTileY);
      bool canMoveDiag = isWalkable(targetTileX, targetTileY);

      if (!canMoveX || !canMoveY || !canMoveDiag) {
        // Trigger door logic if a wall is blocking the path
        if (!canMoveX) tryOpenDoor(targetTileX, currentTileY);
        if (!canMoveY) tryOpenDoor(currentTileX, targetTileY);
        if (!canMoveDiag) tryOpenDoor(targetTileX, targetTileY);

        // Allow sliding: if X is clear but Y/Diag is blocked, return only X movement
        if (canMoveX) return Coordinate2D(intendedMovement.x, 0);
        if (canMoveY) return Coordinate2D(0, intendedMovement.y);
        return const Coordinate2D(0, 0);
      }
    }

    // 2. Check Cardinal (Straight) Movement
    if (movedX && !movedY) {
      if (!isWalkable(targetTileX, currentTileY)) {
        tryOpenDoor(targetTileX, currentTileY);
        return Coordinate2D(0, intendedMovement.y);
      }
    }
    if (movedY && !movedX) {
      if (!isWalkable(currentTileX, targetTileY)) {
        tryOpenDoor(currentTileX, targetTileY);
        return Coordinate2D(intendedMovement.x, 0);
      }
    }

    return intendedMovement;
  }

  /// The per-frame update logic to be implemented by specific enemy types.
  ({Coordinate2D movement, double newAngle}) update({
    required int elapsedMs,
    required int elapsedDeltaMs,
    required Coordinate2D playerPosition,
    required bool Function(int x, int y) isWalkable,
    required void Function(int x, int y) tryOpenDoor,
    required void Function(int damage) onDamagePlayer,
  });

  /// Factory method to spawn the correct [Enemy] subclass based on a Map ID.
  ///
  /// This validates [difficulty] requirements, filters out non-enemy objects,
  /// and ensures [isSharewareMode] restrictions are respected (e.g., no Mutants).
  static Enemy? spawn(
    int objId,
    double x,
    double y,
    Difficulty difficulty, {
    bool isSharewareMode = false,
  }) {
    // Filter out decorative bodies or player starts
    if (objId == MapObject.deadGuard || objId == MapObject.deadAardwolf) {
      return null;
    }
    if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) {
      return null;
    }

    // Prevent bosses from spawning via standard enemy logic
    if (objId >= MapObject.bossHansGrosse &&
        objId <= MapObject.bossFettgesicht) {
      return null;
    }

    final type = EnemyType.fromMapId(objId);
    if (type == null) return null;

    // Check version compatibility
    if (isSharewareMode && !type.existsInShareware) return null;

    final mapData = type.mapData;

    // Resolve spawn orientation and initial AI state
    double spawnAngle = CardinalDirection.fromEnemyIndex(objId).radians;
    EntityState spawnState;

    if (mapData.isPatrolForDifficulty(objId, difficulty)) {
      spawnState = EntityState.patrolling;
    } else if (mapData.isStaticForDifficulty(objId, difficulty)) {
      spawnState = EntityState.idle;
    } else if (mapData.isAmbushForDifficulty(objId, difficulty)) {
      spawnState = EntityState.ambush;
    } else {
      // The ID belongs to this enemy type, but not for this specific difficulty level
      return null;
    }

    // Return the specific instance
    return switch (type) {
      EnemyType.guard => Guard(x: x, y: y, angle: spawnAngle, mapId: objId),
      EnemyType.dog => Dog(x: x, y: y, angle: spawnAngle, mapId: objId),
      EnemyType.ss => SS(x: x, y: y, angle: spawnAngle, mapId: objId),
      EnemyType.mutant => Mutant(x: x, y: y, angle: spawnAngle, mapId: objId),
      EnemyType.officer => Officer(x: x, y: y, angle: spawnAngle, mapId: objId),
    }..state = spawnState;
  }
}
