/*
 * Linux 2.2 joystick interface
 *
 * Copyright 1997 Andreas Mohr
 * Copyright 2000 Wolfgang Schwotzer
 * Copyright 2002 TransGaming Technologies Inc.
 *
 * From inspecting the kernel sources, it seems that it is
 * not possible to detect unplugged joysticks, the driver
 * will always report the last successful readout.
 */

#include "config.h"

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#ifdef HAVE_SYS_IOCTL_H
# include <sys/ioctl.h>
#endif
#ifdef HAVE_LINUX_JOYSTICK_H
#include <linux/joystick.h>
#endif
#ifdef HAVE_SYS_ERRNO_H
#include <sys/errno.h>
#endif

#include "windef.h"
#include "winbase.h"
#include "wingdi.h"
#include "winuser.h"
#include "mmddk.h"
#include "dinput.h"
#include "wine/debug.h"

#include "joy_drv.h"

WINE_DEFAULT_DEBUG_CHANNEL(joystick);

extern void LINUXINPUT_DevName(WINE_JOYSTICK* jstck);

#if defined(HAVE_LINUX_JOYSTICK_H) && defined(HAVE_LINUX_22_JOYSTICK_API)

/* most defs from SDL 1.2 */
static struct {
    char *name;
    int axes;
    int hats;
    int balls;
} special[] = {
    {"MadCatz Panther XL", 3, 2, 1}, /* We don't handle a rudder (axis 8) */
    {"SideWinder Precision Pro", 4, 1, 0},
    {"SideWinder 3D Pro", 4, 1, 0},
    {"Microsoft SideWinder 3D Pro", 4, 1, 0},
    {"Microsoft SideWinder Dual Strike USB version 1.0", 2, 1, 0},
    {"WingMan Interceptor", 3, 3, 0},
    /* WingMan Extreme Analog - not recognized by default
    {"Analog 3-axis 4-button joystick", 2, 1, 0},
    */
    {"WingMan Extreme Digital 3D", 4, 1, 0},
    {"Analog 2-axis 4-button 1-hat FCS joystick", 2, 1, 0},
    {"Microsoft SideWinder Precision 2 Joystick", 4, 1, 0},
    {"Logitech Inc. WingMan Extreme Digital 3D", 4, 1, 0},
    {"Saitek Saitek X45", 6, 1, 0},
    {"Gravis Destroyer AfterShock", 3, 1, 0},
    {NULL}
};

typedef struct {
    int fd;
    int *hat;
    DIDATAFORMAT df;
} JOY_DATA;

typedef struct {
    int fd;
    UINT timer;
    JOYINFOEX cur;
    int hatx, haty;
    CRITICAL_SECTION cs;
} MM_JOY_DATA;

static DWORD analog_pov[5] = {
    JOY_POVFORWARD,
    JOY_POVRIGHT,
    JOY_POVBACKWARD,
    JOY_POVLEFT,
    JOY_POVCENTERED
};

void LINUX22_DevName(WINE_JOYSTICK* jstck)
{
    jstck->devIntf = jstck->joyIntf;
    sprintf(jstck->devname, "/dev/js%d", jstck->devIntf);
}

LONG LINUX22_GetDevCaps(WINE_JOYSTICK* jstck)
{
    LPJOYCAPSA lpCaps = &jstck->caps;
    LPDIDEVCAPS lpDICaps = &jstck->dicaps;
    LPDIDEVICEINSTANCEA lpDIInfo = &jstck->diinfo;
    JMAP	*jmap, *cmap;
    int		dev;
    char	nrOfAxes;
    char	nrOfButtons;
    char	identString[MAX_PATH];
    char	axes, hats, balls;
    int		driverVersion;
    unsigned	u;
#ifdef JSIOCGAXMAP
    BYTE	axmap[ABS_MAX+1];
    int		has_axmap;
#endif

    if ((dev = open(jstck->devname, O_RDONLY | O_NONBLOCK)) < 0) {
	if (errno == ENODEV) return JOYERR_UNPLUGGED;
	else if (errno == ENOENT) return MMSYSERR_NODRIVER;
	else if (errno == EBUSY) return MMSYSERR_ALLOCATED;
	else return MMSYSERR_ERROR;
    }
    if (ioctl(dev, JSIOCGVERSION, &driverVersion) < 0) {
	close(dev);
	return MMSYSERR_NODRIVER;
    }
    if (ioctl(dev, JSIOCGAXES, &nrOfAxes) < 0) nrOfAxes = 2;
    if (ioctl(dev, JSIOCGBUTTONS, &nrOfButtons) < 0) nrOfButtons = 2;
    /* JSIOCGNAME doesn't nul terminate if the buffer is too small so
     * do it here just in case. */
    identString[sizeof(identString)-1] = 0;
    if (ioctl(dev, JSIOCGNAME(sizeof(identString)-1), &identString) < 0) strcpy(identString, "");
#ifdef JSIOCGAXMAP
    has_axmap = ioctl(dev, JSIOCGAXMAP, &axmap);
#endif
    TRACE("Driver: 0x%06x, Name: %s, #Axes: %d, #Buttons: %d\n",
	  driverVersion, identString, nrOfAxes, nrOfButtons);
    close(dev);

    /* get user-defined axis mapping */
    jmap = JSTCK_get_joy_map(identString);
    if (jmap) {
	axes = 0;
	hats = 0;
	balls = 0;
	for (cmap=jmap,u=0; *cmap != JMAP_END; cmap++,u++) {
	    if (*cmap == JMAP_BALL) balls++;
	    else if (*cmap == JMAP_HAT) hats++;
	    else if (!hats && !balls) axes++;
	}
	goto no_axmap;
    }

    /* get hardcoded axis mapping */
    axes = nrOfAxes;
    hats = 0;
    balls = 0;
    for (u=0; special[u].name; u++) {
	if (strcmp(identString, special[u].name) == 0) {
	    axes = special[u].axes;
	    hats = special[u].hats;
	    balls = special[u].balls;
	    goto no_axmap;
	}
    }

#ifdef JSIOCGAXMAP
    if (has_axmap >= 0) {
	/* in the native 2.4 input API, hats are directly detectable;
	 * if we're using the 2.2 joystick interface on Linux 2.4,
	 * we can read the axis mapping to reap similar benefits,
	 * and find out which axes correspond to hats */
	char old_axes = axes;
	for (u=0; u<old_axes; u++) {
	    if (axmap[u] >= ABS_HAT0X && axmap[u] < ABS_HAT3Y &&
		!((axmap[u] - ABS_HAT0X) & 1) &&
		axmap[u+1] == (axmap[u]+1)) {
		hats++;
		/* truncate axes where the hats begin,
		 * we currently don't support axes after hats */
		if (axes > u) axes = u;
	    }
	}
    }
#endif
no_axmap:

    TRACE("detected: axes=%d, hats=%d\n", axes, hats);

    /* FIXME: handle "balls" somehow? */

    lpCaps->wMid = MM_MICROSOFT;
    lpCaps->wPid = MM_PC_JOYSTICK;
    strncpy(lpCaps->szPname, identString, MAXPNAMELEN);
    lpCaps->szPname[MAXPNAMELEN-1] = '\0';
    lpCaps->wXmin = 0;
    lpCaps->wXmax = 0xFFFF;
    lpCaps->wYmin = 0;
    lpCaps->wYmax = 0xFFFF;
    lpCaps->wZmin = 0;
    lpCaps->wZmax = (axes >= 3) ? 0xFFFF : 0;
    lpCaps->wNumButtons = nrOfButtons;
    lpCaps->wRmin = 0;
    lpCaps->wRmax = axes >= 4 ? 0xFFFF : 0;
    lpCaps->wUmin = 0;
    lpCaps->wUmax = axes >= 5 ? 0xFFFF : 0;
    lpCaps->wVmin = 0;
    lpCaps->wVmax = axes >= 6 ? 0xFFFF : 0;
    lpCaps->wMaxAxes = 6; /* same as MS Joystick Driver */
    lpCaps->wNumAxes = axes > 6 ? 6 : axes; /* nr of axes in use */
    lpCaps->wMaxButtons = 32; /* same as MS Joystick Driver */
    strcpy(lpCaps->szRegKey, "");
    strcpy(lpCaps->szOEMVxD, "");
    lpCaps->wCaps = 0;
    switch(axes) {
    case 6: lpCaps->wCaps |= JOYCAPS_HASV;
    case 5: lpCaps->wCaps |= JOYCAPS_HASU;
    case 4: lpCaps->wCaps |= JOYCAPS_HASR;
    case 3: lpCaps->wCaps |= JOYCAPS_HASZ;
    }
    if (hats) {
	lpCaps->wCaps |= JOYCAPS_HASPOV;
	if (nrOfAxes == axes + hats) {
	    /* Linux maps the hat to 1 axis, which probably means it's an analog 4-dir hat */
	    lpCaps->wCaps |= JOYCAPS_POV4DIR;
	} else {
	    /* Linux maps the hat(s) to 2 axes */
	    lpCaps->wCaps |= JOYCAPS_POVCTS;
	}
    }

    memset(lpDICaps, 0, sizeof(DIDEVCAPS));
    lpDICaps->dwSize = sizeof(DIDEVCAPS);
    lpDICaps->dwFlags = DIDC_ATTACHED;
    lpDICaps->dwDevType = DIDEVTYPE_JOYSTICK;
    lpDICaps->dwAxes = axes;
    lpDICaps->dwButtons = nrOfButtons;
    lpDICaps->dwPOVs = hats;

    memset(lpDIInfo, 0, sizeof(DIDEVICEINSTANCEA));
    /* the guidInstance is supposed to identify the joystick,
     * this is enough for that purpose */
    lpDIInfo->guidInstance.Data1 = jstck->joyIntf;
    memcpy(&lpDIInfo->guidProduct, &GUID_Joystick, sizeof(GUID));
    lpDIInfo->dwDevType = DIDEVTYPE_JOYSTICK;
    sprintf(lpDIInfo->tszInstanceName, "Joystick %d", jstck->joyIntf);
    strncpy(lpDIInfo->tszProductName, identString, MAX_PATH);
    lpDIInfo->tszProductName[MAX_PATH-1] = '\0';

    JSTCK_free_joy_map(jmap);

    return JOYERR_NOERROR;
}

void CALLBACK LINUX22_TimeOut(UINT wTimerID, UINT wMessage, DWORD dwUser, DWORD dw1, DWORD dw2)
{
    WINE_JOYSTICK* jstck = (WINE_JOYSTICK*)dwUser;
    MM_JOY_DATA* pdata = jstck->minternal;
    if (!pdata) return;
    EnterCriticalSection(&pdata->cs);
    if (pdata->timer == wTimerID) {
	int dev = pdata->fd;
	pdata->fd = 0;
	pdata->timer = 0;
	close(dev);
	TRACE("closing fd=%d\n", dev);
    }
    LeaveCriticalSection(&pdata->cs);
    /* freeing minternal from in here is not safe, let's just leave it there. */
}

LONG LINUX22_GetPosEx(WINE_JOYSTICK* jstck, LPJOYINFOEX lpInfo)
{
    int			dev;
    struct js_event 	ev;
    MM_JOY_DATA*	pdata;

    pdata = jstck->minternal;
    if (pdata) {
	UINT timer;
	EnterCriticalSection(&pdata->cs);
	timer = pdata->timer;
	dev = pdata->fd;
	pdata->timer = 0;
	if (timer) {
	    timeKillEvent(timer);
	    TRACE("killed timer %d\n", timer);
	}
	LeaveCriticalSection(&pdata->cs);
    } else {
	dev = 0;
    }

    if (!dev && ((dev = open(jstck->devname, O_RDONLY | O_NONBLOCK)) < 0)) {
	if (errno == ENODEV) return JOYERR_UNPLUGGED;
	else if (errno == ENOENT) return MMSYSERR_NODRIVER;
	else if (errno == EBUSY) return MMSYSERR_ALLOCATED;
	else return MMSYSERR_ERROR;
    }

    if (!pdata) {
	pdata = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(MM_JOY_DATA));
	pdata->cur.dwPOV = JOY_POVCENTERED;
	InitializeCriticalSection(&pdata->cs);
	jstck->minternal = pdata;
    }

    TRACE("fd=%d\n", dev);

    /* After opening the device, its state is returned
       with JS_EVENT_INIT flag */
    while ((read(dev, &ev, sizeof(struct js_event))) > 0) {
	if (ev.type & JS_EVENT_AXIS) {
	    if (ev.number < jstck->dicaps.dwAxes) {
		switch (ev.number) {
		case 0:
		    pdata->cur.dwXpos = ev.value + 32767;
		    break;
		case 1:
		    pdata->cur.dwYpos = ev.value + 32767;
		    break;
		case 2:
		    pdata->cur.dwZpos = ev.value + 32767;
		    break;
		case 3:
		    pdata->cur.dwRpos = ev.value + 32767;
		    break;
		case 4:
		    pdata->cur.dwUpos = ev.value + 32767;
		    break;
		case 5:
		    pdata->cur.dwVpos = ev.value + 32767;
		    break;
		default:
		    break;
		}
	    }
	    else {
		ev.number -= jstck->dicaps.dwAxes;
		if (jstck->caps.wCaps & JOYCAPS_POV4DIR &&
		    ev.number < 1) {
		    /* analog hat */
		    pdata->cur.dwPOV = analog_pov[(ev.value/16000)+2];
		}
		else if (jstck->caps.wCaps & JOYCAPS_POVCTS) {
		    if (ev.number == 0) pdata->hatx = ev.value;
		    else if (ev.number == 1) pdata->haty = ev.value;
		}
	    }
	} else if (ev.type & JS_EVENT_BUTTON) {
	    if (ev.value) {
		pdata->cur.dwButtons |= (1 << ev.number);
		/* FIXME: what to do for this field when
		 * multiple buttons are depressed ?
		 */
		pdata->cur.dwButtonNumber = ev.number + 1;
	    } else {
		pdata->cur.dwButtons &= ~(1 << ev.number);
	    }
	}
    }
    /* EAGAIN is returned when the queue is empty */
    if (errno != EAGAIN) {
	/* FIXME: error should not be ignored */
	ERR("Error while reading joystick state (%s)\n", strerror(errno));
    }

    if (lpInfo->dwFlags & JOY_RETURNX)
	lpInfo->dwXpos   = pdata->cur.dwXpos;
    if (lpInfo->dwFlags & JOY_RETURNY)
	lpInfo->dwYpos   = pdata->cur.dwYpos;
    if (lpInfo->dwFlags & JOY_RETURNZ)
	lpInfo->dwZpos   = pdata->cur.dwZpos;
    if (lpInfo->dwFlags & JOY_RETURNR)
	lpInfo->dwRpos   = pdata->cur.dwRpos;
    if (lpInfo->dwFlags & JOY_RETURNU)
	lpInfo->dwUpos   = pdata->cur.dwUpos;
    if (lpInfo->dwFlags & JOY_RETURNV)
	lpInfo->dwVpos   = pdata->cur.dwVpos;

    if (jstck->caps.wCaps & JOYCAPS_POVCTS) {
	TRACE("hatx=%d, haty=%d\n", pdata->hatx, pdata->haty);
	if (lpInfo->dwFlags & JOY_RETURNPOVCTS)
	   lpInfo->dwPOV = JSTCK_calc_povcts(pdata->hatx, pdata->haty);
	else if (lpInfo->dwFlags & JOY_RETURNPOV)
	   lpInfo->dwPOV = JSTCK_calc_pov4(pdata->hatx, pdata->haty);
    }
    else if (lpInfo->dwFlags & (JOY_RETURNPOV | JOY_RETURNPOVCTS))
	lpInfo->dwPOV = pdata->cur.dwPOV;

    if (lpInfo->dwFlags & JOY_RETURNBUTTONS) {
	lpInfo->dwButtons = pdata->cur.dwButtons;
	lpInfo->dwButtonNumber = pdata->cur.dwButtonNumber;
    }

    /* some drivers for digital devices don't like to be closed,
     * and data may be lost when it's closed and immediately reopened.
     * Let's only close if no further read attempts arrive within a second. */
    EnterCriticalSection(&pdata->cs);
    pdata->fd = dev;
    pdata->timer = timeSetEvent(1000, 0, LINUX22_TimeOut, (DWORD)jstck, TIME_ONESHOT | TIME_CALLBACK_FUNCTION);
    TRACE("created timer %d\n", pdata->timer);
    LeaveCriticalSection(&pdata->cs);

    TRACE("x: %ld, y: %ld, z: %ld, r: %ld, u: %ld, v: %ld, pov: %ld, buttons: 0x%04x, flags: 0x%04x\n",
	  lpInfo->dwXpos, lpInfo->dwYpos, lpInfo->dwZpos,
	  lpInfo->dwRpos, lpInfo->dwUpos, lpInfo->dwVpos,
	  lpInfo->dwPOV,
	  (unsigned int)lpInfo->dwButtons,
	  (unsigned int)lpInfo->dwFlags);

    return JOYERR_NOERROR;
}

#define LINUX22_AxisGuids 4
static REFGUID LINUX22_AxisGuid[LINUX22_AxisGuids] = {
    &GUID_XAxis,
    &GUID_YAxis,
    &GUID_ZAxis,
    &GUID_RzAxis
};

HRESULT LINUX22_OpenDIDev(WINE_JOYSTICK* jstck, DWORD dwVersion)
{
    int dev;
    JOY_DATA *pdata;
    JMAP *jmap, *cmap;
    LPDIOBJECTDATAFORMAT obj;
    unsigned u, o;

    TRACE("(%p,%lx)\n", jstck, dwVersion);
    if ((dev = open(jstck->devname, O_RDONLY | O_NONBLOCK)) < 0) {
	if (errno == ENODEV) return DIERR_UNPLUGGED;
	else if (errno == ENOENT) return DIERR_DEVICENOTREG;
	else if (errno == EBUSY) return DIERR_ACQUIRED;
	else return DIERR_GENERIC;
    }
    pdata = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(JOY_DATA));
    pdata->fd = dev;
    if (jstck->caps.wCaps & JOYCAPS_POVCTS)
	pdata->hat = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(int)*jstck->dicaps.dwPOVs*2);

    /* get user-defined axis mapping */
    jmap = JSTCK_get_joy_map(jstck->diinfo.tszProductName);

    /* build data format */
    pdata->df.dwNumObjs = jstck->dicaps.dwAxes + jstck->dicaps.dwPOVs + jstck->dicaps.dwButtons;
    pdata->df.rgodf = HeapAlloc(GetProcessHeap(), 0, sizeof(DIOBJECTDATAFORMAT) * pdata->df.dwNumObjs);
    obj = pdata->df.rgodf;
    o = 0;
    /* add axes */
    cmap = jmap;
    for (u=0; u<jstck->dicaps.dwAxes; u++,obj++) {
	/* skip joy map hats since they don't apply here */
	while (cmap && (*cmap == JMAP_HAT || *cmap == JMAP_BALL)) cmap++;
	/* check for end of joy map */
	if (cmap && (*cmap == JMAP_END)) cmap = NULL;
	
	/* assign GUID to axis */
	if (cmap) obj->pguid = JSTCK_get_map_guid(*cmap++);
	else if (u >= LINUX22_AxisGuids)
	    obj->pguid = &GUID_Slider;
	else
	    obj->pguid = LINUX22_AxisGuid[u];
	obj->dwOfs = o;
	obj->dwType = DIDFT_ABSAXIS | DIDFT_MAKEINSTANCE(u);
	obj->dwFlags = DIDOI_ASPECTPOSITION;
	o += sizeof(LONG);
    }
    /* add hats */
    for (u=0; u<jstck->dicaps.dwPOVs; u++,obj++) {
	obj->pguid = &GUID_POV;
	obj->dwOfs = o;
	obj->dwType = DIDFT_POV | DIDFT_MAKEINSTANCE(u);
	obj->dwFlags = 0;
	o += sizeof(DWORD);
    }
    /* add buttons */
    for (u=0; u<jstck->dicaps.dwButtons; u++,obj++) {
	obj->pguid = &GUID_Button;
	obj->dwOfs = o;
	obj->dwType = DIDFT_PSHBUTTON | DIDFT_MAKEINSTANCE(u);
	obj->dwFlags = 0;
	o += sizeof(BYTE);
    }
    pdata->df.dwDataSize = (o+3)&~3; /* dword-aligned data size */

    JSTCK_free_joy_map(jmap);

    jstck->internal = pdata;
    return DI_OK;
}

HRESULT LINUX22_CloseDIDev(WINE_JOYSTICK* jstck)
{
    JOY_DATA *pdata = jstck->internal;
    TRACE("(%p)\n", jstck);
    if (!pdata) return DI_OK;
    close(pdata->fd);
    HeapFree(GetProcessHeap(), 0, pdata->df.rgodf);
    if (pdata->hat) HeapFree(GetProcessHeap(), 0, pdata->hat);
    HeapFree(GetProcessHeap(), 0, pdata);
    jstck->internal = 0;
    return DI_OK;
}

HRESULT LINUX22_GetDIDataFormat(WINE_JOYSTICK* jstck, LPDIDATAFORMAT* pdf)
{
    JOY_DATA *pdata = jstck->internal;
    TRACE("(%p,%p)\n", jstck, pdf);
    *pdf = &pdata->df;
    return DI_OK;
}

HRESULT LINUX22_GetDIData(WINE_JOYSTICK* jstck, BOOL req, DWORD* dwObj, DWORD* dwData, DWORD* dwTimeStamp)
{
    JOY_DATA *pdata = jstck->internal;
    struct js_event ev;
    DWORD Time;

    while (TRUE) {
	if (read(pdata->fd, &ev, sizeof(struct js_event)) < 0)
	    return S_FALSE;

	/* FIXME: derive timestamp from ev.time */
	Time = GetTickCount();

	if (ev.type & JS_EVENT_BUTTON) {
	    TRACE("button %d -> %d\n", ev.number, ev.value);
	    *dwObj = jstck->dicaps.dwAxes + jstck->dicaps.dwPOVs + ev.number;
	    *dwData = ev.value ? 0x80 : 0x00;
	    *dwTimeStamp = Time;
	    return DI_OK;
	}
	if (ev.type & JS_EVENT_AXIS) {
	    TRACE("axis %d -> %d\n", ev.number, ev.value);
	    if (ev.number < jstck->dicaps.dwAxes) {
		*dwObj = ev.number;
		*dwData = ev.value + 32767;
		*dwTimeStamp = Time;
		return DI_OK;
	    }
	    ev.number -= jstck->dicaps.dwAxes;
	    if (jstck->caps.wCaps & JOYCAPS_POV4DIR) {
		if (ev.number < jstck->dicaps.dwPOVs) {
		    /* analog hat */
		    *dwObj = jstck->dicaps.dwAxes + ev.number;
		    *dwData = analog_pov[(ev.value/16000)+2];
		    *dwTimeStamp = Time;
		    return DI_OK;
		}
		ev.number -= jstck->dicaps.dwPOVs;
	    }
	    else if (jstck->caps.wCaps & JOYCAPS_POVCTS) {
		if (ev.number < jstck->dicaps.dwPOVs*2) {
		    unsigned u = ev.number / 2;
		    pdata->hat[ev.number] = ev.value;
		    *dwObj = jstck->dicaps.dwAxes + u;
		    *dwData = JSTCK_calc_povcts(pdata->hat[u*2], pdata->hat[u*2+1]);
		    *dwTimeStamp = Time;
		    return DI_OK;
		}
		ev.number -= jstck->dicaps.dwPOVs*2;
	    }
	}
    }
}

WINE_JOYSTICK_DRV jdrv_linux22 = {
    LINUX22_DevName, LINUX22_GetDevCaps, LINUX22_GetPosEx,
    NULL, LINUX22_OpenDIDev, LINUX22_CloseDIDev,
    LINUX22_GetDIDataFormat, LINUX22_GetDIData
};

#ifdef HAVE_CORRECT_LINUXINPUT_H

WINE_JOYSTICK_DRV jdrv_linuxinput22 = {
    LINUXINPUT_DevName, LINUX22_GetDevCaps, LINUX22_GetPosEx,
    NULL, LINUX22_OpenDIDev, LINUX22_CloseDIDev,
    LINUX22_GetDIDataFormat, LINUX22_GetDIData
};

#endif

#endif
