module ugui;

import schrift;
import grapheme;
import std::collections::map;
import std::core::mem;
import std::core::mem::allocator;
import std::io;
import std::ascii;


// ---------------------------------------------------------------------------------- //
//                                CODEPOINT                                           //
// ---------------------------------------------------------------------------------- //

// unicode code point, different type for a different hash
alias Codepoint = uint;

<*
@require off != null
@require str.ptr != null
*>
fn Codepoint str_to_codepoint(char[] str, usz* off)
{
	Codepoint cp;
	isz b = grapheme::decode_utf8(str, str.len, (uint*)&cp);
	if (b == 0 || b > str.len) {
		return 0;
	}
	*off = b;
	return cp;
}

//macro uint Codepoint.hash(self) => ((uint)self).hash();

// ---------------------------------------------------------------------------------- //
//                                FONT ATLAS                                          //
// ---------------------------------------------------------------------------------- //

/* width and height of a glyph contain the kering advance
 * (u,v)
 *   +-------------*---+ -
 *   |       ^     |   | ^
 *   |       |oy   |   | |
 *   |       v     |   | |
 *   |      .ii.   |   | |
 *   |     @@@@@@. |   | |
 *   |    V@Mio@@o |   | |
 *   |    :i.  V@V |   | h
 *   |      :oM@@M |   | |
 *   |    :@@@MM@M |   | |
 *   |    @@o  o@M |   | |
 *   |<->:@@.  M@M |   | |
 *   |ox  @@@o@@@@ |   | |
 *   |    :M@@V:@@.|   | v
 *   +-------------*---+ -
 *   |<---- w ---->|
 *   |<------ adv ---->|
 */
struct Glyph {
	Codepoint code;
	ushort u, v;
	ushort w, h;
	short adv, ox, oy;
}

const uint FONT_CACHED = 255;
alias GlyphTable = map::HashMap{Codepoint, Glyph};

faultdef TTF_LOAD_FAILED, MISSING_GLYPH, BAD_GLYPH_METRICS, RENDER_ERROR;

struct Font {
	schrift::Sft sft;
	String path;
	Id id; // font id, same as atlas id
	GlyphTable table;

	float size;
	float ascender, descender, linegap; // Line Metrics
	Atlas atlas;
	bool should_update; // should send update_atlas command, resets at frame_end()
}

fn void? Font.load(&font, String name, ZString path, uint height, float scale)
{
	font.table.init(allocator::mem, capacity: FONT_CACHED);
	font.id = name.hash();

	font.size = height*scale;

	font.sft = {
		.xScale = (double)font.size,
		.yScale = (double)font.size,
		.flags = schrift::SFT_DOWNWARD_Y,
	};

	font.sft.font = schrift::loadfile(path);
	if (font.sft.font == null) {
		font.table.free();
		return TTF_LOAD_FAILED?;
	}

	schrift::SftLMetrics lmetrics;
	schrift::lmetrics(&font.sft, &lmetrics);
	font.ascender  = (float)lmetrics.ascender;
	font.descender = (float)lmetrics.descender;
	font.linegap   = (float)lmetrics.lineGap;
	//io::printfn("ascender:%d, descender:%d, linegap:%d", font.ascender, font.descender, font.linegap);

	// TODO: allocate buffer based on FONT_CACHED and the size of a sample letter
	//       like the letter 'A'
	ushort size = (ushort)font.size*(ushort)($$sqrt((float)FONT_CACHED));
	font.atlas.new(font.id, ATLAS_GRAYSCALE, size, size)!;

	// preallocate the ASCII range
	for (char c = ' '; c < '~'; c++) {
		font.get_glyph((Codepoint)c)!;
	}
}

fn Glyph*? Font.get_glyph(&font, Codepoint code)
{
	Glyph*? gp;
	gp = font.table.get_ref(code);

	if (catch excuse = gp) {
		if (excuse != NOT_FOUND) {
			return excuse?;
		}
	} else {
		return gp;
	}

	// missing glyph, render and place into an atlas
	Glyph glyph;

	schrift::SftGlyph gid;
	schrift::SftGMetrics gmtx;

	if (schrift::lookup(&font.sft, (SftUChar)code, &gid) < 0) {
		return MISSING_GLYPH?;
	}

	if (schrift::gmetrics(&font.sft, gid, &gmtx) < 0) {
		return BAD_GLYPH_METRICS?;
	}

	schrift::SftImage img = {
		.width = gmtx.minWidth,
		.height = gmtx.minHeight,
	};
	char[] pixels = mem::new_array(char, (usz)img.width * img.height);
	img.pixels = pixels;
	if (schrift::render(&font.sft, gid, img) < 0) {
		return RENDER_ERROR?;
	}

	glyph.code = code;
	glyph.w = (ushort)img.width;
	glyph.h = (ushort)img.height;
	glyph.ox = (short)gmtx.leftSideBearing;
	glyph.oy = (short)gmtx.yOffset;
	glyph.adv = (short)gmtx.advanceWidth;
	//io::printfn("code=%c, w=%d, h=%d, ox=%d, oy=%d, adv=%d",
	//    glyph.code, glyph.w, glyph.h, glyph.ox, glyph.oy, glyph.adv);

	Point uv = font.atlas.place(pixels, glyph.w, glyph.h, (ushort)img.width)!;
	glyph.u = uv.x;
	glyph.v = uv.y;

	mem::free(pixels);

	font.table.set(code, glyph);

	font.should_update = true;
	return font.table.get_ref(code);
}

fn void Font.free(&font)
{
	font.atlas.free();
	font.table.free();
	schrift::freefont(font.sft.font);
}


// ---------------------------------------------------------------------------------- //
//                           FONT LOAD AND QUERY                                      //
// ---------------------------------------------------------------------------------- //

fn void? Ctx.load_font(&ctx, String name, ZString path, uint height, float scale = 1.0)
{
	return ctx.font.load(name, path, height, scale);
}

// TODO: check if the font is present in the context
fn Id Ctx.get_font_id(&ctx, String label)
{
	return (Id)label.hash();
}

fn Atlas*? Ctx.get_font_atlas(&ctx, String name)
{
	// TODO: use the font name, for now there is only one font
	if (name.hash() != ctx.font.id) {
		return WRONG_ID?;
	}

	return &ctx.font.atlas;
}

fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float)0.5);

// ---------------------------------------------------------------------------------- //
//                              TEXT MEASUREMENT                                      //
// ---------------------------------------------------------------------------------- //


const uint TAB_SIZE = 4;

struct TextSize {
	Size width, height;
	int area;
}

// Measeure the size of a string.
// width.min: as if each word is broken up by a new line
// width.max: the width of the string left as-is
// height.min: the height of the string left as-is
// height.max: the height of the string with each word broken up by a new line
fn TextSize? Ctx.measure_string(&ctx, String text)
{
	if (text == "") return (TextSize){};

	Font* font = &ctx.font;
	short baseline = (short)font.ascender;
	short line_height = (short)font.line_height();
	short line_gap = (short)font.linegap;
	short space_width = font.get_glyph(' ').adv!;
	short tab_width = space_width * TAB_SIZE;

	isz off;
	usz x;

	TextSize ts;

	short word_width;
	short words = 1;
	Rect bounds; // unaltered text bounds;
	Point origin;

	Codepoint cp = str_to_codepoint(text[off..], &x);
	for (; cp != 0; cp = str_to_codepoint(text[off..], &x)) {
		off += x;
		Glyph* gp = font.get_glyph(cp)!;
		
		// update the text bounds
		switch {
		case cp == '\n':
			origin.x = 0;
			origin.y += line_height + line_gap;
		case cp == '\t':
			origin.x += tab_width;
		case ascii::is_cntrl((char)cp):
			break;
		default:
			Rect b = {
				.x = origin.x + gp.ox,
				.y = origin.y + gp.oy + baseline,
				.w = gp.w,
				.h = gp.h,
			};
			bounds = containing_rect(bounds, b);
			origin.x += gp.adv;
		}
		
		// update the word width
		switch {
		case ascii::is_space((char)cp):
			if (word_width > ts.width.min) ts.width.min = word_width;
			word_width = 0;
			words++;
		default:
			//word_width += gp.w + gp.ox;
			if (off < text.len) {
				word_width += gp.adv;
			} else {
				word_width += gp.w + gp.ox;
			}
		}
	}
	// end of string is also end of word
	if (word_width > ts.width.min) ts.width.min = word_width;

	ts.width.max = bounds.w;
	ts.height.min = bounds.h;
	ts.height.max = words * line_height + line_gap * (words-1);
	ts.area = bounds.w * bounds.h;

	return ts;
}

fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue)
{
	if (text.len == 0 || bounds.w <= 0 || bounds.h <= 0) return;
	ctx.push_scissor(bounds, z_index)!;

	Font* font = &ctx.font;
	Id texture_id = font.id;
	short baseline = (short)font.ascender;
	short line_height = (short)font.line_height();
	short line_gap = (short)font.linegap;
	short space_width = font.get_glyph(' ').adv!;
	short tab_width = space_width * TAB_SIZE;

	Point origin = bounds.position();

	// measure string height inside the bounds to center it vertically
	// FIXME: this is fast but it just gives back the height in LINES, most lines have only short
	//        characters which would result in a lower height
	int string_height = line_height;
	foreach (c: text) {
		string_height += (line_height + line_gap) * (int)(c == '\n');
	}

	switch (anchor) {
	case TOP_LEFT: nextcase;
	case TOP: nextcase;
	case TOP_RIGHT:
		origin.y += 0;
	case LEFT: nextcase;
	case CENTER: nextcase;
	case RIGHT:
		origin.y += (short)(bounds.h - string_height)/2;
	case BOTTOM_LEFT: nextcase;
	case BOTTOM: nextcase;
	case BOTTOM_RIGHT:
		origin.y += (short)(bounds.h - string_height);
	}

	// measure the line until it exits the bounds or the string ends
	usz line_start, line_end;
	do {
		int line_width;
		Point o = {.x = bounds.x, .y = bounds.y};
		
		Codepoint cp;
		isz off = line_start;
		for ITER: (usz x; (cp = str_to_codepoint(text[off..], &x)) != 0; off += x) {
			Glyph* gp = font.get_glyph(cp)!;

			switch {
			case cp == '\n':
				break ITER;
			case cp == '\t':
				o.x += tab_width;
			case ascii::is_cntrl((char)cp):
				break;
			default:
				Rect b = {
					.x = o.x + gp.ox,
					.y = o.y + gp.oy + baseline,
					.w = gp.w,
					.h = gp.h,
				};

				if (b.x + b.w > bounds.x + bounds.w) {
					break ITER;
				}
				o.x += gp.adv;
				line_width += gp.adv;
			}
		}
		line_end = off;
		if (line_end == line_start) break;
		
		// with the line width calculate the right origin and layout the line
		switch (anchor) {
		case TOP_LEFT: nextcase;
		case LEFT: nextcase;
		case BOTTOM_LEFT:
			// TODO: we didn't need to measure the line width with this alignment
			origin.x += 0;
		case TOP: nextcase;
		case CENTER: nextcase;
		case BOTTOM:
			origin.x += (short)(bounds.w - line_width)/2+1;
		case TOP_RIGHT: nextcase;
		case RIGHT: nextcase;
		case BOTTOM_RIGHT:
			origin.x += (short)(bounds.w - line_width);
		}

		// see the fixme when measuring the height
		//ctx.push_rect({.x = origin.x,.y=origin.y,.w=(short)line_width,.h=(short)string_height}, z_index, &&(Style){.bg=0xff000042u.@to_rgba()})!;
		String line = text[line_start:line_end-line_start];
		off = 0;
		for (usz x; (cp = str_to_codepoint(line[off..], &x)) != 0 && off < line.len; off += x) {
			Glyph* gp = font.get_glyph(cp)!;

			// update the text bounds
			switch {
			case cp == '\t':
				origin.x += tab_width;
			case ascii::is_cntrl((char)cp):
				break;
			default:
				Rect b = {
					.x = origin.x + gp.ox,
					.y = origin.y + gp.oy + baseline,
					.w = gp.w,
					.h = gp.h
				};
				Rect uv = {
					.x = gp.u,
					.y = gp.v,
					.w = gp.w,
					.h = gp.h
				};
				ctx.push_sprite(b, uv, texture_id, z_index, hue)!;
				//ctx.push_rect(b, z_index, &&(Style){.bg=0x0000ff66u.@to_rgba()})!;
				origin.x += gp.adv;
			}
			
		}
		// done with the line
		line_start = line_end;
		origin.y += line_height + line_gap; 
	} while(line_end < text.len);
	
	ctx.reset_scissor(z_index)!;
}
