#include "client/display/PlayerController.h"

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

namespace blitz {
namespace input {

static constexpr float DEFAULT_JUMP_VEL = 7.5;
static constexpr float DEFAULT_GRAVITY = 20.0;
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() :
	m_Player(nullptr),
	m_Dx(0.0),
	m_Dy(0.0),
	m_MaxDx(DEFAULT_MAX_LR_SPEED),
	m_MaxDy(DEFAULT_MAX_FB_SPEED),
	m_MaxDz(DEFAULT_JUMP_VEL),
	m_Dz(0.0),
	m_G(DEFAULT_GRAVITY),
	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));
}

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

	static const float MouseSpeed = 0.0025f;

	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()) {
		float lr = static_cast<float>(ImGui::IsKeyDown(ImGuiKey_Z)) - static_cast<float>(ImGui::IsKeyDown(ImGuiKey_S));
		float fb = static_cast<float>(ImGui::IsKeyDown(ImGuiKey_Q)) - static_cast<float>(ImGui::IsKeyDown(ImGuiKey_D));

		// 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_Dx = lr * m_MaxDx * scale;
		m_Dy = fb * m_MaxDy * scale;

		if (ImGui::IsKeyDown(ImGuiKey::ImGuiKey_Space) && m_OnGround) {
			m_Dz = m_MaxDz;
			NotifyListeners(&game::PlayerListener::OnPlayerJump);
		}

		bool canShoot = m_ShootTimer.Update(delta);
		if (ImGui::IsMouseDown(ImGuiMouseButton_Left) && canShoot) {
			NotifyListeners(&game::PlayerListener::OnPlayerShoot, m_Player->GetPosition(), m_Player->GetYaw(), m_Player->GetPitch());
			m_ShootTimer.ApplyCooldown();
		}
	}

	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_Dx, delta);
	m_DySmoother.Tick(m_Dy, 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_Dz * 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_Dz = 0.0f;
	} else {
		m_Dz -= m_G * delta;
	}

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

}  // namespace input
}  // namespace blitz
