/* -*- Mode: C++; indent-tabs-mode: nil; tab-width: 4 -*-
 * -*- coding: utf-8 -*-
 *
 * Copyright (C) 2023 KylinSoft Co., Ltd.
 *
 * 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 3 of the License, or
 * 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, see <http://www.gnu.org/licenses/>.
 */
#include <QIcon>
#include <QMessageBox>
#include "keyboard-xkb.h"
#include "clib-syslog.h"

#define MATEKBD_DESKTOP_SCHEMA  "org.mate.peripherals-keyboard-xkb.general"
#define MATEKBD_KBD_SCHEMA      "org.mate.peripherals-keyboard-xkb.kbd"
#define KNOWN_FILES_KEY         "known-file-list"
#define DISABLE_INDICATOR_KEY   "disable-indicator"

KeyboardManager  *KeyboardXkb::manager  = KeyboardManager::KeyboardManagerNew();
static void      *pa_callback_user_data = NULL;

static XklConfigRegistry*  xkl_registry = NULL;
static MatekbdDesktopConfig  current_desktop_config;
static MatekbdKeyboardConfig current_kbd_config;

/* never terminated */
static MatekbdKeyboardConfig  initial_sys_kbd_config;
static PostActivationCallback pa_callback  = NULL;
static XklEngine*             xkl_engine;
static bool                   inited_ok = false;

KeyboardXkb::KeyboardXkb()
{
    USD_LOG(LOG_DEBUG,"Keyboard Xkb initializing!");
}

KeyboardXkb::~KeyboardXkb()
{
    USD_LOG(LOG_DEBUG,"Keyboard Xkb free");
    if(settings_desktop) {
        delete settings_desktop;
        settings_desktop = nullptr;
    }
    if(settings_kbd) {
        delete settings_kbd;
        settings_kbd = nullptr;
    }
}

void KeyboardXkb::usd_keyboard_xkb_analyze_sysconfig (void)
{
    if (!inited_ok)
        return;
    matekbd_keyboard_config_init (&initial_sys_kbd_config, xkl_engine);
    matekbd_keyboard_config_load_from_x_initial (&initial_sys_kbd_config,NULL);
}

void KeyboardXkb::apply_desktop_settings (void)
{
    if (!inited_ok)
        return;

    manager->usd_keyboard_manager_apply_settings (manager);
    matekbd_desktop_config_load_from_gsettings (&current_desktop_config);

    /* again, probably it would be nice to compare things
       before activating them */
    matekbd_desktop_config_activate (&current_desktop_config);
}

bool KeyboardXkb::try_activating_xkb_config_if_new (MatekbdKeyboardConfig *current_sys_kbd_config)
{
    /* Activate - only if different! */
    if (!matekbd_keyboard_config_equals
        (&current_kbd_config, current_sys_kbd_config)) {
        if (matekbd_keyboard_config_activate (&current_kbd_config)) {
            if (pa_callback != NULL) {
                (*pa_callback) (pa_callback_user_data);
                return TRUE;
            }
        } else {
            return FALSE;
        }
    }
    return TRUE;
}

static void g_strv_behead (gchar **arr)
{
    if (arr == NULL || *arr == NULL)
        return;

    g_free (*arr);
    memmove (arr, arr + 1, g_strv_length (arr) * sizeof (gchar *));
}


bool KeyboardXkb::filter_xkb_config (void)
{
    XklConfigItem *item;
    char *lname;
    char *vname;
    char **lv;
    bool any_change = FALSE;

    xkl_debug (100, "Filtering configuration against the registry\n");

    if (!xkl_registry) {
        xkl_registry = xkl_config_registry_get_instance (xkl_engine);

        /* load all materials, unconditionally! */
        if (!xkl_config_registry_load (xkl_registry, TRUE)){
            g_object_unref (xkl_registry);
            xkl_registry = NULL;
            return FALSE;
        }
    }
    lv = current_kbd_config.layouts_variants;
    item = xkl_config_item_new ();
    while (*lv) {
        xkl_debug (100, "Checking [%s]\n", *lv);

        if (matekbd_keyboard_config_split_items (*lv, &lname, &vname)) {

            bool should_be_dropped = FALSE;
            snprintf (item->name ,sizeof (item->name), "%s",lname);
            if (!xkl_config_registry_find_layout(xkl_registry, item)) {

                xkl_debug (100, "Bad layout [%s]\n",lname);
                should_be_dropped = TRUE;
            } else if (vname) {
                snprintf (item->name,sizeof (item->name), "%s", vname);
                if (!xkl_config_registry_find_variant
                    (xkl_registry, lname, item)) {
                    xkl_debug (100,"Bad variant [%s(%s)]\n",lname, vname);
                    should_be_dropped = TRUE;
                }
            }
            if (should_be_dropped) {
                g_strv_behead (lv);
                any_change = TRUE;
                continue;
            }
        }
        lv++;
    }
    g_object_unref (item);
    return any_change;
}

static void activation_error (void)
{
    Display *dpy = UsdBaseClass::getQx11Info();
    char const *vendor = ServerVendor (dpy);//GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()));
    int release = VendorRelease (dpy);//GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()));
    QMessageBox *dialog;

    /* VNC viewers will not work, do not barrage them with warnings */
    if (NULL != vendor && NULL != strstr (vendor, "VNC"))
        return;
    QString message = QObject::tr("Error activating XKB configuration.\n"
                      "It can happen under various circumstances:\n"
                      " • a bug in libxklavier library\n"
                      " • a bug in X server (xkbcomp, xmodmap utilities)\n"
                      " • X server with incompatible libxkbfile implementation\n\n"
                      "X server version data:\n %1 \n %2 \n"
                      "If you report this situation as a bug, please include:\n"
                      " • The result of <b> xprop -root | grep XKB </b>\n"
                      " • The result of <b> gsettings list-keys org.mate.peripherals-keyboard-xkb.kbd </b>").arg(vendor).arg(release);
    dialog = new QMessageBox();
    dialog->setButtonText(QMessageBox::Close,QObject::tr("Close"));
    dialog->setButtonText(QMessageBox::Warning,QObject::tr("Error"));
    dialog->setText(message);
    dialog->show();
    dialog->close();
    delete dialog;
    dialog = nullptr;

}


void KeyboardXkb::apply_xkb_settings (void)
{
    MatekbdKeyboardConfig current_sys_kbd_config;

    if (!inited_ok)
        return;

    matekbd_keyboard_config_init (&current_sys_kbd_config, xkl_engine);

    matekbd_keyboard_config_load_from_gsettings (&current_kbd_config,
                          &initial_sys_kbd_config);

    matekbd_keyboard_config_load_from_x_current (&current_sys_kbd_config,
                          NULL);

    if (!try_activating_xkb_config_if_new (&current_sys_kbd_config)) {
        if (filter_xkb_config ()) {
            if (!try_activating_xkb_config_if_new(&current_sys_kbd_config)) {
                qWarning ("Could not activate the filtered XKB configuration");
                activation_error (); 
            }
        } else {
            qWarning("Could not activate the XKB configuration");
            activation_error ();
        }
    } else
        xkl_debug (100, "Actual KBD configuration was not changed: redundant notification\n");

    matekbd_keyboard_config_term (&current_sys_kbd_config);
}

void KeyboardXkb::apply_desktop_settings_cb (QString key)
{
    apply_desktop_settings ();
}
void KeyboardXkb::apply_desktop_settings_mate_cb(GSettings *settings,char *key)
{
    apply_desktop_settings ();
}

void KeyboardXkb::apply_xkb_settings_cb (QString key)
{
    apply_xkb_settings ();
}
void KeyboardXkb::apply_xkb_settings_mate_cb (GSettings *settings,char *key)
{
    apply_xkb_settings ();
}

GdkFilterReturn usd_keyboard_xkb_evt_filter (GdkXEvent * xev, GdkEvent * event,gpointer data)
{
    KeyboardXkb *xkb = (KeyboardXkb *)data;
    XEvent *xevent = (XEvent *) xev;

    xkl_engine_filter_events (xkl_engine, xevent);
    return GDK_FILTER_CONTINUE;
}

/* When new Keyboard is plugged in - reload the settings */
void KeyboardXkb::usd_keyboard_new_device (XklEngine * engine)
{
    apply_desktop_settings ();
    apply_xkb_settings ();
}


void KeyboardXkb::usd_keyboard_xkb_init(KeyboardManager* kbd_manager)
{
    USD_LOG(LOG_DEBUG,"init --- XKB");
    Display *display;

    display    = UsdBaseClass::getQx11Info();
    manager    = kbd_manager;
    xkl_engine = xkl_engine_get_instance (display);

    if (xkl_engine) {
        inited_ok = TRUE;
        settings_desktop = new QGSettings(MATEKBD_DESKTOP_SCHEMA);
        settings_kbd     = new QGSettings(MATEKBD_KBD_SCHEMA);

        matekbd_desktop_config_init (&current_desktop_config,xkl_engine);
        matekbd_keyboard_config_init (&current_kbd_config,xkl_engine);
        xkl_engine_backup_names_prop (xkl_engine);

        usd_keyboard_xkb_analyze_sysconfig ();

        matekbd_desktop_config_start_listen (&current_desktop_config, G_CALLBACK (apply_desktop_settings_mate_cb),NULL);

        matekbd_keyboard_config_start_listen (&current_kbd_config,G_CALLBACK (apply_xkb_settings_mate_cb),NULL);

        QObject::connect(settings_desktop,SIGNAL(changed(QString)),this,SLOT(apply_desktop_settings_cb(QString)));

        QObject::connect(settings_kbd,SIGNAL(changed(QString)),this,SLOT(apply_xkb_settings_cb(QString)));

//        gdk_window_add_filter (NULL, (GdkFilterFunc)usd_keyboard_xkb_evt_filter, this);

        if (xkl_engine_get_features (xkl_engine) &XKLF_DEVICE_DISCOVERY)
            g_signal_connect (xkl_engine, "X-new-device",
                              G_CALLBACK(usd_keyboard_new_device), NULL);

        xkl_engine_start_listen (xkl_engine, XKLL_MANAGE_LAYOUTS |XKLL_MANAGE_WINDOW_STATES);

        apply_desktop_settings ();
        apply_xkb_settings ();
    }
}

void KeyboardXkb::usd_keyboard_xkb_shutdown (void)
{
    pa_callback = NULL;
    pa_callback_user_data = NULL;
    manager = NULL;

    if (!inited_ok)
        return;

    xkl_engine_stop_listen (xkl_engine,
                            XKLL_MANAGE_LAYOUTS |
                            XKLL_MANAGE_WINDOW_STATES);

    gdk_window_remove_filter (NULL, (GdkFilterFunc)usd_keyboard_xkb_evt_filter, NULL);

    if (xkl_registry) {
        g_object_unref (xkl_registry);
    }
    g_object_unref (xkl_engine);

    xkl_engine = NULL;
    inited_ok = FALSE;
}
