#include "worldbuilder.hpp"
#include "rand.hpp"
#include <fmt/core.h>
#include <iostream>
#include "components.hpp"
#include "rituals.hpp"
#include "maze.hpp"
#include "textures.hpp"
#include "inventory.hpp"
#include "systems.hpp"

using namespace fmt;
using namespace components;

void WorldBuilder::stylize_rooms() {
  auto& tiles = $map.tiles();
  Config style_config("assets/styles.json");
  json& styles = style_config.json();

  for(auto& room : $map.rooms()) {
    auto& style = styles[Random::uniform(size_t(0), styles.size() - 1)];

    dbc::check(style.contains("floor"),
        fmt::format("no floor spec in style {}", (std::string)style["name"]));
    dbc::check(style.contains("walls"),
        fmt::format("no walls spec in style {}", (std::string)style["name"]));

    auto& floor_name = style["floor"];
    auto& wall_name = style["walls"];
    size_t floor_id = textures::get_id(floor_name);
    size_t wall_id = textures::get_id(wall_name);

    for(matrix::box it{tiles, room.x, room.y, room.width+1, room.height+1}; it.next();) {
      if(tiles[it.y][it.x] == 1) {
        tiles[it.y][it.x] = wall_id;
      } else if(tiles[it.y][it.x] == 0) {
        tiles[it.y][it.x] = floor_id;
      }
    }
  }
}

void WorldBuilder::generate_map() {
  maze::Builder maze($map);

  maze.hunt_and_kill();
  maze.init();

  maze.randomize_rooms();

  if($map.width() > 20) {
    maze.inner_box(4, 2);
  }

  maze.hunt_and_kill();

  $map.enclose();
  $map.init_tiles();

  stylize_rooms();
}

bool WorldBuilder::find_open_spot(Point& pos_out) {
  size_t i = 0;

  // horribly bad but I need to place things _somewhere_ so just fan out
  for(i = 2; i < $map.width(); i++) {
    // rando_rect starts at the top/left corner not center
    for(matrix::rando_box it{$map.walls(), pos_out.x, pos_out.y, i}; it.next();) {
      Point test{size_t(it.x), size_t(it.y)};

      if($map.can_move(test) && !$collision.something_there(test)) {
        pos_out = test;
        return true;
      }
    }
  }

  matrix::dump("FAIL PLACE!", $map.walls(), pos_out.x, pos_out.y);

  dbc::sentinel(fmt::format("failed to place entity in the entire map?: i={}; width={};", i, $map.width()));

  return false;
}

DinkyECS::Entity WorldBuilder::configure_entity_in_map(DinkyECS::World &world, json &entity_data, Point pos) {
  bool found = find_open_spot(pos);
  dbc::check(found, "Failed to find a place for this thing.");

  auto item = world.entity();
  // NOTE: aiming_at is set by the rayview since it knows that
  world.set<Position>(item, {pos.x, pos.y});

  // BUG: See #72, but this will change to a setting for collision
  int inv_count = entity_data.contains("inventory_count") ? (int)entity_data["inventory_count"] : 0;
  bool has_collision = true;

  if(inv_count > 0) {
    world.set<InventoryItem>(item, {entity_data["inventory_count"], entity_data});
    has_collision = false;
  }

  if(entity_data.contains("components")) {
    components::configure_entity(world, item, entity_data["components"]);
  }

  $collision.insert(pos, item, has_collision);

  return item;
}

DinkyECS::Entity WorldBuilder::configure_entity_in_room(DinkyECS::World &world, json &entity_data, int in_room) {
  Point pos_out;
  bool placed = $map.place_entity(in_room, pos_out);
  dbc::check(placed, "failed to randomly place item in room");
  auto entity = configure_entity_in_map(world, entity_data, pos_out);
  return entity;
}


inline json &select_entity_type(GameConfig &config, json &gen_config) {
  int enemy_test = Random::uniform<int>(0,100);
  int device_test = Random::uniform<int>(0, 100);

  if(enemy_test < gen_config["enemy_probability"]) {
    return config.enemies.json();
  } else if(device_test < gen_config["device_probability"]) {
    return config.devices.json();
  } else {
    return config.items.json();
  }
}

inline json& random_entity_data(GameConfig& config, json& gen_config) {
  json& entity_db = select_entity_type(config, gen_config);

  std::vector<std::string> keys;
  for(auto& el : entity_db.items()) {
    auto& data = el.value();

    if(data["placement"] == nullptr) {
      keys.push_back(el.key());
    }
  }

  int rand_entity = Random::uniform<int>(0, keys.size() - 1);
  std::string key = keys[rand_entity];

  return entity_db[key];
}

void WorldBuilder::randomize_entities(DinkyECS::World &world, GameConfig &config) {
  auto& gen_config = config.game["worldgen"];

  for(int room_num = $map.room_count() - 1; room_num > 0; room_num--) {
    // pass that to the config as it'll be a generic json
    auto& entity_data = random_entity_data(config, gen_config);
    configure_entity_in_room(world, entity_data, room_num);
  }

  for(auto& at : $map.$dead_ends) {
    auto& entity_data = random_entity_data(config, gen_config);
    configure_entity_in_map(world, entity_data, at);
  }
}

void WorldBuilder::place_stairs(DinkyECS::World& world, GameConfig& config) {
  auto& device_config = config.devices.json();
  auto entity_data = device_config["STAIRS_DOWN"];

  auto at_end = $map.$dead_ends.back();
  configure_entity_in_map(world, entity_data, at_end);
}

void WorldBuilder::configure_starting_items(DinkyECS::World &world) {
  auto& player = world.get_the<Player>();
  auto& blanket = world.get_the<ritual::Blanket>();
  auto& config = world.get_the<GameConfig>().rituals;

  for(auto& el : config["starting_junk"]) {
    ritual::JunkItem name = el;
    blanket.add(name);
  };

  auto torch_id = System::spawn_item(world, "TORCH_BAD");

  auto &inventory = world.get<inventory::Model>(player.entity);
  inventory.add("hand_r", torch_id);
  world.make_constant(torch_id);

  auto healing = System::spawn_item(world, "POTION_HEALING_SMALL");
  inventory.add("hand_l", healing);
  world.make_constant(healing);
}

void WorldBuilder::place_entities(DinkyECS::World &world) {
  auto &config = world.get_the<GameConfig>();
  // configure a player as a fact of the world
  Position player_pos{0,0};

  if(world.has_the<Player>()) {
    auto& player = world.get_the<Player>();

    // first get a guess from the map
    bool placed = $map.place_entity(0, player_pos.location);
    dbc::check(placed, "map.place_entity failed to position player");

    // then use the collision map to place the player safely
    placed = find_open_spot(player_pos.location);
    dbc::check(placed, "WorldBuild.find_open_spot also failed to position player");

    world.set<Position>(player.entity, player_pos);
    $collision.insert(player_pos.location, player.entity, true);
  } else {
    auto player_data = config.enemies["PLAYER_TILE"];
    auto player_ent = configure_entity_in_room(world, player_data, 0);

    player_pos = world.get<Position>(player_ent);

    // configure player in the world
    Player player{player_ent};
    world.set_the<Player>(player);
    world.set_the<ritual::Belt>({});
    world.set_the<ritual::Blanket>({});
    world.set<inventory::Model>(player_ent, {});
    configure_starting_items(world);
    world.make_constant(player.entity);
  }

  dbc::check(player_pos.location.x != 0 && player_pos.location.y != 0,
      "failed to place the player correctly");

  randomize_entities(world, config);
  place_stairs(world, config);
}

void WorldBuilder::generate(DinkyECS::World &world) {
  generate_map();
  place_entities(world);
}
