/*
 * Copyright 2021-2025 Robert Tari <robert@tari.in>
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3, as published
 * by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranties of
 * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>.
 */

#include <act/act.h>
#include <xkbcommon/xkbregistry.h>
#include <glib-object.h>
#include <libudev.h>
#include "languages.h"
#include "keyboard.h"
#include "system-layouts.h"

gchar *LOMIRI_TO_ISO[][2] =
{
    {"ar", "ara"},
    {"bn", "bd"},
    {"bn-probhat", "bd+probhat"},
    {"bs", "ba"},
    {"cs", "cz"},
    {"da", "dk"},
    {"el", "gr"},
    {"en", "us"},
    {"endv", "us+dvorak"},
    {"eo", "epo"},
    {"fa", "ir"},
    {"fr-ch", "ch+fr"},
    {"gd", "gb+gla"},
    {"he", "il"},
    {"ja", "jp"},
    {"ko", "kr"},
    {"nb", "no"},
    {"sl", "si"},
    {"sr", "rs"},
    {"sv", "se"},
    {"uk", "ua"},
    {NULL, NULL}
};

enum
{
    LAYOUT_CHANGED,
    CONFIG_CHANGED,
    LAST_SIGNAL
};

static guint m_lSignals[LAST_SIGNAL];

struct _KeyboardPrivate
{
    GHashTable *lLayouts;
    guint nLayout;
    guint nLayoutOSK;
    GSList *lLayoutRec;
    GSList *lLayoutRecOSK;
    GSList *lUsers;
    GSettings *pSettings;
    struct udev *pUdev;
    struct udev_monitor *pMonitor;
    GIOChannel *pChannel;
    gboolean bHardwareKeyboard;
    gboolean bSoftwareKeyboard;
    GSettings *pLomiriSettings;
    GSettings *pMaliitSettings;
};

typedef KeyboardPrivate priv_t;

G_DEFINE_TYPE_WITH_PRIVATE(Keyboard, keyboard, G_TYPE_OBJECT)

typedef struct _Layout
{
    gchar *sId;
    gchar *sLanguage;
    gchar *sDescription;

} Layout;

static int emitDelayedSignal(Keyboard *pKeyboard)
{
    g_signal_emit(pKeyboard, m_lSignals[CONFIG_CHANGED], 0);

    return G_SOURCE_REMOVE;
}

static gboolean isGreeter()
{
    const char *sUser = g_get_user_name();

    return g_str_equal(sUser, "lightdm");
}

static void getAccountsService(Keyboard *pKeyboard)
{
    gboolean bChanged = FALSE;

    for (GSList *lUser = pKeyboard->pPrivate->lUsers; lUser; lUser = lUser->next)
    {
        ActUser *pUser = lUser->data;
        gboolean bIsUserLoaded = act_user_is_loaded(pUser);

        if (bIsUserLoaded)
        {
            gint nUid = act_user_get_uid(pUser);
            gchar *sPath = g_strdup_printf("/org/freedesktop/Accounts/User%i", nUid);
            GDBusConnection *pConnection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, NULL);
            GDBusProxy *pProxy = g_dbus_proxy_new_sync(pConnection, G_DBUS_PROXY_FLAGS_NONE, NULL, "org.freedesktop.Accounts", sPath, "org.freedesktop.DBus.Properties", NULL, NULL);
            GVariant *pValue = g_dbus_proxy_call_sync(pProxy, "Get", g_variant_new("(ss)", "org.ayatana.indicator.keyboard.AccountsService", "Layouts"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL);
            GVariant *pChild0 = g_variant_get_child_value(pValue, 0);
            GVariant *pChild1 = g_variant_get_child_value(pChild0, 0);

            GVariantIter *pIter;

            g_variant_get(pChild1, "as", &pIter);

            gchar *sLayout;

            while (g_variant_iter_loop(pIter, "s", &sLayout))
            {
                if (g_slist_find_custom(pKeyboard->pPrivate->lLayoutRec, sLayout, (GCompareFunc)g_strcmp0) == NULL)
                {
                    pKeyboard->pPrivate->lLayoutRec = g_slist_append(pKeyboard->pPrivate->lLayoutRec, g_strdup(sLayout));
                    bChanged = TRUE;
                }
            }

            g_variant_iter_free(pIter);
            g_variant_unref(pChild1);
            g_variant_unref(pChild0);
            g_variant_unref(pValue);
            g_object_unref(pConnection);
            g_free(sPath);
        }
    }

    if (bChanged == TRUE)
    {
        g_timeout_add(500, (GSourceFunc)emitDelayedSignal, pKeyboard);
    }
}

static void onUserLoaded(Keyboard *pKeyboard, ActUser *pUser)
{
    gboolean bIsUserLoaded = act_user_is_loaded(pUser);

    if (bIsUserLoaded)
    {
        getAccountsService(pKeyboard);
        g_signal_handlers_disconnect_by_func(G_OBJECT(pUser), G_CALLBACK(onUserLoaded), pKeyboard);
    }
}

static void onManagerLoaded(Keyboard *pKeyboard)
{
    ActUserManager *pManager = act_user_manager_get_default();
    gboolean bIsLoaded;
    g_object_get(pManager, "is-loaded", &bIsLoaded, NULL);

    if (bIsLoaded)
    {
        pKeyboard->pPrivate->lUsers = act_user_manager_list_users(pManager);

        for (GSList *lUser = pKeyboard->pPrivate->lUsers; lUser; lUser = lUser->next)
        {
            ActUser *pUser = lUser->data;
            gboolean bIsUserLoaded = act_user_is_loaded(pUser);

            if (bIsUserLoaded)
            {
                getAccountsService(pKeyboard);
            }
            else
            {
                g_signal_connect_swapped(pUser, "notify::is-loaded", G_CALLBACK(onUserLoaded), pKeyboard);
            }
        }
    }
}

static void freeLayout(gpointer pData)
{
    Layout *pLayout = pData;

    g_return_if_fail(pLayout != NULL);

    g_free(pLayout->sId);
    g_free(pLayout->sLanguage);
    g_free(pLayout->sDescription);
    g_slice_free(Layout, pLayout);
}

void keyboard_AddSource(Keyboard *pKeyboard)
{
    return;
}

guint keyboard_GetNumLayouts(Keyboard *pKeyboard, gboolean bOSK)
{
    guint nLayouts = 0;

    if (bOSK)
    {
        nLayouts = g_slist_length (pKeyboard->pPrivate->lLayoutRecOSK);
    }
    else
    {
        nLayouts = g_slist_length (pKeyboard->pPrivate->lLayoutRec);
    }

    return nLayouts;
}

guint keyboard_GetLayoutIndex (Keyboard *pKeyboard, gboolean bOSK)
{
    if (bOSK)
    {
        return pKeyboard->pPrivate->nLayoutOSK;
    }
    else
    {
        return pKeyboard->pPrivate->nLayout;
    }
}

void keyboard_GetLayout(Keyboard *pKeyboard, gboolean bOSK, gint nLayout, gchar **pLanguage, gchar **pDescription, gchar **pId)
{

    GSList *lLayoutRec = NULL;

    if (bOSK)
    {
        if (nLayout == -1)
        {
            nLayout = pKeyboard->pPrivate->nLayoutOSK;
        }
        lLayoutRec = pKeyboard->pPrivate->lLayoutRecOSK;
    }
    else
    {
        if (nLayout == -1)
        {
            nLayout = pKeyboard->pPrivate->nLayout;
        }
        lLayoutRec = pKeyboard->pPrivate->lLayoutRec;
    }

    gchar *sLayout = g_slist_nth_data (lLayoutRec, nLayout);
    const Layout *pLayout;
    g_hash_table_lookup_extended(pKeyboard->pPrivate->lLayouts, sLayout, NULL, (gpointer*)&pLayout);

    if (pLanguage != NULL)
    {
        *pLanguage = g_strdup(pLayout->sLanguage);
    }

    if (pDescription != NULL)
    {
        *pDescription = g_strdup(pLayout->sDescription);
    }

    if (pId != NULL)
    {
        *pId = g_strdup (sLayout);
    }
}

void keyboard_SetLayoutHardware(Keyboard *pKeyboard, gint nLayout)
{
    if (isGreeter() == FALSE)
    {
        gchar *sId = g_slist_nth_data (pKeyboard->pPrivate->lLayoutRec, nLayout);

        GVariantBuilder cSettingsBuilder;
        g_variant_builder_init (&cSettingsBuilder, G_VARIANT_TYPE ("a(ss)"));
        g_variant_builder_add (&cSettingsBuilder, "(ss)", "xkb", sId);

        GVariantBuilder cAccountsBuilder;
        g_variant_builder_init (&cAccountsBuilder, G_VARIANT_TYPE ("aa{ss}"));
        GVariantBuilder cDictBuilder;
        g_variant_builder_init (&cDictBuilder, G_VARIANT_TYPE ("a{ss}"));
        g_variant_builder_add (&cDictBuilder, "{ss}", "xkb", sId);
        GVariant *pDict = g_variant_builder_end (&cDictBuilder);
        g_variant_builder_add_value (&cAccountsBuilder, pDict);

        guint nSources = g_slist_length (pKeyboard->pPrivate->lLayoutRec);

        for (guint nSource = 0; nSource < nSources; nSource++)
        {
            if (nSource != nLayout)
            {
                gchar *sId = g_slist_nth_data (pKeyboard->pPrivate->lLayoutRec, nSource);

                g_variant_builder_add (&cSettingsBuilder, "(ss)", "xkb", sId);

                GVariantBuilder cDictBuilder;
                g_variant_builder_init (&cDictBuilder, G_VARIANT_TYPE ("a{ss}"));
                g_variant_builder_add (&cDictBuilder, "{ss}", "xkb", sId);
                GVariant *pDict = g_variant_builder_end (&cDictBuilder);
                g_variant_builder_add_value (&cAccountsBuilder, pDict);
            }
        }

        GVariant *pSettingsSources = g_variant_builder_end (&cSettingsBuilder);
        g_settings_set_value (pKeyboard->pPrivate->pSettings, "sources", pSettingsSources);

        GDBusConnection *pConnection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, NULL);
        gint nUid = geteuid ();
        gchar *sPath = g_strdup_printf ("/org/freedesktop/Accounts/User%i", nUid);
        GDBusProxy *pProxy = g_dbus_proxy_new_sync (pConnection, G_DBUS_PROXY_FLAGS_NONE, NULL, "org.freedesktop.Accounts", sPath, "org.freedesktop.DBus.Properties", NULL, NULL);
        g_free (sPath);
        GVariant *pAccountsSources = g_variant_builder_end (&cAccountsBuilder);

        #ifdef ENABLE_UBUNTU_ACCOUNTSSERVICE
            gchar *sInterface = "org.freedesktop.Accounts.User";
        #else
            gchar *sInterface = "com.lomiri.shell.AccountsService";
        #endif

        GVariant *pRet = g_dbus_proxy_call_sync (pProxy, "Set", g_variant_new ("(ssv)", sInterface, "InputSources", pAccountsSources), G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL);
        g_variant_unref (pRet);
        g_object_unref (pConnection);
    }
    else
    {
        // TODO
        gchar *sCommand;
        gchar *sId = g_slist_nth_data(pKeyboard->pPrivate->lLayoutRec, nLayout);
        gchar **lParamas = g_strsplit(sId, "+", -1);
        guint nParams = g_strv_length(lParamas);

        if (nParams == 1)
        {
            sCommand = g_strdup_printf("setxkbmap -layout %s", lParamas[0]);
        }
        else
        {
            sCommand = g_strdup_printf("setxkbmap -layout %s -variant %s", lParamas[0], lParamas[1]);
        }

        g_strfreev(lParamas);

        gchar *sOutput = NULL;
        GError *pError = NULL;
        gboolean bResult = g_spawn_command_line_sync(sCommand, &sOutput, NULL, NULL, &pError);
        g_free(sCommand);

        if (bResult == FALSE)
        {
            g_message("COMMAND 01 %s: %s", sOutput, pError->message);

            return;
        }

        pKeyboard->pPrivate->nLayout = nLayout;
        g_signal_emit(pKeyboard, m_lSignals[LAYOUT_CHANGED], 0);
    }
}

void keyboard_SetLayoutSoftware(Keyboard *pKeyboard, gint nLayout)
{
    if (isGreeter() == FALSE)
    {
        gchar *sId = g_slist_nth_data (pKeyboard->pPrivate->lLayoutRecOSK, nLayout);
        guint nId = 0;
        gchar *sLayoutOSK = NULL;

        while (LOMIRI_TO_ISO[nId][0] != NULL)
        {
            gboolean bEqual = g_str_equal (LOMIRI_TO_ISO[nId][1], sId);

            if (bEqual)
            {
                sLayoutOSK = LOMIRI_TO_ISO[nId][0];

                break;
            }

            nId++;
        }

        if (!sLayoutOSK)
        {
            sLayoutOSK = sId;
        }

        guint nEnabledLayoutsOSK = g_slist_length (pKeyboard->pPrivate->lLayoutRecOSK);

        GVariantBuilder cLayoutsOSKBuilder;
        g_variant_builder_init (&cLayoutsOSKBuilder, G_VARIANT_TYPE ("as"));
        if (sLayoutOSK)
        {
            g_variant_builder_add (&cLayoutsOSKBuilder, "s", sLayoutOSK);
        }

        for (guint nLayout = 0; nLayout < nEnabledLayoutsOSK; nLayout++)
        {
            gchar *sIdIso = g_slist_nth_data (pKeyboard->pPrivate->lLayoutRecOSK, nLayout);

            nId = 0;
            gchar *sIdLomiri = NULL;
            while (LOMIRI_TO_ISO[nId][0] != NULL)
            {
                gboolean bEqual = g_str_equal (LOMIRI_TO_ISO[nId][1], sIdIso);

                if (bEqual)
                {
                    sIdLomiri = LOMIRI_TO_ISO[nId][0];

                    break;
                }

                nId++;
            }
            if (!sIdLomiri)
            {
                sIdLomiri = sIdIso;
            }

            if (strcmp(sIdLomiri, sLayoutOSK))
            {
                g_variant_builder_add (&cLayoutsOSKBuilder, "s", sIdLomiri);
            }
        }
        GVariant *pEnabledLayoutsOSK = g_variant_builder_end (&cLayoutsOSKBuilder);

        g_settings_set_string (pKeyboard->pPrivate->pMaliitSettings, "active-language", sLayoutOSK);
        g_settings_set_value (pKeyboard->pPrivate->pMaliitSettings, "enabled-languages", pEnabledLayoutsOSK);
    }
    else
    {
        // TODO
    }
}

void keyboard_SetLayout(Keyboard *pKeyboard, gint nLayout, gboolean bOSK)
{
    if (bOSK)
    {
        keyboard_SetLayoutSoftware(pKeyboard, nLayout);
    }
    else
    {
        keyboard_SetLayoutHardware(pKeyboard, nLayout);
    }
}

static void onDispose(GObject *pObject)
{
    Keyboard *self = G_KEYBOARD(pObject);
    g_signal_handlers_disconnect_by_data (self->pPrivate->pSettings, self);
    g_clear_object (&self->pPrivate->pSettings);
    g_signal_handlers_disconnect_by_data (self->pPrivate->pLomiriSettings, self);
    g_clear_object (&self->pPrivate->pLomiriSettings);
    g_signal_handlers_disconnect_by_data (self->pPrivate->pMaliitSettings, self);
    g_clear_object (&self->pPrivate->pMaliitSettings);

    if (self->pPrivate->lLayouts)
    {
        g_hash_table_destroy(self->pPrivate->lLayouts);
    }

    if (self->pPrivate->lLayoutRec)
    {
        g_slist_free_full(self->pPrivate->lLayoutRec, g_free);
    }

    if (self->pPrivate->lLayoutRecOSK)
    {
        g_slist_free_full (self->pPrivate->lLayoutRecOSK, g_free);
    }

    if (self->pPrivate->lUsers)
    {
        g_slist_free(self->pPrivate->lUsers);
    }

    if (self->pPrivate->pChannel)
    {
        g_io_channel_unref (self->pPrivate->pChannel);
    }

    if (self->pPrivate->pMonitor)
    {
        udev_monitor_unref (self->pPrivate->pMonitor);
    }

    if (self->pPrivate->pUdev)
    {
        udev_unref (self->pPrivate->pUdev);
    }

    G_OBJECT_CLASS(keyboard_parent_class)->dispose(pObject);
}

static void keyboard_class_init(KeyboardClass *klass)
{
    GObjectClass *pClass = G_OBJECT_CLASS(klass);
    pClass->dispose = onDispose;
    m_lSignals[LAYOUT_CHANGED] = g_signal_new(KEYBOARD_LAYOUT_CHANGED, G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
    m_lSignals[CONFIG_CHANGED] = g_signal_new(KEYBOARD_CONFIG_CHANGED, G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
}

Keyboard* keyboard_new()
{
    GObject *pObject = g_object_new(G_TYPE_KEYBOARD, NULL);

    return G_KEYBOARD(pObject);
}

static void onSourcesChanged (GSettings *pSettings, const gchar *sKey, gpointer pData)
{
    Keyboard *pKeyboard = G_KEYBOARD (pData);
    gboolean bsignal = FALSE;

    if (pKeyboard->pPrivate->lLayoutRec)
    {
        g_slist_free_full (g_steal_pointer (&pKeyboard->pPrivate->lLayoutRec), g_free);
        bsignal = TRUE;
    }

    GVariant *pList = g_settings_get_value (pSettings, "sources");
    gsize nSources = g_variant_n_children (pList);

    if (nSources)
    {
        GVariantIter cIter;
        g_variant_iter_init (&cIter, pList);
        gchar *sLayout = NULL;

        while (g_variant_iter_loop (&cIter, "(ss)", NULL, &sLayout))
        {
            pKeyboard->pPrivate->lLayoutRec = g_slist_append (pKeyboard->pPrivate->lLayoutRec, g_strdup (sLayout));
        }
    }
    else
    {
        pKeyboard->pPrivate->lLayoutRec = g_slist_append (pKeyboard->pPrivate->lLayoutRec, g_strdup ("us"));
    }

    g_variant_unref (pList);

    if (bsignal)
    {
        g_signal_emit (pKeyboard, m_lSignals[CONFIG_CHANGED], 0);
        g_signal_emit (pKeyboard, m_lSignals[LAYOUT_CHANGED], 0);
    }
}

static void onSoftwareKeyboardEnabled (GSettings *pSettings, const gchar *sKey, gpointer pData)
{
    Keyboard *self = G_KEYBOARD (pData);
    self->pPrivate->bSoftwareKeyboard = g_settings_get_boolean (pSettings, "always-show-osk");
    g_signal_emit (self, m_lSignals[CONFIG_CHANGED], 0);
    g_signal_emit (self, m_lSignals[LAYOUT_CHANGED], 0);
}

static void onSoftwareLayoutChanged (GSettings *pSettings, const gchar *sKey, gpointer pData)
{
    Keyboard *pKeyboard = G_KEYBOARD (pData);
    g_signal_emit (pKeyboard, m_lSignals[LAYOUT_CHANGED], 0);
}

static void onSoftwareLayoutsChanged (GSettings *pSettings, const gchar *sKey, gpointer pData)
{
    Keyboard *pKeyboard = G_KEYBOARD (pData);
    gboolean bsignal = FALSE;

    if (pKeyboard->pPrivate->lLayoutRecOSK)
    {
        g_slist_free_full (g_steal_pointer (&pKeyboard->pPrivate->lLayoutRecOSK), g_free);
        bsignal = TRUE;
    }

    GStrv lLayouts = g_settings_get_strv (pSettings, "enabled-languages");
    guint nLayouts = g_strv_length (lLayouts);

    if (lLayouts)
    {
        for (guint nLayout = 0; nLayout < nLayouts; nLayout++)
        {
            guint nId = 0;
            gchar *sLayout = NULL;

            while (LOMIRI_TO_ISO[nId][0] != NULL)
            {
                gboolean bEqual = g_str_equal (LOMIRI_TO_ISO[nId][0], lLayouts[nLayout]);

                if (bEqual)
                {
                    sLayout = g_strdup (LOMIRI_TO_ISO[nId][1]);

                    break;
                }

                nId++;
            }

            if (!sLayout)
            {
                sLayout = g_strdup (lLayouts[nLayout]);
            }

            pKeyboard->pPrivate->lLayoutRecOSK = g_slist_append (pKeyboard->pPrivate->lLayoutRecOSK, sLayout);
        }

        g_strfreev (lLayouts);
    }

    if (bsignal)
    {
        g_signal_emit (pKeyboard, m_lSignals[CONFIG_CHANGED], 0);
        g_signal_emit (pKeyboard, m_lSignals[LAYOUT_CHANGED], 0);
    }
}

static bool udevDeviceIsHardwareKeyboard (struct udev_device *pDevice)
{
    const gchar *sValue = udev_device_get_property_value (pDevice, "ID_INPUT_KEYBOARD");
    gint nCompared = g_strcmp0 (sValue, "1");

    return nCompared == 0;
}

static gboolean udevHasHardwareKeyboard (struct udev *pUdev)
{
    struct udev_enumerate *pEnumerate = udev_enumerate_new (pUdev);
    udev_enumerate_add_match_subsystem (pEnumerate, "input");
    udev_enumerate_scan_devices (pEnumerate);
    struct udev_list_entry *lEntries = udev_enumerate_get_list_entry (pEnumerate);
    struct udev_list_entry *pEntry;
    gboolean bFound = FALSE;

    udev_list_entry_foreach (pEntry, lEntries)
    {
        const gchar *sPath = udev_list_entry_get_name (pEntry);
        struct udev_device *pDevice = udev_device_new_from_syspath (pUdev, sPath);
        gboolean bKeyboard = udevDeviceIsHardwareKeyboard (pDevice);

        if (bKeyboard)
        {
            bFound = TRUE;
            udev_device_unref (pDevice);

            break;
        }

        udev_device_unref (pDevice);
    }

    udev_enumerate_unref (pEnumerate);

    return bFound;
}

static gboolean onUdevEvent (GIOChannel *pChannel, GIOCondition nCondition, gpointer pData)
{
    Keyboard *self = G_KEYBOARD (pData);
    struct udev_device *pDevice = udev_monitor_receive_device (self->pPrivate->pMonitor);

    if (pDevice)
    {
        gboolean bKeyboard = udevDeviceIsHardwareKeyboard (pDevice);

        if (bKeyboard)
        {
            struct udev *pUdev = udev_device_get_udev (pDevice);
            self->pPrivate->bHardwareKeyboard = udevHasHardwareKeyboard (pUdev);
            g_signal_emit (self, m_lSignals[CONFIG_CHANGED], 0);
            g_signal_emit (self, m_lSignals[LAYOUT_CHANGED], 0);
        }

        udev_device_unref (pDevice);
    }

    return TRUE;
}

gboolean keyboard_hasHardwareKeyboard (Keyboard *self)
{
    return self->pPrivate->bHardwareKeyboard;
}

gboolean keyboard_hasSoftwareKeyboard (Keyboard *self)
{
    return self->pPrivate->bSoftwareKeyboard;
}

static void keyboard_init(Keyboard *self)
{
    self->pPrivate = keyboard_get_instance_private(self);
    self->pPrivate->lLayoutRec = NULL;
    self->pPrivate->lLayoutRecOSK = NULL;
    self->pPrivate->lLayouts = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, freeLayout);

    // Read all available layouts
    struct rxkb_context *pContext = rxkb_context_new(RXKB_CONTEXT_LOAD_EXOTIC_RULES);
    g_assert(pContext);

    if (!rxkb_context_include_path_append_default(pContext))
    {
        g_warning("Failed to include default paths");
    }

    if (!rxkb_context_parse(pContext, "evdev"))
    {
        g_warning("Failed to parse XKB descriptions");
    }

    struct rxkb_layout *pRxkbLayout = rxkb_layout_first(pContext);

    while (pRxkbLayout != NULL)
    {
        const gchar *sLayout = rxkb_layout_get_name(pRxkbLayout);
        const gchar *sVariant = rxkb_layout_get_variant(pRxkbLayout);
        const gchar *sDescription = rxkb_layout_get_description(pRxkbLayout);
        const gchar *sTranslated = g_dgettext("xkeyboard-config", sDescription);
        sDescription = sTranslated;
        Layout *pLayout = g_slice_new0(Layout);

        if (sVariant != NULL && strlen(sVariant) > 0)
        {
            pLayout->sId = g_strjoin("+", sLayout, sVariant, NULL);
            pLayout->sLanguage = g_strdup(lookupLanguage(pLayout->sId));
            pLayout->sDescription = g_strdup(sDescription);
        }
        else
        {
            pLayout->sId = g_strdup(sLayout);
            pLayout->sLanguage = g_strdup(lookupLanguage(pLayout->sId));
            pLayout->sDescription = g_strdup(sDescription);
        }

        g_hash_table_replace(self->pPrivate->lLayouts, pLayout->sId, pLayout);

        pRxkbLayout = rxkb_layout_next(pRxkbLayout);
    }

    rxkb_context_unref(pContext);

    self->pPrivate->nLayoutOSK = 0;

    // Lomiri-specific layouts
    const gchar *LAYOUTS[][3] =
    {
        {"emoji", "emoji", "Emoji"},
        {"Bn", "bn-avro", "Bangla (Avro)"},
        {"Zn", "chewing", "Chinese (Chewing)"},
        {"Zn", "pinyin", "Chinese (Pinyin)"}
    };

    for (guint nLayout = 0; nLayout < 3; nLayout++)
    {
        Layout *pLayout = g_slice_new0 (Layout);
        pLayout->sId = g_strdup (LAYOUTS[nLayout][1]);
        pLayout->sLanguage = g_strdup (LAYOUTS[nLayout][0]);
        pLayout->sDescription = g_strdup (LAYOUTS[nLayout][2]);
        g_hash_table_replace (self->pPrivate->lLayouts, pLayout->sId, pLayout);
    }
    //~Lomiri-specific layouts

    if (isGreeter() == FALSE)
    {
        self->pPrivate->nLayout = 0;
        GSettingsSchemaSource *pSource = g_settings_schema_source_get_default ();
        GSettingsSchema *pSchema = NULL;

        if (pSource)
        {
            pSchema = g_settings_schema_source_lookup (pSource, "org.gnome.desktop.input-sources", FALSE);

            if (pSchema)
            {
                g_settings_schema_unref (pSchema);
                self->pPrivate->pSettings = g_settings_new ("org.gnome.desktop.input-sources");
                g_signal_connect (self->pPrivate->pSettings, "changed::sources", G_CALLBACK (onSourcesChanged), self);
                onSourcesChanged (self->pPrivate->pSettings, "sources", self);
            }
            else
            {
                g_error ("Panic: no org.gnome.desktop.input-sources schema found");
            }
        }
    }
    else
    {
        // Get system layouts
        getSystemLayouts ("/etc/default/keyboard", &self->pPrivate->lLayoutRec, NULL, FALSE);

        if (!self->pPrivate->lLayoutRec)
        {
            getSystemLayouts ("/etc/X11/xorg.conf.d/00-keyboard.conf", &self->pPrivate->lLayoutRec, NULL, FALSE);
        }

        if (!self->pPrivate->lLayoutRec)
        {
            gchar *sId = g_strdup ("us");
            self->pPrivate->lLayoutRec = g_slist_append (self->pPrivate->lLayoutRec, sId);
        }

        self->pPrivate->nLayout = 0;
        //~ Get system layouts

        ActUserManager *pManager = act_user_manager_get_default();
        gboolean bIsLoaded;
        g_object_get(pManager, "is-loaded", &bIsLoaded, NULL);

        if (bIsLoaded)
        {
            self->pPrivate->lUsers = act_user_manager_list_users(pManager);

            for (GSList *lUser = self->pPrivate->lUsers; lUser; lUser = lUser->next)
            {
                ActUser *pUser = lUser->data;
                gboolean bIsUserLoaded = act_user_is_loaded(pUser);

                if (bIsUserLoaded)
                {
                    getAccountsService(self);
                }
                else
                {
                    g_signal_connect_swapped(pUser, "notify::is-loaded", G_CALLBACK(onUserLoaded), self);
                }
            }
        }
        else
        {
            g_signal_connect_object(pManager, "notify::is-loaded", G_CALLBACK(onManagerLoaded), self, G_CONNECT_SWAPPED);
        }
    }

    // Watch for a hardware keyboard
    self->pPrivate->pUdev = udev_new ();
    self->pPrivate->pMonitor = udev_monitor_new_from_netlink (self->pPrivate->pUdev, "udev");
    udev_monitor_filter_add_match_subsystem_devtype (self->pPrivate->pMonitor, "input", NULL);
    udev_monitor_enable_receiving (self->pPrivate->pMonitor);
    gint nFd = udev_monitor_get_fd (self->pPrivate->pMonitor);
    self->pPrivate->bHardwareKeyboard = udevHasHardwareKeyboard (self->pPrivate->pUdev);
    self->pPrivate->pChannel = g_io_channel_unix_new (nFd);
    g_io_add_watch (self->pPrivate->pChannel, G_IO_IN, onUdevEvent, self);
    //~Watch for a hardware keyboard

    // Watch software keyboard
    GSettingsSchemaSource *pSource = g_settings_schema_source_get_default ();
    GSettingsSchema *pSchema = NULL;

    if (pSource)
    {
        pSchema = g_settings_schema_source_lookup (pSource, "com.lomiri.Shell", FALSE);

        if (pSchema)
        {
            g_settings_schema_unref (pSchema);
            self->pPrivate->pLomiriSettings = g_settings_new ("com.lomiri.Shell");
            g_signal_connect (self->pPrivate->pLomiriSettings, "changed::always-show-osk", G_CALLBACK (onSoftwareKeyboardEnabled), self);
            onSoftwareKeyboardEnabled (self->pPrivate->pLomiriSettings, "always-show-osk", self);
        }
        else
        {
            g_error ("Panic: no com.lomiri.Shell schema found");
        }

        pSchema = g_settings_schema_source_lookup (pSource, "com.lomiri.keyboard.maliit", FALSE);

        if (pSchema)
        {
            g_settings_schema_unref (pSchema);
            self->pPrivate->pMaliitSettings = g_settings_new ("com.lomiri.keyboard.maliit");
            g_signal_connect (self->pPrivate->pMaliitSettings, "changed::enabled-languages", G_CALLBACK (onSoftwareLayoutsChanged), self);
            onSoftwareLayoutsChanged (self->pPrivate->pMaliitSettings, "enabled-languages", self);
            g_signal_connect (self->pPrivate->pMaliitSettings, "changed::active-language", G_CALLBACK (onSoftwareLayoutChanged), self);
            onSoftwareLayoutChanged (self->pPrivate->pMaliitSettings, "active-language", self);
        }
        else
        {
            g_error ("Panic: no com.lomiri.keyboard.maliit schema found");
        }
    }
    //~Watch software keyboard
}
