#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <string>
#include "algos/spatialmap.hpp"
#include "algos/dinkyecs.hpp"
#include "algos/rand.hpp"
#include <limits>
#include <fmt/core.h>

using DinkyECS::Entity;
using namespace fmt;


TEST_CASE("SpatialMap::insert", "[spatialmap]") {
  DinkyECS::World world;
  SpatialMap map;

  auto player = world.entity();
  auto item = world.entity();
  auto potion = world.entity();
  auto enemy = world.entity();
  Point at{10,10};
  Point enemy_at{11,11};

  map.insert(at, item, false);
  map.insert(at, potion, false);
  REQUIRE(!map.occupied(at));

  map.insert(at, player, true);
  REQUIRE(map.occupied_by(at) == player);

  REQUIRE_THROWS(map.insert(at, enemy, true));

  map.insert(enemy_at, enemy, true);
  REQUIRE(map.occupied_by(enemy_at) == enemy);
  REQUIRE(map.occupied(enemy_at));
}

TEST_CASE("SpatialMap::remove", "[spatialmap]") {
  DinkyECS::World world;
  SpatialMap map;

  auto player = world.entity();
  auto item = world.entity();
  Point at{120, 120};

  // confirm that things can be in any order
  map.insert(at, player, true);
  map.insert(at, item, false);
  REQUIRE(map.occupied(at));
  REQUIRE(map.occupied_by(at) == player);

  auto data = map.remove(at, player);
  REQUIRE(!map.occupied(at));
  REQUIRE(data.entity == player);
  REQUIRE(data.collision == true);

  REQUIRE_THROWS(map.remove(at, player));
}


TEST_CASE("SpatialMap::move", "[spatialmap]") {
  DinkyECS::World world;
  SpatialMap map;

  auto player = world.entity();
  auto item = world.entity();
  Point at{10, 320};
  map.insert(at, player, true);
  map.insert(at, item, false);
  REQUIRE(map.occupied(at));

  auto enemy = world.entity();
  auto potion = world.entity();
  Point enemy_at{11, 320};
  map.insert(enemy_at, enemy, true);
  map.insert(enemy_at, potion, false);
  REQUIRE(map.occupied(enemy_at));
  REQUIRE(map.occupied_by(enemy_at) == enemy);

  Point target{at.x + 1, at.y};

  // try bad move with a slot that's empty
  REQUIRE_THROWS(map.move({0,0}, target, player));

  // try move into an occupied spot also fails
  REQUIRE_THROWS(map.move(at, target, player));

  // now move to a new spot, need to add them back
  map.insert(at, player, true);
  target.x++; // just move farther
  map.move(at, target, player);

  REQUIRE(map.occupied(target));
  REQUIRE(map.occupied_by(target) == player);
  auto data = map.remove(target, player);
  REQUIRE(data.entity == player);
  REQUIRE(data.collision == true);
}


TEST_CASE("SpatialMap::occupied/something_there", "[spatialmap]") {
  DinkyECS::World world;
  SpatialMap map;

  auto player = world.entity();
  auto item = world.entity();

  Point at{1000, 20};
  // first test empty locations
  REQUIRE(!map.something_there(at));
  REQUIRE(!map.occupied(at));

  // then when there's something without collision
  map.insert(at, item, false);
  REQUIRE(map.something_there(at));
  REQUIRE(!map.occupied(at));

  // finally with collision and an item there
  map.insert(at, player, true);
  REQUIRE(map.something_there(at));
  REQUIRE(map.occupied(at));
  REQUIRE(map.occupied_by(at) == player);

  // then remove the item and still have collision

  map.remove(at, item);
  REQUIRE(map.something_there(at));
  REQUIRE(map.occupied(at));
  REQUIRE(map.occupied_by(at) == player);

  // remove player and back to no collision
  map.remove(at, player);
  REQUIRE(!map.something_there(at));
  REQUIRE(!map.occupied(at));

  // last thing, put just the player in at a new spot
  Point target{at.x+1, at.y+10};
  map.insert(target, player, true);
  REQUIRE(map.something_there(target));
  REQUIRE(map.occupied(target));
  REQUIRE(map.occupied_by(target) == player);
}


TEST_CASE("SpatialMap::get", "[spatialmap]") {
  DinkyECS::World world;
  SpatialMap map;

  auto player = world.entity();
  auto item = world.entity();
  Point at{101, 31};

  // finally with collision and an item there
  map.insert(at, player, true);
  REQUIRE(map.occupied(at));
  REQUIRE(map.occupied_by(at) == player);

  auto entity = map.get(at);
  REQUIRE(player == entity);

  // This probably doesn't work so need to
  // rethink how get works.
  map.insert(at, item, false);
  entity = map.get(at);
  REQUIRE(entity == item);
}

TEST_CASE("SpatialMap::find", "[spatialmap-find]") {
  DinkyECS::World world;
  SpatialMap map;
  Point at{101, 31};
  DinkyECS::Entity should_collide = DinkyECS::NONE;

  for(int i = 0; i < 10; i++) {
    auto ent = world.entity();
    map.insert(at, ent, i == 8);

    if(i == 8) {
      should_collide = ent;
    }
  }

  auto collision = map.find(at, [&](auto data) -> bool {
      return data.collision;
  });

  REQUIRE(collision == should_collide);

  auto no_collide = map.find(at, [&](auto data) -> bool {
      return !data.collision;
  });

  REQUIRE(no_collide != should_collide);
}

TEST_CASE("SpatialMap::neighbors", "[spatialmap-neighbors]") {
  DinkyECS::World world;
  SpatialMap map;

  auto player = world.entity();
  auto enemy1 = world.entity();
  auto enemy2 = world.entity();
  //auto item1 = world.entity();
  //auto item2 = world.entity();
  Point at{101, 31};

  map.insert(at, player, true);
  map.insert({at.x+1, at.y}, enemy1, true);
  map.insert({at.x-1, at.y+1}, enemy2, true);

  auto result = map.neighbors(at, true);
  REQUIRE(result.found);
  REQUIRE(result.nearby.size() == 2);

  bool maybe = result.nearby[0] == enemy1 || result.nearby[1] == enemy1;
  REQUIRE(maybe);

  maybe = result.nearby[0] == enemy2 || result.nearby[1] == enemy2;
  REQUIRE(maybe);

  result = map.neighbors(at, false);
  REQUIRE(result.found);
  REQUIRE(result.nearby.size() == 1);
  REQUIRE(result.nearby[0] == enemy1);
}

TEST_CASE("SpatialMap::distance_sorted", "[spatialmap]") {
  DinkyECS::World world;
  SpatialMap map;

  auto player = world.entity();
  auto enemy1 = world.entity();
  auto item = world.entity();

  map.insert({1,1}, player, true);
  map.insert({4,4}, enemy1, true);
  map.insert({3, 3}, item, false);

  SortedEntities result;
  map.distance_sorted(result, {1, 1}, 100);
  REQUIRE(result.size() == 3);
  REQUIRE(result[0].entity == enemy1);
  REQUIRE(result[1].entity == item);
  REQUIRE(result[2].entity == player);

  int prev_dist = std::numeric_limits<int>::max();
  for(auto rec : result) {
    REQUIRE(rec.dist_square < prev_dist);
    prev_dist = rec.dist_square;
  }
}
