/* 
 * Copyright (C) 2006 Sergey V. Udaltsov <svu@gnome.org>
 * Copyright (C) 2010-2011 Daiki Ueno <ueno@unixuser.org>
 * Copyright (C) 2010-2011 Red Hat, Inc.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 * 
 * This library 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 Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA
 */

/**
 * SECTION:eek-xkb-layout
 * @short_description: Layout engine using XKB configuration
 *
 * The #EekXkbLayout inherits #EekLayout class and arranges keyboard
 * elements using XKB.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif  /* HAVE_CONFIG_H */

#include <X11/keysym.h>
#include <X11/XKBlib.h>
#include <X11/extensions/XKBgeom.h>
#include <string.h>
#include <stdarg.h>
#include <gio/gio.h>

#include "eek-xkb-layout.h"
#include "eek-keyboard.h"
#include "eek-section.h"
#include "eek-key.h"
#include "eek-keysym.h"

#define XKB_COMPONENT_MASK (XkbGBN_GeometryMask |       \
                            XkbGBN_KeyNamesMask |       \
                            XkbGBN_OtherNamesMask |     \
                            XkbGBN_SymbolsMask |        \
                            XkbGBN_IndicatorMapMask)

static void initable_iface_init (GInitableIface *initable_iface);

G_DEFINE_TYPE_WITH_CODE (EekXkbLayout, eek_xkb_layout, EEK_TYPE_LAYOUT,
                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE,
                                                initable_iface_init));

#define EEK_XKB_LAYOUT_GET_PRIVATE(obj)                                  \
    (G_TYPE_INSTANCE_GET_PRIVATE ((obj), EEK_TYPE_XKB_LAYOUT, EekXkbLayoutPrivate))

enum {
    PROP_0,
    PROP_DISPLAY,
    PROP_LAST
};

struct _EekXkbLayoutPrivate
{
    /* Configuration names that should synch'ed to the symbolic names
       in priv->xkb->names.  Since we use GLib's memory allocator,
       don't store any address returned from the X server here. */
    XkbComponentNamesRec names;

    Display *display;

    /* Actual XKB configuration of DISPLAY. */
    XkbDescRec *xkb;

    /* Hash table to cache orefs by shape address. */
    GHashTable *shape_oref_hash;

    gint scale_numerator;
    gint scale_denominator;
};

static guint    find_keycode             (EekXkbLayout *layout,
                                          gchar        *key_name);

static gboolean get_keyboard_from_server (EekXkbLayout *layout,
                                          GError      **error);

static gboolean get_names_from_server    (EekXkbLayout *layout,
                                          GError      **error);

static void     setup_scaling            (EekXkbLayout *layout,
                                          gdouble       width,
                                          gdouble       height);

G_INLINE_FUNC gint
xkb_to_pixmap_coord (EekXkbLayout *layout,
                     gint          n)
{
    EekXkbLayoutPrivate *priv = layout->priv;
    return n * priv->scale_numerator / priv->scale_denominator;
}

G_INLINE_FUNC gdouble
xkb_to_pixmap_double (EekXkbLayout *layout,
                     gdouble       d)
{
    EekXkbLayoutPrivate *priv = layout->priv;
    return d * priv->scale_numerator / priv->scale_denominator;
}

static void
create_key (EekXkbLayout *layout,
            EekKeyboard  *keyboard,
            EekSection   *section,
            gint          column,
            gint          row,
            gdouble       x,
            gdouble       y,
            XkbKeyRec    *xkbkey)
{
    XkbGeometryRec *xkbgeometry;
    XkbBoundsRec *xkbbounds;
    XkbShapeRec *xkbshape;
    XkbOutlineRec *xkboutline;
    EekXkbLayoutPrivate *priv = layout->priv;
    EekKey *key;
    EekBounds bounds;
    EekSymbolMatrix *matrix = NULL;
    gchar name[XkbKeyNameLength + 1];
    KeyCode keycode;
    gint num_groups, num_levels;
    guint oref;
    gpointer v;

    xkbgeometry = priv->xkb->geom;
    xkbshape = &xkbgeometry->shapes[xkbkey->shape_ndx];
    if (g_hash_table_lookup_extended (priv->shape_oref_hash, xkbshape,
                                      NULL, &v)) {
        oref = GPOINTER_TO_UINT(v);
    } else {
        EekOutline *outline;

        xkboutline = xkbshape->primary == NULL ? &xkbshape->outlines[0] :
            xkbshape->primary;

        outline = g_slice_new (EekOutline);
        outline->corner_radius = xkb_to_pixmap_coord(layout,
                                                     xkboutline->corner_radius);

        if (xkboutline->num_points <= 2) { /* rectangular */
            gdouble x1, y1, x2, y2;

            outline->num_points = 4;
            outline->points = g_slice_alloc0 (sizeof (EekPoint) *
                                              outline->num_points);
            if (xkboutline->num_points == 1) {
                x1 = xkb_to_pixmap_coord(layout, xkbshape->bounds.x1);
                y1 = xkb_to_pixmap_coord(layout, xkbshape->bounds.y1);
                x2 = xkb_to_pixmap_coord(layout, xkboutline->points[0].x);
                y2 = xkb_to_pixmap_coord(layout, xkboutline->points[0].y);
            } else {
                x1 = xkb_to_pixmap_coord(layout, xkboutline->points[0].x);
                y1 = xkb_to_pixmap_coord(layout, xkboutline->points[0].y);
                x2 = xkb_to_pixmap_coord(layout, xkboutline->points[1].x);
                y2 = xkb_to_pixmap_coord(layout, xkboutline->points[1].y);
            }
            outline->points[0].x = outline->points[3].x = x1;
            outline->points[0].y = outline->points[1].y = y1;
            outline->points[1].x = outline->points[2].x = x2;
            outline->points[2].y = outline->points[3].y = y2;
        } else {                /* polygon */
            gint i;

            outline->num_points = xkboutline->num_points;
            outline->points = g_new0 (EekPoint, outline->num_points);
            for (i = 0; i < xkboutline->num_points; i++) {
                outline->points[i].x =
                    xkb_to_pixmap_coord(layout, xkboutline->points[i].x);
                outline->points[i].y =
                    xkb_to_pixmap_coord(layout, xkboutline->points[i].y);
            }
        }
        oref = eek_keyboard_add_outline (keyboard, outline);
        eek_outline_free (outline);
        g_hash_table_insert (priv->shape_oref_hash, xkbshape,
                             GUINT_TO_POINTER(oref));
    }

    memset (name, 0, sizeof name);
    memcpy (name, xkbkey->name.name, sizeof name - 1);

    xkbbounds = &xkbgeometry->shapes[xkbkey->shape_ndx].bounds;
    bounds.x = xkb_to_pixmap_coord(layout, xkbbounds->x1 + x);
    bounds.y = xkb_to_pixmap_coord(layout, xkbbounds->y1 + y);
    bounds.width = xkb_to_pixmap_coord(layout, xkbbounds->x2 - xkbbounds->x1);
    bounds.height = xkb_to_pixmap_coord(layout, xkbbounds->y2 - xkbbounds->y1);

    keycode = find_keycode (layout, name);
    if (keycode == EEK_INVALID_KEYCODE) {
        num_groups = num_levels = 0;
        matrix = eek_symbol_matrix_new (0, 0);
    } else {
        KeySym keysym;
        gint i, j;

        num_groups = XkbKeyNumGroups (priv->xkb, keycode);
        num_levels = XkbKeyGroupsWidth (priv->xkb, keycode);
        matrix = eek_symbol_matrix_new (num_groups, num_levels);
        for (i = 0; i < num_groups; i++)
            for (j = 0; j < num_levels; j++) {
                EekModifierType modifier;

                keysym = XkbKeySymEntry (priv->xkb, keycode, j, i);
                modifier = XkbKeysymToModifiers (priv->display, keysym);
                matrix->data[i * num_levels + j] =
                    EEK_SYMBOL(eek_keysym_new_with_modifier (keysym,
                                                             modifier));
            }
    }

    key = eek_section_create_key (section, keycode, column, row);
    eek_element_set_name (EEK_ELEMENT(key), name);
    eek_element_set_bounds (EEK_ELEMENT(key), &bounds);
    eek_key_set_symbol_matrix (key, matrix);
    eek_symbol_matrix_free (matrix);
    eek_key_set_oref (key, oref);
}

static void
create_section (EekXkbLayout  *layout,
                EekKeyboard   *keyboard,
                XkbSectionRec *xkbsection)
{
    XkbGeometryRec *xkbgeometry;
    EekXkbLayoutPrivate *priv;
    EekSection *section;
    EekBounds bounds;
    gchar *name;
    gfloat left, top;
    gint i, j;

    bounds.x = xkb_to_pixmap_coord(layout, xkbsection->left);
    bounds.y = xkb_to_pixmap_coord(layout, xkbsection->top);
    bounds.width = xkb_to_pixmap_coord(layout, xkbsection->width);
    bounds.height = xkb_to_pixmap_coord(layout, xkbsection->height);

    priv = layout->priv;
    xkbgeometry = priv->xkb->geom;
    section = eek_keyboard_create_section (keyboard);
    name = XGetAtomName (priv->display, xkbsection->name);
    eek_element_set_name (EEK_ELEMENT(section), name);
    XFree (name);
    eek_element_set_bounds (EEK_ELEMENT(section), &bounds);
    eek_section_set_angle (section,
                           /* angle is in tenth of degree */
                           xkbsection->angle / 10);

    for (i = 0; i < xkbsection->num_rows; i++) {
        XkbRowRec *xkbrow;

        xkbrow = &xkbsection->rows[i];
        left = xkbrow->left;
        top = xkbrow->top;
        eek_section_add_row (section,
                             xkbrow->num_keys,
                             xkbrow->vertical ?
                             EEK_ORIENTATION_VERTICAL :
                             EEK_ORIENTATION_HORIZONTAL);
        for (j = 0; j < xkbrow->num_keys; j++) {
            XkbKeyRec *xkbkey;
            XkbBoundsRec *xkbbounds;

            xkbkey = &xkbrow->keys[j];
            if (xkbrow->vertical)
                top += xkbkey->gap;
            else
                left += xkbkey->gap;
            create_key (layout, keyboard, section, j, i, left, top, xkbkey);
            xkbbounds = &xkbgeometry->shapes[xkbkey->shape_ndx].bounds;
            if (xkbrow->vertical)
                top += xkbbounds->y2 - xkbbounds->y1;
            else
                left += xkbbounds->x2 - xkbbounds->x1;
        }
    }
}

static void
create_keyboard (EekXkbLayout *layout, EekKeyboard *keyboard)
{
    EekXkbLayoutPrivate *priv = layout->priv;
    XkbGeometryRec *xkbgeometry;
    EekBounds bounds;
    gint i;

    g_return_if_fail (priv->xkb);
    g_return_if_fail (priv->xkb->geom);

    xkbgeometry = priv->xkb->geom;

    eek_element_get_bounds (EEK_ELEMENT(keyboard), &bounds);
    setup_scaling (EEK_XKB_LAYOUT(layout), bounds.width, bounds.height);

    bounds.x = bounds.y = 0;
    bounds.width = xkb_to_pixmap_coord(layout, xkbgeometry->width_mm);
    bounds.height = xkb_to_pixmap_coord(layout, xkbgeometry->height_mm);

    for (i = 0; i < xkbgeometry->num_sections; i++) {
        XkbSectionRec *xkbsection;

        xkbsection = &xkbgeometry->sections[i];
        create_section (layout, keyboard, xkbsection);
    }
    eek_element_set_bounds (EEK_ELEMENT(keyboard), &bounds);
}

static EekKeyboard *
eek_xkb_layout_real_create_keyboard (EekLayout *self,
                                     gdouble    initial_width,
                                     gdouble    initial_height)
{
    EekXkbLayoutPrivate *priv = EEK_XKB_LAYOUT_GET_PRIVATE (self);
    EekBounds bounds;
    EekKeyboard *keyboard;

    keyboard = g_object_new (EEK_TYPE_KEYBOARD, "layout", self, NULL);
    bounds.x = bounds.y = 0.0;
    bounds.width = initial_width;
    bounds.height = initial_height;
    eek_element_set_bounds (EEK_ELEMENT(keyboard), &bounds);

    /* resolve modifiers dynamically assigned at run time */
    eek_keyboard_set_num_lock_mask (keyboard,
                                    XkbKeysymToModifiers (priv->display,
                                                          XK_Num_Lock));
    eek_keyboard_set_alt_gr_mask (keyboard,
                                  XkbKeysymToModifiers (priv->display,
                                                        XK_ISO_Level3_Shift));

    if (priv->shape_oref_hash)
        g_hash_table_destroy (priv->shape_oref_hash);

    priv->shape_oref_hash = g_hash_table_new (g_direct_hash, g_direct_equal);
    create_keyboard (EEK_XKB_LAYOUT(self), keyboard);
    g_hash_table_destroy (priv->shape_oref_hash);

    return keyboard;
}

static void
eek_xkb_layout_finalize (GObject *object)
{
    EekXkbLayoutPrivate *priv = EEK_XKB_LAYOUT_GET_PRIVATE (object);

    g_free (priv->names.keycodes);
    g_free (priv->names.geometry);
    g_free (priv->names.symbols);
    XkbFreeKeyboard (priv->xkb, 0, TRUE);	/* free_all = TRUE */
    G_OBJECT_CLASS (eek_xkb_layout_parent_class)->finalize (object);
}

static void 
eek_xkb_layout_set_property (GObject      *object,
                             guint         prop_id,
                             const GValue *value,
                             GParamSpec   *pspec)
{
    EekXkbLayout *layout = EEK_XKB_LAYOUT (object);

    switch (prop_id) {
    case PROP_DISPLAY:
        layout->priv->display = g_value_get_pointer (value);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void 
eek_xkb_layout_get_property (GObject    *object,
                             guint       prop_id,
                             GValue     *value,
                             GParamSpec *pspec)
{
    EekXkbLayout *layout = EEK_XKB_LAYOUT (object);

    switch (prop_id) {
    case PROP_DISPLAY:
        g_value_set_pointer (value, layout->priv->display);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
eek_xkb_layout_class_init (EekXkbLayoutClass *klass)
{
    EekLayoutClass *layout_class = EEK_LAYOUT_CLASS (klass);
    GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
    GParamSpec *pspec;

    g_type_class_add_private (gobject_class, sizeof (EekXkbLayoutPrivate));

    layout_class->create_keyboard = eek_xkb_layout_real_create_keyboard;

    gobject_class->finalize = eek_xkb_layout_finalize;
    gobject_class->set_property = eek_xkb_layout_set_property;
    gobject_class->get_property = eek_xkb_layout_get_property;

    pspec = g_param_spec_pointer ("display",
                                  "Display",
                                  "X Display",
                                  G_PARAM_READWRITE |
                                  G_PARAM_CONSTRUCT_ONLY);
    g_object_class_install_property (gobject_class, PROP_DISPLAY, pspec);
}

static void
eek_xkb_layout_init (EekXkbLayout *self)
{
    self->priv = EEK_XKB_LAYOUT_GET_PRIVATE (self);
}

static gboolean
get_names_from_server (EekXkbLayout *layout,
                       GError      **error)
{
    EekXkbLayoutPrivate *priv = layout->priv;
    gchar *name;

    XkbGetNames (priv->display, XkbAllNamesMask, priv->xkb);

    if (priv->xkb->names->keycodes <= 0)
        g_warning ("XKB keycodes setting is not loaded properly");
    else {
        name = XGetAtomName (priv->display, priv->xkb->names->keycodes);
        if (!name)
            g_warning ("Can't get the name of keycodes");
        else if (!priv->names.keycodes ||
                 g_strcmp0 (name, priv->names.keycodes)) {
            g_free (priv->names.keycodes);
            priv->names.keycodes = g_strdup (name);
            XFree (name);
        }
    }

    if (priv->xkb->names->geometry <= 0)
        g_warning ("XKB geometry setting is not loaded");
    else {
        name = XGetAtomName (priv->display, priv->xkb->names->geometry);
        if (!name)
            g_warning ("Can't get the name of geometry");
        else if (!priv->names.geometry ||
                 g_strcmp0 (name, priv->names.geometry)) {
            g_free (priv->names.geometry);
            priv->names.geometry = g_strdup (name);
            XFree (name);
        }
    }

    if (priv->xkb->names->symbols <= 0)
        g_warning ("XKB symbols setting is not loaded");
    else {
        name = XGetAtomName (priv->display, priv->xkb->names->symbols);
        if (!name)
            g_warning ("Can't get the name of symbols");
        else if (!priv->names.symbols ||
                 g_strcmp0 (name, priv->names.symbols)) {
            g_free (priv->names.symbols);
            priv->names.symbols = g_strdup (name);
            XFree (name);
        }
    }

    return TRUE;
}

/**
 * eek_xkb_layout_new:
 *
 * Create a new #EekXkbLayout.
 */
EekLayout *
eek_xkb_layout_new (Display *display,
                    GError **error)
{
    return (EekLayout *) g_initable_new (EEK_TYPE_XKB_LAYOUT,
                                         NULL,
                                         error,
                                         "display", display,
                                         NULL);
}

/**
 * eek_xkb_layout_set_names: (skip)
 * @layout: an #EekXkbLayout
 * @names: XKB component names
 * @error: a #GError
 *
 * Set the XKB component names to @layout.
 * Returns: %TRUE if the component names are successfully set, %FALSE otherwise
 */
gboolean
eek_xkb_layout_set_names (EekXkbLayout         *layout,
                          XkbComponentNamesRec *names,
                          GError              **error)
{
    if (g_strcmp0 (names->keycodes, layout->priv->names.keycodes)) {
        g_free (layout->priv->names.keycodes);
        layout->priv->names.keycodes = g_strdup (names->keycodes);
    }

    if (g_strcmp0 (names->geometry, layout->priv->names.geometry)) {
        g_free (layout->priv->names.geometry);
        layout->priv->names.geometry = g_strdup (names->geometry);
    }

    if (g_strcmp0 (names->symbols, layout->priv->names.symbols)) {
        g_free (layout->priv->names.symbols);
        layout->priv->names.symbols = g_strdup (names->symbols);
    }

    return get_keyboard_from_server (layout, error);
}

static gboolean
get_keyboard_from_server (EekXkbLayout *layout,
                          GError      **error)
{
    EekXkbLayoutPrivate *priv = layout->priv;

    if (priv->xkb) {
        XkbFreeKeyboard (priv->xkb, 0, True);
        priv->xkb = NULL;
    }

    if (priv->names.keycodes && priv->names.geometry && priv->names.symbols) {
        priv->xkb = XkbGetKeyboardByName (priv->display,
                                          XkbUseCoreKbd,
                                          &priv->names,
                                          0,
                                          XKB_COMPONENT_MASK,
                                          False);
    } else {
        priv->xkb = XkbGetKeyboard (priv->display,
                                    XKB_COMPONENT_MASK,
                                    XkbUseCoreKbd);
        if (!get_names_from_server (layout, error)) {
            XkbFreeKeyboard (priv->xkb, 0, True);
            priv->xkb = NULL;
        }
    }

    if (priv->xkb == NULL) {
        g_set_error (error,
                     EEK_ERROR,
                     EEK_ERROR_LAYOUT_ERROR,
                     "can't get keyboard from server");
        g_free (priv->names.keycodes);
        priv->names.keycodes = NULL;
        g_free (priv->names.geometry);
        priv->names.geometry = NULL;
        g_free (priv->names.symbols);
        priv->names.symbols = NULL;
        return FALSE;
    }
    return TRUE;
}


static guint
find_keycode (EekXkbLayout *layout, gchar *key_name)
{
#define KEYSYM_NAME_MAX_LENGTH 4
    guint keycode;
    gint i, j;
    XkbKeyNamePtr pkey;
    XkbKeyAliasPtr palias;
    guint is_name_matched;
    gchar *src, *dst;
    EekXkbLayoutPrivate *priv = layout->priv;

    if (!priv->xkb)
        return EEK_INVALID_KEYCODE;

    pkey = priv->xkb->names->keys + priv->xkb->min_key_code;
    for (keycode = priv->xkb->min_key_code;
         keycode <= priv->xkb->max_key_code; keycode++) {
        is_name_matched = 1;
        src = key_name;
        dst = pkey->name;
        for (i = KEYSYM_NAME_MAX_LENGTH; --i >= 0;) {
            if ('\0' == *src)
                break;
            if (*src++ != *dst++) {
                is_name_matched = 0;
                break;
            }
        }
        if (is_name_matched)
            return keycode;
        pkey++;
    }

    palias = priv->xkb->names->key_aliases;
    for (j = priv->xkb->names->num_key_aliases; --j >= 0;) {
        is_name_matched = 1;
        src = key_name;
        dst = palias->alias;
        for (i = KEYSYM_NAME_MAX_LENGTH; --i >= 0;) {
            if ('\0' == *src)
                break;
            if (*src++ != *dst++) {
                is_name_matched = 0;
                break;
            }
        }

        if (is_name_matched) {
            keycode = find_keycode (layout, palias->real);
            return keycode;
        }
        palias++;
    }

    return EEK_INVALID_KEYCODE;
}

static void
setup_scaling (EekXkbLayout *layout,
               gdouble       width,
               gdouble       height)
{
    EekXkbLayoutPrivate *priv = layout->priv;

    g_return_if_fail (priv->xkb);

    g_return_if_fail (priv->xkb->geom->width_mm > 0);
    g_return_if_fail (priv->xkb->geom->height_mm > 0);

    if (width * priv->xkb->geom->height_mm <
        height * priv->xkb->geom->width_mm) {
        priv->scale_numerator = width;
        priv->scale_denominator = priv->xkb->geom->width_mm;
    } else {
        priv->scale_numerator = height;
        priv->scale_denominator = priv->xkb->geom->height_mm;
    }
}

static gboolean
initable_init (GInitable    *initable,
               GCancellable *cancellable,
               GError      **error)
{
    EekXkbLayout *layout = EEK_XKB_LAYOUT (initable);

    if (!get_keyboard_from_server (layout, error))
        return FALSE;

    if (!get_names_from_server (layout, error))
        return FALSE;

    return TRUE;
}

static void
initable_iface_init (GInitableIface *initable_iface)
{
  initable_iface->init = initable_init;
}
