#include "game/systems.hpp"
#include <fmt/core.h>
#include <string>
#include <cmath>
#include "algos/rand.hpp"
#include "algos/spatialmap.hpp"
#include "dbc.hpp"
#include "graphics/lights.hpp"
#include "events.hpp"
#include "game/sound.hpp"
#include "ai/ai.hpp"
#include "ai/ai_debug.hpp"
#include "algos/shiterator.hpp"
#include "combat/battle.hpp"
#include <iostream>
#include "graphics/shaders.hpp"
#include "graphics/textures.hpp"
#include "game/inventory.hpp"
#include "game/level.hpp"
#include "events.hpp"
#include "graphics/animation.hpp"

using std::string;
using namespace fmt;
using namespace combat;
using namespace components;
using namespace DinkyECS;
using lighting::LightSource;

void System::set_position(World& world, SpatialMap& collision, Entity entity, Position pos) {
  world.set<Position>(entity, pos);
  bool has_collision = world.has<Collision>(entity);
  collision.insert(pos.location, entity, has_collision);
}

void System::lighting() {
  auto& level = GameDB::current_level();
  auto& light = *level.lights;
  auto& world = *level.world;
  auto& map = *level.map;

  light.reset_light();

  world.query<Position>([&](auto, auto &position) {
    light.set_light_target(position.location);
  });

  light.path_light(map.walls());

  world.query<Position, LightSource>([&](auto ent, auto &position, auto &lightsource) {
    light.render_light(lightsource, position.location);

    if(ent == level.player) {
      light.update_fow(position.location, lightsource);
    }
  });
}

void System::generate_paths() {
  auto& level = GameDB::current_level();
  const auto &player_pos = GameDB::player_position();

  level.map->set_target(player_pos.location);
  level.map->make_paths();
}

void System::enemy_ai_initialize() {
  auto& level = GameDB::current_level();
  auto& world = *level.world;
  auto& map = *level.map;

  world.query<Position, EnemyConfig>([&](const auto ent, auto& pos, auto& config) {
    if(world.has<ai::EntityAI>(ent)) {
      auto& enemy = world.get<ai::EntityAI>(ent);
      auto& personality = world.get<Personality>(ent);
      int dist_to_player = map.distance(pos.location);

      if(dist_to_player > 1) {
        // not near the player so can't be in combat
        enemy.set_state("enemy_found", false);
        enemy.set_state("in_combat", false);
      }

      enemy.set_state("detect_enemy", dist_to_player < personality.hearing_distance);
      enemy.update();
    } else {
      auto ai_start = ai::load_state(config.ai_start_name);
      auto ai_goal = ai::load_state(config.ai_goal_name);

      ai::EntityAI enemy(config.ai_script, ai_start, ai_goal);
      auto& personality = world.get<Personality>(ent);

      enemy.set_state("tough_personality", personality.tough);
      enemy.set_state("detect_enemy", map.distance(pos.location) < personality.hearing_distance);
      enemy.update();

      world.set<ai::EntityAI>(ent, enemy);
    }
  });
}

void System::enemy_pathing() {
  auto& level = GameDB::current_level();
  auto& world = *level.world;
  auto& map = *level.map;
  const auto &player_pos = GameDB::player_position();

  world.query<Position, Motion>([&](auto ent, auto &position, auto &motion) {
    if(ent != level.player) {
      auto& enemy_ai = world.get<ai::EntityAI>(ent);
      Point out = position.location; // copy
      bool found_path = false;

      if(enemy_ai.wants_to("find_enemy")) {
        found_path = map.random_walk(out, motion.random, PATHING_TOWARD);
      } else if(enemy_ai.wants_to("run_away")) {
        found_path = map.random_walk(out, motion.random, PATHING_AWAY);
      } else {
        motion = {0,0};
        return; // enemy doesn't want to move
      }

      enemy_ai.set_state("cant_move", !found_path);
      enemy_ai.update();

      motion = { int(out.x - position.location.x), int(out.y - position.location.y)};
    }
  });

  map.clear_target(player_pos.location);
}


void System::motion() {
  auto& level = GameDB::current_level();
  auto world = level.world;
  auto map = level.map;
  auto collider = level.collision;

  world->query<Position, Motion>(
    [&](auto ent, auto &position, auto &motion) {
      // skip enemies that aren't moving
      if(motion.dx == 0 && motion.dy == 0) return;

      Point move_to = {
        position.location.x + motion.dx,
        position.location.y + motion.dy
      };

      motion = {0,0}; // clear it after getting it

      dbc::check(map->can_move(move_to), "Enemy pathing failed, move_to is wall.");

      bool cant_move = collider->occupied(move_to);

      if(auto enemy_ai = world->get_if<ai::EntityAI>(ent)) {
        enemy_ai->set_state("cant_move", cant_move);
      }

      // it's a wall, skip
      if(cant_move) return;

      // all good, do the move
      collider->move(position.location, move_to, ent);
      position.location = move_to;
  });
}

void System::distribute_loot(Position target_pos) {
  auto& level = GameDB::current_level();
  auto& world = *level.world;
  auto& config = world.get_the<GameConfig>();
  int inventory_count = Random::uniform(0, 3);
  auto loot_entity = world.entity();

  if(inventory_count > 0) {
    dbc::log("!!!!!!!!!!!!!!!! ============= LOOTING BODIES NOT READY");
  }

  // NOTE: refer to the code in raycaster for this

  // this creates a dead body on the ground
  auto& entity_data = config.devices["DEAD_BODY"];
  components::configure_entity(world, loot_entity, entity_data["components"]);

  set_position(world, *level.collision, loot_entity, target_pos);
  level.world->send<game::Event>(game::Event::ENTITY_SPAWN, loot_entity, {});
}

void System::death() {
  auto& level = GameDB::current_level();
  auto& world = *level.world;
  auto player = world.get_the<Player>();
  std::vector<Entity> dead_things;

  world.query<Combat>([&](auto ent, auto &combat) {
    // bring out yer dead
    if(combat.is_dead() && !combat.dead) {
      combat.dead = true;
      if(ent != player.entity) {
        // we won't change out the player's components later
        dead_things.push_back(ent);
      }
      // we need to send this event for everything that dies
      world.send<game::Event>(game::Event::DEATH, ent, {});
    } else if(combat.almost_dead()) {
      // if enemies are below 50% health they are marked with bad health
      if(world.has<ai::EntityAI>(ent)) {
        auto& enemy_ai = world.get<ai::EntityAI>(ent);
        enemy_ai.set_state("health_good", false);
        enemy_ai.update();
      }
    }
  });

  // this goes through everything that died and changes them to a gravestone
  for(auto ent : dead_things) {
    if(auto snd = world.get_if<Sound>(ent)) {
      sound::stop(snd->attack);
      sound::play(snd->death);
    }

    auto pos = world.get<Position>(ent);

    // need to remove _after_ getting the position
    level.collision->remove(pos.location, ent);

    // distribute_loot is then responsible for putting something there
    System::distribute_loot(pos);

    world.destroy(ent);
  }
}

void System::combat(int attack_id) {
  auto& level = GameDB::current_level();
  auto& collider = *level.collision;
  auto& world = *level.world;
  const auto& player_pos = GameDB::player_position();
  auto& player_combat = world.get<Combat>(level.player);
  auto& player_ai = world.get<ai::EntityAI>(level.player);

  // this is guaranteed to not return the given position
  auto [found, nearby] = collider.neighbors(player_pos.location);
  combat::BattleEngine battle;

  if(found) {
    for(auto entity : nearby) {
      if(world.has<ai::EntityAI>(entity)) {
        auto& enemy_ai = world.get<ai::EntityAI>(entity);
        auto& enemy_combat = world.get<Combat>(entity);
        battle.add_enemy({entity, &enemy_ai, &enemy_combat});
      }
    }

    battle.add_enemy({level.player, &player_ai, &player_combat, true});
    battle.set_all("enemy_found", true);
    battle.set_all("in_combat", true);
    battle.player_request("kill_enemy");
    battle.ap_refresh();
    battle.plan();
  }

  // battle.dump();

  while(auto act = battle.next()) {
    auto [enemy, enemy_action, cost, host_state] = *act;

    // player shouldn't hit theirself
    if(host_state != BattleHostState::not_host) continue;

    components::CombatResult result {
      .attacker=enemy.entity,
      .host_state=host_state,
      .player_did=player_combat.attack(*enemy.combat),
      .enemy_did=0
    };

    if(result.player_did > 0) {
      spawn_attack(world, attack_id, enemy.entity);
    }

    if(enemy_action == "kill_enemy") {
      result.enemy_did = enemy.combat->attack(player_combat);
      animation::animate_entity(world, enemy.entity);
    }

    world.send<game::Event>(game::Event::COMBAT, enemy.entity, result);
  }
}


void System::collision() {
  auto& level = GameDB::current_level();
  auto& collider = *level.collision;
  auto& world = *level.world;
  const auto& player_pos = GameDB::player_position();

  // this is guaranteed to not return the given position
  auto [found, nearby] = collider.neighbors(player_pos.location);
  int combat_count = 0;

  // AI: I think also this would a possible place to run AI decisions
  for(auto entity : nearby) {
    if(world.has<Combat>(entity)) {
      auto combat = world.get<Combat>(entity);
      if(!combat.dead) {
        combat_count++;
        world.send<game::Event>(game::Event::COMBAT_START, entity, entity);
      }
    } else {
      dbc::log($F("UNKNOWN COLLISION TYPE {}", entity));
    }
  }

  if(combat_count == 0) {
    // BUG: this is probably how we get stuck in combat
    world.send<game::Event>(game::Event::NO_NEIGHBORS, level.player, level.player);
  }
}

/*
 * This isn't for destroying something, but just removing it
 * from the world for say, putting into a container or inventory.
 */
void System::remove_from_world(Entity entity) {
  auto& level = GameDB::current_level();
  auto& item_pos = level.world->get<Position>(entity);
  level.collision->remove(item_pos.location, entity);
  // if you don't do this you get the bug that you can pickup
  // an item and it'll also be in your inventory
  level.world->remove<Position>(entity);
}

void System::pickup() {
  auto& level = GameDB::current_level();
  auto& world = *level.world;
  auto& collision = *level.collision;
  auto pos = GameDB::player_position();

  if(!collision.something_there(pos.aiming_at)) return;

  auto entity = level.collision->find(pos.aiming_at, [&](auto data) -> bool {
      return (world.has<InventoryItem>(data.entity) ||
          world.has<Device>(data.entity));
  });

  if(entity == DinkyECS::NONE) {
    dbc::log("no inventory or devices there");
    return;
  }

  // use spatial find to find an item with inventory...
  if(world.has<InventoryItem>(entity)) {
    // NOTE: this might need to be a separate system so that people can leave stuff alone
    remove_from_world(entity);
    // NOTE: chests are different from say a torch, maybe 2 events or the
    // GUI figures out which it is, then when you click either pick it up
    // and move it or show the loot container UI
    world.send<game::Event>(game::Event::LOOT_ITEM, entity, entity);
  } else if(world.has<Device>(entity)) {
    System::device(world, level.player, entity);
  } else {
    dbc::log("BUG: is this a bug in pickup?!");
  }
}

void System::device(World &world, Entity actor, Entity item) {
  auto& device = world.get<Device>(item);
  dbc::log($F("entity {} INTERACTED WITH DEVICE {}", actor, item));

  for(auto event : device.events) {
    if(event == "STAIRS_DOWN") {
      world.send<game::Event>(game::Event::STAIRS_DOWN, actor, device);
    } else if(event == "LOOT_CONTAINER") {
      world.send<game::Event>(game::Event::LOOT_CONTAINER, actor, device);
    } else {
      dbc::log($F(
            "INVALID EVENT {} for device {}",
            event, (std::string)device.config["name"]));
    }
  }
}

void System::move_player(Position move_to) {
  auto& level = GameDB::current_level();
  auto old_pos = level.world->get<Position>(level.player);

  level.world->set<Position>(level.player, move_to);
  level.collision->move(old_pos.location, move_to.location, level.player);
}

std::shared_ptr<sf::Shader> System::sprite_effect(Entity entity) {
  auto world = GameDB::current_world();
  if(auto se = world->get_if<SpriteEffect>(entity)) {
    if(se->frames > 0) {
      se->frames--;
      return se->effect;
    } else {
      world->remove<SpriteEffect>(entity);
      return nullptr;
    }
  } else {
    return nullptr;
  }
}

Entity System::spawn_item(World& world, const std::string& name) {
  auto& config = world.get_the<GameConfig>().items;
  auto& item_config = config[name];
  auto item_id = world.entity();
  world.set<InventoryItem>(item_id, {1, item_config});
  components::configure_entity(world, item_id, item_config["components"]);

  return item_id;
}

void System::drop_item(Entity item) {
  auto& level = GameDB::current_level();
  auto& world = *level.world;
  auto& map = *level.map;
  auto player_pos = GameDB::player_position();

  dbc::check(map.can_move(player_pos.location), "impossible, the player can't be in a wall");

  Position drop_spot = {player_pos.aiming_at.x, player_pos.aiming_at.y};

  // if they're aiming at a wall then drop at their feet
  if(!map.can_move(drop_spot.location)) drop_spot = player_pos;

  set_position(world, *level.collision, item, drop_spot);

  level.world->not_constant(item);
  level.world->send<game::Event>(game::Event::ENTITY_SPAWN, item, {});
}

// NOTE: I think pickup and this need to be different
bool System::place_in_container(Entity cont_id, const std::string& name, Entity world_entity) {
  auto world = GameDB::current_world();
  auto& container = world->get<inventory::Model>(cont_id);

  if(container.has(world_entity)) {
    fmt::println("container {} already has entity {}, skip", cont_id, world_entity);
    // NOTE: I think this would be a move?!
    return false;
  } else if(container.has(name)) {
    // this is an already occupied slot
    fmt::println("container {} already has SLOT {}, skip", cont_id, name);
    return false;
  } else {
    // this should only apply to the player's inventory
    fmt::println("adding {} entity to loot with name {}", world_entity, name);
    container.add(name, world_entity);
    return true;
  }
}

void System::remove_from_container(Entity cont_id, const std::string& slot_id) {
  auto world = GameDB::current_world();
  auto& container = world->get<inventory::Model>(cont_id);
  auto entity = container.get(slot_id);
  container.remove(entity);
}


void System::inventory_swap(Entity container_id, const std::string& a_name, const std::string &b_name) {
  auto& level = GameDB::current_level();
  dbc::check(a_name != b_name, "Attempt to inventory swap the same slot, you should check this and avoid calling me.");

  auto& inventory = level.world->get<inventory::Model>(container_id);

  auto a_ent = inventory.get(a_name);
  auto b_ent = inventory.get(b_name);
  inventory.swap(a_ent, b_ent);
}

bool System::inventory_occupied(Entity container_id, const std::string& name) {
  auto world = GameDB::current_world();
  auto& inventory = world->get<inventory::Model>(container_id);
  return inventory.has(name);
}

void System::use_item(const string& slot_name) {
  auto& level = GameDB::current_level();
  auto& world = *level.world;
  auto& inventory = world.get<inventory::Model>(level.player);
  auto& player_combat = world.get<Combat>(level.player);

  if(!player_combat.can_heal()) return;
  if(!inventory.has(slot_name)) return;

  auto what = inventory.get(slot_name);

  if(auto curative = world.get_if<Curative>(what)) {
    inventory.remove(what);
    player_combat.apply_healing(*curative);
    world.remove<Curative>(what);
  } else {
    dbc::log($F("no usable item at {}", what));
  }
}

game::Event System::shortest_rotate(Point player_at, Point aiming_at, Point target) {
  dbc::check(aiming_at != target, "you're already pointing there.");
  dbc::check(player_at != target, "you can't turn on yourself");

  float target_dx = float(player_at.x) - float(target.x);
  float target_dy = float(player_at.y) - float(target.y);
  float aiming_dx = float(player_at.x) - float(aiming_at.x);
  float aiming_dy = float(player_at.y) - float(aiming_at.y);

  float target_angle = atan2(-target_dy, target_dx) * (180.0 / std::numbers::pi);
  float aiming_angle = atan2(-aiming_dy, aiming_dx) * (180.0 / std::numbers::pi);

  float diff = target_angle - aiming_angle;
  double normalized = fmod(diff + 360.0, 360.0);

  return normalized < 180.0 ? game::Event::ROTATE_LEFT : game::Event::ROTATE_RIGHT;
}

void System::clear_attack() {
  auto world = GameDB::current_world();
  std::vector<Entity> dead_anim;

  world->query<animation::Animation, Temporary>([&](auto ent, auto& anim, auto&) {
    if(!anim.playing) dead_anim.push_back(ent);
  });

  for(auto ent : dead_anim) {
    world->remove<Sprite>(ent);
    world->remove<animation::Animation>(ent);
    world->remove<SpriteEffect>(ent);
    remove_from_world(ent);
  }
}

void System::spawn_attack(World& world, int attack_id, DinkyECS::Entity enemy) {
}


void System::init(Registry& reg) {
  reg.addRender(System::clear_attack);
  reg.addUseItem(System::use_item);
  reg.addMoving(System::move_player);
  reg.addCombat(System::combat);
  reg.addPickup(System::pickup);
  reg.addUpdate(System::generate_paths);
  reg.addUpdate(System::enemy_ai_initialize);
  reg.addUpdate(System::enemy_pathing);
  reg.addUpdate(System::motion);
  reg.addUpdate(System::collision);
  reg.addUpdate(System::lighting);
  reg.addUpdate(System::death);
}
