/* 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/savefile.h"
#include "ags/shared/core/platform.h"
#include "ags/shared/ac/audio_clip_type.h"
#include "ags/engine/ac/global_game.h"
#include "ags/shared/ac/common.h"
#include "ags/shared/ac/view.h"
#include "ags/engine/ac/character.h"
#include "ags/engine/ac/draw.h"
#include "ags/engine/ac/dynamic_sprite.h"
#include "ags/engine/ac/event.h"
#include "ags/engine/ac/game.h"
#include "ags/engine/ac/game_setup.h"
#include "ags/shared/ac/game_setup_struct.h"
#include "ags/engine/ac/game_state.h"
#include "ags/engine/ac/global_character.h"
#include "ags/engine/ac/global_gui.h"
#include "ags/engine/ac/global_hotspot.h"
#include "ags/engine/ac/global_inventory_item.h"
#include "ags/engine/ac/global_translation.h"
#include "ags/engine/ac/gui.h"
#include "ags/engine/ac/hotspot.h"
#include "ags/shared/ac/keycode.h"
#include "ags/engine/ac/mouse.h"
#include "ags/engine/ac/object.h"
#include "ags/engine/ac/path_helper.h"
#include "ags/engine/ac/sys_events.h"
#include "ags/engine/ac/room.h"
#include "ags/engine/ac/room_status.h"
#include "ags/engine/ac/string.h"
#include "ags/engine/ac/system.h"
#include "ags/engine/debugging/debugger.h"
#include "ags/engine/debugging/debug_log.h"
#include "ags/shared/font/fonts.h"
#include "ags/engine/gui/gui_dialog.h"
#include "ags/engine/main/engine.h"
#include "ags/engine/main/game_run.h"
#include "ags/engine/main/graphics_mode.h"
#include "ags/engine/main/game_start.h"
#include "ags/engine/script/script.h"
#include "ags/engine/script/script_runtime.h"
#include "ags/shared/ac/sprite_cache.h"
#include "ags/shared/gfx/bitmap.h"
#include "ags/engine/gfx/graphics_driver.h"
#include "ags/shared/core/asset_manager.h"
#include "ags/engine/main/config.h"
#include "ags/engine/main/game_file.h"
#include "ags/shared/util/path.h"
#include "ags/shared/util/string_utils.h"
#include "ags/engine/media/audio/audio_system.h"
#include "ags/engine/platform/base/sys_main.h"
#include "ags/ags.h"
#include "ags/globals.h"

namespace AGS3 {

using namespace AGS::Shared;

void AbortGame() {
	// make sure scripts stop at the next step
	cancel_all_scripts();
}

void GiveScore(int amnt) {
	GUI::MarkSpecialLabelsForUpdate(kLabelMacro_AllScore);
	_GP(play).score += amnt;

	if ((amnt > 0) && (_GP(play).score_sound >= 0))
		play_audio_clip_by_index(_GP(play).score_sound);

	run_on_event(GE_GOT_SCORE, RuntimeScriptValue().SetInt32(amnt));
}

void restart_game() {
	can_run_delayed_command();
	if (_G(inside_script)) {
		_G(curscript)->queue_action(ePSARestartGame, 0, "RestartGame");
		return;
	}
	try_restore_save(RESTART_POINT_SAVE_GAME_NUMBER);
}

void RestoreGameSlot(int slnum) {
	if (_G(displayed_room) < 0)
		quit("!RestoreGameSlot: a game cannot be restored from within game_start");

	can_run_delayed_command();
	if (_G(inside_script)) {
		_G(curscript)->queue_action(ePSARestoreGame, slnum, "RestoreGameSlot");
		return;
	}
	try_restore_save(slnum);
}

void DeleteSaveSlot(int slnum) {
	String nametouse;
	nametouse = get_save_game_path(slnum);
	Shared::File::DeleteFile(nametouse);
}

void PauseGame() {
	_G(game_paused)++;
	debug_script_log("Game paused (%d)", _G(game_paused));
}
void UnPauseGame() {
	if (_G(game_paused) > 0) {
		_G(game_paused)--;
		debug_script_log("Game un-paused (%d)", _G(game_paused));
	}
}


int IsGamePaused() {
	if (_G(game_paused) > 0) return 1;
	return 0;
}

bool GetSaveSlotDescription(int slnum, String &description) {
	if (read_savedgame_description(get_save_game_path(slnum), description))
		return true;
	description.Format("INVALID SLOT %d", slnum);
	return false;
}

int GetSaveSlotDescription(int slnum, char *desbuf) {
	VALIDATE_STRING(desbuf);
	String description;
	bool res = GetSaveSlotDescription(slnum, description);
	snprintf(desbuf, MAX_MAXSTRLEN, "%s", description.GetCStr());
	return res ? 1 : 0;
}

int LoadSaveSlotScreenshot(int slnum, int width, int height) {
	if (!_GP(spriteset).HasFreeSlots())
		return 0;

	auto screenshot = read_savedgame_screenshot(get_save_game_path(slnum));
	if (!screenshot)
		return 0;

	// resize the sprite to the requested size
	data_to_game_coords(&width, &height);
	if ((screenshot->GetWidth() != width) || (screenshot->GetHeight() != height)) {
		std::unique_ptr<Bitmap> temp(BitmapHelper::CreateBitmap(width, height, screenshot->GetColorDepth()));
		temp->StretchBlt(screenshot.get(),
						 RectWH(0, 0, screenshot->GetWidth(), screenshot->GetHeight()),
						 RectWH(0, 0, width, height));
		screenshot = std::move(temp);
	}
	return add_dynamic_sprite(std::move(screenshot));
}

void FillSaveList(std::vector<SaveListItem> &saves, size_t max_count) {
	if (max_count == 0)
		return; // duh

	String svg_dir = get_save_game_directory();
	String svg_suff = get_save_game_suffix();
	String searchPath = Path::ConcatPaths(svg_dir, String::FromFormat("agssave.???%s", svg_suff.GetCStr()));
	time_t time = 0;

	SaveStateList saveList = ::AGS::g_vm->listSaves();
	for (uint idx = 0; idx < saveList.size(); ++idx) {
		int saveGameSlot = saveList[idx].getSaveSlot();

		// only list games .000 to .xxx (to allow higher slots for other perposes)
		if (saveGameSlot < 0 || saveGameSlot > TOP_LISTEDSAVESLOT)
			continue;

		String description;
		GetSaveSlotDescription(saveGameSlot, description);
		saves.push_back(SaveListItem(saveGameSlot, description, time));
		if (saves.size() >= max_count)
			break;
	}
}

void SetGlobalInt(int index, int valu) {
	if ((index < 0) || (index >= MAXGSVALUES))
		quitprintf("!SetGlobalInt: invalid index %d, supported range is %d - %d", index, 0, MAXGSVALUES - 1);

	if (_GP(play).globalscriptvars[index] != valu) {
		debug_script_log("GlobalInt %d set to %d", index, valu);
	}

	_GP(play).globalscriptvars[index] = valu;
}


int GetGlobalInt(int index) {
	if ((index < 0) || (index >= MAXGSVALUES))
		quitprintf("!GetGlobalInt: invalid index %d, supported range is %d - %d", index, 0, MAXGSVALUES - 1);
	return _GP(play).globalscriptvars[index];
}

void SetGlobalString(int index, const char *newval) {
	if ((index < 0) || (index >= MAXGLOBALSTRINGS))
		quitprintf("!SetGlobalString: invalid index %d, supported range is %d - %d", index, 0, MAXGLOBALSTRINGS - 1);
	debug_script_log("GlobalString %d set to '%s'", index, newval);
	snprintf(_GP(play).globalstrings[index], MAX_MAXSTRLEN, "%s", newval);
}

void GetGlobalString(int index, char *strval) {
	if ((index < 0) || (index >= MAXGLOBALSTRINGS))
		quitprintf("!GetGlobalString: invalid index %d, supported range is %d - %d", index, 0, MAXGLOBALSTRINGS - 1);
	snprintf(strval, MAX_MAXSTRLEN, "%s", _GP(play).globalstrings[index]);
}

// TODO: refactor this method, and use same shared procedure at both normal stop/startup and in RunAGSGame
int RunAGSGame(const String &newgame, unsigned int mode, int data) {

	can_run_delayed_command();

	int AllowedModes = RAGMODE_PRESERVEGLOBALINT | RAGMODE_LOADNOW;

	if ((mode & (~AllowedModes)) != 0)
		quit("!RunAGSGame: mode value unknown");

	if (_G(editor_debugging_enabled)) {
		quit("!RunAGSGame cannot be used while running the game from within the AGS Editor. You must build the game EXE and run it from there to use this function.");
	}

	if ((mode & RAGMODE_LOADNOW) == 0) {
		_GP(ResPaths).GamePak.Path = PathFromInstallDir(newgame);
		_GP(ResPaths).GamePak.Name = newgame;
		_GP(play).takeover_data = data;
		_G(load_new_game_restore) = -1;

		if (_G(inside_script)) {
			_G(curscript)->queue_action(ePSARunAGSGame, mode | RAGMODE_LOADNOW, "RunAGSGame");
			ccInstance::GetCurrentInstance()->Abort();
		} else
			_G(load_new_game) = mode | RAGMODE_LOADNOW;

		return 0;
	}

	// Optionally save legacy GlobalInts
	int savedscriptvars[MAXGSVALUES];
	if ((mode & RAGMODE_PRESERVEGLOBALINT) != 0) {
		memcpy(savedscriptvars, _GP(play).globalscriptvars, sizeof(_GP(play).globalscriptvars));
	}

	unload_old_room();
	_G(displayed_room) = -10;

#if defined (AGS_AUTO_WRITE_USER_CONFIG)
	save_config_file(); // save current user config in case engine fails to run new game
#endif // AGS_AUTO_WRITE_USER_CONFIG
	unload_game();

	// Adjust config (NOTE: normally, RunAGSGame would need a redesign to allow separate config etc per each game)
	_GP(usetup).translation = ""; // reset to default, prevent from trying translation file of game A in game B

	_GP(AssetMgr)->RemoveAllLibraries();

	// TODO: refactor and share same code with the startup!
	if (_GP(AssetMgr)->AddLibrary(_GP(ResPaths).GamePak.Path) != Shared::kAssetNoError)
		quitprintf("!RunAGSGame: unable to load new game file '%s'", _GP(ResPaths).GamePak.Path.GetCStr());
	engine_assign_assetpaths();

	show_preload();

	HError err = load_game_file();
	if (!err)
		quitprintf("!RunAGSGame: error loading new game file:\n%s", err->FullMessage().GetCStr());

	_GP(spriteset).Reset();
	err = _GP(spriteset).InitFile(SpriteFile::DefaultSpriteFileName, SpriteFile::DefaultSpriteIndexName);
	if (!err)
		quitprintf("!RunAGSGame: error loading new sprites:\n%s", err->FullMessage().GetCStr());

	// Restore saved GlobalInts
	if ((mode & RAGMODE_PRESERVEGLOBALINT) != 0) {
		memcpy(_GP(play).globalscriptvars, savedscriptvars, sizeof(_GP(play).globalscriptvars));
	}

	engine_init_game_settings();
	_GP(play).screen_is_faded_out = 1;

	if (_G(load_new_game_restore) >= 0) {
		try_restore_save(_G(load_new_game_restore));
		_G(load_new_game_restore) = -1;
	} else
		start_game();

	return 0;
}

int GetGameParameter(int parm, int data1, int data2, int data3) {
	switch (parm) {
	case GP_SPRITEWIDTH:
		return Game_GetSpriteWidth(data1);
	case GP_SPRITEHEIGHT:
		return Game_GetSpriteHeight(data1);
	case GP_NUMLOOPS:
		return Game_GetLoopCountForView(data1);
	case GP_NUMFRAMES:
		return Game_GetFrameCountForLoop(data1, data2);
	case GP_FRAMESPEED:
	case GP_FRAMEIMAGE:
	case GP_FRAMESOUND:
	case GP_ISFRAMEFLIPPED: {
		AssertFrame("GetGameParameter", data1 - 1, data2, data3);

		ViewFrame *pvf = &_GP(views)[data1 - 1].loops[data2].frames[data3];

		if (parm == GP_FRAMESPEED)
			return pvf->speed;
		else if (parm == GP_FRAMEIMAGE)
			return pvf->pic;
		else if (parm == GP_FRAMESOUND)
			return get_old_style_number_for_sound(pvf->sound);
		else if (parm == GP_ISFRAMEFLIPPED)
			return (pvf->flags & VFLG_FLIPSPRITE) ? 1 : 0;
		else
			quit("GetGameParameter internal error");
		break;
	}
	case GP_ISRUNNEXTLOOP:
		return Game_GetRunNextSettingForLoop(data1, data2);
	case GP_NUMGUIS:
		return _GP(game).numgui;
	case GP_NUMOBJECTS:
		return _G(croom)->numobj;
	case GP_NUMCHARACTERS:
		return _GP(game).numcharacters;
	case GP_NUMINVITEMS:
		return _GP(game).numinvitems;
	default:
		quit("!GetGameParameter: unknown parameter specified");
	}
	return 0;
}

void QuitGame(int dialog) {
	if (dialog) {
		int rcode;
		setup_for_dialog();
		rcode = quitdialog();
		restore_after_dialog();
		if (rcode == 0) return;
	}
	quit("|You have exited.");
}




void SetRestartPoint() {
	save_game(RESTART_POINT_SAVE_GAME_NUMBER, "Restart Game Auto-Save");
}



void SetGameSpeed(int newspd) {
	newspd += _GP(play).game_speed_modifier;
	if (newspd > 1000) newspd = 1000;
	if (newspd < 10) newspd = 10;
	set_game_speed(newspd);
	debug_script_log("Game speed set to %d", newspd);
}

int GetGameSpeed() {
	return ::lround(get_game_fps()) - _GP(play).game_speed_modifier;
}

int SetGameOption(int opt, int newval) {
	if (((opt < OPT_DEBUGMODE) || (opt > OPT_HIGHESTOPTION)) && (opt != OPT_LIPSYNCTEXT)) {
		debug_script_warn("SetGameOption: invalid option specified: %d", opt);
		return 0;
	}

	// Handle forbidden options
	const auto restricted_opts = GameSetupStructBase::GetRestrictedOptions();
	for (auto r_opt : restricted_opts) {
		if (r_opt == opt) {
			debug_script_warn("SetGameOption: option %d cannot be modified at runtime", opt);
			return _GP(game).options[opt];
		}
	}

	// Test if option already has this value
	if (_GP(game).options[opt] == newval)
		return _GP(game).options[opt];

	const int oldval = _GP(game).options[opt];
	_GP(game).options[opt] = newval;

	// Update the game in accordance to the new option value
	switch (opt) {
	case OPT_ANTIGLIDE:
		for (int i = 0; i < _GP(game).numcharacters; i++) {
			if (newval)
				_GP(game).chars[i].flags |= CHF_ANTIGLIDE;
			else
				_GP(game).chars[i].flags &= ~CHF_ANTIGLIDE;
		}
		break;
	case OPT_DISABLEOFF:
		GUI::Options.DisabledStyle = static_cast<GuiDisableStyle>(_GP(game).options[OPT_DISABLEOFF]);
		// If GUI was disabled at this time then also update it, as visual style could've changed
		if (_GP(play).disabled_user_interface > 0) {
			GUI::MarkAllGUIForUpdate(true, false);
		}
		break;
	case OPT_CROSSFADEMUSIC:
		if (_GP(game).audioClipTypes.size() > AUDIOTYPE_LEGACY_MUSIC) {
			// legacy compatibility -- changing crossfade speed here also
			// updates the new audio clip type style
			_GP(game).audioClipTypes[AUDIOTYPE_LEGACY_MUSIC].crossfadeSpeed = newval;
		}
		break;
	case OPT_ANTIALIASFONTS:
		adjust_fonts_for_render_mode(newval != 0);
		break;
	case OPT_RIGHTLEFTWRITE:
		GUI::MarkForTranslationUpdate();
		break;
	case OPT_DUPLICATEINV:
		update_invorder();
		break;
	case OPT_PORTRAITSIDE:
		if (newval == 0) // set back to Left
			_GP(play).swap_portrait_side = 0;
		break;
	default:
		break; // do nothing else
	}

	return oldval;
}

int GetGameOption(int opt) {
	if (((opt < OPT_DEBUGMODE) || (opt > OPT_HIGHESTOPTION)) && (opt != OPT_LIPSYNCTEXT)) {
		debug_script_warn("GetGameOption: invalid option specified: %d", opt);
		return 0;
	}

	return _GP(game).options[opt];
}

void SkipUntilCharacterStops(int cc) {
	if (!is_valid_character(cc))
		quit("!SkipUntilCharacterStops: invalid character specified");
	if (_GP(game).chars[cc].room != _G(displayed_room))
		quitprintf("!SkipUntilCharacterStops: character %s is not in current room %d (it is in room %d)",
				   _GP(game).chars[cc].scrname, _G(displayed_room), _GP(game).chars[cc].room);

	// if they are not currently moving, do nothing
	if (!_GP(game).chars[cc].walking)
		return;

	if (is_in_cutscene())
		quit("!SkipUntilCharacterStops: cannot be used within a cutscene");

	initialize_skippable_cutscene();
	_GP(play).fast_forward = 2;
	_GP(play).skip_until_char_stops = cc;
}

void EndSkippingUntilCharStops() {
	// not currently skipping, so ignore
	if (_GP(play).skip_until_char_stops < 0)
		return;

	stop_fast_forwarding();
	_GP(play).skip_until_char_stops = -1;
}

void StartCutscene(int skipwith) {
	ScriptPosition &last_cutscene_script_pos = _GP(last_cutscene_script_pos);

	if (is_in_cutscene()) {
		quitprintf("!StartCutscene: already in a cutscene; previous started in \"%s\", line %d",
		           last_cutscene_script_pos.Section.GetCStr(), last_cutscene_script_pos.Line);
	}

	if ((skipwith < 1) || (skipwith > 6))
		quit("!StartCutscene: invalid argument, must be 1 to 5.");

	get_script_position(last_cutscene_script_pos);

	// make sure they can't be skipping and cutsceneing at the same time
	EndSkippingUntilCharStops();

	_GP(play).in_cutscene = skipwith;
	initialize_skippable_cutscene();
}

void SkipCutscene() {
	if (is_in_cutscene())
		start_skipping_cutscene();
}

int EndCutscene() {
	if (!is_in_cutscene())
		quit("!EndCutscene: not in a cutscene");

	int retval = _GP(play).fast_forward;
	_GP(play).in_cutscene = 0;
	// Stop it fast-forwarding
	stop_fast_forwarding();

	// make sure that the screen redraws
	invalidate_screen();

	// Return whether the player skipped it
	return retval;
}

void ShowInputBox(const char *msg, char *bufr) {
	VALIDATE_STRING(bufr);
	ShowInputBoxImpl(msg, bufr, MAX_MAXSTRLEN);
}

void ShowInputBoxImpl(const char *msg, char *bufr, size_t buf_len) {
	setup_for_dialog();
	enterstringwindow(get_translation(msg), bufr, buf_len);
	restore_after_dialog();
}

// GetLocationType exported function - just call through
// to the main function with default 0
int GetLocationType(int xxx, int yyy) {
	return __GetLocationType(xxx, yyy, 0);
}

void SaveCursorForLocationChange() {
	// update the current location name
	char tempo[100];
	GetLocationName(game_to_data_coord(_G(mousex)), game_to_data_coord(_G(mousey)), tempo);

	if (_GP(play).get_loc_name_save_cursor != _GP(play).get_loc_name_last_time) {
		_GP(play).get_loc_name_save_cursor = _GP(play).get_loc_name_last_time;
		_GP(play).restore_cursor_mode_to = GetCursorMode();
		_GP(play).restore_cursor_image_to = GetMouseCursor();
		debug_script_log("Saving mouse: mode %d cursor %d", _GP(play).restore_cursor_mode_to, _GP(play).restore_cursor_image_to);
	}
}

void GetLocationName(int xxx, int yyy, char *tempo) {
	if (_G(displayed_room) < 0)
		quit("!GetLocationName: no room has been loaded");

	VALIDATE_STRING(tempo);

	tempo[0] = 0;

	if (GetGUIAt(xxx, yyy) >= 0) {
		int mover = GetInvAt(xxx, yyy);
		if (mover > 0) {
			if (_GP(play).get_loc_name_last_time != 1000 + mover)
				GUI::MarkSpecialLabelsForUpdate(kLabelMacro_Overhotspot);
			_GP(play).get_loc_name_last_time = 1000 + mover;
			snprintf(tempo, MAX_MAXSTRLEN, "%s", get_translation(_GP(game).invinfo[mover].name.GetCStr()));
		} else if ((_GP(play).get_loc_name_last_time > 1000) && (_GP(play).get_loc_name_last_time < 1000 + MAX_INV)) {
			// no longer selecting an item
			GUI::MarkSpecialLabelsForUpdate(kLabelMacro_Overhotspot);
			_GP(play).get_loc_name_last_time = -1;
		}
		return;
	}

	int loctype = GetLocationType(xxx, yyy); // GetLocationType takes screen coords
	VpPoint vpt = _GP(play).ScreenToRoomDivDown(xxx, yyy);
	if (vpt.second < 0)
		return;
	xxx = vpt.first.X;
	yyy = vpt.first.Y;
	if ((xxx >= _GP(thisroom).Width) || (xxx < 0) || (yyy < 0) || (yyy >= _GP(thisroom).Height))
		return;

	int onhs, aa;
	if (loctype == 0) {
		if (_GP(play).get_loc_name_last_time != 0) {
			_GP(play).get_loc_name_last_time = 0;
			GUI::MarkSpecialLabelsForUpdate(kLabelMacro_Overhotspot);
		}
		return;
	}

	// on character
	if (loctype == LOCTYPE_CHAR) {
		onhs = _G(getloctype_index);
		snprintf(tempo, MAX_MAXSTRLEN, "%s", get_translation(_GP(game).chars2[onhs].name_new.GetCStr()));
		if (_GP(play).get_loc_name_last_time != 2000 + onhs)
			GUI::MarkSpecialLabelsForUpdate(kLabelMacro_Overhotspot);
		_GP(play).get_loc_name_last_time = 2000 + onhs;
		return;
	}
	// on object
	if (loctype == LOCTYPE_OBJ) {
		aa = _G(getloctype_index);
		snprintf(tempo, MAX_MAXSTRLEN, "%s", get_translation(_G(croom)->obj[aa].name.GetCStr()));
		// Compatibility: < 3.1.1 games returned space for nameless object
		// (presumably was a bug, but fixing it affected certain games behavior)
		if (_G(loaded_game_file_version) < kGameVersion_311 && tempo[0] == 0) {
			tempo[0] = ' ';
			tempo[1] = 0;
		}
		if (_GP(play).get_loc_name_last_time != 3000 + aa)
			GUI::MarkSpecialLabelsForUpdate(kLabelMacro_Overhotspot);
		_GP(play).get_loc_name_last_time = 3000 + aa;
		return;
	}
	onhs = _G(getloctype_index);
	if (onhs > 0)
		snprintf(tempo, MAX_MAXSTRLEN, "%s", get_translation(_G(croom)->hotspot[onhs].Name.GetCStr()));
	if (_GP(play).get_loc_name_last_time != onhs)
		GUI::MarkSpecialLabelsForUpdate(kLabelMacro_Overhotspot);
	_GP(play).get_loc_name_last_time = onhs;
}

int IsKeyPressed(int keycode) {
	return ags_iskeydown(static_cast<eAGSKeyCode>(keycode));
}

int SaveScreenShot(const char *namm) {
	String fileName;
	String svg_dir = get_save_game_directory();

	if (strchr(namm, '.') == nullptr)
		fileName = Path::MakePath(svg_dir, namm, "bmp");
	else
		fileName = Path::ConcatPaths(svg_dir, namm);

	// NOTE: be aware that by the historical logic AGS makes a screenshot
	// of a "main viewport", that may be smaller in legacy "letterbox" mode.
	const Rect &viewport = _GP(play).GetMainViewport();
	Bitmap *buffer = CopyScreenIntoBitmap(_GP(play).GetMainViewport().GetWidth(), _GP(play).GetMainViewport().GetHeight(), &viewport);
	if (!buffer->SaveToFile(fileName, _G(palette)) != 0) {
		delete buffer;
		return 0;
	}
	delete buffer;
	return 1;  // successful
}

void SetMultitasking(int mode) {
	if ((mode < 0) || (mode > 1))
		quit("!SetMultitasking: invalid mode parameter");
	// Save requested setting
	_GP(usetup).multitasking = mode != 0;

	// Account for the override config option (must be checked first!)
	if ((_GP(usetup).override_multitasking >= 0) && (mode != _GP(usetup).override_multitasking)) {
		Debug::Printf("SetMultitasking: overridden by user config: %d -> %d", mode, _GP(usetup).override_multitasking);
		mode = _GP(usetup).override_multitasking;
	}

	// Must run on background if debugger is connected
	if ((mode == 0) && (_G(editor_debugging_initialized) != 0)) {
		Debug::Printf("SetMultitasking: overridden by the external debugger: %d -> 1", mode);
		mode = 1;
	}

	// Regardless, don't allow background running if exclusive full screen
	if ((mode == 1) && _G(gfxDriver)->GetDisplayMode().IsRealFullscreen()) {
		Debug::Printf("SetMultitasking: overridden by fullscreen: %d -> 0", mode);
		mode = 0;
	}

	// Install engine callbacks for switching in and out the window
	Debug::Printf(kDbgMsg_Info, "Multitasking mode set: %d", mode);
	if (mode == 0) {
		sys_set_background_mode(false);
		sys_evt_set_focus_callbacks(display_switch_in_resume, display_switch_out_suspend);
	} else {
		sys_set_background_mode(true);
		sys_evt_set_focus_callbacks(display_switch_in, display_switch_out);
	}
}

void RoomProcessClick(int xx, int yy, int mood) {
	_G(getloctype_throughgui) = 1;
	int loctype = GetLocationType(xx, yy);
	VpPoint vpt = _GP(play).ScreenToRoomDivDown(xx, yy);
	if (vpt.second < 0)
		return;
	xx = vpt.first.X;
	yy = vpt.first.Y;

	if ((mood == MODE_WALK) && (_GP(game).options[OPT_NOWALKMODE] == 0)) {
		int hsnum = get_hotspot_at(xx, yy);
		if (hsnum < 1);
		else if (_GP(thisroom).Hotspots[hsnum].WalkTo.X < 1);
		else if (_GP(play).auto_use_walkto_points == 0);
		else {
			xx = _GP(thisroom).Hotspots[hsnum].WalkTo.X;
			yy = _GP(thisroom).Hotspots[hsnum].WalkTo.Y;
			debug_script_log("Move to walk-to point hotspot %d", hsnum);
		}
		walk_character(_GP(game).playercharacter, xx, yy, 0, true);
		return;
	}
	_GP(play).usedmode = mood;

	if (loctype == 0) {
		// click on nothing -> hotspot 0
		_G(getloctype_index) = 0;
		loctype = LOCTYPE_HOTSPOT;
	}

	if (loctype == LOCTYPE_CHAR) {
		if (check_click_on_character(xx, yy, mood)) return;
	} else if (loctype == LOCTYPE_OBJ) {
		if (check_click_on_object(xx, yy, mood)) return;
	} else if (loctype == LOCTYPE_HOTSPOT)
		RunHotspotInteraction(_G(getloctype_index), mood);
}

int IsInteractionAvailable(int xx, int yy, int mood) {
	_G(getloctype_throughgui) = 1;
	int loctype = GetLocationType(xx, yy);
	VpPoint vpt = _GP(play).ScreenToRoomDivDown(xx, yy);
	if (vpt.second < 0)
		return 0;
	xx = vpt.first.X;
	yy = vpt.first.Y;

	// You can always walk places
	if ((mood == MODE_WALK) && (_GP(game).options[OPT_NOWALKMODE] == 0))
		return 1;

	_GP(play).check_interaction_only = 1;

	if (loctype == 0) {
		// click on nothing -> hotspot 0
		_G(getloctype_index) = 0;
		loctype = LOCTYPE_HOTSPOT;
	}

	if (loctype == LOCTYPE_CHAR) {
		check_click_on_character(xx, yy, mood);
	} else if (loctype == LOCTYPE_OBJ) {
		check_click_on_object(xx, yy, mood);
	} else if (loctype == LOCTYPE_HOTSPOT)
		RunHotspotInteraction(_G(getloctype_index), mood);

	int ciwas = _GP(play).check_interaction_only;
	_GP(play).check_interaction_only = 0;

	if (ciwas == 2)
		return 1;

	return 0;
}

void GetMessageText(int msg, char *buffer) {
	VALIDATE_STRING(buffer);
	get_message_text(msg, buffer, 0);
}

void SetSpeechFont(int fontnum) {
	if ((fontnum < 0) || (fontnum >= _GP(game).numfonts))
		quit("!SetSpeechFont: invalid font number.");
	_GP(play).speech_font = fontnum;
}

void SetNormalFont(int fontnum) {
	if ((fontnum < 0) || (fontnum >= _GP(game).numfonts))
		quit("!SetNormalFont: invalid font number.");
	_GP(play).normal_font = fontnum;
}

void _sc_AbortGame(const char *text) {
	char displbuf[STD_BUFFER_SIZE] = "!?";
	snprintf(&displbuf[2], STD_BUFFER_SIZE - 3, "%s", text);
	quit(displbuf);
}

int GetGraphicalVariable(const char *varName) {
	InteractionVariable *theVar = FindGraphicalVariable(varName);
	if (theVar == nullptr) {
		quitprintf("!GetGraphicalVariable: interaction variable '%s' not found", varName);
		return 0;
	}
	return theVar->Value;
}

void SetGraphicalVariable(const char *varName, int p_value) {
	InteractionVariable *theVar = FindGraphicalVariable(varName);
	if (theVar == nullptr) {
		quitprintf("!SetGraphicalVariable: interaction variable '%s' not found", varName);
	} else
		theVar->Value = p_value;
}

int WaitImpl(int skip_type, int nloops) {
	// if skipping cutscene and expecting user input: don't wait at all
	if (_GP(play).fast_forward && ((skip_type & ~SKIP_AUTOTIMER) != 0))
		return 0;

	// < 3.6.0 treated negative nloops as "no time";
	// also old engine let nloops to overflow into neg when assigned to wait_counter...
	if (_GP(game).options[OPT_BASESCRIPTAPI] < kScriptAPI_v360) {
		if (nloops < 0 || nloops > INT16_MAX)
			nloops = 0;
	}
	// clamp to int16
	_GP(play).wait_counter = static_cast<int16_t>(Math::Clamp<int>(nloops, -1, INT16_MAX));
	_GP(play).wait_skipped_by = SKIP_NONE;
	_GP(play).wait_skipped_by = SKIP_AUTOTIMER; // we set timer flag by default to simplify that case
	_GP(play).wait_skipped_by_data = 0;
	_GP(play).key_skip_wait = skip_type;

    GameLoopUntilValueIsZero(&_GP(play).wait_counter);

	if (_GP(game).options[OPT_BASESCRIPTAPI] < kScriptAPI_v360) {
		// < 3.6.0 return 1 if skipped by user input, otherwise 0
		return ((_GP(play).wait_skipped_by & (SKIP_KEYPRESS | SKIP_MOUSECLICK)) != 0) ? 1 : 0;
	}
	// >= 3.6.0 return skip (input) type flags with keycode
	return _GP(play).GetWaitSkipResult();
}

void scrWait(int nloops) {
	WaitImpl(SKIP_AUTOTIMER, nloops);
}

int WaitKey(int nloops) {
	return WaitImpl(SKIP_KEYPRESS | SKIP_AUTOTIMER, nloops);
}

int WaitMouse(int nloops) {
	return WaitImpl(SKIP_MOUSECLICK | SKIP_AUTOTIMER, nloops);
}

int WaitMouseKey(int nloops) {
	return WaitImpl(SKIP_KEYPRESS | SKIP_MOUSECLICK | SKIP_AUTOTIMER, nloops);
}

int WaitInput(int input_flags, int nloops) {
	return WaitImpl((input_flags >> SKIP_RESULT_TYPE_SHIFT) | SKIP_AUTOTIMER, nloops);
}

void SkipWait() {
	_GP(play).wait_counter = 0;
}

void scStartRecording(int /*keyToStop*/) {
	debug_script_warn("StartRecording: not supported");
}

} // namespace AGS3
