#define STB_TRUETYPE_IMPLEMENTATION

#include "fontrender.h"
#include "stb_truetype.h"
#include "platform.h"
#include "sys_log.h"

static void* ttfbuf = NULL;
static stbtt_fontinfo ttfinfo;
static GLuint fontquad_vao = 0;
static GLuint fontquad_vbo = 0;
static GLuint fontprog = 0;
static GLint uni_pos = -1;
static GLint uni_box = -1;
static GLint uni_outsize = -1;
static GLint uni_charsize = -1;
extern const GLchar* VtxShader_fontrender;
extern const GLchar* FragShader_fontrender;

int yrFontRender_init(const char* ttf)
{
	//read ttf file
	yrFile* file = yrFile_open(ttf, yrF_read);
	if(!file) return -1;
	int64_t flen = yrFile_seek(file, 0, yrF_seekend);
	yrFile_seek(file, 0, yrF_seekset);
	if(!flen) {
		yrLog(0, "Tried to use an empty font file: %s", ttf);
		yrFile_close(file);
		return -1;
	}
	ttfbuf = malloc(flen);
	if(!ttfbuf) {
		yrLog(0, "Out of memory", ttf);
		yrFile_close(file);
		return -1;
	}
	int64_t rw = yrFile_read(file, flen, ttfbuf);
	yrFile_close(file);
	if(rw != flen) { yrLog(0, "Font file read failed: %s", ttf); goto onerror; }
	
	//initialize stb_truetype
	if(stbtt_InitFont(&ttfinfo, ttfbuf, 0));
	else { yrLog(0, "Invalid font file: %s", ttf); goto onerror; }

	//quad
	glGenVertexArrays(1, &fontquad_vao);
	glGenBuffers(1, &fontquad_vbo);
	glBindVertexArray(fontquad_vao);
	glBindBuffer(GL_ARRAY_BUFFER, fontquad_vbo);
	glBufferData(GL_ARRAY_BUFFER, 4 * 2 * sizeof(GLfloat), NULL, GL_STATIC_DRAW);
	glVertexAttribPointer(0, 2, GL_FLOAT, FALSE, 0, (GLvoid*)0);
	glEnableVertexAttribArray(0);

	float* buf = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
	if(!buf) {
		yrLog(0, "Could not map OpenGL buffer: %x", glGetError());
		goto onerror;
	}
	buf[0*2 + 0] = 0.0f;
	buf[0*2 + 1] = 0.0f;
	buf[1*2 + 0] = 1.0f;
	buf[1*2 + 1] = 0.0f;
	buf[2*2 + 0] = 1.0f;
	buf[2*2 + 1] = 1.0f;
	buf[3*2 + 0] = 0.0f;
	buf[3*2 + 1] = 1.0f;
	glUnmapBuffer(GL_ARRAY_BUFFER);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindVertexArray(0);
	GLenum glerr = glGetError();
	if(glerr != GL_NO_ERROR) {
		yrLog(0, "OpenGL error during font quad buffer write: %x", glGetError());
		goto onerror;
	}

	//font render shader
	int err = 0;
	fontprog = glCreateProgram();
	err = OpenGL_shader(fontprog, GL_VERTEX_SHADER, VtxShader_fontrender);
	if(err) { yrLog(0, "Compilation error(s) were in font vertex shader."); return -1; }
	err = OpenGL_shader(fontprog, GL_FRAGMENT_SHADER, FragShader_fontrender);
	if(err) { yrLog(0, "Compilation error(s) were in font fragment shader."); return -1; }
	glLinkProgram(fontprog);
	glerr = glGetError();
	if(glerr != GL_NO_ERROR) { yrLog(0, "Font shaderprogram failed to link: %x", glerr); return -1; }

	uni_pos			= glGetUniformLocation(fontprog, "pos");
	uni_box			= glGetUniformLocation(fontprog, "box");
	uni_outsize		= glGetUniformLocation(fontprog, "outsize");
	uni_charsize	= glGetUniformLocation(fontprog, "charsize");
	glerr = glGetError();
	if(glerr != GL_NO_ERROR) {
		yrLog(0, "OpenGL error during font shader uniform location acquisition: %x", glerr);
		return -1;
	}

	//done
	return 0;
onerror:
	glDeleteVertexArrays(1, &fontquad_vao);
	glDeleteBuffers(1, &fontquad_vbo);
	glDeleteProgram(fontprog);
	fontquad_vao = 0;
	fontquad_vbo = 0;
	fontprog = 0;
	free(ttfbuf);
	ttfbuf = NULL;
	return -1;
}

void yrFontRender_cleanup(void)
{
	YR_ASSERT(ttfbuf);
	glBindVertexArray(0);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glUseProgram(0);
	glDeleteVertexArrays(1, &fontquad_vao);
	glDeleteBuffers(1, &fontquad_vbo);
	glDeleteProgram(fontprog);
	fontquad_vao = 0;
	fontquad_vbo = 0;
	fontprog = 0;
	free(ttfbuf);
	ttfbuf = NULL;
}

static int get_codepoint(const unsigned char* string, int* off)
{
	if(string[*off] & 0x80 ) {
		//multibyte
		int c = string[*off];
		*off += 1;
		int foo;
			 if((c & 0xe0) == 0xc0) {foo = 1; c &= ~0xe0;}
		else if((c & 0xf0) == 0xe0) {foo = 2; c &= ~0xf0;}
		else if((c & 0xf8) == 0xf0) {foo = 3; c &= ~0xf8;}
		else {return get_codepoint(string, off);}
		while(foo) {
			c <<= 8;
			c |= (string[*off] & ~0xc0);
			*off += 1;
			foo -= 1;
		}
		return c;
	} else {
		//standard ascii
		int c = string[*off];
		*off += 1;
		return c;
	}
}

#define max_lines 128
static int makelines(const char* string, float scale, unsigned width_px, int* breakpos, enum UiTextAlign align, float* pad, int singleline, unsigned* autowidth)
{
	int xpos = 0;
	int off = 0;
	int prevoff = 0;
	int glyph;
	int lastbreak = -1;
	float lastpad = 0;
	int linecount = 0;
	float linelead = 0;
	//reserve some space for ellipsis if singleline
	int ellipsis = 0;
	if(singleline) {
		stbtt_GetCodepointHMetrics(&ttfinfo, '.', &ellipsis, NULL);
		ellipsis *= 3;
		xpos = ellipsis;
	}

	int c = get_codepoint(string, &off);
	if(c) glyph = stbtt_FindGlyphIndex(&ttfinfo, c);

	while(c) {
		//char advance
		int advance;
		int lsb;
		stbtt_GetGlyphHMetrics(&ttfinfo, glyph, &advance, &lsb);
		lsb += xpos;

		if(lsb * scale < linelead) linelead = floorf(lsb * scale);
		
		//line breaking
		int needbreak = (c == '\n') || (scale * (xpos + advance - linelead) > width_px);
		if(needbreak) {
			int breakhere = (c == '\n') || (lastbreak == -1) || singleline;
			breakpos[linecount] = breakhere ? prevoff : lastbreak;
			
			float breakpad = (breakhere ? ((xpos - linelead) * scale) : lastpad);
			switch(align) {
				case taRight:  pad[linecount] = width_px - breakpad; break;
				case taCenter: pad[linecount] = (width_px - breakpad) / 2; break;
				default:
				case taLeft:   pad[linecount] = scale * -linelead; break;
			}
			off = (c=='\n') ? off : breakpos[linecount]; //rewind
			linecount += 1;
			lastbreak = -1;
			linelead = 0.0f;
			xpos = -advance;
			if(singleline || linecount == max_lines) break;
		}
		else {
			//save latest potential breaking point
			if(c <= 255 && isspace(c)) {
				lastbreak = off;
				lastpad = (xpos - linelead)  * scale;
			}
		}

		//kerning adjustment
		prevoff = off;
		c = get_codepoint(string, &off);
		if(c && !needbreak) {
			int nextg = stbtt_FindGlyphIndex(&ttfinfo, c);
			advance += stbtt_GetGlyphKernAdvance(&ttfinfo, glyph, nextg);
			glyph = nextg;
		}
		//apply
		xpos += advance;
	}
	if((!singleline && linecount != max_lines) ||
	   (singleline && linecount==0)) {
		breakpos[linecount] = off;
		switch(align) {
			case taRight:  pad[linecount] = (float) width_px - (xpos - linelead - ellipsis) * scale; break;
			case taCenter: pad[linecount] = (width_px - (xpos - linelead - ellipsis) * scale) / 2; break;
			case taLeft:   pad[linecount] = scale * -linelead; break;
		}
		linecount += 1;
	}
	if(autowidth) {
		*autowidth = (unsigned) ceilf((xpos - linelead - ellipsis) * scale);
	}
	return string[0] ? linecount : 0;
}

GLuint yrFontRender_render(const char* string, float size_px, unsigned width_px, unsigned* height_px, enum UiTextAlign align, int singleline, unsigned* autowidth)
{
	YR_ASSERT(ttfbuf);
	if(autowidth) {
		singleline = 1;
		width_px = 0xfffffffful;
		align = taLeft;
	}
	//calc scale and vertical metrics
	int ascent, descent, linegap;
	stbtt_GetFontVMetrics(&ttfinfo, &ascent, &descent, &linegap);
	float scale = size_px / (ascent - descent);
	unsigned line_height = (unsigned) ceilf(scale * (ascent - descent + linegap));
	unsigned baseline = (unsigned) ceilf(ascent * scale);
	int glyph_nothing = stbtt_FindGlyphIndex(&ttfinfo, 0);
	
	//iterate over text, break into lines and mark breakpoints
	int breakpos[max_lines];
	float breakpad[max_lines];
	int linecount = makelines(string, scale, width_px, breakpos, align, breakpad, singleline, autowidth);
	if(autowidth) width_px = *autowidth;

	//create char tex
	GLuint charbuf_tex = 0;
	unsigned char* charbuf_mem = NULL;
	GLenum glerr;
	int x0,x1,y0,y1;
	stbtt_GetFontBoundingBox(&ttfinfo,&x0,&y0,&x1,&y1);
	GLsizei charbuf_w = (GLsizei) ceilf(scale * (x1 - x0)) + 2;
	GLsizei charbuf_h = (GLsizei) ceilf(scale * (y1 - y0)) + 2;
	charbuf_w = (charbuf_w + 3) & (~3);
	charbuf_h = (charbuf_h + 3) & (~3);

	charbuf_mem = malloc(charbuf_w * charbuf_h);
	if(!charbuf_mem) { yrLog(0, "Out of memory"); return 0; }
	glGenTextures(1, &charbuf_tex);
	glBindTexture(GL_TEXTURE_2D, charbuf_tex);
	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_MAX_LEVEL, 0);
	glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8, charbuf_w, charbuf_h);
	glBindTexture(GL_TEXTURE_2D, 0);
	glerr = glGetError();
	if(glerr != GL_NO_ERROR) {
		//yrLog(0, "OpenGL error while creating character buffer: %x", glerr);
		glDeleteTextures(1, &charbuf_tex);
		free(charbuf_mem);
		return 0;
	}

	//create output tex + fb
	GLuint out_tex = 0, out_fb = 0;
	GLsizei out_w = width_px ? width_px : 1;
	GLsizei out_h = line_height * (linecount ? linecount : 1);
	*height_px = line_height * (singleline ? 1 : linecount);
	
	glGenFramebuffers(1, &out_fb);
	glGenTextures(1, &out_tex);
	glBindFramebuffer(GL_FRAMEBUFFER, out_fb);
	glBindTexture(GL_TEXTURE_2D, out_tex);
	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_MAX_LEVEL, 0);
	glTexStorage2D(GL_TEXTURE_2D, 1, GL_SRGB8_ALPHA8, out_w, out_h);
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, out_tex, 0);

	int ok = (glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE);
	glBindTexture(GL_TEXTURE_2D, 0);
	if(!ok) {
		yrLog(0, "OpenGL error while creating font render framebuffer: %x", glGetError());
		goto onerror;
	}
	glViewport(0, 0, out_w, out_h);
	glDisable(GL_MULTISAMPLE);
	glDisable(GL_DEPTH_TEST);
	glEnable(GL_BLEND);
	glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
	glBindTexture(GL_TEXTURE_2D, charbuf_tex);
	glUseProgram(fontprog);
	glBindVertexArray(fontquad_vao);
	glBindBuffer(GL_ARRAY_BUFFER, fontquad_vbo);
	glClearColor(0.0f,0.0f,0.0f,0.0f);
	glClear(GL_COLOR_BUFFER_BIT);
	glUniform2f(uni_outsize, (GLfloat) out_w, (GLfloat) out_h);
	glUniform2f(uni_charsize, (GLfloat) charbuf_w, (GLfloat) charbuf_h);
	{ GLuint glerr = glGetError(); if(glerr != GL_NO_ERROR)  yrLog(0, "OpenGL error: %x", glerr); }

	//render all lines
	int off = 0;
	float posx = 0.0f;
	float posy = 0.0f;
	for(int l = 0; l < linecount; ++l)
	{
		posx = breakpad[l];
		posy = (float)(l * line_height + baseline);
		int c = get_codepoint(string, &off);
		int glyph = stbtt_FindGlyphIndex(&ttfinfo, c);
		if(!glyph && c <= 255 && isspace(c)) glyph = glyph_nothing;
		while(c)
		{
			//render bitmap
			float x_shift = posx - floorf(posx);
			int xlo,xhi,ylo,yhi;
			memset(charbuf_mem, 0, charbuf_w * charbuf_h);
			stbtt_GetGlyphBitmapBoxSubpixel(&ttfinfo, glyph, scale, scale, x_shift, 0.0f, &xlo, &ylo, &xhi, &yhi);
			if(xhi - xlo <= charbuf_w && yhi - ylo <= charbuf_h)
				stbtt_MakeGlyphBitmapSubpixel(&ttfinfo, charbuf_mem, xhi - xlo, yhi - ylo, charbuf_w, scale, scale, x_shift, 0.0f, glyph);
			else 
				yrLog(0, "Character out of bounds: %c (%i)", c, c);
			//bitmap to tex
			glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, charbuf_w, charbuf_h, GL_RED, GL_UNSIGNED_BYTE, charbuf_mem);
			//place glyph on output texture
			glUniform2f(uni_pos, posx, posy);
			glUniform4f(uni_box, (GLfloat) xlo, (GLfloat) ylo, (GLfloat) xhi, (GLfloat) yhi);
			glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
			//advance pos
			int advance;
			stbtt_GetGlyphHMetrics(&ttfinfo, glyph, &advance, NULL);
			c = (off < breakpos[l]) ? get_codepoint(string, &off) : 0;
			if(c) {
				int nextg = stbtt_FindGlyphIndex(&ttfinfo, c);
				if(!nextg && c <= 255 && isspace(c)) nextg = glyph_nothing;
				advance += stbtt_GetGlyphKernAdvance(&ttfinfo, glyph, nextg);
				glyph = nextg;
			}
			posx += scale * advance;
		}
	}
	//render ellipsis if required
	if(singleline && string[off-1])
	{
		int glyph = stbtt_FindGlyphIndex(&ttfinfo, '.');
		int advance;
		stbtt_GetGlyphHMetrics(&ttfinfo, glyph, &advance, NULL);
		advance += stbtt_GetGlyphKernAdvance(&ttfinfo, glyph, glyph);

		for(int i = 0; i < 3; ++i)
		{
			//render bitmap
			float x_shift = posx - floorf(posx);
			int xlo,xhi,ylo,yhi;
			memset(charbuf_mem, 0, charbuf_w * charbuf_h);
			stbtt_GetGlyphBitmapBoxSubpixel(&ttfinfo, glyph, scale, scale, x_shift, 0.0f, &xlo, &ylo, &xhi, &yhi);
			stbtt_MakeGlyphBitmapSubpixel(&ttfinfo, charbuf_mem, xhi - xlo, yhi - ylo, charbuf_w, scale, scale, x_shift, 0.0f, glyph);
			//bitmap to tex
			glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, charbuf_w, charbuf_h, GL_RED, GL_UNSIGNED_BYTE, charbuf_mem);
			//place glyph on output texture
			glUniform2f(uni_pos, posx, posy);
			glUniform4f(uni_box, (GLfloat) xlo, (GLfloat) ylo, (GLfloat) xhi, (GLfloat) yhi);
			glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
			//advance pos
			posx += scale * advance;
		}
	}
	{ GLuint glerr = glGetError(); if(glerr != GL_NO_ERROR)  yrLog(0, "OpenGL error: %x", glerr); }
	//done
	goto cleanup;
onerror:
	//if something went wrong, clean up the output texture as well
	glBindTexture(GL_TEXTURE_2D, 0);
	glDeleteTextures(1, &out_tex);
	out_tex = 0;
cleanup:
	glBindTexture(GL_TEXTURE_2D, 0);
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	glUseProgram(0);
	glBindVertexArray(0);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glDeleteFramebuffers(1, &out_fb);
	glDeleteTextures(1, &charbuf_tex);
	free(charbuf_mem);
	{ GLuint glerr = glGetError(); if(glerr != GL_NO_ERROR)  yrLog(0, "OpenGL error: %x", glerr); }
	return out_tex;
}