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

#include "lua.h"
#include <glib/gstdio.h>
#include <wp/wp.h>
#include <pipewire/pipewire.h>
#include <wplua/wplua.h>
#include <libintl.h>

#define URI_API "resource:///org/freedesktop/pipewire/wireplumber/m-lua-scripting/api.lua"

void wp_lua_scripting_pod_init (lua_State *L);
void wp_lua_scripting_json_init (lua_State *L);

/* helpers */

static WpCore *
get_wp_core (lua_State *L)
{
  WpCore *core = NULL;
  lua_pushliteral (L, "wireplumber_core");
  lua_gettable (L, LUA_REGISTRYINDEX);
  core = lua_touserdata (L, -1);
  lua_pop (L, 1);
  return core;
}

static WpCore *
get_wp_export_core (lua_State *L)
{
  WpCore *core = NULL;
  lua_pushliteral (L, "wireplumber_export_core");
  lua_gettable (L, LUA_REGISTRYINDEX);
  if (wplua_isobject (L, -1, WP_TYPE_CORE))
    core = wplua_toobject (L, -1);
  lua_pop (L, 1);
  return core ? core : get_wp_core(L);
}

/* GLib */

static int
glib_get_monotonic_time (lua_State *L)
{
  lua_pushinteger (L, g_get_monotonic_time ());
  return 1;
}

static int
glib_get_real_time (lua_State *L)
{
  lua_pushinteger (L, g_get_real_time ());
  return 1;
}

static gboolean
access_parse_mode (const gchar * mode_str, gint *mode)
{
  *mode = 0;

  if (!mode_str)
    return FALSE;
  else {
    for (guint i = 0; i < strlen (mode_str); i++) {
      switch (mode_str[i]) {
        case 'r': *mode |= R_OK; break;
        case 'w': *mode |= W_OK; break;
        case 'x': *mode |= X_OK; break;
        case 'f': *mode |= F_OK; break;
        case '-': break;
        default:
          return FALSE;
      }
    }
  }
  return TRUE;
}

static int
glib_access (lua_State *L)
{
  const gchar *filename = luaL_checkstring (L, 1);
  int mode = 0;
  if (!access_parse_mode (luaL_checkstring (L, 2), &mode))
      luaL_error (L, "invalid mode string: '%s'", lua_tostring (L, 2));
  lua_pushboolean (L, g_access (filename, mode) >= 0);
  return 1;
}

static const luaL_Reg glib_methods[] = {
  { "get_monotonic_time", glib_get_monotonic_time },
  { "get_real_time", glib_get_real_time },
  { "access", glib_access },
  { NULL, NULL }
};

/* GSource */

static int
source_destroy (lua_State *L)
{
  GSource *source = wplua_checkboxed (L, 1, G_TYPE_SOURCE);
  g_source_destroy (source);
  return 0;
}

static const luaL_Reg source_methods[] = {
  { "destroy", source_destroy },
  { NULL, NULL }
};

/* i18n */

static int
i18n_gettext (lua_State *L)
{
  const gchar * msgid = luaL_checkstring (L, 1);
  lua_pushstring (L, dgettext (GETTEXT_PACKAGE, msgid));
  return 1;
}

static int
i18n_ngettext (lua_State *L)
{
  const gchar * msgid = luaL_checkstring (L, 1);
  const gchar *msgid_plural = luaL_checkstring (L, 2);
  gulong n = luaL_checkinteger (L, 3);
  lua_pushstring (L, dngettext (GETTEXT_PACKAGE, msgid, msgid_plural, n));
  return 1;
}

static const luaL_Reg i18n_funcs[] = {
  { "gettext", i18n_gettext },
  { "ngettext", i18n_ngettext },
  { NULL, NULL }
};

/* WpCore */

static int
core_get_info (lua_State *L)
{
  WpCore * core = get_wp_core (L);
  g_autoptr (WpProperties) p = wp_core_get_remote_properties (core);

  lua_newtable (L);
  lua_pushinteger (L, wp_core_get_remote_cookie (core));
  lua_setfield (L, -2, "cookie");
  lua_pushstring (L, wp_core_get_remote_name (core));
  lua_setfield (L, -2, "name");
  lua_pushstring (L, wp_core_get_remote_user_name (core));
  lua_setfield (L, -2, "user_name");
  lua_pushstring (L, wp_core_get_remote_host_name (core));
  lua_setfield (L, -2, "host_name");
  lua_pushstring (L, wp_core_get_remote_version (core));
  lua_setfield (L, -2, "version");
  wplua_properties_to_table (L, p);
  lua_setfield (L, -2, "properties");
  return 1;
}

static int
core_get_vm_type (lua_State *L)
{
  WpCore * core = get_wp_core (L);
  g_autofree gchar *vm = wp_core_get_vm_type (core);
  lua_pushstring (L, vm);
  return 1;
}

static int
core_get_own_bound_id (lua_State *L)
{
  WpCore * core = get_wp_core (L);
  guint32 id = wp_core_get_own_bound_id (core);
  lua_pushinteger (L, id);
  return 1;
}

static int
core_idle_add (lua_State *L)
{
  GSource *source = NULL;
  luaL_checktype (L, 1, LUA_TFUNCTION);
  wp_core_idle_add_closure (get_wp_core (L), &source,
      wplua_function_to_closure (L, 1));
  wplua_pushboxed (L, G_TYPE_SOURCE, source);
  return 1;
}

static int
core_timeout_add (lua_State *L)
{
  GSource *source = NULL;
  lua_Integer timeout_ms = luaL_checkinteger (L, 1);
  luaL_checktype (L, 2, LUA_TFUNCTION);
  wp_core_timeout_add_closure (get_wp_core (L), &source, timeout_ms,
      wplua_function_to_closure (L, 2));
  wplua_pushboxed (L, G_TYPE_SOURCE, source);
  return 1;
}

static void
on_core_done (WpCore * core, GAsyncResult * res, GClosure * closure)
{
  g_autoptr (GError) error = NULL;
  GValue val = G_VALUE_INIT;
  int n_vals = 0;

  if (!wp_core_sync_finish (core, res, &error)) {
    g_value_init (&val, G_TYPE_STRING);
    g_value_set_string (&val, error->message);
    n_vals = 1;
  }
  g_closure_invoke (closure, NULL, n_vals, &val, NULL);
  g_value_unset (&val);
  g_closure_invalidate (closure);
  g_closure_unref (closure);
}

static int
core_sync (lua_State *L)
{
  luaL_checktype (L, 1, LUA_TFUNCTION);
  GClosure * closure = wplua_function_to_closure (L, 1);
  g_closure_sink (g_closure_ref (closure));
  wp_core_sync (get_wp_core (L), NULL, (GAsyncReadyCallback) on_core_done,
      closure);
  return 0;
}

static gboolean
core_disconnect (WpCore * core)
{
  wp_core_disconnect (core);
  return G_SOURCE_REMOVE;
}

static int
core_quit (lua_State *L)
{
  WpCore * core = get_wp_core (L);
  g_autoptr (WpProperties) p = wp_core_get_properties (core);
  const gchar *daemon = wp_properties_get (p, "wireplumber.daemon");
  if (!g_strcmp0 (daemon, "true")) {
    wp_warning ("script attempted to quit, but the engine is "
        "running in the wireplumber daemon; ignoring");
    return 0;
  }

  /* wp_core_disconnect() will immediately destroy the lua plugin
     and the lua engine, so we cannot call it directly */
  wp_core_idle_add (core, NULL, G_SOURCE_FUNC (core_disconnect), core, NULL);
  return 0;
}

#include "require.c"

static int
core_require_api (lua_State *L)
{
  WpCore * core = get_wp_core (L);
  g_autoptr (WpProperties) p = wp_core_get_properties (core);
  const gchar *daemon = wp_properties_get (p, "wireplumber.daemon");
  if (!g_strcmp0 (daemon, "true")) {
    wp_warning ("script attempted to load an API module, but the engine is "
        "running in the wireplumber daemon; ignoring");
    return 0;
  }
  return wp_require_api_transition_new_from_lua (L, core);
}

static const luaL_Reg core_funcs[] = {
  { "get_info", core_get_info },
  { "get_vm_type", core_get_vm_type },
  { "get_own_bound_id", core_get_own_bound_id },
  { "idle_add", core_idle_add },
  { "timeout_add", core_timeout_add },
  { "sync", core_sync },
  { "quit", core_quit },
  { "require_api", core_require_api },
  { NULL, NULL }
};

/* WpLog */

static int
log_log (lua_State *L, GLogLevelFlags lvl)
{
  lua_Debug ar = {0};
  const gchar *message, *tmp;
  gchar domain[25];
  gchar line_str[11];
  gconstpointer instance = NULL;
  GType type = G_TYPE_INVALID;
  int index = 1;

  if (!wp_log_level_is_enabled (lvl))
    return 0;

  g_warn_if_fail (lua_getstack (L, 1, &ar) == 1);
  g_warn_if_fail (lua_getinfo (L, "nSl", &ar) == 1);

  if (wplua_isobject (L, 1, G_TYPE_OBJECT)) {
    instance = wplua_toobject (L, 1);
    type = G_TYPE_FROM_INSTANCE (instance);
    index++;
  }
  else if (wplua_isboxed (L, 1, G_TYPE_BOXED)) {
    instance = wplua_toboxed (L, 1);
    type = wplua_gvalue_userdata_type (L, 1);
    index++;
  }

  message = luaL_checkstring (L, index);
  tmp = ar.source ? g_strrstr (ar.source, ".lua") : NULL;
  snprintf (domain, 25, "script/%.*s",
      tmp ? MIN((gint)(tmp - ar.source), 17) : 17,
      ar.source);
  snprintf (line_str, 11, "%d", ar.currentline);
  ar.name = ar.name ? ar.name : "chunk";

  wp_log_structured_standard (domain, lvl,
      ar.source, line_str, ar.name, type, instance, "%s", message);
  return 0;
}

static int
log_warning (lua_State *L) { return log_log (L, G_LOG_LEVEL_WARNING); }

static int
log_message (lua_State *L) { return log_log (L, G_LOG_LEVEL_MESSAGE); }

static int
log_info (lua_State *L) { return log_log (L, G_LOG_LEVEL_INFO); }

static int
log_debug (lua_State *L) { return log_log (L, G_LOG_LEVEL_DEBUG); }

static int
log_trace (lua_State *L) { return log_log (L, WP_LOG_LEVEL_TRACE); }

static const luaL_Reg log_funcs[] = {
  { "warning", log_warning },
  { "message", log_message },
  { "info", log_info },
  { "debug", log_debug },
  { "trace", log_trace },
  { NULL, NULL }
};

/* WpPlugin */

static int
plugin_find (lua_State *L)
{
  const char *name = luaL_checkstring (L, 1);
  WpPlugin *plugin = wp_plugin_find (get_wp_core (L), name);
  if (plugin)
    wplua_pushobject (L, plugin);
  else
    lua_pushnil (L);
  return 1;
}

static const luaL_Reg plugin_funcs[] = {
  { "find", plugin_find },
  { NULL, NULL }
};

/* WpObject */

static void
object_activate_done (WpObject *o, GAsyncResult * res, GClosure * closure)
{
  g_autoptr (GError) error = NULL;
  GValue val[2] = { G_VALUE_INIT, G_VALUE_INIT };
  int n_vals = 1;

  if (!wp_object_activate_finish (o, res, &error)) {
    wp_message_object (o, "%s", error->message);
    if (closure) {
      g_value_init (&val[1], G_TYPE_STRING);
      g_value_set_string (&val[1], error->message);
      n_vals = 2;
    }
  }
  if (closure) {
    g_value_init_from_instance (&val[0], o);
    g_closure_invoke (closure, NULL, n_vals, val, NULL);
    g_value_unset (&val[0]);
    g_value_unset (&val[1]);
    g_closure_invalidate (closure);
    g_closure_unref (closure);
  }
}

static int
object_activate (lua_State *L)
{
  WpObject *o = wplua_checkobject (L, 1, WP_TYPE_OBJECT);
  WpObjectFeatures features = luaL_checkinteger (L, 2);
  GClosure * closure = luaL_opt (L, wplua_checkclosure, 3, NULL);
  if (closure)
    g_closure_sink (g_closure_ref (closure));
  wp_object_activate (o, features, NULL,
      (GAsyncReadyCallback) object_activate_done, closure);
  return 0;
}

static int
object_deactivate (lua_State *L)
{
  WpObject *o = wplua_checkobject (L, 1, WP_TYPE_OBJECT);
  WpObjectFeatures features = luaL_checkinteger (L, 2);
  wp_object_deactivate (o, features);
  return 0;
}

static int
object_get_active_features (lua_State *L)
{
  WpObject *o = wplua_checkobject (L, 1, WP_TYPE_OBJECT);
  WpObjectFeatures features = wp_object_get_active_features (o);
  lua_pushinteger (L, features);
  return 1;
}

static int
object_get_supported_features (lua_State *L)
{
  WpObject *o = wplua_checkobject (L, 1, WP_TYPE_OBJECT);
  WpObjectFeatures features = wp_object_get_supported_features (o);
  lua_pushinteger (L, features);
  return 1;
}

static const luaL_Reg object_methods[] = {
  { "activate", object_activate },
  { "deactivate", object_deactivate },
  { "get_active_features", object_get_active_features },
  { "get_supported_features", object_get_supported_features },
  { NULL, NULL }
};

/* WpProxy */

static int
proxy_get_interface_type (lua_State *L)
{
  WpProxy * p = wplua_checkobject (L, 1, WP_TYPE_PROXY);
  guint32 version = 0;
  const gchar *type = wp_proxy_get_interface_type (p, &version);
  lua_pushstring (L, type);
  lua_pushinteger (L, version);
  return 2;
}

static const luaL_Reg proxy_methods[] = {
  { "get_interface_type", proxy_get_interface_type },
  { NULL, NULL }
};

/* WpGlobalProxy */

static int
global_proxy_request_destroy (lua_State *L)
{
  WpGlobalProxy * p = wplua_checkobject (L, 1, WP_TYPE_GLOBAL_PROXY);
  wp_global_proxy_request_destroy (p);
  return 0;
}

static const luaL_Reg global_proxy_methods[] = {
  { "request_destroy", global_proxy_request_destroy },
  { NULL, NULL }
};

/* WpIterator */

static int
iterator_next (lua_State *L)
{
  WpIterator *it = wplua_checkboxed (L, 1, WP_TYPE_ITERATOR);
  g_auto (GValue) v = G_VALUE_INIT;
  if (it && wp_iterator_next (it, &v)) {
    return wplua_gvalue_to_lua (L, &v);
  } else {
    lua_pushnil (L);
    return 1;
  }
}

static int
push_wpiterator (lua_State *L, WpIterator *it)
{
  lua_pushcfunction (L, iterator_next);
  wplua_pushboxed (L, WP_TYPE_ITERATOR, it);
  return 2;
}

/* Metadata WpIterator */

static int
metadata_iterator_next (lua_State *L)
{
  WpIterator *it = wplua_checkboxed (L, 1, WP_TYPE_ITERATOR);
  g_auto (GValue) item = G_VALUE_INIT;
  if (wp_iterator_next (it, &item)) {
    guint32 s = 0;
    const gchar *k = NULL, *t = NULL, *v = NULL;
    wp_metadata_iterator_item_extract (&item, &s, &k, &t, &v);
    lua_pushinteger (L, s);
    lua_pushstring (L, k);
    lua_pushstring (L, t);
    lua_pushstring (L, v);
    return 4;
  } else {
    lua_pushnil (L);
    return 1;
  }
}

static int
push_metadata_wpiterator (lua_State *L, WpIterator *it)
{
  lua_pushcfunction (L, metadata_iterator_next);
  wplua_pushboxed (L, WP_TYPE_ITERATOR, it);
  return 2;
}

/* WpObjectInterest */

static GVariant *
constraint_value_to_variant (lua_State *L, int idx)
{
  switch (lua_type (L, idx)) {
  case LUA_TBOOLEAN:
    return g_variant_new_boolean (lua_toboolean (L, idx));
  case LUA_TSTRING:
    return g_variant_new_string (lua_tostring (L, idx));
  case LUA_TNUMBER:
    if (lua_isinteger (L, idx))
      return g_variant_new_int64 (lua_tointeger (L, idx));
    else
      return g_variant_new_double (lua_tonumber (L, idx));
  default:
    return NULL;
  }
}

static void
object_interest_new_add_constraint (lua_State *L, GType type,
    WpObjectInterest *interest)
{
  int constraint_idx;
  WpConstraintType ctype;
  const gchar *subject;
  WpConstraintVerb verb;
  GVariant *value = NULL;

  constraint_idx = lua_absindex (L, -1);

  /* verify this is a Constraint{} */
  if (lua_type (L, constraint_idx) != LUA_TTABLE) {
    luaL_error (L, "Interest: expected Constraint at index %d",
        lua_tointeger (L, -2));
  }

  if (luaL_getmetafield (L, constraint_idx, "__name") == LUA_TNIL ||
      g_strcmp0 (lua_tostring (L, -1), "Constraint") != 0) {
    luaL_error (L, "Interest: expected Constraint at index %d",
        lua_tointeger (L, -2));
  }
  lua_pop (L, 1);

  /* get the constraint type */
  lua_pushliteral (L, "type");
  if (lua_gettable (L, constraint_idx) == LUA_TNUMBER)
    ctype = lua_tointeger (L, -1);
  else
    ctype = WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY;
  lua_pop (L, 1);

  /* get t[1] (the subject) and t[2] (the verb) */
  lua_geti (L, constraint_idx, 1);
  subject = lua_tostring (L, -1);

  lua_geti (L, constraint_idx, 2);
  verb = lua_tostring (L, -1)[0];

  switch (verb) {
  case WP_CONSTRAINT_VERB_EQUALS:
  case WP_CONSTRAINT_VERB_NOT_EQUALS:
  case WP_CONSTRAINT_VERB_MATCHES: {
    lua_geti (L, constraint_idx, 3);
    value = constraint_value_to_variant (L, -1);
    if (G_UNLIKELY (!value))
      luaL_error (L, "Constraint: bad value type");
    break;
  }
  case WP_CONSTRAINT_VERB_IN_RANGE: {
    GVariant *values[2];
    lua_geti (L, constraint_idx, 3);
    lua_geti (L, constraint_idx, 4);
    values[0] = constraint_value_to_variant (L, -2);
    values[1] = constraint_value_to_variant (L, -1);
    if (G_UNLIKELY (!values[0] || !values[1])) {
      g_clear_pointer (&values[0], g_variant_unref);
      g_clear_pointer (&values[1], g_variant_unref);
      luaL_error (L, "Constraint: bad value type");
    }
    value = g_variant_new_tuple (values, 2);
    break;
  }
  case WP_CONSTRAINT_VERB_IN_LIST: {
    GPtrArray *values =
        g_ptr_array_new_with_free_func ((GDestroyNotify) g_variant_unref);
    int i = 3;
    while (lua_geti (L, constraint_idx, i++) != LUA_TNIL) {
      GVariant *tmp = constraint_value_to_variant (L, -1);
      if (G_UNLIKELY (!tmp)) {
        g_ptr_array_unref (values);
        luaL_error (L, "Constraint: bad value type");
      }
      g_ptr_array_add (values, g_variant_ref_sink (tmp));
      lua_pop (L, 1);
    }
    value = g_variant_new_tuple ((GVariant **) values->pdata, values->len);
    g_ptr_array_unref (values);
    break;
  }
  default:
    break;
  }

  wp_object_interest_add_constraint (interest, ctype, subject, verb, value);
  lua_settop (L, constraint_idx);
}

static GType
parse_gtype (const gchar *str)
{
  g_autofree gchar *typestr = NULL;
  GType res = G_TYPE_INVALID;

  g_return_val_if_fail (str, res);

  /* "device" -> "WpDevice" */
  typestr = g_strdup_printf ("Wp%s", str);
  if (typestr[2] != 0) {
    typestr[2] = g_ascii_toupper (typestr[2]);
    res = g_type_from_name (typestr);
  }

  return res;
}

static int
object_interest_new_index (lua_State *L, int idx, GType def_type)
{
  WpObjectInterest *interest = NULL;
  GType type = def_type;

  luaL_checktype (L, idx, LUA_TTABLE);

  /* type = "string" */
  lua_pushliteral (L, "type");
  if (lua_gettable (L, idx) == LUA_TSTRING) {
    type = parse_gtype (lua_tostring (L, -1));
    if (type == G_TYPE_INVALID)
      luaL_error (L, "Interest: unknown type '%s'", lua_tostring (L, -1));
  }
  else if (def_type == G_TYPE_INVALID)
    luaL_error (L, "Interest: expected 'type' as string");
  lua_pop (L, 1);

  interest = wp_object_interest_new_type (type);
  wplua_pushboxed (L, WP_TYPE_OBJECT_INTEREST, interest);

  /* add constraints */
  lua_pushnil (L);
  while (lua_next (L, idx)) {
    /* if the key isn't "type" */
    if (!(lua_type (L, -2) == LUA_TSTRING &&
          !g_strcmp0 ("type", lua_tostring (L, -2))))
      object_interest_new_add_constraint (L, type, interest);
    lua_pop (L, 1);
  }

  return 1;
}

static int
object_interest_new (lua_State *L)
{
  return object_interest_new_index (L, 1, G_TYPE_INVALID);
}

static int
object_interest_matches (lua_State *L)
{
  WpObjectInterest *interest = wplua_checkboxed (L, 1, WP_TYPE_OBJECT_INTEREST);
  gboolean matches = FALSE;

  if (wplua_isobject (L, 2, G_TYPE_OBJECT)) {
    matches = wp_object_interest_matches (interest, wplua_toobject (L, 2));
  }
  else if (lua_istable (L, 2)) {
    g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
    matches = wp_object_interest_matches (interest, props);
  } else
    luaL_argerror (L, 2, "expected GObject or table");

  lua_pushboolean (L, matches);
  return 1;
}

static const luaL_Reg object_interest_methods[] = {
  { "matches", object_interest_matches },
  { NULL, NULL }
};

static WpObjectInterest *
get_optional_object_interest (lua_State *L, int idx, GType def_type)
{
  if (lua_isnoneornil (L, idx))
    return NULL;
  else if (lua_isuserdata (L, idx))
    return wplua_checkboxed (L, idx, WP_TYPE_OBJECT_INTEREST);
  else if (lua_istable (L, idx)) {
    object_interest_new_index (L, idx, def_type);
    return wplua_toboxed (L, -1);
  } else {
    luaL_error (L, "expected Interest or none/nil");
    return NULL;
  }
}

/* WpObjectManager */

static int
object_manager_new (lua_State *L)
{
  WpObjectManager *om;

  /* validate arguments */
  luaL_checktype (L, 1, LUA_TTABLE);

  /* push to Lua asap to have a way to unref in case of error */
  om = wp_object_manager_new ();
  wplua_pushobject (L, om);

  lua_pushnil (L);
  while (lua_next (L, 1)) {
    WpObjectInterest *interest =
        wplua_checkboxed (L, -1, WP_TYPE_OBJECT_INTEREST);
    wp_object_manager_add_interest_full (om, wp_object_interest_ref (interest));
    lua_pop (L, 1);
  }

  /* request all the features for Lua scripts to make their job easier */
  wp_object_manager_request_object_features (om,
      WP_TYPE_OBJECT, WP_OBJECT_FEATURES_ALL);

  return 1;
}

static int
object_manager_activate (lua_State *L)
{
  WpObjectManager *om = wplua_checkobject (L, 1, WP_TYPE_OBJECT_MANAGER);
  wp_core_install_object_manager (get_wp_core (L), om);
  return 0;
}

static int
object_manager_get_n_objects (lua_State *L)
{
  WpObjectManager *om = wplua_checkobject (L, 1, WP_TYPE_OBJECT_MANAGER);
  lua_pushinteger (L, wp_object_manager_get_n_objects (om));
  return 1;
}

static int
object_manager_iterate (lua_State *L)
{
  WpObjectManager *om = wplua_checkobject (L, 1, WP_TYPE_OBJECT_MANAGER);
  WpObjectInterest *oi = get_optional_object_interest (L, 2, G_TYPE_OBJECT);
  WpIterator *it = oi ?
      wp_object_manager_new_filtered_iterator_full (om,
          wp_object_interest_ref (oi)) :
      wp_object_manager_new_iterator (om);
  return push_wpiterator (L, it);
}

static int
object_manager_lookup (lua_State *L)
{
  WpObjectManager *om = wplua_checkobject (L, 1, WP_TYPE_OBJECT_MANAGER);
  WpObjectInterest *oi = get_optional_object_interest (L, 2, G_TYPE_OBJECT);
  WpObject *o = oi ?
      wp_object_manager_lookup_full (om, wp_object_interest_ref (oi)) :
      wp_object_manager_lookup (om, G_TYPE_OBJECT, NULL);
  if (o) {
    wplua_pushobject (L, o);
    return 1;
  }
  return 0;
}

static const luaL_Reg object_manager_methods[] = {
  { "activate", object_manager_activate },
  { "get_n_objects", object_manager_get_n_objects },
  { "iterate", object_manager_iterate },
  { "lookup", object_manager_lookup },
  { NULL, NULL }
};

/* WpMetadata */

static int
metadata_iterate (lua_State *L)
{
  WpMetadata *metadata = wplua_checkobject (L, 1, WP_TYPE_METADATA);
  lua_Integer subject = luaL_checkinteger (L, 2);
  WpIterator *it = wp_metadata_new_iterator (metadata, subject);
  return push_metadata_wpiterator (L, it);
}

static int
metadata_find (lua_State *L)
{
  WpMetadata *metadata = wplua_checkobject (L, 1, WP_TYPE_METADATA);
  lua_Integer subject = luaL_checkinteger (L, 2);
  const char *key = luaL_checkstring (L, 3), *v = NULL, *t = NULL;
  v = wp_metadata_find (metadata, subject, key, &t);
  lua_pushstring (L, v);
  lua_pushstring (L, t);
  return 2;
}

static int
metadata_set (lua_State *L)
{
  WpMetadata *metadata = wplua_checkobject (L, 1, WP_TYPE_METADATA);
  lua_Integer subject = luaL_checkinteger (L, 2);
  const char *key = luaL_opt (L, luaL_checkstring, 3, NULL);
  const char *type = luaL_opt (L, luaL_checkstring, 4, NULL);
  const char *value = luaL_opt (L, luaL_checkstring, 5, NULL);
  wp_metadata_set (metadata, subject, key, type, value);
  return 0;
}

static const luaL_Reg metadata_methods[] = {
  { "iterate", metadata_iterate },
  { "find", metadata_find },
  { "set", metadata_set },
  { NULL, NULL }
};

/* WpImplMetadata */

static int
impl_metadata_new (lua_State *L)
{
  const char *name = luaL_checkstring (L, 1);
  WpProperties *properties = NULL;

  if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
    luaL_checktype (L, 2, LUA_TTABLE);
    properties = wplua_table_to_properties (L, 2);
  }

  WpImplMetadata *m = wp_impl_metadata_new_full (get_wp_core (L),
      name, properties);
  if (m)
    wplua_pushobject (L, m);
  return m ? 1 : 0;
}

/* WpEndpoint */

static const luaL_Reg endpoint_methods[] = {
  { NULL, NULL }
};

/* Device */

static int
device_new (lua_State *L)
{
  const char *factory = luaL_checkstring (L, 1);
  WpProperties *properties = NULL;

  if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
    luaL_checktype (L, 2, LUA_TTABLE);
    properties = wplua_table_to_properties (L, 2);
  }

  WpDevice *d = wp_device_new_from_factory (get_wp_export_core (L),
      factory, properties);
  if (d)
    wplua_pushobject (L, d);
  return d ? 1 : 0;
}

/* WpSpaDevice */

static int
spa_device_new (lua_State *L)
{
  const char *factory = luaL_checkstring (L, 1);
  WpProperties *properties = NULL;

  if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
    luaL_checktype (L, 2, LUA_TTABLE);
    properties = wplua_table_to_properties (L, 2);
  }

  WpSpaDevice *d = wp_spa_device_new_from_spa_factory (get_wp_export_core (L),
      factory, properties);
  if (d)
    wplua_pushobject (L, d);
  return d ? 1 : 0;
}

static int
spa_device_iterate_managed_objects (lua_State *L)
{
  WpSpaDevice *device = wplua_checkobject (L, 1, WP_TYPE_SPA_DEVICE);
  WpIterator *it = wp_spa_device_new_managed_object_iterator (device);
  return push_wpiterator (L, it);
}

static int
spa_device_get_managed_object (lua_State *L)
{
  WpSpaDevice *device = wplua_checkobject (L, 1, WP_TYPE_SPA_DEVICE);
  guint id = luaL_checkinteger (L, 2);
  GObject *obj = wp_spa_device_get_managed_object (device, id);
  if (obj)
    wplua_pushobject (L, obj);
  return obj ? 1 : 0;
}

static int
spa_device_store_managed_object (lua_State *L)
{
  WpSpaDevice *device = wplua_checkobject (L, 1, WP_TYPE_SPA_DEVICE);
  guint id = luaL_checkinteger (L, 2);
  GObject *obj = (lua_type (L, 3) != LUA_TNIL) ?
      g_object_ref (wplua_checkobject (L, 3, G_TYPE_OBJECT)) : NULL;

  wp_spa_device_store_managed_object (device, id, obj);
  return 0;
}

static const luaL_Reg spa_device_methods[] = {
  { "iterate_managed_objects", spa_device_iterate_managed_objects },
  { "get_managed_object", spa_device_get_managed_object },
  { "store_managed_object", spa_device_store_managed_object },
  { NULL, NULL }
};

/* Node */

static int
node_new (lua_State *L)
{
  const char *factory = luaL_checkstring (L, 1);
  WpProperties *properties = NULL;

  if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
    luaL_checktype (L, 2, LUA_TTABLE);
    properties = wplua_table_to_properties (L, 2);
  }

  WpNode *d = wp_node_new_from_factory (get_wp_export_core (L),
      factory, properties);
  if (d)
    wplua_pushobject (L, d);
  return d ? 1 : 0;
}

static int
node_get_state (lua_State *L)
{
  WpNode *node = wplua_checkobject (L, 1, WP_TYPE_NODE);
  const gchar *error = NULL;
  WpNodeState state = wp_node_get_state (node, &error);
  wplua_enum_to_lua (L, state, WP_TYPE_NODE_STATE);
  lua_pushstring (L, error ? error : "");
  return 2;
}

static int
node_get_n_input_ports (lua_State *L)
{
  WpNode *node = wplua_checkobject (L, 1, WP_TYPE_NODE);
  guint max = 0;
  guint ports = wp_node_get_n_input_ports (node, &max);
  lua_pushinteger (L, ports);
  lua_pushinteger (L, max);
  return 2;
}

static int
node_get_n_output_ports (lua_State *L)
{
  WpNode *node = wplua_checkobject (L, 1, WP_TYPE_NODE);
  guint max = 0;
  guint ports = wp_node_get_n_output_ports (node, &max);
  lua_pushinteger (L, ports);
  lua_pushinteger (L, max);
  return 2;
}

static int
node_get_n_ports (lua_State *L)
{
  WpNode *node = wplua_checkobject (L, 1, WP_TYPE_NODE);
  guint ports = wp_node_get_n_ports (node);
  lua_pushinteger (L, ports);
  return 1;
}

static int
node_iterate_ports (lua_State *L)
{
  WpNode *node = wplua_checkobject (L, 1, WP_TYPE_NODE);
  WpObjectInterest *oi = get_optional_object_interest (L, 2, WP_TYPE_PORT);
  WpIterator *it = oi ?
      wp_node_new_ports_filtered_iterator_full (node,
          wp_object_interest_ref (oi)) :
      wp_node_new_ports_iterator (node);
  return push_wpiterator (L, it);
}

static int
node_lookup_port (lua_State *L)
{
  WpNode *node = wplua_checkobject (L, 1, WP_TYPE_NODE);
  WpObjectInterest *oi = get_optional_object_interest (L, 2, WP_TYPE_PORT);
  WpPort *port = oi ?
      wp_node_lookup_port_full (node, wp_object_interest_ref (oi)) :
      wp_node_lookup_port (node, G_TYPE_OBJECT, NULL);
  if (port) {
    wplua_pushobject (L, port);
    return 1;
  }
  return 0;
}

static int
node_send_command (lua_State *L)
{
  WpNode *node = wplua_checkobject (L, 1, WP_TYPE_NODE);
  const char *command = luaL_checkstring (L, 2);
  wp_node_send_command (node, command);
  return 0;
}

static const luaL_Reg node_methods[] = {
  { "get_state", node_get_state },
  { "get_n_input_ports", node_get_n_input_ports },
  { "get_n_output_ports", node_get_n_output_ports },
  { "get_n_ports", node_get_n_ports },
  { "iterate_ports", node_iterate_ports },
  { "lookup_port", node_lookup_port },
  { "send_command", node_send_command },
  { NULL, NULL }
};

/* ImplNode */

static int
impl_node_new (lua_State *L)
{
  const char *factory = luaL_checkstring (L, 1);
  WpProperties *properties = NULL;

  if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
    luaL_checktype (L, 2, LUA_TTABLE);
    properties = wplua_table_to_properties (L, 2);
  }

  WpImplNode *d = wp_impl_node_new_from_pw_factory (get_wp_export_core (L),
     factory, properties);
  if (d)
    wplua_pushobject (L, d);
  return d ? 1 : 0;
}

/* Port */

static int
port_get_direction (lua_State *L)
{
  WpPort *port = wplua_checkobject (L, 1, WP_TYPE_PORT);
  WpDirection direction = wp_port_get_direction (port);
  wplua_enum_to_lua (L, direction, WP_TYPE_DIRECTION);
  return 1;
}

static const luaL_Reg port_methods[] = {
  { "get_direction", port_get_direction },
  { NULL, NULL }
};

/* Link */

static int
link_new (lua_State *L)
{
  const char *factory = luaL_checkstring (L, 1);
  WpProperties *properties = NULL;

  if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
    luaL_checktype (L, 2, LUA_TTABLE);
    properties = wplua_table_to_properties (L, 2);
  }

  WpLink *l = wp_link_new_from_factory (get_wp_core (L), factory, properties);
  if (l)
    wplua_pushobject (L, l);
  return l ? 1 : 0;
}

/* Client */

static gboolean
client_parse_permissions (const gchar * perms_str, guint32 *perms)
{
  *perms = 0;

  if (!perms_str)
    return FALSE;
  else if (g_strcmp0 (perms_str, "all") == 0)
    *perms = PW_PERM_ALL;
  else {
    for (guint i = 0; i < strlen (perms_str); i++) {
      switch (perms_str[i]) {
        case 'r': *perms |= PW_PERM_R; break;
        case 'w': *perms |= PW_PERM_W; break;
        case 'x': *perms |= PW_PERM_X; break;
        case 'm': *perms |= PW_PERM_M; break;
        case '-': break;
        default:
          return FALSE;
      }
    }
  }
  return TRUE;
}

static int
client_update_permissions (lua_State *L)
{
  WpClient *client = wplua_checkobject (L, 1, WP_TYPE_CLIENT);
  g_autoptr (GArray) arr = NULL;

  luaL_checktype (L, 2, LUA_TTABLE);

  lua_pushnil(L);
  while (lua_next (L, -2)) {
    struct pw_permission perm = {0};

    if (lua_type (L, -2) == LUA_TSTRING &&
        (!g_ascii_strcasecmp (lua_tostring(L, -2), "any") ||
         !g_ascii_strcasecmp (lua_tostring(L, -2), "all")))
      perm.id = PW_ID_ANY;
    else if (lua_isinteger (L, -2))
      perm.id = lua_tointeger (L, -2);
    else
      luaL_error (L, "invalid key for permissions array");

    if (!client_parse_permissions (lua_tostring (L, -1), &perm.permissions))
      luaL_error (L, "invalid permission string: '%s'", lua_tostring (L, -1));

    if (!arr)
      arr = g_array_new (FALSE, FALSE, sizeof (struct pw_permission));

    g_array_append_val (arr, perm);
    lua_pop (L, 1);
  }

  wp_client_update_permissions_array (client, arr->len,
      (const struct pw_permission *) arr->data);
  return 0;
}

static int
client_send_error (lua_State *L)
{
  WpClient *client = wplua_checkobject (L, 1, WP_TYPE_CLIENT);
  guint id = luaL_checkinteger (L, 2);
  int res = luaL_checkinteger (L, 3);
  const char *message = luaL_checkstring (L, 4);
  wp_client_send_error (client, id, res, message);
  return 0;
}

static const luaL_Reg client_methods[] = {
  { "update_permissions", client_update_permissions },
  { "send_error", client_send_error },
  { NULL, NULL }
};

/* WpSessionItem */

static int
session_item_new (lua_State *L)
{
  const char *type = luaL_checkstring (L, 1);
  WpSessionItem *si = wp_session_item_make (get_wp_core (L), type);
  if (si)
    wplua_pushobject (L, si);
  return si ? 1 : 0;
}

static int
session_item_get_associated_proxy (lua_State *L)
{
  WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
  const char *typestr = luaL_checkstring (L, 2);
  WpProxy *proxy = wp_session_item_get_associated_proxy (si,
      parse_gtype (typestr));
  if (proxy)
    wplua_pushobject (L, proxy);
  return proxy ? 1 : 0;
}

static int
session_item_reset (lua_State *L)
{
  WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
  wp_session_item_reset (si);
  return 0;
}

static int
session_item_configure (lua_State *L)
{
  WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
  WpProperties *props = wp_properties_new_empty ();

  /* validate arguments */
  luaL_checktype (L, 2, LUA_TTABLE);

  /* build the configuration properties */
  lua_pushnil (L);
  while (lua_next (L, 2)) {
    const gchar *key = NULL;
    g_autofree gchar *var = NULL;

    switch (lua_type (L, -1)) {
      case LUA_TBOOLEAN:
        var = g_strdup_printf ("%u", lua_toboolean (L, -1));
        break;
      case LUA_TNUMBER:
        if (lua_isinteger (L, -1))
          var = g_strdup_printf ("%lld", lua_tointeger (L, -1));
        else
          var = g_strdup_printf ("%f", lua_tonumber (L, -1));
        break;
      case LUA_TSTRING:
        var = g_strdup (lua_tostring (L, -1));
        break;
      case LUA_TUSERDATA: {
        GValue *v = lua_touserdata (L, -1);
        gpointer p = g_value_peek_pointer (v);
        var = g_strdup_printf ("%p", p);
        break;
      }
      default:
        luaL_error (L, "configure does not support lua type ",
            lua_typename(L, lua_type(L, -1)));
        break;
    }

    key = luaL_tolstring (L, -2, NULL);
    wp_properties_set (props, key, var);
    lua_pop (L, 2);
  }

  lua_pushboolean (L, wp_session_item_configure (si, props));
  return 1;
}

static int
session_item_register (lua_State *L)
{
  WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
  wp_session_item_register (g_object_ref (si));
  return 0;
}

static int
session_item_remove (lua_State *L)
{
  WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
  wp_session_item_remove (si);
  return 0;
}

static const luaL_Reg session_item_methods[] = {
  { "get_associated_proxy", session_item_get_associated_proxy },
  { "reset", session_item_reset },
  { "configure", session_item_configure },
  { "register", session_item_register },
  { "remove", session_item_remove },
  { NULL, NULL }
};

/* WpSiAdapter */

static int
si_adapter_get_ports_format (lua_State *L)
{
  WpSiAdapter *adapter = wplua_checkobject (L, 1, WP_TYPE_SI_ADAPTER);
  const gchar *mode = NULL;
  WpSpaPod *format = wp_si_adapter_get_ports_format (adapter, &mode);
  wplua_pushboxed (L, WP_TYPE_SPA_POD, format);
  lua_pushstring (L, mode);
  return 2;
}

static void
si_adapter_set_ports_format_done (WpObject *o, GAsyncResult * res,
    GClosure * closure)
{
  g_autoptr (GError) error = NULL;
  GValue val[2] = { G_VALUE_INIT, G_VALUE_INIT };
  int n_vals = 1;

  if (!wp_si_adapter_set_ports_format_finish (WP_SI_ADAPTER (o), res, &error)) {
    wp_message_object (o, "%s", error->message);
    if (closure) {
      g_value_init (&val[1], G_TYPE_STRING);
      g_value_set_string (&val[1], error->message);
      n_vals = 2;
    }
  }
  if (closure) {
    g_value_init_from_instance (&val[0], o);
    g_closure_invoke (closure, NULL, n_vals, val, NULL);
    g_value_unset (&val[0]);
    g_value_unset (&val[1]);
    g_closure_invalidate (closure);
    g_closure_unref (closure);
  }
}

static int
si_adapter_set_ports_format (lua_State *L)
{
  WpSiAdapter *adapter = wplua_checkobject (L, 1, WP_TYPE_SI_ADAPTER);
  WpSpaPod *format = wplua_checkboxed (L, 2, WP_TYPE_SPA_POD);
  const gchar *mode = luaL_checkstring (L, 3);
  GClosure * closure = luaL_opt (L, wplua_checkclosure, 4, NULL);
  if (closure)
    g_closure_sink (g_closure_ref (closure));
  wp_si_adapter_set_ports_format (adapter, wp_spa_pod_ref (format), mode,
      (GAsyncReadyCallback) si_adapter_set_ports_format_done, closure);
  return 0;
}

static const luaL_Reg si_adapter_methods[] = {
  { "get_ports_format", si_adapter_get_ports_format },
  { "set_ports_format", si_adapter_set_ports_format },
  { NULL, NULL }
};

/* WpPipewireObject */

static int
pipewire_object_iterate_params (lua_State *L)
{
  WpPipewireObject *pwobj = wplua_checkobject (L, 1, WP_TYPE_PIPEWIRE_OBJECT);
  const gchar *id = luaL_checkstring (L, 2);
  WpIterator *it = wp_pipewire_object_enum_params_sync (pwobj, id, NULL);
  return push_wpiterator (L, it);
}

static int
pipewire_object_set_param (lua_State *L)
{
  WpPipewireObject *pwobj = wplua_checkobject (L, 1, WP_TYPE_PIPEWIRE_OBJECT);
  const gchar *id = luaL_checkstring (L, 2);
  WpSpaPod *pod = wplua_checkboxed (L, 3, WP_TYPE_SPA_POD);
  wp_pipewire_object_set_param (pwobj, id, 0, wp_spa_pod_ref (pod));
  return 0;
}

static const luaL_Reg pipewire_object_methods[] = {
  { "iterate_params", pipewire_object_iterate_params },
  { "set_param" , pipewire_object_set_param },
  { "set_params" , pipewire_object_set_param }, /* deprecated, compat only */
  { NULL, NULL }
};

/* WpState */

static int
state_new (lua_State *L)
{
  const gchar *name = luaL_checkstring (L, 1);
  WpState *state = wp_state_new (name);
  wplua_pushobject (L, state);
  return 1;
}

static int
state_clear (lua_State *L)
{
  WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
  wp_state_clear (state);
  return 0;
}

static int
state_save (lua_State *L)
{
  WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
  luaL_checktype (L, 2, LUA_TTABLE);
  g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
  g_autoptr (GError) error = NULL;
  gboolean saved = wp_state_save (state, props, &error);
  lua_pushboolean (L, saved);
  lua_pushstring (L, error ? error->message : "");
  return 2;
}

static int
state_load (lua_State *L)
{
  WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
  g_autoptr (WpProperties) props = wp_state_load (state);
  wplua_properties_to_table (L, props);
  return 1;
}

static const luaL_Reg state_methods[] = {
  { "clear", state_clear },
  { "save" , state_save },
  { "load" , state_load },
  { NULL, NULL }
};

/* ImplModule */

static int
impl_module_new (lua_State *L)
{
  const char *name, *args = NULL;
  WpProperties *properties = NULL;

  name = luaL_checkstring (L, 1);

  if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL)
    args = luaL_checkstring (L, 2);

  if (lua_type (L, 3) != LUA_TNONE && lua_type (L, 3) != LUA_TNIL) {
    luaL_checktype (L, 3, LUA_TTABLE);
    properties = wplua_table_to_properties (L, 3);
  }

  bool load_file = false; // Load args as file path
  if (lua_type (L, 4) != LUA_TNONE && lua_type (L, 4) != LUA_TNIL) {
    luaL_checktype (L, 4, LUA_TBOOLEAN);
    load_file = lua_toboolean(L, 4);
  }

  WpImplModule *m = NULL;
  if (load_file) {
    m = wp_impl_module_load_file (get_wp_export_core (L),
      name, args, properties);
  } else {
    m = wp_impl_module_load (get_wp_export_core (L),
      name, args, properties);
  }

  if (m) {
    wplua_pushobject (L, m);
    return 1;
  } else {
    return 0;
  }
}

void
wp_lua_scripting_api_init (lua_State *L)
{
  g_autoptr (GError) error = NULL;

  luaL_newlib (L, glib_methods);
  lua_setglobal (L, "GLib");

  luaL_newlib (L, i18n_funcs);
  lua_setglobal (L, "I18n");

  luaL_newlib (L, log_funcs);
  lua_setglobal (L, "WpLog");

  luaL_newlib (L, core_funcs);
  lua_setglobal (L, "WpCore");

  luaL_newlib (L, plugin_funcs);
  lua_setglobal (L, "WpPlugin");

  wp_lua_scripting_pod_init (L);
  wp_lua_scripting_json_init (L);

  wplua_register_type_methods (L, G_TYPE_SOURCE,
      NULL, source_methods);
  wplua_register_type_methods (L, WP_TYPE_OBJECT,
      NULL, object_methods);
  wplua_register_type_methods (L, WP_TYPE_PROXY,
      NULL, proxy_methods);
  wplua_register_type_methods (L, WP_TYPE_GLOBAL_PROXY,
      NULL, global_proxy_methods);
  wplua_register_type_methods (L, WP_TYPE_OBJECT_INTEREST,
      object_interest_new, object_interest_methods);
  wplua_register_type_methods (L, WP_TYPE_OBJECT_MANAGER,
      object_manager_new, object_manager_methods);
  wplua_register_type_methods (L, WP_TYPE_METADATA,
      NULL, metadata_methods);
  wplua_register_type_methods (L, WP_TYPE_IMPL_METADATA,
      impl_metadata_new, NULL);
  wplua_register_type_methods (L, WP_TYPE_ENDPOINT,
      NULL, endpoint_methods);
  wplua_register_type_methods (L, WP_TYPE_DEVICE,
      device_new, NULL);
  wplua_register_type_methods (L, WP_TYPE_SPA_DEVICE,
      spa_device_new, spa_device_methods);
  wplua_register_type_methods (L, WP_TYPE_NODE,
      node_new, node_methods);
  wplua_register_type_methods (L, WP_TYPE_IMPL_NODE,
      impl_node_new, NULL);
  wplua_register_type_methods (L, WP_TYPE_PORT,
      NULL, port_methods);
  wplua_register_type_methods (L, WP_TYPE_LINK,
      link_new, NULL);
  wplua_register_type_methods (L, WP_TYPE_CLIENT,
      NULL, client_methods);
  wplua_register_type_methods (L, WP_TYPE_SESSION_ITEM,
      session_item_new, session_item_methods);
  wplua_register_type_methods (L, WP_TYPE_SI_ADAPTER,
      NULL, si_adapter_methods);
  wplua_register_type_methods (L, WP_TYPE_PIPEWIRE_OBJECT,
      NULL, pipewire_object_methods);
  wplua_register_type_methods (L, WP_TYPE_STATE,
      state_new, state_methods);
  wplua_register_type_methods (L, WP_TYPE_IMPL_MODULE,
      impl_module_new, NULL);

  if (!wplua_load_uri (L, URI_API, &error) ||
      !wplua_pcall (L, 0, 0, &error)) {
    wp_critical ("Failed to load api: %s", error->message);
  }
}
