/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2006 Novell, Inc.
 * Copyright (C) 2008 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
 * Lesser 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 - Suite 500, Boston, MA
 * 02110-1335, USA.
 */

#include <config.h>

#include <libintl.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

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

#include "mdm-log.h"

#include "csm-util.h"
#include "csm-manager.h"
#include "csm-session-fill.h"
#include "csm-store.h"
#include "csm-system.h"

#define CSM_DBUS_NAME "org.gnome.SessionManager"

static gboolean failsafe = FALSE;
static gboolean show_version = FALSE;
static gboolean debug = FALSE;
static gboolean please_fail = FALSE;
static const char *session_name = NULL;


static CsmManager *manager = NULL;

static GMainLoop *loop;

void
csm_quit (void)
{
        if (loop != NULL)
                g_main_loop_quit (loop);
        else
                exit(0);
}

static void
csm_main (void)
{
        if (loop == NULL)
                loop = g_main_loop_new (NULL, TRUE);

        g_main_loop_run (loop);
}


static void
on_name_lost (GDBusConnection *connection,
              const char *name,
              gpointer    data)
{
        if (connection == NULL) {
                csm_util_init_error (TRUE, "Lost name on bus: org.gnome.SessionManager");
        } else {
                g_debug ("Calling name lost callback function");

                /*
                 * When the signal handler gets a shutdown signal, it calls
                 * this function to inform CsmManager to not restart
                 * applications in the off chance a handler is already queued
                 * to dispatch following the below call to csm_quit.
                 */
                csm_manager_set_phase (manager, CSM_MANAGER_PHASE_EXIT);

                csm_quit ();
        }
}

static void
on_name_acquired (GDBusConnection *connection,
                  const char *name,
                  gpointer data)
{
        g_debug ("main: Name acquired");

        csm_manager_start (manager);
}

static gboolean
term_or_int_signal_cb (gpointer data)
{
        CsmManager *manager = (CsmManager *)data;

        /* let the fatal signals interrupt us */
        g_debug ("Caught SIGINT/SIGTERM, shutting down normally.");

        csm_manager_logout (manager, CSM_MANAGER_LOGOUT_MODE_FORCE, NULL);

        return FALSE;
}

static void
on_bus_acquired (GDBusConnection *connection,
                 const char *name,
                 gpointer data)
{
        CsmStore *client_store;

        g_debug ("main: Bus acquired");

        client_store = csm_store_new ();

        manager = csm_manager_new (client_store, failsafe);

        g_object_unref (client_store);

        g_unix_signal_add (SIGTERM, term_or_int_signal_cb, manager);
        g_unix_signal_add (SIGINT, term_or_int_signal_cb, manager);

        if (IS_STRING_EMPTY (session_name))
                session_name = _csm_manager_get_default_session (manager);

        if (!csm_session_fill (manager, session_name)) {
                csm_util_init_error (TRUE, "Failed to load session \"%s\"", session_name ? session_name : "(null)");
        }
}

static gboolean
acquire_name (void)
{
        return g_bus_own_name (G_BUS_TYPE_SESSION,
                               CSM_DBUS_NAME,
                               G_BUS_NAME_OWNER_FLAGS_NONE,
                               on_bus_acquired,
                               on_name_acquired,
                               on_name_lost,
                               NULL, NULL);
}

static gboolean
require_dbus_session (int      argc,
                      char   **argv,
                      GError **error)
{
        char **new_argv;
        int    i;

        if (g_getenv ("DBUS_SESSION_BUS_ADDRESS"))
                return TRUE;

        /* Just a sanity check to prevent infinite recursion if
         * dbus-launch fails to set DBUS_SESSION_BUS_ADDRESS 
         */
        g_return_val_if_fail (!g_str_has_prefix (argv[0], "dbus-launch"),
                              TRUE);

        /* +2 for our new arguments, +1 for NULL */
        new_argv = g_malloc ((argc + 3) * sizeof (*argv));

        new_argv[0] = "dbus-launch";
        new_argv[1] = "--exit-with-session";
        for (i = 0; i < argc; i++) {
                new_argv[i + 2] = argv[i];
	}
        new_argv[i + 2] = NULL;
        
        if (!execvp ("dbus-launch", new_argv)) {
                g_set_error (error, 
                             G_SPAWN_ERROR,
                             G_SPAWN_ERROR_FAILED,
                             "No session bus and could not exec dbus-launch: %s",
                             g_strerror (errno));
                return FALSE;
        }

        /* Should not be reached */
        return TRUE;
}

/* Whether ~/.xinputrc (written by im-config / mintlocale-im) selects fcitx5.
 * On Wayland this is the source of truth, since we do not export
 * GTK_IM_MODULE=fcitx there (apps use text-input-v3 via the compositor).
 * Mirrors js/misc/imFramework.js.
 *
 * The "run_im fcitx5" prefix match (with '#'-comment skip) is duplicated in
 * muffin src/core/meta-im-mode.c (we run before muffin exists, so we can't
 * ask it) and cinnamon's cinnamon-settings bin/util.py. If im-config ever
 * changes its line format, all three must change together. */
static gboolean
xinputrc_selects_fcitx (void)
{
        gchar *path;
        gchar *contents = NULL;
        gboolean found = FALSE;

        path = g_build_filename (g_get_home_dir (), ".xinputrc", NULL);
        if (g_file_get_contents (path, &contents, NULL, NULL)) {
                gchar **lines = g_strsplit (contents, "\n", -1);
                int i;

                for (i = 0; lines[i]; i++) {
                        const gchar *line = g_strchug (lines[i]);

                        if (line[0] == '#')
                                continue;
                        if (g_str_has_prefix (line, "run_im fcitx5")) {
                                found = TRUE;
                                break;
                        }
                }
                g_strfreev (lines);
        }

        g_free (contents);
        g_free (path);

        return found;
}

int
main (int argc, char **argv)
{
        struct sigaction  sa;
        GError           *error;
        guint             name_owner_id;
        static char     **override_autostart_dirs = NULL;
        GOptionContext   *options;
        static GOptionEntry entries[] = {
                { "autostart", 'a', 0, G_OPTION_ARG_STRING_ARRAY, &override_autostart_dirs, N_("Override standard autostart directories"), N_("AUTOSTART_DIR") },
                { "session", 0, 0, G_OPTION_ARG_STRING, &session_name, N_("Session to use"), N_("SESSION_NAME") },
                { "debug", 0, 0, G_OPTION_ARG_NONE, &debug, N_("Enable debugging code"), NULL },
                { "failsafe", 'f', 0, G_OPTION_ARG_NONE, &failsafe, N_("Do not load user-specified applications"), NULL },
                { "version", 0, 0, G_OPTION_ARG_NONE, &show_version, N_("Version of this application"), NULL },
                /* Translators: the 'fail whale' is the black dialog we show when something goes seriously wrong */
                { "whale", 0, 0, G_OPTION_ARG_NONE, &please_fail, N_("Show the fail whale dialog for testing"), NULL },
                { NULL, 0, 0, 0, NULL, NULL, NULL }
        };

        GSettings *settings;
        settings = g_settings_new ("org.cinnamon.SessionManager");

        if (g_settings_get_boolean (settings, "x-sync")) {
            csm_util_setenv ("GDK_SYNCHRONIZE", "1");
        }

        /* Make sure that we have a session bus */
        if (!require_dbus_session (argc, argv, &error)) {
                csm_util_init_error (TRUE, "%s", error->message);
        }

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

        if (g_settings_get_boolean (settings, "debug")) {
            debug = TRUE;
        }
        g_clear_object (&settings);

        sa.sa_handler = SIG_IGN;
        sa.sa_flags = 0;
        sigemptyset (&sa.sa_mask);
        sigaction (SIGPIPE, &sa, 0);

        error = NULL;
        options = g_option_context_new (_(" - the Cinnamon session manager"));
        g_option_context_add_main_entries (options, entries, GETTEXT_PACKAGE);
        g_option_context_parse (options, &argc, &argv, &error);

        if (error != NULL) {
                g_warning ("%s", error->message);
                exit (1);
        }

        g_option_context_free (options);

        if (show_version) {
                g_print ("%s %s\n", argv [0], VERSION);
                exit (1);
        }

        if (please_fail) {
                csm_util_init_error (TRUE, "Testing the fail whale");
        }

        csm_util_export_activation_environment (NULL);
        csm_util_export_user_environment (NULL);

        {
                gchar *ibus_path;
                gchar *fcitx_path;
                gboolean is_wayland;
                gboolean fcitx_selected;
                gboolean fcitx_active;

                ibus_path = g_find_program_in_path ("ibus-daemon");
                fcitx_path = g_find_program_in_path ("fcitx5");

                /* When fcitx is the active framework, it owns the Qt input env;
                 * don't override it with the ibus QT_IM_MODULES list (Qt6 prefers that
                 * list over QT_IM_MODULE and would try ibus first). Detection follows
                 * js/misc/imFramework.js: env on X11, ~/.xinputrc on Wayland (where we
                 * do not set GTK_IM_MODULE=fcitx). */
                is_wayland = csm_util_is_wayland_session ();

                if (is_wayland) {
                        fcitx_selected = xinputrc_selects_fcitx ();
                } else {
                        const gchar *gtk_im = g_getenv ("GTK_IM_MODULE");
                        const gchar *xmod = g_getenv ("XMODIFIERS");
                        fcitx_selected = (gtk_im && g_strrstr (gtk_im, "fcitx")) ||
                                         (xmod && g_strrstr (xmod, "fcitx"));
                }
                fcitx_active = fcitx_selected && (fcitx_path != NULL);

                /* On Wayland, apps receive input methods through the compositor
                 * (text-input-v3), not client-side IM modules. Clear any client-side
                 * IM env up front - including values leaked from an earlier same-boot
                 * X11 session via the persistent systemd/D-Bus activation environment,
                 * which would otherwise force apps off text-input-v3 (e.g. a stale
                 * GTK_IM_MODULE=ibus routes GTK to the client-side ibus module, whose
                 * candidate popup can't be positioned on Wayland). Passing "" unsets
                 * each var (csm_util_setenv treats an empty value as unset). The block
                 * below re-establishes just what this session needs. X11 is left
                 * untouched: there the client-side modules are the input path. */
                if (is_wayland) {
                        const gchar *im_vars[] = {
                                "GTK_IM_MODULE", "CLUTTER_IM_MODULE", "QT_IM_MODULE",
                                "QT_IM_MODULES", "XMODIFIERS", "SDL_IM_MODULE",
                        };
                        guint i;

                        for (i = 0; i < G_N_ELEMENTS (im_vars); i++)
                                csm_util_setenv (im_vars[i], "");
                }

                /* SDL_IM_MODULE is the one client-side IM var fcitx sets that ibus
                 * never does (im-config's 21_ibus.rc deliberately skips it, Debian
                 * #1008481), so a fcitx value survives a fcitx->ibus switch via the
                 * activation environment. Clear it whenever fcitx is not active - on
                 * X11 too, since ibus has no SDL module and SDL defaults to ibus when
                 * the var is unset. (Wayland already cleared it above.) */
                if (!is_wayland && !fcitx_active)
                        csm_util_setenv ("SDL_IM_MODULE", "");

                /* XWayland clients cannot reach fcitx through the compositor
                 * (text-input-v3 stops at the Wayland boundary), so they need
                 * the classic client-side paths back: XMODIFIERS drives XIM for
                 * Xlib and GTK X11 apps, and Qt tries the QT_IM_MODULES list in
                 * order, keeping native Qt apps on the compositor's text-input
                 * while xcb apps fall through to the fcitx plugin (Qt5 has only
                 * the singular and loads the fcitx plugin everywhere - fcitx
                 * still draws, mirroring im-config's X11 setup). GTK Wayland
                 * apps keep using text-input-v3: GTK_IM_MODULE stays unset. */
                if (is_wayland && fcitx_active) {
                        csm_util_setenv ("XMODIFIERS", "@im=fcitx");
                        csm_util_setenv ("QT_IM_MODULES", "wayland;fcitx");
                        csm_util_setenv ("QT_IM_MODULE", "fcitx");
                        csm_util_setenv ("SDL_IM_MODULE", "fcitx");
                }

                if (ibus_path && !fcitx_active) {
                        const gchar *p;
                        p = g_getenv ("QT_IM_MODULES");
                        if (!p || !*p)
                                p = "wayland;ibus";
                        csm_util_setenv ("QT_IM_MODULES", p);
                        p = g_getenv ("QT_IM_MODULE");
                        if (!p || !*p)
                                p = "ibus";
                        csm_util_setenv ("QT_IM_MODULE", p);
                        p = g_getenv ("XMODIFIERS");
                        if (!p || !*p)
                                p = "@im=ibus";
                        csm_util_setenv ("XMODIFIERS", p);
                }

                g_free (ibus_path);
                g_free (fcitx_path);
        }

        mdm_log_init ();
        mdm_log_set_debug (debug);

        /* Some third-party programs rely on GNOME_DESKTOP_SESSION_ID to
         * detect if GNOME is running. We keep this for compatibility reasons.
         */
        csm_util_setenv ("GNOME_DESKTOP_SESSION_ID", "this-is-deprecated");

        csm_util_set_autostart_dirs (override_autostart_dirs);

        /* Talk to logind before acquiring a name, since it does synchronous
         * calls at initialization time that invoke a main loop and if we
         * already owned a name, then we would service too early during
         * that main loop.
         */
        g_object_unref (csm_get_system ());

        name_owner_id = acquire_name ();

        csm_main ();

        if (manager != NULL) {
                g_debug ("Unreffing manager");
                g_object_unref (manager);
        }

        csm_util_export_activation_environment (NULL);

        g_bus_unown_name (name_owner_id);

        mdm_log_shutdown ();

        return 0;
}
