#include <gtk/gtk.h>
#include <gdk/gdkx.h>
#include "ibus.h"
#include <stdlib.h>
#include <X11/Xlib.h>
#include <X11/extensions/XTest.h>

#define GREEN "\033[0;32m"
#define RED   "\033[0;31m"
#define NC    "\033[0m"

typedef struct _KeyData {
    guint keyval;
    guint modifiers;
} KeyData;

static const KeyData test_cases[][30] = {
   { { IBUS_KEY_a, 0 }, { IBUS_KEY_comma, IBUS_SHIFT_MASK },
     { IBUS_KEY_b, 0 }, { IBUS_KEY_period, IBUS_SHIFT_MASK },
     { IBUS_KEY_c, 0 }, { IBUS_KEY_slash, IBUS_SHIFT_MASK },
     { IBUS_KEY_d, 0 }, { IBUS_KEY_semicolon, IBUS_SHIFT_MASK },
     { IBUS_KEY_e, 0 }, { IBUS_KEY_apostrophe, IBUS_SHIFT_MASK },
     { IBUS_KEY_f, 0 }, { IBUS_KEY_bracketleft, IBUS_SHIFT_MASK },
     { IBUS_KEY_g, 0 }, { IBUS_KEY_backslash, IBUS_SHIFT_MASK },
     { 0, 0 } },
   { { IBUS_KEY_grave, IBUS_SHIFT_MASK }, { IBUS_KEY_a, IBUS_SHIFT_MASK },
     { IBUS_KEY_1, IBUS_SHIFT_MASK }, { IBUS_KEY_b, IBUS_SHIFT_MASK  },
     { IBUS_KEY_2, IBUS_SHIFT_MASK }, { IBUS_KEY_c, IBUS_SHIFT_MASK  },
     { IBUS_KEY_3, IBUS_SHIFT_MASK }, { IBUS_KEY_d, IBUS_SHIFT_MASK },
     { IBUS_KEY_9, IBUS_SHIFT_MASK }, { IBUS_KEY_e, IBUS_SHIFT_MASK },
     { IBUS_KEY_0, IBUS_SHIFT_MASK }, { IBUS_KEY_f, IBUS_SHIFT_MASK },
     { IBUS_KEY_equal, IBUS_SHIFT_MASK }, { IBUS_KEY_g, IBUS_SHIFT_MASK },
     { 0, 0 } },
   { { 0, 0 } }
};

KeyData test_end_key = { IBUS_KEY_z, IBUS_SHIFT_MASK };

static const gunichar test_results[][60] = {
   { 'a', '<', 'b', '>', 'c', '?', 'd', ':', 'e', '"', 'f', '{', 'g', '|', 0 },
   { '~', 'A', '!', 'B', '@', 'C', '#', 'D', '(', 'E', ')', 'F', '+', 'G', 0 },
   { 0 }
};


IBusBus *m_bus;
IBusEngine *m_engine;

static gboolean window_focus_in_event_cb (GtkWidget     *entry,
                                          GdkEventFocus *event,
                                          gpointer       data);

static IBusEngine *
create_engine_cb (IBusFactory *factory, const gchar *name, gpointer data)
{
    static int i = 1;
    gchar *engine_path =
            g_strdup_printf ("/org/freedesktop/IBus/engine/simpletest/%d",
                             i++);

    m_engine = ibus_engine_new_with_type (IBUS_TYPE_ENGINE_SIMPLE,
                                          name,
                                          engine_path,
                                          ibus_bus_get_connection (m_bus));
    g_free (engine_path);
    return m_engine;
}

static gboolean
register_ibus_engine ()
{
    IBusFactory *factory;
    IBusComponent *component;
    IBusEngineDesc *desc;

    m_bus = ibus_bus_new ();
    if (!ibus_bus_is_connected (m_bus)) {
        g_critical ("ibus-daemon is not running.");
        return FALSE;
    }
    factory = ibus_factory_new (ibus_bus_get_connection (m_bus));
    g_signal_connect (factory, "create-engine",
                      G_CALLBACK (create_engine_cb), NULL);

    component = ibus_component_new (
            "org.freedesktop.IBus.SimpleTest",
            "Simple Engine Test",
            "0.0.1",
            "GPL",
            "Takao Fujiwara <takao.fujiwara1@gmail.com>",
            "https://github.com/ibus/ibus/wiki",
            "",
            "ibus");
    desc = ibus_engine_desc_new (
            "xkbtest:us::eng",
            "XKB Test",
            "XKB Test",
            "en",
            "GPL",
            "Takao Fujiwara <takao.fujiwara1@gmail.com>",
            "ibus-engine",
            "us");
    ibus_component_add_engine (component, desc);
    ibus_bus_register_component (m_bus, component);

    return TRUE;
}

static gboolean
finit (gpointer data)
{
    g_critical ("time out");
    gtk_main_quit ();
    return FALSE;
}

static void
send_key_event (Display *xdisplay,
                guint    keyval,
                guint    modifiers)
{
    static struct {
        guint   state;
        KeySym  keysym;
    } state2keysym[] = {
        { IBUS_CONTROL_MASK, XK_Control_L } ,
        { IBUS_MOD1_MASK,    XK_Alt_L },
        { IBUS_MOD4_MASK,    XK_Super_L },
        { IBUS_SHIFT_MASK,   XK_Shift_L },
        { IBUS_LOCK_MASK,    XK_Caps_Lock },
        { 0,           0L }
    };
    int i;
    guint keycode;
    guint state = modifiers;

    while (state) {
        for (i = 0; state2keysym[i].state; i++) {
            if ((state2keysym[i].state & state) != 0) {
                keycode = XKeysymToKeycode (xdisplay, state2keysym[i].keysym);
                XTestFakeKeyEvent (xdisplay, keycode, True, CurrentTime);
                XSync (xdisplay, False);
                state ^= state2keysym[i].state;
                break;
            }
        }
    }
    keycode = XKeysymToKeycode (xdisplay, keyval);
    XTestFakeKeyEvent (xdisplay, keycode, True, CurrentTime);
    XSync (xdisplay, False);
    XTestFakeKeyEvent (xdisplay, keycode, False, CurrentTime);
    XSync (xdisplay, False);

    state = modifiers;
    while (state) {
        for (i = G_N_ELEMENTS (state2keysym) - 1; i >= 0; i--) {
            if ((state2keysym[i].state & state) != 0) {
                keycode = XKeysymToKeycode (xdisplay, state2keysym[i].keysym);
                XTestFakeKeyEvent (xdisplay, keycode, False, CurrentTime);
                XSync (xdisplay, False);
                state ^= state2keysym[i].state;
                break;
            }
        }
    }
}

static void
set_engine_cb (GObject      *object,
               GAsyncResult *res,
               gpointer      data)
{
    IBusBus *bus = IBUS_BUS (object);
    GtkWidget *entry = GTK_WIDGET (data);
    GdkDisplay *display;
    Display *xdisplay = NULL;
    GError *error = NULL;
    int i, j;

    g_assert (GTK_IS_ENTRY (entry));

    if (!ibus_bus_set_global_engine_async_finish (bus, res, &error)) {
        g_critical ("set engine failed: %s", error->message);
        g_error_free (error);
        return;
    }

    display = gtk_widget_get_display (entry);
    if (GDK_IS_X11_DISPLAY (display)) {
        xdisplay = gdk_x11_display_get_xdisplay (display);
    } else {
#if 0
        xdisplay = XOpenDisplay (NULL);
#else
        g_critical ("No idea to simulate key events in Wayland\n");
#endif
    }
    g_return_if_fail (xdisplay);

    for (i = 0; test_cases[i][0].keyval; i++) {
        for (j = 0; test_cases[i][j].keyval; j++) {
            send_key_event (xdisplay,
                            test_cases[i][j].keyval,
                            test_cases[i][j].modifiers);
        }
        send_key_event (xdisplay, test_end_key.keyval, test_end_key.modifiers);
    }

    g_timeout_add_seconds (10, finit, NULL);
}

static gboolean
window_focus_in_event_cb (GtkWidget *entry, GdkEventFocus *event, gpointer data)
{
    g_assert (m_bus != NULL);
    ibus_bus_set_global_engine_async (m_bus,
                                      "xkbtest:us::eng",
                                      -1,
                                      NULL,
                                      set_engine_cb,
                                      entry);
    return FALSE;
}

static void
window_inserted_text_cb (GtkEntryBuffer *buffer,
                         guint           position,
                         const gchar    *chars,
                         guint           nchars,
                         gpointer        data)
{
    GtkWidget *entry = data;
    static int i = 0;
    static int j = 0;

    if (g_utf8_get_char (chars) == 'Z') {
        int k;
        g_print ("\n" GREEN "PASS" NC ": ");
        for (k = 0; k < j; k++)
            g_print ("%lc(%X) ", test_results[i][k], test_results[i][k]);
        g_print ("\n");
        i++;
        j = 0;
        if (test_results[i][0] == 0)
            gtk_main_quit ();
        else
            gtk_entry_set_text (GTK_ENTRY (entry), "");
        return;
    }
    g_assert (g_utf8_get_char (chars) == test_results[i][j]);
    j++;
}

static void
create_window ()
{
    GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    GtkWidget *entry = gtk_entry_new ();
    GtkEntryBuffer *buffer;

    g_signal_connect (window, "destroy",
                      G_CALLBACK (gtk_main_quit), NULL);
    g_signal_connect (entry, "focus-in-event",
                      G_CALLBACK (window_focus_in_event_cb), NULL);
    buffer = gtk_entry_get_buffer (GTK_ENTRY (entry));
    g_signal_connect (buffer, "inserted-text",
                      G_CALLBACK (window_inserted_text_cb), entry);
    gtk_container_add (GTK_CONTAINER (window), entry);
    gtk_widget_show_all (window);
}

static void
test_keypress (void)
{
    int status = 0;
    GError *error = NULL;

    g_spawn_command_line_sync ("setxkbmap -layout us",
                               NULL, NULL,
                               &status, &error);
    g_assert (register_ibus_engine ());

    create_window ();
    gtk_main ();
}

int
main (int argc, char *argv[])
{
    ibus_init ();
    /* Avoid a warning of "AT-SPI: Could not obtain desktop path or name"
     * with gtk_main().
     */
    g_setenv ("NO_AT_BRIDGE", "1", TRUE);
    g_test_init (&argc, &argv, NULL);
    gtk_init (&argc, &argv);

    g_test_add_func ("/ibus/keyrepss", test_keypress);


    return g_test_run ();
}
