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

using namespace fmt;
using namespace components;

inline void check_player(DinkyECS::World &world, DinkyECS::Entity entity) {
  auto player = world.get_the<Player>();
  dbc::check(player.entity != entity, "player shouldn't be added to world");

  auto tile = world.get<Tile>(player.entity);

  // dbc::check(tile.chr == "\ua66b", format("PLAYER TILE CHANGED {} != {}", tile.chr, "\ua66b"));
}

inline int make_split(Room &cur, bool horiz) {
  size_t dimension = horiz ? cur.height : cur.width;
  int min = dimension / WORLDBUILD_DIVISION;
  int max = dimension - min;

  return Random::uniform<int>(min, max);
}

void WorldBuilder::set_door(Room &room, int value) {
  $map.$walls[room.entry.y][room.entry.x] = value;
  $map.$walls[room.exit.y][room.exit.x] = value;
}

void rand_side(Room &room, Point &door) {
  dbc::check(int(room.width) > 0 && int(room.height) > 0, "Weird room with 0 for height or width.");
  int rand_x = Random::uniform<int>(0, room.width - 1);
  int rand_y = Random::uniform<int>(0, room.height - 1);

  switch(Random::uniform<int>(0,3)) {
    case 0: // north
      door.x = room.x + rand_x;
      door.y = room.y-1;
      break;
    case 1: // south
      door.x = room.x + rand_x;
      door.y = room.y + room.height;
      break;
    case 2: // east
      door.x = room.x + room.width;
      door.y = room.y + rand_y;
      break;
    case 3: // west
      door.x = room.x - 1;
      door.y = room.y + rand_y;
      break;
    default:
      dbc::sentinel("impossible side");
  }
}

void WorldBuilder::add_door(Room &room) {
  rand_side(room, room.entry);
  rand_side(room, room.exit);
}

void WorldBuilder::partition_map(Room &cur, int depth) {
  if(cur.width >= 3 && cur.width <= 6 &&
      cur.height >= 3 && cur.height <= 6)
  {
    $map.add_room(cur);
    return;
  }

  bool horiz = cur.width > cur.height ? false : true;
  int split = make_split(cur, horiz);
  if(split <= 0) return;  // end recursion

  Room left = cur;
  Room right = cur;

  if(horiz) {
    if(split >= int(cur.height)) return; // end recursion

    left.height = size_t(split - 1);
    right.y = cur.y + split;
    right.height = size_t(cur.height - split);
  } else {
    if(split >= int(cur.width)) return; // end recursion

    left.width = size_t(split-1);
    right.x = cur.x + split,
    right.width = size_t(cur.width - split);
  }

  // BUG: min room size should be configurable
  if(depth > 0  && left.width > 2 && left.height > 2) {
    partition_map(left, depth-1);
  }

  // BUG: min room size should be configurable
  if(depth > 0 && right.width > 2 && right.height > 2) {
    partition_map(right, depth-1);
  }
}

void WorldBuilder::update_door(Point &at, int wall_or_space) {
  $map.$walls[at.y][at.x] = wall_or_space;
}


void WorldBuilder::stylize_room(int room, string tile_name, float size) {
  Point pos_out;
  bool placed = $map.place_entity(room, pos_out);
  dbc::check(placed, "failed to place style in room");

  tile_name = tile_name == "FLOOR_TILE" ? "WALL_PLAIN" : tile_name;

  for(matrix::circle it{$map.$walls, pos_out, size}; it.next();) {
    for(int x = it.left; x < it.right; x++) {
      if($map.iswall(x, it.y)) {
        // a wall tile
        $map.$tiles.set_tile(x, it.y, tile_name);
      } else {
        // a floor tile
        $map.$tiles.set_tile(x, it.y, "FLOOR_TILE");
      }
    }
  }
}

void WorldBuilder::generate_rooms() {
  Room root{
    .x = 0,
    .y = 0,
    .width = $map.$width,
    .height = $map.$height
  };
  // BUG: depth should be configurable
  partition_map(root, 10);
  place_rooms();

  dbc::check($map.room_count() > 0, "map generated zero rooms, map too small?");
}

void WorldBuilder::generate_map() {
  generate_rooms();

  PointList holes;
  for(size_t i = 0; i < $map.$rooms.size() - 1; i++) {
    tunnel_doors(holes, $map.$rooms[i], $map.$rooms[i+1]);
  }

  // one last connection from first room to last
  tunnel_doors(holes, $map.$rooms.back(), $map.$rooms.front());

  // place all the holes
  for(auto hole : holes) {

    if(!matrix::inbounds($map.$walls, hole.x, hole.y)) {
      matrix::dump("MAP BEFORE CRASH", $map.$walls, hole.x, hole.y);

      auto err = fmt::format("invalid hold target {},{} map is only {},{}",
        hole.x, hole.y, matrix::width($map.$walls),
        matrix::height($map.$walls));

      dbc::sentinel(err);
    }

    $map.$walls[hole.y][hole.x] = INV_SPACE;
  }

  $map.invert_space();
  $map.expand();
  $map.load_tiles();

  // get only the tiles with no collision to fill rooms
  auto room_types = $map.$tiles.tile_names(false);

  for(size_t i = 0; i < $map.$rooms.size() - 1; i++) {
    size_t room_type = Random::uniform<size_t>(0, room_types.size() - 1);
    int room_size = Random::uniform<int>(100, 800);
    string tile_name = room_types[room_type];
    stylize_room(i, tile_name, room_size * 0.01f);
  }
}


DinkyECS::Entity WorldBuilder::configure_entity_in_map(DinkyECS::World &world, json &entity_data, int in_room) {
  auto item = world.entity();
  Point pos_out;
  bool placed = $map.place_entity(in_room, pos_out);
  dbc::check(placed, "failed to randomly place item in room");
  world.set<Position>(item, {pos_out.x+1, pos_out.y+1});

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

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

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();
  }
}

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

  for(size_t room_num = $map.room_count() - 1; room_num > 0; room_num--) {
    int empty_room = Random::uniform<int>(0, 100);
    if(empty_room < gen_config["empty_room_probability"]) continue;

    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];
    // BUG: this may crash if PLAYER_TILE isn't first
    auto entity_data = entity_db[key];

    // pass that to the config as it'll be a generic json
    auto entity = configure_entity_in_map(world, entity_data, room_num);
    check_player(world, entity);
  }
}

void WorldBuilder::place_stairs(DinkyECS::World& world, GameConfig& config) {
  auto& device_config = config.devices.json();
  auto entity_data = device_config["STAIRS_DOWN"];
  int last_room = $map.room_count() - 1;
  auto entity = configure_entity_in_map(world, entity_data, last_room);
  check_player(world, entity);
}

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

  if(world.has_the<Player>()) {
    auto& player = world.get_the<Player>();
    Point pos_out;
    bool placed = $map.place_entity(0, pos_out);
    dbc::check(placed, "failed to randomly place item in room");
    world.set<Position>(player.entity, {pos_out.x+1, pos_out.y+1});
  } else {
    auto player_data = config.enemies["PLAYER_TILE"];
    auto player_ent = configure_entity_in_map(world, player_data, 0);
    // configure player in the world
    Player player{player_ent};
    world.set_the<Player>(player);
    world.set<Inventory>(player.entity, {5});
    world.make_constant(player.entity);
  }

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

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

void WorldBuilder::make_room(size_t origin_x, size_t origin_y, size_t w, size_t h) {
  $map.INVARIANT();
  dbc::pre("y out of bounds", origin_y + h < $map.$height);
  dbc::pre("x out of bounds", origin_x + w < $map.$width);

  for(size_t y = origin_y; y < origin_y + h; ++y) {
    for(size_t x = origin_x; x < origin_x + w; ++x) {
      $map.$walls[y][x] = INV_SPACE;
    }
  }
}


void WorldBuilder::place_rooms() {
  for(auto &cur : $map.$rooms) {
    // println("ROOM x/y={},{}; w/h={},{}; map={},{}",
    //    cur.x, cur.y, cur.width, cur.height, $map.$width, $map.$height);
    add_door(cur);
    make_room(cur.x, cur.y, cur.width, cur.height);
  }
}

inline bool random_path(Map &map, PointList &holes, Point src, Point target) {
  bool keep_going = false;
  bool target_found = false;
  int count = 0;
  map.set_target(target);
  map.make_paths();
  Matrix &paths = map.paths();

  Point out{src.x, src.y};
  do {
    keep_going = map.neighbors(out, true);
    holes.push_back(out);
    target_found = paths[out.y][out.x] == 0;
  } while(!target_found && keep_going && ++count < WORLDBUILD_MAX_PATH);

  map.INVARIANT();
  map.clear_target(target);

  return target_found;
}

inline void straight_path(Map &map, PointList &holes, Point src, Point target) {
  for(matrix::line dig{src, target}; dig.next();) {
    holes.emplace_back(size_t(dig.x), size_t(dig.y));
    Point expand{(size_t)dig.x+1, (size_t)dig.y};

    if(map.inmap(expand.x, expand.y)) {
      // BUG? should really just move doors away from the edge
      holes.push_back(expand);
    }
  }
}

void WorldBuilder::tunnel_doors(PointList &holes, Room &src, Room &target) {
  int path_type = Random::uniform<int>(0, 10);

  switch(path_type) {
    case 0:
      // then do the rest as random with fallback
      if(!random_path($map, holes, src.exit, target.entry)) {
        straight_path($map, holes, src.exit, target.entry);
      }
      break;
    default:
      // for now do 25% as simple straight paths
      straight_path($map, holes, src.exit, target.entry);
  }
}