#include "matrix.hpp"
#include "constants.hpp"
#include "dbc.hpp"
#include <fmt/core.h>
#include <cmath>
#include <cstdlib>

using namespace fmt;
using std::min, std::max;

inline size_t next_x(size_t x, size_t width) {
  return (x + 1) * ((x + 1) < width);
}

inline size_t next_y(size_t x, size_t y) {
  return y + (x == 0);
}

inline bool at_end(size_t y, size_t height) {
  return y < height;
}

inline bool end_row(size_t x, size_t width) {
  return x == width - 1;
}


namespace matrix {

  each_cell::each_cell(Matrix &mat)
  {
    height = mat.size();
    width = mat[0].size();
  }

  bool each_cell::next() {
    x = next_x(x, width);
    y = next_y(x, y);
    return at_end(y, height);
  }

  each_row::each_row(Matrix &mat) :
    $mat(mat)
  {
    height = $mat.size();
    width = $mat[0].size();
  }

  bool each_row::next() {
    x = next_x(x, width);
    y = next_y(x, y);
    row = end_row(x, width);
    return at_end(y, height);
  }

  in_box::in_box(Matrix &mat, size_t at_x, size_t at_y, size_t size) :
    from_x(at_x), from_y(at_y)
  {
    size_t h = matrix::height(mat);
    size_t w = matrix::width(mat);

    // keeps it from going below zero
    // need extra -1 to compensate for the first next()
    left = max(from_x, size) - size;
    x = left - 1;  // must be -1 for next()
    // keeps it from going above width
    right = min(from_x + size + 1, w);

    // same for these two
    top = max(from_y, size) - size;
    y = top - (left == 0);
    bottom = min(from_y + size + 1, h);
  }

  bool in_box::next() {
    // calc next but allow to go to 0 for next
    x = next_x(x, right);
    // x will go to 0, which signals new line
    y = next_y(x, y);  // this must go here
    // if x==0 then this moves it to min_x
    x = max(x, left);
    // and done

    return at_end(y, bottom);
  }

  float in_box::distance() {
    int dx = from_x - x;
    int dy = from_y - y;

    return sqrt((dx * dx) + (dy * dy));
  }

  void in_box::dump() {
    println("BOX: x={},y={}; left={},right={}; top={},bottom={}",
        x, y, left, right, top, bottom);
  }

  compass::compass(Matrix &mat, size_t x, size_t y) :
    x(x), y(y)
  {
    array<int, 4> x_in{0, 1, 0, -1};
    array<int, 4> y_in{-1, 0, 1, 0};

    for(size_t i = 0; i < 4; i++) {
      int nx = x + x_in[i];
      int ny = y + y_in[i];
      if(matrix::inbounds(mat, nx, ny)) {
        x_dirs[max_dirs] = nx;
        y_dirs[max_dirs] = ny;
        max_dirs++;
      }
    }
  }

  bool compass::next() {
    dir++;
    if(dir < max_dirs) {
      x = x_dirs[dir];
      y = y_dirs[dir];
      return true;
    } else {
      return false;
    }
  }

  flood::flood(Matrix &mat, Point start, int old_val, int new_val) :
    mat(mat), start(start), old_val(old_val), new_val(new_val),
     x(start.x), y(start.y), dirs{mat, start.x, start.y}
  {
    dbc::check(old_val != new_val, "what you doing?");
    current_loc = start;
    q.push(start);
  }

  bool flood::next() {
    if(!q.empty()) {
      if(!dirs.next()) {
        // box is done reset it
        auto current_loc = q.front();
        q.pop();

        dirs = matrix::compass{mat, current_loc.x, current_loc.y};
        dirs.next();
      }

      // get the next thing
      if(mat[dirs.y][dirs.x] <= old_val) {
        mat[dirs.y][dirs.x] = new_val;
        x = dirs.x;
        y = dirs.y;

        q.push({.x=dirs.x, .y=dirs.y});
      }

      return true;
    } else {
      return false;
    }
  }

  line::line(Point start, Point end) :
      x(start.x), y(start.y),
      x1(end.x), y1(end.y)
  {
    dx = std::abs(x1 - x);
    sx = x < x1 ? 1 : -1;
    dy = std::abs(y1 - y) * -1;
    sy = y < y1 ? 1 : -1;
    error = dx + dy;
  }

  bool line::next() {
    if(x != x1 || y != y1) {
      int e2 = 2 * error;

      if(e2 >= dy) {
        error = error + dy;
        x = x + sx;
      }

      if(e2 <= dx) {
        error = error + dx;
        y = y + sy;
      }
      return true;
    } else {
      return false;
    }
  }


  circle::circle(Matrix &mat, Point center, float radius) :
    center_x(center.x), center_y(center.y), radius(radius)
  {
    width = matrix::width(mat);
    height = matrix::height(mat);
    top = max(int(floor(center_y - radius)), 0);
    bottom = min(int(floor(center_y + radius)), height - 1);

    y = top;
  }

  bool circle::next() {
    y++;
    if(y <= bottom) {
      dy = y - center_y;
      dx = floor(sqrt(radius * radius - dy * dy));
      left = max(0, int(center_x) - dx);
      right = min(width, int(center_x) + dx + 1);
      return true;
    } else {
      return false;
    }
  }

  void dump(const std::string &msg, Matrix &map, int show_x, int show_y) {
    println("----------------- {}", msg);

    for(each_row it{map}; it.next();) {
      int cell = map[it.y][it.x];

      if(int(it.x) == show_x && int(it.y) == show_y) {
        print("{:x}<", cell);
      } else if(cell == WALL_PATH_LIMIT) {
        print("# ");
      } else if(cell > 15) {
        print("* ");
      } else {
        print("{:x} ", cell);
      }

      if(it.row) print("\n");
    }
  }
}
