/*
 *  simple-decoder.c - Simple Decoder Application
 *
 *  Copyright (C) 2013-2014 Intel Corporation
 *    Author: Gwenole Beauchesne <gwenole.beauchesne@intel.com>
 *
 *  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
 */

/*
 * This is a really simple decoder application that only accepts raw
 * bitstreams. So, it may be needed to suggest what codec to use to
 * the application.
 */

#include "gst/vaapi/sysdeps.h"
#include <stdarg.h>
#include <gst/vaapi/gstvaapidecoder.h>
#include <gst/vaapi/gstvaapidecoder_h264.h>
#include <gst/vaapi/gstvaapidecoder_jpeg.h>
#include <gst/vaapi/gstvaapidecoder_mpeg2.h>
#include <gst/vaapi/gstvaapidecoder_mpeg4.h>
#include <gst/vaapi/gstvaapidecoder_vc1.h>
#include <gst/vaapi/gstvaapiwindow.h>
#include <gst/vaapi/gstvaapipixmap.h>
#include "codec.h"
#include "output.h"

static gchar *g_codec_str;
static gboolean g_use_pixmap;
static gboolean g_benchmark;

static GOptionEntry g_options[] = {
    { "codec", 'c',
      0,
      G_OPTION_ARG_STRING, &g_codec_str,
      "suggested codec", NULL },
    { "pixmap", 0,
      0,
      G_OPTION_ARG_NONE, &g_use_pixmap,
      "use render-to-pixmap", NULL },
    { "benchmark", 0,
      0,
      G_OPTION_ARG_NONE, &g_benchmark,
      "benchmark mode", NULL },
    { NULL, }
};

typedef enum {
    APP_RUNNING,
    APP_GOT_EOS,
    APP_GOT_ERROR,
} AppEvent;

typedef enum {
    APP_ERROR_NONE,
    APP_ERROR_DECODER,
    APP_ERROR_RENDERER,
} AppError;

typedef struct {
    GstVaapiSurfaceProxy *proxy;
    GstClockTime        pts;
    GstClockTime        duration;
} RenderFrame;

typedef struct {
    GMutex              mutex;
    GMappedFile        *file;
    gchar              *file_name;
    guint               file_offset;
    guint               file_size;
    guchar             *file_data;
    GstVaapiDisplay    *display;
    GstVaapiDecoder    *decoder;
    GThread            *decoder_thread;
    volatile gboolean   decoder_thread_cancel;
    GCond               decoder_ready;
    GAsyncQueue        *decoder_queue;
    GstVaapiCodec       codec;
    guint               fps_n;
    guint               fps_d;
    guint32             frame_duration;
    guint               surface_width;
    guint               surface_height;
    GstVaapiPixmap     *pixmaps[2];
    guint               pixmap_id;
    guint               pixmap_width;
    guint               pixmap_height;
    GstVaapiWindow     *window;
    guint               window_width;
    guint               window_height;
    GThread            *render_thread;
    volatile gboolean   render_thread_cancel;
    GCond               render_ready;
    RenderFrame        *last_frame;
    GError             *error;
    AppEvent            event;
    GCond               event_cond;
    GTimer             *timer;
    guint32             num_frames;
} App;

static inline RenderFrame *
render_frame_new(void)
{
    return g_slice_new(RenderFrame);
}

static void
render_frame_free(RenderFrame *rfp)
{
    if (G_UNLIKELY(!rfp))
        return;
    gst_vaapi_surface_proxy_replace(&rfp->proxy, NULL);
    g_slice_free(RenderFrame, rfp);
}

static inline void
render_frame_replace(RenderFrame **rfp_ptr, RenderFrame *new_rfp)
{
    if (*rfp_ptr)
        render_frame_free(*rfp_ptr);
    *rfp_ptr = new_rfp;
}

#define APP_ERROR app_error_quark()
static GQuark
app_error_quark(void)
{
    static gsize g_quark;

    if (g_once_init_enter(&g_quark)) {
        gsize quark = (gsize)g_quark_from_static_string("AppError");
        g_once_init_leave(&g_quark, quark);
    }
    return g_quark;
}

static void
app_send_error(App *app, GError *error)
{
    g_mutex_lock(&app->mutex);
    app->error = error;
    app->event = APP_GOT_ERROR;
    g_cond_signal(&app->event_cond);
    g_mutex_unlock(&app->mutex);
}

static void
app_send_eos(App *app)
{
    g_mutex_lock(&app->mutex);
    app->event = APP_GOT_EOS;
    g_cond_signal(&app->event_cond);
    g_mutex_unlock(&app->mutex);
}

static const gchar *
get_decoder_status_string(GstVaapiDecoderStatus status)
{
    const gchar *str;

#define DEFINE_STATUS(status, status_string) \
    case GST_VAAPI_DECODER_STATUS_##status:  \
        str = status_string;                 \
        break

    switch (status) {
        DEFINE_STATUS(SUCCESS,                  "<success>");
        DEFINE_STATUS(END_OF_STREAM,            "<EOS>");
        DEFINE_STATUS(ERROR_ALLOCATION_FAILED,  "allocation failed");
        DEFINE_STATUS(ERROR_INIT_FAILED,        "initialization failed");
        DEFINE_STATUS(ERROR_UNSUPPORTED_CODEC,  "unsupported codec");
        DEFINE_STATUS(ERROR_NO_DATA,            "not enough data");
        DEFINE_STATUS(ERROR_NO_SURFACE,         "no surface vailable");
        DEFINE_STATUS(ERROR_INVALID_SURFACE,    "invalid surface");
        DEFINE_STATUS(ERROR_BITSTREAM_PARSER,   "bitstream parser error");
        DEFINE_STATUS(ERROR_UNSUPPORTED_PROFILE,
                      "unsupported profile");
        DEFINE_STATUS(ERROR_UNSUPPORTED_CHROMA_FORMAT,
                      "unsupported chroma-format");
        DEFINE_STATUS(ERROR_INVALID_PARAMETER,  "invalid parameter");
    default:
        str = "<unknown>";
        break;
    }
#undef DEFINE_STATUS

    return str;
}

static const gchar *
get_error_string(AppError error)
{
    const gchar *str;

#define DEFINE_ERROR(error, error_string)       \
    case APP_ERROR_##error:                     \
        str = error_string;                     \
        break

    switch (error) {
        DEFINE_ERROR(NONE,      "<none>");
        DEFINE_ERROR(DECODER,   "decoder");
        DEFINE_ERROR(RENDERER,  "renderer");
    default:
        str = "unknown";
        break;
    }
#undef DEFINE_ERROR

    return str;
}

static void
decoder_release(App *app)
{
    g_mutex_lock(&app->mutex);
    g_cond_signal(&app->decoder_ready);
    g_mutex_unlock(&app->mutex);
}

static gpointer
decoder_thread(gpointer data)
{
    App * const app = data;
    GError *error = NULL;
    GstVaapiDecoderStatus status;
    GstVaapiSurfaceProxy *proxy;
    RenderFrame *rfp;
    GstBuffer *buffer;
    GstClockTime pts;
    gboolean got_surface, got_eos = FALSE;
    gint64 end_time;
    guint ofs;

    g_print("Decoder thread started\n");

#define SEND_ERROR(...)                                                 \
    do {                                                                \
        error = g_error_new(APP_ERROR, APP_ERROR_DECODER, __VA_ARGS__); \
        goto send_error;                                                \
    } while (0)

    pts = g_get_monotonic_time();
    ofs = 0;
    while (!app->decoder_thread_cancel) {
        if (G_UNLIKELY(ofs == app->file_size))
            buffer = NULL;
        else {
            const gsize size = MIN(4096, app->file_size - ofs);
            buffer = gst_buffer_new_wrapped_full(GST_MEMORY_FLAG_READONLY,
                app->file_data, app->file_size, ofs, size, NULL, NULL);
            if (!buffer)
                SEND_ERROR("failed to allocate new buffer");
            ofs += size;
        }
        if (!gst_vaapi_decoder_put_buffer(app->decoder, buffer))
            SEND_ERROR("failed to push buffer to decoder");
        gst_buffer_replace(&buffer, NULL);

    get_surface:
        status = gst_vaapi_decoder_get_surface(app->decoder, &proxy);
        switch (status) {
        case GST_VAAPI_DECODER_STATUS_SUCCESS:
            gst_vaapi_surface_proxy_set_destroy_notify(proxy,
                (GDestroyNotify)decoder_release, app);
            rfp = render_frame_new();
            if (!rfp)
                SEND_ERROR("failed to allocate render frame");
            rfp->proxy = proxy;
            rfp->pts = pts;
            rfp->duration = app->frame_duration;
            pts += app->frame_duration;
            g_async_queue_push(app->decoder_queue, rfp);
            break;
        case GST_VAAPI_DECODER_STATUS_ERROR_NO_DATA:
            /* nothing to do, just continue to the next iteration */
            break;
        case GST_VAAPI_DECODER_STATUS_END_OF_STREAM:
            gst_vaapi_decoder_flush(app->decoder);
            if (got_eos)
                goto send_eos;
            got_eos = TRUE;
            break;
        case GST_VAAPI_DECODER_STATUS_ERROR_NO_SURFACE:
            end_time = g_get_monotonic_time() + G_TIME_SPAN_SECOND;
            g_mutex_lock(&app->mutex);
            got_surface = g_cond_wait_until(&app->decoder_ready, &app->mutex,
                end_time);
            g_mutex_unlock(&app->mutex);
            if (got_surface)
                goto get_surface;
            SEND_ERROR("failed to acquire a surface within one second");
            break;
        default:
            SEND_ERROR("%s", get_decoder_status_string(status));
            break;
        }
    }
    return NULL;

#undef SEND_ERROR

send_eos:
    app_send_eos(app);
    return NULL;

send_error:
    app_send_error(app, error);
    return NULL;
}

static void
app_set_framerate(App *app, guint fps_n, guint fps_d)
{
    if (!fps_n || !fps_d)
        return;

    g_mutex_lock(&app->mutex);
    if (fps_n != app->fps_n || fps_d != app->fps_d) {
        app->fps_n = fps_n;
        app->fps_d = fps_d;
        app->frame_duration = gst_util_uint64_scale(
            GST_TIME_AS_USECONDS(GST_SECOND), fps_d, fps_n);
    }
    g_mutex_unlock(&app->mutex);
}

static void
handle_decoder_state_changes(GstVaapiDecoder *decoder,
    const GstVideoCodecState *codec_state, gpointer user_data)
{
    App * const app = user_data;

    g_assert(app->decoder == decoder);
    app_set_framerate(app, codec_state->info.fps_n, codec_state->info.fps_d);
}

static gboolean
start_decoder(App *app)
{
    GstCaps *caps;

    app->file = g_mapped_file_new(app->file_name, FALSE, NULL);
    if (!app->file)
        return FALSE;

    app->file_size = g_mapped_file_get_length(app->file);
    app->file_data = (guint8 *)g_mapped_file_get_contents(app->file);
    if (!app->file_data)
        return FALSE;

    caps = caps_from_codec(app->codec);
    switch (app->codec) {
    case GST_VAAPI_CODEC_H264:
        app->decoder = gst_vaapi_decoder_h264_new(app->display, caps);
        break;
#if USE_JPEG_DECODER
    case GST_VAAPI_CODEC_JPEG:
        app->decoder = gst_vaapi_decoder_jpeg_new(app->display, caps);
        break;
#endif
    case GST_VAAPI_CODEC_MPEG2:
        app->decoder = gst_vaapi_decoder_mpeg2_new(app->display, caps);
        break;
    case GST_VAAPI_CODEC_MPEG4:
        app->decoder = gst_vaapi_decoder_mpeg4_new(app->display, caps);
        break;
    case GST_VAAPI_CODEC_VC1:
        app->decoder = gst_vaapi_decoder_vc1_new(app->display, caps);
        break;
    default:
        app->decoder = NULL;
        break;
    }
    if (!app->decoder)
        return FALSE;

    gst_vaapi_decoder_set_codec_state_changed_func(app->decoder,
        handle_decoder_state_changes, app);

    g_timer_start(app->timer);

    app->decoder_thread = g_thread_try_new("Decoder Thread", decoder_thread,
        app, NULL);
    if (!app->decoder_thread)
        return FALSE;
    return TRUE;
}

static gboolean
stop_decoder(App *app)
{
    g_timer_stop(app->timer);

    app->decoder_thread_cancel = TRUE;
    g_thread_join(app->decoder_thread);
    g_print("Decoder thread stopped\n");
    return TRUE;
}

static void
ensure_window_size(App *app, GstVaapiSurface *surface)
{
    guint width, height;

    if (gst_vaapi_window_get_fullscreen(app->window))
        return;

    gst_vaapi_surface_get_size(surface, &width, &height);
    if (app->surface_width == width && app->surface_height == height)
        return;
    app->surface_width = width;
    app->surface_height = height;

    gst_vaapi_window_set_size(app->window, width, height);
    gst_vaapi_window_get_size(app->window,
        &app->window_width, &app->window_height);
}

static gboolean
ensure_pixmaps(App *app, GstVaapiSurface *surface,
    const GstVaapiRectangle *crop_rect)
{
    GstVaapiPixmap *pixmaps[G_N_ELEMENTS(app->pixmaps)];
    guint num_pixmaps, i, width, height;
    gboolean success = FALSE;

    if (crop_rect) {
        width  = crop_rect->width;
        height = crop_rect->height;
    }
    else
        gst_vaapi_surface_get_size(surface, &width, &height);
    if (app->pixmap_width == width && app->pixmap_height == height)
        return TRUE;

    for (i = 0, num_pixmaps = 0; i < G_N_ELEMENTS(pixmaps); i++) {
        GstVaapiPixmap * const pixmap =
            video_output_create_pixmap(app->display, GST_VIDEO_FORMAT_xRGB,
                width, height);
        if (!pixmap)
            goto end;
        pixmaps[num_pixmaps++] = pixmap;
    }

    for (i = 0; i < num_pixmaps; i++)
        gst_vaapi_pixmap_replace(&app->pixmaps[i], pixmaps[i]);
    app->pixmap_width = width;
    app->pixmap_height = height;
    success = TRUE;

end:
    for (i = 0; i < num_pixmaps; i++)
        gst_vaapi_pixmap_replace(&pixmaps[i], NULL);
    return success;
}

static inline void
renderer_wait_until(App *app, GstClockTime pts)
{
    g_mutex_lock(&app->mutex);
    do {
    } while (g_cond_wait_until(&app->render_ready, &app->mutex, pts));
    g_mutex_unlock(&app->mutex);
}

static gboolean
renderer_process(App *app, RenderFrame *rfp)
{
    GError *error = NULL;
    GstVaapiSurface *surface;
    const GstVaapiRectangle *crop_rect;

#define SEND_ERROR(...)                                                 \
    do {                                                                \
        error = g_error_new(APP_ERROR, APP_ERROR_RENDERER, __VA_ARGS__); \
        goto send_error;                                                \
    } while (0)

    surface = gst_vaapi_surface_proxy_get_surface(rfp->proxy);
    if (!surface)
        SEND_ERROR("failed to get decoded surface from render frame");

    ensure_window_size(app, surface);

    crop_rect = gst_vaapi_surface_proxy_get_crop_rect(rfp->proxy);
    if (g_use_pixmap && !ensure_pixmaps(app, surface, crop_rect))
        SEND_ERROR("failed to create intermediate pixmaps");

    if (!gst_vaapi_surface_sync(surface))
        SEND_ERROR("failed to sync decoded surface");

    if (G_LIKELY(!g_benchmark))
        renderer_wait_until(app, rfp->pts);

    if (G_UNLIKELY(g_use_pixmap)) {
        GstVaapiPixmap * const pixmap = app->pixmaps[app->pixmap_id];

        if (!gst_vaapi_pixmap_put_surface(pixmap, surface, crop_rect,
                GST_VAAPI_PICTURE_STRUCTURE_FRAME))
            SEND_ERROR("failed to render to pixmap");

        if (!gst_vaapi_window_put_pixmap(app->window, pixmap, NULL, NULL))
            SEND_ERROR("failed to render surface %" GST_VAAPI_ID_FORMAT,
                       GST_VAAPI_ID_ARGS(pixmap));

        app->pixmap_id = (app->pixmap_id + 1) % G_N_ELEMENTS(app->pixmaps);
    }
    else if (!gst_vaapi_window_put_surface(app->window, surface,
                 crop_rect, NULL, GST_VAAPI_PICTURE_STRUCTURE_FRAME))
        SEND_ERROR("failed to render surface %" GST_VAAPI_ID_FORMAT,
                   GST_VAAPI_ID_ARGS(gst_vaapi_surface_get_id(surface)));

    app->num_frames++;

    render_frame_replace(&app->last_frame, rfp);
    return TRUE;

#undef SEND_ERROR

send_error:
    app_send_error(app, error);
    return FALSE;
}

static gpointer
renderer_thread(gpointer data)
{
    App * const app = data;
    RenderFrame *rfp;

    g_print("Render thread started\n");

    while (!app->render_thread_cancel) {
        rfp = g_async_queue_timeout_pop(app->decoder_queue, 1000000);
        if (rfp && !renderer_process(app, rfp))
            break;
    }
    return NULL;
}

static gboolean
flush_decoder_queue(App *app)
{
    RenderFrame *rfp;

    /* Flush pending surfaces */
    do {
        rfp = g_async_queue_try_pop(app->decoder_queue);
        if (!rfp)
            return TRUE;
    } while (renderer_process(app, rfp));
    return FALSE;
}

static gboolean
start_renderer(App *app)
{
    app->render_thread = g_thread_try_new("Renderer Thread", renderer_thread,
        app, NULL);
    if (!app->render_thread)
        return FALSE;
    return TRUE;
}

static gboolean
stop_renderer(App *app)
{
    app->render_thread_cancel = TRUE;
    g_thread_join(app->render_thread);

    g_print("Render thread stopped\n");

    flush_decoder_queue(app);
    render_frame_replace(&app->last_frame, NULL);
    return TRUE;
}

static void
app_free(App *app)
{
    guint i;

    if (!app)
        return;

    if (app->file) {
        g_mapped_file_unref(app->file);
        app->file = NULL;
    }
    g_free(app->file_name);

    for (i = 0; i < G_N_ELEMENTS(app->pixmaps); i++)
        gst_vaapi_pixmap_replace(&app->pixmaps[i], NULL);
    gst_vaapi_decoder_replace(&app->decoder, NULL);
    gst_vaapi_window_replace(&app->window, NULL);
    gst_vaapi_display_replace(&app->display, NULL);

    if (app->decoder_queue) {
        g_async_queue_unref(app->decoder_queue);
        app->decoder_queue = NULL;
    }
    g_cond_clear(&app->decoder_ready);

    if (app->timer) {
        g_timer_destroy(app->timer);
        app->timer = NULL;
    }

    g_cond_clear(&app->render_ready);
    g_cond_clear(&app->event_cond);
    g_mutex_clear(&app->mutex);
    g_slice_free(App, app);
}

static App *
app_new(void)
{
    App *app;

    app = g_slice_new0(App);
    if (!app)
        return NULL;

    g_mutex_init(&app->mutex);
    g_cond_init(&app->event_cond);
    g_cond_init(&app->decoder_ready);
    g_cond_init(&app->render_ready);

    app_set_framerate(app, 60, 1);
    app->window_width = 640;
    app->window_height = 480;

    app->decoder_queue = g_async_queue_new_full(
        (GDestroyNotify)render_frame_free);
    if (!app->decoder_queue)
        goto error;

    app->timer = g_timer_new();
    if (!app->timer)
        goto error;
    return app;

error:
    app_free(app);
    return NULL;
}

static gboolean
app_check_events(App *app)
{
    GError *error = NULL;
    gboolean stop = FALSE;

    do {
        g_mutex_lock(&app->mutex);
        while (app->event == APP_RUNNING)
            g_cond_wait(&app->event_cond, &app->mutex);

        switch (app->event) {
        case APP_GOT_ERROR:
            error = app->error;
            app->error = NULL;
            /* fall-through */
        case APP_GOT_EOS:
            stop = TRUE;
            break;
        default:
            break;
        }
        g_mutex_unlock(&app->mutex);
    } while (!stop);

    if (!error)
        return TRUE;

    g_message("%s error: %s", get_error_string(error->code), error->message);
    g_error_free(error);
    return FALSE;
}

static gboolean
app_run(App *app, int argc, char *argv[])
{
    if (argc < 2) {
        g_message("no bitstream file specified");
        return FALSE;
    }
    app->file_name = g_strdup(argv[1]);

    if (!g_file_test(app->file_name, G_FILE_TEST_IS_REGULAR)) {
        g_message("failed to find file '%s'", app->file_name);
        return FALSE;
    }

    app->codec = identify_codec(app->file_name);
    if (!app->codec) {
        app->codec = identify_codec_from_string(g_codec_str);
        if (!app->codec) {
            g_message("failed to identify codec for '%s'", app->file_name);
            return FALSE;
        }
    }

    g_print("Simple decoder (%s bitstream)\n", string_from_codec(app->codec));

    app->display = video_output_create_display(NULL);
    if (!app->display) {
        g_message("failed to create VA display");
        return FALSE;
    }

    app->window = video_output_create_window(app->display,
        app->window_width, app->window_height);
    if (!app->window) {
        g_message("failed to create window");
        return FALSE;
    }

    gst_vaapi_window_show(app->window);

    if (!start_decoder(app)) {
        g_message("failed to start decoder thread");
        return FALSE;
    }

    if (!start_renderer(app)) {
        g_message("failed to start renderer thread");
        return FALSE;
    }

    app_check_events(app);

    stop_renderer(app);
    stop_decoder(app);

    g_print("Decoded %u frames", app->num_frames);
    if (g_benchmark) {
        const gdouble elapsed = g_timer_elapsed(app->timer, NULL);
        g_print(" in %.2f sec (%.1f fps)\n",
                elapsed, (gdouble)app->num_frames / elapsed);
    }
    g_print("\n");
    return TRUE;
}

int
main(int argc, char *argv[])
{
    App *app;
    gint ret;

    if (!video_output_init(&argc, argv, g_options))
        g_error("failed to initialize video output subsystem");

    app = app_new();
    if (!app)
        g_error("failed to create application context");

    ret = !app_run(app, argc, argv);

    app_free(app);
    g_free(g_codec_str);
    video_output_exit();
    return ret;
}
