/* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

/*
 * This is a very simplistic session manager example that also runs an internal
 * PipeWire server for ease of use. The PipeWire server runs in its own thread
 * and our main thread's WpCore (the AppData.core) connects to it through
 * a socket, as if the PipeWire server was in a different process.
 *
 * This example starts 2 media nodes in the media graph: audiotestsrc & alsasink
 * Then, the session management part constructs endpoints for these nodes
 * and links them by creating an endpoint link.
 */

#include <wp/wp.h>
#include <glib-unix.h>
#include "../common/test-server.h"

#define APP_ERROR_DOMAIN (app_error_domain_quark ())
G_DEFINE_QUARK (app-error, app_error_domain)

typedef struct {
  /* our internal test PipeWire server */
  WpTestServer server;

  /* cmdline arguments */
  const gchar *alsa_device;

  /* our main loop and core */
  GMainContext *context;
  GMainLoop *loop;
  WpCore *core;
  WpSession *session;

  /* nodes provider data */
  WpNode *audiotestsrc;
  WpNode *alsasink;

  /* endpoints provider data */
  WpObjectManager *nodes_om;
  GPtrArray *session_items;

  /* policy manager data */
  GSource *interrupt_source;

} AppData;

/*
 * policy manager: link endpoints together
 */

static void
on_endpoints_changed (WpSession * session, AppData * d)
{
  g_autoptr (WpEndpoint) src = NULL;
  g_autoptr (WpEndpoint) sink = NULL;

  g_print ("Endpoints changed, n_endpoints=%u\n",
      wp_session_get_n_endpoints (session));

  /* a very simplistic lookup, since we don't expect any other endpoints
     to show up here, but this is the general idea...
     match endpoints, create links, cache the state and move forward */
  src = wp_session_lookup_endpoint (session,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", "=s", "Audio/Source", NULL);
  sink = wp_session_lookup_endpoint (session,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", "=s", "Audio/Sink", NULL);

  if (src) {
    g_print ("Got endpoint src: %s\n", wp_endpoint_get_name (src));
  }
  if (sink) {
    g_print ("Got endpoint sink: %s\n", wp_endpoint_get_name (sink));
  }

  if (src && sink) {
    g_autoptr (WpProperties) props = NULL;
    g_autofree gchar * id =
        g_strdup_printf ("%u", wp_proxy_get_bound_id (WP_PROXY (sink)));

    /* only the peer endpoint id is required when linking the default streams;
       everything else will be discovered */
    props = wp_properties_new ("endpoint-link.input.endpoint", id, NULL);
    wp_endpoint_create_link (src, props);
  }
}

static void
on_links_changed (WpSession * session, AppData * d)
{
  guint n_links = wp_session_get_n_links (session);

  /* activate the link - when endpoint links are created,
     they don't do anything unless they are activated first */
  if (n_links == 1) {
    /* lookup with no constraints will just return the first available object */
    g_autoptr (WpEndpointLink) link = wp_session_lookup_link (session, NULL);

    g_print ("Requesting link activation...\n");
    wp_endpoint_link_request_state (link, WP_ENDPOINT_LINK_STATE_ACTIVE);
  }
  else if (n_links == 0) {
    g_print ("Last endpoint link was destroyed; exiting...\n");
    g_main_loop_quit (d->loop);
  }
}

static gboolean
on_interrupted (AppData * d)
{
  g_print ("interrupted; let's try to destroy the link...\n");

  g_autoptr (WpEndpointLink) link = wp_session_lookup_link (d->session, NULL);
  if (link)
    wp_global_proxy_request_destroy (WP_GLOBAL_PROXY (link));

  /* remove the interrupt handler so that we can actually
     interrupt if things get stuck */
  g_clear_pointer (&d->interrupt_source, g_source_unref);
  return G_SOURCE_REMOVE;
}

static void
start_policy_manager (AppData * d)
{
  /* reuse the session pointer that we already have in AppData;
     under other circumstances, we would retrieve the session
     with a WpObjectManager */
  g_signal_connect (d->session, "endpoints-changed",
      G_CALLBACK (on_endpoints_changed), d);
  g_signal_connect (d->session, "links-changed",
      G_CALLBACK (on_links_changed), d);

  d->interrupt_source = g_unix_signal_source_new (SIGINT);
  g_source_set_callback (d->interrupt_source,
      G_SOURCE_FUNC (on_interrupted), d, NULL);
  g_source_attach (d->interrupt_source, d->context);
}

/*
 * endpoints provider: creates endpoints for the discovered nodes
 */

static void
on_si_activated (WpObject * object, GAsyncResult * res, AppData * d)
{
  g_autoptr (GError) error = NULL;

  if (!wp_object_activate_finish (object, res, &error)) {
    g_printerr ("Failed to activate session item: %s\n", error->message);
    g_main_loop_quit (d->loop);
    return;
  }

  g_print ("Item " WP_OBJECT_FORMAT " exported\n", WP_OBJECT_ARGS (object));
}

static void
on_node_added (WpObjectManager * om, WpNode *node, AppData * d)
{
  g_autoptr (WpSessionItem) item = NULL;
  g_autoptr (WpProperties) props = wp_properties_new_empty ();

  g_print ("Node " WP_OBJECT_FORMAT " added, creating session item\n",
      WP_OBJECT_ARGS (node));

  /* load the "si-audio-adapter" Session Item */
  item = wp_session_item_make (d->core, "si-audio-adapter");

  /* and configure it */
  wp_properties_setf (props, "node", "%p", node);
  wp_properties_setf (props, "session", "%p", d->session);
  if (!wp_session_item_configure (item, g_steal_pointer (&props))) {
    g_printerr ("Failed to configure session item\n");
    g_main_loop_quit (d->loop);
    return;
  }

  wp_object_activate (WP_OBJECT (item),
      WP_SESSION_ITEM_FEATURE_ACTIVE | WP_SESSION_ITEM_FEATURE_EXPORTED,
      NULL, (GAsyncReadyCallback) on_si_activated, d);
  g_ptr_array_add (d->session_items, g_steal_pointer (&item));
}

static void
start_endpoints_provider (AppData * d)
{
  g_print ("Installing watch for nodes...\n");

  /* register a WpObjectManager to listen for available nodes */
  /* for example purposes, we pretend we don't have access to the data set by
     start_nodes_provider(), i.e. d->audiotestsrc & d->alsasink */
  d->nodes_om = wp_object_manager_new ();
  wp_object_manager_add_interest (d->nodes_om, WP_TYPE_NODE, NULL);
  wp_object_manager_request_object_features (d->nodes_om, WP_TYPE_NODE,
      WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);

  d->session_items = g_ptr_array_new_with_free_func (g_object_unref);

  /* the object manager will emit 'object-added' for every node that is
     made available, once the node has all the features we requested above */
  g_signal_connect (d->nodes_om, "object-added", G_CALLBACK (on_node_added), d);
  wp_core_install_object_manager (d->core, d->nodes_om);
}

/*
 * nodes provider: creates the nodes
 */

static void
on_node_ready (WpObject * node, GAsyncResult * res, AppData * d)
{
  g_autoptr (GError) error = NULL;

  if (!wp_object_activate_finish (node, res, &error)) {
    g_printerr ("Failed to prepare node: %s\n", error->message);
    g_main_loop_quit (d->loop);
    return;
  }

  g_print ("Node " WP_OBJECT_FORMAT " is ready\n", WP_OBJECT_ARGS (node));
}

static void
start_nodes_provider (AppData * d)
{
  g_print ("Creating nodes...\n");

  d->audiotestsrc = wp_node_new_from_factory (d->core,
      "adapter", /* the pipewire factory name */
      wp_properties_new (
          /* the spa factory name */
          "factory.name", "audiotestsrc",
          /* a friendly name for our node */
          "node.name", "audiotestsrc",
          NULL));
  g_assert (d->audiotestsrc);
  wp_object_activate (WP_OBJECT (d->audiotestsrc),
      WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL, NULL,
      (GAsyncReadyCallback) on_node_ready, d);

  d->alsasink = wp_node_new_from_factory (d->core,
      "adapter", /* the pipewire factory name */
      wp_properties_new (
          /* the spa factory name */
          "factory.name", "api.alsa.pcm.sink",
          /* a friendly name for our node */
          "node.name", "alsasink",
          /* set the device handle (ex. hw:0,0) on the sink */
          "api.alsa.path", d->alsa_device,
          NULL));
  g_assert (d->alsasink);
  wp_object_activate (WP_OBJECT (d->alsasink),
      WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL, NULL,
      (GAsyncReadyCallback) on_node_ready, d);
}

/*
 * main application: loads modules and the session
 */

static void
on_session_ready (WpObject * session, GAsyncResult * res, AppData * d)
{
  g_autoptr (GError) error = NULL;

  if (!wp_object_activate_finish (session, res, &error)) {
    g_printerr ("Failed to prepare session: %s\n", error->message);
    g_main_loop_quit (d->loop);
    return;
  }

  g_print ("Session is ready, starting components...\n");

  start_nodes_provider (d);
  start_endpoints_provider (d);
  start_policy_manager (d);
}

static gboolean
appdata_init (AppData * d, GError ** error)
{
  WpImplSession *session;

  /* setup the internal test PipeWire server */
  wp_test_server_setup (&d->server);
  {
    /* load server modules (pipewire.conf) */
    g_autoptr (WpTestServerLocker) lock =
        wp_test_server_locker_new (&d->server);

    pw_context_add_spa_lib (d->server.context,
        "audiotestsrc", "audiotestsrc/libspa-audiotestsrc");
    pw_context_add_spa_lib (d->server.context,
        "api.alsa.*", "alsa/libspa-alsa");

    if (!pw_context_load_module (d->server.context,
            "libpipewire-module-spa-node-factory", NULL, NULL)) {
        g_set_error (error, APP_ERROR_DOMAIN, 0,
            "Failed to load libpipewire-module-spa-node-factory");
        return FALSE;
    }
    if (!pw_context_load_module (d->server.context,
            "libpipewire-module-link-factory", NULL, NULL)) {
        g_set_error (error, APP_ERROR_DOMAIN, 0,
            "Failed to load libpipewire-module-link-factory");
        return FALSE;
    }
    /* adapter is loaded by pw_context */
  }

  /* init our main loop */
  d->context = g_main_context_new ();
  d->loop = g_main_loop_new (d->context, FALSE);

  /* push the context as the thread default for GTask to work with it,
     otherwise it will try to use the "default" main context, which we are
     not using in our main loop, for demonstration purposes */
  g_main_context_push_thread_default (d->context);

  /* init our core; the "remote.name" key tells it to connect to our
     test server instead of the default "pipewire-0" */
  d->core = wp_core_new (d->context, wp_properties_new (
          "remote.name", d->server.name,
          NULL));

  /* load wireplumber modules (wireplumber.conf) */
  if (!(wp_core_load_component (d->core,
          "libwireplumber-module-si-node", "module", NULL, error)))
    return FALSE;

  if (!(wp_core_load_component (d->core,
          "libwireplumber-module-si-audio-adapter", "module", NULL, error)))
    return FALSE;

  if (!(wp_core_load_component (d->core,
          "libwireplumber-module-si-standard-link", "module", NULL, error)))
    return FALSE;

  /* connect */
  if (!wp_core_connect (d->core)) {
    g_set_error (error, APP_ERROR_DOMAIN, 0,
        "Failed to connect to the test server");
    return FALSE;
  }

  g_print ("Creating session...\n");

  /* create a session */
  d->session = WP_SESSION (session = wp_impl_session_new (d->core));
  wp_impl_session_set_property (session, "session.name", "audio");
  wp_object_activate (WP_OBJECT (session), WP_OBJECT_FEATURES_ALL, NULL,
      (GAsyncReadyCallback) on_session_ready, d);
  return TRUE;
}

static void
appdata_clear (AppData * d)
{
  /* policy manager data */
  g_clear_pointer (&d->interrupt_source, g_source_unref);

  /* endpoints provider data */
  g_clear_pointer (&d->session_items, g_ptr_array_unref);
  g_clear_object (&d->nodes_om);

  /* nodes provider data */
  g_clear_object (&d->audiotestsrc);
  g_clear_object (&d->alsasink);

  /* main app data */
  g_clear_object (&d->session);
  g_clear_object (&d->core);
  g_clear_pointer (&d->loop, g_main_loop_unref);
  g_clear_pointer (&d->context, g_main_context_unref);
  wp_test_server_teardown (&d->server);
}

G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (AppData, appdata_clear)

gint
main (gint argc, gchar *argv[])
{
  g_auto (AppData) data = {0};
  g_autoptr (GError) error = NULL;

  wp_init (WP_INIT_ALL);

  if (argc > 1)
    data.alsa_device = argv[1];
  else
    data.alsa_device = "hw:0,0";

  if (!appdata_init (&data, &error)) {
    g_printerr ("Initialization failed:\n  %s\n", error->message);
    return 1;
  }

  g_main_loop_run (data.loop);
  return 0;
}
