/*
 * simple-encoder.c - Test GstVaapiencoder
 *
 * Copyright (C) 2015 Intel Corporation
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1
 * 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301 USA
 */

#include "gst/vaapi/sysdeps.h"
#include <gst/vaapi/gstvaapiencoder_mpeg2.h>
#include <gst/vaapi/gstvaapiencoder_h264.h>
#include <gst/vaapi/gstvaapisurfacepool.h>
#include <gst/vaapi/gstvaapisurfaceproxy.h>

#include "output.h"
#include "y4mreader.h"

static guint g_bitrate = 0;
static gchar *g_codec_str;
static gchar *g_output_file_name;
static char **g_input_files = NULL;

#define SURFACE_NUM 16

static GOptionEntry g_options[] = {
  {"codec", 'c', 0, G_OPTION_ARG_STRING, &g_codec_str,
      "codec to use for video encoding (h264/mpeg2)", NULL},
  {"bitrate", 'b', 0, G_OPTION_ARG_INT, &g_bitrate,
      "desired bitrate expressed in kbps", NULL},
  {"output", 'o', 0, G_OPTION_ARG_FILENAME, &g_output_file_name,
      "output file name", NULL},
  {G_OPTION_REMAINING, ' ', 0, G_OPTION_ARG_FILENAME_ARRAY, &g_input_files,
      "input file name", NULL},
  {NULL}
};

typedef struct
{
  GstVaapiDisplay *display;
  GstVaapiEncoder *encoder;
  guint read_frames;
  guint encoded_frames;
  guint saved_frames;
  Y4MReader *parser;
  FILE *output_file;
  guint input_stopped:1;
  guint encode_failed:1;
} App;

static inline gchar *
generate_output_filename (const gchar * ext)
{
  gchar *fn;
  int i = 0;

  while (1) {
    fn = g_strdup_printf ("temp%02d.%s", i, ext);
    if (g_file_test (fn, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
      i++;
      g_free (fn);
    } else {
      break;
    }
  }

  return fn;
}

static gboolean
parse_options (int *argc, char *argv[])
{
  GOptionContext *ctx;
  gboolean success;
  GError *error = NULL;

  ctx = g_option_context_new (" - encoder test options");
  if (!ctx)
    return FALSE;

  g_option_context_add_group (ctx, gst_init_get_option_group ());
  g_option_context_add_main_entries (ctx, g_options, NULL);
  g_option_context_set_help_enabled (ctx, TRUE);
  success = g_option_context_parse (ctx, argc, &argv, &error);
  if (!success) {
    g_printerr ("Option parsing failed: %s\n", error->message);
    g_error_free (error);
    goto bail;
  }

  if (!g_codec_str)
    g_codec_str = g_strdup ("h264");
  if (!g_output_file_name)
    g_output_file_name = generate_output_filename (g_codec_str);

bail:
  g_option_context_free (ctx);
  return success;
}

static void
print_yuv_info (App * app)
{
  g_print ("\n");
  g_print ("Encode      : %s\n", g_codec_str);
  g_print ("Resolution  : %dx%d\n", app->parser->width, app->parser->height);
  g_print ("Source YUV  : %s\n", g_input_files ? g_input_files[0] : "stdin");
  g_print ("Frame Rate  : %0.1f fps\n",
      1.0 * app->parser->fps_n / app->parser->fps_d);
  g_print ("Coded file  : %s\n", g_output_file_name);
  g_print ("\n");
}

static void
print_num_frame (App * app)
{
  g_print ("\n");
  g_print ("read frames    : %d\n", app->read_frames);
  g_print ("encoded frames : %d\n", app->encoded_frames);
  g_print ("saved frames   : %d\n", app->saved_frames);
  g_print ("\n");
}

static GstVaapiEncoder *
encoder_new (GstVaapiDisplay * display)
{
  GstVaapiEncoder *encoder = NULL;

  if (!g_strcmp0 (g_codec_str, "mpeg2"))
    encoder = gst_vaapi_encoder_mpeg2_new (display);
  else if (!g_strcmp0 (g_codec_str, "h264"))
    encoder = gst_vaapi_encoder_h264_new (display);
  else
    return NULL;

  gst_vaapi_encoder_set_bitrate (encoder, g_bitrate);

  return encoder;
}

static inline GstVideoCodecState *
new_codec_state (gint width, gint height, gint fps_n, gint fps_d)
{
  GstVideoCodecState *state;

  state = g_slice_new0 (GstVideoCodecState);
  state->ref_count = 1;
  gst_video_info_init (&state->info);
  gst_video_info_set_format (&state->info, GST_VIDEO_FORMAT_ENCODED, width,
      height);

  state->info.fps_n = fps_n;
  state->info.fps_d = fps_d;

  return state;
}

static gboolean
set_format (GstVaapiEncoder * encoder, gint width, gint height, gint fps_n,
    gint fps_d)
{
  GstVideoCodecState *in_state;
  GstVaapiEncoderStatus status;

  in_state = new_codec_state (width, height, fps_n, fps_d);
  status = gst_vaapi_encoder_set_codec_state (encoder, in_state);
  g_slice_free (GstVideoCodecState, in_state);

  return (status == GST_VAAPI_ENCODER_STATUS_SUCCESS);
}

static GstBuffer *
allocate_buffer (GstVaapiCodedBuffer * vbuf)
{
  GstBuffer *buf;
  gssize size;

  size = gst_vaapi_coded_buffer_get_size (vbuf);
  if (size <= 0) {
    g_warning ("Invalid VA buffer size (%zd)", size);
    return NULL;
  }

  buf = gst_buffer_new_and_alloc (size);
  if (!buf) {
    g_warning ("Failed to create output buffer of size %zd", size);
    return NULL;
  }

  if (!gst_vaapi_coded_buffer_copy_into (buf, vbuf)) {
    g_warning ("Failed to copy VA buffer data");
    gst_buffer_unref (buf);
    return NULL;
  }

  return buf;
}

static GstVaapiEncoderStatus
get_encoder_buffer (GstVaapiEncoder * encoder, GstBuffer ** buffer)
{
  GstVaapiCodedBufferProxy *proxy = NULL;
  GstVaapiEncoderStatus status;

  status = gst_vaapi_encoder_get_buffer_with_timeout (encoder, &proxy, 50000);
  if (status < GST_VAAPI_ENCODER_STATUS_SUCCESS) {
    g_warning ("Failed to get a buffer from encoder: %d", status);
    return status;
  } else if (status > GST_VAAPI_ENCODER_STATUS_SUCCESS) {
    return status;
  }

  *buffer = allocate_buffer (GST_VAAPI_CODED_BUFFER_PROXY_BUFFER (proxy));
  gst_vaapi_coded_buffer_proxy_unref (proxy);

  return status;
}

static gboolean
outputs_to_file (GstBuffer * buffer, FILE * file)
{
  GstMapInfo info;
  size_t written;
  gboolean ret = FALSE;

  gst_buffer_map (buffer, &info, GST_MAP_READ);

  if (info.size <= 0 || !info.data)
    return FALSE;

  written = fwrite (info.data, 1, info.size, file);
  if (written < info.size) {
    g_warning ("write file error.");
    goto bail;
  }

  ret = TRUE;

bail:
  gst_buffer_unmap (buffer, &info);
  return ret;
}

static gpointer
get_buffer_thread (gpointer data)
{
  App *app = data;

  GstVaapiEncoderStatus ret;
  GstBuffer *obuf;

  while (1) {
    obuf = NULL;
    ret = get_encoder_buffer (app->encoder, &obuf);
    if (app->input_stopped && ret > GST_VAAPI_ENCODER_STATUS_SUCCESS) {
      break;                    /* finished */
    } else if (ret > GST_VAAPI_ENCODER_STATUS_SUCCESS) {        /* another chance */
      continue;
    }
    if (ret < GST_VAAPI_ENCODER_STATUS_SUCCESS) {       /* fatal error */
      app->encode_failed = TRUE;
      break;
    }

    app->encoded_frames++;
    g_debug ("encoded frame %d, buffer = %p", app->encoded_frames, obuf);

    if (app->output_file && outputs_to_file (obuf, app->output_file))
      app->saved_frames++;

    gst_buffer_unref (obuf);
  }

  if (obuf)
    gst_buffer_replace (&obuf, NULL);

  return NULL;
}

static void
app_free (App * app)
{
  g_return_if_fail (app);

  if (app->parser)
    y4m_reader_close (app->parser);

  if (app->encoder) {
    gst_vaapi_encoder_flush (app->encoder);
    gst_vaapi_encoder_unref (app->encoder);
  }

  if (app->display)
    gst_vaapi_display_unref (app->display);

  if (app->output_file)
    fclose (app->output_file);

  g_slice_free (App, app);
}

static App *
app_new (const gchar * input_fn, const gchar * output_fn)
{
  App *app = g_slice_new0 (App);
  if (!app)
    return NULL;

  app->parser = y4m_reader_open (input_fn);
  if (!app->parser) {
    g_warning ("Could not parse input stream.");
    goto error;
  }

  app->output_file = fopen (output_fn, "w");
  if (app->output_file == NULL) {
    g_warning ("Could not open file \"%s\" for writing: %s.", output_fn,
        g_strerror (errno));
    goto error;
  }

  app->display = video_output_create_display (NULL);
  if (!app->display) {
    g_warning ("Could not create VA display.");
    goto error;
  }

  app->encoder = encoder_new (app->display);
  if (!app->encoder) {
    g_warning ("Could not create encoder.");
    goto error;
  }

  if (!set_format (app->encoder, app->parser->width, app->parser->height,
          app->parser->fps_n, app->parser->fps_d)) {
    g_warning ("Could not set format.");
    goto error;
  }

  return app;

error:
  app_free (app);
  return NULL;
}

static gboolean
upload_frame (GstVaapiEncoder * encoder, GstVaapiSurfaceProxy * proxy)
{
  GstVideoCodecFrame *frame;
  GstVaapiEncoderStatus ret;

  frame = g_slice_new0 (GstVideoCodecFrame);
  gst_video_codec_frame_set_user_data (frame,
      gst_vaapi_surface_proxy_ref (proxy),
      (GDestroyNotify) gst_vaapi_surface_proxy_unref);

  ret = gst_vaapi_encoder_put_frame (encoder, frame);
  return (ret == GST_VAAPI_ENCODER_STATUS_SUCCESS);
}

static gboolean
load_frame (App * app, GstVaapiImage * image)
{
  gboolean ret = FALSE;

  if (!gst_vaapi_image_map (image))
    return FALSE;

  ret = y4m_reader_load_image (app->parser, image);

  if (!gst_vaapi_image_unmap (image))
    return FALSE;

  return ret;
}

static int
app_run (App * app)
{
  GstVaapiImage *image;
  GstVaapiVideoPool *pool;
  GThread *buffer_thread;
  gsize id;
  int ret = EXIT_FAILURE;

  image = gst_vaapi_image_new (app->display, GST_VIDEO_FORMAT_I420,
      app->parser->width, app->parser->height);

  {
    GstVideoInfo vi;
    gst_video_info_set_format (&vi, GST_VIDEO_FORMAT_ENCODED,
        app->parser->width, app->parser->height);
    pool = gst_vaapi_surface_pool_new_full (app->display, &vi, 0);
  }

  buffer_thread = g_thread_new ("get buffer thread", get_buffer_thread, app);

  while (1) {
    GstVaapiSurfaceProxy *proxy;
    GstVaapiSurface *surface;

    if (!load_frame (app, image))
      break;

    if (!gst_vaapi_image_unmap (image))
      break;

    proxy =
        gst_vaapi_surface_proxy_new_from_pool (GST_VAAPI_SURFACE_POOL (pool));
    if (!proxy) {
      g_warning ("Could not get surface proxy from pool.");
      break;
    }
    surface = gst_vaapi_surface_proxy_get_surface (proxy);
    if (!surface) {
      g_warning ("Could not get surface from proxy.");
      break;
    }

    if (!gst_vaapi_surface_put_image (surface, image)) {
      g_warning ("Could not update surface");
      break;
    }

    if (!upload_frame (app->encoder, proxy)) {
      g_warning ("put frame failed");
      break;
    }

    app->read_frames++;
    id = gst_vaapi_surface_get_id (surface);
    g_debug ("input frame %d, surface id = %" G_GSIZE_FORMAT, app->read_frames,
        id);

    gst_vaapi_surface_proxy_unref (proxy);
  }

  app->input_stopped = TRUE;

  g_thread_join (buffer_thread);

  if (!app->encode_failed && feof (app->parser->fp))
    ret = EXIT_SUCCESS;

  gst_vaapi_video_pool_replace (&pool, NULL);
  gst_vaapi_object_unref (image);
  return ret;
}

int
main (int argc, char *argv[])
{
  App *app;
  int ret = EXIT_FAILURE;
  gchar *input_fn;

  if (!parse_options (&argc, argv))
    return EXIT_FAILURE;

  /* @TODO: iterate all the input files */
  input_fn = g_input_files ? g_input_files[0] : NULL;
  if (input_fn && !g_file_test (input_fn,
          G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
    g_warning ("input file \"%s\" doesn't exist", input_fn);
    goto bail;
  }

  app = app_new (input_fn, g_output_file_name);
  if (!app)
    goto bail;

  print_yuv_info (app);
  ret = app_run (app);
  print_num_frame (app);

  app_free (app);

bail:
  g_free (g_codec_str);
  g_free (g_output_file_name);
  g_strfreev (g_input_files);

  gst_deinit ();

  return ret;
}
