#include "dbc.hpp"
#include "goap.hpp"
#include "ai_debug.hpp"
#include "stats.hpp"

namespace ai {

  using namespace nlohmann;
  using namespace dbc;

  bool is_subset(State& source, State& target) {
    State result = source & target;
    return result == target;
  }

  void Action::needs(int name, bool val) {
    if(val) {
      $positive_preconds[name] = true;
      $negative_preconds[name] = false;
    } else {
      $negative_preconds[name] = true;
      $positive_preconds[name] = false;
    }
  }

  void Action::effect(int name, bool val) {
    if(val) {
      $positive_effects[name] = true;
      $negative_effects[name] = false;
    } else {
      $negative_effects[name] = true;
      $positive_effects[name] = false;
    }
  }

  void Action::ignore(int name) {
    $positive_preconds[name] = false;
    $negative_preconds[name] = false;
  }

  bool Action::can_effect(State& state) {
    return ((state & $positive_preconds) == $positive_preconds) &&
      ((state & $negative_preconds) == ALL_ZERO);
  }

  State Action::apply_effect(State& state) {
    return (state | $positive_effects) & ~$negative_effects;
  }

  int distance_to_goal(State from, State to) {
    auto result = from ^ to;
    return result.count();
  }

  Script reconstruct_path(std::unordered_map<Action, Action>& came_from, Action& current) {
    Script total_path{current};
    bool final_found = false;

    for(size_t i = 0; i <= came_from.size() && came_from.contains(current); i++) {
      current = came_from.at(current);
      if(current != FINAL_ACTION) {
        total_path.push_front(current);
      } else {
        final_found = true;
      }
    }

    // this here temporarily while I figure out cycle detects/prevention
    if(!final_found && total_path[0] != FINAL_ACTION) {
      auto error = fmt::format("!!!!! You may have a cycle in your json. No FINAL found. Here's the path: ");
      for(auto& action : total_path) error += fmt::format("{} ", action.name);
      dbc::sentinel(error);
    }

    return total_path;
  }

  inline int h(State start, State goal, Action& action) {
    (void)action; // not sure if cost goes here or on d()
    return distance_to_goal(start, goal) + action.cost;
  }

  inline int d(State start, State goal, Action& action) {
    return distance_to_goal(start, goal) + action.cost;
  }

  ActionState find_lowest(std::unordered_map<ActionState, int>& open_set) {
    check(!open_set.empty(), "open set can't be empty in find_lowest");
    const ActionState *result = nullptr;
    int lowest_score = SCORE_MAX;

    for(auto& kv : open_set) {
      if(kv.second < lowest_score) {
        lowest_score = kv.second;
        result = &kv.first;
      }
    }

    return *result;
  }

  ActionPlan plan_actions(std::vector<Action>& actions, State start, State goal) {
    std::unordered_map<ActionState, int> open_set;
    std::unordered_map<State, bool> closed_set;
    std::unordered_map<Action, Action> came_from;
    std::unordered_map<State, int> g_score;
    ActionState current{FINAL_ACTION, start};

    g_score[start] = 0;
    open_set.insert_or_assign(current, g_score[start] + h(start, goal, current.action));

    while(!open_set.empty()) {
      current = find_lowest(open_set);

      if(is_subset(current.state, goal)) {
        return {true,
          reconstruct_path(came_from, current.action)};
      }

      open_set.erase(current);
      closed_set.insert_or_assign(current.state, true);

      for(auto& neighbor_action : actions) {
        // calculate the State being current/neighbor
        if(!neighbor_action.can_effect(current.state)) {
          continue;
        }

        auto neighbor = neighbor_action.apply_effect(current.state);
        if(closed_set.contains(neighbor)) continue;

        int d_score = d(current.state, neighbor, current.action);
        int tentative_g_score = g_score[current.state] + d_score;
        int neighbor_g_score = g_score.contains(neighbor) ? g_score[neighbor] : SCORE_MAX;
        if(tentative_g_score < neighbor_g_score) {
          came_from.insert_or_assign(neighbor_action, current.action);

          g_score[neighbor] = tentative_g_score;
          // open_set gets the fScore
          ActionState neighbor_as{neighbor_action, neighbor};

          int score = tentative_g_score + h(neighbor, goal, neighbor_as.action);
          // could maintain lowest here and avoid searching all things
          open_set.insert_or_assign(neighbor_as, score);
        }
      }
    }

    return {false, reconstruct_path(came_from, current.action)};
  }
}