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

using namespace fmt;

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 >= 5 && cur.width <= 10 &&
      cur.height >= 5 && cur.height <= 10) {
    $map.$rooms.push_back(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) {
    left.depth = depth - 1;
    partition_map(left, depth-1);
  }

  // BUG: min room size should be configurable
  if(depth > 0 && right.width > 2 && right.height > 2) {
    right.depth = depth - 1;
    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::tunnel_doors(PointList &holes, Room &src, Room &target) {
  $map.set_target(target.entry);
  $map.make_paths();

  bool found = dig_tunnel(holes, src.exit, target.entry);
  dbc::check(found, "room should always be found");

  $map.INVARIANT();
  $map.clear_target(target.entry);
}


void WorldBuilder::stylize_room(int room, string tile_name, float size) {
  Point center = $map.place_entity(room);

  for(matrix::circle it{$map.$walls, center, size}; it.next();) {
    for(int x = it.left; x < it.right; x++) {
      if(!$map.iswall(x, it.y)) {
        $map.$tiles.set_tile(x, it.y, tile_name);
      }
    }
  }
}

void WorldBuilder::generate() {
  PointList holes;
  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?");

  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) {
    $map.$walls[hole.y][hole.x] = INV_SPACE;
  }

  for(matrix::each_cell it{$map.$walls}; it.next();) {
    int is_wall = !$map.$walls[it.y][it.x];
    $map.$walls[it.y][it.x] = is_wall;
  }

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

}

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) {
    cur.x += WORLDBUILD_SHRINK;
    cur.y += WORLDBUILD_SHRINK;
    cur.width -= WORLDBUILD_SHRINK * 2;
    cur.height -= WORLDBUILD_SHRINK * 2;

    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 found = false;
  Matrix &paths = map.paths();
  Point out{src.x, src.y};
  int count = 0;

  do {
    found = map.neighbors(out, true);
    holes.push_back(out);

    if(paths[out.y][out.x] == 0) {
      return true;
    }
  } while(found && ++count < WORLDBUILD_MAX_PATH);

  return found;
}

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

bool WorldBuilder::dig_tunnel(PointList &holes, Point &src, Point &target) {
  Matrix &paths = $map.paths();

  dbc::check(paths[src.y][src.x] != WALL_PATH_LIMIT,
      "source room has path as a wall");
  dbc::check(paths[target.y][target.x] != WALL_PATH_LIMIT,
      "target room has path as a wall");

  if(!random_path($map, holes, src, target)) {
    straight_path(holes, src, target);
  }

  return true;
}
