/*
 * Copyright (C) 2018-2020 Metrological Group B.V.
 * Copyright (C) 2018-2020 Igalia S.L. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * aint with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include "GStreamerVideoEncoder.h"

#if ENABLE(VIDEO) && ENABLE(MEDIA_STREAM) && USE(GSTREAMER)

#include "GStreamerCommon.h"
#include <wtf/StdMap.h>
#include <wtf/glib/WTFGType.h>

using namespace WebCore;

GST_DEBUG_CATEGORY(webrtc_venc_debug);
#define GST_CAT_DEFAULT webrtc_venc_debug

#define KBIT_TO_BIT 1024

static GstStaticPadTemplate sinkTemplate = GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS("video/x-raw(ANY)"));
static GstStaticPadTemplate srcTemplate = GST_STATIC_PAD_TEMPLATE("src", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS("video/x-h264;video/x-vp8;video/x-vp9"));

using SetBitrateFunc = Function<void(GObject* encoder, const char* propertyName, int bitrate)>;
using SetupFunc = Function<void(WebKitWebrtcVideoEncoder*)>;

struct EncoderDefinition {
    GRefPtr<GstCaps> caps;
    const char* name;
    const char* parserName;
    GRefPtr<GstCaps> encodedFormat;
    SetBitrateFunc setBitrate;
    SetupFunc setupEncoder;
    const char* bitratePropertyName;
    const char* keyframeIntervalPropertyName;
};

enum EncoderId {
    None,
    X264,
    OpenH264,
    OmxH264,
    Vp8,
    Vp9
};

class Encoders {
public:
    static StdMap<EncoderId, EncoderDefinition>& singleton()
    {
        static StdMap<EncoderId, EncoderDefinition> encoders;
        return encoders;
    }

    static void registerEncoder(EncoderId id, const char* name, const char* parserName, const char* caps, const char* encodedFormat,
        SetupFunc&& setupEncoder, const char* bitratePropertyName, SetBitrateFunc&& setBitrate, const char* keyframeIntervalPropertyName)
    {
        auto encoderFactory = adoptGRef(gst_element_factory_find(name));
        if (!encoderFactory)
            return;

        if (parserName) {
            auto parserFactory = adoptGRef(gst_element_factory_find(parserName));
            if (!parserFactory)
                return;
        }

        singleton().emplace(std::make_pair(id, (EncoderDefinition) {
            .caps = adoptGRef(gst_caps_from_string(caps)),
            .name = name,
            .parserName = parserName,
            .encodedFormat = encodedFormat ? adoptGRef(gst_caps_from_string(encodedFormat)) : nullptr,
            .setBitrate = WTFMove(setBitrate),
            .setupEncoder = WTFMove(setupEncoder),
            .bitratePropertyName = bitratePropertyName,
            .keyframeIntervalPropertyName = keyframeIntervalPropertyName,
        }));
    }

    static EncoderDefinition* definition(EncoderId id)
    {
        if (id == None)
            return nullptr;
        return &singleton()[id];
    }
};

struct _WebKitWebrtcVideoEncoderPrivate {
    EncoderId encoderId;
    GRefPtr<GstElement> encoder;
    GRefPtr<GstElement> parser;
    unsigned bitrate;
};

#define webkit_webrtc_video_encoder_parent_class parent_class
WEBKIT_DEFINE_TYPE_WITH_CODE(WebKitWebrtcVideoEncoder, webkit_webrtc_video_encoder, GST_TYPE_BIN,
    GST_DEBUG_CATEGORY_INIT(webrtc_venc_debug, "webkitwebrtcencoder", 0, "Video encoder for WebRTC"))

enum {
    PROP_FORMAT = 1,
    PROP_ENCODER,
    PROP_BITRATE,
    PROP_KEYFRAME_INTERVAL,
    N_PROPS
};

static void webrtcVideoEncoderGetProperty(GObject* object, guint prop_id, GValue* value, GParamSpec* pspec)
{
    auto* self = WEBKIT_WEBRTC_VIDEO_ENCODER(object);
    auto* priv = self->priv;

    switch (prop_id) {
    case PROP_FORMAT:
        if (priv->encoderId != None) {
            auto encoder = Encoders::definition(priv->encoderId);
            g_value_set_boxed(value, encoder->caps.get());
        } else
            g_value_set_boxed(value, nullptr);
        break;
    case PROP_ENCODER:
        g_value_set_object(value, priv->encoder.get());
        break;
    case PROP_BITRATE:
        g_value_set_uint(value, priv->bitrate);
        break;
    case PROP_KEYFRAME_INTERVAL:
        if (priv->encoder) {
            auto encoder = Encoders::definition(priv->encoderId);
            g_object_get_property(G_OBJECT(priv->encoder.get()), encoder->keyframeIntervalPropertyName, value);
        }
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void webrtcVideoEncoderSetBitrate(WebKitWebrtcVideoEncoder* self, guint bitrate)
{
    auto* priv = self->priv;
    priv->bitrate = bitrate;

    if (priv->encoderId != None) {
        auto encoder = Encoders::definition(priv->encoderId);
        encoder->setBitrate(G_OBJECT(priv->encoder.get()), encoder->bitratePropertyName, priv->bitrate);
    }
}

static void webrtcVideoEncoderSetEncoder(WebKitWebrtcVideoEncoder* self, GRefPtr<GstElement>&& encoderElement, EncoderId encoderId)
{
    auto* priv = self->priv;
    priv->encoderId = encoderId;
    priv->encoder = WTFMove(encoderElement);

    auto encoder = Encoders::definition(encoderId);
    ASSERT(encoder);
    if (encoder->parserName)
        priv->parser = makeGStreamerElement(encoder->parserName, nullptr);

    encoder->setupEncoder(self);

    gst_bin_add(GST_BIN_CAST(self), priv->encoder.get());

    auto sinkPadTarget = adoptGRef(gst_element_get_static_pad(priv->encoder.get(), "sink"));
    auto sinkPad = adoptGRef(gst_element_get_static_pad(GST_ELEMENT_CAST(self), "sink"));
    gst_ghost_pad_set_target(GST_GHOST_PAD(sinkPad.get()), sinkPadTarget.get());

    GRefPtr<GstPad> srcPadTarget;
    if (priv->parser) {
        gst_bin_add(GST_BIN_CAST(self), priv->parser.get());
        gst_element_link(priv->encoder.get(), priv->parser.get());
        srcPadTarget = adoptGRef(gst_element_get_static_pad(priv->parser.get(), "src"));
    } else
        srcPadTarget = adoptGRef(gst_element_get_static_pad(priv->encoder.get(), "src"));

    if (encoder->encodedFormat) {
        auto* capsfilter = gst_element_factory_make("capsfilter", nullptr);
        g_object_set(capsfilter, "caps", encoder->encodedFormat.get(), nullptr);

        gst_bin_add(GST_BIN_CAST(self), capsfilter);
        auto sinkPad = adoptGRef(gst_element_get_static_pad(capsfilter, "sink"));
        gst_pad_link(srcPadTarget.get(), sinkPad.get());
        srcPadTarget = adoptGRef(gst_element_get_static_pad(capsfilter, "src"));
    }

    auto srcPad = adoptGRef(gst_element_get_static_pad(GST_ELEMENT_CAST(self), "src"));
    gst_ghost_pad_set_target(GST_GHOST_PAD(srcPad.get()), srcPadTarget.get());

    webrtcVideoEncoderSetBitrate(self, priv->bitrate);
}

static void webrtcVideoEncoderSetFormat(WebKitWebrtcVideoEncoder* self, const GstCaps* caps)
{
    if (!caps)
        return;

    for (const auto& pair : Encoders::singleton()) {
        const auto& encoder = pair.second;
        if (gst_caps_can_intersect(encoder.caps.get(), caps)) {
            GRefPtr<GstElement> element = makeGStreamerElement(encoder.name, nullptr);
            webrtcVideoEncoderSetEncoder(self, WTFMove(element), pair.first);
            return;
        }
    }

    GST_ERROR("No encoder found for format %" GST_PTR_FORMAT, caps);
}

static void webrtcVideoEncoderSetProperty(GObject* object, guint prop_id, const GValue* value, GParamSpec* pspec)
{
    auto* self = WEBKIT_WEBRTC_VIDEO_ENCODER(object);
    auto* priv = self->priv;

    switch (prop_id) {
    case PROP_FORMAT:
        webrtcVideoEncoderSetFormat(self, gst_value_get_caps(value));
        break;
    case PROP_BITRATE:
        webrtcVideoEncoderSetBitrate(self, g_value_get_uint(value));
        break;
    case PROP_KEYFRAME_INTERVAL:
        if (priv->encoder) {
            auto encoder = Encoders::definition(priv->encoderId);
            g_object_set(priv->encoder.get(), encoder->keyframeIntervalPropertyName, g_value_get_uint(value), nullptr);
        }
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void setBitrateKbitPerSec(GObject* encoder, const char* propertyName, int bitrate)
{
    g_object_set(encoder, propertyName, bitrate, nullptr);
}

static void setBitrateBitPerSec(GObject* encoder, const char* propertyName, int bitrate)
{
    g_object_set(encoder, propertyName, bitrate * KBIT_TO_BIT, nullptr);
}

static GRefPtr<GstCaps> createSrcPadTemplateCaps()
{
    auto* caps = gst_caps_new_empty();

    for (const auto& [id, encoder] : Encoders::singleton()) {
        if (encoder.encodedFormat)
            caps = gst_caps_merge(caps, gst_caps_ref(encoder.encodedFormat.get()));
        else
            caps = gst_caps_merge(caps, gst_caps_ref(encoder.caps.get()));
    }

    GST_DEBUG("Source pad template caps: %" GST_PTR_FORMAT, caps);
    return caps;
}

static void webrtcVideoEncoderConstructed(GObject* encoder)
{
    auto* self = WEBKIT_WEBRTC_VIDEO_ENCODER(encoder);
    self->priv->encoderId = None;
    gst_element_add_pad(GST_ELEMENT_CAST(self), webkitGstGhostPadFromStaticTemplate(&sinkTemplate, "sink", nullptr));
    gst_element_add_pad(GST_ELEMENT_CAST(self), webkitGstGhostPadFromStaticTemplate(&srcTemplate, "src", nullptr));
}

static void webkit_webrtc_video_encoder_class_init(WebKitWebrtcVideoEncoderClass* klass)
{
    GObjectClass* objectClass = G_OBJECT_CLASS(klass);
    objectClass->constructed = webrtcVideoEncoderConstructed;
    objectClass->get_property = webrtcVideoEncoderGetProperty;
    objectClass->set_property = webrtcVideoEncoderSetProperty;

    GstElementClass* elementClass = GST_ELEMENT_CLASS(klass);
    gst_element_class_set_static_metadata(elementClass, "WebKit WebRTC video encoder", "Codec/Encoder/Video", "Encodes video for streaming", "Igalia");
    gst_element_class_add_pad_template(elementClass, gst_static_pad_template_get(&sinkTemplate));

    Encoders::registerEncoder(OmxH264, "omxh264enc", "h264parse", "video/x-h264",
        "video/x-h264,alignment=au,stream-format=byte-stream,profile=baseline",
        [](WebKitWebrtcVideoEncoder* self) {
            gst_util_set_object_arg(G_OBJECT(self->priv->encoder.get()), "control-rate", "variable");
            g_object_set(self->priv->parser.get(), "config-interval", 1, nullptr);
        }, "target-bitrate", setBitrateBitPerSec, "interval-intraframes");
    Encoders::registerEncoder(X264, "x264enc", "h264parse", "video/x-h264",
        "video/x-h264,alignment=au,stream-format=byte-stream,profile=constrained-baseline",
        [](WebKitWebrtcVideoEncoder* self) {
            gst_util_set_object_arg(G_OBJECT(self->priv->encoder.get()), "tune", "zerolatency");
            gst_util_set_object_arg(G_OBJECT(self->priv->encoder.get()), "speed-preset", "ultrafast");
            g_object_set(G_OBJECT(self->priv->encoder.get()), "key-int-max", 15, "threads", 4, nullptr);
            g_object_set(self->priv->parser.get(), "config-interval", 1, nullptr);
        }, "bitrate", setBitrateKbitPerSec, "key-int-max");
    Encoders::registerEncoder(OpenH264, "openh264enc", "h264parse", "video/x-h264",
        "video/x-h264,alignment=au,stream-format=byte-stream,profile=baseline",
        [](WebKitWebrtcVideoEncoder* self) {
            g_object_set(self->priv->parser.get(), "config-interval", 1, nullptr);
        }, "bitrate", setBitrateBitPerSec, "gop-size");
    Encoders::registerEncoder(Vp8, "vp8enc", nullptr, "video/x-vp8", nullptr,
        [](WebKitWebrtcVideoEncoder* self) {
            gst_preset_load_preset(GST_PRESET(self->priv->encoder.get()), "Profile Realtime");
        }, "target-bitrate", setBitrateBitPerSec, "keyframe-max-dist");
    Encoders::registerEncoder(Vp9, "vp9enc", nullptr, "video/x-vp9", nullptr,
        [](WebKitWebrtcVideoEncoder* self) {
            g_object_set(self->priv->encoder.get(), "threads", 4, "cpu-used", 4, "tile-rows", 2, "row-mt", true, nullptr);
        }, "target-bitrate", setBitrateBitPerSec, "keyframe-max-dist");

    auto srcPadTemplateCaps = createSrcPadTemplateCaps();
    gst_element_class_add_pad_template(elementClass, gst_pad_template_new("src", GST_PAD_SRC, GST_PAD_ALWAYS, srcPadTemplateCaps.get()));

    g_object_class_install_property(objectClass, PROP_FORMAT, g_param_spec_boxed("format", "Format as caps", "Set the caps of the format to be used.", GST_TYPE_CAPS, WEBKIT_PARAM_READWRITE));

    g_object_class_install_property(objectClass, PROP_ENCODER, g_param_spec_object("encoder", "The actual encoder element", "The encoder element", GST_TYPE_ELEMENT, WEBKIT_PARAM_READABLE));

    g_object_class_install_property(objectClass, PROP_BITRATE, g_param_spec_uint("bitrate", "Bitrate", "The bitrate in kbit per second", 0, G_MAXINT, 2048,
        static_cast<GParamFlags>(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT)));

    g_object_class_install_property(objectClass, PROP_KEYFRAME_INTERVAL, g_param_spec_uint("keyframe-interval", "Keyframe interval", "The interval between keyframes", 0, G_MAXINT, 0,
        static_cast<GParamFlags>(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT)));
}

#endif // ENABLE(VIDEO) && ENABLE(MEDIA_STREAM) && USE(GSTREAMER)
