/* Copyright (c) 1990,1991,1992,1993 Chris and John Downey */

/***

* program name:
    xvi
* function:
    Portable version of UNIX "vi" editor, with extensions.
* module name:
    os2vio.c
* module function:
    OS/2 system interface module.

    This is a character-based implementation using the VIO & KBD
    families of system calls. It doesn't use the Presentation Manager
    but, on OS/2 version 1.* at least, it can be made to work in a PM
    shell window by using markexe (see makefile.os2).

    Like the MS-DOS version, this one saves the screen contents &
    restores them when it exits.

    Currently, the mouse input code doesn't work, & so is commented
    out. I suspect that, if we want to have both mouse & keyboard
    input, we have to use a device monitor, or develop a real PM
    implementation.
* history:
    STEVIE - ST Editor for VI Enthusiasts, Version 3.10
    Originally by Tim Thompson (twitch!tjt)
    Extensive modifications by Tony Andrews (onecom!wldrdg!tony)
    Heavily modified by Chris & John Downey
    Last modified by Martin Guy

***/

#include "xvi.h"

#define NOMOUSE

#ifdef __ZTC__
/*
 * Set default stack size.
 *
 * See i286.asm for an explanation of why it has to be so big.
 */
unsigned	_stack = 44 * 1024;
#endif

/*
 * These are globals which are set by the system interface or terminal
 * interface module, & used for various purposes throughout the rest
 * of xvi.
 *
 * Number of rows & columns in the current window.
 */
unsigned	Rows,
		Columns;
/*
 * Current position for screen writes.
 */
unsigned char	virt_row,
		virt_col;
/*
 * Screen cell (character & attribute): current colour is stored here.
 */
unsigned char	curcell [2];

/*
 * Declare this here so we can access the colour elements.
 */
extern	VirtScr	defscr;

/*
 * Time of last keypress or mouse button press (or garbage if
 * (keystrokes < PSVKEYS)).
 *
 * This should only be referenced within a thread's critical section.
 * Referencing a 32-bit variable is not generally an atomic operation
 * on the 80286.
 */
static volatile clock_t lastevent;

#ifndef NOMOUSE
    /*
     * This is FALSE if we don't appear to have a mouse driver.
     */
    static bool_t	usemouse;

    /*
     * Our logical mouse handle.
     */
    static HMOU	mousenum;

#else	/* NOMOUSE */
#   define	usemouse	FALSE
#endif	/* NOMOUSE */

#ifndef NOMOUSE
    /*
     * Hide mouse cursor.
     */
    static void
    hidemouse()
    {
	NOPTRRECT	r;

	r.row = r.col = 0;
	r.cRow = Rows - 1;
	r.cCol = Columns - 1;
	(void) MouRemovePtr((PNOPTRRECT) &r, mousenum);
    }

#endif	/* NOMOUSE */

/*
 * Show mouse cursor. (This is for symmetry with hidemouse().)
 */
#define showmouse()	((void) MouDrawPtr(mousenum))

static long	semvec [2];

/*
 * This semaphore needs to be acquired by a thread before it enters a
 * critical region.
 */
#define		control ((HSEM)(long FAR *)&semvec[0])

/*
 * This semaphore is used for communication between the main thread &
 * the thread which handles automatic buffer preservation. It should
 * be clear when (keystrokes >= PSVKEYS), otherwise it should be set.
 */
#define		psvsema ((HSEM)(long FAR *)&semvec[1])

#ifndef NOMOUSE

static void
mousehandler()
{
    for (;;) {
	MOUEVENTINFO	m;
	unsigned short	status;
	clock_t		start;

#if 0
	if (MouGetDevStatus((PUSHORT) &status, mousenum) != 0
	    ||
	    (status & (MOUSE_UNSUPPORTED_MODE | MOUSE_DISABLED))
	) {
	    hidemouse();
	    (void) MouClose(mousenum);
	    DosExit(EXIT_THREAD, 0);
	}
#endif
	status = MOU_WAIT;
	MouReadEventQue((PMOUEVENTINFO) &m, (PUSHORT) &status, mousenum);
	/*
	 * If we don't get the control semaphore immediately,
	 * we do nothing. Delayed responses to mouse button
	 * presses could be confusing.
	 */
#if 0
	start = clock();
#endif
	if (DosSemRequest(control, SEM_IMMEDIATE_RETURN) != 0)
	    continue;
#if 0
	if (clock() != start) {
	    (void) fprintf(stderr, "mouse thread: %d\n", __LINE__);
	    DosSemClear(control);
	    continue;
	}
#endif
	/*
	 * Start of critical section.
	 */
	if (++keystrokes >= PSVKEYS)
	    lastevent = clock();
	if (State == NORMAL &&
		(m.fs & (MOUSE_BN1_DOWN | MOUSE_BN2_DOWN | MOUSE_BN3_DOWN))) {
	    hidemouse();
	    mouseclick(m.row, m.col);
	    showmouse();
	}
	/*
	 * End of critical section.
	 */
	DosSemClear(control);
    }
}

#endif	/* NOMOUSE */

/*
 * Macro to convert clock ticks to milliseconds.
 */
#if CLK_TCK == 1000
#   define CLK2MS(c)		(c)
#else
#   if CLK_TCK < 1000
#	define CLK2MS(c)	((c) * (1000 / CLK_TCK))
#   else
#	define CLK2MS(c)	((c) / (CLK_TCK / 1000))
#   endif	/* CLK_TCK > 1000 */
#endif	/* CLK_TCK != 1000 */

/*
 * Number of keystrokes or mouse button presses since the last buffer
 * preservation.
 */
volatile int		keystrokes;

/*
 * This function handles automatic buffer preservation. It runs in its
 * own thread, which is only awake when keystrokes >= PSVKEYS and the
 * main thread is waiting for keyboard input. Even then, it spends
 * most of its time asleep.
 */
static void FAR
psvhandler()
{
    for (;;) {
	long	sleeptime;

	DosSemWait(psvsema, SEM_INDEFINITE_WAIT);
	DosSemRequest(control, SEM_INDEFINITE_WAIT);
	/*
	 * Start of critical section.
	 */
	if (keystrokes < PSVKEYS) {
	    sleeptime = 0;
	    /*
	     * If we haven't had at least PSVKEYS
	     * keystrokes, psvsema should be set.
	     */
	    DosSemSet(psvsema);
	} else if ((sleeptime = (long) Pn(P_preservetime) * 1000 -
		      CLK2MS(clock() - lastevent)) <= 0) {
	    /*
	     * If Pn(P_presevetime) seconds haven't yet
	     * elapsed, sleep until they should have - but
	     * NOT within the critical section (!).
	     *
	     * Otherwise do automatic preserve.
	     *
	     * exPreserveAllBuffers() should reset keystrokes to 0.
	     */
	    (void) exPreserveAllBuffers();
	    sleeptime = 0;
	}
	/*
	 * End of critical section.
	 */
	DosSemClear(control);
	/*
	 * Sleep if we have to.
	 */
	if (sleeptime != 0)
	    DosSleep(sleeptime);
    }
}

/*
 * inchar() - get a character from the keyboard.
 *
 * Timeout not implemented yet for OS/2.
 */
int
inchar(long mstimeout)
{
    for (;;) {
	KBDKEYINFO k;
	bool_t	mstatus,
		psvstatus;

	flush_output();

	mstatus = (usemouse && State == NORMAL);
	psvstatus = (keystrokes >= PSVKEYS);
	/*
	 * We don't have to give control to any other thread
	 * if neither of these conditions is true.
	 */
	if (mstatus || psvstatus) {
#ifndef NOMOUSE
	    if (mstatus)
		showmouse();
#endif
	    if (psvstatus && DosSemWait(psvsema, SEM_IMMEDIATE_RETURN)
						    == ERROR_SEM_TIMEOUT) {
		/*
		 * If psvsema is set, clear it.
		 */
		DosSemClear(psvsema);
	    }
	    DosSemClear(control);
	}
	/*
	 * Start of non-critical section.
	 *
	 * Wait for character from keyboard.
	 */
	KbdCharIn((PKBDKEYINFO) &k, IO_WAIT, 0);
	/*
	 * End of non-critical section.
	 */
	if (mstatus || psvstatus) {
	    DosSemRequest(control, SEM_INDEFINITE_WAIT);
#ifndef NOMOUSE
	    if (mstatus)
		hidemouse();
#endif
	}
	if (++keystrokes >= PSVKEYS)
	    lastevent = clock();
	/*
	 * Now deal with the keypress information.
	 */
	if ((unsigned char) k.chChar == (unsigned char) 0xe0) {
	/*
	 * It's (probably) a function key.
	 */
	    if (k.chScan == 0x53)
		/*
		 * It's the delete key.
		 */
		return State == NORMAL ? 'x' : '\b';
	     /* else */
	    if (State == NORMAL) {
		/*
		 * Assume it must be a function key.
		 */
		switch (k.chScan) {
		    case 0x3b: return(K_HELP);	  /* F1 key */
		    case 0x47: return(K_HOME);	  /* home key */
		    case 0x48: return(K_UARROW);  /* up arrow key */
		    case 0x49: return(K_PGUP);	  /* page up key */
		    case 0x4b: return(K_LARROW);  /* left arrow key */
		    case 0x4d: return(K_RARROW);  /* right arrow key */
		    case 0x4f: return(K_END);	  /* end key */
		    case 0x50: return(K_DARROW);  /* down arrow key */
		    case 0x51: return(K_PGDOWN)); /* page down key */
		    case 0x52: return(K_INSERT);  /* insert key */
		    default:
			/* just ignore it ... */
			continue;
		}
		/*
		 * If we aren't in command mode, 0xe0
		 * is a perfectly legitimate
		 * character, & we can't really tell
		 * whether or not it's supposed to be
		 * a function key, so we just have to
		 * return it as is.
		 */
	    }
	}
	return (unsigned char) k.chChar;
    }
}

void
outchar(int c)
{
    curcell [0] = c;
    VioWrtNCell((PBYTE) curcell, 1, virt_row, virt_col, 0);
    if (++virt_col >= Columns) {
	virt_col -= Columns;
	if (++virt_row >= Rows)
	    virt_row = Rows - 1;
    }
}

void
outstr(char* s)
{
    unsigned len = strlen(s);

    VioWrtCharStrAtt((PCH) s, len, virt_row, virt_col,
					 (PBYTE) & curcell [1], 0);
    if ((virt_col += len) >= Columns) {
	virt_col -= Columns;
	if (++virt_row >= Rows)
	    virt_row = Rows - 1;
    }
}

void
erase_display()
{
    curcell[1] = defscr.pv_colours[VSCcolour];
    curcell[0] = ' ';
    VioWrtNCell((PBYTE) curcell, Rows * Columns, 0, 0, 0);
}

void
erase_line()
{
    curcell [0] = ' ';
    VioWrtNCell((PBYTE) curcell, Columns - virt_col, virt_row, virt_col, 0);
}

void
scroll_down(unsigned start, unsigned end, unsigned nlines)
{
    curcell [0] = ' ';
    VioScrollDn(start, 0, end, Columns - 1, nlines, (PBYTE) curcell, 0);
}

void
scroll_up(unsigned start, unsigned end, unsigned nlines)
{
    curcell [0] = ' ';
    VioScrollUp(start, 0, end, Columns - 1, nlines, (PBYTE) curcell, 0);
}

/*
 * Attributes for colour systems
 */
#define BRIGHT	8	/* only available for foreground colours */
#define BLACK	0
#define BLUE	1
#define GREEN	2
#define CYAN	(BLUE | GREEN)
#define RED	4
#define BROWN	(RED | GREEN)
#define YELLOW	(BRIGHT | BROWN)
#define WHITE	(RED | GREEN | BLUE)

/*
 * Macro to set up foreground & background colours,
 * used to look like this:
#define mkcolour(f,b)	((unsigned char) (((b) << 4) | ((f) & 0xf)))
 *
 * but now these parameters have string values, so we do this:
 */
char *
mkcolour(fore, back)
unsigned short	fore, back;
{
    static char hex[] = {
	'0', '1', '2', '3', '4', '5', '6', '7',
	'8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
    };
    static char	buf[] = { '0', 'x', 0, 0, 0 };

    buf[2] = hex[back & 0xf];
    buf[3] = hex[fore & 0xf];
    return(buf);
}

static char				*oldscreen;
static unsigned short			scrsize;
static enum { m_SYS = 0, m_VI = 1 }	curmode;

/*
 * Save screen contents & set up video & keyboard states for editor.
 */
void
sys_startv()
{
    if (curmode == m_VI)
	return;
    if (oldscreen != NULL) {
	/*
	 * Save contents of screen so we can restore them
	 * afterwards.
	 */
	VioReadCellStr((PCH) oldscreen, (PUSHORT) &scrsize, 0, 0, 0);
    }
    set_colour(defscr.pv_colours[VSCcolour]);
    /*
     * Change keyboard status.
     *
     * We only do this when we've disabled keyboard interrupts.
     */
    {
	KBDINFO		k;

	k.cb = sizeof k;
	KbdGetStatus((PKBDINFO) &k, 0);
	k.fsMask = (k.fsMask
		/*
		 * turn these flags off:
		 */
		 & ~(KEYBOARD_ECHO_ON |
		 KEYBOARD_ASCII_MODE |
		 KEYBOARD_MODIFY_STATE |
		 KEYBOARD_MODIFY_INTERIM |
		 KEYBOARD_MODIFY_TURNAROUND |
		 KEYBOARD_2B_TURNAROUND |
		 KEYBOARD_SHIFT_REPORT))
		/*
		 * turn these flags on:
		 */
		| KEYBOARD_ECHO_OFF |
		  KEYBOARD_BINARY_MODE;
	KbdSetStatus((PKBDINFO) &k, 0);
    }
    curmode = m_VI;
}

void
sys_init()
{
    {
	VIOMODEINFO	v;

	/*
	 * Get information about display.
	 */
	v.cb = sizeof v;
	VioGetMode((PVIOMODEINFO) &v, 0);
	Rows = v.row;
	Columns = v.col;
	scrsize = (Rows - 1) * Columns * 2;
	if (v.color >= COLORS_16) {
	    /*
	     * Statically defined values are for mono systems:
	     * these are defaults for colour systems.
	     */
	    set_param(P_colour, mkcolour(BRIGHT | WHITE, BLUE));
	    set_param(P_statuscolour, mkcolour(YELLOW, BLACK));
	    set_param(P_roscolour, mkcolour(BRIGHT | RED, BLACK));
	    set_param(P_systemcolour, mkcolour(BRIGHT | CYAN, BLACK));
	}
    }
    oldscreen = malloc(scrsize);
    /*
     * We have to acquire this semaphore before we start any other
     * threads.
     */
    DosSemSet(control);
#ifndef NOMOUSE
    /*
     * Open mouse device if we can.
     */
    if (MouOpen((PSZ) NULL, (PHMOU) &mousenum) == 0
#if 0
	&& MouSynch(0) != 0
#endif
    ) {
	TID		mousethread;
	short		mask = MOUSE_BN1_DOWN |
			       MOUSE_BN2_DOWN |
			       MOUSE_BN3_DOWN;

	hidemouse();
#if 0
	MouSetEventMask((PUSHORT) &mask, mousenum);
#endif
	/*
	 * Create concurrent thread to handle mouse events.
	 *
	 * According to Microsoft, the ES register should be
	 * set to 0 first.
	 */
	DosCreateThread((PFNTHREAD) mousehandler,
		(es0(), (PTID) &mousethread), (PBYTE) newstack(32000));
	usemouse = TRUE;
    }
#endif	/* NOMOUSE */
    /*
     * Initialize semaphore for automatic buffer preservation. It
     * should only be clear if (keystrokes >= PSVKEYS).
     */
    DosSemSet(psvsema);
    /*
     * Create concurrent thread to do automatic preserves.
     *
     * According to Microsoft, the ES register should be set to 0 first.
     */
    {
	TID psvthread;

	if (DosCreateThread((PFNTHREAD) psvhandler,
		    (es0(), (PTID) &psvthread),
		    (PBYTE) newstack(20000)) != 0) {
	    (void) fputs("Can't create thread for automatic preserves\r\n",
			 stderr);
	    exit(1);
	}
    }
    /*
     * Disable system critical error handler.
     */
    DosError(HARDERROR_DISABLE);
    sys_startv();
}

/*
 * Restore video & keyboard states to what they were when we started.
 *
 * sys_endv() can be called when we're already in system mode, so we
 * have to check.
 */
void
sys_endv()
{
    KBDINFO k;

    if (curmode == m_SYS)
	return;
    k.cb = sizeof k;
    KbdGetStatus((PKBDINFO) &k, 0);
    k.fsMask = (k.fsMask
	    /*
	     * turn these flags off:
	     */
	     & ~(KEYBOARD_ECHO_OFF |
	     KEYBOARD_BINARY_MODE |
	     KEYBOARD_MODIFY_STATE |
	     KEYBOARD_MODIFY_INTERIM |
	     KEYBOARD_MODIFY_TURNAROUND |
	     KEYBOARD_2B_TURNAROUND |
	     KEYBOARD_SHIFT_REPORT))
	    /*
	     * turn these flags on:
	     */
	    | KEYBOARD_ECHO_ON |
	      KEYBOARD_ASCII_MODE;
    KbdSetStatus((PKBDINFO) &k, 0);
    if (oldscreen != (char*) 0)
	/*
	 * Restore contents of screen saved by
	 * sys_startv().
	 */
	VioWrtCellStr((PCH) oldscreen, scrsize, 0, 0, 0);
    tty_goto(Rows - 1, 0);
    set_colour(defscr.pv_colours[VSCsyscolour]);
    erase_line();
    flush_output();
    curmode = m_SYS;
}

void
sys_exit(int r)
{
    sys_endv();
#ifndef NOMOUSE
    if (usemouse)
	MouClose(mousenum);
#endif
    exit(r);
}

void
sleep(unsigned seconds)
{
    DosSleep(seconds * (long) 1000);
}

/*
 * This function is only used by tempfname(). It constructs a filename
 * suffix based on an index number.
 *
 * The suffix ".$$$" is commonly used for temporary file names on
 * MS-DOS & OS/2 systems. We also use the sequence ".$$1", ".$$2" ...
 * ".fff" (all digits are hexadecimal).
 */
static char*
hexsuffix(unsigned i)
{
    static char	suffix[] = ".$$$";
    static char	hextab[] = "0123456789abcdef";
    char	*sp = &suffix[3];

    while (sp > suffix) {
	if (i > 0) {
	    *sp-- = hextab[i & 0xf];
	    i >>= 4;
	} else {
	    *sp-- = '$';
	}
    }
    return suffix;
}

/*
 * Construct unique name for temporary file, to be used as a backup
 * file for the named file.
 */
char *
tempfname(char *srcname)
{
    char	*srctail,
		*srcdot,
		*endp,
		*retp;
    unsigned	indexnum = 0;
    unsigned	baselen;

    srctail = srcdot = NULL;
    endp = srcname;

    while (*endp) {
	switch (*endp++) {
	case '\\':
	case '/':
	    srctail = endp;
	    srcdot = (char*) 0;
	    continue;
	case '.':
	    srcdot = endp - 1;
	}
    }
    if (srctail == NULL) {
	/*
	 * We haven't found any directory separators ('/' or '\\').
	 */
	srctail = srcname;
	/*
	 * Check to see if there's a disk drive name. If there
	 * is, skip over it.
	 */
	if (*srcname && is_alpha(*srcname) && srcname[1] == ':')
	    srctail = &srcname[2];
    }
    /*
     * There isn't a dot in the trailing part of the filename:
     * just add it at the end.
     */
    if (srcdot == NULL)
	srcdot = endp;
    /*
     * Don't make name too long.
     */
    if (srcdot - srctail > MAXNAMLEN - 4)
	srcdot = srctail + MAXNAMLEN - 4;
    if (srcdot - srcname > MAXPATHLEN - 4)
	srcdot = srcname + MAXPATHLEN - 4;
    baselen = srcdot - srcname;
    /*
     * Allocate space for new temporary file name ...
     */
    if ((retp = alloc(baselen + 5)) == (char*) 0)
	return (char*) 0;
    if (baselen > 0)
	(void) memcpy(retp, srcname, baselen);
    do {
	/*
	 * Keep trying this until we get a unique file name.
	 */
	(void) strcpy(&retp[baselen], hexsuffix(indexnum++));
    } while (exists(retp));
    return retp;
}

/*
 * Fake out a pipe by writing output to temp file, running a process with
 * i/o redirected from this file to another temp file, and then reading
 * the second temp file back in.
 *
 * OS/2 does have real pipes, but I don't know how to avoid deadlock
 * when connecting concurrent processes with bidirectional pipes.
 */
bool_t
sys_pipe(cmd, writefunc, readfunc)
char	*cmd;
int	(*writefunc) P((FILE *));
long	(*readfunc) P((FILE *));
{
    char	*temp1;
    FILE	*fp;
    bool_t	retval;

    /*
     * Create first temporary file ...
     */
    if (
	(temp1 = tempfname("xvi_out")) == NULL
	||
	(fp = fopen(temp1, "w")) == NULL
    ) {
	retval = FALSE;
    } else {
	char	*temp2 = NULL;
	int	savcon;
	int	fd1 = -1,
		fd2 = -1;

	/*
	 * ... then write to it & close it ...
	 */
	(void) (*writefunc)(fp);
	(void) fclose(fp);

	/*
	 * ... then re-open it for reading, open second one
	 * for writing & re-arrange file descriptors.
	 *
	 * Note that we assume that the editor's standard
	 * input, output & error files are the same device,
	 * since I can't imagine how any of them could
	 * usefully be redirected to anything else.
	 */

#ifndef O_BINARY
#	define O_BINARY 0
#endif
	if (
	    (savcon = dup(0)) < 3
	    ||
	    (fd1 = open(temp1, O_RDONLY | O_BINARY)) < 3
	    ||
	    (temp2 = tempfname("xvi_in")) == NULL
	    ||
	    (fd2 = open(temp2,
			O_WRONLY | O_CREAT | O_EXCL | O_BINARY, 0600)) < 3
	) {
	    retval = FALSE;
	} else {
	    (void) dup2(fd1, 0);
	    (void) dup2(fd2, 1);
	    (void) dup2(fd2, 2);

	    (void) close(fd1);
	    (void) close(fd2);
	    fd1 = fd2 = -1;

	    /*
	     * Run the command.
	     */
	    (void) system(cmd);

	    /*
	     * Restore our standard input, output & error
	     * files.
	     */
	    (void) dup2(savcon, 0);
	    (void) dup2(savcon, 1);
	    (void) dup2(savcon, 2);

	    /*
	     * Now read from the second temporary file,
	     * close it, & we're done.
	     */
	    if ((fp = fopen(temp2, "r")) == NULL) {
		retval = FALSE;
	    } else {
		(void) (*readfunc)(fp);
		(void) fclose(fp);
		retval = TRUE;
	    }
	}
	/*
	 * Clean up.
	 */
	if (temp2) {
	    (void) remove(temp2);
	    free(temp2);
	}
	if (savcon > 2)
	    (void) close(savcon);
	if (fd1 > 2)
	    (void) close(fd1);
	if (fd2 > 2)
	    (void) close(fd2);
    }

    if (temp1) {
	(void) remove(temp1);
	free(temp1);
    }

    return(retval);
}
