/* 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::enter_function(uint funcaddr, uint argc, uint *argv) {
	uint ix, jx;
	acceleration_func accelFunc;
	int locallen;
	int functype;
	uint modeaddr, opaddr, val;
	int loctype, locnum;
	uint addr = funcaddr;

	accelFunc = accel_get_func(addr);
	if (accelFunc) {
		profile_in(addr, stackptr, true);
		val = (this->*accelFunc)(argc, argv);
		profile_out(stackptr);
		pop_callstub(val);
		return;
	}

	profile_in(addr, stackptr, false);

	/* Check the Glulx type identifier byte. */
	functype = Mem1(addr);
	if (functype != 0xC0 && functype != 0xC1) {
		if (functype >= 0xC0 && functype <= 0xDF)
			fatal_error_i("Call to unknown type of function.", addr);
		else
			fatal_error_i("Call to non-function.", addr);
	}
	addr++;

	/* Bump the frameptr to the top. */
	frameptr = stackptr;

	/* Go through the function's locals-format list, copying it to the
	   call frame. At the same time, we work out how much space the locals
	   will actually take up. (Including padding.) */
	ix = 0;
	locallen = 0;
	while (1) {
		/* Grab two bytes from the locals-format list. These are
		   unsigned (0..255 range). */
		loctype = Mem1(addr);
		addr++;
		locnum = Mem1(addr);
		addr++;

		/* Copy them into the call frame. */
		StkW1(frameptr + 8 + 2 * ix, loctype);
		StkW1(frameptr + 8 + 2 * ix + 1, locnum);
		ix++;

		/* If the type is zero, we're done, except possibly for two more
		   zero bytes in the call frame (to ensure 4-byte alignment.) */
		if (loctype == 0) {
			/* Make sure ix is even. */
			if (ix & 1) {
				StkW1(frameptr + 8 + 2 * ix, 0);
				StkW1(frameptr + 8 + 2 * ix + 1, 0);
				ix++;
			}
			break;
		}

		/* Pad to 4-byte or 2-byte alignment if these locals are 4 or 2
		   bytes long. */
		if (loctype == 4) {
			while (locallen & 3)
				locallen++;
		} else if (loctype == 2) {
			while (locallen & 1)
				locallen++;
		} else if (loctype == 1) {
			/* no padding */
		} else {
			fatal_error("Illegal local type in locals-format list.");
		}

		/* Add the length of the locals themselves. */
		locallen += (loctype * locnum);
	}

	/* Pad the locals to 4-byte alignment. */
	while (locallen & 3)
		locallen++;

	/* We now know how long the locals-frame and locals segments are. */
	localsbase = frameptr + 8 + 2 * ix;
	valstackbase = localsbase + locallen;

	/* Test for stack overflow. */
	/* This really isn't good enough; if the format list overflowed the
	   stack, we've already written outside the stack array. */
	if (valstackbase >= stacksize)
		fatal_error("Stack overflow in function call.");

	/* Fill in the beginning of the stack frame. */
	StkW4(frameptr + 4, 8 + 2 * ix);
	StkW4(frameptr, 8 + 2 * ix + locallen);

	/* Set the stackptr and PC. */
	stackptr = valstackbase;
	pc = addr;

	/* Zero out all the locals. */
	for (jx = 0; jx < (uint)locallen; jx++)
		StkW1(localsbase + jx, 0);

	if (functype == 0xC0) {
		/* Push the function arguments on the stack. The locals have already
		   been zeroed. */
		if (stackptr + 4 * (argc + 1) >= stacksize)
			fatal_error("Stack overflow in function arguments.");
		for (ix = 0; ix < argc; ix++) {
			val = argv[(argc - 1) - ix];
			StkW4(stackptr, val);
			stackptr += 4;
		}
		StkW4(stackptr, argc);
		stackptr += 4;
	} else {
		/* Copy in function arguments. This is a bit gross, since we have to
		   follow the locals format. If there are fewer arguments than locals,
		   that's fine -- we've already zeroed out this space. If there are
		   more arguments than locals, the extras are silently dropped. */
		modeaddr = frameptr + 8;
		opaddr = localsbase;
		ix = 0;
		while (ix < argc) {
			loctype = Stk1(modeaddr);
			modeaddr++;
			locnum = Stk1(modeaddr);
			modeaddr++;
			if (loctype == 0)
				break;
			if (loctype == 4) {
				while (opaddr & 3)
					opaddr++;
				while (ix < argc && locnum) {
					val = argv[ix];
					StkW4(opaddr, val);
					opaddr += 4;
					ix++;
					locnum--;
				}
			} else if (loctype == 2) {
				while (opaddr & 1)
					opaddr++;
				while (ix < argc && locnum) {
					val = argv[ix] & 0xFFFF;
					StkW2(opaddr, val);
					opaddr += 2;
					ix++;
					locnum--;
				}
			} else if (loctype == 1) {
				while (ix < argc && locnum) {
					val = argv[ix] & 0xFF;
					StkW1(opaddr, val);
					opaddr += 1;
					ix++;
					locnum--;
				}
			}
		}
	}

	/* If the debugger is compiled in, check for a breakpoint on this
	   function. (Checking the function address, not the starting PC.) */
	debugger_check_func_breakpoint(funcaddr);
}

void Glulxe::leave_function() {
	profile_out(stackptr);
	stackptr = frameptr;
}

void Glulxe::push_callstub(uint desttype, uint destaddr) {
	if (stackptr + 16 > stacksize)
		fatal_error("Stack overflow in callstub.");
	StkW4(stackptr + 0, desttype);
	StkW4(stackptr + 4, destaddr);
	StkW4(stackptr + 8, pc);
	StkW4(stackptr + 12, frameptr);
	stackptr += 16;
}

void Glulxe::pop_callstub(uint returnvalue) {
	uint desttype, destaddr;
	uint newpc, newframeptr;

	if (stackptr < 16)
		fatal_error("Stack underflow in callstub.");
	stackptr -= 16;

	newframeptr = Stk4(stackptr + 12);
	newpc = Stk4(stackptr + 8);
	destaddr = Stk4(stackptr + 4);
	desttype = Stk4(stackptr + 0);

	pc = newpc;
	frameptr = newframeptr;

	/* Recompute valstackbase and localsbase */
	valstackbase = frameptr + Stk4(frameptr);
	localsbase = frameptr + Stk4(frameptr + 4);

	switch (desttype) {

	case 0x11:
		fatal_error("String-terminator call stub at end of function call.");
		break;

	case 0x10:
		/* This call stub was pushed during a string-decoding operation!
		   We have to restart it. (Note that the return value is discarded.) */
		stream_string(pc, 0xE1, destaddr);
		break;

	case 0x12:
		/* This call stub was pushed during a number-printing operation.
		   Restart that. (Return value discarded.) */
		stream_num(pc, true, destaddr);
		break;

	case 0x13:
		/* This call stub was pushed during a C-string printing operation.
		   We have to restart it. (Note that the return value is discarded.) */
		stream_string(pc, 0xE0, destaddr);
		break;

	case 0x14:
		/* This call stub was pushed during a Unicode printing operation.
		   We have to restart it. (Note that the return value is discarded.) */
		stream_string(pc, 0xE2, destaddr);
		break;

	default:
		/* We're back in the original frame, so we can store the returnvalue.
		   (If we tried to do this before resetting frameptr, a result
		   destination on the stack would go astray.) */
		store_operand(desttype, destaddr, returnvalue);
		break;
	}
}

uint Glulxe::pop_callstub_string(int *bitnum) {
	uint desttype, destaddr, newpc;

	if (stackptr < 16)
		fatal_error("Stack underflow in callstub.");
	stackptr -= 16;

	newpc = Stk4(stackptr + 8);
	destaddr = Stk4(stackptr + 4);
	desttype = Stk4(stackptr + 0);

	pc = newpc;

	if (desttype == 0x11) {
		return 0;
	}
	if (desttype == 0x10) {
		*bitnum = destaddr;
		return pc;
	}

	fatal_error("Function-terminator call stub at end of string.");
	return 0;
}

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