// No flutter imports allowed!
import 'dart:math' as math;

import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
import 'package:wolf_3d_engine/wolf_3d_engine.dart';
import 'package:wolf_3d_entities/wolf_3d_entities.dart';

class WolfEngine {
  WolfEngine({
    required this.data,
    required this.difficulty,
    required this.startingEpisode,
    required this.onGameWon,
  });

  final WolfensteinData data;
  final Difficulty difficulty;
  final int startingEpisode;

  // Standard Dart function instead of Flutter's VoidCallback
  final void Function() onGameWon;

  // Managers
  final InputManager inputManager = InputManager();
  final DoorManager doorManager = DoorManager();
  final PushwallManager pushwallManager = PushwallManager();

  // State
  late Player player;
  late Level currentLevel;
  late WolfLevel activeLevel;
  List<Entity> entities = [];

  int _currentEpisodeIndex = 0;
  int _currentLevelIndex = 0;
  int? _returnLevelIndex;

  double damageFlashOpacity = 0.0;
  bool isInitialized = false;

  void init() {
    _currentEpisodeIndex = startingEpisode;
    _currentLevelIndex = 0;
    _loadLevel();
    isInitialized = true;
  }

  // Expect standard Dart Duration. The host app is responsible for the loop.
  void tick(Duration elapsed) {
    if (!isInitialized) return;

    final inputResult = _processInputs(elapsed);

    doorManager.update(elapsed);
    pushwallManager.update(elapsed, currentLevel);

    player.updateWeaponSwitch();
    player.angle += inputResult.dAngle;

    if (player.angle < 0) player.angle += 2 * math.pi;
    if (player.angle >= 2 * math.pi) player.angle -= 2 * math.pi;

    final Coordinate2D validatedPos = _calculateValidatedPosition(
      player.position,
      inputResult.movement,
    );

    player.x = validatedPos.x;
    player.y = validatedPos.y;

    _updateEntities(elapsed);

    if (damageFlashOpacity > 0) {
      damageFlashOpacity = math.max(0.0, damageFlashOpacity - 0.05);
    }

    player.updateWeapon(
      currentTime: elapsed.inMilliseconds,
      entities: entities,
      isWalkable: isWalkable,
    );
  }

  void _loadLevel() {
    entities.clear();
    damageFlashOpacity = 0.0;

    final episode = data.episodes[_currentEpisodeIndex];
    activeLevel = episode.levels[_currentLevelIndex];

    currentLevel = List.generate(64, (y) => List.from(activeLevel.wallGrid[y]));
    final Level objectLevel = activeLevel.objectGrid;

    doorManager.initDoors(currentLevel);
    pushwallManager.initPushwalls(currentLevel, objectLevel);

    // TODO: Consider abstracting audio so the engine doesn't depend on Wolf3d singleton
    Wolf3d.I.audio.playLevelMusic(activeLevel);

    for (int y = 0; y < 64; y++) {
      for (int x = 0; x < 64; x++) {
        int objId = objectLevel[y][x];

        if (!MapObject.shouldSpawn(objId, difficulty)) continue;

        if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) {
          double spawnAngle = 0.0;
          if (objId == MapObject.playerNorth) {
            spawnAngle = 3 * math.pi / 2;
          } else if (objId == MapObject.playerEast) {
            spawnAngle = 0.0;
          } else if (objId == MapObject.playerSouth) {
            spawnAngle = math.pi / 2;
          } else if (objId == MapObject.playerWest) {
            spawnAngle = math.pi;
          }

          player = Player(x: x + 0.5, y: y + 0.5, angle: spawnAngle);
        } else {
          Entity? newEntity = EntityRegistry.spawn(
            objId,
            x + 0.5,
            y + 0.5,
            difficulty,
            data.sprites.length,
            isSharewareMode: data.version == GameVersion.shareware,
          );
          if (newEntity != null) entities.add(newEntity);
        }
      }
    }

    for (int y = 0; y < 64; y++) {
      for (int x = 0; x < 64; x++) {
        int id = currentLevel[y][x];
        if (!((id >= 1 && id <= 63) || (id >= 90 && id <= 101))) {
          currentLevel[y][x] = 0;
        }
      }
    }

    _bumpPlayerIfStuck();
    print("Loaded Floor: ${_currentLevelIndex + 1} - ${activeLevel.name}");
  }

  void _onLevelCompleted({bool isSecretExit = false}) {
    Wolf3d.I.audio.stopMusic();

    final currentEpisode = data.episodes[_currentEpisodeIndex];

    if (isSecretExit) {
      _returnLevelIndex = _currentLevelIndex + 1;
      _currentLevelIndex = 9;
    } else {
      if (_currentLevelIndex == 9 && _returnLevelIndex != null) {
        _currentLevelIndex = _returnLevelIndex!;
        _returnLevelIndex = null;
      } else {
        _currentLevelIndex++;
      }
    }

    if (_currentLevelIndex >= currentEpisode.levels.length ||
        _currentLevelIndex > 9) {
      print("Episode Completed! You win!");
      onGameWon();
    } else {
      _loadLevel();
    }
  }

  ({Coordinate2D movement, double dAngle}) _processInputs(Duration elapsed) {
    inputManager.update();

    const double moveSpeed = 0.14;
    const double turnSpeed = 0.10;

    Coordinate2D movement = const Coordinate2D(0, 0);
    double dAngle = 0.0;

    if (inputManager.requestedWeapon != null) {
      player.requestWeaponSwitch(inputManager.requestedWeapon!);
    }

    if (inputManager.isFiring) {
      player.fire(elapsed.inMilliseconds);
    } else {
      player.releaseTrigger();
    }

    if (inputManager.isTurningLeft) dAngle -= turnSpeed;
    if (inputManager.isTurningRight) dAngle += turnSpeed;

    Coordinate2D forwardVec = Coordinate2D(
      math.cos(player.angle),
      math.sin(player.angle),
    );

    if (inputManager.isMovingForward) movement += forwardVec * moveSpeed;
    if (inputManager.isMovingBackward) movement -= forwardVec * moveSpeed;

    if (inputManager.isInteracting) {
      int targetX = (player.x + math.cos(player.angle)).toInt();
      int targetY = (player.y + math.sin(player.angle)).toInt();

      if (targetX >= 0 && targetX < 64 && targetY >= 0 && targetY < 64) {
        int wallId = currentLevel[targetY][targetX];
        if (wallId == MapObject.normalElevatorSwitch) {
          _onLevelCompleted(isSecretExit: false);
          return (movement: const Coordinate2D(0, 0), dAngle: 0.0);
        } else if (wallId == MapObject.secretElevatorSwitch) {
          _onLevelCompleted(isSecretExit: true);
          return (movement: const Coordinate2D(0, 0), dAngle: 0.0);
        }

        int objId = activeLevel.objectGrid[targetY][targetX];
        if (objId == MapObject.normalExitTrigger) {
          _onLevelCompleted(isSecretExit: false);
          return (movement: movement, dAngle: dAngle);
        } else if (objId == MapObject.secretExitTrigger) {
          _onLevelCompleted(isSecretExit: true);
          return (movement: movement, dAngle: dAngle);
        }
      }

      doorManager.handleInteraction(player.x, player.y, player.angle);
      pushwallManager.handleInteraction(
        player.x,
        player.y,
        player.angle,
        currentLevel,
      );
    }

    return (movement: movement, dAngle: dAngle);
  }

  Coordinate2D _calculateValidatedPosition(
    Coordinate2D currentPos,
    Coordinate2D movement,
  ) {
    const double margin = 0.3;
    double newX = currentPos.x;
    double newY = currentPos.y;

    Coordinate2D target = currentPos + movement;

    if (movement.x != 0) {
      int checkX = (movement.x > 0)
          ? (target.x + margin).toInt()
          : (target.x - margin).toInt();
      if (isWalkable(checkX, currentPos.y.toInt())) newX = target.x;
    }

    if (movement.y != 0) {
      int checkY = (movement.y > 0)
          ? (target.y + margin).toInt()
          : (target.y - margin).toInt();
      if (isWalkable(newX.toInt(), checkY)) newY = target.y;
    }

    return Coordinate2D(newX, newY);
  }

  void _updateEntities(Duration elapsed) {
    List<Entity> itemsToRemove = [];
    List<Entity> itemsToAdd = [];

    for (Entity entity in entities) {
      if (entity is Enemy) {
        final intent = entity.update(
          elapsedMs: elapsed.inMilliseconds,
          playerPosition: player.position,
          isWalkable: isWalkable,
          tryOpenDoor: doorManager.tryOpenDoor,
          onDamagePlayer: (int damage) {
            player.takeDamage(damage);
            damageFlashOpacity = 0.5;
          },
        );

        entity.angle = intent.newAngle;
        entity.x += intent.movement.x;
        entity.y += intent.movement.y;

        if (entity.state == EntityState.dead &&
            entity.isDying &&
            !entity.hasDroppedItem) {
          entity.hasDroppedItem = true;
          Entity? droppedAmmo = EntityRegistry.spawn(
            MapObject.ammoClip,
            entity.x,
            entity.y,
            difficulty,
            data.sprites.length,
          );
          if (droppedAmmo != null) itemsToAdd.add(droppedAmmo);
        }
      } else if (entity is Collectible) {
        if (player.position.distanceTo(entity.position) < 0.5) {
          if (player.tryPickup(entity)) {
            itemsToRemove.add(entity);
          }
        }
      }
    }

    if (itemsToRemove.isNotEmpty) {
      entities.removeWhere((e) => itemsToRemove.contains(e));
    }
    if (itemsToAdd.isNotEmpty) entities.addAll(itemsToAdd);
  }

  bool isWalkable(int x, int y) {
    if (currentLevel[y][x] == 0) return true;
    if (currentLevel[y][x] >= 90) return doorManager.isDoorOpenEnough(x, y);
    return false;
  }

  void _bumpPlayerIfStuck() {
    int pX = player.x.toInt();
    int pY = player.y.toInt();

    if (pY < 0 ||
        pY >= currentLevel.length ||
        pX < 0 ||
        pX >= currentLevel[0].length ||
        currentLevel[pY][pX] > 0) {
      double shortestDist = double.infinity;
      Coordinate2D nearestSafeSpot = Coordinate2D(1.5, 1.5);

      for (int y = 0; y < currentLevel.length; y++) {
        for (int x = 0; x < currentLevel[y].length; x++) {
          if (currentLevel[y][x] == 0) {
            Coordinate2D safeSpot = Coordinate2D(x + 0.5, y + 0.5);
            double dist = safeSpot.distanceTo(player.position);

            if (dist < shortestDist) {
              shortestDist = dist;
              nearestSafeSpot = safeSpot;
            }
          }
        }
      }
      player.x = nearestSafeSpot.x;
      player.y = nearestSafeSpot.y;
    }
  }
}
