/* Copyright (C) 2025 Wildfire Games.
 * This file is part of 0 A.D.
 *
 * 0 A.D. is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * 0 A.D. is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "precompiled.h"

#include "GameView.h"

#include "graphics/Camera.h"
#include "graphics/CameraController.h"
#include "graphics/CinemaManager.h"
#include "graphics/ColladaManager.h"
#include "graphics/ICameraController.h"
#include "graphics/LOSTexture.h"
#include "graphics/MeshManager.h"
#include "graphics/MiniMapTexture.h"
#include "graphics/ObjectManager.h"
#include "graphics/Patch.h"
#include "graphics/SkeletonAnimManager.h"
#include "graphics/Terrain.h"
#include "graphics/TerrainTextureManager.h"
#include "graphics/TerritoryTexture.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/input.h"
#include "lib/posix/posix_types.h"
#include "lobby/IXmppClient.h"
#include "maths/BoundingBoxAligned.h"
#include "maths/Frustum.h"
#include "maths/Vector3D.h"
#include "ps/Filesystem.h"
#include "ps/Game.h"
#include "ps/Globals.h"
#include "ps/Hotkey.h"
#include "ps/Loader.h"
#include "ps/Profile.h"
#include "ps/TouchInput.h"
#include "ps/World.h"
#include "renderer/Renderer.h"
#include "renderer/SceneRenderer.h"
#include "renderer/WaterManager.h"
#include "simulation2/Simulation2.h"

#include <memory>
#include <string>

namespace Renderer::Backend { class IDevice; }
namespace Renderer::Backend { class IDeviceCommandContext; }

class CGameViewImpl
{
	NONCOPYABLE(CGameViewImpl);
public:
	CGameViewImpl(Renderer::Backend::IDevice* device, CGame* game)
		: Game(game),
		ColladaManager(g_VFS), MeshManager(ColladaManager), SkeletonAnimManager(ColladaManager),
		ObjectManager(MeshManager, SkeletonAnimManager, *game->GetSimulation2()),
		LOSTexture(*game->GetSimulation2()),
		TerritoryTexture(*game->GetSimulation2()),
		MiniMapTexture(device, *game->GetSimulation2()),
		ViewCamera(),
		CullCamera(),
		LockCullCamera(false),
		Culling(true),
		CameraController(new CCameraController(ViewCamera))
	{
	}

	CGame* Game;
	CColladaManager ColladaManager;
	CMeshManager MeshManager;
	CSkeletonAnimManager SkeletonAnimManager;
	CObjectManager ObjectManager;
	CLOSTexture LOSTexture;
	CTerritoryTexture TerritoryTexture;
	CMiniMapTexture MiniMapTexture;

	/**
	 * this camera controls the eye position when rendering
	 */
	CCamera ViewCamera;

	/**
	 * this camera controls the frustum that is used for culling
	 * and shadow calculations
	 *
	 * Note that all code that works with camera movements should only change
	 * m_ViewCamera. The render functions automatically sync the cull camera to
	 * the view camera depending on the value of m_LockCullCamera.
	 */
	CCamera CullCamera;

	/**
	 * When @c true, the cull camera is locked in place.
	 * When @c false, the cull camera follows the view camera.
	 *
	 * Exposed to JS as gameView.lockCullCamera
	 */
	bool LockCullCamera;

	/**
	 * When @c true, culling is enabled so that only models that have a chance of
	 * being visible are sent to the renderer.
	 * Otherwise, the entire world is sent to the renderer.
	 *
	 * Exposed to JS as gameView.culling
	 */
	bool Culling;

	CCinemaManager CinemaManager;

	/**
	 * Controller of the view's camera. We use a std::unique_ptr for an easy
	 * on the fly replacement. It's guaranteed that the pointer is never nulllptr.
	 */
	std::unique_ptr<ICameraController> CameraController;
};

#define IMPLEMENT_BOOLEAN_SETTING(NAME) \
bool CGameView::Get##NAME##Enabled() const \
{ \
	return m->NAME; \
} \
\
void CGameView::Set##NAME##Enabled(bool Enabled) \
{ \
	m->NAME = Enabled; \
}

IMPLEMENT_BOOLEAN_SETTING(Culling);
IMPLEMENT_BOOLEAN_SETTING(LockCullCamera);

bool CGameView::GetConstrainCameraEnabled() const
{
	return m->CameraController->GetConstrainCamera();
}

void CGameView::SetConstrainCameraEnabled(bool enabled)
{
	m->CameraController->SetConstrainCamera(enabled);
}

#undef IMPLEMENT_BOOLEAN_SETTING

CGameView::CGameView(Renderer::Backend::IDevice* device, CGame *pGame):
	m(new CGameViewImpl(device, pGame))
{
	m->CullCamera = m->ViewCamera;
	g_Renderer.GetSceneRenderer().SetSceneCamera(m->ViewCamera, m->CullCamera);
}

CGameView::~CGameView()
{
	UnloadResources();

	delete m;
}

void CGameView::SetViewport(const SViewPort& vp)
{
	m->CameraController->SetViewport(vp);
}

CObjectManager& CGameView::GetObjectManager()
{
	return m->ObjectManager;
}

CCamera* CGameView::GetCamera()
{
	return &m->ViewCamera;
}

CCinemaManager* CGameView::GetCinema()
{
	return &m->CinemaManager;
}

CLOSTexture& CGameView::GetLOSTexture()
{
	return m->LOSTexture;
}

CTerritoryTexture& CGameView::GetTerritoryTexture()
{
	return m->TerritoryTexture;
}

CMiniMapTexture& CGameView::GetMiniMapTexture()
{
	return m->MiniMapTexture;
}

void CGameView::RegisterInit()
{
	// CGameView init
	LDR_Register([this](const double)
	{
		m->CameraController->LoadConfig();
		return 0;
	}, L"CGameView init", 1);

	LDR_Register([](const double)
	{
		return g_TexMan.StartTerrainTextures();
	}, L"StartTerrainTextures", 1);
	LDR_Register([](const double)
	{
		return g_TexMan.PollTerrainTextures();
	}, L"PollTerrainTextures", 60);
}

void CGameView::BeginFrame()
{
	if (m->LockCullCamera == false)
	{
		// Set up cull camera
		m->CullCamera = m->ViewCamera;
	}
	g_Renderer.GetSceneRenderer().SetSceneCamera(m->ViewCamera, m->CullCamera);

	m->Game->CachePlayerColors();
}

void CGameView::Prepare(
	Renderer::Backend::IDeviceCommandContext* deviceCommandContext)
{
	g_Renderer.GetSceneRenderer().PrepareScene(deviceCommandContext, *this);
}

void CGameView::Render(
	Renderer::Backend::IDeviceCommandContext* deviceCommandContext)
{
	g_Renderer.GetSceneRenderer().RenderScene(deviceCommandContext);
}

void CGameView::RenderOverlays(
	Renderer::Backend::IDeviceCommandContext* deviceCommandContext)
{
	g_Renderer.GetSceneRenderer().RenderSceneOverlays(deviceCommandContext);
}

///////////////////////////////////////////////////////////
// This callback is part of the Scene interface
// Submit all objects visible in the given frustum
void CGameView::EnumerateObjects(const CFrustum& frustum, SceneCollector* c)
{
	{
	PROFILE3("submit terrain");

	const CTerrain& terrain = m->Game->GetWorld()->GetTerrain();
	float waterHeight = g_Renderer.GetSceneRenderer().GetWaterManager().m_WaterHeight + 0.001f;
	const ssize_t patchesPerSide = terrain.GetPatchesPerSide();

	// find out which patches will be drawn
	for (ssize_t j=0; j<patchesPerSide; ++j)
	{
		for (ssize_t i=0; i<patchesPerSide; ++i)
		{
			CPatch* const patch = terrain.GetPatch(i,j);	// can't fail

			// If the patch is underwater, calculate a bounding box that also contains the water plane
			CBoundingBoxAligned bounds = patch->GetWorldBounds();
			if(bounds[1].Y < waterHeight)
				bounds[1].Y = waterHeight;

			if (!m->Culling || frustum.IsBoxVisible(bounds))
				c->Submit(patch);
		}
	}
	}

	m->Game->GetSimulation2()->RenderSubmit(*c, frustum, m->Culling);
}

void CGameView::UnloadResources()
{
	g_TexMan.UnloadTerrainTextures();
	g_Renderer.GetSceneRenderer().GetWaterManager().UnloadWaterTextures();
}

void CGameView::Update(const float deltaRealTime)
{
	m->MiniMapTexture.Update(deltaRealTime);

	// If camera movement is being handled by the touch-input system,
	// then we should stop to avoid conflicting with it
	if (g_TouchInput.IsEnabled())
		return;

	if (!g_app_has_focus)
		return;

	m->CinemaManager.Update(deltaRealTime);
	if (m->CinemaManager.IsEnabled())
		return;

	m->CameraController->Update(deltaRealTime);
}

CVector3D CGameView::GetCameraPivot() const
{
	return m->CameraController->GetCameraPivot();
}

CVector3D CGameView::GetCameraPosition() const
{
	return m->CameraController->GetCameraPosition();
}

CVector3D CGameView::GetCameraRotation() const
{
	return m->CameraController->GetCameraRotation();
}

float CGameView::GetCameraZoom() const
{
	return m->CameraController->GetCameraZoom();
}

void CGameView::SetCamera(const CVector3D& pos, float rotX, float rotY, float zoom)
{
	m->CameraController->SetCamera(pos, rotX, rotY, zoom);
}

void CGameView::MoveCameraTarget(const CVector3D& target)
{
	m->CameraController->MoveCameraTarget(target);
}

void CGameView::ResetCameraTarget(const CVector3D& target)
{
	m->CameraController->ResetCameraTarget(target);
}

void CGameView::FollowEntity(entity_id_t entity, bool firstPerson)
{
	m->CameraController->FollowEntity(entity, firstPerson);
}

entity_id_t CGameView::GetFollowedEntity()
{
	return m->CameraController->GetFollowedEntity();
}

InReaction game_view_handler(const SDL_Event_* ev)
{
	// put any events that must be processed even if inactive here
	if (!g_app_has_focus || !g_Game || !g_Game->IsGameStarted() || g_Game->GetView()->GetCinema()->IsEnabled())
		return IN_PASS;

	CGameView *pView=g_Game->GetView();

	return pView->HandleEvent(ev);
}

InReaction CGameView::HandleEvent(const SDL_Event_* ev)
{
	switch(ev->ev.type)
	{
	case SDL_HOTKEYPRESS:
	{
		std::string hotkey = static_cast<const char*>(ev->ev.user.data1);
		CSceneRenderer& sceneRenderer = g_Renderer.GetSceneRenderer();
		if (hotkey == "wireframe")
		{
			if (g_XmppClient && g_rankedGame == true)
				break;
			else if (sceneRenderer.GetModelRenderMode() == SOLID)
			{
				sceneRenderer.SetTerrainRenderMode(EDGED_FACES);
				sceneRenderer.SetWaterRenderMode(EDGED_FACES);
				sceneRenderer.SetModelRenderMode(EDGED_FACES);
				sceneRenderer.SetOverlayRenderMode(EDGED_FACES);
			}
			else if (sceneRenderer.GetModelRenderMode() == EDGED_FACES)
			{
				sceneRenderer.SetTerrainRenderMode(WIREFRAME);
				sceneRenderer.SetWaterRenderMode(WIREFRAME);
				sceneRenderer.SetModelRenderMode(WIREFRAME);
				sceneRenderer.SetOverlayRenderMode(WIREFRAME);
			}
			else
			{
				sceneRenderer.SetTerrainRenderMode(SOLID);
				sceneRenderer.SetWaterRenderMode(SOLID);
				sceneRenderer.SetModelRenderMode(SOLID);
				sceneRenderer.SetOverlayRenderMode(SOLID);
			}
			return IN_HANDLED;
		}
	}
	}

	return m->CameraController->HandleEvent(ev);
}
