#include <GL/glew.h>
#include "colors.h"
#include "Texture.h"
#include "globals.h"
#include <string.h>
#include <cmath>
#include <base_resample.h>
#include "quant.h"
#include <unordered_set>

Texture::Texture(int width, int height) {
	this->width = width;
	this->height = height;
	this->depth = 1;
	this->nearFilter = GL_LINEAR;
	this->farFilter = GL_LINEAR_MIPMAP_LINEAR;
	this->data = new uint8_t[width*height*sizeof(COLOR4)];
	arrayId = -1;
	layer = 0;
	is3d = false;
}

Texture::Texture(int width, int height, int depth) {
	this->width = width;
	this->height = height;
	this->depth = depth;
	this->nearFilter = GL_LINEAR;
	this->farFilter = GL_LINEAR_MIPMAP_LINEAR;
	this->data = new uint8_t[width * height * depth * sizeof(COLOR4)];
	arrayId = -1;
	layer = 0;
	is3d = true;
}

Texture::Texture( int width, int height, void * data )
{
	this->width = width;
	this->height = height;
	this->depth = 1;
	this->nearFilter = GL_LINEAR;
	this->farFilter = GL_LINEAR_MIPMAP_LINEAR;
	this->data = (uint8_t*)data;
	arrayId = -1;
	layer = 0;
	is3d = false;
}

Texture::~Texture()
{
	if (uploaded)
		glDeleteTextures(1, &id);
	if (data)
		delete[] data;
	for (MipTexture& mip : mipmaps) {
		delete[] mip.data;
	}
}

vector<COLOR3> Texture::resample(COLOR3* srcData, int srcW, int srcH, COLOR3* dstData,
	int dstW, int dstH, int mode, bool masked, COLOR3 maskColor) {
	
	vector<COLOR3> palette;

	if (masked && mode != KernelTypeNearest) {
		COLOR3* maskedData = new COLOR3[srcW * srcH];
		memcpy(maskedData, srcData, srcW * srcH * sizeof(COLOR3));

		// replace the mask color with black. Better to fade edges into black than bright blue/pink.
		for (int i = 0; i < srcW * srcH; i++) {
			if (maskedData[i] == maskColor) {
				maskedData[i] = COLOR3(0, 0, 0);
			}
		}
		base::ResampleImage24((byte*)maskedData, srcW, srcH, (byte*)dstData, dstW, dstH, (base::KernelType)mode);
		delete[] maskedData;

		// quantize the image, saving one palette entry for the mask color
		palette = median_cut_quantize(dstData, dstW * dstH, 255);

		// apply the mask color using nearest neighbor sampling
		COLOR3* nearestResamp = new COLOR3[dstW * dstH];
		base::ResampleImage24((byte*)srcData, srcW, srcH, (byte*)nearestResamp, dstW, dstH, base::KernelType::KernelTypeNearest);
		for (int i = 0; i < dstW * dstH; i++) {
			if (nearestResamp[i] == maskColor) {
				dstData[i] = maskColor;
			}
		}
		delete[] nearestResamp;

		while (palette.size() < 255) {
			palette.push_back(COLOR3());
		}
		palette.push_back(maskColor);
	}
	else {
		base::ResampleImage24((byte*)srcData, srcW, srcH, (byte*)dstData, dstW, dstH, (base::KernelType)mode);
		
		if (mode != KernelTypeNearest) {
			palette = median_cut_quantize(dstData, dstW * dstH, 256);
		}
		else {
			unordered_set<COLOR3> uniqueColors;
			for (int i = 0; i < dstW * dstH; i++) {
				uniqueColors.insert(dstData[i]);				
			}
			palette = vector<COLOR3>(uniqueColors.begin(), uniqueColors.end());
		}		
	}

	return palette;
}

void Texture::generateMipMaps(int mipLevels) {
	for (MipTexture& mip : mipmaps) {
		delete[] mip.data;
	}

	mipmaps.clear();
	COLOR4* texdata = (COLOR4*)data;

	// convert to 24bit for resample lib
	COLOR3* data24 = new COLOR3[width * height];
	for (int y = 0; y < height; y++) {
		for (int x = 0; x < width; x++) {
			COLOR4& src = texdata[y * width + x];
			COLOR3& dst = data24[y * width + x];
			dst.r = src.r;
			dst.g = src.g;
			dst.b = src.b;
			if (src.a == 0) {
				dst = COLOR3();
			}
		}
	}

	for (int m = 1; m <= mipLevels; m++) {
		int mipWidth = width >> m;
		int mipHeight = height >> m;
		int scale = 1 << m;

		COLOR3* mipData24 = new COLOR3[mipWidth * mipHeight];
		base::ResampleImage24((byte*)data24, width, height, (byte*)mipData24, mipWidth, mipHeight,
			//base::KernelType::KernelTypeNearest); // for masked textures
			base::KernelType::KernelTypeAverage); // checkerboards look less flickery with this
			//base::KernelType::KernelTypeBilinear);
			//base::KernelType::KernelTypeLanczos3);

		// use nearest sampling to fill the alpha channel
		COLOR4* mipData32 = new COLOR4[mipWidth * mipHeight];
		for (int y = 0; y < mipHeight; y++) {
			for (int x = 0; x < mipWidth; x++) {
				COLOR3& src = mipData24[y * mipWidth + x];
				COLOR4& dst = mipData32[y * mipWidth + x];
				COLOR4& originalSrc = texdata[y * width * scale + x * scale];
				dst.r = src.r;
				dst.g = src.g;
				dst.b = src.b;
				dst.a = originalSrc.a;
			}
		}
		delete[] mipData24;

		MipTexture mip;
		mip.width = mipWidth;
		mip.height = mipHeight;
		mip.data = mipData32;
		mip.level = m;
		mipmaps.push_back(mip);
	}

	delete[] data24;
}

void Texture::upload(int format, bool lightmap)
{
	if (!data) {
		return;
	}
	this->isLightmap = lightmap;
	if (uploaded) {
		glDeleteTextures(1, &id);
	}
	glGenTextures(1, &id);

	if (is3d) {
		int glParam3d = g_opengl_texture_array_support ? GL_TEXTURE_2D_ARRAY : GL_TEXTURE_3D;

		glBindTexture(glParam3d, id); // Binds this texture handle so we can load the data into it

		glTexParameteri(glParam3d, GL_TEXTURE_WRAP_S, GL_REPEAT); // Note: GL_CLAMP is significantly slower
		glTexParameteri(glParam3d, GL_TEXTURE_WRAP_T, GL_REPEAT);

		// Set up filters
		glTexParameteri(glParam3d, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(glParam3d, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

		if (format == GL_RGB)
		{
			glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
		}

		glTexImage3D(glParam3d, 0, format, width, height, depth, 0, format, GL_UNSIGNED_BYTE, data);
	}
	else {
		glBindTexture(GL_TEXTURE_2D, id); // Binds this texture handle so we can load the data into it

		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // Note: GL_CLAMP is significantly slower
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

		// Set up filters
		if (lightmap)
		{
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		}
		else
		{
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		}

		if (format == GL_RGB)
		{
			glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
		}

		glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);

		if (!lightmap && width % 16 == 0 && height % 16 == 0 && format == GL_RGBA) {
			const int mipLevels = 3;
			COLOR4* texdata = (COLOR4*)data;

			for (MipTexture& mip : mipmaps) {
				glTexImage2D(GL_TEXTURE_2D, mip.level , format, mip.width, mip.height, 0, format, GL_UNSIGNED_BYTE, mip.data);
			}

			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -0.5f);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, mipLevels);
		}
		else {
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
		}
	}

	uploaded = true;
}

void Texture::bind()
{	
	int glParam3d = g_opengl_texture_array_support ? GL_TEXTURE_2D_ARRAY : GL_TEXTURE_3D;
	int glFilter3d = g_opengl_texture_array_support ? GL_NEAREST_MIPMAP_LINEAR : GL_NEAREST;

	if (arrayId != -1) {
		// 3D textures break when using any interpolation or mip-mapping.
		// A new shader is needed for bilinear filtering without blending the Z axis.
		glBindTexture(glParam3d, arrayId);
		glTexParameteri(glParam3d, GL_TEXTURE_MIN_FILTER, glFilter3d);
		glTexParameteri(glParam3d, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		return;
	}

	if (is3d) {
		glBindTexture(glParam3d, id);
		glTexParameteri(glParam3d, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(glParam3d, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		return;
	}

	glBindTexture(GL_TEXTURE_2D, id);

	if (isLightmap) {
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	}
	else {
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	}
}
