/*
 * bc_inpt.c
 *
 * Borland C interface, input functions
 *
 */

#include <bios.h>
#include <stdio.h>
#include <string.h>
#include "bc_frotz.h"

#define BUF_LEFT_END()	(input_pointer == input_buffer)
#define BUF_RIGHT_END()	(input_offs == 0)

static char history_buffer[HISTORY_BUFFER_SIZE];

static history_latest_entry = 0;
static history_current_entry = 0;
static history_prefix_len = -1;

static overwrite = 0;

static char *input_buffer = NULL;
static char *input_pointer = NULL;

static input_free = 0;
static input_left = 0;
static input_offs = 0;

static cursor = 0;

static long limit = 0;

/*
 * swap_colours
 *
 * This is a small helper function for switch_cursor. It swaps the
 * current background and foreground colours.
 *
 */

static void swap_colours (void)
{

    _AL = text_fg;
    _AH = text_bg;
    text_fg = _AH;
    text_bg = _AL;

}/* swap_colours */

/*
 * switch_cursor
 *
 * Turn cursor on/off. If there is mouse support then turn the mouse
 * pointer on/off as well. The cursor should not be moved and the
 * contents of the screen should not be changed while the cursor is
 * visible (because of the primitive cursor emulation we use here).
 *
 */

static void switch_cursor (int visible)
{

    /* Turn mouse pointer off */

    if ((h_flags & MOUSE_FLAG) && !visible) {
	asm mov ax,2
	asm int 0x33
    }

    /* Toggle cursor on/off */

    if (cursor)

	if (display <= _TEXT_) {

	    /* Use hardware cursor in text mode */

	    if (display == _MONO_)
		_CX = overwrite ? 0x080f : 0x0a0b;
	    else
		_CX = overwrite ? 0x0408 : 0x0506;

	    if (visible == 0)
		_CX = 0xffff;

	    asm mov ah,2
	    asm mov bh,0
	    asm mov dh,byte ptr cursor_y
	    asm mov dl,byte ptr cursor_x
	    asm int 0x10
	    asm mov ah,1
	    asm int 0x10

	} else {

	    int saved_x;

	    /* Save cursor position */

	    saved_x = cursor_x;

	    /* Emulate cursor in graphic mode */

	    if (visible)
		swap_colours ();

	    os_display_char (input_offs ? (unsigned char) *input_pointer : ' ');

	    if (visible)
		swap_colours ();

	    /* Restore cursor position */

	    cursor_x = saved_x;

	}

    /* Turn mouse pointer on */

    if ((h_flags & MOUSE_FLAG) && visible) {
	asm mov ax,1
	asm int 0x33
    }

}/* switch_cursor */

/*
 * os_cursor_on
 *
 * Make the cursor visible.
 *
 */

void os_cursor_on (void)
{

    cursor = 1;

}/* os_cursor_on */

/*
 * os_cursor_off
 *
 * Turn cursor off.
 *
 */

void os_cursor_off (void)
{

    cursor = 0;

}/* os_cursor_off */

/*
 * get_current_time
 *
 * Return the current system time in 1/10 seconds.
 *
 */

static long get_current_time (void)
{
    long time;

    /* Get the current time of day measured in

	 65536 / 1,193,180 = 0.054925493

       seconds. Multiply this value with

	 959 / 1746 = 0.54925544

       to get the current time in 0.1 seconds. */

    asm mov ah,0
    asm int 0x1a
    asm mov word ptr time,dx
    asm mov word ptr time + 2,cx

    return time * 959 / 1746;

}/* get_current_time */

/*
 * set_timer
 *
 * Set a time limit of timeout/10 seconds if timeout is not zero;
 * otherwise clear the time limit.
 *
 */

static void set_timer (int timeout)
{

    limit = timeout ? get_current_time () + timeout : 0;

}/* set_timer */

/*
 * time_limit_hit
 *
 * Return true if a previously set time limit has been exceeded.
 *
 */

static int out_of_time (void)
{

    if (limit) {

	long now = get_current_time ();

	if (now < 1L * 3600 * 10 && limit > 23L * 3600 * 10)
	    now += 24L * 3600 * 10;

	return now >= limit;

    } else return 0;

}/* out_of_time */

/*
 * get_key
 *
 * Read a keypress or a mouse click. Returns...
 *
 *	0 = time limit exceeded,
 *	8 = the backspace key,
 *	9 = the tabulator key,
 *      13 = the return key,
 *	27 = the escape key,
 *	32...126 = an ASCII character,
 *	129...132 = an arrow key,
 *	133...142 = a function key,
 *	145...154 = a number pad key,
 * 	EURO_MIN...EURO_MAX = a European letter,
 *	253 = mouse double click,
 *	254 = mouse single click,
 *	SPECIAL_KEY_MIN...SPECIAL_KEY_MAX = a special editing key,
 *      HOT_KEY_MIN...HOT_KEY_MAX = a hot key.
 *
 */

static int get_key (void)
{
    static byte arrow_key_map[] = {
	0x48, 0x50, 0x4b, 0x4d
    };
    static byte special_key_map[] = {
	0x47, 0x4f, 0x73, 0x74, 0x53, 0x52, 0x49, 0x51, 0x13
    };
    static byte hot_key_map[] = {
	0x13, 0x19, 0x1f, 0x16, 0x31, 0x2d, 0x20, 0x23
    };

    int key;

    /* Turn on cursor and mouse pointer */

    switch_cursor (1);

    /* Loop until a key was pressed */

    do {

	if (_bios_keybrd (_KEYBRD_READY)) {

	    word code = _bios_keybrd (_KEYBRD_READ);

	    if (byte0 (code)) {

		for (key = 145; key <= 154; key++)
		    if (byte0 (code) == key - 145 + 0x30 && byte1 (code) >= 0x10)
			goto exit_loop;		/* number pad key */

		for (key = EURO_MIN; key <= EURO_MAX; key++)
		    if (byte0 (code) == euro_map[key - EURO_MIN])
			goto exit_loop;		/* European letter */

		key = byte0 (code);

		if (key == 8)
		    goto exit_loop;		/* backspace key */
		if (key == 9)
		    goto exit_loop;		/* tabulator key */
		if (key == 13)
		    goto exit_loop;		/* return key */
		if (key == 27)
		    goto exit_loop;		/* escape key */
		if (key >= 32 && key <= 126)
		    goto exit_loop;		/* ASCII character */

	    } else {

		for (key = 129; key <= 132; key++)
		    if (byte1 (code) == arrow_key_map[key - 129])
			goto exit_loop;		/* arrow key */

		for (/* key is 133 */; key <= 142; key++)
		   if (byte1 (code) == key - 133 + 0x3b)
			goto exit_loop;		/* function key */

		for (key = HOT_KEY_MIN; key <= HOT_KEY_MAX; key++)
		    if (byte1 (code) == hot_key_map[key - HOT_KEY_MIN])
			goto exit_loop;		/* hot key */

		for (key = SPECIAL_KEY_MIN; key <= SPECIAL_KEY_MAX; key++)
		    if (byte1 (code) == special_key_map[key - SPECIAL_KEY_MIN])
			goto exit_loop;		/* special key */

	    }

	} else {

	    key = read_mouse ();

	    if (key == 253)
		goto exit_loop;			/* double mouse click */
	    if (key == 254)
		goto exit_loop;			/* single mouse click */

	}

	key = 0;

    } while (!out_of_time ());

exit_loop:

    /* Turn off mouse pointer and cursor */

    switch_cursor (0);

    /* Return key value */

    return key;

}/* get_key */

/*
 * cursor_left
 *
 * Move the cursor one character to the left.
 *
 */

static void cursor_left (void)
{

    if (!BUF_LEFT_END ()) {

	input_pointer--;
	input_offs++;

	cursor_x -= os_char_width ((unsigned char) *input_pointer);

    }

}/* cursor_left */

/*
 * cursor_right
 *
 * Move the cursor one character to the right.
 *
 */

static void cursor_right (void)
{

    if (!BUF_RIGHT_END ()) {

	cursor_x += os_char_width ((unsigned char) *input_pointer);

	input_pointer++;
	input_offs--;

    }

}/* cursor_right */

/*
 * first_char
 *
 * Move the cursor to the beginning of the input line.
 *
 */

static void first_char (void)
{

    while (!BUF_LEFT_END ())
	cursor_left ();

}/* first_char */

/*
 * last_char
 *
 * Move the cursor to the end of the input line.
 *
 */

static void last_char (void)
{

    while (!BUF_RIGHT_END ())
	cursor_right ();

}/* last_char */

/*
 * prev_word
 *
 * Move the cursor to the start of the previous word.
 *
 */

static void prev_word (void)
{

    do {

	cursor_left ();

	if (BUF_LEFT_END ())
	    return;

    } while (*input_pointer == ' ' || *(input_pointer - 1) != ' ');

}/* prev_word */

/*
 * next_word
 *
 * Move the cursor to the start of the next word.
 *
 */

static void next_word (void)
{

    do {

	cursor_right ();

	if (BUF_RIGHT_END ())
	    return;

    } while (*input_pointer == ' ' || *(input_pointer - 1) != ' ');

}/* next_word */

/*
 * erase_char
 *
 * Remove a character from the screen.
 *
 */

static void erase_char (int c)
{

    if (display != _MCGA_) {	/* overwrite using space characters */

	int spaces = os_char_width (c) / h_font_width;

	while (spaces--)
	    os_display_char (' ');

    } else {                    /* overwrite using invisible character */

	byte saved_fg = text_fg;

	text_fg = text_bg;
	os_display_char (c);
	text_fg = saved_fg;

    }

}/* erase_char */

/*
 * delete_char
 *
 * Delete the character below the cursor.
 *
 */

static void delete_char (void)
{
    int c = (unsigned char) *input_pointer;

    if (!BUF_RIGHT_END ()) {

	int saved_x = cursor_x;

	input_left += os_char_width (c);

	input_offs--;
	input_free++;

	memmove (input_pointer, input_pointer + 1, input_offs + 1);

	os_display_string (input_pointer);

	erase_char (c);

	cursor_x = saved_x;

    }

}/* delete_char */

/*
 * insert_char
 *
 * Insert a character into the input buffer.
 *
 */

static void insert_char (int c)
{

    if (overwrite) delete_char ();

	if (input_left >= os_char_width (c) && input_free) {

	int saved_x = cursor_x;

	input_left -= os_char_width (c);

	input_offs++;
	input_free--;

	memmove (input_pointer + 1, input_pointer, input_offs);

	*input_pointer = c;

	os_display_string (input_pointer);

	cursor_x = saved_x;

	cursor_right ();

    }

}/* insert_char */

/*
 * delete_left
 *
 * Delete the character to the left of the cursor.
 *
 */

static void delete_left (void)
{

    if (!BUF_LEFT_END ()) {
	cursor_left ();
	delete_char ();
    }

}/* delete_left */

/*
 * erase_input
 *
 * Clear the input line.
 *
 */

static void erase_input (void)
{

    last_char ();

    while (input_buffer != input_pointer)
	delete_left ();

}/* erase_input */

/*
 * insert_string
 *
 * Add a string of characters to the input line.
 *
 */

static void insert_string (const char *s)
{
    int c;

    while ((c = (unsigned char) *s++) != 0 && input_left >= os_char_width (c) && input_free)
       insert_char (c);

}/* insert_string */

/*
 * tabulator_key
 *
 * Complete the word at the end of the input line, if possible.
 *
 */

static void tabulator_key (void)
{
    char extension[10];

    if (BUF_RIGHT_END ()) {

	int status = completion (input_buffer, extension);

	insert_string (extension);

	if (status) os_beep (status);

    }

}/* tabulator_key */

/*
 * store_input
 *
 * Copy the current input line to the history buffer.
 *
 */

static void store_input (void)
{

    if (input_pointer - input_buffer >= HISTORY_MIN_ENTRY) {

	const char *ptr = input_buffer;

	last_char ();

	do {

	    if (history_latest_entry++ == HISTORY_BUFFER_SIZE - 1)
		history_latest_entry = 0;

	    history_buffer[history_latest_entry] = *ptr;

	} while (*ptr++);

    }

}/* store_input */

/*
 * truncate_line
 *
 * Truncate the input line to "history_prefix_len" characters.
 *
 */

static void truncate_line (void)
{

    last_char ();

    while (input_pointer - input_buffer > history_prefix_len)
	delete_left ();

}/* truncate_line */

/*
 * new_history_search
 *
 * Prepare a new history search.
 *
 */

static void new_history_search (void)
{

    history_prefix_len = strlen (input_buffer);
    history_current_entry = history_latest_entry;

}/* new_history_search */

/*
 * fetch_entry
 *
 * Copy the current history entry to the input buffer and check if it
 * matches the prefix in the input buffer.
 *
 */

static int fetch_entry (char *buf, int entry)
{
    int i = 0;

    char c;

    do {

	if (entry++ == HISTORY_BUFFER_SIZE - 1)
	    entry = 0;

	c = history_buffer[entry];

	if (i < history_prefix_len && input_buffer[i] != c)
	    return 0;

	buf[i++] = c;

    } while (c);

    return i > history_prefix_len && i > 1;

}/* fetch_entry */

/*
 * get_prev_entry
 *
 * Copy the previous history entry to the input buffer.
 *
 */

static void get_prev_entry (void)
{
    char buf[INPUT_BUFFER_SIZE];

    int i = history_current_entry;

    do {

	do {

	    if (i-- == 0)
		i = HISTORY_BUFFER_SIZE - 1;

	    if (i == history_latest_entry)
		return;

	} while (history_buffer[i]);

    } while (!fetch_entry (buf, i));

    truncate_line ();

    insert_string (buf + history_prefix_len);

    history_current_entry = i;

}/* get_prev_entry */

/*
 * get_next_entry
 *
 * Copy the next history entry to the input buffer.
 *
 */

static void get_next_entry (void)
{
    char buf[INPUT_BUFFER_SIZE];

    int i = history_current_entry;

    truncate_line ();

    do {

	do {

	    if (i == history_latest_entry)
		return;

	    if (i++ == HISTORY_BUFFER_SIZE - 1)
		i = 0;

	} while (history_buffer[i]);

	if (i == history_latest_entry)
	    goto no_further;

    } while (!fetch_entry (buf, i));

    insert_string (buf + history_prefix_len);

no_further:

    history_current_entry = i;

}/* get_next_entry */

/*
 * os_read_line
 *
 * Read a line of input from the keyboard into a buffer. The buffer
 * may already be primed with some text. In this case, the "initial"
 * text is already displayed on the screen. After the input action
 * is complete, the function returns with the terminating key value.
 * The length of the input should not exceed "max" characters plus
 * an extra 0 terminator.
 *
 * Terminating keys are the return key (13) and all function keys
 * (see the Specification of the Z-machine) which are accepted by
 * the is_terminator function. Mouse clicks behave like function
 * keys except that the mouse position is stored in global variables
 * "mouse_x" and "mouse_y" (top left coordinates are (1,1)).
 *
 * Furthermore, Frotz introduces some special terminating keys:
 *
 *     HOT_KEY_PLAYBACK (Alt-P)
 *     HOT_KEY_RECORD (Alt-R)
 *     HOT_KEY_SEED (Alt-S)
 *     HOT_KEY_UNDO (Alt-U)
 *     HOT_KEY_RESTART (Alt-N, "new game")
 *     HOT_KEY_QUIT (Alt-X, "exit game")
 *     HOT_KEY_DEBUGGING (Alt-D)
 *     HOT_KEY_HELP (Alt-H)
 *
 * If the timeout argument is not zero, the input gets interrupted
 * after timeout/10 seconds (and the return value is 0).
 *
 * The complete input line including the cursor must fit in "width"
 * screen units.
 *
 * The function may be called once again to continue after timeouts,
 * misplaced mouse clicks or hot keys. In this case the "continued"
 * flag will be set. This information can be useful if the interface
 * implements input line history.
 *
 * The screen is not scrolled after the return key was pressed. The
 * cursor is at the end of the input line when the function returns.
 *
 * Since Inform 2.2 the helper function "completion" can be called
 * to implement word completion (similar to tcsh under Unix).
 *
 */

int os_read_line (int max, char *buf, int timeout, int width, int continued)
{
    int c;

    overwrite = 0;

    /* Initialise input variables */

    input_buffer = buf;
    input_pointer = buf + strlen (buf);

    input_free = max - (input_pointer - input_buffer);
    input_left = width - os_string_width (buf) - os_char_width (' ');
    input_offs = 0;

    /* Reset history search status if this is a new input line */

    if (!continued)
	new_history_search ();

    /* Calculate time limit */

    set_timer (timeout);

    /* Loop until a terminator is found */

    do {

	c = get_key ();

	/* Backspace, return, escape and tab keys */

	if (c == 8)
	    delete_left ();
	if (c == 13)
	    store_input ();
	if (c == 27)
	    erase_input ();
	if (c == 9)
	    tabulator_key ();

	/* Plain characters (ASCII or European letters) */

	if (c >= 32 && c <= 126 || c >= EURO_MIN && c <= EURO_MAX)
	    insert_char (c);

	/* Editing keys */

	if (cwin == 0) {

	    if (c == 129)
		get_prev_entry ();
	    if (c == 130)
		get_next_entry ();
	    if (c == 131)
		cursor_left ();
	    if (c == 132)
		cursor_right ();

	    if (c >= 129 && c <= 132)
		c = -1;

	    if (c == SPECIAL_KEY_HOME)
		first_char ();
	    if (c == SPECIAL_KEY_END)
		last_char ();
	    if (c == SPECIAL_KEY_WORD_LEFT)
		prev_word ();
	    if (c == SPECIAL_KEY_WORD_RIGHT)
		next_word ();
	    if (c == SPECIAL_KEY_DELETE)
		delete_char ();
	    if (c == SPECIAL_KEY_INSERT)
		overwrite ^= 1;

	    if (c > 0)
		new_history_search ();

	}

	/* Page up/down are replacements for cursor up/down */

	if (c == SPECIAL_KEY_PAGE_UP)
	    c = 129;
	if (c == SPECIAL_KEY_PAGE_DOWN)
	    c = 130;

    } while (!BUF_RIGHT_END () || !is_terminator (c));

    /* Return terminating key */

    return c;

}/* os_read_line */

/*
 * os_read_key
 *
 * Read a single character from the keyboard (or a mouse click) and
 * return it. Input aborts after timeout/10 seconds.
 *
 */

int os_read_key (int timeout)
{
    int key;

    set_timer (timeout);

    do {

	key = get_key ();

    } while (key == 9 || key >= SPECIAL_KEY_MIN && key <= SPECIAL_KEY_MAX);

    return key;

}/* os_read_key */

/*
 * os_read_file_name
 *
 * Return the name of a file. Flag can be one of:
 *
 *    FILE_SAVE     - Save game file
 *    FILE_RESTORE  - Restore game file
 *    FILE_SCRIPT   - Transscript file
 *    FILE_RECORD   - Command file for recording
 *    FILE_PLAYBACK - Command file for playback
 *    FILE_SAVE_AUX - Save auxilary ("preferred settings") file
 *    FILE_LOAD_AUX - Load auxilary ("preferred settings") file
 *
 * The length of the file name is limited by MAX_FILE_NAME. Ideally
 * an interpreter should open a file requester to ask for the file
 * name. If it is unable to do that then this function should call
 * print_string and read_string to ask for a file name.
 *
 */

int os_read_file_name (char *file_name, const char *default_name, int flag)
{
    char *extension;
    FILE *fp;
    int terminal;
    int result;

    int saved_replay = istream_replay;
    int saved_record = ostream_record;

    /* Turn off playback and recording temporarily */

    istream_replay = 0;
    ostream_record = 0;

    /* Select appropriate extension */

    extension = ".aux";

    if (flag == FILE_SAVE || flag == FILE_RESTORE)
	extension = ".sav";
    if (flag == FILE_SCRIPT)
	extension = ".scr";
    if (flag == FILE_RECORD || flag == FILE_PLAYBACK)
	extension = ".rec";

    /* Input file name (reserve four bytes for a file name extension) */

    print_string ("Enter file name (\"");
    print_string (extension);
    print_string ("\" will be added).\nDefault is \"");
    print_string (default_name);
    print_string ("\": ");

    read_string (MAX_FILE_NAME - 4, file_name);

    /* Use the default name if nothing was typed */

    if (file_name[0] == 0)
	strcpy (file_name, default_name);

    if (strchr (file_name, '.') == NULL)
	strcat (file_name, extension);

    /* Make sure it is safe to use this file name */

    result = 1;

    if (flag == FILE_SAVE || flag == FILE_SAVE_AUX || flag == FILE_RECORD) {

	/* OK if the file does not exist */

	if ((fp = fopen (file_name, "rb")) == NULL)
	    goto finished;

	/* OK if this is a pseudo-file (like PRN, CON, NUL) */

	terminal = fp->flags & _F_TERM;

	fclose (fp);

	if (terminal)
	    goto finished;

	/* OK if user wants to overwrite */

	result = read_yes_or_no ("Overwrite existing file");

    }

finished:

    /* Restore state of playback and recording */

    istream_replay = saved_replay;
    ostream_record = saved_record;

    return result;

}/* os_read_file_name */
