#include "systems.hpp"
#include <fmt/core.h>
#include <string>
#include <cmath>
#include "rand.hpp"
#include "spatialmap.hpp"
#include "dbc.hpp"
#include "lights.hpp"
#include "inventory.hpp"
#include "events.hpp"
#include "sound.hpp"
#include "ai.hpp"
#include "ai_debug.hpp"
#include "shiterator.hpp"
#include "rituals.hpp"
#include "battle.hpp"
#include <iostream>
#include "shaders.hpp"

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

void System::lighting(GameLevel &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, auto &position, auto &lightsource) {
    light.render_light(lightsource, position.location);
  });
}

void System::generate_paths(GameLevel &level) {
  auto player = level.world->get_the<Player>();
  const auto &player_position = level.world->get<Position>(player.entity);

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

void System::enemy_ai_initialize(GameLevel &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);

      enemy.set_state("detect_enemy", map.distance(pos.location) < 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(GameLevel &level) {
  auto &world = *level.world;
  auto &map = *level.map;
  auto player = world.get_the<Player>();

  const auto &player_position = world.get<Position>(player.entity);

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

      if(enemy_ai.wants_to("find_enemy")) {
        map.neighbors(out, motion.random, PATHING_TOWARD);
      } else if(enemy_ai.wants_to("run_away")) {
        map.neighbors(out, motion.random, PATHING_AWAY);
      }

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

  map.clear_target(player_position.location);
}

void System::init_positions(DinkyECS::World &world, SpatialMap &collider) {
  world.query<Position>([&](auto ent, auto &pos) {
      if(world.has<Combat>(ent)) {
        const auto& combat = world.get<Combat>(ent);
        if(!combat.dead) {
          collider.insert(pos.location, ent);
        }
      } else {
        collider.insert(pos.location, ent);
      }
  });
}

inline void move_entity(SpatialMap &collider, Map &game_map, Position &position, Motion &motion, DinkyECS::Entity ent) {
  Point move_to = {
    position.location.x + motion.dx,
    position.location.y + motion.dy
  };
  motion = {0,0}; // clear it after getting it

  // it's a wall, skip
  if(!game_map.can_move(move_to)) return;
  // there's collision skip
  if(collider.occupied(move_to)) return;

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

void System::motion(GameLevel &level) {
  level.world->query<Position, Motion>(
    [&](auto ent, auto &position, auto &motion) {
    // don't process entities that don't move
    if(motion.dx != 0 || motion.dy != 0) {
      move_entity(*level.collision, *level.map, position, motion, ent);
    }
  });
}

void System::death(GameLevel &level, components::ComponentMap& components) {
  auto &world = *level.world;
  auto player = world.get_the<Player>();
  auto& config = world.get_the<GameConfig>();
  std::vector<DinkyECS::Entity> dead_things;

  world.query<Combat>([&](auto ent, auto &combat) {
    // bring out yer dead
    if(combat.hp <= 0 && !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<Events::GUI>(Events::GUI::DEATH, ent, {});
    } else if(float(combat.hp) / float(combat.max_hp) < 0.5f) {
      // 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
  // NOTE: this could be a separate system but also could be a function in
  // components::
  for(auto ent : dead_things) {
    // remove their enemy setting
    world.remove<Motion>(ent);
    world.remove<Combat>(ent);
    world.remove<EnemyConfig>(ent);
    world.remove<Personality>(ent);
    world.remove<ai::EntityAI>(ent);
    world.remove<Animation>(ent);
    world.remove<SpriteEffect>(ent);

    if(auto snd = world.get_if<Sound>(ent)) {
      sound::stop(snd->attack);
      sound::play(snd->death);
    }

    auto entity_data = config.items["GRAVE_STONE"];
    components::configure_entity(components, world, ent, entity_data["components"]);
    if(entity_data["inventory_count"] > 0) {
      // right here use a std::any that's already converted instead?
      world.set<InventoryItem>(ent, {entity_data["inventory_count"], entity_data});
    }
  }
}

inline void animate_entity(DinkyECS::World &world, DinkyECS::Entity entity) {
  if(world.has<Animation>(entity)) {
    auto& animation = world.get<Animation>(entity);
    animation.play();
  }

  if(auto snd = world.get_if<Sound>(entity)) {
    sound::play(snd->attack);
  }
}

void System::combat(GameLevel &level, int attack_id) {
  auto &collider = *level.collision;
  auto &world = *level.world;
  auto player = world.get_the<Player>();
  auto& the_belt = world.get_the<ritual::Belt>();

  dbc::check(the_belt.has(attack_id),
      fmt::format("the_belt does not have an attack with id={}", attack_id));

  auto& ritual = the_belt.get(attack_id);
  const auto& player_position = world.get<Position>(player.entity);
  auto& player_combat = world.get<Combat>(player.entity);

  // this is guaranteed to not return the given position
  auto [found, nearby] = collider.neighbors(player_position.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.set_all("enemy_found", true);
    battle.set_all("in_combat", true);
    battle.plan();
  }

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

    Events::Combat result {
      player_combat.attack(enemy.combat), 0
    };

    if(result.player_did > 0) {
      using enum ritual::Element;

      if(ritual.element == FIRE || ritual.element == LIGHTNING) {
        auto effect = shaders::get(
            ritual.element == FIRE ? "flame" : "lightning");
        world.set<SpriteEffect>(enemy.entity, {100, effect});
      }
    }

    if(enemy_action == combat::BattleAction::ATTACK) {
      result.enemy_did = enemy.combat.attack(player_combat);
      animate_entity(world, enemy.entity);
    }

    world.send<Events::GUI>(Events::GUI::COMBAT, enemy.entity, result);
  }
}


void System::collision(GameLevel &level) {
  auto &collider = *level.collision;
  auto &world = *level.world;
  auto player = world.get_the<Player>();

  const auto& player_position = world.get<Position>(player.entity);

  // this is guaranteed to not return the given position
  auto [found, nearby] = collider.neighbors(player_position.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<Events::GUI>(Events::GUI::COMBAT_START, entity, entity);
      }
    } else if(world.has<InventoryItem>(entity)) {
      // BUG: this should really be part of the inventory API and I just
      // call into that to work it, rather than this hard coded crap
      auto item = world.get<InventoryItem>(entity);
      auto& item_pos = world.get<Position>(entity);
      auto& inventory = world.get<Inventory>(player.entity);

      if(world.has<LightSource>(entity)) {
        inventory.add(item);
        world.remove<LightSource>(entity);
      }

      if(world.has<Weapon>(entity)) {
        inventory.add(item);
        world.remove<Weapon>(entity);
      }

      if(world.has<Loot>(entity)) {
        auto &loot = world.get<Loot>(entity);
        inventory.gold += loot.amount;
        world.remove<Loot>(entity);
      }

      if(world.has<Curative>(entity)) {
        inventory.add(item);
        world.remove<Curative>(entity);
      }

      if(auto snd = world.get_if<Sound>(entity)) {
        sound::play(snd->attack);
      }

      collider.remove(item_pos.location);
      world.remove<Tile>(entity);
      world.remove<InventoryItem>(entity);
      world.send<Events::GUI>(Events::GUI::LOOT, entity, item);
    } else if(world.has<Device>(entity)) {
      System::device(world, player.entity, entity);
    } else {
      dbc::log(fmt::format("UNKNOWN COLLISION TYPE {}", entity));
    }
  }

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


void System::device(DinkyECS::World &world, DinkyECS::Entity actor, DinkyECS::Entity item) {
  auto& device = world.get<Device>(item);

  for(auto event : device.events) {
    dbc::log(fmt::format("Device event received {}", event));

    if(event == "Events::GUI::STAIRS_DOWN") {
      world.send<Events::GUI>(Events::GUI::STAIRS_DOWN, actor, device);
    } else {
      dbc::log(fmt::format("EVENT IGNORED {}", event));
    }
  }

  dbc::log(fmt::format("entity {} INTERACTED WITH DEVICE {}", actor, item));
}

void System::plan_motion(DinkyECS::World& world, Point move_to) {
  auto& player = world.get_the<Player>();
  auto& player_position = world.get<Position>(player.entity);
  auto& motion = world.get<Motion>(player.entity);
  motion.dx = move_to.x - player_position.location.x;
  motion.dy = move_to.y - player_position.location.y;
}

/*
 * This one is called inside the MapViewUI very often so
 * just avoid GameMap unlike the others.
 */
std::wstring System::draw_map(GameLevel level, size_t view_x, size_t view_y, int compass_dir) {
  DinkyECS::World &world = *level.world;
  Map &map = *level.map;

  auto player_pos = world.get<Position>(level.player).location;
  Point cam_orig = map.center_camera(player_pos, view_x, view_y);
  auto &tiles = map.tiles();

  // make a grid of chars to work with
  auto grid = shiterator::make<wchar_t>(view_x+1, view_y+1);

  // first fill it with the map cells
  for(shiterator::each_cell_t it{grid}; it.next();) {
    size_t tile_y = size_t(it.y) + cam_orig.y;
    size_t tile_x = size_t(it.x) + cam_orig.x;

    if(tile_x < tiles.$width && tile_y < tiles.$height) {
      grid[it.y][it.x] = tiles.at(tile_x, tile_y).display;
    } else {
      grid[it.y][it.x] = ' ';
    }
  }


  // then get the enemy/item/device tiles and fill those in
  world.query<Position, Tile>([&](auto ent, auto &pos, auto &entity_glyph) {
    if(pos.location.x >= cam_orig.x && pos.location.x <= cam_orig.x + view_x
        && pos.location.y >= cam_orig.y && pos.location.y <= cam_orig.y + view_y) {
      Point view_pos = map.map_to_camera(pos.location, cam_orig);

      if(ent == level.player) {
        grid[view_pos.y][view_pos.x] = COMPASS[compass_dir][0];
      } else {
        grid[view_pos.y][view_pos.x] = entity_glyph.display;
      }
    }
  });

  // then generate the string to display, but this goes away soon
  std::wstring result;

  for(shiterator::each_row_t it{grid}; it.next();) {
    result += grid[it.y][it.x];
    if(it.row) result += '\n';
  }

  return result;
}

void System::player_status(GameLevel &level) {
  auto& combat = level.world->get<Combat>(level.player);
  float percent = float(combat.hp) / float(combat.max_hp);

  if(percent > 0.8) {
    sound::play("hp_status_80");
  } else if(percent > 0.6) {
    sound::play("hp_status_60");
  } else if(percent > 0.3) {
    sound::play("hp_status_30");
  } else if(percent > 0.1) {
    sound::play("hp_status_10");
  } else {
    sound::play("hp_status_00");
  }
}

std::shared_ptr<sf::Shader> System::sprite_effect(GameLevel &level, DinkyECS::Entity entity) {
  if(level.world->has<SpriteEffect>(entity)) {
    auto& se = level.world->get<SpriteEffect>(entity);

    if(se.frames > 0) {
      se.frames--;
      return se.effect;
    } else {
      level.world->remove<SpriteEffect>(entity);
      return nullptr;
    }
  } else {
    return nullptr;
  }
}
