/*
 * Copyright © 2026  Google, Inc.
 *
 *  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.
 */

#ifndef HB_VECTOR_OUTPUT_HH
#define HB_VECTOR_OUTPUT_HH

#include <algorithm>
#include <math.h>
#include <string>
#include <vector>

#include "output-options.hh"
#include "view-options.hh"
#include "hb-vector.h"
#include "hb-ot.h"


struct vector_output_t : output_options_t<>, view_options_t
{
  static const bool repeat_shape = false;

  ~vector_output_t ()
  {
    hb_font_destroy (font);
    hb_font_destroy (upem_font);
  }

  void add_options (option_parser_t *parser)
  {
    parser->set_summary ("Draw text with given font.");
    parser->set_description ("Shows shaped glyph outlines as SVG.");

    output_options_t::add_options (parser);
    view_options_t::add_options (parser);

    GOptionEntry entries[] =
    {
      {"flat",       0, 0, G_OPTION_ARG_NONE,   &this->flat,          "Flatten geometry and disable reuse", nullptr},
      {"precision",  0, 0, G_OPTION_ARG_INT,    &this->precision,     "Decimal precision (default: 2)",     "N"},
      {nullptr}
    };
    parser->add_group (entries,
                       "vector",
                       "Vector options:",
                       "Options for vector output",
                       this);
  }

  void post_parse (GError **error)
  {
    view_options_t::post_parse (error);
    if (error && *error)
      return;

    if (precision < 0)
    {
      g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE,
                   "precision must be non-negative");
      return;
    }

    foreground = HB_COLOR (foreground_color.b,
                           foreground_color.g,
                           foreground_color.r,
                           foreground_color.a);
    background = HB_COLOR (background_color.b,
                           background_color.g,
                           background_color.r,
                           background_color.a);

    load_custom_palette_overrides_from_view ();
  }

  template <typename app_t>
  void init (hb_buffer_t *buffer HB_UNUSED, const app_t *font_opts)
  {
    lines.clear ();
    direction = HB_DIRECTION_INVALID;

    font = hb_font_reference (font_opts->font);
    subpixel_bits = font_opts->subpixel_bits;
    hb_font_get_scale (font, &x_scale, &y_scale);
    upem = hb_face_get_upem (hb_font_get_face (font));
    normalized_stroke_width = get_normalized_stroke_width ();

    upem_font = hb_font_create (hb_font_get_face (font));
    hb_font_set_scale (upem_font, upem, upem);
#ifndef HB_NO_VAR
    unsigned int coords_length = 0;
    const float *coords = hb_font_get_var_coords_design (font, &coords_length);
    if (coords_length)
      hb_font_set_var_coords_design (upem_font, coords, coords_length);
#endif
  }

  void new_line ()
  {
    lines.emplace_back ();
  }

  void consume_text (hb_buffer_t  * HB_UNUSED,
                     const char   * HB_UNUSED,
                     unsigned int   HB_UNUSED,
                     hb_bool_t      HB_UNUSED)
  {}

  void error (const char *message)
  {
    g_printerr ("%s: %s\n", g_get_prgname (), message);
  }

  void consume_glyphs (hb_buffer_t  *buffer,
                       const char   * HB_UNUSED,
                       unsigned int   HB_UNUSED,
                       hb_bool_t      HB_UNUSED)
  {
    if (lines.empty ())
      lines.emplace_back ();

    line_t &line = lines.back ();
    hb_direction_t line_direction = hb_buffer_get_direction (buffer);
    if (line_direction == HB_DIRECTION_INVALID)
      line_direction = HB_DIRECTION_LTR;
    if (direction == HB_DIRECTION_INVALID)
      direction = line_direction;

    unsigned int count = 0;
    const hb_glyph_info_t *infos = hb_buffer_get_glyph_infos (buffer, &count);
    const hb_glyph_position_t *positions = hb_buffer_get_glyph_positions (buffer, &count);

    hb_position_t pen_x = 0;
    hb_position_t pen_y = 0;

    line.glyphs.reserve (line.glyphs.size () + count);
    for (unsigned i = 0; i < count; i++)
    {
      line.glyphs.push_back ({
        infos[i].codepoint,
        scale_to_upem_x (pen_x + positions[i].x_offset),
        scale_to_upem_y (pen_y + positions[i].y_offset),
        scale_to_upem_x (positions[i].x_advance),
        scale_to_upem_y (positions[i].y_advance),
      });

      pen_x += positions[i].x_advance;
      pen_y += positions[i].y_advance;
    }

    line.advance_x = scale_to_upem_x (pen_x);
    line.advance_y = scale_to_upem_y (pen_y);
  }

  template <typename app_t>
  void finish (hb_buffer_t *buffer HB_UNUSED, const app_t *app HB_UNUSED)
  {
    hb_vector_extents_t extents = {};
    if (!compute_extents (&extents))
      return;
    final_extents = extents;

    hb_vector_draw_t *draw = hb_vector_draw_create_or_fail (HB_VECTOR_FORMAT_SVG);
    hb_vector_paint_t *paint = hb_vector_paint_create_or_fail (HB_VECTOR_FORMAT_SVG);

    hb_vector_draw_set_scale_factor (draw, 1.f, 1.f);
    hb_vector_draw_set_extents (draw, &extents);
    hb_vector_svg_set_precision (draw, precision);
    hb_vector_svg_set_flat (draw, flat);

    hb_vector_paint_set_scale_factor (paint, 1.f, 1.f);
    hb_vector_paint_set_extents (paint, &extents);
    hb_vector_paint_set_foreground (paint, foreground);
    hb_vector_paint_set_palette (paint, this->palette);
    apply_custom_palette (paint);
    init_palette_color_cache ();
    hb_vector_svg_paint_set_precision (paint, precision);
    hb_vector_svg_paint_set_flat (paint, flat);

    bool had_draw = false;
    bool had_paint = false;
    hb_vector_extents_mode_t extents_mode = HB_VECTOR_EXTENTS_MODE_NONE;
    const bool use_foreground_palette =
      foreground_use_palette && foreground_palette && foreground_palette->len;
    unsigned palette_glyph_index = 0;

    hb_direction_t dir = direction;
    if (dir == HB_DIRECTION_INVALID)
      dir = HB_DIRECTION_LTR;
    bool vertical = HB_DIRECTION_IS_VERTICAL (dir);

    float asc = 0.f, desc = 0.f, gap = 0.f;
    get_line_metrics (dir, &asc, &desc, &gap);
    float step = fabsf (asc - desc + gap + (float) line_space);
    if (!(step > 0.f))
      step = (float) upem;
    line_step = step;

    for (unsigned li = 0; li < lines.size (); li++)
    {
      float off_x = vertical ? -(step * (float) li) : 0.f;
      float off_y = vertical ?  0.f                  : -(step * (float) li);

      for (const auto &g : lines[li].glyphs)
      {
        float pen_x = g.x + off_x;
        float pen_y = g.y + off_y;

        if (use_foreground_palette)
        {
          const rgba_color_t &c =
            g_array_index (foreground_palette, rgba_color_t,
                           palette_glyph_index++ % foreground_palette->len);
          hb_vector_paint_set_foreground (paint, HB_COLOR (c.b, c.g, c.r, c.a));
        }

        hb_vector_paint_set_transform (paint, 1.f, 0.f, 0.f, 1.f, 0.f, 0.f);
        if (hb_vector_paint_glyph (paint, upem_font, g.gid, pen_x, pen_y,
                                   extents_mode))
        {
          had_paint = true;
          continue;
        }

        if (hb_vector_draw_glyph (draw, upem_font, g.gid, pen_x, pen_y,
                                  extents_mode))
          had_draw = true;
      }
    }

    hb_blob_t *draw_blob = had_draw ? hb_vector_draw_render (draw) : nullptr;
    hb_blob_t *paint_blob = had_paint ? hb_vector_paint_render (paint) : nullptr;

    if (draw_blob && draw_blob != hb_blob_get_empty () &&
        paint_blob && paint_blob != hb_blob_get_empty ())
      write_combined_svg (draw_blob, paint_blob);
    else if (paint_blob && paint_blob != hb_blob_get_empty ())
      write_blob (paint_blob, false);
    else if (draw_blob && draw_blob != hb_blob_get_empty ())
      write_blob (draw_blob, true);

    hb_blob_destroy (draw_blob);
    hb_blob_destroy (paint_blob);
    hb_vector_draw_destroy (draw);
    hb_vector_paint_destroy (paint);

    hb_font_destroy (upem_font);
    upem_font = nullptr;
    hb_font_destroy (font);
    font = nullptr;
    lines.clear ();
  }

  private:

  struct glyph_instance_t
  {
    hb_codepoint_t gid;
    float x;
    float y;
    float ax;
    float ay;
  };

  struct line_t
  {
    float advance_x = 0.f;
    float advance_y = 0.f;
    std::vector<glyph_instance_t> glyphs;
  };

  struct slice_t
  {
    const char *p = nullptr;
    unsigned len = 0;
  };

  bool compute_extents (hb_vector_extents_t *extents)
  {
    if (lines.empty ())
    {
      extents->x = 0;
      extents->y = 0;
      extents->width = 1;
      extents->height = 1;
      return true;
    }

    hb_direction_t dir = direction;
    if (dir == HB_DIRECTION_INVALID)
      dir = HB_DIRECTION_LTR;
    bool vertical = HB_DIRECTION_IS_VERTICAL (dir);

    float asc = 0.f, desc = 0.f, gap = 0.f;
    get_line_metrics (dir, &asc, &desc, &gap);
    float step = fabsf (asc - desc + gap + (float) line_space);
    if (!(step > 0.f))
      step = (float) upem;

    bool valid = false;
    float x1 = 0, y1 = 0, x2 = 0, y2 = 0;
    bool include_logical = include_logical_extents ();
    bool include_ink = include_ink_extents ();

    auto include_point = [&] (float x, float y)
    {
      y = -y;
      if (!valid)
      {
        x1 = x2 = x;
        y1 = y2 = y;
        valid = true;
        return;
      }
      x1 = hb_min (x1, x);
      y1 = hb_min (y1, y);
      x2 = hb_max (x2, x);
      y2 = hb_max (y2, y);
    };

    for (unsigned li = 0; li < lines.size (); li++)
    {
      float off_x = vertical ? -(step * (float) li) : 0.f;
      float off_y = vertical ?  0.f                  : -(step * (float) li);

      const line_t &line = lines[li];

      if (include_logical)
      {
        float log_min = hb_min (desc, asc);
        float log_max = hb_max (desc, asc);
        if (vertical)
        {
          include_point (off_x + log_min, hb_min (0.f, line.advance_y));
          include_point (off_x + log_max, hb_max (0.f, line.advance_y));
        }
        else
        {
          include_point (hb_min (0.f, line.advance_x), off_y + log_min);
          include_point (hb_max (0.f, line.advance_x), off_y + log_max);
        }
      }

      if (include_ink)
        for (const auto &g : line.glyphs)
        {
          hb_glyph_extents_t ge;
          if (!hb_font_get_glyph_extents (upem_font, g.gid, &ge))
            continue;

          float gx = g.x + off_x;
          float gy = g.y + off_y;

          float xa = gx + ge.x_bearing;
          float ya = gy + ge.y_bearing;
          float xb = xa + ge.width;
          float yb = ya + ge.height;

          include_point (xa, ya);
          include_point (xb, yb);
        }
    }

    if (!valid)
    {
      extents->x = 0;
      extents->y = 0;
      extents->width = 1;
      extents->height = 1;
      return true;
    }

    if (x2 <= x1)
      x2 = x1 + 1;
    if (y2 <= y1)
      y2 = y1 + 1;

    x1 -= (float) margin.l;
    y1 -= (float) margin.t;
    x2 += (float) margin.r;
    y2 += (float) margin.b;

    extents->x = x1;
    extents->y = y1;
    extents->width = x2 - x1;
    extents->height = y2 - y1;
    return true;
  }

  float scale_to_upem_x (hb_position_t v) const
  {
    return (float) v * upem / x_scale;
  }

  float scale_to_upem_y (hb_position_t v) const
  {
    return (float) v * upem / y_scale;
  }

  static bool slice_svg (const char *data, unsigned len,
                         slice_t *defs, slice_t *body)
  {
    defs->p = nullptr;
    defs->len = 0;
    body->p = nullptr;
    body->len = 0;

    const char *data_end = data + len;
    const char *start = (const char *) memchr (data, '>', len);
    if (!start)
      return false;
    start += 1;

    if (len < 6)
      return false;

    const char *end = nullptr;
    for (const char *p = data_end - 6; p >= data; p--)
    {
      if (memcmp (p, "</svg>", 6) == 0)
      {
        end = p;
        break;
      }
      if (p == data)
        break;
    }
    if (!end || end <= start)
      return false;

    const char *defs_start = start;
    while (defs_start < end &&
           (*defs_start == ' ' || *defs_start == '\t' ||
            *defs_start == '\r' || *defs_start == '\n'))
      defs_start++;
    if (!(defs_start + 6 <= end && memcmp (defs_start, "<defs>", 6) == 0))
      defs_start = nullptr;

    const char *defs_end = nullptr;
    if (defs_start)
      for (const char *p = defs_start + 6; p + 7 <= end; p++)
        if (memcmp (p, "</defs>", 7) == 0)
        {
          defs_end = p;
          break;
        }

    if (defs_start && defs_end && defs_start < end && defs_end < end)
    {
      defs->p = defs_start + strlen ("<defs>");
      defs->len = (unsigned) (defs_end - defs->p);

      const char *body_start = defs_end + strlen ("</defs>");
      if (body_start < end)
      {
        body->p = body_start;
        body->len = (unsigned) (end - body_start);
      }
    }
    else
    {
      body->p = start;
      body->len = (unsigned) (end - start);
    }

    return true;
  }

  void emit_fill_group_open ()
  {
    const bool has_fill_override = fore || foreground_use_palette;
    const bool has_stroke = stroke_enabled && stroke_width > 0.;
    if (!has_fill_override && !has_stroke)
      return;

    fputs ("<g", out_fp);

    if (has_fill_override)
    {
      unsigned r = hb_color_get_red (foreground);
      unsigned g = hb_color_get_green (foreground);
      unsigned b = hb_color_get_blue (foreground);
      unsigned a = hb_color_get_alpha (foreground);
      fprintf (out_fp, " fill=\"#%02X%02X%02X\"", r, g, b);
      if (a != 255)
        fprintf (out_fp, " fill-opacity=\"%.3f\"", (double) a / 255.);
    }
    else
      fputs (" fill=\"none\"", out_fp);

    if (has_stroke)
      emit_stroke_attrs ();

    fprintf (out_fp, ">\n");
  }

  void emit_fill_group_close ()
  {
    if (!(fore || foreground_use_palette || (stroke_enabled && stroke_width > 0.)))
      return;
    fputs ("</g>\n", out_fp);
  }

  void emit_fill_stroke_attrs_for_color (hb_color_t color)
  {
    unsigned r = hb_color_get_red (color);
    unsigned g = hb_color_get_green (color);
    unsigned b = hb_color_get_blue (color);
    unsigned a = hb_color_get_alpha (color);
    fprintf (out_fp, " fill=\"#%02X%02X%02X\"", r, g, b);
    if (a != 255)
      fprintf (out_fp, " fill-opacity=\"%.3f\"", (double) a / 255.);

  }

  void emit_draw_body_with_palette (slice_t body)
  {
    if (!foreground_palette || !foreground_palette->len || !body.p || !body.len)
      return;

    const char *p = body.p;
    const char *end = body.p + body.len;
    unsigned palette_i = 0;
    const bool has_stroke = stroke_enabled && stroke_width > 0.;
    if (has_stroke)
    {
      fputs ("<g", out_fp);
      emit_stroke_attrs ();
      fputs (">\n", out_fp);
    }
    while (p < end)
    {
      const char *line_end = (const char *) memchr (p, '\n', (size_t) (end - p));
      if (!line_end)
        line_end = end;

      const char *s = p;
      while (s < line_end && (*s == ' ' || *s == '\t' || *s == '\r'))
        s++;
      bool is_elem = s < line_end && *s == '<' && (s + 1 < line_end && s[1] != '/');

      if (is_elem)
      {
        const rgba_color_t &c =
          g_array_index (foreground_palette, rgba_color_t,
                         palette_i++ % foreground_palette->len);
        hb_color_t col = HB_COLOR (c.b, c.g, c.r, c.a);
        fputs ("<g", out_fp);
        emit_fill_stroke_attrs_for_color (col);
        fputs (">", out_fp);
        fwrite (p, 1, (size_t) (line_end - p), out_fp);
        fputs ("</g>\n", out_fp);
      }
      else
      {
        fwrite (p, 1, (size_t) (line_end - p), out_fp);
        fputc ('\n', out_fp);
      }

      p = line_end;
      if (p < end && *p == '\n')
        p++;
    }
    if (has_stroke)
      fputs ("</g>\n", out_fp);
  }

  void emit_stroke_attrs ()
  {
    fprintf (out_fp, " stroke=\"#%02X%02X%02X\"",
             stroke_color.r, stroke_color.g, stroke_color.b);
    if (stroke_color.a != 255)
      fprintf (out_fp, " stroke-opacity=\"%.3f\"", (double) stroke_color.a / 255.);
    fprintf (out_fp, " stroke-width=\"%.*g\" stroke-linecap=\"round\" stroke-linejoin=\"round\"",
             precision + 4, normalized_stroke_width);
  }

  double get_normalized_stroke_width () const
  {
    if (!stroke_enabled || stroke_width <= 0. || !upem || !x_scale || !y_scale)
      return stroke_width;

    /* hb-vector emits geometry in upem space; convert user stroke width
     * from the requested font scale back into that normalized coordinate system.
     */
    double norm_x = stroke_width * scalbn ((double) upem, (int) subpixel_bits) / fabs ((double) x_scale);
    double norm_y = stroke_width * scalbn ((double) upem, (int) subpixel_bits) / fabs ((double) y_scale);
    return 0.5 * (norm_x + norm_y);
  }

  bool lookup_palette_color (unsigned idx, hb_color_t *color) const
  {
    if (idx < custom_palette_has_value.size () && custom_palette_has_value[idx])
    {
      *color = custom_palette_values[idx];
      return true;
    }

    if (!palette_lookup_enabled)
      return false;

    if (palette_cache_state.size () <= idx)
    {
      palette_cache_state.resize (idx + 1, 0);
      palette_cache_values.resize (idx + 1, HB_COLOR (0, 0, 0, 255));
    }

    if (palette_cache_state[idx] == 1)
    {
      *color = palette_cache_values[idx];
      return true;
    }
    if (palette_cache_state[idx] == 2)
      return false;

    hb_color_t fetched = HB_COLOR (0, 0, 0, 255);
    unsigned n = 1;
    if (hb_ot_color_palette_get_colors (palette_face, palette_lookup_index,
                                        idx, &n, &fetched) && n)
    {
      palette_cache_values[idx] = fetched;
      palette_cache_state[idx] = 1;
      *color = fetched;
      return true;
    }

    palette_cache_state[idx] = 2;
    return false;
  }

  static inline const char *
  skip_ascii_ws (const char *p, const char *end)
  {
    while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r'))
      p++;
    return p;
  }

  static inline const char *
  trim_ascii_ws_right (const char *begin, const char *end)
  {
    while (end > begin &&
           (end[-1] == ' ' || end[-1] == '\t' || end[-1] == '\n' || end[-1] == '\r'))
      end--;
    return end;
  }

  struct parsed_var_color_expr_t
  {
    const char *expr_end = nullptr;
    const char *fallback_begin = nullptr;
    const char *fallback_end = nullptr;
    unsigned idx = 0;
    bool parse_ok = false;
    bool have_digits = false;
    bool has_fallback = false;
  };

  static inline const char *
  find_next_var_expr (const char *p, const char *end)
  {
    for (const char *q = p; q + 4 <= end; q++)
      if (memcmp (q, "var(", 4) == 0)
        return q;
    return nullptr;
  }

  static inline const char *
  find_matching_paren (const char *inside, const char *end)
  {
    unsigned depth = 1;
    for (const char *q = inside; q < end; q++)
    {
      if (*q == '(') depth++;
      else if (*q == ')')
      {
        depth--;
        if (!depth)
          return q;
      }
    }
    return nullptr;
  }

  static inline parsed_var_color_expr_t
  parse_var_color_expr (const char *match, const char *end)
  {
    parsed_var_color_expr_t parsed;
    const char *expr_close = find_matching_paren (match + 4, end);
    if (!expr_close)
      return parsed;

    parsed.expr_end = expr_close + 1;
    const char *inside = match + 4;
    const char *inside_end = expr_close;
    const char *q = skip_ascii_ws (inside, inside_end);
    parsed.parse_ok = true;

    if (!(q + 7 <= inside_end && memcmp (q, "--color", 7) == 0))
      parsed.parse_ok = false;
    if (parsed.parse_ok)
      q += 7;

    while (parsed.parse_ok && q < inside_end && *q >= '0' && *q <= '9')
    {
      parsed.have_digits = true;
      parsed.idx = parsed.idx * 10 + (unsigned) (*q - '0');
      q++;
    }
    q = skip_ascii_ws (q, inside_end);

    if (parsed.parse_ok && q < inside_end && *q == ',')
    {
      parsed.has_fallback = true;
      q++;
      parsed.fallback_begin = skip_ascii_ws (q, inside_end);
      parsed.fallback_end = trim_ascii_ws_right (parsed.fallback_begin, inside_end);
      q = inside_end;
    }

    if (parsed.parse_ok && q != inside_end)
      parsed.parse_ok = false;

    return parsed;
  }

  void emit_css_color (hb_color_t color)
  {
    unsigned r = hb_color_get_red (color);
    unsigned g = hb_color_get_green (color);
    unsigned b = hb_color_get_blue (color);
    unsigned a = hb_color_get_alpha (color);
    if (a == 255)
      fprintf (out_fp, "#%02X%02X%02X", r, g, b);
    else
      fprintf (out_fp, "rgba(%u,%u,%u,%.6g)", r, g, b, (double) a / 255.);
  }

  void write_slice_resolving_palette_vars (slice_t s)
  {
    if (!s.p || !s.len)
      return;

    const char *p = s.p;
    const char *end = s.p + s.len;
    while (p < end)
    {
      const char *match = find_next_var_expr (p, end);

      if (!match)
      {
        fwrite (p, 1, (size_t) (end - p), out_fp);
        return;
      }

      if (match > p)
        fwrite (p, 1, (size_t) (match - p), out_fp);

      parsed_var_color_expr_t parsed = parse_var_color_expr (match, end);
      if (!parsed.expr_end)
      {
        fwrite (match, 1, (size_t) (end - match), out_fp);
        return;
      }

      hb_color_t color;
      if (parsed.parse_ok && parsed.have_digits && lookup_palette_color (parsed.idx, &color))
      {
        emit_css_color (color);
      }
      else if (parsed.has_fallback && parsed.fallback_begin &&
               parsed.fallback_end >= parsed.fallback_begin)
      {
        fwrite (parsed.fallback_begin, 1,
                (size_t) (parsed.fallback_end - parsed.fallback_begin), out_fp);
      }
      else
      {
        fwrite (match, 1, (size_t) (parsed.expr_end - match), out_fp);
      }

      p = parsed.expr_end;
    }
  }

  void write_blob (hb_blob_t *blob,
                   hb_bool_t is_draw_blob)
  {
    unsigned in_len = 0;
    const char *in_data = hb_blob_get_data (blob, &in_len);
    if (!in_data || !in_len)
      return;

    const char *hdr_end = (const char *) memchr (in_data, '>', in_len);
    if (!hdr_end)
      return;

    slice_t defs, body;
    if (!slice_svg (in_data, in_len, &defs, &body))
      return;

    fwrite (in_data, 1, (hdr_end - in_data + 1), out_fp);
    fputc ('\n', out_fp);

    if (defs.len)
    {
      fputs ("<defs>\n", out_fp);
      write_slice_resolving_palette_vars (defs);
      fputs ("</defs>\n", out_fp);
    }

    emit_background_rect ();

    if (is_draw_blob && foreground_use_palette && foreground_palette && foreground_palette->len)
      emit_draw_body_with_palette (body);
    else if (is_draw_blob)
      emit_fill_group_open ();

    if (body.len && !(is_draw_blob && foreground_use_palette && foreground_palette && foreground_palette->len))
      write_slice_resolving_palette_vars (body);

    if (is_draw_blob && !(foreground_use_palette && foreground_palette && foreground_palette->len))
      emit_fill_group_close ();

    if (show_extents)
      emit_extents_overlay (direction == HB_DIRECTION_INVALID ? HB_DIRECTION_LTR : direction, line_step);

    fputs ("</svg>\n", out_fp);
  }

  void write_combined_svg (hb_blob_t *draw_blob,
                           hb_blob_t *paint_blob)
  {
    unsigned draw_len = 0, paint_len = 0;
    const char *draw_data = hb_blob_get_data (draw_blob, &draw_len);
    const char *paint_data = hb_blob_get_data (paint_blob, &paint_len);
    if (!draw_data || !paint_data)
      return;

    const char *draw_hdr_end = (const char *) memchr (draw_data, '>', draw_len);
    if (!draw_hdr_end)
      return;

    slice_t draw_defs, draw_body;
    slice_t paint_defs, paint_body;
    if (!slice_svg (draw_data, draw_len, &draw_defs, &draw_body) ||
        !slice_svg (paint_data, paint_len, &paint_defs, &paint_body))
      return;

    fwrite (draw_data, 1, (draw_hdr_end - draw_data + 1), out_fp);
    fputc ('\n', out_fp);

    if (draw_defs.len || paint_defs.len)
    {
      fputs ("<defs>\n", out_fp);
      if (draw_defs.len)
        write_slice_resolving_palette_vars (draw_defs);
      if (paint_defs.len)
        write_slice_resolving_palette_vars (paint_defs);
      fputs ("</defs>\n", out_fp);
    }

    emit_background_rect ();

    if (foreground_use_palette && foreground_palette && foreground_palette->len)
      emit_draw_body_with_palette (draw_body);
    else
    {
      emit_fill_group_open ();
      if (draw_body.len)
        fwrite (draw_body.p, 1, draw_body.len, out_fp);
      emit_fill_group_close ();
    }
    if (paint_body.len)
      write_slice_resolving_palette_vars (paint_body);

    if (show_extents)
      emit_extents_overlay (direction == HB_DIRECTION_INVALID ? HB_DIRECTION_LTR : direction, line_step);

    fputs ("</svg>\n", out_fp);
  }

  void emit_background_rect ()
  {
    if (!has_background)
      return;
    unsigned r = hb_color_get_red (background);
    unsigned g = hb_color_get_green (background);
    unsigned b = hb_color_get_blue (background);
    unsigned a = hb_color_get_alpha (background);
    fprintf (out_fp, "<rect x=\"%.*g\" y=\"%.*g\" width=\"%.*g\" height=\"%.*g\" fill=\"#%02X%02X%02X\"",
             precision + 4, (double) final_extents.x,
             precision + 4, (double) final_extents.y,
             precision + 4, (double) final_extents.width,
             precision + 4, (double) final_extents.height,
             r, g, b);
    if (a != 255)
      fprintf (out_fp, " fill-opacity=\"%.3f\"", (double) a / 255.);
    fputs ("/>\n", out_fp);
  }

  void apply_custom_palette (hb_vector_paint_t *paint)
  {
    hb_vector_paint_clear_custom_palette_colors (paint);
    for (unsigned idx = 0; idx < custom_palette_values.size (); idx++)
      if (idx < custom_palette_has_value.size () && custom_palette_has_value[idx])
        hb_vector_paint_set_custom_palette_color (paint, idx, custom_palette_values[idx]);
  }

  void init_palette_color_cache ()
  {
    palette_cache_values.clear ();
    palette_cache_state.clear ();
    palette_face = nullptr;
    palette_lookup_index = 0;
    palette_lookup_enabled = false;

    hb_face_t *face = hb_font_get_face (upem_font ? upem_font : font);
    if (!face)
      return;

    unsigned palette_count = hb_ot_color_palette_get_count (face);
    if (!palette_count)
      return;

    unsigned palette_index = palette >= 0 ? (unsigned) palette : 0u;
    if (palette_index >= palette_count)
      palette_index = 0;

    palette_face = face;
    palette_lookup_index = palette_index;
    palette_lookup_enabled = true;
  }

  void load_custom_palette_overrides_from_view ()
  {
    custom_palette_values.clear ();
    custom_palette_has_value.clear ();
    if (!custom_palette_entries)
      return;

    for (unsigned i = 0; i < custom_palette_entries->len; i++)
    {
      const custom_palette_entry_t &entry =
        g_array_index (custom_palette_entries, custom_palette_entry_t, i);
      unsigned idx = entry.index;
      if (custom_palette_values.size () <= idx)
      {
        custom_palette_values.resize (idx + 1, HB_COLOR (0, 0, 0, 255));
        custom_palette_has_value.resize (idx + 1, false);
      }
      custom_palette_values[idx] = HB_COLOR (entry.color.b,
                                             entry.color.g,
                                             entry.color.r,
                                             entry.color.a);
      custom_palette_has_value[idx] = true;
    }
  }

  void emit_extents_overlay (hb_direction_t dir, float step)
  {
    const bool vertical = HB_DIRECTION_IS_VERTICAL (dir);
    const float dot_r = hb_max (1.f, step * 0.01f);
    fputs ("<g fill=\"#FF0000\" fill-opacity=\"0.502\" stroke=\"none\">\n", out_fp);
    for (unsigned li = 0; li < lines.size (); li++)
    {
      float off_x = vertical ? -(step * (float) li) : 0.f;
      float off_y = vertical ?  0.f                  : -(step * (float) li);
      const line_t &line = lines[li];
      for (const auto &g : line.glyphs)
      {
        fprintf (out_fp, "<circle cx=\"%.*g\" cy=\"%.*g\" r=\"%.*g\"/>\n",
                 precision + 4, (double) (g.x + off_x),
                 precision + 4, (double) (-(g.y + off_y)),
                 precision + 4, (double) dot_r);
      }
    }
    fputs ("</g>\n", out_fp);

    fprintf (out_fp, "<g fill=\"none\" stroke=\"#FF00FF\" stroke-opacity=\"0.502\" stroke-width=\"%.*g\">\n",
             precision + 4, normalized_stroke_width);
    for (unsigned li = 0; li < lines.size (); li++)
    {
      float off_x = vertical ? -(step * (float) li) : 0.f;
      float off_y = vertical ?  0.f                  : -(step * (float) li);
      const line_t &line = lines[li];
      for (const auto &g : line.glyphs)
      {
        hb_glyph_extents_t ge;
        if (!hb_font_get_glyph_extents (upem_font, g.gid, &ge))
          continue;
        float x = g.x + off_x + ge.x_bearing;
        float y = g.y + off_y + ge.y_bearing;
        float w = ge.width;
        float h = ge.height;
        if (!w || !h)
          continue;
        fprintf (out_fp, "<rect x=\"%.*g\" y=\"%.*g\" width=\"%.*g\" height=\"%.*g\"/>\n",
                 precision + 4, (double) x,
                 precision + 4, (double) (-y),
                 precision + 4, (double) w,
                 precision + 4, (double) (-h));
      }
    }
    fputs ("</g>\n", out_fp);
  }

  void get_line_metrics (hb_direction_t dir, float *asc, float *desc, float *gap) const
  {
    if (have_font_extents)
    {
      if (asc) *asc = (float) font_extents.ascent;
      if (desc) *desc = (float) (-font_extents.descent);
      if (gap) *gap = (float) font_extents.line_gap;
      return;
    }

    hb_font_extents_t fext = {0, 0, 0};
    hb_font_get_extents_for_direction (upem_font, dir, &fext);
    if (asc) *asc = (float) fext.ascender;
    if (desc) *desc = (float) fext.descender;
    if (gap) *gap = (float) fext.line_gap;
  }

  hb_bool_t flat = false;
  int precision = 2;
  hb_color_t background = HB_COLOR (255, 255, 255, 255);
  hb_color_t foreground = HB_COLOR (0, 0, 0, 255);

  hb_font_t *font = nullptr;
  hb_font_t *upem_font = nullptr;
  std::vector<line_t> lines;
  hb_direction_t direction = HB_DIRECTION_INVALID;

  int x_scale = 0;
  int y_scale = 0;
  unsigned upem = 0;
  unsigned subpixel_bits = 0;
  double normalized_stroke_width = DEFAULT_STROKE_WIDTH;
  float line_step = 0.f;
  hb_vector_extents_t final_extents = {0, 0, 1, 1};
  std::vector<hb_color_t> custom_palette_values;
  std::vector<bool> custom_palette_has_value;
  mutable std::vector<hb_color_t> palette_cache_values;
  mutable std::vector<unsigned char> palette_cache_state;
  hb_face_t *palette_face = nullptr;
  unsigned palette_lookup_index = 0;
  bool palette_lookup_enabled = false;
};

#endif
