package vefontcache
/*
Note(Ed): The only reason I didn't directly use harfbuzz is:
https://github.com/saidwho12/hamza 
and seems to be under active development as an alternative.
*/

import "core:c"
import "thirdparty:harfbuzz"

Shape_Key :: u32

/*  A text whose codepoints have had their relevant glyphs and 
	associated data resolved for processing in a draw list generation stage.
	Traditionally a shape only refers to resolving which glyph and 
	its position should be used for rendering.

	For this library's case it also resolves any content that does not have to be done 
	on a per-frame basis for draw list generation:
		* atlas lru codes
		* glyph bounds and scale
		* atlas region the glyph is associated with.

	Ideally the user should resolve this shape once and cache/store it on their side.
	They have the best ability to avoid costly lookups.
*/
Shaped_Text :: struct #packed {
	glyph          : [dynamic]Glyph,
	position       : [dynamic]Vec2,
	visible        : [dynamic]i32,
	atlas_lru_code : [dynamic]Atlas_Key,
	region_kind    : [dynamic]Atlas_Region_Kind,
	bounds         : [dynamic]Range2,
	end_cursor_pos : Vec2,
	size           : Vec2,
	font           : Font_ID, 
	px_size        : f32,
}

// Ease of use cache, can handle thousands of lookups per frame with ease.
// TODO(Ed) It might perform better with a tailored made hashtable implementation for the LRU_Cache or dedicated array struct/procs for the Shaped_Text.
Shaped_Text_Cache :: struct {
	storage       : [dynamic]Shaped_Text,
	state         : LRU_Cache(Shape_Key),
	next_cache_id : i32,
}

// Used by shaper_shape_text_cached, allows user to specify their own proc at compile-time without having to rewrite the caching implementation.
Shaper_Shape_Text_Uncached_Proc :: #type proc( ctx : ^Shaper_Context,
	atlas             : Atlas, 
	glyph_buffer_size : Vec2,
	font              : Font_ID,
	entry             : Entry, 
	font_px_Size      : f32, 
	font_scale        : f32, 
	text_utf8         : string, 
	output            : ^Shaped_Text 
)

// Note(Ed): Not used..
Shaper_Kind :: enum {
	Latin    = 0,
	Harfbuzz = 1,
}

//  Not much here other than just keep track of a harfbuzz var and deciding to keep runtime config here used by the shapers.
Shaper_Context :: struct {
	hb_buffer : harfbuzz.Buffer,

	snap_glyph_position           : b32,
	adv_snap_small_font_threshold : f32,
}

// Only used with harbuzz for now. Resolved during load_font for a font Entry.
Shaper_Info :: struct {
	blob : harfbuzz.Blob,
	face : harfbuzz.Face,
	font : harfbuzz.Font,
}

shaper_init :: proc( ctx : ^Shaper_Context )
{
	ctx.hb_buffer = harfbuzz.buffer_create()
	assert( ctx.hb_buffer != nil, "VEFontCache.shaper_init: Failed to create harfbuzz buffer")
}

shaper_shutdown :: proc( ctx : ^Shaper_Context ) 
{
	if ctx.hb_buffer != nil {
		harfbuzz.buffer_destroy( ctx.hb_buffer )
	}
}

shaper_load_font :: #force_inline proc( ctx : ^Shaper_Context, label : string, data : []byte, user_data : rawptr = nil ) -> (info : Shaper_Info)
{
	info.blob = harfbuzz.blob_create( raw_data(data), cast(c.uint) len(data), harfbuzz.Memory_Mode.READONLY, user_data, nil )
	info.face = harfbuzz.face_create( info.blob, 0 )
	info.font = harfbuzz.font_create( info.face )
	return
}

shaper_unload_font :: #force_inline proc( info : ^Shaper_Info )
{
	if info.font != nil do harfbuzz.font_destroy( info.font )
	if info.face != nil do harfbuzz.face_destroy( info.face )
	if info.blob != nil do harfbuzz.blob_destroy( info.blob )
}

// TODO(Ed): Allow the user to override snap_glyph_position of the shaper context on a per-call basis (as a param)
// Recommended shaper. Very performant.
// TODO(Ed): Would be nice to properly support vertical shaping, right now its strictly just horizontal...
@(optimization_mode="favor_size")
shaper_shape_harfbuzz :: proc( ctx : ^Shaper_Context, 
	atlas             : Atlas, 
	glyph_buffer_size : Vec2,
	font              : Font_ID,
	entry             : Entry, 
	font_px_size      : f32, 
	font_scale        : f32, 
	text_utf8         : string, 
	output            : ^Shaped_Text
)
{
	profile(#procedure)
	assert( ctx != nil )

	clear( & output.glyph )
	clear( & output.position )
	clear( & output.visible )

	current_script := harfbuzz.Script.UNKNOWN
	hb_ucfunc      := harfbuzz.unicode_funcs_get_default()
	harfbuzz.buffer_clear_contents( ctx.hb_buffer )

	ascent   := entry.ascent
	descent  := entry.descent
	line_gap := entry.line_gap
	
	max_line_width := f32(0)
	line_count     := 1
	line_height    := ((ascent - descent + line_gap) * font_scale)

	position : Vec2

	@(optimization_mode="favor_size")
	shape_run :: proc( output : ^Shaped_Text,
		entry  : Entry, 
		buffer : harfbuzz.Buffer,
		script : harfbuzz.Script, 
		
		position       : ^Vec2, 
		max_line_width : ^f32, 
		line_count     : ^int,

		font_px_size : f32,
		font_scale   : f32,

		snap_shape_pos                : b32, 
		adv_snap_small_font_threshold : f32 
	)
	{
		profile(#procedure)
		harfbuzz.buffer_set_script   ( buffer, script )
		harfbuzz.buffer_set_direction( buffer, harfbuzz.script_get_horizontal_direction( script ))
		harfbuzz.buffer_set_language ( buffer, harfbuzz.language_get_default() )

		// Perform the actual shaping of this run using HarfBuzz.
		harfbuzz.buffer_set_content_type( buffer, harfbuzz.Buffer_Content_Type.UNICODE )
		harfbuzz.shape( entry.shaper_info.font, buffer, nil, 0 )

		// Loop over glyphs and append to output buffer.
		glyph_count : u32
		glyph_infos     := harfbuzz.buffer_get_glyph_infos( buffer, & glyph_count )
		glyph_positions := harfbuzz.buffer_get_glyph_positions( buffer, & glyph_count )

		line_height := (entry.ascent - entry.descent + entry.line_gap) * font_scale

		last_cluster := u32(0)
		for index : i32; index < i32(glyph_count); index += 1
		{
			hb_glyph     := glyph_infos    [ index ]
			hb_gposition := glyph_positions[ index ]
			glyph        := cast(Glyph) hb_glyph.codepoint

			if hb_glyph.cluster > 0
			{
				(max_line_width^) = max( max_line_width^, position.x )
				position.x        = 0.0
				position.y       -= line_height
				position.y        = floor(position.y)
				(line_count^)    += 1

				last_cluster = hb_glyph.cluster
				continue
			}
			if abs( font_px_size ) <= adv_snap_small_font_threshold {
				(position^) = ceil( position^ )
			}

			glyph_pos := position^
			offset    := Vec2 { f32(hb_gposition.x_offset), f32(hb_gposition.y_offset) } * font_scale
			glyph_pos += offset

			if snap_shape_pos {
				glyph_pos = ceil(glyph_pos)
			}

			advance := Vec2 { 
				f32(hb_gposition.x_advance) * font_scale, 
				f32(hb_gposition.y_advance) * font_scale
			}
			(position^)      += advance
			(max_line_width^) = max(max_line_width^, position.x)

			// We track all glyphs so that user can use the shape for navigation purposes.
			append( & output.glyph, glyph )
			append( & output.position, glyph_pos)

			// We don't accept all glyphs for rendering, harfbuzz preserves positions of non-visible codepoints (as .notdef glyphs)
			// We also double check to make sure the glyph isn't detected for drawing by the parser.
			visible_glyph := glyph != 0 && ! parser_is_glyph_empty(entry.parser_info, glyph)
			if visible_glyph {
				append( & output.visible, cast(i32) len(output.glyph) - 1 )
			}
		}

		output.end_cursor_pos = position^
		harfbuzz.buffer_clear_contents( buffer )
	}

	// Note(Original Author):
	// We first start with simple bidi and run logic.
	// True CTL is pretty hard and we don't fully support that; patches welcome!

	for codepoint, byte_offset in text_utf8
	{
		hb_codepoint := cast(harfbuzz.Codepoint) codepoint

		script := harfbuzz.unicode_script( hb_ucfunc, hb_codepoint )

		// Can we continue the current run?
		ScriptKind :: harfbuzz.Script

		// These scripts don't break runs because they don't represent script transitions - they adapt to their context. 
		// Maintaining the current shaping run for these scripts ensures correct processing of marks, numbers, 
		// and punctuation within the primary text flow.
		is_neutral_script := script == ScriptKind.UNKNOWN || script == ScriptKind.INHERITED || script == ScriptKind.COMMON

		// Essentially if the script is neutral, or the same as current, 
		// or this is the first codepoint: add it to the buffer and continue the loop.
		if is_neutral_script             \
		|| script      == current_script \
		|| byte_offset == 0 
		{
			harfbuzz.buffer_add( ctx.hb_buffer, hb_codepoint, codepoint == '\n' ? 1 : 0 )
			current_script = is_neutral_script ? current_script : script
			continue
		}

		// End current run since we've encountred a significant script change.
		shape_run( output,
			entry, 
			ctx.hb_buffer, 
			current_script, 
			& position, 
			& max_line_width, 
			& line_count, 
			font_px_size, 
			font_scale, 
			ctx.snap_glyph_position, 
			ctx.adv_snap_small_font_threshold
		)	
		harfbuzz.buffer_add( ctx.hb_buffer, hb_codepoint, codepoint == '\n' ? 1 : 0 )
		current_script = script
	}

	// End the last run if needed
	shape_run( output,
		entry, 
		ctx.hb_buffer, 
		current_script, 
		& position, 
		& max_line_width, 
		& line_count, 
		font_px_size, 
		font_scale, 
		ctx.snap_glyph_position, 
		ctx.adv_snap_small_font_threshold
	)	

	// Set the final size
	output.size.x = max_line_width
	output.size.y = f32(line_count) * line_height

	// Resolve each glyphs: bounds, atlas lru, and the atlas region as we have everything we need now.

	resize( & output.atlas_lru_code, len(output.visible) )
	resize( & output.region_kind,    len(output.visible) )
	resize( & output.bounds,         len(output.visible) )

	profile_begin("atlas_lru_code")
	for vis_id, index in output.visible {
		glyph_id                    := output.glyph[vis_id]
		output.atlas_lru_code[index] = atlas_glyph_lru_code(entry.id, font_px_size, glyph_id)
		// atlas_lru_code is 1:1 with visible index
	}
	profile_end()

	profile_begin("bounds & region")
	for vis_id, index in output.visible {
		glyph_id                 := output.glyph[vis_id]
		bounds                   := & output.bounds[index]
		(bounds ^)                = parser_get_bounds( entry.parser_info, glyph_id )
		bounds_size_scaled       := (bounds.p1 - bounds.p0) * font_scale
		output.region_kind[index] = atlas_decide_region( atlas, glyph_buffer_size, bounds_size_scaled )
		// bounds & region_kind are 1:1 with visible index
	}
	profile_end()

	output.font    = font
	output.px_size = font_px_size
	return
}

// TODO(Ed): Allow the user to override snap_glyph_position of the shaper context on a per-call basis (as an param)
// Basic western alphabet based shaping. Not that much faster than harfbuzz if at all.
shaper_shape_text_latin :: proc( ctx : ^Shaper_Context,
	atlas             : Atlas, 
	glyph_buffer_size : Vec2,
	font              : Font_ID,
	entry             : Entry, 
	font_px_size      : f32, 
	font_scale        : f32, 
	text_utf8         : string, 
	output            : ^Shaped_Text
)
{	
	profile(#procedure)
	assert( ctx != nil )

	clear( & output.glyph )
	clear( & output.position )
	clear( & output.visible )

	line_height := (entry.ascent - entry.descent + entry.line_gap) * font_scale

	line_count     : int = 1
	max_line_width : f32 = 0
	position       : Vec2

	prev_codepoint : rune
	for codepoint, index in text_utf8
	{
		if prev_codepoint > 0 {
			kern       := parser_get_codepoint_kern_advance( entry.parser_info, prev_codepoint, codepoint )
			position.x += f32(kern) * font_scale
		}
		if codepoint == '\n'
		{
			line_count    += 1
			max_line_width = max(max_line_width, position.x)
			position.x     = 0.0
			position.y    -= line_height
			position.y     = position.y
			prev_codepoint = rune(0)
			continue
		}
		if abs( font_px_size ) <= ctx.adv_snap_small_font_threshold {
			position.x = ceil(position.x)
		}

		glyph_index    := parser_find_glyph_index( entry.parser_info, codepoint )
		is_glyph_empty := parser_is_glyph_empty( entry.parser_info, glyph_index )

		if ctx.snap_glyph_position {
			position.x = ceil(position.x)
			position.y = ceil(position.y)
		}
		append( & output.glyph, glyph_index)
		append( & output.position, position)

		if ! is_glyph_empty {
			append( & output.visible, cast(i32) len(output.glyph) - 1 )
		}

		advance, _ := parser_get_codepoint_horizontal_metrics( entry.parser_info, codepoint )
		position.x += f32(advance) * font_scale
		prev_codepoint = codepoint
	}

	output.end_cursor_pos = position
	max_line_width        = max(max_line_width, position.x)

	output.size.x = max_line_width
	output.size.y = f32(line_count) * line_height

	// Resolve each glyphs: bounds, atlas lru, and the atlas region as we have everything we need now.

	resize( & output.atlas_lru_code, len(output.glyph) )
	resize( & output.region_kind,    len(output.glyph) )
	resize( & output.bounds,         len(output.glyph) )

	profile_begin("atlas_lru_code")
	for vis_id, index in output.visible {
		glyph_id                    := output.glyph[vis_id]
		output.atlas_lru_code[index] = atlas_glyph_lru_code(entry.id, font_px_size, glyph_id)
		// atlas_lru_code is 1:1 with visible index
	}
	profile_end()

	profile_begin("bounds & region")
	for vis_id, index in output.visible {
		glyph_id                 := output.glyph[vis_id]
		bounds                   := & output.bounds[index]
		(bounds ^)                = parser_get_bounds( entry.parser_info, glyph_id )
		bounds_size_scaled       := (bounds.p1 - bounds.p0) * font_scale
		output.region_kind[index] = atlas_decide_region( atlas, glyph_buffer_size, bounds_size_scaled )
		// bounds & region_kind are 1:1 with visible index
	}
	profile_end()

	output.font    = font
	output.px_size = font_px_size
}

// Shapes are tracked by the library's context using the shape cache 
// and the key is resolved using the font, the desired pixel size, and the text bytes to be shaped.
// Thus this procedures cost will be proporitonal to how much text it has to sift through.
// djb8_hash is used as its been pretty good for thousands of hashed lines that around 6-250 charactes long
// (and its very fast).
@(optimization_mode="favor_size")
shaper_shape_text_cached :: proc( text_utf8 : string, 
	ctx                 : ^Shaper_Context,
	shape_cache         : ^Shaped_Text_Cache, 
	atlas               : Atlas,
	glyph_buffer_size   : Vec2,
	font                : Font_ID,
	entry               : Entry, 
	font_px_size        : f32, 
	font_scale          : f32, 
	shape_text_uncached : $Shaper_Shape_Text_Uncached_Proc
) -> (shaped_text : Shaped_Text)
{
	profile(#procedure)
	font         := font
	font_px_size := font_px_size
	font_bytes   := to_bytes( & font )
	size_bytes   := to_bytes( & font_px_size )
	text_bytes   := transmute( []byte) text_utf8

	lru_code : Shape_Key
	djb8_hash( & lru_code, font_bytes )
	djb8_hash( & lru_code, size_bytes )
	djb8_hash( & lru_code, text_bytes )

	state := & shape_cache.state

	shape_cache_idx := lru_get( state, lru_code )
	if shape_cache_idx == -1
	{
		if shape_cache.next_cache_id < i32(state.capacity){
			shape_cache_idx            = shape_cache.next_cache_id
			shape_cache.next_cache_id += 1
			evicted := lru_put( state, lru_code, shape_cache_idx )
		}
		else 
		{
			next_evict_idx := lru_get_next_evicted( state ^ )
			assert( next_evict_idx != LRU_Fail_Mask_32 )

			shape_cache_idx = lru_peek( state ^, next_evict_idx, must_find = true )
			assert( shape_cache_idx != - 1 )

			lru_put( state, lru_code, shape_cache_idx )
		}

		storage_entry := & shape_cache.storage[ shape_cache_idx ]
		shape_text_uncached( ctx, atlas, glyph_buffer_size, font, entry, font_px_size, font_scale, text_utf8, storage_entry )

		shaped_text = storage_entry ^
		return
	}

	shaped_text = shape_cache.storage[ shape_cache_idx ]
	return
}
