module ugui;

import std::collections::list;
import std::core::string;

struct LineInfo @local {
	usz start, end;
	short width, height;
	short first_off; // first character offset
}

macro usz LineInfo.len(li) => li.end-li.start;

alias LineStack @local = list::List{LineInfo};

fn short Rect.y_off(Rect bounds, short height, Anchor anchor) @local
{
	short off;

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

fn short Rect.x_off(Rect bounds, short width, Anchor anchor) @local
{
	short off;

	switch (anchor) {
	case TOP_LEFT: nextcase;
	case LEFT: nextcase;
	case BOTTOM_LEFT:
		off = 0;
	case TOP: nextcase;
	case CENTER: nextcase;
	case BOTTOM:
		off = (short)(bounds.w - width)/2;
	case TOP_RIGHT: nextcase;
	case RIGHT: nextcase;
	case BOTTOM_RIGHT:
		off = (short)(bounds.w - width);
	}
	
	return off;
}


// ---------------------------------------------------------------------------------- //
//                                STRING LAYOUT                                       //
// ---------------------------------------------------------------------------------- //


struct GlyphIterator {
	short baseline;
	short line_height;
	short line_gap;
	short space_width;
	short tab_width;
	Rect bounds;
	Anchor anchor;
	bool reflow;
	Font* font;

	LineStack lines;
	usz line_off, line_idx;
	String text;

	Codepoint cp;
	Glyph* gp;
	Point o;
	Rect str_bounds;
}

<*
@param [&inout] self
@param [in] text
@param [&inout] font
*>
fn void? GlyphIterator.init(&self, Allocator allocator, String text, Rect bounds, Font* font, Anchor anchor, bool reflow, uint tab_size)
{
	self.font = font;

	self.line_height = (short)font.line_height();
	self.baseline = (short)font.ascender;
	self.line_gap = (short)font.linegap;
	self.space_width = font.get_glyph(' ').adv!;
	self.tab_width = self.space_width * (short)tab_size;

	self.bounds = bounds;
	self.o = bounds.position();
	self.reflow = reflow;
	self.text = text;
	self.anchor = anchor;

	// if the anchor is top_left we can skip dividing the string line by line, in GlyphIterator.next
	// this has to be accounted for
	if (anchor != TOP_LEFT) {
		self.lines.init(allocator, 4);
		self.populate_lines_stack()!;
		self.line_off = 0;
		self.line_idx = 0;
	
		if (self.lines.len() > 0) {
			self.o.y += bounds.y_off(self.str_bounds.h, anchor);
			self.o.x += bounds.x_off(self.lines[0].width, anchor) - self.lines[0].first_off;
		}
	}
}


fn void? GlyphIterator.populate_lines_stack(&self)
{
	usz line_start;
	LineInfo li;
	Point o = self.o;
	StringIterator ti = self.text.iterator();

	usz prev_off;
	for (Codepoint cp; ti.has_next();) {
		cp = ti.next()!;
		usz off = ti.current;
		bool push = false;

		li.height = self.line_height;
		switch {
		case cp == '\n':
			push = true;
		case cp == '\t':
			o.x += self.tab_width;
		case ascii::is_cntrl((char)cp):
			break;
		default:
			Glyph* gp = self.font.get_glyph(cp)!;

			if (off == line_start) {
				li.first_off = gp.ox;
				o.x -= gp.ox;
			}

			Rect b = gp.bounds().off(o);
			b.y += self.baseline;
			
			if (self.reflow && b.x + b.w > self.bounds.x + self.bounds.w) {
				push = true;
				// roll back this character since it is on the next line
				ti.current = prev_off;
				off = prev_off;
			} else {
				o.x += gp.adv;
				li.width += gp.adv;
			}
		}
		
		if (push) {
			li.start = line_start;
			li.end = off;
			self.lines.push(li);
			self.str_bounds.w = max(self.str_bounds.w, li.width);
			self.str_bounds.h += li.height;

			o.x = self.bounds.x;
			o.y += self.line_height;
			line_start = off;

			li.height = 0;
			li.width = 0;
		}
		
		prev_off = off;
	}
	if (line_start != ti.current) {
		// FIXME: crap, can we not repeat this code?
		li.start = line_start;
		li.end = ti.current;
		self.lines.push(li);
		self.str_bounds.w = max(self.str_bounds.w, li.width);
		self.str_bounds.h += li.height;
	}
	
	self.str_bounds.h += (short)(self.lines.len()-1) * self.line_gap;
}

fn String GlyphIterator.current_line(&self)
{
	LineInfo li = self.lines[self.line_idx];
	return self.text[li.start:li.len()];
}


fn Rect? GlyphIterator.next(&self)
{
	// check if there is a next glyph and maybe update the line and offset indices
	if (self.anchor != TOP_LEFT) {
		if (self.line_idx >= self.lines.len()) {
			return NO_MORE_ELEMENT?;
		}
	
		LineInfo li = self.lines[self.line_idx];
		if (self.line_off >= li.len()) {
			self.line_idx++;
			
			if (self.line_idx >= self.lines.len()) {
				return NO_MORE_ELEMENT?;
			}
			
			self.line_off = 0;
			li = self.lines[self.line_idx];
	
			self.o.y += self.line_height + self.line_gap;
			self.o.x = self.bounds.x + self.bounds.x_off(li.width, self.anchor) - li.first_off;
		}
	} else if (self.line_off >= self.text.len) {
		return NO_MORE_ELEMENT?;
	}

	String t;
	if (self.anchor != TOP_LEFT) {
		t = self.current_line()[self.line_off..];
	} else {
		t = self.text[self.line_off..];
	}
	
	usz read = t.len < 4 ? t.len : 4;
	self.cp = conv::utf8_to_char32(&t[0], &read)!;
	self.line_off += read;
	self.gp = self.font.get_glyph(self.cp)!;
	
	Rect b = {.x = self.o.x, .y = self.o.y};

	switch {
	case self.cp == '\n':
		if (self.anchor == TOP_LEFT) {
			self.o.x = self.bounds.x;
			self.o.y += self.line_height + self.line_gap;
			self.line_idx++;
		}
		break;
	case self.cp == '\t':
		self.o.x += self.tab_width;
	case ascii::is_cntrl((char)self.cp):
		break;
	default:
		b = self.gp.bounds().off(self.o);
		b.y += self.baseline;
		if (self.anchor == TOP_LEFT) {
			//if (self.o.x == self.bounds.x) self.bounds.x -= self.gp.ox;
			if (self.reflow && b.bottom_right().x > self.bounds.bottom_right().x) {
				self.o.x = self.bounds.x - self.gp.ox;
				self.o.y += self.line_height + self.line_gap;
				self.line_idx++;
				b = self.gp.bounds().off(self.o);
				b.y += self.baseline;
			}
		}
		self.o.x += self.gp.adv;
	}

	return b;
}

fn bool GlyphIterator.has_next(&self)
{
	if (self.anchor == TOP_LEFT) {
		return self.line_off < self.text.len;
	}

	if (self.line_idx >= self.lines.len()) {
		return false;
	}
	
	LineInfo li = self.lines[self.line_idx];
	if (self.line_idx == self.lines.len() - 1 && self.line_off >= li.len()) {
		return false;
	}

	return true;
}

fn usz GlyphIterator.current_offset(&self)
{
	if (self.anchor == TOP_LEFT) return self.line_off;
	return self.lines[self.line_idx].start + self.line_off;
}


// layout a string inside a bounding box, following the given alignment (anchor).
<*
@param [&in] ctx
@param [in] text
*>
fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue, bool reflow = false)
{
	if (text == "") return;
	if (bounds.w <= 0 || bounds.h <= 0) return;
	ctx.push_scissor(bounds, z_index)!;

	Font* font = &ctx.font;
	Id texture_id = font.id;
	
	GlyphIterator gi;
	gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
	while (gi.has_next()) {
		Rect b = gi.next()!;
		Rect uv = gi.gp.uv();
		ctx.push_sprite(b, uv, texture_id, z_index, hue)!;
	}

	ctx.reset_scissor(z_index)!;
//	ctx.dbg_rect(str_bounds.off(bounds.position()));
}


// ---------------------------------------------------------------------------------- //
//                              CURSOR AND MOUSE                                      //
// ---------------------------------------------------------------------------------- //


fn Rect? Ctx.get_cursor_position(&ctx, String text, Rect bounds, Anchor anchor, usz cursor, bool reflow = false)
{
	if (bounds.w <= 0 || bounds.h <= 0) return {};

	Font* font = &ctx.font;
	Id texture_id = font.id;
	
	if (text == "") text = "\f";

	GlyphIterator gi;
	gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
	Rect cursor_rect;
	cursor_rect.x = gi.o.x;
	cursor_rect.y = gi.o.y;
	cursor_rect.h = (short)font.line_height();
	if (cursor == 0) return cursor_rect;

	while (gi.has_next()) {
		Rect b = gi.next()!;
		if (gi.current_offset() == cursor) {
			if (gi.cp == '\n') {
				if (!gi.has_next()) {
					cursor_rect.x = bounds.x + bounds.x_off(0, anchor);
					cursor_rect.y = b.y + gi.line_height + gi.line_gap;
				} else {
					gi.next()!;
					cursor_rect.x = gi.o.x - gi.gp.adv;
					cursor_rect.y = gi.o.y;
				}
			} else {
				// Use the updated origin position instead of glyph bounds
				cursor_rect.x = gi.o.x;
				cursor_rect.y = gi.o.y;
			}
			return cursor_rect;
		}
	}

	return {};
}

<*
@param [&in] ctx
@param [in] text
*>
fn usz? Ctx.hit_test_string(&ctx, String text, Rect bounds, Anchor anchor, Point p, bool reflow = false)
{
	if (text == "") return 0;
	if (bounds.w <= 0 || bounds.h <= 0) return 0;

	Font* font = &ctx.font;
	
	GlyphIterator gi;
	gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
	
	usz prev_offset = 0;
	Point prev_o = gi.o;
	
	while (gi.has_next()) {
		Point o_before = gi.o;
		usz offset_before = gi.current_offset();
		Rect b = gi.next()!;
		
		switch {
		case gi.cp == '\n':
			// Check if point is on this line before the newline
			Rect line_rect = {
				.x = prev_o.x,
				.y = prev_o.y,
				.w = (short)(o_before.x - prev_o.x),
				.h = gi.line_height
			};
			if (p.in_rect(line_rect)) return offset_before;
			prev_o = gi.o;
			break;
		case gi.cp == '\t':
			// Check if point is in the tab space
			Rect tab_rect = {
				.x = o_before.x,
				.y = o_before.y,
				.w = gi.tab_width,
				.h = gi.line_height
			};
			if (p.in_rect(tab_rect)) return offset_before;
			break;
		case ascii::is_cntrl((char)gi.cp):
			break;
		default:
			// Create a hit test rect for this character
			Rect hit_rect = {
				.x = o_before.x,
				.y = o_before.y,
				.w = gi.gp.adv,
				.h = gi.line_height
			};

			if (p.in_rect(hit_rect)) {
				// Check if cursor should be before or after this character
				// by checking which half of the character was clicked
				short mid_x = o_before.x + gi.gp.adv / 2;
				if (p.x < mid_x) {
					return offset_before;
				} else {
					return gi.current_offset();
				}
			}
		}
		
		prev_offset = gi.current_offset();
	}
	
	// Point is after all text
	return text.len;
}

// TODO: implement a function `layout_string_with_selection` to avoid iterating over the string twice
fn void? Ctx.draw_string_selection(&ctx, String text, Rect bounds, Anchor anchor, usz start, usz end, int z_index, Color hue, bool reflow = false)
{
	if (text == "") return;
	if (bounds.w <= 0 || bounds.h <= 0) return;
	
	if (start > end) @swap(start, end);
	
	// Ensure start < end
	if (start > end) {
		usz temp = start;
		start = end;
		end = temp;
	}
	
	ctx.push_scissor(bounds, z_index)!;

	Font* font = &ctx.font;
	
	GlyphIterator gi;
	gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;

	Rect sel_rect = { .h = gi.line_height }; // selection rect
	isz sel_line = -1; // selection line
	while (gi.has_next()) {
		Rect b = gi.next()!;
		usz off = gi.current_offset()-1;
		isz line = gi.line_idx;
		bool in_selection = start <= off && off <= end;

		if  (in_selection && line != sel_line) {
			if (sel_line != -1) {
				ctx.push_rect(sel_rect, z_index, &&{.bg = hue})!;
			}
			sel_rect = {.x = gi.o.x - b.w, .y = gi.o.y, .w = 0, .h = gi.line_height};
			sel_line = line;
		}
		if (in_selection) {
			sel_rect.w = gi.o.x - sel_rect.x;
			if (gi.cp == '\n') sel_rect.w += gi.space_width;
		}
		
		if (off > end) break;
	}
	ctx.push_rect(sel_rect, z_index, &&{.bg = hue})!;
	
	ctx.reset_scissor(z_index)!;
}


// ---------------------------------------------------------------------------------- //
//                              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
<*
@param [&in] ctx
@param [in] text
*>
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;

	StringIterator it = text.iterator();
	for (Codepoint cp; it.has_next();) {
		cp = it.next()!;
		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 = gp.bounds().off(origin);
			b.y += baseline;
			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;
}


