/**
 * Copyright (c) 2006-2023 LOVE Development Team
 *
 * This software is provided 'as-is', without any express or implied
 * warranty.  In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 3. This notice may not be removed or altered from any source distribution.
 **/

#include "GraphicsReadback.h"
#include "Buffer.h"
#include "Texture.h"
#include "Graphics.h"
#include "data/ByteData.h"
#include "image/ImageData.h"
#include "image/Image.h"

namespace love
{
namespace graphics
{

love::Type GraphicsReadback::type("GraphicsReadback", &Object::type);

GraphicsReadback::GraphicsReadback(Graphics *gfx, ReadbackMethod method, Buffer *buffer, size_t offset, size_t size, love::data::ByteData *dest, size_t destoffset)
	: dataType(DATA_BUFFER)
	, method(method)
	, bufferData(dest)
{
	const auto &caps = gfx->getCapabilities();

	if (!caps.features[Graphics::FEATURE_COPY_BUFFER])
		throw love::Exception("readbackBuffer is not supported on this system (buffer copy support is required).");

	if (offset + size > buffer->getSize())
		throw love::Exception("Invalid offset or size for the given Buffer.");

	if (dest != nullptr && destoffset + size > dest->getSize())
		throw love::Exception("Invalid destination offset or size for the given ByteData.");

	bufferDataOffset = dest != nullptr ? destoffset : 0;
}

GraphicsReadback::GraphicsReadback(Graphics *gfx, ReadbackMethod method, Texture *texture, int slice, int mipmap, const Rect &rect, love::image::ImageData *dest, int destx, int desty)
	: dataType(DATA_TEXTURE)
	, method(method)
	, imageData(dest)
	, rect(rect)
{
	const auto &caps = gfx->getCapabilities();

	if (gfx->isRenderTargetActive(texture))
		throw love::Exception("readbackTexture cannot be called while that Texture is an active render target.");

	if (!texture->isReadable())
		throw love::Exception("readbackTexture requires a readable Texture.");

	int tw = texture->getPixelWidth(mipmap);
	int th = texture->getPixelHeight(mipmap);
	auto texType = texture->getTextureType();

	if (rect.x < 0 || rect.y < 0 || rect.w <= 0 || rect.h <= 0 || (rect.x + rect.w) > tw || (rect.y + rect.h) > th)
		throw love::Exception("Invalid rectangle dimensions.");

	if (slice < 0 || (texType == TEXTURE_VOLUME && slice >= texture->getDepth(mipmap))
		|| (texType == TEXTURE_2D_ARRAY && slice >= texture->getLayerCount())
		|| (texType == TEXTURE_CUBE && slice >= 6))
	{
		throw love::Exception("Invalid slice index.");
	}

	textureFormat = getLinearPixelFormat(texture->getPixelFormat());

	if (!image::ImageData::validPixelFormat(textureFormat))
	{
		const char *formatname = "unknown";
		love::getConstant(textureFormat, formatname);
		throw love::Exception("ImageData with the '%s' pixel format is not supported.", formatname);
	}

	bool isRT = texture->isRenderTarget();

	if (method == READBACK_ASYNC)
	{
		if (isRT && !caps.features[Graphics::FEATURE_COPY_RENDER_TARGET_TO_BUFFER])
			throw love::Exception("readbackTextureAsync is not supported on this system.");
		else if (!isRT && !caps.features[Graphics::FEATURE_COPY_TEXTURE_TO_BUFFER])
			throw love::Exception("readbackTextureAsync a with non-render-target textures is not supported on this system.");
	}
	else
	{
		if (!isRT && !caps.features[Graphics::FEATURE_COPY_TEXTURE_TO_BUFFER])
			throw love::Exception("readbackTexture with a non-render-target texture is not supported on this system.");
	}

	if (dest != nullptr)
	{
		if (dest->getFormat() != textureFormat)
			throw love::Exception("Destination ImageData pixel format must match the source Texture's format.");

		if (destx < 0 || desty < 0)
			throw love::Exception("Invalid destination ImageData x/y coordinates.");

		if (destx + rect.w > dest->getWidth() || desty + rect.h > dest->getHeight())
			throw love::Exception("The specified rectangle does not fit within the destination ImageData's dimensions.");
	}

	imageDataX = dest != nullptr ? destx : 0;
	imageDataY = dest != nullptr ? desty : 0;
}

GraphicsReadback::~GraphicsReadback()
{
}

love::data::ByteData *GraphicsReadback::getBufferData() const
{
	if (!isComplete())
		return nullptr;
	return bufferData;
}

love::image::ImageData *GraphicsReadback::getImageData() const
{
	if (!isComplete())
		return nullptr;
	return imageData;
}

void *GraphicsReadback::prepareReadbackDest(size_t size)
{
	if (dataType == DATA_TEXTURE)
	{
		if (imageData.get())
		{
			// Not the cleanest, but should work since uncompressed formats always
			// have 1x1 blocks.
			int pixels = imageDataY * imageData->getWidth() + imageDataX;
			size_t offset = getPixelFormatUncompressedRowSize(textureFormat, pixels);

			return (uint8 *) imageData->getData() + offset;
		}
		else
		{
			auto module = Module::getInstance<image::Image>(Module::M_IMAGE);
			if (module == nullptr)
				throw love::Exception("The love.image module must be loaded for readbackTexture.");

			imageData.set(module->newImageData(rect.w, rect.h, textureFormat, nullptr), Acquire::NORETAIN);
			return imageData->getData();
		}
	}
	else
	{
		if (!bufferData.get())
			bufferData.set(new love::data::ByteData(size, false), Acquire::NORETAIN);

		return (uint8 *) bufferData->getData() + bufferDataOffset;
	}
}

GraphicsReadback::Status GraphicsReadback::readbackBuffer(Buffer *buffer, size_t offset, size_t size)
{
	if (buffer == nullptr)
		return STATUS_ERROR;

	const void *data = buffer->map(Buffer::MAP_READ_ONLY, offset, size);

	if (data == nullptr)
		return STATUS_ERROR;

	bool success = true;

	try
	{
		void *dest = prepareReadbackDest(size);
		if (dest == nullptr)
			return STATUS_ERROR;

		if (imageData.get())
		{
			love::thread::Lock lock(imageData->getMutex());

			if (imageData->getWidth() != rect.w)
			{
				// Readback of compressed textures into ImageData isn't supported,
				// so this is fine.
				size_t stride = getPixelFormatUncompressedRowSize(textureFormat, imageData->getWidth());
				size_t rowsize = getPixelFormatUncompressedRowSize(textureFormat, rect.w);

				for (int i = 0; i < rect.h; i++)
				{
					memcpy(dest, data, rowsize);
					dest = (uint8 *) dest + stride;
					data = (uint8 *) data + rowsize;
				}
			}
			else
			{
				memcpy(dest, data, std::min(size, imageData->getSize()));
			}
		}
		else
		{
			memcpy(dest, data, std::min(size, bufferData->getSize()));
		}
	}
	catch (love::Exception &)
	{
		success = false;
	}

	buffer->unmap(offset, size);
	return success ? STATUS_COMPLETE : STATUS_ERROR;
}

} // graphics
} // love
