#include <client/FirstPersonPlayer.h>

#include <godot_cpp/classes/camera3d.hpp>
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/input.hpp>
#include <godot_cpp/classes/input_event_mouse_motion.hpp>
#include <godot_cpp/classes/input_map.hpp>
#include <godot_cpp/core/math.hpp>
#include <godot_cpp/variant/utility_functions.hpp>

using namespace godot;

namespace blitz {

static constexpr float WALK_SPEED = 5.0f;
static constexpr float SPRINT_SPEED = 7.0f;

static constexpr float JUMP_VELOCITY = 4.5f;

static constexpr float GRAVITY = 9.81f;

static constexpr float SENSITIVITY = 0.003f;

static constexpr float BOB_FREQ = 2.0f;
static constexpr float BOB_AMP = 0.08f;

static constexpr float AIR_MOVEMENT = 3.0f;

static constexpr float GROUND_FRICTION = 7.0f;

static constexpr float BASE_FOV = 75.0f;
static constexpr float FOV_CHANGE = 1.5f;
static constexpr float FOV_TRANSITION = 8.0f;
static constexpr float MIN_FOV_VELOCITY = 0.5;
static constexpr float MAX_FOV_VELOCITY = SPRINT_SPEED * 2.0f;

static const float LerpValue = 0.10;
static const float AnimationBlend = 7.0;

void FirstPersonPlayer::_bind_methods() {}

FirstPersonPlayer::FirstPersonPlayer() : Player(), m_BobTime(0) {}

FirstPersonPlayer::~FirstPersonPlayer() {}

void FirstPersonPlayer::_ready() {
	InputMap::get_singleton()->load_from_project_settings();
	if (!Engine::get_singleton()->is_editor_hint()) {
		Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_CAPTURED);
	}
	m_Head = Object::cast_to<Node3D>(find_child("Head"));
	m_Camera = Object::cast_to<Camera3D>(m_Head->find_child("Camera"));
	m_AnimationTree = Object::cast_to<AnimationTree>(find_child("AnimationTree"));
	m_Mesh = Object::cast_to<Node3D>(find_child("Mesh"));

	set_position({0, 0, 0});
	set_velocity({0, 0, 0});
}

void FirstPersonPlayer::_unhandled_input(const godot::Ref<godot::InputEvent>& a_Event) {
	auto* event = Object::cast_to<InputEventMouseMotion>(a_Event.ptr());
	if (event)
		UpdateCamera(*event);

	// TODO: remove
	if (Input::get_singleton()->is_action_just_pressed("escape")) {
		Input::MouseMode current = Input::get_singleton()->get_mouse_mode();
		Input::get_singleton()->set_mouse_mode(
			(current == Input::MOUSE_MODE_CAPTURED) ? Input::MOUSE_MODE_VISIBLE : Input::MOUSE_MODE_CAPTURED);
	}
}

void FirstPersonPlayer::_physics_process(float a_Delta) {
#if DEBUG_ENABLED
	if (Engine::get_singleton()->is_editor_hint()) {
		return;
	}
#endif

	auto* Input = Input::get_singleton();

	if (!is_on_floor())
		set_velocity(get_velocity() - Vector3{0, GRAVITY * a_Delta, 0});

	if (Input->is_action_pressed("jump") && is_on_floor())
		set_velocity({get_velocity().x, JUMP_VELOCITY, get_velocity().z});

	m_Speed = Input->is_action_pressed("sprint") ? SPRINT_SPEED : WALK_SPEED;

	UpdatePosition(a_Delta);

	UpdateFOV(a_Delta);
	UpdateBobbing(a_Delta);

	move_and_slide();

	UpdateAnimation(a_Delta);
}

void FirstPersonPlayer::UpdateBobbing(float a_Delta) {
	m_BobTime += a_Delta * get_velocity().length() * is_on_floor();

	Vector3 newPos{static_cast<float>(Math::cos(m_BobTime * BOB_FREQ / 2.0) * BOB_AMP),
		static_cast<float>(Math::sin(m_BobTime * BOB_FREQ) * BOB_AMP), 0};

	m_Camera->set_transform({m_Camera->get_transform().basis, newPos});
}

void FirstPersonPlayer::UpdateCamera(const InputEventMouseMotion& a_Event) {
	m_Head->rotate_y(-a_Event.get_relative().x * SENSITIVITY);
	m_Mesh->rotate_y(-a_Event.get_relative().x * SENSITIVITY);
	m_Camera->rotate_x(-a_Event.get_relative().y * SENSITIVITY);

	float rotationX = m_Camera->get_rotation().x;
	rotationX = CLAMP(rotationX, Math::deg_to_rad(-80.0), Math::deg_to_rad(80.0));
	m_Camera->set_rotation({rotationX, get_rotation().y, get_rotation().z});
}

void FirstPersonPlayer::UpdatePosition(float delta) {
	auto* Input = Input::get_singleton();

	Vector2 inputDirection = Input->get_vector("move_left", "move_right", "move_forwards", "move_backwards");
	Vector3 direction = (m_Head->get_transform().basis.xform(Vector3(inputDirection.x, 0, inputDirection.y))).normalized();

	if (is_on_floor()) {
		if (!direction.is_zero_approx()) {
			set_velocity({direction.x * m_Speed, get_velocity().y, direction.z * m_Speed});
		} else {
			set_velocity({Math::lerp(static_cast<float>(get_velocity().x), static_cast<float>(direction.x * m_Speed),
							  static_cast<float>(delta * GROUND_FRICTION)),
				get_velocity().y,
				Math::lerp(static_cast<float>(get_velocity().z), static_cast<float>(direction.z * m_Speed),
					static_cast<float>(delta * GROUND_FRICTION))});
		}
	} else {
		set_velocity({Math::lerp(static_cast<float>(get_velocity().x), static_cast<float>(direction.x * m_Speed),
						  static_cast<float>(delta * AIR_MOVEMENT)),
			get_velocity().y,
			Math::lerp(static_cast<float>(get_velocity().z), static_cast<float>(direction.z * m_Speed),
				static_cast<float>(delta * AIR_MOVEMENT))});
	}


	if (!direction.is_zero_approx()) {
		godot::Vector3 newRotation = m_Mesh->get_rotation();
		newRotation.y = godot::UtilityFunctions::lerp_angle(
			newRotation.y, godot::UtilityFunctions::atan2(get_velocity().x, get_velocity().z), LerpValue);
		m_Mesh->set_rotation(newRotation);
	}
}

void FirstPersonPlayer::UpdateFOV(float a_Delta) {
	float velocityClamped = Math::clamp(get_velocity().length(), MIN_FOV_VELOCITY, MAX_FOV_VELOCITY);
	float targetFOV = BASE_FOV + FOV_CHANGE * velocityClamped;
	m_Camera->set_fov(Math::lerp(static_cast<float>(m_Camera->get_fov()), targetFOV, a_Delta * FOV_TRANSITION));
}

void FirstPersonPlayer::UpdateAnimation(float delta) {
	if (is_on_floor()) {
		m_AnimationTree->set("parameters/ground_air_transition/transition_request", "grounded");

		if (get_velocity().length() > 0.2f) {
			if (m_Speed == SPRINT_SPEED) {
				m_AnimationTree->set("parameters/iwr_blend/blend_amount",
					UtilityFunctions::lerp(m_AnimationTree->get("parameters/iwr_blend/blend_amount"), 1.0, delta * AnimationBlend));
			} else {
				m_AnimationTree->set("parameters/iwr_blend/blend_amount",
					UtilityFunctions::lerp(m_AnimationTree->get("parameters/iwr_blend/blend_amount"), 0.0, delta * AnimationBlend));
			}
		} else {
			m_AnimationTree->set("parameters/iwr_blend/blend_amount",
				UtilityFunctions::lerp(m_AnimationTree->get("parameters/iwr_blend/blend_amount"), -1.0, delta * AnimationBlend));
		}
	} else {
		m_AnimationTree->set("parameters/ground_air_transition/transition_request", "air");
	}
}

}  // namespace blitz