#include <catch2/catch_test_macros.hpp>
#include "dinkyecs.hpp"
#include <iostream>
#include <fmt/core.h>

using namespace fmt;
using DinkyECS::Entity;
using std::string;

struct Point {
  size_t x;
  size_t y;
};

struct Player {
  string name;
  Entity eid;
};

struct Position {
  Point location;
};

struct Motion {
  int dx;
  int dy;
  bool random=false;
};

struct Velocity {
  double x, y;
};

struct Gravity {
  double level;
};

struct DaGUI {
  int event;
};

/*
 * Using a function catches instances where I'm not copying
 * the data into the world.
 */
void configure(DinkyECS::World &world, Entity &test) {
  println("---Configuring the base system.");
  Entity test2 = world.entity();

  world.set<Position>(test, {10,20});
  world.set<Velocity>(test, {1,2});

  world.set<Position>(test2, {1,1});
  world.set<Velocity>(test2, {9,19});

  println("---- Setting up the player as a fact in the system.");

  auto player_eid = world.entity();
  Player player_info{"Zed", player_eid};
  // just set some player info as a fact with the entity id
  world.set_the<Player>(player_info);

  world.set<Velocity>(player_eid, {0,0});
  world.set<Position>(player_eid, {0,0});

  auto enemy = world.entity();
  world.set<Velocity>(enemy, {0,0});
  world.set<Position>(enemy, {0,0});

  println("--- Creating facts (singletons)");
  world.set_the<Gravity>({0.9});
}

TEST_CASE("confirm ECS system works", "[ecs]") {
  DinkyECS::World world;
  Entity test = world.entity();

  configure(world, test);

  Position &pos = world.get<Position>(test);
  REQUIRE(pos.location.x == 10);
  REQUIRE(pos.location.y == 20);

  Velocity &vel = world.get<Velocity>(test);
  REQUIRE(vel.x == 1);
  REQUIRE(vel.y == 2);

  world.query<Position>([](const auto &ent, auto &pos) {
      REQUIRE(ent > 0);
      REQUIRE(pos.location.x >= 0);
      REQUIRE(pos.location.y >= 0);
  });

  world.query<Velocity>([](const auto &ent, auto &vel) {
      REQUIRE(ent > 0);
      REQUIRE(vel.x >= 0);
      REQUIRE(vel.y >= 0);
  });

  println("--- Manually get the velocity in position system:");
  world.query<Position>([&](const auto &ent, auto &pos) {
      Velocity &vel = world.get<Velocity>(ent);

      REQUIRE(ent > 0);
      REQUIRE(pos.location.x >= 0);
      REQUIRE(pos.location.y >= 0);
      REQUIRE(ent > 0);
      REQUIRE(vel.x >= 0);
      REQUIRE(vel.y >= 0);
  });

  println("--- Query only entities with Position and Velocity:");
  world.query<Position, Velocity>([&](const auto &ent, auto &pos, auto &vel) {
      Gravity &grav = world.get_the<Gravity>();
      REQUIRE(grav.level <= 1.0f);
      REQUIRE(grav.level > 0.5f);
      REQUIRE(ent > 0);
      REQUIRE(pos.location.x >= 0);
      REQUIRE(pos.location.y >= 0);
      REQUIRE(ent > 0);
      REQUIRE(vel.x >= 0);
      REQUIRE(vel.y >= 0);
  });

  // now remove Velocity
  REQUIRE(world.has<Velocity>(test));
  world.remove<Velocity>(test);
  REQUIRE_THROWS(world.get<Velocity>(test));
  REQUIRE(!world.has<Velocity>(test));

  println("--- After remove test, should only result in test2:");
  world.query<Position, Velocity>([&](const auto &ent, auto &pos, auto &vel) {
      auto &in_position = world.get<Position>(ent);
      auto &in_velocity = world.get<Velocity>(ent);
      REQUIRE(pos.location.x >= 0);
      REQUIRE(pos.location.y >= 0);
      REQUIRE(in_position.location.x == pos.location.x);
      REQUIRE(in_position.location.y == pos.location.y);
      REQUIRE(in_velocity.x == vel.x);
      REQUIRE(in_velocity.y == vel.y);
  });
}

enum GUIEvent {
  HIT, MISS
};

TEST_CASE("confirm that the event system works", "[ecs]") {
  DinkyECS::World world;
  DinkyECS::Entity player = world.entity();

  world.send<GUIEvent>(GUIEvent::HIT, player, string{"hello"});

  bool ready = world.has_event<GUIEvent>();
  REQUIRE(ready == true);

  auto [event, entity, data] = world.recv<GUIEvent>();
  REQUIRE(event == GUIEvent::HIT);
  REQUIRE(entity == player);
  auto &str_data = std::any_cast<string&>(data);
  REQUIRE(string{"hello"} == str_data);

  ready = world.has_event<GUIEvent>();
  REQUIRE(ready == false);
}


TEST_CASE("confirm copying and constants", "[ecs-constants]") {
  DinkyECS::World world1;

  Player player_info{"Zed", world1.entity()};
  world1.set_the<Player>(player_info);

  world1.set<Position>(player_info.eid, {10,10});
  world1.make_constant(player_info.eid);

  DinkyECS::World world2;
  world1.clone_into(world2);

  auto &test1 = world1.get<Position>(player_info.eid);
  auto &test2 = world2.get<Position>(player_info.eid);

  REQUIRE(test2.location.x == test1.location.x);
  REQUIRE(test2.location.y == test1.location.y);

  // check for accidental reference
  test1.location.x = 100;
  REQUIRE(test2.location.x != test1.location.x);

  // test the facts copy over
  auto &player2 = world2.get_the<Player>();
  REQUIRE(player2.eid == player_info.eid);
}


TEST_CASE("test serialization with nlohmann::json", "[ecs-serialize]") {
  /*
  DinkyECS::ComponentMap component_map;
  DinkyECS::Component<Position>(component_map);
  DinkyECS::Component<Velocity>(component_map);
  DinkyECS::Component<Motion>(component_map);
  DinkyECS::Component<Gravity>(component_map);
  DinkyECS::Component<DaGUI>(component_map);

  auto data = R"(
    [
        {
            "_type": "Position",
            "location": {
              "x": 10,
              "y": 5
            }
        },
        {
            "_type": "Motion",
            "dx": 0,
            "dy": 1
        },
        {
            "_type": "Velocity",
            "x": 0.1,
            "y": 10.2
        }
    ]
  )"_json;

  DinkyECS::World world;
  DinkyECS::Entity ent1 = world.entity();
  DinkyECS::Entity ent2 = world.entity();

  DinkyECS::configure(component_map, world, ent1, data);
  DinkyECS::configure(component_map, world, ent2, data);

  world.query<Position, Motion>([&](const auto ent, auto &pos, auto &motion) {
      fmt::println("entity: {}; position={},{} and motion={},{} motion.random={}",
          ent, pos.location.x, pos.location.y,
          motion.dx, motion.dy, motion.random);
      REQUIRE(pos.location.x == 10);
      REQUIRE(pos.location.y == 5);
      REQUIRE(motion.dx == 0);
      REQUIRE(motion.dy == 1);
      REQUIRE(motion.random == false);
  });
  */
}