/* SPDX-License-Identifier: GPL-2.0+ */
/*
 * Copyright (C) 2018 Red Hat, Inc.
 */

#include "nm-default.h"

#include "nm-dbus-object.h"

#include "nm-dbus-manager.h"
#include "NetworkManagerUtils.h"

/*****************************************************************************/

enum {
    EXPORTED_CHANGED,

    LAST_SIGNAL,
};

static guint signals[LAST_SIGNAL] = {0};

G_DEFINE_ABSTRACT_TYPE(NMDBusObject, nm_dbus_object, G_TYPE_OBJECT);

/*****************************************************************************/

#define _NMLOG_DOMAIN LOGD_CORE
#define _NMLOG(level, ...) \
    __NMLOG_DEFAULT_WITH_ADDR(level, _NMLOG_DOMAIN, "dbus-object", __VA_ARGS__)

#define _NMLOG2_DOMAIN LOGD_DBUS_PROPS
#define _NMLOG2(level, ...) \
    __NMLOG_DEFAULT_WITH_ADDR(level, _NMLOG2_DOMAIN, "properties-changed", __VA_ARGS__)

/*****************************************************************************/

static void
_emit_exported_changed(NMDBusObject *self)
{
    g_signal_emit(self, signals[EXPORTED_CHANGED], 0);
}

static char *
_create_export_path(NMDBusObjectClass *klass)
{
    nm_assert(NM_IS_DBUS_OBJECT_CLASS(klass));
    nm_assert(klass->export_path.path);

#if NM_MORE_ASSERTS
    {
        const char *p;

        p = strchr(klass->export_path.path, '%');
        if (klass->export_path.int_counter) {
            nm_assert(p);
            nm_assert(p[1] == 'l');
            nm_assert(p[2] == 'l');
            nm_assert(p[3] == 'u');
            nm_assert(p[4] == '\0');
        } else
            nm_assert(!p);
    }
#endif

    if (klass->export_path.int_counter) {
        NM_PRAGMA_WARNING_DISABLE("-Wformat-nonliteral")
        return g_strdup_printf(klass->export_path.path, ++(*klass->export_path.int_counter));
        NM_PRAGMA_WARNING_REENABLE
    }
    return g_strdup(klass->export_path.path);
}

/**
 * nm_dbus_object_export:
 * @self: an #NMDBusObject
 *
 * Exports @self on all active and future D-Bus connections.
 *
 * The path to export @self on is taken from its #NMObjectClass's %export_path
 * member. If the %export_path contains "%u", then it will be replaced with a
 * monotonically increasing integer ID (with each distinct %export_path having
 * its own counter). Otherwise, %export_path will be used literally (implying
 * that @self must be a singleton).
 *
 * Returns: the path @self was exported under
 */
const char *
nm_dbus_object_export(gpointer /* (NMDBusObject *) */ self)
{
    NMDBusObject * self1      = self;
    static guint64 id_counter = 0;

    g_return_val_if_fail(NM_IS_DBUS_OBJECT(self1), NULL);

    g_return_val_if_fail(!self1->internal.path, self1->internal.path);

    nm_assert(!self1->internal.is_unexporting);

    self1->internal.path = _create_export_path(NM_DBUS_OBJECT_GET_CLASS(self1));

    self1->internal.export_version_id = ++id_counter;

    _LOGT("export: \"%s\"", self1->internal.path);

    _nm_dbus_manager_obj_export(self1);

    _emit_exported_changed(self1);
    return self1->internal.path;
}

/**
 * nm_dbus_object_unexport:
 * @self: an #NMDBusObject
 *
 * Unexports @self on all active D-Bus connections (and prevents it from being
 * auto-exported on future connections).
 */
void
nm_dbus_object_unexport(gpointer /* (NMDBusObject *) */ self)
{
    NMDBusObject *self1 = self;

    g_return_if_fail(NM_IS_DBUS_OBJECT(self1));

    g_return_if_fail(self1->internal.path);

    _LOGT("unexport: \"%s\"", self1->internal.path);

    /* note that we emit the signal *before* actually unexporting the object.
     * The reason is, that listeners want to use this signal to know that
     * the object goes away, and clear their D-Bus path to this object.
     *
     * But this must happen before we actually unregister the object, so
     * that we first emit a D-Bus signal that other objects no longer
     * reference this object, before finally unregistering the object itself.
     *
     * The inconvenient part is, that at this point nm_dbus_object_get_path()
     * still returns the path. So, the callee needs to handle that. Possibly
     * by using "nm_dbus_object_get_path_still_exported()". */
    self1->internal.is_unexporting = TRUE;

    _emit_exported_changed(self1);

    _nm_dbus_manager_obj_unexport(self1);

    nm_clear_g_free(&self1->internal.path);
    self1->internal.export_version_id = 0;

    self1->internal.is_unexporting = FALSE;
}

static gboolean
_unexport_on_idle_cb(gpointer user_data)
{
    gs_unref_object NMDBusObject *self = user_data;

    nm_dbus_object_unexport(self);
    return G_SOURCE_REMOVE;
}

void
nm_dbus_object_unexport_on_idle(gpointer /* (NMDBusObject *) */ self_take)
{
    NMDBusObject *self = g_steal_pointer(&self_take);

    if (!self)
        return;

    g_return_if_fail(NM_IS_DBUS_OBJECT(self));

    g_return_if_fail(self->internal.path);

    /* There is no mechanism to cancel or abort the unexport. It will always
     * gonna happen.
     *
     * However, we register it to block shutdown, so that we ensure that it will happen. */

    nm_shutdown_wait_obj_register_object(self, "unexport-dbus-obj-on-idle");

    /* pass on ownership. */
    g_idle_add(_unexport_on_idle_cb, g_steal_pointer(&self));
}

/*****************************************************************************/

void
_nm_dbus_object_clear_and_unexport(NMDBusObject **location)
{
    NMDBusObject *self;

    g_return_if_fail(location);
    if (!*location)
        return;

    self = g_steal_pointer(location);

    g_return_if_fail(NM_IS_DBUS_OBJECT(self));

    if (self->internal.path)
        nm_dbus_object_unexport(self);

    g_object_unref(self);
}

/*****************************************************************************/

void
nm_dbus_object_emit_signal_variant(NMDBusObject *                     self,
                                   const NMDBusInterfaceInfoExtended *interface_info,
                                   const GDBusSignalInfo *            signal_info,
                                   GVariant *                         args)
{
    if (!self->internal.path) {
        nm_g_variant_unref_floating(args);
        return;
    }
    _nm_dbus_manager_obj_emit_signal(self, interface_info, signal_info, args);
}

void
nm_dbus_object_emit_signal(NMDBusObject *                     self,
                           const NMDBusInterfaceInfoExtended *interface_info,
                           const GDBusSignalInfo *            signal_info,
                           const char *                       format,
                           ...)
{
    va_list ap;

    nm_assert(NM_IS_DBUS_OBJECT(self));
    nm_assert(format);

    if (!self->internal.path)
        return;

    va_start(ap, format);
    _nm_dbus_manager_obj_emit_signal(self,
                                     interface_info,
                                     signal_info,
                                     g_variant_new_va(format, NULL, &ap));
    va_end(ap);
}

/*****************************************************************************/

static void
dispatch_properties_changed(GObject *object, guint n_pspecs, GParamSpec **pspecs)
{
    NMDBusObject *self = NM_DBUS_OBJECT(object);

    if (self->internal.path)
        _nm_dbus_manager_obj_notify(self, n_pspecs, (const GParamSpec *const *) pspecs);

    G_OBJECT_CLASS(nm_dbus_object_parent_class)
        ->dispatch_properties_changed(object, n_pspecs, pspecs);
}

/*****************************************************************************/

static void
nm_dbus_object_init(NMDBusObject *self)
{
    c_list_init(&self->internal.objects_lst);
    c_list_init(&self->internal.registration_lst_head);
    self->internal.bus_manager = nm_g_object_ref(nm_dbus_manager_get());
}

static void
constructed(GObject *object)
{
    NMDBusObjectClass *klass;

    G_OBJECT_CLASS(nm_dbus_object_parent_class)->constructed(object);

    klass = NM_DBUS_OBJECT_GET_CLASS(object);

    if (klass->export_on_construction)
        nm_dbus_object_export((NMDBusObject *) object);

    /* NMDBusObject types should be very careful when overwriting notify().
     * It is possible to do, but this is a reminder that it's probably not
     * a good idea.
     *
     * It's not a good idea, because NMDBusObject uses dispatch_properties_changed()
     * to emit signals about a bunch of property changes. So, we want to make
     * use of g_object_freeze_notify() / g_object_thaw_notify() to combine multiple
     * property changes in one signal on D-Bus. Note that notify() is not invoked
     * while the signal is frozen, that means, whatever you do inside notify()
     * will not make it into the same batch of PropertiesChanged signal. That is
     * confusing, and probably not what you want.
     *
     * Simple solution: don't overwrite notify(). */
    nm_assert(!G_OBJECT_CLASS(klass)->notify);
}

static void
dispose(GObject *object)
{
    NMDBusObject *self = NM_DBUS_OBJECT(object);

    /* Objects should have already been unexported by their owner, unless
     * we are quitting, where many objects stick around until exit.
     */
    if (self->internal.path) {
        if (!nm_dbus_manager_is_stopping(nm_dbus_object_get_manager(self)))
            g_warn_if_reached();
        nm_dbus_object_unexport(self);
    }

    G_OBJECT_CLASS(nm_dbus_object_parent_class)->dispose(object);

    g_clear_object(&self->internal.bus_manager);
}

static void
nm_dbus_object_class_init(NMDBusObjectClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS(klass);

    object_class->constructed                 = constructed;
    object_class->dispose                     = dispose;
    object_class->dispatch_properties_changed = dispatch_properties_changed;

    signals[EXPORTED_CHANGED] = g_signal_new(NM_DBUS_OBJECT_EXPORTED_CHANGED,
                                             G_OBJECT_CLASS_TYPE(object_class),
                                             G_SIGNAL_RUN_FIRST,
                                             0,
                                             NULL,
                                             NULL,
                                             g_cclosure_marshal_VOID__VOID,
                                             G_TYPE_NONE,
                                             0);
}
