import 'dart:math' as math;

import 'package:wolf_3d_dart/wolf_3d_data_types.dart';

/// Defines the primary enemy types and their behavioral metadata.
///
/// This enum maps level object IDs to their corresponding sprite ranges and
/// handles version-specific availability (e.g., Mutants only appearing in Retail).
enum EnemyType {
  /// Standard Brown Guard (The most common enemy).
  guard(
    mapData: EnemyMapData(MapObject.guardStart),
    scoreValue: 100,
    alertSound: SoundEffect.guardHalt,
    attackSound: SoundEffect.enemyFire,
    deathSound: SoundEffect.deathScream1,
    animations: EnemyAnimationMap(
      idle: SpriteFrameRange(50, 57),
      walking: SpriteFrameRange(58, 89),
      dying: SpriteFrameRange(90, 93),
      pain: SpriteFrameRange(94, 94),
      dead: SpriteFrameRange(95, 95),
      attacking: SpriteFrameRange(96, 98),
    ),
  ),

  /// Attack Dog (Fast melee enemy).
  dog(
    mapData: EnemyMapData(MapObject.dogStart),
    scoreValue: 200,
    alertSound: SoundEffect.dogBark,
    attackSound: SoundEffect.dogAttack,
    deathSound: SoundEffect.dogDeath,
    animations: EnemyAnimationMap(
      // Dogs don't have true idle sprites, so map idle to the first walk frame safely
      idle: SpriteFrameRange(99, 106),
      // Dogs have 4 walk frames * 8 octants = 32 sprites
      walking: SpriteFrameRange(99, 130),
      // Jump / Attack is 3 global sprites
      attacking: SpriteFrameRange(135, 137),
      // Dogs don't have pain frames in Wolf3D
      pain: SpriteFrameRange(0, 0),
      // Die is 3 global sprites
      dying: SpriteFrameRange(131, 133),
      // Dead is 1 global sprite
      dead: SpriteFrameRange(134, 134),
    ),
  ),

  /// SS Officer (Blue uniform, machine gun).
  ss(
    mapData: EnemyMapData(MapObject.ssStart),
    scoreValue: 500,
    alertSound: SoundEffect.ssAlert,
    attackSound: SoundEffect.enemyFire,
    deathSound: SoundEffect.ssDeath,
    animations: EnemyAnimationMap(
      idle: SpriteFrameRange(138, 145),
      walking: SpriteFrameRange(146, 177),
      attacking: SpriteFrameRange(184, 186),
      pain: SpriteFrameRange(182, 182),
      dying: SpriteFrameRange(178, 181),
      dead: SpriteFrameRange(183, 183),
    ),
  ),

  /// Undead Mutant (Exclusive to later episodes/retail).
  mutant(
    mapData: EnemyMapData(MapObject.mutantStart, tierOffset: 18),
    scoreValue: 700,
    alertSound: SoundEffect.guardHalt,
    attackSound: SoundEffect.enemyFire,
    deathSound: SoundEffect.deathScream2,
    animations: EnemyAnimationMap(
      idle: SpriteFrameRange(187, 194),
      walking: SpriteFrameRange(195, 226),
      attacking: SpriteFrameRange(234, 237),
      pain: SpriteFrameRange(231, 231),
      dying: SpriteFrameRange(227, 232, excluded: {231}),
      dead: SpriteFrameRange(233, 233),
    ),
    existsInShareware: false,
  ),

  /// High-ranking Officer (White uniform, fast pistol).
  officer(
    mapData: EnemyMapData(MapObject.officerStart),
    scoreValue: 400,
    alertSound: SoundEffect.guardHalt,
    attackSound: SoundEffect.enemyFire,
    deathSound: SoundEffect.deathScream3,
    animations: EnemyAnimationMap(
      idle: SpriteFrameRange(238, 245),
      walking: SpriteFrameRange(246, 277),
      attacking: SpriteFrameRange(285, 287),
      pain: SpriteFrameRange(282, 282),
      dying: SpriteFrameRange(278, 283, excluded: {282}),
      dead: SpriteFrameRange(284, 284),
    ),
    existsInShareware: false,
  )
  ;

  /// Logic for identifying the enemy block in map data.
  final EnemyMapData mapData;

  /// The animation ranges for this enemy type.
  final EnemyAnimationMap animations;

  /// Score granted when this enemy is killed.
  final int scoreValue;

  /// The sound played when this enemy first becomes alerted.
  final SoundEffect alertSound;

  /// The sound played when this enemy attacks.
  final SoundEffect attackSound;

  /// The sound played when this enemy enters its death animation.
  final SoundEffect deathSound;

  /// If false, this enemy type will be ignored when loading shareware data.
  final bool existsInShareware;

  const EnemyType({
    required this.mapData,
    required this.animations,
    required this.scoreValue,
    required this.alertSound,
    required this.attackSound,
    required this.deathSound,
    this.existsInShareware = true,
  });

  /// Identifies an [EnemyType] based on a tile ID from the map's object layer.
  static EnemyType? fromMapId(int id) {
    for (final type in EnemyType.values) {
      final data = type.mapData;

      // Check Easy (Base)
      if (id >= data.baseId && id < data.baseId + 8) return type;

      // Check Medium (+1 Offset)
      int mediumBase = data.baseId + data.tierOffset;
      if (id >= mediumBase && id < mediumBase + 8) return type;

      // Check Hard (+2 Offsets)
      int hardBase = data.baseId + (data.tierOffset * 2);
      if (id >= hardBase && id < hardBase + 8) return type;
    }

    return null;
  }

  /// Safely retrieves the animation map based on the active [GameVersion].
  EnemyAnimationMap? getAnimations(bool isShareware) {
    if (isShareware && !existsInShareware) return null;
    return animations;
  }

  /// Checks if a [spriteIndex] belongs to this enemy type.
  bool claimsSpriteIndex(int index, {bool isShareware = false}) {
    return getAnimations(isShareware)?.getAnimation(index) != null;
  }

  /// Returns canonical hit points from the original source per difficulty.
  int hitPointsFor(Difficulty difficulty) {
    return switch (this) {
      EnemyType.guard => 25,
      EnemyType.officer => 50,
      EnemyType.ss => 100,
      EnemyType.dog => 1,
      EnemyType.mutant => switch (difficulty) {
        Difficulty.baby => 45,
        Difficulty.easy => 55,
        Difficulty.medium => 55,
        Difficulty.hard => 65,
      },
    };
  }

  /// Resolves the [EnemyAnimation] state for a specific [spriteIndex].
  EnemyAnimation? getAnimationFromSprite(
    int spriteIndex, {
    bool isShareware = false,
  }) {
    return getAnimations(isShareware)?.getAnimation(spriteIndex);
  }

  /// Calculates the correct sprite index based on animation state, time, and camera angle.
  ///
  /// Wolfenstein 3D uses "Octant" billboarding, meaning idle and walking states
  /// have 8 different rotations based on the angle between the player and the enemy.
  int getSpriteFromAnimation({
    required EnemyAnimation animation,
    required int elapsedMs,
    required int lastActionTime,
    double angleDiff = 0,
    int? walkFrameOverride,
  }) {
    final range = animations.getRange(animation);

    // --- Octant Calculation ---
    // We split the circle into 8 segments (octants).
    int octant = ((angleDiff + (math.pi / 8)) / (math.pi / 4)).floor() % 8;
    if (octant < 0) octant += 8;

    return switch (animation) {
      // Idle sprites are stored sequentially for all 8 octants.
      EnemyAnimation.idle => range.start + octant,

      // Walking frames alternate: [Frame 1 Octants 0-7], [Frame 2 Octants 0-7], etc.
      EnemyAnimation.walking => () {
        int framesPerAngle = range.length ~/ 8;
        if (framesPerAngle < 1) framesPerAngle = 1;

        int frame = walkFrameOverride ?? (elapsedMs ~/ 150) % framesPerAngle;
        return range.start + (frame * 8) + octant;
      }(),

      // Attack and Dying animations are "Global," meaning they look the same
      // regardless of which way the enemy is facing.
      EnemyAnimation.attacking => () {
        int time = elapsedMs - lastActionTime;
        int mappedFrame = (time ~/ 150).clamp(0, range.length - 1);
        return range.start + mappedFrame;
      }(),

      EnemyAnimation.pain => range.start,

      EnemyAnimation.dying => () {
        int time = elapsedMs - lastActionTime;
        int mappedFrame = (time ~/ 150).clamp(0, range.length - 1);
        return range.start + mappedFrame;
      }(),

      EnemyAnimation.dead => range.start,
    };
  }
}
