#include "unit_world_state.hpp"
#include "entity_health.hpp"
#include "unit.hpp"
#include "utils/godot_macros.hpp"
#include "utils/util_functions.hpp"
#include <godot_cpp/classes/physics_ray_query_parameters3d.hpp>
#include <godot_cpp/classes/world3d.hpp>
#include <godot_cpp/classes/physics_direct_space_state3d.hpp>
#include <godot_cpp/variant/callable_method_pointer.hpp>
#include <godot_cpp/variant/utility_functions.hpp>

void UnitWorldState::_bind_methods() {
#define CLASSNAME UnitWorldState
    GDSIGNAL("attention_changed");
    GDFUNCTION(get_can_see_target);
    GDFUNCTION(get_is_target_dead);
    GDFUNCTION(get_is_at_target);
    GDFUNCTION(get_has_target);
    GDFUNCTION(get_target_node);
    GDFUNCTION(get_is_target_enemy);
    GDFUNCTION(get_is_in_melee_range);
    GDFUNCTION(get_is_health_safe);
    GDFUNCTION(get_parent_global_position);
    GDFUNCTION(get_target_global_position);
}

void UnitWorldState::_enter_tree() { GDGAMEONLY();
    this->parent_unit = gd::Object::cast_to<Unit>(this->get_parent());
    this->agent = this->get_node<gd::NavigationAgent3D>("%NavigationAgent3D");
    this->eye_location = this->get_node<gd::Node3D>("%Eyes");
    this->health = this->get_node<EntityHealth>("%EntityHealth");
}

gd::Variant UnitWorldState::get_world_property(gd::String property) {
    if(this->cache.has(property)) {
        CachedWorldProperty prop{this->cache[property]};
        if(prop.auto_invalidate_time > utils::time_seconds()) {
            return prop.value;
        }
    }
    return ActorWorldState::get_world_property(property);
}

bool UnitWorldState::get_can_see_target() {
    bool value{this->get_can_see_node(this->target_node)};
    this->cache_property("can_see_target", value, utils::time_seconds() + 0.5);
    return value;
}

bool UnitWorldState::get_can_see_node(gd::Node3D *node) const {
    // check if the target has a separate vision target, or is it's own vision target
    gd::Node3D *vision_target{node->get_node<gd::Node3D>("VisionTarget")};
    gd::Transform3D const eyes{this->eye_location->get_global_transform()};
    gd::Vector3 const target_position{vision_target ? vision_target->get_global_position() : node->get_global_position()};

    // construct list for ignored objects including target node and parent unit
    gd::TypedArray<gd::RID> ignore_list{this->parent_unit->get_rid()};
    // ignore target if it is a collision object that might obstruct it's own collision test
    if(gd::CollisionObject3D *as_collision_object{gd::Object::cast_to<gd::CollisionObject3D>(node)})
        ignore_list.push_back(as_collision_object->get_rid());
    // construct raycast query from unit's eye node to vision target. Ignoring parent unit and target if applicable
    gd::Ref<gd::PhysicsRayQueryParameters3D> const query{gd::PhysicsRayQueryParameters3D::create(
        eyes.origin,
        target_position,
        0x1 | 0x4, ignore_list
    )};
    return this->parent_unit->get_world_3d()->get_direct_space_state()->intersect_ray(query).is_empty();
}

bool UnitWorldState::get_is_at_target() const {
    if(this->target_node == nullptr) return true;
    gd::Vector3 const target = this->target_node->get_global_position();
    gd::Vector3 const current = this->parent_unit->get_global_position();
    float const min_dist = this->agent->get_target_desired_distance();
    return (target - current).length_squared() <= min_dist * min_dist;
}

bool UnitWorldState::get_has_target() const {
    return this->target_node != nullptr;
}

bool UnitWorldState::get_is_target_dead() const {
    if(EntityHealth *health{this->target_node->get_node<EntityHealth>("%EntityHealth")})
        return health->get_injury_current() <= 0.f;
    return false;
}

bool UnitWorldState::get_is_target_unit() const {
    return gd::ClassDB::is_parent_class(this->target_node->get_class(), "Unit");
}

bool UnitWorldState::get_is_target_enemy() const {
    Unit *unit{gd::Object::cast_to<Unit>(this->target_node)};
    return unit != nullptr && this->get_is_unit_enemy(unit);
}

bool UnitWorldState::get_is_unit_enemy(Unit *unit) const {
    return unit != nullptr
        && unit->get_team() != UnitTeam::Neutral
        && unit->get_team() != this->parent_unit->get_team();
}

bool UnitWorldState::get_is_in_melee_range() const {
    return this->target_node != nullptr
        && this->target_node->get_global_position()
                .distance_squared_to(this->parent_unit->get_global_position()) <= 2.f * 2.f;
}

bool UnitWorldState::get_is_health_safe() const {
    return float(this->health->get_injury_current()) > (float(this->health->get_injury_max()) / 2.f);
}

gd::Vector3 UnitWorldState::get_parent_global_position() const {
    return this->parent_unit->get_global_position();
}

gd::Vector3 UnitWorldState::get_target_global_position() const {
    return this->target_node == nullptr ? gd::Vector3{0.f, 0.f, 0.f} : this->target_node->get_global_position();
}

void UnitWorldState::set_target_node(gd::Node3D *node) {
    this->cache.clear();
    if(this->target_node != nullptr)
        this->target_node->disconnect("tree_exited", this->target_node_exited_tree.bind(this->target_node));
    this->target_node = node;
    if(node != nullptr)
        node->connect("tree_exited", this->target_node_exited_tree.bind(node));
    this->emit_signal("attention_changed");
}

gd::Node3D *UnitWorldState::get_target_node() const {
    return this->target_node;
}

void UnitWorldState::cache_property(gd::String property, gd::Variant value, double auto_invalidate_time) {
    this->cache[property] = {.value=value, .auto_invalidate_time=auto_invalidate_time};
}

void UnitWorldState::target_destroyed(gd::Node3D *target) {
    if(target == this->target_node)
        this->set_target_node(nullptr);
}
