/*********************************************************\
		 __      __           __  __
		/ /     / / _____    / / / / ______   __     __
	   / /     / / /___  \  / / / / / ___  | / /    / /
	  / /_____/ / _____| / / / / / / /__/ / / /    / /
	 / ______  / / ___  / / / / / / _____/ / /    / /
	/ /     / / / /__/ / / / / / / |____  / |____/ /
   /_/     /_/ |______/ /_/ /_/  \_____/  \_____  /
										 ______/ /
			High Level Game Framework   /_______/

  ------------------------------------------------------

  Copyright (c) 2008 - Rodrigo Braz Monteiro.
  This file is subject to the terms of license.txt.

\*********************************************************/

#ifdef _MSC_VER
#pragma warning(disable: 4996)
#pragma warning(disable: 6011)
#pragma warning(disable: 4100)

//#pragma comment(lib, "freetype243.lib")
#endif

#include "ft2build.h"
#include "freetype/freetype.h"
#include "draw_text_gl.h"
#include "video.h"
#include "exception.h"
#include "draw.h"
using namespace Halley;


///////////////////////////////////////////////////////////////
// Get the smallest power of two that is greater or equal to x
// Code from http://bob.allegronetwork.com/prog/tricks.html
int SmallestPowerOf2(int x) {
	x--;
	x |= (x >> 1);
	x |= (x >> 2);
	x |= (x >> 4);
	x |= (x >> 8);
	x |= (x >> 16);
	x++;
	return x;
}

///////////////
// Constructor
OpenGLText::OpenGLText() {
	//font = shared_ptr<Font>(new Font());
	drawOutline = 0;
	outline = 0;
	lineSpacing = 0;
}


//////////////
// Destructor
OpenGLText::~OpenGLText()
{
	Reset();
}


/////////
// Reset
void OpenGLText::Reset()
{
	textures.clear();
	glyphs.clear();
	outlines.clear();
	//drawOutline = 0;
	//outline = 0;
}


////////////
// Set font
void OpenGLText::SetFont(shared_ptr<Font> f)
{
	// No change required
	if (font && f->size == font->size && f->face == font->face && f->bold == font->bold && f->italics == font->italics) return;

	// Set font
	font = f;

	// Delete all old data
	Reset();
}


//////////////
// Set colour
void OpenGLText::SetColour(Colour _col)
{
	col = _col;
}


//////////////
// Set border
void OpenGLText::SetBorder(Colour _col, float width)
{
	borderCol = _col;
	outline = width+1;
}


/////////
// Print
void OpenGLText::Print(String text, Vector2f pos, Vector2f align, float origScale)
{
	// Adjust scale
	float scale = origScale / Video::GetScale();

	// Adjust position
	pos -= align * (scale * GetExtent(text));

	// Draw outline
	if (outline > 1) {
		shared_ptr<OpenGLText> bord = GetOutline(outline);
		bord->SetColour(borderCol);
		bord->SetLineSpacing(lineSpacing);
		bord->Print(text, pos, Vector2f(0,0), origScale);
	}

	// Set OpenGL
	glEnable(GL_TEXTURE_2D);
	SetBlendType(Texture::ALPHA_BLEND);

	// Draw primary string
	glColor4f(col.r, col.g, col.b, col.a);
	DrawString(text.GetUTF32(), pos, scale);
}


/////////////////
// Draw a string
void OpenGLText::DrawString(StringUTF32 text, Vector2f pos, float scale)
{
	// Variables
	float x = pos.x;
	float y = pos.y;
	size_t len = text.length();
	OpenGLTextGlyph glyph;
	int ascent = font->ascent;
	float dx = x;
	float dy = y - ascent * scale;

	// Draw string
	for (size_t i=0;i<len;i++) {
		// Get current character
		int curChar = text[i];

		// Handle carriage returns
		if (curChar == '\n') {
			dx = x;
			dy -= (font->lineSkip+lineSpacing) * scale;
		}

		// Handle normal glyphs
		else {
			glyph = GetGlyph(curChar);
			glyph.Draw(dx, dy, scale);
			float gw = glyph.GetAdvance();
			dx += gw * scale;
		}
	}
}


/////////////////////////
// Calculate text extent
Vector2f OpenGLText::GetExtent(String _text)
{
	// Variables
	StringUTF32 text = _text.GetUTF32();
	size_t len = text.length();
	OpenGLTextGlyph glyph;
	int dx=0,dy=0;
	Vector2f pos;

	// Simulate drawing of string
	for (size_t i=0;i<len;i++) {
		// Get current character
		int curChar = text[i];

		// Handle carriage returns
		if (curChar == '\n') {
			if (dx > pos.x) pos.x = dx;
			dx = 0;
			dy += font->lineSkip+lineSpacing;
		}

		// Handle normal glyphs
		else {
			glyph = GetGlyph(curChar);
			dx += glyph.GetAdvance();
		}
	}

	// Finish computing
	if (dx > pos.x) pos.x = dx;
	pos.y = dy+font->lineSkip+lineSpacing;

	// Scale and return
	//pos /= Video::GetScale();
	return pos;
}


static bool canBreakAt(int c)
{
	if (c > 127) return false;
	static const std::string breaks = std::string(" ,.!?:\n");
	return breaks.find(c) != std::string::npos;
}

String OpenGLText::Split(String _text, float width)
{
	StringUTF32 text = _text.GetUTF32();
	StringUTF32 tmp;
	StringUTF32 final;

	size_t len = text.length();
	OpenGLTextGlyph glyph;
	int curW = 0;
	int lastW = 0;

	// Simulate drawing of string
	for (size_t i=0;i<len;i++) {
		int curChar = text[i];

		if (canBreakAt(curChar)) {
			final += tmp;
			tmp.clear();
			lastW = 0;
		}

		if (curChar == '\n') {
			curW = 0;
			lastW = 0;
		} else {
			glyph = GetGlyph(curChar);
			float advance = glyph.GetAdvance();
			if (curW + advance < width) {
				curW += advance;
				lastW += advance;
			} else {
				// Too long, break here
				curW = lastW;
				final.push_back('\n');
				size_t tmpLen = tmp.length();
				if (tmpLen > 0) {
					if (tmp[0] == ' ') final += tmp.substr(1);
					else final += tmp;
				}
				tmp.clear();
				if (curChar == ' ') continue;
			}
		}
		tmp += curChar;
	}

	final += tmp;
	return String(final);
}


//////////////////
// Preload glyphs
void OpenGLText::LoadGlyphs(String text)
{
	for (size_t i=0; i<text.length(); i++) {
		GetGlyph(text[i]);
	}
}


/////////////////////
// Unload all glyphs
void OpenGLText::Clear()
{
	Reset();
}


///////////////
// Get a glyph
OpenGLTextGlyph OpenGLText::GetGlyph(int i)
{
	glyphMap::iterator res = glyphs.find(i);

	// Found
	if (res != glyphs.end()) return res->second;

	// Not found, create it
	return CreateGlyph(i);
}


//////////////////
// Create a glyph
OpenGLTextGlyph OpenGLText::CreateGlyph(int n)
{
	// Create glyph
	OpenGLTextGlyph glyph;
	glyph.font = font;
	glyph.value = n;
	glyph.outline = drawOutline+1;
	glyph.GetMetrics();

	// Insert into some texture
	bool ok = false;
	for (unsigned int i=0;i<textures.size();i++) {
		if (textures[i]->TryToInsert(glyph, font, drawOutline)) {
			ok = true;
			break;
		}
	}

	// No texture could fit it, create a new one
	if (!ok) {
		textures.push_back(shared_ptr<OpenGLTextTexture>(new OpenGLTextTexture(512,512)));
		textures.back()->TryToInsert(glyph, font, drawOutline);
	}

	// Set glyph and return it
	glyphs[n] = glyph;
	return glyph;
}


///////////////////////////////
// Get an outlined text drawer
shared_ptr<OpenGLText> OpenGLText::GetOutline(float width)
{
	if (outlines.find(width) == outlines.end()) {
		shared_ptr<OpenGLText> text(new OpenGLText());
		text->SetFont(font);
		text->drawOutline = width;
		outlines[width] = text;
	}
	
	return outlines[width];
}


///////////////////////
// Texture constructor
OpenGLTextTexture::OpenGLTextTexture(int w,int h)
{
	// Properties
	x = y = 0;
	width = SmallestPowerOf2(w);
	height = SmallestPowerOf2(h);
	tex = 0;
	nextY = 0;

	// Generate and bind
	glGenTextures(1, &tex);
	BindTexture(tex);

	// Texture parameters
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
	//glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
	//glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP);
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP);

	// Allocate texture
	glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA8,width,height,0,GL_ALPHA,GL_UNSIGNED_BYTE,NULL);
	if (glGetError()) throw L"Internal OpenGL text renderer error: Could not allocate Text Texture";
}


//////////////////////
// Texture destructor
OpenGLTextTexture::~OpenGLTextTexture()
{
	if (tex) {
		glDeleteTextures(1,&tex);
		tex = 0;
	}
}


//////////////////////////
// Can fit a glyph in it?
bool OpenGLTextTexture::TryToInsert(OpenGLTextGlyph &glyph, shared_ptr<Font> font, float outline)
{
	// Get size
	int margin = 2;
	int w = glyph.w + margin;
	int h = glyph.h + margin;
	if (w > width) return false;
	if (y+h > height) return false;

	// Can fit in this row?
	if (x + w < width) {
		Insert(glyph, font, outline);
		x += w;
		if (y+h > nextY) nextY = y+h;
		return true;
	}

	// Can fit the next row?
	else {
		if (nextY+h > height) return false;
		x = 0;
		y = nextY;
		nextY = y+h;
		Insert(glyph, font, outline);
		x += w;
		return true;
	}
}

#pragma warning(disable: 4189)

//////////
// Insert
void OpenGLTextTexture::Insert(OpenGLTextGlyph &glyph, shared_ptr<Font> font, float outline)
{
	// Glyph data
	int value = glyph.value;
	int w = glyph.w;
	int h = glyph.h;
	//int border2 = glyph.border;
	float extraOutline = ceil(glyph.outline) - glyph.outline;

	// Fill glyph structure
	glyph.x1 = (float(x)+0.5f)/width;
	glyph.y1 = (float(y)+0.5f)/height;
	glyph.x2 = (float(x+w)+0.5f)/width;
	glyph.y2 = (float(y+h)+0.5f)/height;
	glyph.tex = tex;
	glyph.font = font;
	glyph.outline = outline;

	// Render image
	SDL_Surface* img = font->renderGlyph(glyph.value);
	int imgw = img->w;
	if (imgw % 2 == 1) imgw++;
	int imgh = img->h;

	// Extend outline (step 1)
	if (outline > 0) {
		int pad = glyph.outline;
		SDL_Rect dstRect;
		dstRect.x = pad;
		dstRect.y = pad;

		imgw += 2*pad;
		imgh += 2*pad;
		SDL_Rect dstRect2;
		dstRect2.x = 0;
		dstRect2.y = 0;
		dstRect2.w = imgw;
		dstRect2.h = imgh;

		auto f = img->format;
		SDL_Surface* img2 = SDL_CreateRGBSurface(SDL_SWSURFACE, imgw, imgh, f->BitsPerPixel, f->Rmask, f->Gmask, f->Bmask, f->Amask);
		SDL_SetPalette(img2, SDL_LOGPAL, img->format->palette->colors, 0, img->format->palette->ncolors);
		SDL_FillRect(img2, &dstRect2, SDL_MapRGB(img2->format, 0, 0, 0));
		int result = SDL_BlitSurface(img, nullptr, img2, &dstRect);
		SDL_FreeSurface(img);
		img = img2;
	}

	// Convert to alpha
	unsigned char *alpha = GetGlyphAlpha(img);
	SDL_FreeSurface(img);

	// Extend outline (step 2)
	if (outline > 0) {
		unsigned char *alpha2 = ExtendBorder(alpha, imgw, imgh, outline-1);
		delete[] alpha;
		alpha = alpha2;
	}
	unsigned char* lumAlpha = GetLuminanceAlpha(alpha, imgw*imgh);
	delete[] alpha;

	// Upload image to video memory
	BindTexture(tex);
	glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, imgw, imgh, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, lumAlpha);
	delete[] lumAlpha;

	if (int err = glGetError()) {
		throw Exception("Internal OpenGL text renderer error: Error uploading glyph data to video memory.");
	}
}

unsigned char * OpenGLTextTexture::GetConvolutionMatrix(float border)
{
	int offset = std::max(1,int(ceil(border))+1);
	int convStride = offset*2 + 1;
	unsigned char * __restrict conv = new unsigned char[convStride * convStride];

	// Generate convolution matrix
	for (int b=0; b<convStride; b++) {
		int convOff = b*convStride;
		for (int a=0; a<convStride; a++) {
			int convPos = convOff + a;

			// Get distance from cell to center
			float da = (a-offset);
			float db = (b-offset);
			float dist = sqrt(float(da*da + db*db));

			const float limit = 1.0f;
			if (dist <= border)
				conv[convPos] = 255;
			else {
				if (dist >= border+limit)
					conv[convPos] = 0;
				else
					conv[convPos] = 255 - (unsigned char)(255*(dist-border)/limit);
				//conv[convPos] = 0;
			}
		}
	}

	return conv;
}

unsigned char* OpenGLTextTexture::ExtendBorder(const unsigned char* __restrict src, int w, int h, float border)
{
	size_t len = w*h*2;
	int offset = std::max(1, int(ceil(border))+1);
	int convStride = offset*2 + 1;

	const unsigned char * __restrict conv = GetConvolutionMatrix(border);
	unsigned char * __restrict result = new unsigned char[len];
	memset(result, 0, len);

	// Loop through the original image
	for (int y=offset; y<h-offset; y++) {
		int lastValue = 0;
		int nextValue = src[y*w];
		for (int x=offset; x<w-offset; x++) {
			// Get the current pixel
			const int value = nextValue;
			nextValue = src[y*w+x+1];
			if (value == 0) continue;

			// If this isn't more opaque than the last pixel, then it will certainly not be useful for pixels that are to the left of it
			const int startA = lastValue > value? offset : 0;

			// Same as above, but for next pixel
			const int endA = nextValue > value? convStride-offset : convStride;
			const int lenA = endA - startA;

			// Same as above, but for pixel above
			const int valueAbove = src[(y-1)*w+x];	// This is safe because y >= 1
			//const int valueAbove = 0;
			const int startB = valueAbove > value? offset : 0;

			// Same as above, but for pixel below
			const int valueBelow = src[(y+1)*w+x];	// Likewise, y < h-1
			//const int valueBelow = 0;
			const int endB = valueBelow > value? convStride - offset : convStride;

			// Multiply it by the convolution matrix and set values
			const size_t dstOff = (y-offset)*w - offset + startA + x;
			for (int b=startB; b<endB; b++) {
				int convOff = b*convStride + startA;
				size_t dstPos = b*w + dstOff;
				//for (int a = convStride-startA; --a >= 0; ) {
				for (int a = lenA; --a >= 0; ) {
					unsigned char thisVal = (unsigned char)(conv[convOff++] * value >> 8);

					if (result[dstPos] >= thisVal);		// If you're wondering WTF is going on, check Intel's Optimization Reference Manual, 3.4.1.3
					else result[dstPos] = thisVal;
					dstPos++;
				}
			}
		}
	}
	
	delete[] conv;
	return result;
}


unsigned char* OpenGLTextTexture::Subtract(const unsigned char* img1, const unsigned char* img2, size_t w, size_t h)
{
	(void) img1;
	(void) img2;
	(void) w;
	(void) h;
	return NULL;
}

unsigned char* OpenGLTextTexture::GetGlyphAlpha(SDL_Surface *img)
{
	const size_t imgw = img->w;
	const size_t pad = ((imgw % 2 == 1) ? 1 : 0);
	const size_t w = imgw + pad;
	const size_t h = img->h;
	const size_t pitch = img->pitch;
	const size_t len = w * h;
	const size_t bpp = img->format->BytesPerPixel;
	unsigned char *alpha = new unsigned char[len];
	unsigned char *write = alpha;

	const unsigned char* const px = (const unsigned char*) img->pixels;
	for (size_t y=0; y<h; y++) {
		const unsigned char* read = px + (y*pitch);
		for (size_t x=0; x<imgw; x++) {
			*write++ = *read;
			read += bpp;
		}
		for (size_t i=0; i<pad; i++) {
			*write++ = 0;
		}
	}

	return alpha;
}


unsigned char* OpenGLTextTexture::GetLuminanceAlpha(const unsigned char* src, size_t len)
{
	unsigned char* dst = new unsigned char[len*2];
	for (size_t i=0; i<len; i++) {
		dst[2*i] = 255;
		dst[2*i+1] = src[i];
	}
	return dst;
}



////////////////
// Draw a glyph
void OpenGLTextGlyph::Draw(float x,float y,float scale)
{
	float dw = w*scale;
	float dh = h*scale;
	float o = (ceil(outline))*scale;
	float ox = dx - o;
	float oy = dy - o;

	// Store matrix and translate
	glPushMatrix();
	glTranslatef(x+ox, y+oy, 0.0f);

	// Set texture
	BindTexture(tex);

	// Draw quad
	glBegin(GL_QUADS);
	// Top-left
	glTexCoord2f(x1,y1);
	glVertex2f(0,dh);
	// Bottom-left
	glTexCoord2f(x1,y2);
	glVertex2f(0,0);
	// Bottom-right
	glTexCoord2f(x2,y2);
	glVertex2f(dw,0);
	// Top-right
	glTexCoord2f(x2,y1);
	glVertex2f(dw,dh);
	glEnd();

	// Restore matrix
	glPopMatrix();
}


/////////////////////
// Get glyph metrics
void OpenGLTextGlyph::GetMetrics()
{
	int minx, maxx, miny, maxy;
	font->getMetrics(value, minx, maxx, miny, maxy, advance);
	dx = minx;
	dy = miny;
	w = maxx - minx;
	h = maxy - miny;

	border = int(ceil(outline));
	w += 2*border;
	h += 2*border;
}

