#include "rts_player.hpp"
#include "goal_marker.hpp"
#include "godot_cpp/variant/utility_functions.hpp"
#include "unit.hpp"
#include "utils/godot_macros.hpp"
#include "utils/level.hpp"
#include "utils/util_functions.hpp"
#include <godot_cpp/classes/packed_scene.hpp>
#include <godot_cpp/classes/physics_direct_space_state3d.hpp>
#include <godot_cpp/classes/physics_direct_space_state3d.hpp>
#include <godot_cpp/classes/physics_ray_query_parameters3d.hpp>
#include <godot_cpp/classes/viewport.hpp>
#include <godot_cpp/classes/world3d.hpp>
#include <godot_cpp/variant/callable_method_pointer.hpp>

void RTSPlayer::_bind_methods() {
#define CLASSNAME RTSPlayer
    GDPROPERTY_HINTED(ground_marker_scene, gd::Variant::OBJECT, gd::PROPERTY_HINT_RESOURCE_TYPE, "PackedScene");
}

void RTSPlayer::_ready() {
    this->camera = this->get_node<gd::Camera3D>("Camera3D");
}

void RTSPlayer::_process(double delta_time) {
    this->move(this->camera_keys_motion * this->camera_keys_speed * delta_time);
    this->rotate(gd::Vector3{0.f, 1.f, 0.f}, camera_keys_rotation * this->camera_keys_rotation_speed * delta_time);

    this->move(this->camera_mouse_motion * this->camera_mouse_speed);
    this->camera_mouse_motion = {0.f, 0.f};
    this->rotate(gd::Vector3{0.f, 1.f, 0.f}, camera_mouse_rotation * this->camera_mouse_rotation_speed);
    this->camera_mouse_rotation = 0.f;
    this->process_mouse();
}

void RTSPlayer::setup_player_input(utils::PlayerInput *input) {
    input->listen_to("lclick", callable_mp(this, &RTSPlayer::on_lclick));
    input->listen_to("rclick", callable_mp(this, &RTSPlayer::on_rclick));
    input->listen_to("mclick", callable_mp(this, &RTSPlayer::on_mclick));
    input->listen_to("direction_right", "direction_left", callable_mp(this, &RTSPlayer::on_direction_horizontal));
    input->listen_to("direction_down", "direction_up", callable_mp(this, &RTSPlayer::on_direction_vertical));
    input->listen_to("rotate_left", "rotate_right", callable_mp(this, &RTSPlayer::on_rotate_horizontal));
    input->listen_to("_mouse_left", "_mouse_right", callable_mp(this, &RTSPlayer::on_mouse_horizontal));
    input->listen_to("_mouse_down", "_mouse_up", callable_mp(this, &RTSPlayer::on_mouse_vertical));
}

gd::Node *RTSPlayer::to_node() {
    return gd::Object::cast_to<Node>(this);
}

void RTSPlayer::spawn_at_position(gd::Transform3D const &at) {
    this->set_global_transform(at);
}

void RTSPlayer::process_mouse() {
    gd::Viewport *view = this->get_viewport();
    gd::Vector2 const pix_coords = view->get_mouse_position();
    this->cursor_camera_normal = this->camera->project_ray_normal(pix_coords);
}

void RTSPlayer::move(gd::Vector2 motion) {
    gd::Vector3 pos = this->get_global_position();
    this->set_global_position(pos + this->get_forward_direction() * motion.y + this->get_left_direction() * motion.x);
}

gd::Dictionary RTSPlayer::raycast_from_cursor(uint32_t layers) const {
    gd::Vector3 const origin = this->camera->get_global_position();
    gd::Vector3 const destination = origin + this->cursor_camera_normal * 1000.f;
    gd::Ref<gd::PhysicsRayQueryParameters3D> const query{gd::PhysicsRayQueryParameters3D::create(origin, destination, layers)};
    gd::PhysicsDirectSpaceState3D *state = this->get_world_3d()->get_direct_space_state();
    return state->intersect_ray(query);
}

gd::Node3D *RTSPlayer::node_under_cursor(uint32_t layers) const {
    gd::Dictionary result = this->raycast_from_cursor(layers);
    if(!result.is_empty())
        gd::UtilityFunctions::print("Found object ", Object::cast_to<gd::Node>(result["collider"])->get_path());
    else
        gd::UtilityFunctions::print("No object found");
    return result.is_empty() ? nullptr : Object::cast_to<gd::Node3D>(result["collider"]);
}

void RTSPlayer::select_unit_under_cursor() {
    Unit *unit = Object::cast_to<Unit>(this->node_under_cursor(0x2));
    if(unit != nullptr)
        this->select_unit(unit);
}

void RTSPlayer::order_activate_object_under_cursor() {
    if(this->selected_unit == nullptr)
        return;
    gd::Dictionary const dict = this->raycast_from_cursor(0x4);
    if(dict.is_empty())
        return;
    gd::Node3D *hit = gd::Object::cast_to<gd::Node3D>(dict["collider"]);
    GoalMarker *marker = gd::Object::cast_to<GoalMarker>(hit);
    bool const is_temporary_marker = marker == nullptr;
    if(is_temporary_marker) {
        marker = gd::Object::cast_to<GoalMarker>(this->ground_marker_scene->instantiate());
        this->add_child(marker);
        marker->set_as_top_level(true);
        marker->set_global_position(dict["position"]);
    }
    this->selected_unit->plan_for_marker(marker);
    if(is_temporary_marker) {
        this->selected_unit->get_world_state()->connect("attention_changed", callable_mp(marker, &GoalMarker::destroy_on_forgotten));
    }
}

void RTSPlayer::clear_selected_unit() {
    if(this->selected_unit != nullptr) {
        this->selected_unit->disconnect("tree_exited", this->unit_destroyed_callable());
        this->selected_unit = nullptr;
    }
}

void RTSPlayer::select_unit(Unit *unit) {
    this->clear_selected_unit();
    this->selected_unit = unit;
    unit->connect("tree_exited", this->unit_destroyed_callable());
}

void RTSPlayer::on_mouse_horizontal(gd::Ref<gd::InputEvent> event, float value) {
    if(this->mmb_down)
        this->camera_mouse_motion.x = value;
    else if(this->rmb_down)
        this->camera_mouse_rotation = value;
}

void RTSPlayer::on_mouse_vertical(gd::Ref<gd::InputEvent> event, float value) {
    if(this->mmb_down)
        this->camera_mouse_motion.y = value;
}

void RTSPlayer::on_direction_horizontal(gd::Ref<gd::InputEvent> event, float value) {
    this->camera_keys_motion.x = value;
}

void RTSPlayer::on_direction_vertical(gd::Ref<gd::InputEvent> event, float value) {
    this->camera_keys_motion.y = value;
}

void RTSPlayer::on_rotate_horizontal(gd::Ref<gd::InputEvent> event, float value) {
    this->camera_keys_rotation = value;
}

void RTSPlayer::on_rclick(gd::Ref<gd::InputEvent> event, float value) {
    gd::UtilityFunctions::print("on_rclick on ", this->get_path(), " ---------");
    if(value == 0.f && !this->rmb_is_held())
        this->order_activate_object_under_cursor();
    this->rmb_down = value != 0.f;
    this->rmb_last_change = utils::time_seconds();
}

void RTSPlayer::on_lclick(gd::Ref<gd::InputEvent> event, float value) {
    if(value == 0.f && !this->lmb_is_held())
        this->select_unit_under_cursor();
    this->lmb_down = value != 0.f;
    this->lmb_last_change = utils::time_seconds();
}

void RTSPlayer::on_mclick(gd::Ref<gd::InputEvent> event, float value) {
    this->mmb_down = value != 0.f;
}

gd::Vector3 RTSPlayer::get_forward_direction() const {
    gd::Vector3 forward = this->get_global_basis().get_column(2);
    return gd::Vector3{forward.x, 0.f, forward.z}.normalized();
}

gd::Vector3 RTSPlayer::get_left_direction() const {
    gd::Vector3 left = this->get_global_basis().get_column(0);
    return gd::Vector3{left.x, 0.f, left.z}.normalized();
}

bool RTSPlayer::lmb_is_held() {
    return this->lmb_down && utils::time_seconds() >= (this->lmb_last_change + this->time_to_held);
}

bool RTSPlayer::rmb_is_held() {
    return this->rmb_down && utils::time_seconds() >= (this->rmb_last_change + this->time_to_held);
}

gd::Callable RTSPlayer::unit_destroyed_callable() {
    return callable_mp(this, &RTSPlayer::clear_selected_unit);
}

void RTSPlayer::set_ground_marker_scene(gd::Ref<gd::PackedScene> scene) {
    this->ground_marker_scene = scene;
}

gd::Ref<gd::PackedScene> RTSPlayer::get_ground_marker_scene() const {
    return this->ground_marker_scene;
}
