/*
 * olpc-kbdshim-hal.c: -- special touchpad and keyboard support for XO:
 *  - mouse-based scrolling use the XO grab keys
 *  - touchpad and dpad rotation (to match screen rotation)
 *  - user activity monitoring
 *
 * Copyright (C) 2009,2010,2011, Paul G. Fox, inspired in places
 *  by code from mouseemu, by Colin Leroy and others.
 *
 * 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 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
 */

/*
 * This hal support skeleton of this program program is based heavily on:
 *  addon-input.c : Listen to key events and modify hal device objects
 *
 * Copyright (C) 2005 David Zeuthen, <david@fubar.dk>
 * Copyright (C) 2005 Ryan Lortie <desrt@desrt.ca>
 * Copyright (C) 2006 Matthew Garrett <mjg59@srcf.ucam.org>
 * Copyright (C) 2007 Codethink Ltd. Author Rob Taylor <rob.taylor@codethink.co.uk>
 */

#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <syslog.h>
#include <time.h>
#include <unistd.h>

#include <linux/input.h>
#include <linux/uinput.h>

#include <glib/gmain.h>
#include <glib/gprintf.h>
#include <dbus/dbus-glib-lowlevel.h>

#include <hal/libhal.h>

#include "common.h"


/* are we in user-idle state? */
static int idleness;
static int wasidle;
static int idle_delta;

int
setup_input()
{
    return 1;
}

static int
keyboard_event (GIOChannel *channel, int is_local)
{
    struct input_event ev[1];
    gsize read_bytes;
    GError *gerror = NULL;

    if (g_io_channel_read_chars (channel,
                    (gchar*)ev, sizeof(ev),
                    &read_bytes, &gerror) != G_IO_STATUS_NORMAL) {
        die("bad read from keyboard");
    }

    if (read_bytes != sizeof(ev)) {
        dbg(1, "short read of ev (%d)", read_bytes);
        return 0;
    }

    return keyboard_event_worker(ev, is_local);
}

static void
touchpad_event(GIOChannel *channel, int is_local)
{
    struct input_event ev[1];
    gsize read_bytes;
    GError *gerror = NULL;

    if (g_io_channel_read_chars (channel,
                    (gchar*)ev, sizeof(ev),
                    &read_bytes, &gerror) != G_IO_STATUS_NORMAL) {
        die("bad read from pointer");
    }

    if (read_bytes != sizeof(ev)) {
        report("short read of ev (%d)", read_bytes);
        return;
    }

    touchpad_event_worker(ev, is_local);
}

static gboolean
indicate_idleness(gpointer data)
{

    static char useridle[] = "useridleX";

    useridle[8] = idleness + '1';
    if (idleness < MAXTIMERS) idleness++;

    send_event(useridle);
    wasidle = 1;

    if (idledeltas[idleness])
        idle_delta = g_timeout_add_seconds(
                idledeltas[idleness], indicate_idleness, 0);
    else
        idle_delta = 0;

    return FALSE;
}

void
reinit_activity_monitor(void)
{
    wasidle = 1;
    idleness = 0;
    if (idle_delta)
        g_source_remove(idle_delta);
    idle_delta = g_timeout_add_seconds(
            idledeltas[idleness], indicate_idleness, 0);
}

static void
indicate_activity(void)
{
    if (wasidle)
        send_event("useractive");

    reinit_activity_monitor();
    wasidle = 0;
}

static void
get_command(GIOChannel *channel)
{
    GError *gerror = NULL;
    GIOStatus s;
    static GString *cmd;

    dbg(1, "reading from command fifo");

    if (!cmd)
        cmd = g_string_new("");

    s = g_io_channel_read_line_string (channel, cmd, 0, &gerror);
    if (s != G_IO_STATUS_NORMAL) {
        if (s == G_IO_STATUS_AGAIN) {
            return;
        }
        die("read from command fifo");
    }

    command_worker(cmd->str);
}

/* we must use this kernel-compatible implementation */
#define BITS_PER_LONG (sizeof(long) * 8)
#define NBITS(x) ((((x)-1)/BITS_PER_LONG)+1)
#define OFF(x)  ((x)%BITS_PER_LONG)
#define BIT(x)  (1UL<<OFF(x))
#define LONG(x) ((x)/BITS_PER_LONG)
#define test_bit(bit, array)    ((array[LONG(bit)] >> OFF(bit)) & 1)

#define IS_KBD      1
#define IS_POINTER  2
#define IS_LOCAL    4
#define IS_CMDFIFO  8


static LibHalContext *ctx = NULL;
static GMainLoop *gmain = NULL;
static GHashTable *inputs = NULL;
static GList *devices = NULL;

static gboolean
event_io (GIOChannel *channel, GIOCondition condition, gpointer gdata)
{
        unsigned int info = (unsigned int) gdata;
        int pressed = 0;
        dbg(2, "event_io called, info is 0x%x, (%d)",
                info, !!(info & IS_LOCAL));

        if (condition & (G_IO_HUP | G_IO_ERR | G_IO_NVAL))
                return FALSE;

        if (info & IS_KBD) {
            pressed |= keyboard_event(channel, !!(info & IS_LOCAL));
            dbg(3, "kbd activity (%d)", !!(info & IS_LOCAL));
        }

        if (info & IS_POINTER) {
            touchpad_event(channel, !!(info & IS_LOCAL));
            pressed = 1;
            dbg(3, "pointer activity (%d)", !!(info & IS_LOCAL));
        }

        if (info & IS_CMDFIFO) {
            dbg(3, "got command");
            get_command(channel);
        }

        if (pressed)
            indicate_activity();

        return TRUE;
}


static void
add_device (LibHalContext *ctx,
            const char *udi,
            const LibHalPropertySet *properties)
{
        int eventfp;
        GIOChannel *channel;
        unsigned int info;
        const char* device_file;
        struct input_id id;
        unsigned long bit[NBITS(EV_MAX)];

        dbg(1, "add_device called");

        device_file = libhal_ps_get_string (properties, "input.device");
        if (device_file == NULL) {
            report("%s has no property input.device", udi);
            return;
        }

        dbg(1, "adding %s", device_file);

        eventfp = open(device_file, O_RDONLY | O_NONBLOCK);
        if (!eventfp) {
            report("Unable to open %s for reading", device_file);
            return;
        }


        if (ioctl(eventfp, EVIOCGID, &id) < 0) {
            report("failed ioctl EVIOCGID");
            close(eventfp);
            return;
        }

        if (id.bustype == BUS_VIRTUAL &&
                    id.vendor  == 'p' &&
                    id.product == 'g' &&
                    id.version == 'f') {
            dbg(1, "declining to monitor our own output");
            close(eventfp);
            return;
        }

        if (ioctl(eventfp, EVIOCGBIT(0, EV_MAX), bit) < 0) {
            report("failed ioctl EVIOCGBIT");
            close(eventfp);
            return;
        }

        info = 0;

        dbg(1, "testing EV_KEY bit");
        if (test_bit(EV_KEY, bit) && test_bit(EV_REP, bit)) {

            info |= IS_KBD;
            if (is_local_kbd(&id))
                info |= IS_LOCAL;

            report("%s keyboard %s (%02x:%02x:%02x)",
                (info & IS_LOCAL) ? "matched local" : "found",
                device_file, id.bustype, id.vendor, id.product);
        }

        dbg(1, "testing EV_REL bit");
        if ( test_bit(EV_REL, bit) ) {

            info |= IS_POINTER;
            if (is_local_tpad(&id))
                info |= IS_LOCAL;

            report("%s pointer %s (%02x:%02x:%02x)",
                (info & IS_LOCAL) ? "matched local" : "found",
                device_file, id.bustype, id.vendor, id.product);
        }

        if (!(info & IS_KBD) && !(info & IS_POINTER))  {
            dbg(1, "not kbd or pointer");
            close(eventfp);
            return;
        }

        if (ioctl(eventfp, EVIOCGRAB, 1) < 0) {
            report("add_device: couldn't GRAB %s", device_file);
        }

        dbg(1, "Listening on %s", device_file);

        devices = g_list_prepend (devices, g_strdup (device_file));

        channel = g_io_channel_unix_new (eventfp);
        g_io_channel_set_encoding (channel, NULL, NULL);
        g_io_channel_set_buffered (channel, 0);

        g_hash_table_insert (inputs, g_strdup(udi), channel);
        dbg(1, "adding to watch");
        int i = g_io_add_watch_full (channel, G_PRIORITY_DEFAULT,
                             G_IO_IN | G_IO_ERR | G_IO_HUP | G_IO_NVAL,
                             event_io, (gpointer)info, NULL);
        dbg(1, "watch full returned %d", i);
}


static void
remove_device (LibHalContext *ctx,
            const char *udi,
            const LibHalPropertySet *properties)
{

        GIOChannel *channel, **p_channel = &channel;
        const gchar *device_file;
        GList *lp;
        gboolean handling_udi;

        dbg(1, "remove_device called");
        dbg(1, "Removing channel for '%s'", udi);

        handling_udi = g_hash_table_lookup_extended (inputs, udi, NULL, (gpointer *)p_channel);

        if (!handling_udi) {
                report ("DeviceRemove called for unknown device: '%s'.", udi);
                return;
        }

        if (channel) {
                g_io_channel_shutdown(channel, FALSE, NULL);
                g_io_channel_unref (channel);
        }

        g_hash_table_remove (inputs, udi);

        if ((device_file = libhal_ps_get_string (properties, "input.device")) == NULL) {
                report ("%s has no property input.device", udi);
                return;
        }

        lp = g_list_find_custom (devices, device_file, (GCompareFunc) strcmp);
        if (lp) {
                devices = g_list_remove_link (devices, lp);
                g_free (lp->data);
                g_list_free_1 (lp);
        }

        if (g_hash_table_size (inputs) == 0) {
                report("no more devices, exiting");
                g_main_loop_quit (gmain);
        }
}

static void
init_fifo_glib(int fd)
{
    GIOChannel *channel;
    unsigned int info;
    info = IS_CMDFIFO;

    channel = g_io_channel_unix_new (fd);
    g_io_channel_set_encoding (channel, NULL, NULL);
    g_io_channel_set_flags(channel, G_IO_FLAG_NONBLOCK, NULL);
    g_io_add_watch_full (channel, G_PRIORITY_DEFAULT,
             G_IO_IN | G_IO_ERR | G_IO_HUP | G_IO_NVAL,
             event_io, (gpointer)info, NULL);
    dbg(1, "done with glib fifo init");
}

void
monitor_input(void)
{
    DBusConnection *dbus_connection;
    DBusError error;
    const char *commandline;

    if (fifo_fd >= 0)
        init_fifo_glib(fifo_fd);

    dbus_error_init (&error);
    if ((ctx = libhal_ctx_init_direct (&error)) == NULL) {
            report ("Unable to init libhal context");
            goto out;
    }

    if ((dbus_connection = libhal_ctx_get_dbus_connection(ctx)) == NULL) {
            report ("Cannot get DBus connection");
            goto out;
    }

    if ((commandline = getenv ("SINGLETON_COMMAND_LINE")) == NULL) {
            report ("SINGLETON_COMMAND_LINE not set");
            goto out;
    }
    dbg(1, "initial ctx calls succeeded");

    libhal_ctx_set_singleton_device_added (ctx, add_device);
    libhal_ctx_set_singleton_device_removed (ctx, remove_device);
    dbg(1, "set_singleton calls done");

    dbus_connection_setup_with_g_main (dbus_connection, NULL);
    dbus_connection_set_exit_on_disconnect (dbus_connection, 0);
    dbg(1, "dbus connection setup calls done");

    dbus_error_init (&error);

    if (!libhal_device_singleton_addon_is_ready (ctx, commandline, &error)) {
            goto out;
    }
    dbg(1, "dbus singleton ready call done");

    inputs = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);

    gmain = g_main_loop_new (NULL, FALSE);

    reinit_activity_monitor();
    dbg(1, "starting main loop");
    g_main_loop_run (gmain);

    return;

out:
    dbg(1, "An error occured, exiting cleanly");
    if (ctx != NULL) {
            dbus_error_init (&error);
            libhal_ctx_shutdown (ctx, &error);
            libhal_ctx_free (ctx);
    }

    exit(1);
}

