/* 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/btea.h"
#include "common/debug-channels.h"
#include "common/savefile.h"
#include "twp/callback.h"
#include "twp/detection.h"
#include "twp/dialog.h"
#include "twp/ggpack.h"
#include "twp/hud.h"
#include "twp/object.h"
#include "twp/resmanager.h"
#include "twp/room.h"
#include "twp/savegame.h"
#include "twp/squtil.h"
#include "twp/time.h"

namespace Twp {

const static uint32 savegameKey[] = {0xAEA4EDF3, 0xAFF8332A, 0xB5A2DBB4, 0x9B4BA022};

static Common::SharedPtr<Object> actor(const Common::String &key) {
	for (size_t i = 0; i < g_twp->_actors.size(); i++) {
		Common::SharedPtr<Object> a = g_twp->_actors[i];
		if (a->_key == key)
			return a;
	}
	return nullptr;
}

static Common::SharedPtr<Object> actor(int id) {
	for (size_t i = 0; i < g_twp->_actors.size(); i++) {
		Common::SharedPtr<Object> a = g_twp->_actors[i];
		if (a->getId() == id)
			return a;
	}
	return nullptr;
}

static DialogConditionMode parseMode(char mode) {
	switch (mode) {
	case '?':
		return Once;
	case '#':
		return ShowOnce;
	case '&':
		return OnceEver;
	case '$':
		return ShowOnceEver;
	case '^':
		return TempOnce;
	default:
		warning("Invalid dialog condition mode: %c", mode);
		return TempOnce;
	}
}

static DialogConditionState parseState(Common::String &dialog) {
	Common::String dialogName;
	size_t i = 1;
	while (i < dialog.size() && !Common::isDigit(dialog[i])) {
		dialogName += dialog[i];
		i++;
	}

	while (!g_twp->_pack->assetExists((dialogName + ".byack").c_str()) && (i < dialog.size())) {
		dialogName += dialog[i];
		i++;
	}

	Common::String num;
	while ((i < dialog.size()) && Common::isDigit(dialog[i])) {
		num += dialog[i];
		i++;
	}

	DialogConditionState result;
	result.mode = parseMode(dialog[0]);
	result.dialog = Common::move(dialogName);
	result.line = atol(num.c_str());
	result.actorKey = dialog.substr(i);
	return result;
}

static Common::SharedPtr<Room> room(const Common::String &name) {
	for (size_t i = 0; i < g_twp->_rooms.size(); i++) {
		Common::SharedPtr<Room> r(g_twp->_rooms[i]);
		if (r->_name == name) {
			return r;
		}
	}
	return nullptr;
}

static Common::SharedPtr<Object> object(Common::SharedPtr<Room> room, const Common::String &key) {
	for (size_t i = 0; i < room->_layers.size(); i++) {
		Common::SharedPtr<Layer> layer = room->_layers[i];
		for (size_t j = 0; j < layer->_objects.size(); j++) {
			Common::SharedPtr<Object> o = layer->_objects[j];
			if (o->_key == key)
				return o;
		}
	}
	return nullptr;
}

static Common::SharedPtr<Object> object(const Common::String &key) {
	for (size_t i = 0; i < g_twp->_rooms.size(); i++) {
		Common::SharedPtr<Room> r(g_twp->_rooms[i]);
		for (size_t j = 0; j < r->_layers.size(); j++) {
			Common::SharedPtr<Layer> layer(r->_layers[j]);
			for (size_t k = 0; k < layer->_objects.size(); k++) {
				Common::SharedPtr<Object> o(layer->_objects[k]);
				if (o->_key == key)
					return o;
			}
		}
	}
	return nullptr;
}

static SQRESULT toSquirrel(const Common::JSONValue *json, HSQOBJECT &obj) {
	HSQUIRRELVM v = g_twp->getVm();
	SQInteger top = sq_gettop(v);
	sq_resetobject(&obj);
	if (json->isString()) {
		sqpush(v, json->asString());
		if (SQ_FAILED(sqget(v, -1, obj)))
			return sq_throwerror(v, "failed to get string");
	} else if (json->isIntegerNumber()) {
		sqpush(v, static_cast<int>(json->asIntegerNumber()));
		if (SQ_FAILED(sqget(v, -1, obj)))
			return sq_throwerror(v, "failed to get int");
	} else if (json->isBool()) {
		sqpush(v, json->asBool());
		if (SQ_FAILED(sqget(v, -1, obj)))
			return sq_throwerror(v, "failed to get bool");
	} else if (json->isNumber()) {
		sqpush(v, json->asNumber());
		if (SQ_FAILED(sqget(v, -1, obj)))
			return sq_throwerror(v, "failed to get float");
	} else if (json->isNull()) {
	} else if (json->isArray()) {
		sq_newarray(v, 0);
		const Common::JSONArray &jArr = json->asArray();
		for (size_t i = 0; i < jArr.size(); i++) {
			HSQOBJECT tmp;
			if (SQ_FAILED(toSquirrel(jArr[i], tmp)))
				return sq_throwerror(v, "failed to get array element");
			sqpush(v, tmp);
			sq_arrayappend(v, -2);
		}
		if (SQ_FAILED(sqget(v, -1, obj)))
			return sq_throwerror(v, "failed to get array");
	} else if (json->isObject()) {
		const Common::JSONObject &jObject = json->asObject();
		// check if it's a reference to an actor
		if (jObject.contains("_actorKey")) {
			obj = actor(jObject["_actorKey"]->asString())->_table;
		} else if (jObject.contains("_roomKey")) {
			Common::String roomName = jObject["_roomKey"]->asString();
			if (jObject.contains("_objectKey")) {
				Common::String objName = jObject["_objectKey"]->asString();
				Common::SharedPtr<Room> r = room(roomName);
				if (!r)
					warning("room with key=%s not found", roomName.c_str());
				else {
					Common::SharedPtr<Object> o = object(r, objName);
					if (!o)
						warning("room object with key=%s/%s not found", roomName.c_str(), objName.c_str());
					else
						obj = o->_table;
				}
			} else {
				Common::SharedPtr<Room> r = room(roomName);
				if (!r) {
					warning("room with key=%s not found", roomName.c_str());
				} else {
					obj = r->_table;
				}
			}
		} else if (jObject.contains("_objectKey")) {
			Common::String objName = jObject["_objectKey"]->asString();
			Common::SharedPtr<Object> o = object(objName);
			if (!o) {
				warning("object with key=%s not found", objName.c_str());
			} else {
				obj = o->_table;
			}
		} else {
			sq_newtable(v);
			for (auto it = jObject.begin(); it != jObject.end(); it++) {
				sqpush(v, it->_key);
				HSQOBJECT tmp;
				if (SQ_FAILED(toSquirrel(it->_value, tmp)))
					return sq_throwerror(v, "failed to get table element");
				sqpush(v, tmp);
				sq_newslot(v, -3, SQFalse);
			}
			if (SQ_FAILED(sqget(v, -1, obj)))
				return sq_throwerror(v, "failed to get table");
		}
	}
	sq_addref(v, &obj);
	sq_settop(v, top);
	return SQ_OK;
}

static Common::String sub(const Common::String &s, size_t pos, size_t end) {
	return s.substr(pos, s.size() - end);
}

static void setAnimations(Common::SharedPtr<Object> actor, const Common::JSONArray &anims) {
	Common::String headAnim = anims[0]->asString();
	Common::String standAnim = sub(anims[9]->asString(), 0, 7);
	Common::String walkAnim = sub(anims[11]->asString(), 0, 7);
	Common::String reachAnim = sub(anims[15]->asString(), 0, 11);
	actor->setAnimationNames(headAnim, standAnim, walkAnim, reachAnim);
}

static SQRESULT loadActor(Common::SharedPtr<Object> actor, const Common::JSONObject &json) {
	HSQUIRRELVM v = g_twp->getVm();
	actor->setTouchable(!json.contains("_untouchable") || json["_untouchable"]->asIntegerNumber() != 1);
	actor->_node->setVisible(!json.contains("_hidden") || json["_hidden"]->asIntegerNumber() != 1);
	actor->_volume = json.contains("_volume") ? json["_volume"]->asNumber() : 1.0f;
	for (auto it = json.begin(); it != json.end(); it++) {
		if (it->_key == "_animations") {
			setAnimations(actor, it->_value->asArray());
		} else if (it->_key == "_pos") {
			actor->_node->setPos(parseVec2(it->_value->asString()));
		} else if (it->_key == "_costume") {
			Common::String sheet;
			if (json.contains("_costumeSheet"))
				sheet = json["_costumeSheet"]->asString();
			actor->setCostume(it->_value->asString(), sheet);
		} else if (it->_key == "_costumeSheet") {
		} else if (it->_key == "_color") {
			actor->_node->setColor(Color::rgb(it->_value->asIntegerNumber()));
			actor->_node->setAlpha(Color::fromRgba(it->_value->asIntegerNumber()).rgba.a);
		} else if (it->_key == "_dir") {
			actor->setFacing((Facing)it->_value->asIntegerNumber());
		} else if (it->_key == "_lockFacing") {
			actor->lockFacing(it->_value->asIntegerNumber());
		} else if (it->_key == "_useDir") {
			actor->_useDir = (Direction)it->_value->asIntegerNumber();
		} else if (it->_key == "_usePos") {
			actor->_usePos = parseVec2(it->_value->asString());
		} else if (it->_key == "_offset") {
			actor->_node->setOffset(parseVec2(it->_value->asString()));
		} else if (it->_key == "_renderOffset") {
			actor->_node->setRenderOffset(parseVec2(it->_value->asString()));
		} else if (it->_key == "_roomKey") {
			Object::setRoom(actor, room(it->_value->asString()));
		} else if ((it->_key == "_hidden") || (it->_key == "_untouchable") || (it->_key == "_volume")) {
		} else {
			if (!it->_key.hasPrefix("_")) {
				HSQOBJECT tmp;
				if (SQ_FAILED(toSquirrel(it->_value, tmp)))
					return sq_throwerror(v, "failed to get table object");
				if (sqrawexists(actor->_table, it->_key))
					sqsetf(actor->_table, it->_key, tmp);
				else
					sqnewf(actor->_table, it->_key, tmp);
			} else {
				warning("load actor: key '%s' is unknown", it->_key.c_str());
			}
		}
	}
	return SQ_OK;
}

static SQRESULT loadObject(Common::SharedPtr<Object> obj, const Common::JSONObject &json) {
	HSQUIRRELVM v = g_twp->getVm();
	int state = 0;
	if (json.contains("_state"))
		state = json["_state"]->asIntegerNumber();
	if (obj->_node)
		obj->setState(state, true);
	else
		warning("obj '%s' has no node !", obj->_key.c_str());
	bool touchable = true;
	if (json.contains("_touchable"))
		touchable = json["_touchable"]->asIntegerNumber() == 1;
	obj->setTouchable(touchable);
	bool hidden = false;
	if (json.contains("_hidden"))
		hidden = json["_hidden"]->asIntegerNumber() == 1;
	obj->_node->setVisible(!hidden);

	for (auto it = json.begin(); it != json.end(); it++) {
		if ((it->_key == "_state") || (it->_key == "_touchable") || (it->_key == "_hidden")) {
			// discard
		} else if (it->_key == "_pos") {
			obj->_node->setPos(parseVec2(it->_value->asString()));
		} else if (it->_key == "_rotation") {
			obj->_node->setRotation(it->_value->asNumber());
		} else if (it->_key == "_dir") {
			obj->setFacing((Facing)it->_value->asIntegerNumber());
		} else if (it->_key == "_useDir") {
			obj->_useDir = (Direction)it->_value->asIntegerNumber();
		} else if (it->_key == "_usePos") {
			obj->_usePos = parseVec2(it->_value->asString());
		} else if (it->_key == "_offset") {
			obj->_node->setOffset(parseVec2(it->_value->asString()));
		} else if (it->_key == "_renderOffset") {
			obj->_node->setRenderOffset(parseVec2(it->_value->asString()));
		} else if (it->_key == "_roomKey") {
			Object::setRoom(obj, room(it->_value->asString()));
		} else if (!it->_key.hasPrefix("_")) {
			HSQOBJECT tmp;
			if (SQ_FAILED(toSquirrel(it->_value, tmp)))
				return sq_throwerror(v, "failed to get table object");
			if (sqrawexists(obj->_table, it->_key))
				sqsetf(obj->_table, it->_key, tmp);
			else
				sqnewf(obj->_table, it->_key, tmp);
		} else {
			warning("load object (%s): key '%s' is unknown", obj->_key.c_str(), it->_key.c_str());
		}
	}

	if (sqrawexists(obj->_table, "postLoad"))
		sqcall(obj->_table, "postLoad");

	return SQ_OK;
}

static void loadPseudoObjects(Common::SharedPtr<Room> room, const Common::JSONObject &json) {
	for (auto it = json.begin(); it != json.end(); it++) {
		Common::SharedPtr<Object> o(object(room, it->_key));
		if (!o)
			warning("load: room '%s' object '%s' not loaded because it has not been found", room->_name.c_str(), it->_key.c_str());
		else
			loadObject(o, it->_value->asObject());
	}
}

static SQRESULT loadRoom(Common::SharedPtr<Room> room, const Common::JSONObject &json) {
	HSQUIRRELVM v = g_twp->getVm();
	for (auto it = json.begin(); it != json.end(); it++) {
		if (it->_key == "_pseudoObjects") {
			loadPseudoObjects(room, it->_value->asObject());
		} else {
			if (!it->_key.hasPrefix("_")) {
				Common::SharedPtr<Object> o(object(room, it->_key));
				if (!o) {
					HSQOBJECT tmp;
					if (SQ_FAILED(toSquirrel(it->_value, tmp)))
						return sq_throwerror(v, "failed to get table object");
					if (sqrawexists(room->_table, it->_key))
						sqsetf(room->_table, it->_key, tmp);
					else {
						sqnewf(room->_table, it->_key, tmp);
					}
				} else {
					loadObject(o, it->_value->asObject());
				}
			} else {
				warning("Load room: key '{%s}' is unknown", it->_key.c_str());
			}
		}
	}

	if (sqrawexists(room->_table, "postLoad"))
		sqcall(room->_table, "postLoad");

	return SQ_OK;
}

static void setActor(const Common::String &key) {
	if (key.empty())
		return;
	for (size_t i = 0; i < g_twp->_actors.size(); i++) {
		Common::SharedPtr<Object> a = g_twp->_actors[i];
		if (a->_key == key) {
			g_twp->setActor(a, false);
			return;
		}
	}
}

static int32 computeHash(byte *data, size_t n) {
	int32 result = 0x6583463;
	for (size_t i = 0; i < n; i++) {
		result += (int32)data[i];
	}
	return result;
}

bool SaveGameManager::loadGame(Common::SeekableReadStream &stream) {
	Common::Array<byte> data(500016);
	stream.read(data.data(), data.size());
	Common::BTEACrypto::decrypt((uint32 *)data.data(), data.size() / 4, savegameKey);

	MemStream ms;
	if (!ms.open(data.data(), data.size() - 16))
		return false;

	GGHashMapDecoder decoder;
	Common::JSONValue *jSavegame = decoder.open(&ms);
	if (!jSavegame)
		return false;

	// dump savegame as json
	if (DebugMan.isDebugChannelEnabled(kDebugGame)) {
		debugC(kDebugGame, "load game: %s", jSavegame->stringify().c_str());
	}

	const Common::JSONObject &json = jSavegame->asObject();
	long long int version = json["version"]->asIntegerNumber();
	if (version != 2) {
		delete jSavegame;
		error("Cannot load savegame version %lld", version);
		return false;
	}
	uint32 gameTime = (uint32)json["gameTime"]->asNumber();

	sqcall("preLoad");
	loadGameScene(json["gameScene"]->asObject());
	loadDialog(json["dialog"]->asObject());
	if (SQ_FAILED(loadCallbacks(json["callbacks"]->asObject()))) {
		delete jSavegame;
		return false;
	}
	if (SQ_FAILED(loadGlobals(json["globals"]->asObject()))) {
		delete jSavegame;
		return false;
	}
	if (SQ_FAILED(loadActors(json["actors"]->asObject()))) {
		delete jSavegame;
		return false;
	}
	loadInventory(json["inventory"]);
	if (SQ_FAILED(loadRooms(json["rooms"]->asObject()))) {
		delete jSavegame;
		return false;
	}
	g_twp->_time = (float)gameTime;
	g_twp->setTotalPlayTime(gameTime * 1000);
	g_twp->_inputState.setState((InputStateFlag)json["inputState"]->asIntegerNumber());
	if (SQ_FAILED(loadObjects(json["objects"]->asObject()))) {
		delete jSavegame;
		return false;
	}
	g_twp->setRoom(room(json["currentRoom"]->asString()), true);
	setActor(json["selectedActor"]->asString());
	if (g_twp->_actor)
		g_twp->cameraAt(g_twp->_actor->_node->getPos());

	HSQUIRRELVM v = g_twp->getVm();
	sqsetf(sqrootTbl(v), "SAVEBUILD", static_cast<int>(json["savebuild"]->asIntegerNumber()));

	for (auto a : g_twp->_actors) {
		if (sqrawexists(a->_table, "postLoad")) {
			sqcall(a->_table, "postLoad");
		}
	}

	sqcall("postLoad");

	delete jSavegame;
	return true;
}

bool SaveGameManager::getSaveGame(Common::SeekableReadStream *stream, SaveGame &savegame) {
	Common::Array<byte> data(stream->size());
	stream->read(data.data(), data.size());
	Common::BTEACrypto::decrypt((uint32 *)data.data(), data.size() / 4, savegameKey);
	savegame.hashData = *(int32 *)(&data[data.size() - 16]);
	savegame.time = *(int32 *)&data[data.size() - 12];
	int32 hashCheck = computeHash(data.data(), data.size() - 16);
	if (savegame.hashData != hashCheck)
		return false;

	MemStream ms;
	if (!ms.open(data.data(), data.size() - 16))
		return false;

	GGHashMapDecoder decoder;
	savegame.jSavegame.reset(decoder.open(&ms));
	if (!savegame.jSavegame)
		return false;

	const Common::JSONObject &jSavegame = savegame.jSavegame->asObject();
	savegame.gameTime = jSavegame["gameTime"]->asNumber();
	savegame.easyMode = jSavegame["easy_mode"]->asIntegerNumber() != 0;

	return true;
}

void SaveGameManager::loadGameScene(const Common::JSONObject &json) {
	int mode = 0;
	if (json["actorsSelectable"]->asIntegerNumber() != 0)
		mode |= asOn;
	if (json["actorsTempUnselectable"]->asIntegerNumber())
		mode |= asTemporaryUnselectable;
	g_twp->_actorSwitcher._mode = mode;
	// TODO: tmpPrefs().forceTalkieText = json["forceTalkieText"].getInt() != 0;
	const Common::JSONArray &jSelectableActors = json["selectableActors"]->asArray();
	for (size_t i = 0; i < jSelectableActors.size(); i++) {
		const Common::JSONObject &jSelectableActor = jSelectableActors[i]->asObject();
		Common::SharedPtr<Object> act = jSelectableActor.contains("_actorKey") ? actor(jSelectableActor["_actorKey"]->asString()) : nullptr;
		g_twp->_hud->_actorSlots[i].actor = act;
		g_twp->_hud->_actorSlots[i].selectable = jSelectableActor["selectable"]->asIntegerNumber() != 0;
	}
}

void SaveGameManager::loadDialog(const Common::JSONObject &json) {
	debugC(kDebugGame, "loadDialog");
	g_twp->_dialog->_states.clear();
	for (auto it = json.begin(); it != json.end(); it++) {
		Common::String dialog(it->_key);
		// dialog format: mode dialog number actor
		// example: #ChetAgentStreetDialog14reyes
		// mode:
		// ?: once
		// #: showonce
		// &: onceever
		// $: showonceever
		// ^: temponce
		DialogConditionState state = parseState(dialog);
		g_twp->_dialog->_states.push_back(state);
		// TODO: what to do with this dialog value ?
		// let value = property.second.getInt()
	}
}

struct GetHObjects {
	explicit GetHObjects(Common::Array<HSQOBJECT> &objs) : _objs(objs) {}

	void operator()(HSQOBJECT item) {
		_objs.push_back(item);
	}

private:
	Common::Array<HSQOBJECT> &_objs;
};

SQRESULT SaveGameManager::loadCallbacks(const Common::JSONObject &json) {
	HSQUIRRELVM v = g_twp->getVm();
	debugC(kDebugGame, "loadCallbacks");
	g_twp->_callbacks.clear();
	if (!json["callbacks"]->isNull()) {
		const Common::JSONArray &jCallbacks = json["callbacks"]->asArray();
		for (size_t i = 0; i < jCallbacks.size(); i++) {
			const Common::JSONObject &jCallBackHash = jCallbacks[i]->asObject();
			const int id = jCallBackHash["guid"]->asIntegerNumber();
			const float time = jCallBackHash["time"]->isIntegerNumber() ? (float)jCallBackHash["time"]->asIntegerNumber() : 0.f;
			const Common::String name = jCallBackHash["function"]->asString();
			Common::Array<HSQOBJECT> args;
			if (jCallBackHash.contains("param")) {
				HSQOBJECT arg;
				if (SQ_FAILED(toSquirrel(jCallBackHash["param"], arg)))
					return sq_throwerror(v, "failed to get callback arg");
				sqgetitems(arg, GetHObjects(args));
			}
			g_twp->_callbacks.push_back(Common::SharedPtr<Callback>(new Callback(id, time, name, args)));
		}
	}
	g_twp->_resManager->resetIds(json["nextGuid"]->asIntegerNumber());
	return SQ_OK;
}

SQRESULT SaveGameManager::loadGlobals(const Common::JSONObject &json) {
	debugC(kDebugGame, "loadGlobals");
	HSQUIRRELVM v = g_twp->getVm();
	HSQOBJECT g;
	if (SQ_FAILED(sqgetf("g", g)))
		return sq_throwerror(v, "Failed to get globals variable");
	for (auto it = json.begin(); it != json.end(); it++) {
		HSQOBJECT tmp;
		if (SQ_FAILED(toSquirrel(it->_value, tmp)))
			return sq_throwerror(v, "failed to get callback arg");
		sq_addref(v, &tmp);
		sqsetf(g, it->_key, tmp);
	}
	return SQ_OK;
}

SQRESULT SaveGameManager::loadActors(const Common::JSONObject &json) {
	HSQUIRRELVM v = g_twp->getVm();
	for (size_t i = 0; i < g_twp->_actors.size(); i++) {
		Common::SharedPtr<Object> a = g_twp->_actors[i];
		if (a->_key.size() > 0) {
			if (SQ_FAILED(loadActor(a, json[a->_key]->asObject())))
				return sq_throwerror(v, "failed to load actor");
		}
	}
	return SQ_OK;
}

void SaveGameManager::loadInventory(const Common::JSONValue *json) {
	if (json->isObject()) {
		const Common::JSONObject &jInventory = json->asObject();
		const Common::JSONArray &jSlots = jInventory["slots"]->asArray();
		for (int i = 0; i < NUMACTORS; i++) {
			Common::SharedPtr<Object> a(g_twp->_hud->_actorSlots[i].actor);
			if (a) {
				a->_inventory.clear();
				const Common::JSONObject &jSlot = jSlots[i]->asObject();
				if (jSlot.contains("objects")) {
					if (jSlot["objects"]->isArray()) {
						int jiggleCount = 0;
						const Common::JSONArray &jSlotObjects = jSlot["objects"]->asArray();
						for (size_t j = 0; j < jSlotObjects.size(); j++) {
							const Common::JSONValue *jObj = jSlotObjects[j];
							Common::SharedPtr<Object> obj = object(jObj->asString());
							if (!obj)
								warning("inventory obj '%s' not found", jObj->asString().c_str());
							else {
								Object::pickupObject(a, obj);
								obj->_jiggle = jSlot.contains("jiggle") && jSlot["jiggle"]->isArray() && jSlot["jiggle"]->asArray()[jiggleCount++]->asIntegerNumber() != 0;
							}
						}
					}
				}
				a->_inventoryOffset = jSlot["scroll"]->asIntegerNumber();
			}
		}
	}
}

SQRESULT SaveGameManager::loadRooms(const Common::JSONObject &json) {
	HSQUIRRELVM v = g_twp->getVm();
	for (auto it = json.begin(); it != json.end(); it++) {
		if (SQ_FAILED(loadRoom(room(it->_key), it->_value->asObject())))
			return sq_throwerror(v, "failed to load room");
	}
	return SQ_OK;
}

SQRESULT SaveGameManager::loadObjects(const Common::JSONObject &json) {
	HSQUIRRELVM v = g_twp->getVm();
	for (auto it = json.begin(); it != json.end(); it++) {
		Common::SharedPtr<Object> o(object(it->_key));
		if (o) {
			if (SQ_FAILED(loadObject(o, it->_value->asObject())))
				return sq_throwerror(v, "failed to load object");
		} else {
			warning("object '%s' not found", it->_key.c_str());
		}
	}
	return SQ_OK;
}

struct JsonCallback {
	bool skipObj;
	bool pseudo;
	HSQOBJECT *rootTable;
	Common::JSONObject *jObj;
};

static Common::JSONValue *tojson(const HSQOBJECT &obj, bool checkId, bool skipObj = false, bool pseudo = false);

static void fillMissingProperties(const Common::String &k, HSQOBJECT &oTable, void *data) {
	JsonCallback *params = static_cast<JsonCallback *>(data);
	if ((k.size() > 0) && (!k.hasPrefix("_"))) {
		if (!(params->skipObj && g_twp->_resManager->isObject(getId(oTable)) && (params->pseudo || sqrawexists(*params->rootTable, k)))) {
			Common::JSONValue *json = tojson(oTable, true);
			if (json) {
				(*params->jObj)[k] = json;
			}
		}
	}
}

struct GetJArray {
	explicit GetJArray(Common::JSONArray &arr) : _arr(arr) {}

	void operator()(HSQOBJECT item) {
		_arr.push_back(tojson(item, true));
	}

private:
	Common::JSONArray &_arr;
};

static SQRESULT toObject(Common::JSONObject &jObj, const HSQOBJECT &obj, bool checkId, bool skipObj = false, bool pseudo = false) {
	HSQUIRRELVM v = g_twp->getVm();
	if (checkId) {
		SQInteger id = 0;
		if (sqrawexists(obj, "_id") && SQ_FAILED(sqgetf(obj, "_id", id)))
			return sq_throwerror(v, "Failed to get id");
		if (g_twp->_resManager->isActor(id)) {
			Common::SharedPtr<Object> a(actor(id));
			jObj["_actorKey"] = new Common::JSONValue(a->_key);
			return SQ_OK;
		}

		if (g_twp->_resManager->isObject(id)) {
			Common::SharedPtr<Object> o(sqobj(id));
			if (!o)
				return SQ_OK;
			jObj["_objectKey"] = new Common::JSONValue(o->_key);
			if (o->_room && o->_room->_pseudo)
				jObj["_roomKey"] = new Common::JSONValue(o->_room->_name);
			return SQ_OK;
		}

		if (g_twp->_resManager->isRoom(id)) {
			Common::SharedPtr<Room> r(getRoom(id));
			jObj["_roomKey"] = new Common::JSONValue(r->_name);
			return SQ_OK;
		}
	}

	HSQOBJECT rootTbl = sqrootTbl(v);

	JsonCallback params;
	params.jObj = &jObj;
	params.pseudo = pseudo;
	params.rootTable = &rootTbl;
	params.skipObj = skipObj;
	return sqgetpairs(obj, fillMissingProperties, &params);
}

static Common::JSONValue *tojson(const HSQOBJECT &obj, bool checkId, bool skipObj, bool pseudo) {
	switch (obj._type) {
	case OT_INTEGER:
		return new Common::JSONValue(static_cast<long long>(sq_objtointeger(&obj)));
	case OT_FLOAT:
		return new Common::JSONValue(sq_objtofloat(&obj));
	case OT_STRING:
		return new Common::JSONValue(sq_objtostring(&obj));
	case OT_NULL:
		return new Common::JSONValue();
	case OT_ARRAY: {
		Common::JSONArray arr;
		sqgetitems(obj, GetJArray(arr));
		return new Common::JSONValue(arr);
	}
	case OT_TABLE: {
		Common::JSONObject jObj;
		toObject(jObj, obj, checkId, skipObj, pseudo);
		return new Common::JSONValue(jObj);
	}
	default:
		return nullptr;
	}
}

static Common::String removeFileExt(const Common::String &s) {
	size_t i = s.findLastOf('.');
	if (i != Common::String::npos) {
		return s.substr(0, i);
	}
	return s;
}

Common::String toString(const Math::Vector2d &pos) {
	return Common::String::format("{%d,%d}", (int)pos.getX(), (int)pos.getY());
}

// static Common::String getCustomAnim(Common::SharedPtr<Object> actor, const Common::String &name) {
// 	return actor->_animNames.contains(name) ? actor->_animNames[name] : name;
// }

// static Common::JSONValue *getCustomAnims(Common::SharedPtr<Object> actor) {
// 	Common::JSONArray jAnims;
// 	// add head anims
// 	jAnims.push_back(new Common::JSONValue(getCustomAnim(actor, HEAD_ANIMNAME)));
// 	for (int i = 1; i < 7; i++) {
// 		jAnims.push_back(new Common::JSONValue(getCustomAnim(actor, Common::String::format("%s%d", HEAD_ANIMNAME, i))));
// 	}
// 	// add stand anims
// 	jAnims.push_back(new Common::JSONValue(getCustomAnim(actor, Common::String::format("%s%s", STAND_ANIMNAME, "_front"))));
// 	jAnims.push_back(new Common::JSONValue(getCustomAnim(actor, Common::String::format("%s%s", STAND_ANIMNAME, "_back"))));
// 	jAnims.push_back(new Common::JSONValue(getCustomAnim(actor, Common::String::format("%s%s", STAND_ANIMNAME, "_left"))));
// 	jAnims.push_back(new Common::JSONValue(getCustomAnim(actor, Common::String::format("%s%s", STAND_ANIMNAME, "_right"))));
// 	// add walk anims
// 	jAnims.push_back(new Common::JSONValue(getCustomAnim(actor, Common::String::format("%s%s", WALK_ANIMNAME, "_front"))));
// 	jAnims.push_back(new Common::JSONValue(getCustomAnim(actor, Common::String::format("%s%s", WALK_ANIMNAME, "_back"))));
// 	jAnims.push_back(new Common::JSONValue(getCustomAnim(actor, Common::String::format("%s%s", WALK_ANIMNAME, "_right"))));
// 	jAnims.push_back(new Common::JSONValue(getCustomAnim(actor, Common::String::format("%s%s", WALK_ANIMNAME, "_right"))));
// 	// add reach anims
// 	const char *dirs[] = {"_front", "_back", "_right", "_right"};
// 	for (int i = 0; i < ARRAYSIZE(dirs); i++) {
// 		const char *dir = dirs[i];
// 		jAnims.push_back(new Common::JSONValue(getCustomAnim(actor, Common::String::format("%s%s%s", REACH_ANIMNAME, "_low", dir))));
// 		jAnims.push_back(new Common::JSONValue(getCustomAnim(actor, Common::String::format("%s%s%s", REACH_ANIMNAME, "_med", dir))));
// 		jAnims.push_back(new Common::JSONValue(getCustomAnim(actor, Common::String::format("%s%s%s", REACH_ANIMNAME, "_high", dir))));
// 	}
// 	return new Common::JSONValue(jAnims);
// }

static Common::JSONValue *createJActor(Common::SharedPtr<Object> actor) {
	Common::JSONObject jActor;
	toObject(jActor, actor->_table, false);
	int color = actor->_node->getComputedColor().toInt();
	if (color != Color().toInt())
		jActor["_color"] = new Common::JSONValue((long long int)color);
	// if (!actor->_animNames.empty())
	// 	jActor["_animations"] = getCustomAnims(actor);
	jActor["_costume"] = new Common::JSONValue(removeFileExt(actor->_costumeName));
	jActor["_dir"] = new Common::JSONValue((long long int)actor->_facing);
	jActor["_lockFacing"] = new Common::JSONValue((long long int)actor->_facingLockValue);
	jActor["_pos"] = new Common::JSONValue(toString(actor->_node->getPos()));
	if (actor->_useDir != dNone)
		jActor["_useDir"] = new Common::JSONValue((long long int)actor->_useDir);
	if (actor->_usePos != Math::Vector2d(0.f, 0.f))
		jActor["_usePos"] = new Common::JSONValue(toString(actor->_usePos));
	if (actor->_node->getRenderOffset() != Math::Vector2d(0.f, 45.f))
		jActor["_renderOffset"] = new Common::JSONValue(toString(actor->_node->getRenderOffset()));
	if (actor->_costumeSheet.size() > 0)
		jActor["_costumeSheet"] = new Common::JSONValue(actor->_costumeSheet);
	if (actor->_room)
		jActor["_roomKey"] = new Common::JSONValue(actor->_room->_name);
	if (!actor->isTouchable() && actor->_node->isVisible())
		jActor["_untouchable"] = new Common::JSONValue(1LL);
	if (!actor->_node->isVisible())
		jActor["_hidden"] = new Common::JSONValue(1LL);
	if (actor->_volume != 1.f)
		jActor["_volume"] = new Common::JSONValue(actor->_volume);
	//   result.fields.sort(cmpKey);
	return new Common::JSONValue(jActor);
}

static Common::JSONValue *createJActors() {
	Common::JSONObject jActors;
	for (size_t i = 0; i < g_twp->_actors.size(); i++) {
		Common::SharedPtr<Object> a(g_twp->_actors[i]);
		if (a->_key.size() > 0) {
			jActors[a->_key] = createJActor(a);
		}
	}
	// result.fields.sort(cmpKey);
	return new Common::JSONValue(jActors);
}

static Common::JSONValue *createJCallback(const Callback &callback) {
	Common::JSONObject result;
	result["function"] = new Common::JSONValue(callback.getName());
	result["guid"] = new Common::JSONValue((long long int)callback.getId());
	result["time"] = new Common::JSONValue((long long int)MAX(0.f, (callback.getDuration() - callback.getElapsed())));
	Common::JSONArray jArgs;
	const Common::Array<HSQOBJECT> &args = callback.getArgs();
	for (size_t i = 0; i < args.size(); i++) {
		jArgs.push_back(tojson(args[i], false));
	}
	if (jArgs.size() > 0)
		result["param"] = new Common::JSONValue(jArgs);
	return new Common::JSONValue(result);
}

static Common::JSONValue *createJCallbackArray() {
	Common::JSONArray result;
	for (size_t i = 0; i < g_twp->_callbacks.size(); i++) {
		result.push_back(createJCallback(*g_twp->_callbacks[i]));
	}
	return new Common::JSONValue(result);
}

static Common::JSONValue *createJCallbacks() {
	Common::JSONObject json;
	json["callbacks"] = createJCallbackArray();
	json["nextGuid"] = new Common::JSONValue((long long int)g_twp->_resManager->getCallbackId());
	return new Common::JSONValue(json);
}

static Common::JSONValue *createJRoomKey(Common::SharedPtr<Room> room) {
	return new Common::JSONValue(room ? room->_name : "Void");
}

Common::String createJDlgStateKey(const DialogConditionState &state) {
	Common::String s;
	switch (state.mode) {
	case OnceEver:
		s = "&";
		break;
	case ShowOnce:
		s = "#";
		break;
	case Once:
		s = "?";
		break;
	case ShowOnceEver:
		s = "$";
		break;
	default:
		break;
	}
	return Common::String::format("%s%s%d%s", s.c_str(), state.dialog.c_str(), state.line, state.actorKey.c_str());
}

static Common::JSONValue *createJDialog() {
	Common::JSONObject json;
	for (size_t i = 0; i < g_twp->_dialog->_states.size(); i++) {
		const DialogConditionState &state = g_twp->_dialog->_states[i];
		if (state.mode != TempOnce) {
			// TODO: value should be 1 or another value ?
			json[createJDlgStateKey(state)] = new Common::JSONValue(state.mode == ShowOnce ? 2LL : 1LL);
		}
	}
	return new Common::JSONValue(json);
}

static Common::JSONValue *createJEasyMode() {
	HSQOBJECT g;
	if (SQ_FAILED(sqgetf("g", g)))
		error("Failed to get globals variable");
	SQInteger easyMode;
	if (SQ_FAILED(sqgetf(g, "easy_mode", easyMode)))
		error("Failed to get easy_mode variable");
	return new Common::JSONValue((long long int)easyMode);
}

static Common::JSONValue *createJBool(bool value) {
	return new Common::JSONValue(value ? 1LL : 0LL);
}

static Common::JSONValue *createJSelectableActor(const ActorSlot &slot) {
	Common::JSONObject json;
	if (slot.actor) {
		json["_actorKey"] = new Common::JSONValue(slot.actor->_key);
	}
	json["selectable"] = createJBool(slot.selectable);
	return new Common::JSONValue(json);
}

static Common::JSONValue *createJSelectableActors() {
	Common::JSONArray json;
	for (int i = 0; i < NUMACTORS; i++) {
		const ActorSlot &slot = g_twp->_hud->_actorSlots[i];
		json.push_back(createJSelectableActor(slot));
	}
	return new Common::JSONValue(json);
}

static Common::JSONValue *createJGameScene() {
	bool actorsSelectable = asOn & g_twp->_actorSwitcher._mode;
	bool actorsTempUnselectable = asTemporaryUnselectable & g_twp->_actorSwitcher._mode;
	Common::JSONObject json;
	json["actorsSelectable"] = createJBool(actorsSelectable);
	json["actorsTempUnselectable"] = createJBool(actorsTempUnselectable);
	// TODO: json["forceTalkieText"] = createJBool(tmpPrefs().forceTalkieText);
	json["selectableActors"] = createJSelectableActors();
	return new Common::JSONValue(json);
}

static Common::JSONValue *createJGlobals() {
	HSQOBJECT g;
	if (SQ_FAILED(sqgetf("g", g)))
		error("Failed to get globals variable");
	//   result.fields.sort(cmpKey);
	return tojson(g, false);
}

static Common::JSONValue *createJInventory(const ActorSlot &slot) {
	Common::JSONObject json;
	if (!slot.actor) {
		json["scroll"] = new Common::JSONValue(0LL);
	} else {
		Common::JSONArray objKeys;
		Common::JSONArray jiggleArray;
		bool anyJiggle = false;
		for (size_t i = 0; i < slot.actor->_inventory.size(); i++) {
			Common::SharedPtr<Object> obj = slot.actor->_inventory[i];
			if (obj->_jiggle)
				anyJiggle = true;
			jiggleArray.push_back(createJBool(obj->_jiggle));
			objKeys.push_back(new Common::JSONValue(obj->_key));
		}

		if (objKeys.size() > 0) {
			json["objects"] = new Common::JSONValue(objKeys);
		}
		json["scroll"] = new Common::JSONValue((long long int)slot.actor->_inventoryOffset);
		if (anyJiggle)
			json["jiggle"] = new Common::JSONValue(jiggleArray);
	}
	return new Common::JSONValue(json);
}

static Common::JSONValue *createJInventory() {
	Common::JSONArray slots;
	for (int i = 0; i < NUMACTORS; i++) {
		const ActorSlot &slot = g_twp->_hud->_actorSlots[i];
		slots.push_back(createJInventory(slot));
	}
	Common::JSONObject json;
	json["slots"] = new Common::JSONValue(slots);
	return new Common::JSONValue(json);
}

static Common::JSONValue *createJObject(HSQOBJECT &table, Common::SharedPtr<Object> obj) {
	Common::JSONObject json;
	toObject(json, table, false);
	if (obj) {
		if (!obj->_node->isVisible())
			json["_hidden"] = new Common::JSONValue(1LL);
		if (obj->_state != 0)
			json["_state"] = new Common::JSONValue((long long int)obj->_state);
		if (obj->_node->isVisible() && !obj->isTouchable())
			json["_touchable"] = new Common::JSONValue(0LL);
		if (obj->_node->getOffset() != Math::Vector2d())
			json["_offset"] = new Common::JSONValue(toString(obj->_node->getOffset()));
	}
	//   result.fields.sort(cmpKey)
	return new Common::JSONValue(json);
}

static Common::JSONValue *createJObjects() {
	Common::JSONObject json;
	for (const auto &r : g_twp->_rooms) {
		for (const auto &layer : r->_layers) {
			for (auto &obj : layer->_objects) {
				if (obj->_objType != ObjectType::otNone)
					continue;
				if (obj->_room && obj->_room->_pseudo)
					continue;
				json[obj->_key] = createJObject(obj->_table, obj);
			}
		}
	}
	//   result.fields.sort(cmpKey)
	return new Common::JSONValue(json);
}

static Common::JSONValue *createJPseudoObjects(Common::SharedPtr<Room> room) {
	Common::JSONObject json;
	for (auto &layer : room->_layers) {
		for (auto &obj : layer->_objects) {
			if (obj->_objType != ObjectType::otNone)
				continue;
			if (obj->_room && obj->_room->_pseudo)
				continue;
			json[obj->_key] = createJObject(obj->_table, obj);
		}
	}

	//   result.fields.sort(cmpKey)
	return new Common::JSONValue(json);
}

static Common::JSONValue *createJRoom(Common::SharedPtr<Room> room) {
	Common::JSONObject json;
	toObject(json, room->_table, false, true, room->_pseudo);
	if (room->_pseudo) {
		json["_pseudoObjects"] = createJPseudoObjects(room);
	}
	//   result.fields.sort(cmpKey)
	return new Common::JSONValue(json);
}

static Common::JSONValue *createJRooms() {
	Common::JSONObject json;
	for (size_t i = 0; i < g_twp->_rooms.size(); i++) {
		Common::SharedPtr<Room> r(g_twp->_rooms[i]);
		if (r)
			json[r->_name] = createJRoom(r);
	}
	//   result.fields.sort(cmpKey);
	return new Common::JSONValue(json);
}

static Common::JSONValue *createSaveGame() {
	Common::JSONObject json;
	json["actors"] = createJActors();
	json["callbacks"] = createJCallbacks();
	json["currentRoom"] = createJRoomKey(g_twp->_room);
	json["dialog"] = createJDialog();
	json["easy_mode"] = createJEasyMode();
	json["gameGUID"] = new Common::JSONValue("");
	json["gameScene"] = createJGameScene();
	json["gameTime"] = new Common::JSONValue(g_twp->_time);
	json["globals"] = createJGlobals();
	json["inputState"] = new Common::JSONValue((long long int)g_twp->_inputState.getState());
	json["inventory"] = createJInventory();
	json["objects"] = createJObjects();
	json["rooms"] = createJRooms();
	json["savebuild"] = new Common::JSONValue(958LL);
	json["savetime"] = new Common::JSONValue((long long)getTime());
	json["selectedActor"] = new Common::JSONValue(g_twp->_actor ? g_twp->_actor->_key : "");
	json["version"] = new Common::JSONValue((long long int)2);
	return new Common::JSONValue(json);
}

void SaveGameManager::saveGame(Common::WriteStream *ws) {
	Common::JSONValue *data = createSaveGame();

	if (DebugMan.isDebugChannelEnabled(kDebugGame)) {
		debugC(kDebugGame, "save game: %s", data->stringify().c_str());
	}

	const uint32 fullSize = 500000;
	Common::Array<byte> buffer(fullSize + 16);
	OutMemStream stream;
	stream.open(buffer.data(), buffer.size());

	GGHashMapEncoder encoder;
	encoder.open(&stream);
	encoder.write(data->asObject());

	int32 savetime = data->asObject()["savetime"]->asIntegerNumber();
	int32 hash = computeHash(buffer.data(), fullSize);
	byte marker = (8 - ((fullSize + 9) % 8));

	// write at the end 16 bytes: hashdata (4 bytes) + savetime (4 bytes) + marker (8 bytes)
	int32 *p = (int32 *)(buffer.data() + fullSize);
	p[0] = hash;
	p[1] = savetime;
	memset(&p[2], marker, 8);

	// then encode data
	Common::BTEACrypto::encrypt((uint32 *)buffer.data(), buffer.size() / 4, savegameKey);

	// and write data
	ws->write(buffer.data(), buffer.size());
	delete data;

	sqcall("postSave");
}

} // namespace Twp
