/*
 * Copyright (C) 2026  Behdad Esfahbod
 *
 *  This is part of HarfBuzz, a text shaping library.
 *
 * Permission is hereby granted, without written agreement and without
 * license or royalty fees, to use, copy, modify, and distribute this
 * software and its documentation for any purpose, provided that the
 * above copyright notice and the following two paragraphs appear in
 * all copies of this software.
 *
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
 * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
 * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
 * DAMAGE.
 *
 * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
 * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS
 * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
 * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 *
 * Author(s): Behdad Esfahbod
 */

#include "hb-test.h"
#include "hb-gpu.h"
#include <hb-ot.h>
#include <cmath>

#define FONT_FILE      "fonts/Roboto-Regular.abc.ttf"
#define COLR_FONT_FILE "fonts/test_glyphs-glyf_colr_1.ttf"

struct hb_gpu_test_texel_t
{
  int16_t r, g, b, a;
};

static const hb_gpu_test_texel_t *
blob_as_texels (hb_blob_t *blob,
		unsigned  *texel_count)
{
  unsigned length = hb_blob_get_length (blob);
  g_assert_cmpuint (length % sizeof (hb_gpu_test_texel_t), ==, 0);

  *texel_count = length / sizeof (hb_gpu_test_texel_t);
  /* hb_blob_get_data returns malloc'd memory which is suitably
   * aligned for any standard type; the cast is safe. */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-align"
  return (const hb_gpu_test_texel_t *) hb_blob_get_data (blob, nullptr);
#pragma GCC diagnostic pop
}

static hb_bool_t
blob_has_texel (hb_blob_t                   *blob,
		const hb_gpu_test_texel_t  needle)
{
  unsigned texel_count;
  const hb_gpu_test_texel_t *texels = blob_as_texels (blob, &texel_count);

  for (unsigned i = 0; i < texel_count; i++)
    if (texels[i].r == needle.r &&
	texels[i].g == needle.g &&
	texels[i].b == needle.b &&
	texels[i].a == needle.a)
      return true;

  return false;
}


static void
test_create_destroy (void)
{
  hb_gpu_draw_t *draw = hb_gpu_draw_create_or_fail ();
  g_assert_nonnull (draw);

  hb_gpu_draw_t *ref = hb_gpu_draw_reference (draw);
  g_assert_true (ref == draw);
  hb_gpu_draw_destroy (ref);

  hb_gpu_draw_destroy (draw);
}

static void
test_empty_encode (void)
{
  hb_gpu_draw_t *draw = hb_gpu_draw_create_or_fail ();
  g_assert_nonnull (draw);

  /* Encode with no curves should return empty blob and zero extents. */
  hb_glyph_extents_t ext;
  hb_blob_t *blob = hb_gpu_draw_encode (draw, &ext);
  g_assert_nonnull (blob);
  g_assert_cmpuint (hb_blob_get_length (blob), ==, 0);
  hb_blob_destroy (blob);

  g_assert_cmpint (ext.x_bearing, ==, 0);
  g_assert_cmpint (ext.y_bearing, ==, 0);
  g_assert_cmpint (ext.width, ==, 0);
  g_assert_cmpint (ext.height, ==, 0);

  hb_gpu_draw_destroy (draw);
}

static void
test_encode_glyph (void)
{
  hb_face_t *face = hb_test_open_font_file (FONT_FILE);
  g_assert_nonnull (face);
  hb_font_t *font = hb_font_create (face);

  hb_gpu_draw_t *draw = hb_gpu_draw_create_or_fail ();
  g_assert_nonnull (draw);

  /* Draw glyph for 'a' (glyph index from cmap). */
  hb_codepoint_t gid;
  hb_bool_t found = hb_font_get_nominal_glyph (font, 'a', &gid);
  g_assert_true (found);

  hb_gpu_draw_glyph (draw, font, gid);

  /* Encode should produce non-empty blob with non-zero extents. */
  hb_glyph_extents_t ext;
  hb_blob_t *blob = hb_gpu_draw_encode (draw, &ext);
  g_assert_nonnull (blob);
  g_assert_cmpuint (hb_blob_get_length (blob), >, 0);

  /* Blob size should be a multiple of 8 (RGBA16I texels). */
  g_assert_cmpuint (hb_blob_get_length (blob) % 8, ==, 0);

  g_assert_cmpint (ext.width, >, 0);
  g_assert_cmpint (ext.height, <, 0); /* height is negative (y-down) */

  hb_blob_destroy (blob);
  hb_gpu_draw_destroy (draw);
  hb_font_destroy (font);
  hb_face_destroy (face);
}

static void
test_reset (void)
{
  hb_face_t *face = hb_test_open_font_file (FONT_FILE);
  hb_font_t *font = hb_font_create (face);

  hb_gpu_draw_t *draw = hb_gpu_draw_create_or_fail ();
  g_assert_nonnull (draw);

  hb_codepoint_t gid;
  hb_font_get_nominal_glyph (font, 'a', &gid);

  /* Draw, encode, reset, encode again — should work. */
  hb_gpu_draw_glyph (draw, font, gid);
  hb_blob_t *blob1 = hb_gpu_draw_encode (draw, nullptr);
  g_assert_cmpuint (hb_blob_get_length (blob1), >, 0);
  hb_blob_destroy (blob1);

  hb_gpu_draw_reset (draw);

  /* After reset, encode should be empty. */
  hb_blob_t *blob2 = hb_gpu_draw_encode (draw, nullptr);
  g_assert_cmpuint (hb_blob_get_length (blob2), ==, 0);
  hb_blob_destroy (blob2);

  /* Draw again after reset. */
  hb_gpu_draw_glyph (draw, font, gid);
  hb_blob_t *blob3 = hb_gpu_draw_encode (draw, nullptr);
  g_assert_cmpuint (hb_blob_get_length (blob3), >, 0);
  hb_blob_destroy (blob3);

  hb_gpu_draw_destroy (draw);
  hb_font_destroy (font);
  hb_face_destroy (face);
}

static void
test_draw_funcs (void)
{
  hb_draw_funcs_t *funcs = hb_gpu_draw_get_funcs (nullptr);
  g_assert_nonnull (funcs);

  /* Should be the same instance each time. */
  g_assert_true (funcs == hb_gpu_draw_get_funcs (nullptr));
}

static void
test_shader_sources (void)
{
  g_assert_nonnull (hb_gpu_shader_source (HB_GPU_SHADER_STAGE_FRAGMENT, HB_GPU_SHADER_LANG_GLSL));
  g_assert_nonnull (hb_gpu_shader_source (HB_GPU_SHADER_STAGE_VERTEX, HB_GPU_SHADER_LANG_GLSL));
  g_assert_nonnull (hb_gpu_draw_shader_source (HB_GPU_SHADER_STAGE_FRAGMENT, HB_GPU_SHADER_LANG_GLSL));
  g_assert_nonnull (hb_gpu_draw_shader_source (HB_GPU_SHADER_STAGE_VERTEX, HB_GPU_SHADER_LANG_GLSL));
}

static void
test_encode_quantizes_extents_outward (void)
{
  hb_gpu_draw_t *draw = hb_gpu_draw_create_or_fail ();
  g_assert_nonnull (draw);

  hb_draw_funcs_t *funcs = hb_gpu_draw_get_funcs (draw);
  hb_draw_state_t st = HB_DRAW_STATE_DEFAULT;
  hb_draw_move_to (funcs, draw, &st, 0.24f, 0.24f);
  hb_draw_line_to (funcs, draw, &st, 0.76f, 0.24f);
  hb_draw_line_to (funcs, draw, &st, 0.76f, 0.76f);
  hb_draw_close_path (funcs, draw, &st);

  hb_blob_t *blob = hb_gpu_draw_encode (draw, nullptr);
  g_assert_nonnull (blob);

  unsigned texel_count;
  const hb_gpu_test_texel_t *texels = blob_as_texels (blob, &texel_count);
  g_assert_cmpuint (texel_count, >, 0);
  g_assert_cmpint (texels[0].r, ==, 0);
  g_assert_cmpint (texels[0].g, ==, 0);
  g_assert_cmpint (texels[0].b, ==, 4);
  g_assert_cmpint (texels[0].a, ==, 4);

  hb_blob_destroy (blob);
  hb_gpu_draw_destroy (draw);
}

static void
test_encode_preserves_touching_contours (void)
{
  hb_gpu_draw_t *draw = hb_gpu_draw_create_or_fail ();
  g_assert_nonnull (draw);

  hb_draw_funcs_t *funcs = hb_gpu_draw_get_funcs (draw);
  hb_draw_state_t st = HB_DRAW_STATE_DEFAULT;

  hb_draw_move_to (funcs, draw, &st, 0.f, 0.f);
  hb_draw_line_to (funcs, draw, &st, 1.f, 0.f);
  hb_draw_line_to (funcs, draw, &st, 0.f, 1.f);
  hb_draw_close_path (funcs, draw, &st);

  hb_draw_move_to (funcs, draw, &st, 0.f, 0.f);
  hb_draw_quadratic_to (funcs, draw, &st, -0.75f, 0.25f, -0.25f, -1.f);
  hb_draw_close_path (funcs, draw, &st);

  hb_blob_t *blob = hb_gpu_draw_encode (draw, nullptr);
  g_assert_nonnull (blob);

  g_assert_true (blob_has_texel (blob, {0, 0, -3, 1}));

  hb_blob_destroy (blob);
  hb_gpu_draw_destroy (draw);
}

static void
test_recycle_blob (void)
{
  hb_gpu_draw_t *draw = hb_gpu_draw_create_or_fail ();
  g_assert_nonnull (draw);

  hb_draw_funcs_t *funcs = hb_gpu_draw_get_funcs (draw);
  hb_draw_state_t st = HB_DRAW_STATE_DEFAULT;

  hb_draw_move_to (funcs, draw, &st, 0.f, 0.f);
  hb_draw_line_to (funcs, draw, &st, 1.f, 0.f);
  hb_draw_line_to (funcs, draw, &st, 0.f, 1.f);
  hb_draw_close_path (funcs, draw, &st);

  hb_blob_t *blob = hb_gpu_draw_encode (draw, nullptr);
  g_assert_nonnull (blob);
  unsigned first_length = hb_blob_get_length (blob);
  g_assert_cmpuint (first_length, >, 0);

  hb_gpu_draw_recycle_blob (draw, blob);

  hb_gpu_draw_reset (draw);

  hb_draw_move_to (funcs, draw, &st, 0.f, 0.f);
  hb_draw_line_to (funcs, draw, &st, 2.f, 0.f);
  hb_draw_line_to (funcs, draw, &st, 2.f, 2.f);
  hb_draw_line_to (funcs, draw, &st, 0.f, 2.f);
  hb_draw_close_path (funcs, draw, &st);
  hb_draw_move_to (funcs, draw, &st, 0.25f, 0.25f);
  hb_draw_quadratic_to (funcs, draw, &st, 1.f, 1.75f, 1.75f, 0.25f);
  hb_draw_quadratic_to (funcs, draw, &st, 1.f, -0.5f, 0.25f, 0.25f);
  hb_draw_close_path (funcs, draw, &st);

  hb_blob_t *blob2 = hb_gpu_draw_encode (draw, nullptr);
  g_assert_nonnull (blob2);
  g_assert_cmpuint (hb_blob_get_length (blob2), >, first_length);

  hb_blob_destroy (blob2);
  hb_gpu_draw_destroy (draw);
}

/* The fragment shader picks a single horizontal and a single vertical
 * band from the fragment position and only walks that band's curve
 * list.  So every curve must be listed in every band that its
 * *quantized* bounding box reaches into; a curve missing from a band
 * gives fragments in that band the wrong winding number, which shows
 * up as a thin black line through the glyph.
 *
 * https://github.com/harfbuzz/harfbuzz/issues/6131 */
static void
assert_band_membership (hb_blob_t *blob)
{
  unsigned texel_count;
  const hb_gpu_test_texel_t *t = blob_as_texels (blob, &texel_count);
  if (texel_count < 2)
    return;

  /* Coordinates are stored as quarters of a font unit. */
  double min_x = t[0].r / 4., min_y = t[0].g / 4.;
  double max_x = t[0].b / 4., max_y = t[0].a / 4.;
  unsigned num_hbands = (unsigned) t[1].r;
  unsigned num_vbands = (unsigned) t[1].g;
  g_assert_cmpuint (num_hbands, >, 0);
  g_assert_cmpuint (num_vbands, >, 0);
  g_assert_cmpuint (2 + num_hbands + num_vbands, <=, texel_count);

  double hband_size = (max_y - min_y) / num_hbands;
  double vband_size = (max_x - min_x) / num_vbands;

  for (unsigned b = 0; b < num_hbands + num_vbands; b++)
  {
    hb_bool_t horizontal = b < num_hbands;
    unsigned count = (unsigned) (uint16_t) t[2 + b].r;
    unsigned list = (unsigned) ((int) t[2 + b].g + 32768);
    g_assert_cmpuint (list + count, <=, texel_count);

    for (unsigned ci = 0; ci < count; ci++)
    {
      unsigned c = (unsigned) ((int) t[list + ci].r + 32768);
      g_assert_cmpuint (c + 1, <, texel_count);

      /* Every band the curve reaches into, in the axis of this band. */
      int lo, hi;
      if (horizontal)
      {
	int q1 = t[c].g, q2 = t[c].a, q3 = t[c + 1].g;
	if (q1 == q2 && q2 == q3) continue; /* horizontal: skipped by design */
	if (!(hband_size > 0)) continue;
	lo = (int) floor ((MIN (MIN (q1, q2), q3) / 4. - min_y) / hband_size);
	hi = (int) floor ((MAX (MAX (q1, q2), q3) / 4. - min_y) / hband_size);
	lo = MAX (lo, 0);
	hi = MIN (hi, (int) num_hbands - 1);
      }
      else
      {
	int q1 = t[c].r, q2 = t[c].b, q3 = t[c + 1].r;
	if (q1 == q2 && q2 == q3) continue; /* vertical: skipped by design */
	if (!(vband_size > 0)) continue;
	lo = (int) floor ((MIN (MIN (q1, q2), q3) / 4. - min_x) / vband_size);
	hi = (int) floor ((MAX (MAX (q1, q2), q3) / 4. - min_x) / vband_size);
	lo = MAX (lo, 0);
	hi = MIN (hi, (int) num_vbands - 1);
      }

      for (int ob = lo; ob <= hi; ob++)
      {
	unsigned obb = horizontal ? (unsigned) ob : num_hbands + (unsigned) ob;
	unsigned ocount = (unsigned) (uint16_t) t[2 + obb].r;
	unsigned olist = (unsigned) ((int) t[2 + obb].g + 32768);
	g_assert_cmpuint (olist + ocount, <=, texel_count);

	hb_bool_t found = false;
	for (unsigned oi = 0; oi < ocount && !found; oi++)
	  found = (unsigned) ((int) t[olist + oi].r + 32768) == c;
	g_assert_true (found);
      }
    }
  }
}

static void
test_encode_band_membership (void)
{
  hb_face_t *face = hb_test_open_font_file (FONT_FILE);
  g_assert_nonnull (face);
  hb_font_t *font = hb_font_create (face);
  /* A scale other than the font's upem makes the glyph coordinates
   * fractional, so quantizing them rounds.  Band membership derived
   * from the unquantized coordinates then misses bands. */
  hb_font_set_scale (font, 128, 128);

  hb_gpu_draw_t *draw = hb_gpu_draw_create_or_fail ();
  g_assert_nonnull (draw);

  unsigned glyph_count = hb_face_get_glyph_count (face);
  for (hb_codepoint_t gid = 0; gid < glyph_count; gid++)
  {
    hb_gpu_draw_clear (draw);
    hb_gpu_draw_glyph (draw, font, gid);
    hb_blob_t *blob = hb_gpu_draw_encode (draw, nullptr);
    g_assert_nonnull (blob);
    assert_band_membership (blob);
    hb_blob_destroy (blob);
  }

  hb_gpu_draw_destroy (draw);
  hb_font_destroy (font);
  hb_face_destroy (face);
}

static void
test_extents_saturate_overflow (void)
{
  hb_gpu_draw_t *draw = hb_gpu_draw_create_or_fail ();
  g_assert_nonnull (draw);

  hb_draw_funcs_t *funcs = hb_gpu_draw_get_funcs (draw);
  hb_draw_state_t st = HB_DRAW_STATE_DEFAULT;

  hb_draw_move_to (funcs, draw, &st, -0x1p31f, 0.f);
  hb_draw_line_to (funcs, draw, &st, 821.f, 0.f);
  hb_draw_line_to (funcs, draw, &st, 821.f, 1.f);
  hb_draw_close_path (funcs, draw, &st);

  hb_glyph_extents_t ext;
  hb_blob_t *blob = hb_gpu_draw_encode (draw, &ext);
  hb_blob_destroy (blob);

  g_assert_cmpint (ext.x_bearing, ==, G_MININT32);
  g_assert_cmpint (ext.y_bearing, ==, 1);
  g_assert_cmpint (ext.width, ==, G_MAXINT32);
  g_assert_cmpint (ext.height, ==, -1);

  hb_gpu_draw_destroy (draw);
}

/* ===== Paint encoder tests ===== */

static void
test_paint_create_destroy (void)
{
  hb_gpu_paint_t *p = hb_gpu_paint_create_or_fail ();
  g_assert_nonnull (p);

  hb_gpu_paint_t *ref = hb_gpu_paint_reference (p);
  g_assert_true (ref == p);
  hb_gpu_paint_destroy (ref);

  hb_gpu_paint_destroy (p);
}

static void
test_paint_get_funcs (void)
{
  hb_paint_funcs_t *funcs = hb_gpu_paint_get_funcs (nullptr);
  g_assert_nonnull (funcs);
  /* Same instance each call. */
  g_assert_true (funcs == hb_gpu_paint_get_funcs (nullptr));
}

static void
test_paint_shader_sources (void)
{
  /* Fragment helpers exist for all four supported languages. */
  g_assert_nonnull (hb_gpu_paint_shader_source (HB_GPU_SHADER_STAGE_FRAGMENT, HB_GPU_SHADER_LANG_GLSL));
  g_assert_nonnull (hb_gpu_paint_shader_source (HB_GPU_SHADER_STAGE_FRAGMENT, HB_GPU_SHADER_LANG_MSL));
  g_assert_nonnull (hb_gpu_paint_shader_source (HB_GPU_SHADER_STAGE_FRAGMENT, HB_GPU_SHADER_LANG_HLSL));
  g_assert_nonnull (hb_gpu_paint_shader_source (HB_GPU_SHADER_STAGE_FRAGMENT, HB_GPU_SHADER_LANG_WGSL));
}

static void
test_paint_empty_encode (void)
{
  /* Nothing painted -> empty blob (no ops to encode).  Reserved for
   * the "no ink" case; nullptr is only returned for unsupported
   * features. */
  hb_gpu_paint_t *p = hb_gpu_paint_create_or_fail ();
  g_assert_nonnull (p);
  hb_blob_t *blob = hb_gpu_paint_encode (p, nullptr);
  g_assert_nonnull (blob);
  g_assert_cmpuint (hb_blob_get_length (blob), ==, 0);
  hb_blob_destroy (blob);
  hb_gpu_paint_destroy (p);
}

static void
test_paint_encode_monochrome (void)
{
  /* hb_gpu_paint_glyph synthesizes a single foreground-colored
   * layer for non-color glyphs; the encoded blob should be
   * non-empty and 8-byte (RGBA16I texel) aligned. */
  hb_face_t *face = hb_test_open_font_file (FONT_FILE);
  g_assert_nonnull (face);
  hb_font_t *font = hb_font_create (face);

  hb_gpu_paint_t *p = hb_gpu_paint_create_or_fail ();
  g_assert_nonnull (p);

  hb_codepoint_t gid;
  g_assert_true (hb_font_get_nominal_glyph (font, 'a', &gid));
  /* Use hb_gpu_paint_glyph (not _or_fail) so that non-color
   * glyphs still produce a blob via the synthesize fallback. */
  hb_gpu_paint_glyph (p, font, gid);

  hb_glyph_extents_t ext;
  hb_blob_t *blob = hb_gpu_paint_encode (p, &ext);
  g_assert_nonnull (blob);
  g_assert_cmpuint (hb_blob_get_length (blob), >, 0);
  g_assert_cmpuint (hb_blob_get_length (blob) % 8, ==, 0);
  g_assert_cmpint (ext.width, >, 0);
  g_assert_cmpint (ext.height, <, 0);

  hb_blob_destroy (blob);
  hb_gpu_paint_destroy (p);
  hb_font_destroy (font);
  hb_face_destroy (face);
}

static void
test_paint_encode_color (void)
{
  /* Real COLRv1 paint tree: scan for any color glyph that we
   * support end-to-end (the test font deliberately exercises
   * features we may flag as unsupported, so check several rather
   * than asserting on the first). */
  hb_face_t *face = hb_test_open_font_file (COLR_FONT_FILE);
  g_assert_nonnull (face);
  hb_font_t *font = hb_font_create (face);

  hb_gpu_paint_t *p = hb_gpu_paint_create_or_fail ();
  g_assert_nonnull (p);

  unsigned num = hb_face_get_glyph_count (face);
  for (unsigned i = 0; i < num; i++)
  {
    if (!hb_ot_color_glyph_has_paint (face, i))
      continue;
    /* paint_glyph's return is the supported flag; we don't gate
     * on it -- encode handles both cases (returns empty blob for
     * unsupported / empty, nullptr only on allocation failure). */
    hb_gpu_paint_glyph (p, font, i);
    hb_blob_t *blob = hb_gpu_paint_encode (p, nullptr);
    g_assert_nonnull (blob);
    g_assert_cmpuint (hb_blob_get_length (blob) % 8, ==, 0);
    hb_blob_destroy (blob);
  }
  /* Test exists to confirm we don't crash / corrupt while walking
   * a real COLRv1 paint tree, not that our encoder covers every
   * COLRv1 feature. */

  hb_gpu_paint_destroy (p);
  hb_font_destroy (font);
  hb_face_destroy (face);
}

static void
test_paint_auto_clear_on_encode (void)
{
  /* hb_gpu_paint_encode auto-clears so the encoder can be reused
   * straight away; user configuration (palette) is preserved. */
  hb_face_t *face = hb_test_open_font_file (FONT_FILE);
  hb_font_t *font = hb_font_create (face);

  hb_gpu_paint_t *p = hb_gpu_paint_create_or_fail ();
  hb_gpu_paint_set_palette (p, 3);
  g_assert_cmpuint (hb_gpu_paint_get_palette (p), ==, 3);

  hb_codepoint_t gid;
  hb_font_get_nominal_glyph (font, 'a', &gid);

  hb_gpu_paint_glyph (p, font, gid);
  hb_blob_t *blob1 = hb_gpu_paint_encode (p, nullptr);
  g_assert_nonnull (blob1);
  hb_blob_destroy (blob1);

  /* Auto-clear: re-encoding without painting again returns the
   * empty-blob singleton (nothing accumulated). */
  hb_blob_t *empty = hb_gpu_paint_encode (p, nullptr);
  g_assert_nonnull (empty);
  g_assert_cmpuint (hb_blob_get_length (empty), ==, 0);
  hb_blob_destroy (empty);

  /* Palette config is preserved. */
  g_assert_cmpuint (hb_gpu_paint_get_palette (p), ==, 3);

  /* Paint another glyph -- works because state is clean and config
   * is still in effect. */
  hb_font_get_nominal_glyph (font, 'b', &gid);
  hb_gpu_paint_glyph (p, font, gid);
  hb_blob_t *blob2 = hb_gpu_paint_encode (p, nullptr);
  g_assert_nonnull (blob2);
  hb_blob_destroy (blob2);

  hb_gpu_paint_destroy (p);
  hb_font_destroy (font);
  hb_face_destroy (face);
}

static void
test_paint_reset (void)
{
  /* paint_reset wipes user configuration too (vs. paint_clear which
   * keeps it). */
  hb_gpu_paint_t *p = hb_gpu_paint_create_or_fail ();
  hb_gpu_paint_set_palette (p, 5);
  g_assert_cmpuint (hb_gpu_paint_get_palette (p), ==, 5);
  hb_gpu_paint_reset (p);
  g_assert_cmpuint (hb_gpu_paint_get_palette (p), ==, 0);
  hb_gpu_paint_destroy (p);
}

static void
test_paint_recycle_blob (void)
{
  hb_face_t *face = hb_test_open_font_file (FONT_FILE);
  hb_font_t *font = hb_font_create (face);

  hb_gpu_paint_t *p = hb_gpu_paint_create_or_fail ();
  hb_codepoint_t gid;
  hb_font_get_nominal_glyph (font, 'a', &gid);

  hb_gpu_paint_glyph (p, font, gid);
  hb_blob_t *blob = hb_gpu_paint_encode (p, nullptr);
  g_assert_nonnull (blob);
  hb_gpu_paint_recycle_blob (p, blob);

  /* Encoder still works after recycling. */
  hb_gpu_paint_glyph (p, font, gid);
  hb_blob_t *blob2 = hb_gpu_paint_encode (p, nullptr);
  g_assert_nonnull (blob2);
  hb_blob_destroy (blob2);

  hb_gpu_paint_destroy (p);
  hb_font_destroy (font);
  hb_face_destroy (face);
}


static void
test_paint_clip_path_depth_overflow (void)
{
  /* push_clip_path_end must honor the same clip-depth cap as
   * push_clip_glyph and push_clip_path_start.  Interleaving clip
   * pushes drives clip_depth past HB_GPU_PAINT_MAX_CLIP_DEPTH before
   * the path is closed; without a bound check push_clip_path_end
   * wrote past the fixed-size clip_stack (heap-buffer-overflow). */
  hb_gpu_paint_t *p = hb_gpu_paint_create_or_fail ();
  g_assert_nonnull (p);
  hb_gpu_paint_set_scale (p, 1000, 1000);

  hb_paint_funcs_t *pf = hb_gpu_paint_get_funcs (p);
  hb_font_t *font = hb_font_get_empty ();

  void *draw_data = nullptr;
  hb_draw_funcs_t *df = hb_paint_push_clip_path_start (pf, p, &draw_data);
  g_assert_nonnull (df);

  hb_draw_state_t st = HB_DRAW_STATE_DEFAULT;
  hb_draw_move_to (df, draw_data, &st, 0.f, 0.f);
  hb_draw_line_to (df, draw_data, &st, 100.f, 0.f);
  hb_draw_line_to (df, draw_data, &st, 100.f, 100.f);
  hb_draw_close_path (df, draw_data, &st);

  /* Push more clips than the stack holds before closing the path. */
  for (unsigned i = 0; i < 4; i++)
    hb_paint_push_clip_glyph (pf, p, i, font);

  hb_paint_push_clip_path_end (pf, p);

  /* Overflowing the clip stack marks the paint unsupported rather
   * than writing past clip_stack, so encode returns NULL. */
  hb_blob_t *blob = hb_gpu_paint_encode (p, nullptr);
  g_assert_null (blob);

  hb_gpu_paint_destroy (p);
}

static unsigned
repro_color_stops (hb_color_line_t *cl HB_UNUSED, void *cl_data HB_UNUSED,
		   unsigned start, unsigned *count,
		   hb_color_stop_t *stops, void *user_data HB_UNUSED)
{
  static const hb_color_stop_t all[2] = {
    { 0.f, 0, HB_COLOR (255, 0, 0, 255) },
    { 1.f, 0, HB_COLOR (0, 0, 255, 255) },
  };
  if (count)
  {
    unsigned n = 0;
    for (unsigned i = start; i < 2 && n < *count; i++)
      stops[n++] = all[i];
    *count = n;
  }
  return 2;
}

static hb_paint_extend_t
repro_color_extend (hb_color_line_t *cl HB_UNUSED, void *cl_data HB_UNUSED,
		    void *user_data HB_UNUSED)
{
  return HB_PAINT_EXTEND_PAD;
}

static void
test_paint_gradient_overflow_transform (void)
{
  /* A COLRv1 paint tree can nest PaintScale/PaintTransform, which the
   * GPU paint encoder composes multiplicatively into cur_transform.
   * Deep enough nesting overflows the matrix entries to +inf.  When a
   * gradient encoder inverts that transform (linv_q10), a 1/inf
   * determinant gives inv==0 and inf*0==NaN, which clamp_i16 then cast
   * straight to int16 -- float-cast-overflow UB that aborts the
   * asan-ubsan CI job (halt_on_error). */
  hb_gpu_paint_t *p = hb_gpu_paint_create_or_fail ();
  g_assert_nonnull (p);
  hb_gpu_paint_set_scale (p, 1000, 1000);

  hb_paint_funcs_t *pf = hb_gpu_paint_get_funcs (p);

  /* A clip must be on the stack before a gradient is emitted. */
  void *draw_data = nullptr;
  hb_draw_funcs_t *df = hb_paint_push_clip_path_start (pf, p, &draw_data);
  g_assert_nonnull (df);
  hb_draw_state_t st = HB_DRAW_STATE_DEFAULT;
  hb_draw_move_to (df, draw_data, &st, 0.f, 0.f);
  hb_draw_line_to (df, draw_data, &st, 100.f, 0.f);
  hb_draw_line_to (df, draw_data, &st, 100.f, 100.f);
  hb_draw_close_path (df, draw_data, &st);
  hb_paint_push_clip_path_end (pf, p);

  /* Compose large scales until the transform overflows to inf. */
  for (unsigned i = 0; i < 4; i++)
    hb_paint_push_transform (pf, p, 1e19f, 0.f, 0.f, 1e19f, 0.f, 0.f);

  hb_color_line_t cl = {};
  cl.get_color_stops = repro_color_stops;
  cl.get_extend = repro_color_extend;

  hb_paint_linear_gradient (pf, p, &cl, 0.f, 0.f, 1.f, 0.f, 0.f, 1.f);

  hb_blob_t *blob = hb_gpu_paint_encode (p, nullptr);
  hb_blob_destroy (blob);

  hb_gpu_paint_destroy (p);
}


static void
test_shapes (void)
{
  hb_gpu_draw_t *draw = hb_gpu_draw_create_or_fail ();
  g_assert_nonnull (draw);
  /* Use a reasonable scale so the coordinates below stay in
   * the encoder's representable range. */
  hb_gpu_draw_set_scale (draw, 1000, 1000);

  hb_draw_funcs_t *dfuncs = hb_gpu_draw_get_funcs (draw);
  hb_draw_state_t st = HB_DRAW_STATE_DEFAULT;

  /* Tapered line. */
  hb_draw_line (dfuncs, draw, &st,
		10.f, 10.f, 4.f,
		50.f, 30.f, 2.f,
		HB_DRAW_LINE_CAP_BUTT);
  hb_glyph_extents_t ext;
  hb_blob_t *blob = hb_gpu_draw_encode (draw, &ext);
  g_assert_nonnull (blob);
  g_assert_cmpuint (hb_blob_get_length (blob), >, 0);
  g_assert_cmpint (ext.width, >, 0);
  hb_blob_destroy (blob);

  /* Degenerate line (zero length) is silently a no-op. */
  hb_draw_line (dfuncs, draw, &st,
		10.f, 10.f, 3.f,
		10.f, 10.f, 3.f,
		HB_DRAW_LINE_CAP_BUTT);
  blob = hb_gpu_draw_encode (draw, &ext);
  g_assert_cmpuint (hb_blob_get_length (blob), ==, 0);
  hb_blob_destroy (blob);

  /* NaN second width defaults to first; square cap extends each
   * end by half the stroke width along the line direction.
   * Horizontal line of length 40, stroke width 4: with butt
   * caps the bbox width would be 40, with square caps it's
   * 40 + 4 = 44. */
  hb_draw_line (dfuncs, draw, &st,
		10.f, 10.f, 4.f,
		50.f, 10.f, (float) std::nan (""),
		HB_DRAW_LINE_CAP_SQUARE);
  blob = hb_gpu_draw_encode (draw, &ext);
  g_assert_nonnull (blob);
  g_assert_cmpint (ext.width, ==, 44);
  hb_blob_destroy (blob);

  /* Filled rect (stroke_width = NaN). */
  hb_draw_rectangle (dfuncs, draw, &st,
		0.f, 0.f, 20.f, 10.f,
		(float) std::nan (""));
  blob = hb_gpu_draw_encode (draw, &ext);
  g_assert_nonnull (blob);
  g_assert_cmpuint (hb_blob_get_length (blob), >, 0);
  g_assert_cmpint (ext.width, ==, 20);
  hb_blob_destroy (blob);

  /* Stroked rect: outer edge extends stroke_width/2 beyond the
   * nominal rect on each side. */
  hb_draw_rectangle (dfuncs, draw, &st,
		0.f, 0.f, 20.f, 10.f, 2.f);
  blob = hb_gpu_draw_encode (draw, &ext);
  g_assert_nonnull (blob);
  g_assert_cmpuint (hb_blob_get_length (blob), >, 0);
  g_assert_cmpint (ext.width, ==, 22);
  hb_blob_destroy (blob);

  /* Filled circle. */
  hb_draw_circle (dfuncs, draw, &st,
		  50.f, 50.f, 25.f,
		  (float) std::nan (""));
  blob = hb_gpu_draw_encode (draw, &ext);
  g_assert_nonnull (blob);
  g_assert_cmpuint (hb_blob_get_length (blob), >, 0);
  hb_blob_destroy (blob);

  /* Stroked circle. */
  hb_draw_circle (dfuncs, draw, &st,
		  50.f, 50.f, 25.f, 2.f);
  blob = hb_gpu_draw_encode (draw, &ext);
  g_assert_nonnull (blob);
  g_assert_cmpuint (hb_blob_get_length (blob), >, 0);
  hb_blob_destroy (blob);

  hb_gpu_draw_destroy (draw);
}


int
main (int argc, char **argv)
{
  hb_test_init (&argc, &argv);

  hb_test_add (test_create_destroy);
  hb_test_add (test_empty_encode);
  hb_test_add (test_encode_glyph);
  hb_test_add (test_reset);
  hb_test_add (test_draw_funcs);
  hb_test_add (test_shader_sources);
  hb_test_add (test_encode_quantizes_extents_outward);
  hb_test_add (test_encode_preserves_touching_contours);
  hb_test_add (test_recycle_blob);
  hb_test_add (test_encode_band_membership);
  hb_test_add (test_extents_saturate_overflow);
  hb_test_add (test_shapes);

  hb_test_add (test_paint_create_destroy);
  hb_test_add (test_paint_get_funcs);
  hb_test_add (test_paint_shader_sources);
  hb_test_add (test_paint_empty_encode);
  hb_test_add (test_paint_encode_monochrome);
  hb_test_add (test_paint_encode_color);
  hb_test_add (test_paint_auto_clear_on_encode);
  hb_test_add (test_paint_reset);
  hb_test_add (test_paint_recycle_blob);
  hb_test_add (test_paint_clip_path_depth_overflow);
  hb_test_add (test_paint_gradient_overflow_transform);

  return hb_test_run ();
}
