/*
    Created on: Oct 2, 2019

	Copyright 2019 flyinghead

	This file is part of Flycast.

    Flycast 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.

    Flycast 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 Flycast.  If not, see <https://www.gnu.org/licenses/>.
*/
#include "vulkan.h"
#include "vulkan_renderer.h"
#include "drawer.h"
#include "hw/pvr/ta.h"
#include "rend/transform_matrix.h"

bool BaseVulkanRenderer::BaseInit(vk::RenderPass renderPass, int subpass)
{
	texCommandPool.Init();
	fbCommandPool.Init();
	quadPipeline = std::make_unique<QuadPipeline>(false, false);
	quadPipeline->Init(&shaderManager, renderPass, subpass);
	framebufferDrawer = std::make_unique<QuadDrawer>();
	framebufferDrawer->Init(quadPipeline.get());

	return true;
}

void BaseVulkanRenderer::Term()
{
	GetContext()->WaitIdle();
	GetContext()->PresentFrame(nullptr, nullptr, vk::Extent2D(), 0);
#if defined(VIDEO_ROUTING) && defined(TARGET_MAC)
	os_VideoRoutingTermVk();
#endif
	framebufferDrawer.reset();
	quadPipeline.reset();
	textureCache.Clear();
	fogTexture = nullptr;
	paletteTexture = nullptr;
	texCommandPool.Term();
	fbCommandPool.Term();
	framebufferTextures.clear();
	framebufferTexIndex = 0;
	shaderManager.term();
}

BaseTextureCacheData *BaseVulkanRenderer::GetTexture(TSP tsp, TCW tcw, int area)
{
	Texture* tf = textureCache.getTextureCacheData(tsp, tcw, area);

	//update if needed
	if (tf->NeedsUpdate())
	{
		// This kills performance when a frame is skipped and lots of texture updated each frame
		//if (textureCache.IsInFlight(tf, true))
		//	textureCache.DestroyLater(tf);
		tf->SetCommandBuffer(texCommandBuffer);
		if (!tf->Update())
		{
			tf->SetCommandBuffer(nullptr);
			return nullptr;
		}
	}
	else if (tf->IsCustomTextureAvailable())
	{
		tf->deferDeleteResource(&texCommandPool);
		tf->SetCommandBuffer(texCommandBuffer);
		tf->CheckCustomTexture();
	}
	tf->SetCommandBuffer(nullptr);
	textureCache.SetInFlight(tf);

	return tf;
}

void BaseVulkanRenderer::Process(TA_context* ctx)
{
	rendContext = &ctx->rend;
	if (!ctx->rend.isRTT) {
		framebufferRendered = false;
		if (!config::EmulateFramebuffer)
			clearLastFrame = false;
	}
	if (resetTextureCache) {
		textureCache.Clear();
		resetTextureCache = false;
	}

	texCommandPool.BeginFrame();
	textureCache.SetCurrentIndex(texCommandPool.GetIndex());
	textureCache.Cleanup();

	texCommandBuffer = texCommandPool.Allocate();
	texCommandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit));

	ta_parse(ctx, true);

	// TODO can't update fog or palette twice in multi render
	CheckFogTexture();
	CheckPaletteTexture();
	texCommandBuffer.end();
}

void BaseVulkanRenderer::ReInitOSD()
{
	texCommandPool.Init();
	fbCommandPool.Init();
}

void BaseVulkanRenderer::RenderFramebuffer(const FramebufferInfo& info)
{
	rendContext = nullptr;
	framebufferTexIndex = (framebufferTexIndex + 1) % GetContext()->GetSwapChainSize();

	if (framebufferTextures.size() != GetContext()->GetSwapChainSize())
		framebufferTextures.resize(GetContext()->GetSwapChainSize());
	std::unique_ptr<Texture>& curTexture = framebufferTextures[framebufferTexIndex];
	if (!curTexture)
	{
		curTexture = std::make_unique<Texture>();
		curTexture->tex_type = TextureType::_8888;
	}

	fbCommandPool.BeginFrame();
	vk::CommandBuffer commandBuffer = fbCommandPool.Allocate();

	commandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit));
	curTexture->SetCommandBuffer(commandBuffer);
	{
		static const float scopeColor[4] = { 0.0f, 1.0f, 0.0f, 1.0f };
		CommandBufferDebugScope _(commandBuffer, "RenderFramebuffer", scopeColor);

		if (info.fb_r_ctrl.fb_enable == 0 || info.vo_control.blank_video == 1)
		{
			// Video output disabled
			u8 rgba[]{ (u8)info.vo_border_col._red, (u8)info.vo_border_col._green, (u8)info.vo_border_col._blue, 255 };
			curTexture->UploadToGPU(1, 1, rgba, false);
		}
		else
		{
			PixelBuffer<u32> pb;
			int width;
			int height;
			ReadFramebuffer(info, pb, width, height);

			curTexture->UploadToGPU(width, height, (u8*)pb.data(), false);
		}

	}
	curTexture->SetCommandBuffer(nullptr);
	commandBuffer.end();
	fbCommandPool.EndFrame();
	framebufferRendered = true;
	clearLastFrame = false;
}

void BaseVulkanRenderer::RenderVideoRouting()
{
#if defined(VIDEO_ROUTING) && defined(TARGET_MAC)
	if (config::VideoRouting)
	{
		auto device = GetContext()->GetDevice();
		auto srcImage = device.getSwapchainImagesKHR(GetContext()->GetSwapChain())[GetContext()->GetCurrentImageIndex()];
		auto graphicsQueue = device.getQueue(GetContext()->GetGraphicsQueueFamilyIndex(), 0);

		int targetWidth = (config::VideoRoutingScale ? config::VideoRoutingVRes * settings.display.width / settings.display.height : settings.display.width);
		int targetHeight = (config::VideoRoutingScale ? config::VideoRoutingVRes : settings.display.height);

		extern void os_VideoRoutingPublishFrameTexture(const vk::Device& device, const vk::Image& image, const vk::Queue& queue, float x, float y, float w, float h);
		os_VideoRoutingPublishFrameTexture(device, srcImage, graphicsQueue, 0, 0, targetWidth, targetHeight);
	}
	else
	{
		os_VideoRoutingTermVk();
	}
#endif
}

void BaseVulkanRenderer::CheckFogTexture()
{
	if (!fogTexture)
	{
		fogTexture = std::make_unique<Texture>();
		fogTexture->tex_type = TextureType::_8;
		updateFogTable = true;
	}
	if (!updateFogTable || !config::Fog)
		return;
	updateFogTable = false;
	u8 texData[256];
	MakeFogTexture(texData);

	fogTexture->SetCommandBuffer(texCommandBuffer);
	fogTexture->UploadToGPU(128, 2, texData, false);
	fogTexture->SetCommandBuffer(nullptr);
}

void BaseVulkanRenderer::CheckPaletteTexture()
{
	if (!paletteTexture) {
		paletteTexture = std::make_unique<Texture>();
		paletteTexture->tex_type = TextureType::_8888;
	}
	else if (!updatePalette) {
		return;
	}
	updatePalette = false;

	paletteTexture->SetCommandBuffer(texCommandBuffer);
	paletteTexture->UploadToGPU(1024, 1, (u8 *)palette32_ram, false);
	paletteTexture->SetCommandBuffer(nullptr);
}

bool BaseVulkanRenderer::presentFramebuffer()
{
	if (framebufferTexIndex >= (int)framebufferTextures.size())
		return false;
	Texture *fbTexture = framebufferTextures[framebufferTexIndex].get();
	if (fbTexture == nullptr)
		return false;
	GetContext()->PresentFrame(fbTexture->GetImage(), fbTexture->GetImageView(), fbTexture->getSize(),
			getDCFramebufferAspectRatio());
	framebufferRendered = false;
	return true;
}

class VulkanRenderer final : public BaseVulkanRenderer
{
public:
	bool Init() override
	{
		NOTICE_LOG(RENDERER, "VulkanRenderer::Init");
		try {
			textureDrawer.Init(&samplerManager, &shaderManager, &textureCache);
			textureDrawer.SetCommandPool(&texCommandPool);

			screenDrawer.Init(&samplerManager, &shaderManager, viewport);
			screenDrawer.SetCommandPool(&texCommandPool);
			BaseInit(screenDrawer.GetRenderPass());
			emulateFramebuffer = config::EmulateFramebuffer;

			return true;
		} catch (const vk::SystemError& e) {
			ERROR_LOG(RENDERER, "Vulkan system error %s", e.what());
			return false;
		}
	}

	void Term() override
	{
		try {
			DEBUG_LOG(RENDERER, "VulkanRenderer::Term");
			GetContext()->WaitIdle();
			texCommandPool.Term(); // make sure all in-flight buffers are returned
			screenDrawer.Term();
			textureDrawer.Term();
			samplerManager.term();
			BaseVulkanRenderer::Term();
		} catch (const vk::SystemError& e) {
			ERROR_LOG(RENDERER, "Vulkan system error %s", e.what());
		}
	}

	void Process(TA_context* ctx) override
	{
		try {
			if (emulateFramebuffer != config::EmulateFramebuffer)
			{
				screenDrawer.EndRenderPass();
				VulkanContext::Instance()->WaitIdle();
				screenDrawer.Term();
				screenDrawer.Init(&samplerManager, &shaderManager, viewport);
				BaseInit(screenDrawer.GetRenderPass());
				emulateFramebuffer = config::EmulateFramebuffer;
			}
			else if (ctx->rend.isRTT) {
				screenDrawer.EndRenderPass();
			}
			BaseVulkanRenderer::Process(ctx);
		} catch (const vk::OutOfDeviceMemoryError& e) {
			ERROR_LOG(RENDERER, "Vulkan out of device memory: %s", e.what());
			throw RendererException("Out of device memory");
		} catch (const vk::SystemError& e) {
			ERROR_LOG(RENDERER, "Vulkan system error %s", e.what());
			throw RendererException("Vulkan system error");
		}
	}

	bool Render() override
	{
		try {
			Drawer *drawer;
			if (rendContext->isRTT)
				drawer = &textureDrawer;
			else {
				resize(rendContext->framebufferWidth, rendContext->framebufferHeight);
				drawer = &screenDrawer;
			}
			drawer->setRendContext(rendContext);
			drawer->Draw(fogTexture.get(), paletteTexture.get());
			if (config::EmulateFramebuffer || rendContext->isRTT)
				// delay ending the render pass in case of multi render
				drawer->EndRenderPass();

			return !rendContext->isRTT;
		} catch (const vk::OutOfDeviceMemoryError& e) {
			ERROR_LOG(RENDERER, "Vulkan out of device memory: %s", e.what());
			throw RendererException("Out of device memory");
		} catch (const vk::SystemError& e) {
			// Sometimes happens when resizing the window
			ERROR_LOG(RENDERER, "Vulkan system error %s", e.what());
			throw RendererException("Vulkan system error");
		}
	}

	bool Present() override
	{
		if (clearLastFrame)
			return false;
		try {
			if (config::EmulateFramebuffer || framebufferRendered)
				return presentFramebuffer();
			else
				return screenDrawer.PresentFrame();
		} catch (const vk::SystemError& e) {
			ERROR_LOG(RENDERER, "Vulkan system error %s", e.what());
			throw RendererException("Vulkan system error");
		}
	}

protected:
	void resize(int w, int h) override
	{
		if ((u32)w == viewport.width && (u32)h == viewport.height)
			return;
		BaseVulkanRenderer::resize(w, h);
		GetContext()->WaitIdle();
		screenDrawer.Init(&samplerManager, &shaderManager, viewport);
	}

private:
	SamplerManager samplerManager;
	ScreenDrawer screenDrawer;
	TextureDrawer textureDrawer;
	bool emulateFramebuffer = false;
};

Renderer* rend_Vulkan()
{
	return new VulkanRenderer();
}

void ReInitOSD()
{
	if (renderer != nullptr) {
		BaseVulkanRenderer *vkrenderer = dynamic_cast<BaseVulkanRenderer*>(renderer);
		if (vkrenderer != nullptr)
			vkrenderer->ReInitOSD();
	}
}
