#include "mpvwidget.hh"

#include <stdexcept>
#include <QtGui/QOpenGLContext>
#include <QtCore/QMetaObject>

#include "qthelper.hh"
#include "../mpv.h"

static void
wakeup(void *ctx)
{
    QMetaObject::invokeMethod(static_cast<MpvWidget *>(ctx), "on_mpv_events", Qt::QueuedConnection);
}

static void *
get_proc_address(void *ctx, const char *name)
{
    Q_UNUSED(ctx);
    QOpenGLContext *glctx = QOpenGLContext::currentContext();
    if (!glctx)
        return nullptr;
    return reinterpret_cast<void *>(glctx->getProcAddress(QByteArray(name)));
}

MpvWidget::MpvWidget(struct queue *queue, lkt_db *db, QWidget *parent)
    : QOpenGLWidget(parent)
    , m_queue(queue)
    , m_db(db)
{
    setFocusPolicy(Qt::StrongFocus);
    mpv = mpv_create();
    if (!mpv)
        throw std::runtime_error("could not create mpv context");

    mpv_set_option_string(mpv, "input-default-bindings", "yes");
    mpv_set_option_string(mpv, "input-vo-keyboard", "yes");
    int val = 1;
    mpv_set_option(mpv, "osc", MPV_FORMAT_FLAG, &val);

    mpv_set_option_string(mpv, "terminal", "yes");
    mpv_set_option_string(mpv, "msg-level", "all=v");
    if (mpv_initialize(mpv) < 0)
        throw std::runtime_error("could not initialize mpv context");

    // Request hw decoding, just for testing.
    mpv::qt::set_option_variant(mpv, "hwdec", "auto");

    mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_DOUBLE);
    mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_DOUBLE);
    mpv_set_wakeup_callback(mpv, wakeup, this);
}

MpvWidget::~MpvWidget()
{
    makeCurrent();
    if (mpv_gl)
        mpv_render_context_free(mpv_gl);
    mpv_terminate_destroy(mpv);
}

void
MpvWidget::command(const QVariant &params)
{
    mpv::qt::command(mpv, params);
}

void
MpvWidget::setProperty(const QString &name, const QVariant &value)
{
    mpv::qt::set_property(mpv, name, value);
}

QVariant
MpvWidget::getProperty(const QString &name) const
{
    return mpv::qt::get_property(mpv, name);
}

void
MpvWidget::initializeGL()
{
    mpv_opengl_init_params gl_init_params{ get_proc_address, nullptr, nullptr };
    mpv_render_param params[]{ { MPV_RENDER_PARAM_API_TYPE,
                                 const_cast<char *>(MPV_RENDER_API_TYPE_OPENGL) },
                               { MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params },
                               { MPV_RENDER_PARAM_INVALID, nullptr } };

    if (mpv_render_context_create(&mpv_gl, mpv, params) < 0)
        throw std::runtime_error("failed to initialize mpv GL context");
    mpv_render_context_set_update_callback(mpv_gl, MpvWidget::on_update,
                                           reinterpret_cast<void *>(this));
}

void
MpvWidget::paintGL()
{
    mpv_opengl_fbo mpfbo{ static_cast<int>(defaultFramebufferObject()), width(), height(), 0 };
    int flip_y{ 1 };

    mpv_render_param params[] = { { MPV_RENDER_PARAM_OPENGL_FBO, &mpfbo },
                                  { MPV_RENDER_PARAM_FLIP_Y, &flip_y },
                                  { MPV_RENDER_PARAM_INVALID, nullptr } };
    // See render_gl.h on what OpenGL environment mpv expects, and
    // other API details.
    mpv_render_context_render(mpv_gl, params);
}

void
MpvWidget::on_mpv_events()
{
    // Process all events, until the event queue is empty.
    while (mpv) {
        mpv_event *event = mpv_wait_event(mpv, 0);
        if (event->event_id == MPV_EVENT_NONE) {
            break;
        }
        handle_mpv_event(event);
    }
}

void
MpvWidget::handle_mpv_event(mpv_event *event)
{
    switch (event->event_id) {
    case MPV_EVENT_PROPERTY_CHANGE: {
        mpv_event_property *prop = static_cast<mpv_event_property *>(event->data);
        if (strcmp(prop->name, "time-pos") == 0) {
            if (prop->format == MPV_FORMAT_DOUBLE) {
                double time = *static_cast<double *>(prop->data);
                Q_EMIT positionChanged(static_cast<int>(time));
            }
        } else if (strcmp(prop->name, "duration") == 0) {
            if (prop->format == MPV_FORMAT_DOUBLE) {
                double time = *static_cast<double *>(prop->data);
                Q_EMIT durationChanged(static_cast<int>(time));
            }
        }
        break;
    }

    case MPV_EVENT_SHUTDOWN:
    case MPV_EVENT_LOG_MESSAGE:
    case MPV_EVENT_GET_PROPERTY_REPLY:
    case MPV_EVENT_SET_PROPERTY_REPLY:
    case MPV_EVENT_NONE:
    case MPV_EVENT_COMMAND_REPLY:
    case MPV_EVENT_START_FILE:
    case MPV_EVENT_END_FILE:
    case MPV_EVENT_FILE_LOADED:
    case MPV_EVENT_TRACKS_CHANGED:
    case MPV_EVENT_TRACK_SWITCHED:
    case MPV_EVENT_IDLE:
    case MPV_EVENT_PAUSE:
    case MPV_EVENT_UNPAUSE:
    case MPV_EVENT_TICK:
    case MPV_EVENT_SCRIPT_INPUT_DISPATCH:
    case MPV_EVENT_CLIENT_MESSAGE:
    case MPV_EVENT_VIDEO_RECONFIG:
    case MPV_EVENT_AUDIO_RECONFIG:
    case MPV_EVENT_METADATA_UPDATE:
    case MPV_EVENT_SEEK:
    case MPV_EVENT_CHAPTER_CHANGE:
    case MPV_EVENT_PLAYBACK_RESTART:
    case MPV_EVENT_QUEUE_OVERFLOW:
    case MPV_EVENT_HOOK: {
        break;
    }
        // Ignore uninteresting or unknown events.
    }
}

// Make Qt invoke mpv_render_context_render() to draw a new/updated video frame.
void
MpvWidget::maybeUpdate()
{
    // If the Qt window is not visible, Qt's update() will just skip rendering.
    // This confuses mpv's render API, and may lead to small occasional
    // freezes due to video rendering timing out.
    // Handle this by manually redrawing.
    // Note: Qt doesn't seem to provide a way to query whether update() will
    //       be skipped, and the following code still fails when e.g. switching
    //       to a different workspace with a reparenting window manager.
    if (window()->isMinimized()) {
        makeCurrent();
        paintGL();
        context()->swapBuffers(context()->surface());
        doneCurrent();
    } else {
        update();
    }
}

void
MpvWidget::on_update(void *ctx)
{
    QMetaObject::invokeMethod(static_cast<MpvWidget *>(ctx), "maybeUpdate");
}

bool
MpvWidget::get_elapsed(int *elapsed_sec)
{
    (void)elapsed_sec;
    return true;
}

bool
MpvWidget::get_duration(int *dur_sec)
{
    (void)dur_sec;
    return true;
}

bool
MpvWidget::set_paussed(int paused)
{
    (void)paused;
    return true;
}

bool
MpvWidget::set_volume(int vol)
{
    (void)vol;
    return true;
}

bool
MpvWidget::set_position(int sec)
{
    (void)sec;
    return true;
}

bool
MpvWidget::load_file(const char *filepath)
{
    const bool ret = !lmpv_load_file(mpv, filepath);

    if (ret) {
        LOG_DEBUG("WINDOW", "Loaded file: %s", filepath);
        //set_window_title(win);
    } else {
        LOG_ERROR("WINDOW", "Failed to load kara with path: %s", filepath);
    }

    //LOG_DEBUG("WINDOW", "Hinib flag at %d", win->hinib);
    return ret;
}

bool
MpvWidget::toggle_pause()
{
    return true;
}