/*
 * Copyright 1998 by Kazutaka YOKOTA <yokota@zodiac.mech.utsunomiya-u.ac.jp>
 *
 * 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 Kazutaka YOKOTA not be used in
 * advertising or publicity pertaining to distribution of the software without
 * specific, written prior permission.  Kazutaka YOKOTA makes no representations
 * about the suitability of this software for any purpose.  It is provided
 * "as is" without express or implied warranty.
 *
 * KAZUTAKA YOKOTA DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
 * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
 * EVENT SHALL KAZUTAKA YOKOTA 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.
 */

#define NEED_EVENTS
#include "X.h"
#include "Xproto.h"
#include "inputstr.h"
#include "scrnintstr.h"

#include "xf86.h"
#include "xf86Priv.h"
#include "xf86Xinput.h"
#include "xf86_OSproc.h"
#include "xf86OSmouse.h"
#include "xf86_ansic.h"
#include "mouse.h"
#include "mousePriv.h"

/* serial PnP ID string */
typedef struct {
    int revision;	/* PnP revision, 100 for 1.00 */
    char *eisaid;	/* EISA ID including mfr ID and product ID */
    char *serial;	/* serial No, optional */
    char *class;	/* device class, optional */
    char *compat;	/* list of compatible drivers, optional */
    char *description;	/* product description, optional */
    int neisaid;	/* length of the above fields... */
    int nserial;
    int nclass;
    int ncompat;
    int ndescription;
} pnpid_t;

/* symbol table entry */
typedef struct {
    char *name;
    int val;
} symtab_t;

/* PnP EISA/product IDs */
static symtab_t pnpprod[] = {
    { "KML0001",  PROT_THINKING },	/* Kensington ThinkingMouse */
    { "MSH0001",  PROT_IMSERIAL },	/* MS IntelliMouse */
    { "MSH0004",  PROT_IMSERIAL },	/* MS IntelliMouse TrackBall */
    { "KYEEZ00",  PROT_MS },		/* Genius EZScroll */
    { "KYE0001",  PROT_MS },		/* Genius PnP Mouse */
    { "KYE0002",  PROT_MS },		/* MouseSystem (Genius?) SmartScroll */
    { "KYE0003",  PROT_IMSERIAL },	/* Genius NetMouse */
    { "LGI800C",  PROT_IMSERIAL },	/* Logitech MouseMan (4 button model) */
    { "LGI8033",  PROT_IMSERIAL },	/* Logitech Cordless MouseMan Wheel */
    { "LGI8050",  PROT_IMSERIAL },	/* Logitech MouseMan+ */
    { "LGI8051",  PROT_IMSERIAL },	/* Logitech FirstMouse+ */
    { "LGI8001",  PROT_LOGIMAN },	/* Logitech serial */
    { "A4W0005",  PROT_IMSERIAL },	/* A4 Tech 4D/4D+ Mouse */
    { "PEC9802",  PROT_IMSERIAL },	/* 8D Scroll Mouse */

    { "PNP0F00",  PROT_BM },		/* MS bus */
    { "PNP0F01",  PROT_MS },		/* MS serial */
    { "PNP0F02",  PROT_BM },		/* MS InPort */
    { "PNP0F03",  PROT_PS2 },		/* MS PS/2 */
    /*
     * EzScroll returns PNP0F04 in the compatible device field; but it
     * doesn't look compatible... XXX
     */
    { "PNP0F04",  PROT_MSC },		/* MouseSystems */
    { "PNP0F05",  PROT_MSC },		/* MouseSystems */
#ifdef notyet
    { "PNP0F06",  PROT_??? },		/* Genius Mouse */
    { "PNP0F07",  PROT_??? },		/* Genius Mouse */
#endif
    { "PNP0F08",  PROT_LOGIMAN },	/* Logitech serial */
    { "PNP0F09",  PROT_MS },		/* MS BallPoint serial */
    { "PNP0F0A",  PROT_MS },		/* MS PnP serial */
    { "PNP0F0B",  PROT_MS },		/* MS PnP BallPoint serial */
    { "PNP0F0C",  PROT_MS },		/* MS serial compatible */
    { "PNP0F0D",  PROT_BM },		/* MS InPort compatible */
    { "PNP0F0E",  PROT_PS2 },		/* MS PS/2 compatible */
    { "PNP0F0F",  PROT_MS },		/* MS BallPoint compatible */
#ifdef notyet
    { "PNP0F10",  PROT_??? },		/* TI QuickPort */
#endif
    { "PNP0F11",  PROT_BM },		/* MS bus compatible */
    { "PNP0F12",  PROT_PS2 },		/* Logitech PS/2 */
    { "PNP0F13",  PROT_PS2 },		/* PS/2 */
#ifdef notyet
    { "PNP0F14",  PROT_??? },		/* MS Kids Mouse */
#endif
    { "PNP0F15",  PROT_BM },		/* Logitech bus */
#ifdef notyet
    { "PNP0F16",  PROT_??? },		/* Logitech SWIFT */
#endif
    { "PNP0F17",  PROT_LOGIMAN },	/* Logitech serial compat */
    { "PNP0F18",  PROT_BM },		/* Logitech bus compatible */
    { "PNP0F19",  PROT_PS2 },		/* Logitech PS/2 compatible */
#ifdef notyet
    { "PNP0F1A",  PROT_??? },		/* Logitech SWIFT compatible */
    { "PNP0F1B",  PROT_??? },		/* HP Omnibook */
    { "PNP0F1C",  PROT_??? },		/* Compaq LTE TrackBall PS/2 */
    { "PNP0F1D",  PROT_??? },		/* Compaq LTE TrackBall serial */
    { "PNP0F1E",  PROT_??? },		/* MS Kids Trackball */
#endif
    { NULL,	  -1 },
};

static const char *pnpSerial[] = {
	"BaudRate",	"1200",
	"DataBits",	"7",
	"StopBits",	"1",
	"Parity",	"None",
	"FlowControl",	"None",
	"VTime",	"0",
	"VMin",		"1",
	NULL
};

static int pnpgets(InputInfoPtr, char *);
static int pnpparse(InputInfoPtr, pnpid_t *, char *, int);
static symtab_t *pnpproto(pnpid_t *);
static symtab_t *gettoken(symtab_t *, char *, int);

int
MouseGetPnpProtocol(InputInfoPtr pInfo)
{
    char buf[256];	/* PnP ID string may be up to 256 bytes long */
    pnpid_t pnpid;
    symtab_t *t;
    int len;

    if (((len = pnpgets(pInfo, buf)) <= 0) ||
	!pnpparse(pInfo, &pnpid, buf, len))
	return PROT_UNKNOWN;
    if ((t = pnpproto(&pnpid)) == NULL)
	return PROT_UNKNOWN;
    xf86MsgVerb(X_INFO, 2, "%s: PnP-detected protocol ID: %d\n",
		pInfo->name, t->val);
    return (t->val);
}

/*
 * Try to elicit a PnP ID as described in
 * Microsoft, Hayes: "Plug and Play External COM Device Specification,
 * rev 1.00", 1995.
 *
 * The routine does not fully implement the COM Enumerator as per Section
 * 2.1 of the document.  In particular, we don't have idle state in which
 * the driver software monitors the com port for dynamic connection or
 * removal of a device at the port, because `moused' simply quits if no
 * device is found.
 *
 * In addition, as PnP COM device enumeration procedure slightly has
 * changed since its first publication, devices which follow earlier
 * revisions of the above spec. may fail to respond if the rev 1.0
 * procedure is used. XXX
 */
static int
pnpgets(InputInfoPtr pInfo, char *buf)
{
    int i;
    char c;
    pointer pnpOpts;

#if 0
    /*
     * This is the procedure described in rev 1.0 of PnP COM device spec.
     * Unfortunately, some devices which comform to earlier revisions of
     * the spec gets confused and do not return the ID string...
     */

    /* port initialization (2.1.2) */
    if ((i = xf86GetSerialModemState(pInfo->fd)) == -1)
	return 0;
    i |= XF86_M_DTR;		/* DTR = 1 */
    i &= ~XF86_M_RTS;		/* RTS = 0 */
    if (xf86SetSerialModemState(pInfo->fd, i) == -1)
	goto disconnect_idle;
    usleep(200000);
    if ((i = xf86GetSerialModemState(pInfo->fd)) == -1 ||
	(i & XF86_M_DSR) == 0)
	goto disconnect_idle;

    /* port setup, 1st phase (2.1.3) */
    pnpOpts = xf86OptionListCreate(pnpSerial, -1, 1);
    xf86SetSerial(pInfo->fd, pnpOpts);
    i = TIOCM_DTR | TIOCM_RTS;	/* DTR = 0, RTS = 0 */
    xf86SerialModemClearBits(pInfo->fd, i);
    usleep(200000);
    i = TIOCM_DTR;		/* DTR = 1, RTS = 0 */
    xf86SerialModemSetBits(pInfo->fd, i);
    usleep(200000);

    /* wait for response, 1st phase (2.1.4) */
    xf86FlushInput(pInfo->fd);
    i = TIOCM_RTS;		/* DTR = 1, RTS = 1 */
    xf86SerialModemSetBits(pInfo->fd, i);

    /* try to read something */
    if (xf86WaitForInput(pInfo->fd, 200000) <= 0) {

	/* port setup, 2nd phase (2.1.5) */
        i = TIOCM_DTR | TIOCM_RTS;	/* DTR = 0, RTS = 0 */
	xf86SerialModemClearBits(pInfo->fd, i);
        usleep(200000);

	/* wait for response, 2nd phase (2.1.6) */
	xf86FlushInput(pInfo->fd);
        i = TIOCM_DTR | TIOCM_RTS;	/* DTR = 1, RTS = 1 */
	xf86SerialModemSetBits(pInfo->fd, i);

        /* try to read something */
	if (xf86WaitForInput(pInfo->fd, 200000) <= 0)
	    goto connect_idle;
    }
#else
    /*
     * This is a simplified procedure; it simply toggles RTS.
     */

    if ((i = xf86GetSerialModemState(pInfo->fd)) == -1)
	return 0;
    i |= XF86_M_DTR;		/* DTR = 1 */
    i &= ~XF86_M_RTS;		/* RTS = 0 */
    if (xf86SetSerialModemState(pInfo->fd, i) == -1)
	goto disconnect_idle;
    usleep(200000);

    pnpOpts = xf86OptionListCreate(pnpSerial, -1, 1);
    xf86SetSerial(pInfo->fd, pnpOpts);

    /* wait for response */
    xf86FlushInput(pInfo->fd);
    i = XF86_M_DTR | XF86_M_RTS;	/* DTR = 1, RTS = 1 */
    xf86SerialModemSetBits(pInfo->fd, i);

    /* try to read something */
    if (xf86WaitForInput(pInfo->fd, 200000) <= 0)
        goto connect_idle;
#endif

    /* collect PnP COM device ID (2.1.7) */
    i = 0;
    usleep(200000);	/* the mouse must send `Begin ID' within 200msec */
    while (xf86ReadSerial(pInfo->fd, &c, 1) == 1) {
	/* we may see "M", or "M3..." before `Begin ID' */
        if ((c == 0x08) || (c == 0x28)) {	/* Begin ID */
	    buf[i++] = c;
	    break;
        }
	if (xf86WaitForInput(pInfo->fd, 200000) <= 0)
	    break;
    }
    if (i <= 0) {
	/* we haven't seen `Begin ID' in time... */
	goto connect_idle;
    }

    ++c;			/* make it `End ID' */
    for (;;) {
	if (xf86WaitForInput(pInfo->fd, 200000) <= 0)
	    break;

	xf86ReadSerial(pInfo->fd, &buf[i], 1);
        if (buf[i++] == c)	/* End ID */
	    break;
	if (i >= 256)
	    break;
    }
    if (buf[i - 1] != c)
	goto connect_idle;
    return i;

    /*
     * According to PnP spec, we should set DTR = 1 and RTS = 0 while
     * in idle state.  But, `moused' shall set DTR = RTS = 1 and proceed,
     * assuming there is something at the port even if it didn't
     * respond to the PnP enumeration procedure.
     */
disconnect_idle:
    i = XF86_M_DTR | XF86_M_RTS;		/* DTR = 1, RTS = 1 */
    xf86SerialModemSetBits(pInfo->fd, i);
connect_idle:
    return 0;
}

static int
pnpparse(InputInfoPtr pInfo, pnpid_t *id, char *buf, int len)
{
    char s[3];
    int offset;
    int sum = 0;
    int i, j;

    id->revision = 0;
    id->eisaid = NULL;
    id->serial = NULL;
    id->class = NULL;
    id->compat = NULL;
    id->description = NULL;
    id->neisaid = 0;
    id->nserial = 0;
    id->nclass = 0;
    id->ncompat = 0;
    id->ndescription = 0;

    offset = 0x28 - buf[0];

    /* calculate checksum */
    for (i = 0; i < len - 3; ++i) {
	sum += buf[i];
	buf[i] += offset;
    }
    sum += buf[len - 1];
    for (; i < len; ++i)
	buf[i] += offset;
    xf86MsgVerb(X_INFO, 2, "%s: PnP ID string: `%*.*s'\n", pInfo->name,
		len, len, buf);

    /* revision */
    buf[1] -= offset;
    buf[2] -= offset;
    id->revision = ((buf[1] & 0x3f) << 6) | (buf[2] & 0x3f);
    xf86MsgVerb(X_INFO, 2, "%s: PnP rev %d.%02d\n", pInfo->name,
		id->revision / 100, id->revision % 100);

    /* EISA vender and product ID */
    id->eisaid = &buf[3];
    id->neisaid = 7;

    /* option strings */
    i = 10;
    if (buf[i] == '\\') {
        /* device serial # */
        for (j = ++i; i < len; ++i) {
            if (buf[i] == '\\')
		break;
        }
	if (i >= len)
	    i -= 3;
	if (i - j == 8) {
            id->serial = &buf[j];
            id->nserial = 8;
	}
    }
    if (buf[i] == '\\') {
        /* PnP class */
        for (j = ++i; i < len; ++i) {
            if (buf[i] == '\\')
		break;
        }
	if (i >= len)
	    i -= 3;
	if (i > j + 1) {
            id->class = &buf[j];
            id->nclass = i - j;
        }
    }
    if (buf[i] == '\\') {
	/* compatible driver */
        for (j = ++i; i < len; ++i) {
            if (buf[i] == '\\')
		break;
        }
	/*
	 * PnP COM spec prior to v0.96 allowed '*' in this field,
	 * it's not allowed now; just ignore it.
	 */
	if (buf[j] == '*')
	    ++j;
	if (i >= len)
	    i -= 3;
	if (i > j + 1) {
            id->compat = &buf[j];
            id->ncompat = i - j;
        }
    }
    if (buf[i] == '\\') {
	/* product description */
        for (j = ++i; i < len; ++i) {
            if (buf[i] == ';')
		break;
        }
	if (i >= len)
	    i -= 3;
	if (i > j + 1) {
            id->description = &buf[j];
            id->ndescription = i - j;
        }
    }

    /* checksum exists if there are any optional fields */
    if ((id->nserial > 0) || (id->nclass > 0)
	|| (id->ncompat > 0) || (id->ndescription > 0)) {
	xf86MsgVerb(X_INFO, 4, "PnP checksum: 0x%02X\n", pInfo->name, sum);
        sprintf(s, "%02X", sum & 0x0ff);
        if (strncmp(s, &buf[len - 3], 2) != 0) {
#if 0
            /*
	     * Checksum error!!
	     * I found some mice do not comply with the PnP COM device
	     * spec regarding checksum... XXX
	     */
	    return FALSE;
#endif
        }
    }

    return TRUE;
}

static symtab_t *
pnpproto(pnpid_t *id)
{
    symtab_t *t;
    int i, j;

    if (id->nclass > 0)
	if (strncmp(id->class, "MOUSE", id->nclass) != 0)
	    /* this is not a mouse! */
	    return NULL;

    if (id->neisaid > 0) {
        t = gettoken(pnpprod, id->eisaid, id->neisaid);
	if (t->val != -1)
            return t;
    }

    /*
     * The 'Compatible drivers' field may contain more than one
     * ID separated by ','.
     */
    if (id->ncompat <= 0)
	return NULL;
    for (i = 0; i < id->ncompat; ++i) {
        for (j = i; id->compat[i] != ','; ++i)
            if (i >= id->ncompat)
		break;
        if (i > j) {
            t = gettoken(pnpprod, id->compat + j, i - j);
	    if (t->val != -1)
                return t;
	}
    }

    return NULL;
}

/* name/val mapping */

static symtab_t *
gettoken(tab, s, len)
symtab_t *tab;
char *s;
int len;
{
    int i;

    for (i = 0; tab[i].name != NULL; ++i) {
	if (strncmp(tab[i].name, s, len) == 0)
	    break;
    }
    return &tab[i];
}
