/*
 * Copyright © 2011 Alexandr Shadchin
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

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

#include <xorg-server.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <errno.h>
#include <string.h>
#include "synproto.h"
#include "synapticsstr.h"
#include <xf86.h>

#include <fcntl.h>
#include <dev/wscons/wsconsio.h>

#ifdef X_PRIVSEP
extern int priv_open_device(const char *);
#endif

#define DEFAULT_WSMOUSE_DEV		"/dev/wsmouse0"

#define NWSEVENTS	16

struct wsconscomm_proto_data {
    struct wscons_event	events[NWSEVENTS];
    size_t		events_count;
    size_t		events_pos;
};

static Bool
WSConsIsTouchpad(InputInfoPtr pInfo, const char *device)
{
    int wsmouse_type, fd = -1;
    Bool rc = FALSE;

    if (device) {
#ifndef X_PRIVSEP
        fd = open(device, O_RDWR);
#else
        fd = priv_open_device(device);
#endif
    } else
        fd = pInfo->fd;

    if (fd < 0)
        return FALSE;

    if (ioctl(fd, WSMOUSEIO_GTYPE, &wsmouse_type) == -1) {
        xf86IDrvMsg(pInfo, X_ERROR, "cannot get mouse type\n");
        goto out;
    }

    if (wsmouse_type == WSMOUSE_TYPE_SYNAPTICS ||
        wsmouse_type == WSMOUSE_TYPE_SYNAP_SBTN ||
        wsmouse_type == WSMOUSE_TYPE_ALPS ||
        wsmouse_type == WSMOUSE_TYPE_ELANTECH ||
        wsmouse_type == WSMOUSE_TYPE_TOUCHPAD)
        rc = TRUE;

out:
    if (device)
        close(fd);

    return rc;
}

static size_t
WSConsReadEvents(InputInfoPtr pInfo)
{
    SynapticsPrivate *priv = (SynapticsPrivate *)pInfo->private;
    struct wsconscomm_proto_data *proto_data = priv->proto_data;
    ssize_t len;

    proto_data->events_count = proto_data->events_pos = 0;
    len = read(pInfo->fd, proto_data->events, sizeof(proto_data->events));
    if (len <= 0) {
        if (errno != EAGAIN)
            xf86IDrvMsg(pInfo, X_ERROR, "read error %s\n", strerror(errno));
    } else if (len % sizeof(struct wscons_event)) {
        xf86IDrvMsg(pInfo, X_ERROR, "read error, invalid number of bytes\n");
    } else {
        proto_data->events_count = len / sizeof(struct wscons_event);
    }

    return proto_data->events_count;
}

static struct wscons_event *
WSConsGetEvent(InputInfoPtr pInfo)
{
    SynapticsPrivate *priv = (SynapticsPrivate *)pInfo->private;
    struct wsconscomm_proto_data *proto_data = priv->proto_data;
    struct wscons_event *event;

    if (proto_data->events_count == 0 && WSConsReadEvents(pInfo) == 0)
        return NULL;

    event = &proto_data->events[proto_data->events_pos];
    proto_data->events_pos++;
    proto_data->events_count--;

    return event;
}

static Bool
WSConsDeviceOnHook(InputInfoPtr pInfo, SynapticsParameters *para)
{
    int wsmouse_mode = WSMOUSE_NATIVE;

    if (ioctl(pInfo->fd, WSMOUSEIO_SETMODE, &wsmouse_mode) == -1) {
        xf86IDrvMsg(pInfo, X_ERROR, "cannot set native mode\n");
        return FALSE;
    }

    return TRUE;
}

static Bool
WSConsDeviceOffHook(InputInfoPtr pInfo)
{
    int wsmouse_mode = WSMOUSE_COMPAT;

    if (ioctl(pInfo->fd, WSMOUSEIO_SETMODE, &wsmouse_mode) == -1) {
        xf86IDrvMsg(pInfo, X_ERROR, "cannot set compat mode\n");
        return FALSE;
    }

    return TRUE;
}

static Bool
WSConsQueryHardware(InputInfoPtr pInfo)
{
    return WSConsIsTouchpad(pInfo, NULL);
}

static void
WSConsAdjustScrollCoords(SynapticsPrivate *priv, struct SynapticsHwState *hw)
{
    int dx, dy, i;

    dx = hw->x - priv->scroll.last_x;
    dy = hw->y - priv->scroll.last_y;
    priv->scroll.last_x = hw->x;
    priv->scroll.last_y = hw->y;
    for (i = 0; i < SYNAPTICS_MOVE_HISTORY; i++) {
        priv->move_hist[i].x += dx;
        priv->move_hist[i].y += dy;
    }
}

static Bool
WSConsReadHwState(InputInfoPtr pInfo,
    struct CommData *comm, struct SynapticsHwState *hwRet)
{
    SynapticsPrivate *priv = (SynapticsPrivate *)pInfo->private;
    struct wsconscomm_proto_data *proto_data = priv->proto_data;
    struct SynapticsHwState *hw = comm->hwState;
    struct wscons_event *event;
    Bool v, reset = FALSE;

    while ((event = WSConsGetEvent(pInfo)) != NULL) {
        switch (event->type) {
        case WSCONS_EVENT_MOUSE_UP:
        case WSCONS_EVENT_MOUSE_DOWN:
            v = (event->type == WSCONS_EVENT_MOUSE_DOWN) ? TRUE : FALSE;
            switch (event->value) {
            case 0:
                hw->left = v;
                break;
            case 1:
                hw->middle = v;
                break;
            case 2:
                hw->right = v;
                break;
            case 3:
                hw->up = v;
                break;
            case 4:
                hw->down = v;
                break;
            case 5:
                hw->multi[0] = v;
                break;
            case 6:
                hw->multi[1] = v;
                break;
            case 7:
                hw->multi[2] = v;
                break;
            case 8:
                hw->multi[3] = v;
                break;
            case 9:
                hw->multi[4] = v;
                break;
            case 10:
                hw->multi[5] = v;
                break;
            case 11:
                hw->multi[6] = v;
                break;
            case 12:
                hw->multi[7] = v;
                break;
            }
            break;
        case WSCONS_EVENT_MOUSE_ABSOLUTE_X:
            hw->x = event->value;
            hw->cumulative_dx = hw->x;
            break;
        case WSCONS_EVENT_MOUSE_ABSOLUTE_Y:
            hw->y = priv->maxy - event->value + priv->miny;
            hw->cumulative_dy = hw->y;
            break;
        case WSCONS_EVENT_TOUCH_PRESSURE:
            hw->z = event->value;
            break;
        case WSCONS_EVENT_TOUCH_CONTACTS:
            hw->numFingers = event->value;
            if (hw->numFingers == 0)
                hw->fingerWidth = 0;
            else if (hw->fingerWidth == 0)
                hw->fingerWidth = 5;
            break;
        case WSCONS_EVENT_TOUCH_WIDTH:
            hw->fingerWidth = event->value;
            break;
        case WSCONS_EVENT_TOUCH_RESET:
            /* The contact count or the active MT slot has changed. */
            reset = TRUE;
            break;
        case WSCONS_EVENT_SYNC:
            if (reset) {
                /* Ensure that pointer motion stops. */
                priv->count_packet_finger = 0;
                /* Don't use stale coordinates for filtering. */
                priv->hyst_center_x = hw->x;
                priv->hyst_center_y = hw->y;
                if (priv->vert_scroll_twofinger_on
                    || priv->horiz_scroll_twofinger_on) {
                    WSConsAdjustScrollCoords(priv, hw);
                }
            }
            hw->millis = 1000 * event->time.tv_sec +
                event->time.tv_nsec / 1000000;
            SynapticsCopyHwState(hwRet, hw);
            return TRUE;
        default:
            return FALSE;
        }
    }

    return FALSE;
}

static Bool
WSConsAutoDevProbe(InputInfoPtr pInfo, const char *device)
{
    int i;

    if (device && WSConsIsTouchpad(pInfo, device))
        return TRUE;

    if (WSConsIsTouchpad(pInfo, DEFAULT_WSMOUSE_DEV)) {
        xf86IDrvMsg(pInfo, X_PROBED, "auto-dev sets device to %s\n",
            DEFAULT_WSMOUSE_DEV);
        xf86ReplaceStrOption(pInfo->options, "Device", DEFAULT_WSMOUSE_DEV);
        return TRUE;
    }

    return FALSE;
}

static void
WSConsReadDevDimensions(InputInfoPtr pInfo)
{
    SynapticsPrivate *priv = (SynapticsPrivate *)pInfo->private;
    SynapticsParameters *para = &priv->synpara;
    struct wsmouse_calibcoords wsmc;
    int wsmouse_type;

    priv->proto_data = calloc(1, sizeof(struct wsconscomm_proto_data));
    if (priv->proto_data == NULL) {
        xf86IDrvMsg(pInfo, X_ERROR, "failed to allocate protocol data (%s)\n",
            strerror(errno));
        return;
    }

    if (ioctl(pInfo->fd, WSMOUSEIO_GCALIBCOORDS, &wsmc) != 0) {
        free(priv->proto_data);
        priv->proto_data = NULL;
        xf86IDrvMsg(pInfo, X_ERROR, "failed to query axis range (%s)\n",
            strerror(errno));
        return;
    }

    priv->minx = wsmc.minx;
    priv->maxx = wsmc.maxx;
    priv->resx = wsmc.resx;
    xf86IDrvMsg(pInfo, X_PROBED, "x-axis range %d - %d resolution %d\n",
        priv->minx, priv->maxx, priv->resx);

    priv->miny = wsmc.miny;
    priv->maxy = wsmc.maxy;
    priv->resy = wsmc.resy;
    xf86IDrvMsg(pInfo, X_PROBED, "y-axis range %d - %d resolution %d\n",
        priv->miny, priv->maxy, priv->resy);

    priv->minp = 0;
    priv->maxp = 255;

    priv->minw = 0;
    priv->maxw = 15;

    priv->has_pressure = TRUE;
    priv->has_left = TRUE;
    priv->has_right = TRUE;
    priv->has_middle = TRUE;

    if (ioctl(pInfo->fd, WSMOUSEIO_GTYPE, &wsmouse_type) == -1)
        xf86IDrvMsg(pInfo, X_ERROR, "cannot get mouse type\n");

    switch (wsmouse_type) {
    default:
    case WSMOUSE_TYPE_SYNAP_SBTN:
        para->clickpad = TRUE;
        para->has_secondary_buttons = TRUE;
        /* FALLTHROUGH */
    case WSMOUSE_TYPE_SYNAPTICS:
        priv->model = MODEL_SYNAPTICS;
        priv->has_width = TRUE;
        priv->has_double = TRUE;
        priv->has_triple = TRUE;
        break;
    case WSMOUSE_TYPE_ALPS:
        priv->model = MODEL_ALPS;
        priv->has_width = FALSE;
        priv->has_double = FALSE;
        priv->has_triple = FALSE;
        break;
    case WSMOUSE_TYPE_TOUCHPAD:
    	/* Use Elantech settings for the generic type. */
	/* FALLTHROUGH */
    case WSMOUSE_TYPE_ELANTECH:
        priv->model = MODEL_ELANTECH;
        priv->has_width = TRUE;
        priv->has_double = TRUE;
        priv->has_triple = TRUE;
        break;
    }
}

struct SynapticsProtocolOperations wscons_proto_operations = {
    WSConsDeviceOnHook,
    WSConsDeviceOffHook,
    WSConsQueryHardware,
    WSConsReadHwState,
    WSConsAutoDevProbe,
    WSConsReadDevDimensions
};
