module ugui;

import std::collections::map;
import std::core::mem::allocator;
import std::io;

// global style, similar to the css box model
struct Style { // css box model
	Rect  padding;
	Rect  border;
	Rect  margin;

	Color bg;        // background color
	Color fg;        // foreground color
	Color primary;   // primary color
	Color secondary; // secondary color
	Color accent;    // accent color

	ushort radius;
}

const Style DEFAULT_STYLE = {
	.margin  = {2, 2, 2, 2},
	.border  = {2, 2, 2, 2},
	.padding = {1, 1, 1, 1},
	.radius  = 12,
	
	.bg = 0x282828ffu.@to_rgba(),
	.fg = 0xfbf1c7ffu.@to_rgba(),
	.primary = 0xcc241dffu.@to_rgba(),
	.secondary = 0x458588ffu.@to_rgba(),
	.accent = 0xfabd2fffu.@to_rgba(),
};

// style is stored in a hashmap, each style has an Id that can be generated by a string or whatever
alias StyleMap = map::HashMap{Id, Style};

// push or update a new style into the map
fn void StyleMap.register_style(&map, Style* style, Id id)
{
	if (style == null) return;
	map.set(id, *style);
}

// get a style from the map, if the style is not found then use a default style.
fn Style* StyleMap.get_style(&map, Id id)
{
	Style*? s = map.get_ref(id);
	if (catch e = s) {
		return &DEFAULT_STYLE;
	}
	return s;
}

fn int StyleMap.import_style_string(&map, String text)
{
	Parser p;
	p.lex.text = text;
	int added;
	
	while (p.parse_style() == true) {
		added++;
		// set the default style correctly
		if (p.style_id == @str_hash("default")) p.style_id = 0;
		map.register_style(&p.style, p.style_id);
		if (p.lex.peep_token().type == EOF) break;
	}

	return added;
}

fn int Ctx.import_style_from_string(&ctx, String text) => ctx.styles.import_style_string(text);
fn int Ctx.import_style_from_file(&ctx, String path)
{
	char[] text;
	usz size = file::get_size(path)!!;
	text = mem::new_array(char, size);
	file::load_buffer(path, text)!!;
	defer mem::free(text);
	
	int added = ctx.import_style_from_string((String)text);
	return added;
}


// TODO: add a "size" property that controls the size of elements like checkboxes
/*
 * Style can be serialized and deserialized with a subset of CSS
 * <style name> {
 *     padding: left right top bottom;
 *     border: left right top bottom;
 *     margin: left right top bottoms;
 *     radius: uint;
 *     Color: #RRGGBBAA;
 *     Color: #RRGGBBAA;
 *     Color: #RRGGBBAA;
 *     Color: #RRGGBBAA;
 *     Color: #RRGGBBAA;
 * }
 * The field "style name" will be hashed and the hash used as the id int the style map.
 * Fields may be excluded, each excluded field is set to zero.
 * The default unit is pixels, but millimeters is also available. The parser function accepts a scale
 * factor that has to be obtained with the window manager functions. 
*/

module ugui::css;

import std::ascii;
import std::io;

// CSS parser module

enum TokenType {
	INVALID,
	IDENTIFIER,
	PUNCT,
	NUMBER,
	COLOR,
	EOF,
}

enum Unit {
	PIXELS,
	MILLIMETERS
}

struct Token {
	TokenType type;
	usz line, col, off;
	String text;
	union {
		struct {
			float value;
			Unit unit;
		}
		Color color;
	}
}

fn short Token.to_px(&t, float mm_to_px)
{
	if (t.type != NUMBER) {
		unreachable("WFT you cannot convert to pixels a non-number");
	}
	if (t.unit == PIXELS) return (short)(t.value);
	return (short)(t.value * mm_to_px);
}

struct Lexer {
	String text;
	usz line, col, off;
}

macro char Lexer.peep(&lex) => lex.text[lex.off];

fn char Lexer.advance(&lex)
{
	if (lex.off >= lex.text.len) return '\0';

	char c = lex.text[lex.off];
	if (c == '\n') {
		lex.col = 0;
		lex.line++;
	} else {
		lex.col++;
	}
	lex.off++;
	return c;
}

fn Token Lexer.next_token(&lex)
{
	Token t;
	t.type = INVALID;
	t.off = lex.off;
	t.col = lex.col;
	t.line = lex.line;

	if (lex.off >= lex.text.len) {
		return {.type = EOF};
	}

	// skip whitespace
	while (ascii::is_space_m(lex.peep())) {
		if (lex.advance() == 0) return {.type = EOF};
		if (lex.off >= lex.text.len) return {.type = EOF}; 
	}
	t.off = lex.off;

	switch (true) {
	case ascii::is_punct_m(lex.peep()) && lex.peep() != '#': // punctuation
		t.type = PUNCT;
		t.text = lex.text[lex.off:1];
		if (lex.advance() == 0) { t.type = INVALID; break; }
	
	case lex.peep() == '#': // color
		t.type = COLOR;
		if (lex.advance() == 0) { t.type = INVALID; break; }
		usz hex_start = t.off+1;
		while (ascii::is_alnum_m(lex.peep())) {
			if (lex.advance() == 0) { t.type = INVALID; break; }
		}
		if (lex.off - hex_start != 8) {
			io::eprintfn("CSS lexing error at %d:%d: the only suppported color format is #RRGGBBAA", t.line, t.col);
			t.type = INVALID;
			break;
		}
		
		char[10] hex_str = (char[])"0x";
		hex_str[2..] = lex.text[hex_start..lex.off-1];
		uint? color_hex = ((String)hex_str[..]).to_uint();
		if (catch color_hex) {
			t.type = INVALID;
			break;
		}
		t.color = color_hex.to_rgba();
	
	case ascii::is_alpha_m(lex.peep()): // identifier
		t.type = IDENTIFIER;
		while (ascii::is_alnum_m(lex.peep()) || lex.peep() == '-' || lex.peep() == '_') {
			if (lex.advance() == 0) { t.type = INVALID; break; }
		}
		t.text = lex.text[t.off..lex.off-1];
	
	case ascii::is_digit_m(lex.peep()): // number
		t.type = NUMBER;
		t.unit = PIXELS;
		// find the end of the number
		usz end;
		while (ascii::is_alnum_m(lex.peep()) || lex.peep() == '+' || lex.peep() == '-' || lex.peep() == '.') {
			if (lex.advance() == 0) { t.type = INVALID; break; } 
		}
		end = lex.off;
		if (end - t.off > 2) {
			if (lex.text[end-2:2] == "px") {
				t.unit = PIXELS;
				end -= 2;
			} else if (lex.text[end-2:2] == "mm") {
				t.unit = MILLIMETERS;
				end -= 2;
			} else if (lex.text[end-2:2] == "pt" || lex.text[end-2:2] == "em") {
				io::eprintn("units 'em' or 'pt' are not supported at the moment");
				t.type = INVALID;
				break;
			}
		}
		String number_str = lex.text[t.off..end-1];
		float? value = number_str.to_float();
		if (catch value) { t.type = INVALID; break; }
		t.value = value;
		t.text = lex.text[t.off..lex.off-1];
	}
	
	if (t.type == INVALID) {
		io::eprintfn("CSS Lexing ERROR at %d:%d: '%s' is not a valid token", t.line, t.col, lex.text[t.off..lex.off]);
	}
	return t;
}

fn Token Lexer.peep_token(&lex)
{
	Lexer start_state = *lex;
	Token t;
	t = lex.next_token();
	*lex = start_state;
	return t;
}


struct Parser {
	Lexer lex;
	Style style;
	Id style_id;
	float mm_to_px;
}

macro bool Parser.expect_text(&p, Token* t, TokenType type, String text)
{
	*t = p.lex.next_token();
	if (t.type == type && t.text == text) {
		return true;
	}
	io::eprintfn("CSS parsing error at %d:%d: expected type:%s text:'%s' but got type:%s text:'%s'",
	    t.line, t.col, type, text, t.type, t.text);
	return false;
}

macro bool Parser.expect(&p, Token* t, TokenType type)
{
	*t = p.lex.next_token();
	if (t.type == type) return true; 
	io::eprintfn("CSS parsing error at %d:%d: expected %s but got %s", t.line, t.col, type, t.type);
	return false;
}

fn bool Parser.parse_style(&p)
{
	Token t;
	p.style = {};
	p.style_id = 0;
	
	// style name
	if (p.expect(&t, IDENTIFIER) == false) return false;
	p.style_id = t.text.hash();

	// style body
	if (p.expect_text(&t, PUNCT, "{") == false) return false;
	
	while (true) {
		if (p.parse_property() == false) return false;
		t = p.lex.peep_token();
		if (t.type != IDENTIFIER) break;
	}
	
	if (p.expect_text(&t, PUNCT, "}") == false) return false;
	
	return true;
}

fn bool Parser.parse_property(&p)
{
	Token t, prop;
	if (p.expect(&prop, IDENTIFIER) == false) return false;
	if (p.expect_text(&t, PUNCT, ":") == false) return false;

	switch (prop.text) {
	case "padding":
		Rect padding;
		if (p.parse_size(&padding) == false) return false;
		p.style.padding = padding;

	case "border":
		Rect border;
		if (p.parse_size(&border) == false) return false;
		p.style.border = border;

	case "margin":
		Rect margin;
		if (p.parse_size(&margin) == false) return false;
		p.style.margin = margin;

	case "bg":
		Color bg;
		if (p.parse_color(&bg) == false) return false;
		p.style.bg = bg;

	case "fg":
		Color fg;
		if (p.parse_color(&fg) == false) return false;
		p.style.fg = fg;

	case "primary":
		Color primary;
		if (p.parse_color(&primary) == false) return false;
		p.style.primary = primary;

	case "secondary":
		Color secondary;
		if (p.parse_color(&secondary) == false) return false;
		p.style.secondary = secondary;

	case "accent":
		Color accent;
		if (p.parse_color(&accent) == false) return false;
		p.style.accent = accent;

	case "radius":
		short r;
		if (p.parse_number(&r) == false) return false;
		if (r < 0) {
			io::eprintfn("CSS parsing error at %d:%d: radius must be a positive number, got %d", t.line, t.col, r);
			return false;
		}
		p.style.radius = (ushort)r;

	default:
		io::eprintfn("CSS parsing error at %d:%d: '%s' is not a valid property", prop.line, prop.col, prop.text);
		return false;
	}
	if (p.expect_text(&t, PUNCT, ";") == false) return false;
	return true;
}

fn bool Parser.parse_number(&p, short* n)
{
	Token t;
	if (p.expect(&t, NUMBER) == false) return false;
	*n = t.to_px(p.mm_to_px);
	return true;
}

// FIXME: since '#' is punctuation this cannot be done in parsing but it has to be done in lexing
fn bool Parser.parse_color(&p, Color* c)
{
	Token t;
	if (p.expect(&t, COLOR) == false) return false;
	*c = t.color;
	return true;
}

fn bool Parser.parse_size(&p, Rect* r)
{
	short x;
	Token t;
	
	if (p.parse_number(&x) == false) return false;
	
	t = p.lex.peep_token();
	if (t.type == NUMBER) {
		// we got another number so we expect three more
		r.x = x;
		if (p.parse_number(&x) == false) return false;
		r.y = x;
		if (p.parse_number(&x) == false) return false;
		r.w = x;
		if (p.parse_number(&x) == false) return false;
		r.h = x;
		return true;
	} else if (t.type == PUNCT && t.text == ";") {
		// just one number, all dimensions are the same
		r.x = r.y = r.w = r.h = x;
		return true;
	}

	return false;
}
