/*
 * Copyright (C) 2015 Red Hat Inc.
 *
 * 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., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 *
 * Written by:
 *     Jonas Ådahl <jadahl@gmail.com>
 */

#include "config.h"

#include "grd-daemon.h"

#include <gio/gio.h>
#include <glib-unix.h>
#include <glib/gi18n.h>
#include <glib/gstdio.h>
#include <stdio.h>
#include <stdlib.h>

#include "grd-context.h"
#include "grd-daemon-user.h"
#include "grd-dbus-mutter-remote-desktop.h"
#include "grd-dbus-remote-desktop.h"
#include "grd-private.h"
#include "grd-rdp-server.h"
#include "grd-settings-headless.h"
#include "grd-settings-system.h"
#include "grd-settings-user.h"
#include "grd-types.h"
#include "grd-vnc-server.h"

#ifdef HAVE_LIBSYSTEMD
#include "grd-daemon-handover.h"
#include "grd-daemon-system.h"
#endif /* HAVE_LIBSYSTEMD */

#define RDP_SERVER_RESTART_DELAY_MS 3000

#define DEFAULT_MAX_PARALLEL_CONNECTIONS 10

enum
{
  PROP_0,

  PROP_CONTEXT,
};

enum
{
  MUTTER_PROXY_ACQUIRED,
  RDP_SERVER_STARTED,
  RDP_SERVER_STOPPED,

  N_SIGNALS,
};

static guint signals[N_SIGNALS];

typedef struct _GrdDaemonPrivate
{
  GSource *sigint_source;
  GSource *sigterm_source;

  GCancellable *cancellable;
  guint mutter_remote_desktop_watch_name_id;
  guint mutter_screen_cast_watch_name_id;

  GrdContext *context;

  GDBusConnection *connection;
  GrdDBusRemoteDesktopOrgGnomeRemoteDesktop *remote_desktop_interface;

#ifdef HAVE_RDP
  GrdRdpServer *rdp_server;
  unsigned int restart_rdp_server_source_id;
  GrdDBusRemoteDesktopRdpServer *other_rdp_server_iface;
  unsigned int other_rdp_server_watch_name_id;
#endif
#ifdef HAVE_VNC
  GrdVncServer *vnc_server;
#endif
} GrdDaemonPrivate;

G_DEFINE_TYPE_WITH_PRIVATE (GrdDaemon, grd_daemon, G_TYPE_APPLICATION)

#define QUOTE1(a) #a
#define QUOTE(a) QUOTE1(a)

#ifdef HAVE_RDP
static void maybe_start_rdp_server (GrdDaemon *daemon);
#endif

static const GDBusErrorEntry grd_dbus_error_entries[] =
{
  { GRD_DBUS_ERROR_NO_HANDOVER, "org.gnome.RemoteDesktop.Error.NoHandover" },
};

GQuark
grd_dbus_error_quark (void)
{
  static gsize quark = 0;

  g_dbus_error_register_error_domain ("grd-dbus-error-quark",
                                      &quark,
                                      grd_dbus_error_entries,
                                      G_N_ELEMENTS (grd_dbus_error_entries));
  return (GQuark) quark;
}

GCancellable *
grd_daemon_get_cancellable (GrdDaemon *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  return priv->cancellable;
}

GrdContext *
grd_daemon_get_context (GrdDaemon *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  return priv->context;
}

#ifdef HAVE_RDP
GrdRdpServer *
grd_daemon_get_rdp_server (GrdDaemon *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  return priv->rdp_server;
}
#endif /* HAVE_RDP */

static void
export_remote_desktop_interface (GrdDaemon *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);
  GrdRuntimeMode runtime_mode = grd_context_get_runtime_mode (priv->context);

  priv->remote_desktop_interface =
    grd_dbus_remote_desktop_org_gnome_remote_desktop_skeleton_new ();

  switch (runtime_mode)
  {
    case GRD_RUNTIME_MODE_SCREEN_SHARE:
      grd_dbus_remote_desktop_org_gnome_remote_desktop_set_runtime_mode (
        priv->remote_desktop_interface, "screen-share");
      break;
    case GRD_RUNTIME_MODE_HEADLESS:
      grd_dbus_remote_desktop_org_gnome_remote_desktop_set_runtime_mode (
        priv->remote_desktop_interface, "headless");
      break;
    case GRD_RUNTIME_MODE_SYSTEM:
      grd_dbus_remote_desktop_org_gnome_remote_desktop_set_runtime_mode (
        priv->remote_desktop_interface, "system");
      break;
    case GRD_RUNTIME_MODE_HANDOVER:
      grd_dbus_remote_desktop_org_gnome_remote_desktop_set_runtime_mode (
        priv->remote_desktop_interface, "handover");
      break;
  }

  g_dbus_interface_skeleton_export (
    G_DBUS_INTERFACE_SKELETON (priv->remote_desktop_interface),
    priv->connection,
    REMOTE_DESKTOP_OBJECT_PATH,
    NULL);
}

static void
unexport_remote_desktop_interface (GrdDaemon *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  g_dbus_interface_skeleton_unexport (
    G_DBUS_INTERFACE_SKELETON (priv->remote_desktop_interface));

  g_clear_object (&priv->remote_desktop_interface);
}

#ifdef HAVE_RDP
static void
export_rdp_server_interface (GrdDaemon *daemon)
{
  GrdDBusRemoteDesktopRdpServer *rdp_server_interface;
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);
  GrdSettings *settings = grd_context_get_settings (priv->context);

  rdp_server_interface =
    grd_dbus_remote_desktop_rdp_server_skeleton_new ();

  grd_dbus_remote_desktop_rdp_server_set_enabled (rdp_server_interface, FALSE);
  grd_dbus_remote_desktop_rdp_server_set_port (rdp_server_interface, -1);
  g_object_bind_property (settings, "rdp-negotiate-port",
                          rdp_server_interface, "negotiate-port",
                          G_BINDING_SYNC_CREATE);
  g_object_bind_property (settings, "rdp-server-cert-path",
                          rdp_server_interface, "tls-cert",
                          G_BINDING_SYNC_CREATE);
  g_object_bind_property (settings, "rdp-server-fingerprint",
                          rdp_server_interface, "tls-fingerprint",
                          G_BINDING_SYNC_CREATE);
  g_object_bind_property (settings, "rdp-server-key-path",
                          rdp_server_interface, "tls-key",
                          G_BINDING_SYNC_CREATE);
  g_object_bind_property (settings, "rdp-auth-methods",
                          rdp_server_interface, "auth-methods",
                          G_BINDING_SYNC_CREATE);
  g_object_bind_property (settings, "rdp-kerberos-keytab",
                          rdp_server_interface, "kerberos-keytab",
                          G_BINDING_SYNC_CREATE);
  g_object_bind_property (settings, "rdp-view-only",
                          rdp_server_interface, "view-only",
                          G_BINDING_SYNC_CREATE);

  g_dbus_interface_skeleton_export (
    G_DBUS_INTERFACE_SKELETON (rdp_server_interface),
    priv->connection,
    GRD_RDP_SERVER_OBJECT_PATH,
    NULL);

  grd_context_set_rdp_server_interface (priv->context, rdp_server_interface);
}

static void
unexport_rdp_server_interface (GrdDaemon *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);
  GrdDBusRemoteDesktopRdpServer *rdp_server_interface =
    grd_context_get_rdp_server_interface (priv->context);

  g_dbus_interface_skeleton_unexport (
    G_DBUS_INTERFACE_SKELETON (rdp_server_interface));

  grd_context_set_rdp_server_interface (priv->context, NULL);
}

static void
start_rdp_server_when_ready (GrdDaemon *daemon,
                             gboolean   should_start_when_ready)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);
  GrdSettings *settings = grd_context_get_settings (priv->context);

  g_signal_handlers_disconnect_by_func (G_OBJECT (settings),
                                        G_CALLBACK (maybe_start_rdp_server),
                                        daemon);

  if (!should_start_when_ready)
    return;

  g_signal_connect_object (G_OBJECT (settings),
                           "notify::rdp-server-cert",
                           G_CALLBACK (maybe_start_rdp_server),
                           daemon, G_CONNECT_SWAPPED);
  g_signal_connect_object (G_OBJECT (settings),
                           "notify::rdp-server-key",
                           G_CALLBACK (maybe_start_rdp_server),
                           daemon, G_CONNECT_SWAPPED);
  g_signal_connect_object (G_OBJECT (settings),
                           "notify::rdp-auth-methods",
                           G_CALLBACK (maybe_start_rdp_server),
                           daemon, G_CONNECT_SWAPPED);
  g_signal_connect_object (G_OBJECT (settings),
                           "notify::rdp-kerberos-keytab",
                           G_CALLBACK (maybe_start_rdp_server),
                           daemon, G_CONNECT_SWAPPED);
}

static void
stop_rdp_server (GrdDaemon *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  start_rdp_server_when_ready (daemon, FALSE);

  if (!priv->rdp_server)
    return;

  g_signal_emit (daemon, signals[RDP_SERVER_STOPPED], 0);
  grd_rdp_server_stop (priv->rdp_server);
  g_clear_object (&priv->other_rdp_server_iface);
  g_clear_handle_id (&priv->other_rdp_server_watch_name_id, g_bus_unwatch_name);
  g_clear_object (&priv->rdp_server);
  g_clear_handle_id (&priv->restart_rdp_server_source_id, g_source_remove);
  g_message ("RDP server stopped");
}

static void
on_rdp_server_binding_failed (GrdRdpServer *rdp_server,
                              GrdDaemon    *daemon)
{
  stop_rdp_server (daemon);
}

static void
on_other_rdp_server_new_connection (GrdDBusRemoteDesktopRdpServer *interface,
                                    GrdDaemon                     *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  g_assert (priv->rdp_server);

  grd_rdp_server_stop_sessions (priv->rdp_server);
}

static void
on_remote_desktop_rdp_server_proxy_acquired (GObject      *object,
                                             GAsyncResult *result,
                                             gpointer      user_data)
{
  GrdDaemon *daemon = user_data;
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);
  g_autoptr (GrdDBusRemoteDesktopRdpServer) proxy = NULL;
  g_autoptr (GError) error = NULL;

  proxy = grd_dbus_remote_desktop_rdp_server_proxy_new_for_bus_finish (result,
                                                                       &error);
  if (!proxy)
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

      g_warning ("Failed to create remote desktop rdp server proxy: %s",
                 error->message);
      return;
    }

  priv->other_rdp_server_iface = g_steal_pointer (&proxy);
  g_signal_connect (priv->other_rdp_server_iface, "new-connection",
                    G_CALLBACK (on_other_rdp_server_new_connection), daemon);
}

static void
on_remote_desktop_rdp_server_name_appeared (GDBusConnection *connection,
                                            const char      *name,
                                            const char      *name_owner,
                                            gpointer         user_data)
{
  GrdDaemon *daemon = user_data;
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  grd_dbus_remote_desktop_rdp_server_proxy_new (
    connection,
    G_DBUS_PROXY_FLAGS_NONE,
    name,
    GRD_RDP_SERVER_OBJECT_PATH,
    priv->cancellable,
    on_remote_desktop_rdp_server_proxy_acquired,
    daemon);
}

static void
on_remote_desktop_rdp_server_name_vanished (GDBusConnection *connection,
                                            const char      *name,
                                            gpointer         user_data)
{
  GrdDaemon *daemon = user_data;
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  g_clear_object (&priv->other_rdp_server_iface);
}

static void
connect_to_other_rdp_server (GrdDaemon *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);
  GrdRuntimeMode runtime_mode = grd_context_get_runtime_mode (priv->context);
  const char *bus_name;

  if (priv->other_rdp_server_watch_name_id != 0)
    return;

  if (runtime_mode == GRD_RUNTIME_MODE_HEADLESS)
    bus_name = GRD_DAEMON_HANDOVER_APPLICATION_ID;
  else if (runtime_mode == GRD_RUNTIME_MODE_HANDOVER)
    bus_name = GRD_DAEMON_HEADLESS_APPLICATION_ID;
  else
    g_assert_not_reached ();

  priv->other_rdp_server_watch_name_id =
    g_bus_watch_name (G_BUS_TYPE_SESSION,
                      bus_name,
                      G_BUS_NAME_WATCHER_FLAGS_NONE,
                      on_remote_desktop_rdp_server_name_appeared,
                      on_remote_desktop_rdp_server_name_vanished,
                      daemon, NULL);
}

static void
start_rdp_server (GrdDaemon *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);
  GrdRuntimeMode runtime_mode = grd_context_get_runtime_mode (priv->context);
  g_autoptr (GError) error = NULL;

  g_assert (!priv->rdp_server);

  priv->rdp_server = grd_rdp_server_new (priv->context);
  g_signal_connect (priv->rdp_server, "binding-failed",
                    G_CALLBACK (on_rdp_server_binding_failed), daemon);
  if (!grd_rdp_server_start (priv->rdp_server, &error))
    {
      g_warning ("Failed to start RDP server: %s\n", error->message);
      stop_rdp_server (daemon);
    }
  else
    {
      g_signal_emit (daemon, signals[RDP_SERVER_STARTED], 0);
      g_message ("RDP server started");

      if (runtime_mode == GRD_RUNTIME_MODE_HANDOVER ||
          runtime_mode == GRD_RUNTIME_MODE_HEADLESS)
        connect_to_other_rdp_server (daemon);
    }
}

static void
maybe_start_rdp_server (GrdDaemon *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);
  GrdSettings *settings = grd_context_get_settings (priv->context);
  g_autoptr (GError) error = NULL;
  GrdRdpAuthMethods auth_methods = 0;
  g_autofree char *kerberos_keytab = NULL;
  g_autofree char *certificate = NULL;
  g_autofree char *key = NULL;
  gboolean rdp_enabled = FALSE;

  if (priv->rdp_server)
    return;

  if (!GRD_DAEMON_GET_CLASS (daemon)->is_daemon_ready (daemon))
    {
      g_debug ("Daemon not ready, not starting RDP server");
      return;
    }

  g_object_get (G_OBJECT (settings),
                "rdp-enabled", &rdp_enabled,
                "rdp-auth-methods", &auth_methods,
                "rdp-kerberos-keytab", &kerberos_keytab,
                "rdp-server-cert", &certificate,
                "rdp-server-key", &key,
                NULL);

  if (!rdp_enabled)
    {
      g_debug ("RDP not enabled, not starting RDP server");
      return;
    }

  if (!auth_methods)
    {
      g_warning ("No RDP auth methods configured, not enabling server.");
      return;
    }

  if (auth_methods & GRD_RDP_AUTH_METHOD_KERBEROS &&
      !kerberos_keytab)
    {
      g_debug ("Kerberos keytab not set, not starting RDP server");
      return;
    }

  if ((certificate && key) ||
      grd_context_get_runtime_mode (priv->context) == GRD_RUNTIME_MODE_HANDOVER)
    {
      start_rdp_server (daemon);
    }
  else
    {
      g_message ("RDP TLS certificate and key not yet configured properly");
      start_rdp_server_when_ready (daemon, TRUE);
    }
}

static gboolean
restart_rdp_server (gpointer user_data)
{
  GrdDaemon *daemon = user_data;
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  priv->restart_rdp_server_source_id = 0;

  maybe_start_rdp_server (daemon);

  return G_SOURCE_REMOVE;
}

void
grd_daemon_restart_rdp_server_with_delay (GrdDaemon *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  if (priv->restart_rdp_server_source_id)
    return;

  stop_rdp_server (daemon);

  priv->restart_rdp_server_source_id =
    g_timeout_add (RDP_SERVER_RESTART_DELAY_MS, restart_rdp_server, daemon);
}
#endif /* HAVE_RDP */

#ifdef HAVE_VNC
static gboolean
set_string_from_auth_method_enum (GBinding     *binding,
                                  const GValue *source_value,
                                  GValue       *target_value,
                                  gpointer      user_data)
{
  GrdVncAuthMethod src_auth_method = g_value_get_enum (source_value);

  switch (src_auth_method)
    {
    case GRD_VNC_AUTH_METHOD_PROMPT:
      g_value_set_string (target_value, "prompt");
      break;
    case GRD_VNC_AUTH_METHOD_PASSWORD:
      g_value_set_string (target_value, "password");
      break;
    }

  return TRUE;
}

static void
export_vnc_server_interface (GrdDaemon *daemon)
{
  GrdDBusRemoteDesktopVncServer *vnc_server_interface;
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);
  GrdSettings *settings = grd_context_get_settings (priv->context);

  vnc_server_interface =
    grd_dbus_remote_desktop_vnc_server_skeleton_new ();

  grd_dbus_remote_desktop_vnc_server_set_enabled (vnc_server_interface, FALSE);
  grd_dbus_remote_desktop_vnc_server_set_port (vnc_server_interface, -1);
  g_object_bind_property (settings, "vnc-negotiate-port",
                          vnc_server_interface, "negotiate-port",
                          G_BINDING_SYNC_CREATE);
  g_object_bind_property (settings, "vnc-view-only",
                          vnc_server_interface, "view-only",
                          G_BINDING_SYNC_CREATE);
  g_object_bind_property_full (settings, "vnc-auth-method",
                               vnc_server_interface, "auth-method",
                               G_BINDING_SYNC_CREATE,
                               set_string_from_auth_method_enum,
                               NULL, NULL, NULL);

  g_dbus_interface_skeleton_export (
    G_DBUS_INTERFACE_SKELETON (vnc_server_interface),
    priv->connection,
    GRD_VNC_SERVER_OBJECT_PATH,
    NULL);

  grd_context_set_vnc_server_interface (priv->context, vnc_server_interface);
}

static void
unexport_vnc_server_interface (GrdDaemon *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);
  GrdDBusRemoteDesktopVncServer *vnc_server_interface =
    grd_context_get_vnc_server_interface (priv->context);

  g_dbus_interface_skeleton_unexport (
    G_DBUS_INTERFACE_SKELETON (vnc_server_interface));

  grd_context_set_vnc_server_interface (priv->context, NULL);
}

static void
stop_vnc_server (GrdDaemon *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  if (!priv->vnc_server)
    return;

  grd_vnc_server_stop (priv->vnc_server);
  g_clear_object (&priv->vnc_server);
  g_message ("VNC server stopped");
}

static void
start_vnc_server (GrdDaemon *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);
  g_autoptr (GError) error = NULL;

  if (priv->vnc_server)
    return;

  priv->vnc_server = grd_vnc_server_new (priv->context);
  if (!grd_vnc_server_start (priv->vnc_server, &error))
    {
      g_warning ("Failed to initialize VNC server: %s\n", error->message);
      stop_vnc_server (daemon);
    }
  else
    {
      g_message ("VNC server started");
    }
}
#endif /* HAVE_VNC */

static void
export_services_status (GrdDaemon *daemon)
{
  export_remote_desktop_interface (daemon);

#ifdef HAVE_RDP
  export_rdp_server_interface (daemon);
#endif
#ifdef HAVE_VNC
  if (GRD_IS_DAEMON_USER (daemon))
    export_vnc_server_interface (daemon);
#endif
}

static void
unexport_services_status (GrdDaemon *daemon)
{
  unexport_remote_desktop_interface (daemon);

#ifdef HAVE_RDP
  unexport_rdp_server_interface (daemon);
#endif
#ifdef HAVE_VNC
  if (GRD_IS_DAEMON_USER (daemon))
    unexport_vnc_server_interface (daemon);
#endif
}

void
grd_daemon_maybe_enable_services (GrdDaemon *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);
  GrdSettings *settings = grd_context_get_settings (priv->context);
  gboolean rdp_enabled;
  gboolean vnc_enabled;

  if (!GRD_DAEMON_GET_CLASS (daemon)->is_daemon_ready (daemon))
    return;

  grd_context_notify_daemon_ready (priv->context);

  g_object_get (G_OBJECT (settings),
                "rdp-enabled", &rdp_enabled,
                "vnc-enabled", &vnc_enabled,
                NULL);

#ifdef HAVE_RDP
  maybe_start_rdp_server (daemon);
#endif

#ifdef HAVE_VNC
  if (vnc_enabled)
    start_vnc_server (daemon);
#endif
}

void
grd_daemon_disable_services (GrdDaemon *daemon)
{
#ifdef HAVE_RDP
  stop_rdp_server (daemon);
#endif
#ifdef HAVE_VNC
  stop_vnc_server (daemon);
#endif
}

static void
on_mutter_remote_desktop_proxy_acquired (GObject      *object,
                                         GAsyncResult *result,
                                         gpointer      user_data)
{
  GrdDaemon *daemon = user_data;
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);
  GrdDBusMutterRemoteDesktop *proxy;
  g_autoptr (GError) error = NULL;

  proxy = grd_dbus_mutter_remote_desktop_proxy_new_finish (result, &error);
  if (!proxy)
    {
      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        g_warning ("Failed to create remote desktop proxy: %s", error->message);
      return;
    }

  grd_context_set_mutter_remote_desktop_proxy (priv->context, proxy);

  g_signal_emit (daemon, signals[MUTTER_PROXY_ACQUIRED], 0);
}

static void
on_mutter_screen_cast_proxy_acquired (GObject      *object,
                                      GAsyncResult *result,
                                      gpointer      user_data)
{
  GrdDaemon *daemon = user_data;
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);
  GrdDBusMutterScreenCast *proxy;
  g_autoptr (GError) error = NULL;

  proxy = grd_dbus_mutter_screen_cast_proxy_new_finish (result, &error);
  if (!proxy)
    {
      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        g_warning ("Failed to create screen cast proxy: %s", error->message);
      return;
    }

  grd_context_set_mutter_screen_cast_proxy (priv->context, proxy);

  g_signal_emit (daemon, signals[MUTTER_PROXY_ACQUIRED], 0);
}

static void
on_mutter_remote_desktop_name_appeared (GDBusConnection *connection,
                                        const char      *name,
                                        const char      *name_owner,
                                        gpointer         user_data)
{
  GrdDaemon *daemon = user_data;
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  grd_dbus_mutter_remote_desktop_proxy_new (connection,
                                            G_DBUS_PROXY_FLAGS_NONE,
                                            MUTTER_REMOTE_DESKTOP_BUS_NAME,
                                            MUTTER_REMOTE_DESKTOP_OBJECT_PATH,
                                            priv->cancellable,
                                            on_mutter_remote_desktop_proxy_acquired,
                                            daemon);
}

static void
on_mutter_remote_desktop_name_vanished (GDBusConnection *connection,
                                        const char      *name,
                                        gpointer         user_data)
{
  GrdDaemon *daemon = user_data;
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  grd_daemon_disable_services (daemon);

  grd_context_set_mutter_remote_desktop_proxy (priv->context, NULL);
}

static void
on_mutter_screen_cast_name_appeared (GDBusConnection *connection,
                                     const char      *name,
                                     const char      *name_owner,
                                     gpointer         user_data)
{
  GrdDaemon *daemon = user_data;
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  grd_dbus_mutter_screen_cast_proxy_new (connection,
                                         G_DBUS_PROXY_FLAGS_NONE,
                                         MUTTER_SCREEN_CAST_BUS_NAME,
                                         MUTTER_SCREEN_CAST_OBJECT_PATH,
                                         priv->cancellable,
                                         on_mutter_screen_cast_proxy_acquired,
                                         daemon);
}

static void
on_mutter_screen_cast_name_vanished (GDBusConnection *connection,
                                     const char      *name,
                                     gpointer         user_data)
{
  GrdDaemon *daemon = user_data;
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  grd_daemon_disable_services (daemon);

  grd_context_set_mutter_screen_cast_proxy (priv->context, NULL);
}

void
grd_daemon_acquire_mutter_dbus_proxies (GrdDaemon *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  g_clear_handle_id (&priv->mutter_remote_desktop_watch_name_id, g_bus_unwatch_name);
  priv->mutter_remote_desktop_watch_name_id =
    g_bus_watch_name (G_BUS_TYPE_SESSION,
                      MUTTER_REMOTE_DESKTOP_BUS_NAME,
                      G_BUS_NAME_WATCHER_FLAGS_NONE,
                      on_mutter_remote_desktop_name_appeared,
                      on_mutter_remote_desktop_name_vanished,
                      daemon, NULL);

  g_clear_handle_id (&priv->mutter_screen_cast_watch_name_id, g_bus_unwatch_name);
  priv->mutter_screen_cast_watch_name_id =
    g_bus_watch_name (G_BUS_TYPE_SESSION,
                      MUTTER_SCREEN_CAST_BUS_NAME,
                      G_BUS_NAME_WATCHER_FLAGS_NONE,
                      on_mutter_screen_cast_name_appeared,
                      on_mutter_screen_cast_name_vanished,
                      daemon, NULL);
}

#ifdef HAVE_RDP
static void
on_rdp_enabled_changed (GrdSettings *settings,
                        GParamSpec  *pspec,
                        GrdDaemon   *daemon)
{
  gboolean rdp_enabled;

  if (!GRD_DAEMON_GET_CLASS (daemon)->is_daemon_ready (daemon))
    return;

  g_object_get (G_OBJECT (settings), "rdp-enabled", &rdp_enabled, NULL);
  if (rdp_enabled)
    maybe_start_rdp_server (daemon);
  else
    stop_rdp_server (daemon);
}
#endif /* HAVE_RDP */

#ifdef HAVE_VNC
static void
on_vnc_enabled_changed (GrdSettings *settings,
                        GParamSpec  *pspec,
                        GrdDaemon   *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);
  gboolean vnc_enabled;

  if (!GRD_DAEMON_GET_CLASS (daemon)->is_daemon_ready (daemon))
    return;

  g_object_get (G_OBJECT (settings), "vnc-enabled", &vnc_enabled, NULL);
  if (vnc_enabled)
    {
      g_return_if_fail (!priv->vnc_server);
      start_vnc_server (daemon);
    }
  else
    {
      stop_vnc_server (daemon);
    }
}
#endif /* HAVE_VNC */

static void
grd_daemon_init (GrdDaemon *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  priv->cancellable = g_cancellable_new ();
}

static GDBusConnection *
get_daemon_dbus_connection (GrdDaemon *daemon)
{
  g_autoptr (GDBusConnection) connection = NULL;
  g_autoptr (GError) error = NULL;

#if defined(HAVE_RDP) && defined(HAVE_LIBSYSTEMD)
  if (GRD_IS_DAEMON_SYSTEM (daemon))
    connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
  else
#endif /* HAVE_RDP && HAVE_LIBSYSTEMD */
    connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);

  if (!connection)
    {
      g_warning ("Failing acquiring dbus connection: %s", error->message);
      return NULL;
    }

  return g_steal_pointer (&connection);
}

static void
grd_daemon_startup (GApplication *app)
{
  GrdDaemon *daemon = GRD_DAEMON (app);
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);
  GrdSettings *settings = grd_context_get_settings (priv->context);

  priv->connection = get_daemon_dbus_connection (daemon);
  export_services_status (daemon);

#ifdef HAVE_RDP
  if (GRD_IS_SETTINGS_USER (settings) ||
      GRD_IS_SETTINGS_HEADLESS (settings) ||
      GRD_IS_SETTINGS_SYSTEM (settings))
    {
      g_signal_connect (settings, "notify::rdp-enabled",
                        G_CALLBACK (on_rdp_enabled_changed),
                        daemon);
    }
#endif
#ifdef HAVE_VNC
  if (GRD_IS_SETTINGS_USER (settings) ||
      GRD_IS_SETTINGS_HEADLESS (settings))
    {
      g_signal_connect (settings, "notify::vnc-enabled",
                        G_CALLBACK (on_vnc_enabled_changed),
                        daemon);
    }
#endif

  /* Run indefinitely, until told to exit. */
  g_application_hold (app);

  G_APPLICATION_CLASS (grd_daemon_parent_class)->startup (app);
}

static void
grd_daemon_shutdown (GApplication *app)
{
  GrdDaemon *daemon = GRD_DAEMON (app);
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  g_cancellable_cancel (priv->cancellable);
  g_clear_object (&priv->cancellable);

  grd_daemon_disable_services (daemon);

  unexport_services_status (daemon);
  g_clear_object (&priv->connection);

  grd_context_set_mutter_remote_desktop_proxy (priv->context, NULL);
  g_clear_handle_id (&priv->mutter_remote_desktop_watch_name_id, g_bus_unwatch_name);

  grd_context_set_mutter_screen_cast_proxy (priv->context, NULL);
  g_clear_handle_id (&priv->mutter_screen_cast_watch_name_id, g_bus_unwatch_name);

  g_clear_object (&priv->context);

  if (priv->sigterm_source)
    {
      g_source_destroy (priv->sigterm_source);
      g_clear_pointer (&priv->sigterm_source, g_source_unref);
    }
  if (priv->sigint_source)
    {
      g_source_destroy (priv->sigint_source);
      g_clear_pointer (&priv->sigint_source, g_source_unref);
    }

  G_APPLICATION_CLASS (grd_daemon_parent_class)->shutdown (app);
}

static void
grd_daemon_get_property (GObject    *object,
                         guint       prop_id,
                         GValue     *value,
                         GParamSpec *pspec)
{
  GrdDaemon *daemon = GRD_DAEMON (object);
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  switch (prop_id)
    {
    case PROP_CONTEXT:
      g_value_set_object (value, priv->context);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
grd_daemon_set_property (GObject      *object,
                         guint         prop_id,
                         const GValue *value,
                         GParamSpec   *pspec)
{
  GrdDaemon *daemon = GRD_DAEMON (object);
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  switch (prop_id)
    {
    case PROP_CONTEXT:
      priv->context = g_value_get_object (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
grd_daemon_class_init (GrdDaemonClass *klass)
{
  GApplicationClass *g_application_class = G_APPLICATION_CLASS (klass);
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  g_application_class->startup = grd_daemon_startup;
  g_application_class->shutdown = grd_daemon_shutdown;

  object_class->get_property = grd_daemon_get_property;
  object_class->set_property = grd_daemon_set_property;

  g_object_class_install_property (object_class,
                                   PROP_CONTEXT,
                                   g_param_spec_object ("context",
                                                        "GrdContext",
                                                        "The GrdContext instance",
                                                        GRD_TYPE_CONTEXT,
                                                        G_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT_ONLY |
                                                        G_PARAM_STATIC_STRINGS));

  signals[MUTTER_PROXY_ACQUIRED] = g_signal_new ("mutter-proxy-acquired",
                                                 G_TYPE_FROM_CLASS (klass),
                                                 G_SIGNAL_RUN_LAST,
                                                 0,
                                                 NULL, NULL, NULL,
                                                 G_TYPE_NONE, 0);
  signals[RDP_SERVER_STARTED] = g_signal_new ("rdp-server-started",
                                              G_TYPE_FROM_CLASS (klass),
                                              G_SIGNAL_RUN_LAST,
                                              0,
                                              NULL, NULL, NULL,
                                              G_TYPE_NONE, 0);
  signals[RDP_SERVER_STOPPED] = g_signal_new ("rdp-server-stopped",
                                              G_TYPE_FROM_CLASS (klass),
                                              G_SIGNAL_RUN_LAST,
                                              0,
                                              NULL, NULL, NULL,
                                              G_TYPE_NONE, 0);

  grd_dbus_error_quark ();
}

static void
activate_terminate (GAction   *action,
                    GVariant  *parameter,
                    GrdDaemon *daemon)
{
  g_application_release (G_APPLICATION (daemon));
}

static void
add_actions (GApplication *app)
{
  g_autoptr(GSimpleAction) action = NULL;

  action = g_simple_action_new ("terminate", NULL);
  g_signal_connect (action, "activate", G_CALLBACK (activate_terminate), app);
  g_action_map_add_action (G_ACTION_MAP (app), G_ACTION (action));
}

static gboolean
sigint_terminate_daemon (gpointer user_data)
{
  GrdDaemon *daemon = user_data;
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  g_debug ("Received SIGINT signal. Exiting...");
  g_clear_pointer (&priv->sigint_source, g_source_unref);
  g_application_release (G_APPLICATION (daemon));

  return G_SOURCE_REMOVE;
}

static gboolean
sigterm_terminate_daemon (gpointer user_data)
{
  GrdDaemon *daemon = user_data;
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  g_debug ("Received SIGTERM signal. Exiting...");
  g_clear_pointer (&priv->sigterm_source, g_source_unref);
  g_application_release (G_APPLICATION (daemon));

  return G_SOURCE_REMOVE;
}

static void
register_signals (GrdDaemon *daemon)
{
  GrdDaemonPrivate *priv = grd_daemon_get_instance_private (daemon);

  priv->sigint_source = g_unix_signal_source_new (SIGINT);
  g_source_set_callback (priv->sigint_source, sigint_terminate_daemon,
                         daemon, NULL);
  g_source_attach (priv->sigint_source, NULL);

  priv->sigterm_source = g_unix_signal_source_new (SIGTERM);
  g_source_set_callback (priv->sigterm_source, sigterm_terminate_daemon,
                         daemon, NULL);
  g_source_attach (priv->sigterm_source, NULL);
}

static int
count_trues (int n_args, ...)
{
  va_list booleans;
  int booleans_count = 0;

  va_start (booleans, n_args);

  for (int i = 0; i < n_args; ++i)
    booleans_count += va_arg (booleans, gboolean) ? 1 : 0;

  return booleans_count;
}

int
main (int argc, char **argv)
{
  GrdContext *context;
  GrdSettings *settings;
  gboolean print_version = FALSE;
  gboolean headless = FALSE;
  gboolean system = FALSE;
  gboolean handover = FALSE;
  int rdp_port = -1;
  int vnc_port = -1;
  int max_parallel_connections = DEFAULT_MAX_PARALLEL_CONNECTIONS;

  GOptionEntry entries[] = {
    { "version", 0, 0, G_OPTION_ARG_NONE, &print_version,
      "Print version", NULL },
    { "headless", 0, 0, G_OPTION_ARG_NONE, &headless,
      "Run in headless mode", NULL },
#if defined(HAVE_RDP) && defined(HAVE_LIBSYSTEMD)
    { "system", 0, 0, G_OPTION_ARG_NONE, &system,
      "Run in headless mode as a system g-r-d service", NULL },
    { "handover", 0, 0, G_OPTION_ARG_NONE, &handover,
      "Run in headless mode taking a connection from system g-r-d service", NULL },
#endif /* HAVE_RDP && HAVE_LIBSYSTEMD */
    { "rdp-port", 0, 0, G_OPTION_ARG_INT, &rdp_port,
      "RDP port", NULL },
    { "vnc-port", 0, 0, G_OPTION_ARG_INT, &vnc_port,
      "VNC port", NULL },
    { "max-parallel-connections", 0, 0,
      G_OPTION_ARG_INT, &max_parallel_connections,
      "Max number of parallel connections (0 for unlimited, "
      "default: " QUOTE(DEFAULT_MAX_PARALLEL_CONNECTIONS) ")", NULL },
    { NULL }
  };
  g_autoptr (GOptionContext) option_context = NULL;
  g_autoptr (GrdDaemon) daemon = NULL;
  g_autoptr (GError) error = NULL;
  GrdRuntimeMode runtime_mode;

  g_set_application_name (_("GNOME Remote Desktop"));

  option_context = g_option_context_new (NULL);
  g_option_context_add_main_entries (option_context, entries, GETTEXT_PACKAGE);
  if (!g_option_context_parse (option_context, &argc, &argv, &error))
    {
      g_printerr ("Invalid option: %s\n", error->message);
      return EXIT_FAILURE;
    }

  if (print_version)
    {
      g_print ("GNOME Remote Desktop %s\n", VERSION);
      return EXIT_SUCCESS;
    }

  if (count_trues (3, headless, system, handover) > 1)
    {
      g_printerr ("Invalid option: More than one runtime mode specified\n");
      return EXIT_FAILURE;
    }

  if (max_parallel_connections == 0)
    {
      max_parallel_connections = INT_MAX;
    }
  else if (max_parallel_connections < 0)
    {
      g_printerr ("Invalid number of max parallel connections: %d\n",
                  max_parallel_connections);
      return EXIT_FAILURE;
    }

  if (headless)
    runtime_mode = GRD_RUNTIME_MODE_HEADLESS;
  else if (system)
    runtime_mode = GRD_RUNTIME_MODE_SYSTEM;
  else if (handover)
    runtime_mode = GRD_RUNTIME_MODE_HANDOVER;
  else
    runtime_mode = GRD_RUNTIME_MODE_SCREEN_SHARE;

  switch (runtime_mode)
    {
    case GRD_RUNTIME_MODE_SCREEN_SHARE:
    case GRD_RUNTIME_MODE_HEADLESS:
      daemon = GRD_DAEMON (grd_daemon_user_new (runtime_mode, &error));
      break;
#ifdef HAVE_LIBSYSTEMD
    case GRD_RUNTIME_MODE_SYSTEM:
      daemon = GRD_DAEMON (grd_daemon_system_new (&error));
      break;
    case GRD_RUNTIME_MODE_HANDOVER:
      daemon = GRD_DAEMON (grd_daemon_handover_new (&error));
      break;
#else
    case GRD_RUNTIME_MODE_SYSTEM:
    case GRD_RUNTIME_MODE_HANDOVER:
      g_assert_not_reached ();
      break;
#endif /* HAVE_LIBSYSTEMD */
    }

  if (!daemon)
    {
      g_printerr ("Failed to initialize: %s\n", error->message);
      return EXIT_FAILURE;
    }

  add_actions (G_APPLICATION (daemon));
  register_signals (daemon);

  context = grd_daemon_get_context (daemon);
  settings = grd_context_get_settings (context);
  if (rdp_port != -1)
    grd_settings_override_rdp_port (settings, rdp_port);
  if (vnc_port != -1)
    grd_settings_override_vnc_port (settings, vnc_port);

  grd_settings_override_max_parallel_connections (settings,
                                                  max_parallel_connections);

  return g_application_run (G_APPLICATION (daemon), argc, argv);
}
