#include <fmt/core.h>
#include <string>
#include "rand.hpp"
#include "constants.hpp"
#include "maze.hpp"

using std::string;
using matrix::Matrix;

namespace maze {
  inline size_t rand(size_t i, size_t j) {
    if(i < j) {
      return Random::uniform(i, j);
    } else if(j < i) {
      return Random::uniform(j, i);
    } else {
      return i;
    }
  }


  inline bool complete(Matrix& maze) {
    size_t width = matrix::width(maze);
    size_t height = matrix::height(maze);

    for(size_t row = 1; row < height; row += 2) {
      for(size_t col = 1; col < width; col += 2) {
        if(maze[row][col] != 0) return false;
      }
    }

    return true;
  }

  std::vector<Point> neighborsAB(Matrix& maze, Point on) {
    std::vector<Point> result;

    std::array<Point, 4> points{{
      {on.x, on.y - 2},
        {on.x, on.y + 2},
        {on.x - 2, on.y},
        {on.x + 2, on.y}
    }};

    for(auto point : points) {
      if(matrix::inbounds(maze, point.x, point.y)) {
        result.push_back(point);
      }
    }

    return result;
  }

  std::vector<Point> neighbors(Matrix& maze, Point on) {
    std::vector<Point> result;

    std::array<Point, 4> points{{
      {on.x, on.y - 2},
        {on.x, on.y + 2},
        {on.x - 2, on.y},
        {on.x + 2, on.y}
    }};

    for(auto point : points) {
      if(matrix::inbounds(maze, point.x, point.y)) {
        if(maze[point.y][point.x] == WALL_VALUE) {
          result.push_back(point);
        }
      }
    }

    return result;
  }

  inline std::pair<Point, Point> find_coord(Matrix& maze) {
    size_t width = matrix::width(maze);
    size_t height = matrix::height(maze);

    for(size_t y = 1; y < height; y += 2) {
      for(size_t x = 1; x < width; x += 2) {
        if(maze[y][x] == WALL_VALUE) {
          auto found = neighborsAB(maze, {x, y});

          for(auto point : found) {
            if(maze[point.y][point.x] == 0) {
              return {{x, y}, point};
            }
          }
        }
      }
    }

    matrix::dump("BAD MAZE", maze);
    dbc::sentinel("failed to find coord?");
  }

  void Builder::randomize_rooms() {
    // use those dead ends to randomly place rooms
    for(auto at : $dead_ends) {
      if(Random::uniform(0,1)) {
        size_t offset = Random::uniform(0,1);
        Room cur{at.x+offset, at.y+offset, 1, 1};
        $rooms.push_back(cur);
      }
    }
  }

  void Builder::init() {
    matrix::assign($walls, WALL_VALUE);
  }

  void Builder::divide(Point start, Point end) {
    for(matrix::line it{start, end}; it.next();) {
      $walls[it.y][it.x] = 0;
      $walls[it.y+1][it.x] = 0;
    }
  }

  void Builder::hunt_and_kill(Point on) {
    for(auto& room : $rooms) {
      for(matrix::box it{$walls, room.x, room.y, room.width}; it.next();) {
        $walls[it.y][it.x] = 0;
      }
    }

    while(!complete($walls)) {
      auto n = neighbors($walls, on);
      if(n.size() == 0) {
        $dead_ends.push_back(on);
        auto t = find_coord($walls);
        on = t.first;
        $walls[on.y][on.x] = 0;
        size_t row = (on.y + t.second.y) / 2;
        size_t col = (on.x + t.second.x) / 2;
        $walls[row][col] = 0;
      } else {
        auto nb = n[rand(size_t(0), n.size() - 1)];
        $walls[nb.y][nb.x] = 0;

        size_t row = (nb.y + on.y) / 2;
        size_t col = (nb.x + on.x) / 2;
        $walls[row][col] = 0;
        on = nb;
      }
    }

    for(auto at : $dead_ends) {
      for(auto& room : $rooms) {
        Point room_ul{room.x - room.width - 1, room.y - room.height - 1};
        Point room_lr{room.x + room.width - 1, room.y + room.height - 1};

        if(at.x >= room_ul.x && at.y >= room_ul.y &&
            at.x <= room_lr.x && at.y <= room_lr.y)
        {
          for(matrix::compass it{$walls, at.x, at.y}; it.next();) {
            if($walls[it.y][it.x] == 1) {
              $walls[it.y][it.x] = 0;
              break;
            }
          }
        }
      }
    }
  }

  void Builder::inner_donut(float outer_rad, float inner_rad) {
    size_t x = matrix::width($walls) / 2;
    size_t y = matrix::height($walls) / 2;

    for(matrix::circle it{$walls, {x, y}, outer_rad};
        it.next();)
    {
      for(int x = it.left; x < it.right; x++) {
        $walls[it.y][x] = 0;
      }
    }

    for(matrix::circle it{$walls, {x, y}, inner_rad};
        it.next();)
    {
      for(int x = it.left; x < it.right; x++) {
        $walls[it.y][x] = 1;
      }
    }
  }

  void Builder::inner_box(size_t outer_size, size_t inner_size) {
    size_t x = matrix::width($walls) / 2;
    size_t y = matrix::height($walls) / 2;

    for(matrix::box it{$walls, x, y, outer_size};
        it.next();)
    {
      $walls[it.y][it.x] = 0;
    }

    for(matrix::box it{$walls, x, y, inner_size};
        it.next();)
    {
      $walls[it.y][it.x] = 1;
    }
  }


  void Builder::remove_dead_ends() {
    dbc::check($dead_ends.size() > 0, "you have to run an algo first, no dead_ends to remove");
    for(auto at : $dead_ends) {
      for(matrix::compass it{$walls, at.x, at.y}; it.next();) {
        if($walls[it.y][it.x] == 0) {
          int diff_x = at.x - it.x;
          int diff_y = at.y - it.y;
          $walls[at.y + diff_y][at.x + diff_x] = 0;
        }
      }
    }
  }

  void Builder::dump(const std::string& msg) {
    matrix::dump(msg, $walls);
  }
}