#include <algorithm>

#include "Common/Log.h"
#include "Common/GPU/Vulkan/VulkanContext.h"
#include "Common/GPU/Vulkan/VulkanAlloc.h"
#include "Common/GPU/Vulkan/VulkanImage.h"
#include "Common/GPU/Vulkan/VulkanMemory.h"
#include "Common/GPU/Vulkan/VulkanBarrier.h"
#include "Common/StringUtils.h"

using namespace PPSSPP_VK;

VulkanTexture::VulkanTexture(VulkanContext *vulkan, const char *tag)
	: vulkan_(vulkan) {
	truncate_cpy(tag_, tag);
}

void VulkanTexture::Wipe() {
	if (view_ != VK_NULL_HANDLE) {
		vulkan_->Delete().QueueDeleteImageView(view_);
	}
	if (image_ != VK_NULL_HANDLE) {
		_dbg_assert_(allocation_ != VK_NULL_HANDLE);
		vulkan_->Delete().QueueDeleteImageAllocation(image_, allocation_);
	}
}

static bool IsDepthStencilFormat(VkFormat format) {
	switch (format) {
	case VK_FORMAT_D16_UNORM:
	case VK_FORMAT_D16_UNORM_S8_UINT:
	case VK_FORMAT_D24_UNORM_S8_UINT:
	case VK_FORMAT_D32_SFLOAT:
	case VK_FORMAT_D32_SFLOAT_S8_UINT:
		return true;
	default:
		return false;
	}
}

bool VulkanTexture::CreateDirect(int w, int h, int depth, int numMips, VkFormat format, VkImageLayout initialLayout, VkImageUsageFlags usage, VulkanBarrierBatch *barrierBatch, const VkComponentMapping *mapping) {
	if (w == 0 || h == 0 || numMips == 0) {
		ERROR_LOG(Log::G3D, "Can't create a zero-size VulkanTexture");
		return false;
	}
	int maxDim = vulkan_->GetPhysicalDeviceProperties(0).properties.limits.maxImageDimension2D;
	if (w > maxDim || h > maxDim) {
		ERROR_LOG(Log::G3D, "Can't create a texture this large");
		return false;
	}

	Wipe();

	width_ = (int16_t)w;
	height_ = (int16_t)h;
	depth_ = (int16_t)depth;
	numMips_ = (int16_t)numMips;
	format_ = format;

	VkImageAspectFlags aspect = IsDepthStencilFormat(format) ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT;

	VkImageCreateInfo image_create_info{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
	image_create_info.imageType = depth > 1 ? VK_IMAGE_TYPE_3D : VK_IMAGE_TYPE_2D;
	image_create_info.format = format_;
	image_create_info.extent.width = width_;
	image_create_info.extent.height = height_;
	image_create_info.extent.depth = depth;
	image_create_info.mipLevels = numMips;
	image_create_info.arrayLayers = 1;
	image_create_info.samples = VK_SAMPLE_COUNT_1_BIT;
	image_create_info.flags = 0;
	image_create_info.tiling = VK_IMAGE_TILING_OPTIMAL;
	image_create_info.usage = usage;
	if (initialLayout == VK_IMAGE_LAYOUT_PREINITIALIZED) {
		image_create_info.initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED;
	} else {
		image_create_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
	}

	// The graphics debugger always "needs" TRANSFER_SRC but in practice doesn't matter - 
	// unless validation is on. So let's only force it on when being validated, for now.
	if (vulkan_->GetInitFlags() & VulkanInitFlags::VALIDATE) {
		image_create_info.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
	}
	VmaAllocationCreateInfo allocCreateInfo{};
	allocCreateInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY;
	VmaAllocationInfo allocInfo{};
	VkResult res = vmaCreateImage(vulkan_->Allocator(), &image_create_info, &allocCreateInfo, &image_, &allocation_, &allocInfo);
	if (res != VK_SUCCESS) {
		ERROR_LOG(Log::G3D, "vmaCreateImage failed: %s. Destroying image.", VulkanResultToString(res));
		_dbg_assert_msg_(res == VK_ERROR_OUT_OF_HOST_MEMORY || res == VK_ERROR_OUT_OF_DEVICE_MEMORY || res == VK_ERROR_TOO_MANY_OBJECTS, "%d", (int)res);
		view_ = VK_NULL_HANDLE;
		image_ = VK_NULL_HANDLE;
		allocation_ = VK_NULL_HANDLE;
		return false;
	}

	// Apply the tag
	vulkan_->SetDebugName(image_, VK_OBJECT_TYPE_IMAGE, tag_);

	// Write a command to transition the image to the requested layout, if it's not already that layout.
	// TODO: We may generate mipmaps right after, so can't add to the end of frame batch. Well actually depending
	// on the amount of mips we probably sometimes can..

	if (initialLayout != VK_IMAGE_LAYOUT_UNDEFINED && initialLayout != VK_IMAGE_LAYOUT_PREINITIALIZED) {
		VkPipelineStageFlags dstStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
		VkAccessFlagBits dstAccessFlags = (VkAccessFlagBits)0;
		switch (initialLayout) {
		case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:
			dstStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
			dstAccessFlags = VK_ACCESS_TRANSFER_WRITE_BIT;
			break;
		case VK_IMAGE_LAYOUT_GENERAL:
			// We use this initial layout when we're about to write to the image using a compute shader, only.
			dstStage = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
			dstAccessFlags = VK_ACCESS_SHADER_READ_BIT;
			break;
		default:
			// If you planned to use UploadMip, you want VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL. After the
			// upload, you can transition using EndCreate.
			_assert_(false);
			break;
		}
		barrierBatch->TransitionImage(image_, 0, numMips, 1, VK_IMAGE_ASPECT_COLOR_BIT,
			VK_IMAGE_LAYOUT_UNDEFINED, initialLayout,
			0, dstAccessFlags,
			VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, dstStage);
	}

	// Create the view while we're at it.
	VkImageViewCreateInfo view_info{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
	view_info.image = image_;
	view_info.viewType = depth > 1 ? VK_IMAGE_VIEW_TYPE_3D : VK_IMAGE_VIEW_TYPE_2D;
	view_info.format = format_;
	if (mapping) {
		view_info.components = *mapping;
	} else {
		view_info.components = { VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY };
	}
	view_info.subresourceRange.aspectMask = aspect;
	view_info.subresourceRange.baseMipLevel = 0;
	view_info.subresourceRange.levelCount = numMips;
	view_info.subresourceRange.baseArrayLayer = 0;
	view_info.subresourceRange.layerCount = 1;

	res = vkCreateImageView(vulkan_->GetDevice(), &view_info, NULL, &view_);
	if (res != VK_SUCCESS) {
		ERROR_LOG(Log::G3D, "vkCreateImageView failed: %s. Destroying image.", VulkanResultToString(res));
		_assert_msg_(res == VK_ERROR_OUT_OF_HOST_MEMORY || res == VK_ERROR_OUT_OF_DEVICE_MEMORY || res == VK_ERROR_TOO_MANY_OBJECTS, "%d", (int)res);
		vmaDestroyImage(vulkan_->Allocator(), image_, allocation_);
		view_ = VK_NULL_HANDLE;
		image_ = VK_NULL_HANDLE;
		allocation_ = VK_NULL_HANDLE;
		return false;
	}
	vulkan_->SetDebugName(view_, VK_OBJECT_TYPE_IMAGE_VIEW, tag_);

	// Additionally, create an array view, but only if it's a 2D texture.
	if (view_info.viewType == VK_IMAGE_VIEW_TYPE_2D) {
		view_info.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY;
		res = vkCreateImageView(vulkan_->GetDevice(), &view_info, NULL, &arrayView_);
		// Assume that if the above view creation succeeded, so will this.
		_assert_msg_(res == VK_SUCCESS, "View creation failed: %d", (int)res);
		vulkan_->SetDebugName(arrayView_, VK_OBJECT_TYPE_IMAGE_VIEW, tag_);
	}

	return true;
}

void VulkanTexture::CopyBufferToMipLevel(VkCommandBuffer cmd, TextureCopyBatch *copyBatch, int mip, int mipWidth, int mipHeight, int depthLayer, VkBuffer buffer, uint32_t offset, size_t rowLength) {
	VkBufferImageCopy &copy_region = copyBatch->copies.push_uninitialized();
	copy_region.bufferOffset = offset;
	copy_region.bufferRowLength = (uint32_t)rowLength;
	copy_region.bufferImageHeight = 0;  // 2D
	copy_region.imageOffset.x = 0;
	copy_region.imageOffset.y = 0;
	copy_region.imageOffset.z = depthLayer;
	copy_region.imageExtent.width = mipWidth;
	copy_region.imageExtent.height = mipHeight;
	copy_region.imageExtent.depth = 1;
	copy_region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
	copy_region.imageSubresource.mipLevel = mip;
	copy_region.imageSubresource.baseArrayLayer = 0;
	copy_region.imageSubresource.layerCount = 1;

	_dbg_assert_(mip < numMips_);

	if (!copyBatch->buffer) {
		copyBatch->buffer = buffer;
	} else if (copyBatch->buffer != buffer) {
		// Need to flush the batch if this image isn't from the same buffer as the previous ones.
		FinishCopyBatch(cmd, copyBatch);
		copyBatch->buffer = buffer;
	}
}

void VulkanTexture::FinishCopyBatch(VkCommandBuffer cmd, TextureCopyBatch *copyBatch) {
	if (!copyBatch->copies.empty()) {
		vkCmdCopyBufferToImage(cmd, copyBatch->buffer, image_, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, (uint32_t)copyBatch->copies.size(), copyBatch->copies.data());
		copyBatch->copies.clear();
	}
}

void VulkanTexture::ClearMip(VkCommandBuffer cmd, int mip, uint32_t value) {
	// Must be in TRANSFER_DST mode.
	VkClearColorValue clearVal;
	for (int i = 0; i < 4; i++) {
		clearVal.float32[i] = ((value >> (i * 8)) & 0xFF) / 255.0f;
	}
	VkImageSubresourceRange range{};
	range.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
	range.layerCount = 1;
	range.baseMipLevel = mip;
	range.levelCount = 1;
	vkCmdClearColorImage(cmd, image_, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearVal, 1, &range);
}

// Low-quality mipmap generation by bilinear blit, but works okay.
void VulkanTexture::GenerateMips(VkCommandBuffer cmd, int firstMipToGenerate, bool fromCompute) {
	_assert_msg_(firstMipToGenerate > 0, "Cannot generate the first level");
	_assert_msg_(firstMipToGenerate < numMips_, "Can't generate levels beyond storage");

	VulkanBarrierBatch batch;
	// Transition the pre-set levels to GENERAL.

	VkImageMemoryBarrier *barrier = batch.Add(image_,
		fromCompute ? VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT : VK_PIPELINE_STAGE_TRANSFER_BIT,
		VK_PIPELINE_STAGE_TRANSFER_BIT, 0);
	barrier->oldLayout = fromCompute ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
	barrier->newLayout = VK_IMAGE_LAYOUT_GENERAL;
	barrier->srcAccessMask = fromCompute ? VK_ACCESS_SHADER_WRITE_BIT : VK_ACCESS_TRANSFER_WRITE_BIT;
	barrier->dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
	barrier->subresourceRange.levelCount = firstMipToGenerate;

	barrier = batch.Add(image_,
		VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
		VK_PIPELINE_STAGE_TRANSFER_BIT, 0);
	barrier->subresourceRange.baseMipLevel = firstMipToGenerate;
	barrier->subresourceRange.levelCount = numMips_ - firstMipToGenerate;
	barrier->oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
	barrier->newLayout = VK_IMAGE_LAYOUT_GENERAL;
	barrier->srcAccessMask = 0;
	barrier->dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;

	batch.Flush(cmd);

	// Now we can blit and barrier the whole pipeline.
	for (int mip = firstMipToGenerate; mip < numMips_; mip++) {
		VkImageBlit blit{};
		blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
		blit.srcSubresource.layerCount = 1;
		blit.srcSubresource.mipLevel = mip - 1;
		blit.srcOffsets[1].x = std::max(width_ >> (mip - 1), 1);
		blit.srcOffsets[1].y = std::max(height_ >> (mip - 1), 1);
		blit.srcOffsets[1].z = 1;

		blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
		blit.dstSubresource.layerCount = 1;
		blit.dstSubresource.mipLevel = mip;
		blit.dstOffsets[1].x = std::max(width_ >> mip, 1);
		blit.dstOffsets[1].y = std::max(height_ >> mip, 1);
		blit.dstOffsets[1].z = 1;

		// TODO: We could do better with the image transitions - would be enough with one per level
		// for the memory barrier, then one final one for the whole stack when done. This function
		// currently doesn't have a global enough view, though.
		// We should also coalesce barriers across multiple texture uploads in a frame and all kinds of other stuff, but...

		vkCmdBlitImage(cmd, image_, VK_IMAGE_LAYOUT_GENERAL, image_, VK_IMAGE_LAYOUT_GENERAL, 1, &blit, VK_FILTER_LINEAR);

		barrier = batch.Add(image_, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0);
		barrier->subresourceRange.baseMipLevel = mip;
		barrier->oldLayout = VK_IMAGE_LAYOUT_GENERAL;
		barrier->newLayout = VK_IMAGE_LAYOUT_GENERAL;
		barrier->srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
		barrier->dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
		batch.Flush(cmd);
	}
}

void VulkanTexture::EndCreate(VkCommandBuffer cmd, bool vertexTexture, VkPipelineStageFlags prevStage, VkImageLayout layout) {
	VulkanBarrierBatch batch;
	VkImageMemoryBarrier *barrier = batch.Add(image_, prevStage, vertexTexture ? VK_PIPELINE_STAGE_VERTEX_SHADER_BIT : VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0);
	barrier->subresourceRange.levelCount = numMips_;
	barrier->oldLayout = layout;
	barrier->newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
	barrier->srcAccessMask = prevStage == VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT ? VK_ACCESS_SHADER_WRITE_BIT : VK_ACCESS_TRANSFER_WRITE_BIT;
	barrier->dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
	batch.Flush(cmd);
}

void VulkanTexture::PrepareForTransferDst(VkCommandBuffer cmd, int levels) {
	VulkanBarrierBatch batch;
	VkImageMemoryBarrier *barrier = batch.Add(image_, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0);
	barrier->subresourceRange.levelCount = levels;
	barrier->srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
	barrier->dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
	barrier->oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
	barrier->newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
	batch.Flush(cmd);
}

void VulkanTexture::RestoreAfterTransferDst(int levels, VulkanBarrierBatch *barriers) {
	VkImageMemoryBarrier *barrier = barriers->Add(image_, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0);
	barrier->subresourceRange.levelCount = levels;
	barrier->srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
	barrier->dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
	barrier->oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
	barrier->newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
}

VkImageView VulkanTexture::CreateViewForMip(int mip) {
	VkImageViewCreateInfo view_info = { VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
	view_info.image = image_;
	view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
	view_info.format = format_;
	view_info.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
	view_info.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
	view_info.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
	view_info.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
	view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
	view_info.subresourceRange.baseMipLevel = mip;
	view_info.subresourceRange.levelCount = 1;
	view_info.subresourceRange.baseArrayLayer = 0;
	view_info.subresourceRange.layerCount = 1;
	VkImageView view;
	VkResult res = vkCreateImageView(vulkan_->GetDevice(), &view_info, NULL, &view);
	vulkan_->SetDebugName(view, VK_OBJECT_TYPE_IMAGE_VIEW, "mipview");
	_assert_(res == VK_SUCCESS);
	return view;
}

void VulkanTexture::Destroy() {
	if (view_ != VK_NULL_HANDLE) {
		vulkan_->Delete().QueueDeleteImageView(view_);
	}
	if (arrayView_ != VK_NULL_HANDLE) {
		vulkan_->Delete().QueueDeleteImageView(arrayView_);
	}
	if (image_ != VK_NULL_HANDLE) {
		_dbg_assert_(allocation_ != VK_NULL_HANDLE);
		vulkan_->Delete().QueueDeleteImageAllocation(image_, allocation_);
	}
}
