#include "enemy_world_state.hpp"
#include "entity_health.hpp"
#include "unit.hpp"
#include "goap/goal.hpp"
#include "utils/godot_macros.hpp"
#include <godot_cpp/variant/utility_functions.hpp>
#include <cmath>

void EnemyWorldState::_bind_methods() {
#define CLASSNAME EnemyWorldState
    GDPROPERTY_HINTED(available_goals_inspector, gd::Variant::ARRAY, gd::PROPERTY_HINT_ARRAY_TYPE, GDRESOURCETYPE("Goal"));
}

void EnemyWorldState::_ready() {
    this->parent_unit->connect("plan_interrupted", callable_mp(this, &EnemyWorldState::queue_select_and_set_target));
    this->awareness_area = this->get_node<gd::Area3D>("%AwarenessArea");
    this->awareness_area->connect("body_entered", callable_mp(this, &EnemyWorldState::on_awareness_entered));
    this->health = this->get_node<EntityHealth>("%EntityHealth");
    this->health->connect("damage", callable_mp(this, &EnemyWorldState::on_damaged));
}

void EnemyWorldState::_process(double) {
    if(this->select_and_set_target_on_process) {
        this->select_and_set_target_on_process = false;
        this->select_and_set_target();
    }
}

void EnemyWorldState::on_awareness_entered(gd::Node3D *node) {
    if(Unit *unit{gd::Object::cast_to<Unit>(node)}) {
        this->add_aware_unit(unit);
    }
}

void EnemyWorldState::on_awareness_exited(gd::Node3D *node) {
    if(Unit *unit{gd::Object::cast_to<Unit>(node)})
        this->remove_aware_unit(unit);
}

void EnemyWorldState::on_damaged(EntityHealth *, int, Unit *source) {
    if(source != nullptr && !this->known_enemies.has(source))
        this->add_aware_unit(source);
    else if(!this->parent_unit->has_plan())
        this->select_and_set_target();
}

Unit *EnemyWorldState::select_target_from_known_with_priority(float *out_priority) {
    Unit *out{nullptr};
    *out_priority = -INFINITY;
    for(Unit *unit : this->known_enemies) {
        if(!this->get_can_see_node(unit))
            continue;
        float const priority{this->calculate_priority(unit)};
        if(priority > *out_priority) {
            out = unit;
            *out_priority = priority;
        }
    }
    return out;
}

Unit *EnemyWorldState::select_target_from_known() {
    float dummy{};
    return this->select_target_from_known_with_priority(&dummy);
}

void EnemyWorldState::queue_select_and_set_target() {
    this->select_and_set_target_on_process = true;
}

void EnemyWorldState::add_aware_unit(Unit *unit) {
    if(unit == nullptr)
        return;
    if(unit == this->parent_unit)
        return;
    if(this->known_enemies.has(unit))
        return;
    if(unit->get_entity_health()->is_dead())
        return;
    if(!this->get_is_unit_enemy(unit))
        return;
    unit->get_entity_health()->connect("death", this->aware_unit_death.bind(unit));
    this->known_enemies.insert(0, unit);
    if(!this->parent_unit->has_plan())
        this->try_set_target(this->select_target_from_known());
}

void EnemyWorldState::remove_aware_unit(Unit *unit) {
    if(unit == nullptr)
        return;
    if(!this->known_enemies.has(unit))
        return;
    unit->get_entity_health()->disconnect("death", this->aware_unit_death.bind(unit));
    this->known_enemies.erase(unit);
    if(unit == this->target_node)
        this->select_and_set_target();
}

void EnemyWorldState::on_aware_unit_death(Unit *, Unit *dead_entity) {
    this->on_awareness_exited(dead_entity);
}

void EnemyWorldState::select_and_set_target() {
    this->try_set_target(this->select_target_from_known());
}

float EnemyWorldState::calculate_priority(Unit *target) {
    return target->get_global_position().distance_squared_to(this->parent_unit->get_global_position());
}

gd::Ref<goap::Goal> EnemyWorldState::get_goal_for_target(Unit *unit) {
    gd::Node3D *store{this->target_node};
    this->target_node = unit;
    gd::Ref<goap::Goal> best_goal{};
    for(gd::Ref<goap::Goal> const &goal : this->available_goals) {
        if(!goal->check_requirements_met(this)) {
            continue;
        } else if(goal == this->last_goal) {
            best_goal = goal;
        } else {
            this->target_node = store;
            return goal;
        }
    }
    this->target_node = store;
    if(best_goal.is_valid())
        return best_goal;
    gd::UtilityFunctions::push_warning(this->get_path(), " failed to find goal for target ", unit->get_path());
    return nullptr;
}

void EnemyWorldState::try_set_target(Unit *unit) {
    if(unit == nullptr) {
        this->target_node = nullptr;
        this->parent_unit->stop_plan();
        return;
    }
    gd::Ref<goap::Goal> goal{this->get_goal_for_target(unit)};
    this->last_goal = goal;
    if(goal.is_valid())
        this->parent_unit->set_target_goal(unit, goal);
}

void EnemyWorldState::set_available_goals_inspector(gd::Array array) {
    this->available_goals.clear();
    for(int i{0}; i < array.size(); ++i)
        this->available_goals.push_back(array[i]);
}

gd::Array EnemyWorldState::get_available_goals_inspector() const {
    gd::Array a{};
    for(gd::Ref<goap::Goal> const &goal : this->available_goals)
        a.push_back(goal);
    return a;
}
