/*
 * This software is licensed under the terms of the MIT License.
 * See COPYING for further information.
 * ---
 * Copyright (c) 2011-2024, Lukas Weber <laochailan@web.de>.
 * Copyright (c) 2012-2024, Andrei Alexeyev <akari@taisei-project.org>.
 */

#include "font.h"

#include "config.h"
#include "dynarray.h"
#include "events.h"
#include "memory/arena.h"
#include "renderer/api.h"
#include "util.h"
#include "util/glm.h"
#include "util/kvparser.h"
#include "util/rectpack.h"
#include "video.h"

#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_STROKER_H
#include FT_MODULE_H

static void init_fonts(void);
static void post_init_fonts(void);
static void shutdown_fonts(void);
static char *font_path(const char*);
static bool check_font_path(const char*);
static void load_font(ResourceLoadState *st);
static void unload_font(void*);
static bool transfer_font(void*, void*);

ResourceHandler font_res_handler = {
	.type = RES_FONT,
	.typename = "font",
	.subdir = FONT_PATH_PREFIX,

	.procs = {
		.init = init_fonts,
		.post_init = post_init_fonts,
		.shutdown = shutdown_fonts,
		.find = font_path,
		.check = check_font_path,
		.load = load_font,
		.unload = unload_font,
		.transfer = transfer_font,
	},
};

#undef FTERRORS_H_
#define FT_ERRORDEF(e, v, s)  { e, s },
#undef FT_ERROR_START_LIST
#undef FT_ERROR_END_LIST

#define FIXED_CONVERSION(ft_typename, func_typename, factor) \
	attr_unused INLINE float func_typename##_to_float(ft_typename x) { \
		return x * (1.0f / factor); \
	} \
	\
	attr_unused INLINE ft_typename float_to_##func_typename(float x) { \
		return roundf(x * factor); \
	}

FIXED_CONVERSION(FT_F26Dot6, f26dot6, 64.0f)
FIXED_CONVERSION(FT_Fixed, f16dot16, 65536.0f)

#define GLOBAL_RENDER_MODE FT_RENDER_MODE_NORMAL

static const struct ft_error_def {
	FT_Error    err_code;
	const char *err_msg;
} ft_errors[] = {
	#include FT_ERRORS_H
	{ 0, NULL }
};

static const char *ft_error_str(FT_Error err_code) {
	for(const struct ft_error_def *e = ft_errors; e->err_msg; ++e) {
		if(e->err_code == err_code) {
			return e->err_msg;
		}
	}

	return "Unknown error";
}

typedef struct SpriteSheet {
	LIST_INTERFACE(struct SpriteSheet);
	Texture *tex;
	RectPack rectpack;
	uint glyphs;
} SpriteSheet;

typedef LIST_ANCHOR(SpriteSheet) SpriteSheetAnchor;

typedef struct Glyph {
	Sprite sprite;
	SpriteSheet *spritesheet;
	RectPackSection *spritesheet_section;
	GlyphMetrics metrics;
	ulong ft_index;
} Glyph;

struct Font {
	char *source_path;
	DYNAMIC_ARRAY(Glyph) glyphs;
	FT_Face face;
	FT_Stroker stroker;
	long base_face_idx;
	int base_size;
	float base_border_inner;
	float base_border_outer;
	ht_int2int_t charcodes_to_glyph_ofs;
	ht_int2int_t ftindex_to_glyph_ofs;
	FontMetrics metrics;
	bool kerning;

#ifdef DEBUG
	char debug_label[64];
#endif
};

static struct {
	FT_Library lib;
	ShaderProgram *default_shader;
	Texture *render_tex;
	Framebuffer *render_buf;
	SpriteSheetAnchor spritesheets;
	MemArena arena;
	RectPackSectionPool rpspool;

	struct {
		SDL_mutex *new_face;
		SDL_mutex *done_face;
	} mutex;

	ResourceGroup rg;
} globals;

static float global_font_scale(void) {
	float w, h;
	video_get_viewport_size(&w, &h);
	return max(0.1f, (h / SCREEN_H) * config_get_float(CONFIG_TEXT_QUALITY));
}

static void reload_fonts(float quality);

static bool fonts_event(SDL_Event *event, void *arg) {
	if(!IS_TAISEI_EVENT(event->type)) {
		return false;
	}

	switch(TAISEI_EVENT(event->type)) {
		case TE_VIDEO_MODE_CHANGED: {
			reload_fonts(global_font_scale());
			break;
		}

		case TE_CONFIG_UPDATED: {
			if(event->user.code == CONFIG_TEXT_QUALITY) {
				ConfigValue *val = event->user.data1;
				val->f = max(0.1f, val->f);
				reload_fonts(global_font_scale());
			}

			return false;
		}
	}

	return false;
}

static void try_create_mutex(SDL_mutex **mtx) {
	if((*mtx = SDL_CreateMutex()) == NULL) {
		log_sdl_error(LOG_WARN, "SDL_CreateMutex");
	}
}

static void *ft_alloc(FT_Memory mem, long size) {
	return mem_alloc(size);
}

static void ft_free(FT_Memory mem, void *block) {
	mem_free(block);
}

static void *ft_realloc(FT_Memory mem, long cur_size, long new_size, void *block) {
	return mem_realloc(block, new_size);
}

static void init_fonts(void) {
	FT_Error err;

	marena_init(&globals.arena, sizeof(RectPackSection) * 128);

	static typeof(*(FT_Memory)0) ft_mem = {
		.alloc = ft_alloc,
		.free = ft_free,
		.realloc = ft_realloc,
	};

	try_create_mutex(&globals.mutex.new_face);
	try_create_mutex(&globals.mutex.done_face);

	if((err = FT_New_Library(&ft_mem, &globals.lib))) {
		log_fatal("FT_New_Library() failed: %s", ft_error_str(err));
	}

	FT_Add_Default_Modules(globals.lib);

	events_register_handler(&(EventHandler) {
		fonts_event, NULL, EPRIO_SYSTEM,
	});

	res_group_init(&globals.rg);

	res_group_preload(&globals.rg, RES_FONT, RESF_DEFAULT,
		"standard",
	NULL);

	// WARNING: Preloading the default shader here is unsafe.

	globals.render_tex = r_texture_create(&(TextureParams) {
		.filter = { TEX_FILTER_LINEAR, TEX_FILTER_LINEAR },
		.wrap   = { TEX_WRAP_CLAMP, TEX_WRAP_CLAMP },
		.type   = TEX_TYPE_RGBA,
		.flags  = TEX_FLAG_STREAM,
		.width  = 1024,
		.height = 1024,
	});

	#ifdef DEBUG
	char buf[128];
	snprintf(buf, sizeof(buf), "Font render texture");
	r_texture_set_debug_label(globals.render_tex, buf);
	#endif

	globals.render_buf = r_framebuffer_create();
	r_framebuffer_attach(globals.render_buf, globals.render_tex, 0, FRAMEBUFFER_ATTACH_COLOR0);
	r_framebuffer_viewport(globals.render_buf, 0, 0, 1024, 1024);
}

static void post_init_fonts(void) {
	res_group_preload(&globals.rg, RES_SHADER_PROGRAM, RESF_DEFAULT, "text_default", NULL);
	globals.default_shader = res_shader("text_default");
}

static void shutdown_fonts(void) {
	res_group_release(&globals.rg);
	res_purge();
	r_texture_destroy(globals.render_tex);
	r_framebuffer_destroy(globals.render_buf);
	events_unregister_handler(fonts_event);
	FT_Done_Library(globals.lib);
	SDL_DestroyMutex(globals.mutex.new_face);
	SDL_DestroyMutex(globals.mutex.done_face);
	marena_deinit(&globals.arena);
}

static char *font_path(const char *name) {
	return strjoin(FONT_PATH_PREFIX, name, FONT_EXTENSION, NULL);
}

bool check_font_path(const char *path) {
	return strstartswith(path, FONT_PATH_PREFIX) && strendswith(path, FONT_EXTENSION);
}

static ulong ftstream_read(FT_Stream stream, ulong offset, uchar *buffer, ulong count) {
	SDL_RWops *rwops = stream->descriptor.pointer;
	ulong error = count ? 0 : 1;

	if(SDL_RWseek(rwops, offset, RW_SEEK_SET) < 0) {
		log_error("Can't seek in stream (%s)", (char*)stream->pathname.pointer);
		return error;
	}

	return SDL_RWread(rwops, buffer, 1, count);
}

static void ftstream_close(FT_Stream stream) {
	SDL_RWclose((SDL_RWops*)stream->descriptor.pointer);
}

static FT_Error FT_Open_Face_Thread_Safe(FT_Library library, const FT_Open_Args *args, FT_Long face_index, FT_Face *aface) {
	FT_Error err;
	SDL_LockMutex(globals.mutex.new_face);
	err = FT_Open_Face(library, args, face_index, aface);
	SDL_UnlockMutex(globals.mutex.new_face);
	return err;
}

static FT_Error FT_Done_Face_Thread_Safe(FT_Face face) {
	FT_Error err;
	SDL_LockMutex(globals.mutex.done_face);
	err = FT_Done_Face(face);
	SDL_UnlockMutex(globals.mutex.done_face);
	return err;
}

static FT_Face load_font_face(char *vfspath, long index) {
	char *syspath = vfs_repr(vfspath, true);

	SDL_RWops *rwops = vfs_open(vfspath, VFS_MODE_READ | VFS_MODE_SEEKABLE);

	if(!rwops) {
		log_error("VFS error: %s", vfs_get_error());
		mem_free(syspath);
		return NULL;
	}

	FT_Stream ftstream = ALLOC(typeof(*ftstream), {
		.descriptor.pointer = rwops,
		.pathname.pointer = syspath,
		.read = ftstream_read,
		.close = ftstream_close,
		.size = SDL_RWsize(rwops),
	});

	FT_Open_Args ftargs = {
		.flags = FT_OPEN_STREAM,
		.stream = ftstream,
	};

	FT_Face face;
	FT_Error err;

	if((err = FT_Open_Face_Thread_Safe(globals.lib, &ftargs, index, &face))) {
		log_error("Failed to load font '%s' (face %li): FT_Open_Face() failed: %s", syspath, index, ft_error_str(err));
		mem_free(syspath);
		mem_free(ftstream);
		return NULL;
	}

	if(!(FT_IS_SCALABLE(face))) {
		log_error("Font '%s' (face %li) is not scalable. This is not supported, sorry. Load aborted", syspath, index);
		FT_Done_Face_Thread_Safe(face);
		mem_free(syspath);
		mem_free(ftstream);
		return NULL;
	}

	log_info("Loaded font '%s' (face %li)", syspath, index);
	return face;
}

static FT_Error set_font_size(Font *fnt, float scale) {
	FT_Error err = FT_Err_Ok;
	uint pxsize = fnt->base_size;

	assert(fnt != NULL);
	assert(fnt->face != NULL);

	pxsize = float_to_f26dot6(pxsize * scale);

	if((err = FT_Set_Char_Size(fnt->face, 0, pxsize, 0, 0))) {
		log_error("FT_Set_Char_Size(%u) failed: %s", pxsize, ft_error_str(err));
		return err;
	}

	if(fnt->stroker) {
		FT_Stroker_Done(fnt->stroker);
	}

	if((err = FT_Stroker_New(globals.lib, &fnt->stroker))) {
		log_error("FT_Stroker_New() failed: %s", ft_error_str(err));
		return err;
	}

	FT_Face face = fnt->face;
	FT_Fixed fixed_scale = face->size->metrics.y_scale;
	fnt->metrics.ascent = f26dot6_to_float(FT_MulFix(face->ascender, fixed_scale));
	fnt->metrics.descent = f26dot6_to_float(FT_MulFix(face->descender, fixed_scale));
	fnt->metrics.max_glyph_height = fnt->metrics.ascent - fnt->metrics.descent;
	fnt->metrics.lineskip = f26dot6_to_float(FT_MulFix(face->height, fixed_scale));
	fnt->metrics.scale = scale;

	return err;
}

bool font_get_kerning_available(Font *font) {
	return FT_HAS_KERNING(font->face);
}

bool font_get_kerning_enabled(Font *font) {
	return font->kerning;
}

void font_set_kerning_enabled(Font *font, bool newval) {
	font->kerning = (newval && FT_HAS_KERNING(font->face));
}

// TODO: Figure out sensible values for these; maybe make them depend on font size in some way.
#define SS_WIDTH 2048
#define SS_HEIGHT 2048

#define SS_TEXTURE_TYPE TEX_TYPE_RGB_8
#define SS_TEXTURE_FLAGS 0

INLINE RectPackSectionSource rps_source(void) {
	return (RectPackSectionSource) {
		.arena = &globals.arena,
		.pool = &globals.rpspool,
	};
}

static SpriteSheet *add_spritesheet(SpriteSheetAnchor *spritesheets) {
	auto ss = ALLOC(SpriteSheet, {
		.tex = r_texture_create(&(TextureParams) {
			.width = SS_WIDTH,
			.height = SS_HEIGHT,
			.type = SS_TEXTURE_TYPE,
			.flags = SS_TEXTURE_FLAGS,
			.filter.mag = TEX_FILTER_LINEAR,
			.filter.min = TEX_FILTER_LINEAR,
			.wrap.s = TEX_WRAP_CLAMP,
			.wrap.t = TEX_WRAP_CLAMP,
			.mipmaps = 1,
			.mipmap_mode = TEX_MIPMAP_MANUAL,
		})
	});

	rectpack_init(&ss->rectpack, SS_WIDTH, SS_HEIGHT);

#ifdef DEBUG
	char buf[128];
	snprintf(buf, sizeof(buf), "Fonts spritesheet %p", (void*)ss);
	r_texture_set_debug_label(ss->tex, buf);
#endif

	r_texture_clear(ss->tex, RGBA(0, 0, 0, 0));
	alist_append(spritesheets, ss);
	return ss;
}

// The padding is needed to prevent glyph edges from bleeding in due to linear filtering.
#define GLYPH_SPRITE_PADDING 1

static bool add_glyph_to_spritesheet(Glyph *glyph, Pixmap *pixmap, SpriteSheet *ss) {
	uint padded_w = pixmap->width + 2 * GLYPH_SPRITE_PADDING;
	uint padded_h = pixmap->height + 2 * GLYPH_SPRITE_PADDING;

	glyph->spritesheet_section = rectpack_add(&ss->rectpack, rps_source(), padded_w, padded_h, false);

	if(glyph->spritesheet_section == NULL) {
		return false;
	}

	glyph->spritesheet = ss;

	cmplx ofs = GLYPH_SPRITE_PADDING * (1+I);

	Rect sprite_pos = rectpack_section_rect(glyph->spritesheet_section);
	sprite_pos.bottom_right += ofs;
	sprite_pos.top_left += ofs;

	r_texture_fill_region(
		ss->tex,
		0, 0,
		rect_x(sprite_pos),
		rect_y(sprite_pos),
		pixmap
	);

	Sprite *sprite = &glyph->sprite;
	sprite->tex = ss->tex;
	sprite->w = pixmap->width;
	sprite->h = pixmap->height;

	sprite_set_denormalized_tex_coords(sprite, (FloatRect) {
		.x = rect_x(sprite_pos),
		.y = rect_y(sprite_pos),
		.w = pixmap->width,
		.h = pixmap->height,
	});

	++ss->glyphs;

	return true;
}

static bool add_glyph_to_spritesheets(Glyph *glyph, Pixmap *pixmap, SpriteSheetAnchor *spritesheets) {
	bool result;

	for(SpriteSheet *ss = spritesheets->first; ss; ss = ss->next) {
		if((result = add_glyph_to_spritesheet(glyph, pixmap, ss))) {
			return result;
		}
	}

	return add_glyph_to_spritesheet(glyph, pixmap, add_spritesheet(spritesheets));
}

static const char *pixmode_name(FT_Pixel_Mode mode) {
	switch(mode) {
		case FT_PIXEL_MODE_NONE   : return "FT_PIXEL_MODE_NONE";
		case FT_PIXEL_MODE_MONO   : return "FT_PIXEL_MODE_MONO";
		case FT_PIXEL_MODE_GRAY   : return "FT_PIXEL_MODE_GRAY";
		case FT_PIXEL_MODE_GRAY2  : return "FT_PIXEL_MODE_GRAY2";
		case FT_PIXEL_MODE_GRAY4  : return "FT_PIXEL_MODE_GRAY4";
		case FT_PIXEL_MODE_LCD    : return "FT_PIXEL_MODE_LCD";
		case FT_PIXEL_MODE_LCD_V  : return "FT_PIXEL_MODE_LCD_V";
		// case FT_PIXEL_MODE_RGBA: return "FT_PIXEL_MODE_RGBA";
		default                   : return "FT_PIXEL_MODE_UNKNOWN";
	}
}

static void delete_spritesheet(SpriteSheetAnchor *spritesheets, SpriteSheet *ss) {
	r_texture_destroy(ss->tex);
	assert(rectpack_is_empty(&ss->rectpack));
	alist_unlink(spritesheets, ss);
	mem_free(ss);
}

static Glyph *load_glyph(Font *font, FT_UInt gindex, SpriteSheetAnchor *spritesheets) {
	// log_debug("Loading glyph 0x%08x", gindex);

	FT_Render_Mode render_mode = GLOBAL_RENDER_MODE;
	FT_Error err = FT_Load_Glyph(font->face, gindex,
		FT_LOAD_NO_BITMAP |
		FT_LOAD_TARGET_(render_mode) |
	0);

	if(err) {
		log_warn("FT_Load_Glyph(%u) failed: %s", gindex, ft_error_str(err));
		return NULL;
	}

	Glyph *glyph = dynarray_append(&font->glyphs, {
		.metrics = {
			.bearing_x = f26dot6_to_float(font->face->glyph->metrics.horiBearingX),
			.bearing_y = f26dot6_to_float(font->face->glyph->metrics.horiBearingY),
			.width = f26dot6_to_float(font->face->glyph->metrics.width),
			.height = f26dot6_to_float(font->face->glyph->metrics.height),
			.advance = f26dot6_to_float(font->face->glyph->metrics.horiAdvance),
			.lsb_delta = f26dot6_to_float(font->face->glyph->lsb_delta),
			.rsb_delta = f26dot6_to_float(font->face->glyph->rsb_delta),
		}
	});

	FT_Glyph g_src = NULL, g_fill = NULL, g_border = NULL, g_inner = NULL;
	FT_BitmapGlyph g_bm_fill = NULL, g_bm_border = NULL, g_bm_inner = NULL;
	FT_Get_Glyph(font->face->glyph, &g_src);
	FT_Glyph_Copy(g_src, &g_fill);
	FT_Glyph_Copy(g_src, &g_border);
	FT_Glyph_Copy(g_src, &g_inner);

	assert(g_src->format == FT_GLYPH_FORMAT_OUTLINE);

	bool have_bitmap = FT_Glyph_To_Bitmap(&g_fill, render_mode, NULL, true) == FT_Err_Ok;

	if(have_bitmap) {
		have_bitmap = ((FT_BitmapGlyph)g_fill)->bitmap.width > 0;
	}

	if(!have_bitmap) {
		// Some glyphs may be invisible, but we still need the metrics data for them (e.g. space)
		memset(&glyph->sprite, 0, sizeof(Sprite));
	} else {
		FT_Stroker_Set(font->stroker,
			FT_MulFix(
				float_to_f26dot6(font->base_border_outer),
				font->face->size->metrics.y_scale),
			FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0
		);

		FT_Glyph_StrokeBorder(&g_border, font->stroker, false, true);
		FT_Glyph_To_Bitmap(&g_border, GLOBAL_RENDER_MODE, NULL, true);

		FT_Stroker_Set(font->stroker,
			FT_MulFix(
				float_to_f26dot6(font->base_border_inner),
				font->face->size->metrics.y_scale),
			FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_BEVEL, 0
		);

		FT_Glyph_StrokeBorder(&g_inner, font->stroker, true, true);
		FT_Glyph_To_Bitmap(&g_inner, GLOBAL_RENDER_MODE, NULL, true);

		g_bm_fill = (FT_BitmapGlyph)g_fill;
		g_bm_border = (FT_BitmapGlyph)g_border;
		g_bm_inner = (FT_BitmapGlyph)g_inner;

		if(
			g_bm_fill->bitmap.pixel_mode != FT_PIXEL_MODE_GRAY ||
			g_bm_border->bitmap.pixel_mode != FT_PIXEL_MODE_GRAY ||
			g_bm_inner->bitmap.pixel_mode != FT_PIXEL_MODE_GRAY
		) {
			log_warn(
				"Glyph %u returned bitmap with pixel format %s. Only %s is supported, sorry. Ignoring",
				gindex,
				pixmode_name(g_bm_fill->bitmap.pixel_mode),
				pixmode_name(FT_PIXEL_MODE_GRAY)
			);

			FT_Done_Glyph(g_src);
			FT_Done_Glyph(g_fill);
			FT_Done_Glyph(g_border);
			FT_Done_Glyph(g_inner);
			font->glyphs.num_elements--;
			return NULL;
		}

		Pixmap px;
		px.origin = PIXMAP_ORIGIN_BOTTOMLEFT;
		px.format = PIXMAP_FORMAT_RGB8;
		px.width = max(g_bm_fill->bitmap.width, max(g_bm_border->bitmap.width, g_bm_inner->bitmap.width));
		px.height = max(g_bm_fill->bitmap.rows, max(g_bm_border->bitmap.rows, g_bm_inner->bitmap.rows));
		px.data.rg8 = pixmap_alloc_buffer_for_copy(&px, &px.data_size);

		int ref_left = g_bm_border->left;
		int ref_top = g_bm_border->top;

		ssize_t fill_ofs_x   =  (g_bm_fill->left   - ref_left);
		ssize_t fill_ofs_y   = -(g_bm_fill->top    - ref_top);
		ssize_t border_ofs_x =  (g_bm_border->left - ref_left);
		ssize_t border_ofs_y = -(g_bm_border->top  - ref_top);
		ssize_t inner_ofs_x  =  (g_bm_inner->left  - ref_left);
		ssize_t inner_ofs_y  = -(g_bm_inner->top   - ref_top);

		for(ssize_t x = 0; x < px.width; ++x) {
			for(ssize_t y = 0; y < px.height; ++y) {
				PixelRGB8 *p = px.data.rgb8 + (x + y * px.width);

				ssize_t fill_coord_x = x - fill_ofs_x;
				ssize_t fill_coord_y = y - fill_ofs_y;
				ssize_t border_coord_x = x - border_ofs_x;
				ssize_t border_coord_y = y - border_ofs_y;
				ssize_t inner_coord_x = x - inner_ofs_x;
				ssize_t inner_coord_y = y - inner_ofs_y;

				ssize_t fill_index   = fill_coord_x   + (g_bm_fill->bitmap.rows   - fill_coord_y   - 1) * g_bm_fill->bitmap.pitch;
				ssize_t border_index = border_coord_x + (g_bm_border->bitmap.rows - border_coord_y - 1) * g_bm_border->bitmap.pitch;
				ssize_t inner_index  = inner_coord_x  + (g_bm_inner->bitmap.rows  - inner_coord_y  - 1) * g_bm_inner->bitmap.pitch;

				if(
					fill_coord_x >= 0 && fill_coord_x < g_bm_fill->bitmap.width &&
					fill_coord_y >= 0 && fill_coord_y < g_bm_fill->bitmap.rows
				) {
					p->r = g_bm_fill->bitmap.buffer[fill_index];
				} else {
					p->r = 0;
				}

				if(
					border_coord_x >= 0 && border_coord_x < g_bm_border->bitmap.width &&
					border_coord_y >= 0 && border_coord_y < g_bm_border->bitmap.rows
				) {
					p->g = g_bm_border->bitmap.buffer[border_index];
				} else {
					p->g = 0;
				}

				if(
					inner_coord_x >= 0 && inner_coord_x < g_bm_inner->bitmap.width &&
					inner_coord_y >= 0 && inner_coord_y < g_bm_inner->bitmap.rows
				) {
					p->b = g_bm_inner->bitmap.buffer[inner_index];
				} else {
					p->b = 0;
				}
			}
		}

		TextureTypeQueryResult qr = { 0 };

		// TODO: Only query this once on init.
		if(r_texture_type_query(SS_TEXTURE_TYPE, SS_TEXTURE_FLAGS, px.format, px.origin, &qr)) {
			pixmap_convert_inplace_realloc(&px, qr.optimal_pixmap_format);
			pixmap_flip_to_origin_inplace(&px, qr.optimal_pixmap_origin);
		} else {
			log_error("Texture query failed!");
			assert(0);
		}

		if(!add_glyph_to_spritesheets(glyph, &px, spritesheets)) {
			log_error(
				"Glyph %u fill can't fit into any spritesheets (padded bitmap size: %ux%u; max spritesheet size: %ux%u)",
				gindex,
				px.width + 2 * GLYPH_SPRITE_PADDING,
				px.height + 2 * GLYPH_SPRITE_PADDING,
				SS_WIDTH,
				SS_HEIGHT
			);

			mem_free(px.data.rg8);
			FT_Done_Glyph(g_src);
			FT_Done_Glyph(g_fill);
			FT_Done_Glyph(g_border);
			FT_Done_Glyph(g_inner);
			--font->glyphs.num_elements;
			return NULL;
		}

		mem_free(px.data.rg8);

		float xpad = px.width - g_bm_fill->bitmap.width;
		float ypad = px.height - g_bm_fill->bitmap.rows;
		glyph->sprite.padding.extent.w = xpad;
		glyph->sprite.padding.extent.h = ypad;
		glyph->sprite.padding.offset.x = -xpad;
		glyph->sprite.padding.offset.y = -ypad;
		glyph->sprite.extent.as_cmplx += glyph->sprite.padding.extent.as_cmplx;
	}

	FT_Done_Glyph(g_src);
	FT_Done_Glyph(g_fill);
	FT_Done_Glyph(g_border);
	FT_Done_Glyph(g_inner);

	glyph->ft_index = gindex;
	return glyph;
}

static Glyph *get_glyph(Font *fnt, charcode_t cp) {
	int64_t ofs;

	if(!ht_lookup(&fnt->charcodes_to_glyph_ofs, cp, &ofs)) {
		Glyph *glyph;
		uint ft_index = FT_Get_Char_Index(fnt->face, cp);
		// log_debug("Glyph for charcode 0x%08lx not cached", cp);

		if(ft_index == 0 && cp != UNICODE_UNKNOWN) {
			log_debug("Font has no glyph for charcode 0x%08lx", cp);
			glyph = get_glyph(fnt, UNICODE_UNKNOWN);
			ofs = glyph ? dynarray_indexof(&fnt->glyphs, glyph) : -1;
		} else if(!ht_lookup(&fnt->ftindex_to_glyph_ofs, ft_index, &ofs)) {
			glyph = load_glyph(fnt, ft_index, &globals.spritesheets);
			ofs = glyph ? dynarray_indexof(&fnt->glyphs, glyph) : -1;
			ht_set(&fnt->ftindex_to_glyph_ofs, ft_index, ofs);
		}

		ht_set(&fnt->charcodes_to_glyph_ofs, cp, ofs);
	}

	return ofs < 0 ? NULL : dynarray_get_ptr(&fnt->glyphs, ofs);
}

attr_nonnull(1)
static void wipe_glyph_cache(Font *font) {
	dynarray_foreach_elem(&font->glyphs, Glyph *g, {
		SpriteSheet *ss = g->spritesheet;

		if(ss == NULL) {
			continue;
		}

		RectPack *rp = &ss->rectpack;
		RectPackSection *section = g->spritesheet_section;
		assume(section != NULL);

		rectpack_reclaim(rp, rps_source(), section);

		if(rectpack_is_empty(rp)) {
			delete_spritesheet(&globals.spritesheets, ss);
		}
	});

	ht_unset_all(&font->charcodes_to_glyph_ofs);
	ht_unset_all(&font->ftindex_to_glyph_ofs);

	font->glyphs.num_elements = 0;
}

static void free_font_resources(Font *font) {
	if(font->face) {
		FT_Stream stream = font->face->stream;
		FT_Done_Face_Thread_Safe(font->face);

		if(stream) {
			mem_free(stream->pathname.pointer);
			mem_free(stream);
		}
	}

	if(font->stroker) {
		FT_Stroker_Done(font->stroker);
	}

	wipe_glyph_cache(font);

	ht_destroy(&font->charcodes_to_glyph_ofs);
	ht_destroy(&font->ftindex_to_glyph_ofs);

	mem_free(font->source_path);
	dynarray_free_data(&font->glyphs);
}

static void finish_reload(ResourceLoadState *st);

void load_font(ResourceLoadState *st) {
	Font font = {
		.base_border_inner = 0.5f,
		.base_border_outer = 1.5f,
	};

	SDL_RWops *rw = res_open_file(st, st->path, VFS_MODE_READ);

	if(UNLIKELY(!rw)) {
		log_error("VFS error: %s", vfs_get_error());
		res_load_failed(st);
		return;
	}

	bool parsed = parse_keyvalue_stream_with_spec(rw, (KVSpec[]){
		{ "source",        .out_str   = &font.source_path },
		{ "size",          .out_int   = &font.base_size },
		{ "face",          .out_long  = &font.base_face_idx },
		{ "border_inner",  .out_float = &font.base_border_inner, },
		{ "border_outer",  .out_float = &font.base_border_outer, },
		{ NULL }
	});

	SDL_RWclose(rw);

	if(UNLIKELY(!parsed)) {
		log_error("Failed to parse font file '%s'", st->path);
		mem_free(font.source_path);
		res_load_failed(st);
		return;
	}

	if(!font.source_path) {
		log_error("%s: No source path specified", st->path);
		res_load_failed(st);
		return;
	}

	ht_create(&font.charcodes_to_glyph_ofs);
	ht_create(&font.ftindex_to_glyph_ofs);

	if(!(font.face = load_font_face(font.source_path, font.base_face_idx))) {
		free_font_resources(&font);
		res_load_failed(st);
	}

	if(set_font_size(&font, global_font_scale())) {
		free_font_resources(&font);
		res_load_failed(st);
		return;
	}

	dynarray_ensure_capacity(&font.glyphs, 32);

#ifdef DEBUG
	strlcpy(font.debug_label, st->name, sizeof(font.debug_label));
#endif

	font_set_kerning_enabled(&font, true);
	Font *newfont = memdup(&font, sizeof(font));

	if(st->flags & RESF_RELOAD) {
		// workaround to avoid data race (font in use on main thread)
		res_load_continue_on_main(st, finish_reload, newfont);
	} else {
		res_load_finished(st, newfont);
	}
}

static void finish_reload(ResourceLoadState *st) {
	res_load_finished(st, st->opaque);
}

void unload_font(void *vfont) {
	free_font_resources(vfont);
	mem_free(vfont);
}

struct rlfonts_arg {
	float quality;
};

attr_nonnull(1)
static void reload_font(Font *font, float quality) {
	if(font->metrics.scale != quality) {
		wipe_glyph_cache(font);
		set_font_size(font, quality);
	}
}

static void *reload_font_callback(const char *name, Resource *res, void *varg) {
	struct rlfonts_arg *a = varg;
	reload_font((Font*)res->data, a->quality);
	return NULL;
}

static void reload_fonts(float quality) {
	res_for_each(RES_FONT, reload_font_callback, &(struct rlfonts_arg) { quality });
}

static inline float apply_kerning(Font *font, uint prev_index, Glyph *gthis) {
	FT_Vector kvec;

	if(!font_get_kerning_enabled(font) || prev_index == 0) {
		return 0;
	}

	if(!FT_Get_Kerning(font->face, prev_index, gthis->ft_index, FT_KERNING_UNFITTED, &kvec)) {
		return f26dot6_to_float(kvec.x);
	}

	return 0;
}

static float apply_subpixel_shift(Glyph *g, float prev_rsb_delta) {
	float shift = 0;
	float diff = prev_rsb_delta - g->metrics.lsb_delta;

#if 0
	if(diff > 32 * FIXED_TO_FLOAT) {
		shift = -FIXED_TO_FLOAT;
	} else if(diff < -31 * FIXED_TO_FLOAT) {
		shift = FIXED_TO_FLOAT;
	}
#else
	shift -= diff;
#endif

	return shift;
}

typedef struct Cursor {
	Font *font;
	float x;
	float prev_rsb_delta;
	uint prev_glyph_idx;
} Cursor;

static Cursor cursor_init(Font *font) {
	return (Cursor) { .font = font };
}

static void cursor_reset(Cursor *c) {
	c->x = 0;
	c->prev_rsb_delta = 0;
	c->prev_glyph_idx = 0;
}

static float cursor_advance(Cursor *c, Glyph *g) {
	if(g == NULL) {
		return c->x;
	}

	c->x += apply_kerning(c->font, c->prev_glyph_idx, g);
	c->x += apply_subpixel_shift(g, c->prev_rsb_delta);

	float startpos = c->x;
	c->x += g->metrics.advance;

	c->prev_rsb_delta = g->metrics.rsb_delta;
	c->prev_glyph_idx = g->ft_index;

	return startpos;
}

float text_ucs4_width_raw(Font *font, const uint32_t *text, uint maxlines) {
	const uint32_t *tptr = text;
	uint numlines = 0;
	float width = 0;
	Cursor c = cursor_init(font);

	while(*tptr) {
		uint32_t uchar = *tptr++;

		if(uchar == '\n') {
			if(++numlines == maxlines) {
				break;
			}

			cursor_reset(&c);
			continue;
		}

		cursor_advance(&c, get_glyph(font, uchar));

		if(c.x > width) {
			width = c.x;
		}
	}

	return width;
}

float text_width_raw(Font *font, const char *text, uint maxlines) {
	uint32_t buf[strlen(text) + 1];
	utf8_to_ucs4(text, ARRAY_SIZE(buf), buf);
	return text_ucs4_width_raw(font, buf, maxlines);
}

void text_ucs4_bbox(Font *font, const uint32_t *text, uint maxlines, TextBBox *bbox) {
	const uint32_t *tptr = text;
	uint numlines = 0;

	memset(bbox, 0, sizeof(*bbox));
	float y = 0;
	Cursor c = cursor_init(font);

	while(*tptr) {
		uint32_t uchar = *tptr++;

		if(uchar == '\n') {
			if(++numlines == maxlines) {
				break;
			}

			y += font->metrics.lineskip;
			cursor_reset(&c);
			continue;
		}

		Glyph *glyph = get_glyph(font, uchar);

		if(glyph == NULL) {
			continue;
		}

		float x = cursor_advance(&c, glyph);
		bbox->x.max = max(bbox->x.max, c.x);

		FloatOffset ofs = glyph->sprite.padding.offset;

		float g_x0 = x + glyph->metrics.bearing_x + ofs.x;
		float g_x1 = g_x0 + max(glyph->metrics.width, glyph->sprite.w);

		bbox->x.max = max(bbox->x.max, g_x0);
		bbox->x.max = max(bbox->x.max, g_x1);
		bbox->x.min = min(bbox->x.min, g_x0);
		bbox->x.min = min(bbox->x.min, g_x1);

		float g_y0 = y - glyph->metrics.bearing_y + ofs.y;
		float g_y1 = g_y0 + max(glyph->metrics.height, glyph->sprite.h);

		bbox->y.max = max(bbox->y.max, g_y0);
		bbox->y.max = max(bbox->y.max, g_y1);
		bbox->y.min = min(bbox->y.min, g_y0);
		bbox->y.min = min(bbox->y.min, g_y1);
	}
}

void text_bbox(Font *font, const char *text, uint maxlines, TextBBox *bbox) {
	uint32_t buf[strlen(text) + 1];
	utf8_to_ucs4(text, ARRAY_SIZE(buf), buf);
	text_ucs4_bbox(font, buf, maxlines, bbox);
}

float text_ucs4_width(Font *font, const uint32_t *text, uint maxlines) {
	return text_ucs4_width_raw(font, text, maxlines) / font->metrics.scale;
}

float text_width(Font *font, const char *text, uint maxlines) {
	return text_width_raw(font, text, maxlines) / font->metrics.scale;
}

float text_ucs4_height_raw(Font *font, const uint32_t *text, uint maxlines) {
	// FIXME: I'm not sure this is correct. Perhaps it should consider max_glyph_height at least?

	uint text_lines = 1;
	const uint32_t *tptr = text;

	while((tptr = ucs4chr(tptr, '\n'))) {
		if(text_lines++ == maxlines) {
			break;
		}

		if(*tptr == '\n') {
			++tptr;
		}
	}

	return font->metrics.lineskip * text_lines;
}

float text_height_raw(Font *font, const char *text, uint maxlines) {
	uint32_t buf[strlen(text) + 1];
	utf8_to_ucs4(text, ARRAY_SIZE(buf), buf);
	return text_ucs4_height_raw(font, buf, maxlines);
}

float text_ucs4_height(Font *font, const uint32_t *text, uint maxlines) {
	return text_ucs4_height_raw(font, text, maxlines) / font->metrics.scale;
}

float text_height(Font *font, const char *text, uint maxlines) {
	return text_height_raw(font, text, maxlines) / font->metrics.scale;
}

static inline void adjust_xpos(
	Font *font, const uint32_t *ucs4text, Alignment align, float x_orig, float *x
) {
	float line_width;

	switch(align) {
		case ALIGN_LEFT: {
			*x = x_orig;
			break;
		}

		case ALIGN_RIGHT: {
			line_width = text_ucs4_width_raw(font, ucs4text, 1);
			*x = x_orig - line_width;
			break;
		}

		case ALIGN_CENTER: {
			line_width = text_ucs4_width_raw(font, ucs4text, 1);
			*x = x_orig - line_width * 0.5;
			break;
		}
	}
}

ShaderProgram *text_get_default_shader(void) {
	return globals.default_shader;
}

attr_returns_nonnull
static Font *font_from_params(const TextParams *params) {
	Font *font = params->font_ptr;

	if(font == NULL) {
		if(params->font != NULL) {
			font = res_font(params->font);
		} else {
			font = res_font("standard");
		}
	}

	assert(font != NULL);
	return font;
}

static void set_batch_texture(SpriteStateParams *stp, Texture *tex) {
	if(stp->primary_texture != tex) {
		stp->primary_texture = tex;
		r_sprite_batch_prepare_state(stp);
	}
}

attr_nonnull(1, 2, 3)
static float _text_ucs4_draw(Font *font, const uint32_t *ucs4text, const TextParams *params) {
	SpriteStateParams batch_state_params;

	memcpy(batch_state_params.aux_textures, params->aux_textures, sizeof(batch_state_params.aux_textures));

	if((batch_state_params.blend = params->blend) == 0) {
		batch_state_params.blend = r_blend_current();
	}

	if((batch_state_params.shader = params->shader_ptr) == NULL) {
		if(params->shader != NULL) {
			batch_state_params.shader = res_shader(params->shader);
		} else {
			batch_state_params.shader = r_shader_current();
		}
	}

	batch_state_params.primary_texture = NULL;

	TextBBox bbox;
	float y = params->pos.y;
	float scale = font->metrics.scale;
	float iscale = 1.0f / scale;

	Cursor c = cursor_init(font);
	c.x = params->pos.x;

	struct {
		struct { float min, max; } x, y;
		float w, h;
	} overlay;

	text_ucs4_bbox(font, ucs4text, 0, &bbox);

	Color color;

	if(params->color == NULL) {
		// XXX: sprite batch code defaults this to RGB(1, 1, 1)
		color = *r_color_current();
	} else {
		color = *params->color;
	}

	ShaderCustomParams shader_params;

	if(params->shader_params == NULL) {
		memset(&shader_params, 0, sizeof(shader_params));
	} else {
		shader_params = *params->shader_params;
	}

	mat4 mat_texture;
	mat4 mat_model;

	r_mat_tex_current(mat_texture);
	r_mat_mv_current(mat_model);

	glm_translate(mat_model, (vec3) { c.x, y } );
	glm_scale(mat_model, (vec3) { iscale, iscale, 1 } );

	float orig_x = c.x;
	float orig_y = y;
	c.x = y = 0;

	adjust_xpos(font, ucs4text, params->align, 0, &c.x);

	if(params->overlay_projection) {
		FloatRect *op = params->overlay_projection;
		overlay.x.min = (op->x - orig_x) * scale;
		overlay.x.max = overlay.x.min + op->w * scale;
		overlay.y.min = (op->y - orig_y) * scale;
		overlay.y.max = overlay.y.min + op->h * scale;
	} else {
		overlay.x.min = bbox.x.min + c.x;
		overlay.x.max = bbox.x.max + c.x;
		overlay.y.min = bbox.y.min - font->metrics.descent;
		overlay.y.max = bbox.y.max - font->metrics.descent;
	}

	overlay.w = overlay.x.max - overlay.x.min;
	overlay.h = overlay.y.max - overlay.y.min;

	glm_scale(mat_texture, (vec3) { 1/overlay.w, 1/overlay.h, 1.0 });
	glm_translate(mat_texture, (vec3) { -overlay.x.min, overlay.y.min, 0 });

	// FIXME: is there a better way?
	float texmat_offset_sign;

	if(r_supports(RFEAT_TEXTURE_BOTTOMLEFT_ORIGIN)) {
		texmat_offset_sign = -1;
	} else {
		texmat_offset_sign = 1;
	}

	const uint32_t *tptr = ucs4text;

	while(*tptr) {
		uint32_t uchar = *tptr++;

		if(uchar == '\n') {
			cursor_reset(&c);
			adjust_xpos(font, tptr, params->align, 0, &c.x);
			y += font->metrics.lineskip;
			continue;
		}

		Glyph *glyph = get_glyph(font, uchar);

		if(glyph == NULL) {
			continue;
		}

		float x = cursor_advance(&c, glyph);

		if(glyph->sprite.tex == NULL) {
			continue;
		}

		Sprite *spr = &glyph->sprite;
		set_batch_texture(&batch_state_params, spr->tex);

		SpriteInstanceAttribs attribs;
		attribs.rgba = color;
		attribs.custom = shader_params;

		FloatOffset ofs = spr->padding.offset;
		FloatExtent imgdims = spr->extent;
		imgdims.as_cmplx -= spr->padding.extent.as_cmplx;

		float g_x = x + glyph->metrics.bearing_x + spr->w * 0.5f + ofs.x;
		float g_y = y - glyph->metrics.bearing_y + spr->h * 0.5f - font->metrics.descent + ofs.y;

		glm_translate_to(mat_texture, (vec3) {
			g_x - imgdims.w * 0.5f,
			g_y * texmat_offset_sign + overlay.h - imgdims.h * 0.5f
		}, attribs.tex_transform);
		glm_scale(attribs.tex_transform, (vec3) { imgdims.w, imgdims.h, 1.0 });

		glm_translate_to(mat_model, (vec3) { g_x, g_y }, attribs.mv_transform);
		glm_scale(attribs.mv_transform, (vec3) { imgdims.w, imgdims.h, 1.0 } );

		attribs.texrect = spr->tex_area;

		// NOTE: Glyphs have their sprite w/h unadjusted for scale.
		attribs.sprite_size.w = imgdims.w * iscale;
		attribs.sprite_size.h = imgdims.h * iscale;

		if(params->glyph_callback.func != NULL) {
			params->glyph_callback.func(
				font, uchar, &attribs, params->glyph_callback.userdata);
		}

		r_sprite_batch_add_instance(&attribs);
	}

	return c.x * iscale;
}

static float _text_draw(Font *font, const char *text, const TextParams *params) {
	uint32_t buf[strlen(text) + 1];
	utf8_to_ucs4(text, ARRAY_SIZE(buf), buf);

	if(params->max_width > 0) {
		text_ucs4_shorten(font, buf, params->max_width);
	}

	return _text_ucs4_draw(font, buf, params);
}

float text_draw(const char *text, const TextParams *params) {
	return _text_draw(font_from_params(params), text, params);
}

float text_ucs4_draw(const uint32_t *text, const TextParams *params) {
	Font *font = font_from_params(params);

	if(params->max_width > 0) {
		uint32_t buf[ucs4len(text) + 1];
		memcpy(buf, text, sizeof(buf));
		text_ucs4_shorten(font, buf, params->max_width);
		return _text_ucs4_draw(font, buf, params);
	}

	return _text_ucs4_draw(font, text, params);
}

float text_draw_wrapped(const char *text, float max_width, const TextParams *params) {
	Font *font = font_from_params(params);
	char buf[strlen(text) * 2 + 1];
	text_wrap(font, text, max_width, buf, sizeof(buf));
	return _text_draw(font, buf, params);
}

void text_render(const char *text, Font *font, Sprite *out_sprite, TextBBox *out_bbox) {
	text_bbox(font, text, 0, out_bbox);

	float bbox_width = out_bbox->x.max - out_bbox->x.min;
	float bbox_height = out_bbox->y.max - out_bbox->y.min;

	if(bbox_height < font->metrics.max_glyph_height) {
		out_bbox->y.min -= font->metrics.max_glyph_height - bbox_height;
		bbox_height = out_bbox->y.max - out_bbox->y.min;
	}

	Texture *tex = globals.render_tex;

	uint tex_new_w = ceilf(bbox_width); // max(tex->w, bbox_width);
	uint tex_new_h = ceilf(bbox_height); // max(tex->h, bbox_height);
	uint tex_w, tex_h;
	r_texture_get_size(tex, 0, &tex_w, &tex_h);

	if(tex_new_w != tex_w || tex_new_h != tex_h) {
		log_info(
			"Resizing texture: %ix%i --> %ix%i",
			tex_w, tex_h,
			tex_new_w, tex_new_h
		);

		TextureParams params;
		r_texture_get_params(tex, &params);
		r_texture_destroy(tex);
		params.width = tex_new_w;
		params.height = tex_new_h;
		params.mipmaps = 0;
		globals.render_tex = tex = r_texture_create(&params);

		r_texture_set_debug_label(tex, "Font render texture");
		r_framebuffer_attach(globals.render_buf, tex, 0, FRAMEBUFFER_ATTACH_COLOR0);
		r_framebuffer_viewport(globals.render_buf, 0, 0, tex_new_w, tex_new_h);
	}

	r_state_push();

	r_framebuffer(globals.render_buf);
	r_clear(BUFFER_COLOR, RGBA(0, 0, 0, 0), 1);

	r_blend(BLEND_PREMUL_ALPHA);
	r_disable(RCAP_CULL_FACE);
	r_disable(RCAP_DEPTH_TEST);

	r_mat_mv_push_identity();
	r_mat_proj_push_ortho(tex_new_w, tex_new_h);
	r_mat_tex_push_identity();

	// HACK: Coordinates are in texel space, font scale must not be used.
	// This probably should be exposed in the text_draw API.
	float fontscale = font->metrics.scale;
	font->metrics.scale = 1;

	text_draw(text, &(TextParams) {
		.font_ptr = font,
		.pos = { -out_bbox->x.min, -out_bbox->y.min + font->metrics.descent },
		.color = RGB(1, 1, 1),
		.shader = "text_default",
	});

	font->metrics.scale = fontscale;
	r_flush_sprites();

	r_mat_tex_pop();
	r_mat_proj_pop();
	r_mat_mv_pop();

	r_state_pop();

	out_sprite->tex = tex;
	out_sprite->tex_area.w = bbox_width / tex_new_w;
	out_sprite->tex_area.h = bbox_height / tex_new_h;
	out_sprite->tex_area.x = 0;
	out_sprite->tex_area.y = 0;
	out_sprite->w = bbox_width / font->metrics.scale;
	out_sprite->h = bbox_height / font->metrics.scale;
}

void text_ucs4_shorten(Font *font, uint32_t *text, float width) {
	assert(!ucs4chr(text, '\n'));

	if(text_ucs4_width(font, text, 0) <= width) {
		return;
	}

	int l = ucs4len(text);

	do {
		if(l < 1) {
			return;
		}

		text[l] = 0;
		text[l - 1] = UNICODE_ELLIPSIS;
		--l;
	} while(text_ucs4_width(font, text, 0) > width);
}

void text_wrap(Font *font, const char *src, float width, char *buf, size_t bufsize) {
	assert(bufsize > strlen(src) + 1);
	assert(width > 0);

	char src_copy[strlen(src) + 1];
	char *sptr = src_copy;
	char *next = NULL;
	char *curline = buf;

	strcpy(src_copy, src);
	*buf = 0;

	while((next = strtok_r(NULL, " \t", &sptr))) {
		float curwidth;

		if(!*next) {
			continue;
		}

		if(*curline) {
			curwidth = text_width(font, curline, 0);
		} else {
			curwidth = 0;
		}

		char tmpbuf[strlen(curline) + strlen(next) + 2];
		strcpy(tmpbuf, curline);
		strcat(tmpbuf, " ");
		strcat(tmpbuf, next);

		float totalwidth = text_width(font, tmpbuf, 0);

		if(totalwidth > width) {
			if(curwidth == 0) {
				log_fatal(
					"Single word '%s' won't fit on one line. "
					"Word width: %g, max width: %g, source string: %s",
					next, text_width(font, tmpbuf, 0), width, src
				);
			}

			strlcat(buf, "\n", bufsize);
			curline = strchr(curline, 0);
		} else {
			if(*curline) {
				strlcat(buf, " ", bufsize);
			}
		}

		strlcat(buf, next, bufsize);
	}
}

const FontMetrics *font_get_metrics(Font *font) {
	return &font->metrics;
}

float font_get_lineskip(Font *font) {
	return font->metrics.lineskip / font->metrics.scale;
}

float font_get_ascent(Font *font) {
	return font->metrics.descent / font->metrics.scale;
}

float font_get_descent(Font *font) {
	return font->metrics.descent / font->metrics.scale;
}

const GlyphMetrics *font_get_char_metrics(Font *font, charcode_t c) {
	Glyph *g = get_glyph(font, c);

	if(!g) {
		return NULL;
	}

	return &g->metrics;
}

static bool transfer_font(void *dst, void *src) {
	auto dfont = CASTPTR_ASSUME_ALIGNED(dst, Font);
	auto sfont = CASTPTR_ASSUME_ALIGNED(src, Font);
	free_font_resources(dfont);
	*dfont = *sfont;
	mem_free(sfont);
	return true;
}
