/* GStreamer object detection overlay
 * Copyright (C) <2023> Collabora Ltd.
 *  @author: Aaron Boxer <aaron.boxer@collabora.com>
 *  @author: Daniel Morin <daniel.morin@collabora.com>
 *
 * gstobjectdetectionoverlay.c
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

/**
 * SECTION:element-objectdetectionoverlay
 * @title: objectdetectionoverlay
 * @see_also: #GstObjectDetectionOverlay
 *
 * This element create a graphical representation of the analytics object
 * detection metadata attached to video stream and overlay graphics above the
 * video.
 *
 * The object detection overlay element monitor video stream for
 * @GstAnalyticsRelationMeta and query @GstAnalyticsODMtd. Retrieved
 * @GstAnalyticsODMtd are then used to generate an overlay highlighing objects
 * detected.
 *
 * ## Example launch line
 * |[
 * gst-launch-1.0 multifilesrc location=/onnx-models/images/bus.jpg ! jpegdec ! videoconvert ! onnxinference execution-provider=cpu model-file=/onnx-models/models/ssd_mobilenet_v1_coco.onnx ! ssdobjectdetector label-file=/onnx-models/labels/COCO_classes.txt ! objectdetectionoverlay object-detection-outline-color=0xFF0000FF draw-labels=true ! videoconvertscale ! imagefreeze ! autovideosink
 * ]| This pipeline create an overlay representing results of an object detetion
 * analysis.
 *
 * Since: 1.24
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <gst/gst.h>
#include <gst/video/video.h>
#include <gst/analytics/analytics.h>
#include <pango/pangocairo.h>
#include <math.h>

#include "gstobjectdetectionoverlay.h"

struct _GstObjectDetectionOverlay
{
  GstVideoFilter parent;

  cairo_matrix_t cairo_matrix;
  gsize render_len;

  /* stream metrics */
  GstVideoInfo *in_info;
  GMutex stream_event_mutex;
  gboolean flushing;
  gboolean eos;

  /* properties */
  guint od_outline_color;
  guint od_outline_stroke_width;
  gboolean draw_labels;
  gboolean draw_tracking_labels;
  gboolean filled_box;
  guint labels_color;
  gdouble labels_stroke_width;
  gdouble labels_outline_ofs;
  GstClockTime expire_overlay;
  gboolean tracking_outline_colors;

  /* composition */
  gboolean attach_compo_to_buffer;
  GstBuffer *canvas;
  gint canvas_length;
  GstVideoOverlayComposition *composition;
  GstClockTime last_composition_update;

  /* Graphic Outline */
  PangoContext *pango_context;
  PangoLayout *pango_layout;

};


#define MINIMUM_TEXT_OUTLINE_OFFSET 1.0
#define ROTATION_EPSILON 0.001f /* Threshold to consider there's a rotation */

GST_DEBUG_CATEGORY_STATIC (objectdetectionoverlay_debug);
#define GST_CAT_DEFAULT objectdetectionoverlay_debug

enum
{
  PROP_OD_OUTLINE_COLOR = 1,
  PROP_DRAW_LABELS,
  PROP_DRAW_TRACKING_LABELS,
  PROP_LABELS_COLOR,
  PROP_FILLED_BOX,
  PROP_EXPIRE_OVERLAY,
  PROP_TRACKING_OUTLINE_COLORS,
  _PROP_COUNT
};

typedef struct _GstObjectDetectionOverlayPangoCairoContext
    GstObjectDetectionOverlayPangoCairoContext;

struct _GstObjectDetectionOverlayPangoCairoContext
{
  cairo_t *cr;
  cairo_surface_t *surface;
  guint8 *data;
  cairo_matrix_t *cairo_matrix;
};

#define VIDEO_FORMATS GST_VIDEO_OVERLAY_COMPOSITION_BLEND_FORMATS
#define OBJECT_DETECTION_OVERLAY_CAPS GST_VIDEO_CAPS_MAKE (VIDEO_FORMATS)

static GstStaticCaps sw_template_caps =
GST_STATIC_CAPS (OBJECT_DETECTION_OVERLAY_CAPS);

static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS (OBJECT_DETECTION_OVERLAY_CAPS)
    );

static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS (OBJECT_DETECTION_OVERLAY_CAPS)
    );

G_DEFINE_TYPE (GstObjectDetectionOverlay,
    gst_object_detection_overlay, GST_TYPE_VIDEO_FILTER);

#define parent_class gst_object_detection_overlay_parent_class

GST_ELEMENT_REGISTER_DEFINE (objectdetectionoverlay, "objectdetectionoverlay",
    GST_RANK_NONE, GST_TYPE_OBJECT_DETECTION_OVERLAY);

static void gst_object_detection_overlay_set_property (GObject * object,
    guint prop_id, const GValue * value, GParamSpec * pspec);

static void gst_object_detection_overlay_get_property (GObject * object,
    guint prop_id, GValue * value, GParamSpec * pspec);

static gboolean gst_object_detection_overlay_sink_event (GstBaseTransform *
    trans, GstEvent * event);

static gboolean gst_object_detection_overlay_start (GstBaseTransform * trans);
static gboolean gst_object_detection_overlay_stop (GstBaseTransform * trans);


static gboolean gst_object_detection_overlay_set_info (GstVideoFilter * filter,
    GstCaps * incaps, GstVideoInfo * in_info, GstCaps * outcaps,
    GstVideoInfo * out_info);

static GstFlowReturn
gst_object_detection_overlay_transform_frame_ip (GstVideoFilter * filter,
    GstVideoFrame * buf);

static void gst_object_detection_overlay_finalize (GObject * object);

static void
gst_object_detection_overlay_render_boundingbox (GstObjectDetectionOverlay
    * overlay, GstObjectDetectionOverlayPangoCairoContext * cairo_ctx,
    GstAnalyticsRelationMeta * rmeta, GstAnalyticsODMtd * od_mtd);

static void
gst_object_detection_overlay_render_text_annotation (GstObjectDetectionOverlay
    * overlay, GstObjectDetectionOverlayPangoCairoContext * cairo_ctx,
    GstAnalyticsODMtd * od_mtd, const gchar * annotation);

static void
    gst_object_detection_overlay_render_tracking_text_annotation
    (GstObjectDetectionOverlay * overlay,
    GstObjectDetectionOverlayPangoCairoContext * ctx,
    GstAnalyticsRelationMeta * rmeta, const GstAnalyticsODMtd * od_mtd);


static void
gst_object_detection_overlay_class_init (GstObjectDetectionOverlayClass * klass)
{
  GObjectClass *gobject_class;
  GstElementClass *element_class;
  GstBaseTransformClass *basetransform_class;
  GstVideoFilterClass *videofilter_class;

  gobject_class = (GObjectClass *) klass;
  gobject_class->set_property = gst_object_detection_overlay_set_property;
  gobject_class->get_property = gst_object_detection_overlay_get_property;
  gobject_class->finalize = gst_object_detection_overlay_finalize;

  /**
   * GstObjectDetectionOverlay:object-detection-outline-color
   *
   * Object Detetion Overlay outline color
   * ARGB format (ex. 0xFFFF0000 for red)
   *
   * Since: 1.24
   */
  g_object_class_install_property (gobject_class, PROP_OD_OUTLINE_COLOR,
      g_param_spec_uint ("object-detection-outline-color",
          "Object detection outline color",
          "Color (ARGB) to use for object detection overlay outline",
          0, G_MAXUINT, 0xFFFFFFFF,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  /**
   * GstObjectDetectionOverlay:draw-labels
   *
   * Control labels drawing
   *
   * Since: 1.24
   */
  g_object_class_install_property (gobject_class, PROP_DRAW_LABELS,
      g_param_spec_boolean ("draw-labels",
          "Draw labels",
          "Draw object labels",
          TRUE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  /**
   * GstObjectDetectionOverlay:draw-tracking-labels
   *
   * Control tracking labels drawing
   *
   * Since: 1.28
   */
  g_object_class_install_property (gobject_class, PROP_DRAW_TRACKING_LABELS,
      g_param_spec_boolean ("draw-tracking-labels",
          "Draw tracking labels",
          "Draw object tracking labels",
          TRUE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  /**
   * GstObjectDetectionOverlay:labels-color
   *
   * Control labels color
   * Format ARGB (ex. 0xFFFF0000 for red)
   *
   * Since: 1.24
   */
  g_object_class_install_property (gobject_class, PROP_LABELS_COLOR,
      g_param_spec_uint ("labels-color",
          "Labels color",
          "Color (ARGB) to use for object labels",
          0, G_MAXUINT, 0xFFFFFF, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

   /**
   * GstObjectDetectionOverlay:filled-box
   *
   * Draw filled-box in the region where the object is detected is masked.
   * Filling color will be based on object-detection-outline-color.
   *
   * Since: 1.28
   */
  g_object_class_install_property (gobject_class, PROP_FILLED_BOX,
      g_param_spec_boolean ("filled-box",
          "Filled box",
          "Draw a filled box",
          TRUE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

   /**
   * GstObjectDetectionOverlay:expire-overlay
   *
   * Re-uses the last overlay for the specified amount of time before
   * expiring it (in ns), NONE for never
   *
   * Since: 1.28
   */
  g_object_class_install_property (gobject_class, PROP_EXPIRE_OVERLAY,
      g_param_spec_uint64 ("expire-overlay",
          "Expire overlay",
          "Re-uses the last overlay for the specified amount of time before"
          " expiring it (in ns), MAX for never",
          0, GST_CLOCK_TIME_NONE, GST_SECOND,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

   /**
   * GstObjectDetectionOverlay:tracking-outline-colors
   *
   * In the presence of tracking information, each object will get its
   * own color, ignores object-detection-outline-color
   *
   * Since: 1.28
   */
  g_object_class_install_property (gobject_class, PROP_TRACKING_OUTLINE_COLORS,
      g_param_spec_boolean ("tracking-outline-colors",
          "Tracking outline colors",
          "In the presence of tracking information, each object will get"
          " its own color, ignores object-detection-outline-color",
          TRUE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  element_class = (GstElementClass *) klass;

  gst_element_class_add_static_pad_template (element_class, &sink_template);
  gst_element_class_add_static_pad_template (element_class, &src_template);
  gst_element_class_set_static_metadata (element_class,
      "Object Detection Overlay",
      "Analyzer/Visualization/Video",
      "Overlay a visual representation of analytics metadata on the video",
      "Daniel Morin");

  basetransform_class = (GstBaseTransformClass *) klass;
  basetransform_class->passthrough_on_same_caps = FALSE;
  basetransform_class->start =
      GST_DEBUG_FUNCPTR (gst_object_detection_overlay_start);

  basetransform_class->stop =
      GST_DEBUG_FUNCPTR (gst_object_detection_overlay_stop);

  basetransform_class->sink_event =
      GST_DEBUG_FUNCPTR (gst_object_detection_overlay_sink_event);

  videofilter_class = (GstVideoFilterClass *) klass;
  videofilter_class->set_info =
      GST_DEBUG_FUNCPTR (gst_object_detection_overlay_set_info);
  videofilter_class->transform_frame_ip =
      GST_DEBUG_FUNCPTR (gst_object_detection_overlay_transform_frame_ip);
}

static void
gst_object_detection_overlay_adj_labels_outline_ofs (GstObjectDetectionOverlay *
    overlay, PangoFontDescription * desc)
{
  gint font_size = pango_font_description_get_size (desc) / PANGO_SCALE;
  overlay->labels_outline_ofs = (double) (font_size) / 15.0;
  if (overlay->labels_outline_ofs < MINIMUM_TEXT_OUTLINE_OFFSET)
    overlay->labels_outline_ofs = MINIMUM_TEXT_OUTLINE_OFFSET;
}

static void
gst_object_detection_overlay_finalize (GObject * object)
{
  gst_object_detection_overlay_stop (GST_BASE_TRANSFORM (object));

  G_OBJECT_CLASS (parent_class)->finalize (object);
}

static void
gst_object_detection_overlay_init (GstObjectDetectionOverlay * overlay)
{
  overlay->pango_context = NULL;
  overlay->pango_layout = NULL;
  overlay->od_outline_color = 0xFFFFFFFF;
  overlay->draw_labels = TRUE;
  overlay->draw_tracking_labels = TRUE;
  overlay->labels_color = 0xFFFFFFFF;
  overlay->filled_box = FALSE;
  overlay->in_info = &GST_VIDEO_FILTER (overlay)->in_info;
  overlay->attach_compo_to_buffer = TRUE;
  overlay->canvas = NULL;
  overlay->labels_stroke_width = 1.0;
  overlay->od_outline_stroke_width = 2;
  overlay->composition = NULL;
  overlay->flushing = FALSE;
  overlay->expire_overlay = GST_SECOND;
  overlay->tracking_outline_colors = TRUE;
  GST_DEBUG_CATEGORY_INIT (objectdetectionoverlay_debug,
      "analytics_overlay_od", 0, "Object detection overlay");
}

static void
gst_object_detection_overlay_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstObjectDetectionOverlay *overlay;
  overlay = GST_OBJECT_DETECTION_OVERLAY (object);

  switch (prop_id) {
    case PROP_OD_OUTLINE_COLOR:
      overlay->od_outline_color = g_value_get_uint (value);
      break;
    case PROP_DRAW_LABELS:
      overlay->draw_labels = g_value_get_boolean (value);
      break;
    case PROP_DRAW_TRACKING_LABELS:
      overlay->draw_tracking_labels = g_value_get_boolean (value);
      break;
    case PROP_LABELS_COLOR:
      overlay->labels_color = g_value_get_uint (value);
      break;
    case PROP_FILLED_BOX:
      overlay->filled_box = g_value_get_boolean (value);
      break;
    case PROP_EXPIRE_OVERLAY:
      overlay->expire_overlay = g_value_get_uint64 (value);
      break;
    case PROP_TRACKING_OUTLINE_COLORS:
      overlay->tracking_outline_colors = g_value_get_boolean (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
gst_object_detection_overlay_get_property (GObject * object,
    guint prop_id, GValue * value, GParamSpec * pspec)
{
  GstObjectDetectionOverlay *od_overlay = GST_OBJECT_DETECTION_OVERLAY (object);

  switch (prop_id) {
    case PROP_OD_OUTLINE_COLOR:
      g_value_set_uint (value, od_overlay->od_outline_color);
      break;
    case PROP_DRAW_LABELS:
      g_value_set_boolean (value, od_overlay->draw_labels);
      break;
    case PROP_DRAW_TRACKING_LABELS:
      g_value_set_boolean (value, od_overlay->draw_tracking_labels);
      break;
    case PROP_LABELS_COLOR:
      g_value_set_uint (value, od_overlay->labels_color);
      break;
    case PROP_FILLED_BOX:
      g_value_set_boolean (value, od_overlay->filled_box);
      break;
    case PROP_EXPIRE_OVERLAY:
      g_value_set_uint64 (value, od_overlay->expire_overlay);
      break;
    case PROP_TRACKING_OUTLINE_COLORS:
      g_value_set_boolean (value, od_overlay->tracking_outline_colors);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static gboolean
gst_object_detection_overlay_can_handle_caps (GstCaps * incaps)
{
  gboolean ret;
  GstCaps *caps;

  caps = gst_static_caps_get (&sw_template_caps);
  ret = gst_caps_is_subset (incaps, caps);
  gst_caps_unref (caps);

  return ret;
}

static gboolean
gst_object_detection_overlay_negotiate (GstObjectDetectionOverlay * overlay,
    GstCaps * caps)
{
  GstBaseTransform *basetransform = GST_BASE_TRANSFORM (overlay);
  gboolean upstream_has_meta = FALSE;
  gboolean caps_has_meta = FALSE;
  gboolean alloc_has_meta = FALSE;
  gboolean attach = FALSE;
  gboolean ret = TRUE;
  guint width, height;
  GstCapsFeatures *f;
  GstCaps *overlay_caps;
  GstQuery *query;
  guint alloc_index;
  GstPad *srcpad = basetransform->srcpad;
  GstPad *sinkpad = basetransform->sinkpad;

  GST_DEBUG_OBJECT (overlay, "performing negotiation");

  /* Clear any pending reconfigure to avoid negotiating twice */
  gst_pad_check_reconfigure (sinkpad);

  /* Check if upstream caps have meta */
  if ((f = gst_caps_get_features (caps, 0))) {
    GST_DEBUG_OBJECT (overlay, "upstream has caps");
    upstream_has_meta = gst_caps_features_contains (f,
        GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION);
  }

  /* Initialize dimensions */
  width = GST_VIDEO_INFO_WIDTH (overlay->in_info);
  height = GST_VIDEO_INFO_HEIGHT (overlay->in_info);
  GST_DEBUG_OBJECT (overlay, "initial dims: %ux%u", width, height);

  if (upstream_has_meta) {
    overlay_caps = gst_caps_ref (caps);
  } else {
    GstCaps *peercaps;

    /* BaseTransform requires caps for the allocation query to work */
    overlay_caps = gst_caps_copy (caps);
    f = gst_caps_get_features (overlay_caps, 0);
    gst_caps_features_add (f,
        GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION);

    /* Then check if downstream accept overlay composition in caps */
    /* FIXME: We should probably check if downstream *prefers* the
     * overlay meta, and only enforce usage of it if we can't handle
     * the format ourselves and thus would have to drop the overlays.
     * Otherwise we should prefer what downstream wants here.
     */
    peercaps = gst_pad_peer_query_caps (srcpad, overlay_caps);
    caps_has_meta = !gst_caps_is_empty (peercaps);
    gst_caps_unref (peercaps);

    GST_DEBUG_OBJECT (overlay, "caps have overlay meta %d", caps_has_meta);
  }

  if (upstream_has_meta || caps_has_meta) {
    /* Send caps immediately, it's needed by GstBaseTransform to get a reply
     * from allocation query */
    GST_BASE_TRANSFORM_CLASS (parent_class)->set_caps (basetransform, caps,
        overlay_caps);
    ret = gst_pad_set_caps (srcpad, overlay_caps);

    /* First check if the allocation meta has compositon */
    query = gst_query_new_allocation (overlay_caps, FALSE);

    if (!gst_pad_peer_query (srcpad, query)) {
      /* no problem, we use the query defaults */
      GST_DEBUG_OBJECT (overlay, "ALLOCATION query failed");

      /* In case we were flushing, mark reconfigure and fail this method,
       * will make it retry */
      if (overlay->flushing)
        ret = FALSE;
    }

    alloc_has_meta = gst_query_find_allocation_meta (query,
        GST_VIDEO_OVERLAY_COMPOSITION_META_API_TYPE, &alloc_index);

    GST_DEBUG_OBJECT (overlay, "sink alloc has overlay meta %d",
        alloc_has_meta);

    if (alloc_has_meta) {
      const GstStructure *params;

      gst_query_parse_nth_allocation_meta (query, alloc_index, &params);
      if (params) {
        if (gst_structure_get (params, "width", G_TYPE_UINT, &width,
                "height", G_TYPE_UINT, &height, NULL)) {
          GST_DEBUG_OBJECT (overlay, "received window size: %dx%d", width,
              height);
          g_assert (width != 0 && height != 0);
        }
      }
    }

    gst_query_unref (query);
  }

  /* Update render size if needed */
  overlay->canvas_length = width * height;

  /* For backward compatibility, we will prefer blitting if downstream
   * allocation does not support the meta. In other case we will prefer
   * attaching, and will fail the negotiation in the unlikely case we are
   * force to blit, but format isn't supported. */

  if (upstream_has_meta) {
    attach = TRUE;
  } else if (caps_has_meta) {
    if (alloc_has_meta) {
      attach = TRUE;
    } else {
      /* Don't attach unless we cannot handle the format */
      attach = !gst_object_detection_overlay_can_handle_caps (caps);
    }
  } else {
    ret = gst_object_detection_overlay_can_handle_caps (caps);
  }

  /* If we attach, then pick the overlay caps */
  if (attach) {
    GST_DEBUG_OBJECT (overlay, "Using caps %" GST_PTR_FORMAT, overlay_caps);
    /* Caps where already sent */
  } else if (ret) {
    GST_DEBUG_OBJECT (overlay, "Using caps %" GST_PTR_FORMAT, caps);
    GST_BASE_TRANSFORM_CLASS (parent_class)->set_caps (basetransform, caps,
        caps);
    ret = gst_pad_set_caps (srcpad, caps);
  }

  overlay->attach_compo_to_buffer = attach;

  if (attach) {
    GST_BASE_TRANSFORM_CLASS (parent_class)->passthrough_on_same_caps = FALSE;
  }

  if (!ret) {
    GST_DEBUG_OBJECT (overlay, "negotiation failed, schedule reconfigure");
    gst_pad_mark_reconfigure (srcpad);
  }

  gst_caps_unref (overlay_caps);

  return ret;
}

static gboolean
gst_object_detection_overlay_setcaps (GstObjectDetectionOverlay * overlay,
    GstCaps * caps)
{
  gboolean ret = FALSE;

  if (!gst_video_info_from_caps (overlay->in_info, caps))
    goto invalid_caps;

  ret = gst_object_detection_overlay_negotiate (overlay, caps);
  GST_VIDEO_FILTER (overlay)->negotiated = ret;

  if (!overlay->attach_compo_to_buffer &&
      !gst_object_detection_overlay_can_handle_caps (caps)) {
    GST_DEBUG_OBJECT (overlay, "unsupported caps %" GST_PTR_FORMAT, caps);
    ret = FALSE;
  }

  return ret;

  /* ERRORS */
invalid_caps:
  {
    GST_DEBUG_OBJECT (overlay, "could not parse caps");
    return FALSE;
  }
}

static gboolean
gst_object_detection_overlay_sink_event (GstBaseTransform * trans,
    GstEvent * event)
{
  gboolean ret = FALSE;
  GST_DEBUG_OBJECT (trans, "received sink event %s",
      GST_EVENT_TYPE_NAME (event));

  GstObjectDetectionOverlay *overlay = GST_OBJECT_DETECTION_OVERLAY (trans);

  switch (GST_EVENT_TYPE (event)) {
    case GST_EVENT_CAPS:
    {
      GstCaps *caps;
      gst_event_parse_caps (event, &caps);
      ret = gst_object_detection_overlay_setcaps (overlay, caps);
      gst_event_unref (event);
      break;
    }
    case GST_EVENT_EOS:
      g_mutex_lock (&overlay->stream_event_mutex);
      GST_INFO_OBJECT (overlay, "EOS");
      overlay->eos = TRUE;
      g_mutex_unlock (&overlay->stream_event_mutex);
      ret = GST_BASE_TRANSFORM_CLASS (parent_class)->sink_event (trans, event);
      break;
    case GST_EVENT_FLUSH_START:
      g_mutex_lock (&overlay->stream_event_mutex);
      GST_INFO_OBJECT (overlay, "Flush start");
      overlay->flushing = TRUE;
      g_mutex_unlock (&overlay->stream_event_mutex);
      ret = GST_BASE_TRANSFORM_CLASS (parent_class)->sink_event (trans, event);
      break;
    case GST_EVENT_FLUSH_STOP:
      g_mutex_lock (&overlay->stream_event_mutex);
      GST_INFO_OBJECT (overlay, "Flush stop");
      overlay->eos = FALSE;
      overlay->flushing = FALSE;
      g_mutex_unlock (&overlay->stream_event_mutex);
      ret = GST_BASE_TRANSFORM_CLASS (parent_class)->sink_event (trans, event);
      break;
    default:
      ret = GST_BASE_TRANSFORM_CLASS (parent_class)->sink_event (trans, event);
      break;
  }

  return ret;
}

static gboolean
gst_object_detection_overlay_start (GstBaseTransform * trans)
{
  GstObjectDetectionOverlay *overlay = GST_OBJECT_DETECTION_OVERLAY (trans);
  PangoFontDescription *desc;
  PangoFontMap *fontmap;

  fontmap = pango_cairo_font_map_new ();
  overlay->pango_context =
      pango_font_map_create_context (PANGO_FONT_MAP (fontmap));
  g_object_unref (fontmap);
  overlay->pango_layout = pango_layout_new (overlay->pango_context);
  desc = pango_context_get_font_description (overlay->pango_context);
  pango_font_description_set_size (desc, 10000);
  pango_font_description_set_weight (desc, PANGO_WEIGHT_ULTRALIGHT);
  pango_context_set_font_description (overlay->pango_context, desc);
  pango_layout_set_alignment (overlay->pango_layout, PANGO_ALIGN_LEFT);

  gst_object_detection_overlay_adj_labels_outline_ofs (overlay, desc);
  GST_DEBUG_OBJECT (overlay, "labels_outline_offset %f",
      overlay->labels_outline_ofs);

  return TRUE;
}

static gboolean
gst_object_detection_overlay_stop (GstBaseTransform * trans)
{
  GstObjectDetectionOverlay *overlay = GST_OBJECT_DETECTION_OVERLAY (trans);

  g_clear_object (&overlay->pango_layout);
  g_clear_object (&overlay->pango_context);
  gst_clear_buffer (&overlay->canvas);

  return TRUE;
}

static gboolean
gst_object_detection_overlay_set_info (GstVideoFilter * filter,
    GstCaps * incaps, GstVideoInfo * in_info, GstCaps * outcaps,
    GstVideoInfo * out_info)
{
  GstObjectDetectionOverlay *overlay = GST_OBJECT_DETECTION_OVERLAY (filter);
  GST_DEBUG_OBJECT (filter, "set_info incaps:%" GST_PTR_FORMAT, incaps);
  GST_DEBUG_OBJECT (filter, "set_info outcaps:%" GST_PTR_FORMAT, outcaps);

  filter->in_info = *in_info;
  filter->out_info = *out_info;

  cairo_matrix_init_scale (&overlay->cairo_matrix, 1, 1);
  overlay->render_len = GST_VIDEO_INFO_WIDTH (in_info) *
      GST_VIDEO_INFO_HEIGHT (in_info) * 4;
  return TRUE;
}

static void
gst_object_detection_overlay_create_cairo_context (GstObjectDetectionOverlay *
    overlay, GstObjectDetectionOverlayPangoCairoContext * cairo_ctx,
    guint8 * data)
{
  cairo_ctx->cairo_matrix = &overlay->cairo_matrix;
  cairo_ctx->surface = cairo_image_surface_create_for_data (data,
      CAIRO_FORMAT_ARGB32, GST_VIDEO_INFO_WIDTH (overlay->in_info),
      GST_VIDEO_INFO_HEIGHT (overlay->in_info),
      GST_VIDEO_INFO_WIDTH (overlay->in_info) * 4);
  cairo_ctx->cr = cairo_create (cairo_ctx->surface);

  /* clear surface */
  cairo_set_operator (cairo_ctx->cr, CAIRO_OPERATOR_CLEAR);
  cairo_paint (cairo_ctx->cr);
  cairo_set_operator (cairo_ctx->cr, CAIRO_OPERATOR_OVER);

  /* apply transformations */
  cairo_set_matrix (cairo_ctx->cr, cairo_ctx->cairo_matrix);
  cairo_save (cairo_ctx->cr);
}

static void
    gst_object_detection_overlay_destroy_cairo_context
    (GstObjectDetectionOverlayPangoCairoContext * cairo_ctx)
{
  cairo_restore (cairo_ctx->cr);
  cairo_destroy (cairo_ctx->cr);
  cairo_surface_destroy (cairo_ctx->surface);
}

static GstFlowReturn
gst_object_detection_overlay_transform_frame_ip (GstVideoFilter * filter,
    GstVideoFrame * frame)
{
  GstBaseTransform *baset = GST_BASE_TRANSFORM (filter);
  GstObjectDetectionOverlay *overlay = GST_OBJECT_DETECTION_OVERLAY (filter);
  gpointer state = NULL;
  GstVideoOverlayRectangle *rectangle = NULL;
  gchar str_buf[5];
  GstAnalyticsMtd rlt_mtd;
  GstAnalyticsODMtd *od_mtd;
  gint x, y, w, h;
  gfloat loc_confi_lvl;
  gboolean success;
  GstClockTime rt = GST_CLOCK_TIME_NONE;

  GST_DEBUG_OBJECT (filter, "buffer writeable=%d",
      gst_buffer_is_writable (frame->buffer));

  g_mutex_lock (&overlay->stream_event_mutex);
  if (overlay->eos || overlay->flushing) {
    g_mutex_unlock (&overlay->stream_event_mutex);
    return GST_FLOW_EOS;
  }
  g_mutex_unlock (&overlay->stream_event_mutex);

  if (baset->have_segment)
    rt = gst_segment_to_running_time (&baset->segment, GST_FORMAT_TIME,
        GST_BUFFER_PTS (frame->buffer));

  GstAnalyticsRelationMeta *rmeta = (GstAnalyticsRelationMeta *)
      gst_buffer_get_meta (GST_BUFFER (frame->buffer),
      GST_ANALYTICS_RELATION_META_API_TYPE);

  if (rmeta) {
    GST_DEBUG_OBJECT (filter, "received buffer with analytics relation meta");

    GstBuffer *buffer;
    GstMapInfo map;
    GstObjectDetectionOverlayPangoCairoContext cairo_ctx;

    buffer = gst_buffer_new_and_alloc (overlay->render_len);
    gst_buffer_add_video_meta (buffer,
        GST_VIDEO_FRAME_FLAG_NONE, GST_VIDEO_OVERLAY_COMPOSITION_FORMAT_RGB,
        GST_VIDEO_INFO_WIDTH (overlay->in_info),
        GST_VIDEO_INFO_HEIGHT (overlay->in_info));

    gst_buffer_replace (&overlay->canvas, buffer);
    gst_buffer_unref (buffer);

    gst_buffer_map (buffer, &map, GST_MAP_READWRITE);
    memset (map.data, 0, overlay->render_len);

    gst_object_detection_overlay_create_cairo_context (overlay,
        &cairo_ctx, map.data);

    if (overlay->composition)
      gst_video_overlay_composition_unref (overlay->composition);

    overlay->composition = gst_video_overlay_composition_new (NULL);

    overlay->last_composition_update = rt;

    /* Get quark represent object detection metadata type */
    GstAnalyticsMtdType rlt_type = gst_analytics_od_mtd_get_mtd_type ();
    while (gst_analytics_relation_meta_iterate (rmeta, &state, rlt_type,
            &rlt_mtd)) {
      od_mtd = (GstAnalyticsODMtd *) & rlt_mtd;
      GST_DEBUG_OBJECT (filter, "buffer contain OD mtd");

      /* Quark representing the type of the object detected by OD */
      GQuark od_obj_type = gst_analytics_od_mtd_get_obj_type (od_mtd);

      // Find classification metadata attached to object detection metadata
      GstAnalyticsMtd cls_rlt_mtd;
      success = gst_analytics_relation_meta_get_direct_related (rmeta,
          gst_analytics_mtd_get_id (
              (GstAnalyticsMtd *) od_mtd),
          GST_ANALYTICS_REL_TYPE_RELATE_TO,
          gst_analytics_cls_mtd_get_mtd_type (), NULL, &cls_rlt_mtd);

      gst_object_detection_overlay_render_boundingbox
          (GST_OBJECT_DETECTION_OVERLAY (filter), &cairo_ctx, rmeta, od_mtd);

      if (overlay->draw_labels) {
        if (success) {
          /* Use associated classification analytics-meta */
          g_snprintf (str_buf, sizeof (str_buf), "%04.2f",
              gst_analytics_cls_mtd_get_level (
                  (GstAnalyticsClsMtd *) & cls_rlt_mtd, 0));

          od_obj_type = gst_analytics_cls_mtd_get_quark (&cls_rlt_mtd, 0);
        } else {
          /* Use basic class type directly on OD.
           * Here we want the confidence level of the bbox but to retrieve
           * we need to also retrieve the bbox location. */
          gst_analytics_od_mtd_get_location (od_mtd, &x, &y, &w, &h,
              &loc_confi_lvl);
          GST_TRACE_OBJECT (filter, "obj {type: %s loc:[(%u,%u)-(%ux%u)] @ %f}",
              g_quark_to_string (od_obj_type), x, y, w, h, loc_confi_lvl);

          g_snprintf (str_buf, sizeof (str_buf), "%04.2f", loc_confi_lvl);
        }
        gchar *text = g_strdup_printf ("%s (c=%s)",
            g_quark_to_string (od_obj_type), str_buf);

        gst_object_detection_overlay_render_text_annotation
            (GST_OBJECT_DETECTION_OVERLAY (filter), &cairo_ctx, od_mtd, text);

        g_free (text);
      }

      if (overlay->draw_tracking_labels)
        gst_object_detection_overlay_render_tracking_text_annotation
            (GST_OBJECT_DETECTION_OVERLAY (filter), &cairo_ctx, rmeta, od_mtd);
    }

    rectangle = gst_video_overlay_rectangle_new_raw (overlay->canvas,
        0, 0, GST_VIDEO_INFO_WIDTH (overlay->in_info),
        GST_VIDEO_INFO_HEIGHT (overlay->in_info),
        GST_VIDEO_OVERLAY_FORMAT_FLAG_PREMULTIPLIED_ALPHA);
    gst_video_overlay_composition_add_rectangle (overlay->composition,
        rectangle);
    gst_video_overlay_rectangle_unref (rectangle);

    gst_object_detection_overlay_destroy_cairo_context (&cairo_ctx);
    gst_buffer_unmap (buffer, &map);

  } else {
    if (rt != GST_CLOCK_TIME_NONE &&
        overlay->expire_overlay != GST_CLOCK_TIME_NONE &&
        overlay->last_composition_update != GST_CLOCK_TIME_NONE &&
        overlay->composition &&
        overlay->last_composition_update + overlay->expire_overlay <= rt) {
      gst_video_overlay_composition_unref (overlay->composition);
      overlay->composition = NULL;
    }
  }

  if (overlay->composition) {
    GST_DEBUG_OBJECT (filter, "have composition");

    if (overlay->attach_compo_to_buffer) {
      GST_DEBUG_OBJECT (filter, "attach");

      gst_buffer_add_video_overlay_composition_meta (frame->buffer,
          overlay->composition);
    } else {
      gst_video_overlay_composition_blend (overlay->composition, frame);
    }
  }

  return GST_FLOW_OK;
}


/*
 * HSV version using golden angle distribution around the color wheel.
 * This ensures maximum color separation by dividing the color wheel optimally.
 * Sequence: 0°, 180°, 90°, 270°, 45°, 225°, 135°, 315°, etc.
 *
 * Returns RGB color as uint32_t in format 0x00RRGGBB
 */
static guint32
generate_track_color_hsv (guint32 track_id)
{
  gfloat h = 0.0f;
  gfloat increment = 0.5f;
  // Fixed saturation and value for consistent appearance
  const gfloat S = 0.85f;       // High saturation for vivid colors
  const gfloat V = 0.95f;       // High value for bright colors

  /* Start from 1 to avoid special case */
  track_id++;

  /* Calculate hue using bit-reversal pattern for optimal distribution */
  /* Gives us the sequence: 0, 0.5, 0.25, 0.75, 0.125, 0.625, 0.375, 0.875, .. */
  while (track_id > 1) {
    if (track_id & 1) {
      h += increment;
    }
    track_id >>= 1;
    increment *= 0.5f;
  }

  /* Keep hue in [0, 1) range */
  while (h >= 1.0f)
    h -= 1.0f;

  /* Convert HSV to RGB */
  int hi = (int) (h * 6.0f);
  gfloat f = h * 6.0f - hi;
  gfloat p = V * (1.0f - S);
  gfloat q = V * (1.0f - f * S);
  gfloat t = V * (1.0f - (1.0f - f) * S);

  gfloat r, g, b;
  switch (hi % 6) {
    case 0:
      r = V;
      g = t;
      b = p;
      break;
    case 1:
      r = q;
      g = V;
      b = p;
      break;
    case 2:
      r = p;
      g = V;
      b = t;
      break;
    case 3:
      r = p;
      g = q;
      b = V;
      break;
    case 4:
      r = t;
      g = p;
      b = V;
      break;
    case 5:
      r = V;
      g = p;
      b = q;
      break;
    default:
      r = g = b = 0;
      break;
  }

  guint8 r8 = (guint8) (r * 255.0f);
  guint8 g8 = (guint8) (g * 255.0f);
  guint8 b8 = (guint8) (b * 255.0f);

  return ((guint32) r8 << 16) | ((guint32) g8 << 8) | (guint32) b8 | 0xFF000000;
}

static void
gst_object_detection_overlay_render_boundingbox (GstObjectDetectionOverlay
    * overlay, GstObjectDetectionOverlayPangoCairoContext * ctx,
    GstAnalyticsRelationMeta * rmeta, GstAnalyticsODMtd * od_mtd)
{
  gint x, y, w, h;
  gfloat r = 0.0f;
  gfloat _dummy;
  gint maxw = GST_VIDEO_INFO_WIDTH (overlay->in_info) - 1;
  gint maxh = GST_VIDEO_INFO_HEIGHT (overlay->in_info) - 1;
  GstAnalyticsTrackingMtd tracking_mtd;
  guint32 color;

  cairo_save (ctx->cr);
  gst_analytics_od_mtd_get_oriented_location (od_mtd, &x, &y, &w, &h, &r,
      &_dummy);

  x = CLAMP (x, 0, maxw);
  y = CLAMP (y, 0, maxh);
  w = CLAMP (w, 0, maxw - x);
  h = CLAMP (h, 0, maxh - y);

  if (overlay->tracking_outline_colors &&
      gst_analytics_relation_meta_get_direct_related (rmeta, od_mtd->id,
          GST_ANALYTICS_REL_TYPE_RELATE_TO,
          gst_analytics_tracking_mtd_get_mtd_type (), NULL, &tracking_mtd)) {
    guint64 tid;

    gst_analytics_tracking_mtd_get_info (&tracking_mtd, &tid, NULL, NULL, NULL);

    color = generate_track_color_hsv (tid & 0xFFFFFFF);
  } else {
    color = overlay->od_outline_color;
  }

  /* Set bounding box stroke color and width */
  cairo_set_source_rgba (ctx->cr,
      ((color >> 16) & 0xFF) / 255.0,
      ((color >> 8) & 0xFF) / 255.0,
      (color & 0xFF) / 255.0, ((color >> 24) & 0xFF) / 255.0);
  cairo_set_line_width (ctx->cr, overlay->od_outline_stroke_width);

  /* draw bounding box */
  if (fabsf (r) < ROTATION_EPSILON) {
    /* Fast path: axis-aligned rectangle */
    cairo_rectangle (ctx->cr, x, y, w, h);
  } else {
    /* Rotated path: calculate 4 corners and draw */
    gfloat xc = x + w / 2.0f;
    gfloat yc = y + h / 2.0f;
    gfloat cos_r = cosf (r);
    gfloat sin_r = sinf (r);

    /* Corner offsets (pre-rotation, relative to center) */
    gfloat corners[4][2] = {
      {-w / 2.0f, -h / 2.0f},   /* top-left */
      {w / 2.0f, -h / 2.0f},    /* top-right */
      {w / 2.0f, h / 2.0f},     /* bottom-right */
      {-w / 2.0f, h / 2.0f}     /* bottom-left */
    };

    /* Draw rotated box */
    for (int i = 0; i < 4; i++) {
      gfloat dx = corners[i][0];
      gfloat dy = corners[i][1];
      gfloat rx = dx * cos_r - dy * sin_r + xc;
      gfloat ry = dx * sin_r + dy * cos_r + yc;

      if (i == 0)
        cairo_move_to (ctx->cr, rx, ry);
      else
        cairo_line_to (ctx->cr, rx, ry);
    }
    cairo_close_path (ctx->cr);
  }

  if (overlay->filled_box == FALSE)
    cairo_stroke (ctx->cr);
  else
    cairo_fill (ctx->cr);

  cairo_restore (ctx->cr);
}

static void
gst_object_detection_overlay_render_text_annotation (GstObjectDetectionOverlay
    * overlay, GstObjectDetectionOverlayPangoCairoContext * ctx,
    GstAnalyticsODMtd * od_mtd, const gchar * annotation)
{
  PangoRectangle ink_rect, logical_rect;
  gint x, y, w, h;
  gfloat _dummy;
  gint maxw = GST_VIDEO_INFO_WIDTH (overlay->in_info) - 1;
  gint maxh = GST_VIDEO_INFO_HEIGHT (overlay->in_info) - 1;

  cairo_save (ctx->cr);
  gst_analytics_od_mtd_get_location (od_mtd, &x, &y, &w, &h, &_dummy);

  x = CLAMP (x, 0, maxw);
  y = CLAMP (y, 0, maxh);
  w = CLAMP (w, 0, maxw - x);
  h = CLAMP (h, 0, maxh - y);

  /* Set label strokes color and width */
  cairo_set_source_rgba (ctx->cr,
      ((overlay->labels_color >> 16) & 0xFF) / 255.0,
      ((overlay->labels_color >> 8) & 0xFF) / 255.0,
      ((overlay->labels_color) & 0xFF) / 255.0,
      ((overlay->labels_color >> 24) & 0xFF) / 255.0);
  cairo_set_line_width (ctx->cr, overlay->labels_stroke_width);

  pango_layout_set_markup (overlay->pango_layout, annotation,
      strlen (annotation));
  pango_layout_get_pixel_extents (overlay->pango_layout, &ink_rect,
      &logical_rect);
  GST_DEBUG_OBJECT (overlay, "logical_rect:(%d,%d),%dx%d", logical_rect.x,
      logical_rect.y, logical_rect.width, logical_rect.height);
  GST_DEBUG_OBJECT (overlay, "ink_rect:(%d,%d),%dx%d", ink_rect.x, ink_rect.y,
      ink_rect.width, ink_rect.height);
  cairo_move_to (ctx->cr, x + overlay->labels_outline_ofs,
      y - logical_rect.height - overlay->labels_outline_ofs);

  pango_cairo_layout_path (ctx->cr, overlay->pango_layout);
  cairo_stroke (ctx->cr);
  cairo_restore (ctx->cr);
}

static void
    gst_object_detection_overlay_render_tracking_text_annotation
    (GstObjectDetectionOverlay * overlay,
    GstObjectDetectionOverlayPangoCairoContext * ctx,
    GstAnalyticsRelationMeta * rmeta, const GstAnalyticsODMtd * od_mtd)
{
  GstAnalyticsMtd tracking_mtd;
  guint64 tid;
  PangoRectangle ink_rect, logical_rect;
  gint x, y, w, h;
  gint maxw = GST_VIDEO_INFO_WIDTH (overlay->in_info) - 1;
  gint maxh = GST_VIDEO_INFO_HEIGHT (overlay->in_info) - 1;

  gchar *annotation;

  if (!gst_analytics_relation_meta_get_direct_related (rmeta, od_mtd->id,
          GST_ANALYTICS_REL_TYPE_RELATE_TO,
          gst_analytics_tracking_mtd_get_mtd_type (), NULL, &tracking_mtd))
    return;

  gst_analytics_od_mtd_get_location (od_mtd, &x, &y, &w, &h, NULL);
  gst_analytics_tracking_mtd_get_info (&tracking_mtd, &tid, NULL, NULL, NULL);

  cairo_save (ctx->cr);
  x = CLAMP (x, 0, maxw);
  y = CLAMP (y, 0, maxh);
  w = CLAMP (w, 0, maxw - x);
  h = CLAMP (h, 0, maxh - y);

  /* Set label strokes color and width */
  cairo_set_source_rgba (ctx->cr,
      ((overlay->labels_color >> 16) & 0xFF) / 255.0,
      ((overlay->labels_color >> 8) & 0xFF) / 255.0,
      ((overlay->labels_color) & 0xFF) / 255.0,
      ((overlay->labels_color >> 24) & 0xFF) / 255.0);

  cairo_set_line_width (ctx->cr, overlay->labels_stroke_width);

  annotation = g_strdup_printf ("Track: %" G_GUINT64_FORMAT, tid);
  pango_layout_set_markup (overlay->pango_layout, annotation,
      strlen (annotation));
  g_free (annotation);

  pango_layout_get_pixel_extents (overlay->pango_layout, &ink_rect,
      &logical_rect);

  GST_LOG_OBJECT (overlay, "logical_rect:(%d,%d),%dx%d", logical_rect.x,
      logical_rect.y, logical_rect.width, logical_rect.height);
  GST_LOG_OBJECT (overlay, "ink_rect:(%d,%d),%dx%d", ink_rect.x, ink_rect.y,
      ink_rect.width, ink_rect.height);
  cairo_move_to (ctx->cr, x + overlay->labels_outline_ofs,
      y + h - logical_rect.height - overlay->labels_outline_ofs);

  pango_cairo_layout_path (ctx->cr, overlay->pango_layout);
  cairo_stroke (ctx->cr);
  cairo_restore (ctx->cr);
}
