#include "unit.hpp"
#include "entity_health.hpp"
#include "goap/action.hpp"
#include "goap/goal.hpp"
#include "rts_states.hpp"
#include "utils/godot_macros.hpp"
#include <godot_cpp/classes/navigation_agent3d.hpp>
#include <godot_cpp/classes/navigation_server3d.hpp>
#include <godot_cpp/classes/world3d.hpp>
#include <godot_cpp/variant/callable_method_pointer.hpp>
#include <godot_cpp/variant/utility_functions.hpp>

void Unit::_bind_methods() {
#define CLASSNAME Unit
    GDSIGNAL("goal_finished");
    GDSIGNAL("plan_failed");
    GDSIGNAL("plan_interrupted");
    GDPROPERTY_HINTED(configure_team, gd::Variant::INT, gd::PROPERTY_HINT_ENUM, UnitTeam::get_property_hint());
    GDPROPERTY_HINTED(movement_speed, gd::Variant::FLOAT, gd::PROPERTY_HINT_RANGE, "0.1,5.0");
    GDFUNCTION(use_weapon);
}

void Unit::_enter_tree() {
    this->agent = this->get_node<gd::NavigationAgent3D>("%NavigationAgent3D");
    this->agent->connect("velocity_computed", callable_mp(this, &Unit::on_velocity_computed));
    this->planner = this->get_node<goap::Planner>("%Planner");
    this->world_state = this->get_node<UnitWorldState>("%ActorWorldState");
    this->world_state->connect("attention_changed", callable_mp(this, &Unit::stop_plan));
    this->anim_player = this->get_node<gd::AnimationPlayer>("%AnimationPlayer");
    this->health = this->get_node<EntityHealth>("%EntityHealth");
    this->health->connect("death", callable_mp(this, &Unit::on_death));
    this->inventory = this->get_node<Inventory>("%Inventory");
}

void Unit::_physics_process(double) {
    this->move_and_slide();
    if(!this->get_velocity().is_zero_approx()) {
        gd::RID const map_id{this->get_world_3d()->get_navigation_map()};
        gd::Vector3 const offset{0.f, -float(this->agent->get_path_height_offset()), 0.f};
        gd::Vector3 const adjusted_pos{this->get_global_position() + offset};
        this->set_global_position(gd::NavigationServer3D::get_singleton()->map_get_closest_point(map_id, adjusted_pos) + offset);
    }
    this->set_velocity({0.f, 0.f, 0.f});
}

void Unit::stop_plan() {
    this->current_goal.unref();
    this->current_plan.clear();
    if(this->state && !this->state->is_queued_for_deletion())
        this->destroy_state();
    this->state = nullptr;
    this->call_deferred("emit_signal", "plan_failed");
}

void Unit::begin_marker_temporary(GoalMarker *marker) {
    this->world_state->set_target_node(marker);
    this->set_goal_and_plan(marker->get_goal());
    // destroy temporary marker if goal is already achieved or failed
    // connect observers if a plan was formed
    if(this->current_plan.is_empty()) {
        marker->destroy_on_forgotten();
    } else {
        this->world_state->connect("attention_changed", callable_mp(marker, &GoalMarker::destroy_on_forgotten));
        this->connect("goal_finished", callable_mp(marker, &GoalMarker::destroy_on_forgotten));
    }
    this->next_action();
}

void Unit::set_target_goal(gd::Node3D *target, gd::Ref<goap::Goal> goal) {
    this->destroy_state();
    this->world_state->set_target_node(target);
    this->begin_goal(goal);
}

void Unit::begin_goal(gd::Ref<goap::Goal> goal) {
    this->set_goal_and_plan(goal);
    this->next_action();
}

void Unit::use_weapon() {
    gd::Node3D *target{this->world_state->get_target_node()};
    Item const *weapon{this->inventory->get_weapon()};
    if(weapon == nullptr) {
        // try and do a melee attack instead when no weapon is equipped
        Unit *target_unit{gd::Object::cast_to<Unit>(target)};
        if(target_unit != nullptr && this->world_state->get_can_see_target() && this->world_state->get_is_in_melee_range())
            target_unit->get_entity_health()->damaged_by(1, this);
    } else {
        this->inventory->get_weapon()->try_use_on(this, target);
    }
}

void Unit::aim_at(gd::Node3D *target) {
    gd::Vector3 z{target->get_global_position() - this->get_global_position()};
    z.y = 0;
    z.normalize();
    gd::Vector3 x{gd::Vector3{0.f, 1.f, 0.f}.cross(z)};
    this->set_global_basis({x, z.cross(x), z});
}

void Unit::on_unconscious(Unit *damage_source) {
    this->destroy_state();
    this->anim_player->stop();
    this->anim_player->play("death");
}

void Unit::on_death(Unit *damage_source) {
    this->destroy_state();
    this->anim_player->stop();
    this->anim_player->play("death");
}

void Unit::on_velocity_computed(gd::Vector3 vel) {
    gd::Vector3 const pos{this->get_global_position()};
    if(vel.x == 0 && vel.z == 0)
        return;
    this->set_velocity(vel.normalized() * this->movement_speed);
    this->look_at(pos - gd::Vector3{vel.x, 0.f, vel.z});
}

void Unit::destroy_state() {
    if(this->state == nullptr || this->state->is_queued_for_deletion() || !this->state->is_inside_tree())
        return;
    this->state->interrupt_state();
    this->state = nullptr;
}

void Unit::state_finished() {
    if(this->current_plan.is_empty())
        this->emit_signal("goal_finished");
    this->next_action();
}

void Unit::next_action() {
    // cannot perform actions while dead
    if(!this->health->is_conscious())
        return;
    // destroy active state if relevant
    if(this->state != nullptr && !this->state->is_queued_for_deletion())
        this->destroy_state();
    this->state = nullptr;
    if(this->current_plan.is_empty())
        return;
    goap::Action const *action{this->current_plan.get(0)};
    if(!action->is_possible(this->world_state)) {
        this->emit_signal("plan_interrupted");
        return;
    }
    // pop next action and apply state
    this->state = action->get_apply_state(this->world_state);
    if(state == nullptr) {
        this->emit_signal("plan_interrupted");
        return;
    }
    this->current_plan.remove_at(0);
    this->add_child(this->state);
    this->state->connect("state_finished", this->on_state_finished);
    this->state->connect("state_failed", this->on_plan_failed);
}

void Unit::replan_goal() {
    this->begin_goal(this->current_goal);
}

void Unit::set_goal_and_plan(gd::Ref<goap::Goal> goal) {
    this->current_goal = goal;
    if(goal.is_null()) {
        this->current_plan.clear();
    } else {
        this->current_plan = this->planner->plan_for_goal(goal);
        if(this->current_plan.is_empty()) {
            this->current_goal.unref();
        }
    }
}

UnitWorldState *Unit::get_world_state() const {
    return this->world_state;
}

UnitTeam Unit::get_team() const {
    return this->team;
}

void Unit::set_configure_team(int team) {
    this->team = team;
}

int Unit::get_configure_team() const {
    return this->team;
}

bool Unit::has_plan() const {
    return !this->current_plan.is_empty();
}

EntityHealth *Unit::get_entity_health() {
    return this->health;
}

void Unit::set_movement_speed(float speed) {
    this->movement_speed = speed;
}

float Unit::get_movement_speed() const {
    return this->movement_speed;
}

#ifdef DEBUG_ENABLED
gd::String Unit::DEBUG_print_debug_info() {
    gd::String debug_info{"goal: "};
    if(!this->current_goal.is_valid()) {
        debug_info += "No goal assigned";
    } else {
        debug_info += this->current_goal->get_path();
        debug_info += "\nplan:";
        if(this->state != nullptr) {
            debug_info += "\n   ";
            debug_info += this->state->get_action()->get_class();
            debug_info += " (currently in-progress)";
        }
        for(goap::Action const *action : this->current_plan) {
            debug_info += "\n   ";
            debug_info += action->get_class();
        }
    }
    if(this->state != nullptr) {
        debug_info += "\ncurrent state: ";
        debug_info += this->state->get_class();
    }
    gd::UtilityFunctions::print(this->get_path(), "debug info:\n", debug_info);
    return debug_info;
}
#endif
