/* 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/system.h"
#include "common/translation.h"

#include "director/types.h"
#include "gui/message.h"

#include "graphics/macgui/macwindowmanager.h"

#include "director/director.h"
#include "director/cast.h"
#include "director/channel.h"
#include "director/debugger.h"
#include "director/frame.h"
#include "director/movie.h"
#include "director/score.h"
#include "director/sound.h"
#include "director/sprite.h"
#include "director/stxt.h"
#include "director/util.h"
#include "director/window.h"
#include "director/castmember/castmember.h"
#include "director/castmember/bitmap.h"
#include "director/castmember/palette.h"
#include "director/castmember/text.h"
#include "director/castmember/transition.h"
#include "director/lingo/lingo-builtins.h"
#include "director/lingo/lingo-code.h"
#include "director/lingo/lingo-codegen.h"
#include "director/lingo/lingo-utils.h"

#include "image/pict.h"

namespace Director {

static const BuiltinProto builtins[] = {
	// Math
	{ "abs",			LB::b_abs,			1, 1, 200, FBLTIN },	// D2 function
	{ "atan",			LB::b_atan,			1, 1, 400, FBLTIN },	//			D4 f
	{ "cos",			LB::b_cos,			1, 1, 400, FBLTIN },	//			D4 f
	{ "exp",			LB::b_exp,			1, 1, 400, FBLTIN },	//			D4 f
	{ "float",			LB::b_float,		1, 1, 400, FBLTIN },	//			D4 f
	{ "integer",		LB::b_integer,		1, 1, 300, FBLTIN },	//		D3 f
	{ "log",			LB::b_log,			1, 1, 400, FBLTIN },	//			D4 f
	{ "pi",				LB::b_pi,			0, 0, 400, FBLTIN },	//			D4 f
	{ "power",			LB::b_power,		2, 2, 400, FBLTIN },	//			D4 f
	{ "random",			LB::b_random,		1, 1, 200, FBLTIN },	// D2 f
	{ "sin",			LB::b_sin,			1, 1, 400, FBLTIN },	//			D4 f
	{ "sqrt",			LB::b_sqrt,			1, 1, 200, FBLTIN },	// D2 f
	{ "tan",			LB::b_tan,			1, 1, 400, FBLTIN },	//			D4 f
	{ "void",			LB::b_void,			0, 0, 600, FBLTIN },	// 					D6 f
	// String
	{ "chars",			LB::b_chars,		3, 3, 200, FBLTIN },	// D2 f
	{ "charToNum",		LB::b_charToNum,	1, 1, 200, FBLTIN },	// D2 f
	{ "length",			LB::b_length,		1, 1, 200, FBLTIN },	// D2 f
	{ "numToChar",		LB::b_numToChar,	1, 1, 200, FBLTIN },	// D2 f
	{ "offset",			LB::b_offset,		2, 3, 200, FBLTIN },	// D2 f
	{ "string",			LB::b_string,		1, 1, 200, FBLTIN },	// D2 f
	{ "value",		 	LB::b_value,		1, 1, 200, FBLTIN },	// D2 f
	// Lists
	{ "add",			LB::b_add,			2, 2, 400, HBLTIN_LIST },	//			D4 handler
	{ "addAt",			LB::b_addAt,		3, 3, 400, HBLTIN_LIST },	//			D4 h
	{ "addProp",		LB::b_addProp,		3, 3, 400, HBLTIN_LIST },	//			D4 h
	{ "append",			LB::b_append,		2, 2, 400, HBLTIN_LIST },	//			D4 h
	{ "count",			LB::b_count,		1, 1, 400, FBLTIN_LIST },	//			D4 f
	{ "deleteAt",		LB::b_deleteAt,		2, 2, 400, HBLTIN_LIST },	//			D4 h
	{ "deleteOne",		LB::b_deleteOne,	2, 2, 400, HBLTIN_LIST },	//			D4 h, documented in D5
	{ "deleteProp",		LB::b_deleteProp,	2, 2, 400, HBLTIN_LIST },	//			D4 h
	{ "duplicate",		LB::b_duplicateList,1, 1, 500, FBLTIN_LIST },	//				D5 f
	{ "findPos",		LB::b_findPos,		2, 2, 400, FBLTIN_LIST },	//			D4 f
	{ "findPosNear",	LB::b_findPosNear,	2, 2, 400, FBLTIN_LIST },	//			D4 f
	{ "getaProp",		LB::b_getaProp,		2, 2, 400, FBLTIN_LIST },	//			D4 f
	{ "getAt",			LB::b_getAt,		2, 2, 400, FBLTIN_LIST },	//			D4 f
	{ "getLast",		LB::b_getLast,		1, 1, 400, FBLTIN_LIST },	//			D4 f
	{ "getOne",			LB::b_getOne,		2, 2, 400, FBLTIN_LIST },	//			D4 f
	{ "getPos",			LB::b_getPos,		2, 2, 400, FBLTIN_LIST },	//			D4 f
	{ "getProp",		LB::b_getProp,		2, 2, 400, FBLTIN_LIST },	//			D4 f
	{ "getPropAt",		LB::b_getPropAt,	2, 2, 400, FBLTIN_LIST },	//			D4 f
	{ "list",			LB::b_list,			-1,0, 400, FBLTIN_LIST },	//			D4 f
	{ "listP",			LB::b_listP,		1, 1, 400, FBLTIN_LIST },	//			D4 f
	{ "max",			LB::b_max,			-1,0, 400, FBLTIN_LIST },	//			D4 f
	{ "min",			LB::b_min,			-1,0, 400, FBLTIN_LIST },	//			D4 f
	{ "setaProp",		LB::b_setaProp,		3, 3, 400, HBLTIN_LIST },	//			D4 h
	{ "setAt",			LB::b_setAt,		3, 3, 400, HBLTIN_LIST },	//			D4 h
	{ "setProp",		LB::b_setProp,		3, 3, 400, HBLTIN_LIST },	//			D4 h
	{ "sort",			LB::b_sort,			1, 1, 400, HBLTIN_LIST },	//			D4 h
	// Files
	{ "closeDA",	 	LB::b_closeDA, 		0, 0, 200, CBLTIN },	// D2 c
	{ "closeResFile",	LB::b_closeResFile,	0, 1, 200, CBLTIN },	// D2 c
	{ "closeXlib",		LB::b_closeXlib,	0, 1, 200, CBLTIN },	// D2 c
	{ "getNthFileNameInFolder",LB::b_getNthFileNameInFolder,2,2,400,FBLTIN }, //D4 f
	{ "open",			LB::b_open,			1, 2, 200, CBLTIN },	// D2 c
	{ "openDA",	 		LB::b_openDA, 		1, 1, 200, CBLTIN },	// D2 c
	{ "openResFile",	LB::b_openResFile,	1, 1, 200, CBLTIN },	// D2 c
	{ "openXlib",		LB::b_openXlib,		1, 1, 200, CBLTIN },	// D2 c
	{ "save",			LB::b_save,			1, 1, 500, CBLTIN },	//				D5 c
	{ "saveMovie",		LB::b_saveMovie,	0, 1, 400, CBLTIN },	//			D4 c
	{ "setCallBack",	LB::b_setCallBack,	2, 2, 200, CBLTIN },	// D2 c
	{ "showResFile",	LB::b_showResFile,	0, 1, 200, CBLTIN },	// D2 c
	{ "showXlib",		LB::b_showXlib,		0, 1, 200, CBLTIN },	// D2 c
	{ "xFactoryList",	LB::b_xFactoryList,	1, 1, 300, FBLTIN },	//		D3 f
	{ "xtra",			LB::b_xtra,			1, 1, 500, FBLTIN },	//				D5 f
	// Control
	{ "abort",			LB::b_abort,		0, 0, 400, CBLTIN },	//			D4 c
	{ "cancelIdleLoad",	LB::b_cancelIdleLoad,1,1, 500, CBLTIN },	//				D5 c
	{ "call",			LB::b_call,			-1,0, 600, CBLTIN },	// 					D6 c
	{ "callAncestor",	LB::b_callAncestor,	-1,0, 600, CBLTIN },	// 					D6 c
	{ "continue",		LB::b_continue,		0, 0, 200, CBLTIN },	// D2 c
	{ "dontPassEvent",	LB::b_dontPassEvent,0, 0, 200, CBLTIN },	// D2 c
	{ "delay",	 		LB::b_delay,		1, 1, 200, CBLTIN },	// D2 c
	{ "do",		 		LB::b_do,			1, 1, 200, CBLTIN },	// D2 c
	{ "finishIdleLoad",	LB::b_finishIdleLoad,1,1, 500, CBLTIN },	//				D5 c
	{ "go",		 		LB::b_go,			1, 2, 200, CBLTIN },	// D2 c
	{ "halt",	 		LB::b_halt,			0, 0, 400, CBLTIN },	//			D4 c
	{ "idleLoadDone",	LB::b_idleLoadDone,	1, 1, 500, FBLTIN },	//				D5 f
	{ "nothing",		LB::b_nothing,		0, 0, 200, CBLTIN },	// D2 c
	{ "pass",			LB::b_pass,			0, 0, 400, CBLTIN },	//			D4 c
	{ "pause",			LB::b_pause,		0, 0, 200, CBLTIN },	// D2 c
	{ "play",			LB::b_play,			0, 2, 200, CBLTIN },	// D2 c
	{ "playAccel",		LB::b_playAccel,	-1,0, 200, CBLTIN },	// D2
		// play done												// D2
	{ "preLoad",		LB::b_preLoad,		-1,0, 300, CBLTIN },	//		D3.1 c
	{ "preLoadCast",	LB::b_preLoadCast,	-1,0, 300, CBLTIN },	//		D3.1 c
	{ "preLoadMember",	LB::b_preLoadCast,	-1,0, 500, CBLTIN },	//				D5 c
	{ "preLoadMovie",	LB::b_preLoadMovie,	1, 1, 500, CBLTIN },	//				D5 c
	{ "quit",			LB::b_quit,			0, 0, 200, CBLTIN },	// D2 c
	{ "restart",		LB::b_restart,		0, 0, 200, CBLTIN },	// D2 c
	{ "return",			LB::b_return,		0, 1, 200, CBLTIN },	// D2 f
	{ "send",			LB::b_call,			-1,0, 400, CBLTIN },	//			D4 c, undocumented
	{ "sendAncestor",	LB::b_callAncestor,	-1,0, 400, CBLTIN },	//			D4 c, undocumented
	{ "shutDown",		LB::b_shutDown,		0, 0, 200, CBLTIN },	// D2 c
	{ "startTimer",		LB::b_startTimer,	0, 0, 200, CBLTIN },	// D2 c
	{ "stopEvent",		LB::b_stopEvent,	0, 0, 600, CBLTIN },	//						D6 c
		// when keyDown												// D2
		// when mouseDown											// D2
		// when mouseUp												// D2
		// when timeOut												// D2
	// Types
	{ "factory",		LB::b_factory,		1, 1, 300, FBLTIN },	//		D3
	{ "floatP",			LB::b_floatP,		1, 1, 300, FBLTIN },	//		D3
	{ "ilk",	 		LB::b_ilk,			1, 2, 400, FBLTIN },	//			D4 f
	{ "integerp",		LB::b_integerp,		1, 1, 200, FBLTIN },	// D2 f
	{ "objectp",		LB::b_objectp,		1, 1, 200, FBLTIN },	// D2 f
	{ "pictureP",		LB::b_pictureP,		1, 1, 400, FBLTIN },	//			D4 f
	{ "stringp",		LB::b_stringp,		1, 1, 200, FBLTIN },	// D2 f
	{ "symbolp",		LB::b_symbolp,		1, 1, 200, FBLTIN },	// D2 f
	{ "symbol",		LB::b_symbol,		1, 1, 600, FBLTIN },	//							D6 f
	{ "voidP",			LB::b_voidP,		1, 1, 400, FBLTIN },	//			D4 f
	// Misc
	{ "alert",	 		LB::b_alert,		1, 1, 200, CBLTIN },	// D2 c
	{ "clearGlobals",	LB::b_clearGlobals,	0, 0, 300, CBLTIN },	//		D3.1 c
	{ "cursor",	 		LB::b_cursor,		1, 1, 200, CBLTIN },	// D2 c
	{ "framesToHMS",	LB::b_framesToHMS,	4, 4, 300, FBLTIN },	//		D3 f
	{ "HMStoFrames",	LB::b_HMStoFrames,	4, 4, 300, FBLTIN },	//		D3 f
	{ "param",	 		LB::b_param,		1, 1, 400, FBLTIN },	//			D4 f
	{ "printFrom",	 	LB::b_printFrom,	-1,0, 200, CBLTIN },	// D2 c
	{ "put",			LB::b_put,			-1,0, 200, CBLTIN },	// D2
		// set														// D2
	{ "setPref",		LB::b_setPref,		2, 2, 600, CBLTIN },	// 					D6 c
	{ "showGlobals",	LB::b_showGlobals,	0, 0, 200, CBLTIN },	// D2 c
	{ "showLocals",		LB::b_showLocals,	0, 0, 200, CBLTIN },	// D2 c
	// Score
	{ "constrainH",		LB::b_constrainH,	2, 2, 200, FBLTIN },	// D2 f
	{ "constrainV",		LB::b_constrainV,	2, 2, 200, FBLTIN },	// D2 f
	{ "copyToClipBoard",LB::b_copyToClipBoard,1,1,400, CBLTIN }, 	//			D4 c
	{ "duplicate",		LB::b_duplicate,	1, 2, 400, CBLTIN },	//			D4 c
	{ "editableText",	LB::b_editableText,	0, 0, 200, CBLTIN },	// D2
	{ "erase",			LB::b_erase,		1, 1, 400, CBLTIN },	//			D4 c
	{ "findEmpty",		LB::b_findEmpty,	1, 1, 400, FBLTIN },	//			D4 f
		// go															// D2
	{ "importFileInto",	LB::b_importFileInto,2,2, 400, CBLTIN },	//			D4 c
	{ "installMenu",	LB::b_installMenu,	1, 1, 200, CBLTIN },	// D2 c
	{ "label",			LB::b_label,		1, 1, 200, FBLTIN },	// D2 f
	{ "marker",			LB::b_marker,		1, 1, 200, FBLTIN },	// D2 f
	{ "move",			LB::b_move,			1, 2, 400, CBLTIN },	//			D4 c
	{ "moveableSprite",	LB::b_moveableSprite,0, 0, 200, CBLTIN },	// D2, FIXME: the field in D4+
	{ "pasteClipBoardInto",LB::b_pasteClipBoardInto,1,1,400,CBLTIN },//			D4 c
	{ "puppetPalette",	LB::b_puppetPalette, -1,0, 200, CBLTIN },	// D2 c
	{ "puppetSound",	LB::b_puppetSound,	-1,0, 200, CBLTIN },	// D2 c
	{ "puppetSprite",	LB::b_puppetSprite,	-1,0, 200, CBLTIN },	// D2 c
	{ "puppetTempo",	LB::b_puppetTempo,	1, 1, 200, CBLTIN },	// D2 c
	{ "puppetTransition",LB::b_puppetTransition,-1,0,200, CBLTIN },	// D2 c
	{ "ramNeeded",		LB::b_ramNeeded,	2, 2, 300, FBLTIN },	//		D3.1 f
	{ "rollOver",		LB::b_rollOver,		1, 1, 200, FBLTIN },	// D2 f
	{ "sendAllSprites",	LB::b_sendAllSprites,-1,0,600, CBLTIN },	// 					D6 c
	{ "sendSprite",		LB::b_sendSprite,	-1,0, 600, CBLTIN },	// 					D6 c
	{ "spriteBox",		LB::b_spriteBox,	5, 5, 200, CBLTIN },	// D2 c
	{ "unLoad",			LB::b_unLoad,		0, 2, 300, CBLTIN },	//		D3.1 c
	{ "unLoadCast",		LB::b_unLoadCast,	0, 2, 300, CBLTIN },	//		D3.1 c
	{ "unLoadMember",	LB::b_unLoadCast,	0, 2, 500, CBLTIN },	//				D5 c
	{ "unLoadMovie",	LB::b_unLoadMovie,	1, 1, 500, CBLTIN },	//				D5 c
	{ "updateStage",	LB::b_updateStage,	0, 0, 200, CBLTIN },	// D2 c
	{ "zoomBox",		LB::b_zoomBox,		-1,0, 200, CBLTIN },	// D2 c
	{"immediateSprite", LB::b_immediateSprite,-1,0,200,CBLTIN },	// D2 c
	// Score recording
	{ "clearFrame",		LB::b_clearFrame,	0, 0, 500, CBLTIN },	//				D5 c
	{ "deleteFrame",	LB::b_deleteFrame,	0, 0, 500, CBLTIN },	//				D5 c
	{ "duplicateFrame",	LB::b_duplicateFrame,0,0, 500, CBLTIN },	//				D5 c
	{ "insertFrame",	LB::b_insertFrame,	0, 0, 500, CBLTIN },	//				D5 c
	{ "updateFrame",	LB::b_updateFrame,	0, 0, 500, CBLTIN },	//				D5 c
	// Point
	{ "point",			LB::b_point,		2, 2, 400, FBLTIN },	//			D4 f
	// Rect
	{ "inflate",        LB::b_inflate,      2, 3, 400, FBLTIN },    //			D4 f
	{ "inside",			LB::b_inside,		2, 2, 400, FBLTIN },	//			D4 f
	{ "intersect",		LB::b_intersect,	2, 2, 400, FBLTIN },	//			D4 f
	{ "map",			LB::b_map,			3, 3, 400, FBLTIN },	//			D4 f
	{ "rect",			LB::b_rect,			2, 4, 400, FBLTIN },	//			D4 f
	{ "union",			LB::b_union,		2, 2, 400, FBLTIN },	//			D4 f
	// Sound
	{ "beep",	 		LB::b_beep,			0, 1, 200, CBLTIN },	// D2
	{ "isPastCuePoint",LB::b_isPastCuePoint,2, 2, 600, FBLTIN },	// 					D6 f
	{ "mci",	 		LB::b_mci,			1, 1, 300, CBLTIN },	//		D3.1 c
	{ "mciwait",		LB::b_mciwait,		1, 1, 300, CBLTIN },	//		D3.1 c
	{ "sound",			LB::b_sound,		2, 3, 300, CBLTIN },	//		D3 c
	{ "soundBusy",		LB::b_soundBusy,	1, 1, 300, FBLTIN },	//		D3 f
	// Constants
	{ "backspace",		LB::b_backspace,	0, 0, 200, KBLTIN },	// D2 konstant
	{ "empty",			LB::b_empty,		0, 0, 200, KBLTIN },	// D2 k
	{ "enter",			LB::b_enter,		0, 0, 200, KBLTIN },	// D2 k
	{ "false",			LB::b_false,		0, 0, 200, KBLTIN },	// D2 k
	{ "quote",			LB::b_quote,		0, 0, 200, KBLTIN },	// D2 k
	{ "return",			LB::b_returnconst,	0, 0, 200, KBLTIN },	// D2 k
	{ "tab",			LB::b_tab,			0, 0, 200, KBLTIN },	// D2 k
	{ "true",			LB::b_true,			0, 0, 200, KBLTIN },	// D2 k
	{ "version",		LB::b_version,		0, 0, 300, KBLTIN },	//		D3 k
	// References
	{ "cast",			LB::b_cast,			1, 1, 400, FBLTIN },	//			D4 f
	{ "castLib",		LB::b_castLib,		1, 1, 500, FBLTIN },	//				D5 f
	{ "member",			LB::b_member,		1, 2, 500, FBLTIN },	//				D5 f
	{ "script",			LB::b_script,		1, 2, 400, FBLTIN },	//			D4 f
	{ "sprite",			LB::b_sprite,		1, 1, 500, FBLTIN },	//				D5 f
	{ "window",			LB::b_window,		1, 1, 400, FBLTIN },	//			D4 f
	{ "windowPresent",	LB::b_windowPresent,1, 1, 500, FBLTIN },	//				D5 f
	// Field operations
	{ "charPosToLoc",	LB::b_charPosToLoc, 2, 2, 500, FBLTIN },	//				D5 f
	{ "lineHeight",		LB::b_lineHeight,   2, 2, 500, FBLTIN },	//				D5 f
	{ "linePosToLocV",	LB::b_linePosToLocV,2, 2, 500, FBLTIN },	//				D5 f
	{ "locToCharPos",	LB::b_locToCharPos, 2, 2, 500, FBLTIN },	//				D5 f
	{ "locVToLinePos",	LB::b_locVToLinePos,2, 2, 500, FBLTIN },	//				D5 f
	{ "scrollByLine",	LB::b_scrollByLine, 2, 2, 500, CBLTIN },	//				D5 c
	{ "scrollByPage",	LB::b_scrollByPage, 2, 2, 500, CBLTIN },	//				D5 c
	// Chunk operations
	{ "numberOfChars",	LB::b_numberofchars,1, 1, 300, FBLTIN },	//			D3 f
	{ "numberOfItems",	LB::b_numberofitems,1, 1, 300, FBLTIN },	//			D3 f
	{ "numberOfLines",	LB::b_numberoflines,1, 1, 300, FBLTIN },	//			D3 f
	{ "numberOfWords",	LB::b_numberofwords,1, 1, 300, FBLTIN },	//			D3 f
	// Digital video operations
	{ "trackCount",		LB::b_trackCount,	1, 1, 500, FBLTIN },	//				D5 f
	{ "trackStartTime",	LB::b_trackStartTime,1,1, 500, FBLTIN },	//				D5 f
	{ "trackStopTime",	LB::b_trackStopTime,1, 1, 500, FBLTIN },	//				D5 f
	{ "trackType",		LB::b_trackType,	1, 1, 500, FBLTIN },	//				D5 f

	// Save session
	{ "beginRecording", LB::b_beginRecording,0, 1, 500, CBLTIN },	//				D5 c
	{ "endRecording",	LB::b_endRecording,	 0, 0, 500, CBLTIN },	//				D5 c

	// Shockwave
	{ "externalParamCount", LB::b_externalParamCount, 0, 0, 600, FBLTIN }, //			D6 f
	{ "externalParamName",  LB::b_externalParamName, 1, 1, 600, FBLTIN },  // 			D6 f
	{ "externalParamValue", LB::b_externalParamValue, 1, 1, 600, FBLTIN },  // 			D6 f
	{ "frameReady", LB::b_frameReady, 0, 2, 600, FBLTIN }, 			//					D6 f
	{ "getPref", LB::b_getPref, 1, 1, 600, FBLTIN }, 				//					D6 f

	// ScummVM Asserts: Used for testing ScummVM's Lingo implementation
	{ "scummvmAssert",	LB::b_scummvmassert,1, 2, 200, HBLTIN },
	{ "scummvmAssertEqual",	LB::b_scummvmassertequal,2,3,200,HBLTIN },
	{ "scummvmNoFatalError", LB::b_scummvmNoFatalError,1,1,200,HBLTIN },

	// XCMD/XFCN (HyperCard), normally exposed
	{ "GetVolumes", LB::b_getVolumes, 0, 0, 400, FBLTIN },

	{ nullptr, nullptr, 0, 0, 0, VOIDSYM }
};

/* These are related to Director Services API, used by Xtras
   to talk to Director. Unused in ScummVM. Leaving here for reference.

   Media Info:
   composite			//	D5
   editableMedia		//			D7
   image				//	D5
   palette				//	D5
   sound				//	D5
   score				//	D5
   scriptStyles			//		D6
   text					//	D5
   textSyles			//	D5

   Media Format:
   macColorTable		//	D5
   macGWorld			//	D5
   macPICT				//	D5
   macSnd				//	D5
   macTEStyles			//	D5
   moaHandle			//	D5
   moaPixels			//	D5
   moaSound				//	D5
   moaTEStyles			//	D5
   winDIB				//	D5
   winPALETTE			//	D5
   winPICT				//	D5
   winWAVE				//	D5

   Frame properties:
   palette				//	D5
   paletteFrames		//	D5
   paletteOverTime		//	D5
   paletteRef			//	D5
   paletteSpeed			//	D5
   paletteTransitionType//	D5
   script				//	D5
   tempo				//	D5
   transition			//	D5
   waitClick			//	D5
   waitDigitalVideo		//	D5
   waitSeconds			//	D5
   waitSound			//	D5

   Palette properties:
   fadeToBlack			//	D5
   fadeToWhite			//	D5
   normal				//	D5

   Sound properties:
   member				//	D5
   scoreColor			//	D5

   Sprite properties:
   member				//	D5
   scoreColor			//	D5
   script				//	D5
   scriptNum			//	D5
   size					//	D5
   loc					//	D5
   foreColor			//	D5
   color				//	D5
   backColor			//	D5
   bgColor				//			D7
   ink					//	D5
   trails				//	D5
   moveableSprite		//	D5
   editableText			//	D5
   blend				//	D5
   stretch				//	D5
   tweened				//		D6

   General properties:
   authorMode			//	D5
   folderName			//	D5
   maxMember			//	D5
   minMember			//	D5
   memberCount			//	D5
   modified				//	D5
   name					//	D5
   pathName				//	D5
   preloadMode			//				D8
   selectiomn			//	D5
   soundDevice			//			D7
   version				//	D5

   Movie properties:
   active3dRenderer		//				D8.5
   activeCast			//	D5
   activeCastLib		//	D5
   bgStageColor			//			D7
   castCount			//	D5
   createName			//	D5
   defaultColorDepth	//	D5
   defaultPalette		//	D5
   defaultStageRect		//	D5
   editShortcutsEnabled	//				D8
   enableFlashLingo		//				D8.5
   enableInkmodeLimitations//			D8
   frame				//	D5
   instance				//	D5
   lastChannel			//			D7
   modified				//	D5
   modifyName			//	D5
   movieAboutInfo		//			D7
   movieCopyrightInfo	//			D7
   movieFileVersion		//				D8
   movieImageCompression//				D8
   movieImageQuality	//				D8
   name					//	D5
   okToQueryKeyboard	//				D8.5
   pathName				//	D5
   playing				//	D5
   preferred3dRenderer	//				D8.5
   remapPalettes		//	D5
   scriptExecutionStyle	//					D10
   safePlayer			//		D6
   scoreSelection		//	D5
   stageColor			//	D5
   tempo				//	D5
   tempoScaleFactor		//			D7
   urlAdmin				//			D7
   version				//	D5

   Time Frame Prop:
   label				//	D5
   palette				//	D5
   script				//	D5
   tempo				//	D5
   transition			//	D5

   Host Info:
   appFileSpec			//	D5

   Types:
	bitmap
	button
	digitalVideo
	field
	filmLoop
	movie
	ole
	palette
	richText
	shape
	script
	sound
	transition
*/

void Lingo::initBuiltIns() {
	initBuiltIns(builtins);
}

void Lingo::initBuiltIns(const BuiltinProto protos[]) {
	for (const BuiltinProto *blt = protos; blt->name; blt++) {
		if (blt->version > _vm->getVersion())
			continue;

		Symbol sym;

		sym.name = new Common::String(blt->name);
		sym.type = blt->type;
		sym.nargs = blt->minArgs;
		sym.maxArgs = blt->maxArgs;

		sym.u.bltin = blt->func;

		switch (blt->type) {
		case CBLTIN:
			_builtinCmds[blt->name] = sym;
			break;
		case FBLTIN_LIST:
			_builtinListHandlers[blt->name] = sym; // fall-through
		case FBLTIN:
			_builtinFuncs[blt->name] = sym;
			break;
		case HBLTIN_LIST:
			_builtinListHandlers[blt->name] = sym; // fall-through
		case HBLTIN:
			_builtinCmds[blt->name] = sym;
			_builtinFuncs[blt->name] = sym;
			break;
		case KBLTIN:
			_builtinConsts[blt->name] = sym;
		default:
			break;
		}
	}
}

void Lingo::cleanupBuiltIns() {
	_builtinCmds.clear();
	_builtinFuncs.clear();
	_builtinConsts.clear();
}

void Lingo::cleanupBuiltIns(const BuiltinProto protos[]) {
	for (const BuiltinProto *blt = protos; blt->name; blt++) {
		switch (blt->type) {
		case CBLTIN:
			_builtinCmds.erase(blt->name);
			break;
		case FBLTIN:
			_builtinFuncs.erase(blt->name);
			break;
		case HBLTIN:
			_builtinCmds.erase(blt->name);
			_builtinFuncs.erase(blt->name);
			break;
		case KBLTIN:
			_builtinConsts.erase(blt->name);
		default:
			break;
		}
	}
}

void Lingo::printArgs(const char *funcname, int nargs, const char *prefix) {
	Common::String s;
	if (prefix)
		s += Common::String(prefix);

	s += Common::String(funcname);
	s += '(';

	for (int i = 0; i < nargs; i++) {
		Datum d = _state->stack[_state->stack.size() - nargs + i];

		s += d.asString(true);

		if (i != nargs - 1)
			s += ", ";
	}

	s += ")";

	debug(3, "%s", s.c_str());
}

void Lingo::convertVOIDtoString(int arg, int nargs) {
	if (_state->stack[_state->stack.size() - nargs + arg].type == VOID) {
		if (_state->stack[_state->stack.size() - nargs + arg].u.s != nullptr)
			g_lingo->_state->stack[_state->stack.size() - nargs + arg].type = STRING;
		else
			warning("Incorrect convertVOIDtoString for arg %d of %d", arg, nargs);
	}
}

void Lingo::dropStack(int nargs) {
	for (int i = 0; i < nargs; i++)
		pop();
}

void Lingo::drop(uint num) {
	if (num > _state->stack.size() - 1) {
		warning("Incorrect number of elements to drop from stack: %d > %d", num, _state->stack.size() - 1);
		return;
	}
	_state->stack.remove_at(_state->stack.size() - 1 - num);
}


///////////////////
// Math
///////////////////
void LB::b_abs(int nargs) {
	Datum d = g_lingo->pop();
	Datum res(0);

	if (d.type == INT)
		res = Datum(ABS(d.u.i));
	else if (d.type == FLOAT)
		res = Datum(ABS(d.u.f));

	g_lingo->push(res);
}

void LB::b_atan(int nargs) {
	Datum d = g_lingo->pop();
	Datum res(atan(d.asFloat()));
	g_lingo->push(res);
}

void LB::b_cos(int nargs) {
	Datum d = g_lingo->pop();
	Datum res(cos(d.asFloat()));
	g_lingo->push(res);
}

void LB::b_exp(int nargs) {
	Datum d = g_lingo->pop();
	// Lingo uses int, so we're enforcing it
	Datum res((double)exp((double)d.asInt()));
	g_lingo->push(res);
}

void LB::b_float(int nargs) {
	Datum d = g_lingo->pop();
	Datum res;

	if (d.type == STRING) {
		Common::String src = d.asString();
		char *endPtr = nullptr;
		double result = strtod(src.c_str(), &endPtr);
		if (*endPtr == 0) {
			res = result;
		} else {
			// for some reason, float(str) will return str if it doesn't work
			res = d;
		}
	} else if (d.type == INT || d.type == FLOAT) {
		res = d.asFloat();
	} else {
		warning("b_float: Attempted to process invalid type %s, returning same value", d.type2str());
		res = d;
	}

	g_lingo->push(res);
}

void LB::b_integer(int nargs) {
	Datum d = g_lingo->pop();
	Datum res;

	if (d.type == FLOAT) {
		if (g_director->getVersion() < 500) {	// Note that D4 behaves differently from asInt()
			res = (int)(d.u.f + 0.5);		// Yes, +0.5 even for negative numbers
		} else {
			res = (int)round(d.u.f);
		}
	} else if (d.type == STRING) {
		Common::String src = d.asString();
		char *endPtr = nullptr;
		int result = (int)strtol(src.c_str(), &endPtr, 10);
		// Stop conditions found by probing D4 for windows:
		// repeat with i = 0 to 255
		//   put i & " = " & integer("12345" & numToChar(i))
		// end repeat
		if (endPtr && endPtr != src.c_str() && (
			(*endPtr >= 0 && *endPtr < 45) ||
			(*endPtr == 47) ||
			(*endPtr >= 58 && *endPtr < 65) ||
			(*endPtr >= 91 && *endPtr < 95) ||
			(*endPtr == 96) ||
			(*endPtr >= 123))
		) {
			res = result;
		}
	} else {
		res = d.asInt();
	}

	g_lingo->push(res);
}

void LB::b_log(int nargs) {
	Datum d = g_lingo->pop();
	Datum res(log(d.asFloat()));
	g_lingo->push(res);
}

void LB::b_pi(int nargs) {
	Datum res((double)M_PI);
	g_lingo->push(res);
}

void LB::b_power(int nargs) {
	Datum d1 = g_lingo->pop();
	Datum d2 = g_lingo->pop();
	Datum res(pow(d2.asFloat(), d1.asFloat()));
	g_lingo->push(res);
}

void LB::b_random(int nargs) {
	int max = g_lingo->pop().asInt();
	Datum res;
	// Output in D4/D5 seems to be bounded from 1-65535, regardless of input.
	if (max <= 0) {
		res = Datum((int)(g_director->_rnd.getRandom(65535) + 1));
	} else {
		max = MIN(max, 65535);
		res = Datum((int)(g_director->_rnd.getRandom(max) + 1));
	}
	g_lingo->push(res);
}

void LB::b_sin(int nargs) {
	Datum d = g_lingo->pop();
	Datum res(sin(d.asFloat()));
	g_lingo->push(res);
}

void LB::b_sqrt(int nargs) {
	Datum d = g_lingo->pop();
	Datum res;
	if (d.type == INT) {
		// integer input returns the sqrt rounded to the nearest int
		res = Datum((int)round(sqrt(d.asInt())));
	} else if (d.type == FLOAT) {
		// float input returns float output
		res = Datum(sqrt(d.asFloat()));
	} else if (d.type == STRING) {
		// string input attempts to coerce to float, else crash
		Common::String src = d.asString();
		char *endPtr = nullptr;
		double result = strtod(src.c_str(), &endPtr);
		if (*endPtr == 0) {
			res = Datum(sqrt(result));
		} else {
			g_lingo->lingoError("b_sqrt: Invalid string");
		}
	} else if (d.type == VOID) {
		// void input returns float 0.0
		res = Datum(0.0);
	} else {
		g_lingo->lingoError("b_sqrt: Invalid type");
	}
	g_lingo->push(res);
}

void LB::b_tan(int nargs) {
	Datum d = g_lingo->pop();
	Datum res(tan(d.asFloat()));
	g_lingo->push(res);
}

void LB::b_void(int nargs) {
	g_lingo->pushVoid();
}

///////////////////
// String
///////////////////
void LB::b_chars(int nargs) {
	Datum d3 = g_lingo->pop();
	Datum d2 = g_lingo->pop();
	Datum s = g_lingo->pop();
	TYPECHECK(s, STRING);

	if (g_director->getVersion() < 400 && (d2.type == FLOAT || d3.type == FLOAT)) {
		warning("LB::b_chars: Called with a float in Director 2 and 3 mode. chars' can't handle floats");
		g_lingo->push(Datum(0));
		return;
	}

	int to = d3.asInt();
	int from = d2.asInt();

	Common::U32String src = s.asString().decode(Common::kUtf8);

	int len = src.size();
	int f = MAX(0, MIN(len, from - 1));
	int t = MAX(0, MIN(len, to));

	Common::String res;
	if (f > t) {
		res = Common::String("");
	} else {
		res = src.substr(f, t - f).encode(Common::kUtf8);
	}

	g_lingo->push(res);
}

void LB::b_charToNum(int nargs) {
	Datum d = g_lingo->pop();
	TYPECHECK(d, STRING);

	Common::U32String str = d.asString().decode(Common::kUtf8);
	if (str.size() == 0) {
		g_lingo->push(0);
		return;
	}

	g_lingo->push(charToNum(str[0]));
}

void LB::b_length(int nargs) {
	Datum d = g_lingo->pop();
	if (d.type == INT || d.type == FLOAT || d.type == VOID) {
		g_lingo->push(0);
		return;
	}
	TYPECHECK(d, STRING);

	Common::U32String src = d.asString().decode(Common::kUtf8);
	int res = src.size();
	g_lingo->push(res);
}

void LB::b_numToChar(int nargs) {
	Datum d = g_lingo->pop();
	if (g_director->getVersion() < 400) {
		TYPECHECK(d, INT);
	} else if (d.type != INT) {
		warning("BUILDBOT: b_numToChar: Unimplemented behaviour for arg of type %s", (d).type2str());
		return;
	}

	int num = d.asInt();
	g_lingo->push(Common::U32String(numToChar(num)).encode(Common::kUtf8));
}

void LB::b_offset(int nargs) {
	if (nargs == 3) {
		b_offsetRect(nargs);
		return;
	}
	Common::String source = g_lingo->pop().asString();
	Common::String target = g_lingo->pop().asString();

	const char *str = d_strstr(source.c_str(), target.c_str());

	if (str == nullptr)
		g_lingo->push(Datum(0));
	else
		g_lingo->push(Datum(int(str - source.c_str() + 1)));
}

void LB::b_string(int nargs) {
	Datum d = g_lingo->pop();
	Datum res(d.asString());
	g_lingo->push(res);
}

void LB::b_value(int nargs) {
	Datum d = g_lingo->pop();
	if (d.type != STRING) {
		g_lingo->push(d);
		return;
	}

	Common::String expr = d.asString();
	expr.trim();
	if (expr.empty()) {
		g_lingo->push(Datum(0));
		return;
	}
	Common::String code = "return " + expr;
	// Compile the code to an anonymous function and call it
	ScriptContext *sc = g_lingo->_compiler->compileAnonymous(code, kLPPTrimGarbage);
	if (!sc) {
		warning("b_value(): Failed to parse expression \"%s\", returning void", expr.c_str());
		g_lingo->pushVoid();
		return;
	}
	Symbol sym = sc->_eventHandlers[kEventGeneric];
	LC::call(sym, 0, true);
}

///////////////////
// Lists
///////////////////
void LB::b_add(int nargs) {
	Datum value = g_lingo->pop();
	Datum list = g_lingo->pop();

	TYPECHECK(list, ARRAY);

	// If the list is sorted, keep the sort
	if (list.u.farr->_sorted) {
		if (list.u.farr->arr.empty()) {
			list.u.farr->arr.push_back(value);
		} else {
			// TODO: We'd better do a binary search here
			uint pos = list.u.farr->arr.size();
			for (uint i = 0; i < list.u.farr->arr.size(); i++) {
				if (list.u.farr->arr[i] > value) { // We are using Datum::compareTo() here
					pos = i;
					break;
				}
			}
			list.u.farr->arr.insert_at(pos, value);
		}
	} else {
		list.u.farr->arr.push_back(value);
		list.u.farr->_sorted = false;		// Drop the sorted flag
	}
}

void LB::b_addAt(int nargs) {
	Datum value = g_lingo->pop();
	Datum indexD = g_lingo->pop();
	Datum list = g_lingo->pop();

	TYPECHECK2(indexD, INT, FLOAT);
	int index = indexD.asInt();
	TYPECHECK(list, ARRAY);

	int size = list.u.farr->arr.size();
	if (index > size) {
		for (int i = 0; i < index - size - 1; i++)
			list.u.farr->arr.push_back(Datum(0));
	}
	list.u.farr->arr.insert_at(index - 1, value);
}

void LB::b_addProp(int nargs) {
	Datum value = g_lingo->pop();
	Datum prop = g_lingo->pop();
	Datum list = g_lingo->pop();

	TYPECHECK(list, PARRAY);

	PCell cell = PCell(prop, value);

	if (list.u.parr->_sorted) {
		if (list.u.parr->arr.empty())
			list.u.parr->arr.push_back(cell);
		else {
			uint pos = list.u.parr->arr.size();
			for (uint i = 0; i < list.u.parr->arr.size(); i++) {
				if (list.u.parr->arr[i].p > cell.p) {
					pos = i;
					break;
				}
			}
			list.u.parr->arr.insert_at(pos, cell);
		}
	} else {
		list.u.parr->arr.push_back(cell);
	}
}

void LB::b_append(int nargs) {
	Datum value = g_lingo->pop();
	Datum list = g_lingo->pop();

	TYPECHECK(list, ARRAY);

	list.u.farr->arr.push_back(value);
	list.u.farr->_sorted = false;		// Drop the sorted flag
}

void LB::b_count(int nargs) {
	Datum list = g_lingo->pop();
	Datum result;
	result.type = INT;

	switch (list.type) {
	case ARRAY:
	case RECT:
	case POINT:
		result.u.i = list.u.farr->arr.size();
		break;
	case PARRAY:
		result.u.i = list.u.parr->arr.size();
		break;
	case OBJECT:
		result.u.i = list.u.obj->getPropCount();
		break;
	default:
		TYPECHECK3(list, ARRAY, PARRAY, OBJECT);
	}

	g_lingo->push(result);
}

void LB::b_deleteAt(int nargs) {
	Datum indexD = g_lingo->pop();
	Datum list = g_lingo->pop();
	TYPECHECK2(indexD, INT, FLOAT);
	TYPECHECK2(list, ARRAY, PARRAY);
	int index = indexD.asInt();

	switch (list.type) {
	case ARRAY:
		list.u.farr->arr.remove_at(index - 1);
		break;
	case PARRAY:
		list.u.parr->arr.remove_at(index - 1);
		break;
	default:
		break;
	}
}

void LB::b_deleteOne(int nargs) {
	Datum val = g_lingo->pop();
	Datum list = g_lingo->pop();
	TYPECHECK2(list, ARRAY, PARRAY);

	switch (list.type) {
	case ARRAY: {
		g_lingo->push(list);
		g_lingo->push(val);
		b_getPos(nargs);
		int index = g_lingo->pop().asInt();
		if (index > 0) {
			list.u.farr->arr.remove_at(index - 1);
		}
		break;
	}
	case PARRAY: {
		Datum d;
		int index = LC::compareArrays(LC::eqData, list, val, true, true).u.i;
		if (index > 0) {
			list.u.parr->arr.remove_at(index - 1);
		}
		break;
	}
	default:
		TYPECHECK2(list, ARRAY, PARRAY);
	}
}


void LB::b_deleteProp(int nargs) {
	Datum prop = g_lingo->pop();
	Datum list = g_lingo->pop();
	TYPECHECK2(list, ARRAY, PARRAY);

	switch (list.type) {
	case ARRAY:
		g_lingo->push(list);
		g_lingo->push(prop);
		b_deleteAt(nargs);
		break;
	case PARRAY: {
		int index = LC::compareArrays(LC::eqData, list, prop, true).u.i;
		if (index > 0) {
			list.u.parr->arr.remove_at(index - 1);
		}
		break;
	}
	default:
		break;
	}
}


void LB::b_duplicateList(int nargs) {
	Datum list = g_lingo->pop();
	TYPECHECK2(list, ARRAY, PARRAY);
	g_lingo->push(list.clone());
}


void LB::b_findPos(int nargs) {
	Datum prop = g_lingo->pop();
	Datum list = g_lingo->pop();
	Datum d(g_lingo->getVoid());
	TYPECHECK2(list, ARRAY, PARRAY);

	if (list.type == ARRAY) {
		if (list.u.farr->_sorted) {
			int index = LC::compareArrays(LC::eqData, list, prop, true).u.i;
			if (index > 0)
				d = index;
			else
				d = 0;
		} else {
			if (prop.asInt() > 0 && prop.asInt() <= (int)list.u.farr->arr.size())
				d = prop.asInt();
			else
				d = 0;
		}
	} else {
		int index = LC::compareArrays(LC::eqData, list, prop, true).u.i;
		if (index > 0) {
			d = index;
		}
	}

	g_lingo->push(d);
}

void LB::b_findPosNear(int nargs) {
	Datum prop = g_lingo->pop();
	Datum list = g_lingo->pop();
	Datum res;
	TYPECHECK2(list, PARRAY, ARRAY);

	// This requires more testing, but D4-D6 test show that it does not work as described.
	// The example:
	//   findPosNear([#Nile:2, #Pharaoh:4, #Raja:0], #Ni)  supposed to return 1, for #Nile, but it returns 4
	if (list.type == PARRAY) {
		res = Datum((int)list.u.parr->arr.size() + 1); // Set it to the end of array by default
		int index = LC::compareArrays(list.u.parr->_sorted ? LC::geData : LC::eqData, list, prop, true).u.i;
		if (index != 0) {
			res = index;
		}
	} else if (list.type == ARRAY) {
		res = prop; // set it to the returned value
		int index = LC::compareArrays(list.u.farr->_sorted ? LC::geData : LC::eqData, list, prop, true).u.i;
		if (index != 0) {
			res = index;
		}
	}

	g_lingo->push(res);
}

void LB::b_getaProp(int nargs) {
	Datum prop = g_lingo->pop();
	Datum list = g_lingo->pop();

	switch (list.type) {
	case ARRAY:
		g_lingo->push(list);
		g_lingo->push(prop);
		b_getAt(nargs);
		break;
	case PARRAY: {
		Datum d;
		int index = LC::compareArrays(LC::eqData, list, prop, true).u.i;
		if (index > 0) {
			d = list.u.parr->arr[index - 1].v;
		}
		g_lingo->push(d);
		break;
	}
	case OBJECT:
		{
			if (prop.type != SYMBOL) {
				g_lingo->lingoError("b_getaProp(): symbol expected");
				return;
			}
			Datum d;
			if (list.u.obj->hasProp(*prop.u.s))
				d = list.u.obj->getProp(*prop.u.s);
			g_lingo->push(d);
		}
		break;
	default:
		TYPECHECK3(list, ARRAY, PARRAY, OBJECT);
		break;
	}
}

void LB::b_getAt(int nargs) {
	Datum indexD = g_lingo->pop();
	TYPECHECK2(indexD, INT, FLOAT);
	Datum list = g_lingo->pop();
	int index = indexD.asInt();

	switch (list.type) {
	case ARRAY:
	case POINT:
	case RECT:
		ARRBOUNDSCHECK(index, list);
		g_lingo->push(list.u.farr->arr[index - 1]);
		break;
	case PARRAY:
		ARRBOUNDSCHECK(index, list);
		g_lingo->push(list.u.parr->arr[index - 1].v);
		break;
	default:
		TYPECHECK4(list, ARRAY, PARRAY, POINT, RECT);
	}
}

void LB::b_getLast(int nargs) {
	Datum list = g_lingo->pop();
	switch (list.type) {
	case ARRAY:
		if (list.u.farr->arr.empty()) {
			g_lingo->pushVoid();
		} else {
			g_lingo->push(list.u.farr->arr.back());
		}
		break;
	case PARRAY:
		if (list.u.farr->arr.empty()) {
			g_lingo->pushVoid();
		} else {
			g_lingo->push(list.u.parr->arr.back().v);
		}
		break;
	default:
		TYPECHECK(list, ARRAY);
	}
}

void LB::b_getOne(int nargs) {
	Datum val = g_lingo->pop();
	Datum list = g_lingo->pop();

	switch (list.type) {
	case ARRAY:
		g_lingo->push(list);
		g_lingo->push(val);
		b_getPos(nargs);
		break;
	case PARRAY: {
		Datum d;
		int index = LC::compareArrays(LC::eqData, list, val, true, true).u.i;
		if (index > 0) {
			d = list.u.parr->arr[index - 1].p;
		}
		g_lingo->push(d);
		break;
	}
	default:
		TYPECHECK2(list, ARRAY, PARRAY);
	}
}

void LB::b_getPos(int nargs) {
	Datum val = g_lingo->pop();
	Datum list = g_lingo->pop();
	TYPECHECK2(list, ARRAY, PARRAY);

	switch (list.type) {
	case ARRAY: {
		Datum d(0);
		int index = LC::compareArrays(LC::eqDataStrict, list, val, true).u.i;
		if (index > 0) {
			d.u.i = index;
		}
		g_lingo->push(d);
		break;
	}
	case PARRAY: {
		Datum d(0);
		int index = LC::compareArrays(LC::eqDataStrict, list, val, true, true).u.i;
		if (index > 0) {
			d.u.i = index;
		}
		g_lingo->push(d);
		break;
	}
	default:
		break;
	}
}

void LB::b_getProp(int nargs) {
	Datum prop = g_lingo->pop();
	Datum list = g_lingo->pop();

	switch (list.type) {
	case ARRAY:
		if (g_director->getVersion() < 500) {
			// D4 allows getProp to be called on ARRAYs
			g_lingo->push(list);
			g_lingo->push(prop);
			b_getAt(nargs);
		} else {
			g_lingo->lingoError("BUILDBOT: b_getProp: Attempted to call on an ARRAY");
		}
		break;
	case PARRAY: {
		int index = LC::compareArrays(LC::eqData, list, prop, true).u.i;
		if (index > 0) {
			g_lingo->push(list.u.parr->arr[index - 1].v);
		} else {
			g_lingo->lingoError("BUILDBOT: b_getProp: Property %s not found", prop.asString().c_str());
		}
		break;
	}
	case OBJECT:
		{
			if (prop.type != SYMBOL) {
				g_lingo->lingoError("BUILDBOT: b_getProp(): symbol expected, got %s", prop.type2str());
				return;
			}
			Datum d;
			if (list.u.obj->hasProp(*prop.u.s))
				d = list.u.obj->getProp(*prop.u.s);
			g_lingo->push(d);
		}
		break;
	default:
		TYPECHECK3(list, ARRAY, PARRAY, OBJECT);
		break;
	}
}

void LB::b_getPropAt(int nargs) {
	Datum indexD = g_lingo->pop();
	Datum list = g_lingo->pop();
	TYPECHECK2(indexD, INT, FLOAT);
	int index = indexD.asInt();
	switch (list.type) {
	case PARRAY:
		{
			if ((index <= 0) || (index > (int)list.u.parr->arr.size())) {
				g_lingo->lingoError("b_getPropAt(): index out of range");
				return;
			}
			g_lingo->push(list.u.parr->arr[index - 1].p);
		}
		break;
	case OBJECT:
		{
			if ((index <= 0) || (index > (int)list.u.obj->getPropCount())) {
				g_lingo->lingoError("b_getPropAt(): index out of range");
				return;
			}
			Common::String key = list.u.obj->getPropAt(index);
			Datum result(key);
			result.type = SYMBOL;
			g_lingo->push(result);
		}
		break;
	default:
		TYPECHECK2(list, PARRAY, OBJECT);
		break;
	}

}

void LB::b_list(int nargs) {
	Datum result;
	result.type = ARRAY;
	result.u.farr = new FArray;

	for (int i = 0; i < nargs; i++)
		result.u.farr->arr.insert_at(0, g_lingo->pop());

	g_lingo->push(result);
}

void LB::b_listP(int nargs) {
	Datum list = g_lingo->pop();
	Datum d(0);
	if (list.type == ARRAY || list.type == PARRAY) {
		d.u.i = 1;
	}
	g_lingo->push(d);
}

void LB::b_max(int nargs) {
	Datum max;
	max.type = INT;
	max.u.i = 0;

	bool hasVoidQuirk = g_director->getVersion() < 500;

	Common::Array<Datum> testArr;

	if (nargs == 1) {
		Datum d = g_lingo->pop();
		if (d.type == ARRAY) {
			for (auto &it : d.u.farr->arr) {
				testArr.push_back(it);
			}
		} else {
			max = d;
		}
	} else if (nargs > 0) {
		for (int i = 0; i < nargs; i++) {
			Datum d = g_lingo->peek(nargs - i - 1);
			if (d.type == ARRAY) {
				warning("b_max: undefined behavior: array mixed with other args");
			}
			testArr.push_back(d);
		}
		g_lingo->dropStack(nargs);
	}

	if (!testArr.empty()) {
		// D4: if there is a VOID for the first arg of max, return VOID
		if (hasVoidQuirk && testArr[0].type == VOID) {
			g_lingo->pushVoid();
			return;
		}
		// The trick seems to be to compare each item in sequence.
		// D4: If we encounter a VOID, treat it as the smallest value possible.
		// D5+: If we encounter a string, and the current maximum is a VOID, it gets ignored.
		// D5+: If we encounter a VOID, and the current maximum is a STRING, set it to VOID.

		max = testArr[0];
		for (int i = 1; i < (int)testArr.size(); i++) {
			if (!hasVoidQuirk) {
				if ((max.type == VOID) && (testArr[i].type == STRING)) {
					continue;
				} else if ((testArr[i].type == VOID) && (max.type == STRING)) {
					max = Datum();
					continue;
				}
			}
			if (testArr[i] > max) {
				max = testArr[i];
			}
		}
	}

	g_lingo->push(max);
}

void LB::b_min(int nargs) {
	Datum min;
	min.type = INT;
	min.u.i = 0;

	bool hasVoidQuirk = g_director->getVersion() < 500;

	Common::Array<Datum> testArr;

	if (nargs == 1) {
		Datum d = g_lingo->pop();
		if (d.type == ARRAY) {
			for (auto &it : d.u.farr->arr) {
				testArr.push_back(it);
			}
		} else {
			min = d;
		}
	} else if (nargs > 0) {
		for (int i = 0; i < nargs; i++) {
			Datum d = g_lingo->peek(nargs - i - 1);
			if (d.type == ARRAY) {
				warning("b_min: undefined behavior: array mixed with other args");
			}
			testArr.push_back(d);
		}
		g_lingo->dropStack(nargs);
	}
	if (!testArr.empty()) {
		// D4: if there is a VOID for the last arg of min, return VOID
		if (hasVoidQuirk && testArr[testArr.size()-1].type == VOID) {
			g_lingo->pushVoid();
			return;
		}
		// The trick seems to be to compare each item in sequence.
		// If we encounter a VOID, and the current minimum is a number or a symbol, it gets converted to VOID.
		// If we encounter a VOID, and the current minimum is a string, we ignore it.
		// If the current minimum is VOID, and we encounter a string, the current minimum is set to the string.

		min = testArr[0];
		for (int i = 1; i < (int)testArr.size(); i++) {
			if (testArr[i].type == VOID) {
				if (min.type != STRING) {
					min = Datum();
				}
				continue;
			// D4: if the current minimum is VOID, set the next value as the new minimum
			// D5+: if the current minimum is VOID, and the next value is a string (coercable or not), set it as the new minimum
			} else if ((min.type == VOID) && (hasVoidQuirk || (testArr[i].type == STRING))) {
				min = testArr[i];
				continue;
			}
			if (testArr[i] < min) {
				min = testArr[i];
			}
		}
	}
	g_lingo->push(min);
}

void LB::b_setaProp(int nargs) {
	Datum value = g_lingo->pop();
	Datum prop = g_lingo->pop();
	Datum list = g_lingo->pop();

	switch (list.type) {
	case ARRAY:
		g_lingo->push(list);
		g_lingo->push(prop);
		g_lingo->push(value);
		b_setAt(nargs);
		break;
	case PARRAY: {
		int index = LC::compareArrays(LC::eqData, list, prop, true).u.i;
		if (index > 0) {
			list.u.parr->arr[index - 1].v = value;
		} else {
			PCell cell = PCell(prop, value);
			list.u.parr->arr.push_back(cell);
		}
		break;
	}
	case OBJECT:
		{
			if (prop.type != SYMBOL) {
				g_lingo->lingoError("b_setaProp(): symbol expected");
				return;
			}
			list.u.obj->setProp(*prop.u.s, value);
		}
		break;
	default:
		TYPECHECK2(list, ARRAY, PARRAY);
	}
}

void LB::b_setAt(int nargs) {
	Datum value = g_lingo->pop();
	Datum indexD = g_lingo->pop();
	Datum list = g_lingo->pop();

	TYPECHECK2(indexD, INT, FLOAT);
	TYPECHECK3(list, ARRAY, PARRAY, RECT);
	int index = indexD.asInt();

	switch (list.type) {
	case ARRAY:
		if ((uint)index <= list.u.farr->arr.size()) {
			list.u.farr->arr[index - 1] = value;
		} else {
			int inserts = index - list.u.farr->arr.size();
			while (--inserts)
				list.u.farr->arr.push_back(Datum(0));
			list.u.farr->arr.push_back(value);
		}
		break;
	case PARRAY:
		ARRBOUNDSCHECK(index, list);
		list.u.parr->arr[index - 1].v = value;
		break;
	case RECT:
		ARRBOUNDSCHECK(index, list);
		list.u.farr->arr[index-1] = value;
	default:
		break;
	}
}

void LB::b_setProp(int nargs) {
	Datum value = g_lingo->pop();
	Datum prop = g_lingo->pop();
	Datum list = g_lingo->pop();

	switch (list.type) {
	case PARRAY:
		{
			int index = LC::compareArrays(LC::eqData, list, prop, true).u.i;
			if (index > 0) {
				list.u.parr->arr[index - 1].v = value;
			} else {
				warning("b_setProp: Property not found");
			}
		}
		break;
	case OBJECT:
		{
			if (prop.type != SYMBOL) {
				g_lingo->lingoError("BUILDBOT: b_setProp(): symbol expected, got %s", prop.type2str());
				return;
			}
			// unlike the PARRAY case, OBJECT seems to create
			// new properties without throwing an error
			list.u.obj->setProp(*prop.u.s, value);
		}
		break;
	default:
		TYPECHECK2(list, PARRAY, OBJECT);
		break;
	}
}

static bool sortArrayHelper(const Datum &lhs, const Datum &rhs) {
	return lhs < rhs;
}

static bool sortNumericArrayHelper(const Datum &lhs, const Datum &rhs) {
	return lhs.asFloat() < rhs.asFloat();
}

static bool sortPArrayHelper(const PCell &lhs, const PCell &rhs) {
	return lhs.p < rhs.p;
}

static bool sortNumericPArrayHelper(const PCell &lhs, const PCell &rhs) {
	return lhs.p.asFloat() < rhs.p.asFloat();
}

void LB::b_sort(int nargs) {
	// in D4 manual, p266. linear list is sorted by values
	// property list is sorted alphabetically by properties
	// once the list is sorted, it maintains its sort order even when we add new variables using add command
	// see b_append to get more details.
	Datum list = g_lingo->pop();

	if (list.type == ARRAY) {
		if (list.u.farr->_sorted)
			return;

		// Check to see if the array is full of numbers
		bool isNumeric = true;
		for (const auto &it : list.u.farr->arr) {
			isNumeric &= it.isNumeric();
		}
		if (isNumeric) {
			// Sorting an array of numbers will use numeric sort order.
			Common::sort(list.u.farr->arr.begin(), list.u.farr->arr.end(), sortNumericArrayHelper);
		} else {
			// Sorting an array of strings will use the string sort order.
			// Sorting an array of mixed types is undefined behaviour; sometimes the interpreter
			// will give an answer nearly the same as the string sort order, other times
			// the interpreter will softlock.
			Common::sort(list.u.farr->arr.begin(), list.u.farr->arr.end(), sortArrayHelper);
		}
		list.u.farr->_sorted = true;

	} else if (list.type == PARRAY) {
		if (list.u.parr->_sorted)
			return;

		// Check to see if the array is full of numbers
		bool isNumeric = true;
		for (const auto &it : list.u.parr->arr) {
			isNumeric &= it.p.isNumeric();
		}
		if (isNumeric) {
			Common::sort(list.u.parr->arr.begin(), list.u.parr->arr.end(), sortNumericPArrayHelper);
		} else {
			Common::sort(list.u.parr->arr.begin(), list.u.parr->arr.end(), sortPArrayHelper);
		}
		list.u.parr->_sorted = true;

	} else {
		warning("LB::b_sort can not handle argument of type %s", list.type2str());
	}
}


///////////////////
// Files
///////////////////
void LB::b_closeDA(int nargs) {
	warning("BUILDBOT: closeDA is not supported in ScummVM");
}

void LB::b_closeResFile(int nargs) {
	// closeResFile closes only resource files that were opened with openResFile.

	if (nargs == 0) { // Close all open resource files
		for (auto &it : g_director->_openResFiles)
			g_director->_allOpenResFiles.remove(it._key);

		g_director->_openResFiles.clear();
		return;
	}

	Datum d = g_lingo->pop();
	Common::Path resFileName(g_director->getCurrentWindow()->getCurrentPath() + d.asString(), g_director->_dirSeparator);

	if (g_director->_openResFiles.contains(resFileName)) {
		g_director->_openResFiles.erase(resFileName);

		g_director->_allOpenResFiles.remove(resFileName);
	}
}

void LB::b_closeXlib(int nargs) {
	if (nargs ==0) { // Close all Xlibs
		g_lingo->closeOpenXLibs();
		return;
	}

	Datum d = g_lingo->pop();
	Common::String xlibName = getFileName(d.asString());
	g_lingo->closeXLib(xlibName);
}

void LB::b_getNthFileNameInFolder(int nargs) {
	int fileNum = g_lingo->pop().asInt() - 1;
	Common::String pathRaw = g_lingo->pop().asString();
	if (pathRaw.empty()) {
		// If we receive a blank string as a path, it shouldn't match anything.
		g_lingo->push(Datum(""));
		return;
	}

	// getNthFileNameInFolder requires an absolute path as an input.
	// relative paths will not match anything.
	Common::Path path = findAbsolutePath(pathRaw, true);
	// for directory, we either return the correct path, which we can access recursively.
	// or we get a wrong path, which will lead us to a non-exist file node

	Common::StringArray directory_list = path.splitComponents();
	Common::FSNode d = Common::FSNode(*g_director->getGameDataDir());
	for (auto &it : directory_list) {
		d = d.getChild(it);
		if (!d.exists())
			break;
	}

	Datum r("");
	Common::Array<Common::String> fileNameList;

	// Update the game quirks archive in case our save state has changed.
	// This is necessary because we may save a game and then try to open a game in the same session.
	g_director->gameQuirks(g_director->getGameId(), g_director->getPlatform());

	// First, mix in any files injected from the quirks
	Common::Archive *cache = SearchMan.getArchive(kQuirksCacheArchive);
	if (cache) {
		Common::ArchiveMemberList files;

		cache->listMatchingMembers(files, path.append(path.empty() ? "*" : "/*", '/'), true);

		for (auto &fi : files) {
			fileNameList.push_back(Common::lastPathComponent(fi->getName(), '/'));
		}
	}

	// Next, mix in files from the game filesystem (if they exist)
	if (d.exists()) {
		Common::FSList f;
		if (!d.getChildren(f, Common::FSNode::kListAll)) {
			warning("Cannot access directory %s", path.toString(Common::Path::kNativeSeparator).c_str());
		} else {
			for (uint i = 0; i < f.size(); i++)
				fileNameList.push_back(f[i].getName());
		}
	}

	if (!fileNameList.empty() && (uint)fileNum < fileNameList.size()) {
		// Sort files alphabetically
		Common::sort(fileNameList.begin(), fileNameList.end());
		r = Datum(fileNameList[fileNum]);
	}

	g_lingo->push(r);
}

void LB::b_open(int nargs) {

	Datum ex = g_lingo->pop();
	Datum d;
	if (nargs == 2)
		d = g_lingo->pop();
	warning("LB::b_open(): Unsupported command open encountered -> The movie tried to open %s %s", ex.asString().c_str(), d.type != VOID ? d.asString().c_str() : "");

	if (!debugChannelSet(-1, kDebugFewFramesOnly) &&
		!(g_director->getGameGID() == GID_TEST || g_director->getGameGID() == GID_TESTALL)) {
		Common::U32String message = Common::String::format("Unsupported command open encountered -> The movie tried to execute open %s %s!", ex.asString().c_str(), d.type != VOID ? d.asString().c_str() : "");
		GUI::MessageDialog dialog(message, _("Ok"));
		dialog.runModal();
	}
}

void LB::b_openDA(int nargs) {
	Datum d = g_lingo->pop();

	warning("BUILDBOT: openDA is not supported in ScummVM");
}

void LB::b_openResFile(int nargs) {
	Datum d = g_lingo->pop();
	Common::Path resPath(g_director->getCurrentWindow()->getCurrentPath() + d.asString(), g_director->_dirSeparator);

	if (g_director->getPlatform() == Common::kPlatformWindows) {
 		warning("STUB: BUILDBOT: b_openResFile(%s) on Windows", d.asString().c_str());
 		return;
 	}

	if (!g_director->_allSeenResFiles.contains(resPath)) {
		MacArchive *arch = new MacArchive();
		if (arch->openFile(findPath(resPath))) {
			g_director->_openResFiles.setVal(resPath, arch);
			g_director->_allSeenResFiles.setVal(resPath, arch);
			g_director->addArchiveToOpenList(resPath);
		} else {
			delete arch;
		}
	}
}

void LB::b_openXlib(int nargs) {
	Common::String xlibName;

	Datum d = g_lingo->pop();

	Common::Path xlibPath = findXLibPath(d.asString(), true, false);
	if (g_director->getPlatform() == Common::kPlatformMacintosh) {
		// try opening the file as a Macintosh resource fork
		MacArchive *resFile = new MacArchive();
		if (resFile->openFile(xlibPath)) {
			uint32 XCOD = MKTAG('X', 'C', 'O', 'D');
			uint32 XCMD = MKTAG('X', 'C', 'M', 'D');
			uint32 XFCN = MKTAG('X', 'F', 'C', 'N');

			Common::Array<uint16> rsrcList = resFile->getResourceIDList(XCOD);

			for (uint i = 0; i < rsrcList.size(); i++) {
				xlibName = resFile->getResourceDetail(XCOD, rsrcList[i]).name.c_str();
				g_lingo->openXLib(xlibName, kXObj, xlibPath);
			}

			rsrcList = resFile->getResourceIDList(XCMD);
			for (uint i = 0; i < rsrcList.size(); i++) {
				xlibName = resFile->getResourceDetail(XCMD, rsrcList[i]).name.c_str();
				g_lingo->openXLib(xlibName, kXObj, xlibPath);
			}

			rsrcList = resFile->getResourceIDList(XFCN);
			for (uint i = 0; i < rsrcList.size(); i++) {
				xlibName = resFile->getResourceDetail(XFCN, rsrcList[i]).name.c_str();
				g_lingo->openXLib(xlibName, kXObj, xlibPath);
			}
			delete resFile;
			return;
		}
		delete resFile;
	}

	xlibName = getFileName(d.asString());

	// TODO: Figure out a nicer way of differentiating Xtras from XLibs on Mac
	if (xlibName.hasSuffixIgnoreCase(".x16") || xlibName.hasSuffixIgnoreCase(".x32")) {
		g_lingo->openXLib(xlibName, kXtraObj, xlibPath);
	} else {
		g_lingo->openXLib(xlibName, kXObj, xlibPath);
	}
}

void LB::b_save(int nargs) {
	g_lingo->printSTUBWithArglist("b_save", nargs);

	g_lingo->dropStack(nargs);
}

void LB::b_saveMovie(int nargs) {
	Common::String filename;
	if (nargs) {
		filename = g_lingo->pop().asString();
	}
	g_director->getCurrentMovie()->getArchive()->writeToFile(filename, g_director->getCurrentMovie());
}

void LB::b_setCallBack(int nargs) {
	g_lingo->printSTUBWithArglist("b_setCallBack", nargs);
	g_lingo->dropStack(nargs);
}

void LB::b_showResFile(int nargs) {
	if (nargs)
		g_lingo->pop();
	Common::String out;
	for (auto &it : g_director->_allOpenResFiles)
		out += it.toString(g_director->_dirSeparator) + "\n";
	g_debugger->debugLogFile(out, false);
}

void LB::b_showXlib(int nargs) {
	if (nargs)
		g_lingo->pop();
	Common::String out;
	for (auto &it : g_lingo->_openXLibs)
		out += it._key + "\n";
	g_debugger->debugLogFile(out, false);
}

void LB::b_xFactoryList(int nargs) {
	g_lingo->pop();
	Datum d("");

	for (auto &it : g_lingo->_openXLibs)
		*d.u.s += it._key + "\n";
	g_lingo->push(d);
}

void LB::b_xtra(int nargs) {
	Datum d = g_lingo->pop();
	if (d.type == INT) {
		int i = d.asInt() - 1; // Lingo index for XTRAs start at 1
		if (i >= 0 && (uint)i < g_lingo->_openXtraObjects.size()) {
			Datum var = g_lingo->_openXtraObjects[i];
			g_lingo->push(var);
			return;
		}
	} else {
		Common::String name = d.asString();
		for (uint i = 0; i < g_lingo->_openXtras.size(); i++) {
			if (name.equalsIgnoreCase(g_lingo->_openXtras[i])) {
				Datum var = g_lingo->_openXtraObjects[i];
				g_lingo->push(var);
				return;
			}
		}
	}
	g_lingo->lingoError("Xtra not found: %s", d.asString().c_str());
}

///////////////////
// Control
///////////////////
void LB::b_abort(int nargs) {
	g_lingo->_abort = true;
}

void LB::b_call(int nargs) {
	g_lingo->printSTUBWithArglist("b_call", nargs);
	g_lingo->dropStack(nargs);
}

void LB::b_callAncestor(int nargs) {
	g_lingo->printSTUBWithArglist("b_callAncestor", nargs);
	g_lingo->dropStack(nargs);
}

void LB::b_cancelIdleLoad(int nargs) {
	g_lingo->printSTUBWithArglist("b_cancelIdleLoad", nargs);
	g_lingo->dropStack(nargs);
}

void LB::b_continue(int nargs) {
	g_director->_playbackPaused = false;
}

void LB::b_dontPassEvent(int nargs) {
	g_lingo->_passEvent = false;
}

void LB::b_nothing(int nargs) {
	// Noop
}

void LB::b_delay(int nargs) {
	Datum d = g_lingo->pop();
	g_director->getCurrentMovie()->getScore()->setDelay(d.asInt());
}

void LB::b_do(int nargs) {
	Common::String code = g_lingo->pop().asString();

	if (code.empty())
		return;

	ScriptContext *sc = g_lingo->_compiler->compileAnonymous(code);
	if (!sc) {
		warning("b_do(): compilation failed, ignoring");
		return;
	} else if (!sc->_eventHandlers.contains(kEventGeneric)) {
		warning("b_do(): compiled code did not return handler, ignoring");
		return;
	}
	Symbol sym = sc->_eventHandlers[kEventGeneric];

	// Check if we have anything to execute
	if (sym.type == VOIDSYM)
		return;

	LC::call(sym, 0, false);
}

void LB::b_finishIdleLoad(int nargs) {
	g_lingo->printSTUBWithArglist("b_finishIdleLoad", nargs);
	g_lingo->dropStack(nargs);
}

void LB::b_go(int nargs) {
	// Builtin function for go as used by the Director bytecode engine.
	//
	// Accepted arguments:
	// "loop"
	// "next"
	// "previous"
	// (STRING|INT) frame
	// STRING movie, (STRING|INT) frame

	if (nargs >= 1 && nargs <= 2) {
		Datum firstArg = g_lingo->pop();
		nargs -= 1;
		bool callSpecial = false;

		if (firstArg.type == SYMBOL) {
			if (*firstArg.u.s == "loop") {
				g_lingo->func_gotoloop();
				callSpecial = true;
			} else if (*firstArg.u.s == "next") {
				g_lingo->func_gotonext();
				callSpecial = true;
			} else if (*firstArg.u.s == "previous") {
				g_lingo->func_gotoprevious();
				callSpecial = true;
			}
		}

		if (!callSpecial) {
			Datum movie;
			Datum frame;

			if (nargs > 0 && firstArg.type == STRING) {
				movie = firstArg;
				TYPECHECK(movie, STRING);

				frame = g_lingo->pop();
				nargs -= 1;
			// Even if there's more than one argument, if the first
			// arg is an int, Director discards the remainder and
			// treats it as the frame.
			} else if (nargs > 0 && firstArg.type == INT) {
				frame = g_lingo->pop();
				nargs -= 1;
			} else {
				frame = firstArg;
			}

			if (frame.type != STRING && frame.type != INT) {
				warning("b_go: frame arg should be of type STRING or INT, not %s", frame.type2str());
			}

			g_lingo->func_goto(frame, movie, true);
		}

		if (nargs > 0) {
			warning("b_go: ignoring %d extra args", nargs);
			g_lingo->dropStack(nargs);
		}

	} else {
		warning("b_go: expected 1 or 2 args, not %d", nargs);
		g_lingo->dropStack(nargs);
	}
}

void LB::b_halt(int nargs) {
	b_quit(nargs);

	warning("Movie halted");
}

void LB::b_idleLoadDone(int nargs) {
	g_lingo->printSTUBWithArglist("b_idleLoadDone", nargs);
	g_lingo->dropStack(nargs);
	Datum res(1);
	g_lingo->push(res);
}

void LB::b_pass(int nargs) {
	g_lingo->_passEvent = true;
}

void LB::b_pause(int nargs) {
	g_director->_playbackPaused = true;
}

void LB::b_play(int nargs) {
	// Builtin function for play as used by the Director bytecode engine.
	//
	// Accepted arguments:
	// 0  									# "play done"
	// (STRING|INT) frame
	// STRING movie, (STRING|INT) frame

	Datum movie;
	Datum frame;

	switch (nargs) {
	case 2:
		movie = g_lingo->pop();
		// fall through
	case 1:
		frame = g_lingo->pop();
		if (!(frame.type == INT && frame.u.i == 0 && nargs == 1))
			break;
		// fall through
	case 0:
		frame.type = SYMBOL;
		frame.u.s = new Common::String("done");
		break;
	default:
		warning("b_play: expected 0, 1 or 2 args, not %d", nargs);
		g_lingo->dropStack(nargs);

		return;
	}

	g_lingo->func_play(frame, movie);
}

void LB::b_playAccel(int nargs) {
	g_lingo->printSTUBWithArglist("b_playAccel", nargs);

/*
	byFrame	Read one frame at a time from disk.
	click		Stop and pass on mouse event.
	clickStop	Stop and don't pass on mouse event.
	loop		Play movie continuously.
	noFlush		Prevents the current interactive movie from being
				removed from memory when the Accelerator
				document is loaded.
	noSound		Don’t play sound.
	noUpdate	Don’t update screen at end of movie.
	playRect, l, t, r, b
				Stop the movie when the pointer is moved outside the coordinates (left, top, right, bottom).
	repeat, n 	Number of times to repeat.
	sync		Attempt to play in sync with scan rate of monitor.
	whatFits	Play only what fits into memory.
	tempo, n	Set the tempo of the movie.
*/

	g_lingo->dropStack(nargs);
}

void LB::b_preLoad(int nargs) {
	// We always pretend we preloaded all frames
	// Returning the number of the last frame successfully "loaded"
	if (nargs == 0) {
		g_lingo->_theResult = Datum((int)g_director->getCurrentMovie()->getScore()->getFramesNum());
		return;
	}

	g_lingo->_theResult = g_lingo->pop();

	if (nargs > 1)
		g_lingo->dropStack(nargs - 1);
}

void LB::b_preLoadCast(int nargs) {
	// We always pretend we preloaded all cast
	// Returning the number of the last cast successfully "loaded"

	if (nargs > 1) {
		g_lingo->_theResult = g_lingo->pop();
	} else {
		g_lingo->_theResult = 1;
	}

	if (nargs == 2)
		g_lingo->pop();
}

void LB::b_preLoadMovie(int nargs) {
	g_lingo->printSTUBWithArglist("b_preLoadMovie", nargs);
	g_lingo->dropStack(nargs);
}

void LB::b_framesToHMS(int nargs) {
	int fractionalSeconds = g_lingo->pop().asInt();
	int dropFrame = g_lingo->pop().asInt();
	int fps = g_lingo->pop().asInt();
	int frames = g_lingo->pop().asInt();

	fps = MAX(1, fps);

	bool negative = frames < 0;
	if (negative)
		frames = -frames;

	int framesPerMin = 60 * fps;
	int framesPerHour = 60 * framesPerMin;

	if (dropFrame)
		warning("STUB: b_framesToHMS: Unhandled dropFrame option");

	int h = MIN(frames / framesPerHour, 99);
	int m = (frames % framesPerHour) / framesPerMin;
	int s = (frames % framesPerMin) / fps;

	int residual;
	if (fractionalSeconds) {
		int ms = (1000 * (frames % fps)) / fps;
		residual = (ms + 5) / 10; // round to nearest centisecond
	} else {
		residual = frames % fps;
	}

	Common::String hms = Common::String::format(
		"%c%02d:%02d:%02d.%02d%c",
		negative ? '-' : ' ',
		h, m, s, residual,
		dropFrame ? 'd' : ' '
	);

	g_lingo->push(hms);
}

void LB::b_HMStoFrames(int nargs) {
	// The original implementation of this accepts some really weird,
	// seemingly malformed input strings.
	// (Try, for example, "12345678901234567890")
	// It's probably not worth supporting them all unless we need to,
	// so only well-formed input is handled right now.

	int fractionalSeconds = g_lingo->pop().asInt();
	int dropFrame = g_lingo->pop().asInt();
	int fps = g_lingo->pop().asInt();
	Common::String hms = g_lingo->pop().asString();

	if (fps <= 0)
		fps = 1;

	const char *ptr = hms.c_str();
	while (Common::isSpace(*ptr))
		ptr++;

	// read sign
	bool negative = false;
	if (*ptr == '-') {
		negative = true;
		ptr++;
	}

	// read HH, MM, and SS
	int secs = 0;
	for (int i = 0; i < 3; i++) {
		if (*ptr == ':' || Common::isSpace(*ptr))
			ptr ++;

		if (!Common::isDigit(*ptr))
			break;

		int part = 0;
		for (int j = 0; j < 2 && Common::isDigit(*ptr); j++) {
			part = (10 * part) + (*ptr - '0');
			ptr++;
		}
		secs = (60 * secs) + part;
	}
	int frames = secs * fps;

	// read FF
	if (*ptr == '.') {
		ptr++;

		int part = 0;
		for (int i = 0; i < 2 && Common::isDigit(*ptr); i++) {
			part = (10 * part) + (*ptr - '0');
			ptr++;
		}
		if (fractionalSeconds) {
			frames += (part * fps + 50) / 100; // round to nearest frame
		} else {
			frames += part;
		}
	}

	// read D
	if (*ptr == 'd' || *ptr == 'D') {
		ptr++;
		dropFrame = 1;
	}

	while (Common::isSpace(*ptr))
		ptr++;

	if (*ptr != '\0')
		warning("b_HMStoFrames: Unexpected character '%c'", *ptr);

	if (dropFrame)
		warning("STUB: b_HMStoFrames: Unhandled dropFrame option");

	if (negative)
		frames = -frames;

	g_lingo->push(frames);
}

void LB::b_param(int nargs) {
	int pos = g_lingo->pop().asInt();
	Datum result;
	CFrame *cf = g_lingo->_state->callstack[g_lingo->_state->callstack.size() - 1];
	// for named parameters, b_param must return what the current value is (i.e.
	// if the handler has changed it, return that)
	if (pos > 0 && cf->sp.argNames && pos <= (int)cf->sp.argNames->size()) {
		Datum func((*cf->sp.argNames)[pos - 1]);
		func.type = LOCALREF;
		result = g_lingo->varFetch(func);
	} else if (pos > 0 && pos <= (int)cf->paramList.size()) {
		// otherwise, if a function was called with extra unnamed arguments,
		// return that.
		result = cf->paramList[pos - 1];
	} else {
		warning("Invalid argument position %d", pos);
	}
	g_lingo->push(result);
}

void LB::b_printFrom(int nargs) {
	warning("BUILDBOT: printFrom is not supported in ScummVM");

	g_lingo->dropStack(nargs);
}

void LB::b_quit(int nargs) {
	Movie *movie = g_director->getCurrentMovie();
	if (movie)
		movie->getScore()->_playState = kPlayStopped;

	g_lingo->pushVoid();
}

void LB::b_return(int nargs) {
	CFrame *fp = g_lingo->_state->callstack.back();

	Datum retVal;
	if (nargs > 0) {
		retVal = g_lingo->pop();
	}

	// clear any temp values from loops
	while (g_lingo->_state->stack.size() > fp->stackSizeBefore)
		g_lingo->pop();

	// Do not allow a factory's mNew method to return a value
	if (nargs > 0 && !(g_lingo->_state->me.type == OBJECT && g_lingo->_state->me.u.obj->getObjType() == kFactoryObj
			&& fp->sp.name->equalsIgnoreCase("mNew"))) {
		g_lingo->push(retVal);
	}

	LC::procret();
}

void LB::b_restart(int nargs) {
	b_quit(nargs);

	warning("Computer restarts");
}

void LB::b_shutDown(int nargs) {
	b_quit(nargs);

	warning("Computer shuts down");
}

void LB::b_startTimer(int nargs) {
	g_director->getCurrentMovie()->_lastTimerReset = g_director->getMacTicks();
}

void LB::b_stopEvent(int nargs) {
	warning("STUB: b_stopEvent");
	// TEquivalent to the dontPassEvent command used in earlier
	//versions of Director, this command also applies to sprite scripts.
	g_lingo->_passEvent = false;
}

///////////////////
// Types
///////////////////
void LB::b_factory(int nargs) {
	Datum factoryName = g_lingo->pop();
	factoryName.type = GLOBALREF;
	Datum o = g_lingo->varFetch(factoryName);
	if (o.type == OBJECT && (o.u.obj->getObjType() & (kFactoryObj | kXObj))
			&& o.u.obj->getName().equalsIgnoreCase(*factoryName.u.s) && o.u.obj->getInheritanceLevel() == 1) {
		g_lingo->push(o);
	} else {
		g_lingo->push(Datum(0));
	}
}

void LB::b_floatP(int nargs) {
	Datum d = g_lingo->pop();
	Datum res((d.type == FLOAT) ? 1 : 0);
	g_lingo->push(res);
}

void LB::b_ilk(int nargs) {
	Datum res(0);
	if (nargs == 1) {
		// Single-argument mode returns the type of the item as a symbol.
		// D4 is inconsistent about what types this variant is allowed to work with; e.g. #integer is fine,
		// but #proplist is not. For now, give a response for all types.
		Datum item = g_lingo->pop();
		res = Datum(Common::String(item.type2str(true)));
		res.type = SYMBOL;
		g_lingo->push(res);
		return;
	}

	if (nargs > 2) {
		warning("b_ilk: dropping %d extra args", nargs - 2);
		g_lingo->dropStack(nargs - 2);
	}

	// Two argument mode checks the type of the item against a symbol.
	Datum type = g_lingo->pop();
	Datum item = g_lingo->pop();
	if (type.type != SYMBOL) {
		warning("b_ilk: expected a symbol for second arg");
	} else {
		Common::String typeCopy = type.asString();

		// A special case is #list, which is the equivalent of checking the item type is one of #linearlist,
		// #proplist, #point and #rect.
		if (typeCopy.equalsIgnoreCase("list")) {
			res.u.i = item.type == ARRAY ? 1 : 0;
			res.u.i |= item.type == PARRAY ? 1 : 0;
			res.u.i |= item.type == POINT ? 1 : 0;
			res.u.i |= item.type == RECT ? 1 : 0;
		} else {
			res.u.i = typeCopy.equalsIgnoreCase(item.type2str(true)) ? 1 : 0;
		}
	}
	g_lingo->push(res);
}

void LB::b_integerp(int nargs) {
	Datum d = g_lingo->pop();
	Datum res((d.type == INT) ? 1 : 0);
	g_lingo->push(res);
}

void LB::b_objectp(int nargs) {
	Datum d = g_lingo->pop();
	Datum res;
	if (d.type == OBJECT) {
		res = !d.u.obj->isDisposed();
	} else if (d.type == ARRAY || d.type == PARRAY) {
		res = 1;
	} else {
		res = 0;
	}
	g_lingo->push(res);
}

void LB::b_pictureP(int nargs) {
	Datum d = g_lingo->pop();
	Datum res((d.type == PICTUREREF) ? 1 : 0);
	g_lingo->push(res);
}

void LB::b_stringp(int nargs) {
	Datum d = g_lingo->pop();
	Datum res((d.type == STRING) ? 1 : 0);
	g_lingo->push(res);
}

void LB::b_symbol(int nargs) {
	Datum d = g_lingo->pop();
	switch (d.type) {
	case SYMBOL:
		g_lingo->push(d);
		break;
	case STRING:
		{
			Common::String payload = d.asString();
			if ((payload.size() == 0) || ((payload.size() == 1) && (payload[0] == ' '))) {
				payload = "";
			} else if (payload.size() == 1) {
				// if the string is one character, allow it
				// unless it's space, in which case return a zero-length symbol
			} else {
				// if the string is more than one character, cut it at the first non [a-zA-Z0-9_] character
				for (unsigned int i = 0; i < payload.size(); i++) {
					if (!Common::isAlnum(payload[i]) && (payload[i] != '_')) {
						payload = payload.substr(0, i);
						break;
					}
				}
			}

			Datum result(payload);
			result.type = SYMBOL;
			g_lingo->push(result);
		}
		break;
	default:
		g_lingo->pushVoid();
		break;
	}
}

void LB::b_symbolp(int nargs) {
	Datum d = g_lingo->pop();
	Datum res((d.type == SYMBOL) ? 1 : 0);
	g_lingo->push(res);
}


void LB::b_voidP(int nargs) {
	Datum d = g_lingo->pop();
	Datum res((d.type == VOID) ? 1 : 0);
	g_lingo->push(res);
}


///////////////////
// Misc
///////////////////
void LB::b_alert(int nargs) {
	// Let the movie know not to record mouse and key events
	// While there is an GUI alert box being shown
	// It may happen the user clicks on the button on the GUI message and
	// due to it getting recorded as a movie event, causes unpredictable changes
	g_director->getCurrentMovie()->_inGuiMessageBox = true;

	Datum d = g_lingo->pop();

	Common::String alert = d.asString();
	warning("b_alert(%s)", alert.c_str());

	if (g_director->getGameGID() == GID_TEST) {
		warning("b_alert: Skipping due to tests");

		return;
	}

	if (!debugChannelSet(-1, kDebugFewFramesOnly)) {
		g_director->_wm->clearHandlingWidgets();
		GUI::MessageDialog dialog(alert.c_str(), _("OK"));
		dialog.runModal();
	}

	// Movie can process events as normal now
	g_director->getCurrentMovie()->_inGuiMessageBox = false;
}

void LB::b_clearGlobals(int nargs) {
	for (auto &it : g_lingo->_globalvars) {
		if (!it._value.ignoreGlobal) {
			// For some reason, factory objects are not removed
			// by this command.
			if (it._value.type == OBJECT && it._value.u.obj->getObjType() & (kFactoryObj | kScriptObj))
				continue;
			g_lingo->_globalvars.erase(it._key);
		}
	}
}

void LB::b_cursor(int nargs) {
	Datum d = g_lingo->pop();
	g_lingo->func_cursor(d);
}

void LB::b_put(int nargs) {
	// Prints a statement to the Message window
	Common::String output;
	for (int i = nargs - 1; i >= 0; i--) {
		output += g_lingo->peek(i).asString(true);
		if (i > 0)
			output += " ";
	}
	if (g_debugger->isActive()) {
		g_debugger->debugLogFile(output, true);
	} else {
		debug("-- %s", output.c_str());
	}
	g_lingo->dropStack(nargs);
}

void LB::b_setPref(int nargs) {
	g_lingo->printSTUBWithArglist("b_setPref", nargs);
	g_lingo->dropStack(nargs);
}

void LB::b_showGlobals(int nargs) {
	b_version(0);
	Datum ver = g_lingo->pop();
	Common::String global_out = "-- Global Variables --\nversion = ";
	global_out += ver.asString() + "\n";
	if (g_lingo->_globalvars.size()) {
		for (auto it = g_lingo->_globalvars.begin(); it != g_lingo->_globalvars.end(); it++) {
			if (!it->_value.ignoreGlobal) {
				global_out += it->_key + " = " + it->_value.asString() + "\n";
			}
		}
	}
	g_debugger->debugLogFile(global_out, false);
}

void LB::b_showLocals(int nargs) {
	Common::String local_out = "-- Local Variables --\n";
	if (g_lingo->_state->localVars) {
		for (auto it = g_lingo->_state->localVars->begin(); it != g_lingo->_state->localVars->end(); it++) {
			local_out += it->_key + " = " + it->_value.asString() + "\n";
		}
	}
	g_debugger->debugLogFile(local_out, false);
}

///////////////////
// Score
///////////////////
void LB::b_constrainH(int nargs) {
	Datum num = g_lingo->pop();
	Datum sprite = g_lingo->pop();
	Score *score = g_director->getCurrentMovie()->getScore();
	int res = 0;
	if (score) {
		Channel *ch = score->getChannelById(sprite.asInt());
		if (ch) {
			res = CLIP<int> (num.asInt(), ch->getBbox().left, ch->getBbox().right);
		} else {
			warning("b_constrainH: cannot find channel %d", sprite.asInt());
		}
	} else {
		warning("b_constrainH: no score");
	}

	g_lingo->push(Datum(res));
}

void LB::b_constrainV(int nargs) {
	Datum num = g_lingo->pop();
	Datum sprite = g_lingo->pop();

	Score *score = g_director->getCurrentMovie()->getScore();
	int res = 0;
	if (score) {
		Channel *ch = score->getChannelById(sprite.asInt());
		if (ch) {
			res = CLIP<int> (num.asInt(), ch->getBbox().top, ch->getBbox().bottom);
		} else {
			warning("b_constrainH: cannot find channel %d", sprite.asInt());
		}
	} else {
		warning("b_constrainV: no score");
	}

	g_lingo->push(Datum(res));
}

void LB::b_copyToClipBoard(int nargs) {
	Datum d = g_lingo->pop();
	g_director->_clipBoard = new CastMemberID(d.asMemberID());
}

void LB::b_duplicate(int nargs) {
	Datum to;
	Datum from;
	Movie *movie = g_director->getCurrentMovie();

	if (nargs >= 2) {
		nargs -= 2;
		g_lingo->dropStack(nargs);
		to = g_lingo->pop();
		from = g_lingo->pop();
		if (from.type != CASTREF)
			error("b_duplicate(): expected CASTREF for from, got %s", from.type2str());
		if (to.type == INT)
			to = Datum(CastMemberID(to.asInt(), DEFAULT_CAST_LIB));
		else if (to.type != CASTREF)
			error("b_duplicate(): expected CASTREF or INT for to, got %s", to.type2str());
	} else if (nargs == 1) {
		// use next available slot in the same cast library
		from = g_lingo->pop();
		if (from.type != CASTREF)
			error("b_duplicate(): expected CASTREF for from, got %s", from.type2str());
		if (!movie->getCasts()->contains(from.u.cast->castLib))
			error("b_duplicate(): couldn't find cast lib %d", from.u.cast->castLib);

		Cast *cast = movie->getCasts()->getVal(from.u.cast->castLib);
		to = Datum(CastMemberID(cast->getNextUnusedID(), from.u.cast->castLib));
	} else {
		error("b_duplicate(): expected at least 1 argument");
	}

	if (!movie->duplicateCastMember(*from.u.cast, *to.u.cast)) {
		warning("b_duplicate(): failed to copy cast member %s to %s", from.u.cast->asString().c_str(), to.u.cast->asString().c_str());
	}

	Score *score = movie->getScore();
	// force redraw any sprites
	score->refreshPointersForCastMemberID(*to.u.cast);
	b_updateStage(0);
	g_lingo->push(Datum(to.u.cast->member));
}

void LB::b_editableText(int nargs) {
	// editableText is deprecated in D4+ with the addition of "the editableText",
	// but is still a valid function call.
	Score *sc = g_director->getCurrentMovie()->getScore();
	if (!sc) {
		warning("b_editableText: no score");
		g_lingo->dropStack(nargs);
		return;
	}

	if (nargs == 2) {
		Datum state = g_lingo->pop();
		Datum sprite = g_lingo->pop();
		if ((uint)sprite.asInt() < sc->_channels.size()) {
			sc->getSpriteById(sprite.asInt())->_editable = state.asInt();
			sc->getOriginalSpriteById(sprite.asInt())->_editable = state.asInt();
		} else {
			warning("b_editableText: sprite index out of bounds");
		}
	} else if (nargs == 0) {
		g_lingo->dropStack(nargs);

		if (g_lingo->_currentChannelId == -1) {
			warning("b_editableText: channel Id is missing");
			return;
		}
		sc->getSpriteById(g_lingo->_currentChannelId)->_editable = true;
		sc->getOriginalSpriteById(g_lingo->_currentChannelId)->_editable = true;
	} else {
		warning("b_editableText: unexpectedly received %d arguments", nargs);
		g_lingo->dropStack(nargs);
	}
}

void LB::b_erase(int nargs) {
	Datum d = g_lingo->pop();
	Movie *movie = g_director->getCurrentMovie();
	CastMember *eraseCast = movie->getCastMember(d.asMemberID());

	if (eraseCast) {
		eraseCast->_erase = true;
		Common::Array<Channel *> channels = movie->getScore()->_channels;

		for (uint i = 0; i < channels.size(); i++) {
			if (channels[i]->_sprite->_castId == d.asMemberID()) {
				channels[i]->_dirty = true;
			}
		}
	}
}

void LB::b_findEmpty(int nargs) {
	Datum d = g_lingo->pop();
	Cast *cast = g_director->getCurrentMovie()->getCast();
	uint16 c_start = cast->_castArrayStart;
	uint16 c_end = cast->_castArrayEnd;

	if (d.type != CASTREF) {
		warning("Incorrect argument type for findEmpty");
		return;
	}

	Datum res;

	if (d.u.cast->member > c_end) {
		res = d.u.cast->member;
		g_lingo->push(res);
		return;
	}

	if (d.u.cast->member > c_start) {
		c_start = (uint16) d.u.cast->member;
	}

	for (uint16 i = c_start; i <= c_end; i++) {
		if (!(cast->getCastMember(i) && cast->getCastMember(i)->_type != kCastTypeNull)) {
			res = i;
			g_lingo->push(res);
			return;
		}
	}

	res = (int) c_end + 1;
	g_lingo->push(res);
}

void LB::b_importFileInto(int nargs) {

	Common::String file = g_lingo->pop().asString();
	Datum dst = g_lingo->pop();

	if (!dst.isCastRef()) {
		warning("b_importFileInto(): bad cast ref field type: %s", dst.type2str());
		return;
	}

	CastMemberID memberID = *dst.u.cast;

	Movie *movie = g_director->getCurrentMovie();
	Score *score = movie->getScore();
	Cast *cast = movie->getCast(memberID);
	if (!cast) {
		return;
	}

	Common::Path path = findPath(file);
	if (path.empty()) {
		warning("b_importFileInto(): couldn't find target file %s", file.c_str());
		return;
	}

	cast->importFileInto(memberID.member, path);

	score->refreshPointersForCastMemberID(dst.asMemberID());
}

void menuCommandsCallback(int action, Common::String &text, void *data) {
	g_director->getCurrentMovie()->queueInputEvent(kEventMenuCallback, action);
}

void LB::b_installMenu(int nargs) {
	// installMenu castNum
	Datum d = g_lingo->pop();

	CastMemberID memberID = d.asMemberID(kCastText);
	if (memberID.member == 0) {
		debugC(3, kDebugLoading, "LB::b_installMenu(): removing menu");
		g_director->_wm->removeMenu();
		return;
	}
	Movie *movie = g_director->getCurrentMovie();
	CastMember *member = movie->getCastMember(memberID);
	if (!member) {
		g_lingo->lingoError("installMenu: Unknown %s", memberID.asString().c_str());
		return;
	}
	if (member->_type != kCastText) {
		g_lingo->lingoError("installMenu: %s is not a field", memberID.asString().c_str());
		return;
	}
	TextCastMember *field = static_cast<TextCastMember *>(member);

	Common::String menuStxt = field->getRawText();
	// clang reports linenum variable is unused
	// int linenum = -1; // We increment it before processing

	Graphics::MacMenu *menu = g_director->_wm->addMenu();
	int submenu = -1;
	Common::String submenuText;
	Common::String command;
	int commandId = 100;

	menu->setCommandsCallback(menuCommandsCallback, g_director);

	debugC(3, kDebugLoading, "LB::b_installMenu(): installing menu - '%s'", Common::toPrintable(menuStxt).c_str());

	LingoArchive *mainArchive = movie->getMainLingoArch();

	// STXT sections use Mac-style carriage returns for line breaks.
	const char LINE_BREAK_CHAR = '\x0D';
	// Menu definitions use the character 0xc5 to denote a code separator.
	// For Mac D4 and below, this is ≈. For Windows D4 and below, this is Å.
	char CODE_SEPARATOR_CHAR = '\xC5';
	char CODE_SEPARATOR_CHAR_2 = '\xC5';
	if (g_director->getVersion() >= 500) {
		// D5 changed this to be the pipe | character, the same in Windows and Mac.
		CODE_SEPARATOR_CHAR = '\x7C';
		// FIXME: For some reason there are games which use º (Mac) or ¼ (Win) and it works too?
		CODE_SEPARATOR_CHAR_2 = '\xBC';
	}
	// Continuation character is 0xac to denote a line running over.
	// For Mac, this is ¨. For Windows, this is ¬.
	const char CONTINUATION_CHAR = '\xAC';
	// Menu definitions use the character 0xc3 to denote a checkmark.
	// For Mac, this is √. For Windows, this is Ã.
	// This is used by MacMenu::createSubMenuFromString.

	Common::String line;

	for (auto it = menuStxt.begin(); it != menuStxt.end(); ++it) {
		line.clear();
		while (it != menuStxt.end() && *it != LINE_BREAK_CHAR) {
			if (*it == '-') {
				it++;
				if (it != menuStxt.end() && *it == '-') { // rest of the line is a comment
					while (it != menuStxt.end() && *it != LINE_BREAK_CHAR) {
						it++;
					}
					break;
				}
				line += '-';
			} else if (*it == CONTINUATION_CHAR) { // fast forward to the next line
				it++;
				if (*it == LINE_BREAK_CHAR) {
					line += ' ';
					it++;
				}
			} else if (*it == LINE_BREAK_CHAR) {
				break;
			} else {
				line += *it++;
			}
		}
		// clang reports linenum variable is unused
		// linenum++;

		if (line.empty())
			continue;

		if (line.hasPrefixIgnoreCase("menu:")) {
			const char *p = &line.c_str()[5];

			while (*p && (*p == ' ' || *p == '\t'))
				p++;

			if (!submenuText.empty()) { // Adding submenu for previous menu
				menu->createSubMenuFromString(submenu, submenuText.c_str(), 0);
			}

			if (!strcmp(p, "@"))
				p = "\xf0";	// Apple symbol

			submenu = menu->addMenuItem(nullptr, Common::String(p));

			submenuText.clear();

			continue;
		}

		// Split the line at the code separator
		size_t sepOffset = line.find(CODE_SEPARATOR_CHAR);
		if (sepOffset == Common::String::npos)
			sepOffset = line.find(CODE_SEPARATOR_CHAR_2);

		Common::String text;

		if (sepOffset != Common::String::npos) {
			text = Common::String(line.c_str(), line.c_str() + sepOffset);
			command = Common::String(line.c_str() + sepOffset + 1);
		} else {
			text = line;
			command = "";
		}

		text.trim();
		command.trim();

		submenuText += text;

		if (!submenuText.empty()) {
			if (!command.empty()) {
				while (mainArchive->getScriptContext(kEventScript, commandId)) {
					commandId++;
				}
				mainArchive->replaceCode(command.decode(Common::kMacRoman), kEventScript, commandId);
				submenuText += Common::String::format("[%d];", commandId);
			} else {
				submenuText += ';';
			}
		}

		if (it == menuStxt.end()) // if we reached end of string, do not increment it but break
			break;
	}

	if (!submenuText.empty()) {
		menu->createSubMenuFromString(submenu, submenuText.c_str(), 0);
	}
}

void LB::b_label(int nargs) {
	// label functions as marker when the input is an int
	Datum d = g_lingo->pop();
	int marker;
	if (d.type == STRING) {
		marker = g_lingo->func_label(d);
	} else {
		marker = g_lingo->func_marker(d.asInt());
	}
	g_lingo->push(marker);
}

void LB::b_marker(int nargs) {
	// marker functions as label when the input is a string
	Datum d = g_lingo->pop();
	int marker;
	if (d.type == STRING) {
		marker = g_lingo->func_label(d);
	} else {
		marker = g_lingo->func_marker(d.asInt());
	}
	g_lingo->push(marker);
}

void LB::b_move(int nargs) {
	Datum src, dest;

	if (nargs == 1) {
		int id = (int) g_director->getCurrentMovie()->getCast()->_castArrayStart;
		CastMemberID castId(id, DEFAULT_CAST_LIB);
		Datum d = Datum(castId);
		g_lingo->push(d);
		b_findEmpty(1);
	} else if (nargs == 2) {
		// pass
	} else {
		ARGNUMCHECK(2);
		g_lingo->dropStack(nargs);
		return;
	}
	dest = g_lingo->pop();
	src = g_lingo->pop();

	// Convert dest datum to type CASTREF if it is INT.
	// Confirmed to always move to DEFAULT_CAST_LIB in D5
	if (dest.type == INT) {
		dest = Datum(CastMemberID(dest.asInt(), DEFAULT_CAST_LIB));
	}

	// No need to move if src and dest are same
	if (src == dest) {
		return;
	}

	Movie *movie = g_director->getCurrentMovie();
	CastMember *toMove = movie->getCastMember(src.asMemberID());

	if (!toMove) {
		warning("b_move: Source CastMember doesn't exist");
		return;
	}

	g_lingo->push(dest);
	Score *score = movie->getScore();
	uint16 frame = score->getCurrentFrameNum();

	score->renderFrame(frame, kRenderForceUpdate);

	movie->duplicateCastMember(src.asMemberID(), dest.asMemberID());
	movie->eraseCastMember(src.asMemberID());

	score->refreshPointersForCastMemberID(dest.asMemberID());
	score->refreshPointersForCastMemberID(src.asMemberID());

	score->renderFrame(frame, kRenderForceUpdate);
}

void LB::b_moveableSprite(int nargs) {
	Movie *movie = g_director->getCurrentMovie();
	Score *score = movie->getScore();
	Frame *frame = score->_currentFrame;

	if (g_lingo->_currentChannelId == -1) {
		warning("b_moveableSprite: channel Id is missing");
		assert(0);
		return;
	}

	// since we are using value copying, in order to make it taking effect immediately. we modify the sprites in channel
	if (score->_channels[g_lingo->_currentChannelId])
		score->_channels[g_lingo->_currentChannelId]->_sprite->_moveable = true;
	frame->_sprites[g_lingo->_currentChannelId]->_moveable = true;
}

void LB::b_pasteClipBoardInto(int nargs) {
	Datum to = g_lingo->pop();
	if (!g_director->_clipBoard) {
		warning("LB::b_pasteClipBoardInto(): Nothing to paste from clipboard, skipping paste..");
		return;
	}

	Movie *movie = g_director->getCurrentMovie();

	CastMember *castMember = movie->getCastMember(*g_director->_clipBoard);
	if (!castMember) {
		warning("LB:B_pasteClipBoardInto(): castMember not found");
		return;
	}

	Score *score = movie->getScore();
	castMember->setModified(true);
	movie->duplicateCastMember(*g_director->_clipBoard, *to.u.cast);
	score->refreshPointersForCastMemberID(to.asMemberID());
}

static const struct PaletteNames {
	const char *name;
	PaletteType type;
} paletteNames[] = {
	{ "System", kClutSystemMac },
	{ "System - Mac", kClutSystemMac },
	{ "Rainbow", kClutRainbow },
	{ "Grayscale", kClutGrayscale },
	{ "Pastels", kClutPastels },
	{ "Vivid", kClutVivid },
	{ "NTSC", kClutNTSC },
	{ "Metallic", kClutMetallic },
	//{ "Web 216", },
	//{ "VGA", },
	{ "System - Win", kClutSystemWinD5 },
	{ "SYSTEM - WIN (DIR 4)", kClutSystemWin },

	// Japanese palette names.
	// TODO: Check encoding. Original is SJIS
	{ "\x83V\x83X\x83""e\x83\x80 - Mac", kClutSystemMac },				// システム - Mac
	{ "\x83\x8C>\x83""C\x83\x93\x83{\x81[", kClutRainbow },				// レインボー
	{ "\x83O\x83>\x8C\x81[\x83X\x83P\x81[\x83\x8B", kClutGrayscale },	// グレースケール
	{ "\x83p\x83>X\x83""e\x83\x8B", kClutPastels },						// パステル
	{ "\x83r\x83>r\x83""b\x83h", kClutVivid },							// ビビッド
	{ "\x83\x81\x83^\x83\x8A\x83""b\x83N", kClutMetallic },				// メタリック
	{ "\x83V\x83X\x83""e\x83\x80 - Win", kClutSystemWinD5 },							// システム - Win
	{ "\x83V\x83X\x83""e\x83\x80 - Win (Dir 4)", kClutSystemWin },		// システム - Win (Dir 4)
};

void LB::b_puppetPalette(int nargs) {
	g_lingo->convertVOIDtoString(0, nargs);
	int numFrames = 0, speed = 0;
	CastMemberID palette(0, 0);
	Datum d;
	Movie *movie = g_director->getCurrentMovie();

	switch (nargs) {
	case 3:
		numFrames = g_lingo->pop().asInt();
		// fall through
	case 2:
		speed = g_lingo->pop().asInt();
		// fall through
	case 1:
		d = g_lingo->pop();

		if (d.type == STRING) {
			// TODO: It seems that there are not strings for Mac and Win system palette
			Common::String palStr = d.asString();

			for (int i = 0; i < ARRAYSIZE(paletteNames); i++) {
				if (palStr.equalsIgnoreCase(paletteNames[i].name)) {
					palette = CastMemberID(paletteNames[i].type, -1);
					if (g_director->getVersion() < 500 && paletteNames[i].type == kClutSystemWinD5)
						palette.member = kClutSystemWin;
					break;
				}
			}

		}
		if (palette.isNull()) {
			CastMember *member = movie->getCastMember(d.asMemberID());

			if (member && member->_type == kCastPalette)
				palette = ((PaletteCastMember *)member)->getPaletteId();
		}
		break;
	default:
		ARGNUMCHECK(1);
		return;
	}

	Score *score = movie->getScore();
	if (!palette.isNull()) {
		g_director->setPalette(palette);
		score->_puppetPalette = true;
	} else {
		// Setting puppetPalette to 0 disables it (Lingo Dictionary, 226)

		score->_puppetPalette = false;

		// FIXME: set system palette decided by platform, should be fixed after windows palette is working.
		// try to set mac system palette if lastPalette is 0.
		if (g_director->_lastPalette.isNull())
			g_director->setPalette(CastMemberID(kClutSystemMac, -1));
		else
			g_director->setPalette(g_director->_lastPalette);
	}

	// TODO: Implement advanced features that use these.
	if (numFrames || speed)
		warning("b_puppetPalette: Skipping extra features");
}

void LB::b_puppetSound(int nargs) {

	if (nargs < 1 || nargs >= 3) {
		warning("b_puppetSound(): needs 1 or 2 args, got %d", nargs);
		if (nargs < 1)
			return;
		g_lingo->dropStack(nargs - 2);
	}

	// TODO
	// Midi variant is similar to playAccel
	//
	// and contains these commands:
	//
	// midiBeat
	// midiContinue
	// midiSong
	// midiSongpointer
	// midiStart
	// midiStop

	DirectorSound *sound = g_director->getCurrentWindow()->getSoundManager();
	Score *score = g_director->getCurrentMovie()->getScore();

	if (!score) {
		warning("b_puppetSound(): no score");
		return;
	}

	// Most variants of puppetSound don't actually play the sound
	// until the playback head moves or updateStage is called.
	// So we'll just queue it to be played later.

	if (nargs == 1) {
		CastMemberID castMember = g_lingo->pop().asMemberID(kCastSound);

		// in D2 manual p206, puppetSound 0 will turn off the puppet status of sound
		sound->setPuppetSound(castMember, 1);
	} else {
		if (g_director->getVersion() < 400) {
			// in D2/3/3.1 interactivity manual, 2 args represent the menu and submenu sounds
			int submenu = g_lingo->pop().asInt();
			int menu = g_lingo->pop().asInt();

			if (menu < kMinSampledMenu || menu > kMaxSampledMenu)
				warning("LB::puppetSound: menu number is not available");

			sound->setPuppetSound(SoundID(kSoundExternal, menu, submenu), 1);
		} else {
			// Two-argument puppetSound is undocumented in D4.
			// It is however documented in the D5 Lingo dictionary.
			Datum arg2 = g_lingo->pop();
			Datum arg1 = g_lingo->pop();
			int channel = 1;
			CastMemberID castMember;
			if (arg1.type == STRING) {
				// Apparently if the first argument is a string, it will be evaluated as per the 1-arg case
				castMember = arg1.asMemberID(kCastSound);
			} else {
				// FIXME: Figure out how to deal with multilib in D5+
				castMember = arg2.asMemberID(kCastSound);
				channel = arg1.asInt();
			}
			sound->setPuppetSound(castMember, channel);

			// The D4 two-arg variant of puppetSound plays
			// immediately for some inexplicable reason.
			sound->playPuppetSound(channel);
		}
	}
}

void LB::b_immediateSprite(int nargs) {
	Score *sc = g_director->getCurrentMovie()->getScore();
	if (!sc) {
		warning("b_immediateSprite: no score");
		g_lingo->dropStack(nargs);
		return;
	}

	if (nargs == 2) {
		Datum state = g_lingo->pop();
		Datum sprite = g_lingo->pop();

		if ((uint)sprite.asInt() < sc->_channels.size()) {
			sc->getSpriteById(sprite.asInt())->_immediate = (bool)state.asInt();
		} else {
			warning("b_immediateSprite: sprite index out of bounds");
		}
	} else if (nargs == 0 && g_director->getVersion() < 400) {
		g_lingo->dropStack(nargs);

		if (g_lingo->_currentChannelId == -1) {
			warning("b_immediateSprite: channel Id is missing");
			return;
		}
		sc->getSpriteById(g_lingo->_currentChannelId)->_immediate = true;
	} else {
		warning("b_immediateSprite: unexpectedly received %d arguments", nargs);
		g_lingo->dropStack(nargs);
	}
}

void LB::b_puppetSprite(int nargs) {
	Score *sc = g_director->getCurrentMovie()->getScore();
	if (!sc) {
		warning("b_puppetSprite: no score");
		g_lingo->dropStack(nargs);
		return;
	}

	if (nargs == 2) {
		Datum state = g_lingo->pop();
		Datum sprite = g_lingo->pop();

		if ((uint)sprite.asInt() < sc->_channels.size()) {
			 sc->getSpriteById(sprite.asInt())->_puppet = (bool)state.asInt();
		} else {
			warning("b_puppetSprite: sprite index out of bounds");
		}
	} else if (nargs == 0 && g_director->getVersion() < 400) {
		g_lingo->dropStack(nargs);

		if (g_lingo->_currentChannelId == -1) {
			warning("b_puppetSprite: channel Id is missing");
			return;
		}
		sc->getSpriteById(g_lingo->_currentChannelId)->_puppet = true;
	} else {
		warning("b_puppetSprite: unexpectedly received %d arguments", nargs);
		g_lingo->dropStack(nargs);
	}
}

void LB::b_puppetTempo(int nargs) {
	g_director->getCurrentMovie()->getScore()->setPuppetTempo(g_lingo->pop().asInt());
}

void LB::b_puppetTransition(int nargs) {
	// puppetTransition whichTransition [, time] [, chunkSize] [, changeArea]
	Window *stage = g_director->getCurrentWindow();
	uint16 duration = 250, area = 1, chunkSize = 1, type = 0;

	TransitionCastMember *tcast = nullptr;

	switch (nargs) {
	case 4:
		area = g_lingo->pop().asInt();
		// fall through
	case 3:
		chunkSize = g_lingo->pop().asInt();
		// fall through
	case 2:
		// units are quarter-seconds
		duration = g_lingo->pop().asInt() * 250;
		// fall through
	case 1:
		{
			Datum d = g_lingo->pop();
			if (d.type == CASTREF) {
				CastMemberID castId = d.asMemberID();
				CastMember *cast = stage->getCurrentMovie()->getCastMember(castId);
				if (cast->_type == kCastTransition) {
					tcast = (TransitionCastMember *)cast;
				} else {
					warning("b_puppetTransition: expected transition cast member");
					return;
				}
			} else {
				type = ((TransitionType)(d.asInt()));
			}
		}
		break;
	default:
		ARGNUMCHECK(1);
		g_lingo->dropStack(nargs);
		return;
	}

	if (stage->_puppetTransition) {
		warning("b_puppetTransition: Transition already queued");
		delete stage->_puppetTransition;
		stage->_puppetTransition = nullptr;
	}
	if (tcast) {
		stage->_puppetTransition = new TransParams(tcast->_durationMillis, tcast->_area, tcast->_chunkSize, tcast->_transType);
	} else {
		stage->_puppetTransition = new TransParams(duration, area, chunkSize, ((TransitionType)type));
	}
	debugC(3, kDebugImages, "b_puppetTransition(): type: %d, duration: %d, chunkSize: %d, area: %d",
			stage->_puppetTransition->type, stage->_puppetTransition->duration, stage->_puppetTransition->chunkSize, stage->_puppetTransition->area);

}

void LB::b_ramNeeded(int nargs) {
	Datum d = g_lingo->pop();
	Datum d2 = g_lingo->pop();

	// We do not need RAM, we have it all

	g_lingo->push(Datum(0));
}

void LB::b_rollOver(int nargs) {
	Datum d = g_lingo->pop();
	Datum res(0);
	int arg = 0;
	if (d.type == SPRITEREF) {
		arg = d.u.i;
	} else {
		arg = d.asInt();
	}

	Score *score = g_director->getCurrentMovie()->getScore();

	if (!score) {
		warning("b_rollOver: Reference to an empty score");
		return;
	}

	if (arg >= (int32) score->_channels.size()) {
		g_lingo->push(res);
		return;
	}

	Common::Point pos = g_director->getCurrentWindow()->getMousePos();

	if (score->checkSpriteRollOver(arg, pos))
		res.u.i = 1; // TRUE

	g_lingo->push(res);
}

void LB::b_sendAllSprites(int nargs) {
	g_lingo->printSTUBWithArglist("b_sendAllSprites", nargs);
	g_lingo->dropStack(nargs);
}

void LB::b_sendSprite(int nargs) {
	g_lingo->printSTUBWithArglist("b_sendSprite", nargs);
	g_lingo->dropStack(nargs);
}

void LB::b_spriteBox(int nargs) {
	int b = g_lingo->pop().asInt();
	int r = g_lingo->pop().asInt();
	int t = g_lingo->pop().asInt();
	int l = g_lingo->pop().asInt();
	int spriteId = g_lingo->pop().asInt();
	Channel *channel = g_director->getCurrentMovie()->getScore()->getChannelById(spriteId);

	if (!channel)
		return;

	// This automatically sets the stretch mode
	channel->_sprite->_stretch = true;

	g_director->getCurrentWindow()->addDirtyRect(channel->getBbox());
	channel->setBbox(
		l < r ? l : r,
		t < b ? t : b,
		r > l ? r : l,
		b > t ? b : t
	);
	if (channel->_sprite->_cast)
		channel->_sprite->_cast->setModified(true);
	channel->_dirty = true;
}

void LB::b_unLoad(int nargs) {
	// No op for us, we do not unload casts

	g_lingo->dropStack(nargs);
}

void LB::b_unLoadCast(int nargs) {
	// No op for us, we do not unload casts

	g_lingo->dropStack(nargs);
}

void LB::b_unLoadMovie(int nargs) {
	g_lingo->printSTUBWithArglist("b_unLoadMovie", nargs);
	g_lingo->dropStack(nargs);
}

void LB::b_zoomBox(int nargs) {
	// zoomBox startSprite, endSprite [, delatTicks]
	//   ticks are in 1/60th, default 1
	if (nargs < 2 || nargs > 3) {
		warning("b_zoomBox: expected 2 or 3 arguments, got %d", nargs);

		g_lingo->dropStack(nargs);

		return;
	}

	int delayTicks = 1;
	if (nargs > 2) {
		Datum d = g_lingo->pop();
		delayTicks = d.asInt();
	}

	int endSpriteId = g_lingo->pop().asInt();
	int startSpriteId = g_lingo->pop().asInt();

	Score *score = g_director->getCurrentMovie()->getScore();
	uint16 curFrame = score->getCurrentFrameNum();

	Common::Rect startRect = score->_channels[startSpriteId]->getBbox();
	if (startRect.isEmpty()) {
		warning("b_zoomBox: unknown start sprite #%d", startSpriteId);
		return;
	}

	// Looks for endSprite in the current frame, otherwise
	// Looks for endSprite in the next frame
	Common::Rect endRect = score->_channels[endSpriteId]->getBbox();
	if (endRect.isEmpty()) {
		if ((uint)curFrame + 1 < score->getFramesNum()) {
			Frame *nextFrame = score->getFrameData(curFrame + 1);
			Channel endChannel(nullptr, nextFrame->_sprites[endSpriteId]);
			endRect = endChannel.getBbox();
			delete nextFrame;
		}
	}

	if (endRect.isEmpty()) {
		if ((uint)curFrame - 1 > 0) {
			Frame *prevFrame = score->getFrameData(curFrame - 1);
			Channel endChannel(nullptr, prevFrame->_sprites[endSpriteId]);
			endRect = endChannel.getBbox();
			delete prevFrame;
		}
	}

	if (endRect.isEmpty()) {
		warning("b_zoomBox: unknown end sprite #%d", endSpriteId);
		return;
	}

	if (Director::g_director->desktopEnabled()) {
		Director::Datum stageRect = Director::g_director->getStage()->getStageRect();
		Common::Point stageOffset(stageRect.u.farr->arr[0].asInt(), stageRect.u.farr->arr[1].asInt());
		startRect.translate(stageOffset.x, stageOffset.y);
		endRect.translate(stageOffset.x, stageOffset.y);
	}

	Graphics::ZoomBox *box = new Graphics::ZoomBox;
	box->start = startRect;
	box->end = endRect;
	box->delay = delayTicks;
	box->step = 0;
	box->startTime = g_system->getMillis();
	box->nextTime  = g_system->getMillis() + 1000 * box->step / 60;

	g_director->_wm->addZoomBox(box);
}

void LB::b_updateStage(int nargs) {
	if (g_director->getGameGID() == GID_TEST) {
		warning("b_updateStage: Skipping due to tests");

		return;
	}

	Movie *movie = g_director->getCurrentMovie();
	if (!movie) {
		warning("b_updateStage: no movie");

		return;
	}

	Score *score = movie->getScore();
	Window *window = movie->getWindow();

	if (score->_disableGoPlayUpdateStage) {
		warning("Lingo::b_updateStage(): ignoring updateStage due to disableGoPlayUpdateStage flag");
		return;
	}

	score->updateWidgets(movie->_videoPlayback);
	if (window->_puppetTransition) {
		window->playTransition(score->getCurrentFrameNum(), kRenderModeNormal, window->_puppetTransition->duration, window->_puppetTransition->area, window->_puppetTransition->chunkSize, window->_puppetTransition->type, score->_currentFrame->_mainChannels.scoreCachedPaletteId);
		delete window->_puppetTransition;
		window->_puppetTransition = nullptr;
	} else {
		movie->getWindow()->render();
	}

	// play any puppet sounds that have been queued
	score->playSoundChannel(true);

	if (score->_cursorDirty) {
		score->renderCursor(movie->getWindow()->getMousePos());
		score->_cursorDirty = false;
	}

	g_director->draw();

	if (debugChannelSet(-1, kDebugFewFramesOnly)) {
		g_director->_framesRan++;
		warning("LB::b_updateStage(): ran frame %0d", g_director->_framesRan);

		if (g_director->_framesRan > kFewFamesMaxCounter) {
			warning("b_updateStage(): exiting due to debug few frames only");
			score->_playState = kPlayStopped;
		}
	}
}

///////////////////
// Score recording
///////////////////

void LB::b_clearFrame(int nargs) {
	g_lingo->printSTUBWithArglist("b_clearFrame", nargs);
	g_lingo->dropStack(nargs);
}

void LB::b_deleteFrame(int nargs) {
	g_lingo->printSTUBWithArglist("b_deleteFrame", nargs);
	g_lingo->dropStack(nargs);
}

void LB::b_duplicateFrame(int nargs) {
	g_lingo->printSTUBWithArglist("b_duplicateFrame", nargs);
	g_lingo->dropStack(nargs);
}

void LB::b_insertFrame(int nargs) {
	g_lingo->printSTUBWithArglist("b_insertFrame", nargs);
	g_lingo->dropStack(nargs);
}

void LB::b_updateFrame(int nargs) {
	g_lingo->printSTUBWithArglist("b_updateFrame", nargs);
	g_lingo->dropStack(nargs);
}

///////////////////
// Point
///////////////////
void LB::b_point(int nargs) {
	Datum y(g_lingo->pop().asInt());
	Datum x(g_lingo->pop().asInt());
	Datum d;

	d.u.farr = new FArray;

	d.u.farr->arr.push_back(x);
	d.u.farr->arr.push_back(y);
	d.type = POINT;

	g_lingo->push(d);
}

void LB::b_rect(int nargs) {
	Datum d(0);

	if (nargs == 4) {
		Datum bottom(g_lingo->pop().asInt());
		Datum right(g_lingo->pop().asInt());
		Datum top(g_lingo->pop().asInt());
		Datum left(g_lingo->pop().asInt());

		d.u.farr = new FArray;
		d.u.farr->arr.push_back(left);
		d.u.farr->arr.push_back(top);
		d.u.farr->arr.push_back(right);
		d.u.farr->arr.push_back(bottom);
		d.type = RECT;
	} else if (nargs == 2) {
		Datum p2 = g_lingo->pop();
		Datum p1 = g_lingo->pop();

		if (p2.type == POINT && p1.type == POINT) {
			d.u.farr = new FArray;
			d.u.farr->arr.push_back(p1.u.farr->arr[0]);
			d.u.farr->arr.push_back(p1.u.farr->arr[1]);
			d.u.farr->arr.push_back(p2.u.farr->arr[0]);
			d.u.farr->arr.push_back(p2.u.farr->arr[1]);
			d.type = RECT;
		} else
			warning("LB::b_rect: Rect need 2 Point variable as argument");

	} else {
		warning("LB::b_rect: Rect doesn't support %d args", nargs);
		g_lingo->dropStack(nargs);
	}

	g_lingo->push(d);
}


void LB::b_intersect(int nargs) {
	Datum d;
	Datum r2 = g_lingo->pop();
	Datum r1 = g_lingo->pop();
	TYPECHECK(r1, RECT);
	TYPECHECK(r2, RECT);
	Common::Rect rect1(r1.u.farr->arr[0].asInt(), r1.u.farr->arr[1].asInt(), r1.u.farr->arr[2].asInt(), r1.u.farr->arr[3].asInt());
	Common::Rect rect2(r2.u.farr->arr[0].asInt(), r2.u.farr->arr[1].asInt(), r2.u.farr->arr[2].asInt(), r2.u.farr->arr[3].asInt());

	d = rect1.intersects(rect2);

	g_lingo->push(d);
}

void LB::b_inflate(int nargs) {
	int inflateHeight = g_lingo->pop().asInt();
	int inflateWidth = g_lingo->pop().asInt();
	Datum sR = g_lingo->pop();

	TYPECHECK(sR, RECT);

	Common::Rect sourceRect(sR.u.farr->arr[0].asInt(),
		                    sR.u.farr->arr[1].asInt(),
		                    sR.u.farr->arr[2].asInt(),
		                    sR.u.farr->arr[3].asInt());

	int inflatedLeft = sourceRect.left - inflateWidth;
	int inflatedTop = sourceRect.top - inflateHeight;
	int inflatedRight = sourceRect.right + inflateWidth;
	int inflatedBottom = sourceRect.bottom + inflateHeight;

	Datum inflatedRect;
	inflatedRect.type = RECT;

	inflatedRect.u.farr = new FArray();
	inflatedRect.u.farr->arr.push_back(Datum(inflatedLeft));
	inflatedRect.u.farr->arr.push_back(Datum(inflatedTop));
	inflatedRect.u.farr->arr.push_back(Datum(inflatedRight));
	inflatedRect.u.farr->arr.push_back(Datum(inflatedBottom));

	g_lingo->push(inflatedRect);
}

void LB::b_inside(int nargs) {
	Datum d;
	Datum r2 = g_lingo->pop();
	Datum p1 = g_lingo->pop();
	TYPECHECK(r2, RECT);
	TYPECHECK(p1, POINT);

	Common::Rect rect2(r2.u.farr->arr[0].asInt(), r2.u.farr->arr[1].asInt(), r2.u.farr->arr[2].asInt(), r2.u.farr->arr[3].asInt());
	Common::Point point1(p1.u.farr->arr[0].asInt(), p1.u.farr->arr[1].asInt());

	d = rect2.contains(point1);

	g_lingo->push(d);
}

void LB::b_map(int nargs) {
	Datum toRect = g_lingo->pop();
	Datum fromRect = g_lingo->pop();
	Datum srcArr = g_lingo->pop();

	if (!(toRect.type == RECT || (toRect.type == ARRAY && toRect.u.farr->arr.size() == 4)) ||
		!(fromRect.type == RECT || (fromRect.type == ARRAY && fromRect.u.farr->arr.size() == 4))) {
		warning("LB::b_map(): Invalid Datum Type of source and destination Rects");
		return;
	}

	int toWidth = toRect.u.farr->arr[2].u.i - toRect.u.farr->arr[0].u.i;
	int toHeight = toRect.u.farr->arr[3].u.i - toRect.u.farr->arr[1].u.i;
	int fromWidth = fromRect.u.farr->arr[2].u.i - fromRect.u.farr->arr[0].u.i;
	int fromHeight = fromRect.u.farr->arr[3].u.i - fromRect.u.farr->arr[1].u.i;

	if (!(srcArr.type == POINT ||
		srcArr.type == RECT ||
		(srcArr.type == ARRAY && (srcArr.u.farr->arr.size() == 2 || srcArr.u.farr->arr.size() == 4)))) {
		warning("LB::b_map(): Invalid Datum type of input Point / Rect");
		return;
	}

	Datum d;
	d.type = POINT;
	d.u.farr = new FArray();
	d.u.farr->arr.push_back((srcArr.u.farr->arr[0].u.i - fromRect.u.farr->arr[0].u.i) * (toWidth / fromWidth) + toRect.u.farr->arr[0].u.i);
	d.u.farr->arr.push_back((srcArr.u.farr->arr[1].u.i - fromRect.u.farr->arr[1].u.i) * (toHeight / fromHeight) + toRect.u.farr->arr[1].u.i);

	if (srcArr.type == RECT ||
		(srcArr.type == ARRAY && srcArr.u.farr->arr.size() == 4)) {
		d.type = RECT;
		d.u.farr->arr.push_back((srcArr.u.farr->arr[2].u.i - srcArr.u.farr->arr[0].u.i) * (toWidth / fromWidth) + d.u.farr->arr[0].u.i);
		d.u.farr->arr.push_back((srcArr.u.farr->arr[3].u.i - srcArr.u.farr->arr[1].u.i) * (toWidth / fromWidth) + d.u.farr->arr[1].u.i);
	}

	g_lingo->push(d);
}

void LB::b_offsetRect(int nargs) {
	Datum vert = g_lingo->pop();
	Datum hori = g_lingo->pop();
	Datum rect = g_lingo->pop();

	if (vert.type != INT ||
		hori.type != INT ||
		!(rect.type == RECT || (rect.type == ARRAY && rect.u.farr->arr.size() >= 4))) {
		warning(" LB::b_offsetRect(): Invalid DatumType of inputs");
		g_lingo->push(rect);
	}

	//When vert is positive, rect moves higher
	//When hori is positive, rect moves towards right

	rect.u.farr->arr[0].u.i += hori.u.i;
	rect.u.farr->arr[2].u.i += hori.u.i;
	rect.u.farr->arr[1].u.i -= vert.u.i;
	rect.u.farr->arr[3].u.i -= vert.u.i;


	g_lingo->push(rect);
}

void LB::b_union(int nargs) {
	if (nargs != 2) {
		warning("Wrong number of arguments for b_union: Expected 2, got %d", nargs);
		g_lingo->dropStack(nargs);
		g_lingo->push(Datum(0));
		return;
	}

	Datum rect1 = g_lingo->pop();
	Datum rect2 = g_lingo->pop();

	if (rect1.type != RECT || rect2.type != RECT) {
		warning("Wrong type of arguments for b_union");
		g_lingo->push(Datum(0));
		return;
	}

	Datum res;
	res.type = RECT;

	res.u.farr = new FArray();
	res.u.farr->arr.push_back(Datum(MIN(rect1.u.farr->arr[0].u.i, rect2.u.farr->arr[0].u.i)));
	res.u.farr->arr.push_back(Datum(MIN(rect1.u.farr->arr[1].u.i, rect2.u.farr->arr[1].u.i)));
	res.u.farr->arr.push_back(Datum(MAX(rect1.u.farr->arr[2].u.i, rect2.u.farr->arr[2].u.i)));
	res.u.farr->arr.push_back(Datum(MAX(rect1.u.farr->arr[3].u.i, rect2.u.farr->arr[3].u.i)));

	g_lingo->push(res);
}


///////////////////
// Sound
///////////////////
void LB::b_beep(int nargs) {
	int repeat = 1;
	if (nargs == 1) {
		Datum d = g_lingo->pop();
		repeat = d.u.i;
	}
	g_lingo->func_beep(repeat);
}

void LB::b_isPastCuePoint(int nargs) {
	g_lingo->printSTUBWithArglist("b_isPastCuePoint", nargs);
	g_lingo->dropStack(nargs);
	g_lingo->push(Datum());
}

void LB::b_mci(int nargs) {
	Datum d = g_lingo->pop();

	g_lingo->func_mci(d.asString());
}

void LB::b_mciwait(int nargs) {
	Datum d = g_lingo->pop();

	g_lingo->func_mciwait(d.asString());
}

void LB::b_sound(int nargs) {
	// Builtin function for sound as used by the Director bytecode engine.
	//
	// Accepted arguments:
	// "close", INT soundChannel
	// "fadeIn", INT soundChannel(, INT ticks)
	// "fadeOut", INT soundChannel(, INT ticks)
	// "playFile", INT soundChannel, STRING fileName
	// "stop", INT soundChannel

	if (nargs < 2 || nargs > 3) {
		warning("b_sound: expected 2 or 3 args, not %d", nargs);
		g_lingo->dropStack(nargs);

		return;
	}

	int ticks;
	Datum secondArg = g_lingo->pop();
	Datum firstArg = g_lingo->pop();
	Datum verb;
	if (nargs > 2) {
		verb = g_lingo->pop();
	} else {
		verb = firstArg;
		firstArg = secondArg;
	}

	if (verb.type != STRING && verb.type != SYMBOL) {
		warning("b_sound: verb arg should be of type STRING, not %s", verb.type2str());
		return;
	}

	DirectorSound *soundManager = g_director->getCurrentWindow()->getSoundManager();
	Score *score = g_director->getCurrentMovie()->getScore();

	if (verb.u.s->equalsIgnoreCase("close") || verb.u.s->equalsIgnoreCase("stop")) {
		if (nargs != 2) {
			warning("sound %s: expected 1 argument, got %d", verb.u.s->c_str(), nargs - 1);
			return;
		}

		TYPECHECK(firstArg, INT);
		soundManager->stopSound(firstArg.u.i);
	} else if (verb.u.s->equalsIgnoreCase("fadeIn")) {
		if (nargs > 2) {
			TYPECHECK2(secondArg, INT, FLOAT);
			ticks = secondArg.asInt();
		} else {
			ticks = 15 * (60 / score->_currentFrameRate);
		}

		TYPECHECK(firstArg, INT);
		soundManager->registerFade(firstArg.u.i, true, ticks);
		score->_activeFade = true;
		return;
	} else if (verb.u.s->equalsIgnoreCase("fadeOut")) {
		if (nargs > 2) {
			TYPECHECK2(secondArg, INT, FLOAT);
			ticks = secondArg.asInt();
		} else {
			ticks = 15 * (60 / score->_currentFrameRate);
		}

		TYPECHECK2(firstArg, INT, FLOAT);
		soundManager->registerFade(firstArg.asInt(), false, ticks);
		score->_activeFade = true;
		return;
	} else if (verb.u.s->equalsIgnoreCase("playFile")) {
		ARGNUMCHECK(3)

		TYPECHECK(firstArg, INT);
		TYPECHECK(secondArg, STRING);

		soundManager->playFile(*secondArg.u.s, firstArg.u.i);
	} else {
		warning("b_sound: unknown verb %s", verb.u.s->c_str());
	}
}

void LB::b_soundBusy(int nargs) {
	DirectorSound *sound = g_director->getCurrentWindow()->getSoundManager();
	Datum whichChannel = g_lingo->pop();

	// Horror Tour 2 calls this with a void argument
	TYPECHECK2(whichChannel, INT, VOID);
	int channel = whichChannel.u.i;
	if (whichChannel.type == VOID) {
		channel = 1;
	}

	bool isBusy = sound->isChannelActive(channel);
	Datum result;
	result.type = INT;
	result.u.i = isBusy ? 1 : 0;
	g_lingo->push(result);
}

///////////////////
// Constants
///////////////////
void LB::b_backspace(int nargs) {
	g_lingo->push(Datum(Common::String("\b")));
}

void LB::b_empty(int nargs) {
	g_lingo->push(Datum(Common::String("")));
}

void LB::b_enter(int nargs) {
	g_lingo->push(Datum(Common::String("\03")));
}

void LB::b_false(int nargs) {
	g_lingo->push(Datum(0));
}

void LB::b_quote(int nargs) {
	g_lingo->push(Datum(Common::String("\"")));
}

void LB::b_returnconst(int nargs) {
	g_lingo->push(Datum(Common::String("\r")));
}

void LB::b_tab(int nargs) {
	g_lingo->push(Datum(Common::String("\t")));
}

void LB::b_true(int nargs) {
	g_lingo->push(Datum(1));
}

void LB::b_version(int nargs) {
	int major = g_director->getVersion() / 100;
	int minor = (g_director->getVersion() / 10) % 10;
	int patch = g_director->getVersion() % 10;
	Common::String res;
	if (patch) {
		res = Common::String::format("%d.%d.%d", major, minor, patch);
	} else {
		res = Common::String::format("%d.%d", major, minor);
	}
	g_lingo->push(res);
}

///////////////////
// References
///////////////////
void LB::b_cast(int nargs) {
	Datum d = g_lingo->pop();
	Datum res = d.asMemberID();
	res.type = CASTREF;
	g_lingo->push(res);
}

void LB::b_castLib(int nargs) {
	Datum d = g_lingo->pop();
	Datum res(0);
	if (d.type == STRING) {
		Movie *movie = g_director->getCurrentMovie();
		res.u.i = movie->getCastLibIDByName(*d.u.s);
	} else {
		res.u.i = d.asInt();
	}
	res.type = CASTLIBREF;
	g_lingo->push(res);
}

void LB::b_member(int nargs) {
	CastMemberID res;
	if (nargs == 1) {
		Datum member = g_lingo->pop();
		res = member.asMemberID();
	} else if (nargs == 2) {
		Datum library = g_lingo->pop();
		Datum member = g_lingo->pop();
		res = g_lingo->toCastMemberID(member, library);
	}

	if (res.member > g_lingo->getMembersNum(res.castLib)) {
		if (g_director->getVersion() < 600) {
			g_lingo->lingoError("b_member: Cast member ID out of range");
			return;
		}
	}
	g_lingo->push(res);
}

void LB::b_script(int nargs) {
	CastMemberID memberID;
	if (nargs == 1) {
		Datum member = g_lingo->pop();
		memberID = member.asMemberID();
	} else if (nargs == 2) {
		Datum library = g_lingo->pop();
		Datum member = g_lingo->pop();
		memberID = g_lingo->toCastMemberID(member, library);
	}
	CastMember *cast = g_director->getCurrentMovie()->getCastMember(memberID);

	if (cast) {
		ScriptContext *script = nullptr;

		if (cast->_type == kCastLingoScript) {
			// script cast can be either a movie script, score script, or parent script (D5+)
			script = g_director->getCurrentMovie()->getScriptContext(kMovieScript, memberID);
			if (!script)
				script = g_director->getCurrentMovie()->getScriptContext(kScoreScript, memberID);
			if (!script)
				script = g_director->getCurrentMovie()->getScriptContext(kParentScript, memberID);
		} else {
			script = g_director->getCurrentMovie()->getScriptContext(kCastScript, memberID);
		}

		if (script) {
			g_lingo->push(script);
			return;
		}
	}
	warning("b_script(): No script context found for '%s'", memberID.asString().c_str());
	g_lingo->push(Datum());
}

void LB::b_sprite(int nargs) {
	Datum d = g_lingo->pop();
	Datum res(d.asInt());
	res.type = SPRITEREF;
	g_lingo->push(res);
}

void LB::b_window(int nargs) {
	Datum d = g_lingo->pop();
	FArray *windowList = g_lingo->_windowList.u.farr;

	// Refer window by-indexing, lingo can request using "window #index" where #index is the index of window that is previously
	// created, in tutorial workshop `rect of window`, a window is created using 'open(window "ball")' and the same window is
	// referenced by 'window 1', ie 'put the rect of window 1 into field 9'
	if (d.type == INT || d.type == FLOAT) {
		int windowIndex = d.asInt() - 1;

		if (windowIndex >= 0 && windowIndex < (int)windowList->arr.size()) {
			if (windowList->arr[windowIndex].type == OBJECT && windowList->arr[windowIndex].u.obj->getObjType() == kWindowObj) {
				Window *window = static_cast<Window *>(windowList->arr[windowIndex].u.obj);
				g_lingo->push(window);
				return;
			}
		} else {
			warning("LB::b_window: Window referenced by index %d, out of bounds.", windowIndex);
		}
	}

	Common::String windowName = d.asString();
	Window *window = g_director->getOrCreateWindow(windowName);
	bool isNewWindow = true;
	for (auto &it : windowList->arr) {
		if (it.type == OBJECT && it.u.obj == window) {
			isNewWindow = false;
			break;
		}
	}
	if (isNewWindow)
		windowList->arr.push_back(Datum(window));

	g_lingo->push(window);
}

void LB::b_windowPresent(int nargs) {
	g_lingo->printSTUBWithArglist("b_windowPresent", nargs);
	g_lingo->dropStack(nargs);
	g_lingo->push(Datum(1));
}

void LB::b_charPosToLoc(int nargs) {
	g_lingo->printSTUBWithArglist("b_charPosToLoc", nargs);
	g_lingo->dropStack(nargs);
	Datum res(Common::Point(0, 0));
	g_lingo->push(res);
}

void LB::b_linePosToLocV(int nargs) {
	g_lingo->printSTUBWithArglist("b_linePosToLocV", nargs);
	g_lingo->dropStack(nargs);
	Datum res(0);
	g_lingo->push(res);
}

void LB::b_locToCharPos(int nargs) {
	g_lingo->printSTUBWithArglist("b_locToCharPos", nargs);
	g_lingo->dropStack(nargs);
	Datum res(0);
	g_lingo->push(res);
}

void LB::b_locVToLinePos(int nargs) {
	g_lingo->printSTUBWithArglist("b_locVToLinePos", nargs);
	g_lingo->dropStack(nargs);
	Datum res(0);
	g_lingo->push(res);
}

void LB::b_scrollByLine(int nargs) {
	ARGNUMCHECK(2);
	Datum count = g_lingo->pop();
	Datum member = g_lingo->pop();
	CastMemberID id = member.asMemberID();
	Movie *movie = g_director->getCurrentMovie();
	CastMember *cast = movie->getCastMember(id);
	if (!cast) {
		g_lingo->lingoError("b_scrollByLine: Could not resolve cast member %s", id.asString().c_str());
	} else if (cast->_type != kCastText) {
		g_lingo->lingoError("b_scrollByLine: Expected cast member %s to be text, got %s", id.asString().c_str(), castType2str(cast->_type));
	} else {
		((TextCastMember *)cast)->scrollByLine(count.asInt());
	}

}

void LB::b_scrollByPage(int nargs) {
	g_lingo->printSTUBWithArglist("b_scrollByPage", nargs);
	g_lingo->dropStack(nargs);
}

void LB::b_lineHeight(int nargs) {
	Datum lineNum = g_lingo->pop();
	Datum castRef = g_lingo->pop();

	if (castRef.type != CASTREF) {
		g_lingo->lingoError("Incorrect argument type for lineHeight");
		g_lingo->push(1);
        return;
	}

	CastMember *member = g_director->getCurrentMovie()->getCastMember(*castRef.u.cast);

	if (member->_type != kCastText) {
		g_lingo->lingoError("Incorrect member type for lineHeight");
		g_lingo->push(1);
		return;
	}

	// MacText::getLineHeigt() is zero-indexed, we are one-indexed
	int lineHeight = ((TextCastMember *)member)->getLineHeight(lineNum.u.i - 1);
	g_lingo->push(lineHeight);
}

void LB::b_numberofchars(int nargs) {
	Datum d = g_lingo->pop();
	Datum chunkRef = LC::lastChunk(kChunkChar, d);
	g_lingo->push(chunkRef.u.cref->startChunk);
}

void LB::b_numberofitems(int nargs) {
	Datum d = g_lingo->pop();
	Datum chunkRef = LC::lastChunk(kChunkItem, d);
	g_lingo->push(chunkRef.u.cref->startChunk);
}

void LB::b_numberoflines(int nargs) {
	Datum d = g_lingo->pop();
	Datum chunkRef = LC::lastChunk(kChunkLine, d);
	g_lingo->push(chunkRef.u.cref->startChunk);
}

void LB::b_numberofwords(int nargs) {
	Datum d = g_lingo->pop();
	Datum chunkRef = LC::lastChunk(kChunkWord, d);
	g_lingo->push(chunkRef.u.cref->startChunk);
}

void LB::b_trackCount(int nargs) {
	g_lingo->printSTUBWithArglist("b_trackCount", nargs);
	g_lingo->dropStack(nargs);
	g_lingo->push(1);
}

void LB::b_trackStartTime(int nargs) {
	g_lingo->printSTUBWithArglist("b_trackStartTime", nargs);
	g_lingo->dropStack(nargs);
	g_lingo->push(0);
}

void LB::b_trackStopTime(int nargs) {
	g_lingo->printSTUBWithArglist("b_trackStopTime", nargs);
	g_lingo->dropStack(nargs);
	g_lingo->push(0);
}

void LB::b_trackType(int nargs) {
	g_lingo->printSTUBWithArglist("b_trackType", nargs);
	g_lingo->dropStack(nargs);
	// Possible values are #video, #sound, #text, and #music
	// also #timeCode is present but seemingly not used
	Datum result("video");
	result.type = SYMBOL;
	g_lingo->push(result);
}

void LB::b_scummvmassert(int nargs) {
	Datum line = g_lingo->pop();
	Datum d = g_lingo->pop();

	if (d.asInt() == 0) {
		warning("BUILDBOT: LB::b_scummvmassert: is false at line %d", line.asInt());
	}
	if (debugChannelSet(-1, kDebugLingoStrict)) {
		assert(d.asInt() != 0);
	}
}

void LB::b_scummvmassertequal(int nargs) {
	Datum line = g_lingo->pop();
	Datum d2 = g_lingo->pop();
	Datum d1 = g_lingo->pop();

	int result;

	if (d1.type == ARRAY && d2.type == ARRAY) {
		result = LC::eqData(d1, d2).u.i;
	} else if (d1.type == PARRAY && d2.type == PARRAY) {
		result = LC::eqData(d1, d2).u.i;
	} else {
		result = (d1 == d2);
	}

	if (!result) {
		warning("BUILDBOT: LB::b_scummvmassertequal: %s is not equal %s at line %d", formatStringForDump(d1.asString()).c_str(), formatStringForDump(d2.asString()).c_str(), line.asInt());
	}
	if (debugChannelSet(-1, kDebugLingoStrict)) {
		assert(result == 1);
	}
}

void LB::b_scummvmNoFatalError(int nargs) {
	Datum flag = g_lingo->pop();

	g_director->_noFatalLingoError = (flag.asInt() != 0);

	debug("> scummvmNoFatalEror is set to %d", g_director->_noFatalLingoError);
}

void LB::b_getVolumes(int nargs) {
	// Right now, only "Journeyman Project 2: Buried in Time" is known to check
	// for its volume name.
	Datum d;
	d.type = ARRAY;
	d.u.farr = new FArray;
	d.u.farr->arr.push_back(Datum("Buried in Time\252 1"));

	g_lingo->push(d);
}

void LB::b_beginRecording(int nargs) {
	g_lingo->printSTUBWithArglist("b_beginRecording", nargs);
	g_lingo->dropStack(nargs);
}

void LB::b_endRecording(int nargs) {
	g_lingo->printSTUBWithArglist("b_endRecording", nargs);
	g_lingo->dropStack(nargs);
}

// Shockwave D6
void LB::b_externalParamCount(int nargs) {
	g_lingo->printSTUBWithArglist("b_externalParamCount", nargs);
	g_lingo->dropStack(nargs);
	g_lingo->push(Datum());
}

void LB::b_externalParamName(int nargs) {
	g_lingo->printSTUBWithArglist("b_externalParamName", nargs);
	g_lingo->dropStack(nargs);
	g_lingo->push(Datum());
}

void LB::b_externalParamValue(int nargs) {
	g_lingo->printSTUBWithArglist("b_externalParamValue", nargs);
	g_lingo->dropStack(nargs);
	g_lingo->push(Datum());
}

void LB::b_frameReady(int nargs) {
	g_lingo->printSTUBWithArglist("b_frameReady", nargs);
	g_lingo->dropStack(nargs);
	g_lingo->push(Datum());
}

void LB::b_getPref(int nargs) {
	g_lingo->printSTUBWithArglist("b_getPref", nargs);
	g_lingo->dropStack(nargs);
	g_lingo->push(Datum());
}

void LB::b_netPresent(int nargs) {
	// Once NETLINGO.X32 is implmemented, this should return 1
	g_lingo->printSTUBWithArglist("b_netPresent", nargs);
	g_lingo->dropStack(nargs);
	g_lingo->push(Datum());
}

} // End of namespace Director
