/*
 * olpc-kbdshim-udev.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) 2011, Paul G. Fox
 *
 * 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
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#ifdef PGF_CROSS
#include "arm-cross/libudev.h"
#else
#include <libudev.h>
#endif
#include <ctype.h>

#include <linux/input.h>
#include "common.h"

#define LOG_PROPERTY(path, prop, val)                           \
    dbg(3, "getting property %s on %s "                         \
           "returned \"%s\"",                                 \
           (prop), (path), (val) ? (val) : "(null)")
#define LOG_SYSATTR(path, attr, val)                            \
    dbg(3, "getting attribute %s on %s "                        \
           "returned \"%s\"",                                 \
           (attr), (path), (val) ? (val) : "(null)")

static struct udev_monitor *udevmon;
int udev_fd = -1;
int maxfd = -1;

static int fd_flags[FD_SETSIZE];
static char *fd_paths[FD_SETSIZE];
#define IS_KBD      0x01
#define IS_TPAD     0x02
#define IS_CMDFIFO  0x04
#define IS_UDEVMON  0x08
#define IS_OTHER    0x10
#define IS_EBOOK    0x20
#define IS_TSCRN    0x40
#define IS_LOCAL    0x80
#define IS_TPAD_ABS 0x100

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

/* bit array ops */
#define bits2bytes(x) ((((x)-1)/8)+1)
#define test_bit(bit, array) ( array[(bit)/8] & (1 << (bit)%8) )

int
device_is_duplicate(const char *p, int fd)
{
    int i;
    dbg(2, "checking syspath %s for dup.", p);
    for (i = 0; i <= maxfd; i++) {
        if (!fd_paths[i] || i == fd)
            continue;
        dbg(4, "checking against %s.", fd_paths[i]);
        if (!strcmp(p, fd_paths[i]))
            return 1;
    }
    fd_paths[fd] = strdup(p);
    return 0;
}

static void
device_added(struct udev_device *udev_device)
{
    const char *path, *name = NULL;
    const char *syspath;
    struct udev_device *parent;
    int dfd;
    struct input_id id;
    unsigned char bit[bits2bytes(EV_MAX)];

    path = udev_device_get_devnode(udev_device);
    syspath = udev_device_get_syspath(udev_device);
    dbg(4, "in device_added, path %s, syspath %s", path, syspath);

    if (!path || !syspath)
        return;

    if ((dfd = open(path, O_RDONLY)) < 0) {
        report("failed to open input device %s (%s)", name, path);
        return;
    }

    dbg(4, "calling get_property_value");
    if (!udev_device_get_property_value(udev_device, "ID_INPUT")) {
        dbg(4, "ignoring device %s without "
               "property ID_INPUT set", path);
        close(dfd);
        return;
    }

    parent = udev_device_get_parent(udev_device);
    dbg(4, "got parent %p", parent);
    if (parent) {
        const char *ppath = udev_device_get_devnode(parent);

        name = udev_device_get_sysattr_value(parent, "name");
        LOG_SYSATTR(ppath, "name", name);
        if (!name) {
            name = udev_device_get_property_value(parent, "NAME");
            LOG_PROPERTY(ppath, "NAME", name);
        }

    }
    dbg(4, "got name %s, syspath %s", name, syspath);
    if (!name)
        name = "(unnamed)";

    if (device_is_duplicate(syspath, dfd)) {
        report("device %s already added.  ignoring.", name);
        close(dfd);
        return;
    }

    if (ioctl(dfd, EVIOCGID, &id) < 0) {
        dbg(4, "failed ioctl EVIOCGID on %s", path);
        close(dfd);
        return;
    }

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

    dbg(2, "checking input device %s (%s)", name, path);

    if (ioctl(dfd, EVIOCGBIT(0, EV_MAX), bit) < 0) {
        report("failed ioctl EVIOCGBIT on %s", path);
        close(dfd);
        return;
    }

    dbg(4, "done with ioctls on %s, %s", name, path);

    /* is this the ebook switch? */
    if (test_bit(EV_SW, bit)) {
        unsigned char swbit[bits2bytes(EV_MAX)];
        dbg(4, "checking a switch", name, path);
        if (ioctl(dfd, EVIOCGBIT(EV_SW, SW_CNT), swbit) < 0) {
            report("failed ioctl EVIOCGBIT EV_SW on %s", path);
            close(dfd);
            return;
        }

        if (!test_bit(SW_TABLET_MODE, swbit)) {
            close(dfd);
            return;
        }

        report("fd %d: found ebook switch", dfd);
        fd_flags[dfd] = IS_EBOOK;

        if (dfd > maxfd)
            maxfd = dfd;

        return;
    }

    if (!test_bit(EV_KEY, bit)) {
        /* heuristic #1 -- all our interesting input devices have
         * at least one key or button.  the accelerometer, in particular,
         * does not, and we definitely want to skip it.  */
        dbg(1, "no EV_KEY on %s, ignoring", name);
        close(dfd);
        return;
    }

    if (test_bit(EV_KEY, bit) && test_bit(EV_REP, bit)) {
        fd_flags[dfd] = IS_KBD;
        if (is_local_kbd(&id))
            fd_flags[dfd] |= IS_LOCAL;

        report("fd %d: found keyboard (%s) %s (%02x:%02x:%02x)", dfd, name,
            path, id.bustype, id.vendor, id.product);
    }

    if (test_bit(EV_REL, bit)) {
        fd_flags[dfd] = IS_TPAD;
        if (is_local_tpad(&id))
            fd_flags[dfd] |= IS_LOCAL;

        report("fd %d: found touchpad (%s) %s (%02x:%02x:%02x)", dfd, name,
            path, id.bustype, id.vendor, id.product);

        /* unless it was already set on the commandline,
         * establish the path to the disabling node */
        if (!local_tpad_enabled[0]) {
            snprintf(local_tpad_enabled, sizeof(local_tpad_enabled),
                "%s/device/device/enabled", syspath);
        }

    }

    if (test_bit(EV_ABS, bit)) {
        unsigned char keybit[bits2bytes(KEY_MAX)];
        if (ioctl(dfd, EVIOCGBIT(EV_KEY, KEY_CNT), keybit) < 0) {
            report("failed ioctl EVIOCGBIT EV_KEY on %s", path);
            close(dfd);
            return;
        }

        if (test_bit(BTN_LEFT, keybit)) {
            /* if the absolute device has a left button, we assume that it's
             * the HGPK pentablet or another similar absolute input mouse. */
            fd_flags[dfd] = IS_TPAD_ABS;

            if (is_local_tpad(&id))
                fd_flags[dfd] |= IS_LOCAL;

            report("fd %d: found absolute touchpad (%s) %s (%02x:%02x:%02x)",
                dfd, name, path, id.bustype, id.vendor, id.product);

            /* unless it was already set on the commandline,
             * establish the path to the disabling node */
            if (!local_tpad_enabled[0]) {
                snprintf(local_tpad_enabled, sizeof(local_tpad_enabled),
                    "%s/device/device/enabled", syspath);
            }
        } else {
            /* It's the touchscreen.  we want activity reports from it; we also
             * grab and filter it to hack in basic swipe and scroll
             * gesture support.  if we have other ABS devices someday
             * (like some touchpads in native mode), we can check for the
             * multitouch buttons to reject these.
             */

            struct input_absinfo info;
            int axes[] = {
                ABS_X, ABS_Y, ABS_PRESSURE,
                ABS_MT_POSITION_X, ABS_MT_POSITION_Y,
                ABS_MT_TOUCH_MAJOR, ABS_MT_WIDTH_MAJOR,
                ABS_MT_TRACKING_ID, -1
            };
            int i;

            fd_flags[dfd] = IS_TSCRN;
            report("fd %d: found touchscreen (%s) %s (%02x:%02x:%02x)", dfd, name,
                path, id.bustype, id.vendor, id.product);
            for (i=0; axes[i] != -1; i++) {
                if (ioctl(dfd, EVIOCGABS(axes[i]), &info) >= 0)
                    touchscreen_config_axis(axes[i], &info);
            }
            setup_uinput(1);
        }
    }

    if ((fd_flags[dfd] & (IS_KBD|IS_TPAD|IS_TPAD_ABS|IS_TSCRN)) == 0)  {
        dbg(1, "%s is not an interesting device", name);
        close(dfd);
        return;
    }

    if ((fd_flags[dfd] & IS_OTHER) == 0) {
        if (ioctl(dfd, EVIOCGRAB, 1) < 0)
            report("couldn't EVIOCGRAB %s", path);
        else
            dbg(2, "EVIOCGRAB succeeded %s", path);
    }

    if (dfd > maxfd)
        maxfd = dfd;

    return;
}

static void
device_removed(struct udev_device *device)
{
    const char *syspath = udev_device_get_syspath(device);
    dbg(4, "removing syspath %s", syspath);
}


int
setup_input(void)
{
    struct udev *udev;
    struct udev_enumerate *enumerate;
    struct udev_list_entry *devices, *device;

    udev = udev_new();
    if (!udev)
        return 0;
    udevmon = udev_monitor_new_from_netlink(udev, "udev");
    if (!udevmon)
        return 0;
    udev_monitor_filter_add_match_subsystem_devtype(udevmon, "input", NULL);

    if (udev_monitor_enable_receiving(udevmon)) {
        report("failed to bind the udev monitor");
        return 0;
    }

    enumerate = udev_enumerate_new(udev);
    if (!enumerate)
        return 0;
    udev_enumerate_add_match_subsystem(enumerate, "input");
    udev_enumerate_scan_devices(enumerate);
    devices = udev_enumerate_get_list_entry(enumerate);
    udev_list_entry_foreach(device, devices) {
        const char *syspath = udev_list_entry_get_name(device);
        struct udev_device *udev_device = udev_device_new_from_syspath(udev, syspath);
        device_added(udev_device);
        udev_device_unref(udev_device);
    }
    udev_enumerate_unref(enumerate);

    udev_fd = udev_monitor_get_fd(udevmon);

    return 1;
}

#if NEEDED
void
destroy_input(void)
{
    struct udev *udev;

    if (!udevmon)
        return;

    udev = udev_monitor_get_udev(udevmon);

    udev_monitor_unref(udevmon);
    udevmon = NULL;
    udev_unref(udev);
}
#endif



static void
device_handler(void)
{
    struct udev_device *udev_device;
    const char *action;

    dbg(4, "in device handler");
    udev_device = udev_monitor_receive_device(udevmon);
    if (!udev_device)
        return;

    action = udev_device_get_action(udev_device);
    dbg(4, "further in device handler, action %s", action);

    if (action) {
        if (!strcmp(action, "add"))
            device_added(udev_device);
        else if (!strcmp(action, "remove"))
            device_removed(udev_device);
    }

    udev_device_unref(udev_device);
}

static int
keyboard_event (int kbd_fd, int is_local)
{
    struct input_event ev[1];

    dbg(4, "keyboard_event");
    if (read(kbd_fd, ev, sizeof(ev)) != sizeof(ev)) {
        report("bad read from keyboard, no more fd %d", kbd_fd);
        close(kbd_fd);
        fd_flags[kbd_fd] = 0;
        return 0;
    }

    return keyboard_event_worker(ev, is_local);
}

void
touchpad_event(int tpad_fd, int is_local)
{
    struct input_event ev[1];

    dbg(4, "touchpad");
    if (read(tpad_fd, ev, sizeof(ev)) != sizeof(ev)) {
        report("bad read from touchpad, no more fd %d", tpad_fd);
        close(tpad_fd);
        fd_flags[tpad_fd] = 0;
        return;
    }

    touchpad_event_worker(ev, is_local);
}

void
abs_touchpad_event(int tpad_fd, int is_local)
{
    struct input_event ev[1];

    dbg(4, "abs_touchpad");
    if (read(tpad_fd, ev, sizeof(ev)) != sizeof(ev)) {
        report("bad read from touchpad, no more fd %d", tpad_fd);
        close(tpad_fd);
        fd_flags[tpad_fd] = 0;
        return;
    }

    abs_touchpad_event_worker(ev, is_local);
}

void
ebook_event(int ebook_fd)
{
    struct input_event ev[1];

    dbg(4, "ebook");
    if (read(ebook_fd, ev, sizeof(ev)) != sizeof(ev)) {
        report("bad read from ebook, no more fd %d", ebook_fd);
        close(ebook_fd);
        fd_flags[ebook_fd] = 0;
        return;
    }

    ebook_event_worker(ev);
}

void
other_event(int oth_fd, int is_local)
{
    struct input_event ev[1];

    dbg(4, "other");
    if (read(oth_fd, ev, sizeof(ev)) != sizeof(ev)) {
        report("bad read from other, no more fd %d", oth_fd);
        close(oth_fd);
        fd_flags[oth_fd] = 0;
        return;
    }

    /* do nothing with the data.  just consume it. */

}

void
tscrn_event(int tscrn_fd, int is_local)
{
    struct input_event ev[1];

    dbg(4, "tscrn");
    if (read(tscrn_fd, ev, sizeof(ev)) != sizeof(ev)) {
        report("bad read from other, no more fd %d", tscrn_fd);
        close(tscrn_fd);
        fd_flags[tscrn_fd] = 0;
        return;
    }

    touchscreen_event_worker(ev, is_local);
}

static void
indicate_idleness(void)
{
    static char useridle[] = "useridleX";

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

    send_event(useridle);
    wasidle = 1;
}

void
reinit_activity_monitor(void)
{
    wasidle = 1;
    idleness = 0;
}

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

    reinit_activity_monitor();
    wasidle = 0;
}

static void
get_command(void)
{
    int n;
    char command[128];
    char *cp, *cmdp;

    n = read(fifo_fd, command, sizeof(command)-1);

    if (n < 1) {
        if (n < 0)
            die("read from rotation fifo");
        deinit_fifo();
    }
    command[n] = '\0';

    /* more than one command may have been read */
    for (cp = command; ; cp = 0) {
        cmdp = strtok(cp, "\n");
        if (!cmdp)
            break;
        command_worker(cmdp);
    }
}


void
monitor_input(void)
{
    fd_set inputs, errors;
    int r;
    struct timeval tv;
    struct timeval *tvp;
    int pressed;
    int fd;

    if (fifo_fd >= 0) {
        fd_flags[fifo_fd] = IS_CMDFIFO;
        dbg(4, "fifo_fd is fd %d", fifo_fd);
        if (fifo_fd > maxfd)
            maxfd = fifo_fd;
    }
    if (udev_fd >= 0) {
        fd_flags[udev_fd] = IS_UDEVMON;
        dbg(4, "udev_fd is fd %d", udev_fd);
        if (udev_fd > maxfd)
            maxfd = udev_fd;
    }

    indicate_activity();


    while (1) {

        FD_ZERO(&inputs);
        FD_ZERO(&errors);
        for (fd = 0; fd <= maxfd; fd++) {
            if (fd_flags[fd]) {
                FD_SET(fd, &inputs);
                FD_SET(fd, &errors);
            }
        }

        if (idledeltas[idleness]) {
            tv.tv_sec = idledeltas[idleness];
            tv.tv_usec = 0;
            tvp = &tv;
        } else {
            tvp = 0;
        }

        r = select(maxfd+1, &inputs, NULL, &errors, tvp);
        if (r < 0)
            die("select failed");

        dbg(4, "back from select: %d", r);

        if (r == 0) {
            indicate_idleness();
            continue;
        }

        pressed = 0;
        for (fd = 0; fd <= maxfd; fd++) {

            if (!fd_flags[fd])
                continue;

            if (FD_ISSET(fd, &inputs) || FD_ISSET(fd, &errors)) {
                dbg(4, "fd %d inputs %d errors %d", fd,
                        FD_ISSET(fd, &inputs), FD_ISSET(fd, &errors));

                if (fd_flags[fd] & IS_KBD) {

                    pressed |= keyboard_event(fd, !!(fd_flags[fd] & IS_LOCAL));

                } else if (fd_flags[fd] & IS_TPAD) {

                    touchpad_event(fd, !!(fd_flags[fd] & IS_LOCAL));
                    pressed = 1;

                } else if (fd_flags[fd] & IS_TPAD_ABS) {

                    abs_touchpad_event(fd, !!(fd_flags[fd] & IS_LOCAL));
                    pressed = 1;

                } else if (fd_flags[fd] & IS_EBOOK) {

                    ebook_event(fd);

                } else if (fd_flags[fd] & IS_TSCRN) {

                    tscrn_event(fd, 1);
                    pressed = 1;

                } else if (fd_flags[fd] & IS_OTHER) {

                    other_event(fd, 0);
                    pressed = 1;

                } else if (fd_flags[fd] & IS_CMDFIFO) {

                    get_command();

                } else if (fd_flags[fd] & IS_UDEVMON) {

                    device_handler();

                }
            }
        }

        if (pressed)
            indicate_activity();

    }

}
