#include "turret.hpp"
#include "beam.hpp"
#include "utils/godot_macros.h"
#include <godot_cpp/classes/global_constants.hpp>
#include <godot_cpp/classes/physics_direct_space_state3d.hpp>
#include <godot_cpp/classes/physics_ray_query_parameters3d.hpp>
#include <godot_cpp/classes/time.hpp>
#include <godot_cpp/classes/world3d.hpp>
#include <godot_cpp/variant/utility_functions.hpp>

namespace godot {
void Turret::_bind_methods() {
#define CLASSNAME Turret
    GDPROPERTY_HINTED(attack_classes, Variant::ARRAY, PROPERTY_HINT_ARRAY_TYPE, "StringName");
    GDPROPERTY_HINTED(beam_scene, Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "PackedScene");
    GDPROPERTY_HINTED(guiding_beam_scene, Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "PackedScene");
    GDPROPERTY(fire_time, Variant::FLOAT);
    GDPROPERTY(charge_time, Variant::FLOAT);
    GDPROPERTY(lock_time, Variant::FLOAT);
}

void Turret::_enter_tree() { GDGAMEONLY();
    this->set_freeze_enabled(true);
    Area3D *awareness_area = this->get_node<Area3D>("AwarenessArea");
    awareness_area->connect("body_entered", callable_mp(this, &Turret::detect_node));
    awareness_area->connect("body_exited", callable_mp(this, &Turret::lose_node));
    this->gun_node = this->get_node<Node3D>("Gun");
    this->awareness_changed();
    this->gun_node->set_as_top_level(true);
    this->gun_node_offset = this->gun_node->get_position();
}

void Turret::_process(double delta_time) { GDGAMEONLY();
    switch(state) {
    default:
        break;
    case TurretState::LOCKING:
        this->lead_target(delta_time);
        break;
    }
    this->try_next_state();
    this->gun_node->set_global_position(this->get_global_position() + this->gun_node_offset);
}

void Turret::awareness_changed() {
    if(this->state != TurretState::WAITING)
        return;

    this->current_target = this->select_target();
    if(this->current_target == nullptr) {
        this->state = TurretState::WAITING;
    } else {
        this->state = TurretState::LOCKING;
        UtilityFunctions::print(this->get_path(), " new target ", this->current_target->get_path());
    }
    this->last_state_switch = Time::get_singleton()->get_ticks_msec() * 0.001f;
}

Node3D *Turret::select_target() {
    for(Node3D *node : this->awareness) {
        if(!this->invert_attack_classes && this->attack_classes.has(node->get_class())) {
            UtilityFunctions::print(this->get_path(), " selecting regular target ", node->get_path());
            return node;
        }
        else if(this->invert_attack_classes && !this->attack_classes.has(node->get_class())) {
            UtilityFunctions::print(this->get_path(), " selecting inverted target ", node->get_path());
            return node;
        }
    }
    return nullptr;
}

void Turret::lead_target(double delta_time) {
    if(this->current_target == nullptr)
        return;
    Vector3 const target_position = this->current_target->get_global_position();
    if(this->current_target->is_class("CarPlayer")) {
        Vector3 const target_heading = (target_position - this->last_target_position) / delta_time;
        this->aim_position = target_position + (target_heading * this->lead_distance);
    } else {
        this->aim_position = target_position;
    }

    Vector3 const z = (this->aim_position - this->gun_node->get_global_position()).normalized();
    Vector3 const x = z.cross({0.f, 1.f, 0.f});
    Vector3 const y = x.cross(z);

    this->gun_node->set_global_basis({x,y,z});
    this->last_target_position = target_position;
}

void Turret::detect_node(Node3D *node) {
    if(!this->awareness.has(node)) {
        this->awareness.insert(node);
        this->awareness_changed();
    }
}

void Turret::lose_node(Node3D *node) {
    if(this->awareness.has(node)) {
        this->awareness.erase(node);
        if(node == this->current_target) {
            this->current_target = nullptr;
            this->awareness_changed();
        }
    }
}

void Turret::try_next_state() {
    TurretState next_state = this->state;
    float time = Time::get_singleton()->get_ticks_msec() * 0.001f;
    switch(this->state) {
    case TurretState::LOCKING:
        if(time >= this->last_state_switch + this->lock_time)
            next_state = TurretState::CHARGING;
        break;
    case TurretState::CHARGING:
        if(time >= this->last_state_switch + this->charge_time)
            next_state = TurretState::FIRING;
        break; 
    case TurretState::WAITING:
    case TurretState::FIRING:
        return;
    }
    if(this->state == next_state)
        return;
    else this->state = next_state;
    this->last_state_switch = time;
    switch(this->state) {
    case TurretState::FIRING:
        this->create_beam();
        break;
    case TurretState::CHARGING:
        this->create_guiding_beam();
    default: break;
    }
}

void Turret::create_beam() {
    Vector3 const from = this->gun_node->get_global_position();
    Vector3 const to = from + this->gun_node->get_global_basis().get_column(2) * 1000.f;
    Ref<PhysicsRayQueryParameters3D> query{PhysicsRayQueryParameters3D::create(from, to)};
    Dictionary result{this->get_world_3d()->get_direct_space_state()->intersect_ray(query)};
    if(result.is_empty()) return;
    Beam *beam{Object::cast_to<Beam>(this->beam_scene->instantiate())};
    this->gun_node->add_child(beam);
    beam->set_from_to(from, result["position"]);
    beam->set_end_time(this->fire_time);
    beam->connect("tree_exited", callable_mp(this, &Turret::beam_ended));
}

void Turret::create_guiding_beam() {
    Vector3 const from = this->gun_node->get_global_position();
    Vector3 const to = from + this->gun_node->get_global_basis().get_column(2) * 1000.f;
    Ref<PhysicsRayQueryParameters3D> query{PhysicsRayQueryParameters3D::create(from, to)};
    Dictionary result{this->get_world_3d()->get_direct_space_state()->intersect_ray(query)};
    if(result.is_empty()) return;
    Beam *beam{Object::cast_to<Beam>(this->guiding_beam_scene->instantiate())};
    this->gun_node->add_child(beam);
    beam->set_from_to(from, result["position"]);
    beam->set_end_time(this->charge_time);
}

void Turret::beam_ended() {
    this->state = TurretState::LOCKING;
    this->last_state_switch = Time::get_singleton()->get_ticks_msec() * 0.001f;
}

void Turret::set_fire_time(float val) {
    this->fire_time = val;
}

float Turret::get_fire_time() const {
    return this->fire_time;
}

void Turret::set_charge_time(float val) {
    this->charge_time = val;
}

float Turret::get_charge_time() const {
    return this->charge_time;
}

void Turret::set_lock_time(float val) {
    this->lock_time = val;
}

float Turret::get_lock_time() const {
    return this->lock_time;
}

void Turret::set_invert_targets(bool value) {
    UtilityFunctions::print(this->get_path(), " invert targets");
    this->invert_attack_classes = value;
    this->current_target = nullptr;
    this->awareness_changed();
    if(this->state != TurretState::FIRING)
        this->state = TurretState::WAITING;
}

void Turret::set_attack_classes(Array array) {
    this->attack_classes.clear();
    for(int i = 0; i < array.size(); ++i) {
        if(array[i].get_type() == Variant::STRING_NAME && !this->attack_classes.has(array[i]))
            this->attack_classes.push_back(array[i]);
        else
            this->attack_classes.push_back("");
    }
}

Array Turret::get_attack_classes() const {
    Array array{};
    for(StringName const &str : this->attack_classes)
        array.push_back(str);
    return array;
}

void Turret::set_beam_scene(Ref<PackedScene> scene) {
    this->beam_scene = scene;
}

Ref<PackedScene> Turret::get_beam_scene() const {
    return this->beam_scene;
}

void Turret::set_guiding_beam_scene(Ref<PackedScene> scene) {
    this->guiding_beam_scene = scene;
}

Ref<PackedScene> Turret::get_guiding_beam_scene() const {
    return this->guiding_beam_scene;
}
}
