/**
 ** lnxinpu2.c ---- alternate mouse and keyboard interface for Linux
 **
 ** Copyright (c) 1995 Csaba Biegl, 820 Stirrup Dr, Nashville, TN 37221
 ** [e-mail: csaba@vuse.vanderbilt.edu]
 **
 ** This file is part of the GRX graphics library.
 **
 ** The GRX graphics library is free software; you can redistribute it
 ** and/or modify it under some conditions; see the "copying.grx" file
 ** for details.
 **
 ** This library 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.
 **
 ** Contributions by: (See "doc/credits.doc" for details)
 ** Mauro Condarelli (mc5686@mclink.it)
 ** Hartmut Schirmer (hsc@techfak.uni-kiel.de)
 ** Andris Pavenis (pavenis@acad.latnet.lv)
 **
 ** This version of lnxinput.c handles the mouse directly, not using
 ** the svgalib. Changes:
 **
 ** New mouse code. Try to open /dev/psaux. Only works with PS2 mice
 ** (M.Alvarez 07/02/2002).
 **
 ** Now works with PS2 and IMPS2 mice (the wheel is not used)
 ** (M.Alvarez 29/12/2002).
 **
 ** Now calls _LnxfbSwitchConsoleAndWait if _lnxfb_waiting_to_switch_console
 ** is true (M.Alvarez 1/3/2003).
 **/

#include <stdio.h>
#include <termio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <time.h>

#include "libgrx.h"
#include "input.h"
#include "arith.h"
#include "memcopy.h"
#include "memfill.h"

#include "grxkeys.h"

extern int _lnxfb_waiting_to_switch_console;
extern void _LnxfbSwitchConsoleAndWait(void);

/*
 * keyboard stuff
 */
static struct termio kbd_setup;
static struct termio kbd_reset;
static int kbd_initted = FALSE;
static int kbd_enabled = TRUE;
static int kbd_isatty;
static int kbd_lastchr;
static int kbd_filedsc;
static enum { normal, test, wait } kbd_mode;

static void kbd_restore(void)
{
    if (kbd_initted && kbd_isatty && (kbd_mode != normal)) {
	ioctl(kbd_filedsc, TCSETA, &kbd_reset);
	kbd_mode = normal;
    }
}

static void kbd_init(void)
{
    if (!kbd_initted) {
	kbd_initted = TRUE;
	kbd_lastchr = EOF;
	kbd_filedsc = fileno(stdin);
	kbd_isatty = isatty(kbd_filedsc);
	if (kbd_isatty) {
	    ioctl(kbd_filedsc, TCGETA, &kbd_setup);
	    ioctl(kbd_filedsc, TCGETA, &kbd_reset);
	    kbd_setup.c_lflag &= ~(ICANON | ECHO);
	    kbd_setup.c_iflag &= ~(INLCR | ICRNL);
	    atexit(kbd_restore);
	    kbd_mode = normal;
	}
    }
}

static int n = 0;
static int getByte(int remove)
{
    static unsigned char keybuf[10];
    int ch, f;

    if (n < 0)
	n = 0;
    if (n <= 0) {
	f = read(kbd_filedsc, keybuf + n, 10 - n);
	if (f > 0) {
	    n += f;
	}
    }
    if (n <= 0)
	return EOF;
    ch = keybuf[0];
    if (remove) {
	--n;
	memmove(&keybuf[0], &keybuf[1], n * sizeof(unsigned char));
    }
    return ch;
}

static const GrKeyType fnk[] = {
    GrKey_F6, GrKey_F7, GrKey_F8, GrKey_F9, GrKey_F10, GrKey_Escape,
    /* shift F1 == F11, shift F2 == F12  What code should be returned ?? */
    GrKey_Shift_F1, GrKey_Shift_F2,
    GrKey_Shift_F3, GrKey_Shift_F4,
    GrKey_Escape, GrKey_Shift_F5, GrKey_Shift_F6, GrKey_Escape,
    GrKey_Shift_F7, GrKey_Shift_F8, GrKey_Shift_F9, GrKey_Shift_F10
};

static const GrKeyType alt[] = {
    GrKey_Escape, GrKey_Alt_Escape,
    0x7f, GrKey_Alt_Backspace,
    'q', GrKey_Alt_Q,
    'w', GrKey_Alt_W,
    'e', GrKey_Alt_E,
    'r', GrKey_Alt_R,
    't', GrKey_Alt_T,
    'y', GrKey_Alt_Y,
    'u', GrKey_Alt_U,
    'i', GrKey_Alt_I,
    'o', GrKey_Alt_O,
    'p', GrKey_Alt_P,
    '[', GrKey_Alt_LBracket,
    ']', GrKey_Alt_RBracket,
    GrKey_Return, GrKey_Alt_Return,
    'a', GrKey_Alt_A,
    's', GrKey_Alt_S,
    'd', GrKey_Alt_D,
    'f', GrKey_Alt_F,
    'g', GrKey_Alt_G,
    'h', GrKey_Alt_H,
    'j', GrKey_Alt_J,
    'k', GrKey_Alt_K,
    'l', GrKey_Alt_L,
    ';', GrKey_Alt_Semicolon,
    '\'', GrKey_Alt_Quote,
    '\x60', GrKey_Alt_Backquote,
    '\\', GrKey_Alt_Backslash,
    'z', GrKey_Alt_Z,
    'x', GrKey_Alt_X,
    'c', GrKey_Alt_C,
    'v', GrKey_Alt_V,
    'b', GrKey_Alt_B,
    'n', GrKey_Alt_N,
    'm', GrKey_Alt_M,
    ',', GrKey_Alt_Comma,
    '.', GrKey_Alt_Period,
    '/', GrKey_Alt_Slash,
/*                GrKey_Alt_F1 */
/*                GrKey_Alt_F2 */
/*                GrKey_Alt_F3 */
/*                GrKey_Alt_F4 */
/*                GrKey_Alt_F5 */
/*                GrKey_Alt_F6 */
/*                GrKey_Alt_F7 */
/*                GrKey_Alt_F8 */
/*                GrKey_Alt_F9 */
/*                GrKey_Alt_F10 */
    '1', GrKey_Alt_1,
    '2', GrKey_Alt_2,
    '3', GrKey_Alt_3,
    '4', GrKey_Alt_4,
    '5', GrKey_Alt_5,
    '6', GrKey_Alt_6,
    '7', GrKey_Alt_7,
    '8', GrKey_Alt_8,
    '9', GrKey_Alt_9,
    '0', GrKey_Alt_0,
    '-', GrKey_Alt_Dash,
    '=', GrKey_Alt_Equals,
/*                GrKey_Alt_F11 */
/*                GrKey_Alt_F12 */
/*                GrKey_Alt_KPSlash */
    GrKey_Tab, GrKey_Alt_Tab,
/*                GrKey_Alt_Enter */
    '<', GrKey_Alt_LAngle,
    '>', GrKey_Alt_RAngle,
    '@', GrKey_Alt_At,
    '{', GrKey_Alt_LBrace,
    '|', GrKey_Alt_Pipe,
    '}', GrKey_Alt_RBrace,
    0x0000, GrKey_NoKey
};

static GrKeyType keytrans(GrKeyType k, const GrKeyType * t)
{
    while (t[0] != 0x0000 && t[1] != GrKey_NoKey) {
	if (k == t[0])
	    return t[1];
	t += 2;
    }
    return GrKey_OutsideValidRange;
}

static int inkey(void)
{
    int Key, Key2;

  Restart:
    Key = getByte(1);
    if (Key != GrKey_Escape) {
	if (Key == 0x7f)
	    return (GrKey_BackSpace);
	return (Key);
    }
    if (n == 0)
	return (GrKey_Escape);
    /* We have ^[ and something more after that */

    Key = getByte(1);
    if (Key == EOF)
	return (GrKey_Escape);
    if (Key != '[')
	return (keytrans(Key, alt));	/* Check for Alt+Key */

    /* We have ^[[ */
    if (n == 0)
	return (GrKey_Alt_LBracket);

    Key = getByte(1);
    if (Key == EOF)
	return (GrKey_Alt_LBracket);;
    switch (Key) {
    case 'A':
	return (GrKey_Up);
    case 'B':
	return (GrKey_Down);
    case 'C':
	return (GrKey_Right);
    case 'D':
	return (GrKey_Left);
    case 'G':
	return (GrKey_Center);
    case '[':
	Key = getByte(1);
	switch (Key) {
	case 'A':
	    return (GrKey_F1);
	case 'B':
	    return (GrKey_F2);
	case 'C':
	    return (GrKey_F3);
	case 'D':
	    return (GrKey_F4);
	case 'E':
	    return (GrKey_F5);
	default:
	    goto Restart;
	}
    default:
	if (Key >= '1' && Key <= '6') {
	    Key2 = getByte(1);
	    if (Key2 == '~')
		switch (Key) {
		case '1':
		    return (GrKey_Home);
		case '2':
		    return (GrKey_Insert);
		case '3':
		    return (GrKey_Delete);
		case '4':
		    return (GrKey_End);
		case '5':
		    return (GrKey_PageUp);
		case '6':
		    return (GrKey_PageDown);
	    } else if (Key2 >= '0' && Key2 <= '9') {
		int index = 10 * (Key - '0') + (Key2 - '0');
		if (getByte(1) != '~')
		    goto Restart;
		if (17 <= index && index <= 34)
		    return (fnk[index - 17]);
		goto Restart;
	    }
	}
	goto Restart;
    }
}

static int validKey(int key, int valid)
{
    if (key < 0 || key > GrKey_LastDefinedKeycode || key == EOF)
	return valid;
    return key;
}

int _GrCheckKeyboardHit(void)
{
    if (_lnxfb_waiting_to_switch_console)
	_LnxfbSwitchConsoleAndWait();

    if (!kbd_initted) {
	kbd_init();
    }
    if (!kbd_isatty) {
	return (TRUE);
    }
    if (kbd_lastchr != EOF) {
	return (TRUE);
    }
    if (kbd_mode != test) {
	kbd_setup.c_cc[VMIN] = 0;
	kbd_setup.c_cc[VTIME] = 0;
	if (ioctl(kbd_filedsc, TCSETAW, &kbd_setup) == EOF)
	    return (FALSE);
	kbd_mode = test;
    }
    kbd_lastchr = validKey(inkey(), EOF);
    return (kbd_lastchr != EOF);
}

int _GrReadCharFromKeyboard(void)
{
    int key = EOF;

    if (!kbd_initted) {
	kbd_init();
    }
    if (!kbd_isatty) {
	return (getc(stdin));
    }
    do {
        if (_lnxfb_waiting_to_switch_console)
	    _LnxfbSwitchConsoleAndWait();

	if (kbd_lastchr != EOF) {
	    key = kbd_lastchr;
	    kbd_lastchr = EOF;
	    break;
	}
	if (kbd_mode == test) {
	    /* we're in test mode. Look for key without mode switch ... */
	    _GrCheckKeyboardHit();
	    if (kbd_lastchr != EOF) {
		key = kbd_lastchr;
		kbd_lastchr = EOF;
		break;
	    }
	}
	/* no key till now. Wait of it ... */
	if (kbd_mode != wait) {
	    kbd_setup.c_cc[VMIN] = 0;
	    kbd_setup.c_cc[VTIME] = 1;
	    if (ioctl(kbd_filedsc, TCSETAW, &kbd_setup) == EOF)
		return (EOF);
	    kbd_mode = wait;
	}
	if ((key = inkey()) != EOF) break;
    } while (1);
    return validKey(key, GrKey_OutsideValidRange);
}

/*
 * Mouse stuff
 */

#define PS2_LEFTBUTTON   1
#define PS2_MIDDLEBUTTON 4
#define PS2_RIGHTBUTTON  2
#define PS2_FOURTHBUTTON 8
#define PS2_FIFTHBUTTON 16

static int mou_filedsc;
static int mou_buttons;
static int mou_enabled = TRUE;
static long evt_lasttime;

static void uninit(void)
{
    if (MOUINFO->msstatus > 1)
	MOUINFO->msstatus = 1;
}

int GrMouseDetect(void)
{
    if (MOUINFO->msstatus == 0) {
	MOUINFO->msstatus = (-1);	/* assume missing */
	mou_filedsc = open("/dev/psaux", O_RDONLY | O_NDELAY);
	if (mou_filedsc >= 0) {
	    MOUINFO->msstatus = 1;	/* present, but not initted */
	    atexit(uninit);
	}
    }
    return ((MOUINFO->msstatus > 0) ? TRUE : FALSE);
}

void GrMouseInitN(int queue_size)
{
    uninit();
    queue_size = umax(4, umin(256, queue_size));
    init_queue(queue_size);
    kbd_init();
    if (GrMouseDetect()) {
	GrMouseSetSpeed(1, 1);
	GrMouseSetAccel(100, 1);
	GrMouseSetLimits(0, 0, SCRN->gc_xmax, SCRN->gc_ymax);
	GrMouseWarp((SCRN->gc_xmax >> 1), (SCRN->gc_ymax >> 1));
	_GrInitMouseCursor();
	MOUINFO->msstatus = 2;
	mou_buttons = 0;
    }
    GrMouseEventEnable(TRUE, TRUE);
    real_time(evt_lasttime);
    MOUINFO->uninit = uninit;
}

void GrMouseSetSpeed(int spmult, int spdiv)
{
    MOUINFO->spmult = umin(16, umax(1, spmult));
    MOUINFO->spdiv = umin(16, umax(1, spdiv));
}

void GrMouseSetAccel(int thresh, int accel)
{
    MOUINFO->thresh = umin(64, umax(1, thresh));
    MOUINFO->accel = umin(16, umax(1, accel));
}

void GrMouseSetLimits(int x1, int y1, int x2, int y2)
{
    isort(x1, x2);
    isort(y1, y2);
    MOUINFO->xmin = imax(0, imin(x1, SCRN->gc_xmax));
    MOUINFO->ymin = imax(0, imin(y1, SCRN->gc_ymax));
    MOUINFO->xmax = imax(0, imin(x2, SCRN->gc_xmax));
    MOUINFO->ymax = imax(0, imin(y2, SCRN->gc_ymax));
}

void GrMouseWarp(int x, int y)
{
    MOUINFO->xpos = imax(MOUINFO->xmin, imin(MOUINFO->xmax, x));
    MOUINFO->ypos = imax(MOUINFO->ymin, imin(MOUINFO->ymax, y));
    GrMouseUpdateCursor();
}

void GrMouseEventEnable(int enable_kb, int enable_ms)
{
    kbd_enabled = enable_kb;
    mou_enabled = enable_ms;
}

/* works with PS2 and IMPS2 mice, but the wheel is not used */
static int _GrReadPS2MouseData(int *mb, int *mx, int *my)
{
    static int imps2 = 0;
    static int count_to_imps2 = 0;
    static int last_packed_ok = 0;
    unsigned char buf[4];
    int evr = 0;

    *mx = *my = 0;
    do {
	if (read(mou_filedsc, buf, 1) < 1)
	    return evr;
	if (!(buf[0] & 0x08)) {	/* sync lost */
	    if (!imps2) {
		if (last_packed_ok) {	/* an IMPS2 mouse? */
		    count_to_imps2++;
		    if (count_to_imps2 > 20)
			imps2 = 1;
		} else
		    count_to_imps2 = 0;
		last_packed_ok = 0;
	    }
	    return evr;
	}
	while (read(mou_filedsc, buf + 1, 1) != 1);
	while (read(mou_filedsc, buf + 2, 1) != 1);
	if (imps2) /* the IMPS2 protocol is 4 bytes wide */
	    while (read(mou_filedsc, buf + 3, 1) != 1);
	*mb = buf[0] & 0x7;
	*mx += (buf[0] & 0x10) ? buf[1] - 256 : buf[1];
	*my += (buf[0] & 0x20) ? -(buf[2] - 256) : -buf[2];
	last_packed_ok = 1;
	evr = 1;
    } while (*mb == mou_buttons);
    return evr;
}

void _GrUpdateInputs(void)
{
    GrMouseEvent ev;
    int mb, mx, my, gotevt;

    for (;;) {
	gotevt = FALSE;
	if (_lnxfb_waiting_to_switch_console)
	    _LnxfbSwitchConsoleAndWait();
	if (mou_enabled && (MOUINFO->msstatus == 2)) {
	    if (_GrReadPS2MouseData(&mb, &mx, &my)) {
		update_coord(x, mx);
		update_coord(y, my);
		if (mb != mou_buttons) {
		    fill_mouse_ev(ev,
				  mou_buttons, mb,
				  PS2_LEFTBUTTON,
				  PS2_MIDDLEBUTTON, PS2_RIGHTBUTTON,
				  PS2_FOURTHBUTTON, PS2_FIFTHBUTTON, 0);
		    real_dtime(ev.dtime, evt_lasttime);
		    enqueue_event(ev);
		    MOUINFO->moved = FALSE;
		    mou_buttons = mb;
		    gotevt = TRUE;
		}
	    }
	}
	if (kbd_enabled && kbd_isatty && _GrCheckKeyboardHit()) {
	    int key = _GrReadCharFromKeyboard();
	    fill_keybd_ev(ev, key, 0);
	    real_dtime(ev.dtime, evt_lasttime);
	    enqueue_event(ev);
	    MOUINFO->moved = FALSE;
	    gotevt = TRUE;
	}
	if (!gotevt)
	    break;
    }
}

void GrMouseGetEventT(int flags, GrMouseEvent * ev, long tout)
{
    int msdraw;
    if (MOUINFO->msstatus == 0)
	GrMouseInit();
    msdraw = !MOUINFO->displayed && !(flags & GR_M_NOPAINT);
    if (msdraw)
	GrMouseDisplayCursor();
    for (;;) {
	fd_set readfds;
	struct timeval wtime;
	int N;
	_GrUpdateInputs();
	GrMouseUpdateCursor();
	while (MOUINFO->qlength > 0) {
	    dequeue_event((*ev));
	    if (ev->flags & flags) {
		if (msdraw)
		    GrMouseEraseCursor();
		return;
	    }
	}
	if ((flags & GR_M_POLL) ||
	    (tout == 0) || (MOUINFO->moved && (flags & GR_M_MOTION))) {
	    fill_mouse_ev((*ev),
			  mou_buttons, mou_buttons,
			  PS2_LEFTBUTTON,
			  PS2_MIDDLEBUTTON, PS2_RIGHTBUTTON,
			  PS2_FOURTHBUTTON, PS2_FIFTHBUTTON, 0);
	    if (ev->flags)	/* something happend */
		real_dtime(ev->dtime, evt_lasttime);
	    else
		ev->dtime = -1;	/* special time is nothing happend */
	    MOUINFO->moved = FALSE;
	    if (msdraw)
		GrMouseEraseCursor();
	    return;
	}
	FD_ZERO(&readfds);
	N = 0;
	if (kbd_enabled && kbd_isatty) {
	    FD_SET(kbd_filedsc, &readfds);
	    N = kbd_filedsc + 1;
	}
	if (mou_enabled && (MOUINFO->msstatus == 2)) {
	    FD_SET(mou_filedsc, &readfds);
	    N = umax(N, (mou_filedsc + 1));
	}
	if (N == 0) {
	    if (tout > 0)
		usleep(tout * 1000);
	    tout = 0;
	    continue;
	}
	if (tout > 0) {
	    wtime.tv_sec = (tout / 1000);
	    wtime.tv_usec = (tout % 1000) * 1000;
	    if (select(N, &readfds, NULL, NULL, &wtime) < 1)
		tout = 0;
	    continue;
	}
	select(N, &readfds, NULL, NULL, NULL);
    }
}
