import 'dart:math' as math;

import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
import 'package:wolf_3d_entities/src/entities/enemies/enemy_animation.dart';

class EnemyAnimationMap {
  final SpriteFrameRange idle;
  final SpriteFrameRange walking;
  final SpriteFrameRange attacking;
  final SpriteFrameRange pain;
  final SpriteFrameRange dying;
  final SpriteFrameRange dead;

  const EnemyAnimationMap({
    required this.idle,
    required this.walking,
    required this.attacking,
    required this.pain,
    required this.dying,
    required this.dead,
  });

  EnemyAnimation? getAnimation(int spriteIndex) {
    if (idle.contains(spriteIndex)) return EnemyAnimation.idle;
    if (walking.contains(spriteIndex)) return EnemyAnimation.walking;
    if (attacking.contains(spriteIndex)) return EnemyAnimation.attacking;
    if (pain.contains(spriteIndex)) return EnemyAnimation.pain;
    if (dying.contains(spriteIndex)) return EnemyAnimation.dying;
    if (dead.contains(spriteIndex)) return EnemyAnimation.dead;
    return null;
  }

  SpriteFrameRange getRange(EnemyAnimation animation) {
    return switch (animation) {
      EnemyAnimation.idle => idle,
      EnemyAnimation.walking => walking,
      EnemyAnimation.attacking => attacking,
      EnemyAnimation.pain => pain,
      EnemyAnimation.dying => dying,
      EnemyAnimation.dead => dead,
    };
  }
}

enum EnemyType {
  guard(
    mapData: EnemyMapData(MapObject.guardStart),
    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),
    ),
  ),
  dog(
    mapData: EnemyMapData(MapObject.dogStart),
    animations: EnemyAnimationMap(
      idle: SpriteFrameRange(99, 106),
      walking: SpriteFrameRange(107, 130),
      attacking: SpriteFrameRange(135, 137),
      pain: SpriteFrameRange(131, 131),
      dying: SpriteFrameRange(132, 134),
      dead: SpriteFrameRange(137, 137),
    ),
  ),
  ss(
    mapData: EnemyMapData(MapObject.ssStart),
    animations: EnemyAnimationMap(
      idle: SpriteFrameRange(138, 145),
      walking: SpriteFrameRange(146, 178),
      attacking: SpriteFrameRange(184, 186),
      pain: SpriteFrameRange(182, 182),
      dying: SpriteFrameRange(179, 181),
      dead: SpriteFrameRange(183, 183),
    ),
  ),
  mutant(
    mapData: EnemyMapData(MapObject.mutantStart),
    animations: EnemyAnimationMap(
      idle: SpriteFrameRange(187, 194),
      walking: SpriteFrameRange(195, 226),
      attacking: SpriteFrameRange(234, 237),
      pain: SpriteFrameRange(231, 231),
      dying: SpriteFrameRange(227, 230),
      dead: SpriteFrameRange(232, 232),
    ),
    existsInShareware: false,
  ),
  officer(
    mapData: EnemyMapData(MapObject.officerStart),
    animations: EnemyAnimationMap(
      idle: SpriteFrameRange(238, 245),
      walking: SpriteFrameRange(246, 277),
      attacking: SpriteFrameRange(285, 287),
      pain: SpriteFrameRange(282, 282),
      dying: SpriteFrameRange(278, 281),
      dead: SpriteFrameRange(283, 283),
    ),
    existsInShareware: false,
  );

  final EnemyMapData mapData;
  final EnemyAnimationMap animations;
  final bool existsInShareware;

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

  static EnemyType? fromMapId(int id) {
    for (final type in EnemyType.values) {
      if (type.mapData.claimsId(id)) return type;
    }
    return null;
  }

  /// Returns the animations only if the enemy actually exists in the current version.
  EnemyAnimationMap? getAnimations(bool isShareware) {
    if (isShareware && !existsInShareware) return null;
    return animations;
  }

  bool claimsSpriteIndex(int index, {bool isShareware = false}) {
    return getAnimations(isShareware)?.getAnimation(index) != null;
  }

  EnemyAnimation? getAnimationFromSprite(
    int spriteIndex, {
    bool isShareware = false,
  }) {
    return getAnimations(isShareware)?.getAnimation(spriteIndex);
  }

  int getSpriteFromAnimation({
    required EnemyAnimation animation,
    required int elapsedMs,
    required int lastActionTime,
    double angleDiff = 0,
    int? walkFrameOverride,
  }) {
    // We don't need to check isShareware here, because if the entity exists
    // in the game world, it implicitly passed the version check during spawn.
    final range = animations.getRange(animation);

    int octant = ((angleDiff + (math.pi / 8)) / (math.pi / 4)).floor() % 8;
    if (octant < 0) octant += 8;

    return switch (animation) {
      EnemyAnimation.idle => range.start + octant,
      EnemyAnimation.walking => () {
        int framesPerAngle = range.length ~/ 8;
        if (framesPerAngle < 1) framesPerAngle = 1;

        int frame = walkFrameOverride ?? (elapsedMs ~/ 150) % framesPerAngle;
        return range.start + (frame * 8) + octant;
      }(),
      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,
    };
  }
}
