import 'dart:math' as math;

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

enum EnemyAnimation { idle, walking, attacking, pain, dying, dead }

enum EnemyType {
  guard(mapBaseId: 108, spriteBaseIdx: 50),
  dog(mapBaseId: 216, spriteBaseIdx: 99),
  ss(mapBaseId: 180, spriteBaseIdx: 138),
  mutant(mapBaseId: 252, spriteBaseIdx: 187),
  officer(mapBaseId: 144, spriteBaseIdx: 238);

  final int mapBaseId;
  final int spriteBaseIdx;

  const EnemyType({required this.mapBaseId, required this.spriteBaseIdx});

  /// Helper to check if a specific TED5 Map ID belongs to this enemy
  bool claimsMapId(int id) => id >= mapBaseId && id <= mapBaseId + 35;

  /// Helper to find which EnemyType a given Map ID belongs to
  static EnemyType? fromMapId(int id) {
    for (final type in EnemyType.values) {
      if (type.claimsMapId(id)) return type;
    }
    return null;
  }

  bool claimsSpriteIndex(int index) {
    return switch (this) {
      // Walk, Action, & Death: 50-98
      EnemyType.guard => index >= 50 && index <= 98,
      // Walk, Action, & Death: 99-137
      EnemyType.dog => index >= 99 && index <= 137,
      // Walk, Action, & Death: 138-186
      EnemyType.ss => index >= 138 && index <= 186,
      // Walk, Action, & Death: 187-237
      EnemyType.mutant => index >= 187 && index <= 237,
      // Walk, Action, & Death: 238-287
      EnemyType.officer => index >= 238 && index <= 287,
    };
  }

  /// Returns the current animation state for a given sprite index.
  /// Returns null if the sprite index does not belong to this enemy.
  EnemyAnimation? getAnimationFromSprite(int spriteIndex) {
    if (!claimsSpriteIndex(spriteIndex)) return null;

    // By working with offsets, we don't have to hardcode the 100+ sprite indices!
    int offset = spriteIndex - spriteBaseIdx;

    // All standard enemies use offsets 0-7 for their 8 directional Idle frames
    if (offset >= 0 && offset <= 7) return EnemyAnimation.idle;

    // The action frames vary slightly depending on the enemy type
    return switch (this) {
      EnemyType.guard || EnemyType.ss => switch (offset) {
        >= 8 && <= 39 => EnemyAnimation.walking, // 4 frames * 8 directions
        >= 40 && <= 42 => EnemyAnimation.attacking, // Aim, Fire, Recoil
        43 => EnemyAnimation.pain,
        >= 44 && <= 46 => EnemyAnimation.dying,
        _ => EnemyAnimation.dead, // Catch-all for final frames
      },

      EnemyType.officer || EnemyType.mutant => switch (offset) {
        >= 8 && <= 39 => EnemyAnimation.walking,
        >= 40 && <= 41 => EnemyAnimation.attacking, // Only 2 attack frames!
        42 => EnemyAnimation.pain,
        >= 43 && <= 45 => EnemyAnimation.dying,
        _ => EnemyAnimation.dead,
      },

      EnemyType.dog => switch (offset) {
        // Dogs are special: 3 walk frames (24 total) and NO pain frame!
        >= 8 && <= 31 => EnemyAnimation.walking,
        >= 32 && <= 34 => EnemyAnimation.attacking, // Leap and bite
        >= 35 && <= 37 => EnemyAnimation.dying,
        _ => EnemyAnimation.dead,
      },
    };
  }

  int getSpriteFromAnimation({
    required EnemyAnimation animation,
    required int elapsedMs,
    required int lastActionTime,
    double angleDiff = 0,
    int? walkFrameOverride, // Optional for custom timing
  }) {
    // 1. Calculate Octant for directional sprites (Idle/Walk)
    int octant = ((angleDiff + (math.pi / 8)) / (math.pi / 4)).floor() % 8;
    if (octant < 0) octant += 8;

    return switch (animation) {
      EnemyAnimation.idle => spriteBaseIdx + octant,

      EnemyAnimation.walking => () {
        int frameCount = this == EnemyType.dog ? 3 : 4;
        int frame = walkFrameOverride ?? (elapsedMs ~/ 150) % frameCount;
        return (spriteBaseIdx + 8) + (frame * 8) + octant;
      }(),

      EnemyAnimation.attacking => () {
        int time = elapsedMs - lastActionTime;
        return switch (this) {
          EnemyType.guard || EnemyType.ss || EnemyType.dog =>
            spriteBaseIdx +
                (time < 150
                    ? 40
                    : time < 300
                    ? 41
                    : 40),
          EnemyType.officer ||
          EnemyType.mutant => spriteBaseIdx + (time < 200 ? 40 : 41),
        };
      }(),

      EnemyAnimation.pain => spriteBaseIdx + (this == EnemyType.dog ? 32 : 42),

      EnemyAnimation.dying => () {
        int frame = (elapsedMs - lastActionTime) ~/ 150;
        int maxFrames = this == EnemyType.dog ? 2 : 3;
        int offset = this == EnemyType.dog ? 35 : 43;
        return spriteBaseIdx + offset + (frame.clamp(0, maxFrames));
      }(),

      EnemyAnimation.dead => spriteBaseIdx + (this == EnemyType.dog ? 37 : 45),
    };
  }
}

abstract class Enemy extends Entity {
  Enemy({
    required super.x,
    required super.y,
    required super.spriteIndex,
    super.angle,
    super.state,
    super.mapId,
    super.lastActionTime,
  });

  int health = 25;
  int damage = 10;
  bool isDying = false;
  bool hasDroppedItem = false;

  // Replaces ob->temp2 for reaction delays
  int reactionTimeMs = 0;

  void takeDamage(int amount, int currentTime) {
    if (state == EntityState.dead) return;

    health -= amount;
    lastActionTime = currentTime;

    if (health <= 0) {
      state = EntityState.dead;
      isDying = true;
    } else if (math.Random().nextDouble() < 0.5) {
      state = EntityState.pain;
    } else {
      state = EntityState.patrolling;
    }
  }

  void checkWakeUp({
    required int elapsedMs,
    required Coordinate2D playerPosition,
    required bool Function(int x, int y) isWalkable,
    int baseReactionMs = 200,
    int reactionVarianceMs = 600,
  }) {
    if (state == EntityState.idle &&
        hasLineOfSight(playerPosition, isWalkable)) {
      if (reactionTimeMs == 0) {
        reactionTimeMs =
            elapsedMs +
            baseReactionMs +
            math.Random().nextInt(reactionVarianceMs);
      } else if (elapsedMs >= reactionTimeMs) {
        state = EntityState.patrolling;
        lastActionTime = elapsedMs;
        reactionTimeMs = 0;
      }
    }
  }

  // Matches WL_STATE.C's 'CheckLine' using canonical Integer DDA traversal
  bool hasLineOfSight(
    Coordinate2D playerPosition,
    bool Function(int x, int y) isWalkable,
  ) {
    // 1. Proximity Check (Matches WL_STATE.C 'MINSIGHT')
    // If the player is very close, sight is automatic regardless of facing angle.
    // This compensates for our lack of a noise/gunshot alert system!
    if (position.distanceTo(playerPosition) < 1.2) {
      return true;
    }

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

    while (diff <= -math.pi) {
      diff += 2 * math.pi;
    }
    while (diff > math.pi) {
      diff -= 2 * math.pi;
    }

    if (diff.abs() > math.pi / 2) return false;

    // 3. Map Check (Corrected Integer Bresenham)
    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;
      if (currentX == targetX && currentY == targetY) break;

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

  Coordinate2D getValidMovement(
    Coordinate2D intendedMovement,
    bool Function(int x, int y) isWalkable,
    void Function(int x, int y) tryOpenDoor,
  ) {
    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 doors if they are blocking the path
        if (!canMoveX) tryOpenDoor(targetTileX, currentTileY);
        if (!canMoveY) tryOpenDoor(currentTileX, targetTileY);
        if (!canMoveDiag) tryOpenDoor(targetTileX, targetTileY);

        if (canMoveX) return Coordinate2D(intendedMovement.x, 0);
        if (canMoveY) return Coordinate2D(0, intendedMovement.y);
        return const Coordinate2D(0, 0);
      }
    }

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

    return intendedMovement;
  }

  // Updated Signature
  ({Coordinate2D movement, double newAngle}) update({
    required int elapsedMs,
    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,
  });

  /// Centralized factory to handle all enemy spawning logic
  static Enemy? spawn(
    int objId,
    double x,
    double y,
    Difficulty difficulty, {
    bool isSharewareMode = false,
  }) {
    // 1. Check Difficulty & Compatibility
    if (!MapObject.shouldSpawn(objId, difficulty)) return null;

    // If the checkbox is checked, block non-Shareware enemies
    if (isSharewareMode && !MapObject.isSharewareCompatible(objId)) return null;

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

    bool isPatrolling = objId >= type.mapBaseId + 18;
    double spawnAngle = MapObject.getAngle(objId);

    // 2. 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 = isPatrolling ? EntityState.patrolling : EntityState.idle;
  }
}
