/*
 * 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
 */

#ifndef HB_NO_RASTER_SVG

#include "hb.hh"

#include "hb-raster-svg-clip.hh"

#include "hb-raster.h"
#include "hb-raster-paint.hh"
#include "hb-raster-svg.hh"
#include "hb-raster-svg-base.hh"
#include "hb-decycler.hh"

#include <math.h>

static inline bool
svg_transform_is_identity (const hb_svg_transform_t &t)
{
  return t.xx == 1.f && t.yx == 0.f &&
         t.xy == 0.f && t.yy == 1.f &&
         t.dx == 0.f && t.dy == 0.f;
}

static bool
svg_parse_element_transform (hb_svg_xml_parser_t &parser,
                             hb_svg_transform_t *out)
{
  hb_svg_style_props_t style_props;
  svg_parse_style_props (parser.find_attr ("style"), &style_props);
  hb_svg_str_t transform = svg_pick_attr_or_style (parser, style_props.transform, "transform");
  if (!transform.len)
    return false;
  hb_raster_svg_parse_transform (transform, out);
  return true;
}

struct hb_svg_clip_collect_context_t
{
  hb_svg_defs_t *defs;
  hb_svg_clip_path_def_t *clip;
  const char *doc_start;
  unsigned doc_len;
  const OT::SVG::accelerator_t *svg_accel;
  const OT::SVG::svg_doc_cache_t *doc_cache;
  hb_decycler_t *use_decycler;
  bool *had_alloc_failure;
};

static inline void
svg_clip_append_shape (hb_svg_clip_collect_context_t *ctx,
                       const hb_svg_shape_emit_data_t &shape,
                       const hb_svg_transform_t &transform)
{
  hb_svg_clip_shape_t clip_shape;
  clip_shape.shape = shape;
  if (!svg_transform_is_identity (transform))
  {
    clip_shape.has_transform = true;
    clip_shape.transform = transform;
  }
  if (likely (ctx->defs->clip_shapes.push_or_fail (clip_shape)))
    ctx->clip->shape_count++;
  else if (ctx->had_alloc_failure)
    *ctx->had_alloc_failure = true;
}

static inline bool
svg_resolve_element_visibility (hb_svg_xml_parser_t &parser,
                                bool parent_visible)
{
  hb_svg_style_props_t style_props;
  svg_parse_style_props (parser.find_attr ("style"), &style_props);
  hb_svg_str_t display_str = svg_pick_attr_or_style (parser, style_props.display, "display");
  hb_svg_str_t visibility_str = svg_pick_attr_or_style (parser, style_props.visibility, "visibility");
  if (display_str.trim ().eq_ascii_ci ("none"))
    return false;
  hb_svg_str_t vis_trim = visibility_str.trim ();
  if (!vis_trim.len || vis_trim.eq_ascii_ci ("inherit"))
    return parent_visible;
  if (vis_trim.eq_ascii_ci ("hidden") ||
      vis_trim.eq_ascii_ci ("collapse"))
    return false;
  if (vis_trim.eq_ascii_ci ("visible"))
    return true;
  return parent_visible;
}

static void
svg_clip_collect_ref_element (hb_svg_clip_collect_context_t *ctx,
                              hb_svg_xml_parser_t &parser,
                              const hb_svg_transform_t &base_transform,
                              unsigned depth,
                              bool suppress_viewbox_once = false,
                              bool parent_visible = true,
                              bool allow_symbol_once = false);

static void
svg_clip_collect_use_target (hb_svg_clip_collect_context_t *ctx,
                             hb_svg_xml_parser_t &use_parser,
                             const hb_svg_transform_t &base_transform,
                             unsigned depth)
{
  const unsigned SVG_MAX_CLIP_USE_DEPTH = 64;
  if (depth >= SVG_MAX_CLIP_USE_DEPTH)
    return;

  hb_svg_str_t href = hb_raster_svg_find_href_attr (use_parser);
  hb_svg_str_t ref_id;
  if (!hb_raster_svg_parse_local_id_ref (href, &ref_id, nullptr))
    return;

  const char *found = nullptr;
  if (!hb_raster_svg_find_element_by_id (ctx->doc_start, ctx->doc_len,
                                         ctx->svg_accel, ctx->doc_cache,
                                         ref_id, &found))
    return;

  hb_decycler_node_t node (*ctx->use_decycler);
  if (unlikely (!node.visit ((uintptr_t) found)))
    return;

  hb_svg_transform_t effective = base_transform;
  float use_x = 0.f, use_y = 0.f, use_w = 0.f, use_h = 0.f;
  hb_raster_svg_parse_use_geometry (use_parser, &use_x, &use_y, &use_w, &use_h);
  if (use_x != 0.f || use_y != 0.f)
  {
    hb_svg_transform_t tr;
    tr.dx = use_x;
    tr.dy = use_y;
    effective.multiply (tr);
  }

  unsigned remaining = ctx->doc_len - (unsigned) (found - ctx->doc_start);
  hb_svg_xml_parser_t ref_parser (found, remaining);
  hb_svg_token_type_t rt = ref_parser.next ();
  if (rt != SVG_TOKEN_OPEN_TAG && rt != SVG_TOKEN_SELF_CLOSE_TAG)
    return;

  bool viewport_mapped = false;
  hb_svg_transform_t vb_t;
  if (hb_raster_svg_compute_use_target_viewbox_transform (ref_parser, use_w, use_h, &vb_t))
  {
    effective.multiply (vb_t);
    viewport_mapped = true;
  }

  bool allow_symbol = ref_parser.tag_name.eq ("symbol");
  svg_clip_collect_ref_element (ctx, ref_parser, effective, depth + 1,
                                viewport_mapped, true, allow_symbol);
}

static void
svg_clip_collect_ref_element (hb_svg_clip_collect_context_t *ctx,
                              hb_svg_xml_parser_t &parser,
                              const hb_svg_transform_t &base_transform,
                              unsigned depth,
                              bool suppress_viewbox_once,
                              bool parent_visible,
                              bool allow_symbol_once)
{
  const unsigned SVG_MAX_CLIP_REF_DEPTH = 64;
  if (depth >= SVG_MAX_CLIP_REF_DEPTH)
  {
    if (!parser.self_closing)
      svg_skip_subtree (parser);
    return;
  }

  bool is_visible = svg_resolve_element_visibility (parser, parent_visible);
  if (!is_visible)
  {
    if (!parser.self_closing)
      svg_skip_subtree (parser);
    return;
  }

  /* Definitions are not directly renderable clip geometry. */
  if (parser.tag_name.eq ("defs"))
  {
    if (!parser.self_closing)
      svg_skip_subtree (parser);
    return;
  }
  if (parser.tag_name.eq ("symbol") && !allow_symbol_once)
  {
    if (!parser.self_closing)
      svg_skip_subtree (parser);
    return;
  }
  if (parser.tag_name.eq ("symbol"))
    allow_symbol_once = false;

  hb_svg_transform_t effective = base_transform;
  hb_svg_style_props_t geom_style_props;
  svg_parse_style_props (parser.find_attr ("style"), &geom_style_props);
  hb_svg_transform_t local_t;
  if (svg_parse_element_transform (parser, &local_t))
    effective.multiply (local_t);
  if (parser.tag_name.eq ("svg"))
  {
    float svg_x = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (parser, geom_style_props.x, "x"));
    float svg_y = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (parser, geom_style_props.y, "y"));
    if (svg_x != 0.f || svg_y != 0.f)
    {
      hb_svg_transform_t tr;
      tr.dx = svg_x;
      tr.dy = svg_y;
      effective.multiply (tr);
    }

    if (!suppress_viewbox_once)
    {
      float vb_x = 0.f, vb_y = 0.f, vb_w = 0.f, vb_h = 0.f;
    if (hb_raster_svg_parse_viewbox (parser.find_attr ("viewBox"),
                                     &vb_x, &vb_y, &vb_w, &vb_h))
    {
      float viewport_w = hb_raster_svg_parse_non_percent_length (svg_pick_attr_or_style (parser, geom_style_props.width, "width"));
      float 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))
      {
        viewport_w = vb_w;
          viewport_h = vb_h;
        }
        hb_svg_transform_t vb_t;
        if (hb_raster_svg_compute_viewbox_transform (viewport_w, viewport_h,
                                                     vb_x, vb_y, vb_w, vb_h,
                                                     parser.find_attr ("preserveAspectRatio"),
                                                     &vb_t))
          effective.multiply (vb_t);
      }
      suppress_viewbox_once = false;
    }
  }

  hb_svg_shape_emit_data_t shape;
  if (hb_raster_svg_parse_shape_tag (parser, &shape))
  {
    svg_clip_append_shape (ctx, shape, effective);
    if (!parser.self_closing)
      svg_skip_subtree (parser);
    return;
  }

  if (parser.tag_name.eq ("use"))
  {
    svg_clip_collect_use_target (ctx, parser, effective, depth + 1);
    if (!parser.self_closing)
      svg_skip_subtree (parser);
    return;
  }

  bool is_container = hb_raster_svg_tag_is_container (parser.tag_name);
  if (!is_container || parser.self_closing)
  {
    if (!parser.self_closing)
      svg_skip_subtree (parser);
    return;
  }

  int inner_depth = 1;
  while (inner_depth > 0)
  {
    hb_svg_token_type_t tok = parser.next ();
    if (tok == SVG_TOKEN_EOF) break;
    if (tok == SVG_TOKEN_CLOSE_TAG)
    {
      inner_depth--;
      continue;
    }
    if (tok == SVG_TOKEN_OPEN_TAG || tok == SVG_TOKEN_SELF_CLOSE_TAG)
      svg_clip_collect_ref_element (ctx, parser, effective, depth + 1, false, is_visible, false);
  }
}

void
hb_raster_svg_process_clip_path_def (hb_svg_defs_t *defs,
                           hb_svg_xml_parser_t &parser,
                           hb_svg_token_type_t tok,
                           const char *doc_start,
                           unsigned doc_len,
                           const OT::SVG::accelerator_t *svg_accel,
                           const OT::SVG::svg_doc_cache_t *doc_cache)
{
  hb_svg_clip_path_def_t clip;
  hb_svg_str_t id = parser.find_attr ("id");
  hb_svg_str_t units = parser.find_attr ("clipPathUnits").trim ();
  if (units.eq_ascii_ci ("objectBoundingBox"))
    clip.units_user_space = false;
  else if (units.eq_ascii_ci ("userSpaceOnUse"))
    clip.units_user_space = true;
  else
    clip.units_user_space = true;

  hb_svg_transform_t cp_t;
  if (svg_parse_element_transform (parser, &cp_t))
  {
    clip.has_clip_transform = true;
    clip.clip_transform = cp_t;
  }

  clip.first_shape = defs->clip_shapes.length;
  clip.shape_count = 0;

  if (tok == SVG_TOKEN_OPEN_TAG)
  {
    const unsigned SVG_MAX_CLIP_DEPTH = 64;
    hb_svg_transform_t inherited[SVG_MAX_CLIP_DEPTH];
    bool inherited_visibility[SVG_MAX_CLIP_DEPTH];
    inherited[0] = hb_svg_transform_t ();
    inherited[1] = hb_svg_transform_t ();
    inherited_visibility[0] = true;
    inherited_visibility[1] = true;
    hb_decycler_t use_decycler;

    int cdepth = 1;
    bool had_alloc_failure = false;
    hb_svg_clip_collect_context_t collect_ctx = {
      defs, &clip,
      doc_start, doc_len, svg_accel, doc_cache,
      &use_decycler, &had_alloc_failure
    };
    while (cdepth > 0)
    {
      hb_svg_token_type_t ct = parser.next ();
      if (ct == SVG_TOKEN_EOF) break;
      if (ct == SVG_TOKEN_CLOSE_TAG) { cdepth--; continue; }
      if (ct == SVG_TOKEN_OPEN_TAG || ct == SVG_TOKEN_SELF_CLOSE_TAG)
      {
        if (parser.tag_name.eq ("symbol"))
        {
          if (ct == SVG_TOKEN_OPEN_TAG)
          {
            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++;
            }
          }
          continue;
        }

        if (parser.tag_name.eq ("defs"))
        {
          if (ct == SVG_TOKEN_OPEN_TAG)
          {
            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++;
            }
          }
          continue;
        }

        bool parent_visible = (unsigned) cdepth < SVG_MAX_CLIP_DEPTH
                            ? inherited_visibility[cdepth]
                            : true;
        bool is_visible = svg_resolve_element_visibility (parser, parent_visible);
        bool is_hidden = !is_visible;
        if (is_hidden)
        {
          if (ct == SVG_TOKEN_OPEN_TAG)
          {
            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++;
            }
          }
          continue;
        }

        hb_svg_transform_t effective = (unsigned) cdepth < SVG_MAX_CLIP_DEPTH
                                     ? inherited[cdepth]
                                     : hb_svg_transform_t ();

        hb_svg_transform_t local;
        bool has_local_transform = svg_parse_element_transform (parser, &local);
        if (has_local_transform)
        {
          effective.multiply (local);
        }

        if (parser.tag_name.eq ("use"))
        {
          svg_clip_collect_use_target (&collect_ctx, parser, effective, 0);
        }
        else
        {
          hb_svg_shape_emit_data_t shape;
          if (hb_raster_svg_parse_shape_tag (parser, &shape))
            svg_clip_append_shape (&collect_ctx, shape, effective);
        }

        if (ct == SVG_TOKEN_OPEN_TAG)
        {
          if ((unsigned) (cdepth + 1) < SVG_MAX_CLIP_DEPTH)
          {
            inherited[cdepth + 1] = effective;
            inherited_visibility[cdepth + 1] = is_visible;
          }
          cdepth++;
        }
      }
    }
    if (had_alloc_failure)
      id = {};
  }

  if (id.len)
    (void) defs->add_clip_path (hb_bytes_t (id.data, id.len), clip);
}

struct hb_svg_clip_emit_data_t
{
  const hb_svg_defs_t *defs;
  const hb_svg_clip_path_def_t *clip;
  hb_transform_t<> base_transform;
  hb_transform_t<> bbox_transform;
  bool has_bbox_transform = false;
};

static inline hb_transform_t<>
svg_to_hb_transform (const hb_svg_transform_t &t)
{
  return hb_transform_t<> (t.xx, t.yx, t.xy, t.yy, t.dx, t.dy);
}

static void
svg_clip_path_emit (hb_draw_funcs_t *dfuncs,
                    void *draw_data,
                    void *user_data)
{
  hb_raster_draw_t *rdr = (hb_raster_draw_t *) draw_data;
  hb_svg_clip_emit_data_t *ed = (hb_svg_clip_emit_data_t *) user_data;
  const hb_svg_clip_path_def_t *clip = ed->clip;

  for (unsigned i = 0; i < clip->shape_count; i++)
  {
    const hb_svg_clip_shape_t &s = ed->defs->clip_shapes[clip->first_shape + i];
    hb_transform_t<> t = ed->base_transform;
    if (ed->has_bbox_transform)
      t.multiply (ed->bbox_transform);
    if (clip->has_clip_transform)
      t.multiply (svg_to_hb_transform (clip->clip_transform));
    if (s.has_transform)
      t.multiply (svg_to_hb_transform (s.transform));

    hb_raster_draw_set_transform (rdr, t.xx, t.yx, t.xy, t.yy, t.x0, t.y0);

    hb_svg_shape_emit_data_t shape = s.shape;
    hb_raster_svg_shape_path_emit (dfuncs, draw_data, &shape);
  }
}

bool
hb_raster_svg_push_clip_path_ref (hb_raster_paint_t *paint,
                        hb_svg_defs_t *defs,
                        hb_svg_str_t clip_path_str,
                        const hb_extents_t<> *object_bbox)
{
  if (clip_path_str.is_null ()) return false;
  hb_svg_str_t trimmed = clip_path_str.trim ();
  if (!trimmed.len || trimmed.eq_ascii_ci ("none")) return false;

  hb_svg_str_t clip_id;
  if (!hb_raster_svg_parse_local_id_ref (trimmed, &clip_id, nullptr))
    return false;

  const hb_svg_clip_path_def_t *clip = defs->find_clip_path (hb_bytes_t (clip_id.data, clip_id.len));
  if (!clip) return false;

  hb_svg_clip_emit_data_t ed;
  ed.defs = defs;
  ed.clip = clip;
  ed.base_transform = paint->current_effective_transform ();

  if (!clip->units_user_space)
  {
    if (!object_bbox || object_bbox->is_empty ())
      return false;
    float w = object_bbox->xmax - object_bbox->xmin;
    float h = object_bbox->ymax - object_bbox->ymin;
    if (!(std::isfinite (w) && std::isfinite (h)) || w <= 0.f || h <= 0.f)
      return false;
    ed.has_bbox_transform = true;
    ed.bbox_transform = hb_transform_t<> (w, 0, 0, h, object_bbox->xmin, object_bbox->ymin);
  }

  hb_raster_paint_push_clip_path (paint, svg_clip_path_emit, &ed);
  return true;
}

#endif /* !HB_NO_RASTER_SVG */
