/* 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/glulx/glulx.h"

namespace Glk {
namespace Glulx {

/* This code is actually very general; it could work for almost any
   32-bit VM which remotely resembles Glulx or the Z-machine in design.

   To be precise, we make the following assumptions:

   - An argument list is an array of 32-bit values, which can represent
	 either integers or addresses.
   - We can read or write to a 32-bit integer in VM memory using the macros
	 ReadMemory(addr) and WriteMemory(addr), where addr is an address
	 taken from the argument list.
   - A character array is a sequence of bytes somewhere in VM memory.
	 The array can be turned into a C char array by the macro
	 CaptureCArray(addr, len), and released by ReleaseCArray().
	 The passin, passout hints may be used to avoid unnecessary copying.
   - An integer array is a sequence of integers somewhere in VM memory.
	 The array can be turned into a C integer array by the macro
	 CaptureIArray(addr, len), and released by ReleaseIArray().
	 These macros are responsible for fixing byte-order and alignment
	 (if the C ABI does not match the VM's). The passin, passout hints
	 may be used to avoid unnecessary copying.
   - A Glk object array is a sequence of integers in VM memory. It is
	 turned into a C pointer array (remember that C pointers may be more
	 than 4 bytes!) The pointer array is allocated by
	 CapturePtrArray(addr, len, objclass) and released by ReleasePtrArray().
	 Again, the macros handle the conversion.
   - A Glk structure (such as event_t) is a set of integers somewhere
	 in VM memory, which can be read and written with the macros
	 ReadStructField(addr, fieldnum) and WriteStructField(addr, fieldnum).
	 The fieldnum is an integer (from 0 to 3, for event_t.)
   - A VM string can be turned into a C-style string with the macro
	 ptr = DecodeVMString(addr). After the string is used, this code
	 calls ReleaseVMString(ptr), which should free any memory that
	 DecodeVMString allocates.
   - A VM Unicode string can be turned into a zero-terminated array
	 of 32-bit integers, in the same way, with DecodeVMUstring
	 and ReleaseVMUstring.

	 To work this code over for a new VM, just diddle the macros.
*/

static gidispatch_rock_t classtable_register(void *obj, uint objclass) {
	return g_vm->glulxe_classtable_register(obj, objclass);
}

static void classtable_unregister(void *obj, uint objclass, gidispatch_rock_t objrock) {
	g_vm->glulxe_classtable_unregister(obj, objclass, objrock);
}

static gidispatch_rock_t retained_register(void *array, uint len, const char *typecode) {
	return g_vm->glulxe_retained_register(array, len, typecode);
}

static void retained_unregister(void *array, uint len, const char *typecode, gidispatch_rock_t objrock) {
	g_vm->glulxe_retained_unregister(array, len, typecode, objrock);
}

void Glulx::glkopInit() {
	library_select_hook = nullptr;
	arrays = nullptr;
	num_classes = 0;
	classes = nullptr;
}

bool Glulx::init_dispatch() {
	int ix;

	/* Set up the game-ID hook. (This is ifdeffed because not all Glk
	   libraries have this call.) */
#ifdef GI_DISPA_GAME_ID_AVAILABLE
	gidispatch_set_game_id_hook(&get_game_id);
#endif /* GI_DISPA_GAME_ID_AVAILABLE */

	/* Allocate the class hash tables. */
	num_classes = gidispatch_count_classes();
	classes = (classtable_t **)glulx_malloc(num_classes  * sizeof(classtable_t *));
	if (!classes)
		return false;

	for (ix = 0; ix < num_classes; ix++) {
		classes[ix] = new_classtable((glulx_random() % (uint)(101)) + 1);
		if (!classes[ix])
			return false;
	}

	/* Set up the two callbacks. */
	gidispatch_set_object_registry(&classtable_register, &classtable_unregister);
	gidispatch_set_retained_registry(&retained_register, &retained_unregister);

	/* If the library supports autorestore callbacks, set those up too.
	   (These are only used in iosglk, currently.) */
#ifdef GIDISPATCH_AUTORESTORE_REGISTRY
	gidispatch_set_autorestore_registry(&glulxe_array_locate, &glulxe_array_restore);
#endif /* GIDISPATCH_AUTORESTORE_REGISTRY */

	return true;
}

uint Glulx::perform_glk(uint funcnum, uint numargs, uint *arglist) {
	uint retval = 0;

	switch (funcnum) {
	/* To speed life up, we implement commonly-used Glk functions
	   directly -- instead of bothering with the whole prototype
	   mess. */

	case 0x0047: /* stream_set_current */
		if (numargs != 1)
			goto WrongArgNum;
		glk_stream_set_current(find_stream_by_id(arglist[0]));
		break;
	case 0x0048: /* stream_get_current */
		if (numargs != 0)
			goto WrongArgNum;
		retval = find_id_for_stream(glk_stream_get_current());
		break;
	case 0x0080: /* put_char */
		if (numargs != 1)
			goto WrongArgNum;
		glk_put_char(arglist[0] & 0xFF);
		break;
	case 0x0081: /* put_char_stream */
		if (numargs != 2)
			goto WrongArgNum;
		glk_put_char_stream(find_stream_by_id(arglist[0]), arglist[1] & 0xFF);
		break;
	case 0x00C0: /* select */
		/* call a library hook on every glk_select() */
		if (library_select_hook)
			library_select_hook(arglist[0]);
		/* but then fall through to full dispatcher, because there's no real
		   need for speed here */
		goto FullDispatcher;
	case 0x00A0: /* char_to_lower */
		if (numargs != 1)
			goto WrongArgNum;
		retval = glk_char_to_lower(arglist[0] & 0xFF);
		break;
	case 0x00A1: /* char_to_upper */
		if (numargs != 1)
			goto WrongArgNum;
		retval = glk_char_to_upper(arglist[0] & 0xFF);
		break;
	case 0x0128: /* put_char_uni */
		if (numargs != 1)
			goto WrongArgNum;
		glk_put_char_uni(arglist[0]);
		break;
	case 0x012B: /* put_char_stream_uni */
		if (numargs != 2)
			goto WrongArgNum;
		glk_put_char_stream_uni(find_stream_by_id(arglist[0]), arglist[1]);
		break;

WrongArgNum:
		error("Wrong number of arguments to Glk function.");
		break;

FullDispatcher:
	default: {
		/* Go through the full dispatcher prototype foo. */
		const char *proto, *cx;
		dispatch_splot_t splot;
		int argnum, argnum2;

		/* Grab the string. */
		proto = gidispatch_prototype(funcnum);
		if (!proto)
			error("Unknown Glk function.");

		splot.varglist = arglist;
		splot.numvargs = numargs;
		splot.retval = &retval;

		/* The work goes in four phases. First, we figure out how many
		   arguments we want, and allocate space for the Glk argument
		   list. Then we go through the Glulx arguments and load them
		   into the Glk list. Then we call. Then we go through the
		   arguments again, unloading the data back into Glulx memory. */

		/* Phase 0. */
		prepare_glk_args(proto, &splot);

		/* Phase 1. */
		argnum = 0;
		cx = proto;
		parse_glk_args(&splot, &cx, 0, &argnum, 0, 0);

		/* Phase 2. */
		gidispatch_call(funcnum, argnum, splot.garglist);

		// WORKAROUND: For stream_open_file calls, for savegame handling
		// we need to store a copy of what the savegame description was,
		// for use when we actually generate the savefile contents
		if (funcnum == 0x42) {
			frefid_t fref = (frefid_t)splot.garglist[0]._opaqueref;
			_savegameDescription = fref->_description;
		}

		/* Phase 3. */
		argnum2 = 0;
		cx = proto;
		unparse_glk_args(&splot, &cx, 0, &argnum2, 0, 0);
		if (argnum != argnum2)
			error("Argument counts did not match.");

		break;
	}
	}

	return retval;
}

const char *Glulx::read_prefix(const char *cx, int *isref, int *isarray, int *passin, int *passout,
								int *nullok, int *isretained,  int *isreturn) {
	*isref = false;
	*passin = false;
	*passout = false;
	*nullok = true;
	*isarray = false;
	*isretained = false;
	*isreturn = false;
	while (1) {
		if (*cx == '<') {
			*isref = true;
			*passout = true;
		} else if (*cx == '>') {
			*isref = true;
			*passin = true;
		} else if (*cx == '&') {
			*isref = true;
			*passout = true;
			*passin = true;
		} else if (*cx == '+') {
			*nullok = false;
		} else if (*cx == ':') {
			*isref = true;
			*passout = true;
			*nullok = false;
			*isreturn = true;
		} else if (*cx == '#') {
			*isarray = true;
		} else if (*cx == '!') {
			*isretained = true;
		} else {
			break;
		}
		cx++;
	}
	return cx;
}

void Glulx::prepare_glk_args(const char *proto, dispatch_splot_t *splot) {
	static gluniversal_t *garglist = nullptr;
	static int garglist_size = 0;

	int ix;
	int numwanted, numvargswanted, maxargs;
	const char *cx;

	cx = proto;
	numwanted = 0;
	while (*cx >= '0' && *cx <= '9') {
		numwanted = 10 * numwanted + (*cx - '0');
		cx++;
	}
	splot->numwanted = numwanted;

	maxargs = 0;
	numvargswanted = 0;
	for (ix = 0; ix < numwanted; ix++) {
		int isref, passin, passout, nullok, isarray, isretained, isreturn;
		cx = read_prefix(cx, &isref, &isarray, &passin, &passout, &nullok,
		                 &isretained, &isreturn);
		if (isref) {
			maxargs += 2;
		} else {
			maxargs += 1;
		}
		if (!isreturn) {
			if (isarray) {
				numvargswanted += 2;
			} else {
				numvargswanted += 1;
			}
		}

		if (*cx == 'I' || *cx == 'C') {
			cx += 2;
		} else if (*cx == 'Q') {
			cx += 2;
		} else if (*cx == 'S' || *cx == 'U') {
			cx += 1;
		} else if (*cx == '[') {
			int refdepth, nwx;
			cx++;
			nwx = 0;
			while (*cx >= '0' && *cx <= '9') {
				nwx = 10 * nwx + (*cx - '0');
				cx++;
			}
			maxargs += nwx; /* This is *only* correct because all structs contain
						 plain values. */
			refdepth = 1;
			while (refdepth > 0) {
				if (*cx == '[')
					refdepth++;
				else if (*cx == ']')
					refdepth--;
				cx++;
			}
		} else {
			error("Illegal format string.");
		}
	}

	if (*cx != ':' && *cx != '\0')
		error("Illegal format string.");

	splot->maxargs = maxargs;

	if (splot->numvargs != numvargswanted)
		error("Wrong number of arguments to Glk function.");

	if (garglist && garglist_size < maxargs) {
		glulx_free(garglist);
		garglist = nullptr;
		garglist_size = 0;
	}
	if (!garglist) {
		garglist_size = maxargs + 16;
		garglist = (gluniversal_t *)glulx_malloc(garglist_size
		           * sizeof(gluniversal_t));
	}
	if (!garglist)
		error("Unable to allocate storage for Glk arguments.");

	splot->garglist = garglist;
}

void Glulx::parse_glk_args(dispatch_splot_t *splot, const char **proto, int depth, int *argnumptr,
							uint subaddress, int subpassin) {
	const char *cx;
	int ix, argx;
	int gargnum, numwanted;
	void *opref;
	gluniversal_t *garglist;
	uint *varglist;

	garglist = splot->garglist;
	varglist = splot->varglist;
	gargnum = *argnumptr;
	cx = *proto;

	numwanted = 0;
	while (*cx >= '0' && *cx <= '9') {
		numwanted = 10 * numwanted + (*cx - '0');
		cx++;
	}

	for (argx = 0, ix = 0; argx < numwanted; argx++, ix++) {
		char typeclass;
		int skipval;
		int isref, passin, passout, nullok, isarray, isretained, isreturn;
		cx = read_prefix(cx, &isref, &isarray, &passin, &passout, &nullok,
		                 &isretained, &isreturn);

		typeclass = *cx;
		cx++;

		skipval = false;
		if (isref) {
			if (!isreturn && varglist[ix] == 0) {
				if (!nullok)
					error("Zero passed invalidly to Glk function.");
				garglist[gargnum]._ptrflag = false;
				gargnum++;
				skipval = true;
			} else {
				garglist[gargnum]._ptrflag = true;
				gargnum++;
			}
		}
		if (!skipval) {
			uint thisval;

			if (typeclass == '[') {

				parse_glk_args(splot, &cx, depth + 1, &gargnum, varglist[ix], passin);

			} else if (isarray) {
				/* definitely isref */

				switch (typeclass) {
				case 'C':
					/* This test checks for a giant array length, which is
					   deprecated. It displays a warning and cuts it down to
					   something reasonable. Future releases of this interpreter
					   may remove this test and go on to verify_array_addresses(),
					   which treats this case as a fatal error. */
					if (varglist[ix + 1] > endmem
					        || varglist[ix] + varglist[ix + 1] > endmem) {
						nonfatal_warning_i("Memory access was much too long -- perhaps a print_to_array call with only one argument", varglist[ix + 1]);
						varglist[ix + 1] = endmem - varglist[ix];
					}
					verify_array_addresses(varglist[ix], varglist[ix + 1], 1);
					garglist[gargnum]._array = CaptureCArray(varglist[ix], varglist[ix + 1], passin);
					gargnum++;
					ix++;
					garglist[gargnum]._uint = varglist[ix];
					gargnum++;
					cx++;
					break;
				case 'I':
					/* See comment above. */
					if (varglist[ix + 1] > endmem / 4
					        || varglist[ix + 1] > (endmem - varglist[ix]) / 4) {
						nonfatal_warning_i("Memory access was much too long -- perhaps a print_to_array call with only one argument", varglist[ix + 1]);
						varglist[ix + 1] = (endmem - varglist[ix]) / 4;
					}
					verify_array_addresses(varglist[ix], varglist[ix + 1], 4);
					garglist[gargnum]._array = CaptureIArray(varglist[ix], varglist[ix + 1], passin);
					gargnum++;
					ix++;
					garglist[gargnum]._uint = varglist[ix];
					gargnum++;
					cx++;
					break;
				case 'Q':
					/* This case was added after the giant arrays were deprecated,
					   so we don't bother to allow for that case. We just verify
					   the length. */
					verify_array_addresses(varglist[ix], varglist[ix + 1], 4);
					garglist[gargnum]._array = CapturePtrArray(varglist[ix], varglist[ix + 1], (*cx - 'a'), passin);
					gargnum++;
					ix++;
					garglist[gargnum]._uint = varglist[ix];
					gargnum++;
					cx++;
					break;
				default:
					error("Illegal format string.");
					break;
				}
			} else {
				/* a plain value or a reference to one. */

				if (isreturn) {
					thisval = 0;
				} else if (depth > 0) {
					/* Definitely not isref or isarray. */
					if (subpassin)
						thisval = ReadStructField(subaddress, ix);
					else
						thisval = 0;
				} else if (isref) {
					if (passin)
						thisval = ReadMemory(varglist[ix]);
					else
						thisval = 0;
				} else {
					thisval = varglist[ix];
				}

				switch (typeclass) {
				case 'I':
					if (*cx == 'u')
						garglist[gargnum]._uint = (uint)(thisval);
					else if (*cx == 's')
						garglist[gargnum]._sint = (int)(thisval);
					else
						error("Illegal format string.");
					gargnum++;
					cx++;
					break;
				case 'Q':
					if (thisval) {
						opref = classes_get(*cx - 'a', thisval);
						if (!opref) {
							error("Reference to nonexistent Glk object.");
						}
					} else {
						opref = nullptr;
					}
					garglist[gargnum]._opaqueref = opref;
					gargnum++;
					cx++;
					break;
				case 'C':
					if (*cx == 'u')
						garglist[gargnum]._uch = (unsigned char)(thisval);
					else if (*cx == 's')
						garglist[gargnum]._sch = (signed char)(thisval);
					else if (*cx == 'n')
						garglist[gargnum]._ch = (char)(thisval);
					else
						error("Illegal format string.");
					gargnum++;
					cx++;
					break;
				case 'S':
					garglist[gargnum]._charstr = DecodeVMString(thisval);
					gargnum++;
					break;
#ifdef GLK_MODULE_UNICODE
				case 'U':
					garglist[gargnum]._unicharstr = DecodeVMUstring(thisval);
					gargnum++;
					break;
#endif /* GLK_MODULE_UNICODE */
				default:
					error("Illegal format string.");
					break;
				}
			}
		} else {
			/* We got a null reference, so we have to skip the format element. */
			if (typeclass == '[') {
				int numsubwanted, refdepth;
				numsubwanted = 0;
				while (*cx >= '0' && *cx <= '9') {
					numsubwanted = 10 * numsubwanted + (*cx - '0');
					cx++;
				}
				refdepth = 1;
				while (refdepth > 0) {
					if (*cx == '[')
						refdepth++;
					else if (*cx == ']')
						refdepth--;
					cx++;
				}
			} else if (typeclass == 'S' || typeclass == 'U') {
				/* leave it */
			} else {
				cx++;
				if (isarray)
					ix++;
			}
		}
	}

	if (depth > 0) {
		if (*cx != ']')
			error("Illegal format string.");
		cx++;
	} else {
		if (*cx != ':' && *cx != '\0')
			error("Illegal format string.");
	}

	*proto = cx;
	*argnumptr = gargnum;
}

void Glulx::unparse_glk_args(dispatch_splot_t *splot, const char **proto, int depth,
							  int *argnumptr, uint subaddress, int subpassout) {
	const char *cx;
	int ix, argx;
	int gargnum, numwanted;
	void *opref;
	gluniversal_t *garglist;
	uint *varglist;

	garglist = splot->garglist;
	varglist = splot->varglist;
	gargnum = *argnumptr;
	cx = *proto;

	numwanted = 0;
	while (*cx >= '0' && *cx <= '9') {
		numwanted = 10 * numwanted + (*cx - '0');
		cx++;
	}

	for (argx = 0, ix = 0; argx < numwanted; argx++, ix++) {
		char typeclass;
		int skipval;
		int isref, passin, passout, nullok, isarray, isretained, isreturn;
		cx = read_prefix(cx, &isref, &isarray, &passin, &passout, &nullok,
		                 &isretained, &isreturn);

		typeclass = *cx;
		cx++;

		skipval = false;
		if (isref) {
			if (!isreturn && varglist[ix] == 0) {
				if (!nullok)
					error("Zero passed invalidly to Glk function.");
				garglist[gargnum]._ptrflag = false;
				gargnum++;
				skipval = true;
			} else {
				garglist[gargnum]._ptrflag = true;
				gargnum++;
			}
		}
		if (!skipval) {
			uint thisval = 0;

			if (typeclass == '[') {

				unparse_glk_args(splot, &cx, depth + 1, &gargnum, varglist[ix], passout);

			} else if (isarray) {
				/* definitely isref */

				switch (typeclass) {
				case 'C':
					ReleaseCArray((char *)garglist[gargnum]._array, varglist[ix], varglist[ix + 1], passout);
					gargnum++;
					ix++;
					gargnum++;
					cx++;
					break;
				case 'I':
					ReleaseIArray((uint *)garglist[gargnum]._array, varglist[ix], varglist[ix + 1], passout);
					gargnum++;
					ix++;
					gargnum++;
					cx++;
					break;
				case 'Q':
					ReleasePtrArray((void **)garglist[gargnum]._array, varglist[ix], varglist[ix + 1], (*cx - 'a'), passout);
					gargnum++;
					ix++;
					gargnum++;
					cx++;
					break;
				default:
					error("Illegal format string.");
					break;
				}
			} else {
				/* a plain value or a reference to one. */

				if (isreturn || (depth > 0 && subpassout) || (isref && passout)) {
					skipval = false;
				} else {
					skipval = true;
				}

				switch (typeclass) {
				case 'I':
					if (!skipval) {
						if (*cx == 'u')
							thisval = (uint)garglist[gargnum]._uint;
						else if (*cx == 's')
							thisval = (uint)garglist[gargnum]._sint;
						else
							error("Illegal format string.");
					}
					gargnum++;
					cx++;
					break;
				case 'Q':
					if (!skipval) {
						opref = garglist[gargnum]._opaqueref;
						if (opref) {
							gidispatch_rock_t objrock = gidispatch_get_objrock(opref, *cx - 'a');
							assert(objrock.ptr);
							thisval = ((classref_t *)objrock.ptr)->id;
						} else {
							thisval = 0;
						}
					}
					gargnum++;
					cx++;
					break;
				case 'C':
					if (!skipval) {
						if (*cx == 'u')
							thisval = (uint)garglist[gargnum]._uch;
						else if (*cx == 's')
							thisval = (uint)garglist[gargnum]._sch;
						else if (*cx == 'n')
							thisval = (uint)garglist[gargnum]._ch;
						else
							error("Illegal format string.");
					}
					gargnum++;
					cx++;
					break;
				case 'S':
					if (garglist[gargnum]._charstr)
						ReleaseVMString(garglist[gargnum]._charstr);
					gargnum++;
					break;
#ifdef GLK_MODULE_UNICODE
				case 'U':
					if (garglist[gargnum]._unicharstr)
						ReleaseVMUstring(garglist[gargnum]._unicharstr);
					gargnum++;
					break;
#endif /* GLK_MODULE_UNICODE */
				default:
					error("Illegal format string.");
					break;
				}

				if (isreturn) {
					*(splot->retval) = thisval;
				} else if (depth > 0) {
					/* Definitely not isref or isarray. */
					if (subpassout)
						WriteStructField(subaddress, ix, thisval);
				} else if (isref) {
					if (passout)
						WriteMemory(varglist[ix], thisval);
				}
			}
		} else {
			/* We got a null reference, so we have to skip the format element. */
			if (typeclass == '[') {
				int numsubwanted, refdepth;
				numsubwanted = 0;
				while (*cx >= '0' && *cx <= '9') {
					numsubwanted = 10 * numsubwanted + (*cx - '0');
					cx++;
				}
				refdepth = 1;
				while (refdepth > 0) {
					if (*cx == '[')
						refdepth++;
					else if (*cx == ']')
						refdepth--;
					cx++;
				}
			} else if (typeclass == 'S' || typeclass == 'U') {
				/* leave it */
			} else {
				cx++;
				if (isarray)
					ix++;
			}
		}
	}

	if (depth > 0) {
		if (*cx != ']')
			error("Illegal format string.");
		cx++;
	} else {
		if (*cx != ':' && *cx != '\0')
			error("Illegal format string.");
	}

	*proto = cx;
	*argnumptr = gargnum;
}

strid_t Glulx::find_stream_by_id(uint objid) {
	if (!objid)
		return nullptr;

	// Recall that class 1 ("b") is streams
	return (strid_t)classes_get(gidisp_Class_Stream, objid);
}

uint Glulx::find_id_for_window(winid_t win) {
	gidispatch_rock_t objrock;

	if (!win)
		return 0;

	objrock = gidispatch_get_objrock(win, gidisp_Class_Window);
	if (!objrock.ptr)
		return 0;
	return ((classref_t *)objrock.ptr)->id;
}

uint Glulx::find_id_for_stream(strid_t str) {
	gidispatch_rock_t objrock;

	if (!str)
		return 0;

	objrock = gidispatch_get_objrock(str, gidisp_Class_Stream);
	if (!objrock.ptr)
		return 0;
	return ((classref_t *)objrock.ptr)->id;
}

uint Glulx::find_id_for_fileref(frefid_t fref) {
	gidispatch_rock_t objrock;

	if (!fref)
		return 0;

	objrock = gidispatch_get_objrock(fref, gidisp_Class_Fileref);
	if (!objrock.ptr)
		return 0;
	return ((classref_t *)objrock.ptr)->id;
}

uint Glulx::find_id_for_schannel(schanid_t schan) {
	gidispatch_rock_t objrock;

	if (!schan)
		return 0;

	objrock = gidispatch_get_objrock(schan, gidisp_Class_Schannel);
	if (!objrock.ptr)
		return 0;
	return ((classref_t *)objrock.ptr)->id;
}

classtable_t *Glulx::new_classtable(uint firstid) {
	int ix;
	classtable_t *ctab = (classtable_t *)glulx_malloc(sizeof(classtable_t));
	if (!ctab)
		return nullptr;

	for (ix = 0; ix < CLASSHASH_SIZE; ix++)
		ctab->bucket[ix] = nullptr;

	ctab->lastid = firstid;

	return ctab;
}

void *Glulx::classes_get(int classid, uint objid) {
	classtable_t *ctab;
	classref_t *cref;
	if (classid < 0 || classid >= num_classes)
		return nullptr;
	ctab = classes[classid];
	cref = ctab->bucket[objid % CLASSHASH_SIZE];
	for (; cref; cref = cref->next) {
		if (cref->id == objid)
			return cref->obj;
	}
	return nullptr;
}

classref_t *Glulx::classes_put(int classid, void *obj, uint origid) {
	int bucknum;
	classtable_t *ctab;
	classref_t *cref;
	if (classid < 0 || classid >= num_classes)
		return nullptr;
	ctab = classes[classid];
	cref = (classref_t *)glulx_malloc(sizeof(classref_t));
	if (!cref)
		return nullptr;
	cref->obj = obj;
	if (!origid) {
		cref->id = ctab->lastid;
		ctab->lastid++;
	} else {
		cref->id = origid;
		if (ctab->lastid <= origid)
			ctab->lastid = origid + 1;
	}
	bucknum = cref->id % CLASSHASH_SIZE;
	cref->bucknum = bucknum;
	cref->next = ctab->bucket[bucknum];
	ctab->bucket[bucknum] = cref;
	return cref;
}

void Glulx::classes_remove(int classid, void *obj) {
	classtable_t *ctab;
	classref_t *cref;
	classref_t **crefp;
	gidispatch_rock_t objrock;
	if (classid < 0 || classid >= num_classes)
		return;
	ctab = classes[classid];
	objrock = gidispatch_get_objrock(obj, classid);
	cref = (classref_t *)objrock.ptr;
	if (!cref)
		return;
	crefp = &(ctab->bucket[cref->bucknum]);
	for (; *crefp; crefp = &((*crefp)->next)) {
		if ((*crefp) == cref) {
			*crefp = cref->next;
			if (!cref->obj) {
				nonfatal_warning("attempt to free nullptr object!");
			}
			cref->obj = nullptr;
			cref->id = 0;
			cref->next = nullptr;
			glulx_free(cref);
			return;
		}
	}
	return;
}

gidispatch_rock_t Glulx::glulxe_classtable_register(void *obj,  uint objclass) {
	classref_t *cref;
	gidispatch_rock_t objrock;
	cref = classes_put(objclass, obj, 0);
	objrock.ptr = cref;
	return objrock;
}

void Glulx::glulxe_classtable_unregister(void *obj, uint objclass,
		gidispatch_rock_t objrock) {
	classes_remove(objclass, obj);
}

gidispatch_rock_t Glulx::glulxe_classtable_register_existing(void *obj, uint objclass, uint dispid) {
	classref_t *cref;
	gidispatch_rock_t objrock;
	cref = classes_put(objclass, obj, dispid);
	objrock.ptr = cref;
	return objrock;
}

char *Glulx::grab_temp_c_array(uint addr, uint len, int passin) {
	arrayref_t *arref = nullptr;
	char *arr = nullptr;
	uint ix, addr2;

	if (len) {
		arr = (char *)glulx_malloc(len * sizeof(char));
		arref = (arrayref_t *)glulx_malloc(sizeof(arrayref_t));
		if (!arr || !arref)
			error("Unable to allocate space for array argument to Glk call.");

		arref->array = arr;
		arref->addr = addr;
		arref->elemsize = 1;
		arref->retained = false;
		arref->len = len;
		arref->next = arrays;
		arrays = arref;

		if (passin) {
			for (ix = 0, addr2 = addr; ix < len; ix++, addr2 += 1) {
				arr[ix] = Mem1(addr2);
			}
		}
	}

	return arr;
}

void Glulx::release_temp_c_array(char *arr, uint addr, uint len, int passout) {
	arrayref_t *arref = nullptr;
	arrayref_t **aptr;
	uint ix, val, addr2;

	if (arr) {
		for (aptr = (&arrays); (*aptr); aptr = (&((*aptr)->next))) {
			if ((*aptr)->array == arr)
				break;
		}
		arref = *aptr;
		if (!arref)
			error("Unable to re-find array argument in Glk call.");
		if (arref->addr != addr || arref->len != len)
			error("Mismatched array argument in Glk call.");

		if (arref->retained) {
			return;
		}

		*aptr = arref->next;
		arref->next = nullptr;

		if (passout) {
			for (ix = 0, addr2 = addr; ix < len; ix++, addr2 += 1) {
				val = arr[ix];
				MemW1(addr2, val);
			}
		}
		glulx_free(arr);
		glulx_free(arref);
	}
}

uint *Glulx::grab_temp_i_array(uint addr, uint len, int passin) {
	arrayref_t *arref = nullptr;
	uint *arr = nullptr;
	uint ix, addr2;

	if (len) {
		arr = (uint *)glulx_malloc(len * sizeof(uint));
		arref = (arrayref_t *)glulx_malloc(sizeof(arrayref_t));
		if (!arr || !arref)
			error("Unable to allocate space for array argument to Glk call.");

		arref->array = arr;
		arref->addr = addr;
		arref->elemsize = 4;
		arref->retained = false;
		arref->len = len;
		arref->next = arrays;
		arrays = arref;

		if (passin) {
			for (ix = 0, addr2 = addr; ix < len; ix++, addr2 += 4) {
				arr[ix] = Mem4(addr2);
			}
		}
	}

	return arr;
}

void Glulx::release_temp_i_array(uint *arr, uint addr, uint len, int passout) {
	arrayref_t *arref = nullptr;
	arrayref_t **aptr;
	uint ix, val, addr2;

	if (arr) {
		for (aptr = (&arrays); (*aptr); aptr = (&((*aptr)->next))) {
			if ((*aptr)->array == arr)
				break;
		}
		arref = *aptr;
		if (!arref)
			error("Unable to re-find array argument in Glk call.");
		if (arref->addr != addr || arref->len != len)
			error("Mismatched array argument in Glk call.");

		if (arref->retained) {
			return;
		}

		*aptr = arref->next;
		arref->next = nullptr;

		if (passout) {
			for (ix = 0, addr2 = addr; ix < len; ix++, addr2 += 4) {
				val = arr[ix];
				MemW4(addr2, val);
			}
		}
		glulx_free(arr);
		glulx_free(arref);
	}
}

void **Glulx::grab_temp_ptr_array(uint addr, uint len, int objclass, int passin) {
	arrayref_t *arref = nullptr;
	void **arr = nullptr;
	uint ix, addr2;

	if (len) {
		arr = (void **)glulx_malloc(len * sizeof(void *));
		arref = (arrayref_t *)glulx_malloc(sizeof(arrayref_t));
		if (!arr || !arref)
			error("Unable to allocate space for array argument to Glk call.");

		arref->array = arr;
		arref->addr = addr;
		arref->elemsize = sizeof(void *);
		arref->retained = false;
		arref->len = len;
		arref->next = arrays;
		arrays = arref;

		if (passin) {
			for (ix = 0, addr2 = addr; ix < len; ix++, addr2 += 4) {
				uint thisval = Mem4(addr2);
				if (thisval)
					arr[ix] = classes_get(objclass, thisval);
				else
					arr[ix] = nullptr;
			}
		}
	}

	return arr;
}

void Glulx::release_temp_ptr_array(void **arr, uint addr, uint len, int objclass, int passout) {
	arrayref_t *arref = nullptr;
	arrayref_t **aptr;
	uint ix, val, addr2;

	if (arr) {
		for (aptr = (&arrays); (*aptr); aptr = (&((*aptr)->next))) {
			if ((*aptr)->array == arr)
				break;
		}
		arref = *aptr;
		if (!arref)
			error("Unable to re-find array argument in Glk call.");
		if (arref->addr != addr || arref->len != len)
			error("Mismatched array argument in Glk call.");

		if (arref->retained) {
			return;
		}

		*aptr = arref->next;
		arref->next = nullptr;

		if (passout) {
			for (ix = 0, addr2 = addr; ix < len; ix++, addr2 += 4) {
				void *opref = arr[ix];
				if (opref) {
					gidispatch_rock_t objrock =
					    gidispatch_get_objrock(opref, objclass);
					val = ((classref_t *)objrock.ptr)->id;
				} else {
					val = 0;
				}
				MemW4(addr2, val);
			}
		}
		glulx_free(arr);
		glulx_free(arref);
	}
}

gidispatch_rock_t Glulx::glulxe_retained_register(void *array, uint len, const char *typecode) {
	gidispatch_rock_t rock;
	arrayref_t *arref = nullptr;
	arrayref_t **aptr;
	uint elemsize = 0;

	if (typecode[4] == 'C')
		elemsize = 1;
	else if (typecode[4] == 'I')
		elemsize = 4;

	if (!elemsize || array == nullptr) {
		rock.ptr = nullptr;
		return rock;
	}

	for (aptr = (&arrays); (*aptr); aptr = (&((*aptr)->next))) {
		if ((*aptr)->array == array)
			break;
	}
	arref = *aptr;
	if (!arref)
		error("Unable to re-find array argument in Glk call.");
	if (arref->elemsize != elemsize || arref->len != len)
		error("Mismatched array argument in Glk call.");

	arref->retained = true;

	rock.ptr = arref;
	return rock;
}

void Glulx::glulxe_retained_unregister(void *array, uint len, const  char *typecode, gidispatch_rock_t objrock) {
	arrayref_t *arref = nullptr;
	arrayref_t **aptr;
	uint ix, addr2, val;
	uint elemsize = 0;

	// TODO: See if original GLULXE has code I'm overlooking to cleanly close everything before freeing memmap
	if (!memmap)
		return;

	if (typecode[4] == 'C')
		elemsize = 1;
	else if (typecode[4] == 'I')
		elemsize = 4;

	if (!elemsize || array == nullptr) {
		return;
	}

	for (aptr = (&arrays); (*aptr); aptr = (&((*aptr)->next))) {
		if ((*aptr)->array == array)
			break;
	}
	arref = *aptr;
	if (!arref)
		error("Unable to re-find array argument in Glk call.");
	if (arref != objrock.ptr)
		error("Mismatched array reference in Glk call.");
	if (!arref->retained)
		error("Unretained array reference in Glk call.");
	if (arref->elemsize != elemsize || arref->len != len)
		error("Mismatched array argument in Glk call.");

	*aptr = arref->next;
	arref->next = nullptr;

	if (elemsize == 1) {
		for (ix = 0, addr2 = arref->addr; ix < arref->len; ix++, addr2 += 1) {
			val = ((char *)array)[ix];
			MemW1(addr2, val);
		}
	} else if (elemsize == 4) {
		for (ix = 0, addr2 = arref->addr; ix < arref->len; ix++, addr2 += 4) {
			val = ((uint *)array)[ix];
			MemW4(addr2, val);
		}
	}

	glulx_free(array);
	glulx_free(arref);
}

long Glulx::glulxe_array_locate(void *array, uint len, char *typecode, gidispatch_rock_t objrock, int *elemsizeref) {
	arrayref_t *arref = nullptr;
	arrayref_t **aptr;
	uint elemsize = 0;

	if (typecode[4] == 'C')
		elemsize = 1;
	else if (typecode[4] == 'I')
		elemsize = 4;

	if (!elemsize || array == nullptr) {
		*elemsizeref = 0; /* No need to save the array separately */
		return (unsigned char *)array - memmap;
	}

	for (aptr = (&arrays); (*aptr); aptr = (&((*aptr)->next))) {
		if ((*aptr)->array == array)
			break;
	}
	arref = *aptr;
	if (!arref)
		error("Unable to re-find array argument in array_locate.");
	if (arref != objrock.ptr)
		error("Mismatched array reference in array_locate.");
	if (!arref->retained)
		error("Unretained array reference in array_locate.");
	if (arref->elemsize != elemsize || arref->len != len)
		error("Mismatched array argument in array_locate.");

	*elemsizeref = arref->elemsize;
	return arref->addr;
}

gidispatch_rock_t Glulx::glulxe_array_restore(long bufkey, uint len, char *typecode, void **arrayref) {
	gidispatch_rock_t rock;
	int elemsize = 0;

	if (typecode[4] == 'C')
		elemsize = 1;
	else if (typecode[4] == 'I')
		elemsize = 4;

	if (!elemsize) {
		unsigned char *buf = memmap + bufkey;
		*arrayref = buf;
		rock.ptr = nullptr;
		return rock;
	}

	if (elemsize == 1) {
		char *cbuf = grab_temp_c_array(bufkey, len, false);
		rock = glulxe_retained_register(cbuf, len, typecode);
		*arrayref = cbuf;
	} else {
		uint *ubuf = grab_temp_i_array(bufkey, len, false);
		rock = glulxe_retained_register(ubuf, len, typecode);
		*arrayref = ubuf;
	}
	return rock;
}

void Glulx::set_library_select_hook(void (*func)(uint)) {
	library_select_hook = func;
}

char *Glulx::get_game_id() {
	/* This buffer gets rewritten on every call, but that's okay -- the caller
	   is supposed to copy out the result. */
	static char buf[2 * 64 + 2];
	int ix, jx;

	if (!memmap)
		return nullptr;

	for (ix = 0, jx = 0; ix < 64; ix++) {
		char ch = memmap[ix];
		int val = ((ch >> 4) & 0x0F);
		buf[jx++] = ((val < 10) ? (val + '0') : (val + 'A' - 10));
		val = (ch & 0x0F);
		buf[jx++] = ((val < 10) ? (val + '0') : (val + 'A' - 10));
	}
	buf[jx++] = '\0';

	return buf;
}

uint Glulx::ReadMemory(uint addr) {
	if (addr == 0xffffffff) {
		stackptr -= 4;
		return Stk4(stackptr);
	} else {
		return Mem4(addr);
	}
}

void Glulx::WriteMemory(uint addr, uint val) {
	if (addr == 0xffffffff) {
		StkW4(stackptr, (val));
		stackptr += 4;
	} else {
		MemW4(addr, val);
	}
}

char *Glulx::CaptureCArray(uint addr, uint len, int passin) {
	return grab_temp_c_array(addr, len, passin);
}

void Glulx::ReleaseCArray(char *ptr, uint addr, uint len, int passout) {
	release_temp_c_array(ptr, addr, len, passout);
}

uint *Glulx::CaptureIArray(uint addr, uint len, int passin) {
	return grab_temp_i_array(addr, len, passin);
}

void Glulx::ReleaseIArray(uint *ptr, uint addr, uint len, int passout) {
	release_temp_i_array(ptr, addr, len, passout);
}

void **Glulx::CapturePtrArray(uint addr, uint len, int objclass, int passin) {
	return grab_temp_ptr_array(addr, len, objclass, passin);
}

void Glulx::ReleasePtrArray(void **ptr, uint addr, uint len, int objclass, int passout) {
	return release_temp_ptr_array(ptr, addr, len, objclass, passout);
}

uint Glulx::ReadStructField(uint addr, uint fieldnum) {
	if (addr == 0xffffffff) {
		stackptr -= 4;
		return Stk4(stackptr);
	} else {
		return Mem4(addr + (fieldnum * 4));
	}
}

void Glulx::WriteStructField(uint addr, uint fieldnum, uint val) {
	if (addr == 0xffffffff) {
		StkW4(stackptr, val);
		stackptr += 4;
	} else {
		MemW4(addr + (fieldnum * 4), val);
	}
}

char *Glulx::DecodeVMString(uint addr) {
	return make_temp_string(addr);
}

void Glulx::ReleaseVMString(char *ptr) {
	free_temp_string(ptr);
}

uint32 *Glulx::DecodeVMUstring(uint addr) {
	return make_temp_ustring(addr);
}

void Glulx::ReleaseVMUstring(uint32 *ptr) {
	free_temp_ustring(ptr);
}

} // End of namespace Glulx
} // End of namespace Glk
