#include "autowalker.hpp"
#include "inventory.hpp"
#include "ai_debug.hpp"

template<typename Comp>
int number_left(gui::FSM& fsm) {
  int count = 0;

  fsm.$level.world->query<components::Position, Comp>(
    [&](const auto ent, auto&, auto&) {
        if(ent != fsm.$level.player) {
          count++;
        }
    });

  return count;
}

template<typename Comp>
Pathing compute_paths(gui::FSM& fsm) {
  auto& walls_original = fsm.$level.map->$walls;
  auto walls_copy = walls_original;

  Pathing paths{matrix::width(walls_copy), matrix::height(walls_copy)};

  fsm.$level.world->query<components::Position>(
  [&](const auto ent, auto& position) {
      if(ent != fsm.$level.player) {
        if(fsm.$level.world->has<Comp>(ent)) {
          paths.set_target(position.location);
        } else {
          // this will mark that spot as a wall so we don't path there temporarily
          walls_copy[position.location.y][position.location.x] = WALL_PATH_LIMIT;
        }
      }
  });

  paths.compute_paths(walls_copy);

  return paths;
}

void Autowalker::log(std::wstring msg) {
  fsm.$status_ui.log(msg);
}

void Autowalker::status(std::wstring msg) {
  fsm.$main_ui.$overlay_ui.show_text("bottom", msg);
}

void Autowalker::close_status() {
  fsm.$main_ui.$overlay_ui.close_text("bottom");
}

Pathing Autowalker::path_to_enemies() {
  return compute_paths<components::Combat>(fsm);
}

Pathing Autowalker::path_to_items() {
  return compute_paths<components::InventoryItem>(fsm);
}

Pathing Autowalker::path_to_devices() {
  return compute_paths<components::Device>(fsm);
}


void Autowalker::handle_window_events() {
  fsm.$window.handleEvents(
      [&](const sf::Event::KeyPressed &) {
        fsm.autowalking = false;
        close_status();
        log(L"Aborting autowalk.");
      },
      [&](const sf::Event::MouseButtonPressed &) {
        fsm.autowalking = false;
        close_status();
        log(L"Aborting autowalk.");
      }
  );
}

void Autowalker::process_combat() {
  while(fsm.in_state(gui::State::IN_COMBAT)
      || fsm.in_state(gui::State::ATTACKING))
  {
    if(fsm.in_state(gui::State::ATTACKING)) {
      send_event(gui::Event::TICK);
    } else {
      send_event(gui::Event::ATTACK);;
    }
  }
}

Point Autowalker::get_current_position() {
  auto& player_position = fsm.$level.world->get<components::Position>(fsm.$level.player);
  return player_position.location;
}

void Autowalker::path_fail(Matrix& bad_paths, Point pos) {
  status(L"PATH FAIL");
  log(L"Autowalk failed to find a path.");
  matrix::dump("MOVE FAIL PATHS", bad_paths, pos.x, pos.y);
  send_event(gui::Event::STAIRS_DOWN);
}

bool Autowalker::path_player(Pathing& paths, Point& target_out) {
  bool found = paths.random_walk(target_out, false, PATHING_TOWARD, 4, 8);

  if(!found) {
    // failed to find a linear path, try diagonal
    if(!paths.random_walk(target_out, false, PATHING_TOWARD, 8, 8)) {
      path_fail(paths.$paths, target_out);
      return false;
    }
  }

  if(!fsm.$level.map->can_move(target_out)) {
    path_fail(paths.$paths, target_out);
    return false;
  }

  return true;
}

void Autowalker::rotate_player(Point current, Point target) {
  int delta_x = int(target.x) - int(current.x);
  int delta_y = int(target.y) - int(current.y);

  int facing = fsm.$main_ui.$compass_dir;
  int target_facing = 0;

  /* This is a massive pile of garbage. Need a way
   * to determine player facing direction without
   * hacking into the compass, and also do accurate
   * turns.
   */
  if(delta_x == -1 && delta_y == 0) {
    // west
    target_facing = 4;
  } else if(delta_x == 1 && delta_y == 0) {
    // east
    target_facing = 0;
  } else if(delta_x == 0 && delta_y == 1) {
    // south
    target_facing = 2;
  } else if(delta_x == 0 && delta_y == -1) {
    // north
    target_facing = 6;
  } else if(delta_x == 1 && delta_y == -1) {
    // north east
    target_facing = 7;
  } else if(delta_x == 1 && delta_y == 1) {
    // south east
    target_facing = 1;
  } else if(delta_x == -1 && delta_y == 1) {
    // south west
    target_facing = 3;
  } else if(delta_x == -1 && delta_y == -1) {
    // north west
    target_facing = 5;
  } else {
    dbc::sentinel(
        fmt::format("got more than 8 direction result: "
          "current={},{} "
          "target={},{} "
          "delta={},{} ",
          current.x, current.y,
          target.x, target.y,
          delta_x, delta_y));
  }

  auto dir = facing > target_facing ? gui::Event::ROTATE_LEFT : gui::Event::ROTATE_RIGHT;

  while(facing != target_facing) {
    send_event(dir);
    facing = fsm.$main_ui.$compass_dir;
  }

  while(fsm.in_state(gui::State::ROTATING)) send_event(gui::Event::TICK);

  dbc::check(fsm.$main_ui.$compass_dir == target_facing,
      "player isn't facing the correct direction");
}

struct InventoryStats {
  int healing = 0;
  int other = 0;
};

ai::State Autowalker::update_state(ai::State start) {
  int enemy_count = number_left<components::Combat>(fsm);
  int item_count = number_left<components::InventoryItem>(fsm);

  ai::set(start, "no_more_enemies", enemy_count == 0);
  ai::set(start, "no_more_items", item_count == 0);
  ai::set(start, "enemy_found",
      fsm.in_state(gui::State::IN_COMBAT) ||
      fsm.in_state(gui::State::ATTACKING));
  ai::set(start, "health_good", player_health_good());
  ai::set(start, "in_combat",
      fsm.in_state(gui::State::IN_COMBAT) ||
      fsm.in_state(gui::State::ATTACKING));

  auto inv = player_item_count();
  ai::set(start, "have_item", inv.other > 0 || inv.healing > 0);
  ai::set(start, "have_healing", inv.healing > 0);

  return start;
}

void Autowalker::handle_boss_fight() {
  // skip the boss fight for now
  if(fsm.in_state(gui::State::NEXT_LEVEL)) {
    // eventually we'll have AI handle this too
    send_event(gui::Event::STAIRS_DOWN);
  }
}

void Autowalker::handle_player_walk(ai::State& start, ai::State& goal) {
  start = update_state(start);
  auto a_plan = ai::plan("Host::actions", start, goal);
  auto action = a_plan.script.front();

  if(action.name == "find_enemy") {
    // this is where to test if enemy found and update state
    status(L"FINDING ENEMY");
    auto paths = path_to_enemies();
    process_move(paths);
    send_event(gui::Event::ATTACK);
  } else if(action.name == "kill_enemy") {
    status(L"KILLING ENEMY");

    // TODO: find the enemy and then rotate toward them
    Point current = get_current_position();
    if(fsm.in_state(gui::State::IN_COMBAT)) {
      rotate_player(current, {current.x - 1, current.y - 1});
      dbc::log("TODO: you should find the enemy and face them instead of THIS GARBAGE!");
    }

    process_combat();
  } else if(action.name == "use_healing") {
    status(L"USING HEALING");
    player_use_healing();
  } else if(action.name == "collect_items") {
    status(L"COLLECTING ITEMS");
    auto paths = path_to_items();
    process_move(paths);
    // path to the items and get them all
  } else if(action == ai::FINAL_ACTION) {
    close_status();
    log(L"FINAL ACTION! Autowalk done.");
    fsm.autowalking = false;
    ai::dump_script("AUTOWALK", start, a_plan.script);
  } else {
    close_status();
    dbc::log(fmt::format("Unknown action: {}", action.name));
  }
}



void Autowalker::autowalk() {
  handle_window_events();
  if(!fsm.autowalking) {
    close_status();
    return;
  }

  int move_attempts = 0;

  auto start = ai::load_state("Host::initial_state");
  auto goal = ai::load_state("Host::final_state");

  do {
    handle_window_events();
    handle_boss_fight();
    handle_player_walk(start, goal);

    move_attempts++;
  } while(move_attempts < 100 && fsm.autowalking);
}

void Autowalker::process_move(Pathing& paths) {
  Point current = get_current_position();
  Point target = current;

  if(!path_player(paths, target)) {
    close_status();
    log(L"No paths found, aborting autowalk.");
    return;
  }

  rotate_player(current, target);

  send_event(gui::Event::MOVE_FORWARD);
  while(fsm.in_state(gui::State::MOVING)) send_event(gui::Event::TICK);
}

void Autowalker::send_event(gui::Event ev) {
  fsm.event(ev);
  fsm.render();
  fsm.handle_world_events();
}

bool Autowalker::player_health_good() {
  auto combat = fsm.$level.world->get<components::Combat>(fsm.$level.player);
  return float(combat.hp) / float(combat.max_hp) > 0.5f;
}

InventoryStats Autowalker::player_item_count() {
  auto& inventory = fsm.$level.world->get<components::Inventory>(fsm.$level.player);
  InventoryStats stats;

  for(auto& item : inventory.items) {
    if(item.data["id"] == "POTION_HEALING_SMALL") {
      stats.healing += item.count;
    } else {
      stats.other += item.count;
    }
  }

  return stats;
}

void Autowalker::player_use_healing() {
  auto& inventory = fsm.$level.world->get<components::Inventory>(fsm.$level.player);
  // find the healing slot
  for(size_t slot = 0; slot < inventory.count(); slot++) {
    auto& item = inventory.get(slot);
    if(item.data["id"] == "POTION_HEALING_SMALL") {
      inventory.use(fsm.$level, slot);
      fsm.$status_ui.update();
      return;
    }
  }
}

void Autowalker::start_autowalk() {
  fsm.autowalking = true;
}