
// SPDX-License-Identifier: CC-BY-NC-SA-4.0
//
// Copyright (C) 2026 Bit by Bit Signal Processing LLC (https://bxbsp.com)
//
// This work is placed under the "Creative Commons Attribution
// NonCommercial ShareAlike 4.0 International" license, known
// by the shortened acronym "CC-BY-NC-SA-4.0".
//
// This work is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// A CC-BY-NC-SA-4.0 license allows you to use this work for
// noncommercial purposes so long as attribution is made to the
// original author.  Modified versions of this work may be distributed,
// but only under the same license.  For further details, see the
// Creative Commons License "CC-BY-NC-SA-4.0".
//
// You should have received a copy of the CC-BY-NC-SA-4.0 license
// along with this work. If not, see
// <https://creativecommons.org/licenses/by-nc-sa/4.0/>.
//

#include "draw_text.hh"
#include "fontlist.hh"

#include <ft2build.h>
#include FT_FREETYPE_H
#include <freetype/ftcolor.h>
#include <math.h>
#include "panel.hh"
#include <harfbuzz/hb.h>
#include <harfbuzz/hb-ft.h>
#include "unalingua.hh"

class draw_text
{  
public:
  draw_text();

  bool      font_has_character(font* f, char32_t c);
  font*     get_font_for_character(char32_t c, font* f = 0);
  FT_Face*  get_face_for_character(char32_t c, font* f = 0);

  float draw_segment(const char32_t* text, FT_Face* face, color c, panel*p, int x, int y, int x_offset, int y_offset, int flags=0 );

  float calculate_segment_width(const char32_t* text, FT_Face* face);
  void draw_multicolored(const char32_t* text, color* c, int start_color, panel* p, int x, int y, int flags = 0);

  int calculate_width(const char32_t* text);
  int calculate_height(const char32_t* text);

  void set_size(int height_pixels);
  int  get_size();
  ~draw_text();

private:
  int height;
  int bitmap_height;

  struct FT_LibraryRec_**  library;
  
  void draw_bitmap_into_panel(struct FT_Bitmap_* bitmap, color c, panel* p, int x, int y);
  void draw_bitmap_into_panel_rotate_90_right(struct FT_Bitmap_* bitmap, color c, panel* p, int x, int y);
  void draw_bitmap_into_panel_rotate_90_left(struct FT_Bitmap_* bitmap, color c, panel* p, int x, int y);
  void draw_color_bitmap_into_panel(struct FT_Bitmap_* bitmap, panel* p, int x, int y, float scale);
  void draw_color_bitmap_into_panel_rotate_90_right(struct FT_Bitmap_* bitmap, panel* p, int x, int y, float scale);
  void draw_color_bitmap_into_panel_rotate_90_left(struct FT_Bitmap_* bitmap, panel* p, int x, int y, float scale);
};


//
// I named a function draw_text and also a class draw_text.  Need to disambiguate here.
//
static class draw_text dt;


void global_set_text_size(int height_pixels)
{
  dt.set_size(height_pixels);
}


int global_get_text_size()
{
  return dt.get_size();
}



void draw_text::set_size(int height_pixels)
{
  height = height_pixels;
}

int draw_text::get_size()
{
  return height;
}

draw_text::draw_text()
{
  height = 16;
  
  library = new FT_Library;
  
  FT_Error error;
  
  error = FT_Init_FreeType( library );
  if (error)
    {
      printf("Error:  Freetype2 failed to initialize.\n");
      exit(20);
    }
}


draw_text::~draw_text()
{
  delete library;
}

void draw_text::draw_bitmap_into_panel(FT_Bitmap* bitmap, color c, panel* panel, int x, int y)
{
  for(int i=0; i<(int)bitmap->width; i++)
    {
      for(int j=0; j<(int)bitmap->rows; j++)
	{
	  point p(x+i,y+j);
	  float frac = bitmap->buffer[j * bitmap->width + i] / 255.0;
	  panel->draw_point_antialiased(p, c, frac);
	}
    }
}


void draw_text::draw_bitmap_into_panel_rotate_90_right(FT_Bitmap* bitmap, color c, panel* panel, int x, int y)
{
  for(int i=0; i<(int)bitmap->width; i++)
    {
      for(int j=0; j<(int)bitmap->rows; j++)
	{
	  point p(x-j,y+i);
	  float frac = bitmap->buffer[j * bitmap->width + i] / 255.0;
	  panel->draw_point_antialiased(p, c, frac);
	}
    }
}


void draw_text::draw_bitmap_into_panel_rotate_90_left(FT_Bitmap* bitmap, color c, panel* panel, int x, int y)
{
  for(int i=0; i<(int)bitmap->width; i++)
    {
      for(int j=0; j<(int)bitmap->rows; j++)
	{
	  point p(x+j,y-i);
	  float frac = bitmap->buffer[j * bitmap->width + i] / 255.0;
	  panel->draw_point_antialiased(p, c, frac);
	}
    }
}




void draw_text::draw_color_bitmap_into_panel(FT_Bitmap* bitmap, panel* panel, int x, int y, float scale)
{
  FT_Color* array = (FT_Color*) bitmap->buffer;
  for(int i=0; i<(int)bitmap->width; i++)
    {
      for(int j=0; j<(int)bitmap->rows; j++)
	{
	  point p(int(floor(x+i*scale)),int(floor(y+j*scale)));
	  FT_Color ftc = array[j * bitmap->width + i];
	  color c = color_from_floats(ftc.red/255.0,ftc.green/255.0,ftc.blue/255.0);
	  float a = ftc.alpha/255.0;
	  panel->draw_point_antialiased(p, c, a);
	}
    }
}



void draw_text::draw_color_bitmap_into_panel_rotate_90_right(FT_Bitmap* bitmap, panel* panel, int x, int y, float scale)
{
  FT_Color* array = (FT_Color*) bitmap->buffer;
  for(int i=0; i<(int)bitmap->width; i++)
    {
      for(int j=0; j<(int)bitmap->rows; j++)
	{
	  point p(floor(x-j*scale),floor(y+i*scale));
	  FT_Color ftc = array[j * bitmap->width + i];
	  color c = color_from_floats(ftc.red/255.0,ftc.green/255.0,ftc.blue/255.0);
	  float a = ftc.alpha/255.0;
	  panel->draw_point_antialiased(p, c, a);
	}
    }
}



void draw_text::draw_color_bitmap_into_panel_rotate_90_left(FT_Bitmap* bitmap, panel* panel, int x, int y, float scale)
{
  FT_Color* array = (FT_Color*) bitmap->buffer;
  for(int i=0; i<(int)bitmap->width; i++)
    {
      for(int j=0; j<(int)bitmap->rows; j++)
	{
	  point p(floor(x+j*scale),floor(y-i*scale));
	  FT_Color ftc = array[j * bitmap->width + i];
	  color c = color_from_floats(ftc.red/255.0,ftc.green/255.0,ftc.blue/255.0);
	  float a = ftc.alpha/255.0;
	  panel->draw_point_antialiased(p, c, a);
	}
    }
}


bool draw_text::font_has_character(font* f, char32_t ch)
{
  for(int i=0;;i++)
    {	    
      if(ch<=f->codes[i].end && ch>=f->codes[i].start)
        {
          // This font has a mapping for this unicode
          return true;
        }
      if(f->codes[i].end==0)
        break;
    }
  
  return false;
}

font* draw_text::get_font_for_character(char32_t ch, font* ff)
{
  if(ff && font_has_character(ff, ch))
    return ff;
  
  int  font_num = -1;
  bool got_font = false;
  font* f = 0;

  // First, find a face that can draw this unicode character
  // and that is present on this system from our list.
  while(!got_font)
    {
      font_num++;

      f = &fontlist[font_num];

      if(f->face==(FT_Face*)-1)
	{
	  // Already tried to load this font, and it wouldn't load.
	  continue;
	}
      
      //printf("Trying font_num %d, \"%s\".\n", font_num, f->name);	    

      if(f->name==0)
	{
	  // Got to the end of the list.  No font has the unicode.  Just use the first font.
	  font_num = -1;  // This flags the case where no font matches
	  f = &fontlist[0];
	  got_font = true;
	  //printf("No font found for unicode character.  Using default font.\n");
	}
      else
	{
          bool this_font_has_it = font_has_character(f, ch);

	  if(!this_font_has_it)
	    continue;
	}
      
      // Next, load it if it isn't loaded
      if(f->face)
	{
	  //printf("Character %lld is in font \"%s\".  Already loaded.\n", (long long)ch, f->name);
	  break;
	}
      else
	{
	  //printf("Character %lld is in font \"%s\".  Attempting to load the font.\n", (long long)ch, f->name);
	  got_font = false;
	  f->face    = new FT_Face;
	  int error = FT_New_Face(*library,
				  f->name,
				  0,
				  f->face);

	  //printf("Load of font \"%s\" returned %s\n", f->name, error?"FAILURE":"SUCCESS");
	  
	  if(error)
	    {
	      delete f->face;
	      f->face = (FT_Face*)-1;  // Flag that this font doesn't load
	      
	      //printf("Error loading font \"%s\"\n", f->name);
	      
	      // Base font is missing, and no other font has the character.  Abort.
	      if(font_num==-1)
		{
		  printf("ERROR:  Can't find any suitable font.\n");
		  exit(0);
		}
	      
	      // Font is probably missing from this system.  Try some other font, that may be present
	      continue;
	    }
	  else
	    {
	      got_font = true;
	    }
	}
    }

  return f;
}


FT_Face* draw_text::get_face_for_character(char32_t c, font* f)
{
  if(!f)
    f = get_font_for_character(c);

  //printf("Got font %s for %ld\n", f->name, (long)(char32_t)c);
  
  // Next, set the height if it isn't the current height
  
  if(f->height!=height)
    {
      //height=109;
      f->height = height;
      f->bitmap_height = height;
      int error = FT_Set_Pixel_Sizes(*f->face,   /* handle to face object */
				     0,          /* pixel_width           */
				     height );   /* pixel_height          */

      if(error)
	{
	  if((*f->face)->num_fixed_sizes == 0)
	    {
	      printf("Error setting size of font \"%s\" to %d\n", f->name, height);
	      printf("ERROR: Font also has no fixed sizes.\n");
	      exit(20);
	    }
	  
	  //printf("Font \"%s\" has %d sizes:\n", f->name, (*f->face)->num_fixed_sizes);
	  int best_match = 0;
	  int best_height = (*f->face)->available_sizes[0].height;
	  //printf("   Size %d\n", best_height);
	  int diff = std::abs(height - best_height);
	  for(int i=1; i<(*f->face)->num_fixed_sizes; ++i)
	    {
	      int new_height = (*f->face)->available_sizes[i].height;
	      //printf("   Size %d\n", new_height);
	      int ndiff = std::abs(height - new_height);
	      if(ndiff<diff)
		{
		  best_height = new_height;
		  best_match = i;
		  diff = ndiff;
		}
	    }

	  //printf("For font \"%s\", can't get size %d, so using size %d\n", f->name, height, best_height);

	  f->bitmap_height = best_height;
	  error = FT_Select_Size(*f->face, best_match);

	  if(error)
	    {
	      printf("ERROR: Can't select font size that font \"%s\" says it has.\n", f->name);
	      exit(20);
	    }
	}
    }

  bitmap_height = f->bitmap_height;

  return f->face;
}


float draw_text::draw_segment(const char32_t* text, FT_Face* face, color c, panel* p, int x, int y, int x_offset, int y_offset, int rotate_flags)
{
  // For some reason the font I tested didn't seem to be centered vertically.  Fix that the best way I know how.
  // It might be an issue with that one font, in which case this is bad.
  int centering_adjustment = 3 * height / 16 - 2;

  
  hb_font_t*    hb_font   = hb_ft_font_create(*face, NULL);
  //hb_font_t*    hb_font   = hb_ft_font_create_referenced(*face);
  hb_buffer_t*  hb_buffer = hb_buffer_create();

  hb_buffer_add_utf32(hb_buffer, (uint32_t*)text, -1, 0, -1);
  hb_buffer_guess_segment_properties(hb_buffer);
  hb_shape(hb_font, hb_buffer, NULL, 0);

  int                   num_glyphs = hb_buffer_get_length(hb_buffer);
  hb_glyph_info_t*      info       = hb_buffer_get_glyph_infos(hb_buffer, NULL);
  hb_glyph_position_t*  pos        = hb_buffer_get_glyph_positions(hb_buffer, NULL);
  
  FT_GlyphSlot  slot  = (*face)->glyph;  /* a small shortcut */
  float         scale = height / (float)slot->bitmap_top;

  float segment_width = 0;

  bool has_color = FT_HAS_COLOR(*face);
  
  for(int n=0; n<num_glyphs; n++)
    {
      hb_codepoint_t glyph_id = info[n].codepoint;
	 
      if(has_color)
	{
	  segment_width += pos[n].x_advance*scale;
	  
	  /* load glyph image into the slot (erase previous one) */
	  int error = FT_Load_Glyph(*face, glyph_id, FT_LOAD_COLOR|FT_LOAD_DEFAULT);
	  if(!error)
	    {
	      if(rotate_flags==DRAW_TEXT_ROTATE_90_RIGHT)
		{
		  draw_color_bitmap_into_panel_rotate_90_right(&slot->bitmap, p, x - pos[n].y_offset/64.0 - y_offset + slot->bitmap_top*scale + centering_adjustment, y + pos[n].x_offset/64.0 + x_offset + slot->bitmap_left*scale, scale);
		  y += int(floor(pos[n].x_advance*scale)) >> 6;
		}
	      else if(rotate_flags==DRAW_TEXT_ROTATE_90_LEFT)
		{
		  draw_color_bitmap_into_panel_rotate_90_left(&slot->bitmap, p, x + pos[n].y_offset/64.0 + y_offset - slot->bitmap_top*scale - centering_adjustment, y - pos[n].x_offset/64.0 - x_offset - slot->bitmap_left*scale, scale);
		  y -= int(floor(pos[n].x_advance*scale)) >> 6;
		}
	      else
		{
		  draw_color_bitmap_into_panel(&slot->bitmap, p, pos[n].x_offset/64.0 + x + x_offset + slot->bitmap_left*scale, pos[n].y_offset/64.0 + y + y_offset - slot->bitmap_top*scale - centering_adjustment, scale);
		  x += int(floor(pos[n].x_advance*scale)) >> 6;
		}
	    }
	}
      else
	{
	  segment_width += pos[n].x_advance;

	  /* load glyph image into the slot (erase previous one) */
	  int error = FT_Load_Glyph(*face, glyph_id, FT_LOAD_RENDER);
	  if(!error)
	    {
	      if(rotate_flags==DRAW_TEXT_ROTATE_90_RIGHT)
		{
		  draw_bitmap_into_panel_rotate_90_right(&slot->bitmap, c, p, x - pos[n].y_offset/64.0 - y_offset + slot->bitmap_top + centering_adjustment, y + pos[n].x_offset/64.0 + x_offset + slot->bitmap_left);
		  y += pos[n].x_advance >> 6;
		}
	      else if(rotate_flags==DRAW_TEXT_ROTATE_90_LEFT)
		{
		  draw_bitmap_into_panel_rotate_90_left(&slot->bitmap, c, p, x + pos[n].y_offset/64.0 + y_offset - slot->bitmap_top - centering_adjustment, y - pos[n].x_offset/64.0 - x_offset - slot->bitmap_left);
		  y -= pos[n].x_advance >> 6;
		}
	      else
		{
		  draw_bitmap_into_panel(&slot->bitmap, c, p, pos[n].x_offset/64.0 + x + x_offset + slot->bitmap_left, pos[n].y_offset/64.0 + y + y_offset - slot->bitmap_top - centering_adjustment);
		  x += pos[n].x_advance >> 6;
		}
	    }
	}
    }

  return segment_width / 64.0;
}



void draw_text::draw_multicolored(const char32_t* text, color* colors, int start_color, panel* p, int x, int y, int flags)
{
  int x_flags      = flags & DRAW_TEXT_X_MASK;
  int y_flags      = flags & DRAW_TEXT_Y_MASK;
  int rotate_flags = flags & DRAW_TEXT_ROTATE_MASK;
  
  int x_offset = ( ( x_flags == DRAW_TEXT_X_LEFT  )  ?   0                          :
		   ( x_flags == DRAW_TEXT_X_RIGHT )  ?  -calculate_width(text)      :
		   /* center */                         -calculate_width(text) / 2  );
  
  int y_offset = ( ( y_flags == DRAW_TEXT_Y_TOP  )   ?  calculate_height(text)     :
		   ( y_flags == DRAW_TEXT_Y_BOTTOM ) ?  0                          :
		   /* center */                         calculate_height(text)/2   );

  int start_position = 0;
  char32_t buffer[1000];

  int current_color_index = start_color;

  for(;;)
    {
      if(!text[start_position])
	break;

      if(text[start_position]>=10 && text[start_position]<32)
	{
	  current_color_index = text[start_position]-10;
	  start_position++;
	  continue;
	}
      
      font* f = get_font_for_character(text[start_position]);
      buffer[0] = text[start_position];
      FT_Face* face = get_face_for_character(buffer[0], f);
      bool has_color = FT_HAS_COLOR(*face);

      int n;
      for(n=1;;n++)
	{
	  buffer[n] = text[start_position+n];

	  if(!text[start_position+n])
	    break;

	  if(buffer[n]>=10 && buffer[n]<32)
	    break;
	  
	  if(text[start_position+n]==' ' && !has_color)
	    {
	      // Render spaces in the same font as precedes them.
	      // However, the emoji font seems to mess up rendering
	      // of spaces.  It is the only color font (I think),
	      // so for it, don't include any spaces.
	      continue;
	    }
	  
	  if(text[start_position+n]>='0' && text[start_position+n]<='9')
	    continue;
	  
	  font* f2 = get_font_for_character(text[start_position+n], f);

	  if(f==f2)
	    continue;

	  break;
	}


      // Render spaces between font changes only in the default font
      // Can do this by removing trailing spaces unless they're an
      // all-space segment.  All-space segments will then render in
      // their default font.
      if(buffer[0] != ' ')
	{
	  while(buffer[n-1]==' ')
	    n--;
	}

      buffer[n] = 0;
      start_position += n;


      color c = colors[current_color_index];

      float length = draw_segment(buffer, face, c, p, x, y, x_offset, y_offset, rotate_flags);
      x_offset += length;
    } 
}



float draw_text::calculate_segment_width(const char32_t* text, FT_Face* face)
{
  hb_font_t*    hb_font   = hb_ft_font_create(*face, NULL);
  //hb_font_t*    hb_font   = hb_ft_font_create_referenced(*face);
  hb_buffer_t*  hb_buffer = hb_buffer_create();

  hb_buffer_add_utf32(hb_buffer, (uint32_t*)text, -1, 0, -1);
  hb_buffer_guess_segment_properties(hb_buffer);
  hb_shape(hb_font, hb_buffer, NULL, 0);

  int                   num_glyphs = hb_buffer_get_length(hb_buffer);
  hb_glyph_info_t*      info       = hb_buffer_get_glyph_infos(hb_buffer, NULL);
  hb_glyph_position_t*  pos        = hb_buffer_get_glyph_positions(hb_buffer, NULL);

  int error = FT_Load_Glyph(*face, info[0].codepoint, FT_LOAD_DEFAULT);
  if(error)
    {
      printf("Error loading glyph for font sizing.\n");
    }
  
  FT_GlyphSlot  slot = (*face)->glyph;  /* a small shortcut */
  float         scale = height / (float)slot->bitmap_top;

  float segment_width = 0;

  bool has_color = FT_HAS_COLOR(*face);
  
  for(int n=0; n<num_glyphs; n++)
    {
      if(has_color)
	segment_width += pos[n].x_advance*scale;
      else
	segment_width += pos[n].x_advance;
    }

  return segment_width / 64.0;
}



int draw_text::calculate_width(const char32_t* text)
{
  float text_width = 0;

  int start_position = 0;
  char32_t buffer[1000];

  for(;;)
    {
      if(!text[start_position])
	break;

      if(text[start_position]>=10 && text[start_position]<32)
	{
	  start_position++;
	  continue;
	}
      
      font* f = get_font_for_character(text[start_position]);
      buffer[0] = text[start_position];
      FT_Face* face = get_face_for_character(buffer[0]);
      bool has_color = FT_HAS_COLOR(*face);

      int n;
      for(n=1;;n++)
	{
	  buffer[n] = text[start_position+n];

	  if(!text[start_position+n])
	    break;

	  if(buffer[n]>=10 && buffer[n]<32)
	    break;
	  
	  if(text[start_position+n]==' ' && !has_color)
	    {
	      // Render spaces in the same font as precedes them.
	      // However, the emoji font seems to mess up rendering
	      // of spaces.  It is the only color font (I think),
	      // so for it, don't include any spaces.
	      continue;
	    }
	  
	  if(text[start_position+n]>='0' && text[start_position+n]<='9')
	    continue;
	  
	  font* f2 = get_font_for_character(text[start_position+n]);

	  if(f==f2)
	    continue;

	  break;
	}


      // Render spaces between font changes only in the default font
      // Can do this by removing trailing spaces unless they're an
      // all-space segment.  All-space segments will then render in
      // their default font.
      if(buffer[0] != ' ')
	{
	  while(buffer[n-1]==' ')
	    n--;
	}

      buffer[n] = 0;
      start_position += n;

      float length = calculate_segment_width(buffer, face);

      text_width += length;
    }

  return (int)text_width;
}


int draw_text::calculate_height(const char32_t* text)
{
  return height;
}



template <typename T> void global_draw_text(const T* text, color c, panel* p, int x, int y, int flags)
{
  char32_t* t = new_UTF32(text);
  dt_colors cc;
  cc.c[0] = c;
  dt.draw_multicolored(t, cc.c, 0, p, x, y, flags);
  delete [] t;
}

template <typename T> void global_draw_multicolored_text(const T* text, dt_colors& c, int start_color, panel* p, int x, int y, int flags)
{
  char32_t* t = new_UTF32(text);
  dt.draw_multicolored(t, c.c, start_color, p, x, y, flags);
  delete [] t;
}

template <class T> int global_calculate_text_width(const T* text)
{
  char32_t* t = new_UTF32(text);
  int w = dt.calculate_width(t);
  delete [] t;
  return w;
}

template <class T> int global_calculate_text_height(const T* text)
{
  char32_t* t = new_UTF32(text);
  int h = dt.calculate_height(t);
  delete [] t;
  return h;
}


template void global_draw_text<char>(const char* text, color c, panel* p, int x, int y, int flags);
template void global_draw_text<wchar_t>(const wchar_t* text, color c, panel* p, int x, int y, int flags);
template void global_draw_text<char16_t>(const char16_t* text, color c, panel* p, int x, int y, int flags);
template void global_draw_text<char32_t>(const char32_t* text, color c, panel* p, int x, int y, int flags);

template void global_draw_multicolored_text<char>(const char* text, dt_colors& c, int start_color, panel* p, int x, int y, int flags);
template void global_draw_multicolored_text<wchar_t>(const wchar_t* text, dt_colors& c, int start_color, panel* p, int x, int y, int flags);
template void global_draw_multicolored_text<char16_t>(const char16_t* text, dt_colors& c, int start_color, panel* p, int x, int y, int flags);
template void global_draw_multicolored_text<char32_t>(const char32_t* text, dt_colors& c, int start_color, panel* p, int x, int y, int flags);

template int global_calculate_text_width<char>     (const char* text);
template int global_calculate_text_width<wchar_t>  (const wchar_t* text);
template int global_calculate_text_width<char16_t> (const char16_t* text);
template int global_calculate_text_width<char32_t> (const char32_t* text);

template int global_calculate_text_height<char>     (const char* text);
template int global_calculate_text_height<wchar_t>  (const wchar_t* text);
template int global_calculate_text_height<char16_t> (const char16_t* text);
template int global_calculate_text_height<char32_t> (const char32_t* text);
