/*

Z-Machine runtime functions
===========================

Copyright (c) 2020 The ifvms.js team
MIT licenced
https://github.com/curiousdannii/ifvms.js

*/

'use strict';

/*

TODO:
	Save/restore: table, name, prompt support

*/

const file = require( '../common/file.js' )
const utils = require( '../common/utils.js' )
const extend = utils.extend
const U2S = utils.U2S16
const S2U = utils.S2U16

// A clone function which ignores the properties we don't want to serialise
function clone( obj )
{
	const recurse = obj => typeof obj === 'object' ? clone( obj ) : obj
	const newobj = {}

	if ( Array.isArray( obj ) )
	{
		return obj.map( recurse )
	}

	for ( let prop in obj )
	{
		if ( prop !== 'buffer' && prop !== 'str' )
		{
			newobj[prop] = recurse( obj[prop] )
		}
	}
	return newobj
}

// Test whether we are running on a littleEndian system
const littleEndian = (function()
{
	var testUint8Array = new Uint8Array( 2 ),
	testUint16Array = new Uint16Array( testUint8Array.buffer );
	testUint16Array[0] = 1;
	return testUint8Array[0] === 1;
})()

function fix_stack_endianness( view, start, end, auto )
{
	if ( littleEndian && !auto )
	{
		while ( start < end )
		{
			view.setUint16( start, view.getUint16( start, 1 ) );
			start += 2;
		}
	}
}

module.exports = {

	art_shift: function( number, places )
	{
		return places > 0 ? number << places : number >> -places;
	},

	// Call a routine
	call: function( addr, storer, next, args )
	{
		// 6.4.3: Calls to 0 instead just store 0
		if ( addr === 0 )
		{
			if ( storer >= 0 )
			{
				this.variable( storer, 0 );
			}
			return this.pc = next;
		}

		// Get the number of locals and advance the pc
		this.pc = addr * this.addr_multipler;
		var locals_count = this.m.getUint8( this.pc++ ),

		stack = this.stack,
		i = 0,

		// Write the current stack use
		frameptr = this.frameptr;
		stack.setUint16( frameptr + 6, this.sp );
		this.frames.push( frameptr );

		// Create a new frame
		frameptr = this.frameptr = this.s.byteOffset + this.sp * 2;
		// Return address
		stack.setUint32( frameptr, next << 8 );
		// Flags
		stack.setUint8( frameptr + 3, ( storer >= 0 ? 0 : 0x10 ) | locals_count );
		// Storer
		stack.setUint8( frameptr + 4, storer >= 0 ? storer : 0 );
		// Supplied arguments
		stack.setUint8( frameptr + 5, ( 1 << args.length ) - 1 );

		// Create the locals and stack
		this.make_stacks();
		this.sp = 0;
		while ( i < locals_count )
		{
			this.l[i] = i < args.length ? args[i] : ( this.version < 5 ? this.m.getUint16( this.pc + i * 2 ) : 0 );
			i++;
		}
		if ( this.version < 5 )
		{
			this.pc += locals_count * 2;
		}
	},

	clear_attr: function( object, attribute )
	{
		var addr = this.objects + ( this.version3 ? 9 : 14 ) * object + ( attribute / 8 ) | 0;
		this.ram.setUint8( addr, this.m.getUint8( addr ) & ~( 0x80 >> attribute % 8 ) );
	},

	copy_table: function( first, second, size )
	{
		size = U2S( size );
		var ram = this.ram,
		i = 0,
		allowcorrupt = size < 0;
		size = Math.abs( size );

		// Simple case, zeroes
		if ( second === 0 )
		{
			while ( i < size )
			{
				ram.setUint8( first + i++, 0 );
			}
			return;
		}

		if ( allowcorrupt )
		{
			while ( i < size )
			{
				ram.setUint8( second + i, this.m.getUint8( first + i++ ) );
			}
		}
		else
		{
			ram.setUint8Array( second, this.m.getUint8Array( first, size ) );
		}
	},

	do_autorestore: function( snapshot )
	{
		const Glk = this.Glk

		// Restore Glk
		Glk.restore_allstate( snapshot.glk )

		// Get references to our Glk objects
		this.io = snapshot.io
		const RockBox = new Glk.RefBox()
		let obj
		while ( obj = Glk.glk_window_iterate( obj, RockBox ) )
		{
			if ( RockBox.value === 201 )
			{
				this.mainwin = obj
				if ( obj.linebuf )
				{
					snapshot.read_data.buffer = obj.linebuf
				}
			}
			if ( RockBox.value === 202 )
			{
				this.statuswin = obj
			}
			if ( RockBox.value === 203 )
			{
				this.upperwin = obj
			}
		}
		obj = null
		while ( obj = Glk.glk_stream_iterate( obj, RockBox ) )
		{
			if ( RockBox.value === 210 )
			{
				this.io.streams[2].str = obj
			}
			if ( RockBox.value === 211 )
			{
				this.io.streams[4].str = obj
			}
		}

		// Restart and restore the RAM and stacks
		this.restart(1)
		this.restore_file(this.options.Dialog.streaming ? new Uint8Array(snapshot.ram) : Uint8Array.from(snapshot.ram), 1)

		// Set remaining data from the snapshot
		this.read_data = snapshot.read_data
		this.xorshift_seed = snapshot.xorshift_seed
	},

	do_autosave: function( save )
	{
		if ( !this.options.Dialog )
		{
			throw new Error( 'A reference to Dialog is required' )
		}

		let snapshot = null
		if ( ( save || 0 ) >= 0 )
		{
			const ram = this.save_file(this.pc, 1)
			snapshot = {
				glk: this.Glk.save_allstate(),
				io: clone( this.io ),
				ram: this.options.Dialog.streaming ? ram : Array.from(new Uint8Array(ram)),
				read_data: clone( this.read_data ),
				xorshift_seed: this.xorshift_seed,
			}
		}

		this.options.Dialog.autosave_write( this.signature, snapshot )
	},

	encode_text: function( zscii, length, from, target )
	{
		this.ram.setUint8Array( target, this.encode( this.m.getUint8Array( zscii + from, length ) ) );
	},

	// Access the extension table
	extension_table: function( word, value )
	{
		var addr = this.extension;
		if ( !addr || word > this.extension_count )
		{
			return 0;
		}
		addr += 2 * word;
		if ( value === undefined )
		{
			return this.m.getUint16( addr );
		}
		this.ram.setUint16( addr, value );
	},

	// Find the address of a property, or given the previous property, the number of the next
	find_prop: function( object, property, prev )
	{
		var memory = this.m,
		version3 = this.version3,

		this_property_byte, this_property,
		last_property = 0,

		// Get this property table
		properties = memory.getUint16( this.objects + ( version3 ? 9 : 14 ) * object + ( version3 ? 7 : 12 ) );

		// Skip over the object's short name
		properties += memory.getUint8( properties ) * 2 + 1;

		// Run through the properties
		while ( 1 )
		{
			this_property_byte = memory.getUint8( properties );
			this_property = this_property_byte & ( version3 ? 0x1F : 0x3F );

			// Found the previous property, so return this one's number
			if ( last_property === prev )
			{
				return this_property;
			}
			// Found the property! Return its address
			if ( this_property === property )
			{
				// Must include the offset
				return properties + ( !version3 && this_property_byte & 0x80 ? 2 : 1 );
			}
			// Gone past the property
			if ( this_property < property )
			{
				return 0;
			}

			// Go to next property
			last_property = this_property;

			// Calculate the size of this property and skip to the next
			if ( version3 )
			{
				properties += ( this_property_byte >> 5 ) + 2;
			}
			else
			{
				if ( this_property_byte & 0x80 )
				{
					this_property = memory.getUint8( properties + 1 ) & 0x3F;
					properties += this_property ? this_property + 2 : 66;
				}
				else
				{
					properties += this_property_byte & 0x40 ? 3 : 2;
				}
			}
		}
	},

	// 1.2 spec @gestalt
	gestalt: function( id /*, arg*/ )
	{
		switch ( id )
		{
			case 1:
				return 0x0102;
		}
		return 0;
	},

	// Get the first child of an object
	get_child: function( obj )
	{
		if ( this.version3 )
		{
			return this.m.getUint8( this.objects + 9 * obj + 6 );
		}
		else
		{
			return this.m.getUint16( this.objects + 14 * obj + 10 );
		}
	},

	get_sibling: function( obj )
	{
		if ( this.version3 )
		{
			return this.m.getUint8( this.objects + 9 * obj + 5 );
		}
		else
		{
			return this.m.getUint16( this.objects + 14 * obj + 8 );
		}
	},

	get_parent: function( obj )
	{
		if ( this.version3 )
		{
			return this.m.getUint8( this.objects + 9 * obj + 4 );
		}
		else
		{
			return this.m.getUint16( this.objects + 14 * obj + 6 );
		}
	},

	get_prop: function( object, property )
	{
		var memory = this.m,

		// Try to find the property
		addr = this.find_prop( object, property ),
		len;

		// If we have the property
		if ( addr )
		{
			len = memory.getUint8( addr - 1 );
			// Assume we're being called for a valid short property
			return memory[ ( this.version3 ? len >> 5 : len & 0x40 ) ? 'getUint16' : 'getUint8' ]( addr );
		}

		// Use the default properties table
		// Remember that properties are 1-indexed
		return memory.getUint16( this.properties + 2 * ( property - 1 ) );
	},

	// Get the length of a property
	// This opcode expects the address of the property data, not a property block
	get_prop_len: function( addr )
	{
		// Spec 1.1
		if ( addr === 0 )
		{
			return 0;
		}

		var value = this.m.getUint8( addr - 1 );

		// Version 3
		if ( this.version3 )
		{
			return ( value >> 5 ) + 1;
		}

		// Two size/number bytes
		if ( value & 0x80 )
		{
			value &= 0x3F;
			return value === 0 ? 64 : value;
		}
		// One byte size/number
		return value & 0x40 ? 2 : 1;
	},

	// Quick hack for @inc/@dec/@inc_chk/@dec_chk
	incdec: function( varnum, change )
	{
		if ( varnum === 0 )
		{
			this.s[this.sp - 1] += change;
			return this.s[this.sp - 1];
		}
		if ( --varnum < 15 )
		{
			this.l[varnum] += change;
			return this.l[varnum];
		}
		else
		{
			var offset = this.globals + ( varnum - 15 ) * 2;
			this.ram.setUint16( offset, this.m.getUint16( offset ) + change );
			return this.ram.getUint16( offset );
		}
	},

	// Indirect variables
	indirect: function( variable, value )
	{
		if ( variable === 0 )
		{
			if ( arguments.length > 1 )
			{
				return this.s[this.sp - 1] = value;
			}
			else
			{
				return this.s[this.sp - 1];
			}
		}
		return this.variable( variable, value );
	},

	insert_obj: function( obj, dest )
	{
		// First remove the obj from wherever it was
		this.remove_obj( obj );
		// Now add it to the destination
		this.set_family( obj, dest, dest, obj, obj, this.get_child( dest ) );
	},

	// @jeq
	jeq: function()
	{
		var i = 1;

		// Account for many arguments
		while ( i < arguments.length )
		{
			if ( arguments[i++] === arguments[0] )
			{
				return 1;
			}
		}
	},

	jin: function( child, parent )
	{
		return this.get_parent( child ) === parent;
	},

	log: function( message )
	{
		if ( this.options.GlkOte )
		{
			this.options.GlkOte.log( message );
		}
	},

	log_shift: function( number, places )
	{
		return places > 0 ? number << places : number >>> -places;
	},

	make_stacks: function()
	{
		var locals_count = this.stack.getUint8( this.frameptr + 3 ) & 0x0F;
		this.l = new Uint16Array( this.stack.buffer, this.frameptr + 8, locals_count );
		this.s = new Uint16Array( this.stack.buffer, this.frameptr + 8 + locals_count * 2 );
	},

	put_prop: function( object, property, value )
	{
		// Try to find the property
		var addr = this.find_prop( object, property ),
		len;

		if ( addr )
		{
			len = this.m.getUint8( addr - 1 );

			// Assume we're being called for a valid short property
			this.ram[ ( this.version3 ? len >> 5 : len & 0x40 ) ? 'setUint16' : 'setUint8' ]( addr, value );
		}
	},

	random: function( range )
	{
		var seed = this.xorshift_seed;

		// Switch to the Xorshift RNG (or switch off if range == 0)
		if ( range < 1 )
		{
			this.xorshift_seed = range;
			return 0;
		}

		// Pure randomness
		if ( seed === 0 )
		{
			return 1 + ( Math.random() * range ) | 0;
		}

		// Based on the discussions in this forum topic, we will not implement the sequential mode recommended in the standard
		// http://www.intfiction.org/forum/viewtopic.php?f=38&t=16023

		// Instead implement a 32 bit Xorshift generator
		seed ^= ( seed << 13 );
		seed ^= ( seed >> 17 );
		this.xorshift_seed = ( seed ^= ( seed << 5 ) );
		return 1 + ( ( seed & 0x7FFF ) % range );
	},

	remove_obj: function( obj )
	{
		var parent = this.get_parent( obj ),
		older_sibling,
		younger_sibling,
		temp_younger;

		// No parent, do nothing
		if ( parent === 0 )
		{
			return;
		}

		older_sibling = this.get_child( parent );
		younger_sibling = this.get_sibling( obj );

		// obj is first child
		if ( older_sibling === obj )
		{
			this.set_family( obj, 0, parent, younger_sibling );
		}
		// obj isn't first child, so fix the older sibling
		else
		{
			// Go through the tree until we find the older sibling
			while ( 1 )
			{
				temp_younger = this.get_sibling( older_sibling );
				if ( temp_younger === obj )
				{
					break;
				}
				older_sibling = temp_younger;
			}
			this.set_family( obj, 0, 0, 0, older_sibling, younger_sibling );
		}
	},

	// (Re)start the VM
	restart: function(autorestoring)
	{
		var ram = this.ram,
		version = ram.getUint8( 0x00 ),
		version3 = version === 3,
		addr_multipler = version3 ? 2 : ( version === 8 ? 8 : 4 ),
		flags2 = ram.getUint8( 0x11 ),
		property_defaults = ram.getUint16( 0x0A ),
		extension = ( version > 4 ) ? ram.getUint16( 0x36 ) : 0,
		stack = utils.MemoryView( this.options.stack_len );

		// Reset the RAM, but preserve flags 2
		ram.setUint8Array( 0, this.origram );
		ram.setUint8( 0x11, flags2 );

		extend( this, {

			// Locals and stacks of various kinds
			stack: stack,
			frameptr: 0,
			frames: [],
			s: new Uint16Array( stack.buffer, 8 ),
			sp: 0,
			l: [],
			undo: [],
			undo_len: 0,
			
			glk_blocking_call: null,

			// Get some header variables
			version: version,
			version3: version3,
			pc: ram.getUint16( 0x06 ),
			properties: property_defaults,
			objects: property_defaults + ( version3 ? 53 : 112 ), // 62-9 or 126-14 - if we take this now then we won't need to always decrement the object number
			globals: ram.getUint16( 0x0C ),
			// staticmem: set in prepare()
			eof: ( ram.getUint16( 0x1A ) || 65536 ) * addr_multipler,
			extension: extension,
			extension_count: extension ? ram.getUint16( extension ) : 0,

			// Routine and string multiplier
			addr_multipler: addr_multipler,

			// Opcodes for this version of the Z-Machine
			opcodes: require( './opcodes.js' )( version ),

		});

		this.init_text();
		if (!autorestoring)
		{
			this.init_io()
		}

		// Update the header
		this.update_header();
	},

	// Request a restore
	restore: function( pc )
	{
		this.pc = pc;
		this.fileref_create_by_prompt({
			func: 'restore',
			mode: 0x02,
			usage: 0x01,
		});
	},

	restore_file: function( data, autorestoring )
	{
		var ram = this.ram,
		quetzal = new file.Quetzal( data ),
		qmem = quetzal.memory,
		stack = this.stack,
		flags2 = ram.getUint8( 0x11 ),
		temp,
		i = 0, j = 0;
		
		// Check this is a savefile for this story
		if ( ram.getUint16( 0x02 ) !== quetzal.release || ram.getUint16( 0x1C ) !== quetzal.checksum )
		{
			return 0;
		}
		while ( i < 6 )
		{
			if ( ram.getUint8( 0x12 + i ) !== quetzal.serial[i++] )
			{
				return 0;
			}
		}
		i = 0;

		// Memory chunk
		// Reset the RAM
		ram.setUint8Array( 0, this.origram );
		if ( quetzal.compressed )
		{
			while ( i < qmem.length )
			{
				temp = qmem[i++];
				// Same memory
				if ( temp === 0 )
				{
					j += 1 + qmem[i++];
				}
				else
				{
					ram.setUint8( j, temp ^ this.origram[j++] );
				}
			}
		}
		else
		{
			ram.setUint8Array( 0, qmem );
		}
		// Preserve flags 2
		ram.setUint8( 0x11, flags2 );

		// Stacks
		stack.setUint8Array( 0, quetzal.stacks );
		this.frames = [];
		i = 0;
		while ( i < quetzal.stacks.byteLength )
		{
			this.frameptr = i;
			this.frames.push( i );
			// Swap the bytes of the locals and stacks
			fix_stack_endianness( stack, j = i + 8, j += ( stack.getUint8( i + 3 ) & 0x0F ) * 2, autorestoring )
			fix_stack_endianness( stack, j, j += stack.getUint16( i + 6 ) * 2, autorestoring )
			i = j;
		}
		this.frames.pop();
		this.sp = stack.getUint16( this.frameptr + 6 );
		this.make_stacks();

		this.pc = quetzal.pc;
		this.update_header();

		// Collapse the upper window (8.6.1.3)
		if ( this.version3 )
		{
			this.split_window( 0 );
		}

		return 2;
	},

	restore_undo: function()
	{
		if ( this.undo.length === 0 )
		{
			return 0;
		}

		var state = this.undo.pop();
		this.frameptr = state.frameptr;
		this.pc = state.pc;
		this.undo_len -= ( state.ram.byteLength + state.stack.byteLength );

		// Replace the ram, preserving flags 2
		state.ram[0x11] = this.m.getUint8( 0x11 );
		this.ram.setUint8Array( 0, state.ram );

		// Fix up the stack
		this.frames = state.frames;
		this.sp = state.sp;
		this.stack.setUint8Array( 0, state.stack );
		this.make_stacks();

		this.variable( state.var, 2 );
		return 1;
	},

	// Return from a routine
	ret: function( result )
	{
		var stack = this.stack,

		// Get the storer and return pc from this frame
		frameptr = this.frameptr,
		storer = stack.getUint8( frameptr + 3 ) & 0x10 ? -1 : stack.getUint8( frameptr + 4 );
		this.pc = stack.getUint32( frameptr ) >> 8;

		// Recreate the locals and stacks from the previous frame
		frameptr = this.frameptr = this.frames.pop();
		this.make_stacks();
		this.sp = stack.getUint16( frameptr + 6 );

		// Store the result if there is one
		if ( storer >= 0 )
		{
			this.variable( storer, result || 0 );
		}
	},

	// pc is the address of the storer operand (or branch in v3)
	save: function( pc )
	{
		this.pc = pc;
		this.fileref_create_by_prompt({
			func: 'save',
			mode: 0x01,
			usage: 0x01,
		});
	},
	
	save_file: function( pc, autosaving )
	{
		var memory = this.m,
		quetzal = new file.Quetzal(),
		stack = utils.MemoryView( this.stack.buffer.slice() ),
		zeroes = 0,
		i, j,
		frameptr = this.frameptr,
		abyte;

		// IFhd chunk
		quetzal.release = memory.getUint16( 0x02 );
		quetzal.serial = memory.getUint8Array( 0x12, 6 );
		quetzal.checksum = memory.getUint16( 0x1C );
		quetzal.pc = pc;

		// Memory chunk
		if ( autosaving )
		{
			quetzal.memory = this.m.getUint8Array( 0, this.staticmem )
		}
		else
		{
			const compressed_mem = []
			quetzal.compressed = 1;
			for ( i = 0; i < this.staticmem; i++ )
			{
				abyte = memory.getUint8( i ) ^ this.origram[i];
				if ( abyte === 0 )
				{
					if ( ++zeroes === 256 )
					{
						compressed_mem.push( 0, 255 );
						zeroes = 0;
					}
				}
				else
				{
					if ( zeroes )
					{
						compressed_mem.push( 0, zeroes - 1 );
						zeroes = 0;
					}
					compressed_mem.push( abyte );
				}
			}
			quetzal.memory = compressed_mem;
		}

		// Stacks
		// Set the current sp
		stack.setUint16( frameptr + 6, this.sp );

		// Swap the bytes of the locals and stacks
		if ( littleEndian && !autosaving )
		{
			const frames = this.frames.slice()
			frames.push( frameptr )
			for ( i = 0; i < frames.length; i++ )
			{
				frameptr = frames[i];
				fix_stack_endianness( stack, j = frameptr + 8, j += ( stack.getUint8( frameptr + 3 ) & 0x0F ) * 2 );
				fix_stack_endianness( stack, j, j += stack.getUint16( frameptr + 6 ) * 2 );
			}
		}
		quetzal.stacks = stack.getUint8Array( 0, this.frameptr + 8 + ( stack.getUint8( frameptr + 3 ) & 0x0F ) * 2 + this.sp * 2 );

		return quetzal.write();
	},
	
	save_restore_handler: function( str )
	{
		var memory = this.m,
		Glk = this.Glk,
		result = 0,
		buffer = [],
		temp, iftrue, offset;
		
		if ( str )
		{
			// Save
			if ( this.fileref_data.func === 'save' )
			{
				Glk.glk_put_buffer_stream( str, new Uint8Array( this.save_file( this.pc ) ) );
				result = 1;
			}
			// Restore
			else
			{
				buffer = new Uint8Array( 128 * 1024 );
				Glk.glk_get_buffer_stream( str, buffer );
				result = this.restore_file( buffer.buffer );
			}
			Glk.glk_stream_close( str );
		}
		
		// Store the result / branch in z3
		if ( this.version3 )
		{
			// Calculate the branch
			temp = memory.getUint8( this.pc++ );
			iftrue = temp & 0x80;
			offset = temp & 0x40 ?
				// single byte address
				temp & 0x3F :
				// word address, but first get the second byte of it
				( temp << 8 | memory.getUint8( this.pc++ ) ) << 18 >> 18;

			if ( !result === !iftrue )
			{
				if ( offset === 0 || offset === 1 )
				{
					this.ret( offset );
				}
				else
				{
					this.pc += offset - 2;
				}
			}
		}
		else
		{
			this.variable( memory.getUint8( this.pc++ ), result );
		}
	},

	save_undo: function( pc, variable )
	{
		// Drop an old undo state if we've reached the limit, but always save at least one state
		var state
		if ( this.undo_len > this.options.undo_len )
		{
			state = this.undo.shift()
			this.undo_len -= ( state.ram.byteLength + state.stack.byteLength )
		}
		state = {
			frameptr: this.frameptr,
			frames: this.frames.slice(),
			pc: pc,
			ram: this.m.getUint8Array( 0, this.staticmem ),
			sp: this.sp,
			stack: this.stack.getUint8Array( 0, this.s.byteOffset + this.sp * 2 ),
			var: variable,
		}
		this.undo_len += ( state.ram.byteLength + state.stack.byteLength )
		this.undo.push( state )
		return 1
	},

	scan_table: function( key, addr, length, form )
	{
		form = form || 0x82;
		var memoryfunc = form & 0x80 ? 'getUint16' : 'getUint8';
		form &= 0x7F;
		length = addr + length * form;

		while ( addr < length )
		{
			if ( this.m[memoryfunc]( addr ) === key )
			{
				return addr;
			}
			addr += form;
		}
		return 0;
	},

	set_attr: function( object, attribute )
	{
		var addr = this.objects + ( this.version3 ? 9 : 14 ) * object + ( attribute / 8 ) | 0;
		this.ram.setUint8( addr, this.m.getUint8( addr ) | 0x80 >> attribute % 8 );
	},

	set_family: function( obj, newparent, parent, child, bigsis, lilsis )
	{
		var ram = this.ram,
		objects = this.objects;

		if ( this.version3 )
		{
			// Set the new parent of the obj
			ram.setUint8( objects + 9 * obj + 4, newparent );
			// Update the parent's first child if needed
			if ( parent )
			{
				ram.setUint8( objects + 9 * parent + 6, child );
			}
			// Update the little sister of a big sister
			if ( bigsis )
			{
				ram.setUint8( objects + 9 * bigsis + 5, lilsis );
			}
		}
		else
		{
			// Set the new parent of the obj
			ram.setUint16( objects + 14 * obj + 6, newparent );
			// Update the parent's first child if needed
			if ( parent )
			{
				ram.setUint16( objects + 14 * parent + 10, child );
			}
			// Update the little sister of a big sister
			if ( bigsis )
			{
				ram.setUint16( objects + 14 * bigsis + 8, lilsis );
			}
		}
	},

	test: function( bitmap, flag )
	{
		return ( bitmap & flag ) === flag;
	},

	test_attr: function( object, attribute )
	{
		return ( this.m.getUint8( this.objects + ( this.version3 ? 9 : 14 ) * object + ( attribute / 8 ) | 0 ) << attribute % 8 ) & 0x80;
	},

	// Read or write a variable
	variable: function( variable, value )
	{
		var havevalue = value !== undefined,
		offset;
		if ( variable === 0 )
		{
			if ( havevalue )
			{
				this.s[this.sp++] = value;
			}
			else
			{
				return this.s[--this.sp];
			}
		}
		else if ( --variable < 15 )
		{
			if ( havevalue )
			{
				this.l[variable] = value;
			}
			else
			{
				return this.l[variable];
			}
		}
		else
		{
			offset = this.globals + ( variable - 15 ) * 2;
			if ( havevalue )
			{
				this.ram.setUint16( offset, value );
			}
			else
			{
				return this.m.getUint16( offset );
			}
		}
		return value;
	},

	// Utilities for signed arithmetic
	U2S: U2S,
	S2U: S2U,

};
