#include "rts_actions.hpp"
#include "nav_marker.hpp"
#include "nav_room.hpp"
#include "rts_states.hpp"
#include "goap/actor_world_state.hpp"
#include "goap/state.hpp"
#include <godot_cpp/core/memory.hpp>
#include <godot_cpp/variant/basis.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <cmath>

MoveToTarget::MoveToTarget()
: Action() {
    this->effects.insert("is_at_target", true);
}

goap::State *MoveToTarget::get_apply_state(goap::ActorWorldState *context) const {
    gd::Node3D *target{gd::Object::cast_to<gd::Node3D>(context->get_world_property("target_node"))};
    if(target == nullptr) {
        gd::UtilityFunctions::push_warning("Failed to get target node of ", context->get_path());
        return nullptr;
    } else {
        MoveTo *state{this->create_state<MoveTo>()};
        state->target_node = target;
        return state;
    }
}

FireAtTarget::FireAtTarget()
: Action() {
    this->required.insert("can_see_target", true);
    this->effects.insert("is_target_dead", true);
}

goap::State *FireAtTarget::get_apply_state(goap::ActorWorldState *) const {
    Animate *state{this->create_state<Animate>()};
    state->animation = "fire_weapon";
    return state;
}

FindTarget::FindTarget()
: Action() {
    this->require_state_complete = false;
    this->effects.insert("can_see_target", true);
}

goap::State *FindTarget::get_apply_state(goap::ActorWorldState *context) const {
    gd::Node3D *target{gd::Object::cast_to<gd::Node3D>(context->get_world_property("target_node"))};
    MoveTo *state{this->create_state<MoveTo>()};
    state->target_node = target;
    return state;
}

GetInMeleeRange::GetInMeleeRange()
: Action() {
    this->require_state_complete = false;
    this->required.insert("can_see_target", true);
    this->effects.insert("is_in_melee_range", true);
}

goap::State *GetInMeleeRange::get_apply_state(goap::ActorWorldState *context) const {
    gd::Node3D *target{gd::Object::cast_to<gd::Node3D>(context->get_world_property("target_node"))};
    MoveTo *state{this->create_state<MoveTo>()};
    state->target_node = target;
    return state;
}

MeleeAttack::MeleeAttack()
: Action() {
    this->required.insert("can_see_target", true);
    this->required.insert("is_in_melee_range", true);
    this->effects.insert("is_target_dead", true);
}

goap::State *MeleeAttack::get_apply_state(goap::ActorWorldState *) const {
    Animate *state{this->create_state<Animate>()};
    state->animation = "melee_attack";
    return state;
}

TankSelfHeal::TankSelfHeal()
: Action() {
    this->required.insert("can_see_target", false);
    this->effects.insert("is_health_safe", true);
}

goap::State *TankSelfHeal::get_apply_state(goap::ActorWorldState *) const {
    Animate *state{this->create_state<Animate>()};
    state->animation = "self_heal";
    return state;
}

TakeCover::TakeCover()
: Action () {
    this->effects.insert("can_see_target", false);
}

bool TakeCover::procedural_is_possible(goap::ActorWorldState *context) const {
    // positions of the context and the target 
    gd::Vector3 const context_position{context->get_world_property("parent_global_position")};
    gd::Vector3 const target_position{context->get_world_property("target_global_position")};
    // if there is no available navigation room, it is not possible to find a cover marker
    NavRoom *room{NavRoom::get_closest_room(context_position)};
    if(room == nullptr)
        return false;
    // immediately return true if there is at least one marker that counts as cover from the target
    for(NavMarker *marker : room->get_markers())
        if(TakeCover::is_marker_cover_from(marker, target_position, context_position))
            return true;
    return false;
}

goap::State *TakeCover::get_apply_state(goap::ActorWorldState *context) const {
    // positions of the context and the target to hide from
    gd::Vector3 const context_position{context->get_world_property("parent_global_position")};
    gd::Vector3 const target_position{context->get_world_property("target_global_position")};
    // find the room this entity is located in
    NavRoom *room{NavRoom::get_closest_room(context_position)};
    if(room == nullptr)
        return nullptr;
    NavMarker *best_marker{nullptr}; // marker with the best score found so far
    float best_score{0.f}; // best score found so far, starts at zero because negative values should not be considered
    for(NavMarker *marker : room->get_markers()) {
        float const score{this->score_cover_marker(marker, target_position, context_position)};
        if(score > best_score) {
            best_score = score;
            best_marker = marker;
        }
    }
    // don't bother creating a state if there is no target
    if(best_marker == nullptr)
        return nullptr;
    // create state, set target, and return
    MoveTo *state{this->create_state<MoveTo>()};
    state->target_node = best_marker;
    return state;
}

bool TakeCover::is_marker_cover_from(NavMarker *marker, gd::Vector3 const &target_position, gd::Vector3 const &context_position) {
    gd::Vector3 const marker_position{marker->get_global_position()};
    float const distance_to_target{target_position.distance_to(context_position)};
    return marker->get_marker_type() == MarkerType::Cover
        && marker->get_global_basis().get_column(2).dot(marker_position - target_position) < -2.f
        && context_position.distance_to(marker_position) < (distance_to_target * 2.f);
}

float TakeCover::score_cover_marker(class NavMarker* marker, const gd::Vector3& target_position, const gd::Vector3& context_position) const {
    if(!TakeCover::is_marker_cover_from(marker, target_position, context_position))
        return 0.f;
    gd::Vector3 const marker_position{marker->get_global_position()}; // position of the marker being considered
    float const target_distance{marker_position.distance_to(target_position)};
    float const context_distance{marker_position.distance_to(context_position)};
    // score is a weighted comparison between distances to the target and context
    // marker distance from context grows faster than marker distance from target
    return (target_distance) - (context_distance);
}

