/*  Copyright (C) 2015 Centricular Ltd
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
 * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <gst/gst.h>
#include <gst/video/gstvideosink.h>
#include <gst/video/video.h>

#define STR_HELPER(x) #x
#define STR(x) STR_HELPER(x)

/* Change this to set the output resolution */
#define OUTPUT_VIDEO_WIDTH  1280
#define OUTPUT_VIDEO_HEIGHT 720


#define DEFAULT_ALPHA_OVERLAY   0.5
#define DEFAULT_DECKLINK_MODE   2
#define RAW_VIDEO_CAPS_STR "video/x-raw,format=RGBA"

GST_DEBUG_CATEGORY_STATIC (playout);
#define GST_CAT_DEFAULT playout

typedef enum
{
  PLAYOUT_APP_STATE_READY,      /* Newly created */
  PLAYOUT_APP_STATE_PLAYING,    /* Playing an item */
  PLAYOUT_APP_STATE_EOS         /* Finished playing, all items EOS */
} PlayoutAppState;

typedef struct
{
  /* Application state */
  PlayoutAppState state;

  /* An array of PlayoutItems that will be played in sequence */
  GPtrArray *play_queue;
  /* Index of the currently-playing item */
  gint play_queue_current;
  /* Lock access to the play queue */
  GMutex play_queue_lock;

  GMainLoop *main_loop;

  /* Pipeline */
  GstElement *pipeline;

  /* Output */
  GstElement *video_mixer;
  GstElement *video_convert;
  GstElement *video_sink;
  GstElement *video_interlace;

  /* The duration of all items that have been played in ns.
   * Only updates when a new item is activated. */
  guint64 elapsed_duration;
} PlayoutApp;

typedef enum
{
  PLAYOUT_ITEM_STATE_NEW,       /* Newly created */
  PLAYOUT_ITEM_STATE_PREPARED,  /* Prepared and ready to activate */
  PLAYOUT_ITEM_STATE_ACTIVATED, /* Activated */
  PLAYOUT_ITEM_STATE_EOS        /* At least one pad is EOS */
} PlayoutItemState;

typedef struct
{
  PlayoutApp *app;
  PlayoutItemState state;

  gchar *fn;

  GstElement *decoder;          /* bin with uridecodebin + converters */

  GstPad *video_pad;            /* decoder bin video src ghostpad */

  /* When this item has finished preparing and all pads have been connected to
   * mixers, the pads will be blocked till it's this item's turn to be played */
  gulong video_pad_probe_block_id;

  guint64 running_time;
} PlayoutItem;

static PlayoutApp *playout_app_new ();
static void playout_app_free (PlayoutApp * app);
static PlayoutItem *playout_item_new (PlayoutApp * app, const gchar * fn);
static void playout_item_free (PlayoutItem * item);

static gboolean playout_app_prepare_item (PlayoutApp * app, PlayoutItem * item);
static gboolean playout_app_activate_item (PlayoutApp * app,
    PlayoutItem * item);
static gboolean playout_app_activate_next_item (PlayoutApp * app);
static gboolean playout_app_activate_next_item_early (PlayoutApp * app);
static PlayoutItem *playout_app_get_current_item (PlayoutApp * app);

static PlayoutApp *playout_app_new ()
{
  GstElement *video_capsfilter;
  GstElement *queue;
  GstCaps *caps;
  PlayoutApp *app;

  app = g_new0 (PlayoutApp, 1);

  app->state = PLAYOUT_APP_STATE_READY;

  app->play_queue =
      g_ptr_array_new_with_free_func ((GDestroyNotify) playout_item_free);
  app->play_queue_current = -1;
  g_mutex_init (&app->play_queue_lock);

  app->main_loop = g_main_loop_new (NULL, FALSE);

  app->pipeline = gst_pipeline_new ("pipeline");

  /* It's best to set a caps filter for the video output format */
  app->video_mixer = gst_element_factory_make ("glvideomixer", "video_mixer");
  /* Set the background as black; faster while compositing, and allows us to
   * rescale videos with a different aspect ratio than the output in a way that
   * adds black borders automatically */
  g_object_set (app->video_mixer, "background", 0, NULL);
  queue = gst_element_factory_make ("queue", "vsink_queue");
  app->video_sink = gst_element_factory_make ("xvimagesink", NULL);

  app->video_convert = gst_element_factory_make ("videoconvert", NULL);

  //g_object_set (app->video_sink, "async", TRUE, NULL);
  video_capsfilter = gst_element_factory_make ("capsfilter",
      "video_mixer_capsfilter");
  caps = gst_caps_from_string (RAW_VIDEO_CAPS_STR);
  g_object_set (video_capsfilter, "caps", caps, NULL);
  gst_caps_unref (caps);
  
  gst_bin_add_many (GST_BIN (app->pipeline), app->video_mixer,
    video_capsfilter, app->video_convert, queue, 
    app->video_sink, NULL);
  gst_element_link_many (app->video_mixer, 
    video_capsfilter, app->video_convert, queue,
    app->video_sink, NULL);

  return app;
}

static void
playout_app_free (PlayoutApp * app)
{
  GST_DEBUG ("Freeing app");
  g_ptr_array_unref (app->play_queue);
  g_main_loop_unref (app->main_loop);
  gst_element_set_state (app->pipeline, GST_STATE_NULL);
  gst_object_unref (app->pipeline);
  g_free (app);
}

static void
playout_app_eos (GstBus * bus, GstMessage * msg, PlayoutApp * app)
{
  g_print ("All streams EOS, exiting...\n");
  g_main_loop_quit (app->main_loop);
}

static PlayoutItem *
playout_item_new (PlayoutApp * app, const gchar * fn)
{
  PlayoutItem *item = g_new0 (PlayoutItem, 1);

  item->app = app;
  item->state = PLAYOUT_ITEM_STATE_NEW;
  item->fn = g_strdup (fn);

  return item;
}

static gboolean
playout_item_remove (PlayoutItem * item)
{
  PlayoutApp *app;
  GST_DEBUG ("%s: removing and freeing", item->fn);

  app = item->app;

  g_mutex_lock (&app->play_queue_lock);
  g_ptr_array_remove (app->play_queue, item);
  /* This item has been removed from the array, decrement the index */
  app->play_queue_current--;
  g_mutex_unlock (&app->play_queue_lock);

  /* Don't call this again */
  return FALSE;
}

/* Unlink and release the pad */
static gboolean
playout_remove_pad (GstPad * srcpad)
{
  GstPad *sinkpad;
  GstElement *mixer;

  sinkpad = gst_pad_get_peer (srcpad);
  if (!sinkpad)
    return FALSE;
  if (!gst_pad_unlink (srcpad, sinkpad))
    return FALSE;
  mixer = gst_pad_get_parent_element (sinkpad);
  gst_element_release_request_pad (mixer, sinkpad);
  GST_DEBUG ("Released some pad");

  gst_object_unref (sinkpad);
  gst_object_unref (mixer);
  return FALSE;
}

static GstPadProbeReturn
playout_item_pad_probe_blocked (GstPad * srcpad, GstPadProbeInfo * info,
    PlayoutItem * item)
{
  if (srcpad == item->video_pad) {
    item->video_pad_probe_block_id = GST_PAD_PROBE_INFO_ID (info);
  } else {
    g_assert_not_reached ();
  }
  return GST_PAD_PROBE_OK;
}

static GstPadProbeReturn
playout_item_pad_probe_video_pad_running_time (GstPad * srcpad,
    GstPadProbeInfo * info, PlayoutItem * item)
{
  GstEvent *event;
  GstBuffer *buffer;
  guint64 running_time;
  const GstSegment *segment;

  buffer = GST_PAD_PROBE_INFO_BUFFER (info);
  event = gst_pad_get_sticky_event (srcpad, GST_EVENT_SEGMENT, 0);

  if (event) {
    gst_event_parse_segment (event, &segment);
    gst_event_unref (event);
    running_time = gst_segment_to_running_time (segment, GST_FORMAT_TIME,
        GST_BUFFER_PTS (buffer));
  } else {
    GST_WARNING ("%s: unable to parse event for segment; falling back to pts. "
        "Output will probably have glitches.", item->fn);
    running_time = GST_BUFFER_PTS (buffer);
  }

  item->running_time = running_time + GST_BUFFER_DURATION (buffer);
  GST_TRACE ("%s: running time is %" G_GUINT64_FORMAT ", duration is %"
      G_GUINT64_FORMAT, item->fn, item->running_time,
      GST_BUFFER_DURATION (buffer));

  return GST_PAD_PROBE_PASS;
}


static GstPadProbeReturn
playout_item_pad_probe_event (GstPad * srcpad, GstPadProbeInfo * info,
    PlayoutItem * item)
{
  GstEventType type;
  gboolean ret = TRUE;
  GstPadProbeReturn probe_ret = GST_PAD_PROBE_DROP;

  type = GST_EVENT_TYPE (GST_PAD_PROBE_INFO_DATA (info));
  if (type != GST_EVENT_EOS)
    return GST_PAD_PROBE_PASS;

  /* We might get two EOSes on this pad if we send an artificial EOS. Remove
   * the probe so this is only called once for each pad */
  gst_pad_remove_probe (srcpad, GST_PAD_PROBE_INFO_ID (info));

  GST_DEBUG ("%s: recvd some EOS", item->fn);

  if (srcpad == item->video_pad) {
    GST_WARNING ("%s: video pad was EOS", item->fn);

    //if (item->app->state != PLAYOUT_APP_STATE_EOS) {
      playout_remove_pad (srcpad);
      GST_DEBUG ("%s: released video pad", item->fn);
    //}

  }
  //if (item->state != PLAYOUT_ITEM_STATE_EOS) {
    /* Update the total elapsed duration from the item's current running time */
    item->app->elapsed_duration += item->running_time;

    /* Activate the next item if and only if this is the first pad to go EOS */
    ret = playout_app_activate_next_item (item->app);
    if (!ret) {
      GST_DEBUG ("%s: App is going EOS", item->fn);
      item->app->state = PLAYOUT_APP_STATE_EOS;
      probe_ret = GST_PAD_PROBE_PASS;
    }
  //}
  item->state = PLAYOUT_ITEM_STATE_EOS;
  /* NOTE: If the srcpad has been unlinked, the return value is useless */
  return probe_ret;
}

/* On the "pad-added" signal of uridecodebin, add converters and connect to
 * video mixer */
static void
playout_item_new_pad (GstElement * uridecodebin, GstPad * pad,
    PlayoutItem * item)
{
  GstStructure *s;
  GstCaps *caps;
  GstPad *sinkpad, *srcpad;
  GstElement *queue;
  GstPadProbeType block_probe_type;

  caps = gst_pad_get_current_caps (pad);
  s = gst_caps_get_structure (caps, 0);
  GST_DEBUG ("%s: new pad: %p, caps: %s", item->fn, pad,
      gst_structure_get_name (s));

  if (gst_structure_has_name (s, "video/x-raw")) {
    if (item->video_pad != NULL)
      /* Ignore all video pads after the first one */
      goto out;
    goto video;
  } else {
    goto out;
  }

video:
  {

    queue = gst_element_factory_make ("queue", NULL);
    gst_bin_add (GST_BIN (item->decoder), queue);

    if (!gst_element_sync_state_with_parent (queue)) {
      GST_ERROR ("%s: unable to sync video queue state with decoder", item->fn);
      goto out;
    }

    sinkpad = gst_element_get_static_pad (queue, "sink");
    gst_pad_link (pad, sinkpad);
    gst_object_unref (sinkpad);

    /* Convert the queue src pad to a ghostpad on the bin */
    srcpad = gst_element_get_static_pad (queue, "src");
    item->video_pad = gst_ghost_pad_new (NULL, srcpad);
    gst_pad_set_active (item->video_pad, TRUE);
    gst_element_add_pad (item->decoder, item->video_pad);
    gst_object_unref (srcpad);

    /* Set a buffer pad probe that constantly updates the item's
     * elapsed_duration using the duration of each video buffer */
    gst_pad_add_probe (srcpad, GST_PAD_PROBE_TYPE_BUFFER,
        (GstPadProbeCallback) playout_item_pad_probe_video_pad_running_time,
        item, NULL);
    srcpad = item->video_pad;
    goto done;
  }

done:
  /* We let events and queries through */
  block_probe_type = GST_PAD_PROBE_TYPE_BLOCK |
      GST_PAD_PROBE_TYPE_BUFFER | GST_PAD_PROBE_TYPE_BUFFER_LIST;
  /* If the app is already playing an item, block everything except queries
   * till we need to play this item */
  if (item->app->state != PLAYOUT_APP_STATE_READY)
    gst_pad_add_probe (srcpad, block_probe_type,
        (GstPadProbeCallback) playout_item_pad_probe_blocked, item, NULL);
  /* Probe events for EOS */
  gst_pad_add_probe (srcpad, GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM,
      (GstPadProbeCallback) playout_item_pad_probe_event, item, NULL);

out:
  gst_caps_unref (caps);
}

/* All pads on uridecodebin have finished being populated; the item has been
 * prepared and is ready to be activated */
static void
playout_item_no_more_pads (GstElement * uridecodebin, GstPad * pad,
    PlayoutItem * item)
{
  item->state = PLAYOUT_ITEM_STATE_PREPARED;
  GST_DEBUG ("%s: prepared", item->fn);

  if (item->app->state != PLAYOUT_APP_STATE_READY)
    /* This item will be activated when the previous one is EOS */
    return;

  GST_DEBUG ("Application isn't already playing; activate the item and prepare"
      " the next one");

  playout_app_activate_item (item->app, item);
  item->state = PLAYOUT_ITEM_STATE_ACTIVATED;
  item->app->state = PLAYOUT_APP_STATE_PLAYING;

  if (item->app->play_queue->len > 1)
    playout_app_prepare_item (item->app,
        g_ptr_array_index (item->app->play_queue, 1));
}

static GstElement *
playout_item_create_decoder (PlayoutItem * item)
{
  GstElement *bin, *dec;
  GError *err = NULL;
  gchar *uri;

  uri = gst_filename_to_uri (item->fn, &err);
  if (err != NULL) {
    GST_WARNING ("Could not convert '%s' to uri: %s", item->fn, err->message);
    g_error_free (err);
    return NULL;
  }

  bin = gst_bin_new (NULL);

  dec = gst_element_factory_make ("uridecodebin", NULL);
  g_object_set (dec, "uri", uri, NULL);
  g_free (uri);

  gst_bin_add (GST_BIN (bin), dec);

  g_signal_connect (dec, "pad-added", G_CALLBACK (playout_item_new_pad), item);
  g_signal_connect (dec, "no-more-pads", G_CALLBACK (playout_item_no_more_pads),
      item);

  return bin;
}

static void
playout_item_free (PlayoutItem * item)
{
  gst_element_set_state (item->decoder, GST_STATE_NULL);
  gst_bin_remove (GST_BIN (item->app->pipeline), item->decoder);

  g_free (item->fn);
  g_free (item);
}

static guint64
playout_item_pad_get_segment_time (GstPad * srcpad)
{
  GstEvent *event;
  const GstSegment *segment;

  event = gst_pad_get_sticky_event (srcpad, GST_EVENT_SEGMENT, 0);
  if (!event)
    return 0;
  gst_event_parse_segment (event, &segment);
  gst_event_unref (event);
  return segment->time;
}

static void
playout_app_add_item (PlayoutApp * app, const gchar * fn)
{
  PlayoutItem *item;

  item = playout_item_new (app, fn);

  g_mutex_lock (&app->play_queue_lock);
  g_ptr_array_add (app->play_queue, item);
  g_mutex_unlock (&app->play_queue_lock);
}

static PlayoutItem *
playout_app_get_current_item (PlayoutApp * app)
{
  if (app->play_queue_current < 0 ||
      app->play_queue->len < (app->play_queue_current + 1))
    return NULL;

  return g_ptr_array_index (app->play_queue, app->play_queue_current);
}

static gboolean
playout_app_prepare_item (PlayoutApp * app, PlayoutItem * item)
{
  if (item->decoder != NULL)
    return TRUE;

  item->decoder = playout_item_create_decoder (item);

  if (item->decoder == NULL)
    return FALSE;

  gst_bin_add (GST_BIN (app->pipeline), item->decoder);

  if (!gst_element_sync_state_with_parent (item->decoder)) {
    GST_ERROR ("%s: unable to sync state with parent", item->fn);
    return FALSE;
  }

  GST_DEBUG ("%s: preparing", item->fn);

  /* All further processing is done in the "no-more-pads" callback of
   * uridecodebin */
  return TRUE;
}

/* Called exactly once for each item */
static gboolean
playout_app_activate_item (PlayoutApp * app, PlayoutItem * item)
{
  GstPad *sinkpad;
  guint64 segment_time;

  if (item->state != PLAYOUT_ITEM_STATE_PREPARED) {
    GST_ERROR ("Item %s is not ready to be activated!", item->fn);
    return FALSE;
  }

  if (item->video_pad) {
    GST_DEBUG ("%s: hooking up video pad to mixer", item->fn);
    sinkpad = gst_element_get_request_pad (app->video_mixer, "sink_%u");

    gst_pad_link (item->video_pad, sinkpad);

    if (app->elapsed_duration > 0) {
      GST_DEBUG ("%s: set pad offset to %" G_GUINT64_FORMAT "ms", item->fn,
          app->elapsed_duration / GST_MSECOND);
      gst_pad_set_offset (item->video_pad, app->elapsed_duration);
    }

    if (item->video_pad_probe_block_id > 0) {
      GST_DEBUG ("%s: removing video pad block probe", item->fn);
      gst_pad_remove_probe (item->video_pad, item->video_pad_probe_block_id);
    }
    gst_object_unref (sinkpad);
  }

  item->state = PLAYOUT_ITEM_STATE_ACTIVATED;
  g_mutex_lock (&item->app->play_queue_lock);
  item->app->play_queue_current++;
  g_mutex_unlock (&item->app->play_queue_lock);

  GST_DEBUG ("%s: activated", item->fn);

  return TRUE;
}

/* Activate the next item, and prepare the one after that for later activation */
static gboolean
playout_app_activate_next_item (PlayoutApp * app)
{
  PlayoutItem *item;
  gboolean ret;

  if (app->play_queue->len < (app->play_queue_current + 2)) {
    g_print ("No more items to play\n");
    return FALSE;
  }

  item = g_ptr_array_index (app->play_queue, app->play_queue_current + 1);
  ret = playout_app_activate_item (app, item);
  if (!ret) {
    /* Tell caller, who can then decide whether to skip or error out */
    GST_ERROR ("%s: unable to activate", item->fn);
    return FALSE;
  }
  return ret;
}

static gboolean
playout_app_activate_next_item_early (PlayoutApp * app)
{
  PlayoutItem *item;
  item = playout_app_get_current_item (app);
  if (!item) {
    GST_WARNING ("Unable to switch early, no current item");
    return FALSE;
  }

  GST_DEBUG ("Sending video EOS to %s", item->fn);
  gst_pad_push_event (item->video_pad, gst_event_new_eos ());
  /* Return FALSE so this function is called only once */
  return FALSE;
}

static gboolean
playout_app_play (PlayoutApp * app)
{
  PlayoutItem *item;

  item = app->play_queue->len ? g_ptr_array_index (app->play_queue, 0) : NULL;
  if (!item) {
    g_printerr ("Nothing to play\n");
    return FALSE;
  }

  playout_app_prepare_item (app, item);
  return TRUE;
}

int
main (int argc, char **argv)
{
  GstBus *bus;
  gint switch_after_ms = 0;
  gchar **f, **filenames = NULL;
  GOptionEntry options[] = {
    {"switch-after", 's', 0, G_OPTION_ARG_INT, &switch_after_ms, "Time after "
          "which the next item will be forcibly activated", "MILLISECONDS"},
    {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &filenames, NULL},
    {NULL}
  };
  GOptionContext *ctx;
  PlayoutApp *app;
  GError *err = NULL;

  ctx = g_option_context_new ("FILENAME1 [FILENAME2] [FILENAME3] ...");
  g_option_context_add_main_entries (ctx, options, NULL);
  g_option_context_add_group (ctx, gst_init_get_option_group ());

  if (!g_option_context_parse (ctx, &argc, &argv, &err)) {
    if (err)
      g_printerr ("Error initializing: %s\n", err->message);
    else
      g_printerr ("Error initializing: Unknown error!\n");
    return 1;
  }
  g_option_context_free (ctx);

  GST_DEBUG_CATEGORY_INIT (playout, "playout", 0, "Playout example app");

  app = playout_app_new ();

  if (filenames == NULL || *filenames == NULL) {
    g_printerr ("Usage: %s FILENAME1 FILENAME2\n", argv[0]);
    return 1;
  }

  for (f = filenames; f != NULL && *f != NULL; ++f)
    playout_app_add_item (app, *f);

  g_strfreev (filenames);

  if (!playout_app_play (app))
    return 1;

  bus = gst_pipeline_get_bus (GST_PIPELINE (app->pipeline));
  gst_bus_add_signal_watch (bus);
  g_signal_connect (bus, "message::eos", G_CALLBACK (playout_app_eos), app);
  gst_object_unref (bus);

  gst_element_set_state (app->pipeline, GST_STATE_PLAYING);

  if (switch_after_ms)
    g_timeout_add (switch_after_ms,
        (GSourceFunc) playout_app_activate_next_item_early, app);

  GST_DEBUG ("Running mainloop");
  g_main_loop_run (app->main_loop);

  playout_app_free (app);

  return 0;
}

