#include "constants.hpp"
#include "algos/pathing.hpp"
#include "dbc.hpp"
#include <vector>

using std::vector;

inline void add_neighbors(PointList &neighbors, Matrix &closed, size_t y, size_t x) {
  for(matrix::box it{closed, x, y, 1}; it.next();) {
    if(closed[it.y][it.x] == 0) {
      closed[it.y][it.x] = 1;
      neighbors.emplace_back(it.x, it.y);
    }
  }
}

int Pathing::compute_paths(Matrix &walls) {
  INVARIANT();
  dbc::check(walls[0].size() == $width,
      $F("Pathing::compute_paths called with walls.width={} but paths $width={}", walls[0].size(), $width));

  dbc::check(walls.size() == $height,
      $F("Pathing::compute_paths called with walls.height={} but paths $height={}", walls[0].size(), $height));

  // Initialize the new array with every cell at limit distance
  matrix::assign($paths, WALL_PATH_LIMIT);

  Matrix closed = walls;
  PointList starting_cells;
  PointList open_cells;

  // First pass: Add starting cells and put them in closed
  for(size_t counter = 0; counter < $height * $width; counter++) {
    size_t x = counter % $width;
    size_t y = counter / $width;
    if($input[y][x] == 0) {
      $paths[y][x] = 0;
      closed[y][x] = 1;
      starting_cells.emplace_back(x,y);
    }
  }

  // Second pass: Add border to open
  for(auto sp : starting_cells) {
    add_neighbors(open_cells, closed, sp.y, sp.x);
  }

  // Third pass: Iterate filling in the open list
  int counter = 1; // leave this here so it's available below
  for(; counter < WALL_PATH_LIMIT && !open_cells.empty(); ++counter) {
    PointList next_open;
    for(auto sp : open_cells) {
      $paths[sp.y][sp.x] = counter;
      add_neighbors(next_open, closed, sp.y, sp.x);
    }
    open_cells = next_open;
  }

  // Last pass: flood last cells
  for(auto sp : open_cells) {
    $paths[sp.y][sp.x] = counter;
  }

  return counter;
}

void Pathing::set_target(const Point &at, int value) {
  $input[at.y][at.x] = value;
}

void Pathing::clear_target(const Point &at) {
  $input[at.y][at.x] = 1;
}

PathingResult Pathing::find_path(Point &out, int direction, bool diag)
{
  // get the current dijkstra number
  int cur = $paths[out.y][out.x];
  int target = cur;
  // BUG: can I shortcut if target == 0 now?

  bool found = false;


  // a lambda makes it easy to capture what we have to change
  auto next_step = [&](size_t x, size_t y) -> bool {
    target = $paths[y][x];
    // don't go through walls
    // BUG: should actually do a collision check
    // BUG: can I shortcut if target == 0 now?
    if(target == WALL_PATH_LIMIT) return false;

    int weight = cur - target;

    if(weight == direction) {
      out = {x, y};
      found = true;
      // only break if this is a lower path
      return true;
    } else if(weight == 0) {
      out = {x, y};
      found = true;
      // only found an equal path, keep checking
    }

    // this says keep going
    return false;
  };


  if(diag) {
    // BUG: maybe a special alternative to box that doesn't do the central cell?
    for(matrix::box it{$paths, out.x, out.y, 1}; it.next();) {
      bool should_stop = next_step(it.x, it.y);
      if(should_stop) break;
    }
  } else {
    for(matrix::compass it{$paths, out.x, out.y}; it.next();) {
      bool should_stop = next_step(it.x, it.y);
      if(should_stop) break;
    }
  }

  if(target == 0) {
    return PathingResult::FOUND;
  } else if(!found) {
    return PathingResult::FAIL;
  } else {
    return PathingResult::CONTINUE;
  }
}

bool Pathing::INVARIANT() {
  using dbc::check;

  check($paths.size() == $height, "paths wrong height");
  check($paths[0].size() == $width, "paths wrong width");
  check($input.size() == $height, "input wrong height");
  check($input[0].size() == $width, "input wrong width");

  return true;
}

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