import 'dart:developer';
import 'dart:math' as math;

import 'package:wolf_3d_dart/src/engine/save/game_session_snapshot.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_entities.dart';

enum WeaponSwitchState { idle, lowering, raising }

class Player {
  // Spatial
  double x;
  double y;
  double angle;

  // Stats
  int health = 100;
  int ammo = 8;
  int score = 0;
  int lives = 3;

  // Damage flash
  double damageFlash = 0.0; // 0.0 is none, 1.0 is maximum red
  final double damageFlashFadeSpeed = 0.05; // How fast it fades per tick

  // Bonus flash
  double bonusFlash = 0.0; // 0.0 is none, 1.0 is maximum white
  final double bonusFlashFadeSpeed = 0.05; // How fast it fades per tick

  // Chaingun pickup face (classic GOTGATLINGPIC)
  int _chaingunPickupFaceMsRemaining = 0;
  static const int _chaingunPickupFaceDurationMs = 900;

  // Additional classic face states.
  bool _mutantDeathFaceActive = false;
  bool _godModeFaceEnabled = false;

  // Classic face animation (UpdateFace/FACETICS random glance frames)
  math.Random _faceRng = math.Random(0);
  int _faceSeed = 0;
  int _faceFrame = 0;
  double _faceCountTics = 0.0;
  int _nextFaceChangeThreshold = 0;

  // Inventory
  bool hasGoldKey = false;
  bool hasSilverKey = false;
  bool hasMachineGun = false;
  bool hasChainGun = false;

  // Weapon System
  late Weapon currentWeapon;
  final Map<WeaponType, Weapon?> weapons = {
    WeaponType.knife: Knife(),
    WeaponType.pistol: Pistol(),
    WeaponType.machineGun: null,
    WeaponType.chainGun: null,
  };

  WeaponSwitchState switchState = WeaponSwitchState.idle;
  WeaponType? pendingWeaponType;

  // 0.0 is resting, 500.0 is fully off-screen
  double weaponAnimOffset = 0.0;

  static const double _weaponSwitchTravel = 500.0;
  static const double _weaponSwitchPhaseTics = 6.0;
  static const double _ticRate = 70.0;
  static const double _weaponSwitchUnitsPerSecond =
      (_weaponSwitchTravel * _ticRate) / _weaponSwitchPhaseTics;

  Player({required this.x, required this.y, required this.angle}) {
    currentWeapon = weapons[WeaponType.pistol]!;
    setHudFaceAnimationSeed(0);
  }

  // Helper getter to interface with the RaycasterPainter
  Coordinate2D get position => Coordinate2D(x, y);

  bool get isChaingunPickupFaceActive => _chaingunPickupFaceMsRemaining > 0;
  bool get isMutantDeathFaceActive => _mutantDeathFaceActive;
  bool get isGodModeFaceEnabled => _godModeFaceEnabled;
  int get hudFaceFrame => _faceFrame;

  void setHudFaceAnimationSeed(int seed) {
    _faceSeed = seed;
    _faceRng = math.Random(seed);
    _faceFrame = 0;
    _faceCountTics = 0.0;
    _nextFaceChangeThreshold = _faceRng.nextInt(256);
    _mutantDeathFaceActive = false;
    _godModeFaceEnabled = false;
  }

  void setGodModeFaceEnabled(bool enabled) {
    _godModeFaceEnabled = enabled;
  }

  PlayerSaveState toSaveState() {
    final Map<WeaponType, WeaponSaveState> weaponStates =
        <WeaponType, WeaponSaveState>{};
    for (final MapEntry<WeaponType, Weapon?> entry in weapons.entries) {
      final Weapon? weapon = entry.value;
      if (weapon == null) {
        continue;
      }
      weaponStates[entry.key] = weapon.toSaveState();
    }

    return PlayerSaveState(
      x: x,
      y: y,
      angle: angle,
      health: health,
      ammo: ammo,
      score: score,
      lives: lives,
      damageFlash: damageFlash,
      bonusFlash: bonusFlash,
      chaingunPickupFaceMsRemaining: _chaingunPickupFaceMsRemaining,
      mutantDeathFaceActive: _mutantDeathFaceActive,
      godModeFaceEnabled: _godModeFaceEnabled,
      faceSeed: _faceSeed,
      faceFrame: _faceFrame,
      faceCountTics: _faceCountTics,
      nextFaceChangeThreshold: _nextFaceChangeThreshold,
      hasGoldKey: hasGoldKey,
      hasSilverKey: hasSilverKey,
      hasMachineGun: hasMachineGun,
      hasChainGun: hasChainGun,
      currentWeaponType: currentWeapon.type,
      weaponStates: weaponStates,
      switchStateIndex: switchState.index,
      pendingWeaponType: pendingWeaponType,
      weaponAnimOffset: weaponAnimOffset,
    );
  }

  void restoreFromSaveState(PlayerSaveState saveState) {
    x = saveState.x;
    y = saveState.y;
    angle = saveState.angle;
    health = saveState.health;
    ammo = saveState.ammo;
    score = saveState.score;
    lives = saveState.lives;
    damageFlash = saveState.damageFlash;
    bonusFlash = saveState.bonusFlash;
    _chaingunPickupFaceMsRemaining = saveState.chaingunPickupFaceMsRemaining;
    _mutantDeathFaceActive = saveState.mutantDeathFaceActive;
    _godModeFaceEnabled = saveState.godModeFaceEnabled;
    _faceSeed = saveState.faceSeed;
    _faceRng = math.Random(_faceSeed);
    _faceFrame = saveState.faceFrame;
    _faceCountTics = saveState.faceCountTics;
    _nextFaceChangeThreshold = saveState.nextFaceChangeThreshold;
    hasGoldKey = saveState.hasGoldKey;
    hasSilverKey = saveState.hasSilverKey;
    hasMachineGun = saveState.hasMachineGun;
    hasChainGun = saveState.hasChainGun;
    switchState = WeaponSwitchState.values[saveState.switchStateIndex];
    pendingWeaponType = saveState.pendingWeaponType;
    weaponAnimOffset = saveState.weaponAnimOffset;

    weapons.updateAll((_, _) => null);
    for (final MapEntry<WeaponType, WeaponSaveState> entry
        in saveState.weaponStates.entries) {
      final weapon = _createWeapon(entry.key);
      weapon.restoreFromSaveState(entry.value);
      weapons[entry.key] = weapon;
    }

    currentWeapon =
        weapons[saveState.currentWeaponType] ??
        _createWeapon(saveState.currentWeaponType);
    final WeaponSaveState? currentWeaponState =
        saveState.weaponStates[saveState.currentWeaponType];
    if (currentWeaponState != null) {
      currentWeapon.restoreFromSaveState(currentWeaponState);
    }
  }

  Weapon _createWeapon(WeaponType weaponType) {
    return switch (weaponType) {
      WeaponType.knife => Knife(),
      WeaponType.pistol => Pistol(),
      WeaponType.machineGun => MachineGun(),
      WeaponType.chainGun => ChainGun(),
    };
  }

  // --- General Update ---

  void tick(Duration elapsed) {
    // Fade the damage flash over time
    if (damageFlash > 0.0) {
      // Assuming 60fps, we fade it out
      damageFlash = math.max(0.0, damageFlash - damageFlashFadeSpeed);
    }

    if (bonusFlash > 0.0) {
      bonusFlash = math.max(0.0, bonusFlash - bonusFlashFadeSpeed);
    }

    if (_chaingunPickupFaceMsRemaining > 0) {
      _chaingunPickupFaceMsRemaining = math.max(
        0,
        _chaingunPickupFaceMsRemaining - elapsed.inMilliseconds,
      );
    } else {
      _faceCountTics += (elapsed.inMilliseconds * 70) / 1000.0;
      if (_faceCountTics > _nextFaceChangeThreshold) {
        int nextFrame = _faceRng.nextInt(4);
        if (nextFrame == 3) {
          nextFrame = 1;
        }
        _faceFrame = nextFrame;
        _faceCountTics = 0.0;
        _nextFaceChangeThreshold = _faceRng.nextInt(256);
      }
    }

    updateWeaponSwitch(elapsed);
  }

  // --- Weapon Switching & Animation Logic ---

  void updateWeaponSwitch(Duration elapsed) {
    final double delta =
        _weaponSwitchUnitsPerSecond * (elapsed.inMicroseconds / 1000000.0);
    if (delta <= 0.0) {
      return;
    }

    if (switchState == WeaponSwitchState.lowering) {
      // If the map doesn't contain the pending weapon, stop immediately
      if (weapons[pendingWeaponType] == null) {
        switchState = WeaponSwitchState.idle;
        return;
      }

      weaponAnimOffset += delta;
      if (weaponAnimOffset >= _weaponSwitchTravel) {
        weaponAnimOffset = _weaponSwitchTravel;

        // We already know it's not null now, but we can keep the
        // fallback to pistol just to be extra safe.
        currentWeapon = weapons[pendingWeaponType]!;

        switchState = WeaponSwitchState.raising;
      }
    } else if (switchState == WeaponSwitchState.raising) {
      weaponAnimOffset -= delta;
      if (weaponAnimOffset <= 0) {
        weaponAnimOffset = 0.0;
        switchState = WeaponSwitchState.idle;
      }
    }
  }

  void requestWeaponSwitch(WeaponType weaponType) {
    if (switchState != WeaponSwitchState.idle) return;
    if (currentWeapon.state != WeaponState.idle) return;
    if (weaponType == currentWeapon.type) return;
    if (!weapons.containsKey(weaponType)) return;
    if (weaponType != WeaponType.knife && ammo <= 0) return;

    pendingWeaponType = weaponType;
    switchState = WeaponSwitchState.lowering;

    log("[PLAYER] Requesting weapon switch to ${weaponType.name}.");
  }

  // --- Health & Damage ---

  void takeDamage(int damage, {EnemyType? attackerType}) {
    health = math.max(0, health - damage);

    // Spike the damage flash based on how much damage was taken
    // A 10 damage hit gives a 0.5 flash, a 20 damage hit maxes it out at 1.0
    damageFlash = math.min(1.0, damageFlash + (damage * 0.05));
    _chaingunPickupFaceMsRemaining = 0;
    _mutantDeathFaceActive = health <= 0 && attackerType == EnemyType.mutant;

    if (health <= 0) {
      log("[PLAYER] Died! Final Score: $score");
    } else {
      log("[PLAYER] Took $damage damage ($health)");
    }
  }

  void heal(int amount) {
    final int newHealth = math.min(100, health + amount);
    if (health < 100) {
      log("[PLAYER] Healed for $amount ($health -> $newHealth)");
    }
    health = newHealth;
    _chaingunPickupFaceMsRemaining = 0;
    _mutantDeathFaceActive = false;
  }

  void triggerBonusFlash() {
    bonusFlash = 1.0;
  }

  void triggerChaingunPickupFace({int? durationMs}) {
    _chaingunPickupFaceMsRemaining = durationMs == null
        ? _chaingunPickupFaceDurationMs
        : math.max(0, durationMs);
  }

  void addAmmo(int amount) {
    final int newAmmo = math.min(99, ammo + amount);
    if (ammo < 99) {
      log("[PLAYER] Picked up ammo $amount ($ammo -> $newAmmo)");
    }
    ammo = newAmmo;
  }

  void addLives(int amount) {
    lives = math.min(9, lives + amount);
    log("[PLAYER] Adding $amount extra lives (Total Lives: $lives)");
  }

  /// Attempts to collect [item] and returns the SFX to play.
  ///
  /// Returns `null` when the item was not collected (for example: full health).
  SoundEffect? tryPickup(Collectible item) {
    final effect = item.tryCollect(
      CollectiblePickupContext(
        health: health,
        ammo: ammo,
        hasGoldKey: hasGoldKey,
        hasSilverKey: hasSilverKey,
        hasMachineGun: hasMachineGun,
        hasChainGun: hasChainGun,
        currentWeaponType: currentWeapon.type,
      ),
    );
    if (effect == null) return null;

    log("[PLAYER] Collected ${item.runtimeType} - Effect: $effect");

    if (effect.healthToRestore > 0) {
      heal(effect.healthToRestore);
    }

    if (effect.ammoToAdd > 0) {
      addAmmo(effect.ammoToAdd);
    }

    if (effect.scoreToAdd > 0) {
      log("[PLAYER] Adding ${effect.scoreToAdd} score.");

      score += effect.scoreToAdd;
    }

    if (effect.extraLivesToAdd > 0) {
      addLives(effect.extraLivesToAdd);
    }

    if (effect.grantGoldKey) {
      log("[PLAYER] Collected Gold Key.");
      hasGoldKey = true;
    }

    if (effect.grantSilverKey) {
      log("[PLAYER] Collected Silver Key.");
      hasSilverKey = true;
    }

    if (effect.grantWeapon case final weaponType?) {
      if (weapons[weaponType] == null) {
        weapons[weaponType] = switch (weaponType) {
          WeaponType.machineGun => MachineGun(),
          WeaponType.chainGun => ChainGun(),
          WeaponType.pistol => Pistol(),
          WeaponType.knife => Knife(),
        };
      }

      if (weaponType == WeaponType.machineGun) hasMachineGun = true;
      if (weaponType == WeaponType.chainGun) {
        hasChainGun = true;
        triggerChaingunPickupFace();
      }

      log("[PLAYER] Collected ${weaponType.name}.");
    }

    if (effect.requestWeaponSwitch case final weaponType?) {
      requestWeaponSwitch(weaponType);
    }

    triggerBonusFlash();

    return effect.pickupSoundEffect;
  }

  bool fire(int currentTime) {
    if (switchState != WeaponSwitchState.idle) return false;

    // We pass the isFiring state to handle automatic vs semi-auto behavior
    bool shotFired = currentWeapon.fire(currentTime, currentAmmo: ammo);

    if (shotFired && currentWeapon.type != WeaponType.knife) {
      ammo--;
    }

    return shotFired;
  }

  void releaseTrigger() {
    currentWeapon.releaseTrigger();
  }

  /// Returns true only on the specific frame where the hit should be calculated
  void updateWeapon({
    required int currentTime,
    required List<Entity> entities,
    required bool Function(int x, int y) isWalkable,
  }) {
    int oldFrame = currentWeapon.frameIndex;
    currentWeapon.update(currentTime);

    // If we just crossed into the firing frame...
    if (currentWeapon.state == WeaponState.firing &&
        oldFrame == 0 &&
        currentWeapon.frameIndex == 1) {
      currentWeapon.performHitscan(
        playerX: x,
        playerY: y,
        playerAngle: angle,
        entities: entities,
        isWalkable: isWalkable,
        currentTime: currentTime,
        onEnemyKilled: (Enemy killedEnemy) {
          score += killedEnemy.scoreValue;
          log(
            "[PLAYER] Killed ${killedEnemy.runtimeType}! +${killedEnemy.scoreValue} (Score: $score)",
          );
        },
      );
    }

    if (currentWeapon.state == WeaponState.idle &&
        ammo <= 0 &&
        currentWeapon.type != WeaponType.knife) {
      log("[PLAYER] Out of ammo, switching to knife.");
      requestWeaponSwitch(WeaponType.knife);
    }
  }
}
