/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */

#include "config.h"

#ifdef HAVE_MALLINFO
#include <malloc.h>
#endif
#include <stdlib.h>
#include <string.h>

#include <clutter/clutter.h>
#include <dbus/dbus-shared.h>
#include <glib/gi18n-lib.h>

#if USE_GIR20
#include <girepository/girepository.h>
#else
#include <girepository.h>
#endif

#include <meta/main.h>
#include <meta/meta-plugin.h>
#include <meta/prefs.h>
#include <meta/util.h>

#include <atk-bridge.h>
#include "cinnamon-global.h"
#include "cinnamon-global-private.h"
#include "cinnamon-perf-log.h"
#include "st.h"

extern GType cinnamon_plugin_get_type (void);

#define CINNAMON_DBUS_SERVICE "org.Cinnamon"
#define MAGNIFIER_DBUS_SERVICE "org.gnome.Magnifier"

static void
cinnamon_dbus_acquire_name (GDBusProxy *bus,
                         guint32     request_name_flags,
                         guint32    *request_name_result,
                         gchar      *name,
                         gboolean    fatal)
{
  GError *error = NULL;
  GVariant *request_name_variant;

  if (!(request_name_variant = g_dbus_proxy_call_sync (bus,
                                                       "RequestName",
                                                       g_variant_new ("(su)", name, request_name_flags),
                                                       0, /* call flags */
                                                       -1, /* timeout */
                                                       NULL, /* cancellable */
                                                       &error)))
    {
      g_printerr ("failed to acquire %s: %s\n", name, error->message);
      g_clear_error (&error);
      if (!fatal)
        return;
      exit (1);
    }
  g_variant_get (request_name_variant, "(u)", request_name_result);
  g_variant_unref (request_name_variant);
}

static void
cinnamon_dbus_acquire_names (GDBusProxy *bus,
                          guint32     request_name_flags,
                          gchar      *name,
                          gboolean    fatal, ...) G_GNUC_NULL_TERMINATED;

static void
cinnamon_dbus_acquire_names (GDBusProxy *bus,
                          guint32     request_name_flags,
                          gchar      *name,
                          gboolean    fatal, ...)
{
  va_list al;
  guint32 request_name_result;
  va_start (al, fatal);
  for (;;)
  {
    cinnamon_dbus_acquire_name (bus,
                             request_name_flags,
                             &request_name_result,
                             name, fatal);
    name = va_arg (al, gchar *);
    if (!name)
      break;
    fatal = va_arg (al, gboolean);
  }
  va_end (al);
}

static void
cinnamon_dbus_init (gboolean  replace,
                    gboolean *session_running)
{
  GDBusConnection *session;
  GDBusProxy *bus;
  GError *error = NULL;
  GVariant *session_result;
  guint32 request_name_flags;
  guint32 request_name_result;

  session = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);

  if (error) {
    g_printerr ("Failed to connect to session bus: %s", error->message);
    g_error_free (error);
    exit (1);
  }

  bus = g_dbus_proxy_new_sync (session,
                               G_DBUS_PROXY_FLAGS_NONE,
                               NULL, /* interface info */
                               "org.freedesktop.DBus",
                               "/org/freedesktop/DBus",
                               "org.freedesktop.DBus",
                               NULL, /* cancellable */
                               &error);

  if (error) {
    g_printerr ("Failed to create org.freedesktop.DBus proxy: %s", error->message);
    g_error_free (error);
    exit (1);
  }
  request_name_flags = G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT;
  if (replace)
    request_name_flags |= DBUS_NAME_FLAG_REPLACE_EXISTING;

  cinnamon_dbus_acquire_name (bus,
                           request_name_flags,
                           &request_name_result,
                           CINNAMON_DBUS_SERVICE, TRUE);
  if (!(request_name_result == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER
        || request_name_result == DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER))
    {
      g_printerr (CINNAMON_DBUS_SERVICE " already exists on bus and --replace not specified\n");
      exit (1);
    }

  /*
   * We always specify REPLACE_EXISTING to ensure we kill off
   * the existing service if it was running.
   */
  request_name_flags |= G_BUS_NAME_OWNER_FLAGS_REPLACE;

  cinnamon_dbus_acquire_names (bus,
                            request_name_flags,
  /* Also grab org.gnome.Panel to replace any existing panel process */
                            "org.gnome.Panel", TRUE,
  /* ...and the org.gnome.Magnifier service. */
                            MAGNIFIER_DBUS_SERVICE, FALSE,
                            NULL);
  /* ...and the on-screen keyboard service */
  cinnamon_dbus_acquire_name (bus,
                           DBUS_NAME_FLAG_REPLACE_EXISTING,
                           &request_name_result,
                           "org.gnome.Caribou.Keyboard", FALSE);

  /* At login, cinnamon.desktop requests that cinnamon-session start cinnamon
   * during CSM_MANAGER_PHASE_WINDOW_MANAGER.  This call should return FALSE.
   *
   * By the time main.js gets around to setting up the startup animation,
   * cinnamon-session is in running mode, and this would return TRUE, so this
   * check has to be done before registering with cinnamon-session (which is its
   * queue that it can continue to the remaining phases).
   *
   * When cinnamon is restarted during the session, this should always return
   * TRUE.
   *
   * This gets passed to cinnamon-global, which main.js can access and help it
   * to decide whether or not to run the startup animation.
   *
   * Timeout after 1 second - this shouldn't be allowed to hold up cinnamon's
   * start-up.
   */
  session_result = g_dbus_connection_call_sync (session,
                                                "org.gnome.SessionManager",
                                                "/org/gnome/SessionManager",
                                                "org.gnome.SessionManager",
                                                "IsSessionRunning",
                                                NULL,
                                                G_VARIANT_TYPE ("(b)"),
                                                G_DBUS_CALL_FLAGS_NONE,
                                                1000,
                                                NULL,
                                                &error);

  if (session_result)
    {
      g_variant_get (session_result, "(b)", session_running);
      g_variant_unref (session_result);
    }
  else
    {
      *session_running = FALSE; // This will let the setting decide;
      g_clear_error (&error);
    }

  g_object_unref (bus);
  g_object_unref (session);
}

static void
malloc_statistics_callback (CinnamonPerfLog *perf_log,
                            gpointer      data)
{
#ifdef HAVE_MALLINFO
  struct mallinfo info = mallinfo ();

  cinnamon_perf_log_update_statistic_i (perf_log,
                                     "malloc.arenaSize",
                                     info.arena);
  cinnamon_perf_log_update_statistic_i (perf_log,
                                     "malloc.mmapSize",
                                     info.hblkhd);
  cinnamon_perf_log_update_statistic_i (perf_log,
                                     "malloc.usedSize",
                                     info.uordblks);
#endif
}

static void
cinnamon_a11y_init (void)
{
  cally_accessibility_init ();

  if (clutter_get_accessibility_enabled () == FALSE)
    {
      g_warning ("Accessibility: clutter has no accessibility enabled"
                 " skipping the atk-bridge load");
    }
  else
    {
      atk_bridge_adaptor_init (NULL, NULL);
    }
}

static void
cinnamon_perf_log_init (void)
{
  CinnamonPerfLog *perf_log = cinnamon_perf_log_get_default ();

  /* For probably historical reasons, mallinfo() defines the returned values,
   * even those in bytes as int, not size_t. We're determined not to use
   * more than 2G of malloc'ed memory, so are OK with that.
   */
  cinnamon_perf_log_define_statistic (perf_log,
                                   "malloc.arenaSize",
                                   "Amount of memory allocated by malloc() with brk(), in bytes",
                                   "i");
  cinnamon_perf_log_define_statistic (perf_log,
                                   "malloc.mmapSize",
                                   "Amount of memory allocated by malloc() with mmap(), in bytes",
                                   "i");
  cinnamon_perf_log_define_statistic (perf_log,
                                   "malloc.usedSize",
                                   "Amount of allocated memory currently in use",
                                   "i");

  cinnamon_perf_log_add_statistics_callback (perf_log,
                                          malloc_statistics_callback,
                                          NULL, NULL);
}

static void
muted_log_handler (const char     *log_domain,
                   GLogLevelFlags  log_level,
                   const char     *message,
                   gpointer        data)
{
  /* Intentionally empty to discard message */
}

static gboolean
print_version (const gchar    *option_name,
               const gchar    *value,
               gpointer        data,
               GError        **error)
{
  g_print ("Cinnamon %s\n", VERSION);
  exit (0);
}

GOptionEntry gnome_cinnamon_options[] = {
  {
    "version", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
    print_version,
    N_("Print version"),
    NULL
  },
  { NULL }
};

static void
center_pointer_on_screen ()
{
  Display *dpy;
  Window root_window;
  Screen *screen;

  dpy = XOpenDisplay(0);
  root_window = XRootWindow(dpy, 0);
  XSelectInput(dpy, root_window, KeyReleaseMask);
  screen = DefaultScreenOfDisplay(dpy);
  XWarpPointer(dpy, None, root_window, 0, 0, 0, 0, WidthOfScreen(screen)/2, HeightOfScreen(screen)/2);
  XFlush(dpy);
}

int
main (int argc, char **argv)
{
  GOptionContext *ctx;
  GError *error = NULL;
  int ecode;
  gboolean session_running;
  gchar *env_no_gail;
  gchar *env_no_at_bridge;

  bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
  bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
  textdomain (GETTEXT_PACKAGE);

  g_setenv ("GDK_SCALE", "1", TRUE);

  ctx = meta_get_option_context ();
  g_option_context_add_main_entries (ctx, gnome_cinnamon_options, GETTEXT_PACKAGE);
  if (!g_option_context_parse (ctx, &argc, &argv, &error))
    {
      g_printerr ("%s: %s\n", argv[0], error->message);
      exit (1);
    }

  g_option_context_free (ctx);

  meta_plugin_manager_set_plugin_type (cinnamon_plugin_get_type ());

  /* Prevent meta_init() from causing gtk to load gail and at-bridge */
  env_no_gail = g_strdup (g_getenv ("NO_GAIL"));
  env_no_at_bridge = g_strdup (g_getenv ("NO_AT_BRIDGE"));
  g_setenv ("NO_GAIL", "1", TRUE);
  g_setenv ("NO_AT_BRIDGE", "1", TRUE);
  meta_init ();
  if (env_no_gail != NULL)
    {
      g_setenv ("NO_GAIL", env_no_gail, TRUE);
      g_free (env_no_gail);
    }
  else
    g_unsetenv ("NO_GAIL");
  if (env_no_at_bridge != NULL)
    {
      g_setenv ("NO_AT_BRIDGE", env_no_at_bridge, TRUE);
      g_free (env_no_at_bridge);
    }
  else
    g_unsetenv ("NO_AT_BRIDGE");

  /* FIXME: Add gjs API to set this stuff and don't depend on the
   * environment.  These propagate to child processes.
   */
  g_setenv ("GJS_DEBUG_OUTPUT", "stderr", TRUE);
  g_setenv ("GJS_DEBUG_TOPICS", "JS ERROR;JS LOG", TRUE);

  g_setenv ("CINNAMON_VERSION", VERSION, TRUE);

  if (!meta_is_wayland_compositor ())
    {
      center_pointer_on_screen ();
    }

  cinnamon_dbus_init (meta_get_replace_current_wm (),
                      &session_running);
  cinnamon_a11y_init ();
  cinnamon_perf_log_init ();

#if USE_GIR20
  g_autoptr (GIRepository) repo = NULL;
  repo = gi_repository_dup_default ();

  gi_repository_prepend_search_path (repo, CINNAMON_PKGLIBDIR);
  gi_repository_prepend_search_path (repo, MUFFIN_TYPELIB_DIR);
#else
  g_irepository_prepend_search_path (CINNAMON_PKGLIBDIR);
  g_irepository_prepend_search_path (MUFFIN_TYPELIB_DIR);
#endif

  /* We need to explicitly add the directories where the private libraries are
   * installed to the GIR's library path, so that they can be found at runtime
   * when linking using DT_RUNPATH (instead of DT_RPATH), which is the default
   * for some linkers (e.g. gold) and in some distros (e.g. Debian).
   */

#if USE_GIR20
  gi_repository_prepend_library_path (repo, CINNAMON_PKGLIBDIR);
  gi_repository_prepend_library_path (repo, MUFFIN_TYPELIB_DIR);
#else
  g_irepository_prepend_library_path (CINNAMON_PKGLIBDIR);
  g_irepository_prepend_library_path (MUFFIN_TYPELIB_DIR);
#endif

  /* Disable debug spew from various libraries */
  g_log_set_handler ("Cvc", G_LOG_LEVEL_DEBUG,
                     muted_log_handler, NULL);
  g_log_set_handler ("AccountsService", G_LOG_LEVEL_DEBUG,
                     muted_log_handler, NULL);
  g_log_set_handler ("Bluetooth", G_LOG_LEVEL_DEBUG | G_LOG_LEVEL_MESSAGE,
                     muted_log_handler, NULL);
  g_log_set_handler ("XApp", G_LOG_LEVEL_DEBUG,
                     muted_log_handler, NULL);
  /* Initialize the global object */
  _cinnamon_global_init ("session-running", session_running,
                         NULL);

  g_unsetenv ("GDK_SCALE");

  ecode = meta_run ();

  if (g_getenv ("CINNAMON_ENABLE_CLEANUP"))
    {
      g_printerr ("Doing final cleanup...\n");
      g_object_unref (cinnamon_global_get ());
    }

  return ecode;
}
