/* GStreamer
 *  Copyright (C) <2024> V-Nova International Limited
 *
 * 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 <math.h>

#include <gst/codecparsers/gstlcevcmeta.h>

#include <lcevc_eil.h>

#include "gstlcevcencoder.h"
#include "gstlcevcencoderutils.h"

GST_DEBUG_CATEGORY_STATIC (lcevcencoder_debug);
#define GST_CAT_DEFAULT (lcevcencoder_debug)

#define DEFAULT_MIN_BITRATE 0
#define DEFAULT_MAX_BITRATE 2048000
#define DEFAULT_BITRATE 0
#define DEFAULT_SEI_LCEVC TRUE
#define DEFAULT_MIN_GOP_LENGTH -2
#define DEFAULT_GOP_LENGTH -2
#define DEFAULT_DEBUG FALSE

/* The max number of frames the encoder can receive without encoding a frame */
#define MAX_DELAYED_FRAMES 65

struct GstEILContext_
{
  grefcount ref;
  EILContext context;
};
typedef struct GstEILContext_ GstEILContext;

static void
log_callback (void *userdata, int32_t level, const char *msg)
{
  GstLcevcEncoder *eil = GST_LCEVC_ENCODER (userdata);
  gchar *new_msg = NULL;

  if (strlen (msg) <= 1)
    return;

  /* Remove \n from msg */
  new_msg = g_strdup (msg);
  new_msg[strlen (msg) - 1] = '\0';

  switch (level) {
    case EIL_LL_Error:
      GST_ERROR_OBJECT (eil, "EIL: %s", new_msg);
      break;
    case EIL_LL_Warning:
      GST_WARNING_OBJECT (eil, "EIL: %s", new_msg);
      break;
    case EIL_LL_Info:
      GST_INFO_OBJECT (eil, "EIL: %s", new_msg);
      break;
    case EIL_LL_Debug:
      GST_DEBUG_OBJECT (eil, "EIL: %s", new_msg);
      break;
    case EIL_LL_Verbose:
      GST_TRACE_OBJECT (eil, "EIL: %s", new_msg);
      break;
    default:
      break;
  }

  g_clear_pointer (&new_msg, g_free);
}

static GstEILContext *
gst_eil_context_new (GstLcevcEncoder * eil, const gchar * plugin_name,
    gboolean debug)
{
  GstEILContext *ctx = g_new0 (GstEILContext, 1);
  EILOpenSettings settings;
  EILReturnCode rc;

  g_return_val_if_fail (plugin_name, NULL);

  /* Initialize settings to default values */
  rc = EIL_OpenSettingsDefault (&settings);
  if (rc != EIL_RC_Success)
    return NULL;

  /* Set settings */
  settings.base_encoder = plugin_name;
  if (debug) {
    settings.log_callback = log_callback;
    settings.log_userdata = eil;
  }

  /* Open EIL context */
  if ((rc = EIL_Open (&settings, &ctx->context)) != EIL_RC_Success)
    return NULL;

  /* Init refcount */
  g_ref_count_init (&ctx->ref);
  return ctx;
}

static void
gst_eil_context_unref (GstEILContext * ctx)
{
  if (g_ref_count_dec (&ctx->ref)) {
    EIL_Close (ctx->context);
    g_free (ctx);
  }
}

static GstEILContext *
gst_eil_context_ref (GstEILContext * ctx)
{
  g_ref_count_inc (&ctx->ref);
  return ctx;
}

typedef struct
{
  GstEILContext *ctx;
  EILOutput *output;
} OutputData;

static OutputData *
output_data_new (GstEILContext * ctx, EILOutput * output)
{
  OutputData *data = g_new0 (OutputData, 1);
  data->ctx = gst_eil_context_ref (ctx);
  data->output = output;
  return data;
}

static void
output_data_free (gpointer p)
{
  OutputData *data = p;
  EIL_ReleaseOutput (data->ctx->context, data->output);
  g_clear_pointer (&data->ctx, gst_eil_context_unref);
  g_free (data);
}

enum
{
  PROP_0,
  PROP_PLUGIN_NAME,
  PROP_PLUGIN_PROPS,
  PROP_BITRATE,
  PROP_SEI_LCEVC,
  PROP_GOP_LENGTH,
  PROP_DEBUG,
};

typedef struct
{
  /* Props */
  gchar *plugin_name;
  gchar *plugin_props;
  guint bitrate;
  gboolean sei_lcevc;
  gint gop_length;
  gboolean debug;

  /* EIL */
  GstEILContext *ctx;
  GHashTable *plugin_props_spec;

  /* Input info */
  GstVideoInfo in_info;
  EILColourFormat in_format;
  EILFrameType in_frame_type;

  GstClockTime out_ts_offset;
} GstLcevcEncoderPrivate;

#define gst_lcevc_encoder_parent_class parent_class
G_DEFINE_ABSTRACT_TYPE_WITH_CODE (GstLcevcEncoder, gst_lcevc_encoder,
    GST_TYPE_VIDEO_ENCODER,
    G_ADD_PRIVATE (GstLcevcEncoder);
    GST_DEBUG_CATEGORY_INIT (lcevcencoder_debug, "lcevcencoder", 0,
        "lcevcencoder"));

static GstStaticPadTemplate gst_lcevc_encoder_sink_template =
GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE
        (GST_LCEVC_ENCODER_UTILS_SUPPORTED_FORMATS))
    );

static void
gst_lcevc_encoder_finalize (GObject * obj)
{
  GstLcevcEncoder *eil = GST_LCEVC_ENCODER (obj);
  GstLcevcEncoderPrivate *priv = gst_lcevc_encoder_get_instance_private (eil);

  g_clear_pointer (&priv->plugin_name, g_free);

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

static void
gst_lcevc_encoder_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstLcevcEncoder *self = GST_LCEVC_ENCODER (object);
  GstLcevcEncoderPrivate *priv = gst_lcevc_encoder_get_instance_private (self);

  switch (prop_id) {
    case PROP_PLUGIN_NAME:
      g_clear_pointer (&priv->plugin_name, g_free);
      priv->plugin_name = g_value_dup_string (value);
      break;
    case PROP_PLUGIN_PROPS:
      g_clear_pointer (&priv->plugin_props, g_free);
      priv->plugin_props = g_value_dup_string (value);
      break;
    case PROP_BITRATE:
      priv->bitrate = g_value_get_uint (value);
      break;
    case PROP_SEI_LCEVC:
      priv->sei_lcevc = g_value_get_boolean (value);
      break;
    case PROP_GOP_LENGTH:
      priv->gop_length = g_value_get_int (value);
      break;
    case PROP_DEBUG:
      priv->debug = g_value_get_boolean (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
gst_lcevc_encoder_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  GstLcevcEncoder *self = GST_LCEVC_ENCODER (object);
  GstLcevcEncoderPrivate *priv = gst_lcevc_encoder_get_instance_private (self);

  switch (prop_id) {
    case PROP_PLUGIN_NAME:
      g_value_set_string (value, priv->plugin_name);
      break;
    case PROP_PLUGIN_PROPS:
      g_value_set_string (value, priv->plugin_props);
      break;
    case PROP_BITRATE:
      g_value_set_uint (value, priv->bitrate);
      break;
    case PROP_SEI_LCEVC:
      g_value_set_boolean (value, priv->sei_lcevc);
      break;
    case PROP_GOP_LENGTH:
      g_value_set_int (value, priv->gop_length);
      break;
    case PROP_DEBUG:
      g_value_set_boolean (value, priv->debug);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static GHashTable *
get_plugin_props_spec (GstEILContext * ctx, const gchar * plugin_name)
{
  EILReturnCode rc;
  GHashTable *res;
  EILPropertyGroups groups;

  rc = EIL_QueryPropertyGroups (ctx->context, &groups);
  if (rc != EIL_RC_Success)
    return NULL;

  res = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);

  for (guint32 i = 0; i < groups.group_count; i++) {
    EILPropertyGroup *group = &groups.group[i];

    for (guint32 j = 0; j < group->property_count; j++) {
      EILProperty *property = &group->properties[j];
      g_hash_table_insert (res, g_strdup (property->name),
          GINT_TO_POINTER (property->type));
    }
  }

  return res;
}

static gboolean
open_eil_context (GstLcevcEncoder * eil)
{
  GstLcevcEncoderClass *klass = GST_LCEVC_ENCODER_GET_CLASS (eil);
  GstLcevcEncoderPrivate *priv = gst_lcevc_encoder_get_instance_private (eil);
  const gchar *plugin_name;

  g_return_val_if_fail (!priv->ctx, FALSE);

  /* Get the plugin name */
  if (priv->plugin_name)
    plugin_name = priv->plugin_name;
  else if (klass->get_eil_plugin_name)
    plugin_name = klass->get_eil_plugin_name (eil);
  else
    return FALSE;

  /* Create the EIL context */
  priv->ctx = gst_eil_context_new (eil, plugin_name, priv->debug);
  if (!priv->ctx)
    return FALSE;

  /* Get the plugin properties spec */
  priv->plugin_props_spec = get_plugin_props_spec (priv->ctx, plugin_name);
  if (!priv->plugin_props_spec) {
    g_clear_pointer (&priv->ctx, gst_eil_context_unref);
    return FALSE;
  }

  return TRUE;
}

static void
close_eil_context (GstLcevcEncoder * eil)
{
  GstLcevcEncoderPrivate *priv = gst_lcevc_encoder_get_instance_private (eil);

  /* Flush */
  EIL_Encode (priv->ctx->context, NULL);

  /* Clear properties spec */
  g_clear_pointer (&priv->plugin_props_spec, g_hash_table_unref);

  /* Clear context */
  g_clear_pointer (&priv->ctx, gst_eil_context_unref);
}

static gboolean
gst_lcevc_encoder_start (GstVideoEncoder * encoder)
{
  GstLcevcEncoder *eil = GST_LCEVC_ENCODER (encoder);
  GstLcevcEncoderPrivate *priv = gst_lcevc_encoder_get_instance_private (eil);

  /* Open EIL context */
  if (!open_eil_context (eil)) {
    GST_ELEMENT_ERROR (encoder, LIBRARY, INIT, (NULL),
        ("Couldn't initialize EIL context"));
    return FALSE;
  }

  /* Reset out TS offset */
  priv->out_ts_offset = 0;

  return TRUE;
}

static gboolean
gst_lcevc_encoder_stop (GstVideoEncoder * encoder)
{
  GstLcevcEncoder *eil = GST_LCEVC_ENCODER (encoder);

  /* Close EIL context */
  close_eil_context (eil);

  return TRUE;
}

static gboolean
try_parse_number (const char *value, double *parsed)
{
  char *endptr;

  /* Skip leading spaces */
  while (g_ascii_isspace (*value))
    value++;

  /* Parse number */
  *parsed = g_strtod (value, &endptr);

  /* Allow trailing spaces */
  while (g_ascii_isspace (*endptr))
    value++;

  /* Ceck no extra characters after number and spaces */
  if (*endptr != '\0')
    return FALSE;

  return TRUE;
}

static GString *
build_json_props (GstLcevcEncoder * eil)
{
  GstLcevcEncoderPrivate *priv = gst_lcevc_encoder_get_instance_private (eil);
  gchar *key_value;
  GString *res = g_string_new ("{");

  /* I/O props */
  if (priv->sei_lcevc) {
    /* separate_output=false */
    g_string_append_printf (res, "\"%s\"", "separate_output");
    g_string_append (res, ":");
    g_string_append (res, "false");
  } else {
    /* separate_output=true */
    g_string_append_printf (res, "\"%s\"", "separate_output");
    g_string_append (res, ": ");
    g_string_append (res, "true");
    g_string_append (res, ", ");
    /* output_format=raw */
    g_string_append_printf (res, "\"%s\"", "output_format");
    g_string_append (res, ": ");
    g_string_append_printf (res, "\"%s\"", "raw");
  }

  if (!priv->plugin_props)
    goto done;

  /* Plugin props */
  key_value = strtok (priv->plugin_props, ";");
  while (key_value != NULL) {
    const gchar *val_str = strchr (key_value, '=');
    if (val_str) {
      gsize key_size = val_str - key_value;
      if (key_size > 0) {
        gchar *key = g_strndup (key_value, key_size);
        gpointer p = g_hash_table_lookup (priv->plugin_props_spec, key);

        /* Add key */
        g_string_append (res, ", ");
        g_string_append_printf (res, "\"%s\"", key);
        g_string_append (res, ": ");

        /* Convert value to type defined by spec and add it, otherwise add the
         * value as it is */
        if (p) {
          EILPropertyType spec = GPOINTER_TO_INT (p);

          switch (spec) {
            case EIL_PT_Int8:
            case EIL_PT_Int16:
            case EIL_PT_Int32:
            case EIL_PT_Int64:{
              gint64 val = g_ascii_strtoll (val_str + 1, NULL, 10);
              g_string_append_printf (res, "%ld", val);
              break;
            }
            case EIL_PT_Uint8:
            case EIL_PT_Uint16:
            case EIL_PT_Uint32:
            case EIL_PT_Uint64:{
              guint64 val = g_ascii_strtoull (val_str + 1, NULL, 10);
              g_string_append_printf (res, "%lu", val);
              break;
            }
            case EIL_PT_Float:
            case EIL_PT_Double:{
              double val = g_ascii_strtod (val_str + 1, NULL);
              g_string_append_printf (res, "%f", val);
              break;
            }
            case EIL_PT_Boolean:{
              if (g_str_equal (val_str + 1, "TRUE") ||
                  g_str_equal (val_str + 1, "True") ||
                  g_str_equal (val_str + 1, "true") ||
                  g_str_equal (val_str + 1, "1"))
                g_string_append (res, "true");
              else
                g_string_append (res, "false");
              break;
            }
            case EIL_PT_String:
            default:
              g_string_append_printf (res, "\"%s\"", val_str + 1);
              break;
          }
        } else {
          double val;
          if (try_parse_number (val_str + 1, &val)) {
            if (val == ceil (val))
              g_string_append_printf (res, "%d", (gint) val);
            else
              g_string_append_printf (res, "%f", val);
          } else {
            g_string_append_printf (res, "\"%s\"", val_str + 1);
          }
        }

        g_free (key);
      } else {
        GST_WARNING_OBJECT (eil, "Key value pair %s does not have key",
            key_value);
        goto error;
      }
    } else {
      GST_WARNING_OBJECT (eil, "Key value pair %s does not have '=' char",
          key_value);
      goto error;
    }

    key_value = strtok (NULL, ";");
  }

done:
  res = g_string_append (res, "}");
  return res;

error:
  g_string_free (res, TRUE);
  return NULL;
}

static void
on_encoded_output (void *data, EILOutput * output)
{
  GstLcevcEncoder *eil = GST_LCEVC_ENCODER (data);
  GstLcevcEncoderPrivate *priv = gst_lcevc_encoder_get_instance_private (eil);
  GstVideoCodecFrame *frame;
  GstClockTime pts;

  if (!output) {
    GST_INFO_OBJECT (eil, "All EIL Pictures processed");
    return;
  }

  frame = output->user_data;
  pts = frame->input_buffer->pts;

  GST_INFO_OBJECT (eil, "Received output frame %ld with lcevc size %d", pts,
      output->lcevc_length);

  /* The EIL DTS can be negative, we need to do the conversion so it can be
   * stored in a GstClockTime (guint64). The EIL PTS can never be negative
   * because it is set using the input buffer PTS, which is a GstClockTime. */
  if (output->dts < 0 && priv->out_ts_offset == 0) {
    priv->out_ts_offset = -1 * output->dts;
    GST_INFO_OBJECT (eil, "Output DTS offset set to %ld", priv->out_ts_offset);
  }

  /* Created output buffer with output data */
  frame->output_buffer = gst_buffer_new_wrapped_full (0,
      (gpointer) output->data, output->data_length, 0, output->data_length,
      output_data_new (priv->ctx, output), output_data_free);
  frame->pts = priv->out_ts_offset + output->pts;
  frame->dts = priv->out_ts_offset + output->dts;

  /* Add LCEVC metadata to output buffer if present */
  if (output->lcevc_length > 0) {
    GstBuffer *lcevc_data = gst_buffer_new_memdup ((gpointer) output->lcevc,
        output->lcevc_length);
    gst_buffer_add_lcevc_meta (frame->output_buffer, lcevc_data);
    gst_buffer_unref (lcevc_data);
  }

  /* Set Delta unit flag if this is not a key frame */
  if (!output->keyframe)
    GST_BUFFER_FLAG_SET (frame->output_buffer, GST_BUFFER_FLAG_DELTA_UNIT);
  else
    GST_VIDEO_CODEC_FRAME_SET_SYNC_POINT (frame);

  gst_video_encoder_finish_frame (GST_VIDEO_ENCODER (eil), frame);
}

static void
gst_lcevc_encoder_set_latency (GstLcevcEncoder * eil, GstVideoInfo * info)
{
  GstLcevcEncoderPrivate *priv = gst_lcevc_encoder_get_instance_private (eil);
  gint delayed_frames;
  GstClockTime latency;

  /* The GOP affects the number of delayed frames */
  if (priv->gop_length == -2 || priv->gop_length == -1)
    delayed_frames = MAX_DELAYED_FRAMES;
  else
    delayed_frames = MIN (5 + priv->gop_length, MAX_DELAYED_FRAMES);

  latency =
      gst_util_uint64_scale_ceil (GST_SECOND * GST_VIDEO_INFO_FPS_D (info),
      delayed_frames, GST_VIDEO_INFO_FPS_N (info));
  gst_video_encoder_set_latency (GST_VIDEO_ENCODER (eil), latency, latency);

  GST_INFO_OBJECT (eil, "Updated latency to %" GST_TIME_FORMAT " (%d frames)",
      GST_TIME_ARGS (latency), delayed_frames);
}

static gboolean
gst_lcevc_encoder_set_format (GstVideoEncoder * encoder,
    GstVideoCodecState * state)
{
  GstLcevcEncoder *eil = GST_LCEVC_ENCODER (encoder);
  GstLcevcEncoderClass *klass = GST_LCEVC_ENCODER_GET_CLASS (eil);
  GstLcevcEncoderPrivate *priv = gst_lcevc_encoder_get_instance_private (eil);
  EILInitSettings settings;
  GString *properties_json;
  GstVideoInterlaceMode interlace_mode;
  EILReturnCode rc;
  GstCaps *outcaps;
  GstVideoCodecState *s;
  gint width = GST_VIDEO_INFO_WIDTH (&state->info);
  gint height = GST_VIDEO_INFO_HEIGHT (&state->info);

  /* Set input info, format and frame type */
  priv->in_info = state->info;
  priv->in_format =
      gst_lcevc_encoder_utils_get_color_format (GST_VIDEO_INFO_FORMAT
      (&state->info));
  interlace_mode = GST_VIDEO_INFO_INTERLACE_MODE (&state->info);
  switch (interlace_mode) {
    case GST_VIDEO_INTERLACE_MODE_PROGRESSIVE:
      priv->in_frame_type = EIL_FrameType_Progressive;
      break;
    case GST_VIDEO_INTERLACE_MODE_INTERLEAVED:
      priv->in_frame_type = EIL_FrameType_Interlaced;
      break;
    case GST_VIDEO_INTERLACE_MODE_FIELDS:
      priv->in_frame_type = EIL_FrameType_Field;
      break;
    default:
      GST_ELEMENT_ERROR (eil, STREAM, FORMAT, (NULL),
          ("Interlace mode %s not supported",
              gst_video_interlace_mode_to_string (interlace_mode)));
      return FALSE;
  }

  /* Init EIL Settings to default values */
  rc = EIL_InitSettingsDefault (&settings);
  if (rc != EIL_RC_Success) {
    GST_ELEMENT_ERROR (eil, LIBRARY, INIT, (NULL),
        ("Unabled to initialize EIL Settings"));
    return FALSE;
  }

  /* Set basic EIL settings */
  settings.color_format = priv->in_format;
  settings.memory_type = EIL_MT_Host;
  settings.width = width;
  settings.height = height;
  settings.fps_num = GST_VIDEO_INFO_FPS_N (&state->info);
  settings.fps_denom = GST_VIDEO_INFO_FPS_D (&state->info);
  settings.bitrate = priv->bitrate;
  settings.gop_length = priv->gop_length;
  settings.external_input = 1;

  /* Set properties JSON EIL setting */
  properties_json = build_json_props (eil);
  if (!properties_json) {
    GST_ELEMENT_ERROR (eil, RESOURCE, SETTINGS, (NULL),
        ("Could not parse plugin properties to JSON"));
    return FALSE;
  }
  settings.properties_json = properties_json->str;
  GST_INFO_OBJECT (eil, "Properties JSON: %s", properties_json->str);

  /* Initialize EIL */
  rc = EIL_Initialise (priv->ctx->context, &settings);
  g_string_free (properties_json, TRUE);
  if (rc != EIL_RC_Success)
    return FALSE;

  /* Get output caps */
  g_assert (klass->get_output_caps);
  outcaps = klass->get_output_caps (eil);
  if (!outcaps) {
    GST_ELEMENT_ERROR (eil, RESOURCE, NOT_FOUND, (NULL),
        ("Could not get output caps"));
    return FALSE;
  }

  /* Set width, height and pixel aspect ration.
   * The values from settings are updated to base width and height after
   * initialization.
   */
  if (width != settings.width || height != settings.height) {
    GST_VIDEO_INFO_WIDTH (&state->info) = settings.width;
    GST_VIDEO_INFO_HEIGHT (&state->info) = settings.height;
    /* If changed, the new width and height values are always half of what they
     * used to be, so update the pixel aspect ratio accordingly */
    GST_VIDEO_INFO_PAR_N (&state->info) = width > settings.width ? 2 : 1;
    GST_VIDEO_INFO_PAR_D (&state->info) = height > settings.height ? 2 : 1;
    GST_INFO_OBJECT (eil,
        "Base resolution changed: w=%d h=%d -> w=%d h=%d (par_n=%d, par_d=%d)",
        width, height, settings.width, settings.height,
        GST_VIDEO_INFO_PAR_N (&state->info),
        GST_VIDEO_INFO_PAR_D (&state->info));
  }

  /* Set output state */
  s = gst_video_encoder_set_output_state (encoder, outcaps, state);
  if (!s) {
    GST_ELEMENT_ERROR (eil, STREAM, FORMAT, (NULL),
        ("Could not set output state"));
    return FALSE;
  }

  /* Set output callback */
  EIL_SetOnEncodedCallback (priv->ctx->context, eil, on_encoded_output);

  /* Update latency */
  gst_lcevc_encoder_set_latency (eil, &s->info);

  gst_video_codec_state_unref (s);
  return TRUE;
}

static gboolean
gst_lcevc_encoder_sink_event (GstVideoEncoder * encoder, GstEvent * event)
{
  GstLcevcEncoder *eil = GST_LCEVC_ENCODER (encoder);
  GstLcevcEncoderPrivate *priv = gst_lcevc_encoder_get_instance_private (eil);

  switch (GST_EVENT_TYPE (event)) {
    case GST_EVENT_EOS:
      /* Flush on EOS */
      GST_INFO_OBJECT (eil, "EOS received, flushing encoder");
      EIL_Encode (priv->ctx->context, NULL);
      break;

    default:
      break;
  }

  return GST_VIDEO_ENCODER_CLASS (parent_class)->sink_event (encoder, event);
}

static GstFlowReturn
gst_lcevc_encoder_handle_frame (GstVideoEncoder * encoder,
    GstVideoCodecFrame * frame)
{
  GstLcevcEncoder *eil = GST_LCEVC_ENCODER (encoder);
  GstLcevcEncoderPrivate *priv = gst_lcevc_encoder_get_instance_private (eil);
  GstClockTime pts = frame->input_buffer->pts;
  GstVideoFrame video_frame = { 0, };
  EILPicture picture;

  /* Map the input buffer */
  if (!gst_video_frame_map (&video_frame, &priv->in_info, frame->input_buffer,
          GST_MAP_READ)) {
    GST_ELEMENT_ERROR (eil, STREAM, ENCODE, (NULL),
        ("Could not map input buffer %ld", pts));
    goto error;
  }

  /* Initialize EIL picture */
  if (EIL_InitPictureDefault (&picture) != EIL_RC_Success) {
    GST_ELEMENT_ERROR (eil, STREAM, ENCODE, (NULL),
        ("Could not initialize EIL picture %ld", pts));
    goto error;
  }

  /* Set frame values on EIL picture */
  if (!gst_lcevc_encoder_utils_init_eil_picture (priv->in_frame_type,
          &video_frame, pts, &picture)) {
    GST_ELEMENT_ERROR (eil, STREAM, ENCODE, (NULL),
        ("Could not set frame values on EIL picture %ld", pts));
    goto error;
  }

  /* Set input frame as user data. This will be set in the encoded output as
   * user data, which will help us getting the associated frame */
  picture.user_data = frame;

  /* Encode frame */
  if (EIL_Encode (priv->ctx->context, &picture) != EIL_RC_Success) {
    GST_ELEMENT_ERROR (eil, STREAM, ENCODE, (NULL),
        ("Could not encode input frame %ld", pts));
    goto error;
  }

  GST_INFO_OBJECT (eil, "Sent input frame %ld", pts);

  gst_video_frame_unmap (&video_frame);
  return GST_FLOW_OK;

error:
  gst_video_frame_unmap (&video_frame);
  return GST_FLOW_ERROR;
}

static void
gst_lcevc_encoder_class_init (GstLcevcEncoderClass * klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  GstElementClass *element_class = GST_ELEMENT_CLASS (klass);
  GstVideoEncoderClass *video_encoder_class = GST_VIDEO_ENCODER_CLASS (klass);

  gst_element_class_add_static_pad_template (element_class,
      &gst_lcevc_encoder_sink_template);

  gst_type_mark_as_plugin_api (GST_TYPE_LCEVC_ENCODER, 0);

  gobject_class->finalize = gst_lcevc_encoder_finalize;
  gobject_class->set_property = gst_lcevc_encoder_set_property;
  gobject_class->get_property = gst_lcevc_encoder_get_property;

  video_encoder_class->start = gst_lcevc_encoder_start;
  video_encoder_class->stop = gst_lcevc_encoder_stop;
  video_encoder_class->set_format = gst_lcevc_encoder_set_format;
  video_encoder_class->sink_event = gst_lcevc_encoder_sink_event;
  video_encoder_class->handle_frame = gst_lcevc_encoder_handle_frame;

  g_object_class_install_property (gobject_class, PROP_PLUGIN_NAME,
      g_param_spec_string ("plugin-name", "Plugin Name",
          "The name of the EIL plugin to use (NULL = auto)",
          NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_PLUGIN_PROPS,
      g_param_spec_string ("plugin-props", "Plugin Props",
          "A semi-colon list of key value pair properties for the EIL plugin",
          NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_BITRATE,
      g_param_spec_uint ("bitrate", "Bitrate",
          "Bitrate in kbit/sec (0 = auto)",
          DEFAULT_MIN_BITRATE, DEFAULT_MAX_BITRATE, DEFAULT_BITRATE,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_SEI_LCEVC,
      g_param_spec_boolean ("sei-lcevc", "SEI LCEVC",
          "Whether to have LCEVC data as SEI (in the video stream) or not (attached to buffers as GstMeta)",
          DEFAULT_SEI_LCEVC, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_GOP_LENGTH,
      g_param_spec_int ("gop-length", "GOP Length",
          "The group of pictures length (-2 = auto, -1 = infinite, 0 = intra-only)",
          DEFAULT_MIN_GOP_LENGTH, INT_MAX, DEFAULT_GOP_LENGTH,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_DEBUG,
      g_param_spec_boolean ("debug", "Debug",
          "Whether to show EIL SDK logs or not",
          DEFAULT_DEBUG, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
}

static void
gst_lcevc_encoder_init (GstLcevcEncoder * eil)
{
  GstLcevcEncoderPrivate *priv = gst_lcevc_encoder_get_instance_private (eil);

  /* Props */
  priv->plugin_name = NULL;
  priv->plugin_props = NULL;
  priv->bitrate = DEFAULT_BITRATE;
  priv->sei_lcevc = DEFAULT_SEI_LCEVC;
  priv->gop_length = DEFAULT_GOP_LENGTH;
}