#include "autowalker.hpp"
#include "ai_debug.hpp"
#include "gui/ritual_ui.hpp"
#include "game_level.hpp"
#include "systems.hpp"

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

template<typename Comp>
int number_left() {
  int count = 0;
  auto world = GameDB::current_world();
  auto player = GameDB::the_player();

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

  return count;
}

template<typename Comp>
Pathing compute_paths() {
  auto& level = GameDB::current_level();
  auto walls_copy = level.map->$walls;

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

  System::multi_path<Comp>(level, paths, walls_copy);

  return paths;
}

DinkyECS::Entity Autowalker::camera_aim() {
  auto& level = GameDB::current_level();
  auto player_pos = GameDB::player_position();

  // what happens if there's two things at that spot
  if(level.collision->something_there(player_pos.aiming_at)) {
    return level.collision->get(player_pos.aiming_at);
  } else {
    return DinkyECS::NONE;
  }
}

void Autowalker::log(std::wstring msg) {
  fsm.$map_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>();
}

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

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

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

bool Autowalker::path_player(Pathing& paths, Point& target_out) {
  auto& level = GameDB::current_level();
  auto found = paths.find_path(target_out, PATHING_TOWARD, false);

  if(found == PathingResult::FAIL) {
    // failed to find a linear path, try diagonal
    if(paths.find_path(target_out, PATHING_TOWARD, true) == PathingResult::FAIL) {
      path_fail("random_walk", paths.$paths, target_out);
      return false;
    }
  }

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

  return true;
}

void Autowalker::rotate_player(Point target) {
  auto &player = GameDB::player_position();

  if(target == player.location) {
    dbc::log("player stuck at a locatoin");
    fsm.autowalking = false;
    return;
  }

  auto dir = System::shortest_rotate(player.location, player.aiming_at, target);

  for(int i = 0; player.aiming_at != target; i++) {
    if(i > 10) {
      dbc::log("HIT OVER ROTATE BUG!");
      break;
    }

    send_event(dir);

    while(fsm.in_state(gui::State::ROTATING) ||
        fsm.in_state(gui::State::COMBAT_ROTATE))
    {
      send_event(gui::Event::TICK);
    }
  }

  fsm.autowalking = player.aiming_at == target;
}

void Autowalker::update_state(ai::EntityAI& player_ai) {
  int enemy_count = number_left<components::Combat>();
  int item_count = number_left<components::InventoryItem>();

  player_ai.set_state("no_more_enemies", enemy_count == 0);
  player_ai.set_state("no_more_items", item_count == 0);

  player_ai.set_state("enemy_found", found_enemy());
  player_ai.set_state("health_good", player_health_good());

  player_ai.set_state("in_combat",
      fsm.in_state(gui::State::IN_COMBAT) ||
      fsm.in_state(gui::State::ATTACKING));

  auto inv = player_item_count();
  player_ai.set_state("have_item", inv.other > 0 || inv.healing > 0);
  player_ai.set_state("have_healing", inv.healing > 0);

  player_ai.update();
}

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);
    face_enemy();
  }
}

void Autowalker::handle_player_walk(ai::State& start, ai::State& goal) {
  ai::EntityAI player_ai("Host::actions", start, goal);
  update_state(player_ai);
  auto level = GameDB::current_level();

  if(player_ai.wants_to("find_enemy")) {
    status(L"FINDING ENEMY");
    auto paths = path_to_enemies();
    process_move(paths, [&](auto target) -> bool {
       return level.collision->occupied(target);
    });
    face_enemy();
  } else if(player_ai.wants_to("kill_enemy")) {
    status(L"KILLING ENEMY");

    if(fsm.in_state(gui::State::IN_COMBAT)) {
      if(face_enemy()) {
        process_combat();
      }
    }
  } else if(player_ai.wants_to("use_healing")) {
    status(L"USING HEALING");
    player_use_healing();
  } else if(player_ai.wants_to("collect_items") || player_ai.wants_to("find_healing")) {
    fmt::println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    status(player_ai.wants_to("collect_items") ? L"COLLECTING ITEMS" : L"FIND HEALING");
    player_ai.dump();

    auto paths = path_to_items();

    bool found_it = process_move(paths, [&](auto target) -> bool {
      if(!level.collision->something_there(target)) return false;

      auto entity = level.collision->get(target);
      return (
          level.world->has<components::Curative>(entity) ||
          level.world->has<ritual::JunkPile>(entity));
    });

    if(found_it) pickup_item();
    fmt::println("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
  } else if(!player_ai.active()) {
    close_status();
    log(L"FINAL ACTION! Autowalk done.");
    fsm.autowalking = false;
  } else {
    close_status();
    dbc::log(fmt::format("Unknown action: {}", player_ai.to_string()));
  }
}

void Autowalker::craft_weapon() {
  if(!weapon_crafted) {
    auto& ritual_ui = fsm.$status_ui.$ritual_ui;
    fsm.$status_ui.$gui.click_on("ritual_ui");

    while(!ritual_ui.in_state(gui::ritual::State::OPENED)) {
      send_event(gui::Event::TICK);
    }

    ritual_ui.$gui.click_on("inv_slot0");
    send_event(gui::Event::TICK);
    ritual_ui.$gui.click_on("inv_slot1");
    send_event(gui::Event::TICK);

    while(!ritual_ui.in_state(gui::ritual::State::CRAFTING)) {
      send_event(gui::Event::TICK);
    }

    ritual_ui.$gui.click_on("result_image", true);
    send_event(gui::Event::TICK);

    ritual_ui.$gui.click_on("ritual_ui");
    send_event(gui::Event::TICK);
    weapon_crafted = true;
  }
}

void Autowalker::open_map() {
  if(!map_opened_once) {
    if(!fsm.$map_open) {
      send_event(gui::Event::MAP_OPEN);
      map_opened_once = true;
    }
  }
}

void Autowalker::close_map() {
  if(fsm.$map_open) {
    send_event(gui::Event::MAP_OPEN);
  }
}

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

  craft_weapon();
  open_map();
  face_enemy();

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

    close_map();

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

bool Autowalker::process_move(Pathing& paths, std::function<bool(Point)> is_that_it) {
  // target has to start at the player location then...
  auto target_out = GameDB::player_position().location;

  // ... target gets modified as an out parameter to find the path
  if(!path_player(paths, target_out)) {
    close_status();
    log(L"No paths found, aborting autowalk.");
    return false;
  }

  if(rayview->aiming_at != target_out) rotate_player(target_out);

  bool found_it = is_that_it(target_out);

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

  return found_it;
}

bool Autowalker::found_enemy() {
  auto& level = GameDB::current_level();
  auto player = GameDB::player_position();

  for(matrix::compass it{level.map->$walls, player.location.x, player.location.y}; it.next();) {
    Point aim{it.x, it.y};
    auto aimed_ent = level.collision->occupied_by(player.aiming_at);
    if(aim != player.aiming_at || aimed_ent == DinkyECS::NONE) continue;

    if(level.world->has<components::Combat>(aimed_ent)) return true;
  }

  return false;
}

bool Autowalker::found_item() {
  auto world = GameDB::current_world();
  auto aimed_at = camera_aim();
  return aimed_at != DinkyECS::NONE && world->has<components::InventoryItem>(aimed_at);
}

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

bool Autowalker::player_health_good() {
  auto world = GameDB::current_world();
  auto player = GameDB::the_player();
  auto combat = world->get<components::Combat>(player);
  float health = float(combat.hp) / float(combat.max_hp);
  fmt::println("!!!!!!!!!! HEALTH: {}", health);
  return health > 0.5f;
}

InventoryStats Autowalker::player_item_count() {
  InventoryStats stats;
  auto& level = GameDB::current_level();
  auto& inventory = level.world->get<inventory::Model>(level.player);

  if(inventory.has("pocket_r")) {
    stats.other += 1;
    stats.healing += 1;
  }

  if(inventory.has("pocket_l")) {
    stats.other += 1;
    stats.healing += 1;
  }

  return stats;
}

void Autowalker::player_use_healing() {
  auto& level = GameDB::current_level();
  auto& inventory = level.world->get<inventory::Model>(level.player);

  if(inventory.has("pocket_r")) {
    auto gui_id = fsm.$status_ui.$gui.entity("pocket_r");
    send_event(gui::Event::USE_ITEM, gui_id);
  }

  if(inventory.has("pocket_l")) {
    auto gui_id = fsm.$status_ui.$gui.entity("pocket_l");
    send_event(gui::Event::USE_ITEM, gui_id);
  }
}

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

void Autowalker::face_target(Point target) {
  if(rayview->aiming_at != target) rotate_player(target);
}

bool Autowalker::face_enemy() {
  auto& level = GameDB::current_level();
  auto player_at = GameDB::player_position();

  auto [found, neighbors] = level.collision->neighbors(player_at.location, true);

  if(found) {
    auto enemy_pos = level.world->get<components::Position>(neighbors[0]);
    face_target(enemy_pos.location);
  } else {
    dbc::log("No enemies nearby, moving on.");
  }

  return found;
}

void Autowalker::click_inventory(const std::string& name, guecs::Modifiers mods) {
  auto& cell = fsm.$status_ui.$gui.cell_for(name);
  fsm.$status_ui.mouse(cell.mid_x, cell.mid_y, mods);
  fsm.handle_world_events();
}

void Autowalker::pocket_potion(GameDB::Level &level) {
  auto& inventory = level.world->get<inventory::Model>(level.player);

  if(inventory.has("pocket_r") && inventory.has("pocket_l")) {
    player_use_healing();
  }

  send_event(gui::Event::AIM_CLICK);

  if(inventory.has("pocket_r")) {
    click_inventory("pocket_l", {1 << guecs::ModBit::left});
  } else {
    click_inventory("pocket_r", {1 << guecs::ModBit::left});
  }
}

void Autowalker::pickup_item() {
  auto& level = GameDB::current_level();
  auto& player_pos = GameDB::player_position();
  auto collision = level.collision;

  if(collision->something_there(player_pos.aiming_at)) {
    auto entity = collision->get(player_pos.aiming_at);
    fmt::println("AIMING AT entity {} @ {},{}",
        entity, player_pos.aiming_at.x, player_pos.aiming_at.y);

    if(level.world->has<components::Curative>(entity)) {
      pocket_potion(level);
      status(L"A POTION");
    } else {
      send_event(gui::Event::AIM_CLICK);
      status(L"I DON'T KNOW");
    }
  }
}
