/***
  This file is part of PulseAudio.

  Copyright 2018 Collabora Ltd.
    Author: George Kiagiadakis <george.kiagiadakis@collabora.com>

  PulseAudio 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.

  PulseAudio 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
  General Public License for more details.

  You should have received a copy of the GNU Lesser General Public License
  along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
***/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdbool.h>

#include <pulsecore/core-util.h>
#include <pulsecore/llist.h>
#include <pulsecore/module.h>
#include <pulsecore/mutex.h>
#include <pulsecore/modargs.h>
#include <pulsecore/dynarray.h>
#include <pulsecore/semaphore.h>

#include "m4a_afb_comm.h"

#define DEFAULT_URI "ws://localhost:1234/api?token="
#define AHL_4A_API "ahl-4a"

PA_MODULE_AUTHOR("George Kiagiadakis");
PA_MODULE_DESCRIPTION("Makes PulseAudio work as a client of the AGL Advanced Audio Architecture");
PA_MODULE_LOAD_ONCE(true);
PA_MODULE_VERSION(PACKAGE_VERSION);
PA_MODULE_USAGE(
          "uri=<afb websocket uri>"
          "default_role=<role>");

static const char* const valid_modargs[] = {
    "uri",
    "default_role",
    NULL
};

typedef struct _m4a_stream {
    pa_module *self;

    union {
        pa_sink_input *sink_input;
        pa_source_output *source_output;
    };

    char *role;
    char *device_uri;

    bool entity_is_sink;
    bool entity_loaded;
    uint32_t entity_id;
    uint32_t entity_module_id;

    pa_semaphore *semaphore;

    PA_LLIST_FIELDS(struct _m4a_stream);
} m4a_stream;

typedef struct {
    /* configuration data */
    pa_modargs *ma;

    /* operational data */
    pa_mutex *lock;
    bool null_sink_loaded;
    uint32_t null_sink_id;
    uint32_t null_sink_module_id;
    pa_dynarray *roles;  /* element-type: char* */
    PA_LLIST_HEAD(m4a_stream, streams);
    pa_hook_slot
        *sink_input_new_slot,
        *sink_input_put_slot,
        *sink_input_unlink_post_slot;
    m4a_afb_comm *comm;
} m4a_data;

static bool role_exists(m4a_data *d, const char *role) {
    char *r;
    int idx;

    PA_DYNARRAY_FOREACH(r, d->roles, idx) {
        if (!strcmp(role, r))
            return true;
    }
    return false;
}

static char *decide_role(m4a_data *d, pa_proplist *p) {
    const char *role;

    role = pa_proplist_gets(p, PA_PROP_MEDIA_ROLE);
    if (role && role_exists(d, role)) {
        return pa_xstrdup(role);
    }

    /* FIXME we might want to do some mapping here between the standard
     * pulseaudio roles and whatever roles might be available in 4a */

    /* try the default role specified in the module arguments, or fall back
     * to "multimedia", which we can just hope that it exists */
    role = pa_modargs_get_value(d->ma, "default_role", "multimedia");
    if (role_exists(d, role)) {
        return pa_xstrdup(role);
    }

    return NULL;
}

static pa_sink *load_sink_module(pa_core *core, const char *module, char *params) {
    pa_module *m = NULL;
    pa_sink *target = NULL;
    uint32_t idx;

    if (pa_module_load(&m, core, module, params) < 0) {
        pa_log("Failed to load module %s %s", module, params);
        return NULL;
    }
    pa_xfree(params);
    PA_IDXSET_FOREACH(target, core->sinks, idx) {
        if (target->module == m)
            break;
    }
    if (target->module != m) {
        pa_log("%s didn't create any sinks", module);
        pa_module_unload(m, false);
        return NULL;
    }
    return target;
}

/* invoked in the afb communication thread */
static void got_roles_cb(enum m4a_afb_reply r,
                         const pa_json_object *response,
                         pa_module *self) {
    m4a_data *d = self->userdata;
    int length, i;
    const pa_json_object *jr;
    char *role;

    pa_mutex_lock(d->lock);

    if (r == M4A_AFB_REPLY_OK && pa_json_object_get_type(response) == PA_JSON_TYPE_ARRAY) {
        length = pa_json_object_get_array_length(response);
        for (i = 0; i < length; i++) {
            jr = pa_json_object_get_array_member(response, i);
            if (pa_json_object_get_type(jr) == PA_JSON_TYPE_STRING) {
                role = pa_xstrdup(pa_json_object_get_string(jr));
                pa_log_debug("Found 4a role: %s", role);
                pa_dynarray_append(d->roles, role);
            }
        }
    }

    pa_mutex_unlock(d->lock);
}

static m4a_stream *m4a_stream_new(pa_module *self, bool sink) {
    m4a_data *d = self->userdata;
    m4a_stream *stream;

    stream = pa_xnew0(m4a_stream, 1);
    stream->self = self;
    stream->entity_is_sink = sink;
    stream->semaphore = pa_semaphore_new(0);
    pa_mutex_lock(d->lock);
    PA_LLIST_PREPEND(m4a_stream, d->streams, stream);
    pa_mutex_unlock(d->lock);
    return stream;
}

static void m4a_stream_free(m4a_stream *stream) {
    m4a_data *d = stream->self->userdata;

    if (stream->role)
        pa_xfree(stream->role);
    if (stream->device_uri)
        pa_xfree(stream->device_uri);
    if (stream->entity_loaded)
        pa_module_unload_request_by_index(stream->self->core,
                                          stream->entity_module_id, false);
    pa_semaphore_free(stream->semaphore);
    pa_mutex_lock(d->lock);
    PA_LLIST_REMOVE(m4a_stream, d->streams, stream);
    pa_mutex_unlock(d->lock);
    pa_xfree(stream);
}

static pa_hook_result_t sink_input_new_cb(pa_core *core,
                                          pa_sink_input *i,
                                          pa_module *self) {
    m4a_data *d = self->userdata;
    pa_sink *sink;

    pa_core_assert_ref(core);

    /* first, forcibly set the sink to be our null sink */
    sink = pa_idxset_get_by_index(core->sinks, d->null_sink_id);
    if (sink) {
        i->sink = sink;
        i->sink_requested_by_application = false;
    }

    return PA_HOOK_OK;
}

/* invoked in the afb communication thread */
static void device_open_cb(enum m4a_afb_reply r,
                           const pa_json_object *response,
                           m4a_stream *stream) {
    const pa_json_object *jdu;
    const char *device_uri = NULL;

    if (r == M4A_AFB_REPLY_OK && pa_json_object_get_type(response) == PA_JSON_TYPE_OBJECT) {
        jdu = pa_json_object_get_object_member(response, "device_uri");
        if (jdu && pa_json_object_get_type(jdu) == PA_JSON_TYPE_STRING) {
            device_uri = pa_json_object_get_string(jdu);
        }
    }

    pa_log_debug("4A replied: %s, device_uri: %s",
                 (r == M4A_AFB_REPLY_OK) ? "OK" : "ERROR",
                 device_uri ? device_uri : "(null)");

    stream->device_uri = pa_xstrdup(device_uri);
    pa_semaphore_post(stream->semaphore);
}

static pa_hook_result_t sink_input_put_cb(pa_core *core,
                                          pa_sink_input *i,
                                          pa_module *self) {
    m4a_data *d = self->userdata;
    char *role;

    pa_mutex_lock(d->lock);
    role = decide_role(d, i->proplist);
    pa_mutex_unlock(d->lock);

    pa_log_info("New sink_input with role: %s", role ? role : "(null)");

    if (role) {
        m4a_stream *stream = m4a_stream_new(self, true);
        stream->role = role;
        stream->sink_input = i;

        pa_log_debug("Calling 4A to open the device");

        m4a_afb_call_async(d->comm, AHL_4A_API, role,
                           pa_xstrdup("{\"action\":\"open\"}"),
                           (m4a_afb_done_cb_t) device_open_cb, stream);

        pa_log_debug("waiting for 4A to reply");
        pa_semaphore_wait(stream->semaphore);

        if (stream->device_uri) {
            pa_sink *s;

            if ((s = load_sink_module(self->core, "module-alsa-sink",
                        pa_sprintf_malloc("device=%s", stream->device_uri)))) {
                stream->entity_id = s->index;
                stream->entity_module_id = s->module->index;
                stream->entity_loaded = true;

                pa_log_info("moving sink_input to alsa sink");
                pa_sink_input_move_to(i, s, false);
            } else {
                pa_xfree(stream->device_uri);
                stream->device_uri = NULL;
            }
        }

        if (!stream->device_uri) {
            pa_log_info("sink_input is not authorized to connect to 4A");
            //TODO maybe queue and play this stream when 4A allows it

            m4a_stream_free(stream);
            return PA_HOOK_CANCEL;
        }
    }

    return PA_HOOK_OK;
}

/* invoked in the afb communication thread */
static void device_close_cb(enum m4a_afb_reply r,
                            const pa_json_object *response,
                            m4a_stream *stream) {
    pa_log_debug("4A replied: %s",
                 (r == M4A_AFB_REPLY_OK) ? "OK" : "ERROR");
    m4a_stream_free(stream);
}

static pa_hook_result_t sink_input_unlink_post_cb(pa_core *core,
                                                  pa_sink_input *i,
                                                  pa_module *self) {
    m4a_data *d = self->userdata;
    m4a_stream *stream = NULL;

    PA_LLIST_FOREACH(stream, d->streams) {
        if (stream->sink_input == i)
            break;
    }

    if (stream && stream->sink_input == i) {
        pa_log_debug("unloading module-alsa-sink (%s)", stream->role);

        pa_module_unload_by_index(stream->self->core,
                                  stream->entity_module_id, false);
        stream->entity_loaded = false;

        m4a_afb_call_async(d->comm, AHL_4A_API, stream->role,
                        pa_xstrdup("{\"action\":\"close\"}"),
                        (m4a_afb_done_cb_t) device_close_cb, stream);
    }

    return PA_HOOK_OK;
}

int pa__init(pa_module *self) {
    m4a_data *d;
    const char *uri;
    pa_sink *null_sink;

    pa_assert(self);

    self->userdata = d = pa_xnew0(m4a_data, 1);
    d->roles = pa_dynarray_new(pa_xfree);
    d->lock = pa_mutex_new(false, false);

    if (!(d->ma = pa_modargs_new(self->argument, valid_modargs))) {
        pa_log("Failed to parse module arguments");
        goto fail;
    }

    uri = pa_modargs_get_value(d->ma, "uri", DEFAULT_URI);
    if (!(d->comm = m4a_afb_comm_new(uri))) {
        goto fail;
    }

    if (!m4a_afb_call_async(d->comm, AHL_4A_API, "get_roles", NULL,
                            (m4a_afb_done_cb_t) got_roles_cb, self)) {
        goto fail;
    }

    null_sink = load_sink_module(self->core, "module-null-sink", pa_sprintf_malloc(
            "sink_name=aaaa_null_sink sink_properties='device.description=\"%s\"'",
            _("4A Null Output")));
    d->null_sink_id = null_sink->index;
    d->null_sink_module_id = null_sink->module->index;
    d->null_sink_loaded = true;

    d->sink_input_new_slot = pa_hook_connect(&self->core->hooks[PA_CORE_HOOK_SINK_INPUT_NEW],
            PA_HOOK_NORMAL, (pa_hook_cb_t) sink_input_new_cb, self);
    d->sink_input_put_slot = pa_hook_connect(&self->core->hooks[PA_CORE_HOOK_SINK_INPUT_PUT],
            PA_HOOK_NORMAL, (pa_hook_cb_t) sink_input_put_cb, self);
    d->sink_input_unlink_post_slot = pa_hook_connect(&self->core->hooks[PA_CORE_HOOK_SINK_INPUT_UNLINK_POST],
            PA_HOOK_NORMAL, (pa_hook_cb_t) sink_input_unlink_post_cb, self);
    return 0;

fail:
    pa__done(self);
    return -1;
}

void pa__done(pa_module *self) {
    m4a_data *d;

    pa_assert(self);

    d = self->userdata;

    while (d->streams)
        m4a_stream_free(d->streams);

    pa_dynarray_free(d->roles);
    pa_mutex_free(d->lock);

    if (d->ma)
        pa_modargs_free(d->ma);

    if (d->sink_input_new_slot)
        pa_hook_slot_free(d->sink_input_new_slot);
    if (d->sink_input_put_slot)
        pa_hook_slot_free(d->sink_input_put_slot);
    if (d->sink_input_unlink_post_slot)
        pa_hook_slot_free(d->sink_input_unlink_post_slot);

    if (d->null_sink_loaded)
        pa_module_unload_request_by_index(self->core, d->null_sink_module_id, false);

    if (d->comm)
        m4a_afb_comm_free(d->comm);
}