/* 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 2
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

#include "glk/glulxe/glulxe.h"

namespace Glk {
namespace Glulxe {

void Glulxe::setup_vm() {
	byte buf[4 * 7];

	pc = 0;           // Clear this, so that error messages are cleaner.
	prevpc = 0;

	// Read in all the size constants from the game file header
	stream_char_handler = nullptr;
	stream_unichar_handler = nullptr;

	_gameFile.seek(gamefile_start + 8);
	if (_gameFile.read(buf, 4 * 7) != (4 * 7))
		fatal_error("The game file header is too short.");

	ramstart = Read4(buf + 0);
	endgamefile = Read4(buf + 4);
	origendmem = Read4(buf + 8);
	stacksize = Read4(buf + 12);
	startfuncaddr = Read4(buf + 16);
	origstringtable = Read4(buf + 20);
	checksum = Read4(buf + 24);

	// Set the protection range to (0, 0), meaning "off".
	protectstart = 0;
	protectend = 0;

	// Do a few sanity checks.
	if ((ramstart & 0xFF)
	        || (endgamefile & 0xFF)
	        || (origendmem & 0xFF)
	        || (stacksize & 0xFF)) {
		nonfatal_warning("One of the segment boundaries in the header is not "
		                 "256-byte aligned.");
	}

	if (endgamefile != gamefile_len) {
		nonfatal_warning("The gamefile length does not match the header "
		                 "endgamefile length.");
	}

	if (ramstart < 0x100 || endgamefile < ramstart || origendmem < endgamefile) {
		fatal_error("The segment boundaries in the header are in an impossible "
		            "order.");
	}
	if (stacksize < 0x100) {
		fatal_error("The stack size in the header is too small.");
	}

	/* Allocate main memory and the stack. This is where memory allocation
	   errors are most likely to occur. */
	endmem = origendmem;
	memmap = (byte *)glulx_malloc(origendmem);
	if (!memmap) {
		fatal_error("Unable to allocate Glulx memory space.");
	}
	stack = (byte *)glulx_malloc(stacksize);
	if (!stack) {
		glulx_free(memmap);
		memmap = nullptr;
		fatal_error("Unable to allocate Glulx stack space.");
	}
	stringtable = 0;

	// Initialize various other things in the terp.
	init_operands();
	init_serial();

	// Set up the initial machine state.
	vm_restart();

	/* If the debugger is compiled in, check that the debug data matches
	   the game. (This only prints warnings for mismatch.) */
	debugger_check_story_file();

	/* Also, set up any start-time debugger state. This may do a block-
	   and-debug, if the user has requested that. */
	debugger_setup_start_state();
}

void Glulxe::finalize_vm() {
	stream_set_table(0);

	if (memmap) {
		glulx_free(memmap);
		memmap = nullptr;
	}
	if (stack) {
		glulx_free(stack);
		stack = nullptr;
	}

	final_serial();
}

void Glulxe::vm_restart() {
	uint lx;
	int res;
	int bufpos;
	char buf[0x100];

	/* Deactivate the heap (if it was active). */
	heap_clear();

	/* Reset memory to the original size. */
	lx = change_memsize(origendmem, false);
	if (lx)
		fatal_error("Memory could not be reset to its original size.");

	/* Load in all of main memory. We do this in 256-byte chunks, because
	   why rely on OS stream buffering? */
	_gameFile.seek(gamefile_start);
	bufpos = 0x100;

	for (lx = 0; lx < endgamefile; lx++) {
		if (bufpos >= 0x100) {
			int count = _gameFile.read(buf, 0x100);
			if (count != 0x100) {
				fatal_error("The game file ended unexpectedly.");
			}
			bufpos = 0;
		}

		res = buf[bufpos++];
		if (lx >= protectstart && lx < protectend)
			continue;
		memmap[lx] = res;
	}
	for (lx = endgamefile; lx < origendmem; lx++) {
		memmap[lx] = 0;
	}

	/* Reset all the registers */
	stackptr = 0;
	frameptr = 0;
	pc = 0;
	prevpc = 0;
	stream_set_iosys(0, 0);
	stream_set_table(origstringtable);
	valstackbase = 0;
	localsbase = 0;

	/* Note that we do not reset the protection range. */

	/* Push the first function call. (No arguments.) */
	enter_function(startfuncaddr, 0, nullptr);

	/* We're now ready to execute. */
}

uint Glulxe::change_memsize(uint newlen, bool internal) {
	uint lx;
	unsigned char *newmemmap;

	if (newlen == endmem)
		return 0;

#ifdef FIXED_MEMSIZE
	return 1;
#else /* FIXED_MEMSIZE */

	if ((!internal) && heap_is_active())
		fatal_error("Cannot resize Glulx memory space while heap is active.");

	if (newlen < origendmem)
		fatal_error("Cannot resize Glulx memory space smaller than it started.");

	if (newlen & 0xFF)
		fatal_error("Can only resize Glulx memory space to a 256-byte boundary.");

	newmemmap = (unsigned char *)glulx_realloc(memmap, newlen);
	if (!newmemmap) {
		/* The old block is still in place, unchanged. */
		return 1;
	}
	memmap = newmemmap;

	if (newlen > endmem) {
		for (lx = endmem; lx < newlen; lx++) {
			memmap[lx] = 0;
		}
	}

	endmem = newlen;

	return 0;

#endif /* FIXED_MEMSIZE */
}

uint *Glulxe::pop_arguments(uint count, uint addr) {
	uint ix;
	uint argptr;
	uint *array;

#define MAXARGS (32)
	static uint statarray[MAXARGS];
	static uint *dynarray = nullptr;
	static uint dynarray_size = 0;

	if (count == 0)
		return nullptr;

	if (count <= MAXARGS) {
		/* Store in the static array. */
		array = statarray;
	} else {
		if (!dynarray) {
			dynarray_size = count + 8;
			dynarray = (uint *)glulx_malloc(sizeof(uint) * dynarray_size);
			if (!dynarray)
				fatal_error("Unable to allocate function arguments.");
			array = dynarray;
		} else {
			if (dynarray_size >= count) {
				/* It fits. */
				array = dynarray;
			} else {
				dynarray_size = count + 8;
				dynarray = (uint *)glulx_realloc(dynarray, sizeof(uint) * dynarray_size);
				if (!dynarray)
					fatal_error("Unable to reallocate function arguments.");
				array = dynarray;
			}
		}
	}

	if (!addr) {
		if (stackptr < valstackbase + 4 * count)
			fatal_error("Stack underflow in arguments.");
		stackptr -= 4 * count;
		for (ix = 0; ix < count; ix++) {
			argptr = stackptr + 4 * ((count - 1) - ix);
			array[ix] = Stk4(argptr);
		}
	} else {
		for (ix = 0; ix < count; ix++) {
			array[ix] = Mem4(addr);
			addr += 4;
		}
	}

	return array;
}

void Glulxe::verify_address(uint addr, uint count) {
	if (addr >= endmem)
		fatal_error_i("Memory access out of range", addr);
	if (count > 1) {
		addr += (count - 1);
		if (addr >= endmem)
			fatal_error_i("Memory access out of range", addr);
	}
}

void Glulxe::verify_address_write(uint addr, uint count) {
	if (addr < ramstart)
		fatal_error_i("Memory write to read-only address", addr);
	if (addr >= endmem)
		fatal_error_i("Memory access out of range", addr);
	if (count > 1) {
		addr += (count - 1);
		if (addr >= endmem)
			fatal_error_i("Memory access out of range", addr);
	}
}

void Glulxe::verify_array_addresses(uint addr, uint count, uint size) {
	uint bytecount;
	if (addr >= endmem)
		fatal_error_i("Memory access out of range", addr);

	if (count == 0)
		return;
	bytecount = count * size;

	/* If just multiplying by the element size overflows, we have trouble. */
	if (bytecount < count)
		fatal_error_i("Memory access way too long", addr);

	/* If the byte length by itself is too long, or if its end overflows,
	   we have trouble. */
	if (bytecount > endmem || addr + bytecount < addr)
		fatal_error_i("Memory access much too long", addr);
	/* The simple length test. */
	if (addr + bytecount > endmem)
		fatal_error_i("Memory access too long", addr);
}

} // End of namespace Glulxe
} // End of namespace Glk
