/*
 * 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-raster-svg.hh"
#include "hb-raster-svg-base.hh"
#include "hb-raster-svg-parse.hh"
#include "hb-raster-svg-context.hh"
#include "hb-raster-svg-defs-scan.hh"
#include "hb-raster-svg-gradient.hh"
#include "hb-raster-svg-clip.hh"
#include "hb-raster-svg-bbox.hh"
#include "hb-raster-svg-fill.hh"
#include "hb-raster-svg-use.hh"
#include "OT/Color/svg/svg.hh"

#include <assert.h>
#include <stdio.h>

#ifndef HB_NO_RASTER_SVG

#define SVG_MAX_DEPTH 32

/*
 * 11. Element renderer — recursive SVG rendering
 */

static void svg_render_element (hb_svg_render_context_t *ctx,
				hb_svg_xml_parser_t &parser,
				const struct hb_svg_cascade_t &inherited);

/* Gradient def parsing lives in hb-raster-svg-gradient.* */

/* Clip-path defs and push helpers live in hb-raster-svg-clip.* */

/* Render a single shape element */
static void
svg_render_shape (hb_svg_render_context_t *ctx,
		  hb_svg_shape_emit_data_t &shape,
		  const hb_svg_cascade_t &state,
		  hb_svg_str_t transform_str)
{
  bool has_transform = transform_str.len > 0;
  bool has_opacity = state.opacity < 1.f;
  bool has_clip_path = false;
  bool pushed_transform = false;
  bool pushed_opacity_group = false;

  if (has_opacity)
    pushed_opacity_group = ctx->push_group ();

  if (has_transform)
  {
    hb_svg_transform_t t;
    hb_raster_svg_parse_transform (transform_str, &t);
    pushed_transform = ctx->push_transform (t.xx, t.yx, t.xy, t.yy, t.dx, t.dy);
  }

  hb_extents_t<> bbox;
  bool has_bbox = hb_raster_svg_compute_shape_bbox (shape, &bbox);
  has_clip_path = hb_raster_svg_push_clip_path_ref (ctx->paint, &ctx->defs, state.clip_path,
					  has_bbox ? &bbox : nullptr);

  /* Clip with shape path, then fill */
  hb_raster_paint_push_clip_path (ctx->paint, hb_raster_svg_shape_path_emit, &shape);

  /* Default fill is black */
  if (state.fill.is_null ())
  {
    hb_color_t black = HB_COLOR (0, 0, 0, 255);
    if (state.fill_opacity < 1.f)
      black = HB_COLOR (0, 0, 0, (uint8_t) (255 * state.fill_opacity + 0.5f));
    ctx->paint_color (black);
  }
  else
  {
    hb_svg_fill_context_t fill_ctx = {ctx->paint, ctx->pfuncs, ctx->font, ctx->palette, &ctx->defs};
    hb_raster_svg_emit_fill (&fill_ctx, state.fill, state.fill_opacity, has_bbox ? &bbox : nullptr, state.color);
  }

  ctx->pop_clip ();
  if (has_clip_path)
    ctx->pop_clip ();

  if (pushed_transform)
    ctx->pop_transform ();

  if (pushed_opacity_group)
    ctx->pop_group (HB_PAINT_COMPOSITE_MODE_SRC_OVER);
}

static void
svg_render_container_element (hb_svg_render_context_t *ctx,
			      hb_svg_xml_parser_t &parser,
			      hb_svg_str_t tag,
			      bool self_closing,
			      const hb_svg_cascade_t &state,
			      hb_svg_str_t transform_str,
			      hb_svg_str_t clip_path_str)
{
  bool has_transform = transform_str.len > 0;
  bool has_opacity = state.opacity < 1.f;
  bool has_clip = false;
  bool has_viewbox = false;
  bool has_viewbox_transform = false;
  bool has_svg_translate = false;
  float svg_x = 0.f, svg_y = 0.f;
  float viewport_w = 0.f, viewport_h = 0.f;
  hb_svg_transform_t viewbox_t;
  float vb_x = 0, vb_y = 0, vb_w = 0, vb_h = 0;
  bool pushed_opacity_group = false;
  bool pushed_transform = false;
  bool pushed_svg_translate = false;
  bool pushed_viewbox_transform = false;

  if (tag.eq ("svg") || tag.eq ("symbol"))
  {
    hb_svg_style_props_t geom_style_props;
    svg_parse_style_props (parser.find_attr ("style"), &geom_style_props);

    if (tag.eq ("svg"))
    {
      svg_x = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (parser, geom_style_props.x, "x"));
      svg_y = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (parser, geom_style_props.y, "y"));
      has_svg_translate = (svg_x != 0.f || svg_y != 0.f);
    }

    hb_svg_str_t viewbox_str = parser.find_attr ("viewBox");
    if (hb_raster_svg_parse_viewbox (viewbox_str, &vb_x, &vb_y, &vb_w, &vb_h))
    {
      has_viewbox = true;

      if (tag.eq ("svg"))
      {
        viewport_w = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (parser, geom_style_props.width, "width"));
        viewport_h = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (parser, geom_style_props.height, "height"));
        if (!(viewport_w > 0.f && viewport_h > 0.f))
        {
          if (ctx->depth == 1)
          {
            unsigned upem = hb_font_get_face (ctx->font)->get_upem ();
            viewport_w = (float) upem;
            viewport_h = (float) upem;
          }
          else
          {
            viewport_w = vb_w;
            viewport_h = vb_h;
          }
        }
        has_viewbox_transform =
          hb_raster_svg_compute_viewbox_transform (viewport_w, viewport_h,
                                                   vb_x, vb_y, vb_w, vb_h,
                                                   parser.find_attr ("preserveAspectRatio"),
                                                   &viewbox_t);
      }
    }
  }

  if (ctx->suppress_viewbox_once)
  {
    has_viewbox = false;
    has_viewbox_transform = false;
    ctx->suppress_viewbox_once = false;
  }

  if (has_opacity)
    pushed_opacity_group = ctx->push_group ();

  if (has_transform)
  {
    hb_svg_transform_t t;
    hb_raster_svg_parse_transform (transform_str, &t);
    pushed_transform = ctx->push_transform (t.xx, t.yx, t.xy, t.yy, t.dx, t.dy);
  }

  if (has_svg_translate)
    pushed_svg_translate = ctx->push_transform (1, 0, 0, 1, svg_x, svg_y);

  if (has_viewbox_transform)
    pushed_viewbox_transform = ctx->push_transform (viewbox_t.xx, viewbox_t.yx, viewbox_t.xy, viewbox_t.yy,
                                                    viewbox_t.dx, viewbox_t.dy);
  else if (has_viewbox && vb_w > 0 && vb_h > 0)
    pushed_viewbox_transform = ctx->push_transform (1, 0, 0, 1, -vb_x, -vb_y);

  has_clip = hb_raster_svg_push_clip_path_ref (ctx->paint, &ctx->defs, clip_path_str, nullptr);

  if (!self_closing)
  {
    int depth = 1;
    while (depth > 0)
    {
      hb_svg_token_type_t tok = parser.next ();
      if (tok == SVG_TOKEN_EOF) break;

      if (tok == SVG_TOKEN_CLOSE_TAG)
      {
	depth--;
	continue;
      }

      if (tok == SVG_TOKEN_OPEN_TAG || tok == SVG_TOKEN_SELF_CLOSE_TAG)
      {
	hb_svg_str_t child_tag = parser.tag_name;
	if (parser.tag_name.eq ("defs"))
	{
	  if (tok != SVG_TOKEN_SELF_CLOSE_TAG)
	  {
	    hb_svg_defs_scan_context_t scan_ctx = {
	      &ctx->defs, ctx->pfuncs, ctx->paint,
	      ctx->foreground, hb_font_get_face (ctx->font),
	      ctx->palette,
	      ctx->doc_start, ctx->doc_len,
	      ctx->svg_accel, ctx->doc_cache
	    };
	    hb_raster_svg_process_defs_element (&scan_ctx, parser);
	  }
	  continue;
	}
	svg_render_element (ctx, parser, state);
	if (tok == SVG_TOKEN_OPEN_TAG &&
	    !hb_raster_svg_tag_is_container_or_use (child_tag))
	{
	  /* Skip children of non-container elements we don't handle. */
	  int skip_depth = 1;
	  while (skip_depth > 0)
	  {
	    hb_svg_token_type_t st = parser.next ();
	    if (st == SVG_TOKEN_EOF) break;
	    if (st == SVG_TOKEN_CLOSE_TAG) skip_depth--;
	    else if (st == SVG_TOKEN_OPEN_TAG) skip_depth++;
	  }
	}
      }
    }
  }

  if (pushed_viewbox_transform)
    ctx->pop_transform ();

  if (pushed_svg_translate)
    ctx->pop_transform ();

  if (has_clip)
    ctx->pop_clip ();

  if (pushed_transform)
    ctx->pop_transform ();

  if (pushed_opacity_group)
    ctx->pop_group (HB_PAINT_COMPOSITE_MODE_SRC_OVER);
}

static bool
svg_render_primitive_shape_element (hb_svg_render_context_t *ctx,
				    hb_svg_xml_parser_t &parser,
				    const hb_svg_cascade_t &state,
				    hb_svg_str_t transform_str)
{
  hb_svg_str_t tag = parser.tag_name;
  if (!(tag.eq ("path") || tag.eq ("rect") || tag.eq ("circle") ||
        tag.eq ("ellipse") || tag.eq ("line") || tag.eq ("polyline") ||
        tag.eq ("polygon")))
    return false;

  hb_svg_shape_emit_data_t shape;
  if (hb_raster_svg_parse_shape_tag (parser, &shape))
    svg_render_shape (ctx, shape, state, transform_str);
  return true;
}

static void
svg_render_use_callback (void *render_user,
			 hb_svg_xml_parser_t &parser,
			 const void *state,
			 bool viewport_mapped)
{
  hb_svg_render_context_t *ctx = (hb_svg_render_context_t *) render_user;
  bool old_suppress = ctx->suppress_viewbox_once;
  bool old_allow_symbol = ctx->allow_symbol_render_once;
  if (viewport_mapped)
    ctx->suppress_viewbox_once = true;
  if (parser.tag_name.eq ("symbol"))
    ctx->allow_symbol_render_once = true;
  svg_render_element (ctx, parser, *(const hb_svg_cascade_t *) state);
  ctx->suppress_viewbox_once = old_suppress;
  ctx->allow_symbol_render_once = old_allow_symbol;
}

/* Render one element (may be a container or shape) */
static void
svg_render_element (hb_svg_render_context_t *ctx,
		    hb_svg_xml_parser_t &parser,
		    const hb_svg_cascade_t &inherited)
{
  if (ctx->depth >= SVG_MAX_DEPTH) return;

  const HB_UNUSED unsigned transform_depth = ctx->paint->transform_stack.length;
  const HB_UNUSED unsigned clip_depth = ctx->paint->clip_stack.length;
  const HB_UNUSED unsigned surface_depth = ctx->paint->surface_stack.length;

  ctx->depth++;

  hb_svg_str_t tag = parser.tag_name;
  bool self_closing = parser.self_closing;

  /* Extract common attributes */
  hb_svg_str_t style = parser.find_attr ("style");
  hb_svg_style_props_t style_props;
  svg_parse_style_props (style, &style_props);
  hb_svg_str_t fill_attr = svg_pick_attr_or_style (parser, style_props.fill, "fill");
  hb_svg_str_t fill_opacity_str = svg_pick_attr_or_style (parser, style_props.fill_opacity, "fill-opacity");
  hb_svg_str_t opacity_str = svg_pick_attr_or_style (parser, style_props.opacity, "opacity");
  hb_svg_str_t transform_str = svg_pick_attr_or_style (parser, style_props.transform, "transform");
  hb_svg_str_t clip_path_attr = svg_pick_attr_or_style (parser, style_props.clip_path, "clip-path");
  hb_svg_str_t display_str = svg_pick_attr_or_style (parser, style_props.display, "display");
  hb_svg_str_t color_str = svg_pick_attr_or_style (parser, style_props.color, "color");
  hb_svg_str_t visibility_str = svg_pick_attr_or_style (parser, style_props.visibility, "visibility");

  hb_svg_cascade_t state = inherited;
  state.fill = (fill_attr.is_null () || svg_str_is_inherit (fill_attr)) ? inherited.fill : fill_attr;
  state.fill_opacity = (fill_opacity_str.len && !svg_str_is_inherit (fill_opacity_str))
		       ? svg_parse_float_clamped01 (fill_opacity_str)
		       : inherited.fill_opacity;
  state.opacity = (opacity_str.len && !svg_str_is_inherit (opacity_str) && !svg_str_is_none (opacity_str))
		  ? svg_parse_float_clamped01 (opacity_str)
		  : (svg_str_is_inherit (opacity_str) ? inherited.opacity : 1.f);
  state.clip_path = (clip_path_attr.is_null () || svg_str_is_inherit (clip_path_attr))
		    ? inherited.clip_path
		    : clip_path_attr;
  if (svg_str_is_inherit (transform_str) || svg_str_is_none (transform_str))
    transform_str = {};
  state.color = inherited.color;
  bool is_none = false;
  if (color_str.len && !color_str.trim ().eq_ascii_ci ("inherit"))
    state.color = hb_raster_svg_parse_color (color_str, ctx->pfuncs, ctx->paint,
				   inherited.color, hb_font_get_face (ctx->font),
				   ctx->palette, &is_none);
  state.visibility = inherited.visibility;
  hb_svg_str_t visibility_trim = visibility_str.trim ();
  if (visibility_trim.len && !visibility_trim.eq_ascii_ci ("inherit"))
    state.visibility = !(visibility_trim.eq_ascii_ci ("hidden") ||
			 visibility_trim.eq_ascii_ci ("collapse"));

  if (display_str.trim ().eq_ascii_ci ("none"))
  {
    if (!self_closing)
      svg_skip_subtree (parser);
    ctx->depth--;
    assert (ctx->paint->transform_stack.length == transform_depth);
    assert (ctx->paint->clip_stack.length == clip_depth);
    assert (ctx->paint->surface_stack.length == surface_depth);
    return;
  }
  if (!state.visibility)
  {
    if (!self_closing)
      svg_skip_subtree (parser);
    ctx->depth--;
    assert (ctx->paint->transform_stack.length == transform_depth);
    assert (ctx->paint->clip_stack.length == clip_depth);
    assert (ctx->paint->surface_stack.length == surface_depth);
    return;
  }

  if (tag.eq ("symbol") && !ctx->allow_symbol_render_once)
  {
    if (!self_closing)
      svg_skip_subtree (parser);
    ctx->depth--;
    assert (ctx->paint->transform_stack.length == transform_depth);
    assert (ctx->paint->clip_stack.length == clip_depth);
    assert (ctx->paint->surface_stack.length == surface_depth);
    return;
  }
  if (tag.eq ("symbol"))
    ctx->allow_symbol_render_once = false;

  if (hb_raster_svg_tag_is_container (tag))
    svg_render_container_element (ctx, parser, tag, self_closing,
				  state, transform_str, state.clip_path);
  else if (svg_render_primitive_shape_element (ctx, parser, state, transform_str))
    ;
  else if (tag.eq ("use"))
  {
    hb_svg_use_context_t use_ctx = {ctx->paint, ctx->pfuncs,
				    ctx->doc_start, ctx->doc_len,
				    ctx->svg_accel, ctx->doc_cache,
				    &ctx->use_decycler};
    hb_raster_svg_render_use_element (&use_ctx, parser, &state, transform_str,
			    svg_render_use_callback, ctx);
  }

  ctx->depth--;

  assert (ctx->paint->transform_stack.length == transform_depth);
  assert (ctx->paint->clip_stack.length == clip_depth);
  assert (ctx->paint->surface_stack.length == surface_depth);
}


/*
 * 12. Entry point
 */

hb_bool_t
hb_raster_svg_render (hb_raster_paint_t *paint,
		      hb_blob_t *blob,
		      hb_codepoint_t glyph,
		      hb_font_t *font,
		      unsigned palette,
		      hb_color_t foreground)
{
  bool ret = false;
  hb_blob_t *render_blob = nullptr;
  hb_face_t *face HB_UNUSED = hb_font_get_face (font);
  const OT::SVG::svg_doc_cache_t *doc_cache = nullptr;
#ifndef HB_NO_SVG
  unsigned doc_index = 0;
  hb_codepoint_t start_glyph = HB_CODEPOINT_INVALID;
  hb_codepoint_t end_glyph = HB_CODEPOINT_INVALID;
#endif
  hb_paint_funcs_t *pfuncs = hb_raster_paint_get_funcs ();
  hb_svg_render_context_t ctx;
  hb_svg_cascade_t initial_state;
  hb_svg_defs_scan_context_t scan_ctx;
  bool found_glyph = false;
#ifndef HB_NO_SVG
  unsigned glyph_start = 0, glyph_end = 0;
#endif

  unsigned data_len = 0;
  const char *data = nullptr;
  render_blob = OT::hb_ot_svg_reference_normalized_blob (blob, &data, &data_len);
  if (!render_blob || !data_len)
    goto done;

#ifndef HB_NO_SVG
  if (face &&
      hb_ot_color_glyph_get_svg_document_index (face, glyph, &doc_index) &&
      hb_ot_color_get_svg_document_glyph_range (face, doc_index, &start_glyph, &end_glyph))
    doc_cache = face->table.SVG->get_or_create_doc_cache (render_blob, data, data_len,
                                                          doc_index, start_glyph, end_glyph);

  if (doc_cache)
    data = face->table.SVG->doc_cache_get_svg (doc_cache, &data_len);
#endif

  ctx.paint = paint;
  ctx.pfuncs = pfuncs;
  ctx.font = font;
  ctx.palette = palette;
  ctx.foreground = foreground;
  ctx.doc_start = data;
  ctx.doc_len = data_len;
#ifndef HB_NO_SVG
  ctx.svg_accel = face ? face->table.SVG.get () : nullptr;
#endif
  ctx.doc_cache = doc_cache;

  initial_state.color = foreground;

  scan_ctx = {
    &ctx.defs, ctx.pfuncs, ctx.paint,
    ctx.foreground, hb_font_get_face (ctx.font),
    ctx.palette,
    ctx.doc_start, ctx.doc_len,
    ctx.svg_accel, ctx.doc_cache
  };
  hb_raster_svg_collect_defs (&scan_ctx, data, data_len);

#ifndef HB_NO_SVG
  if (doc_cache && face->table.SVG->doc_cache_get_glyph_span (doc_cache, glyph, &glyph_start, &glyph_end))
  {
    hb_svg_xml_parser_t parser (data + glyph_start, glyph_end - glyph_start);
    hb_svg_token_type_t tok = parser.next ();
    if (tok == SVG_TOKEN_OPEN_TAG || tok == SVG_TOKEN_SELF_CLOSE_TAG)
    {
      bool pushed_font_transform = hb_raster_svg_push_font_transform (ctx.pfuncs, ctx.paint, font);
      bool pushed_flip_transform = ctx.push_transform (1, 0, 0, -1, 0, 0);
	      svg_render_element (&ctx, parser, initial_state);
      if (pushed_flip_transform)
        ctx.pop_transform ();
      if (pushed_font_transform)
        hb_paint_pop_transform (ctx.pfuncs, ctx.paint);
      found_glyph = true;
    }
  }
#endif
  if (!found_glyph)
  {
    /* Fallback for malformed/uncached docs: linear scan by glyph id. */
    char glyph_id_str[32];
    int glyph_id_len = snprintf (glyph_id_str, sizeof (glyph_id_str), "glyph%u", glyph);
    if (glyph_id_len <= 0 || (unsigned) glyph_id_len >= sizeof (glyph_id_str))
      return false;
    hb_svg_xml_parser_t parser (data, data_len);
    while (true)
    {
      hb_svg_token_type_t tok = parser.next ();
      if (tok == SVG_TOKEN_EOF) break;

      if (tok == SVG_TOKEN_OPEN_TAG || tok == SVG_TOKEN_SELF_CLOSE_TAG)
      {
        hb_svg_str_t id = parser.find_attr ("id");
        if (id.len)
        {
          if (id.len == (unsigned) glyph_id_len &&
              0 == hb_memcmp (id.data, glyph_id_str, (unsigned) glyph_id_len))
          {
            bool pushed_font_transform = hb_raster_svg_push_font_transform (ctx.pfuncs, ctx.paint, font);
            bool pushed_flip_transform = ctx.push_transform (1, 0, 0, -1, 0, 0);
	            svg_render_element (&ctx, parser, initial_state);
            if (pushed_flip_transform)
              ctx.pop_transform ();
            if (pushed_font_transform)
              hb_paint_pop_transform (ctx.pfuncs, ctx.paint);
            found_glyph = true;
            break;
          }
        }
      }
    }
  }

  ret = found_glyph;

done:
  hb_blob_destroy (render_blob);
  return ret;
}

#endif /* !HB_NO_RASTER_SVG */
