/*
 * Copyright (C) 2022 Rafał Dzięgiel <rafostar.github@gmail.com>
 *
 * 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.
 */

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

#include "gstclappersink.h"
#include "gstgtkutils.h"

#define DEFAULT_FORCE_ASPECT_RATIO  TRUE
#define DEFAULT_PAR_N               1
#define DEFAULT_PAR_D               1
#define DEFAULT_KEEP_LAST_FRAME     FALSE
#define DEFAULT_ROTATION            GST_VIDEO_ORIENTATION_AUTO

#define WINDOW_CSS_CLASS_NAME       "clappersinkwindow"

enum
{
  PROP_0,
  PROP_WIDGET,
  PROP_FORCE_ASPECT_RATIO,
  PROP_PIXEL_ASPECT_RATIO,
  PROP_KEEP_LAST_FRAME,
  PROP_ROTATE_METHOD,
  PROP_LAST
};

#define GST_CAT_DEFAULT gst_clapper_sink_debug
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);

static void gst_clapper_sink_navigation_interface_init (
    GstNavigationInterface *iface);

#define parent_class gst_clapper_sink_parent_class
G_DEFINE_TYPE_WITH_CODE (GstClapperSink, gst_clapper_sink, GST_TYPE_VIDEO_SINK,
    G_IMPLEMENT_INTERFACE (GST_TYPE_NAVIGATION,
        gst_clapper_sink_navigation_interface_init));
GST_ELEMENT_REGISTER_DEFINE (clappersink, "clappersink", GST_RANK_NONE,
    GST_TYPE_CLAPPER_SINK);

static void
window_clear_no_lock (GstClapperSink *self)
{
  if (!self->window)
    return;

  GST_TRACE_OBJECT (self, "Window clear");

  if (self->window_destroy_id) {
    g_signal_handler_disconnect (self->window, self->window_destroy_id);
    self->window_destroy_id = 0;
  }
  self->window = NULL;
}

static void
widget_clear_no_lock (GstClapperSink *self)
{
  if (!self->widget)
    return;

  GST_TRACE_OBJECT (self, "Widget clear");

  if (self->widget_destroy_id) {
    g_signal_handler_disconnect (self->widget, self->widget_destroy_id);
    self->widget_destroy_id = 0;
  }
  g_clear_object (&self->widget);
}

static void
widget_destroy_cb (GtkWidget *widget, GstClapperSink *self)
{
  GST_CLAPPER_SINK_LOCK (self);
  widget_clear_no_lock (self);
  GST_CLAPPER_SINK_UNLOCK (self);
}

static void
window_destroy_cb (GtkWidget *window, GstClapperSink *self)
{
  GST_DEBUG_OBJECT (self, "Window destroy");

  GST_CLAPPER_SINK_LOCK (self);

  widget_clear_no_lock (self);
  window_clear_no_lock (self);

  GST_CLAPPER_SINK_UNLOCK (self);
}

static void
calculate_stream_coords (GstClapperSink *self, GtkWidget *widget,
    gdouble x, gdouble y, gdouble *stream_x, gdouble *stream_y)
{
  GstVideoRectangle result;
  gint scaled_width, scaled_height, scale_factor;
  gint video_width, video_height;
  gboolean force_aspect_ratio;

  GST_CLAPPER_SINK_LOCK (self);

  gst_gtk_get_width_height_for_rotation (GST_VIDEO_INFO_WIDTH (&self->v_info),
      GST_VIDEO_INFO_HEIGHT (&self->v_info), &video_height, &video_width,
      gst_clapper_paintable_get_rotation (self->paintable));
  force_aspect_ratio = self->force_aspect_ratio;

  GST_CLAPPER_SINK_UNLOCK (self);

  scale_factor = gtk_widget_get_scale_factor (widget);
  scaled_width = gtk_widget_get_width (widget) * scale_factor;
  scaled_height = gtk_widget_get_height (widget) * scale_factor;

  if (force_aspect_ratio) {
    GstVideoRectangle src, dst;

    src.x = 0;
    src.y = 0;
    src.w = gdk_paintable_get_intrinsic_width ((GdkPaintable *) self->paintable);
    src.h = gdk_paintable_get_intrinsic_height ((GdkPaintable *) self->paintable);

    dst.x = 0;
    dst.y = 0;
    dst.w = scaled_width;
    dst.h = scaled_height;

    gst_video_center_rect (&src, &dst, &result, TRUE);
  } else {
    result.x = 0;
    result.y = 0;
    result.w = scaled_width;
    result.h = scaled_height;
  }

  /* Display coordinates to stream coordinates */
  *stream_x = (result.w > 0)
      ? (x - result.x) / result.w * video_width
      : 0;
  *stream_y = (result.h > 0)
      ? (y - result.y) / result.h * video_height
      : 0;

  /* Clip to stream size */
  *stream_x = CLAMP (*stream_x, 0, video_width);
  *stream_y = CLAMP (*stream_y, 0, video_height);

  GST_LOG ("Transform coords %fx%f => %fx%f", x, y, *stream_x, *stream_y);
}

static void
gst_clapper_sink_widget_motion_event (GtkEventControllerMotion *motion,
    gdouble x, gdouble y, GstClapperSink *self)
{
  GtkWidget *widget;
  gdouble stream_x, stream_y;
  gboolean is_inactive;

  if (x == self->last_pos_x && y == self->last_pos_y)
    return;

  GST_OBJECT_LOCK (self);
  is_inactive = (GST_STATE (self) < GST_STATE_PLAYING);
  GST_OBJECT_UNLOCK (self);

  if (is_inactive)
    return;

  self->last_pos_x = x;
  self->last_pos_y = y;

  widget = gtk_event_controller_get_widget ((GtkEventController *) motion);
  calculate_stream_coords (self, widget, x, y, &stream_x, &stream_y);
  GST_LOG ("Event \"mouse-move\", x: %f, y: %f", stream_x, stream_y);

  gst_navigation_send_mouse_event ((GstNavigation *) self, "mouse-move",
      0, stream_x, stream_y);
}

static void
gst_clapper_sink_widget_button_event (GtkGestureClick *click,
    gint n_press, gdouble x, gdouble y, GstClapperSink *self)
{
  GtkWidget *widget;
  GdkEvent *event;
  GdkEventType event_type;
  const gchar *event_name;
  gdouble stream_x, stream_y;
  gboolean is_inactive;

  GST_OBJECT_LOCK (self);
  is_inactive = (GST_STATE (self) < GST_STATE_PLAYING);
  GST_OBJECT_UNLOCK (self);

  if (is_inactive)
    return;

  event = gtk_event_controller_get_current_event ((GtkEventController *) click);
  event_type = gdk_event_get_event_type (event);

  /* FIXME: Touchscreen handling should probably use new touch events from GStreamer 1.22 */
  event_name = (event_type == GDK_BUTTON_PRESS || event_type == GDK_TOUCH_BEGIN)
      ? "mouse-button-press"
      : (event_type == GDK_BUTTON_RELEASE || event_type == GDK_TOUCH_END)
      ? "mouse-button-release"
      : NULL;

  /* Can be NULL on touch */
  if (!event_name)
    return;

  widget = gtk_event_controller_get_widget ((GtkEventController *) click);
  calculate_stream_coords (self, widget, x, y, &stream_x, &stream_y);
  GST_LOG ("Event \"%s\", x: %f, y: %f", event_name, stream_x, stream_y);

  /* Gesture is set to handle only primary button, so we do not have to check */
  gst_navigation_send_mouse_event ((GstNavigation *) self, event_name,
      1, stream_x, stream_y);
}

/* Must call from main thread only with a lock */
static GtkWidget *
gst_clapper_sink_get_widget (GstClapperSink *self)
{
  if (G_UNLIKELY (!self->widget)) {
    GtkEventController *controller;
    GtkGesture *gesture;

    /* Make sure GTK is initialized */
    if (!gtk_init_check ()) {
      GST_ERROR_OBJECT (self, "Could not ensure GTK initialization");
      return NULL;
    }

    self->widget = gtk_picture_new ();

    /* Otherwise widget in grid will appear as a 1x1px
     * video which might be misleading for users */
    gtk_widget_set_hexpand (self->widget, TRUE);
    gtk_widget_set_vexpand (self->widget, TRUE);

    gtk_widget_set_focusable (self->widget, TRUE);
    gtk_widget_set_can_focus (self->widget, TRUE);

    controller = gtk_event_controller_motion_new ();
    g_signal_connect (controller, "motion",
        G_CALLBACK (gst_clapper_sink_widget_motion_event), self);
    gtk_widget_add_controller (self->widget, controller);

    gesture = gtk_gesture_click_new ();
    gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), 1);
    g_signal_connect (gesture, "pressed",
        G_CALLBACK (gst_clapper_sink_widget_button_event), self);
    g_signal_connect (gesture, "released",
        G_CALLBACK (gst_clapper_sink_widget_button_event), self);
    gtk_widget_add_controller (self->widget, GTK_EVENT_CONTROLLER (gesture));

    /* TODO: Implement touch events once we depend on GStreamer 1.22 */

    /* Take floating ref */
    g_object_ref_sink (self->widget);

    /* Set widget back pointer */
    gst_clapper_paintable_set_widget (self->paintable, self->widget);

    /* Set earlier remembered property */
#if GTK_CHECK_VERSION(4,8,0)
    if (self->force_aspect_ratio)
      gtk_picture_set_content_fit (GTK_PICTURE (self->widget), GTK_CONTENT_FIT_CONTAIN);
    else
      gtk_picture_set_content_fit (GTK_PICTURE (self->widget), GTK_CONTENT_FIT_FILL);
#else
    gtk_picture_set_keep_aspect_ratio (GTK_PICTURE (self->widget),
        self->force_aspect_ratio);
#endif

    gtk_picture_set_paintable (GTK_PICTURE (self->widget), GDK_PAINTABLE (self->paintable));

    self->widget_destroy_id = g_signal_connect (self->widget,
        "destroy", G_CALLBACK (widget_destroy_cb), self);
  }

  return self->widget;
}

static GtkWidget *
gst_clapper_sink_obtain_widget (GstClapperSink *self)
{
  GtkWidget *widget;

  GST_CLAPPER_SINK_LOCK (self);
  widget = gst_clapper_sink_get_widget (self);
  if (widget)
    g_object_ref (widget);
  GST_CLAPPER_SINK_UNLOCK (self);

  return widget;
}

static void
gst_clapper_sink_get_property (GObject *object, guint prop_id,
    GValue *value, GParamSpec *pspec)
{
  GstClapperSink *self = GST_CLAPPER_SINK_CAST (object);

  GST_CLAPPER_SINK_LOCK (self);

  switch (prop_id) {
    case PROP_WIDGET:
      if (self->widget) {
        g_value_set_object (value, self->widget);
      } else {
        GtkWidget *widget;

        GST_CLAPPER_SINK_UNLOCK (self);
        widget = gst_gtk_invoke_on_main ((GThreadFunc) gst_clapper_sink_obtain_widget, self);
        GST_CLAPPER_SINK_LOCK (self);

        g_value_set_object (value, widget);
        g_object_unref (widget);
      }
      break;
    case PROP_FORCE_ASPECT_RATIO:
      g_value_set_boolean (value, self->force_aspect_ratio);
      break;
    case PROP_PIXEL_ASPECT_RATIO:
      gst_value_set_fraction (value, self->par_n, self->par_d);
      break;
    case PROP_KEEP_LAST_FRAME:
      g_value_set_boolean (value, self->keep_last_frame);
      break;
    case PROP_ROTATE_METHOD:
      g_value_set_enum (value, self->rotation_mode);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }

  GST_CLAPPER_SINK_UNLOCK (self);
}

static void
gst_clapper_sink_set_property (GObject *object, guint prop_id,
    const GValue *value, GParamSpec *pspec)
{
  GstClapperSink *self = GST_CLAPPER_SINK_CAST (object);

  GST_CLAPPER_SINK_LOCK (self);

  switch (prop_id) {
    case PROP_FORCE_ASPECT_RATIO:
      self->force_aspect_ratio = g_value_get_boolean (value);

      if (self->widget) {
#if GTK_CHECK_VERSION(4,8,0)
        if (self->force_aspect_ratio)
          gtk_picture_set_content_fit (GTK_PICTURE (self->widget), GTK_CONTENT_FIT_CONTAIN);
        else
          gtk_picture_set_content_fit (GTK_PICTURE (self->widget), GTK_CONTENT_FIT_FILL);
#else
        gtk_picture_set_keep_aspect_ratio (GTK_PICTURE (self->widget),
            self->force_aspect_ratio);
#endif
      }
      break;
    case PROP_PIXEL_ASPECT_RATIO:
      self->par_n = gst_value_get_fraction_numerator (value);
      self->par_d = gst_value_get_fraction_denominator (value);

      gst_clapper_paintable_set_pixel_aspect_ratio (self->paintable,
          self->par_n, self->par_d);
      break;
    case PROP_KEEP_LAST_FRAME:
      self->keep_last_frame = g_value_get_boolean (value);
      break;
    case PROP_ROTATE_METHOD:
      self->rotation_mode = g_value_get_enum (value);

      gst_clapper_paintable_set_rotation (self->paintable,
          (self->rotation_mode == GST_VIDEO_ORIENTATION_AUTO) ?
              self->stream_orientation : self->rotation_mode);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }

  GST_CLAPPER_SINK_UNLOCK (self);
}

static void
gst_clapper_sink_navigation_send_event (GstNavigation *navigation,
    GstStructure *structure)
{
  GstClapperSink *sink = GST_CLAPPER_SINK_CAST (navigation);
  GstEvent *event;

  GST_TRACE_OBJECT (sink, "Navigation event: %" GST_PTR_FORMAT, structure);
  event = gst_event_new_navigation (structure);

  if (G_LIKELY (event)) {
    GstPad *pad;

    pad = gst_pad_get_peer (GST_VIDEO_SINK_PAD (sink));

    if (G_LIKELY (pad)) {
      if (!gst_pad_send_event (pad, gst_event_ref (event))) {
        /* If upstream didn't handle the event we'll post a message with it
         * for the application in case it wants to do something with it */
        gst_element_post_message (GST_ELEMENT_CAST (sink),
            gst_navigation_message_new_event (GST_OBJECT_CAST (sink), event));
      }
      gst_object_unref (pad);
    }
    gst_event_unref (event);
  }
}

static gboolean
gst_clapper_sink_propose_allocation (GstBaseSink *bsink, GstQuery *query)
{
  GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink);
  GstClapperImporter *importer = NULL;
  GstCaps *caps;
  GstVideoInfo info;
  guint size, min_buffers;
  gboolean need_pool;

  gst_query_parse_allocation (query, &caps, &need_pool);

  if (!caps) {
    GST_DEBUG_OBJECT (self, "No caps specified");
    return FALSE;
  }

  if (!gst_video_info_from_caps (&info, caps)) {
    GST_DEBUG_OBJECT (self, "Invalid caps specified");
    return FALSE;
  }

  GST_CLAPPER_SINK_LOCK (self);
  if (self->importer)
    importer = gst_object_ref (self->importer);
  GST_CLAPPER_SINK_UNLOCK (self);

  if (!importer) {
    GST_DEBUG_OBJECT (self, "No importer to propose allocation");
    return FALSE;
  }

  /* Normal size of a frame */
  size = GST_VIDEO_INFO_SIZE (&info);

  /* We keep around current buffer and a pending one */
  min_buffers = 3;

  if (need_pool) {
    GstBufferPool *pool;
    GstStructure *config = NULL;

    GST_DEBUG_OBJECT (self, "Need to create buffer pool");
    pool = gst_clapper_importer_create_pool (importer, &config);

    if (pool) {
      /* If we did not get config, use default one */
      if (!config)
        config = gst_buffer_pool_get_config (pool);

      gst_buffer_pool_config_set_params (config, caps, size, min_buffers, 0);

      if (!gst_buffer_pool_set_config (pool, config)) {
        gst_object_unref (pool);
        gst_object_unref (importer);

        GST_ERROR_OBJECT (self, "Failed to set config");
        return FALSE;
      }

      gst_query_add_allocation_pool (query, pool, size, min_buffers, 0);
      gst_object_unref (pool);
    } else if (config) {
      GST_WARNING_OBJECT (self, "Got config without a pool to apply it");
      gst_structure_free (config);
    }
  }

  gst_clapper_importer_add_allocation_metas (importer, query);
  gst_object_unref (importer);

  return TRUE;
}

static gboolean
gst_clapper_sink_query (GstBaseSink *bsink, GstQuery *query)
{
  GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink);
  gboolean res = FALSE;

  if (GST_QUERY_TYPE (query) == GST_QUERY_CONTEXT) {
    GST_CLAPPER_SINK_LOCK (self);
    res = gst_clapper_importer_loader_handle_context_query (self->loader, bsink, query);
    GST_CLAPPER_SINK_UNLOCK (self);
  }

  if (res)
    return TRUE;

  return GST_BASE_SINK_CLASS (parent_class)->query (bsink, query);
}

static gboolean
gst_clapper_sink_start_on_main (GstClapperSink *self)
{
  GtkWidget *widget;

  GST_CLAPPER_SINK_LOCK (self);

  /* Make sure widget is created */
  if (!(widget = gst_clapper_sink_get_widget (self))) {
    GST_CLAPPER_SINK_UNLOCK (self);

    return FALSE;
  }

  /* When no toplevel window, make our own */
  if (G_UNLIKELY (!gtk_widget_get_root (widget) && !self->window)) {
    GtkWidget *toplevel, *parent;
    GtkCssProvider *provider;
    gchar *win_title;

    if ((parent = gtk_widget_get_parent (widget))) {
      GtkWidget *temp_parent;

      while ((temp_parent = gtk_widget_get_parent (parent)))
        parent = temp_parent;
    }
    toplevel = (parent) ? parent : widget;

    self->window = (GtkWindow *) gtk_window_new ();
    gtk_widget_add_css_class (GTK_WIDGET (self->window), WINDOW_CSS_CLASS_NAME);

    provider = gtk_css_provider_new ();
    gtk_css_provider_load_from_data (provider,
        "." WINDOW_CSS_CLASS_NAME " { background: none; }", -1);
    gtk_style_context_add_provider_for_display (
        gdk_display_get_default (), GTK_STYLE_PROVIDER (provider),
        GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    g_object_unref (provider);

    win_title = g_strdup_printf ("Clapper Sink - GTK %u.%u.%u Window",
        gtk_get_major_version (),
        gtk_get_minor_version (),
        gtk_get_micro_version ());

    /* Set some common default size, adding stock headerbar height
     * to it in order to display 4:3 aspect video widget */
    gtk_window_set_default_size (self->window, 640, 480 + 37);
    gtk_window_set_title (self->window, win_title);
    gtk_window_set_child (self->window, toplevel);

    g_free (win_title);

    self->window_destroy_id = g_signal_connect (self->window,
        "destroy", G_CALLBACK (window_destroy_cb), self);

    GST_INFO_OBJECT (self, "Presenting window");
    gtk_window_present (self->window);
  }

  GST_CLAPPER_SINK_UNLOCK (self);

  return TRUE;
}

static gboolean
gst_clapper_sink_start (GstBaseSink *bsink)
{
  GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink);

  GST_INFO_OBJECT (self, "Start");

  if (G_UNLIKELY (!(! !gst_gtk_invoke_on_main ((GThreadFunc) (GCallback)
      gst_clapper_sink_start_on_main, self)))) {
    GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND,
        ("GtkWidget could not be created"), (NULL));

    return FALSE;
  }

  return TRUE;
}

static gboolean
gst_clapper_sink_stop_on_main (GstClapperSink *self)
{
  GtkWindow *window = NULL;

  GST_CLAPPER_SINK_LOCK (self);
  if (self->window)
    window = g_object_ref (self->window);
  GST_CLAPPER_SINK_UNLOCK (self);

  if (window) {
    gtk_window_destroy (window);
    g_object_unref (window);
  }

  return TRUE;
}

static gboolean
gst_clapper_sink_stop (GstBaseSink *bsink)
{
  GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink);
  gboolean has_window;

  GST_INFO_OBJECT (self, "Stop");

  GST_CLAPPER_SINK_LOCK (self);
  has_window = (self->window != NULL);
  GST_CLAPPER_SINK_UNLOCK (self);

  if (G_UNLIKELY (has_window)) {
    return (! !gst_gtk_invoke_on_main ((GThreadFunc) (GCallback)
        gst_clapper_sink_stop_on_main, self));
  }

  return TRUE;
}

static gboolean
gst_clapper_sink_event (GstBaseSink *bsink, GstEvent *event)
{
  GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink);
  GstTagList *taglist;
  GstVideoOrientationMethod orientation;

  switch (GST_EVENT_TYPE (event)) {
    case GST_EVENT_TAG:
      gst_event_parse_tag (event, &taglist);

      if (gst_video_orientation_from_tag (taglist, &orientation)) {
        GST_CLAPPER_SINK_LOCK (self);
        self->stream_orientation = orientation;
        if (self->rotation_mode == GST_VIDEO_ORIENTATION_AUTO)
          gst_clapper_paintable_set_rotation (self->paintable, orientation);
        GST_CLAPPER_SINK_UNLOCK (self);
      }
      break;
    default:
      break;
  }

  return GST_BASE_SINK_CLASS (parent_class)->event (bsink, event);
}

static GstStateChangeReturn
gst_clapper_sink_change_state (GstElement *element, GstStateChange transition)
{
  GstClapperSink *self = GST_CLAPPER_SINK_CAST (element);

  GST_DEBUG_OBJECT (self, "Changing state: %s => %s",
      gst_element_state_get_name (GST_STATE_TRANSITION_CURRENT (transition)),
      gst_element_state_get_name (GST_STATE_TRANSITION_NEXT (transition)));

  switch (transition) {
    case GST_STATE_CHANGE_NULL_TO_READY:
      /* Reset stream_orientation */
      GST_CLAPPER_SINK_LOCK (self);
      self->stream_orientation = GST_VIDEO_ORIENTATION_IDENTITY;
      if (self->rotation_mode == GST_VIDEO_ORIENTATION_AUTO)
        gst_clapper_paintable_set_rotation (self->paintable, self->stream_orientation);
      GST_CLAPPER_SINK_UNLOCK (self);
      break;
    case GST_STATE_CHANGE_PAUSED_TO_READY:
      GST_CLAPPER_SINK_LOCK (self);
      if (!self->keep_last_frame && self->importer) {
        gst_clapper_importer_set_buffer (self->importer, NULL);
        gst_clapper_paintable_queue_draw (self->paintable);
      }
      GST_CLAPPER_SINK_UNLOCK (self);
      break;
    default:
      break;
  }

  return GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
}

static void
gst_clapper_sink_get_times (GstBaseSink *bsink, GstBuffer *buffer,
    GstClockTime *start, GstClockTime *end)
{
  if (!GST_BUFFER_TIMESTAMP_IS_VALID (buffer))
    return;

  *start = GST_BUFFER_TIMESTAMP (buffer);

  if (GST_BUFFER_DURATION_IS_VALID (buffer)) {
    *end = *start + GST_BUFFER_DURATION (buffer);
  } else {
    GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink);
    gint fps_n, fps_d;

    GST_CLAPPER_SINK_LOCK (self);
    fps_n = GST_VIDEO_INFO_FPS_N (&self->v_info);
    fps_d = GST_VIDEO_INFO_FPS_D (&self->v_info);
    GST_CLAPPER_SINK_UNLOCK (self);

    if (fps_n > 0)
      *end = *start + gst_util_uint64_scale_int (GST_SECOND, fps_d, fps_n);
  }
}

static GstCaps *
gst_clapper_sink_get_caps (GstBaseSink *bsink, GstCaps *filter)
{
  GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink);
  GstCaps *result, *tmp;

  tmp = gst_clapper_importer_loader_make_actual_caps (self->loader);

  if (filter) {
    GST_DEBUG ("Intersecting with filter caps: %" GST_PTR_FORMAT, filter);
    result = gst_caps_intersect_full (filter, tmp, GST_CAPS_INTERSECT_FIRST);
    gst_caps_unref (tmp);
  } else {
    result = tmp;
  }
  GST_DEBUG ("Returning caps: %" GST_PTR_FORMAT, result);

  return result;
}

static gboolean
gst_clapper_sink_set_caps (GstBaseSink *bsink, GstCaps *caps)
{
  GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink);

  GST_INFO_OBJECT (self, "Set caps: %" GST_PTR_FORMAT, caps);
  GST_CLAPPER_SINK_LOCK (self);

  if (G_UNLIKELY (!self->widget)) {
    GST_CLAPPER_SINK_UNLOCK (self);
    GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND,
        ("Output widget was destroyed"), (NULL));

    return FALSE;
  }

  if (!gst_clapper_importer_loader_find_importer_for_caps (self->loader, caps, &self->importer)) {
    GST_CLAPPER_SINK_UNLOCK (self);
    GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND,
        ("No importer for given caps found"), (NULL));

    return FALSE;
  }
  gst_clapper_paintable_set_importer (self->paintable, self->importer);

  GST_CLAPPER_SINK_UNLOCK (self);

  return GST_BASE_SINK_CLASS (parent_class)->set_caps (bsink, caps);
}

static gboolean
gst_clapper_sink_set_info (GstVideoSink *vsink, GstCaps *caps, const GstVideoInfo *info)
{
  GstClapperSink *self = GST_CLAPPER_SINK_CAST (vsink);
  gboolean res;

  GST_CLAPPER_SINK_LOCK (self);

  self->v_info = *info;
  GST_DEBUG_OBJECT (self, "Video info changed");

  res = gst_clapper_paintable_set_video_info (self->paintable, info);
  GST_CLAPPER_SINK_UNLOCK (self);

  return res;
}

static GstFlowReturn
gst_clapper_sink_show_frame (GstVideoSink *vsink, GstBuffer *buffer)
{
  GstClapperSink *self = GST_CLAPPER_SINK_CAST (vsink);

  GST_TRACE ("Got %" GST_PTR_FORMAT, buffer);
  GST_CLAPPER_SINK_LOCK (self);

  if (G_UNLIKELY (!self->widget)) {
    GST_CLAPPER_SINK_UNLOCK (self);
    GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND,
        ("Output widget was destroyed"), (NULL));

    return GST_FLOW_ERROR;
  }

  gst_clapper_importer_set_buffer (self->importer, buffer);
  gst_clapper_paintable_queue_draw (self->paintable);

  GST_CLAPPER_SINK_UNLOCK (self);

  return GST_FLOW_OK;
}

static void
gst_clapper_sink_init (GstClapperSink *self)
{
  GObjectClass *gobject_class;

  gobject_class = (GObjectClass *) GST_CLAPPER_SINK_GET_CLASS (self);

  /* HACK: install here instead of class init to avoid GStreamer
   * plugin scanner GObject type conflicts with older GTK versions */
  if (!g_object_class_find_property (gobject_class, "widget")) {
    g_object_class_install_property (gobject_class, PROP_WIDGET,
        g_param_spec_object ("widget", "GTK Widget",
            "The GtkWidget to place in the widget hierarchy",
            GTK_TYPE_WIDGET, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
  }

  self->force_aspect_ratio = DEFAULT_FORCE_ASPECT_RATIO;
  self->par_n = DEFAULT_PAR_N;
  self->par_d = DEFAULT_PAR_D;
  self->keep_last_frame = DEFAULT_KEEP_LAST_FRAME;
  self->rotation_mode = DEFAULT_ROTATION;

  g_mutex_init (&self->lock);
  gst_video_info_init (&self->v_info);

  self->paintable = gst_clapper_paintable_new ();
  self->loader = gst_clapper_importer_loader_new ();
}

static void
gst_clapper_sink_dispose (GObject *object)
{
  GstClapperSink *self = GST_CLAPPER_SINK_CAST (object);

  GST_CLAPPER_SINK_LOCK (self);

  window_clear_no_lock (self);
  widget_clear_no_lock (self);

  g_clear_object (&self->paintable);
  gst_clear_object (&self->importer);

  GST_CLAPPER_SINK_UNLOCK (self);

  GST_CALL_PARENT (G_OBJECT_CLASS, dispose, (object));
}

static void
gst_clapper_sink_finalize (GObject *object)
{
  GstClapperSink *self = GST_CLAPPER_SINK_CAST (object);

  GST_TRACE ("Finalize");

  gst_clear_object (&self->loader);
  g_mutex_clear (&self->lock);

  GST_CALL_PARENT (G_OBJECT_CLASS, finalize, (object));
}

static void
gst_clapper_sink_class_init (GstClapperSinkClass *klass)
{
  GstPadTemplate *sink_pad_templ;

  GObjectClass *gobject_class = (GObjectClass *) klass;
  GstElementClass *gstelement_class = (GstElementClass *) klass;
  GstBaseSinkClass *gstbasesink_class = (GstBaseSinkClass *) klass;
  GstVideoSinkClass *gstvideosink_class = (GstVideoSinkClass *) klass;

  GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappersink", 0,
      "Clapper Sink");

  gobject_class->get_property = gst_clapper_sink_get_property;
  gobject_class->set_property = gst_clapper_sink_set_property;
  gobject_class->dispose = gst_clapper_sink_dispose;
  gobject_class->finalize = gst_clapper_sink_finalize;

  g_object_class_install_property (gobject_class, PROP_FORCE_ASPECT_RATIO,
      g_param_spec_boolean ("force-aspect-ratio", "Force aspect ratio",
          "When enabled, scaling will respect original aspect ratio",
          DEFAULT_FORCE_ASPECT_RATIO,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_PIXEL_ASPECT_RATIO,
      gst_param_spec_fraction ("pixel-aspect-ratio", "Pixel Aspect Ratio",
          "The pixel aspect ratio of the device",
          DEFAULT_PAR_N, DEFAULT_PAR_D,
          G_MAXINT, 1, 1, 1, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_KEEP_LAST_FRAME,
      g_param_spec_boolean ("keep-last-frame", "Keep last frame",
          "Keep showing last video frame after playback instead of black screen",
          DEFAULT_KEEP_LAST_FRAME,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_ROTATE_METHOD,
      g_param_spec_enum ("rotate-method", "Rotate Method",
          "Rotate method to use",
          GST_TYPE_VIDEO_ORIENTATION_METHOD, DEFAULT_ROTATION,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  gstelement_class->change_state = gst_clapper_sink_change_state;

  gstbasesink_class->get_caps = gst_clapper_sink_get_caps;
  gstbasesink_class->set_caps = gst_clapper_sink_set_caps;
  gstbasesink_class->get_times = gst_clapper_sink_get_times;
  gstbasesink_class->propose_allocation = gst_clapper_sink_propose_allocation;
  gstbasesink_class->query = gst_clapper_sink_query;
  gstbasesink_class->start = gst_clapper_sink_start;
  gstbasesink_class->stop = gst_clapper_sink_stop;
  gstbasesink_class->event = gst_clapper_sink_event;

  gstvideosink_class->set_info = gst_clapper_sink_set_info;
  gstvideosink_class->show_frame = gst_clapper_sink_show_frame;

  gst_element_class_set_static_metadata (gstelement_class,
      "Clapper video sink",
      "Sink/Video", "A GTK4 video sink used by Clapper media player",
      "Rafał Dzięgiel <rafostar.github@gmail.com>");

  sink_pad_templ = gst_clapper_importer_loader_make_sink_pad_template ();
  gst_element_class_add_pad_template (gstelement_class, sink_pad_templ);
}

/*
 * GstNavigationInterface
 */
static void
gst_clapper_sink_navigation_interface_init (GstNavigationInterface *iface)
{
  /* TODO: Port to "send_event_simple" once we depend on GStreamer 1.22 */
  iface->send_event = gst_clapper_sink_navigation_send_event;
}
