/* GStreamer
 * Copyright (C) 2023 Seungha Yang <seungha@centricular.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 "gstdwritetimeoverlay.h"
#include <mutex>
#include <strsafe.h>

GST_DEBUG_CATEGORY_STATIC (dwrite_time_overlay_debug);
#define GST_CAT_DEFAULT dwrite_time_overlay_debug

typedef enum
{
  GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_TIME,
  GST_DWRITE_TIME_OVERLAY_TIME_LINE_STREAM_TIME,
  GST_DWRITE_TIME_OVERLAY_TIME_LINE_RUNNING_TIME,
  GST_DWRITE_TIME_OVERLAY_TIME_LINE_TIME_CODE,
  GST_DWRITE_TIME_OVERLAY_TIME_LINE_ELAPSED_RUNNING_TIME,
  GST_DWRITE_TIME_OVERLAY_TIME_LINE_REFERENCE_TIMESTAMP,
  GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_COUNT,
  GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_OFFSET,
} GstDWriteTimeOverlayTimeLine;

#define GST_TYPE_DWrite_TIME_OVERLAY_TIME_LINE (gst_dwrite_time_overlay_time_line_type ())
static GType
gst_dwrite_time_overlay_time_line_type (void)
{
  static GType type;
  static const GEnumValue modes[] = {
    {GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_TIME,
        "buffer-time", "buffer-time"},
    {GST_DWRITE_TIME_OVERLAY_TIME_LINE_STREAM_TIME,
        "stream-time", "stream-time"},
    {GST_DWRITE_TIME_OVERLAY_TIME_LINE_RUNNING_TIME,
        "running-time", "running-time"},
    {GST_DWRITE_TIME_OVERLAY_TIME_LINE_TIME_CODE, "time-code", "time-code"},
    {GST_DWRITE_TIME_OVERLAY_TIME_LINE_ELAPSED_RUNNING_TIME,
        "elapsed-running-time", "elapsed-running-time"},
    {GST_DWRITE_TIME_OVERLAY_TIME_LINE_REFERENCE_TIMESTAMP,
        "reference-timestamp", "reference-timestamp"},
    {GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_COUNT,
        "buffer-count", "buffer-count"},
    {GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_OFFSET,
        "buffer-offset", "buffer-offset"},
    {0, nullptr, nullptr},
  };

  GST_DWRITE_CALL_ONCE_BEGIN {
    type = g_enum_register_static ("GstDWriteTimeOverlayTimeLine", modes);
  } GST_DWRITE_CALL_ONCE_END;

  return type;
}

enum
{
  PROP_0,
  PROP_TIME_LINE,
  PROP_SHOW_TIMES_AS_DATES,
  PROP_DATETIME_EPOCH,
  PROP_DATETIME_FORMAT,
  PROP_REFERENCE_TIMESTAMP_CAPS,
};

#define DEFAULT_TIME_LINE GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_TIME
#define DEFAULT_SHOW_TIMES_AS_DATES FALSE
#define DEFAULT_DATETIME_FORMAT "%F %T" /* YYYY-MM-DD hh:mm:ss */

static GstStaticCaps ntp_reference_timestamp_caps =
GST_STATIC_CAPS ("timestamp/x-ntp");

struct GstDWriteTimeOverlayPrivate
{
  GstDWriteTimeOverlayPrivate ()
  {
    datetime_epoch = g_date_time_new_utc (1900, 1, 1, 0, 0, 0);
    reference_timestamp_caps =
        gst_static_caps_get (&ntp_reference_timestamp_caps);
  }

   ~GstDWriteTimeOverlayPrivate ()
  {
    if (datetime_epoch)
      g_date_time_unref (datetime_epoch);
    gst_clear_caps (&reference_timestamp_caps);
  }

  std::mutex lock;
  GstDWriteTimeOverlayTimeLine time_line = DEFAULT_TIME_LINE;

  gboolean show_times_as_dates = DEFAULT_SHOW_TIMES_AS_DATES;
  guint64 buffer_count = 0;
  std::string datetime_format = DEFAULT_DATETIME_FORMAT;
  GDateTime *datetime_epoch;
  GstCaps *reference_timestamp_caps;
  GstClockTime first_running_time = GST_CLOCK_TIME_NONE;
};

struct _GstDWriteTimeOverlay
{
  GstDWriteBaseOverlay parent;

  GstDWriteTimeOverlayPrivate *priv;
};

static void gst_dwrite_time_overlay_finalize (GObject * object);
static void gst_dwrite_time_overlay_set_property (GObject * object,
    guint prop_id, const GValue * value, GParamSpec * pspec);
static void gst_dwrite_time_overlay_get_property (GObject * object,
    guint prop_id, GValue * value, GParamSpec * pspec);
static gboolean gst_dwrite_time_overlay_sink_event (GstBaseTransform * trans,
    GstEvent * event);
static gboolean gst_dwrite_time_overlay_start (GstBaseTransform * overlay);
static WString gst_dwrite_time_overlay_get_text (GstDWriteBaseOverlay * overlay,
    const WString & default_text, GstBuffer * buffer);

#define gst_dwrite_time_overlay_parent_class parent_class
G_DEFINE_TYPE (GstDWriteTimeOverlay, gst_dwrite_time_overlay,
    GST_TYPE_DWRITE_BASE_OVERLAY);

static void
gst_dwrite_time_overlay_class_init (GstDWriteTimeOverlayClass * klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GstElementClass *element_class = GST_ELEMENT_CLASS (klass);
  GstBaseTransformClass *trans_class = GST_BASE_TRANSFORM_CLASS (klass);
  GstDWriteBaseOverlayClass *overlay_class =
      GST_DWRITE_BASE_OVERLAY_CLASS (klass);

  object_class->finalize = gst_dwrite_time_overlay_finalize;
  object_class->set_property = gst_dwrite_time_overlay_set_property;
  object_class->get_property = gst_dwrite_time_overlay_get_property;

  g_object_class_install_property (object_class, PROP_TIME_LINE,
      g_param_spec_enum ("time-mode", "Time Mode", "What time to show",
          GST_TYPE_DWrite_TIME_OVERLAY_TIME_LINE, DEFAULT_TIME_LINE,
          (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
  g_object_class_install_property (object_class, PROP_DATETIME_EPOCH,
      g_param_spec_boxed ("datetime-epoch", "Datetime Epoch",
          "When showing times as dates, the initial date from which time "
          "is counted, if not specified prime epoch is used (1900-01-01)",
          G_TYPE_DATE_TIME,
          (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
  g_object_class_install_property (object_class, PROP_DATETIME_FORMAT,
      g_param_spec_string ("datetime-format", "Datetime Format",
          "When showing times as dates, the format to render date and time in",
          DEFAULT_DATETIME_FORMAT,
          (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
  g_object_class_install_property (object_class, PROP_SHOW_TIMES_AS_DATES,
      g_param_spec_boolean ("show-times-as-dates", "Show times as dates",
          "Whether to display times, counted from datetime-epoch, as dates",
          DEFAULT_SHOW_TIMES_AS_DATES,
          (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
  g_object_class_install_property (object_class, PROP_REFERENCE_TIMESTAMP_CAPS,
      g_param_spec_boxed ("reference-timestamp-caps",
          "Reference Timestamp Caps",
          "Caps to use for the reference timestamp time mode",
          GST_TYPE_CAPS,
          (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));

  gst_element_class_set_static_metadata (element_class,
      "DirectWrite Time Overlay", "Filter/Editor/Video",
      "Overlays buffer time stamps on a video stream",
      "Seungha Yang <seungha@centricular.com>");

  trans_class->sink_event =
      GST_DEBUG_FUNCPTR (gst_dwrite_time_overlay_sink_event);

  trans_class->start = GST_DEBUG_FUNCPTR (gst_dwrite_time_overlay_start);
  overlay_class->get_text =
      GST_DEBUG_FUNCPTR (gst_dwrite_time_overlay_get_text);

  GST_DEBUG_CATEGORY_INIT (dwrite_time_overlay_debug,
      "dwritetimeoverlay", 0, "dwritetimeoverlay");

  gst_type_mark_as_plugin_api (GST_TYPE_DWrite_TIME_OVERLAY_TIME_LINE,
      (GstPluginAPIFlags) 0);
}

static void
gst_dwrite_time_overlay_init (GstDWriteTimeOverlay * self)
{
  g_object_set (self, "text-alignment", DWRITE_TEXT_ALIGNMENT_LEADING,
      "paragraph-alignment", DWRITE_PARAGRAPH_ALIGNMENT_NEAR, nullptr);

  self->priv = new GstDWriteTimeOverlayPrivate ();
}

static void
gst_dwrite_time_overlay_finalize (GObject * object)
{
  GstDWriteTimeOverlay *self = GST_DWRITE_TIME_OVERLAY (object);

  delete self->priv;

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

static void
gst_dwrite_time_overlay_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstDWriteTimeOverlay *self = GST_DWRITE_TIME_OVERLAY (object);
  GstDWriteTimeOverlayPrivate *priv = self->priv;
  std::lock_guard < std::mutex > lk (priv->lock);

  switch (prop_id) {
    case PROP_TIME_LINE:
      priv->time_line = (GstDWriteTimeOverlayTimeLine) g_value_get_enum (value);
      break;
    case PROP_SHOW_TIMES_AS_DATES:
      priv->show_times_as_dates = g_value_get_boolean (value);
      break;
    case PROP_DATETIME_EPOCH:
      g_date_time_unref (priv->datetime_epoch);
      priv->datetime_epoch = (GDateTime *) g_value_dup_boxed (value);
      break;
    case PROP_DATETIME_FORMAT:
    {
      const gchar *format = g_value_get_string (value);
      if (format)
        priv->datetime_format = format;
      else
        priv->datetime_format = DEFAULT_DATETIME_FORMAT;
      break;
    }
    case PROP_REFERENCE_TIMESTAMP_CAPS:
      gst_clear_caps (&priv->reference_timestamp_caps);
      priv->reference_timestamp_caps = (GstCaps *) g_value_dup_boxed (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
gst_dwrite_time_overlay_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  GstDWriteTimeOverlay *self = GST_DWRITE_TIME_OVERLAY (object);
  GstDWriteTimeOverlayPrivate *priv = self->priv;
  std::lock_guard < std::mutex > lk (priv->lock);

  switch (prop_id) {
    case PROP_TIME_LINE:
      g_value_set_enum (value, priv->time_line);
      break;
    case PROP_SHOW_TIMES_AS_DATES:
      g_value_set_boolean (value, priv->show_times_as_dates);
      break;
    case PROP_DATETIME_EPOCH:
      g_value_set_boxed (value, priv->datetime_epoch);
      break;
    case PROP_DATETIME_FORMAT:
      g_value_set_string (value, priv->datetime_format.c_str ());
      break;
    case PROP_REFERENCE_TIMESTAMP_CAPS:
      g_value_set_boxed (value, priv->reference_timestamp_caps);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static gboolean
gst_dwrite_time_overlay_start (GstBaseTransform * trans)
{
  GstDWriteTimeOverlay *self = GST_DWRITE_TIME_OVERLAY (trans);
  GstDWriteTimeOverlayPrivate *priv = self->priv;

  priv->first_running_time = GST_CLOCK_TIME_NONE;
  priv->buffer_count = 0;

  return GST_BASE_TRANSFORM_CLASS (parent_class)->start (trans);
}

static gboolean
gst_dwrite_time_overlay_sink_event (GstBaseTransform * trans, GstEvent * event)
{
  GstDWriteTimeOverlay *self = GST_DWRITE_TIME_OVERLAY (trans);
  GstDWriteTimeOverlayPrivate *priv = self->priv;

  switch (GST_EVENT_TYPE (event)) {
    case GST_EVENT_FLUSH_STOP:
      priv->first_running_time = GST_CLOCK_TIME_NONE;
      break;
    default:
      break;
  }

  return GST_BASE_TRANSFORM_CLASS (parent_class)->sink_event (trans, event);
}

static WString
gst_dwrite_time_overlay_render_time (GstDWriteTimeOverlay * self,
    GstClockTime time)
{
  wchar_t text[256];
  HRESULT hr;
  guint h, m, s, ms;

  if (!GST_CLOCK_TIME_IS_VALID (time))
    return WString ();

  h = (guint) (time / (GST_SECOND * 60 * 60));
  m = (guint) ((time / (GST_SECOND * 60)) % 60);
  s = (guint) ((time / GST_SECOND) % 60);
  ms = (guint) ((time % GST_SECOND) / (1000 * 1000));

  hr = StringCbPrintfW (text, sizeof (text), L"%u:%02u:%02u.%03u", h, m, s, ms);

  if (FAILED (hr))
    return WString ();

  return WString (text);
}

static WString
gst_dwrite_time_overlay_get_text (GstDWriteBaseOverlay * overlay,
    const WString & default_text, GstBuffer * buffer)
{
  GstDWriteTimeOverlay *self = GST_DWRITE_TIME_OVERLAY (overlay);
  GstDWriteTimeOverlayPrivate *priv = self->priv;
  WString time_str;
  WString ret;
  std::lock_guard < std::mutex > lk (priv->lock);
  gboolean show_buffer_count = FALSE;

  if (priv->time_line == GST_DWRITE_TIME_OVERLAY_TIME_LINE_TIME_CODE) {
    GstVideoTimeCodeMeta *tc_meta =
        gst_buffer_get_video_time_code_meta (buffer);
    if (!tc_meta) {
      GST_DEBUG_OBJECT (self, "buffer without valid timecode");
      time_str = L"00:00:00:00";
    } else {
      gchar *str = gst_video_time_code_to_string (&tc_meta->tc);
      GST_DEBUG_OBJECT (self, "buffer with timecode %s", str);
      time_str = gst_dwrite_string_to_wstring (str);
      g_free (str);
    }
  } else {
    GstBaseTransform *trans = GST_BASE_TRANSFORM (overlay);
    GstClockTime ts, ts_buffer;
    GstSegment *seg = &trans->segment;

    ts = ts_buffer = GST_BUFFER_TIMESTAMP (buffer);
    if (GST_CLOCK_TIME_IS_VALID (ts)) {
      switch (priv->time_line) {
        case GST_DWRITE_TIME_OVERLAY_TIME_LINE_STREAM_TIME:
          ts = gst_segment_to_stream_time (seg, GST_FORMAT_TIME, ts_buffer);
          break;
        case GST_DWRITE_TIME_OVERLAY_TIME_LINE_RUNNING_TIME:
          ts = gst_segment_to_running_time (seg, GST_FORMAT_TIME, ts_buffer);
          break;
        case GST_DWRITE_TIME_OVERLAY_TIME_LINE_ELAPSED_RUNNING_TIME:
          ts = gst_segment_to_running_time (seg, GST_FORMAT_TIME, ts_buffer);
          if (!GST_CLOCK_TIME_IS_VALID (priv->first_running_time))
            priv->first_running_time = ts;
          ts -= priv->first_running_time;
          break;
        case GST_DWRITE_TIME_OVERLAY_TIME_LINE_REFERENCE_TIMESTAMP:
        {
          GstReferenceTimestampMeta *meta;
          if (priv->reference_timestamp_caps) {
            meta = gst_buffer_get_reference_timestamp_meta (buffer,
                priv->reference_timestamp_caps);
            if (meta)
              ts = meta->timestamp;
            else
              ts = 0;
          } else {
            ts = 0;
          }
          break;
        }
        case GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_COUNT:
          show_buffer_count = TRUE;
          priv->buffer_count++;
          break;
        case GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_OFFSET:
          show_buffer_count = TRUE;
          ts = gst_segment_to_running_time (seg, GST_FORMAT_TIME, ts_buffer);
          priv->buffer_count = gst_util_uint64_scale (ts, overlay->info.fps_n,
              overlay->info.fps_d * GST_SECOND);
          break;
        case GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_TIME:
        default:
          ts = ts_buffer;
          break;
      }

      if (show_buffer_count) {
        time_str = std::to_wstring (priv->buffer_count);
      } else if (priv->show_times_as_dates) {
        GDateTime *datetime;
        gchar *str;

        datetime =
            g_date_time_add_seconds (priv->datetime_epoch,
            ((gdouble) ts) / GST_SECOND);

        str = g_date_time_format (datetime, priv->datetime_format.c_str ());
        time_str = gst_dwrite_string_to_wstring (str);

        g_free (str);
        g_date_time_unref (datetime);
      } else {
        time_str = gst_dwrite_time_overlay_render_time (self, ts);
      }
    }
  }

  if (default_text.empty ())
    return time_str;

  return default_text + WString (L" ") + time_str;
}