/**
 * Copyright (c) 2006-2020 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.
 **/

// LOVE
#include "common/config.h"
#include "Texture.h"
#include "Graphics.h"

// C
#include <cmath>
#include <algorithm>


namespace love
{
namespace graphics
{

uint64 SamplerState::toKey() const
{
	union { float f; uint32 i; } conv;
	conv.f = lodBias;

	return (minFilter << 0) | (magFilter << 1) | (mipmapFilter << 2)
	     | (wrapU << 4) | (wrapV << 7) | (wrapW << 10)
	     | (maxAnisotropy << 12) | (minLod << 16) | (maxLod << 20)
	     | (depthSampleMode.hasValue << 24) | (depthSampleMode.value << 25)
	     | ((uint64)conv.i << 32);
}

SamplerState SamplerState::fromKey(uint64 key)
{
	const uint32 BITS_1 = 0x1;
	const uint32 BITS_2 = 0x3;
	const uint32 BITS_3 = 0x7;
	const uint32 BITS_4 = 0xF;

	SamplerState s;

	s.minFilter = (FilterMode) ((key >> 0) & BITS_1);
	s.magFilter = (FilterMode) ((key >> 1) & BITS_1);
	s.mipmapFilter = (MipmapFilterMode) ((key >> 2) & BITS_2);

	s.wrapU = (WrapMode) ((key >> 4 ) & BITS_3);
	s.wrapV = (WrapMode) ((key >> 7 ) & BITS_3);
	s.wrapW = (WrapMode) ((key >> 10) & BITS_3);

	s.maxAnisotropy = (key >> 12) & BITS_4;

	s.minLod = (key >> 16) & BITS_4;
	s.maxLod = (key >> 20) & BITS_4;

	s.depthSampleMode.hasValue = ((key >> 24) & BITS_1) != 0;
	s.depthSampleMode.value = (CompareMode) ((key >> 25) & BITS_4);

	union { float f; uint32 i; } conv;
	conv.i = (uint32) (key >> 32);
	s.lodBias = conv.f;

	return s;
}

bool SamplerState::isClampZeroOrOne(WrapMode w)
{
	return w == WRAP_CLAMP_ONE || w == WRAP_CLAMP_ZERO;
}

static StringMap<SamplerState::FilterMode, SamplerState::FILTER_MAX_ENUM>::Entry filterModeEntries[] =
{
	{ "linear",  SamplerState::FILTER_LINEAR  },
	{ "nearest", SamplerState::FILTER_NEAREST },
};

static StringMap<SamplerState::FilterMode, SamplerState::FILTER_MAX_ENUM> filterModes(filterModeEntries, sizeof(filterModeEntries));

static StringMap<SamplerState::MipmapFilterMode, SamplerState::MIPMAP_FILTER_MAX_ENUM>::Entry mipmapFilterModeEntries[] =
{
	{ "none",    SamplerState::MIPMAP_FILTER_NONE    },
	{ "linear",  SamplerState::MIPMAP_FILTER_LINEAR  },
	{ "nearest", SamplerState::MIPMAP_FILTER_NEAREST },
};

static StringMap<SamplerState::MipmapFilterMode, SamplerState::MIPMAP_FILTER_MAX_ENUM> mipmapFilterModes(mipmapFilterModeEntries, sizeof(mipmapFilterModeEntries));

static StringMap<SamplerState::WrapMode, SamplerState::WRAP_MAX_ENUM>::Entry wrapModeEntries[] =
{
	{ "clamp",          SamplerState::WRAP_CLAMP           },
	{ "clampzero",      SamplerState::WRAP_CLAMP_ZERO      },
	{ "clampone",       SamplerState::WRAP_CLAMP_ONE       },
	{ "repeat",         SamplerState::WRAP_REPEAT          },
	{ "mirroredrepeat", SamplerState::WRAP_MIRRORED_REPEAT },
};

static StringMap<SamplerState::WrapMode, SamplerState::WRAP_MAX_ENUM> wrapModes(wrapModeEntries, sizeof(wrapModeEntries));

bool SamplerState::getConstant(const char *in, FilterMode &out)
{
	return filterModes.find(in, out);
}

bool SamplerState::getConstant(FilterMode in, const char *&out)
{
	return filterModes.find(in, out);
}

std::vector<std::string> SamplerState::getConstants(FilterMode)
{
	return filterModes.getNames();
}

bool SamplerState::getConstant(const char *in, MipmapFilterMode &out)
{
	return mipmapFilterModes.find(in, out);
}

bool SamplerState::getConstant(MipmapFilterMode in, const char *&out)
{
	return mipmapFilterModes.find(in, out);
}

std::vector<std::string> SamplerState::getConstants(MipmapFilterMode)
{
	return mipmapFilterModes.getNames();
}

bool SamplerState::getConstant(const char *in, WrapMode &out)
{
	return wrapModes.find(in, out);
}

bool SamplerState::getConstant(WrapMode in, const char *&out)
{
	return wrapModes.find(in, out);
}

std::vector<std::string> SamplerState::getConstants(WrapMode)
{
	return wrapModes.getNames();
}

love::Type Texture::type("Texture", &Drawable::type);
int Texture::textureCount = 0;
int64 Texture::totalGraphicsMemory = 0;

Texture::Texture(const Settings &settings, const Slices *slices)
	: texType(settings.type)
	, format(settings.format)
	, renderTarget(settings.renderTarget)
	, readable(true)
	, mipmapsMode(settings.mipmaps)
	, sRGB(isGammaCorrect() && !settings.linear)
	, width(settings.width)
	, height(settings.height)
	, depth(settings.type == TEXTURE_VOLUME ? settings.layers : 1)
	, layers(settings.type == TEXTURE_2D_ARRAY ? settings.layers : 1)
	, mipmapCount(1)
	, pixelWidth(0)
	, pixelHeight(0)
	, requestedMSAA(settings.msaa > 1 ? settings.msaa : 0)
	, samplerState()
	, graphicsMemorySize(0)
	, usingDefaultTexture(false)
{
	auto gfx = Module::getInstance<Graphics>(Module::M_GRAPHICS);

	if (slices != nullptr && slices->getMipmapCount() > 0 && slices->getSliceCount() > 0)
	{
		texType = slices->getTextureType();

		if (requestedMSAA > 1)
			throw love::Exception("MSAA textures cannot be created from image data.");

		int dataMipmaps = 1;
		if (slices->validate() && slices->getMipmapCount() > 1)
			dataMipmaps = slices->getMipmapCount();

		love::image::ImageDataBase *slice = slices->get(0, 0);

		format = slice->getFormat();
		if (sRGB)
			format = getSRGBPixelFormat(format);

		pixelWidth = slice->getWidth();
		pixelHeight = slice->getHeight();

		if (texType == TEXTURE_2D_ARRAY)
			layers = slices->getSliceCount();
		else if (texType == TEXTURE_VOLUME)
			depth = slices->getSliceCount();

		width  = (int) (pixelWidth / settings.dpiScale + 0.5);
		height = (int) (pixelHeight / settings.dpiScale + 0.5);

		if (isCompressed() && dataMipmaps <= 1)
			mipmapsMode = MIPMAPS_NONE;
	}
	else
	{
		if (isCompressed())
			throw love::Exception("Compressed textures must be created with initial data.");

		pixelWidth = (int) ((width * settings.dpiScale) + 0.5);
		pixelHeight = (int) ((height * settings.dpiScale) + 0.5);
	}

	if (settings.readable.hasValue)
		readable = settings.readable.value;
	else
		readable = !renderTarget || !isPixelFormatDepthStencil(format);

	format = gfx->getSizedFormat(format, renderTarget, readable);

	if (mipmapsMode == MIPMAPS_AUTO && isCompressed())
		mipmapsMode = MIPMAPS_MANUAL;

	if (mipmapsMode != MIPMAPS_NONE)
		mipmapCount = getTotalMipmapCount(pixelWidth, pixelHeight, depth);

	if (mipmapsMode == MIPMAPS_AUTO && isPixelFormatDepthStencil(format))
		throw love::Exception("Automatic mipmap generation cannot be used for depth/stencil textures.");

	if (pixelWidth <= 0 || pixelHeight <= 0 || layers <= 0 || depth <= 0)
		throw love::Exception("Texture dimensions must be greater than 0.");

	if (texType != TEXTURE_2D && requestedMSAA > 1)
		throw love::Exception("MSAA is only supported for textures with the 2D texture type.");

	if (!renderTarget && requestedMSAA > 1)
		throw love::Exception("MSAA is only supported with render target textures.");

	if (readable && isPixelFormatDepthStencil(format) && settings.msaa > 1)
		throw love::Exception("Readable depth/stencil textures with MSAA are not currently supported.");

	if ((!readable || settings.msaa > 1) && mipmapsMode != MIPMAPS_NONE)
		throw love::Exception("Non-readable and MSAA textures cannot have mipmaps.");

	if (!readable && texType != TEXTURE_2D)
		throw love::Exception("Non-readable pixel formats are only supported for 2D texture types.");

	if (isCompressed() && renderTarget)
		throw love::Exception("Compressed textures cannot be render targets.");

	if (!gfx->isPixelFormatSupported(format, renderTarget, readable, sRGB))
	{
		const char *fstr = "unknown";
		love::getConstant(format, fstr);

		const char *readablestr = "";
		if (readable != !isPixelFormatDepthStencil(format))
			readablestr = readable ? " readable" : " non-readable";

		const char *rtstr = "";
		if (renderTarget)
			rtstr = " as a render target";

		throw love::Exception("The %s%s pixel format is not supported%s on this system.", fstr, readablestr, rtstr);
	}

	if (!gfx->getCapabilities().textureTypes[texType])
	{
		const char *textypestr = "unknown";
		Texture::getConstant(texType, textypestr);
		throw love::Exception("%s textures are not supported on this system.", textypestr);
	}

	validateDimensions(renderTarget || !readable);

	samplerState = gfx->getDefaultSamplerState();

	if (getMipmapCount() == 1)
		samplerState.mipmapFilter = SamplerState::MIPMAP_FILTER_NONE;

	Quad::Viewport v = {0, 0, (double) width, (double) height};
	quad.set(new Quad(v, width, height), Acquire::NORETAIN);

	++textureCount;
}

Texture::~Texture()
{
	--textureCount;
	setGraphicsMemorySize(0);
}

void Texture::setGraphicsMemorySize(int64 bytes)
{
	totalGraphicsMemory = std::max(totalGraphicsMemory - graphicsMemorySize, (int64) 0);

	bytes = std::max(bytes, (int64) 0);
	graphicsMemorySize = bytes;
	totalGraphicsMemory += bytes;
}

void Texture::draw(Graphics *gfx, const Matrix4 &m)
{
	draw(gfx, quad, m);
}

void Texture::draw(Graphics *gfx, Quad *q, const Matrix4 &localTransform)
{
	if (!readable)
		throw love::Exception("Textures with non-readable formats cannot be drawn.");

	if (renderTarget && gfx->isRenderTargetActive(this))
		throw love::Exception("Cannot render a Texture to itself.");

	if (texType == TEXTURE_2D_ARRAY)
	{
		drawLayer(gfx, q->getLayer(), q, localTransform);
		return;
	}

	const Matrix4 &tm = gfx->getTransform();
	bool is2D = tm.isAffine2DTransform();

	Graphics::BatchedDrawCommand cmd;
	cmd.formats[0] = getSinglePositionFormat(is2D);
	cmd.formats[1] = CommonFormat::STf_RGBAub;
	cmd.indexMode = TRIANGLEINDEX_QUADS;
	cmd.vertexCount = 4;
	cmd.texture = this;

	Graphics::BatchedVertexData data = gfx->requestBatchedDraw(cmd);

	Matrix4 t(tm, localTransform);

	if (is2D)
		t.transformXY((Vector2 *) data.stream[0], q->getVertexPositions(), 4);
	else
		t.transformXY0((Vector3 *) data.stream[0], q->getVertexPositions(), 4);

	const Vector2 *texcoords = q->getVertexTexCoords();
	STf_RGBAub *vertexdata = (STf_RGBAub *) data.stream[1];

	Color32 c = toColor32(gfx->getColor());

	for (int i = 0; i < 4; i++)
	{
		vertexdata[i].s = texcoords[i].x;
		vertexdata[i].t = texcoords[i].y;
		vertexdata[i].color = c;
	}
}

void Texture::drawLayer(Graphics *gfx, int layer, const Matrix4 &m)
{
	drawLayer(gfx, layer, quad, m);
}

void Texture::drawLayer(Graphics *gfx, int layer, Quad *q, const Matrix4 &m)
{
	if (!readable)
		throw love::Exception("Textures with non-readable formats cannot be drawn.");

	if (renderTarget && gfx->isRenderTargetActive(this, layer))
		throw love::Exception("Cannot render a Texture to itself.");

	if (texType != TEXTURE_2D_ARRAY)
		throw love::Exception("drawLayer can only be used with Array Textures.");

	if (layer < 0 || layer >= layers)
		throw love::Exception("Invalid layer: %d (Texture has %d layers)", layer + 1, layers);

	Color32 c = toColor32(gfx->getColor());

	const Matrix4 &tm = gfx->getTransform();
	bool is2D = tm.isAffine2DTransform();

	Matrix4 t(tm, m);

	Graphics::BatchedDrawCommand cmd;
	cmd.formats[0] = getSinglePositionFormat(is2D);
	cmd.formats[1] = CommonFormat::STPf_RGBAub;
	cmd.indexMode = TRIANGLEINDEX_QUADS;
	cmd.vertexCount = 4;
	cmd.texture = this;
	cmd.standardShaderType = Shader::STANDARD_ARRAY;

	Graphics::BatchedVertexData data = gfx->requestBatchedDraw(cmd);

	if (is2D)
		t.transformXY((Vector2 *) data.stream[0], q->getVertexPositions(), 4);
	else
		t.transformXY0((Vector3 *) data.stream[0], q->getVertexPositions(), 4);

	const Vector2 *texcoords = q->getVertexTexCoords();
	STPf_RGBAub *vertexdata = (STPf_RGBAub *) data.stream[1];

	for (int i = 0; i < 4; i++)
	{
		vertexdata[i].s = texcoords[i].x;
		vertexdata[i].t = texcoords[i].y;
		vertexdata[i].p = (float) layer;
		vertexdata[i].color = c;
	}
}

void Texture::uploadImageData(love::image::ImageDataBase *d, int level, int slice, int x, int y)
{
	love::image::ImageData *id = dynamic_cast<love::image::ImageData *>(d);

	love::thread::EmptyLock lock;
	if (id != nullptr)
		lock.setLock(id->getMutex());

	Rect rect = {x, y, d->getWidth(), d->getHeight()};
	uploadByteData(d->getFormat(), d->getData(), d->getSize(), level, slice, rect, d);
}

void Texture::replacePixels(love::image::ImageDataBase *d, int slice, int mipmap, int x, int y, bool reloadmipmaps)
{
	if (!isReadable())
		throw love::Exception("replacePixels can only be called on readable Textures.");

	if (getMSAA() > 1)
		throw love::Exception("replacePixels cannot be called on a MSAA Texture.");

	auto gfx = Module::getInstance<Graphics>(Module::M_GRAPHICS);
	if (gfx != nullptr && gfx->isRenderTargetActive(this))
		throw love::Exception("replacePixels cannot be called on this Texture while it's an active render target.");

	// No effect if the texture hasn't been created yet.
	if (getHandle() == 0 || usingDefaultTexture)
		return;

	if (d->getFormat() != getPixelFormat())
		throw love::Exception("Pixel formats must match.");

	if (mipmap < 0 || mipmap >= getMipmapCount())
		throw love::Exception("Invalid texture mipmap index %d.", mipmap + 1);

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

	Rect rect = {x, y, d->getWidth(), d->getHeight()};

	int mipw = getPixelWidth(mipmap);
	int miph = getPixelHeight(mipmap);

	if (rect.x < 0 || rect.y < 0 || rect.w <= 0 || rect.h <= 0
		|| (rect.x + rect.w) > mipw || (rect.y + rect.h) > miph)
	{
		throw love::Exception("Invalid rectangle dimensions (x=%d, y=%d, w=%d, h=%d) for %dx%d Texture.", rect.x, rect.y, rect.w, rect.h, mipw, miph);
	}

	// We don't currently support partial updates of compressed textures.
	if (isPixelFormatCompressed(d->getFormat()) && (rect.x != 0 || rect.y != 0 || rect.w != mipw || rect.h != miph))
		throw love::Exception("Compressed textures only support replacing the entire Texture.");

	Graphics::flushBatchedDrawsGlobal();

	uploadImageData(d, mipmap, slice, x, y);

	if (reloadmipmaps && mipmap == 0 && getMipmapCount() > 1)
		generateMipmaps();
}

void Texture::replacePixels(const void *data, size_t size, int slice, int mipmap, const Rect &rect, bool reloadmipmaps)
{
	if (!isReadable() || getMSAA() > 1)
		return;

	auto gfx = Module::getInstance<Graphics>(Module::M_GRAPHICS);
	if (gfx != nullptr && gfx->isRenderTargetActive(this))
		return;

	Graphics::flushBatchedDrawsGlobal();

	uploadByteData(format, data, size, mipmap, slice, rect, nullptr);

	if (reloadmipmaps && mipmap == 0 && getMipmapCount() > 1)
		generateMipmaps();
}

void Texture::generateMipmaps()
{
	if (getMipmapCount() == 1 || getMipmapsMode() == MIPMAPS_NONE)
		throw love::Exception("generateMipmaps can only be called on a Texture which was created with mipmaps enabled.");

	if (isPixelFormatCompressed(format))
		throw love::Exception("generateMipmaps cannot be called on a compressed Texture.");

	if (isPixelFormatDepthStencil(format))
		throw love::Exception("generateMipmaps cannot be called on a depth/stencil Texture.");

	generateMipmapsInternal();
}

love::image::ImageData *Texture::newImageData(love::image::Image *module, int slice, int mipmap, const Rect &r)
{
	if (!isReadable())
		throw love::Exception("Texture:newImageData cannot be called on non-readable Textures.");

	if (!isRenderTarget())
		throw love::Exception("Texture:newImageData can only be called on render target Textures.");

	if (isPixelFormatDepthStencil(getPixelFormat()))
		throw love::Exception("Texture:newImageData cannot be called on Textures with depth/stencil pixel formats.");

	if (r.x < 0 || r.y < 0 || r.w <= 0 || r.h <= 0 || (r.x + r.w) > getPixelWidth(mipmap) || (r.y + r.h) > getPixelHeight(mipmap))
		throw love::Exception("Invalid rectangle dimensions.");

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

	Graphics *gfx = Module::getInstance<Graphics>(Module::M_GRAPHICS);
	if (gfx != nullptr && gfx->isRenderTargetActive(this))
		throw love::Exception("Texture:newImageData cannot be called while that Texture is an active render target.");

	PixelFormat dataformat = getLinearPixelFormat(getPixelFormat());

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

	auto imagedata = module->newImageData(r.w, r.h, dataformat);

	readbackImageData(imagedata, slice, mipmap, r);

	return imagedata;
}

TextureType Texture::getTextureType() const
{
	return texType;
}

PixelFormat Texture::getPixelFormat() const
{
	return format;
}

Texture::MipmapsMode Texture::getMipmapsMode() const
{
	return mipmapsMode;
}

bool Texture::isRenderTarget() const
{
	return renderTarget;
}

bool Texture::isReadable() const
{
	return readable;
}

bool Texture::isCompressed() const
{
	return isPixelFormatCompressed(format);
}

bool Texture::isFormatLinear() const
{
	return isGammaCorrect() && !sRGB && format != PIXELFORMAT_sRGBA8_UNORM;
}

bool Texture::isValidSlice(int slice) const
{
	if (slice < 0)
		return false;

	if (texType == TEXTURE_CUBE)
		return slice < 6;
	else if (texType == TEXTURE_VOLUME)
		return slice < depth;
	else if (texType == TEXTURE_2D_ARRAY)
		return slice < layers;
	else if (slice > 0)
		return false;

	return true;
}

int Texture::getWidth(int mip) const
{
	return std::max(width >> mip, 1);
}

int Texture::getHeight(int mip) const
{
	return std::max(height >> mip, 1);
}

int Texture::getDepth(int mip) const
{
	return std::max(depth >> mip, 1);
}

int Texture::getLayerCount() const
{
	return layers;
}

int Texture::getMipmapCount() const
{
	return mipmapCount;
}

int Texture::getPixelWidth(int mip) const
{
	return std::max(pixelWidth >> mip, 1);
}

int Texture::getPixelHeight(int mip) const
{
	return std::max(pixelHeight >> mip, 1);
}

float Texture::getDPIScale() const
{
	return (float) pixelHeight / (float) height;
}

int Texture::getRequestedMSAA() const
{
	return requestedMSAA;
}

void Texture::setSamplerState(const SamplerState &s)
{
	if (!readable)
		return;

	if (s.depthSampleMode.hasValue && !isPixelFormatDepth(format))
		throw love::Exception("Only depth textures can have a depth sample compare mode.");

	Graphics::flushBatchedDrawsGlobal();

	samplerState = s;

	if (samplerState.mipmapFilter != SamplerState::MIPMAP_FILTER_NONE && getMipmapCount() == 1)
		samplerState.mipmapFilter = SamplerState::MIPMAP_FILTER_NONE;

	if (texType == TEXTURE_CUBE)
		samplerState.wrapU = samplerState.wrapV = samplerState.wrapW = SamplerState::WRAP_CLAMP;
}

const SamplerState &Texture::getSamplerState() const
{
	return samplerState;
}

Quad *Texture::getQuad() const
{
	return quad;
}

int Texture::getTotalMipmapCount(int w, int h)
{
	return (int) log2(std::max(w, h)) + 1;
}

int Texture::getTotalMipmapCount(int w, int h, int d)
{
	return (int) log2(std::max(std::max(w, h), d)) + 1;
}

bool Texture::validateDimensions(bool throwException) const
{
	bool success = true;

	auto gfx = Module::getInstance<Graphics>(Module::M_GRAPHICS);
	if (gfx == nullptr)
		return false;

	const Graphics::Capabilities &caps = gfx->getCapabilities();

	int max2Dsize   = (int) caps.limits[Graphics::LIMIT_TEXTURE_SIZE];
	int max3Dsize   = (int) caps.limits[Graphics::LIMIT_VOLUME_TEXTURE_SIZE];
	int maxcubesize = (int) caps.limits[Graphics::LIMIT_CUBE_TEXTURE_SIZE];
	int maxlayers   = (int) caps.limits[Graphics::LIMIT_TEXTURE_LAYERS];

	int largestdim = 0;
	const char *largestname = nullptr;

	if ((texType == TEXTURE_2D || texType == TEXTURE_2D_ARRAY) && (pixelWidth > max2Dsize || pixelHeight > max2Dsize))
	{
		success = false;
		largestdim = std::max(pixelWidth, pixelHeight);
		largestname = pixelWidth > pixelHeight ? "pixel width" : "pixel height";
	}
	else if (texType == TEXTURE_2D_ARRAY && layers > maxlayers)
	{
		success = false;
		largestdim = layers;
		largestname = "array layer count";
	}
	else if (texType == TEXTURE_CUBE && (pixelWidth > maxcubesize || pixelWidth != pixelHeight))
	{
		success = false;
		largestdim = std::max(pixelWidth, pixelHeight);
		largestname = pixelWidth > pixelHeight ? "pixel width" : "pixel height";

		if (throwException && pixelWidth != pixelHeight)
			throw love::Exception("Cubemap textures must have equal width and height.");
	}
	else if (texType == TEXTURE_VOLUME && (pixelWidth > max3Dsize || pixelHeight > max3Dsize || depth > max3Dsize))
	{
		success = false;
		largestdim = std::max(std::max(pixelWidth, pixelHeight), depth);
		if (largestdim == pixelWidth)
			largestname = "pixel width";
		else if (largestdim == pixelHeight)
			largestname = "pixel height";
		else
			largestname = "pixel depth";
	}

	if (throwException && largestname != nullptr)
		throw love::Exception("Cannot create texture: %s of %d is too large for this system.", largestname, largestdim);

	return success;
}

Texture::Slices::Slices(TextureType textype)
	: textureType(textype)
{
}

void Texture::Slices::clear()
{
	data.clear();
}

void Texture::Slices::set(int slice, int mipmap, love::image::ImageDataBase *d)
{
	if (textureType == TEXTURE_VOLUME)
	{
		if (mipmap >= (int) data.size())
			data.resize(mipmap + 1);

		if (slice >= (int) data[mipmap].size())
			data[mipmap].resize(slice + 1);

		data[mipmap][slice].set(d);
	}
	else
	{
		if (slice >= (int) data.size())
			data.resize(slice + 1);

		if (mipmap >= (int) data[slice].size())
			data[slice].resize(mipmap + 1);

		data[slice][mipmap].set(d);
	}
}

love::image::ImageDataBase *Texture::Slices::get(int slice, int mipmap) const
{
	if (slice < 0 || slice >= getSliceCount(mipmap))
		return nullptr;

	if (mipmap < 0 || mipmap >= getMipmapCount(slice))
		return nullptr;

	if (textureType == TEXTURE_VOLUME)
		return data[mipmap][slice].get();
	else
		return data[slice][mipmap].get();
}

void Texture::Slices::add(love::image::CompressedImageData *cdata, int startslice, int startmip, bool addallslices, bool addallmips)
{
	int slicecount = addallslices ? cdata->getSliceCount() : 1;
	int mipcount = addallmips ? cdata->getMipmapCount() : 1;

	for (int mip = 0; mip < mipcount; mip++)
	{
		for (int slice = 0; slice < slicecount; slice++)
			set(startslice + slice, startmip + mip, cdata->getSlice(slice, mip));
	}
}

int Texture::Slices::getSliceCount(int mip) const
{
	if (textureType == TEXTURE_VOLUME)
	{
		if (mip < 0 || mip >= (int) data.size())
			return 0;

		return (int) data[mip].size();
	}
	else
		return (int) data.size();
}

int Texture::Slices::getMipmapCount(int slice) const
{
	if (textureType == TEXTURE_VOLUME)
		return (int) data.size();
	else
	{
		if (slice < 0 || slice >= (int) data.size())
			return 0;

		return data[slice].size();
	}
}

bool Texture::Slices::validate() const
{
	int slicecount = getSliceCount();
	int mipcount = getMipmapCount(0);

	if (slicecount == 0 || mipcount == 0)
		throw love::Exception("At least one ImageData or CompressedImageData is required.");

	if (textureType == TEXTURE_CUBE && slicecount != 6)
		throw love::Exception("Cube textures must have exactly 6 sides.");

	image::ImageDataBase *firstdata = get(0, 0);

	int w = firstdata->getWidth();
	int h = firstdata->getHeight();
	int depth = textureType == TEXTURE_VOLUME ? slicecount : 1;
	PixelFormat format = firstdata->getFormat();

	int expectedmips = Texture::getTotalMipmapCount(w, h, depth);

	if (mipcount != expectedmips && mipcount != 1)
		throw love::Exception("Texture does not have all required mipmap levels (expected %d, got %d)", expectedmips, mipcount);

	if (textureType == TEXTURE_CUBE && w != h)
		throw love::Exception("Cube textures must have equal widths and heights for each cube face.");

	int mipw = w;
	int miph = h;
	int mipslices = slicecount;

	for (int mip = 0; mip < mipcount; mip++)
	{
		if (textureType == TEXTURE_VOLUME)
		{
			slicecount = getSliceCount(mip);

			if (slicecount != mipslices)
				throw love::Exception("Invalid number of image data layers in mipmap level %d (expected %d, got %d)", mip+1, mipslices, slicecount);
		}

		for (int slice = 0; slice < slicecount; slice++)
		{
			auto slicedata = get(slice, mip);

			if (slicedata == nullptr)
				throw love::Exception("Missing image data (slice %d, mipmap level %d)", slice+1, mip+1);

			int realw = slicedata->getWidth();
			int realh = slicedata->getHeight();

			if (getMipmapCount(slice) != mipcount)
				throw love::Exception("All texture layers must have the same mipmap count.");

			if (mipw != realw)
				throw love::Exception("Width of image data (slice %d, mipmap level %d) is incorrect (expected %d, got %d)", slice+1, mip+1, mipw, realw);

			if (miph != realh)
				throw love::Exception("Height of image data (slice %d, mipmap level %d) is incorrect (expected %d, got %d)", slice+1, mip+1, miph, realh);

			if (format != slicedata->getFormat())
				throw love::Exception("All texture slices and mipmaps must have the same pixel format.");
		}

		mipw = std::max(mipw / 2, 1);
		miph = std::max(miph / 2, 1);

		if (textureType == TEXTURE_VOLUME)
			mipslices = std::max(mipslices / 2, 1);
	}

	return true;
}

static StringMap<TextureType, TEXTURE_MAX_ENUM>::Entry texTypeEntries[] =
{
	{ "2d",     TEXTURE_2D       },
	{ "volume", TEXTURE_VOLUME   },
	{ "array",  TEXTURE_2D_ARRAY },
	{ "cube",   TEXTURE_CUBE     },
};

static StringMap<TextureType, TEXTURE_MAX_ENUM> texTypes(texTypeEntries, sizeof(texTypeEntries));

static StringMap<Texture::MipmapsMode, Texture::MIPMAPS_MAX_ENUM>::Entry mipmapEntries[] =
{
	{ "none",   Texture::MIPMAPS_NONE   },
	{ "manual", Texture::MIPMAPS_MANUAL },
	{ "auto",   Texture::MIPMAPS_AUTO   },
};

static StringMap<Texture::MipmapsMode, Texture::MIPMAPS_MAX_ENUM> mipmapModes(mipmapEntries, sizeof(mipmapEntries));

static StringMap<Texture::SettingType, Texture::SETTING_MAX_ENUM>::Entry settingTypeEntries[] =
{
	{ "width",        Texture::SETTING_WIDTH         },
	{ "height",       Texture::SETTING_HEIGHT        },
	{ "layers",       Texture::SETTING_LAYERS        },
	{ "mipmaps",      Texture::SETTING_MIPMAPS       },
	{ "format",       Texture::SETTING_FORMAT        },
	{ "linear",       Texture::SETTING_LINEAR        },
	{ "type",         Texture::SETTING_TYPE          },
	{ "dpiscale",     Texture::SETTING_DPI_SCALE     },
	{ "msaa",         Texture::SETTING_MSAA          },
	{ "canvas",       Texture::SETTING_RENDER_TARGET },
	{ "readable",     Texture::SETTING_READABLE      },
};

static StringMap<Texture::SettingType, Texture::SETTING_MAX_ENUM> settingTypes(settingTypeEntries, sizeof(settingTypeEntries));

bool Texture::getConstant(const char *in, TextureType &out)
{
	return texTypes.find(in, out);
}

bool Texture::getConstant(TextureType in, const char *&out)
{
	return texTypes.find(in, out);
}

std::vector<std::string> Texture::getConstants(TextureType)
{
	return texTypes.getNames();
}

bool Texture::getConstant(const char *in, MipmapsMode &out)
{
	return mipmapModes.find(in, out);
}

bool Texture::getConstant(MipmapsMode in, const char *&out)
{
	return mipmapModes.find(in, out);
}

std::vector<std::string> Texture::getConstants(MipmapsMode)
{
	return mipmapModes.getNames();
}

bool Texture::getConstant(const char *in, SettingType &out)
{
	return settingTypes.find(in, out);
}

bool Texture::getConstant(SettingType in, const char *&out)
{
	return settingTypes.find(in, out);
}

const char *Texture::getConstant(SettingType in)
{
	const char *name = nullptr;
	getConstant(in, name);
	return name;
}

std::vector<std::string> Texture::getConstants(SettingType)
{
	return settingTypes.getNames();
}

} // graphics
} // love
