/*
 * QEMU DBus audio
 *
 * Copyright (c) 2021 Red Hat, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "qemu/osdep.h"
#include "qemu/error-report.h"
#include "qemu/host-utils.h"
#include "qemu/module.h"
#include "qemu/timer.h"
#include "qemu/dbus.h"

#include <gio/gunixfdlist.h>
#include "ui/dbus-display1.h"

#define AUDIO_CAP "dbus"
#include "audio.h"
#include "audio_int.h"
#include "trace.h"

#define DBUS_DISPLAY1_AUDIO_PATH DBUS_DISPLAY1_ROOT "/Audio"

#define DBUS_AUDIO_NSAMPLES 1024 /* could be configured? */

typedef struct DBusAudio {
    GDBusObjectManagerServer *server;
    bool p2p;
    GDBusObjectSkeleton *audio;
    QemuDBusDisplay1Audio *iface;
    GHashTable *out_listeners;
    GHashTable *in_listeners;
} DBusAudio;

typedef struct DBusVoiceOut {
    HWVoiceOut hw;
    bool enabled;
    RateCtl rate;

    void *buf;
    size_t buf_pos;
    size_t buf_size;

    bool has_volume;
    Volume volume;
} DBusVoiceOut;

typedef struct DBusVoiceIn {
    HWVoiceIn hw;
    bool enabled;
    RateCtl rate;

    bool has_volume;
    Volume volume;
} DBusVoiceIn;

static void *dbus_get_buffer_out(HWVoiceOut *hw, size_t *size)
{
    DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw);

    if (!vo->buf) {
        vo->buf_size = hw->samples * hw->info.bytes_per_frame;
        vo->buf = g_malloc(vo->buf_size);
        vo->buf_pos = 0;
    }

    *size = MIN(vo->buf_size - vo->buf_pos, *size);
    *size = audio_rate_get_bytes(&vo->rate, &hw->info, *size);

    return vo->buf + vo->buf_pos;

}

static size_t dbus_put_buffer_out(HWVoiceOut *hw, void *buf, size_t size)
{
    DBusAudio *da = (DBusAudio *)hw->s->drv_opaque;
    DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw);
    GHashTableIter iter;
    QemuDBusDisplay1AudioOutListener *listener = NULL;
    g_autoptr(GBytes) bytes = NULL;
    g_autoptr(GVariant) v_data = NULL;

    assert(buf == vo->buf + vo->buf_pos && vo->buf_pos + size <= vo->buf_size);
    vo->buf_pos += size;

    trace_dbus_audio_put_buffer_out(size);

    if (vo->buf_pos < vo->buf_size) {
        return size;
    }

    bytes = g_bytes_new_take(g_steal_pointer(&vo->buf), vo->buf_size);
    v_data = g_variant_new_from_bytes(G_VARIANT_TYPE("ay"), bytes, TRUE);
    g_variant_ref_sink(v_data);

    g_hash_table_iter_init(&iter, da->out_listeners);
    while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) {
        qemu_dbus_display1_audio_out_listener_call_write(
            listener,
            (uintptr_t)hw,
            v_data,
            G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
    }

    return size;
}

#if HOST_BIG_ENDIAN
#define AUDIO_HOST_BE TRUE
#else
#define AUDIO_HOST_BE FALSE
#endif

static void
dbus_init_out_listener(QemuDBusDisplay1AudioOutListener *listener,
                       HWVoiceOut *hw)
{
    qemu_dbus_display1_audio_out_listener_call_init(
        listener,
        (uintptr_t)hw,
        hw->info.bits,
        hw->info.is_signed,
        hw->info.is_float,
        hw->info.freq,
        hw->info.nchannels,
        hw->info.bytes_per_frame,
        hw->info.bytes_per_second,
        hw->info.swap_endianness ? !AUDIO_HOST_BE : AUDIO_HOST_BE,
        G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
}

static int
dbus_init_out(HWVoiceOut *hw, struct audsettings *as, void *drv_opaque)
{
    DBusAudio *da = (DBusAudio *)hw->s->drv_opaque;
    DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw);
    GHashTableIter iter;
    QemuDBusDisplay1AudioOutListener *listener = NULL;

    audio_pcm_init_info(&hw->info, as);
    hw->samples = DBUS_AUDIO_NSAMPLES;
    audio_rate_start(&vo->rate);

    g_hash_table_iter_init(&iter, da->out_listeners);
    while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) {
        dbus_init_out_listener(listener, hw);
    }
    return 0;
}

static void
dbus_fini_out(HWVoiceOut *hw)
{
    DBusAudio *da = (DBusAudio *)hw->s->drv_opaque;
    DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw);
    GHashTableIter iter;
    QemuDBusDisplay1AudioOutListener *listener = NULL;

    g_hash_table_iter_init(&iter, da->out_listeners);
    while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) {
        qemu_dbus_display1_audio_out_listener_call_fini(
            listener,
            (uintptr_t)hw,
            G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
    }

    g_clear_pointer(&vo->buf, g_free);
}

static void
dbus_enable_out(HWVoiceOut *hw, bool enable)
{
    DBusAudio *da = (DBusAudio *)hw->s->drv_opaque;
    DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw);
    GHashTableIter iter;
    QemuDBusDisplay1AudioOutListener *listener = NULL;

    vo->enabled = enable;
    if (enable) {
        audio_rate_start(&vo->rate);
    }

    g_hash_table_iter_init(&iter, da->out_listeners);
    while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) {
        qemu_dbus_display1_audio_out_listener_call_set_enabled(
            listener, (uintptr_t)hw, enable,
            G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
    }
}

static void
dbus_volume_out_listener(HWVoiceOut *hw,
                         QemuDBusDisplay1AudioOutListener *listener)
{
    DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw);
    Volume *vol = &vo->volume;
    g_autoptr(GBytes) bytes = NULL;
    GVariant *v_vol = NULL;

    if (!vo->has_volume) {
        return;
    }

    assert(vol->channels < sizeof(vol->vol));
    bytes = g_bytes_new(vol->vol, vol->channels);
    v_vol = g_variant_new_from_bytes(G_VARIANT_TYPE("ay"), bytes, TRUE);
    qemu_dbus_display1_audio_out_listener_call_set_volume(
        listener, (uintptr_t)hw, vol->mute, v_vol,
        G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
}

static void
dbus_volume_out(HWVoiceOut *hw, Volume *vol)
{
    DBusAudio *da = (DBusAudio *)hw->s->drv_opaque;
    DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw);
    GHashTableIter iter;
    QemuDBusDisplay1AudioOutListener *listener = NULL;

    vo->has_volume = true;
    vo->volume = *vol;

    g_hash_table_iter_init(&iter, da->out_listeners);
    while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) {
        dbus_volume_out_listener(hw, listener);
    }
}

static void
dbus_init_in_listener(QemuDBusDisplay1AudioInListener *listener, HWVoiceIn *hw)
{
    qemu_dbus_display1_audio_in_listener_call_init(
        listener,
        (uintptr_t)hw,
        hw->info.bits,
        hw->info.is_signed,
        hw->info.is_float,
        hw->info.freq,
        hw->info.nchannels,
        hw->info.bytes_per_frame,
        hw->info.bytes_per_second,
        hw->info.swap_endianness ? !AUDIO_HOST_BE : AUDIO_HOST_BE,
        G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
}

static int
dbus_init_in(HWVoiceIn *hw, struct audsettings *as, void *drv_opaque)
{
    DBusAudio *da = (DBusAudio *)hw->s->drv_opaque;
    DBusVoiceIn *vo = container_of(hw, DBusVoiceIn, hw);
    GHashTableIter iter;
    QemuDBusDisplay1AudioInListener *listener = NULL;

    audio_pcm_init_info(&hw->info, as);
    hw->samples = DBUS_AUDIO_NSAMPLES;
    audio_rate_start(&vo->rate);

    g_hash_table_iter_init(&iter, da->in_listeners);
    while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) {
        dbus_init_in_listener(listener, hw);
    }
    return 0;
}

static void
dbus_fini_in(HWVoiceIn *hw)
{
    DBusAudio *da = (DBusAudio *)hw->s->drv_opaque;
    GHashTableIter iter;
    QemuDBusDisplay1AudioInListener *listener = NULL;

    g_hash_table_iter_init(&iter, da->in_listeners);
    while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) {
        qemu_dbus_display1_audio_in_listener_call_fini(
            listener,
            (uintptr_t)hw,
            G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
    }
}

static void
dbus_volume_in_listener(HWVoiceIn *hw,
                         QemuDBusDisplay1AudioInListener *listener)
{
    DBusVoiceIn *vo = container_of(hw, DBusVoiceIn, hw);
    Volume *vol = &vo->volume;
    g_autoptr(GBytes) bytes = NULL;
    GVariant *v_vol = NULL;

    if (!vo->has_volume) {
        return;
    }

    assert(vol->channels < sizeof(vol->vol));
    bytes = g_bytes_new(vol->vol, vol->channels);
    v_vol = g_variant_new_from_bytes(G_VARIANT_TYPE("ay"), bytes, TRUE);
    qemu_dbus_display1_audio_in_listener_call_set_volume(
        listener, (uintptr_t)hw, vol->mute, v_vol,
        G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
}

static void
dbus_volume_in(HWVoiceIn *hw, Volume *vol)
{
    DBusAudio *da = (DBusAudio *)hw->s->drv_opaque;
    DBusVoiceIn *vo = container_of(hw, DBusVoiceIn, hw);
    GHashTableIter iter;
    QemuDBusDisplay1AudioInListener *listener = NULL;

    vo->has_volume = true;
    vo->volume = *vol;

    g_hash_table_iter_init(&iter, da->in_listeners);
    while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) {
        dbus_volume_in_listener(hw, listener);
    }
}

static size_t
dbus_read(HWVoiceIn *hw, void *buf, size_t size)
{
    DBusAudio *da = (DBusAudio *)hw->s->drv_opaque;
    /* DBusVoiceIn *vo = container_of(hw, DBusVoiceIn, hw); */
    GHashTableIter iter;
    QemuDBusDisplay1AudioInListener *listener = NULL;

    trace_dbus_audio_read(size);

    /* size = audio_rate_get_bytes(&vo->rate, &hw->info, size); */

    g_hash_table_iter_init(&iter, da->in_listeners);
    while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) {
        g_autoptr(GVariant) v_data = NULL;
        const char *data;
        gsize n = 0;

        if (qemu_dbus_display1_audio_in_listener_call_read_sync(
                listener,
                (uintptr_t)hw,
                size,
                G_DBUS_CALL_FLAGS_NONE, -1,
                &v_data, NULL, NULL)) {
            data = g_variant_get_fixed_array(v_data, &n, 1);
            g_warn_if_fail(n <= size);
            size = MIN(n, size);
            memcpy(buf, data, size);
            break;
        }
    }

    return size;
}

static void
dbus_enable_in(HWVoiceIn *hw, bool enable)
{
    DBusAudio *da = (DBusAudio *)hw->s->drv_opaque;
    DBusVoiceIn *vo = container_of(hw, DBusVoiceIn, hw);
    GHashTableIter iter;
    QemuDBusDisplay1AudioInListener *listener = NULL;

    vo->enabled = enable;
    if (enable) {
        audio_rate_start(&vo->rate);
    }

    g_hash_table_iter_init(&iter, da->in_listeners);
    while (g_hash_table_iter_next(&iter, NULL, (void **)&listener)) {
        qemu_dbus_display1_audio_in_listener_call_set_enabled(
            listener, (uintptr_t)hw, enable,
            G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
    }
}

static void *
dbus_audio_init(Audiodev *dev)
{
    DBusAudio *da = g_new0(DBusAudio, 1);

    da->out_listeners = g_hash_table_new_full(g_str_hash, g_str_equal,
                                                g_free, g_object_unref);
    da->in_listeners = g_hash_table_new_full(g_str_hash, g_str_equal,
                                               g_free, g_object_unref);
    return da;
}

static void
dbus_audio_fini(void *opaque)
{
    DBusAudio *da = opaque;

    if (da->server) {
        g_dbus_object_manager_server_unexport(da->server,
                                              DBUS_DISPLAY1_AUDIO_PATH);
    }
    g_clear_object(&da->audio);
    g_clear_object(&da->iface);
    g_clear_pointer(&da->in_listeners, g_hash_table_unref);
    g_clear_pointer(&da->out_listeners, g_hash_table_unref);
    g_clear_object(&da->server);
    g_free(da);
}

static void
listener_out_vanished_cb(GDBusConnection *connection,
                         gboolean remote_peer_vanished,
                         GError *error,
                         DBusAudio *da)
{
    char *name = g_object_get_data(G_OBJECT(connection), "name");

    g_hash_table_remove(da->out_listeners, name);
}

static void
listener_in_vanished_cb(GDBusConnection *connection,
                        gboolean remote_peer_vanished,
                        GError *error,
                        DBusAudio *da)
{
    char *name = g_object_get_data(G_OBJECT(connection), "name");

    g_hash_table_remove(da->in_listeners, name);
}

static gboolean
dbus_audio_register_listener(AudioState *s,
                             GDBusMethodInvocation *invocation,
                             GUnixFDList *fd_list,
                             GVariant *arg_listener,
                             bool out)
{
    DBusAudio *da = s->drv_opaque;
    const char *sender =
        da->p2p ? "p2p" : g_dbus_method_invocation_get_sender(invocation);
    g_autoptr(GDBusConnection) listener_conn = NULL;
    g_autoptr(GError) err = NULL;
    g_autoptr(GSocket) socket = NULL;
    g_autoptr(GSocketConnection) socket_conn = NULL;
    g_autofree char *guid = g_dbus_generate_guid();
    GHashTable *listeners = out ? da->out_listeners : da->in_listeners;
    GObject *listener;
    int fd;

    trace_dbus_audio_register(sender, out ? "out" : "in");

    if (g_hash_table_contains(listeners, sender)) {
        g_dbus_method_invocation_return_error(invocation,
                                              DBUS_DISPLAY_ERROR,
                                              DBUS_DISPLAY_ERROR_INVALID,
                                              "`%s` is already registered!",
                                              sender);
        return DBUS_METHOD_INVOCATION_HANDLED;
    }

    fd = g_unix_fd_list_get(fd_list, g_variant_get_handle(arg_listener), &err);
    if (err) {
        g_dbus_method_invocation_return_error(invocation,
                                              DBUS_DISPLAY_ERROR,
                                              DBUS_DISPLAY_ERROR_FAILED,
                                              "Couldn't get peer fd: %s",
                                              err->message);
        return DBUS_METHOD_INVOCATION_HANDLED;
    }

    socket = g_socket_new_from_fd(fd, &err);
    if (err) {
        g_dbus_method_invocation_return_error(invocation,
                                              DBUS_DISPLAY_ERROR,
                                              DBUS_DISPLAY_ERROR_FAILED,
                                              "Couldn't make a socket: %s",
                                              err->message);
        return DBUS_METHOD_INVOCATION_HANDLED;
    }
    socket_conn = g_socket_connection_factory_create_connection(socket);
    if (out) {
        qemu_dbus_display1_audio_complete_register_out_listener(
            da->iface, invocation, NULL);
    } else {
        qemu_dbus_display1_audio_complete_register_in_listener(
            da->iface, invocation, NULL);
    }

    listener_conn =
        g_dbus_connection_new_sync(
            G_IO_STREAM(socket_conn),
            guid,
            G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_SERVER,
            NULL, NULL, &err);
    if (err) {
        error_report("Failed to setup peer connection: %s", err->message);
        return DBUS_METHOD_INVOCATION_HANDLED;
    }

    listener = out ?
        G_OBJECT(qemu_dbus_display1_audio_out_listener_proxy_new_sync(
            listener_conn,
            G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
            NULL,
            "/org/qemu/Display1/AudioOutListener",
            NULL,
            &err)) :
        G_OBJECT(qemu_dbus_display1_audio_in_listener_proxy_new_sync(
            listener_conn,
            G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
            NULL,
            "/org/qemu/Display1/AudioInListener",
            NULL,
            &err));
    if (!listener) {
        error_report("Failed to setup proxy: %s", err->message);
        return DBUS_METHOD_INVOCATION_HANDLED;
    }

    if (out) {
        HWVoiceOut *hw;

        QLIST_FOREACH(hw, &s->hw_head_out, entries) {
            DBusVoiceOut *vo = container_of(hw, DBusVoiceOut, hw);
            QemuDBusDisplay1AudioOutListener *l =
                QEMU_DBUS_DISPLAY1_AUDIO_OUT_LISTENER(listener);

            dbus_init_out_listener(l, hw);
            qemu_dbus_display1_audio_out_listener_call_set_enabled(
                l, (uintptr_t)hw, vo->enabled,
                G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
        }
    } else {
        HWVoiceIn *hw;

        QLIST_FOREACH(hw, &s->hw_head_in, entries) {
            DBusVoiceIn *vo = container_of(hw, DBusVoiceIn, hw);
            QemuDBusDisplay1AudioInListener *l =
                QEMU_DBUS_DISPLAY1_AUDIO_IN_LISTENER(listener);

            dbus_init_in_listener(
                QEMU_DBUS_DISPLAY1_AUDIO_IN_LISTENER(listener), hw);
            qemu_dbus_display1_audio_in_listener_call_set_enabled(
                l, (uintptr_t)hw, vo->enabled,
                G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
        }
    }

    g_object_set_data_full(G_OBJECT(listener_conn), "name",
                           g_strdup(sender), g_free);
    g_hash_table_insert(listeners, g_strdup(sender), listener);
    g_object_connect(listener_conn,
                     "signal::closed",
                     out ? listener_out_vanished_cb : listener_in_vanished_cb,
                     da,
                     NULL);

    return DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
dbus_audio_register_out_listener(AudioState *s,
                                 GDBusMethodInvocation *invocation,
                                 GUnixFDList *fd_list,
                                 GVariant *arg_listener)
{
    return dbus_audio_register_listener(s, invocation,
                                        fd_list, arg_listener, true);

}

static gboolean
dbus_audio_register_in_listener(AudioState *s,
                                GDBusMethodInvocation *invocation,
                                GUnixFDList *fd_list,
                                GVariant *arg_listener)
{
    return dbus_audio_register_listener(s, invocation,
                                        fd_list, arg_listener, false);
}

static void
dbus_audio_set_server(AudioState *s, GDBusObjectManagerServer *server, bool p2p)
{
    DBusAudio *da = s->drv_opaque;

    g_assert(da);
    g_assert(!da->server);

    da->server = g_object_ref(server);
    da->p2p = p2p;

    da->audio = g_dbus_object_skeleton_new(DBUS_DISPLAY1_AUDIO_PATH);
    da->iface = qemu_dbus_display1_audio_skeleton_new();
    g_object_connect(da->iface,
                     "swapped-signal::handle-register-in-listener",
                     dbus_audio_register_in_listener, s,
                     "swapped-signal::handle-register-out-listener",
                     dbus_audio_register_out_listener, s,
                     NULL);

    g_dbus_object_skeleton_add_interface(G_DBUS_OBJECT_SKELETON(da->audio),
                                         G_DBUS_INTERFACE_SKELETON(da->iface));
    g_dbus_object_manager_server_export(da->server, da->audio);
}

static struct audio_pcm_ops dbus_pcm_ops = {
    .init_out = dbus_init_out,
    .fini_out = dbus_fini_out,
    .write    = audio_generic_write,
    .get_buffer_out = dbus_get_buffer_out,
    .put_buffer_out = dbus_put_buffer_out,
    .enable_out = dbus_enable_out,
    .volume_out = dbus_volume_out,

    .init_in  = dbus_init_in,
    .fini_in  = dbus_fini_in,
    .read     = dbus_read,
    .run_buffer_in = audio_generic_run_buffer_in,
    .enable_in = dbus_enable_in,
    .volume_in = dbus_volume_in,
};

static struct audio_driver dbus_audio_driver = {
    .name            = "dbus",
    .descr           = "Timer based audio exposed with DBus interface",
    .init            = dbus_audio_init,
    .fini            = dbus_audio_fini,
    .set_dbus_server = dbus_audio_set_server,
    .pcm_ops         = &dbus_pcm_ops,
    .can_be_default  = 1,
    .max_voices_out  = INT_MAX,
    .max_voices_in   = INT_MAX,
    .voice_size_out  = sizeof(DBusVoiceOut),
    .voice_size_in   = sizeof(DBusVoiceIn)
};

static void register_audio_dbus(void)
{
    audio_driver_register(&dbus_audio_driver);
}
type_init(register_audio_dbus);

module_dep("ui-dbus")
