/*
 * Copyright © 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.hh"

#include "hb-face.hh"
#include "hb-vector-svg-subset.hh"
#ifndef HB_NO_SVG
#include "OT/Color/svg/svg.hh"
#include "hb-raster-svg-parse.hh"
#endif
#include "hb-vector-svg-utils.hh"
#include "hb-map.hh"
#include "hb-ot-color.h"

#include <ctype.h>
#include <stdio.h>
#include <string.h>

#ifndef HB_NO_SVG
#include "hb-vector-svg.hh"

static bool
hb_svg_range_is_valid (unsigned start,
                       unsigned end,
                       unsigned len)
{
  return start <= end && end <= len;
}

static bool
hb_svg_append_with_prefix (hb_vector_t<char> *out,
                           const char *s,
                           unsigned n,
                           const char *prefix,
                           unsigned prefix_len)
{
  unsigned i = 0;
  while (i < n)
  {
    if (i + 4 <= n && !memcmp (s + i, "id=\"", 4))
    {
      if (!hb_svg_append_len (out, s + i, 4)) return false;
      i += 4;
      if (!hb_svg_append_len (out, prefix, prefix_len)) return false;
      while (i < n && s[i] != '"')
      {
        if (!hb_svg_append_c (out, s[i])) return false;
        i++;
      }
      continue;
    }
    if (i + 4 <= n && !memcmp (s + i, "id='", 4))
    {
      if (!hb_svg_append_len (out, s + i, 4)) return false;
      i += 4;
      if (!hb_svg_append_len (out, prefix, prefix_len)) return false;
      while (i < n && s[i] != '\'')
      {
        if (!hb_svg_append_c (out, s[i])) return false;
        i++;
      }
      continue;
    }
    if (i + 7 <= n && !memcmp (s + i, "href=\"#", 7))
    {
      if (!hb_svg_append_len (out, s + i, 7)) return false;
      i += 7;
      if (!hb_svg_append_len (out, prefix, prefix_len)) return false;
      while (i < n && s[i] != '"')
      {
        if (!hb_svg_append_c (out, s[i])) return false;
        i++;
      }
      continue;
    }
    if (i + 7 <= n && !memcmp (s + i, "href='#", 7))
    {
      if (!hb_svg_append_len (out, s + i, 7)) return false;
      i += 7;
      if (!hb_svg_append_len (out, prefix, prefix_len)) return false;
      while (i < n && s[i] != '\'')
      {
        if (!hb_svg_append_c (out, s[i])) return false;
        i++;
      }
      continue;
    }
    if (i + 13 <= n && !memcmp (s + i, "xlink:href=\"#", 13))
    {
      if (!hb_svg_append_len (out, s + i, 13)) return false;
      i += 13;
      if (!hb_svg_append_len (out, prefix, prefix_len)) return false;
      while (i < n && s[i] != '"')
      {
        if (!hb_svg_append_c (out, s[i])) return false;
        i++;
      }
      continue;
    }
    if (i + 13 <= n && !memcmp (s + i, "xlink:href='#", 13))
    {
      if (!hb_svg_append_len (out, s + i, 13)) return false;
      i += 13;
      if (!hb_svg_append_len (out, prefix, prefix_len)) return false;
      while (i < n && s[i] != '\'')
      {
        if (!hb_svg_append_c (out, s[i])) return false;
        i++;
      }
      continue;
    }
    if (i + 5 <= n && !memcmp (s + i, "url(#", 5))
    {
      if (!hb_svg_append_len (out, s + i, 5)) return false;
      i += 5;
      if (!hb_svg_append_len (out, prefix, prefix_len)) return false;
      while (i < n && s[i] != ')')
      {
        if (!hb_svg_append_c (out, s[i])) return false;
        i++;
      }
      continue;
    }
    if (i + 6 <= n && !memcmp (s + i, "url(\"#", 6))
    {
      if (!hb_svg_append_len (out, s + i, 6)) return false;
      i += 6;
      if (!hb_svg_append_len (out, prefix, prefix_len)) return false;
      while (i < n && s[i] != '"')
      {
        if (!hb_svg_append_c (out, s[i])) return false;
        i++;
      }
      continue;
    }
    if (i + 6 <= n && !memcmp (s + i, "url('#", 6))
    {
      if (!hb_svg_append_len (out, s + i, 6)) return false;
      i += 6;
      if (!hb_svg_append_len (out, prefix, prefix_len)) return false;
      while (i < n && s[i] != '\'')
      {
        if (!hb_svg_append_c (out, s[i])) return false;
        i++;
      }
      continue;
    }
    if (!hb_svg_append_c (out, s[i]))
      return false;
    i++;
  }
  return true;
}

static bool
hb_svg_add_unique_id (hb_vector_t<OT::SVG::svg_id_span_t> *v,
                      hb_hashmap_t<OT::SVG::svg_id_span_t, hb_bool_t> *seen_ids,
                      const char *p,
                      unsigned n)
{
  if (!n) return true;
  OT::SVG::svg_id_span_t key = {p, n};
  if (seen_ids->has (key))
    return true;
  if (unlikely (!seen_ids->set (key, true)))
    return false;
  auto *slot = v->push ();
  if (unlikely (v->in_error ()))
    return false;
  *slot = key;
  return true;
}

static bool
hb_svg_collect_refs (const char *s,
                     unsigned n,
                     hb_vector_t<OT::SVG::svg_id_span_t> *ids,
                     hb_hashmap_t<OT::SVG::svg_id_span_t, hb_bool_t> *seen_ids)
{
  unsigned i = 0;
  while (i < n)
  {
    if (i + 7 <= n && !memcmp (s + i, "href=\"#", 7))
    {
      i += 7;
      unsigned b = i;
      while (i < n && s[i] != '"' && s[i] != '\'' && s[i] != ' ' && s[i] != '>') i++;
      if (i > b && unlikely (!hb_svg_add_unique_id (ids, seen_ids, s + b, i - b))) return false;
      continue;
    }
    if (i + 7 <= n && !memcmp (s + i, "href='#", 7))
    {
      i += 7;
      unsigned b = i;
      while (i < n && s[i] != '\'' && s[i] != '"' && s[i] != ' ' && s[i] != '>') i++;
      if (i > b && unlikely (!hb_svg_add_unique_id (ids, seen_ids, s + b, i - b))) return false;
      continue;
    }
    if (i + 13 <= n && !memcmp (s + i, "xlink:href=\"#", 13))
    {
      i += 13;
      unsigned b = i;
      while (i < n && s[i] != '"' && s[i] != '\'' && s[i] != ' ' && s[i] != '>') i++;
      if (i > b && unlikely (!hb_svg_add_unique_id (ids, seen_ids, s + b, i - b))) return false;
      continue;
    }
    if (i + 13 <= n && !memcmp (s + i, "xlink:href='#", 13))
    {
      i += 13;
      unsigned b = i;
      while (i < n && s[i] != '\'' && s[i] != '"' && s[i] != ' ' && s[i] != '>') i++;
      if (i > b && unlikely (!hb_svg_add_unique_id (ids, seen_ids, s + b, i - b))) return false;
      continue;
    }
    if (i + 5 <= n && !memcmp (s + i, "url(#", 5))
    {
      i += 5;
      unsigned b = i;
      while (i < n && s[i] != ')') i++;
      if (i > b && unlikely (!hb_svg_add_unique_id (ids, seen_ids, s + b, i - b))) return false;
      continue;
    }
    if (i + 6 <= n && !memcmp (s + i, "url(\"#", 6))
    {
      i += 6;
      unsigned b = i;
      while (i < n && s[i] != '"') i++;
      if (i > b && unlikely (!hb_svg_add_unique_id (ids, seen_ids, s + b, i - b))) return false;
      continue;
    }
    if (i + 6 <= n && !memcmp (s + i, "url('#", 6))
    {
      i += 6;
      unsigned b = i;
      while (i < n && s[i] != '\'') i++;
      if (i > b && unlikely (!hb_svg_add_unique_id (ids, seen_ids, s + b, i - b))) return false;
      continue;
    }
    i++;
  }
  return true;
}

static bool
hb_svg_find_root_open_tag (const char *svg,
                           unsigned len,
                           unsigned *root_open_len,
                           bool *missing_viewport)
{
  hb_svg_xml_parser_t parser (svg, len);
  hb_svg_token_type_t tok = parser.next ();
  if (!((tok == SVG_TOKEN_OPEN_TAG || tok == SVG_TOKEN_SELF_CLOSE_TAG) &&
        parser.tag_name.eq ("svg")))
    return false;

  if (missing_viewport)
  {
    *missing_viewport =
      parser.find_attr ("width").is_null () ||
      parser.find_attr ("height").is_null ();
  }

  if (root_open_len)
    *root_open_len = (unsigned) (parser.p - parser.tag_start);
  return true;
}

bool
hb_svg_subset_glyph_image (hb_face_t *face,
                           hb_blob_t *image,
                           hb_codepoint_t glyph,
                           unsigned *image_counter,
                           hb_vector_t<char> *defs_dst,
                           hb_vector_t<char> *body_dst)
{
  bool ret = false;
  hb_blob_t *normalized_image = nullptr;
  unsigned len = 0;
  const char *svg = nullptr;
  unsigned doc_index = 0;
  hb_codepoint_t start_glyph = HB_CODEPOINT_INVALID;
  hb_codepoint_t end_glyph = HB_CODEPOINT_INVALID;
  const OT::SVG::svg_doc_cache_t *doc_cache = nullptr;
  unsigned glyph_start = 0, glyph_end = 0;
  const hb_vector_t<OT::SVG::svg_defs_entry_t> *defs_entries = nullptr;
  hb_vector_t<OT::SVG::svg_id_span_t> needed_ids;
  hb_hashmap_t<OT::SVG::svg_id_span_t, hb_bool_t> needed_ids_set;
  hb_vector_t<unsigned> chosen_defs;
  hb_vector_t<uint8_t> chosen_def_marks;
  unsigned root_open_len = 0;
  bool glyph_is_root_svg = false;
  bool root_missing_viewport = false;
  char prefix[32];
  int prefix_len = 0;

  if (glyph == HB_CODEPOINT_INVALID || !image_counter || !defs_dst || !body_dst)
    goto done;

  normalized_image = OT::hb_ot_svg_reference_normalized_blob (image, &svg, &len);
  if (!normalized_image || !svg || !len)
    goto done;

  if (!hb_ot_color_glyph_get_svg_document_index (face, glyph, &doc_index))
    goto done;

  if (!hb_ot_color_get_svg_document_glyph_range (face, doc_index, &start_glyph, &end_glyph))
    goto done;

  doc_cache = face->table.SVG->get_or_create_doc_cache (normalized_image, svg, len,
                                                        doc_index, start_glyph, end_glyph);
  if (!doc_cache)
    goto done;
  svg = face->table.SVG->doc_cache_get_svg (doc_cache, &len);

  if (!face->table.SVG->doc_cache_get_glyph_span (doc_cache, glyph, &glyph_start, &glyph_end))
    goto done;
  if (unlikely (!hb_svg_range_is_valid (glyph_start, glyph_end, len)))
    goto done;

  defs_entries = face->table.SVG->doc_cache_get_defs_entries (doc_cache);
  if (!hb_svg_find_root_open_tag (svg, len, &root_open_len, &root_missing_viewport))
    goto done;
  glyph_is_root_svg = glyph_start == 0;

  needed_ids.alloc (16);
  if (!hb_svg_collect_refs (svg + glyph_start, glyph_end - glyph_start,
                            &needed_ids, &needed_ids_set))
    goto done;

  chosen_defs.alloc (16);
  if (unlikely (!chosen_def_marks.resize ((int) defs_entries->length)))
    goto done;
  hb_memset (chosen_def_marks.arrayZ, 0, defs_entries->length);
  for (unsigned qi = 0; qi < needed_ids.length; qi++)
  {
    const auto &need = needed_ids.arrayZ[qi];
    for (unsigned i = 0; i < defs_entries->length; i++)
    {
      const auto &e = defs_entries->arrayZ[i];
      if (e.id.len == need.len && !memcmp (e.id.p, need.p, need.len))
      {
        if (!chosen_def_marks.arrayZ[i])
        {
          chosen_def_marks.arrayZ[i] = 1;
          if (unlikely (!chosen_defs.push_or_fail (i)))
            goto done;
          if (unlikely (!hb_svg_range_is_valid (e.start, e.end, len)))
            goto done;
          if (!hb_svg_collect_refs (svg + e.start, e.end - e.start,
                                    &needed_ids, &needed_ids_set))
            goto done;
        }
        break;
      }
    }
  }

  prefix_len = snprintf (prefix, sizeof (prefix), "hbimg%u_", (*image_counter)++);
  if (prefix_len <= 0 || (unsigned) prefix_len >= sizeof (prefix))
    goto done;

  body_dst->alloc (body_dst->length + (glyph_end - glyph_start) + 64);

  for (unsigned i = 0; i < chosen_defs.length; i++)
  {
    const auto &e = defs_entries->arrayZ[chosen_defs.arrayZ[i]];
    if (unlikely (!hb_svg_range_is_valid (e.start, e.end, len)))
      goto done;
    if (!hb_svg_append_with_prefix (defs_dst, svg + e.start, e.end - e.start, prefix, (unsigned) prefix_len))
      goto done;
    if (!hb_svg_append_c (defs_dst, '\n'))
      goto done;
  }

  if (glyph_is_root_svg && root_missing_viewport && root_open_len > 1)
  {
    if (unlikely (root_open_len > glyph_end - glyph_start))
      goto done;
    unsigned upem = face->get_upem ();
    if (!hb_svg_append_with_prefix (body_dst,
                                    svg + glyph_start,
                                    root_open_len - 1,
                                    prefix,
                                    (unsigned) prefix_len))
      goto done;
    if (!hb_svg_append_str (body_dst, " width=\""))
      goto done;
    if (!hb_svg_append_unsigned (body_dst, upem))
      goto done;
    if (!hb_svg_append_str (body_dst, "\" height=\""))
      goto done;
    if (!hb_svg_append_unsigned (body_dst, upem))
      goto done;
    if (!hb_svg_append_str (body_dst, "\" overflow=\"visible"))
      goto done;
    if (!hb_svg_append_c (body_dst, '"'))
      goto done;
    if (!hb_svg_append_c (body_dst, '>'))
      goto done;
    ret = hb_svg_append_with_prefix (body_dst,
                                     svg + glyph_start + root_open_len,
                                     glyph_end - glyph_start - root_open_len,
                                     prefix,
                                     (unsigned) prefix_len);
  }
  else if (glyph_is_root_svg && root_open_len > 1)
  {
    if (unlikely (root_open_len > glyph_end - glyph_start))
      goto done;
    if (!hb_svg_append_with_prefix (body_dst,
                                    svg + glyph_start,
                                    root_open_len - 1,
                                    prefix,
                                    (unsigned) prefix_len))
      goto done;
    if (!hb_svg_append_str (body_dst, " overflow=\"visible\""))
      goto done;
    if (!hb_svg_append_c (body_dst, '>'))
      goto done;
    ret = hb_svg_append_with_prefix (body_dst,
                                     svg + glyph_start + root_open_len,
                                     glyph_end - glyph_start - root_open_len,
                                     prefix,
                                     (unsigned) prefix_len);
  }
  else
    ret = hb_svg_append_with_prefix (body_dst,
                                     svg + glyph_start,
                                     glyph_end - glyph_start,
                                     prefix,
                                     (unsigned) prefix_len);

done:
  hb_blob_destroy (normalized_image);
  return ret;
}
#else
bool
hb_svg_subset_glyph_image (hb_face_t *face HB_UNUSED,
                           hb_blob_t *image HB_UNUSED,
                           hb_codepoint_t glyph HB_UNUSED,
                           unsigned *image_counter HB_UNUSED,
                           hb_vector_t<char> *defs_dst HB_UNUSED,
                           hb_vector_t<char> *body_dst HB_UNUSED)
{
  return false;
}
#endif
