/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "glk/jacl/jacl.h"
#include "glk/jacl/csv.h"
#include "glk/jacl/types.h"
#include "glk/jacl/language.h"
#include "glk/jacl/prototypes.h"
#include "glk/jacl/version.h"

namespace Glk {
namespace JACL {

int convert_to_utf32(unsigned char *text);

uint                status_width, status_height;

schanid_t           sound_channel[8] = { nullptr, nullptr, nullptr, nullptr,
										 nullptr, nullptr, nullptr, nullptr
									   };

event_t             *cancelled_event;

extern struct csv_parser parser_csv;

extern char         text_buffer[];
extern const char   *word[];
extern short int    quoted[];
extern short int    punctuated[];
extern int          wp;

extern int          custom_error;
extern int          interrupted;

extern int          jpp_error;

extern int          it;
extern int          them[];
extern int          her;
extern int          him;

extern int          oops_word;

#ifdef WINGLK
struct  string_type *resolved_string;
#endif

char            include_directory[81] = "\0";
char            temp_directory[81] = "\0";
char            data_directory[81] = "\0";
char            special_prompt[81] = "\n: \0";
char            file_prompt[5] = ": \0";
char            bookmark[81] = "\0";
char            walkthru[81] = "\0";

char            function_name[81];

extern char default_function[84];
char            override_[81];

char            temp_buffer[1024];
char            error_buffer[1024];
unsigned char   chunk_buffer[4096];
#ifndef NOUNICODE
glui32          chunk_buffer_uni[4096];
#endif
char            proxy_buffer[1024];

char            oops_buffer[1024];
char            oopsed_current[1024];
char            last_command[1024];
const char      *blank_command = "blankjacl\0";
const char      *current_command = (const char *)nullptr;
char            command_buffer[1024];
#ifndef NOUNICODE
glui32          command_buffer_uni[1024];
#endif
char            players_command[1024];

int             walkthru_running = FALSE;

int             start_of_last_command;
int             start_of_this_command;

int             objects, integers, functions, strings;
int             jpp_error = FALSE;

/* A STREAM FOR THE GAME FILE, WHEN IT'S OPEN. */
strid_t         game_stream = nullptr;

/* THE STREAM FOR OPENING UP THE ARCHIVE CONTAINING GRAPHICS AND SOUND */
strid_t             blorb_stream;

/* A FILE REFERENCE FOR THE TRANSCRIPT FILE. */
static frefid_t script_fref = nullptr;
/* A STREAM FOR THE TRANSCRIPT FILE, WHEN IT'S OPEN. */
static strid_t script_stream = nullptr;

int             noun[4];
int             player = 0;

int             noun3_backup;
int             player_backup = 0;

int             variable_contents;
int             oec;
int            *object_element_address,
			   *object_backup_address;

short int       spaced = TRUE;

int             delay = 0;

/* START OF GLK STUFF */

/* POINTERS TO THE GLK WINDOWS */
winid_t mainwin = nullptr;
winid_t statuswin = nullptr;
winid_t promptwin = nullptr;
winid_t inputwin = nullptr;
winid_t current_window = nullptr;

/* POINTERS TO THE WINDOWS STREAMS */
strid_t mainstr = nullptr;
strid_t statusstr = nullptr;
strid_t promptstr = nullptr;
strid_t inputstr = nullptr;

/* END OF GLK STUFF */

char            user_id[] = "local";
char            prefix[81] = "\0";
char            blorb[81] = "\0";
char            game_path[256] = "\0";
char            game_file[256] = "\0";
char            processed_file[256] = "\0";

struct object_type *object[MAX_OBJECTS];
struct integer_type *integer_table = nullptr;
struct cinteger_type *cinteger_table = nullptr;
struct window_type *window_table = nullptr;
struct attribute_type *attribute_table = nullptr;
struct string_type *string_table = nullptr;
struct string_type *cstring_table = nullptr;
struct function_type *function_table = nullptr;
struct function_type *executing_function = nullptr;
struct command_type *completion_list = nullptr;
struct word_type *grammar_table = nullptr;
struct synonym_type *synonym_table = nullptr;
struct filter_type *filter_table = nullptr;

// Forward declarations
static void word_check();
static void version_info();


void glk_main() {
	int             index;

	override_[0] = 0;

	/* ALLOC AN EVENT TO STORE A CANCELLED EVENT IN */
	if ((cancelled_event = (event_t *) malloc(sizeof(event_t))) == nullptr)
		outofmem();

	/* CREATE style_User1 FOR USE IN THE STATUS LINE */
	g_vm->glk_stylehint_set(wintype_TextGrid, style_User1, stylehint_ReverseColor, 1);
	g_vm->glk_stylehint_set(wintype_TextBuffer, style_User2, stylehint_ReverseColor, 1);

	/* OPEN THE MAIN WINDOW THE GLK WINDOWS */
	mainwin = g_vm->glk_window_open(nullptr, 0, 0, wintype_TextBuffer, 1);

	if (!mainwin) {
		/* IT'S POSSIBLE THAT THE MAIN WINDOW FAILED TO OPEN. THERE's
		 * NOTHING WE CAN DO WITHOUT IT, SO EXIT. */
		return;
	} else {
		/* GET A REFERENCE TO mainwin's STREAM */
		mainstr = g_vm->glk_window_get_stream(mainwin);
	}

	/* SET THE CURRENT OUTPUT STREAM TO PRINT TO IT. */
	jacl_set_window(mainwin);

	/* OPEN A THIRD WINDOW: A TEXT GRID, BELOW THE MAIN WINDOW, ONE LINE
	 * HIGH. THIS IS THE WINDOW TO DISPLAY THE COMMAND PROMPT IN */
	//promptwin = g_vm->glk_window_open(mainwin, winmethod_Below | winmethod_Fixed,
	//    3, wintype_TextBuffer, 0);

	/* SET THIS TO DETERMINE THE SYSTEM OF INPUT TO USE */
	//inputwin = promptwin;
	inputwin = mainwin;

	if (jpp_error) {
		/* THERE WAS AN ERROR DURING PREPROCESSING. NOW THAT THERE IS AN
		 * OPEN GLK WINDOW, OUTPUT THE ERROR MESSAGE AND EXIT */
		log_error(error_buffer, FALSE);
		terminate(200);
		return;
	}

	// INTIALISE THE CSV PARSER
	csv_init(&parser_csv, CSV_APPEND_NULL);

	/* NO PREPROCESSOR ERRORS, LOAD THE GAME FILE */
	read_gamefile();

	execute("+bootstrap");

	// OPEN A SECOND WINDOW: A TEXT GRID, ABOVE THE MAIN WINDOW, ONE LINE
	// HIGH. IT IS POSSIBLE THAT THIS WILL FAIL ALSO, BUT WE ACCEPT THAT.
	statuswin = g_vm->glk_window_open(mainwin, winmethod_Above | winmethod_Fixed,
	                                  0, wintype_TextGrid, 0);

	// GET A REFERENCE TO statuswin's STREAM
	if (statuswin != nullptr) {
		statusstr = g_vm->glk_window_get_stream(statuswin);
	}

#ifdef WINGLK
	if ((resolved_string = cstring_resolve("game_title")) != NULL) {
		wing_vm->glk_window_set_title(resolved_string->value);
	} else {
		Common::sprintf_s(temp_buffer, "JACL v%d.%d.%d ", J_VERSION, J_RELEASE, J_BUILD);
		wing_vm->glk_window_set_title(temp_buffer);
	}
#endif

	if (SOUND_SUPPORTED->value) {
		/* CREATE THE EIGHT SOUND CHANNELS */
		for (index = 0; index < 8; index++) {
			sound_channel[index] = g_vm->glk_schannel_create(0);
		}
	}

	jacl_set_window(mainwin);

	execute("+intro");

	if (object[2] == nullptr) {
		log_error(CANT_RUN, PLUS_STDERR);
		terminate(43);
		return;
	}

	if (g_vm->shouldQuit()) // terminate() could be called by now when game data is bad
		return;

	/* DUMMY RETRIEVE OF 'HERE' FOR TESTING OF GAME STATE */
	get_here();

	eachturn();

	/* TOP OF COMMAND LOOP */
	while (!g_vm->shouldQuit()) {
		int gotline;
		event_t ev;

		custom_error = FALSE;

		jacl_set_window(mainwin);

		execute("+bottom");

		status_line();

		if (current_command != nullptr) {
			Common::strcpy_s(last_command, current_command);
		}

		if (inputwin == promptwin) {
			g_vm->glk_window_clear(promptwin);
			jacl_set_window(inputwin);
		}

		// If loading a savegame from the launcher, do it now
		if (g_vm->loadingSavegame()) {
			// Load the game
			if (g_vm->loadLauncherSavegame()) {
				// Do a look action
				const uint32 LOOK[5] = { 'l', 'o', 'o', 'k', 0 };
				Common::copy(LOOK, LOOK + 5, command_buffer_uni);
				ev.val1 = 4;
			} else {
				continue;
			}
		} else {
			/* OUTPUT THE CUSTOM COMMAND PROMPT */
			write_text(string_resolve("command_prompt")->value);

#ifdef NOUNICODE
			g_vm->glk_request_line_event(inputwin, command_buffer, 255, 0);
#else
			g_vm->glk_request_line_event_uni(inputwin, command_buffer_uni, 255, 0);
#endif

			jacl_set_window(inputwin);

			gotline = FALSE;

			while (!gotline) {
				/* GRAB AN EVENT. */
				g_vm->glk_select(&ev);
				if (g_vm->shouldQuit())
					return;

				switch (ev.type) {

				case evtype_LineInput:
					if (ev.window == inputwin) {
						gotline = TRUE;
						jacl_set_window(mainwin);
						/* REALLY THE EVENT CAN *ONLY* BE FROM MAINWIN,
						 * BECAUSE WE NEVER REQUEST LINE INPUT FROM THE
						 * STATUS WINDOW. BUT WE DO A PARANOIA TEST,
						 * BECAUSE COMMANDBUF IS ONLY FILLED IF THE LINE
						 * EVENT COMES FROM THE MAINWIN REQUEST. IF THE
						 * LINE EVENT COMES FROM ANYWHERE ELSE, WE IGNORE
						 * IT. */
					}
					break;

				case evtype_SoundNotify:
					/* A SOUND HAS FINISHED PLAYING CALL +sound_finished
					 * WITH THE RESOURCE NUMBER AS THE FIRST ARGUMENT
					 * AND THE CHANNEL NUMBER AS THE SECOND ARGUMENT */
					Common::sprintf_s(temp_buffer, "+sound_finished<%d<%d", (int) ev.val1, (int) ev.val2 - 1);
					execute(temp_buffer);
					break;

				case evtype_Timer:
					/* A TIMER EVENT IS TRIGGERED PERIODICALLY IF THE GAME
					 * REQUESTS THEM. THIS SIMPLY EXECUTES THE FUNCTION
					 * +timer WHICH IS LIKE +eachturn EXCEPT IT DOESN'T
					 * WAIT FOR THE PLAYER TO TYPE A COMMAND */

					jacl_set_window(mainwin);
					execute("+timer");
					break;

				case evtype_Arrange:
					/* WINDOWS HAVE CHANGED SIZE, SO WE HAVE TO REDRAW THE
					 * STATUS WINDOW. */
					status_line();
					break;

				default:
					break;
				}
			}
		}
		// THE PLAYER'S INPUT WILL BE UTF-32. CONVERT IT TO UTF-8 AND NULL TERMINATE IT
#ifndef NOUNICODE
		convert_to_utf8(command_buffer_uni, ev.val1);
#endif

		current_command = command_buffer;

		/* SET ALL THE OUTPUT TO GO TO mainwin NOW THE COMMAND HAS BEEN READ */
		if (inputwin == promptwin) {
			jacl_set_window(mainwin);
			write_text(string_resolve("command_prompt")->value);
			g_vm->glk_set_style(style_Input);
			write_text(current_command);
			g_vm->glk_set_style(style_Normal);
			write_text("^");
		}

		execute("+top");

		index = 0;

		if (*current_command) {
			while (*(current_command + index) && index < 1024) {
				if (*(current_command + index) == '\r' || *(current_command + index) == '\n') {
					break;
				} else {
					text_buffer[index] = *(current_command + index);
					index++;
				}
			}
		}

		text_buffer[index] = 0;

		if (text_buffer[0] == 0) {
			/* NO COMMAND WAS SPECIFIED, FILL THE COMMAND IN AS 'blankjacl'
			 * FOR THE GAME TO PROCESS AS DESIRED */
			Common::strcpy_s(text_buffer, 1024, "blankjacl");
			current_command = blank_command;
		}

		command_encapsulate();
		jacl_truncate();

		index = 0;

		/* SET THE INTEGER INTERRUPTED TO FALSE. IF THIS IS SET TO
		 * TRUE BY ANY COMMAND, FURTHER PROCESSING WILL STOP */
		INTERRUPTED->value = FALSE;

		interrupted = FALSE;

		if (word[0] != nullptr) {
			if (strcmp(word[0], "undo")) {
				/* COMMAND DOES NOT EQUAL undo */
				save_game_state();
			}

			if (word[0][0] == '*') {
				if (script_stream) {
					write_text(cstring_resolve("COMMENT_RECORDED")->value);
				} else {
					write_text(cstring_resolve("COMMENT_IGNORED")->value);
				}
			} else {
				/* COMMAND IS NOT NULL, START PROCESSING IT */
				preparse();
			}
		} else {
			/* NO COMMAND WAS SPECIFIED, FILL THE COMMAND IN AS 'blankjacl'
			 * FOR THE GAME TO PROCESS AS DESIRED */
			Common::strcpy_s(text_buffer, 1024, "blankjacl");
			command_encapsulate();
			preparse();
		}
	}
}

void preparse() {
	int position;

	// THE INTERRUPTED VARIABLE IS USED TO STOP LATER ACTIONS IN A COMMAND
	// IF ANY ONE
	while (word[wp] != nullptr && INTERRUPTED->value == FALSE) {
		//printf("--- preparse %s\n", word[wp]);
		// PROCESS THE CURRENT COMMAND
		// CREATE THE command STRINGS FROM THIS POINT ONWARDS SO THE VERB OF
		// THE CURRENT COMMAND IS ALWAYS command[0].

		clear_cstring("command");

		position = wp;

		while (word[position] != nullptr && strcmp(word[position], cstring_resolve("THEN_WORD")->value)) {
			add_cstring("command", word[position]);
			position++;
		};

		// PROCESS THE COMMAND
		word_check();

		/* THE PREVIOUS COMMAND HAS FINISHED, LOOK FOR ANOTHER COMMAND */
		while (word[wp] != nullptr) {
			if (word[wp] != nullptr && !strcmp(word[wp], cstring_resolve("THEN_WORD")->value)) {
				wp++;
				break;
			}
			wp++;
		}
	}
}

void word_check() {
	int index;

	/* REMEMBER THE START OF THIS COMMAND TO SUPPORT 'oops' AND 'again' */
	start_of_this_command = wp;
	//printf("--- command starts at %d\n", start_of_this_command);

	/* START CHECKING THE PLAYER'S COMMAND FOR SYSTEM COMMANDS */
	if (!strcmp(word[wp], cstring_resolve("QUIT_WORD")->value) || !strcmp(word[wp], "q")) {
		if (execute("+quit_game") == FALSE) {
			TIME->value = FALSE;
			write_text(cstring_resolve("SURE_QUIT")->value);
			if (get_yes_or_no()) {
				newline();
				execute("+score");
				terminate(0);
				return;
			} else {
				write_text(cstring_resolve("RETURN_GAME")->value);
			}
		}
	} else if (!strcmp(word[wp], cstring_resolve("RESTART_WORD")->value)) {
		if (execute("+restart_game") == FALSE) {
			TIME->value = FALSE;
			write_text(cstring_resolve("SURE_RESTART")->value);
			if (get_yes_or_no()) {
				write_text(cstring_resolve("RESTARTING")->value);
				restart_game();
				g_vm->glk_window_clear(current_window);
				execute("+intro");
				eachturn();
			} else {
				write_text(cstring_resolve("RETURN_GAME")->value);
			}
		}
	} else if (!strcmp(word[wp], cstring_resolve("UNDO_WORD")->value)) {
		if (execute("+undo_move") == FALSE) {
			undoing();
		}
	} else if (!strcmp(word[wp], cstring_resolve("OOPS_WORD")->value) || !strcmp(word[wp], "o")) {
		//printf("--- oops word is %d\n", oops_word);
		if (word[++wp] != nullptr) {
			if (oops_word == -1) {
				if (TOTAL_MOVES->value == 0) {
					write_text(cstring_resolve("NO_MOVES")->value);
					TIME->value = FALSE;
				} else {
					write_text(cstring_resolve("CANT_CORRECT")->value);
					TIME->value = FALSE;
				}
			} else {
				Common::strcpy_s(oops_buffer, word[wp]);
				Common::strcpy_s(text_buffer, 1024, last_command);
				command_encapsulate();
				//printf("--- trying to replace %s with %s\n", word[oops_word], oops_buffer);
				jacl_truncate();
				word[oops_word] = (char *)&oops_buffer;

				/* BUILD A PLAIN STRING REPRESENTING THE NEW COMMAND */
				oopsed_current[0] = 0;
				index = 0;

				while (word[index] != nullptr) {
					if (oopsed_current[0] != 0) {
						Common::strcat_s(oopsed_current, " ");
					}

					Common::strcat_s(oopsed_current, word[index]);

					index++;
				}

				current_command = oopsed_current;
				//printf("--- current command is: %s\n", current_command);

				/* PROCESS THE FIXED COMMAND ONLY */
				wp = start_of_last_command;
				word_check();
			}
		} else {
			write_text(cstring_resolve("BAD_OOPS")->value);
			TIME->value = FALSE;
		}
	} else if (!strcmp(word[wp], cstring_resolve("AGAIN_WORD")->value) || !strcmp(word[wp], "g")) {
		if (TOTAL_MOVES->value == 0) {
			write_text(cstring_resolve("NO_MOVES")->value);
			TIME->value = FALSE;
		} else if (last_command[0] == 0) {
			write_text(cstring_resolve("NOT_CLEVER")->value);
			TIME->value = FALSE;
		} else {
			Common::strcpy_s(text_buffer, 1024, last_command);
			current_command = last_command;
			command_encapsulate();
			jacl_truncate();
			//printf("--- command started at %d\n", start_of_last_command);
			wp = start_of_last_command;
			word_check();
		}
	} else if (!strcmp(word[wp], cstring_resolve("SCRIPT_WORD")->value) || !strcmp(word[wp], "transcript")) {
		scripting();
	} else if (!strcmp(word[wp], cstring_resolve("UNSCRIPT_WORD")->value)) {
		if (!script_stream) {
			write_text(cstring_resolve("SCRIPTING_ALREADY_OFF")->value);
		} else {
			/* Close the file. */
			g_vm->glk_put_string_stream(script_stream, "\nEND OF A TRANSCRIPT\n");
			g_vm->glk_stream_close(script_stream, nullptr);
			write_text(cstring_resolve("SCRIPTING_OFF")->value);
			script_stream = nullptr;
		}
	} else if (!strcmp(word[wp], cstring_resolve("WALKTHRU_WORD")->value)) {
		walking_thru();
	} else if (!strcmp(word[wp], cstring_resolve("INFO_WORD")->value) || !strcmp(word[wp], "version")) {
		version_info();
		write_text("you can redistribute it and/or modify it under the ");
		write_text("terms of the GNU General Public License as published by ");
		write_text("the Free Software Foundation; either version 3 of the ");
		write_text("License, or any later version.^^");
		write_text("This program is distributed in the hope that it will be ");
		write_text("useful, but WITHOUT ANY WARRANTY; without even the ");
		write_text("implied warranty of MERCHANTABILITY or FITNESS FOR A ");
		write_text("PARTICULAR PURPOSE. See the GNU General Public License ");
		write_text("for more details.^^");
		write_text("You should have received a copy of the GNU General ");
		write_text("Public License along with this program; if not, write ");
		write_text("to the Free Software Foundation, Inc., 675 Mass Ave, ");
		write_text("Cambridge, MA 02139, USA.^^");
		Common::sprintf_s(temp_buffer, "OBJECTS DEFINED:   %d^", objects);
		write_text(temp_buffer);
		TIME->value = FALSE;
	} else {
		/* NO WORD HAS BEEN MARKED AS AN ERROR YET*/
		oops_word = -1;

		/* THIS IS NOT A SYSTEM COMMAND, CALL parser TO PROCESS THE COMMAND */
		parser();
	}

	start_of_last_command = start_of_this_command;
}

void version_info() {
	char            buffer[80];

	Common::sprintf_s(buffer, "JACL Interpreter v%d.%d.%d ", J_VERSION, J_RELEASE,
	        J_BUILD);
	write_text(buffer);
	Common::sprintf_s(buffer, "/ %d object.^", MAX_OBJECTS);
	write_text(buffer);
	write_text("Copyright (c) 1992-2010 Stuart Allen.^^");
}

void save_game_state() {
	/* THIS FUNCTION MAKES AN IN-MEMORY COPY OF THE GAME STATE AFTER EACH
	 * OF THE PLAYER'S COMMANDS SO THE 'undo' COMMAND CAN BE USED */
	int             index,
	                counter;

	struct integer_type *current_integer = integer_table;
	struct function_type *current_function = function_table;

	do {
		current_function->call_count_backup = current_function->call_count;
		current_function = current_function->next_function;
	} while (current_function != nullptr);

	do {
		current_integer->value_backup = current_integer->value;
		current_integer = current_integer->next_integer;
	} while (current_integer != nullptr);

	for (index = 1; index <= objects; index++) {
		if (object[index]->nosave)
			continue;

		for (counter = 0; counter < 16; counter++) {
			object[index]->integer_backup[counter] =
			    object[index]->integer[counter];
		}

		object[index]->attributes_backup = object[index]->attributes;
		object[index]->user_attributes_backup = object[index]->user_attributes;
	}

	player_backup = player;
	noun3_backup = noun[3];
}

int save_interaction() {
	if (g_vm->saveGame().getCode() == Common::kNoError) {
		return (TRUE);
	} else {
		write_text(cstring_resolve("CANT_SAVE")->value);
		return (FALSE);
	}
}

void restore_game_state() {
	/* THIS FUNCTION IS CALLED AS A RESULT OF THE PLAYER USING THE 'undo'
	 * COMMAND */
	int             index,
	                counter;

	struct integer_type *current_integer = integer_table;
	struct function_type *current_function = function_table;

	do {
		current_function->call_count = current_function->call_count_backup;
		current_function = current_function->next_function;
	} while (current_function != nullptr);


	do {
		current_integer->value = current_integer->value_backup;
		current_integer = current_integer->next_integer;
	} while (current_integer != nullptr);

	for (index = 1; index <= objects; index++) {
		if (object[index]->nosave)
			continue;

		for (counter = 0; counter < 16; counter++)
			object[index]->integer[counter] =
			    object[index]->integer_backup[counter];

		object[index]->attributes = object[index]->attributes_backup;
		object[index]->user_attributes = object[index]->user_attributes_backup;
	}

	player = player_backup;
	noun[3] = noun3_backup;

	write_text(cstring_resolve("MOVE_UNDONE")->value);
	object[HERE]->attributes &= ~1;
	execute("+top");
	execute("+look_around");
	execute("+bottom");
	TIME->value = FALSE;
}

void write_text(const char *string_buffer) {
	int index, length;
	if (g_vm->loadingSavegame())
		return;

	if (!strcmp(string_buffer, "tilde")) {
		g_vm->glk_put_string("~");
		return;
	} else if (!strcmp(string_buffer, "caret")) {
		g_vm->glk_put_string("^");
		return;
	}

	length = strlen(string_buffer);

	for (index = 0; index < length; index++) {
		if (*(string_buffer + index) == '^') {
			chunk_buffer[index] = '\n';
		} else if (*(string_buffer + index) == '~') {
			chunk_buffer[index] = '\"';
		} else {
			chunk_buffer[index] = *(string_buffer + index);
		}
	}

	chunk_buffer[index] = 0;

	/* PRINT THE CONTENTS OF string_buffer */
#ifdef NOUNICODE
	g_vm->glk_put_string(chunk_buffer);
#else
	chunk_buffer_uni[(glui32) convert_to_utf32(chunk_buffer)] = 0;
	g_vm->glk_put_string_uni(chunk_buffer_uni);
#endif
}

void jacl_sleep(unsigned int mseconds) {
	g_system->delayMillis(mseconds);
}

void status_line() {
	int cursor, index;
	winid_t pair_window;

	if (!statuswin) {
		return;
	} else {
		// THERE IS AN EXISTING STATUS WINDOW, MAKE SURE A NEW SIZE HASN'T BEEN
		// REQUESTED
		g_vm->glk_window_get_size(statuswin, &status_width, &status_height);
		if (status_height != (uint)integer_resolve("status_window")->value) {
			// HEIGHT HAS CHANGED, UPDATE THE WINDOW
			pair_window = g_vm->glk_window_get_parent(statuswin);
			g_vm->glk_window_set_arrangement(pair_window, winmethod_Above | winmethod_Fixed, integer_resolve("status_window")->value, statuswin);
			g_vm->glk_window_get_size(statuswin, &status_width, &status_height);
		}
	}

	if (status_height == 0) {
		// THE STATUS WINDOW CAN'T BE CLOSED, ONLY SET TO HAVE A HEIGHT OF ZERO
		return;
	}

	jacl_set_window(statuswin);
	g_vm->glk_window_clear(statuswin);

	if (execute("+update_status_window") == FALSE) {
		g_vm->glk_set_style(style_User1);

		/* DISPLAY THE INVERSE STATUS LINE AT THE TOP OF THE SCREEN */
		for (index = 0; index < (int)status_width; index++) {
			temp_buffer[index] = ' ';
		}
		temp_buffer[index] = 0;
		write_text(temp_buffer);

		/* PRINT THE LOCATION'S TITLE ON THE LEFT. */
		g_vm->glk_window_move_cursor(statuswin, 1, 0);
		write_text(sentence_output(HERE, TRUE));

		/* BUILD THE SCORE/ MOVES STRING */
		temp_buffer[0] = 0;
		Common::sprintf_s(temp_buffer, "Score: %d  Moves: %d", SCORE->value, TOTAL_MOVES->value);

		cursor = status_width - strlen(temp_buffer);
		cursor--;
		g_vm->glk_window_move_cursor(statuswin, cursor, 0);
		write_text(temp_buffer);
	}

	jacl_set_window(mainwin);

}

void newline() {
	/* START A NEW LINE ON THE SCREEN */
	write_text("\n");
}

void more(const char *message) {
	jacl_set_window(inputwin);

	if (inputwin == promptwin) {
		g_vm->glk_window_clear(promptwin);
		newline();
	}

	g_vm->glk_set_style(style_Emphasized);
	write_text(message);
	g_vm->glk_set_style(style_Normal);

	(void)get_key();

	if (inputwin == mainwin) newline();
}

int get_key() {
	event_t ev;

	g_vm->glk_request_char_event(inputwin);

	while (!g_vm->shouldQuit()) {
		g_vm->glk_select(&ev);

		switch (ev.type) {
		case evtype_CharInput:
			if (ev.window == inputwin) {
				return (ev.val1);
			}
			break;

		default:
			break;
		}
	}

	return 0;
}

int get_number(int insist, int low, int high) {
	char *cx;
	char commandbuf[256];
	int response;
	int gotline;
	event_t ev;

	status_line();

	Common::sprintf_s(temp_buffer, cstring_resolve("TYPE_NUMBER")->value, low, high);

	/* THIS LOOP IS IDENTICAL TO THE MAIN COMMAND LOOP IN g_vm->glk_main(). */

	while (1) {
		if (inputwin == promptwin) {
			g_vm->glk_window_clear(promptwin);
			jacl_set_window(inputwin);
		}

		write_text(temp_buffer);
		jacl_set_window(mainwin);

		g_vm->glk_request_line_event(inputwin, commandbuf, 255, 0);

		gotline = FALSE;
		while (!gotline && g_vm->shouldQuit()) {
			g_vm->glk_select(&ev);

			switch (ev.type) {
			case evtype_LineInput:
				if (ev.window == inputwin) {
					gotline = TRUE;
				}
				break;

			case evtype_Arrange:
				status_line();
				break;

			default:
				break;
			}
		}

		commandbuf[ev.val1] = '\0';
		for (cx = commandbuf; *cx == ' '; cx++) { };

		if (validate(cx)) {
			response = atoi(cx);
			if (response >= low && response <= high) {
				return (response);
			}
		}

		if (!insist) {
			return (-1);
		} else {
			write_text(cstring_resolve("INVALID_SELECTION")->value);
		}
	}
}

void get_string(char *string_buffer) {
	char *cx;
	char commandbuf[256];
	int gotline;
	event_t ev;

	status_line();

	/* THIS LOOP IS IDENTICAL TO THE MAIN COMMAND LOOP IN g_vm->glk_main(). */

	if (inputwin == promptwin) {
		g_vm->glk_window_clear(promptwin);
		jacl_set_window(inputwin);
	}

	jacl_set_window(mainwin);

	g_vm->glk_request_line_event(inputwin, commandbuf, 255, 0);

	gotline = FALSE;
	while (!gotline && !g_vm->shouldQuit()) {
		g_vm->glk_select(&ev);

		switch (ev.type) {
		case evtype_LineInput:
			if (ev.window == inputwin) {
				gotline = TRUE;
			}
			break;

		case evtype_Arrange:
			status_line();
			break;

		default:
			break;
		}
	}

	commandbuf[ev.val1] = '\0';
	for (cx = commandbuf; *cx == ' '; cx++) { };

	// COPY UP TO 255 BYTES OF THE ENTERED TEXT INTO THE SUPPLIED STRING
	strncpy(string_buffer, cx, 255);
}

int get_yes_or_no() {
	char *cx;
	char commandbuf[256];
	int gotline;
	event_t ev;

	status_line();

	/* THIS LOOP IS IDENTICAL TO THE MAIN COMMAND LOOP IN g_vm->glk_main(). */

	while (1) {
		if (inputwin == promptwin) {
			g_vm->glk_window_clear(promptwin);
			jacl_set_window(inputwin);
		}

		write_text(cstring_resolve("YES_OR_NO")->value);
		jacl_set_window(mainwin);

		g_vm->glk_request_line_event(inputwin, commandbuf, 255, 0);

		gotline = FALSE;
		while (!gotline && !g_vm->shouldQuit()) {

			g_vm->glk_select(&ev);

			switch (ev.type) {
			case evtype_LineInput:
				if (ev.window == inputwin) {
					gotline = TRUE;
				}
				break;

			case evtype_Arrange:
				status_line();
				break;

			default:
				break;
			}
		}

		commandbuf[ev.val1] = '\0';
		for (cx = commandbuf; *cx == ' '; cx++) { };

		// PUSH THE FIRST NON-SPACE CHARACTER TO LOWER FOR COMPARISON
		// WITH CONSTANT
		*cx = tolower(*cx);

		if (*cx == cstring_resolve("YES_WORD")->value[0]) {
			return TRUE;
		} else if (*cx == cstring_resolve("NO_WORD")->value[0]) {
			return FALSE;
		}

	}
}

char get_character(const char *message) {
	char *cx;
	char commandbuf[256];
	int gotline;
	event_t ev;

	status_line();

	/* THIS LOOP IS IDENTICAL TO THE MAIN COMMAND LOOP IN g_vm->glk_main(). */

	while (!g_vm->shouldQuit()) {
		if (inputwin == promptwin) {
			g_vm->glk_window_clear(promptwin);
			jacl_set_window(inputwin);
		}

		write_text(message);
		g_vm->glk_request_line_event(inputwin, commandbuf, 255, 0);
		jacl_set_window(mainwin);

		gotline = FALSE;
		while (!gotline && !g_vm->shouldQuit()) {

			g_vm->glk_select(&ev);

			switch (ev.type) {
			case evtype_LineInput:
				if (ev.window == inputwin) {
					gotline = TRUE;
				}
				break;

			case evtype_Arrange:
				status_line();
				break;

			default:
				break;
			}
		}

		commandbuf[ev.val1] = '\0';
		for (cx = commandbuf; *cx == ' '; cx++) { };

		return (*cx);
	}


	return '\0';
}

strid_t open_glk_file(uint usage, uint mode, char *filename) {

	frefid_t    file_reference;
	strid_t     stream_reference;

	file_reference = g_vm->glk_fileref_create_by_name(usage, filename, 0);

	if (file_reference) {
		stream_reference = g_vm->glk_stream_open_file(file_reference, (FileMode)mode, 0);

		if (stream_reference) {
			/* WE'RE DONE WITH THE FILE REFERENCE NOW THAT THE STREAM
			 * HAS BEEN SUCCESSFULLY OPENED */
			g_vm->glk_fileref_destroy(file_reference);

			return (stream_reference);
		}
	}

	return (strid_t) nullptr;
}

void scripting() {
	if (script_stream) {
		write_text(cstring_resolve("SCRIPTING_ALREADY_ON")->value);
		return;
	}

	/* IF WE'VE TURNED ON SCRIPTING BEFORE, USE THE SAME FILE REFERENCE;
	 * OTHERWISE, PROMPT THE PLAYER FOR A FILE. */
	if (!script_fref) {
		script_fref = g_vm->glk_fileref_create_by_prompt(
		                  fileusage_Transcript | fileusage_TextMode,
		                  filemode_WriteAppend, 0);
		if (!script_fref) {
			write_text(cstring_resolve("CANT_WRITE_SCRIPT")->value);
			return;
		}
	}

	/* OPEN THE TRANSCRIPT FILE */
	script_stream = g_vm->glk_stream_open_file(script_fref, filemode_WriteAppend, 0);

	if (!script_stream) {
		write_text(cstring_resolve("CANT_WRITE_SCRIPT")->value);
		return;
	}
	write_text(cstring_resolve("SCRIPTING_ON")->value);
	g_vm->glk_window_set_echo_stream(mainwin, script_stream);
	g_vm->glk_put_string_stream(script_stream, "TRANSCRIPT OF: ");
	g_vm->glk_put_string_stream(script_stream, cstring_resolve("game_title")->value);
	g_vm->glk_put_string_stream(script_stream, "\n");
}

void undoing() {
	if (TOTAL_MOVES->value && strcmp(last_command, cstring_resolve("UNDO_WORD")->value)) {
		restore_game_state();
	} else {
		write_text(cstring_resolve("NO_UNDO")->value);
		TIME->value = FALSE;
	}
}

void walking_thru() {
	int result, index;

	int length;
	char script_line[81];

	/* A FILE REFERENCE FOR THE WALKTHRU FILE. */
	frefid_t walkthru_fref = nullptr;

	/* A STREAM FOR THE WALKTHRU FILE, WHEN IT'S OPEN. */
	strid_t walkthru_stream = nullptr;

	walkthru_fref = g_vm->glk_fileref_create_by_prompt(fileusage_Data | fileusage_TextMode, filemode_Read, 0);

	if (!walkthru_fref) {
		write_text(cstring_resolve("ERROR_READING_WALKTHRU")->value);
		return;
	}

	/* OPEN THE WALKTHRU FILE */
	walkthru_stream = g_vm->glk_stream_open_file(walkthru_fref, filemode_Read, 0);

	if (!walkthru_stream) {
		write_text(cstring_resolve("ERROR_READING_WALKTHRU")->value);
		return;
	}

	walkthru_running = TRUE;

	/* ISSUE ALL THE COMMANDS STORE IN THE WALKTHRU FILE */

	/* WE'RE DONE WITH THE FILE REFERENCE NOW THAT THE STREAM
	 * HAS BEEN SUCCESSFULLY OPENED */
	g_vm->glk_fileref_destroy(walkthru_fref);

	result = g_vm->glk_get_line_stream(walkthru_stream, text_buffer, (glui32) 80);

	/* SET TO LOWER CASE AND STRIP NEWLINES */
	length = strlen(text_buffer);
	for (index = 0; index < length; index++) {
		if (text_buffer[index] == '\r' ||
		        text_buffer[index] == '\n') {
			text_buffer[index] = 0;
			break;
		}
	}

	Common::strcpy_s(script_line, text_buffer);

	while (result && INTERRUPTED->value == FALSE) {
		/* THERE COULD BE A LOT OF PROCESSING GOING ON HERE BEFORE GETTING
		 * TO THE NEXT EVENT LOOP SO CALL g_vm->glk_tick AFTER EACH LINE READ */
		g_vm->glk_tick();
		command_encapsulate();
		jacl_truncate();
		if (word[0] != nullptr) {
			custom_error = FALSE;

			execute("+bottom");

			write_text(string_resolve("command_prompt")->value);
			g_vm->glk_set_style(style_Input);
			write_text(script_line);
			newline();
			g_vm->glk_set_style(style_Normal);

			execute("+top");

			preparse();
		}

		result = g_vm->glk_get_line_stream(walkthru_stream, text_buffer, (glui32) 80);

		/* SET TO LOWER CASE AND STRIP NEWLINES */
		length = strlen(text_buffer);
		for (index = 0; index < length; index++) {
			if (text_buffer[index] == '\r' ||
			        text_buffer[index] == '\n') {
				text_buffer[index] = 0;
				break;
			}
		}

		Common::strcpy_s(script_line, text_buffer);
	}

	/* CLOSE THE STREAM */
	g_vm->glk_stream_close(walkthru_stream, nullptr);

	/* FINISH UP */
	walkthru_running = FALSE;
}

int restore_interaction() {
	if (g_vm->loadGame().getCode() != Common::kNoError) {
		write_text(cstring_resolve("CANT_RESTORE")->value);
		return (FALSE);
	} else {
		return (TRUE);
	}
}

glui32 glk_get_bin_line_stream(strid_t file_stream, char *buffer, glui32 max_length) {
	int character = 0;

	int index = 0;

	character = g_vm->glk_get_char_stream(file_stream);
	while (character != -1 && index < (int) max_length) {
		*(buffer + index) = (char) character;
		index++;
		if (character == (int) '\n' ||
		        character == (int) '\r') {
			break;
		}
		character = g_vm->glk_get_char_stream(file_stream);
	};

	*(buffer + index) = 0;

	return ((glui32) index);
}

void jacl_set_window(winid_t new_window) {
	current_window = new_window;
	g_vm->glk_set_window(new_window);
}

#ifdef READLINE
char **command_completion(const char *text, int start, int end) {
	/* READLINE TAB COMPLETION CODE */
	char **options;

	options = (const char **) NULL;

	if (start == 0)
		options = completion_matches(text, verb_generator);
	else
		options = completion_matches(text, object_generator);

	return (options);
}
#endif

const char *object_generator(const char *text, int state) {
	static int len;
	static struct command_type *now;
	struct command_type *to_send;
	struct name_type *current_name = (struct name_type *) nullptr;

	/* IF THIS IS A NEW WORD TO COMPLETE, INITIALIZE NOW. THIS INCLUDES
	SAVING THE LENGTH OF TEXT FOR EFFICIENCY, AND INITIALIZING THE INDEX
	VARIABLE TO 0. */

	if (!state) {
		/* BUILD THE LIST */
		int index;
		completion_list = nullptr;

		/* LOOP THROUGH ALL THE OBJECTS AND SEE IF THEY ARE IN
		   THE CURRENT LOCATION */
		for (index = 1; index <= objects; index++) {
			if (parent_of(HERE, index, UNRESTRICT) && !(object[index]->attributes & NO_TAB)) {
				/* LOOP THROUGH ALL THE OBJECTS NAMES AND
				   THEM TO THE COMPLETION LIST */
				current_name = object[index]->first_name;
				while (current_name) {
					add_word(current_name->name);
					current_name = current_name->next_name;
				}
			}
		}
		now = completion_list;
		len = strlen(text);
	}

	while (now != nullptr) {
		if (!strncmp(text, now->word, len)) {
			to_send = now;
			now = now->next;
			return ((const char *)to_send->word);
		}
		now = now->next;
	}

	return (const char *)nullptr;
}

const char *verb_generator(const char *text, int state) {
	static int len;
	static struct command_type *now;
	struct command_type *to_send;
	struct word_type *pointer;

	/* IF THIS IS A NEW WORD TO COMPLETE, INITIALIZE NOW. THIS INCLUDES
	SAVING THE LENGTH OF TEXT FOR EFFICIENCY, AND INITIALIZING THE INDEX
	VARIABLE TO 0. */

	if (!state) {
		/* BUILD THE LIST */
		completion_list = nullptr;

		pointer = grammar_table;
		while (pointer != nullptr) {
			add_word(pointer->word);
			pointer = pointer->next_sibling;
		}

		add_word("walkthru");

		now = completion_list;
		len = strlen(text);
	}

	while (now != nullptr) {
		if (!strncmp(text, now->word, len)) {
			to_send = now;
			now = now->next;

			/* MALLOC A COPY AND RETURN A POINTER TO THE COPY */
			return ((const char *)to_send->word);
		}
		now = now->next;
	}

	return (const char *)nullptr;
}

/* ADD A COPY OF STRING TO A LIST OF STRINGS IF IT IS NOT
   ALREADY IN THE LIST. THIS IS FOR THE USE OF READLINE */
void add_word(const char *newWord) {
	static struct command_type *current_word = nullptr;
	struct command_type *previous_word = nullptr;

	/* DON'T ADD WORDS SUCH AS *present TO THE LIST*/
	if (*newWord == '*')
		return;

	if (current_word != nullptr)
		previous_word = current_word;

	current_word = (struct command_type *) malloc(sizeof(struct command_type));

	if (current_word != nullptr) {
		if (completion_list == nullptr) {
			completion_list = current_word;
		}

		strncpy(current_word->word, newWord, 40);
		current_word->word[40] = 0;
		current_word->next = nullptr;

		if (previous_word != nullptr) {
			previous_word->next = current_word;
		}
	}
}

void convert_to_utf8(glui32 *text, int len) {
	int i, k;

	i = 0;
	k = 0;

	/*convert UTF-32 to UTF-8 */
	while (i < len) {
		if (text[i] < 0x80) {
			command_buffer[k] = text[i];
			k++;
		} else if (text[i] < 0x800) {
			command_buffer[k  ] = (0xC0 | ((text[i] & 0x7C0) >> 6));
			command_buffer[k + 1] = (0x80 | (text[i] & 0x03F));
			k = k + 2;
		} else if (text[i] < 0x10000) {
			command_buffer[k  ] = (0xE0 | ((text[i] & 0xF000) >> 12));
			command_buffer[k + 1] = (0x80 | ((text[i] & 0x0FC0) >>  6));
			command_buffer[k + 2] = (0x80 | (text[i] & 0x003F));
			k = k + 3;
		} else if (text[i] < 0x200000) {
			command_buffer[k  ] = (0xF0 | ((text[i] & 0x1C0000) >> 18));
			command_buffer[k + 1] = (0x80 | ((text[i] & 0x03F000) >> 12));
			command_buffer[k + 2] = (0x80 | ((text[i] & 0x000FC0) >>  6));
			command_buffer[k + 3] = (0x80 | (text[i] & 0x00003F));
			k = k + 4;
		} else {
			command_buffer[k] = '?';
			k++;
		}
		i++;
	}

	/* null-terminated string */
	command_buffer[k] = '\0';
}

#ifndef NOUNICODE
int convert_to_utf32(unsigned char *text) {
	int text_len;
	int rlen;

	if (!text) {
		return 0;
	}

	text_len = strlen((const char *)text);

	if (!text_len) {
		return 0;
	}

	rlen = (int) parse_utf8(text, text_len, chunk_buffer_uni, text_len);

	return (rlen);
}

glui32 parse_utf8(unsigned char *buf, glui32 buflen, glui32 *out, glui32 outlen) {
	glui32 pos = 0;
	glui32 outpos = 0;
	glui32 res;
	glui32 val0, val1, val2, val3;

	while (outpos < outlen) {
		if (pos >= buflen)
			break;

		val0 = buf[pos++];

		if (val0 < 0x80) {
			res = val0;
			out[outpos++] = res;
			continue;
		}

		if ((val0 & 0xe0) == 0xc0) {
			if (pos + 1 > buflen) {
				warning("incomplete two-byte character");
				break;
			}
			val1 = buf[pos++];
			if ((val1 & 0xc0) != 0x80) {
				warning("malformed two-byte character");
				break;
			}
			res = (val0 & 0x1f) << 6;
			res |= (val1 & 0x3f);
			out[outpos++] = res;
			continue;
		}

		if ((val0 & 0xf0) == 0xe0) {
			if (pos + 2 > buflen) {
				warning("incomplete three-byte character");
				break;
			}
			val1 = buf[pos++];
			val2 = buf[pos++];
			if ((val1 & 0xc0) != 0x80) {
				warning("malformed three-byte character");
				break;
			}
			if ((val2 & 0xc0) != 0x80) {
				warning("malformed three-byte character");
				break;
			}
			res = (((val0 & 0xf) << 12)  & 0x0000f000);
			res |= (((val1 & 0x3f) << 6) & 0x00000fc0);
			res |= (((val2 & 0x3f))    & 0x0000003f);
			out[outpos++] = res;
			continue;
		}

		if ((val0 & 0xf0) == 0xf0) {
			if ((val0 & 0xf8) != 0xf0) {
				warning("malformed four-byte character");
				break;
			}
			if (pos + 3 > buflen) {
				warning("incomplete four-byte character");
				break;
			}
			val1 = buf[pos++];
			val2 = buf[pos++];
			val3 = buf[pos++];
			if ((val1 & 0xc0) != 0x80) {
				warning("malformed four-byte character");
				break;
			}
			if ((val2 & 0xc0) != 0x80) {
				warning("malformed four-byte character");
				break;
			}
			if ((val3 & 0xc0) != 0x80) {
				warning("malformed four-byte character");
				break;
			}
			res = (((val0 & 0x7) << 18)   & 0x1c0000);
			res |= (((val1 & 0x3f) << 12) & 0x03f000);
			res |= (((val2 & 0x3f) << 6)  & 0x000fc0);
			res |= (((val3 & 0x3f))     & 0x00003f);
			out[outpos++] = res;
			continue;
		}

		warning("malformed character");
	}

	return outpos;
}
#endif

} // End of namespace JACL
} // End of namespace Glk
