/*
 * file "BCinput.c"
 *
 * Borland C front end, input functions
 *
 */

#include <bios.h>
#include <string.h>
#include <stdio.h>
#include "frotz.h"
#include "bcfrotz.h"

#ifndef HISTORY_BUFSIZE
#define HISTORY_BUFSIZE 500
#endif

extern bool is_terminator (zchar);

extern bool read_yes_or_no (const char *);
extern void read_string (int, zchar *);

extern int completion (const zchar *, zchar *);

static long limit = 0;

static struct {
    zchar buffer[HISTORY_BUFSIZE];
    int latest;
    int current;
    int prefix_len;
} history;

static struct {
    zchar *buffer;
    int pos;
    int length;
    int max_length;
    int width;
    int max_width;
} input;

static bool overwrite = FALSE;

int end_of_sound_flag;

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

static void swap_colours (void)
{
    byte temp;

    temp = text_fg;
    text_fg = text_bg;
    text_bg = temp;

}/* 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 (bool cursor)
{

    if (display <= _TEXT_) {

	/* Use hardware cursor in text mode */

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

	if (!cursor)
	    _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 = cursor_x;

	if (cursor)
	    swap_colours ();

	if (input.pos < input.length)
	    os_display_char (input.buffer[input.pos]);
	else
	    os_display_char (' ');

	if (cursor)
	    swap_colours ();

	cursor_x = saved_x;

    }

}/* switch_cursor */

/*
 * 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 != 0) ? get_current_time () + timeout : 0;

}/* set_timer */

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

static bool out_of_time (void)
{

    if (limit != 0) {

	long now = get_current_time ();

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

	return now >= limit;

    } else return FALSE;

}/* out_of_time */

/*
 * get_key
 *
 * Read a keypress or a mouse click. Returns...
 *
 *	ZC_TIME_OUT = time limit exceeded,
 *	ZC_BACKSPACE = the backspace key,
 *	ZC_RETURN = the return key,
 *	ZC_HKEY_MIN...ZC_HKEY_MAX = a hot key,
 *	ZC_ESCAPE = the escape key,
 *	ZC_ASCII_MIN...ZC_ASCII_MAX = ASCII character,
 *	ZC_ARROW_MIN...ZC_ARROW_MAX = an arrow key,
 *	ZC_FKEY_MIN...ZC_FKEY_MAX = a function key,
 *	ZC_NUMPAD_MIN...ZC_NUMPAD_MAX = a number pad key,
 *	ZC_SINGLE_CLICK = single mouse click,
 *	ZC_DOUBLE_CLICK = double mouse click,
 *	ZC_LATIN1_MIN+1...ZC_LATIN1_MAX = ISO Latin-1 character,
 *	SPECIAL_KEY_MIN...SPECIAL_KEY_MAX = a special editing key.
 *
 */

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

    int key;

    /* Loop until a key was pressed */

    if (cursor)
	switch_cursor (TRUE);

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

    do {

#ifdef SOUND_SUPPORT
	if (end_of_sound_flag)
	    end_of_sound ();
#endif

	if (_bios_keybrd (_KEYBRD_READY)) {

	    word code = _bios_keybrd (_KEYBRD_READ);
	    byte code0 = code;
	    byte code1 = code >> 8;

	    if (code0 != 0 && code0 != 9) {

		key = code0 - '0' + ZC_NUMPAD_MIN;
		if (key >= ZC_NUMPAD_MIN && key <= ZC_NUMPAD_MAX
		    && code1 >= 0x10)
		    goto exit_loop;

		for (key = ZC_LATIN1_MIN + 1; key <= ZC_LATIN1_MAX; key++)
		    if (code0 == latin1_to_ibm[key - ZC_LATIN1_MIN])
			goto exit_loop;

		key = code0;

		if (key == ZC_BACKSPACE)
		    goto exit_loop;
		if (key == ZC_RETURN)
		    goto exit_loop;
		if (key == ZC_ESCAPE)
		    goto exit_loop;
		if (key >= ZC_ASCII_MIN && key <= ZC_ASCII_MAX)
		    goto exit_loop;

	    } else {

		for (key = ZC_ARROW_MIN; key <= ZC_ARROW_MAX; key++)
		    if (code1 == arrow_key_map[key - ZC_ARROW_MIN])
			goto exit_loop;

		key = code1 - 0x3b + ZC_FKEY_MIN;
		if (key >= ZC_FKEY_MIN && key <= ZC_FKEY_MAX - 2)
		    goto exit_loop;

		for (key = ZC_HKEY_MIN; key <= ZC_HKEY_MAX; key++)
		    if (code1 == hot_key_map[key - ZC_HKEY_MIN])
			goto exit_loop;

		for (key = SPECIAL_KEY_MIN; key <= SPECIAL_KEY_MAX; key++)
		    if (code1 == special_key_map[key - SPECIAL_KEY_MIN])
			goto exit_loop;

	    }

	} else {

	    int clicks = read_mouse ();

	    if (clicks == 1)
		{ key = ZC_SINGLE_CLICK; goto exit_loop; }
	    if (clicks == 2)
		{ key = ZC_DOUBLE_CLICK; goto exit_loop; }

	}

	key = ZC_TIME_OUT;

    } while (!out_of_time ());

exit_loop:

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

    if (cursor)
	switch_cursor (FALSE);

    return key;

}/* get_key */

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

static void cursor_left (void)
{

    if (input.pos > 0)
	cursor_x -= os_char_width (input.buffer[--input.pos]);

}/* cursor_left */

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

static void cursor_right (void)
{

    if (input.pos < input.length)
	cursor_x += os_char_width (input.buffer[input.pos++]);

}/* cursor_right */

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

static void first_char (void)
{

    while (input.pos > 0)
	cursor_left ();

}/* first_char */

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

static void last_char (void)
{

    while (input.pos < input.length)
	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 (input.pos == 0)
	    return;

    } while (input.buffer[input.pos] == ' ' || input.buffer[input.pos - 1] != ' ');

}/* prev_word */

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

static void next_word (void)
{

    do {

	cursor_right ();

	if (input.pos == input.length)
	    return;

    } while (input.buffer[input.pos] == ' ' || input.buffer[input.pos - 1] != ' ');

}/* next_word */

/*
 * input_move
 *
 * Helper function to move parts of the input buffer:
 *
 *    newc != 0, oldc == 0: INSERT
 *    newc != 0, oldc != 0: OVERWRITE
 *    newc == 0, oldc != 0: DELETE
 *    newc == 0, oldc == 0: NO OPERATION
 *
 */

#define H(x) (x ? 1 : 0)

static void input_move (zchar newc, zchar oldc)
{
    int newwidth = (newc != 0) ? os_char_width (newc) : 0;
    int oldwidth = (oldc != 0) ? os_char_width (oldc) : 0;

    zchar *p = input.buffer + input.pos;

    int saved_x = cursor_x;

    int updated_width = input.width + newwidth - oldwidth;
    int updated_length = input.length + H (newc) - H (oldc);

    if (updated_width > input.max_width)
	return;
    if (updated_length > input.max_length)
	return;

    input.width = updated_width;
    input.length = updated_length;

    if (oldc != 0 && newc == 0)
	memmove (p, p + 1, updated_length - input.pos + 1);
    if (newc != 0 && oldc == 0)
	memmove (p + 1, p, updated_length - input.pos);

    if (newc != 0)
	*p = newc;

    os_display_string (p);

    switch_scrn_attr (TRUE);

    if (oldwidth > newwidth)

	os_erase_area (
	    cursor_y + 1,
	    cursor_x + 1,
	    cursor_y + h_font_height,
	    cursor_x + oldwidth - newwidth,
	    -1);

    switch_scrn_attr (FALSE);

    cursor_x = saved_x;

    if (newc != 0)
	cursor_right ();

}/* input_move */

#undef H(x)

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

static void delete_char (void)
{

    input_move (0, input.buffer[input.pos]);

}/* delete_char */

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

static void delete_left (void)
{

    if (input.pos > 0) {
	cursor_left ();
	delete_char ();
    }

}/* delete_left */

/*
 * truncate_line
 *
 * Truncate the input line to n characters.
 *
 */

static void truncate_line (int n)
{

    last_char ();

    while (input.length > n)
	delete_left ();

}/* truncate_line */

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

static void insert_char (zchar newc)
{
    zchar oldc = 0;

    if (overwrite)
	oldc = input.buffer[input.pos];

    input_move (newc, oldc);

}/* insert_char */

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

static void insert_string (const zchar *s)
{

    while (*s != 0) {

	if (input.length + 1 > input.max_length)
	    break;
	if (input.width + os_char_width (*s) > input.max_width)
	    break;

	insert_char (*s++);

    }

}/* insert_string */

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

static void tabulator_key (void)
{
    int status;

    if (input.pos == input.length) {

	zchar extension[10];

	status = completion (input.buffer, extension);
	insert_string (extension);

    } else status = 2;

    /* Beep if the completion was impossible or ambiguous */

    if (status != 0)
	os_beep (status);

}/* tabulator_key */

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

static void store_input (void)
{

    if (input.length >= HISTORY_MIN_ENTRY) {

	const zchar *ptr = input.buffer;

	do {

	    if (history.latest++ == HISTORY_BUFSIZE - 1)
		history.latest = 0;

	    history.buffer[history.latest] = *ptr;

	} while (*ptr++ != 0);

    }

}/* store_input */

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

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

    zchar c;

    do {

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

	c = history.buffer[entry];

	if (i < history.prefix_len && input.buffer[i] != c)
	    return FALSE;

	buf[i++] = c;

    } while (c != 0);

    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)
{
    zchar buf[INPUT_BUFFER_SIZE];

    int i = history.current;

    do {

	do {

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

	    if (i == history.latest)
		return;

	} while (history.buffer[i] != 0);

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

    truncate_line (history.prefix_len);

    insert_string (buf + history.prefix_len);

    history.current = i;

}/* get_prev_entry */

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

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

    int i = history.current;

    truncate_line (history.prefix_len);

    do {

	do {

	    if (i == history.latest)
		return;

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

	} while (history.buffer[i] != 0);

	if (i == history.latest)
	    goto no_further;

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

    insert_string (buf + history.prefix_len);

no_further:

    history.current = 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:
 *
 *     ZC_HKEY_PLAYBACK (Alt-P)
 *     ZC_HKEY_RECORD (Alt-R)
 *     ZC_HKEY_SEED (Alt-S)
 *     ZC_HKEY_UNDO (Alt-U)
 *     ZC_HKEY_RESTART (Alt-N, "new game")
 *     ZC_HKEY_QUIT (Alt-X, "exit game")
 *     ZC_HKEY_DEBUGGING (Alt-D)
 *     ZC_HKEY_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).
 *
 */

#define new_history_search() \
    { history.prefix_len = input.pos; history.current = history.latest; }

zchar os_read_line (int max, zchar *buf, int timeout, int width, int continued)
{
    int key = continued ? 9999 : 0;

    /* Initialise input variables */

    input.buffer = buf;
    input.pos = strlen ((char *) buf);
    input.length = strlen ((char *) buf);
    input.max_length = max;
    input.width = os_string_width (buf);
    input.max_width = width - os_char_width (' ');

    /* Calculate time limit */

    set_timer (timeout);

    /* Loop until a terminator is found */

    do {

	if (key != 9999)
	    new_history_search ();

	/* Get next key from mouse or keyboard */

	key = get_key (TRUE);

	if (key < ZC_ASCII_MIN || key > ZC_ASCII_MAX && key < ZC_LATIN1_MIN || key > ZC_LATIN1_MAX) {

	    /* Ignore time-outs if the cursor is not at end of the line */

	    if (key == ZC_TIME_OUT && input.pos < input.length)
		key = 9999;

	    /* Backspace, return and escape keys */

	    if (key == ZC_BACKSPACE)
		delete_left ();
	    if (key == ZC_RETURN)
		store_input ();
	    if (key == ZC_ESCAPE)
		truncate_line (0);

	    /* Editing keys */

	    if (cwin == 0) {

		if (key == ZC_ARROW_UP)
		    get_prev_entry ();
		if (key == ZC_ARROW_DOWN)
		    get_next_entry ();
		if (key == ZC_ARROW_LEFT)
		    cursor_left ();
		if (key == ZC_ARROW_RIGHT)
		    cursor_right ();

		if (key >= ZC_ARROW_MIN && key <= ZC_ARROW_MAX)
		    key = 9999;

		if (key == SPECIAL_KEY_HOME)
		    first_char ();
		if (key == SPECIAL_KEY_END)
		    last_char ();
		if (key == SPECIAL_KEY_WORD_LEFT)
		    prev_word ();
		if (key == SPECIAL_KEY_WORD_RIGHT)
		    next_word ();
		if (key == SPECIAL_KEY_DELETE)
		    delete_char ();
		if (key == SPECIAL_KEY_INSERT)
		    overwrite = !overwrite;
		if (key == SPECIAL_KEY_TAB)
		    tabulator_key ();

	    }

	    if (key == SPECIAL_KEY_PAGE_UP)
		key = ZC_ARROW_UP;
	    if (key == SPECIAL_KEY_PAGE_DOWN)
		key = ZC_ARROW_DOWN;

	} else insert_char (key);

    } while (key > 0xff || !is_terminator (key));

    last_char ();

    overwrite = FALSE;

    /* Return terminating key */

    return key;

}/* os_read_line */

#undef new_history_search()

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

zchar os_read_key (int timeout, bool cursor)
{
    int key;

    set_timer (timeout);

    do {

	key = get_key (cursor);

    } while (key > 0xff);

    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;
    bool terminal;
    bool result;

    bool saved_replay = istream_replay;
    bool saved_record = ostream_record;

    /* Turn off playback and recording temporarily */

    istream_replay = FALSE;
    ostream_record = FALSE;

    /* 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, (zchar *) 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 = TRUE;

    /* OK if the file is opened for reading */

    if (flag != FILE_SAVE && flag != FILE_SAVE_AUX && flag != FILE_RECORD)
	goto finished;

    /* 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 */
