#include "hbapi.h"
#include "hbapiitm.h"

#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define STBI_NO_HDR
#define STBI_NO_LINEAR
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION
#define STB_TRUETYPE_IMPLEMENTATION
#include "vendor/stb/stb_image.h"
#include "vendor/stb/stb_image_write.h"
#include "vendor/stb/stb_truetype.h"

#define LLIMG_MAGIC 0x4c4c494du
#define LLFONT_MAGIC 0x4c4c464eu
#define LL_PI 3.14159265358979323846

typedef struct {
   uint32_t magic;
   int width;
   int height;
   unsigned char *pixels;
} LLImage;

typedef struct {
   uint32_t magic;
   unsigned char *data;
   size_t size;
   stbtt_fontinfo info;
} LLFont;

static int s_active_images = 0;

static LLImage * ll_image( int param )
{
   LLImage *image = ( LLImage * ) hb_parptr( param );
   return image && image->magic == LLIMG_MAGIC ? image : NULL;
}

static LLFont * ll_font( int param )
{
   LLFont *font = ( LLFont * ) hb_parptr( param );
   return font && font->magic == LLFONT_MAGIC ? font : NULL;
}

static uint32_t ll_color( int param )
{
   return ( uint32_t ) hb_parnint( param );
}

static double ll_clamp( double value, double minimum, double maximum )
{
   return value < minimum ? minimum : ( value > maximum ? maximum : value );
}

HB_FUNC( LLIMG_SIN ) { hb_retnd( sin( hb_parnd( 1 ) ) ); }
HB_FUNC( LLIMG_COS ) { hb_retnd( cos( hb_parnd( 1 ) ) ); }
HB_FUNC( LLIMG_FLOOR ) { hb_retnd( floor( hb_parnd( 1 ) ) ); }

static void ll_blend( LLImage *image, int x, int y, uint32_t color, double coverage )
{
   unsigned char *dst;
   double sa, da, oa;
   double sr, sg, sb, dr, dg, db;

   if( !image || x < 0 || y < 0 || x >= image->width || y >= image->height || coverage <= 0 )
      return;

   dst = image->pixels + ( ( size_t ) y * image->width + x ) * 4;
   sa = ( ( color & 0xffu ) / 255.0 ) * ll_clamp( coverage, 0, 1 );
   if( sa <= 0 )
      return;
   da = dst[ 3 ] / 255.0;
   oa = sa + da * ( 1.0 - sa );
   sr = ( ( color >> 24 ) & 0xffu ) / 255.0;
   sg = ( ( color >> 16 ) & 0xffu ) / 255.0;
   sb = ( ( color >> 8 ) & 0xffu ) / 255.0;
   dr = dst[ 0 ] / 255.0;
   dg = dst[ 1 ] / 255.0;
   db = dst[ 2 ] / 255.0;
   if( oa > 0 ) {
      dst[ 0 ] = ( unsigned char ) floor( ( sr * sa + dr * da * ( 1.0 - sa ) ) / oa * 255.0 + 0.5 );
      dst[ 1 ] = ( unsigned char ) floor( ( sg * sa + dg * da * ( 1.0 - sa ) ) / oa * 255.0 + 0.5 );
      dst[ 2 ] = ( unsigned char ) floor( ( sb * sa + db * da * ( 1.0 - sa ) ) / oa * 255.0 + 0.5 );
      dst[ 3 ] = ( unsigned char ) floor( oa * 255.0 + 0.5 );
   }
}

static void ll_rotate_point( double x, double y, double cx, double cy, double angle, double *rx, double *ry )
{
   double radians = angle * LL_PI / 180.0;
   double cosine = cos( radians );
   double sine = sin( radians );
   double dx = x - cx;
   double dy = y - cy;
   *rx = cx + cosine * dx - sine * dy;
   *ry = cy + sine * dx + cosine * dy;
}

static void ll_inverse_point( double x, double y, double cx, double cy, double angle, double *rx, double *ry )
{
   ll_rotate_point( x, y, cx, cy, -angle, rx, ry );
}

static void ll_rotated_bounds( double x, double y, double width, double height, double angle, double cx, double cy,
                               int *left, int *top, int *right, int *bottom )
{
   double px[ 4 ], py[ 4 ];
   double minx, miny, maxx, maxy;
   int i;
   ll_rotate_point( x, y, cx, cy, angle, &px[ 0 ], &py[ 0 ] );
   ll_rotate_point( x + width, y, cx, cy, angle, &px[ 1 ], &py[ 1 ] );
   ll_rotate_point( x + width, y + height, cx, cy, angle, &px[ 2 ], &py[ 2 ] );
   ll_rotate_point( x, y + height, cx, cy, angle, &px[ 3 ], &py[ 3 ] );
   minx = maxx = px[ 0 ]; miny = maxy = py[ 0 ];
   for( i = 1; i < 4; ++i ) {
      if( px[ i ] < minx ) minx = px[ i ];
      if( px[ i ] > maxx ) maxx = px[ i ];
      if( py[ i ] < miny ) miny = py[ i ];
      if( py[ i ] > maxy ) maxy = py[ i ];
   }
   *left = ( int ) floor( minx - 1 ); *top = ( int ) floor( miny - 1 );
   *right = ( int ) ceil( maxx + 1 ); *bottom = ( int ) ceil( maxy + 1 );
}

static double ll_rounded_rect_inside( double px, double py, double x, double y, double width, double height, double radius )
{
   double nearestX, nearestY, dx, dy;
   if( px < x || py < y || px > x + width || py > y + height )
      return 0;
   radius = ll_clamp( radius, 0, fmin( width, height ) / 2.0 );
   if( radius <= 0 )
      return 1;
   nearestX = ll_clamp( px, x + radius, x + width - radius );
   nearestY = ll_clamp( py, y + radius, y + height - radius );
   dx = px - nearestX; dy = py - nearestY;
   return dx * dx + dy * dy <= radius * radius ? 1 : 0;
}

HB_FUNC( LLIMG_CREATE )
{
   int width = hb_parni( 1 );
   int height = hb_parni( 2 );
   LLImage *image;
   size_t bytes;
   if( width < 1 || height < 1 || ( size_t ) width > SIZE_MAX / 4 / ( size_t ) height ) {
      hb_retptr( NULL );
      return;
   }
   bytes = ( size_t ) width * height * 4;
   image = ( LLImage * ) calloc( 1, sizeof( LLImage ) );
   if( !image ) { hb_retptr( NULL ); return; }
   image->pixels = ( unsigned char * ) calloc( 1, bytes );
   if( !image->pixels ) { free( image ); hb_retptr( NULL ); return; }
   image->magic = LLIMG_MAGIC; image->width = width; image->height = height;
   ++s_active_images;
   hb_retptr( image );
}

HB_FUNC( LLIMG_FREE )
{
   LLImage *image = ll_image( 1 );
   if( image ) {
      image->magic = 0;
      free( image->pixels );
      image->pixels = NULL;
      free( image );
      --s_active_images;
   }
}

HB_FUNC( LLIMG_ACTIVE_CANVASES ) { hb_retni( s_active_images ); }

HB_FUNC( LLIMG_SAVE_PNG )
{
   LLImage *image = ll_image( 1 );
   const char *file = hb_parc( 2 );
   /* Nivel 1 mantiene PNG sin pérdida y evita que la compresión monopolice la exportación. */
   stbi_write_png_compression_level = 1;
   stbi_write_force_png_filter = 0;
   hb_retl( image && file && *file && stbi_write_png( file, image->width, image->height, 4, image->pixels, image->width * 4 ) != 0 );
}

static unsigned char * ll_flatten_rgb( LLImage *image, uint32_t background )
{
   unsigned char *rgb;
   size_t count, index;
   unsigned int br, bg, bb;
   if( !image ) return NULL;
   count = ( size_t ) image->width * image->height;
   if( count > SIZE_MAX / 3 ) return NULL;
   rgb = ( unsigned char * ) malloc( count * 3 );
   if( !rgb ) return NULL;
   br = ( background >> 24 ) & 0xffu;
   bg = ( background >> 16 ) & 0xffu;
   bb = ( background >> 8 ) & 0xffu;
   for( index = 0; index < count; ++index ) {
      const unsigned char *source = image->pixels + index * 4;
      unsigned char *target = rgb + index * 3;
      unsigned int alpha = source[ 3 ];
      target[ 0 ] = ( unsigned char ) ( ( source[ 0 ] * alpha + br * ( 255 - alpha ) + 127 ) / 255 );
      target[ 1 ] = ( unsigned char ) ( ( source[ 1 ] * alpha + bg * ( 255 - alpha ) + 127 ) / 255 );
      target[ 2 ] = ( unsigned char ) ( ( source[ 2 ] * alpha + bb * ( 255 - alpha ) + 127 ) / 255 );
   }
   return rgb;
}

HB_FUNC( LLIMG_SAVE_JPG )
{
   LLImage *image = ll_image( 1 );
   const char *file = hb_parc( 2 );
   int quality = hb_parni( 3 );
   unsigned char *rgb;
   int result;
   if( !image || !file || !*file || quality < 1 || quality > 100 ) { hb_retl( HB_FALSE ); return; }
   rgb = ll_flatten_rgb( image, ll_color( 4 ) );
   if( !rgb ) { hb_retl( HB_FALSE ); return; }
   result = stbi_write_jpg( file, image->width, image->height, 3, rgb, quality );
   free( rgb );
   hb_retl( result != 0 );
}

HB_FUNC( LLIMG_SAVE_BMP )
{
   LLImage *image = ll_image( 1 );
   const char *file = hb_parc( 2 );
   unsigned char *rgb;
   int result;
   if( !image || !file || !*file ) { hb_retl( HB_FALSE ); return; }
   rgb = ll_flatten_rgb( image, ll_color( 3 ) );
   if( !rgb ) { hb_retl( HB_FALSE ); return; }
   result = stbi_write_bmp( file, image->width, image->height, 3, rgb );
   free( rgb );
   hb_retl( result != 0 );
}

HB_FUNC( LLIMG_PIXEL )
{
   LLImage *image = ll_image( 1 );
   int x = hb_parni( 2 ), y = hb_parni( 3 );
   PHB_ITEM result = hb_itemArrayNew( 4 );
   if( image && x >= 0 && y >= 0 && x < image->width && y < image->height ) {
      unsigned char *pixel = image->pixels + ( ( size_t ) y * image->width + x ) * 4;
      hb_arraySetNI( result, 1, pixel[ 0 ] ); hb_arraySetNI( result, 2, pixel[ 1 ] );
      hb_arraySetNI( result, 3, pixel[ 2 ] ); hb_arraySetNI( result, 4, pixel[ 3 ] );
   }
   hb_itemReturnRelease( result );
}

HB_FUNC( LLIMG_RECT )
{
   LLImage *image = ll_image( 1 );
   double x = hb_parnd( 2 ), y = hb_parnd( 3 ), width = hb_parnd( 4 ), height = hb_parnd( 5 );
   double radius = hb_parnd( 6 ), angle = hb_parnd( 7 ), cx = hb_parnd( 8 ), cy = hb_parnd( 9 );
   uint32_t color = ll_color( 10 );
   int left, top, right, bottom, px, py, sx, sy;
   if( !image || width <= 0 || height <= 0 || ( color & 0xffu ) == 0 ) return;
   ll_rotated_bounds( x, y, width, height, angle, cx, cy, &left, &top, &right, &bottom );
   for( py = top; py < bottom; ++py ) for( px = left; px < right; ++px ) {
      int hits = 0;
      for( sy = 0; sy < 4; ++sy ) for( sx = 0; sx < 4; ++sx ) {
         double rx, ry;
         ll_inverse_point( px + ( sx + 0.5 ) / 4.0, py + ( sy + 0.5 ) / 4.0, cx, cy, angle, &rx, &ry );
         hits += ll_rounded_rect_inside( rx, ry, x, y, width, height, radius ) > 0;
      }
      if( hits ) ll_blend( image, px, py, color, hits / 16.0 );
   }
}

HB_FUNC( LLIMG_ELLIPSE )
{
   LLImage *image = ll_image( 1 );
   double x = hb_parnd( 2 ), y = hb_parnd( 3 ), width = hb_parnd( 4 ), height = hb_parnd( 5 );
   double angle = hb_parnd( 6 ), cx = hb_parnd( 7 ), cy = hb_parnd( 8 );
   uint32_t color = ll_color( 9 );
   int left, top, right, bottom, px, py, sx, sy;
   if( !image || width <= 0 || height <= 0 || ( color & 0xffu ) == 0 ) return;
   ll_rotated_bounds( x, y, width, height, angle, cx, cy, &left, &top, &right, &bottom );
   for( py = top; py < bottom; ++py ) for( px = left; px < right; ++px ) {
      int hits = 0;
      for( sy = 0; sy < 4; ++sy ) for( sx = 0; sx < 4; ++sx ) {
         double rx, ry, nx, ny;
         ll_inverse_point( px + ( sx + 0.5 ) / 4.0, py + ( sy + 0.5 ) / 4.0, cx, cy, angle, &rx, &ry );
         nx = ( rx - ( x + width / 2 ) ) / ( width / 2 );
         ny = ( ry - ( y + height / 2 ) ) / ( height / 2 );
         hits += nx * nx + ny * ny <= 1.0;
      }
      if( hits ) ll_blend( image, px, py, color, hits / 16.0 );
   }
}

static double ll_segment_distance( double px, double py, double x1, double y1, double x2, double y2 )
{
   double dx = x2 - x1, dy = y2 - y1, length2 = dx * dx + dy * dy;
   double t = length2 > 0 ? ( ( px - x1 ) * dx + ( py - y1 ) * dy ) / length2 : 0;
   double qx, qy;
   t = ll_clamp( t, 0, 1 ); qx = x1 + t * dx; qy = y1 + t * dy;
   dx = px - qx; dy = py - qy;
   return sqrt( dx * dx + dy * dy );
}

HB_FUNC( LLIMG_LINE )
{
   LLImage *image = ll_image( 1 );
   double x1 = hb_parnd( 2 ), y1 = hb_parnd( 3 ), x2 = hb_parnd( 4 ), y2 = hb_parnd( 5 );
   double width = fmax( 0.2, hb_parnd( 6 ) );
   uint32_t color = ll_color( 7 );
   int left = ( int ) floor( fmin( x1, x2 ) - width ), top = ( int ) floor( fmin( y1, y2 ) - width );
   int right = ( int ) ceil( fmax( x1, x2 ) + width ), bottom = ( int ) ceil( fmax( y1, y2 ) + width );
   int x, y;
   if( !image || ( color & 0xffu ) == 0 ) return;
   for( y = top; y < bottom; ++y ) for( x = left; x < right; ++x ) {
      double distance = ll_segment_distance( x + 0.5, y + 0.5, x1, y1, x2, y2 );
      double coverage = ll_clamp( width / 2 + 0.5 - distance, 0, 1 );
      if( coverage > 0 ) ll_blend( image, x, y, color, coverage );
   }
}

HB_FUNC( LLIMG_DRAW_IMAGE )
{
   LLImage *image = ll_image( 1 );
   const unsigned char *data = ( const unsigned char * ) hb_parc( 2 );
   HB_SIZE dataSize = hb_parclen( 2 );
   double x = hb_parnd( 3 ), y = hb_parnd( 4 ), width = hb_parnd( 5 ), height = hb_parnd( 6 );
   double angle = hb_parnd( 7 ), cx = hb_parnd( 8 ), cy = hb_parnd( 9 ), opacity = ll_clamp( hb_parnd( 10 ), 0, 1 );
   int sourceWidth, sourceHeight, channels, left, top, right, bottom, px, py;
   unsigned char *source;
   if( !image || !data || dataSize == 0 || width <= 0 || height <= 0 ) { hb_retl( HB_FALSE ); return; }
   source = stbi_load_from_memory( data, ( int ) dataSize, &sourceWidth, &sourceHeight, &channels, 4 );
   if( !source ) { hb_retl( HB_FALSE ); return; }
   ll_rotated_bounds( x, y, width, height, angle, cx, cy, &left, &top, &right, &bottom );
   for( py = top; py < bottom; ++py ) for( px = left; px < right; ++px ) {
      double rx, ry, u, v;
      int sx, sy;
      unsigned char *sample;
      uint32_t color;
      ll_inverse_point( px + 0.5, py + 0.5, cx, cy, angle, &rx, &ry );
      u = ( rx - x ) / width; v = ( ry - y ) / height;
      if( u < 0 || v < 0 || u >= 1 || v >= 1 ) continue;
      sx = ( int ) floor( u * sourceWidth ); sy = ( int ) floor( v * sourceHeight );
      if( sx >= sourceWidth ) sx = sourceWidth - 1;
      if( sy >= sourceHeight ) sy = sourceHeight - 1;
      sample = source + ( ( size_t ) sy * sourceWidth + sx ) * 4;
      color = ( ( uint32_t ) sample[ 0 ] << 24 ) | ( ( uint32_t ) sample[ 1 ] << 16 ) |
              ( ( uint32_t ) sample[ 2 ] << 8 ) | ( uint32_t ) floor( sample[ 3 ] * opacity + 0.5 );
      ll_blend( image, px, py, color, 1 );
   }
   stbi_image_free( source );
   hb_retl( HB_TRUE );
}

HB_FUNC( LLIMG_IMAGE_INFO )
{
   const unsigned char *data = ( const unsigned char * ) hb_parc( 1 );
   HB_SIZE dataSize = hb_parclen( 1 );
   int width = 0, height = 0, channels = 0;
   PHB_ITEM result = hb_itemArrayNew( 3 );
   if( data && dataSize > 0 && stbi_info_from_memory( data, ( int ) dataSize, &width, &height, &channels ) ) {
      hb_arraySetNI( result, 1, width ); hb_arraySetNI( result, 2, height ); hb_arraySetNI( result, 3, channels );
   }
   hb_itemReturnRelease( result );
}

HB_FUNC( LLIMG_FILE_INFO )
{
   const char *path = hb_parc( 1 );
   int width = 0, height = 0, channels = 0;
   unsigned char *pixels = path ? stbi_load( path, &width, &height, &channels, 4 ) : NULL;
   PHB_ITEM result = hb_itemArrayNew( 9 );
   if( pixels ) {
      size_t count = ( size_t ) width * height, i;
      HB_MAXINT transparent = 0, opaque = 0, white = 0, black = 0, partial = 0;
      hb_arraySetNI( result, 1, width ); hb_arraySetNI( result, 2, height ); hb_arraySetNI( result, 3, channels );
      for( i = 0; i < count; ++i ) {
         unsigned char *p = pixels + i * 4;
         if( p[ 3 ] == 0 ) ++transparent;
         else if( p[ 3 ] == 255 ) { ++opaque; if( p[ 0 ] == 255 && p[ 1 ] == 255 && p[ 2 ] == 255 ) ++white; if( p[ 0 ] == 0 && p[ 1 ] == 0 && p[ 2 ] == 0 ) ++black; }
         else ++partial;
      }
      hb_arraySetNInt( result, 4, transparent ); hb_arraySetNInt( result, 5, opaque );
      hb_arraySetNInt( result, 6, white ); hb_arraySetNInt( result, 7, black ); hb_arraySetNInt( result, 8, partial );
      hb_arraySetNI( result, 9, pixels[ 3 ] );
      stbi_image_free( pixels );
   }
   hb_itemReturnRelease( result );
}

HB_FUNC( LLIMG_FILE_PIXEL )
{
   const char *path = hb_parc( 1 );
   int x = hb_parni( 2 ), y = hb_parni( 3 ), width = 0, height = 0, channels = 0;
   unsigned char *pixels = path ? stbi_load( path, &width, &height, &channels, 4 ) : NULL;
   PHB_ITEM result = hb_itemArrayNew( 4 );
   if( pixels && x >= 0 && y >= 0 && x < width && y < height ) {
      unsigned char *p = pixels + ( ( size_t ) y * width + x ) * 4;
      hb_arraySetNI( result, 1, p[ 0 ] ); hb_arraySetNI( result, 2, p[ 1 ] );
      hb_arraySetNI( result, 3, p[ 2 ] ); hb_arraySetNI( result, 4, p[ 3 ] );
   }
   if( pixels ) stbi_image_free( pixels );
   hb_itemReturnRelease( result );
}

HB_FUNC( LLIMG_FILE_REGION_OPAQUE )
{
   const char *path = hb_parc( 1 );
   int left = hb_parni( 2 ), top = hb_parni( 3 ), right = hb_parni( 4 ), bottom = hb_parni( 5 );
   int width = 0, height = 0, channels = 0, x, y;
   HB_MAXINT count = 0;
   unsigned char *pixels = path ? stbi_load( path, &width, &height, &channels, 4 ) : NULL;
   if( pixels ) {
      if( left < 0 ) left = 0;
      if( top < 0 ) top = 0;
      if( right > width ) right = width;
      if( bottom > height ) bottom = height;
      for( y = top; y < bottom; ++y ) for( x = left; x < right; ++x )
         if( pixels[ ( ( size_t ) y * width + x ) * 4 + 3 ] > 0 ) ++count;
      stbi_image_free( pixels );
   }
   hb_retnint( count );
}

HB_FUNC( LLIMG_FONT_CREATE )
{
   const char *path = hb_parc( 1 );
   FILE *file;
   long size;
   LLFont *font;
   if( !path || !*path || !( file = fopen( path, "rb" ) ) ) { hb_retptr( NULL ); return; }
   fseek( file, 0, SEEK_END ); size = ftell( file ); fseek( file, 0, SEEK_SET );
   if( size <= 0 ) { fclose( file ); hb_retptr( NULL ); return; }
   font = ( LLFont * ) calloc( 1, sizeof( LLFont ) );
   if( !font ) { fclose( file ); hb_retptr( NULL ); return; }
   font->data = ( unsigned char * ) malloc( ( size_t ) size );
   if( !font->data || fread( font->data, 1, ( size_t ) size, file ) != ( size_t ) size ) {
      fclose( file ); free( font->data ); free( font ); hb_retptr( NULL ); return;
   }
   fclose( file );
   if( !stbtt_InitFont( &font->info, font->data, stbtt_GetFontOffsetForIndex( font->data, 0 ) ) ) {
      free( font->data ); free( font ); hb_retptr( NULL ); return;
   }
   font->magic = LLFONT_MAGIC; font->size = ( size_t ) size;
   hb_retptr( font );
}

HB_FUNC( LLIMG_FONT_FREE )
{
   LLFont *font = ll_font( 1 );
   if( font ) { font->magic = 0; free( font->data ); font->data = NULL; free( font ); }
}

static int ll_utf8_next( const unsigned char **text )
{
   const unsigned char *s = *text;
   int cp;
   if( *s < 0x80 ) { cp = *s++; }
   else if( ( *s & 0xe0 ) == 0xc0 && s[ 1 ] ) { cp = ( ( s[ 0 ] & 31 ) << 6 ) | ( s[ 1 ] & 63 ); s += 2; }
   else if( ( *s & 0xf0 ) == 0xe0 && s[ 1 ] && s[ 2 ] ) { cp = ( ( s[ 0 ] & 15 ) << 12 ) | ( ( s[ 1 ] & 63 ) << 6 ) | ( s[ 2 ] & 63 ); s += 3; }
   else if( ( *s & 0xf8 ) == 0xf0 && s[ 1 ] && s[ 2 ] && s[ 3 ] ) { cp = ( ( s[ 0 ] & 7 ) << 18 ) | ( ( s[ 1 ] & 63 ) << 12 ) | ( ( s[ 2 ] & 63 ) << 6 ) | ( s[ 3 ] & 63 ); s += 4; }
   else { cp = 0xfffd; ++s; }
   *text = s;
   return cp;
}

static double ll_text_width( LLFont *font, double size, const char *value )
{
   const unsigned char *text = ( const unsigned char * ) value;
   float scale = stbtt_ScaleForPixelHeight( &font->info, ( float ) size );
   double width = 0;
   int previous = 0;
   while( text && *text ) {
      int cp = ll_utf8_next( &text ), advance, bearing;
      if( previous ) width += stbtt_GetCodepointKernAdvance( &font->info, previous, cp ) * scale;
      stbtt_GetCodepointHMetrics( &font->info, cp, &advance, &bearing );
      width += advance * scale; previous = cp;
   }
   return width;
}

HB_FUNC( LLIMG_TEXT_WIDTH )
{
   LLFont *font = ll_font( 1 );
   const char *text = hb_parc( 3 );
   hb_retnd( font && text ? ll_text_width( font, hb_parnd( 2 ), text ) : 0 );
}

HB_FUNC( LLIMG_FONT_METRICS )
{
   LLFont *font = ll_font( 1 );
   double size = hb_parnd( 2 );
   PHB_ITEM result = hb_itemArrayNew( 3 );
   if( font ) {
      int ascent, descent, gap;
      float scale = stbtt_ScaleForPixelHeight( &font->info, ( float ) size );
      stbtt_GetFontVMetrics( &font->info, &ascent, &descent, &gap );
      hb_arraySetND( result, 1, ascent * scale ); hb_arraySetND( result, 2, descent * scale ); hb_arraySetND( result, 3, gap * scale );
   }
   hb_itemReturnRelease( result );
}

HB_FUNC( LLIMG_DRAW_TEXT )
{
   LLImage *image = ll_image( 1 );
   LLFont *font = ll_font( 2 );
   double size = hb_parnd( 3 );
   const unsigned char *text = ( const unsigned char * ) hb_parc( 4 );
   double penX = hb_parnd( 5 ), baseline = hb_parnd( 6 );
   uint32_t color = ll_color( 7 );
   double angle = hb_parnd( 8 ), cx = hb_parnd( 9 ), cy = hb_parnd( 10 );
   float scale;
   int previous = 0;
   if( !image || !font || !text || size <= 0 ) return;
   scale = stbtt_ScaleForPixelHeight( &font->info, ( float ) size );
   while( *text ) {
      int cp = ll_utf8_next( &text ), advance, bearing, x0, y0, x1, y1, gx, gy;
      unsigned char *bitmap;
      if( previous ) penX += stbtt_GetCodepointKernAdvance( &font->info, previous, cp ) * scale;
      stbtt_GetCodepointBitmapBox( &font->info, cp, scale, scale, &x0, &y0, &x1, &y1 );
      bitmap = stbtt_GetCodepointBitmap( &font->info, scale, scale, cp, &gx, &gy, NULL, NULL );
      if( bitmap ) {
         int bw = gx, bh = gy, bx, by;
         for( by = 0; by < bh; ++by ) for( bx = 0; bx < bw; ++bx ) {
            unsigned char alpha = bitmap[ by * bw + bx ];
            if( alpha ) {
               double dx, dy;
               ll_rotate_point( penX + x0 + bx + 0.5, baseline + y0 + by + 0.5, cx, cy, angle, &dx, &dy );
               ll_blend( image, ( int ) floor( dx ), ( int ) floor( dy ), color, alpha / 255.0 );
            }
         }
         stbtt_FreeBitmap( bitmap, NULL );
      }
      stbtt_GetCodepointHMetrics( &font->info, cp, &advance, &bearing );
      penX += advance * scale; previous = cp;
   }
}
