#include "client/render/MainRenderer.h"

#include "blitz/maths/Maths.h"
#include "blitz/misc/Easing.h"
#include "blitz/misc/Format.h"
#include "blitz/misc/Log.h"
#include "blitz/misc/Test.h"
#include "blitz/misc/Time.h"
#include "client/Client.h"
#include "client/game/ClientGame.h"
#include "client/render/OpenGL.h"
#include "client/render/loader/GLLoader.h"
#include "client/render/loader/ModelLoader.h"
#include "client/render/loader/TextureLoader.h"
#include "client/render/shader/BulletShader.h"
#include "client/render/shader/EntityShader.h"
#include "client/render/shader/GunShader.h"
#include "client/render/shader/WorldShader.h"
#include "imgui.h"

namespace blitz {
namespace render {

static const Vec4f SkyColor = {0.6f, 0.8f, 1.0f, 1.0f};
static const Vec4f MenuColor = {0.0f, 0.0f, 0.0f, 0.0f};

MainRenderer::MainRenderer(Client* client) :
	m_Client(client), m_PlayerController(m_Client), m_ShootTime(0), m_BulletRenderer(m_Camera) {

	LoadModels();

	client->BindListener(this);
	m_PlayerController.BindListener(this);
	m_PlayerController.BindListener(client);

	m_EntityShader = std::make_unique<shader::EntityShader>();
	blitz_debug_assert(m_EntityShader->LoadShader());

	m_WorldShader = std::make_unique<shader::WorldShader>();
	blitz_debug_assert(m_WorldShader->LoadShader());

	m_GunShader = std::make_unique<shader::GunShader>();
	blitz_debug_assert(m_GunShader->LoadShader());

	glEnable(GL_DEPTH_TEST);
	glEnable(GL_TEXTURE_2D);

	// TODO: fix that
	glEnable(GL_CULL_FACE);
	glCullFace(GL_FRONT);
	glFrontFace(GL_CW);

	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}

MainRenderer::~MainRenderer() {}

void MainRenderer::LoadModels() {
	m_WorldModel = ModelLoader::LoadModel("base_deambu.glb");
	m_PlayerModel = ModelLoader::LoadModel("human.obj");
	m_GunModel = ModelLoader::LoadModel("fingergun.obj");

	m_Texture = TextureLoader::LoadGLTexture("carrelage.png");
	glBindTexture(GL_TEXTURE_2D, m_Texture);
}

void MainRenderer::RenderEntity(const GL::VertexArray& vao, const Vec3f& position, float yaw) {
	m_EntityShader->Start();
	m_EntityShader->SetModelTransform(maths::Dot(maths::RotateY(yaw - maths::PI / 2.0f), maths::Translate(position)));
	vao.Bind();
	glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(vao.GetVertexCount()), GL_UNSIGNED_INT, nullptr);
	vao.Unbind();
}

void MainRenderer::RenderWorld(const GL::VertexArray& vao) {
	m_WorldShader->Start();
	vao.Bind();
	glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(vao.GetVertexCount()), GL_UNSIGNED_INT, nullptr);
	vao.Unbind();
}

void MainRenderer::Render() {
	if (!m_Client->IsConnected()) {
		glClearColor(MenuColor.r, MenuColor.g, MenuColor.b, MenuColor.a);
		return;
	}

	glClear(GL_DEPTH_BUFFER_BIT);
	glClearColor(SkyColor.r, SkyColor.g, SkyColor.b, SkyColor.a);

	RenderWorld();
	RenderPlayers();
	RenderGun();

	m_BulletRenderer.Render();
}

void MainRenderer::RenderPlayers() {
	for (const auto& [playerId, player] : m_Client->GetGame()->GetPlayers()) {
		if (playerId != m_Client->GetPlayerID()) {
			for (auto& Vao : m_PlayerModel.mVaos) {
				RenderEntity(*Vao.get(), player.GetPosition(), player.GetYaw());
			}
		}
	}
}

void MainRenderer::OnGameConfigUpdate() {
	// we need to do that in order to prevent the animation from glitching
	m_ShootTime = 0.0f;
}

void MainRenderer::OnLocalPlayerShoot(const Vec3f& position, float yaw, float pitch) {
	m_ShootTime = 60.0f / static_cast<float>(m_Client->GetGame()->GetGameConfig().FiringRate);
}

void MainRenderer::OnPlayerShoot(game::PlayerID player, const Vec3f& position, float yaw, float pitch) {
	m_BulletRenderer.AddBullet(
		position + Vec3f{0.0f, Camera::GetPlayerEyeHeight(), 0.0f}, yaw, pitch, player == m_Client->GetPlayerID());
}

void MainRenderer::RenderGun() {
	if (!m_Camera.GetAttachedPlayer())
		return;

	m_GunShader->Start();

	float progression = 1.0f - (m_ShootTime / static_cast<float>(60.0f / m_Client->GetGame()->GetGameConfig().FiringRate));

	float angle = progression * progression * progression * 7 - progression * progression * 11 + progression * 4;

	Mat4f transform = maths::RotateX(-angle);
	m_GunShader->SetModelTransform(transform);

	for (auto& Vao : m_GunModel.mVaos) {
		Vao->Bind();
		glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(Vao->GetVertexCount()), GL_UNSIGNED_INT, nullptr);
		Vao->Unbind();
	}
}

void MainRenderer::RenderWorld() {
	// temporary code
	if (m_Camera.GetAttachedPlayer()) {
		m_WorldShader->Start();
		m_WorldShader->SetLightPosition(m_Camera.GetAttachedPlayer()->GetPosition());
	}

	for (auto& Vao : m_WorldModel.mVaos) {
		RenderWorld(*Vao.get());
	}
}

void MainRenderer::Update() {
	if (!m_Client->IsConnected() || !m_Camera.GetAttachedPlayer())
		return;

	float delta = ImGui::GetIO().DeltaTime;

	m_PlayerController.Update(delta);
	m_Camera.Update(delta);
	m_BulletRenderer.Update(delta);

	Mat4f projectionMatrix = m_Camera.GetPerspectiveMatrix();
	Mat4f viewMatrix = m_Camera.GetViewMatrix();

	m_EntityShader->Start();
	m_EntityShader->SetProjectionMatrix(projectionMatrix);
	m_EntityShader->SetViewMatrix(viewMatrix);

	m_WorldShader->Start();
	m_WorldShader->SetProjectionMatrix(projectionMatrix);
	m_WorldShader->SetViewMatrix(viewMatrix);

	m_GunShader->Start();
	m_GunShader->SetProjectionMatrix(projectionMatrix);

	m_ShootTime = std::max(0.0f, m_ShootTime - delta);
}

void MainRenderer::OnSpectatorChange(const game::PlayerID player) {
	game::Player* playerPtr = m_Client->GetGame()->GetPlayerById(player);

	m_Camera.SetAttachedPlayer(playerPtr);
	m_PlayerController.SetAttachedPlayer(playerPtr);
}

}  // namespace render
}  // namespace blitz
