#include "constants.hpp"
#include "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);
    }
  }
}

/*
 * Used https://github.com/HenrYxZ/dijkstra-map as a reference.
 */
void Pathing::compute_paths(Matrix &walls) {
  INVARIANT();
  dbc::check(walls[0].size() == $width,
      fmt::format("Pathing::compute_paths called with walls.width={} but paths $width={}", walls[0].size(), $width));

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

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

  Matrix closed = walls;
  PointList starting_pixels;
  PointList open_pixels;

  // First pass: Add starting pixels 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_pixels.emplace_back(x,y);
    }
  }

  // Second pass: Add border to open
  for(auto sp : starting_pixels) {
    add_neighbors(open_pixels, 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_pixels.empty(); ++counter) {
    PointList next_open;
    for(auto sp : open_pixels) {
      $paths[sp.y][sp.x] = counter;
      add_neighbors(next_open, closed, sp.y, sp.x);
    }
    open_pixels = next_open;
  }

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

void Pathing::set_target(const Point &at, int value) {
  // FUTURE: I'll eventually allow setting this to negatives for priority
  $input[at.y][at.x] = value;
}

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

/*
 * This is a weird discovery, but if you randomly select a starting point on
 * the 8 compass, but only check 4 directions from there, it does the best
 * pathing so far. It will walk around items, navigate around enemies, find
 * paths through corners, etc. If you change slice_count/dist_count to just
 * 4 it fails more frequently.
 *
 * Look in the autowalker.cpp:path_player function for an example of what
 * I'm doing.  I start with 4/8 and it finds paths 99% of the time, but
 * if that fails I do a full 8 direction search.  This weirdly finds the
 * best directions to go more often.
 */
bool Pathing::random_walk(Point &out, bool random,
    int direction, size_t slice_count, size_t dist_size)
{
  bool zero_found = false;

  // first 4 directions are n/s/e/w for most enemies
  std::array<Point, DIRECTION_MAX> dirs{{
      {out.x,out.y-1}, // north
      {out.x+1,out.y}, // east
      {out.x,out.y+1}, // south
      {out.x-1,out.y}, // west

      // the player and some enemies are more "agile"
      {out.x+1,out.y-1}, // north east
      {out.x+1,out.y+1}, // south east
      {out.x-1,out.y+1}, // south west
      {out.x-1,out.y-1} // north west
  }};

  dbc::check(slice_count <= dirs.size(), "slize_count must be <= DIRECTION_MAX");
  dbc::check(dist_size <= dirs.size(), "dist_size must be <= DIRECTION_MAX");

  // get the current dijkstra number
  int cur = $paths[out.y][out.x];

  // pick a random start of directions
  int rand_start = Random::uniform<int>(0, dist_size);

  // go through all possible directions
  for(size_t i = 0; i < slice_count; i++) {
    // but start at the random start, effectively randomizing
    // which valid direction to go
    // BUG: this might be wrong given the above ranom from 0-size
    Point dir = dirs[(i + rand_start) % dist_size];
    if(!shiterator::inbounds($paths, dir.x, dir.y)) continue; //skip unpathable stuff
    int weight = cur - $paths[dir.y][dir.x];

    if(weight == direction) {
      // no matter what we follow direct paths
      out = dir;
      return true;
    } else if(random && weight == 0) {
      // if random is selected and it's a 0 path take it
      out = dir;
      return true;
    } else if(weight == 0) {
      // otherwise keep the last zero path for after
      out = dir;
      zero_found = true;
    }
  }

  // if we reach this then either zero was found and
  // zero_found is set true, or it wasn't and nothing found
  return zero_found;
}

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;
}
