/**
 ** grev_lnx.c ---- MGRX events, linux console input
 **
 ** Copyright (c) 1995 Csaba Biegl, 820 Stirrup Dr, Nashville, TN 37221
 ** [e-mail: csaba@vuse.vanderbilt.edu]
 ** Copyright (C) 2006 Mariano Alvarez Fernandez
 ** [e-mail: malfer@telefonica.net]
 **
 ** 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:
 ** 080120 M.Alvarez, intl support
 **
 **/

#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 "ninput.h"
#include "arith.h"
#include "memcopy.h"
#include "memfill.h"

#include "mgrxkeys.h"

static struct termio kbd_setup;
static struct termio kbd_reset;
static int kbd_initted = FALSE;
static int kbd_isatty;
static int kbd_lastchr;
static int kbd_filedsc;
static enum { normal, test, wait } kbd_mode;

#define PS2_LEFTBUTTON   1
#define PS2_MIDDLEBUTTON 4
#define PS2_RIGHTBUTTON  2

static int mou_filedsc;
static int mou_buttons;

extern int _lnxfb_waiting_to_switch_console;
extern void _LnxfbSwitchConsoleAndWait(void);

static int kbd_lastmod = 0;
static int kbsysencoding = 0;

static void kbd_restore(void);
static void kbd_init(void);
static int getByte(int remove);
static unsigned short keytrans(unsigned short k, const unsigned short * t);
static int inkey(void);
static int validKey(int key, int valid);
static int _CheckKeyboardHit(void);
static int _ReadCharFromKeyboard(void);
static int _ReadPS2MouseData(int *mb, int *mx, int *my);

#define update_coord(WHICH,MICKEYS) do {                                    \
        static int fract = 0;                                               \
        int delta,ddelta,pos;                                               \
        delta   = (MICKEYS) * MOUINFO->spmult;                              \
        ddelta  = (delta + fract) / MOUINFO->spdiv;                         \
        fract   = (delta + fract) % MOUINFO->spdiv;                         \
        if(iabs(ddelta) >= MOUINFO->thresh) ddelta *= MOUINFO->accel;       \
        pos     = MOUINFO->WHICH##pos + ddelta;                             \
        if(pos  < MOUINFO->WHICH##min) pos = MOUINFO->WHICH##min;           \
        if(pos  > MOUINFO->WHICH##max) pos = MOUINFO->WHICH##max;           \
        if(pos != MOUINFO->WHICH##pos) {                                    \
        MOUINFO->WHICH##pos = pos;                                          \
        }                                                                   \
} while(0)

/**
 ** _GrEventInit - Initializes inputs
 **
 ** Returns  0 on success
 **         -1 on error
 **
 ** For internal use only
 **/

int _GrEventInit(void)
{
    char *s;

    kbd_init();
    if (GrMouseDetect()) {
        mou_buttons = 0;
    }

    s = getenv("LANG");
    if (strstr(s,"UTF-8"))
      kbsysencoding = GRENC_UTF_8;
    else {
      s = getenv("MGRXKBSYSENCODING");
      if (s != NULL) kbsysencoding = GrFindEncoding(s);
      if (kbsysencoding < 0) kbsysencoding = GRENC_ISO_8859_1;
    }

    return 1;
}

/**
 ** _GrEventUnInit - UnInitializes inputs
 **
 ** For internal use only
 **/

void _GrEventUnInit(void)
{
    kbd_restore();
}

/**
 ** _GrGetKbSysEncoding - Get kb system encoding
 **
 **/

int _GrGetKbSysEncoding(void)
{
    return kbsysencoding;
}

/**
 ** _GrReadInputs - Reads inputs and sets events
 **
 ** For internal use only
 **/

int _GrReadInputs(void)
{
    GrEvent evaux;
    int nev = 0;
    int mb = 0, mx = 0, my = 0;

    if (_lnxfb_waiting_to_switch_console)
        _LnxfbSwitchConsoleAndWait();

    if (MOUINFO->msstatus == 2) {
        if (_ReadPS2MouseData(&mb, &mx, &my)) {
            update_coord(x, mx);
            update_coord(y, my);
            /** NOTE we know PS2_LEFTBUTTON == GRMOUSE_LB_STATUS, etc */
            MOUINFO->bstatus = mb;
            GrMouseUpdateCursor();
            MOUINFO->moved  = TRUE;
            if (mb != mou_buttons) {
                evaux.type = GREV_MOUSE;
                evaux.kbstat = kbd_lastmod;
                evaux.p2 = MOUINFO->xpos;
                evaux.p3 = MOUINFO->ypos;
                if ((mb & PS2_LEFTBUTTON) != (mou_buttons & PS2_LEFTBUTTON)) {
                    if (mb & PS2_LEFTBUTTON)
                        evaux.p1 = GRMOUSE_LB_PRESSED;
                    else
                        evaux.p1 = GRMOUSE_LB_RELEASED;
                    GrEventEnqueue(&evaux);
                    nev++;
                }
                if ((mb & PS2_MIDDLEBUTTON) != (mou_buttons & PS2_MIDDLEBUTTON)) {
                    if (mb & PS2_MIDDLEBUTTON)
                        evaux.p1 = GRMOUSE_MB_PRESSED;
                    else
                        evaux.p1 = GRMOUSE_MB_RELEASED;
                    GrEventEnqueue(&evaux);
                    nev++;
                }
                if ((mb & PS2_RIGHTBUTTON) != (mou_buttons & PS2_RIGHTBUTTON)) {
                    if (mb & PS2_RIGHTBUTTON)
                        evaux.p1 = GRMOUSE_RB_PRESSED;
                    else
                        evaux.p1 = GRMOUSE_RB_RELEASED;
                    GrEventEnqueue(&evaux);
                    nev++;
                }
                mou_buttons = mb;
                MOUINFO->moved = FALSE;
            }
            else if ((MOUINFO->genmmove == GR_GEN_MMOVE_ALWAYS) ||
                     ((MOUINFO->genmmove == GR_GEN_MMOVE_IFBUT) &&
                     (MOUINFO->bstatus != 0))) {
                evaux.type = GREV_MMOVE;
                evaux.kbstat = kbd_lastmod;
                evaux.p1 = MOUINFO->bstatus;
                evaux.p2 = MOUINFO->xpos;
                evaux.p3 = MOUINFO->ypos;
                GrEventEnqueue(&evaux);
                MOUINFO->moved = FALSE;
                nev++;
           }
        }
    }

    if (kbd_isatty && _CheckKeyboardHit()) {
        unsigned int key = _ReadCharFromKeyboard();
        evaux.type = GREV_PREKEY;
        evaux.kbstat = kbd_lastmod;
        evaux.p3 = 0;
        if (key > 0xff) {
            evaux.p1 = key;
            evaux.p2 = GRKEY_KEYCODE;
        }
        else if ((key >= 0x80) && (kbsysencoding == GRENC_UTF_8)) {
            evaux.p1 = 0;
            if ((key & 0xe0) == 0xc0) {  /* two bytes */
                evaux.cp1[0] = key;
                evaux.cp1[1] = _ReadCharFromKeyboard();
                evaux.p2 = 2;
            }
            else if ((key & 0xf0) == 0xe0) {  /* three bytes */
                evaux.cp1[0] = key;
                evaux.cp1[1] = _ReadCharFromKeyboard();
                evaux.cp1[2] = _ReadCharFromKeyboard();
                evaux.p2 = 3;
            }
            else {  /* four bytes */
                evaux.cp1[0] = key;
                evaux.cp1[1] = _ReadCharFromKeyboard();
                evaux.cp1[2] = _ReadCharFromKeyboard();
                evaux.cp1[3] = _ReadCharFromKeyboard();
                evaux.p2 = 3;
            }
        }
        else {
            evaux.p1 = key;
            evaux.p2 = 1;
        }
        GrEventEnqueue(&evaux);
        MOUINFO->moved = FALSE;
        nev++;
    }

    if (nev == 0) {
        usleep(1000L);   // wait 1 ms to not eat 100% cpu
    }

    return nev;
}

/**
 ** _GrMouseDetect - Returns true if a mouse is detected
 **
 ** For internal use only
 **/

int _GrMouseDetect(void)
{
    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 */
        return 1;
    }
    return 0;
}

/**
 ** GrMouseSetSpeed
 **
 **/

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

/**
 ** GrMouseSetAccel
 **
 **/

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

/**
 ** GrMouseSetLimits
 **
 **/

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));
}

/**
 ** GrMouseWarp
 **
 **/

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();
}

/** Internal keyboard functions **/

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 unsigned short 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 unsigned short 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 unsigned short keytrans(unsigned short k, const unsigned short * 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;
}

static int _CheckKeyboardHit(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);
}

static int _ReadCharFromKeyboard(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 ... */
            _CheckKeyboardHit();
            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);
}

/** Internal mouse functions **/

static int _ReadPS2MouseData(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;
}
