#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <string>
#include "config.hpp"
#include "matrix.hpp"
#include "rand.hpp"
#include "levelmanager.hpp"
#include <nlohmann/json.hpp>
#include <fstream>

using namespace nlohmann;
using namespace fmt;
using std::string;
using matrix::Matrix;

shared_ptr<Map> make_map() {
  // BUG? I mean, it's a shared_ptr so it should keep it around but....
  LevelManager levels;
  GameLevel level = levels.current();
  return level.map;
}

TEST_CASE("basic matrix iterator", "[matrix:basic]") {
  std::ifstream infile("./tests/dijkstra.json");
  json data = json::parse(infile);
  auto test = data[0];

  Matrix walls = test["walls"];

  // tests going through straight cells but also
  // using two iterators on one matrix (or two)
  matrix::each_cell cells{walls};
  cells.next(); // kick it off
  size_t row_count = 0;

  for(matrix::each_row it{walls};
      it.next(); cells.next())
  {
    REQUIRE(walls[cells.y][cells.x] == walls[it.y][it.x]);
    row_count += it.row;
  }

  REQUIRE(row_count == walls.size());

  {
    // test getting the correct height in the middle
    row_count = 0;
    matrix::box box{walls, 2,2, 1};

    while(box.next()) {
      row_count += box.x == box.left;
      walls[box.y][box.x] = 3;
    }
    //matrix::dump("2,2 WALLS", walls, 2, 2);

    REQUIRE(row_count == 3);
  }

  {
    // matrix::dump("1:1 POINT", walls, 1,1);
    // confirm boxes have the right number of rows
    // when x goes to 0 on first next call
    row_count = 0;
    matrix::box box{walls, 1, 1, 1};

    while(box.next()) {
      row_count += box.x == box.left;
    }
    REQUIRE(row_count == 3);
  }

  {
    matrix::compass star{walls, 1, 1};
    while(star.next()) {
      println("START IS {},{}=={}", star.x, star.y, walls[star.y][star.x]);
      walls[star.y][star.x] = 11;
    }
    // matrix::dump("STAR POINT", walls, 1,1);
  }
}

inline void random_matrix(Matrix &out) {
  for(size_t y = 0; y < out.size(); y++) {
    for(size_t x = 0; x < out[0].size(); x++) {
      out[y][x] = Random::uniform<int>(-10,10);
    }
  }
}

TEST_CASE("thrash matrix iterators", "[matrix]") {
  for(int count = 0; count < 5; count++) {
    size_t width = Random::uniform<size_t>(1, 100);
    size_t height = Random::uniform<size_t>(1, 100);

    Matrix test(height, matrix::Row(width));
    random_matrix(test);

    // first make a randomized matrix
    matrix::each_cell cells{test};
    cells.next(); // kick off the other iterator

    for(matrix::each_row it{test};
        it.next(); cells.next())
    {
      REQUIRE(test[cells.y][cells.x] == test[it.y][it.x]);
    }
  }
}

TEST_CASE("thrash box distance iterators", "[matrix:distance]") {
  size_t width = Random::uniform<size_t>(10, 21);
  size_t height = Random::uniform<size_t>(10, 25);

  Matrix result(height, matrix::Row(width));
  matrix::assign(result, 0);

  size_t size = Random::uniform<int>(4, 10);

  Point target{width/2, height/2};
  matrix::box box{result, target.x, target.y, size};
  while(box.next()) {
    result[box.y][box.x] = box.distance();
  }

  // matrix::dump(format("MAP {}x{} @ {},{}; BOX {}x{}; size: {}",
  //       matrix::width(result), matrix::height(result),
  //       target.x, target.y, box.right - box.left, box.bottom - box.top, size),
  //       result, target.x, target.y);
}

TEST_CASE("thrash box iterators", "[matrix]") {
  for(int count = 0; count < 5; count++) {
    size_t width = Random::uniform<size_t>(1, 25);
    size_t height = Random::uniform<size_t>(1, 33);

    Matrix test(height, matrix::Row(width));
    random_matrix(test);

    // this will be greater than the random_matrix cells
    int test_i = Random::uniform<size_t>(20,30);

    // go through every cell
    for(matrix::each_cell target{test}; target.next();) {
      PointList result;
      // make a random size box
      size_t size = Random::uniform<int>(1, 33);
      matrix::box box{test, target.x, target.y, size};

      while(box.next()) {
        test[box.y][box.x] = test_i;
        result.push_back({box.x, box.y});
      }

      for(auto point : result) {
        REQUIRE(test[point.y][point.x] == test_i);
        test[point.y][point.x] = 10;  // kind of reset it for another try
      }
    }
  }
}

TEST_CASE("thrash compass iterators", "[matrix:compass]") {
  for(int count = 0; count < 5; count++) {
    size_t width = Random::uniform<size_t>(1, 25);
    size_t height = Random::uniform<size_t>(1, 33);

    Matrix test(height, matrix::Row(width));
    random_matrix(test);

    // this will be greater than the random_matrix cells
    int test_i = Random::uniform<size_t>(20,30);

    // go through every cell
    for(matrix::each_cell target{test}; target.next();) {
      PointList result;
      // make a random size box
      matrix::compass compass{test, target.x, target.y};

      while(compass.next()) {
        test[compass.y][compass.x] = test_i;
        result.push_back({compass.x, compass.y});
      }

      for(auto point : result) {
        REQUIRE(test[point.y][point.x] == test_i);
        test[point.y][point.x] = 10;  // kind of reset it for another try
      }
    }
  }
}

TEST_CASE("prototype line algorithm", "[matrix:line]") {
  size_t width = Random::uniform<size_t>(10, 12);
  size_t height = Random::uniform<size_t>(10, 15);
  Map map(width,height);
  // create a target for the paths
  Point start{.x=map.width() / 2, .y=map.height()/2};

  for(matrix::box box{map.walls(), start.x, start.y, 3};
      box.next();)
  {
    Matrix result = map.walls();
    result[start.y][start.x] = 1;
    Point end{.x=box.x, .y=box.y};

    for(matrix::line it{start, end}; it.next();)
    {
      REQUIRE(map.inmap(it.x, it.y));
      result[it.y][it.x] = 15;
    }

    result[start.y][start.x] = 15;

    // matrix::dump("RESULT AFTER LINE", result, end.x, end.y);

    bool f_found = false;
    for(matrix::each_cell it{result}; it.next();) {
      if(result[it.y][it.x] == 15) {
        f_found = true;
        break;
      }
    }

    REQUIRE(f_found);
  }
}

TEST_CASE("prototype circle algorithm", "[matrix:circle]") {
  for(int count = 0; count < 5; count++) {
    size_t width = Random::uniform<size_t>(10, 13);
    size_t height = Random::uniform<size_t>(10, 15);
    int pos_mod = Random::uniform<int>(-3,3);
    Map map(width,height);

    // create a target for the paths
    Point start{.x=map.width() / 2 + pos_mod, .y=map.height()/2 + pos_mod};

    for(float radius = 1.0f; radius < 4.0f; radius += 0.1f) {
      // use an empty map
      Matrix result = map.walls();

      for(matrix::circle it{result, start, radius}; it.next();) {
        for(int x = it.left; x < it.right; x++) {
          // println("top={}, bottom={}, center.y={}, dy={}, left={}, right={}, x={}, y={}", it.top, it.bottom, it.center.y, it.dy, it.left, it.right, x, it.y);
          // println("RESULT {},{}", matrix::width(result), matrix::height(result));
          REQUIRE(it.y >= 0);
          REQUIRE(x >= 0);
          REQUIRE(it.y < int(matrix::height(result)));
          REQUIRE(x < int(matrix::width(result)));
          result[it.y][x] += 1;
        }
      }

      // matrix::dump(format("RESULT AFTER CIRCLE radius {}", radius), result, start.x, start.y);
    }
  }
}

TEST_CASE("viewport iterator", "[matrix:viewport]") {
  components::init();
  size_t width = Random::uniform<size_t>(20, 22);
  size_t height = Random::uniform<size_t>(21, 25);
  shared_ptr<Map> map = make_map();

  size_t view_width = width/2;
  size_t view_height = height/2;
  Point player;
  REQUIRE(map->place_entity(1, player));
  Point start = map->center_camera(player, view_width, view_height);

  size_t end_x = std::min(view_width, map->width() - start.x);
  size_t end_y = std::min(view_height, map->height() - start.y);

  matrix::viewport it{map->walls(), start, int(view_width), int(view_height)};

  for(size_t y = 0; y < end_y; ++y) {
    for(size_t x = 0; x < end_x && it.next(); ++x) {
      // still working on this
    }
  }
}

TEST_CASE("random rectangle", "[matrix:rando_rect]") {
  components::init();
  for(int i = 0; i < 5; i++) {
    shared_ptr<Map> map = make_map();
    map->invert_space();
    auto wall_copy = map->walls();

    for(size_t rnum = 0; rnum < map->room_count(); rnum++) {
      Room &room = map->room(rnum);
      Point pos;

      for(matrix::rando_rect it{map->walls(), room.x, room.y, room.width, room.height}; it.next();)
      {
        REQUIRE(size_t(it.x) >= room.x);
        REQUIRE(size_t(it.y) >= room.y);
        REQUIRE(size_t(it.x) <= room.x + room.width);
        REQUIRE(size_t(it.y) <= room.y + room.height);

        wall_copy[it.y][it.x] = wall_copy[it.y][it.x] + 5;
      }
    }
    // matrix::dump("WALLS FILLED", wall_copy);
  }
}

TEST_CASE("standard rectangle", "[matrix:rectangle]") {
  components::init();
  for(int i = 0; i < 5; i++) {
    shared_ptr<Map> map = make_map();
    auto wall_copy = map->walls();

    for(size_t rnum = 0; rnum < map->room_count(); rnum++) {
      Room &room = map->room(rnum);
      Point pos;

      for(matrix::rectangle it{map->walls(), room.x, room.y, room.width, room.height}; it.next();)
      {
        REQUIRE(size_t(it.x) >= room.x);
        REQUIRE(size_t(it.y) >= room.y);
        REQUIRE(size_t(it.x) <= room.x + room.width);
        REQUIRE(size_t(it.y) <= room.y + room.height);

        wall_copy[it.y][it.x] = wall_copy[it.y][it.x] + 5;
      }
    }

    // matrix::dump("WALLS FILLED", wall_copy);
  }
}
