#include "dbc.hpp"
#include "ai.hpp"

namespace ai {
  using namespace nlohmann;
  using namespace dbc;

  static AIManager AIMGR;
  static bool initialized = false;

  inline void validate_profile(nlohmann::json& profile) {
    for(auto& [name_key, value] : profile.items()) {
      check(value < STATE_MAX,
        fmt::format("profile field {} has value {} greater than STATE_MAX {}", (std::string)name_key, (int)value, STATE_MAX));
    }
  }

  Action config_action(AIProfile& profile, nlohmann::json& config) {
    check(config.contains("name"), "config_action: action config missing name");
    check(config.contains("cost"), "config_action: action config missing cost");

    Action result(config["name"], config["cost"]);

    check(config.contains("needs"),
        fmt::format("config_action: no 'needs' field", result.name));
    check(config.contains("effects"),
        fmt::format("config_action: no 'effects' field", result.name));

    for(auto& [name_key, value] : config["needs"].items()) {
      check(profile.contains(name_key), fmt::format("config_action({}): profile does not have need named {}", result.name, name_key));
      result.needs(profile.at(name_key), bool(value));
    }

    for(auto& [name_key, value] : config["effects"].items()) {
      check(profile.contains(name_key), fmt::format("config_action({}): profile does not have effect named {}", result.name, name_key));

      result.effect(profile.at(name_key), bool(value));
    }

    return result;
  }

  State config_state(AIProfile& profile, nlohmann::json& config) {
    State result;

    for(auto& [name_key, value] : config.items()) {
      check(profile.contains(name_key), fmt::format("config_state: profile does not have name {}", name_key));

      int name_id = profile.at(name_key);
      result[name_id] = bool(value);
    }

    return result;
  }

  /*
   * This is only used in tests so I can load different fixtures.
   */
  void reset() {
    initialized = false;
    AIMGR.actions.clear();
    AIMGR.states.clear();
    AIMGR.scripts.clear();
    AIMGR.profile = json({});
  }

  void init(std::string config_path) {
    if(!initialized) {
      Config config(config_path);

      // profile specifies what keys (bitset indexes) are allowed
      // and how they map to the bitset of State
      validate_profile(config["profile"]);

      // relies on json conversion?
      AIMGR.profile = config["profile"];

      // load all actions
      auto& actions = config["actions"];
      for(auto& action_vars : actions) {
        auto the_action = config_action(AIMGR.profile, action_vars);
        AIMGR.actions.insert_or_assign(the_action.name, the_action);
      }

      // load all states
      auto& states = config["states"];
      for(auto& [name, state_vars] : states.items()) {
        auto the_state = config_state(AIMGR.profile, state_vars);
        AIMGR.states.insert_or_assign(name, the_state);
      }

      auto& scripts = config["scripts"];
      for(auto& [script_name, action_names] : scripts.items()) {
        std::vector<Action> the_script;

        for(auto name : action_names) {
          check(AIMGR.actions.contains(name),
              fmt::format("ai::init(): script {} uses action {} that doesn't exist",
                (std::string)script_name, (std::string)name));

          the_script.push_back(AIMGR.actions.at(name));
        }

        AIMGR.scripts.insert_or_assign(script_name, the_script);
      }
      initialized = true;
    } else {
      dbc::sentinel("DOUBLE INIT: AI manager should only be intialized once if not in tests.");
    }
  }

  void check_valid_action(std::string name, std::string msg) {
    dbc::check(AIMGR.actions.contains(name),
        fmt::format("{} tried to access action that doesn't exist {}",
            msg, name));
  }

  State load_state(std::string state_name) {
    check(initialized, "you forgot to initialize the AI first.");
    check(AIMGR.states.contains(state_name), fmt::format(
          "ai::load_state({}): state does not exist in config",
          state_name));

    return AIMGR.states.at(state_name);
  }

  Action load_action(std::string action_name) {
    check(initialized, "you forgot to initialize the AI first.");
    check(AIMGR.states.contains(action_name), fmt::format(
          "ai::load_action({}): action does not exist in config",
          action_name));
    return AIMGR.actions.at(action_name);
  }

  std::vector<Action> load_script(std::string script_name) {
    check(AIMGR.scripts.contains(script_name), fmt::format(
          "ai::load_script(): no script named {} configured", script_name));
    return AIMGR.scripts.at(script_name);
  }

  ActionPlan plan(std::string script_name, State start, State goal) {
    // BUG: could probably memoize here, since:
    // same script+same start+same goal will/should produce the same results

    check(initialized, "you forgot to initialize the AI first.");
    auto script = load_script(script_name);
    return plan_actions(script, start, goal);
  }

  int state_id(std::string name) {
    check(AIMGR.profile.contains(name), fmt::format(
          "ai::state_id({}): id is not configured in profile",
          name));
    return AIMGR.profile.at(name);
  }

  void set(State& state, std::string name, bool value) {
    // resort by best fit
    state.set(state_id(name), value);
  }

  bool test(State state, std::string name) {
    return state.test(state_id(name));
  }

  void EntityAI::fit_sort() {
    if(active()) {
      std::sort(plan.script.begin(), plan.script.end(),
          [&](auto& l, auto& r) {
            int l_cost = l.cost + ai::distance_to_goal(start, goal);
            int r_cost = r.cost + ai::distance_to_goal(start, goal);
            return l_cost < r_cost;
          });
    }
  }

  std::string& EntityAI::wants_to() {
    return plan.script[0].name;
  }

  bool EntityAI::wants_to(std::string name) {
    ai::check_valid_action(name, "EntityAI::wants_to");
    return plan.script.size() > 0 && plan.script[0].name == name;
  }

  bool EntityAI::active() {
    if(plan.script.size() == 1) {
      return plan.script[0] != FINAL_ACTION;
    } else {
      return plan.script.size() != 0;
    }
  }

  void EntityAI::set_state(std::string name, bool setting) {
    fit_sort();
    ai::set(start, name, setting);
  }

  bool EntityAI::get_state(std::string name) {
    return ai::test(start, name);
  }

  void EntityAI::update() {
    plan = ai::plan(script, start, goal);
    fit_sort();
  }

  AIProfile* profile() {
    return &AIMGR.profile;
  }

}
