/****************************************************************************
**
** Copyright (C) 2016 Jolla Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include <qvideoframe.h>
#include <qvideosink.h>
#include <QDebug>
#include <QMap>
#include <QThread>
#include <QEvent>
#include <QCoreApplication>

#include <private/qfactoryloader_p.h>
#include "qgstvideobuffer_p.h"
#include "qgstreamervideosink_p.h"

#include "qgstvideorenderersink_p.h"

#include <gst/video/video.h>
#include <gst/video/gstvideometa.h>
#include <qloggingcategory.h>
#include <qdebug.h>

#include "qgstutils_p.h"

#include <QtGui/private/qrhi_p.h>
#if QT_CONFIG(gstreamer_gl)
#include <gst/gl/gl.h>
#endif // #if QT_CONFIG(gstreamer_gl)

// DMA support
#if QT_CONFIG(linux_dmabuf)
#include <gst/allocators/gstdmabuf.h>
#endif

//#define DEBUG_VIDEO_SURFACE_SINK

Q_LOGGING_CATEGORY(qLcGstVideoRenderer, "qt.multimedia.gstvideorenderer")

QT_BEGIN_NAMESPACE

QGstVideoRenderer::QGstVideoRenderer(QGstreamerVideoSink *sink)
    : m_sink(sink)
{
    createSurfaceCaps();
}

QGstVideoRenderer::~QGstVideoRenderer()
{
}

void QGstVideoRenderer::createSurfaceCaps()
{
    QRhi *rhi = m_sink->rhi();
    Q_UNUSED(rhi);

    QGstMutableCaps caps;
    caps.create();

    // All the formats that both we and gstreamer support
    auto formats = QList<QVideoFrameFormat::PixelFormat>()
                   << QVideoFrameFormat::Format_YUV420P
                   << QVideoFrameFormat::Format_YUV422P
                   << QVideoFrameFormat::Format_YV12
                   << QVideoFrameFormat::Format_UYVY
                   << QVideoFrameFormat::Format_YUYV
                   << QVideoFrameFormat::Format_NV12
                   << QVideoFrameFormat::Format_NV21
                   << QVideoFrameFormat::Format_AYUV
                   << QVideoFrameFormat::Format_P010
                   << QVideoFrameFormat::Format_XRGB8888
                   << QVideoFrameFormat::Format_XBGR8888
                   << QVideoFrameFormat::Format_RGBX8888
                   << QVideoFrameFormat::Format_BGRX8888
                   << QVideoFrameFormat::Format_ARGB8888
                   << QVideoFrameFormat::Format_ABGR8888
                   << QVideoFrameFormat::Format_RGBA8888
                   << QVideoFrameFormat::Format_BGRA8888
                   << QVideoFrameFormat::Format_Y8
                   << QVideoFrameFormat::Format_Y16
        ;
#if QT_CONFIG(gstreamer_gl)
    if (rhi && rhi->backend() == QRhi::OpenGLES2) {
        caps.addPixelFormats(formats, GST_CAPS_FEATURE_MEMORY_GL_MEMORY);
#if QT_CONFIG(linux_dmabuf)
        if (m_sink->eglDisplay() && m_sink->eglImageTargetTexture2D()) {
            // We currently do not handle planar DMA buffers, as it's somewhat unclear how to
            // convert the planar EGLImage into something we can use from OpenGL
            auto singlePlaneFormats = QList<QVideoFrameFormat::PixelFormat>()
                           << QVideoFrameFormat::Format_UYVY
                           << QVideoFrameFormat::Format_YUYV
                           << QVideoFrameFormat::Format_AYUV
                           << QVideoFrameFormat::Format_XRGB8888
                           << QVideoFrameFormat::Format_XBGR8888
                           << QVideoFrameFormat::Format_RGBX8888
                           << QVideoFrameFormat::Format_BGRX8888
                           << QVideoFrameFormat::Format_ARGB8888
                           << QVideoFrameFormat::Format_ABGR8888
                           << QVideoFrameFormat::Format_RGBA8888
                           << QVideoFrameFormat::Format_BGRA8888
                           << QVideoFrameFormat::Format_Y8
                           << QVideoFrameFormat::Format_Y16
                ;
            caps.addPixelFormats(singlePlaneFormats, GST_CAPS_FEATURE_MEMORY_DMABUF);
        }
#endif
    }
#endif
    caps.addPixelFormats(formats);

    m_surfaceCaps = caps;
}

QGstMutableCaps QGstVideoRenderer::caps()
{
    QMutexLocker locker(&m_mutex);

    return m_surfaceCaps;
}

bool QGstVideoRenderer::start(GstCaps *caps)
{
    qCDebug(qLcGstVideoRenderer) << "QGstVideoRenderer::start" << QGstCaps(caps).toString();
    QMutexLocker locker(&m_mutex);

    m_frameMirrored = false;
    m_frameRotationAngle = QVideoFrame::Rotation0;

    if (m_active) {
        m_flush = true;
        m_stop = true;
    }

    m_startCaps = QGstMutableCaps(caps, QGstMutableCaps::NeedsRef);

    /*
    Waiting for start() to be invoked in the main thread may block
    if gstreamer blocks the main thread until this call is finished.
    This situation is rare and usually caused by setState(Null)
    while pipeline is being prerolled.

    The proper solution to this involves controlling gstreamer pipeline from
    other thread than video surface.

    Currently start() fails if wait() timed out.
    */
    if (!waitForAsyncEvent(&locker, &m_setupCondition, 1000) && !m_startCaps.isNull()) {
        qWarning() << "Failed to start video surface due to main thread blocked.";
        m_startCaps = {};
    }

    return m_active;
}

void QGstVideoRenderer::stop()
{
    QMutexLocker locker(&m_mutex);

    if (!m_active)
        return;

    m_flush = true;
    m_stop = true;

    m_startCaps = {};

    waitForAsyncEvent(&locker, &m_setupCondition, 500);
}

void QGstVideoRenderer::unlock()
{
    QMutexLocker locker(&m_mutex);

    m_setupCondition.wakeAll();
    m_renderCondition.wakeAll();
}

bool QGstVideoRenderer::proposeAllocation(GstQuery *query)
{
    Q_UNUSED(query);
    QMutexLocker locker(&m_mutex);
    return m_active;
}

void QGstVideoRenderer::flush()
{
    QMutexLocker locker(&m_mutex);

    m_flush = true;
    m_renderBuffer = nullptr;
    m_renderCondition.wakeAll();

    notify();
}

GstFlowReturn QGstVideoRenderer::render(GstBuffer *buffer)
{
    QMutexLocker locker(&m_mutex);
    qCDebug(qLcGstVideoRenderer) << "QGstVideoRenderer::render";

    m_renderReturn = GST_FLOW_OK;
    m_renderBuffer = buffer;

    waitForAsyncEvent(&locker, &m_renderCondition, 300);

    m_renderBuffer = nullptr;

    return m_renderReturn;
}

bool QGstVideoRenderer::query(GstQuery *query)
{
#if QT_CONFIG(gstreamer_gl)
    if (GST_QUERY_TYPE(query) == GST_QUERY_CONTEXT) {
        const gchar *type;
        gst_query_parse_context_type(query, &type);

        if (strcmp(type, "gst.gl.local_context") != 0)
            return false;

        auto *gstGlContext = m_sink->gstGlLocalContext();
        if (!gstGlContext)
            return false;

        gst_query_set_context(query, gstGlContext);

        return true;
    }
#else
    Q_UNUSED(query);
#endif
    return false;
}

void QGstVideoRenderer::gstEvent(GstEvent *event)
{
    if (GST_EVENT_TYPE(event) != GST_EVENT_TAG)
        return;

    GstTagList *taglist = nullptr;
    gst_event_parse_tag(event, &taglist);
    if (!taglist)
        return;

    gchar *value = nullptr;
    if (!gst_tag_list_get_string(taglist, GST_TAG_IMAGE_ORIENTATION, &value))
        return;

    constexpr const char rotate[] = "rotate-";
    constexpr const char flipRotate[] = "flip-rotate-";
    constexpr size_t rotateLen = sizeof(rotate) - 1;
    constexpr size_t flipRotateLen = sizeof(flipRotate) - 1;

    bool mirrored = false;
    int rotationAngle = 0;

    if (!strncmp(rotate, value, rotateLen)) {
        rotationAngle = atoi(value + rotateLen);
    } else if (!strncmp(flipRotate, value, flipRotateLen)) {
        // To flip by horizontal axis is the same as to mirror by vertical axis
        // and rotate by 180 degrees.
        mirrored = true;
        rotationAngle = (180 + atoi(value + flipRotateLen)) % 360;
    }

    QMutexLocker locker(&m_mutex);
    m_frameMirrored = mirrored;
    switch (rotationAngle) {
    case 0: m_frameRotationAngle = QVideoFrame::Rotation0; break;
    case 90: m_frameRotationAngle = QVideoFrame::Rotation90; break;
    case 180: m_frameRotationAngle = QVideoFrame::Rotation180; break;
    case 270: m_frameRotationAngle = QVideoFrame::Rotation270; break;
    default: m_frameRotationAngle = QVideoFrame::Rotation0;
    }
}

bool QGstVideoRenderer::event(QEvent *event)
{
    if (event->type() == QEvent::UpdateRequest) {
        QMutexLocker locker(&m_mutex);

        if (m_notified) {
            while (handleEvent(&locker)) {}
            m_notified = false;
        }
        return true;
    }

    return QObject::event(event);
}

bool QGstVideoRenderer::handleEvent(QMutexLocker<QMutex> *locker)
{
    if (m_flush) {
        m_flush = false;
        if (m_active) {
            locker->unlock();

            if (m_sink && !m_flushed)
                m_sink->setVideoFrame(QVideoFrame());
            m_flushed = true;
            locker->relock();
        }
    } else if (m_stop) {
        m_stop = false;

        if (m_active) {
            m_active = false;
            m_flushed = true;
        }
    } else if (!m_startCaps.isNull()) {
        Q_ASSERT(!m_active);

        auto startCaps = m_startCaps;
        m_startCaps = nullptr;

        if (m_sink) {
            locker->unlock();

            m_flushed = true;
            m_format = startCaps.formatForCaps(&m_videoInfo);
            memoryFormat = startCaps.memoryFormat();

            locker->relock();
            m_active = m_format.isValid();
        } else if (m_active) {
            m_active = false;
            m_flushed = true;
        }

    } else if (m_renderBuffer) {
        GstBuffer *buffer = m_renderBuffer;
        m_renderBuffer = nullptr;
        m_renderReturn = GST_FLOW_ERROR;

        qCDebug(qLcGstVideoRenderer) << "QGstVideoRenderer::handleEvent(renderBuffer)" << m_active << m_sink;
        if (m_active && m_sink) {
            gst_buffer_ref(buffer);

            locker->unlock();

            m_flushed = false;

            auto meta = gst_buffer_get_video_crop_meta (buffer);
            if (meta) {
                QRect vp(meta->x, meta->y, meta->width, meta->height);
                if (m_format.viewport() != vp) {
                    qCDebug(qLcGstVideoRenderer) << Q_FUNC_INFO << " Update viewport on Metadata: [" << meta->height << "x" << meta->width << " | " << meta->x << "x" << meta->y << "]";
                    // Update viewport if data is not the same
                    m_format.setViewport(vp);
                }
            }

            if (m_sink->inStoppedState()) {
                qCDebug(qLcGstVideoRenderer) << "    sending empty video frame";
                m_sink->setVideoFrame(QVideoFrame());
            } else {
                QGstVideoBuffer *videoBuffer = new QGstVideoBuffer(buffer, m_videoInfo, m_sink, m_format, memoryFormat);
                QVideoFrame frame(videoBuffer, m_format);
                QGstUtils::setFrameTimeStamps(&frame, buffer);
                frame.setMirrored(m_frameMirrored);
                frame.setRotationAngle(m_frameRotationAngle);

                qCDebug(qLcGstVideoRenderer) << "    sending video frame";
                m_sink->setVideoFrame(frame);
            }

            gst_buffer_unref(buffer);

            locker->relock();

            m_renderReturn = GST_FLOW_OK;
        }

        m_renderCondition.wakeAll();
    } else {
        m_setupCondition.wakeAll();

        return false;
    }
    return true;
}

void QGstVideoRenderer::notify()
{
    if (!m_notified) {
        m_notified = true;
        QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest));
    }
}

bool QGstVideoRenderer::waitForAsyncEvent(
        QMutexLocker<QMutex> *locker, QWaitCondition *condition, unsigned long time)
{
    if (QThread::currentThread() == thread()) {
        while (handleEvent(locker)) {}
        m_notified = false;

        return true;
    }

    notify();

    return condition->wait(&m_mutex, time);
}

static GstVideoSinkClass *sink_parent_class;
static thread_local QGstreamerVideoSink *current_sink;

#define VO_SINK(s) QGstVideoRendererSink *sink(reinterpret_cast<QGstVideoRendererSink *>(s))

QGstVideoRendererSink *QGstVideoRendererSink::createSink(QGstreamerVideoSink *sink)
{
    setSink(sink);
    QGstVideoRendererSink *gstSink = reinterpret_cast<QGstVideoRendererSink *>(
            g_object_new(QGstVideoRendererSink::get_type(), nullptr));

    g_signal_connect(G_OBJECT(gstSink), "notify::show-preroll-frame", G_CALLBACK(handleShowPrerollChange), gstSink);

    return gstSink;
}

void QGstVideoRendererSink::setSink(QGstreamerVideoSink *sink)
{
    current_sink = sink;
    get_type();
}

GType QGstVideoRendererSink::get_type()
{
    static GType type = 0;

    if (type == 0) {
        static const GTypeInfo info =
        {
            sizeof(QGstVideoRendererSinkClass),                    // class_size
            base_init,                                         // base_init
            nullptr,                                           // base_finalize
            class_init,                                        // class_init
            nullptr,                                           // class_finalize
            nullptr,                                           // class_data
            sizeof(QGstVideoRendererSink),                         // instance_size
            0,                                                 // n_preallocs
            instance_init,                                     // instance_init
            nullptr                                                  // value_table
        };

        type = g_type_register_static(
                GST_TYPE_VIDEO_SINK, "QGstVideoRendererSink", &info, GTypeFlags(0));

        // Register the sink type to be used in custom piplines.
        // When surface is ready the sink can be used.
        gst_element_register(nullptr, "qtvideosink", GST_RANK_PRIMARY, type);
    }

    return type;
}

void QGstVideoRendererSink::class_init(gpointer g_class, gpointer class_data)
{
    Q_UNUSED(class_data);

    sink_parent_class = reinterpret_cast<GstVideoSinkClass *>(g_type_class_peek_parent(g_class));

    GstVideoSinkClass *video_sink_class = reinterpret_cast<GstVideoSinkClass *>(g_class);
    video_sink_class->show_frame = QGstVideoRendererSink::show_frame;

    GstBaseSinkClass *base_sink_class = reinterpret_cast<GstBaseSinkClass *>(g_class);
    base_sink_class->get_caps = QGstVideoRendererSink::get_caps;
    base_sink_class->set_caps = QGstVideoRendererSink::set_caps;
    base_sink_class->propose_allocation = QGstVideoRendererSink::propose_allocation;
    base_sink_class->stop = QGstVideoRendererSink::stop;
    base_sink_class->unlock = QGstVideoRendererSink::unlock;
    base_sink_class->query = QGstVideoRendererSink::query;
    base_sink_class->event = QGstVideoRendererSink::event;

    GstElementClass *element_class = reinterpret_cast<GstElementClass *>(g_class);
    element_class->change_state = QGstVideoRendererSink::change_state;
    gst_element_class_set_metadata(element_class,
        "Qt built-in video renderer sink",
        "Sink/Video",
        "Qt default built-in video renderer sink",
        "The Qt Company");

    GObjectClass *object_class = reinterpret_cast<GObjectClass *>(g_class);
    object_class->finalize = QGstVideoRendererSink::finalize;
}

void QGstVideoRendererSink::base_init(gpointer g_class)
{
    static GstStaticPadTemplate sink_pad_template = GST_STATIC_PAD_TEMPLATE(
            "sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS(
                    "video/x-raw, "
                    "framerate = (fraction) [ 0, MAX ], "
                    "width = (int) [ 1, MAX ], "
                    "height = (int) [ 1, MAX ]"));

    gst_element_class_add_pad_template(
            GST_ELEMENT_CLASS(g_class), gst_static_pad_template_get(&sink_pad_template));
}

void QGstVideoRendererSink::instance_init(GTypeInstance *instance, gpointer g_class)
{
    Q_UNUSED(g_class);
    VO_SINK(instance);

    Q_ASSERT(current_sink);

    sink->renderer = new QGstVideoRenderer(current_sink);
    sink->renderer->moveToThread(current_sink->thread());
    current_sink = nullptr;
}

void QGstVideoRendererSink::finalize(GObject *object)
{
    VO_SINK(object);

    delete sink->renderer;

    // Chain up
    G_OBJECT_CLASS(sink_parent_class)->finalize(object);
}

void QGstVideoRendererSink::handleShowPrerollChange(GObject *o, GParamSpec *p, gpointer d)
{
    Q_UNUSED(o);
    Q_UNUSED(p);
    QGstVideoRendererSink *sink = reinterpret_cast<QGstVideoRendererSink *>(d);

    gboolean showPrerollFrame = true; // "show-preroll-frame" property is true by default
    g_object_get(G_OBJECT(sink), "show-preroll-frame", &showPrerollFrame, nullptr);

    if (!showPrerollFrame) {
        GstState state = GST_STATE_VOID_PENDING;
        GstClockTime timeout = 10000000; // 10 ms
        gst_element_get_state(GST_ELEMENT(sink), &state, nullptr, timeout);
        // show-preroll-frame being set to 'false' while in GST_STATE_PAUSED means
        // the QMediaPlayer was stopped from the paused state.
        // We need to flush the current frame.
        if (state == GST_STATE_PAUSED)
            sink->renderer->flush();
    }
}

GstStateChangeReturn QGstVideoRendererSink::change_state(
        GstElement *element, GstStateChange transition)
{
    QGstVideoRendererSink *sink = reinterpret_cast<QGstVideoRendererSink *>(element);

    gboolean showPrerollFrame = true; // "show-preroll-frame" property is true by default
    g_object_get(G_OBJECT(element), "show-preroll-frame", &showPrerollFrame, nullptr);

    // If show-preroll-frame is 'false' when transitioning from GST_STATE_PLAYING to
    // GST_STATE_PAUSED, it means the QMediaPlayer was stopped.
    // We need to flush the current frame.
    if (transition == GST_STATE_CHANGE_PLAYING_TO_PAUSED && !showPrerollFrame)
        sink->renderer->flush();

    return GST_ELEMENT_CLASS(sink_parent_class)->change_state(element, transition);
}

GstCaps *QGstVideoRendererSink::get_caps(GstBaseSink *base, GstCaps *filter)
{
    VO_SINK(base);

    QGstMutableCaps caps = sink->renderer->caps();
    if (filter)
        caps = gst_caps_intersect(caps.get(), filter);

    gst_caps_ref(caps.get());
    return caps.get();
}

gboolean QGstVideoRendererSink::set_caps(GstBaseSink *base, GstCaps *caps)
{
    VO_SINK(base);

    qCDebug(qLcGstVideoRenderer) << "set_caps:" << QGstCaps(caps).toString();

    if (!caps) {
        sink->renderer->stop();

        return TRUE;
    } else if (sink->renderer->start(caps)) {
        return TRUE;
    } else {
        return FALSE;
    }
}

gboolean QGstVideoRendererSink::propose_allocation(GstBaseSink *base, GstQuery *query)
{
    VO_SINK(base);
    return sink->renderer->proposeAllocation(query);
}

gboolean QGstVideoRendererSink::stop(GstBaseSink *base)
{
    VO_SINK(base);
    sink->renderer->stop();
    return TRUE;
}

gboolean QGstVideoRendererSink::unlock(GstBaseSink *base)
{
    VO_SINK(base);
    sink->renderer->unlock();
    return TRUE;
}

GstFlowReturn QGstVideoRendererSink::show_frame(GstVideoSink *base, GstBuffer *buffer)
{
    VO_SINK(base);
    return sink->renderer->render(buffer);
}

gboolean QGstVideoRendererSink::query(GstBaseSink *base, GstQuery *query)
{
    VO_SINK(base);
    if (sink->renderer->query(query))
        return TRUE;

    return GST_BASE_SINK_CLASS(sink_parent_class)->query(base, query);
}

gboolean QGstVideoRendererSink::event(GstBaseSink *base, GstEvent * event)
{
    VO_SINK(base);
    sink->renderer->gstEvent(event);
    return GST_BASE_SINK_CLASS(sink_parent_class)->event(base, event);
}

QT_END_NAMESPACE
