// Copyright (c) 2024 Epic Games Tools
// Licensed under the MIT license (https://opensource.org/license/mit/)

////////////////////////////////
//~ rjf: Basic Functions

#if !defined(BLAKE2_H)
#define HAVE_SSE2
#include "third_party/blake2/blake2.h"
#include "third_party/blake2/blake2b.c"
#endif

internal U128
fnt_hash_from_string(String8 string)
{
  U128 result = {0};
  blake2b((U8 *)&result.u64[0], sizeof(result), string.str, string.size, 0, 0);
  return result;
}

internal U64
fnt_little_hash_from_string(String8 string)
{
  U64 result = 5381;
  for(U64 i = 0; i < string.size; i += 1)
  {
    result = ((result << 5) + result) + string.str[i];
  }
  return result;
}

internal Vec2S32
fnt_vertex_from_corner(Corner corner)
{
  Vec2S32 result = {0};
  switch(corner)
  {
    default: break;
    case Corner_00:{result = v2s32(0, 0);}break;
    case Corner_01:{result = v2s32(0, 1);}break;
    case Corner_10:{result = v2s32(1, 0);}break;
    case Corner_11:{result = v2s32(1, 1);}break;
  }
  return result;
}

////////////////////////////////
//~ rjf: Font Tags

internal FNT_Tag
fnt_tag_zero(void)
{
  FNT_Tag result = {0};
  return result;
}

internal B32
fnt_tag_match(FNT_Tag a, FNT_Tag b)
{
  return a.u64[0] == b.u64[0] && a.u64[1] == b.u64[1];
}

internal FP_Handle
fnt_handle_from_tag(FNT_Tag tag)
{
  ProfBeginFunction();
  U64 slot_idx = tag.u64[1] % f_state->font_hash_table_size;
  FNT_FontHashNode *existing_node = 0;
  {
    for(FNT_FontHashNode *n = f_state->font_hash_table[slot_idx].first; n != 0 ; n = n->hash_next)
    {
      if(MemoryMatchStruct(&tag, &n->tag))
      {
        existing_node = n;
        break;
      }
    }
  }
  FP_Handle result = {0};
  if(existing_node != 0)
  {
    result = existing_node->handle;
  }
  ProfEnd();
  return result;
}

internal FP_Metrics
fnt_fp_metrics_from_tag(FNT_Tag tag)
{
  ProfBeginFunction();
  U64 slot_idx = tag.u64[1] % f_state->font_hash_table_size;
  FNT_FontHashNode *existing_node = 0;
  {
    for(FNT_FontHashNode *n = f_state->font_hash_table[slot_idx].first; n != 0 ; n = n->hash_next)
    {
      if(MemoryMatchStruct(&tag, &n->tag))
      {
        existing_node = n;
        break;
      }
    }
  }
  FP_Metrics result = {0};
  if(existing_node != 0)
  {
    result = existing_node->metrics;
  }
  ProfEnd();
  return result;
}

internal FNT_Tag
fnt_tag_from_path(String8 path)
{
  ProfBeginFunction();
  
  //- rjf: produce tag from hash of path
  FNT_Tag result = {0};
  {
    U128 hash = fnt_hash_from_string(path);
    MemoryCopy(&result, &hash, sizeof(result));
    result.u64[1] |= bit64;
  }
  
  //- rjf: tag -> slot index
  U64 slot_idx = result.u64[1] % f_state->font_hash_table_size;
  
  //- rjf: slot * tag -> existing node
  FNT_FontHashNode *existing_node = 0;
  {
    for(FNT_FontHashNode *n = f_state->font_hash_table[slot_idx].first; n != 0 ; n = n->hash_next)
    {
      if(MemoryMatchStruct(&result, &n->tag))
      {
        existing_node = n;
        break;
      }
    }
  }
  
  //- rjf: allocate & push new node if we don't have an existing one
  FNT_FontHashNode *new_node = 0;
  if(existing_node == 0)
  {
    FNT_FontHashSlot *slot = &f_state->font_hash_table[slot_idx];
    new_node = push_array(f_state->arena, FNT_FontHashNode, 1);
    new_node->tag = result;
    new_node->handle = fp_font_open(path);
    new_node->metrics = fp_metrics_from_font(new_node->handle);
    new_node->path = push_str8_copy(f_state->arena, path);
    SLLQueuePush_N(slot->first, slot->last, new_node, hash_next);
  }
  
  //- rjf: return
  ProfEnd();
  return result;
}

internal FNT_Tag
fnt_tag_from_static_data_string(String8 *data_ptr)
{
  ProfBeginFunction();
  
  //- rjf: produce tag hash of ptr
  FNT_Tag result = {0};
  {
    U128 hash = fnt_hash_from_string(str8((U8 *)&data_ptr, sizeof(String8 *)));
    MemoryCopy(&result, &hash, sizeof(result));
    result.u64[1] &= ~bit64;
  }
  
  //- rjf: tag -> slot index
  U64 slot_idx = result.u64[1] % f_state->font_hash_table_size;
  
  //- rjf: slot * tag -> existing node
  FNT_FontHashNode *existing_node = 0;
  {
    for(FNT_FontHashNode *n = f_state->font_hash_table[slot_idx].first; n != 0 ; n = n->hash_next)
    {
      if(MemoryMatchStruct(&result, &n->tag))
      {
        existing_node = n;
        break;
      }
    }
  }
  
  //- rjf: allocate & push new node if we don't have an existing one
  FNT_FontHashNode *new_node = 0;
  if(existing_node == 0)
  {
    FNT_FontHashSlot *slot = &f_state->font_hash_table[slot_idx];
    new_node = push_array(f_state->arena, FNT_FontHashNode, 1);
    new_node->tag = result;
    new_node->handle = fp_font_open_from_static_data_string(data_ptr);
    new_node->metrics = fp_metrics_from_font(new_node->handle);
    new_node->path = str8_lit("");
    SLLQueuePush_N(slot->first, slot->last, new_node, hash_next);
  }
  
  //- rjf: return
  ProfEnd();
  return result;
}

internal String8
fnt_path_from_tag(FNT_Tag tag)
{
  //- rjf: tag -> slot index
  U64 slot_idx = tag.u64[1] % f_state->font_hash_table_size;
  
  //- rjf: slot * tag -> existing node
  FNT_FontHashNode *existing_node = 0;
  {
    for(FNT_FontHashNode *n = f_state->font_hash_table[slot_idx].first; n != 0 ; n = n->hash_next)
    {
      if(MemoryMatchStruct(&tag, &n->tag))
      {
        existing_node = n;
        break;
      }
    }
  }
  
  //- rjf: existing node -> path
  String8 result = {0};
  if(existing_node != 0)
  {
    result = existing_node->path;
  }
  
  return result;
}

////////////////////////////////
//~ rjf: Atlas

internal Rng2S16
fnt_atlas_region_alloc(Arena *arena, FNT_Atlas *atlas, Vec2S16 needed_size)
{
  ProfBeginFunction();
  
  //- rjf: find node with best-fit size
  Vec2S16 region_p0 = {0};
  Vec2S16 region_sz = {0};
  Corner node_corner = Corner_Invalid;
  FNT_AtlasRegionNode *node = 0;
  {
    Vec2S16 n_supported_size = atlas->root_dim;
    for(FNT_AtlasRegionNode *n = atlas->root, *next = 0; n != 0; n = next, next = 0)
    {
      // rjf: we've traversed to a taken node.
      if(n->flags & FNT_AtlasRegionNodeFlag_Taken)
      {
        break;
      }
      
      // rjf: calculate if this node can be allocated (all children are non-allocated)
      B32 n_can_be_allocated = (n->num_allocated_descendants == 0);
      
      // rjf: fill size
      if(n_can_be_allocated)
      {
        region_sz = n_supported_size;
      }
      
      // rjf: calculate size of this node's children
      Vec2S16 child_size = v2s16(n_supported_size.x/2, n_supported_size.y/2);
      
      // rjf: find best next child
      FNT_AtlasRegionNode *best_child = 0;
      if(child_size.x >= needed_size.x && child_size.y >= needed_size.y)
      {
        for(Corner corner = (Corner)0; corner < Corner_COUNT; corner = (Corner)(corner+1))
        {
          if(n->children[corner] == 0)
          {
            n->children[corner] = push_array(arena, FNT_AtlasRegionNode, 1);
            n->children[corner]->parent = n;
            n->children[corner]->max_free_size[Corner_00] = 
              n->children[corner]->max_free_size[Corner_01] = 
              n->children[corner]->max_free_size[Corner_10] = 
              n->children[corner]->max_free_size[Corner_11] = v2s16(child_size.x/2, child_size.y/2);
          }
          if(n->max_free_size[corner].x >= needed_size.x && 
             n->max_free_size[corner].y >= needed_size.y)
          {
            best_child = n->children[corner];
            node_corner = corner;
            Vec2S32 side_vertex = fnt_vertex_from_corner(corner);
            region_p0.x += side_vertex.x*child_size.x;
            region_p0.y += side_vertex.y*child_size.y;
            break;
          }
        }
      }
      
      // rjf: resolve node to this node if it can be allocated and children
      // don't fit, or keep going to the next best child
      if(n_can_be_allocated && best_child == 0)
      {
        node = n;
      }
      else
      {
        next = best_child;
        n_supported_size = child_size;
      }
    }
  }
  
  //- rjf: we're taking the subtree rooted by `node`. mark up all parents
  if(node != 0 && node_corner != Corner_Invalid)
  {
    node->flags |= FNT_AtlasRegionNodeFlag_Taken;
    if(node->parent != 0)
    {
      MemoryZeroStruct(&node->parent->max_free_size[node_corner]);
    }
    for(FNT_AtlasRegionNode *p = node->parent; p != 0; p = p->parent)
    {
      p->num_allocated_descendants += 1;
      FNT_AtlasRegionNode *parent = p->parent;
      if(parent != 0)
      {
        Corner p_corner = (p == parent->children[Corner_00] ? Corner_00 :
                           p == parent->children[Corner_01] ? Corner_01 :
                           p == parent->children[Corner_10] ? Corner_10 :
                           p == parent->children[Corner_11] ? Corner_11 :
                           Corner_Invalid);
        if(p_corner == Corner_Invalid)
        {
          InvalidPath;
        }
        parent->max_free_size[p_corner].x = Max(Max(p->max_free_size[Corner_00].x,
                                                    p->max_free_size[Corner_01].x),
                                                Max(p->max_free_size[Corner_10].x,
                                                    p->max_free_size[Corner_11].x));
        parent->max_free_size[p_corner].y = Max(Max(p->max_free_size[Corner_00].y,
                                                    p->max_free_size[Corner_01].y),
                                                Max(p->max_free_size[Corner_10].y,
                                                    p->max_free_size[Corner_11].y));
      }
    }
  }
  
  //- rjf: fill rectangular region & return
  Rng2S16 result = {0};
  result.p0 = region_p0;
  result.p1 = add_2s16(region_p0, region_sz);
  ProfEnd();
  return result;
}

internal void
fnt_atlas_region_release(FNT_Atlas *atlas, Rng2S16 region)
{
  ProfBeginFunction();
  
  //- rjf: extract region size
  Vec2S16 region_size = v2s16(region.x1 - region.x0, region.y1 - region.y0);
  
  //- rjf: map region to associated node
  Vec2S16 calc_region_size = {0};
  FNT_AtlasRegionNode *node = 0;
  Corner node_corner = Corner_Invalid;
  {
    Vec2S16 n_p0 = v2s16(0, 0);
    Vec2S16 n_sz = atlas->root_dim;
    for(FNT_AtlasRegionNode *n = atlas->root, *next = 0; n != 0; n = next)
    {
      // rjf: is the region within this node's boundaries? (either this node, or a descendant)
      if(n_p0.x <= region.p0.x && region.p0.x < n_p0.x+n_sz.x &&
         n_p0.y <= region.p0.y && region.p0.y < n_p0.y+n_sz.y)
      {
        // rjf: check the region against this node
        if(region.p0.x == n_p0.x && region.p0.y == n_p0.y && 
           region_size.x == n_sz.x && region_size.y == n_sz.y)
        {
          node = n;
          calc_region_size = n_sz;
          break;
        }
        // rjf: check the region against children & iterate
        else
        {
          Vec2S16 r_midpoint = v2s16(region.p0.x + region_size.x/2,
                                     region.p0.y + region_size.y/2);
          Vec2S16 n_midpoint = v2s16(n_p0.x + n_sz.x/2,
                                     n_p0.y + n_sz.y/2);
          Corner next_corner = Corner_Invalid;
          if(r_midpoint.x <= n_midpoint.x && r_midpoint.y <= n_midpoint.y)
          {
            next_corner = Corner_00;
          }
          else if(r_midpoint.x <= n_midpoint.x && n_midpoint.y <= r_midpoint.y)
          {
            next_corner = Corner_01;
          }
          else if(n_midpoint.x <= r_midpoint.x && r_midpoint.y <= n_midpoint.y)
          {
            next_corner = Corner_10;
          }
          else if(n_midpoint.x <= r_midpoint.x && n_midpoint.y <= r_midpoint.y)
          {
            next_corner = Corner_11;
          }
          next = n->children[next_corner];
          node_corner = next_corner;
          n_sz.x /= 2;
          n_sz.y /= 2;
          Vec2S32 side_vertex = fnt_vertex_from_corner(node_corner);
          n_p0.x += side_vertex.x*n_sz.x;
          n_p0.y += side_vertex.y*n_sz.y;
        }
      }
      else
      {
        break; 
      }
    }
  }
  
  //- rjf: free node
  if(node != 0 && node_corner != Corner_Invalid)
  {
    node->flags &= ~FNT_AtlasRegionNodeFlag_Taken;
    if(node->parent != 0)
    {
      node->parent->max_free_size[node_corner] = calc_region_size;
    }
    for(FNT_AtlasRegionNode *p = node->parent; p != 0; p = p->parent)
    {
      p->num_allocated_descendants -= 1;
      FNT_AtlasRegionNode *parent = p->parent;
      if(parent != 0)
      {
        Corner p_corner = (p == parent->children[Corner_00] ? Corner_00 :
                           p == parent->children[Corner_01] ? Corner_01 :
                           p == parent->children[Corner_10] ? Corner_10 :
                           p == parent->children[Corner_11] ? Corner_11 :
                           Corner_Invalid);
        if(p_corner == Corner_Invalid)
        {
          InvalidPath;
        }
        parent->max_free_size[p_corner].x = Max(Max(p->max_free_size[Corner_00].x,
                                                    p->max_free_size[Corner_01].x),
                                                Max(p->max_free_size[Corner_10].x,
                                                    p->max_free_size[Corner_11].x));
        parent->max_free_size[p_corner].y = Max(Max(p->max_free_size[Corner_00].y,
                                                    p->max_free_size[Corner_01].y),
                                                Max(p->max_free_size[Corner_10].y,
                                                    p->max_free_size[Corner_11].y));
      }
    }
  }
  ProfEnd();
}

////////////////////////////////
//~ rjf: Piece Type Functions

internal FNT_Piece *
fnt_piece_chunk_list_push_new(Arena *arena, FNT_PieceChunkList *list, U64 cap)
{
  FNT_PieceChunkNode *node = list->last;
  if(node == 0 || node->count >= node->cap)
  {
    node = push_array(arena, FNT_PieceChunkNode, 1);
    node->v = push_array_no_zero(arena, FNT_Piece, cap);
    node->cap = cap;
    SLLQueuePush(list->first, list->last, node);
    list->node_count += 1;
  }
  FNT_Piece *result = node->v + node->count;
  node->count += 1;
  list->total_piece_count += 1;
  return result;
}

internal void
fnt_piece_chunk_list_push(Arena *arena, FNT_PieceChunkList *list, U64 cap, FNT_Piece *piece)
{
  FNT_Piece *new_piece = fnt_piece_chunk_list_push_new(arena, list, cap);
  MemoryCopyStruct(new_piece, piece);
}

internal FNT_PieceArray
fnt_piece_array_from_chunk_list(Arena *arena, FNT_PieceChunkList *list)
{
  FNT_PieceArray array = {0};
  array.count = list->total_piece_count;
  array.v = push_array_no_zero(arena, FNT_Piece, array.count);
  U64 write_idx = 0;
  for(FNT_PieceChunkNode *node = list->first; node != 0; node = node->next)
  {
    MemoryCopy(array.v + write_idx, node->v, node->count * sizeof(FNT_Piece));
    write_idx += node->count;
  }
  return array;
}

internal FNT_PieceArray
fnt_piece_array_copy(Arena *arena, FNT_PieceArray *src)
{
  FNT_PieceArray dst = {0};
  dst.count = src->count;
  dst.v = push_array_no_zero(arena, FNT_Piece, dst.count);
  MemoryCopy(dst.v, src->v, sizeof(FNT_Piece)*dst.count);
  return dst;
}

////////////////////////////////
//~ rjf: Rasterization Cache

internal FNT_Hash2StyleRasterCacheNode *
fnt_hash2style_from_tag_size_flags(FNT_Tag tag, F32 size, FNT_RasterFlags flags)
{
  //- rjf: tag * size -> style hash
  U64 style_hash = {0};
  {
    F64 size_f64 = size;
    U64 buffer[] =
    {
      tag.u64[0],
      tag.u64[1],
      *(U64 *)(&size_f64),
      (U64)flags,
    };
    style_hash = fnt_little_hash_from_string(str8((U8 *)buffer, sizeof(buffer)));
  }
  
  //- rjf: style hash -> style node
  FNT_Hash2StyleRasterCacheNode *hash2style_node = 0;
  {
    ProfBegin("style hash -> style node");
    U64 slot_idx = style_hash%f_state->hash2style_slots_count;
    FNT_Hash2StyleRasterCacheSlot *slot = &f_state->hash2style_slots[slot_idx];
    for(FNT_Hash2StyleRasterCacheNode *n = slot->first;
        n != 0;
        n = n->hash_next)
    {
      if(n->style_hash == style_hash)
      {
        hash2style_node = n;
        break;
      }
    }
    if(Unlikely(hash2style_node == 0))
    {
      FNT_Metrics metrics = fnt_metrics_from_tag_size(tag, size);
      hash2style_node = push_array(f_state->arena, FNT_Hash2StyleRasterCacheNode, 1);
      DLLPushBack_NP(slot->first, slot->last, hash2style_node, hash_next, hash_prev);
      hash2style_node->style_hash = style_hash;
      hash2style_node->ascent = metrics.ascent;
      hash2style_node->descent= metrics.descent;
      hash2style_node->utf8_class1_direct_map = push_array_no_zero(f_state->arena, F_RasterCacheInfo, 256);
      hash2style_node->hash2info_slots_count = 1024;
      hash2style_node->hash2info_slots = push_array(f_state->arena, FNT_Hash2InfoRasterCacheSlot, hash2style_node->hash2info_slots_count);
    }
    ProfEnd();
  }
  
  return hash2style_node;
}

internal FNT_Run
fnt_push_run_from_string(Arena *arena, FNT_Tag tag, F32 size, F32 base_align_px, F32 tab_size_px, FNT_RasterFlags flags, String8 string)
{
  ProfBeginFunction();
  
  //- rjf: map tag/size to style node
  FNT_Hash2StyleRasterCacheNode *hash2style_node = fnt_hash2style_from_tag_size_flags(tag, size, flags);
  
  //- rjf: decode string & produce run pieces
  B32 first = 1;
  FNT_PieceChunkList piece_chunks = {0};
  Vec2F32 dim = {0};
  B32 font_handle_mapped_on_miss = 0;
  FP_Handle font_handle = {0};
  U64 piece_substring_start_idx = 0;
  for(U64 idx = 0; idx < string.size; first = 0)
  {
    //- rjf: decode next codepoint & get piece substring, or continuation rule
    String8 piece_substring;
    B32 need_another_codepoint = 0;
    switch(utf8_class[string.str[idx]>>3])
    {
      case 1:
      {
        piece_substring.str = &string.str[idx];
        piece_substring.size = 1;
        idx += 1;
      }break;
      default:
      {
        UnicodeDecode decode = utf8_decode(string.str+idx, string.size-idx);
        idx += decode.inc;
        if(decode.inc == 0) { break; }
        piece_substring.str = string.str + piece_substring_start_idx;
        piece_substring.size = decode.inc;
        // NOTE(rjf): assuming 1 codepoint per piece for now.
      }break;
    }
    
    //- rjf: need another codepoint? -> continue
    if(need_another_codepoint)
    {
      continue;
    }
    
    //- rjf: do not need another codepoint? -> bump piece start idx
    {
      piece_substring_start_idx = idx;
    }
    
    //- rjf: determine if this piece is a tab - if so, use space info to draw
    B32 is_tab = (piece_substring.size == 1 && piece_substring.str[0] == '\t');
    if(is_tab)
    {
      piece_substring = str8_lit(" ");
    }
    
    //- rjf: piece substring -> raster cache info
    F_RasterCacheInfo *info = 0;
    U64 piece_hash = 0;
    {
      // rjf: fast path for utf8 class 1 -> direct map
      if(piece_substring.size == 1 && hash2style_node->utf8_class1_direct_map_mask[piece_substring.str[0]/64] & (1ull<<(piece_substring.str[0]%64)))
      {
        info = &hash2style_node->utf8_class1_direct_map[piece_substring.str[0]];
      }
      
      // rjf: more general, slower path for other glyphs
      if(piece_substring.size > 1)
      {
        piece_hash = fnt_little_hash_from_string(piece_substring);
        U64 slot_idx = piece_hash%hash2style_node->hash2info_slots_count;
        FNT_Hash2InfoRasterCacheSlot *slot = &hash2style_node->hash2info_slots[slot_idx];
        for(F_Hash2InfoRasterCacheNode *node = slot->first; node != 0; node = node->hash_next)
        {
          if(node->hash == piece_hash)
          {
            info = &node->info;
            break;
          }
        }
      }
    }
    
    //- rjf: no info found -> miss... fill this hash in the cache
    if(info == 0)
    {
      ProfBegin("no info found -> miss... fill this hash in the cache");
      Temp scratch = scratch_begin(&arena, 1);
      
      // rjf: grab font handle for this tag if we don't have one already
      if(font_handle_mapped_on_miss == 0)
      {
        font_handle_mapped_on_miss = 1;
        
        // rjf: tag -> font slot index
        U64 font_slot_idx = tag.u64[1] % f_state->font_hash_table_size;
        
        // rjf: tag * slot -> existing node
        FNT_FontHashNode *existing_node = 0;
        {
          for(FNT_FontHashNode *n = f_state->font_hash_table[font_slot_idx].first; n != 0 ; n = n->hash_next)
          {
            if(MemoryMatchStruct(&n->tag, &tag))
            {
              existing_node = n;
              break;
            }
          }
        }
        
        // rjf: existing node -> font handle
        if(existing_node != 0)
        {
          font_handle = existing_node->handle;
        }
      }
      
      // rjf: call into font provider to rasterize this substring
      FP_RasterResult raster = {0};
      if(size > 0)
      {
        FP_RasterFlags fp_flags = 0;
        if(flags & FNT_RasterFlag_Smooth) { fp_flags |= FP_RasterFlag_Smooth; }
        if(flags & FNT_RasterFlag_Hinted) { fp_flags |= FP_RasterFlag_Hinted; }
        raster = fp_raster(scratch.arena, font_handle, floor_f32(size), flags, piece_substring);
      }
      
      // rjf: allocate portion of an atlas to upload the rasterization
      S16 chosen_atlas_num = 0;
      FNT_Atlas *chosen_atlas = 0;
      Rng2S16 chosen_atlas_region = {0};
      if(raster.atlas_dim.x != 0 && raster.atlas_dim.y != 0)
      {
        U64 num_atlases = 0;
        for(FNT_Atlas *atlas = f_state->first_atlas;; atlas = atlas->next, num_atlases += 1)
        {
          // rjf: create atlas if needed
          if(atlas == 0 && num_atlases < 64)
          {
            atlas = push_array(f_state->arena, FNT_Atlas, 1);
            DLLPushBack(f_state->first_atlas, f_state->last_atlas, atlas);
            atlas->root_dim = v2s16(1024, 1024);
            atlas->root = push_array(f_state->arena, FNT_AtlasRegionNode, 1);
            atlas->root->max_free_size[Corner_00] =
              atlas->root->max_free_size[Corner_01] =
              atlas->root->max_free_size[Corner_10] =
              atlas->root->max_free_size[Corner_11] = v2s16(atlas->root_dim.x/2, atlas->root_dim.y/2);
            atlas->texture = r_tex2d_alloc(R_ResourceKind_Dynamic, v2s32((S32)atlas->root_dim.x, (S32)atlas->root_dim.y), R_Tex2DFormat_RGBA8, 0);
          }
          
          // rjf: allocate from atlas
          if(atlas != 0)
          {
            Vec2S16 needed_dimensions = v2s16(raster.atlas_dim.x + 2, raster.atlas_dim.y + 2);
            chosen_atlas_region = fnt_atlas_region_alloc(f_state->arena, atlas, needed_dimensions);
            if(chosen_atlas_region.x1 != chosen_atlas_region.x0)
            {
              chosen_atlas = atlas;
              chosen_atlas_num = (S32)num_atlases;
              break;
            }
          }
          else
          {
            break;
          }
        }
      }
      
      // rjf: upload rasterization to allocated region of atlas texture memory
      if(chosen_atlas != 0)
      {
        Rng2S32 subregion =
        {
          chosen_atlas_region.x0,
          chosen_atlas_region.y0,
          chosen_atlas_region.x0 + raster.atlas_dim.x,
          chosen_atlas_region.y0 + raster.atlas_dim.y
        };
        r_fill_tex2d_region(chosen_atlas->texture, subregion, raster.atlas);
      }
      
      // rjf: allocate & fill & push node
      {
        if(piece_substring.size == 1)
        {
          info = &hash2style_node->utf8_class1_direct_map[piece_substring.str[0]];
          hash2style_node->utf8_class1_direct_map_mask[piece_substring.str[0]/64] |= (1ull<<(piece_substring.str[0]%64));
        }
        else
        {
          U64 slot_idx = piece_hash%hash2style_node->hash2info_slots_count;
          FNT_Hash2InfoRasterCacheSlot *slot = &hash2style_node->hash2info_slots[slot_idx];
          F_Hash2InfoRasterCacheNode *node = push_array_no_zero(f_state->arena, F_Hash2InfoRasterCacheNode, 1);
          DLLPushBack_NP(slot->first, slot->last, node, hash_next, hash_prev);
          node->hash = piece_hash;
          info = &node->info;
        }
        if(info != 0)
        {
          info->subrect    = chosen_atlas_region;
          info->atlas_num  = chosen_atlas_num;
          info->raster_dim = raster.atlas_dim;
          info->advance    = raster.advance;
        }
      }
      
      scratch_end(scratch);
      ProfEnd();
    }
    
    //- rjf: push piece for this raster portion
    if(info != 0)
    {
      // rjf: find atlas
      FNT_Atlas *atlas = 0;
      {
        if(info->subrect.x1 != 0 && info->subrect.y1 != 0)
        {
          S32 num = 0;
          for(FNT_Atlas *a = f_state->first_atlas; a != 0; a = a->next, num += 1)
          {
            if(info->atlas_num == num)
            {
              atlas = a;
              break;
            }
          }
        }
      }
      
      // rjf: on tabs -> expand advance
      F32 advance = info->advance;
      if(is_tab)
      {
        advance = floor_f32(tab_size_px) - mod_f32(floor_f32(base_align_px), floor_f32(tab_size_px));
      }
      
      // rjf: push piece
      {
        FNT_Piece *piece = fnt_piece_chunk_list_push_new(arena, &piece_chunks, string.size);
        {
          piece->texture = atlas ? atlas->texture : r_handle_zero();
          piece->subrect = r2s16p(info->subrect.x0,
                                  info->subrect.y0,
                                  info->subrect.x0 + info->raster_dim.x,
                                  info->subrect.y0 + info->raster_dim.y);
          piece->advance = advance;
          piece->decode_size = piece_substring.size;
          piece->offset = v2s16(0, -(hash2style_node->ascent + hash2style_node->descent));
        }
        base_align_px += advance;
        dim.x += piece->advance;
        dim.y = Max(dim.y, info->raster_dim.y);
      }
    }
  }
  
  //- rjf: tighten & return
  FNT_Run run = {0};
  {
    if(piece_chunks.node_count == 1)
    {
      run.pieces.v = piece_chunks.first->v;
      run.pieces.count = piece_chunks.first->count;
    }
    else
    {
      run.pieces = fnt_piece_array_from_chunk_list(arena, &piece_chunks);
    }
    run.dim = dim;
    run.ascent  = hash2style_node->ascent;
    run.descent = hash2style_node->descent;
  }
  
  ProfEnd();
  return run;
}

internal String8List
fnt_wrapped_string_lines_from_font_size_string_max(Arena *arena, FNT_Tag font, F32 size, F32 base_align_px, F32 tab_size_px, String8 string, F32 max)
{
  String8List list = {0};
  {
    Temp scratch = scratch_begin(&arena, 1);
    FNT_Run run = fnt_push_run_from_string(scratch.arena, font, size, base_align_px, tab_size_px, 0, string);
    F32 off_px = 0;
    U64 off_bytes = 0;
    U64 line_start_off_bytes = 0;
    U64 line_end_off_bytes = 0;
    B32 seeking_word_end = 0;
    F32 word_start_off_px = 0;
    FNT_Piece *last_word_start_piece = 0;
    U64 last_word_start_off_bytes = 0;
    FNT_Piece *pieces_first = run.pieces.v;
    FNT_Piece *pieces_opl = run.pieces.v + run.pieces.count;
    for(FNT_Piece *piece = pieces_first, *next = 0; piece != 0 && piece <= pieces_opl; piece = next)
    {
      if(piece != 0) {next = piece+1;}
      
      // rjf: gather info
      U8 byte         = off_bytes < string.size ? string.str[off_bytes] : 0;
      F32 advance     = (piece != 0) ? piece->advance : 0;
      U64 decode_size = (piece != 0) ? piece->decode_size : 0;
      
      // rjf: find start/end of words
      B32 is_first_byte_of_word = 0;
      B32 is_first_space_after_word = 0;
      if(!seeking_word_end && !char_is_space(byte))
      {
        seeking_word_end = 1;
        is_first_byte_of_word = 1;
        last_word_start_off_bytes = off_bytes;
        last_word_start_piece = piece;
        word_start_off_px = off_px;
      }
      else if(seeking_word_end && char_is_space(byte))
      {
        seeking_word_end = 0;
        is_first_space_after_word = 1;
      }
      else if(seeking_word_end && byte == 0)
      {
        is_first_space_after_word = 1;
      }
      
      // rjf: determine properties of this advance
      B32 is_illegal = (off_px >= max);
      B32 is_next_illegal = (off_px + advance >= max);
      B32 is_end = (byte == 0);
      
      // rjf: legal word end -> extend line
      if(is_first_space_after_word && !is_illegal)
      {
        line_end_off_bytes = off_bytes;
      }
      
      // rjf: illegal mid-word split -> wrap mid-word
      if(is_next_illegal && word_start_off_px == 0)
      {
        String8 line = str8(string.str + line_start_off_bytes, off_bytes - line_start_off_bytes);
        line = str8_skip_chop_whitespace(line);
        if(line.size != 0)
        {
          str8_list_push(arena, &list, line);
        }
        off_px = advance;
        line_start_off_bytes = off_bytes;
        line_end_off_bytes = off_bytes;
        word_start_off_px = 0;
        last_word_start_piece = piece;
        last_word_start_off_bytes = off_bytes;
        off_bytes += decode_size;
      }
      
      // rjf: illegal word end -> wrap line
      else if(is_first_space_after_word && (is_illegal || is_end))
      {
        String8 line = str8(string.str + line_start_off_bytes, line_end_off_bytes - line_start_off_bytes);
        line = str8_skip_chop_whitespace(line);
        if(line.size != 0)
        {
          str8_list_push(arena, &list, line);
        }
        line_start_off_bytes = line_end_off_bytes;
        if(is_illegal)
        {
          off_px = 0;
          word_start_off_px = 0;
          off_bytes = last_word_start_off_bytes;
          next = last_word_start_piece;
        }
      }
      
      // rjf: advance offsets otherwise
      else
      {
        off_px += advance;
        off_bytes += decode_size;
      }
      
      // rjf: 0 piece and 0 next -> done
      if(piece == 0 && next == 0)
      {
        break;
      }
    }
    scratch_end(scratch);
  }
  return list;
}

internal Vec2F32
fnt_dim_from_tag_size_string(FNT_Tag tag, F32 size, F32 base_align_px, F32 tab_size_px, String8 string)
{
  ProfBeginFunction();
  Temp scratch = scratch_begin(0, 0);
  Vec2F32 result = {0};
  FNT_Run run = fnt_push_run_from_string(scratch.arena, tag, size, base_align_px, tab_size_px, 0, string);
  result = run.dim;
  scratch_end(scratch);
  ProfEnd();
  return result;
}

internal Vec2F32
fnt_dim_from_tag_size_string_list(FNT_Tag tag, F32 size, F32 base_align_px, F32 tab_size_px, String8List list)
{
  ProfBeginFunction();
  Vec2F32 sum = {0};
  for(String8Node *n = list.first; n != 0; n = n->next)
  {
    Vec2F32 str_dim = fnt_dim_from_tag_size_string(tag, size, base_align_px, tab_size_px, n->string);
    sum.x += str_dim.x;
    sum.y = Max(sum.y, str_dim.y);
  }
  ProfEnd();
  return sum;
}

internal F32
fnt_column_size_from_tag_size(FNT_Tag tag, F32 size)
{
  F32 result = fnt_dim_from_tag_size_string(tag, size, 0, 0, str8_lit("H")).x;
  return result;
}

internal U64
fnt_char_pos_from_tag_size_string_p(FNT_Tag tag, F32 size, F32 base_align_px, F32 tab_size_px, String8 string, F32 p)
{
  Temp scratch = scratch_begin(0, 0);
  U64 best_offset_bytes = 0;
  F32 best_offset_px = inf32();
  U64 offset_bytes = 0;
  F32 offset_px = 0.f;
  FNT_Run run = fnt_push_run_from_string(scratch.arena, tag, size, base_align_px, tab_size_px, 0, string);
  for(U64 idx = 0; idx <= run.pieces.count; idx += 1)
  {
    F32 this_piece_offset_px = abs_f32(offset_px - p);
    if(this_piece_offset_px < best_offset_px)
    {
      best_offset_bytes = offset_bytes;
      best_offset_px = this_piece_offset_px;
    }
    if(idx < run.pieces.count)
    {
      FNT_Piece *piece = &run.pieces.v[idx];
      offset_px += piece->advance;
      offset_bytes += piece->decode_size;
    }
  }
  scratch_end(scratch);
  return best_offset_bytes;
}

////////////////////////////////
//~ rjf: Metrics

internal FNT_Metrics
fnt_metrics_from_tag_size(FNT_Tag tag, F32 size)
{
  ProfBeginFunction();
  FP_Metrics metrics = fnt_fp_metrics_from_tag(tag);
  FNT_Metrics result = {0};
  {
    result.ascent   = floor_f32(size) * metrics.ascent / metrics.design_units_per_em;
    result.descent  = floor_f32(size) * metrics.descent / metrics.design_units_per_em;
    result.line_gap = floor_f32(size) * metrics.line_gap / metrics.design_units_per_em;
    result.capital_height = floor_f32(size) * metrics.capital_height / metrics.design_units_per_em;
  }
  ProfEnd();
  return result;
}

internal F32
fnt_line_height_from_metrics(FNT_Metrics *metrics)
{
  return metrics->ascent + metrics->descent + metrics->line_gap;
}

////////////////////////////////
//~ rjf: Main Calls

internal void
fnt_init(void)
{
  Arena *arena = arena_alloc();
  f_state = push_array(arena, FNT_State, 1);
  f_state->arena = arena;
  f_state->font_hash_table_size = 64;
  f_state->font_hash_table = push_array(arena, FNT_FontHashSlot, f_state->font_hash_table_size);
  f_state->hash2style_slots_count = 1024;
  f_state->hash2style_slots = push_array(arena, FNT_Hash2StyleRasterCacheSlot, f_state->hash2style_slots_count);
}
