#include "tunnels_player.hpp"
#include "character_actor.hpp"
#include "character_data.hpp"
#include "goal_marker.hpp"
#include "godot_cpp/variant/callable_method_pointer.hpp"
#include "godot_cpp/variant/utility_functions.hpp"
#include "planner.hpp"
#include "tunnels_game_mode.hpp"
#include "tunnels_game_state.hpp"
#include "utils/game_root.hpp"
#include "utils/godot_macros.h"
#include "utils/player_input.hpp"
#include <algorithm>
#include <godot_cpp/classes/input_event.hpp>
#include <godot_cpp/classes/physics_direct_space_state3d.hpp>
#include <godot_cpp/classes/physics_ray_query_parameters3d.hpp>
#include <godot_cpp/classes/resource_loader.hpp>
#include <godot_cpp/classes/scene_state.hpp>
#include <godot_cpp/classes/viewport.hpp>
#include <godot_cpp/classes/world3d.hpp>
#include <godot_cpp/variant/plane.hpp>
#include <godot_cpp/variant/projection.hpp>

namespace godot {
void TunnelsPlayer::_bind_methods() {
#define CLASSNAME TunnelsPlayer
    GDPROPERTY_HINTED(camera_rotation_ramp, Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "Curve");
}

void TunnelsPlayer::_enter_tree() { GDGAMEONLY();
    this->initialize_character();
    this->camera_rotation_ramp->bake();
    this->reticle = this->get_node<Node3D>("Reticle");
}

void TunnelsPlayer::_ready() {
    this->camera = this->get_viewport()->get_camera_3d();
}

void TunnelsPlayer::_exit_tree() { GDGAMEONLY();
    GameRoot::get_singleton()->remove_player(this->get_player_id());
}

void TunnelsPlayer::_process(double delta_time) { GDGAMEONLY();
    this->process_mouse_location(delta_time); // get the current screen location of the cursor
    // convert screen location to world location on the same y plane as the player character
    Vector3 const mouse_world_location = this->get_mouse_world_position(Vector3{0.f, 1.f, 0.f}, this->character->get_global_position().y + 1.f);
    // move the reticle
    this->reticle->set_global_position(mouse_world_location);
    // rotate the camera
    this->process_camera_rotation(delta_time);
    switch(this->state) {
    default:
    case State::ManualControl:
        // send the current wasd input to the character
        this->character->move(this->get_world_move_input().normalized());
        // fall through to shared functionality
    case State::Tactics:
        // send the current world cursor position the character
        this->character->aim(mouse_world_location);
        // move the camera along with the character
        this->set_global_position(this->character->get_global_position());
        break;
    }
}

void TunnelsPlayer::process_mouse_location(double delta_time) {
    Viewport *view = this->get_viewport();
    Vector2 const pixel_location = view->get_mouse_position();
    // convert cursor's global pixel position to normalized screen coordinates
    this->mouse_location = pixel_location / view->get_visible_rect().get_size();
    // get the direction the mouse is pointing in the world
    this->mouse_world_ray_normal = this->camera->project_ray_normal(pixel_location);
}

void TunnelsPlayer::process_camera_rotation(double delta_time) {
    Vector3 rotation = this->get_global_rotation();
    // the influence of the mouse's y position on the rotation speed
    float const y_multiplier = std::max(TunnelsPlayer::ROTATION_Y_MIN_INFLUENCE, this->mouse_location.y);
    float const margin = TunnelsPlayer::ROTATION_MARGIN * (this->state == State::Tactics ? TunnelsPlayer::ROTATION_MARGIN_TACTICS_MUL : 1.f);
    // rotate the camera when the mouse is close to the edge of the screen
    if(this->mouse_location.x < margin) {
        // normalized measurement of how far into the rotation margin the mouse is
        float const normalized{1.f - (this->mouse_location.x / margin)};
        // rotate based on delta time and use a curve to make the rotation zone feel more natural
        rotation.y += delta_time * double(TunnelsPlayer::ROTATION_SPEED * camera_rotation_ramp->sample(normalized) * y_multiplier);
    }
    if(this->mouse_location.x > 1.f - margin) {
        float const normalized{((this->mouse_location.x - (1.f - margin)) / margin)};
        rotation.y -= delta_time * double(TunnelsPlayer::ROTATION_SPEED * camera_rotation_ramp->sample(normalized) * y_multiplier);
    }

    // wrap rotation to avoid going into invalid numbers
    while(rotation.y > 6.283185)
        rotation.y -= 6.283185;
    while(rotation.y < 0.f)
        rotation.y += 6.283185;
    // apply new rotation
    this->set_global_rotation(rotation);
}

void TunnelsPlayer::setup_player_input(PlayerInput *input) {
    input->listen_to(PlayerInput::Listener("move_left", "move_right", callable_mp(this, &TunnelsPlayer::horizontal_move_input)));
    input->listen_to(PlayerInput::Listener("move_forward", "move_backward", callable_mp(this, &TunnelsPlayer::vertical_move_input)));
    input->listen_to(PlayerInput::Listener("fire", callable_mp(this, &TunnelsPlayer::fire_pressed)));
    input->listen_to(PlayerInput::Listener("tactics_mode", callable_mp(this, &TunnelsPlayer::mode_switch_input)));
}

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

void TunnelsPlayer::spawn_at_position(Transform3D const &at) {
    this->character->set_global_transform(at);
    this->set_global_basis(at.get_basis());
}

void TunnelsPlayer::horizontal_move_input(Ref<InputEvent> event, float value) {
    this->move_input.x = value;
}

void TunnelsPlayer::vertical_move_input(Ref<InputEvent> event, float value) {
    this->move_input.y = value;
}

void TunnelsPlayer::mode_switch_input(Ref<InputEvent> event, float value) {
    if(value != 0.f)
        this->state = this->state == State::Tactics ? State::ManualControl : State::Tactics;
}

void TunnelsPlayer::fire_pressed(Ref<InputEvent> event, float value) {
    switch(this->state) {
    case State::ManualControl:
        this->character->set_firing(value != 0);
        break;
    case State::Tactics:
        if(value == 1.f)
            this->try_select_marker();
        break;
    }
}

void TunnelsPlayer::try_select_marker() {
    UtilityFunctions::print("TunnelsPlayer::try_select_marker()");
    Transform3D const &camera_trans{this->camera->get_global_transform()};
    // prepare raycast query
    Ref<PhysicsRayQueryParameters3D> params{PhysicsRayQueryParameters3D::create(camera_trans.origin, camera_trans.origin + this->mouse_world_ray_normal * 1000.f)};
    params->set_collision_mask(1u << 3u);
    params->set_collide_with_areas(true);
    // fetch current physics state and cast ray
    PhysicsDirectSpaceState3D *state = this->get_world_3d()->get_direct_space_state();
    Dictionary dict{state->intersect_ray(params)};
    // fail if nothing was hit
    if(dict.is_empty())
        return;
    // attempt to cast hit node to a marker
    GoalMarker *marker{Object::cast_to<GoalMarker>(dict["collider"])};
    // fail if hit object is not a marker
    if(marker == nullptr)
        return;
    UtilityFunctions::print("Hit: ", marker->get_path());
    // select a character to send orders to
    CharacterActor *target_character{nullptr};
    for(CharacterActor *loop_character : Ref<TunnelsGameMode>(GameRoot::get_singleton()->get_game_mode())->get_player_characters()) {
        if(loop_character != this->character) {
            target_character = loop_character;
            break;
        }
    }
    // no non-player ally was found
    if(target_character == nullptr)
        return;
    // cache planner component
    goap::Planner *planner{target_character->get_planner()};
    // cache previous target in case planning fails
    Node *previous_target{target_character->get_target()};
    // attempt to find a plan to marker's goal
    target_character->set_target(marker);
    if(planner->can_do(marker->get_goal())) {
        planner->add_goal(marker->get_goal());
        planner->make_plan();
        target_character->force_update_action();
        UtilityFunctions::print("Made plan for character ", target_character->get_path());
    } else {
        // reset character to the state it was in before attempts to change goal
        UtilityFunctions::push_warning("Failed to make plan for ", marker->get_goal()->get_path());
        target_character->set_target(previous_target);
    }
}

void TunnelsPlayer::initialize_character() {
    Ref<PackedScene> player_scene = ResourceLoader::get_singleton()->load("res://player_character.tscn");
    // check if the player scene is a valid player character
    if(player_scene.is_null() || !player_scene.is_valid())
        return;
    if(player_scene->get_state()->get_node_type(0) != StringName("CharacterActor"))
        return;
    UtilityFunctions::print("initialize_character pos: ", this->get_global_position());
    // instantiate and store the player character
    this->character = Object::cast_to<CharacterActor>(player_scene->instantiate());
    this->get_parent()->add_child(this->character);
    this->character->set_global_transform(this->get_global_transform());
    Ref<TunnelsGameState> game_state = GameRoot::get_singleton()->get_game_state();
    Ref<CharacterData> character = game_state->get_characters()[0];
    this->character->set_character_data(game_state->get_characters()[0]);
    // disable navmesh navigation and start using player input
    this->character->set_manual_mode(true);
    Ref<TunnelsGameMode>(GameRoot::get_singleton()->get_game_mode())->set_manual_character(this->character);
}

Vector3 TunnelsPlayer::get_world_move_input() const {
    Basis const basis = this->get_global_transform().get_basis();
    // get the forward and left vectors, ensuring that they won't produce {0,0,0} when flattened
    Vector3 x = basis.get_column(0);
    if(x.x == 0.f && x.z == 0.f)
        x = basis.get_column(1);
    Vector3 z = basis.get_column(2);
    if(z.x == 0.f && z.z == 0.f)
        z = basis.get_column(1);
    // convert input vector to world normal by way of flattened forward and left units
    return this->move_input.x * Vector3{x.x, 0.f, x.z}.normalized()
         + this->move_input.y * Vector3{z.x, 0.f, z.z}.normalized();
}

Vector3 TunnelsPlayer::get_mouse_world_position(Vector3 axis, float depth) const {
    // cache camera location
    Vector3 const cam_origin = this->camera->get_global_position();
    // get the ray and origin depths along the axis
    float const cam_depth = axis.dot(cam_origin);
    float const ray_step = axis.dot(this->mouse_world_ray_normal);
    // calculate the number of "steps" the ray needs to take to get the target depth from the origin along the axis
    float const distance = depth - cam_depth;
    float const steps = distance / ray_step;
    return cam_origin + this->mouse_world_ray_normal * steps;
}

void TunnelsPlayer::set_camera_rotation_ramp(Ref<Curve> curve) {
    this->camera_rotation_ramp = curve;
}

Ref<Curve> TunnelsPlayer::get_camera_rotation_ramp() const {
    return this->camera_rotation_ramp;
}

CharacterActor *TunnelsPlayer::get_character() const {
    return this->character;
}

float const TunnelsPlayer::ROTATION_SPEED{0.5f};
float const TunnelsPlayer::ROTATION_Y_MIN_INFLUENCE{7.f};
float const TunnelsPlayer::ROTATION_MARGIN{0.4f};
float const TunnelsPlayer::ROTATION_MARGIN_TACTICS_MUL{0.6f};
}
