/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2022 Joan Torres <joan.torres@suse.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

#include "config.h"

#include "gdm-common.h"
#include "gdm-remote-display.h"
#include "gdm-remote-display-factory.h"
#include "gdm-remote-display-factory-glue.h"

#define GDM_REMOTE_DISPLAY_FACTORY_DBUS_PATH "/org/gnome/DisplayManager/RemoteDisplayFactory"

struct _GdmRemoteDisplayFactory
{
        GdmDisplayFactory            parent;

        GdmDBusRemoteDisplayFactory *skeleton;
        GDBusConnection             *connection;

        int                          remote_displays_count;

       gboolean                      suspend_inhibited;
       int                           inhibit_suspend_fd;
};

static void     gdm_remote_display_factory_class_init    (GdmRemoteDisplayFactoryClass *klass);
static void     gdm_remote_display_factory_init          (GdmRemoteDisplayFactory      *factory);
static void     gdm_remote_display_factory_finalize      (GObject                     *object);

static gpointer remote_display_factory_object = NULL;

G_DEFINE_TYPE (GdmRemoteDisplayFactory, gdm_remote_display_factory, GDM_TYPE_DISPLAY_FACTORY)

static gboolean
gdm_remote_display_factory_create_display (GdmRemoteDisplayFactory *factory,
                                           const char              *autologin_user,
                                           const char              *remote_id,
                                           const char              *remote_hostname)
{
        g_autoptr (GdmDisplay) display = NULL;
        GdmDisplayStore *store;

        g_debug ("GdmRemoteDisplayFactory: Creating remote display");

        display = gdm_remote_display_new (remote_id,
                                          remote_hostname);

        g_object_set (display,
                      "autologin-user", autologin_user,
                      NULL);

        store = gdm_display_factory_get_display_store (GDM_DISPLAY_FACTORY (factory));
        gdm_display_store_add (store, display);

        if (!gdm_display_prepare (display)) {
                gdm_display_unmanage (display);
                return FALSE;
        }

        return TRUE;
}

static gboolean
handle_create_remote_display (GdmDBusRemoteDisplayFactory *skeleton,
                              GDBusMethodInvocation       *invocation,
                              GVariant                    *properties,
                              GdmRemoteDisplayFactory     *factory)
{
        g_autofree char *remote_id = NULL;
        g_autofree char *remote_hostname = NULL;

        g_variant_lookup (properties, "remote-id", "o", &remote_id);
        if (!remote_id) {
                g_dbus_method_invocation_return_error_literal (invocation,
                                                               G_DBUS_ERROR,
                                                               G_DBUS_ERROR_INVALID_ARGS,
                                                               "Missing remote-id in properties");
                return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

        g_variant_lookup (properties, "hostname", "s", &remote_hostname);

        if (!gdm_remote_display_factory_create_display (factory, NULL, remote_id, remote_hostname))
                g_dbus_method_invocation_return_error_literal (invocation,
                                                               G_DBUS_ERROR,
                                                               G_DBUS_ERROR_FAILED,
                                                               "Error creating remote display");
        else
                gdm_dbus_remote_display_factory_complete_create_remote_display (factory->skeleton,
                                                                                invocation);

        return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
handle_create_user_display (GdmDBusRemoteDisplayFactory *skeleton,
                            GDBusMethodInvocation       *invocation,
                            const char                  *user,
                            GdmRemoteDisplayFactory     *factory)
{
        g_auto (GStrv) sessions = NULL;
        g_autoptr (GError) error = NULL;

        if (!gdm_display_factory_on_user_display_creation (GDM_DISPLAY_FACTORY (factory),
                                                           user,
                                                           &error)) {
                g_dbus_method_invocation_return_gerror (invocation, error);
                return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

        if (!gdm_remote_display_factory_create_display (factory, user, NULL, NULL)) {
                g_dbus_method_invocation_return_error (invocation,
                                                       G_DBUS_ERROR,
                                                       G_DBUS_ERROR_FAILED,
                                                       "Error creating remote display");
                return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

        gdm_dbus_remote_display_factory_complete_create_user_display (factory->skeleton,
                                                                      invocation);

        return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
handle_destroy_user_display (GdmDBusRemoteDisplayFactory *skeleton,
                             GDBusMethodInvocation       *invocation,
                             const char                  *user,
                             GdmRemoteDisplayFactory     *factory)
{
        g_autoptr (GError) error = NULL;

        if (!gdm_display_factory_on_user_display_destruction (GDM_DISPLAY_FACTORY (factory),
                                                              user,
                                                              &error)) {
                g_dbus_method_invocation_return_gerror (invocation, error);
                return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

        gdm_dbus_remote_display_factory_complete_destroy_user_display (skeleton,
                                                                       invocation);

        return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
on_authorize_method (GdmDBusRemoteDisplayFactory *skeleton,
                     GDBusMethodInvocation       *invocation,
                     GdmRemoteDisplayFactory     *factory)
{
        g_autoptr (GError) error = NULL;

        if (!gdm_display_factory_authorize_manage_user_displays (GDM_DISPLAY_FACTORY (factory),
                                                                 invocation,
                                                                 &error)) {
                g_dbus_method_invocation_return_gerror (invocation, error);
                return FALSE;
        }

        return TRUE;
}

static gboolean
register_factory (GdmRemoteDisplayFactory *factory)
{
        GError *error = NULL;

        factory->connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
        if (factory->connection == NULL) {
                g_critical ("Error getting system bus: %s", error->message);
                g_error_free (error);
                exit (EXIT_FAILURE);
        }

        factory->skeleton = GDM_DBUS_REMOTE_DISPLAY_FACTORY (gdm_dbus_remote_display_factory_skeleton_new ());

        g_signal_connect (factory->skeleton,
                          "handle-create-remote-display",
                          G_CALLBACK (handle_create_remote_display),
                          factory);

        g_signal_connect (factory->skeleton,
                          "handle-create-user-display",
                          G_CALLBACK (handle_create_user_display),
                          factory);

        g_signal_connect (factory->skeleton,
                          "handle-destroy-user-display",
                          G_CALLBACK (handle_destroy_user_display),
                          factory);

        g_signal_connect (factory->skeleton, "g-authorize-method",
                          G_CALLBACK (on_authorize_method),
                          factory);

        if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (factory->skeleton),
                                               factory->connection,
                                               GDM_REMOTE_DISPLAY_FACTORY_DBUS_PATH,
                                               &error)) {
                g_critical ("Error exporting RemoteDisplayFactory object: %s", error->message);
                g_error_free (error);
                exit (EXIT_FAILURE);
        }

        return TRUE;
}

static void
gdm_remote_display_factory_finalize (GObject *object)
{
        GdmRemoteDisplayFactory *factory;

        g_return_if_fail (object != NULL);
        g_return_if_fail (GDM_IS_REMOTE_DISPLAY_FACTORY (object));

        factory = GDM_REMOTE_DISPLAY_FACTORY (object);

        g_return_if_fail (factory != NULL);

        g_clear_object (&factory->connection);
        g_clear_object (&factory->skeleton);

        if (factory->inhibit_suspend_fd != -1)
                close (factory->inhibit_suspend_fd);

        G_OBJECT_CLASS (gdm_remote_display_factory_parent_class)->finalize (object);
}

static GObject *
gdm_remote_display_factory_constructor (GType                  type,
                                        guint                  n_construct_properties,
                                        GObjectConstructParam *construct_properties)
{
        GdmRemoteDisplayFactory *factory;
        gboolean                 res;

        factory = GDM_REMOTE_DISPLAY_FACTORY (G_OBJECT_CLASS (gdm_remote_display_factory_parent_class)->constructor (type,
                                                                                                                     n_construct_properties,
                                                                                                                     construct_properties));

        res = register_factory (factory);
        if (!res) {
                g_warning ("Unable to register remote display factory with system bus");
        }

        return G_OBJECT (factory);
}

static void
on_inhibit_suspend_done (GObject      *source,
                         GAsyncResult *result,
                         gpointer      user_data)
{
        GDBusProxy *proxy = G_DBUS_PROXY (source);
        GdmRemoteDisplayFactory *factory = user_data;
        g_autoptr (GUnixFDList) fd_list = NULL;
        g_autoptr (GVariant) res = NULL;
        g_autoptr (GError) error = NULL;
        int idx;

        res = g_dbus_proxy_call_with_unix_fd_list_finish (proxy,
                                                          &fd_list,
                                                          result,
                                                          &error);
        if (!res) {
                g_warning ("Unable to inhibit suspend: %s", error->message);
                factory->suspend_inhibited = FALSE;
                return;
        }

        g_variant_get (res, "(h)", &idx);
        factory->inhibit_suspend_fd = g_unix_fd_list_get (fd_list, idx, &error);
        if (factory->inhibit_suspend_fd == -1) {
                g_warning ("Failed to receive system inhibitor fd: %s", error->message);
                factory->suspend_inhibited = FALSE;
        }
}

static void
inhibit_suspend (GdmRemoteDisplayFactory *factory)
{
        g_autoptr (GDBusProxy) proxy = NULL;
        g_autoptr (GError) error = NULL;

        if (factory->suspend_inhibited)
                return;

        proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM,
                                               G_DBUS_PROXY_FLAGS_NONE,
                                               NULL,
                                               "org.freedesktop.login1",
                                               "/org/freedesktop/login1",
                                               "org.freedesktop.login1.Manager",
                                               NULL,
                                               &error);
        if (!proxy) {
                g_warning ("Failed to acquire login1 manager proxy: %s", error->message);
                return;
        }

        factory->suspend_inhibited = TRUE;
        g_dbus_proxy_call_with_unix_fd_list (proxy,
                                             "Inhibit",
                                             g_variant_new ("(ssss)",
                                                            "sleep",
                                                            g_get_user_name (),
                                                            "Remote sessions would die on suspend",
                                                            "block"),
                                             G_DBUS_CALL_FLAGS_NONE,
                                             G_MAXINT,
                                             NULL,
                                             NULL,
                                             on_inhibit_suspend_done,
                                             factory);
}


static void
on_display_added (GdmDisplayStore         *store,
                  const char              *id,
                  GdmRemoteDisplayFactory *factory)
{
        GdmDisplay *display = gdm_display_store_lookup (store, id);

        if (!GDM_IS_REMOTE_DISPLAY (display))
                return;

        factory->remote_displays_count++;

        if (factory->remote_displays_count > 0)
                inhibit_suspend (factory);
}

static void
uninhibit_suspend (GdmRemoteDisplayFactory *factory)
{
        if (factory->inhibit_suspend_fd == -1)
                return;

        close (factory->inhibit_suspend_fd);
        factory->inhibit_suspend_fd = -1;
        factory->suspend_inhibited = FALSE;
}

static void
on_display_removed (GdmDisplayStore         *display_store,
                    GdmDisplay              *display,
                    GdmRemoteDisplayFactory *factory)
{
        g_assert (factory->remote_displays_count >= 0);

        if (!GDM_IS_REMOTE_DISPLAY (display))
                return;

        factory->remote_displays_count--;

        if (factory->remote_displays_count == 0)
                uninhibit_suspend (factory);
}

static gboolean
gdm_remote_display_factory_start (GdmDisplayFactory *base_factory)
{
        GdmRemoteDisplayFactory *factory = GDM_REMOTE_DISPLAY_FACTORY (base_factory);
        GdmDisplayStore *store;

        g_return_val_if_fail (GDM_IS_REMOTE_DISPLAY_FACTORY (factory), FALSE);

        store = gdm_display_factory_get_display_store (GDM_DISPLAY_FACTORY (factory));

        g_signal_connect_object (G_OBJECT (store),
                                 "display-added",
                                 G_CALLBACK (on_display_added),
                                 factory,
                                 G_CONNECT_DEFAULT);

        g_signal_connect_object (G_OBJECT (store),
                                 "display-removed",
                                 G_CALLBACK (on_display_removed),
                                 factory,
                                 G_CONNECT_DEFAULT);

        return TRUE;
}

static gboolean
gdm_remote_display_factory_stop (GdmDisplayFactory *base_factory)
{
        GdmRemoteDisplayFactory *factory = GDM_REMOTE_DISPLAY_FACTORY (base_factory);
        g_return_val_if_fail (GDM_IS_REMOTE_DISPLAY_FACTORY (factory), FALSE);
        return TRUE;
}

static void
gdm_remote_display_factory_class_init (GdmRemoteDisplayFactoryClass *klass)
{
        GObjectClass *object_class = G_OBJECT_CLASS (klass);
        GdmDisplayFactoryClass *factory_class = GDM_DISPLAY_FACTORY_CLASS (klass);

        object_class->finalize = gdm_remote_display_factory_finalize;
        object_class->constructor = gdm_remote_display_factory_constructor;

        factory_class->start = gdm_remote_display_factory_start;
        factory_class->stop = gdm_remote_display_factory_stop;
}

static void
gdm_remote_display_factory_init (GdmRemoteDisplayFactory *factory)
{
  factory->inhibit_suspend_fd = -1;
}

GdmRemoteDisplayFactory *
gdm_remote_display_factory_new (GdmDisplayStore *store)
{
        if (remote_display_factory_object != NULL) {
                g_object_ref (remote_display_factory_object);
        } else {
                remote_display_factory_object = g_object_new (GDM_TYPE_REMOTE_DISPLAY_FACTORY,
                                                              "display-store", store,
                                                              NULL);
                g_object_add_weak_pointer (remote_display_factory_object,
                                           (gpointer *) &remote_display_factory_object);
        }

        return GDM_REMOTE_DISPLAY_FACTORY (remote_display_factory_object);
}
