/* 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 "common/config-manager.h"
#include "common/file.h"
#include "common/substream.h"

#include "director/director.h"
#include "director/cast.h"
#include "director/debugger.h"
#include "director/movie.h"
#include "director/window.h"
#include "director/castmember/castmember.h"
#include "director/castmember/script.h"
#include "director/lingo/lingo-code.h"
#include "director/lingo/lingo-codegen.h"
#include "director/lingo/lingo-builtins.h"
#include "director/lingo/lingo-bytecode.h"
#include "director/lingo/lingo-the.h"

namespace Director {

static const LingoV4Bytecode lingoV4[] = {
	{ 0x01, LC::c_procret,		"" },
	{ 0x02, LC::c_procret,		"" },
	{ 0x03, LC::cb_zeropush,	"" },
	{ 0x04, LC::c_mul,			"" },
	{ 0x05, LC::c_add,			"" },
	{ 0x06, LC::c_sub,			"" },
	{ 0x07, LC::c_div,			"" },
	{ 0x08, LC::c_mod,			"" },
	{ 0x09, LC::c_negate,		"" },
	{ 0x0a, LC::c_ampersand,	"" },
	{ 0x0b, LC::c_concat,		"" },
	{ 0x0c, LC::c_lt,			"" },
	{ 0x0d, LC::c_le,			"" },
	{ 0x0e, LC::c_neq,			"" },
	{ 0x0f, LC::c_eq,			"" },
	{ 0x10, LC::c_gt,			"" },
	{ 0x11, LC::c_ge,			"" },
	{ 0x12, LC::c_and,			"" },
	{ 0x13, LC::c_or,			"" },
	{ 0x14, LC::c_not,			"" },
	{ 0x15, LC::c_contains,		"" },
	{ 0x16, LC::c_starts,		"" },
	{ 0x17, LC::c_of,			"" },
	{ 0x18, LC::cb_hilite,		"" },
	{ 0x19, LC::c_intersects,	"" },
	{ 0x1a, LC::c_within,		"" },
	{ 0x1b, LC::c_field,		"" },
	{ 0x1c, LC::c_tell,			"" },
	{ 0x1d, LC::c_telldone,		"" },
	{ 0x1e, LC::cb_list,		"" },
	{ 0x1f, LC::cb_proplist,	"" },

	{ 0x41, LC::c_intpush,		"B" },
	{ 0x42, LC::c_argcnoretpush,"b" },
	{ 0x43, LC::c_argcpush,		"b" },
	// 0x44, push a constant
	{ 0x45, LC::c_namepush,		"bN" },
	{ 0x46, LC::cb_varrefpush,  "bN" },
	{ 0x48, LC::cb_globalpush,	"bN" }, // used in event scripts
	{ 0x49, LC::cb_globalpush,	"bN" },
	{ 0x4a, LC::cb_thepush,		"bN" },
	{ 0x4b, LC::cb_varpush,		"bpaN" },
	{ 0x4c, LC::cb_varpush,		"bpvN" },
	{ 0x4e, LC::cb_globalassign,"bN" }, // used in event scripts
	{ 0x4f, LC::cb_globalassign,"bN" },
	{ 0x50, LC::cb_theassign,	"bN" },
	{ 0x51, LC::cb_varassign,	"bpaN" },
	{ 0x52, LC::cb_varassign,	"bpvN" },
	{ 0x53, LC::c_jump,			"jb" },
	{ 0x54, LC::c_jump,			"jbn" },
	{ 0x55, LC::c_jumpifz,		"jb" },
	{ 0x56, LC::cb_localcall,	"b" },
	{ 0x57, LC::cb_call,		"bN" },
	{ 0x58, LC::cb_objectcall,  "b" },
	{ 0x59, LC::cb_v4assign,	"b" },
	{ 0x5a, LC::cb_v4assign2,	"b" },
	{ 0x5b, LC::cb_delete, 		"b" },
	{ 0x5c, LC::cb_v4theentitypush, "b" },
	{ 0x5d, LC::cb_v4theentityassign, "b" },
	{ 0x5f, LC::cb_thepush2,	"bN" },
	{ 0x60, LC::cb_theassign2,	"bN" },
	{ 0x61, LC::cb_objectfieldpush, "bN" },
	{ 0x62, LC::cb_objectfieldassign, "bN" },
	{ 0x63, LC::cb_call,		"bN" }, // tellcall
	{ 0x64, LC::c_stackpeek, 	"b" },
	{ 0x65, LC::c_stackdrop, 	"b" },
	{ 0x66, LC::cb_v4theentitynamepush, "bN" },

	{ 0x81, LC::c_intpush,		"W" },
	{ 0x82, LC::c_argcnoretpush,"w" },
	{ 0x83, LC::c_argcpush,		"w" },
	// 0x84, push a constant
	{ 0x85, LC::c_namepush,		"wN" },
	{ 0x86, LC::cb_varrefpush,  "wN" },
	{ 0x88, LC::cb_globalpush,	"wN" }, // used in event scripts
	{ 0x89, LC::cb_globalpush,	"wN" },
	{ 0x8a, LC::cb_thepush,		"wN" },
	{ 0x8b, LC::cb_varpush,		"wpaN" },
	{ 0x8c, LC::cb_varpush,		"wpvN" },
	{ 0x8e, LC::cb_globalassign,"wN" }, // used in event scripts
	{ 0x8f, LC::cb_globalassign,"wN" },
	{ 0x90, LC::cb_theassign, 	"wN" },
	{ 0x91, LC::cb_varassign,	"wpaN" },
	{ 0x92, LC::cb_varassign,	"wpvN" },
	{ 0x93, LC::c_jump,			"jw" },
	{ 0x94, LC::c_jump,			"jwn" },
	{ 0x95, LC::c_jumpifz,		"jw" },
	{ 0x96, LC::cb_localcall,	"w" },
	{ 0x97, LC::cb_call,		"wN" },
	{ 0x98, LC::cb_objectcall,  "w" },
	{ 0x99, LC::cb_v4assign,	"w" },
	{ 0x9a, LC::cb_v4assign2,	"w" },
	{ 0x9c, LC::cb_v4theentitypush, "w" },
	{ 0x9d, LC::cb_v4theentityassign, "w" },
	{ 0x9f, LC::cb_thepush2, 	"wN" },
	{ 0xa0, LC::cb_theassign2, "wN" },
	{ 0xa1, LC::cb_objectfieldpush, "wN" },
	{ 0xa2, LC::cb_objectfieldassign, "wN" },
	{ 0xa3, LC::cb_call,		"wN" }, // tellcall
	{ 0xa4, LC::c_stackpeek, 	"w" },
	{ 0xa5, LC::c_stackdrop, 	"w" },
	{ 0xa6, LC::cb_v4theentitynamepush, "wN" },
	{ 0, nullptr, nullptr }
};

static const LingoV4TheEntity lingoV4TheEntity[] = {
	{ 0x00, 0x00, kTheFloatPrecision,	kTheNOField,		true, kTEANOArgs },
	{ 0x00, 0x01, kTheMouseDownScript,	kTheNOField,		true, kTEANOArgs },
	{ 0x00, 0x02, kTheMouseUpScript,	kTheNOField,		true, kTEANOArgs },
	{ 0x00, 0x03, kTheKeyDownScript,	kTheNOField,		true, kTEANOArgs },
	{ 0x00, 0x04, kTheKeyUpScript,		kTheNOField,		true, kTEANOArgs },
	{ 0x00, 0x05, kTheTimeoutScript,	kTheNOField,		true, kTEANOArgs },
	{ 0x00, 0x06, kTheTime,				kTheShort,			false, kTEANOArgs },
	{ 0x00, 0x07, kTheTime,				kTheAbbr,			false, kTEANOArgs },
	{ 0x00, 0x08, kTheTime,				kTheLong,			false, kTEANOArgs },
	{ 0x00, 0x09, kTheDate,				kTheShort,			false, kTEANOArgs },
	{ 0x00, 0x0a, kTheDate,				kTheAbbr,			false, kTEANOArgs },
	{ 0x00, 0x0b, kTheDate,				kTheLong,			false, kTEANOArgs },
	{ 0x00, 0x0c, kTheChars,			kTheLast,			false, kTEAString },
	{ 0x00, 0x0d, kTheWords,			kTheLast,			false, kTEAString },
	{ 0x00, 0x0e, kTheItems,			kTheLast,			false, kTEAString },
	{ 0x00, 0x0f, kTheLines,			kTheLast,			false, kTEAString },

	{ 0x01, 0x01, kTheChars,			kTheNumber,			false, kTEAString },
	{ 0x01, 0x02, kTheWords,			kTheNumber,			false, kTEAString },
	{ 0x01, 0x03, kTheItems,			kTheNumber,			false, kTEAString },
	{ 0x01, 0x04, kTheLines,			kTheNumber,			false, kTEAString },

	{ 0x02, 0x01, kTheMenu,				kTheName,			false, kTEAMenuId },
	{ 0x02, 0x02, kTheMenuItems,		kTheNumber,			false, kTEAMenuId },

	{ 0x03, 0x01, kTheMenuItem,			kTheName,			true, kTEAMenuIdItemId },
	{ 0x03, 0x02, kTheMenuItem,			kTheCheckMark,		true, kTEAMenuIdItemId },
	{ 0x03, 0x03, kTheMenuItem,			kTheEnabled,		true, kTEAMenuIdItemId },
	{ 0x03, 0x04, kTheMenuItem,			kTheScript,			true, kTEAMenuIdItemId },

	{ 0x04, 0x01, kTheSoundEntity,		kTheVolume,			true, kTEAItemId },

	{ 0x06, 0x01, kTheSprite,			kTheType,			true, kTEAItemId },
	{ 0x06, 0x02, kTheSprite,			kTheBackColor,		true, kTEAItemId },
	{ 0x06, 0x03, kTheSprite,			kTheBottom,			true, kTEAItemId },
	{ 0x06, 0x04, kTheSprite,			kTheCastNum,		true, kTEAItemId },
	{ 0x06, 0x05, kTheSprite,			kTheConstraint,		true, kTEAItemId },
	{ 0x06, 0x06, kTheSprite,			kTheCursor,			true, kTEAItemId },
	{ 0x06, 0x07, kTheSprite,			kTheForeColor,		true, kTEAItemId },
	{ 0x06, 0x08, kTheSprite,			kTheHeight,			true, kTEAItemId },
	{ 0x06, 0x09, kTheSprite,			kTheImmediate,		true, kTEAItemId },
	{ 0x06, 0x0a, kTheSprite,			kTheInk,			true, kTEAItemId },
	{ 0x06, 0x0b, kTheSprite,			kTheLeft,			true, kTEAItemId },
	{ 0x06, 0x0c, kTheSprite,			kTheLineSize,		true, kTEAItemId },
	{ 0x06, 0x0d, kTheSprite,			kTheLocH,			true, kTEAItemId },
	{ 0x06, 0x0e, kTheSprite,			kTheLocV,			true, kTEAItemId },
	{ 0x06, 0x0f, kTheSprite,			kTheMovieRate,		true, kTEAItemId },
	{ 0x06, 0x10, kTheSprite,			kTheMovieTime,		true, kTEAItemId },
	{ 0x06, 0x11, kTheSprite,			kThePattern,		true, kTEAItemId },
	{ 0x06, 0x12, kTheSprite,			kThePuppet,			true, kTEAItemId },
	{ 0x06, 0x13, kTheSprite,			kTheRight,			true, kTEAItemId },
	{ 0x06, 0x14, kTheSprite,			kTheStartTime,		true, kTEAItemId },
	{ 0x06, 0x15, kTheSprite,			kTheStopTime,		true, kTEAItemId },
	{ 0x06, 0x16, kTheSprite,			kTheStretch,		true, kTEAItemId },
	{ 0x06, 0x17, kTheSprite,			kTheTop,			true, kTEAItemId },
	{ 0x06, 0x18, kTheSprite,			kTheTrails,			true, kTEAItemId },
	{ 0x06, 0x19, kTheSprite,			kTheVisible,		true, kTEAItemId },
	{ 0x06, 0x1a, kTheSprite,			kTheVolume,			true, kTEAItemId },
	{ 0x06, 0x1b, kTheSprite,			kTheWidth,			true, kTEAItemId },
	{ 0x06, 0x1c, kTheSprite,			kTheBlend,			true, kTEAItemId },
	{ 0x06, 0x1d, kTheSprite,			kTheScriptNum,		true, kTEAItemId },
	{ 0x06, 0x1e, kTheSprite,			kTheMoveableSprite,	true, kTEAItemId },
	{ 0x06, 0x1f, kTheSprite,			kTheEditableText,	true, kTEAItemId },
	{ 0x06, 0x20, kTheSprite,			kTheScoreColor,		true, kTEAItemId },
	{ 0x06, 0x21, kTheSprite,			kTheLoc,			true, kTEAItemId },
	{ 0x06, 0x22, kTheSprite,			kTheRect,			true, kTEAItemId },
	{ 0x06, 0x23, kTheSprite,			kTheMemberNum,		true, kTEAItemId }, // D5
	{ 0x06, 0x24, kTheSprite, 			kTheCastLibNum, 	true, kTEAItemId }, // D5
	{ 0x06, 0x25, kTheSprite,			kTheMember,			true, kTEAItemId }, // D5
	{ 0x06, 0x26, kTheSprite,			kTheScriptInstanceList,	true, kTEAItemId }, // D6
	{ 0x06, 0x27, kTheSprite,			kTheCurrentTime,	true, kTEAItemId }, // D6
	{ 0x06, 0x28, kTheSprite,			kTheMostRecentCuePoint,	true, kTEAItemId }, // D6
	{ 0x06, 0x29, kTheSprite,			kTheTweened,		true, kTEAItemId }, // D6
	{ 0x06, 0x2a, kTheSprite,			kTheName,			true, kTEAItemId }, // D6

	{ 0x07, 0x01, kTheBeepOn,			kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x02, kTheButtonStyle,		kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x03, kTheCenterStage,		kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x04, kTheCheckBoxAccess,	kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x05, kTheCheckBoxType,		kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x06, kTheColorDepth,		kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x07, kTheColorQD,			kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x08, kTheExitLock,			kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x09, kTheFixStageSize,		kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x0a, kTheFullColorPermit,	kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x0b, kTheImageDirect,		kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x0c, kTheDoubleClick,		kTheNOField,		true, kTEANOArgs },
//	{ 0x07, 0x0d, ???,					kTheNOField,		true, kTEANOArgs }, // key
	{ 0x07, 0x0e, kTheLastClick,		kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x0f, kTheLastEvent,		kTheNOField,		true, kTEANOArgs },
//	{ 0x07, 0x10, ???,					kTheNOField,		true, kTEANOArgs }, // keyCode
	{ 0x07, 0x11, kTheLastKey,			kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x12, kTheLastRoll,			kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x13, kTheTimeoutLapsed,	kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x14, kTheMultiSound,		kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x15, kThePauseState,		kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x16, kTheQuickTimePresent,	kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x17, kTheSelEnd,			kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x18, kTheSelStart,			kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x19, kTheSoundEnabled,		kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x1a, kTheSoundLevel,		kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x1b, kTheStageColor,		kTheNOField,		true, kTEANOArgs },
//	{ 0x07, 0x1c, ????,					kTheNOField,		true, kTEANOArgs }, // indicates dontPassEvent was called
	{ 0x07, 0x1d, kTheSwitchColorDepth,	kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x1e, kTheTimeoutKeyDown,	kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x1f, kTheTimeoutLength,	kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x20, kTheTimeoutMouse,		kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x21, kTheTimeoutPlay,		kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x22, kTheTimer,			kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x23, kThePreLoadRAM,		kTheNOField,		true, kTEANOArgs },
	{ 0x07, 0x24, kTheVideoForWindowsPresent, kTheNOField,	true, kTEANOArgs }, // D5
	// netPresent
	// safePlayer
	// soundKeepDevice
	// soundMixMedia

	{ 0x08, 0x01, kThePerFrameHook,		kTheNOField,		true, kTEANOArgs },
	{ 0x08, 0x02, kTheCastMembers,		kTheNumber,			false, kTEANOArgs },
	{ 0x08, 0x03, kTheMenus,			kTheNumber,			false, kTEANOArgs },
	{ 0x08, 0x04, kTheCastLibs,			kTheNumber,			false, kTEANOArgs }, // D5
	{ 0x08, 0x05, kTheXtras,			kTheNumber,			false, kTEANOArgs }, // D5

	{ 0x09, 0x01, kTheCast,				kTheName,			true, kTEAItemId },
	{ 0x09, 0x02, kTheCast,				kTheText,			true, kTEAItemId },
	{ 0x09, 0x03, kTheCast,	 			kTheTextStyle,		true, kTEAItemId },
	{ 0x09, 0x04, kTheCast,	 			kTheTextFont,		true, kTEAItemId },
	{ 0x09, 0x05, kTheCast,	 			kTheTextHeight,		true, kTEAItemId },
	{ 0x09, 0x06, kTheCast,	 			kTheTextAlign,		true, kTEAItemId },
	{ 0x09, 0x07, kTheCast,	 			kTheTextSize,		true, kTEAItemId },
	{ 0x09, 0x08, kTheCast,				kThePicture,		true, kTEAItemId },
	{ 0x09, 0x09, kTheCast,				kTheHilite,			true, kTEAItemId },
	{ 0x09, 0x0a, kTheCast,				kTheNumber,			true, kTEAItemId },
	{ 0x09, 0x0b, kTheCast,				kTheSize,			true, kTEAItemId },
	{ 0x09, 0x11, kTheCast,				kTheForeColor,		true, kTEAItemId },
	{ 0x09, 0x12, kTheCast,				kTheBackColor,		true, kTEAItemId },
	{ 0x09, 0x13, kTheCast,				kTheType,			true, kTEAItemId }, // D5

	// the chunk of cast
	{ 0x0a, 0x03, kTheChunk,			kTheTextStyle,		true, kTEAChunk },
	{ 0x0a, 0x04, kTheChunk,			kTheTextFont,		true, kTEAChunk },
	{ 0x0a, 0x05, kTheChunk,			kTheTextHeight,		true, kTEAChunk },
	{ 0x0a, 0x07, kTheChunk,			kTheTextSize,		true, kTEAChunk },
	{ 0x0a, 0x11, kTheChunk,			kTheForeColor,		true, kTEAChunk },

	{ 0x0b, 0x01, kTheField,			kTheName,			true, kTEAItemId },
	{ 0x0b, 0x02, kTheField,			kTheText,			true, kTEAItemId },
	{ 0x0b, 0x03, kTheField,			kTheTextStyle,		true, kTEAItemId },
	{ 0x0b, 0x04, kTheField,			kTheTextFont,		true, kTEAItemId },
	{ 0x0b, 0x05, kTheField,			kTheTextHeight,		true, kTEAItemId },
	{ 0x0b, 0x06, kTheField,			kTheTextAlign,		true, kTEAItemId },
	{ 0x0b, 0x07, kTheField,			kTheTextSize,		true, kTEAItemId },
	{ 0x0b, 0x09, kTheField,			kTheHilite,			true, kTEAItemId },
	{ 0x0b, 0x11, kTheField,			kTheForeColor,		true, kTEAItemId },

	// the chunk of field
	{ 0x0c, 0x03, kTheChunk,			kTheTextStyle,		true, kTEAChunk },
	{ 0x0c, 0x04, kTheChunk,			kTheTextFont,		true, kTEAChunk },
	{ 0x0c, 0x05, kTheChunk,			kTheTextHeight,		true, kTEAChunk },
	{ 0x0c, 0x07, kTheChunk,			kTheTextSize,		true, kTEAChunk },
	{ 0x0c, 0x11, kTheChunk,			kTheForeColor,		true, kTEAChunk },

	{ 0x0d, 0x0c, kTheCast,				kTheLoop,			true, kTEAItemId },
	{ 0x0d, 0x0d, kTheCast,				kTheDuration,		true, kTEAItemId },
	{ 0x0d, 0x0e, kTheCast,				kTheController,		true, kTEAItemId },
	{ 0x0d, 0x0f, kTheCast,				kTheDirectToStage,	true, kTEAItemId },
	{ 0x0d, 0x10, kTheCast,				kTheSound,			true, kTEAItemId },

	{ 0xff, 0, 0, 0, false, kTEANOArgs }
};


void Lingo::initBytecode() {
	// All new bytecodes must have respective entry in funcDescr[]
	// array in lingo-code.cpp, which is used for decompilation
	//
	// Check that all opcodes have entries
	Common::HashMap<inst, bool> list;
	bool bailout = false;

	// Build reverse hashmap
	for (auto &it : _functions)
		list[(inst)it._key] = true;

	for (const LingoV4Bytecode *op = lingoV4; op->opcode; op++) {
		_lingoV4[op->opcode] = op;

		if (!list.contains(op->func)) {
			warning("Lingo::initBytecode(): Missing prototype for opcode 0x%02x", op->opcode);
			bailout = true;
		}
	}

	if (bailout)
		error("Lingo::initBytecode(): Add entries to funcDescr[] in lingo-code.cpp");

	for (const LingoV4TheEntity *ent = lingoV4TheEntity; ent->bank != 0xff; ent++) {
		_lingoV4TheEntity[(ent->bank << 8) + ent->firstArg] = ent;
	}
}

Datum Lingo::findVarV4(int varType, const Datum &id) {
	Datum res;
	switch (varType) {
	case 1: // global
	case 2: // global
	case 3: // property/instance
		if (id.type == SYMBOL) {
			res = id;
			res.type = (varType == 3) ? PROPREF : GLOBALREF;
		} else {
			warning("BUILDBOT: findVarV4: expected ID for var type %d to be SYMBOL, got %s", varType, id.type2str());
		}
		break;
	case 4: // arg
	case 5: // local
		{
			Common::Array<CFrame *> &callstack = _state->callstack;
			if (callstack.empty()) {
				warning("BUILDBOT: findVarV4: no call frame");
				return res;
			}
			int stride = 6;
			if (g_director->getVersion() >= 500) {
				stride = 8;
			}
			if (id.asInt() % stride != 0) {
				warning("BUILDBOT: findVarV4: invalid var ID %d for var type %d (not divisible by %d)", id.asInt(), varType, stride);
				return res;
			}
			int varIndex = id.asInt() / stride;
			Common::Array<Common::String> *varNames = (varType == 4)
				? callstack.back()->sp.argNames
				: callstack.back()->sp.varNames;

			if (varIndex < (int)varNames->size()) {
				res = (*varNames)[varIndex];
				res.type = LOCALREF;
			} else {
				warning("BUILDBOT: findVarV4: invalid var ID %d for var type %d (too high)", id.asInt(), varType);
			}
		}
		break;
	case 6: // field
		if (g_director->getVersion() < 500) {
			res = id.asMemberID();
		} else {
			Datum member = g_lingo->pop();
			res = g_lingo->toCastMemberID(member, id);
		}
		res.type = FIELDREF;
		break;
	default:
		warning("BUILDBOT: findVarV4: unhandled var type %d", varType);
		break;
	}
	return res;
}

void LC::cb_unk() {
	uint opcode = g_lingo->readInt();
	warning("STUB: opcode 0x%02x", opcode);
}

void LC::cb_unk1() {
	uint opcode = g_lingo->readInt();
	uint arg1 = g_lingo->readInt();
	warning("STUB: opcode 0x%02x (%d)", opcode, arg1);
}

void LC::cb_unk2() {
	uint opcode = g_lingo->readInt();
	uint arg1 = g_lingo->readInt();
	uint arg2 = g_lingo->readInt();
	warning("STUB: opcode 0x%02x (%d, %d)", opcode, arg1, arg2);
}

void LC::cb_delete() {
	int varType = g_lingo->readInt();
	Datum varID = g_lingo->pop();
	Datum var = g_lingo->findVarV4(varType, varID);
	Datum chunkRef = readChunkRef(var);
	g_lingo->push(chunkRef);
	LC::c_delete();
}

void LC::cb_hilite() {
	Datum fieldID = g_lingo->pop().asMemberID();
	fieldID.type = FIELDREF;
	Datum chunkRef = readChunkRef(fieldID);
	g_lingo->push(chunkRef);
	LC::c_hilite();
}

void LC::cb_localcall() {
	int functionId = g_lingo->readInt();

	Datum nargs = g_lingo->pop();
	if ((nargs.type == ARGC) || (nargs.type == ARGCNORET)) {
		Common::String name = g_lingo->_state->context->_functionNames[functionId];
		if (debugChannelSet(3, kDebugLingoExec))
			g_lingo->printArgs(name.c_str(), nargs.u.i, "localcall:");

		LC::call(name, nargs.u.i, nargs.type == ARGC);

	} else {
		warning("cb_localcall: first arg should be of type ARGC or ARGCNORET, not %s", nargs.type2str());
	}

}


void LC::cb_objectcall() {
	int varType = g_lingo->readInt();
	Datum varId = g_lingo->pop();
	Datum nargs = g_lingo->pop();

	Datum var = g_lingo->findVarV4(varType, varId);
	if (!var.isVarRef()) {
		warning("cb_objectcall: first arg did not resolve to variable");
		return;
	}

	if ((nargs.type != ARGC) && (nargs.type != ARGCNORET)) {
		warning("cb_objectcall: second arg should be of type ARGC or ARGCNORET, not %s", nargs.type2str());
		return;
	}

	if (nargs.u.i > 0) {
		Datum &firstArg = g_lingo->_state->stack[g_lingo->_state->stack.size() - nargs.u.i];
		// The first arg could be either a method name or a variable name
		if (firstArg.type == SYMBOL) {
			firstArg.type = VARREF;
		}
	}

	LC::call(*var.u.s, nargs.u.i, nargs.type == ARGC);
}


void LC::cb_v4assign() {
	int arg = g_lingo->readInt();
	int op = (arg >> 4) & 0xF;
	int varType = arg & 0xF;
	Datum varId = g_lingo->pop();

	Datum var = g_lingo->findVarV4(varType, varId);
	g_lingo->push(var);

	switch (op) {
	case 1:
		// put value into var
		LC::c_assign();
		break;
	case 2:
		// put value after var
		LC::c_putafter();
		break;
	case 3:
		// put value before var
		LC::c_putbefore();
		break;
	default:
		warning("cb_v4assign: unknown operator %d", op);
		g_lingo->pop();
		break;
	}
}


void LC::cb_list() {
	Datum nargs = g_lingo->pop();
	if ((nargs.type != ARGC) && (nargs.type != ARGCNORET)) {
		error("cb_list: first arg should be of type ARGC or ARGCNORET, not %s", nargs.type2str());
	}
	LB::b_list(nargs.u.i);
}


void LC::cb_proplist() {
	Datum nargs = g_lingo->pop();
	if ((nargs.type != ARGC) && (nargs.type != ARGCNORET)) {
		error("cb_proplist: first arg should be of type ARGC or ARGCNORET, not %s", nargs.type2str());
	}
	int arraySize = nargs.u.i;
	if (arraySize % 2) {
		warning("cb_proplist: list should have an even number of entries, ignoring the last one");
	}

	Datum result;
	result.type = PARRAY;
	result.u.parr = new PArray;
	arraySize /= 2;

	for (int i = 0; i < arraySize; i++) {
		Datum v = g_lingo->pop();
		Datum p = g_lingo->pop();

		PCell cell = PCell(p, v);
		result.u.parr->arr.insert_at(0, cell);
	};

	if (nargs.u.i % 2)
		g_lingo->pop();

	g_lingo->push(result);
}


void LC::cb_call() {
	Common::String name = g_lingo->readString();

	Datum nargs = g_lingo->pop();
	if ((nargs.type == ARGC) || (nargs.type == ARGCNORET)) {
		LC::call(name, nargs.u.i, nargs.type == ARGC);

	} else {
		warning("cb_call: first arg should be of type ARGC or ARGCNORET, not %s", nargs.type2str());
	}

}


void LC::cb_globalpush() {
	Common::String name = g_lingo->readString();
	Datum target(name);
	target.type = GLOBALREF;
	debugC(3, kDebugLingoExec, "cb_globalpush: pushing %s to stack", name.c_str());
	Datum result = g_lingo->varFetch(target);
	g_lingo->push(result);
}


void LC::cb_globalassign() {
	Common::String name = g_lingo->readString();
	Datum target(name);
	target.type = GLOBALREF;
	debugC(3, kDebugLingoExec, "cb_globalassign: assigning to %s", name.c_str());
	Datum source = g_lingo->pop();
	g_lingo->varAssign(target, source);
}

void LC::cb_objectfieldassign() {
	Common::String fieldName = g_lingo->readString();
	Datum value = g_lingo->pop();
	Datum object = g_lingo->pop();
	g_lingo->setObjectProp(object, fieldName, value);
}

void LC::cb_objectfieldpush() {
	Common::String fieldName = g_lingo->readString();
	Datum object = g_lingo->pop();
	g_lingo->getObjectProp(object, fieldName);
}

void LC::cb_varrefpush() {
	Common::String name = g_lingo->readString();
	Datum result(name);
	result.type = SYMBOL;
	g_lingo->push(result);
}

void LC::cb_theassign() {
	// cb_theassign is for setting script/factory-level properties
	Common::String name = g_lingo->readString();
	Datum value = g_lingo->pop();
	if (g_lingo->_state->me.type == OBJECT) {
		// Don't bother checking if the property is defined, leave that to the object.
		// For D3-style anonymous objects/factories, you are allowed to define whatever properties you like.
		g_debugger->propWriteHook(name);
		g_lingo->_state->me.u.obj->setProp(name, value);
	} else {
		warning("cb_theassign: no me object");
	}
}

void LC::cb_theassign2() {
	// cb_theassign2 is for setting movie-level properties
	Common::String name = g_lingo->readString();
	Datum value = g_lingo->pop();

	if (g_lingo->_theEntities.contains(name)) {
		const TheEntity *entity = g_lingo->_theEntities[name];
		Datum id;
		id.u.i = 0;
		id.type = VOID;
		g_lingo->setTheEntity(entity->entity, id, kTEANOArgs, value);
	} else {
		warning("LC::cb_theassign2 Can't assign theEntity: (%s)", name.c_str());

		if (debugChannelSet(-1, kDebugLingoStrict)) {
			error("Uncaught Lingo error");
		}
	}
}

void LC::cb_thepush() {
	Common::String name = g_lingo->readString();
	if (g_lingo->_state->me.type == OBJECT) {
		if (g_lingo->_state->me.u.obj->hasProp(name)) {
			g_lingo->push(g_lingo->_state->me.u.obj->getProp(name));
			g_debugger->propReadHook(name);
			return;
		}

		if (name.equals("me")) {
			g_lingo->push(g_lingo->_state->me);
			g_debugger->propReadHook(name);
			return;
		}

		warning("cb_thepush: me object has no property '%s', type: %d", name.c_str(), g_lingo->_state->me.type);
	} else {
		debugC(1, kDebugLingoExec, "cb_thepush: attempted to access property '%s' with no me object, returning VOID", name.c_str());
	}
	g_lingo->pushVoid();
}

void LC::cb_thepush2() {
	Datum result;
	Common::String name = g_lingo->readString();
	if (g_lingo->_theEntities.contains(name)) {
		const TheEntity *entity = g_lingo->_theEntities[name];
		Datum id;
		id.u.i = 0;
		id.type = VOID;
		debugC(3, kDebugLingoExec, "cb_thepush: pushing value of entity %s to stack", name.c_str());
		result = g_lingo->getTheEntity(entity->entity, id, kTEANOArgs);
	} else {
		warning("LC::cb_thepush2 Can't find theEntity: (%s)", name.c_str());
		result.type = VOID;
	}
	g_lingo->push(result);
}

void LC::cb_varpush() {
	Common::String name = g_lingo->readString();
	Datum target(name);
	target.type = LOCALREF;
	debugC(3, kDebugLingoExec, "cb_varpush: pushing %s to stack", name.c_str());
	Datum result = g_lingo->varFetch(target);
	g_lingo->push(result);
}


void LC::cb_varassign() {
	Common::String name = g_lingo->readString();
	Datum target(name);
	target.type = LOCALREF;
	debugC(3, kDebugLingoExec, "cb_varassign: assigning to %s", name.c_str());
	Datum source = g_lingo->pop();
	// Local variables should be initialised by the script, no varCreate here
	g_lingo->varAssign(target, source);
}


void LC::cb_v4assign2() {
	int arg = g_lingo->readInt();
	int op = (arg >> 4) & 0xF;
	int varType = arg & 0xF;
	Datum varId = g_lingo->pop();

	Datum var = g_lingo->findVarV4(varType, varId);
	Datum ref = readChunkRef(var);
	g_lingo->push(ref);

	switch (op) {
	case 1:
		// put value into chunk
		LC::c_assign();
		break;
	case 2:
		// put value after chunk
		LC::c_putafter();
		break;
	case 3:
		// put value before chunk
		LC::c_putbefore();
		break;
	default:
		warning("cb_v4assign2: unknown operator %d", op);
		g_lingo->pop();
		break;
	}
}


void LC::cb_v4theentitypush() {
	int bank = g_lingo->readInt();

	int firstArg = g_lingo->pop().asInt();
	Datum result;
	result.u.s = nullptr;
	result.type = VOID;

	int key = (bank << 8) + firstArg;
	if (g_lingo->_lingoV4TheEntity.contains(key)) {
		debugC(3, kDebugLingoExec, "cb_v4theentitypush: mapping 0x%02x, 0x%02x", bank, firstArg);
		int entity = g_lingo->_lingoV4TheEntity[key]->entity;
		int field = g_lingo->_lingoV4TheEntity[key]->field;
		switch (g_lingo->_lingoV4TheEntity[key]->type) {
		case kTEANOArgs:
			{
				Datum id;
				id.u.i = 0;
				id.type = VOID;
				debugC(3, kDebugLingoExec, "cb_v4theentitypush: calling getTheEntity(%s, VOID, %s)", g_lingo->entity2str(entity), g_lingo->field2str(field));
				result = g_lingo->getTheEntity(entity, id, field);
			}
			break;
		case kTEAItemId:
			{
				Datum id = g_lingo->pop();
				if ((entity == kTheCast || entity == kTheField) && g_director->getVersion() >= 500) {
					// For "the member", D5 and above have a lib ID followed by a member ID
					// Pre-resolve them here.
					Datum member = g_lingo->pop();
					CastMemberID resolved = g_lingo->toCastMemberID(member, id);
					id = Datum(resolved);
				}
				debugC(3, kDebugLingoExec, "cb_v4theentitypush: calling getTheEntity(%s, %s, %s)", g_lingo->entity2str(entity), id.asString(true).c_str(), g_lingo->field2str(field));
				result = g_lingo->getTheEntity(entity, id, field);
			}
			break;
		case kTEAMenuId:
			{
				Datum id = g_lingo->pop();
				debugC(3, kDebugLingoExec, "cb_v4theentitypush: calling getTheEntity(%s, %s, %s)", g_lingo->entity2str(entity), id.asString(true).c_str(), g_lingo->field2str(field));
				if (id.type == INT) {
					int menuId = id.u.i;
					id.u.menu = new MenuReference();
					id.u.menu->menuIdNum = menuId;
				} else if (id.type == STRING) {
					Common::String *menuId = id.u.s;
					id.u.menu = new MenuReference();
					id.u.menu->menuIdStr = menuId;
				} else {
					warning("LC::cb_v4theentitypush : Unknown type of menu Reference %d of entity type %d", id.type, g_lingo->_lingoV4TheEntity[key]->type);
					break;
				}
				id.type = MENUREF;

				result = g_lingo->getTheEntity(entity, id, field);
			}
			break;
		case kTEAString:
			{
				Datum stringArg = g_lingo->pop();
				ChunkType chunkType = kChunkChar;
				switch (entity) {
				case kTheChars:
					chunkType = kChunkChar;
					break;
				case kTheWords:
					chunkType = kChunkWord;
					break;
				case kTheLines:
					chunkType = kChunkLine;
					break;
				case kTheItems:
					chunkType = kChunkItem;
					break;
				}
				Datum chunkRef = LC::lastChunk(chunkType, stringArg);
				switch (field) {
				case kTheLast:
					result = chunkRef.eval();
					break;
				case kTheNumber:
					result = chunkRef.u.cref->startChunk;
					break;
				}
			}
			break;
		case kTEAMenuIdItemId:
			{
				/*Datum menuId = */g_lingo->pop();
				/*Datum itemId = */g_lingo->pop();
				warning("cb_v4theentitypush: STUB: kTEAMenuIdItemId");
			}
			break;
		case kTEAChunk:
			{
				Datum fieldRef = g_lingo->pop().asMemberID();
				fieldRef.type = FIELDREF;
				Datum chunkRef = readChunkRef(fieldRef);
				result = g_lingo->getTheEntity(entity, chunkRef, field);
			}
			break;
		default:
			warning("cb_v4theentitypush: unknown call type %d", g_lingo->_lingoV4TheEntity[key]->type);
			break;
		}
	} else {
		warning("cb_v4theentitypush: BUILDBOT: unhandled mapping 0x%02x 0x%02x", bank, firstArg);
	}

	g_lingo->push(result);
}


void LC::cb_v4theentitynamepush() {
	Datum nargs = g_lingo->pop();
	if ((nargs.type == ARGC) || (nargs.type == ARGCNORET)) {
		if (nargs.u.i > 0) {
			warning("cb_v4theentitynamepush: expecting argc to be 0, not %d", nargs.u.i);
			for (int i = 0; i < nargs.u.i; i++) {
				g_lingo->pop();
			}
		}
	} else {
		warning("cb_v4theentitynamepush: first arg should be of type ARGC or ARGCNORET, not %s", nargs.type2str());
	}

	Common::String name = g_lingo->readString();

	Datum id;
	id.u.s = nullptr;
	id.type = VOID;

	if (!g_lingo->_theEntities.contains(name)) {
		warning("BUILDBOT: cb_v4theentitynamepush: missing the entity %s", name.c_str());
		g_lingo->push(Datum());
		return;
	}
	const TheEntity *entity = g_lingo->_theEntities[name];

	debugC(3, kDebugLingoExec, "cb_v4theentitynamepush: %s", name.c_str());
	debugC(3, kDebugLingoExec, "cb_v4theentitynamepush: calling getTheEntity(%s, VOID, kTheNOField)", g_lingo->entity2str(entity->entity));
	Datum result = g_lingo->getTheEntity(entity->entity, id, kTheNOField);

	g_lingo->push(result);
}


void LC::cb_v4theentityassign() {
	int bank = g_lingo->readInt();

	int firstArg = g_lingo->pop().asInt();
	Datum value = g_lingo->pop();
	Datum result;
	result.u.s = nullptr;
	result.type = VOID;

	int key = (bank << 8) + firstArg;
	if (!g_lingo->_lingoV4TheEntity.contains(key)) {
		warning("cb_v4theentityassign: unhandled mapping 0x%02x 0x%02x", bank, firstArg);

		return;
	}

	debugC(3, kDebugLingoExec, "cb_v4theentityassign: mapping 0x%02x, 0x%02x", bank, firstArg);

	if (!g_lingo->_lingoV4TheEntity[key]->writable) {
		warning("cb_v4theentityassign: non-writable mapping 0x%02x 0x%02x", bank, firstArg);

		return;
	}

	int entity = g_lingo->_lingoV4TheEntity[key]->entity;
	int field = g_lingo->_lingoV4TheEntity[key]->field;
	switch (g_lingo->_lingoV4TheEntity[key]->type) {
	case kTEANOArgs:
		{
			Datum id;
			id.u.s = nullptr;
			id.type = VOID;
			debugC(3, kDebugLingoExec, "cb_v4theentityassign: calling setTheEntity(%s, VOID, %s, %s)", g_lingo->entity2str(entity), g_lingo->field2str(field), value.asString(true).c_str());
			g_lingo->setTheEntity(entity, id, field, value);
		}
		break;
	case kTEAItemId:
		{
			Datum id = g_lingo->pop();
			if ((entity == kTheCast || entity == kTheField) && g_director->getVersion() >= 500) {
				// For "the member", D5 and above have a lib ID followed by a member ID
				// Pre-resolve them here.
				Datum member = g_lingo->pop();
				CastMemberID resolved = g_lingo->toCastMemberID(member, id);
				id = Datum(resolved);
			}
			debugC(3, kDebugLingoExec, "cb_v4theentityassign: calling setTheEntity(%s, %s, %s, %s)", g_lingo->entity2str(entity), id.asString(true).c_str(), g_lingo->field2str(field), value.asString(true).c_str());
			g_lingo->setTheEntity(entity, id, field, value);
		}
		break;
	case kTEAMenuId:
		{
			Datum id = g_lingo->pop();
			if (id.type == INT) {
				int menuId = id.u.i;
				id.u.menu = new MenuReference();
				id.u.menu->menuIdNum = menuId;
			} else if (id.type == STRING) {
				Common::String *menuId = id.u.s;
				id.u.menu = new MenuReference();
				id.u.menu->menuIdStr = menuId;
			} else {
				warning("LC::cb_v4theentityassign : Unknown type of menu Reference %d of entity type %d", id.type, g_lingo->_lingoV4TheEntity[key]->type);
				break;
			}
			id.type = MENUREF;
			debugC(3, kDebugLingoExec, "cb_v4theentityassign: calling setTheEntity(%s, %s, %s, %s)", g_lingo->entity2str(entity), id.asString(true).c_str(), g_lingo->field2str(field), value.asString(true).c_str());
			g_lingo->setTheEntity(entity, id, field, value);
		}
		break;
	case kTEAString:
		{
			/*Datum stringArg = */g_lingo->pop();
			warning("cb_v4theentityassign: STUB: kTEAString");
		}
		break;
	case kTEAMenuIdItemId:
		{
			Datum menuId = g_lingo->pop();
			Datum itemId = g_lingo->pop();
			Datum menuDatum;
			menuDatum.type = MENUREF;
			menuDatum.u.menu = new MenuReference();
			if (menuId.type == INT) {
				menuDatum.u.menu->menuIdNum = menuId.u.i;
			} else if (menuId.type == STRING) {
				menuDatum.u.menu->menuIdStr = menuId.u.s;
			} else {
				warning("LC::cb_v4theentityassign : Unknown type of menu Reference %d of entity type %d", menuId.type, g_lingo->_lingoV4TheEntity[key]->type);
				break;
			}
			if (itemId.type == INT) {
				menuDatum.u.menu->menuItemIdNum = itemId.u.i;
			} else if (itemId.type == STRING) {
				menuDatum.u.menu->menuItemIdStr = itemId.u.s;
			} else {
				warning("LC::cb_v4theentityassign : Unknown type of menuItem Reference %d of entity type %d", itemId.type, g_lingo->_lingoV4TheEntity[key]->type);
				break;
			}
			g_lingo->setTheEntity(entity, menuDatum, field, value);
		}
		break;
	case kTEAChunk:
		{
			Datum fieldRef = g_lingo->pop().asMemberID();
			fieldRef.type = FIELDREF;
			Datum chunkRef = readChunkRef(fieldRef);
			g_lingo->setTheEntity(entity, chunkRef, field, value);
		}
		break;
	default:
		warning("cb_v4theentityassign: unknown call type %d", g_lingo->_lingoV4TheEntity[key]->type);
		break;
	}
}

void LC::cb_zeropush() {
	Datum d(0);
	g_lingo->push(d);
}

ScriptContext *LingoCompiler::compileLingoV4(Common::SeekableReadStreamEndian &stream, uint16 lctxIndex, LingoArchive *archive, const Common::String &archName, uint16 version) {
	if (stream.size() < 0x5c) {
		warning("Lscr header too small");
		return nullptr;
	}

	if (debugChannelSet(5, kDebugLoading)) {
		debugC(5, kDebugLoading, "Lscr header:");
		stream.hexdump(0x5c);
	}

	// read the Lscr header!
	// documentation:
	// https://docs.google.com/document/d/1jDBXE4Wv1AEga-o1Wi8xtlNZY4K2fHxW2Xs8RgARrqk/edit
	// https://github.com/Earthquake-Project/Format-Documentation/blob/master/structure/scripting/FormatNotes_Scripts.txt
	// https://github.com/Earthquake-Project/ProjectorRays/blob/master/src/chunk/Script.ts
	// (none of the above are totally complete)

	// unk1
	for (uint32 i = 0; i < 0x8; i++) {
		stream.readByte();
	}

	// offset 8
	/* uint32 length = */ stream.readUint32();
	/* uint32 length2 = */ stream.readUint32();
	uint16 codeStoreOffset = stream.readUint16();

	/* uint16 scriptId = */ stream.readUint16() /* + 1 */;
	// This field *should* match the script's index in Lctx, but this
	// is unreliable. (e.g. script 261 in DATA/LEVEL1.DIR in betterd-win
	// has this field incorrectly set to 263 instead of 261.)
	// Thus, ignore it and simply use the script's index in Lctx.
	uint16 scriptId = lctxIndex;

	// unk2
	stream.readSint16BE();
	uint16 parentNumber = stream.readSint16BE();

	for (uint32 i = 0; i < 0xC; i++) {
		stream.readByte();
	}

	// offset 36
	/* uint16 unk3 = */ stream.readUint16();
	uint32 scriptFlags = stream.readUint32();
	debugC(1, kDebugCompile, "Script flags (%d 0x%x):", scriptFlags, scriptFlags);
	debugC(1, kDebugCompile, "unused: %d funcsGlobal: %d varsGlobal: %d unk3: %d", (scriptFlags & kScriptFlagUnused) != 0, (scriptFlags & kScriptFlagFuncsGlobal) != 0, (scriptFlags & kScriptFlagVarsGlobal) != 0, (scriptFlags & kScriptFlagUnk3) != 0);
	debugC(1, kDebugCompile, "factoryDef: %d unk5: %d unk6: %d unk7: %d", (scriptFlags & kScriptFlagFactoryDef) != 0, (scriptFlags & kScriptFlagUnk5) != 0, (scriptFlags & kScriptFlagUnk6) != 0, (scriptFlags & kScriptFlagUnk7) != 0);
	debugC(1, kDebugCompile, "hasFactory: %d eventScript: %d eventScript2: %d unkB: %d", (scriptFlags & kScriptFlagHasFactory) != 0, (scriptFlags & kScriptFlagEventScript) != 0, (scriptFlags & kScriptFlagEventScript2) != 0, (scriptFlags & kScriptFlagUnkB) != 0);
	debugC(1, kDebugCompile, "unkC: %d unkD: %d unkE: %d unkF: %d", (scriptFlags & kScriptFlagUnkC) != 0, (scriptFlags & kScriptFlagUnkD) != 0, (scriptFlags & kScriptFlagUnkE) != 0, (scriptFlags & kScriptFlagUnkF) != 0);

	if (scriptFlags & kScriptFlagUnused) {
		warning("Script %d is unused", scriptId);
		return nullptr;
	}

	// unk4
	for (uint32 i = 0; i < 0x4; i++) {
		stream.readByte();
	}
	/* uint16 _assemblyId = */ stream.readUint16();
	// The script is coupled with to a Cast via the script ID.
	// This _assemblyId isn't always correct.

	int16 factoryNameId = stream.readSint16();

	// offset 50 - contents map

	/* uint16 eventMapCount = */ stream.readUint16();
	/* uint32 eventMapOffset = */ stream.readUint32();
	/* uint32 eventMapFlags = */ stream.readUint32();
	// The event map is an int16 array used to quickly access events.
	// Its first item is the index of the mouseDown handler or -1,
	// its second item is the index of the mouseUp handler or -1, etc.
	// eventMapFlags & (1 << 0) indicates there is a mouseDown handler,
	// eventMapFlags & (1 << 1) indicates there is a mouseUp handler, etc.
	// We probably don't need to read this since we already did something
	// similar with _eventHandlers.

	uint16 propertiesCount = stream.readUint16();
	uint32 propertiesOffset = stream.readUint32();
	uint16 globalsCount = stream.readUint16();
	uint32 globalsOffset = stream.readUint32();
	uint16 functionsCount = stream.readUint16();
	uint32 functionsOffset = stream.readUint32();
	uint16 constsCount = stream.readUint16();
	uint32 constsOffset = stream.readUint32();
	/* uint16 constsStoreCount = */ stream.readUint32();
	uint32 constsStoreOffset = stream.readUint32();

	// initialise the script
	ScriptType scriptType = kCastScript;
	Common::String castName;
	CastMember *member = archive->cast->getCastMemberByScriptId(scriptId);
	if (member) {
		if (member->_type == kCastLingoScript)
			scriptType = ((ScriptCastMember *)member)->_scriptType;

		_assemblyId = member->getID();
		CastMemberInfo *info = member->getInfo();
		if (info)
			castName = info->name;
	} else {
		warning("Script %d has no associated cast member", scriptId);
		scriptType = kMovieScript;
	}

	_assemblyArchive = archive;

	ScriptContext *sc = nullptr;
	Common::String factoryName;
	if (scriptFlags & kScriptFlagFactoryDef) {
		if (0 <= factoryNameId && factoryNameId < (int16)archive->names.size()) {
			factoryName = archive->names[factoryNameId];
		} else {
			warning("Factory %d has unknown name id %d, skipping define", scriptId, factoryNameId);
			return nullptr;
		}
		debugC(1, kDebugCompile, "Add V4 script %d: factory '%s'", scriptId, factoryName.c_str());

		sc = _assemblyContext = new ScriptContext(factoryName, scriptType, _assemblyId, parentNumber, scriptId);
		registerFactory(factoryName);
	} else {
		debugC(1, kDebugCompile, "Add V4 script %d: %s %d", scriptId, scriptType2str(scriptType), _assemblyId);

		sc = _assemblyContext = new ScriptContext(!castName.empty() ? castName : Common::String::format("%d", _assemblyId), scriptType, _assemblyId, archive->cast->_castLibID, parentNumber, scriptId);
	}

	// initialise each property
	if ((uint32)stream.size() < propertiesOffset + propertiesCount * 2) {
		warning("Lscr properties store missing");
		return nullptr;
	}
	stream.seek(propertiesOffset);
	if (debugChannelSet(5, kDebugLoading)) {
		debugC(5, kDebugLoading, "Lscr property list:");
		stream.hexdump(propertiesCount * 2);
	}
	for (uint16 i = 0; i < propertiesCount; i++) {
		int16 index = stream.readSint16();
		if (index == -1) {
			debugC(5, kDebugLoading, "[end of list]");
			break;
		} else if (0 <= index && index < (int16)archive->names.size()) {
			const char *name = archive->names[index].c_str();
			debugC(5, kDebugLoading, "%d: %s", i, name);
			_assemblyContext->setProp(name, Datum(), true);
		} else {
			warning("Property %d has unknown name id %d, skipping define", i, index);
		}
	}

	// initialise each global variable
	if ((uint32)stream.size() < globalsOffset + globalsCount * 2) {
		warning("Lscr globals store missing");
		return nullptr;
	}

	stream.seek(globalsOffset);
	if (debugChannelSet(5, kDebugLoading)) {
		debugC(5, kDebugLoading, "Lscr globals list:");
		stream.hexdump(globalsCount * 2);
	}
	for (uint16 i = 0; i < globalsCount; i++) {
		int16 index = stream.readSint16();
		if (0 <= index && index < (int16)archive->names.size()) {
			const char *name = archive->names[index].c_str();
			if (!g_lingo->_globalvars.contains(name)) {
				g_lingo->_globalvars[name] = Datum();
				debugC(5, kDebugLoading, "%d: %s", i, name);
			} else {
				debugC(5, kDebugLoading, "%d: %s (already defined)", i, name);
			}
		} else {
			warning("Global %d has unknown name id %d, skipping define", i, index);
		}
	}

	// preload all the constants!
	// these are stored as a reference table of 6 byte entries, followed by a storage area.

	// copy the storage area first.
	uint32 constsStoreSize = stream.size() - constsStoreOffset;

	if ((uint32)stream.size() < constsStoreOffset) {
		warning("Lscr consts store missing");
		return nullptr;
	}

	stream.seek(constsStoreOffset);

	if (debugChannelSet(5, kDebugLoading)) {
		debugC(5, kDebugLoading, "Lscr consts store:");
		stream.hexdump(constsStoreSize);
	}

	byte *constsStore = (byte *)malloc(constsStoreSize);
	stream.read(constsStore, constsStoreSize);

	// read each entry in the reference table.
	stream.seek(constsOffset);
	int constsIndexSize = MAX((int)constsStoreOffset - (int)constsOffset, 0);
	if (debugChannelSet(5, kDebugLoading)) {
		debugC(5, kDebugLoading, "Lscr consts index:");
		stream.hexdump(constsIndexSize);
	}
	for (uint16 i = 0; i < constsCount; i++) {
		Datum constant;
		uint32 constType = 0;
		if (version >= kFileVer500) {
			constType = stream.readUint32();
		} else {
			constType = (uint32)stream.readUint16();
		}

		uint32 value = stream.readUint32();
		switch (constType) {
		case 1: // String type
			{
				Common::String str;
				uint32 pointer = value;
				if (pointer + 4 > constsStoreSize) {
					error("Constant string is too small");
					break;
				}
				uint32 length = READ_BE_UINT32(&constsStore[pointer]);
				pointer += 4;
				uint32 end = pointer + length;
				if (end > constsStoreSize) {
					error("Constant string is too large");
					break;
				}
				while (pointer < end) {
					if (constsStore[pointer] == '\0') {
						break;
					} else {
						str += constsStore[pointer];
					}
					pointer += 1;
				}
				if (pointer > constsStoreSize) {
					warning("Constant string has no null terminator");
					break;
				}
				constant.type = STRING;
				constant.u.s = new Common::String(archive->cast->decodeString(str), Common::kUtf8);
			}
			break;
		case 4: // Integer type
			constant.type = INT;
			constant.u.i = (int)value;
			break;
		case 9:  // Float type
			{
				constant.type = FLOAT;
				if (value + 4 > constsStoreSize) {
					warning("Constant float end offset is out of bounds");
					break;
				}

				uint32 pointer = value;
				uint32 length = READ_BE_UINT32(&constsStore[pointer]);
				pointer += 4;
				uint32 end = pointer + length;
				if (end > constsStoreSize) {
					error("Constant float is too large");
					break;
				}

				if (length == 10) {
					// Floats are stored as an "80 bit IEEE Standard 754 floating
					// point number (Standard Apple Numeric Environment [SANE] data type
					// Extended).
					constant.u.f = readAppleFloat80(&constsStore[pointer]);
				} else if (length == 8) {
					constant.u.f = READ_BE_FLOAT64(&constsStore[pointer]);
				} else {
					error("Constant float expected to be 8 or 10 bytes but got %d", length);
				}

			}
			break;
		default:
			warning("Unknown constant type %d", constType);
			break;
		}
		_assemblyContext->_constants.push_back(constant);
	}
	free(constsStore);

	// parse each function!
	// these are stored as a code storage area, followed by a reference table of 42 byte entries.

	// copy the storage area first.
	if ((uint32)stream.size() < functionsOffset) {
		warning("Lscr functions store missing");
		return nullptr;
	}

	uint32 codeStoreSize = functionsOffset - codeStoreOffset;
	stream.seek(codeStoreOffset);
	byte *codeStore = (byte *)malloc(codeStoreSize);
	stream.read(codeStore, codeStoreSize);

	Common::DumpFile out;
	bool skipdump = false;

	if (ConfMan.getBool("dump_scripts")) {
		Common::Path buf;
		if (scriptFlags & kScriptFlagFactoryDef) {
			buf = dumpFactoryName(encodePathForDump(archName).c_str(), factoryName.c_str(), "lscr");
		} else {
			buf = dumpScriptName(encodePathForDump(archName).c_str(), scriptType, _assemblyId, "lscr");
		}

		if (!out.open(buf, true)) {
			warning("Lingo::addCodeV4(): Can not open dump file %s", buf.toString(Common::Path::kNativeSeparator).c_str());
			skipdump = true;
		} else {
			warning("Lingo::addCodeV4(): Dumping Lscr to %s", buf.toString(Common::Path::kNativeSeparator).c_str());
		}
	}

	// read each entry in the function table.
	stream.seek(functionsOffset);
	for (uint16 i = 0; i < functionsCount; i++) {
		if (debugChannelSet(5, kDebugLoading)) {
			debugC(5, kDebugLoading, "Function %d header:", i);
			stream.hexdump(0x2a);
		}

		int16 nameIndex = stream.readUint16();
		stream.readUint16();
		uint32 length = stream.readUint32();
		uint32 startOffset = stream.readUint32();
		uint16 argCount = stream.readUint16();
		uint32 argOffset = stream.readUint32();
		uint16 varCount = stream.readUint16();
		uint32 varOffset = stream.readUint32();
		stream.readUint16();
		stream.readUint16();
		stream.readUint16();
		stream.readUint16();
		stream.readUint16();
		stream.readUint16();
		stream.readUint16();
		stream.readUint16();
		stream.readUint16();

		if (startOffset < codeStoreOffset) {
			warning("Function %d start offset is out of bounds!", i);
			continue;
		} else if (startOffset + length >= codeStoreOffset + codeStoreSize) {
			warning("Function %d end offset is out of bounds", i);
			continue;
		}

		// Fetch argument name list
		Common::Array<Common::String> *argNames = new Common::Array<Common::String>;
		Common::HashMap<uint16, uint16> argMap;
		if (argOffset < codeStoreOffset) {
			warning("Function %d argument names start offset is out of bounds!", i);
		} else if (argOffset + argCount*2 >= codeStoreOffset + codeStoreSize) {
			warning("Function %d argument names end offset is out of bounds!", i);
		} else {
			debugC(5, kDebugLoading, "Function %d argument list:", i);
			uint16 namePointer = argOffset - codeStoreOffset;
			for (int j = 0; j < argCount; j++) {
				int16 index = READ_BE_INT16(&codeStore[namePointer]);
				namePointer += 2;
				Common::String name;
				if (0 <= index && index < (int16)archive->names.size()) {
					name = archive->names[index];
					argMap[j] = index;
				} else if (j == 0 && (scriptFlags & kScriptFlagFactoryDef)) {
					name = "me";

					// find or add "me" in the names list
					for (index = 0; index < (int16)archive->names.size(); index++) {
						if (archive->names[index].equalsIgnoreCase(name))
							break;
					}
					if (index == (int16)archive->names.size())
						archive->names.push_back(name);

					argMap[j] = index;
				} else {
					name = Common::String::format("arg_%d", j);
					warning("Argument has unknown name id %d, using name %s", index, name.c_str());
				}
				debugC(5, kDebugLoading, "%d: %s", j, name.c_str());
				argNames->push_back(name);
			}
		}

		// Fetch variable name list
		Common::Array<Common::String> *varNames = new Common::Array<Common::String>;
		Common::HashMap<uint16, uint16> varMap;
		if (varOffset < codeStoreOffset) {
			warning("Function %d variable names start offset is out of bounds!", i);
		} else if (varOffset + varCount*2 >= codeStoreOffset + codeStoreSize) {
			warning("Function %d variable names end offset is out of bounds!", i);
		} else {
			debugC(5, kDebugLoading, "Function %d variable list:", i);
			uint16 namePointer = varOffset - codeStoreOffset;
			for (int j = 0; j < varCount; j++) {
				int16 index = READ_BE_INT16(&codeStore[namePointer]);
				namePointer += 2;
				Common::String name;
				if (0 <= index && index < (int16)archive->names.size()) {
					name = archive->names[index];
					varMap[j] = index;
				} else {
					name = Common::String::format("var_%d", j);
					warning("Variable has unknown name id %d, using name %s", j, name.c_str());
				}
				debugC(5, kDebugLoading, "%d: %s", j, name.c_str());
				varNames->push_back(name);
			}
		}

		_currentAssembly = new ScriptData;

		if (debugChannelSet(5, kDebugLoading)) {
			debugC(5, kDebugLoading, "Function %d code:", i);
			Common::hexdump(codeStore, length, 16, startOffset);
		}

		uint32 pointer = startOffset - codeStoreOffset;
		Common::Array<uint32> offsetList;
		Common::Array<uint32> jumpList;
		Common::Array<uint32> byteOffsets;

		// Size of an entry in the consts index.
		int constEntrySize = 0;
		if (version >= kFileVer500) {
			// For D5 this is uint32 type + uint32 offset
			constEntrySize = 8;
		} else {
			// For D4 this is uint16 type + uint32 offset
			constEntrySize = 6;
		}

		while (pointer < startOffset + length - codeStoreOffset) {
			uint8 opcode = codeStore[pointer];
			pointer += 1;

			if (opcode == 0x44 || opcode == 0x84) {
				// Opcode for pushing a value from the constants table.
				// Rewrite these to inline the constant into our bytecode.
				offsetList.push_back(_currentAssembly->size());
				byteOffsets.push_back(_currentAssembly->size());
				int arg = 0;
				if (opcode == 0x84) {
					arg = (uint16)READ_BE_UINT16(&codeStore[pointer]);
					pointer += 2;
				} else {
					arg = (uint8)codeStore[pointer];
					pointer += 1;
				}
				// The argument is a byte offset to an entry in the consts index.
				// As such, it should be an exact multiple of the entry size.
				if (arg % constEntrySize) {
					warning("Opcode 0x%02x arg %d not a multiple of %d", opcode, arg, constEntrySize);
				}
				arg /= constEntrySize;
				Datum constant = _assemblyContext->_constants[arg];
				switch (constant.type) {
				case INT:
					code1(LC::c_intpush);
					break;
				case FLOAT:
					code1(LC::c_floatpush);
					break;
				case STRING:
					code1(LC::c_stringpush);
					break;
				default:
					error("Unknown constant type %d", constant.type);
					break;
				}
				if (opcode == 0x84) {
					offsetList.push_back(_currentAssembly->size());
					offsetList.push_back(_currentAssembly->size());
					byteOffsets.push_back(_currentAssembly->size());
					byteOffsets.push_back(_currentAssembly->size());
				} else {
					offsetList.push_back(_currentAssembly->size());
					byteOffsets.push_back(_currentAssembly->size());
				}
				switch (constant.type) {
				case INT:
					codeInt(constant.u.i);
					break;
				case FLOAT:
					codeFloat(constant.u.f);
					break;
				case STRING:
					codeString(constant.u.s->c_str());
					break;
				default:
					error("Unknown constant type %d", constant.type);
					break;
				}
			} else if (g_lingo->_lingoV4.contains(opcode)) {
				offsetList.push_back(_currentAssembly->size());
				byteOffsets.push_back(_currentAssembly->size());
				code1(g_lingo->_lingoV4[opcode]->func);

				size_t argc = strlen(g_lingo->_lingoV4[opcode]->proto);
				if (argc) {
					bool codeName = false;
					int arg = 0;
					for (uint c = 0; c < argc; c++) {
						switch (g_lingo->_lingoV4[opcode]->proto[c]) {
						case 'b':
							// read one uint8 as an argument
							offsetList.push_back(_currentAssembly->size());
							byteOffsets.push_back(_currentAssembly->size());
							arg = (uint8)codeStore[pointer];
							pointer += 1;
							break;
						case 'B':
							// read one int8 as an argument
							offsetList.push_back(_currentAssembly->size());
							byteOffsets.push_back(_currentAssembly->size());
							arg = (int8)codeStore[pointer];
							pointer += 1;
							break;
						case 'w':
							// read one uint16 as an argument
							offsetList.push_back(_currentAssembly->size());
							offsetList.push_back(_currentAssembly->size());
							byteOffsets.push_back(_currentAssembly->size());
							byteOffsets.push_back(_currentAssembly->size());
							arg = (uint16)READ_BE_UINT16(&codeStore[pointer]);
							pointer += 2;
							break;
						case 'W':
							// read one int16 as an argument
							offsetList.push_back(_currentAssembly->size());
							offsetList.push_back(_currentAssembly->size());
							byteOffsets.push_back(_currentAssembly->size());
							byteOffsets.push_back(_currentAssembly->size());
							arg = (int16)READ_BE_INT16(&codeStore[pointer]);
							pointer += 2;
							break;
						case 'n':
							// argument is negative
							arg *= -1;
							break;
						case 'p':
							// argument is some kind of denormalised offset
							if (arg % constEntrySize) {
								warning("Argument %d was expected to be a multiple of %d", arg, constEntrySize);
							}
							arg /= constEntrySize;
							break;
						case 'a':
							// argument is a function argument ID
							if (argMap.contains(arg)) {
								arg = argMap[arg];
							} else {
								warning("No argument name found for ID %d", arg);
								arg = -1;
							}
							break;
						case 'v':
							// argument is a local variable ID
							if (varMap.contains(arg)) {
								arg = varMap[arg];
							} else {
								warning("No variable name found for ID %d", arg);
								arg = -1;
							}
							break;
						case 'j':
							// argument refers to a code offset; fix alignment in post
							jumpList.push_back(offsetList.size());
							break;
						case 'N':
							// argument is a name in the name table
							codeName = true;
							break;
						default:
							break;
						}
					}
					if (codeName) {
						codeString(_assemblyArchive->getName(arg).c_str());
					} else {
						codeInt(arg);
					}
				}
			} else {
				// unimplemented instruction
				if (opcode < 0x40) { // 1 byte instruction
					debugC(5, kDebugCompile, "Unimplemented opcode: 0x%02x", opcode);
					offsetList.push_back(_currentAssembly->size());
					byteOffsets.push_back(_currentAssembly->size());
					code1(LC::cb_unk);
					codeInt(opcode);
				} else if (opcode < 0x80) { // 2 byte instruction
					debugC(5, kDebugCompile, "Unimplemented opcode: 0x%02x (%d)", opcode, (uint)codeStore[pointer]);
					offsetList.push_back(_currentAssembly->size());
					byteOffsets.push_back(_currentAssembly->size());
					code1(LC::cb_unk1);
					codeInt(opcode);
					offsetList.push_back(_currentAssembly->size());
					byteOffsets.push_back(_currentAssembly->size());
					codeInt((uint)codeStore[pointer]);
					pointer += 1;
				} else { // 3 byte instruction
					debugC(5, kDebugCompile, "Unimplemented opcode: 0x%02x (%d, %d)", opcode, (uint)codeStore[pointer], (uint)codeStore[pointer+1]);
					offsetList.push_back(_currentAssembly->size());
					byteOffsets.push_back(_currentAssembly->size());
					code1(LC::cb_unk2);
					codeInt(opcode);
					offsetList.push_back(_currentAssembly->size());
					byteOffsets.push_back(_currentAssembly->size());
					codeInt((uint)codeStore[pointer]);
					offsetList.push_back(_currentAssembly->size());
					byteOffsets.push_back(_currentAssembly->size());
					codeInt((uint)codeStore[pointer+1]);
					pointer += 2;
				}
			}
		}

		// Add backstop
		code1(STOP);

		// Rewrite every offset flagged as a jump based on the new code alignment.
		for (uint j = 0; j < jumpList.size(); j++) {
			int originalJumpAddressLoc = jumpList[j];
			int originalJumpInstructionLoc = originalJumpAddressLoc-1;
			int jumpAddressPc = offsetList[originalJumpAddressLoc];
			int jump = (int)READ_UINT32(&((*_currentAssembly)[jumpAddressPc]));
			int oldTarget = originalJumpInstructionLoc + jump;
			if ((oldTarget >= 0) && (oldTarget < (int)offsetList.size())) {
				int newJump = offsetList[oldTarget];
				WRITE_UINT32(&((*_currentAssembly)[jumpAddressPc]), newJump - jumpAddressPc + 1);
			} else {
				warning("Jump of %d from position %d is outside the function!", jump, originalJumpAddressLoc);
			}
		}

		// Attach to handlers

		Common::String functionName;
		if (0 <= nameIndex && nameIndex < (int16)archive->names.size()) {
			functionName = archive->names[nameIndex];
		} else if (i == 0 && (scriptFlags & kScriptFlagEventScript)) {
			// event script (lingo not contained within a handler)
			functionName = g_lingo->_eventHandlerTypes[kEventGeneric];
		}

		Symbol sym;
		if (!functionName.empty()) {
			debugC(5, kDebugLoading, "Function %d binding: %s()", i, functionName.c_str());
			sym = _assemblyContext->define(functionName, _currentAssembly, argNames, varNames);
		} else {
			warning("Function has unknown name id %d, skipping define", nameIndex);
			sym.name = new Common::String();
			sym.type = HANDLER;
			sym.u.defn = _currentAssembly;
			sym.nargs = argCount;
			sym.maxArgs = argCount;
			sym.argNames = argNames;
			sym.varNames = varNames;
		}

		_assemblyContext->_functionByteOffsets[functionName] = byteOffsets;

		if (!skipdump && ConfMan.getBool("dump_scripts")) {
			out.writeString(g_lingo->formatFunctionBody(sym));
		}

		_assemblyContext->_functionNames.push_back(*sym.name);
		_currentAssembly = nullptr;
	}

	if (!_assemblyContext->isFactory()) {
		// Register this context's functions with the containing archive.
		if (scriptType == kScoreScript || scriptType == kMovieScript) {
			for (auto &it : _assemblyContext->_functionHandlers) {
				if (!_assemblyArchive->functionHandlers.contains(it._key)) {
					_assemblyArchive->functionHandlers[it._key] = it._value;
				}
			}
		}
	}

	if (!skipdump && ConfMan.getBool("dump_scripts")) {
		out.flush();
		out.close();
	}

	free(codeStore);
	_assemblyArchive = nullptr;
	_assemblyContext = nullptr;
	_assemblyId = -1;

	return sc;
}

void LingoArchive::addCodeV4(Common::SeekableReadStreamEndian &stream, uint16 lctxIndex, const Common::String &archName, uint16 version) {
	ScriptContext *ctx = g_lingo->_compiler->compileLingoV4(stream, lctxIndex, this, archName, version);
	if (ctx) {
		lctxContexts[lctxIndex] = ctx;
		ctx->incRefCount();
	}
}

void LingoArchive::addNamesV4(Common::SeekableReadStreamEndian &stream) {
	debugC(1, kDebugCompile, "Add V4 script name index");

	if (stream.size() < 0x14) {
		warning("Lnam header too small");
		return;
	}

	// read the Lnam header!
	if (debugChannelSet(5, kDebugLoading)) {
		debugC(5, kDebugLoading, "Lnam header:");
		stream.hexdump(0x14);
	}

	stream.readUint16();
	stream.readUint16();
	stream.readUint16();
	stream.readUint16();

	uint32 size = stream.readUint32(); // size of Lnam
	stream.readUint32(); // size of Lnam again
	uint16 offset = stream.readUint16();
	uint16 count = stream.readUint16();

	if ((uint32)stream.size() != size) {
		warning("Lnam content missing");
		return;
	}

	stream.seek(offset);

	names.clear();

	for (uint16 i = 0; i < count; i++) {
		Common::String name = stream.readPascalString();

		names.push_back(name);
		debugC(5, kDebugLoading, "%d: \"%s\"", i, name.c_str());
	}

}

} // end of namespace Director
