#include "client/display/PlayerController.h"

#include "blitz/game/Player.h"
#include "blitz/maths/Maths.h"
#include "blitz/misc/Log.h"
#include "client/Client.h"
#include "client/config/BlitzConfig.h"
#include "client/display/InputManager.h"
#include "client/game/ClientGame.h"
#include "imgui.h"
#include <algorithm>
#include <iostream>

namespace blitz {
namespace input {

static constexpr float DEFAULT_JUMP_VEL = 7.5;
static constexpr float DEFAULT_MAX_LR_SPEED = 10.;
static constexpr float DEFAULT_MAX_FB_SPEED = 10.;
static constexpr float DEFAULT_LR_SPEED_SMOOTHING_TIME = 1.0;
static constexpr float DEFAULT_FB_SPEED_SMOOTHING_TIME = 1.0;

PlayerController::PlayerController(Client* client) :
	m_Player(nullptr),
	m_Client(client),
	m_Velocity({}),
	m_MaxVelocity(DEFAULT_MAX_LR_SPEED, DEFAULT_MAX_FB_SPEED, DEFAULT_JUMP_VEL),
	m_OnGround(true) {
	m_DxSmoother.Current = 0.0f;
	m_DySmoother.Current = 0.0f;
	m_DxSmoother.SetSmoothingTime(DEFAULT_LR_SPEED_SMOOTHING_TIME);
	m_DySmoother.SetSmoothingTime(DEFAULT_FB_SPEED_SMOOTHING_TIME);
	InputManager::BindMouseMoveCallback(
		std::bind(&PlayerController::MouseMotionEvent, this, std::placeholders::_1, std::placeholders::_2));
	client->BindListener(this);
}

void PlayerController::OnGameConfigUpdate() {
	UpdateShootTimer(m_Client->GetGame()->GetGameConfig().FiringRate);
}

void PlayerController::UpdateShootTimer(int bpm) {
	m_ShootTimer.SetCooldown(60.0f / static_cast<float>(bpm));
	m_ShootTimer.Reset();
}

void PlayerController::MouseMotionEvent(int deltaX, int deltaY) {
	if (!m_Player || !InputManager::MouseGrabbed())
		return;

	float MouseSpeed = m_Client->GetConfig()->GetMouseSpeed();

	m_Player->AddYaw(deltaX * MouseSpeed);
	m_Player->AddPitch(deltaY * -MouseSpeed);
	m_Player->SetPitch(std::clamp(m_Player->GetPitch(), -maths::PI / 2.0f + 0.001f, maths::PI / 2.0f - 0.001f));
}

void PlayerController::Update(float delta) {
	if (!m_Player)
		return;

	if (InputManager::MouseGrabbed()) {
		Keybinds keys = m_Client->GetConfig()->GetKeys();
		float lr = static_cast<float>(ImGui::IsKeyDown((ImGuiKey(keys[kaAvancer])))) -
				   static_cast<float>(ImGui::IsKeyDown((ImGuiKey(keys[kaReculer]))));
		float fb = static_cast<float>(ImGui::IsKeyDown((ImGuiKey(keys[kaGauche])))) -
				   static_cast<float>(ImGui::IsKeyDown((ImGuiKey(keys[kaDroite]))));

		// scale values in such a way that clamps ||(lr, fb)|| to 1.0
		float scale = 1.0f / std::max(sqrt(lr * lr + fb * fb), 1.0f);

		m_Velocity.x = lr * m_MaxVelocity.x * scale;
		m_Velocity.y = fb * m_MaxVelocity.y * scale;

		if (ImGui::IsKeyDown(ImGuiKey::ImGuiKey_Space) && m_OnGround) {
			m_Velocity.z = m_MaxVelocity.z;
			NotifyListeners(&game::PlayerInputListener::OnLocalPlayerJump);
		}

		bool canShoot = m_ShootTimer.Update(ImGui::GetIO().DeltaTime);
		if (ImGui::IsMouseDown(ImGuiMouseButton_Left) && canShoot) {
			NotifyListeners(
				&game::PlayerInputListener::OnLocalPlayerShoot, m_Player->GetPosition(), m_Player->GetYaw(), m_Player->GetPitch());
			m_ShootTimer.ApplyCooldown();
		}
	} else {
		m_Velocity.x = m_Velocity.y = 0.0f;
	}

	UpdatePosition(delta);
}

void PlayerController::UpdatePosition(const float delta) {

	float yaw = m_Player->GetYaw();
	float sine = std::sin(yaw);
	float cosine = std::cos(yaw);

	m_DxSmoother.Tick(m_Velocity.x, delta);
	m_DySmoother.Tick(m_Velocity.y, delta);

	float dx_smooth = m_DxSmoother.Current;
	float dy_smooth = m_DySmoother.Current;

	float dx = (dx_smooth * cosine + dy_smooth * sine) * delta;
	float dy = (dx_smooth * sine - dy_smooth * cosine) * delta;

	float dz = m_Velocity.z * delta;

	// the floor here is y-level zero, once downwards collision lands it will be dynmicallly calculated
	// assumed to be a negative number
	const float floor_dist = 0.0f - m_Player->GetPosition().y;

	if ((m_OnGround = (dz <= floor_dist))) {
		dz = floor_dist;
		m_Velocity.z = 0.0f;
	} else {
		m_Velocity.z -= m_Client->GetGame()->GetGameConfig().Gravity * 1.0f * delta;
	}

	m_Player->AddPosition({dx, dz, dy});
}

}  // namespace input
}  // namespace blitz
