/*
* Copyright 1990,91 by Thomas Roell, Dinkelscherben, Germany.
* Copyright 1993 by David Dawes <dawes@xfree86.org>
* Copyright 2002 by SuSE Linux AG, Author: Egbert Eich
* Copyright 1994-2002 by The XFree86 Project, Inc.
* Copyright 2002 by Paul Elliott
* (Ported from xf86-input-mouse, above copyrights taken from there)
* Copyright 2008 by Chris Salch
* Copyright © 2008 Red Hat, Inc.
*
* Permission to use, copy, modify, distribute, and sell this software
* and its documentation for any purpose is hereby granted without
* fee, provided that the above copyright notice appear in all copies
* and that both that copyright notice and this permission notice
* appear in supporting documentation, and that the name of the authors
* not be used in advertising or publicity pertaining to distribution of the
* software without specific, written prior permission.  The authors make no
* representations about the suitability of this software for any
* purpose.  It is provided "as is" without express or implied
* warranty.
*
* THE AUTHORS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
* INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
* NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY SPECIAL, 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.
*
*/

/* Mouse wheel emulation code. */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "evdev.h"

#include <X11/Xatom.h>
#include <xf86.h>
#include <xf86Xinput.h>
#include <exevents.h>

#include <evdev-properties.h>

#define WHEEL_NOT_CONFIGURED 0

static Atom prop_wheel_emu      = 0;
static Atom prop_wheel_axismap  = 0;
static Atom prop_wheel_inertia  = 0;
static Atom prop_wheel_timeout  = 0;
static Atom prop_wheel_button   = 0;

/* Local Funciton Prototypes */
static int EvdevWheelEmuInertia(InputInfoPtr pInfo, WheelAxisPtr axis, int value);

/* Filter mouse button events */
BOOL
EvdevWheelEmuFilterButton(InputInfoPtr pInfo, unsigned int button, int value)
{
    EvdevPtr pEvdev = (EvdevPtr)pInfo->private;
    int ms;

    /* Has wheel emulation been configured to be enabled? */
    if (!pEvdev->emulateWheel.enabled)
	return FALSE;

    /* Check for EmulateWheelButton */
    if (pEvdev->emulateWheel.button == button) {
	pEvdev->emulateWheel.button_state = value;

        if (value)
            /* Start the timer when the button is pressed */
            pEvdev->emulateWheel.expires = pEvdev->emulateWheel.timeout +
                                           GetTimeInMillis();
        else {
            ms = pEvdev->emulateWheel.expires - GetTimeInMillis();
            if (ms > 0) {
                /*
                 * If the button is released early enough emit the button
                 * press/release events
                 */
                EvdevQueueButtonClicks(pInfo, button, 1);
            }
        }

	return TRUE;
    }

    /* Don't care about this event */
    return FALSE;
}

/* Filter mouse wheel events */
BOOL
EvdevWheelEmuFilterMotion(InputInfoPtr pInfo, struct input_event *pEv)
{
    EvdevPtr pEvdev = (EvdevPtr)pInfo->private;
    WheelAxisPtr pAxis = NULL;
    int value = pEv->value;

    /* Has wheel emulation been configured to be enabled? */
    if (!pEvdev->emulateWheel.enabled)
	return FALSE;

    /* Handle our motion events if the emuWheel button is pressed
     * wheel button of 0 means always emulate wheel.
     */
    if (pEvdev->emulateWheel.button_state || !pEvdev->emulateWheel.button) {
        /* Just return if the timeout hasn't expired yet */
        if (pEvdev->emulateWheel.button)
        {
            int ms = pEvdev->emulateWheel.expires - GetTimeInMillis();
            if (ms > 0)
                return TRUE;
        }

	if(pEv->type == EV_ABS) {
	    int axis = pEvdev->abs_axis_map[pEv->code];
	    int oldValue;

	    if (axis > -1 && valuator_mask_fetch(pEvdev->old_vals, axis, &oldValue)) {
		valuator_mask_set(pEvdev->abs_vals, axis, value);
		value -= oldValue; /* make value into a differential measurement */
	    } else
                value = 0; /* avoid a jump on the first touch */
	}

	switch(pEv->code) {

	/* ABS_X has the same value as REL_X, so this case catches both */
	case REL_X:
	    pAxis = &(pEvdev->emulateWheel.X);
	    break;

	/* ABS_Y has the same value as REL_Y, so this case catches both */
	case REL_Y:
	    pAxis = &(pEvdev->emulateWheel.Y);
	    break;

	default:
	    break;
	}

	/* If we found REL_X, REL_Y, ABS_X or ABS_Y then emulate a mouse
	   wheel.
	 */
	if (pAxis)
	    EvdevWheelEmuInertia(pInfo, pAxis, value);

	/* Eat motion events while emulateWheel button pressed. */
	return TRUE;
    }

    return FALSE;
}

/* Simulate inertia for our emulated mouse wheel.
   Returns the number of wheel events generated.
 */
static int
EvdevWheelEmuInertia(InputInfoPtr pInfo, WheelAxisPtr axis, int value)
{
    EvdevPtr pEvdev = (EvdevPtr)pInfo->private;
    int button;
    int inertia;
    int rc = 0;

    /* if this axis has not been configured, just eat the motion */
    if (!axis->up_button)
	return rc;

    axis->traveled_distance += value;

    if (axis->traveled_distance < 0) {
	button = axis->up_button;
	inertia = -pEvdev->emulateWheel.inertia;
    } else {
	button = axis->down_button;
	inertia = pEvdev->emulateWheel.inertia;
    }

    /* Produce button press events for wheel motion */
    while(abs(axis->traveled_distance) > pEvdev->emulateWheel.inertia) {
	axis->traveled_distance -= inertia;
	EvdevQueueButtonClicks(pInfo, button, 1);
	rc++;
    }
    return rc;
}

/* Handle button mapping here to avoid code duplication,
returns true if a button mapping was found. */
static BOOL
EvdevWheelEmuHandleButtonMap(InputInfoPtr pInfo, WheelAxisPtr pAxis,
                             const char *axis_name)
{
    EvdevPtr pEvdev = (EvdevPtr)pInfo->private;
    char *option_string;

    pAxis->up_button = WHEEL_NOT_CONFIGURED;

    /* Check to see if there is configuration for this axis */
    option_string = xf86SetStrOption(pInfo->options, axis_name, NULL);
    if (option_string) {
	int up_button = 0;
	int down_button = 0;
	char *msg = NULL;

	if ((sscanf(option_string, "%d %d", &up_button, &down_button) == 2) &&
	    ((up_button > 0) && (up_button <= EVDEV_MAXBUTTONS)) &&
	    ((down_button > 0) && (down_button <= EVDEV_MAXBUTTONS))) {

	    /* Use xstrdup to allocate a string for us */
	    msg = xstrdup("buttons XX and YY");

	    if (msg)
		sprintf(msg, "buttons %d and %d", up_button, down_button);

	    pAxis->up_button = up_button;
	    pAxis->down_button = down_button;

	    /* Update the number of buttons if needed */
	    if (up_button > pEvdev->num_buttons) pEvdev->num_buttons = up_button;
	    if (down_button > pEvdev->num_buttons) pEvdev->num_buttons = down_button;

	} else {
	    xf86IDrvMsg(pInfo, X_WARNING, "Invalid %s value:\"%s\"\n",
		        axis_name, option_string);
	}
	free(option_string);

	/* Clean up and log what happened */
	if (msg) {
	    xf86IDrvMsg(pInfo, X_CONFIG, "%s: %s\n", axis_name, msg);
	    free(msg);
	    return TRUE;
	}
    }
    return FALSE;
}

/* Setup the basic configuration options used by mouse wheel emulation */
void
EvdevWheelEmuPreInit(InputInfoPtr pInfo)
{
    EvdevPtr pEvdev = (EvdevPtr)pInfo->private;
    int wheelButton;
    int inertia;
    int timeout;

    if (xf86SetBoolOption(pInfo->options, "EmulateWheel", FALSE)) {
	pEvdev->emulateWheel.enabled = TRUE;
    } else
        pEvdev->emulateWheel.enabled = FALSE;

    wheelButton = xf86SetIntOption(pInfo->options, "EmulateWheelButton", 4);

    if ((wheelButton < 0) || (wheelButton > EVDEV_MAXBUTTONS)) {
        xf86IDrvMsg(pInfo, X_WARNING, "Invalid EmulateWheelButton value: %d\n",
                    wheelButton);
        xf86IDrvMsg(pInfo, X_WARNING, "Wheel emulation disabled.\n");

        pEvdev->emulateWheel.enabled = FALSE;
    }

    pEvdev->emulateWheel.button = wheelButton;

    inertia = xf86SetIntOption(pInfo->options, "EmulateWheelInertia", 10);

    if (inertia <= 0) {
        xf86IDrvMsg(pInfo, X_WARNING, "Invalid EmulateWheelInertia value: %d\n",
                    inertia);
        xf86IDrvMsg(pInfo, X_WARNING, "Using built-in inertia value.\n");

        inertia = 10;
    }

    pEvdev->emulateWheel.inertia = inertia;

    timeout = xf86SetIntOption(pInfo->options, "EmulateWheelTimeout", 200);

    if (timeout < 0) {
        xf86IDrvMsg(pInfo, X_WARNING, "Invalid EmulateWheelTimeout value: %d\n",
                    timeout);
        xf86IDrvMsg(pInfo, X_WARNING, "Using built-in timeout value.\n");

        timeout = 200;
    }

    pEvdev->emulateWheel.timeout = timeout;

    /* Configure the Y axis or default it */
    if (!EvdevWheelEmuHandleButtonMap(pInfo, &(pEvdev->emulateWheel.Y),
                "YAxisMapping")) {
        /* Default the Y axis to sane values */
        pEvdev->emulateWheel.Y.up_button = 4;
        pEvdev->emulateWheel.Y.down_button = 5;

        /* Simpler to check just the largest value in this case */
        /* XXX: we should post this to the server */
        if (5 > pEvdev->num_buttons)
            pEvdev->num_buttons = 5;

        /* Display default Configuration */
        xf86IDrvMsg(pInfo, X_CONFIG, "YAxisMapping: buttons %d and %d\n",
                    pEvdev->emulateWheel.Y.up_button,
                    pEvdev->emulateWheel.Y.down_button);
    }


    /* This axis should default to an unconfigured state as most people
       are not going to expect a Horizontal wheel. */
    EvdevWheelEmuHandleButtonMap(pInfo, &(pEvdev->emulateWheel.X),
            "XAxisMapping");

    /* Used by the inertia code */
    pEvdev->emulateWheel.X.traveled_distance = 0;
    pEvdev->emulateWheel.Y.traveled_distance = 0;

    xf86IDrvMsg(pInfo, X_CONFIG,
                "EmulateWheelButton: %d, "
                "EmulateWheelInertia: %d, "
                "EmulateWheelTimeout: %d\n",
                pEvdev->emulateWheel.button, inertia, timeout);
}

static int
EvdevWheelEmuSetProperty(DeviceIntPtr dev, Atom atom, XIPropertyValuePtr val,
                         BOOL checkonly)
{
    InputInfoPtr pInfo  = dev->public.devicePrivate;
    EvdevPtr     pEvdev = pInfo->private;

    if (atom == prop_wheel_emu)
    {
        if (val->format != 8 || val->size != 1 || val->type != XA_INTEGER)
            return BadMatch;

        if (!checkonly)
        {
            pEvdev->emulateWheel.enabled = *((BOOL*)val->data);
            /* Don't enable with zero inertia, otherwise we may get stuck in an
             * infinite loop */
            if (pEvdev->emulateWheel.inertia <= 0)
            {
                pEvdev->emulateWheel.inertia = 10;
                /* We may get here before the property is actually enabled */
                if (prop_wheel_inertia)
                    XIChangeDeviceProperty(dev, prop_wheel_inertia, XA_INTEGER,
                            16, PropModeReplace, 1,
                            &pEvdev->emulateWheel.inertia, TRUE);
            }
        }
    }
    else if (atom == prop_wheel_button)
    {
        int bt;

        if (val->format != 8 || val->size != 1 || val->type != XA_INTEGER)
            return BadMatch;

        bt = *((CARD8*)val->data);

        if (bt < 0 || bt >= EVDEV_MAXBUTTONS)
            return BadValue;

        if (!checkonly)
            pEvdev->emulateWheel.button = bt;
    } else if (atom == prop_wheel_axismap)
    {
        if (val->format != 8 || val->size != 4 || val->type != XA_INTEGER)
            return BadMatch;

        if (!checkonly)
        {
            pEvdev->emulateWheel.X.up_button = *((CARD8*)val->data);
            pEvdev->emulateWheel.X.down_button = *(((CARD8*)val->data) + 1);
            pEvdev->emulateWheel.Y.up_button = *(((CARD8*)val->data) + 2);
            pEvdev->emulateWheel.Y.down_button = *(((CARD8*)val->data) + 3);
        }
    } else if (atom == prop_wheel_inertia)
    {
        int inertia;

        if (val->format != 16 || val->size != 1 || val->type != XA_INTEGER)
            return BadMatch;

        inertia = *((CARD16*)val->data);

        if (inertia <= 0)
            return BadValue;

        if (!checkonly)
            pEvdev->emulateWheel.inertia = inertia;
    } else if (atom == prop_wheel_timeout)
    {
        int timeout;

        if (val->format != 16 || val->size != 1 || val->type != XA_INTEGER)
            return BadMatch;

        timeout = *((CARD16*)val->data);

        if (timeout < 0)
            return BadValue;

        if (!checkonly)
            pEvdev->emulateWheel.timeout = timeout;
    }
    return Success;
}

void
EvdevWheelEmuInitProperty(DeviceIntPtr dev)
{
    InputInfoPtr pInfo  = dev->public.devicePrivate;
    EvdevPtr     pEvdev = pInfo->private;
    int          rc     = TRUE;
    char         vals[4];

    if (!dev->button) /* don't init prop for keyboards */
        return;

    prop_wheel_emu = MakeAtom(EVDEV_PROP_WHEEL, strlen(EVDEV_PROP_WHEEL), TRUE);
    rc = XIChangeDeviceProperty(dev, prop_wheel_emu, XA_INTEGER, 8,
                                PropModeReplace, 1,
                                &pEvdev->emulateWheel.enabled, FALSE);
    if (rc != Success)
        return;

    XISetDevicePropertyDeletable(dev, prop_wheel_emu, FALSE);

    vals[0] = pEvdev->emulateWheel.X.up_button;
    vals[1] = pEvdev->emulateWheel.X.down_button;
    vals[2] = pEvdev->emulateWheel.Y.up_button;
    vals[3] = pEvdev->emulateWheel.Y.down_button;

    prop_wheel_axismap = MakeAtom(EVDEV_PROP_WHEEL_AXES, strlen(EVDEV_PROP_WHEEL_AXES), TRUE);
    rc = XIChangeDeviceProperty(dev, prop_wheel_axismap, XA_INTEGER, 8,
                                PropModeReplace, 4, vals, FALSE);

    if (rc != Success)
        return;

    XISetDevicePropertyDeletable(dev, prop_wheel_axismap, FALSE);

    prop_wheel_inertia = MakeAtom(EVDEV_PROP_WHEEL_INERTIA, strlen(EVDEV_PROP_WHEEL_INERTIA), TRUE);
    rc = XIChangeDeviceProperty(dev, prop_wheel_inertia, XA_INTEGER, 16,
                                PropModeReplace, 1,
                                &pEvdev->emulateWheel.inertia, FALSE);
    if (rc != Success)
        return;

    XISetDevicePropertyDeletable(dev, prop_wheel_inertia, FALSE);

    prop_wheel_timeout = MakeAtom(EVDEV_PROP_WHEEL_TIMEOUT, strlen(EVDEV_PROP_WHEEL_TIMEOUT), TRUE);
    rc = XIChangeDeviceProperty(dev, prop_wheel_timeout, XA_INTEGER, 16,
                                PropModeReplace, 1,
                                &pEvdev->emulateWheel.timeout, FALSE);
    if (rc != Success)
        return;

    XISetDevicePropertyDeletable(dev, prop_wheel_timeout, FALSE);

    prop_wheel_button = MakeAtom(EVDEV_PROP_WHEEL_BUTTON, strlen(EVDEV_PROP_WHEEL_BUTTON), TRUE);
    rc = XIChangeDeviceProperty(dev, prop_wheel_button, XA_INTEGER, 8,
                                PropModeReplace, 1,
                                &pEvdev->emulateWheel.button, FALSE);
    if (rc != Success)
        return;

    XISetDevicePropertyDeletable(dev, prop_wheel_button, FALSE);

    XIRegisterPropertyHandler(dev, EvdevWheelEmuSetProperty, NULL, NULL);
}
