/*
spacenavd - a free software replacement driver for 6dof space-mice.
Copyright (C) 2007-2025 John Tsiombikas <nuclear@mutantstargoat.com>

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 3 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 "config.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <errno.h>
#ifdef HAVE_ALLOCA_H
#include <alloca.h>
#endif
#ifdef HAVE_MALLOC_H
#include <malloc.h>
#endif
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/ioctl.h>

#include "dev_serial.h"
#include "dev.h"
#include "event.h"
#include "logger.h"
#include "proto.h"

#if  defined(__i386__) || defined(__ia64__) || defined(WIN32) || \
    (defined(__alpha__) || defined(__alpha)) || \
     defined(__arm__) || \
    (defined(__mips__) && defined(__MIPSEL__)) || \
     defined(__SYMBIAN32__) || \
     defined(__x86_64__) || \
     defined(__LITTLE_ENDIAN__)
#define SBALL_LITTLE_ENDIAN
#else
#define SBALL_BIG_ENDIAN
#endif

#define INP_BUF_SZ	256

#define EVQUEUE_SZ	64

enum {
	SB4000	= 1,
	FLIPXY	= 2
};

struct sball {
	int fd;
	unsigned int flags;

	char buf[INP_BUF_SZ];
	int len;

	short mot[6];
	unsigned int keystate, keymask;

	struct termios saved_term;
	int saved_mstat;

	struct dev_input evqueue[EVQUEUE_SZ];
	int evq_rd, evq_wr;

	struct device *dev;

	int (*parse)(struct sball*, int, char*, int);
};


static void close_dev_serial(struct device *dev);
static int read_dev_serial(struct device *dev, struct dev_input *inp);

static int stty_sball(struct sball *sb);
static int stty_mag(struct sball *sb);
static void stty_save(struct sball *sb);
static void stty_restore(struct sball *sb);

static int proc_input(struct sball *sb);

static int mag_parsepkt(struct sball *sb, int id, char *data, int len);
static int sball_parsepkt(struct sball *sb, int id, char *data, int len);

static int guess_num_buttons(struct device *dev, const char *verstr);

static void make_printable(char *buf, int len);
static int read_timeout(int fd, char *buf, int bufsz, long tm_usec);

static void enqueue_motion(struct sball *sb, int axis, int val);
static void gen_button_events(struct sball *sb, unsigned int prev);

static char *memstr(char *buf, int len, const char *str);


int open_dev_serial(struct device *dev)
{
	int fd, sz;
	char buf[128];
	struct sball *sb = 0;

	if((fd = open(dev->path, O_RDWR | O_NOCTTY | O_NONBLOCK)) == -1) {
		logmsg(LOG_ERR, "open_dev_serial: failed to open device: %s: %s\n", dev->path, strerror(errno));
		return -1;
	}
	if(!isatty(fd)) {
		logmsg(LOG_ERR, "open_dev_serial: refusing to use %s: not a TTY\n", dev->path);
		close(fd);
		return -1;
	}

	if(!(sb = calloc(1, sizeof *sb))) {
		logmsg(LOG_ERR, "open_dev_serial: failed to allocate sball object\n");
		goto err;
	}
	sb->dev = dev;
	dev->data = sb;
	dev->fd = sb->fd = fd;
	dev->num_axes = 6;
	dev->close = close_dev_serial;
	dev->read = read_dev_serial;

	stty_save(sb);

	if(stty_sball(sb) == -1) {
		goto err;
	}

	/* Apparently some spaceballs take some time to initialize, and it's
	 * necessary to wait for a little while before we start sending commands.
	 */
	sleep(1);

	write(fd, "\r@RESET\r", 8);

	if((sz = read_timeout(fd, buf, sizeof buf - 1, 2000000)) > 0 && memstr(buf, sz, "@1")) {
		/* we got a response, so it's a spaceball */
		make_printable(buf, sz);
		logmsg(LOG_INFO, "Spaceball detected: %s\n", buf);
		strcpy(dev->name, "Spaceball");

		dev->num_buttons = guess_num_buttons(dev, buf);
		sb->keymask = 0xffff >> (16 - dev->num_buttons);
		logmsg(LOG_INFO, "%d buttons\n", dev->num_buttons);

		/* set binary mode and enable automatic data packet sending. also request
		 * a key event to find out as soon as possible if this is a 4000flx with
		 * 12 buttons
		*/
		write(fd, "\rCB\rMSSV\rk\r", 11);

		sb->parse = sball_parsepkt;
		return 0;
	}

	/* try as a magellan spacemouse */
	if(stty_mag(sb) == -1) {
		goto err;
	}
	write(fd, "vQ\r", 3);

	if((sz = read_timeout(fd, buf, sizeof buf - 1, 250000)) > 0 && buf[0] == 'v') {
		make_printable(buf, sz);
		logmsg(LOG_INFO, "Magellan SpaceMouse detected:\n%s\n", buf);
		strcpy(dev->name, "Magellan SpaceMouse");

		dev->num_buttons = guess_num_buttons(dev, buf);
		sb->keymask = 0xffff >> (16 - dev->num_buttons);
		logmsg(LOG_INFO, "%d buttons\n", dev->num_buttons);

		/* set 3D mode, not-dominant-axis, pass through motion and button packets */
		write(fd, "m3\r", 3);
		/* also attempt the compress mode-set command with extended keys enabled */
		write(fd, "c3B\r", 4);

		sb->parse = mag_parsepkt;
		return 0;
	}

err:
	stty_restore(sb);
	close(fd);
	free(sb);
	return -1;
}

static void close_dev_serial(struct device *dev)
{
	if(dev->data) {
		stty_restore(dev->data);
		close(dev->fd);
	}
	dev->data = 0;
}

static int read_dev_serial(struct device *dev, struct dev_input *inp)
{
	int sz;
	struct sball *sb = dev->data;

	if(!sb) return -1;

	while((sz = read(sb->fd, sb->buf + sb->len,  INP_BUF_SZ - sb->len - 1)) > 0) {
		sb->len += sz;
		proc_input(sb);
	}

	/* if we fill the input buffer, make a last attempt to parse it, and discard
	 * it so we can receive more
	 */
	if(sb->len >= INP_BUF_SZ) {
		proc_input(sb);
		sb->len = 0;
	}

	if(sb->evq_rd != sb->evq_wr) {
		*inp = sb->evqueue[sb->evq_rd];
		sb->evq_rd = (sb->evq_rd + 1) & (EVQUEUE_SZ - 1);
		return 0;
	}
	return -1;
}

/* Labtec spaceball: 9600 8n1 XON/XOFF
 * Can't use canonical mode to assemble input into lines for the spaceball,
 * because binary data received for motion events can include newlines which
 * would be eaten up by the line discipline. Therefore we'll rely on VTIME=1 to
 * hopefully get more than 1 byte at a time. Alternatively we could request
 * printable reports, but I don't feel like implementing that.
 */
static int stty_sball(struct sball *sb)
{
	int mstat;
	struct termios term;

	term = sb->saved_term;
	term.c_oflag = 0;
	term.c_lflag = 0;
	term.c_cc[VMIN] = 0;
	term.c_cc[VTIME] = 1;

	term.c_cflag = CLOCAL | CREAD | CS8 | HUPCL;
	term.c_iflag = IGNBRK | IGNPAR | IXON | IXOFF;

	cfsetispeed(&term, B9600);
	cfsetospeed(&term, B9600);

	if(tcsetattr(sb->fd, TCSAFLUSH, &term) == -1) {
		perror("open_dev_serial: tcsetattr");
		return -1;
	}
	tcflush(sb->fd, TCIOFLUSH);

	mstat = sb->saved_mstat | TIOCM_DTR | TIOCM_RTS;
	ioctl(sb->fd, TIOCMGET, &mstat);
	return 0;
}

/* Logicad magellan spacemouse: 9600 8n2 CTS/RTS
 * Since the magellan devices don't seem to send any newlines, we can rely on
 * canonical mode to feed us nice whole lines at a time.
 */
static int stty_mag(struct sball *sb)
{
	int mstat;
	struct termios term;

	term = sb->saved_term;
	term.c_oflag = 0;
	term.c_lflag = ICANON;
	term.c_cc[VMIN] = 0;
	term.c_cc[VTIME] = 0;
	term.c_cc[VEOF] = 0;
	term.c_cc[VEOL] = '\r';
	term.c_cc[VEOL2] = 0;
	term.c_cc[VERASE] = 0;
	term.c_cc[VKILL] = 0;

	term.c_cflag = CLOCAL | CREAD | CS8 | CSTOPB | HUPCL;
#ifdef CCTS_OFLOW
	term.c_cflag |= CCTS_OFLOW;
#elif defined(CRTSCTS)
	term.c_cflag |= CRTSCTS;
#endif
	term.c_iflag = IGNBRK | IGNPAR;

	cfsetispeed(&term, B9600);
	cfsetospeed(&term, B9600);

	if(tcsetattr(sb->fd, TCSAFLUSH, &term) == -1) {
		perror("open_dev_serial: tcsetattr");
		return -1;
	}
	tcflush(sb->fd, TCIOFLUSH);

	mstat = sb->saved_mstat | TIOCM_DTR | TIOCM_RTS;
	ioctl(sb->fd, TIOCMGET, &mstat);
	return 0;
}

static void stty_save(struct sball *sb)
{
	tcgetattr(sb->fd, &sb->saved_term);
	ioctl(sb->fd, TIOCMGET, &sb->saved_mstat);
}

static void stty_restore(struct sball *sb)
{
	tcsetattr(sb->fd, TCSAFLUSH, &sb->saved_term);
	tcflush(sb->fd, TCIOFLUSH);
	ioctl(sb->fd, TIOCMSET, &sb->saved_mstat);
}


static int proc_input(struct sball *sb)
{
	int sz;
	char *bptr = sb->buf;
	char *start = sb->buf;
	char *end = sb->buf + sb->len;

	/* see if we have a CR in the buffer */
	while(bptr < end) {
		if(*bptr == '\r') {
			*bptr = 0;
			sb->parse(sb, *start, start + 1, bptr - start - 1);
			start = ++bptr;
		} else {
			bptr++;
		}
	}

	sz = start - sb->buf;
	if(sz > 0) {
		memmove(sb->buf, start, sz);
		sb->len -= sz;
	}
	return 0;
}

static int mag_parsepkt(struct sball *sb, int id, char *data, int len)
{
	int i, prev, motion_pending = 0;
	unsigned int prev_key;

	/*logmsg(LOG_DEBUG, "magellan packet: %c - %s (%d bytes)\n", (char)id, data, len);*/

	switch(id) {
	case 'd':
		if(len != 24) {
			logmsg(LOG_WARNING, "magellan: invalid data packet, expected 24 bytes, got: %d\n", len);
			return -1;
		}
		for(i=0; i<6; i++) {
			prev = sb->mot[i];
			sb->mot[i] = ((((int)data[0] & 0xf) << 12) | (((int)data[1] & 0xf) << 8) |
					(((int)data[2] & 0xf) << 4) | (data[3] & 0xf)) - 0x8000;
			data += 4;

			/* flip the Z axis sign to match the spaceball */
			if(i == 2 || i == 5) {
				sb->mot[i] = -sb->mot[i];
			}

			if(sb->mot[i] != prev) {
				enqueue_motion(sb, i, sb->mot[i]);
				motion_pending++;
			}
		}
		if(motion_pending) {
			enqueue_motion(sb, -1, 0);
		}
		break;

	case 'k':
		if(len < 3) {
			logmsg(LOG_WARNING, "magellan: invalid keyboard pakcet, expected 3 bytes, got: %d\n", len);
			return -1;
		}
		prev_key = sb->keystate;
		sb->keystate = (data[0] & 0xf) | ((data[1] & 0xf) << 4) | (((unsigned int)data[2] & 0xf) << 8);
		if(len > 3) {
			sb->keystate |= ((unsigned int)data[3] & 0xf) << 12;
		}

		if(sb->keystate != prev_key) {
			gen_button_events(sb, prev_key);
		}
		break;

	case 'e':
		if(data[0] == 1) {
			logmsg(LOG_WARNING, "magellan error: illegal command: %c%c\n", data[1], data[2]);
		} else if(data[0] == 2) {
			logmsg(LOG_WARNING, "magellan error: framing error\n");
		} else {
			logmsg(LOG_WARNING, "magellan error: unknown device error\n");
		}
		return -1;

	default:
		break;
	}
	return 0;
}

static int sball_parsepkt(struct sball *sb, int id, char *data, int len)
{
	int i, prev, motion_pending = 0;
	char c, *rd, *wr;
	unsigned int prev_key;
	char *errbuf, *errbuf_end;

	errbuf = alloca(len * 16 + 32);

	/* decode data packet, replacing escaped values with the correct ones */
	rd = wr = data;
	while(rd < data + len) {
		if((c = *rd++) == '^') {
			switch(*rd++) {
			case 'Q':
				*wr++ = 0x11;	/* XON */
				break;
			case 'S':
				*wr++ = 0x13;	/* XOFF */
				break;
			case 'M':
				*wr++ = 13;		/* CR */
				break;
			case '^':
				*wr++ = '^';
				break;
			default:
				logmsg(LOG_WARNING, "sball decode: ignoring invalid escape code: %xh\n", (unsigned int)c);
			}
		} else {
			*wr++ = c;
		}
	}
	len = wr - data;	/* update the decoded length */

	switch(id) {
	case 'D':
		if(len != 14) {
			logmsg(LOG_WARNING, "sball: invalid data packet, expected 14 bytes, got: %d\n", len);
			return -1;
		}

#ifndef SBALL_BIG_ENDIAN
		rd = data;
		for(i=0; i<6; i++) {
			rd += 2;
			c = rd[0];
			rd[0] = rd[1];
			rd[1] = c;
		}
#endif

		for(i=0; i<6; i++) {
			char *dest = (char*)(sb->mot + i);
			data += 2;
			prev = sb->mot[i];
			*dest++ = data[0];
			*dest++ = data[1];

			if(sb->mot[i] != prev) {
				enqueue_motion(sb, i, sb->mot[i]);
				motion_pending++;
			}
		}
		if(motion_pending) {
			enqueue_motion(sb, -1, 0);
		}
		break;

	case 'K':
		if(len != 2) {
			logmsg(LOG_WARNING, "sball: invalid key packet, expected 2 bytes, got: %d\n", len);
			return -1;
		}
		if(sb->flags & SB4000) break;	/* ignore K packets from spaceball 4000 devices */

		prev_key = sb->keystate;
		/* data[1] bits 0-3 -> buttons 0,1,2,3
		 * data[1] bits 4,5 (3003 L/R) -> buttons 0, 1
		 * data[0] bits 0-2 -> buttons 4,5,6
		 * data[0] bit 4 is (2003 pick) -> button 7
		 */
		sb->keystate = ((data[1] & 0xf) | ((data[1] >> 4) & 3) | ((data[0] & 7) << 4) |
			((data[0] & 0x10) << 3)) & sb->keymask;

		if(sb->keystate != prev_key) {
			gen_button_events(sb, prev_key);
		}
		break;

	case '.':
		if(len != 2) {
			logmsg(LOG_WARNING, "sball: invalid sb4k key packet, expected 2 bytes, got: %d\n", len);
			return -1;
		}
		/* spaceball 4000 key packet */
		if(!(sb->flags & SB4000)) {
			logmsg(LOG_INFO, "Switching to spaceball 4000flx/5000flx-a mode (12 buttons)            \n");
			sb->flags |= SB4000;
			sb->dev->num_buttons = 12;	/* might have guessed 8 before */
			sb->keymask = 0xfff;
			strcpy(sb->dev->name, "Spaceball 4000FLX");
			sb->dev->type = DEV_SB4000;
		}
		/* update orientation flag (actually don't bother) */
		/*
		if(data[0] & 0x20) {
			sb->flags |= FLIPXY;
		} else {
			sb->flags &= ~FLIPXY;
		}
		*/

		prev_key = sb->keystate;
		/* data[1] bits 0-5 -> buttons 0,1,2,3,4,5
		 * data[1] bit 7 -> button 6
		 * data[0] bits 0-4 -> buttons 7,8,9,10,11
		 */
		sb->keystate = (data[1] & 0x3f) | ((data[1] & 0x80) >> 1) | ((data[0] & 0x1f) << 7);
		if(sb->keystate != prev_key) {
			gen_button_events(sb, prev_key);
		}
		break;

	case 'E':
		strcpy(errbuf, "sball: error:");
		errbuf_end = errbuf + 13;
		for(i=0; i<len; i++) {
			if(isprint((int)data[i])) {
				errbuf_end += sprintf(errbuf_end, " %c", data[i]);
			} else {
				errbuf_end += sprintf(errbuf_end, " %02xh", (unsigned int)data[i]);
			}
		}
		logmsg(LOG_WARNING, errbuf);
		break;

	case 'M':	/* ignore MSS responses */
	case '?':	/* ignore unrecognized command errors */
		break;

	default:
		/* DEBUG */
		errbuf_end = errbuf + sprintf(errbuf, "sball: got '%c' packet:", (char)id);
		for(i=0; i<len; i++) {
			errbuf_end += sprintf(errbuf_end, " %02x", (unsigned int)data[i]);
		}
		strcpy(errbuf_end, "\n");
		logmsg(LOG_WARNING, errbuf);
	}
	return 0;
}

static int guess_num_buttons(struct device *dev, const char *verstr)
{
	int major, minor;
	const char *s;

	if((s = strstr(verstr, "Firmware version"))) {	/* spaceball */
		/* try to guess based on firmware number */
		if(sscanf(s + 17, "%d.%d", &major, &minor) == 2 && major == 2) {
			if(minor == 35 || minor == 62 || minor == 63) {
				dev->type = DEV_SB3003;
				strcpy(dev->name, "Spaceball 3003/3003C");
				return 2;	/* spaceball 3003/3003C */
			}
			if(minor == 43 || minor == 45) {
				dev->type = DEV_SB4000;
				strcpy(dev->name, "Spaceball 4000FLX/5000FLX-A");
				return 12;	/* spaceball 4000flx/5000flx-a */
			}
			if(minor == 2 || minor == 13 || minor == 15 || minor == 42) {
				/* 2.42 is also used by spaceball 4000flx. we'll guess 2003c for
				 * now, and change the buttons to 12 first time we get a '.'
				 * packet. I'll also request a key report during init to make
				 * sure this happens as soon as possible, before clients have a
				 * chance to connect.
				 */
				dev->type = DEV_SB2003;
				strcpy(dev->name, "Spaceball 1003/2003/2003C");
				return 8;	/* spaceball 1003/2003/2003c */
			}
		}
	}

	if(strstr(verstr, "MAGELLAN")) {
		dev->type = DEV_SM;
		strcpy(dev->name, "Magellan SpaceMouse");
		return 11; /* magellan spacemouse (assume ext buttons on plus/xt) */
	}

	if(strstr(verstr, "SPACEBALL")) {
		dev->type = DEV_SM5000;
		strcpy(dev->name, "Spaceball 5000");
		return 12; /* spaceball 5000 */
	}

	if(strstr(verstr, "CadMan")) {
		dev->type = DEV_SMCADMAN;
		strcpy(dev->name, "CadMan");
		return 4;
	}

	logmsg(LOG_DEBUG, "Can't guess number of buttons, default to 8, report this as a bug!\n");
	return 8;
}

static void make_printable(char *buf, int len)
{
	int i, c;
	char *wr = buf;

	for(i=0; i<len; i++) {
		c = *buf++;
		if(c == '\r') {
			*wr++ = '\n';
			while(*buf == '\n' || *buf == '\r') buf++;
		} else {
			*wr++ = c;
		}
	}
	*wr = 0;
}

static int read_timeout(int fd, char *buf, int bufsz, long tm_usec)
{
	int res;
	long usec, sz = 0;
	struct timeval tv0, tv;
	fd_set rdset;

	if(!buf || bufsz <= 0) return -1;

	usec = tm_usec;
	gettimeofday(&tv0, 0);

	while(sz < bufsz && usec > 0) {
		tv.tv_sec = usec / 1000000;
		tv.tv_usec = usec % 1000000;

		FD_ZERO(&rdset);
		FD_SET(fd, &rdset);
		if((res = select(fd + 1, &rdset, 0, 0, &tv)) > 0 && FD_ISSET(fd, &rdset)) {
			sz += read(fd, buf + sz, bufsz - sz);
			buf[sz] = 0;
			tm_usec = usec = 128000;	/* wait 128ms for the rest of the message to appear */
			gettimeofday(&tv0, 0);
			continue;
		}
		if(res == -1 && (errno == EWOULDBLOCK || errno == EAGAIN)) {
			break;
		}
		gettimeofday(&tv, 0);
		usec = tm_usec - ((tv.tv_sec - tv0.tv_sec) * 1000000 + (tv.tv_usec - tv0.tv_usec));
	}

	return sz > 0 ? sz : -1;
}

static void enqueue_motion(struct sball *sb, int axis, int val)
{
	struct dev_input *inp = sb->evqueue + sb->evq_wr;

	sb->evq_wr = (sb->evq_wr + 1) & (EVQUEUE_SZ - 1);
	if(sb->evq_wr == sb->evq_rd) {
		/* overflow, drop the oldest event */
		sb->evq_rd = (sb->evq_rd + 1) & (EVQUEUE_SZ - 1);
	}

	if(axis >= 0) {
		inp->type = INP_MOTION;
		inp->idx = axis;
		inp->val = val;
	} else {
		inp->type = INP_FLUSH;
	}
}

static void gen_button_events(struct sball *sb, unsigned int prev)
{
	int i;
	unsigned int bit = 1;
	unsigned int diff = sb->keystate ^ prev;
	struct dev_input *inp;

	for(i=0; i<16; i++) {
		if(diff & bit) {
			inp = sb->evqueue + sb->evq_wr;
			sb->evq_wr = (sb->evq_wr + 1) & (EVQUEUE_SZ - 1);
			if(sb->evq_wr == sb->evq_rd) {
				/* overflow, drop the oldest event */
				sb->evq_rd = (sb->evq_rd + 1) & (EVQUEUE_SZ - 1);
			}

			inp->type = INP_BUTTON;
			inp->idx = i;
			inp->val = sb->keystate & bit ? 1 : 0;
		}
		bit <<= 1;
	}
}

static char *memstr(char *buf, int len, const char *str)
{
	int i, slen = strlen(str);
	for(i=0; i<len - slen; i++) {
		if(memcmp(buf + i, str, slen) == 0) {
			return buf + i;
		}
	}
	return 0;
}
