/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

#include "GraphicsAppManager.hpp"
#include "Base/IPlatform.hpp"
#include "Base/Logger.hpp"
#include "Base/FrameMetadata.hpp"
#include "PlatformProducer.hpp"
#include "Input/InputManager.hpp"
#include "Base/EngineConfiguration.hpp"
#include <chrono>
#include <thread>
#include <stdexcept>

#if __has_include("tracy/Tracy.hpp")
	#include <tracy/Tracy.hpp>
	#define HAS_TRACY
#endif

namespace OpenVulkano
{
	using clock = std::chrono::steady_clock;

	GraphicsAppManager::GraphicsAppManager(OpenVulkano::IGraphicsApp* app, RenderAPI::RenderApi renderApi)
		: app(app), renderApi(renderApi)
	{
		Utils::SetThreadName("Main");
#ifdef HAS_TRACY
		ZoneScoped;
#endif


		Logger::SetupLogger();
		if (!app)
		{
			constexpr auto msg = "The app must not be null!";
			Logger::MANAGER->error(msg);
			throw std::runtime_error(msg);
		}
		platform = std::unique_ptr<IPlatform>(PlatformProducer::CreatePlatform(renderApi));
		window = platform->MakeWindow();
		renderer = std::unique_ptr<IRenderer>(PlatformProducer::CreateRenderManager(renderApi));
		app->SetGraphicsAppManager(this);
		window->SetWindowHandler(this);
		inputManager = Input::InputManager::GetInstance();
		engineConfig = EngineConfiguration::GetEngineConfiguration();
		engineConfig->OnFpsCapChanged += EventHandler(this, &GraphicsAppManager::UpdateCappedFpsInfo);
		// set initial values
		if (engineConfig->GetFpsCap() > 0)
		{
			UpdateCappedFpsInfo(engineConfig->GetFpsCap());
		}
	}

	GraphicsAppManager::GraphicsAppManager(IGraphicsApp* app, IWindow* window, RenderAPI::RenderApi renderApi)
			: app(app), renderApi(renderApi)
	{
		Utils::SetThreadName("Main");
#ifdef HAS_TRACY
		ZoneScoped;
#endif

		Logger::SetupLogger();
		if (!app)
		{
			constexpr auto msg = "The app must not be null!";
			Logger::MANAGER->error(msg);
			throw std::runtime_error(msg);
		}
		//platform = std::unique_ptr<IPlatform>(PlatformProducer::CreatePlatform(renderApi));
		this->window = window;
		renderer = std::unique_ptr<IRenderer>(PlatformProducer::CreateRenderManager(renderApi));
		app->SetGraphicsAppManager(this);
		window->SetWindowHandler(this);
		inputManager = OpenVulkano::Input::InputManager::GetInstance();
		engineConfig = OpenVulkano::EngineConfiguration::GetEngineConfiguration();
		engineConfig->OnFpsCapChanged += EventHandler(this, &GraphicsAppManager::UpdateCappedFpsInfo);
		// set initial values
		if (engineConfig->GetFpsCap() > 0)
		{
			UpdateCappedFpsInfo(engineConfig->GetFpsCap());
		}
	}

	void GraphicsAppManager::UpdateCappedFpsInfo(int32_t newFpsCap)
	{ 
		if (newFpsCap <= 0)
		{
			cappedFrameTime = std::chrono::microseconds(0);
		}
		else
		{
			cappedFrameTime = std::chrono::microseconds(1'000'000 / newFpsCap);
		}
	}

	void GraphicsAppManager::OnCappedFPS(const auto& frameStartTime) 
	{
		auto frameEndTime = frameStartTime + cappedFrameTime;
		while (clock::now() < frameEndTime)
		{
			std::this_thread::yield();
		}
	}

	GraphicsAppManager::~GraphicsAppManager() noexcept
	{
		if (windowTitleFormat.empty()) return;
		engineConfig->OnFpsCapChanged -= EventHandler(this, &GraphicsAppManager::UpdateCappedFpsInfo);
		ShutDown();
	}

	void GraphicsAppManager::Stop()
	{
		running = false;
		Logger::MANAGER->info("Graphics application stopped");
	}

	void GraphicsAppManager::Pause()
	{
		paused = true;
		frameTimer.Stop();
		Logger::MANAGER->info("Graphics application paused");
	}

	void GraphicsAppManager::Resume()
	{
		paused = false;
		frameTimer.Start();
		Logger::MANAGER->info("Graphics application resumed");
	}

	void GraphicsAppManager::Run()
	{
		running = true;
		StartUp();
		frameTimer.Reset();
		Loop(); // Runs the rendering loop
		ShutDown();
	}

	void GraphicsAppManager::StartUp()
	{
#ifdef HAS_TRACY
		ZoneScoped;
#endif
		try
		{
			Logger::MANAGER->info("Initializing ...");
			app->Init();
			if (platform) platform->Init();
			window->Init(renderApi);
			//TODO restore window settings if there are any set
			renderer->Init(static_cast<IGraphicsAppManager*>(this), window);
			windowTitleFormat = app->GetAppName() + " " + static_cast<std::string>(app->GetAppVersion()) + " - " + renderer->GetMainRenderDeviceName() + " - {:.1f} fps ({:.1f} ms)";
			app->InitPostGraphics();
			Logger::MANAGER->info("Initialized");
		}
		catch (std::exception& e)
		{
			Logger::MANAGER->error("Failed to initiate: {0}", e.what());
			running = false;
#ifdef DEBUG
			throw e;
#endif
		}
	}

	void GraphicsAppManager::Loop()
	{
		while (running)
		{
			LoopTick();
		}
	}

	void GraphicsAppManager::LoopTick()
	{
#ifdef HAS_TRACY
		FrameMark;
		ZoneScoped;
#endif

		if (platform) platform->Tick();
		if (paused)
		{ // The rendering is paused
			// No need to burn cpu time if the app is paused
			std::this_thread::sleep_for(std::chrono::milliseconds(50));
		}
		else
		{
			auto start = clock::now();
			inputManager->Tick();
			app->Tick();
			if (CURRENT_FRAME.needsRedraw)
			{
				CURRENT_FRAME.needsRedraw = !EngineConfiguration::GetEngineConfiguration()->GetLazyRendering();
				renderer->Tick();
			}
			if (cappedFrameTime.count())
			{
				OnCappedFPS(start);
			}
			frameTimer.Tick();
			UpdateFps();
			CURRENT_FRAME.frameId = frameCount;
			CURRENT_FRAME.frameTime = frameTimer.GetTickSeconds();
		}
	}

	void GraphicsAppManager::ShutDown()
	{
#ifdef HAS_TRACY
		ZoneScoped;
#endif
		Logger::MANAGER->info("Shutting down ...");
		app->Close();
		renderer->Close();
		window->Close();
		if (platform) platform->Close();
		windowTitleFormat = "";
		app->CloseFinalize();
		Logger::MANAGER->info("Shutdown complete");
	}

	void GraphicsAppManager::UpdateFps()
	{
		frameCount++;
		fpsTimer += frameTimer.GetTickSeconds();

		if(fpsTimer > 1.0f)
		{
			avgFps = static_cast<float>(frameCount - lastFrameCount) / fpsTimer;
			avgFrameTime = (1 / avgFps) * 1000.0f;
			lastFrameCount = frameCount;
			fpsTimer = 0;
			if (window->HasTitle())
			{
				window->SetTitle(fmt::format(fmt::runtime(windowTitleFormat), avgFps, avgFrameTime));
			}
		}
	}

	void GraphicsAppManager::OnWindowMinimize(OpenVulkano::IWindow* window)
	{
		if (window != this->window) return;
		Pause();
	}

	void GraphicsAppManager::OnWindowRestore(OpenVulkano::IWindow* window)
	{
		if (window != this->window) return;
		Resume();
		CURRENT_FRAME.needsRedraw = true;
	}

	void GraphicsAppManager::OnWindowFocusLost(IWindow* window)
	{}

	void GraphicsAppManager::OnWindowFocusGained(IWindow* window)
	{}

	void GraphicsAppManager::OnWindowMove(IWindow* window, int posX, int posY)
	{
		//TODO save window pos
	}

	void GraphicsAppManager::OnWindowResize(OpenVulkano::IWindow* window, const uint32_t newWidth, const uint32_t newHeight)
	{
		if(window != this->window) return;
		if (window->GetWidth() != newWidth || window->GetHeight() != newHeight) window->SetSize(newWidth, newHeight);
		renderer->Resize(newWidth, newHeight);
		CURRENT_FRAME.needsRedraw = true;
	}

	void GraphicsAppManager::OnWindowClose(OpenVulkano::IWindow* window)
	{
		window->SetWindowHasBeenDestroyed();
		if (window != this->window) return;
		Stop();
	}
}