/* 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 "gui/saveload-dialog.h"

#if defined(USE_CLOUD) && defined(USE_LIBCURL)
#include "backends/cloud/cloudmanager.h"
#include "backends/cloud/savessyncrequest.h"
#include "backends/networking/curl/connectionmanager.h"
#endif

#include "common/translation.h"
#include "common/config-manager.h"

#include "gui/message.h"
#include "gui/gui-manager.h"
#include "gui/ThemeEval.h"
#include "gui/widgets/edittext.h"

#include "graphics/scaler.h"
#include "common/savefile.h"
#include "engines/engine.h"

namespace GUI {

#define SCALEVALUE(val) ((val) * g_gui.getScaleFactor())

#if defined(USE_CLOUD) && defined(USE_LIBCURL)

enum {
	kCancelSyncCmd = 'PDCS',
	kBackgroundSyncCmd = 'PDBS'
};

SaveLoadCloudSyncProgressDialog::SaveLoadCloudSyncProgressDialog(bool canRunInBackground, SaveLoadChooserDialog *parent)
	: Dialog("SaveLoadCloudSyncProgress"), _close(false), _pollFrame(0), _parent(parent)
{
	_label = new StaticTextWidget(this, "SaveLoadCloudSyncProgress.TitleText", _("Downloading saves..."));
	uint32 progress = (uint32)(100 * CloudMan.getSyncDownloadingProgress());
	_progressBar = new SliderWidget(this, "SaveLoadCloudSyncProgress.ProgressBar");
	_progressBar->setMinValue(0);
	_progressBar->setMaxValue(100);
	_progressBar->setValue(progress);
	_progressBar->setEnabled(false);
	_percentLabel = new StaticTextWidget(this, "SaveLoadCloudSyncProgress.PercentText", Common::String::format("%u %%", progress));
	new ButtonWidget(this, "SaveLoadCloudSyncProgress.Cancel", _("Cancel"), Common::U32String(), kCancelSyncCmd, Common::ASCII_ESCAPE);	// Cancel dialog
	ButtonWidget *backgroundButton = new ButtonWidget(this, "SaveLoadCloudSyncProgress.Background", _("Run in background"), Common::U32String(), kBackgroundSyncCmd, Common::ASCII_RETURN);	// Confirm dialog
	backgroundButton->setEnabled(canRunInBackground);
}

SaveLoadCloudSyncProgressDialog::~SaveLoadCloudSyncProgressDialog() {
}

void SaveLoadCloudSyncProgressDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) {
	switch(cmd) {
	case kCancelSyncCmd:
		setResult(kCancelSyncCmd);
		close();
		break;

	case kBackgroundSyncCmd:
		_close = true;
		break;

	default:
		break;
	}

	Dialog::handleCommand(sender, cmd, data);
}

void SaveLoadCloudSyncProgressDialog::handleTickle() {
	pollCloudMan();

	if (_close) {
		setResult(kBackgroundSyncCmd);
		close();
	}

	Dialog::handleTickle();
}

void SaveLoadCloudSyncProgressDialog::pollCloudMan() {
	_pollFrame = (_pollFrame + 1) % 60;
	if (_pollFrame != 1)
		return;

	const bool syncing = CloudMan.isSyncing();
	const uint32 progress = (uint32)(100 * CloudMan.getSyncDownloadingProgress());

	if (!syncing || progress == 100) {
		_close = true;
	}

	Cloud::Storage::SyncDownloadingInfo info;
	CloudMan.getSyncDownloadingInfo(info);

	const char *downloadedUnits, *totalUnits;
	Common::String downloaded = Common::getHumanReadableBytes(info.bytesDownloaded, downloadedUnits);
	Common::String total = Common::getHumanReadableBytes(info.bytesToDownload, totalUnits);

	Common::String progressPercent = Common::String::format("%u %%", progress);
	Common::String filesDownloaded = Common::String::format("%llu", (unsigned long long)info.filesDownloaded);
	Common::String filesToDownload = Common::String::format("%llu", (unsigned long long)info.filesToDownload);

	_percentLabel->setLabel(
		Common::U32String::format(
			_("%s (%s %S / %s %S, %s / %s files)"),
			progressPercent.c_str(),
			downloaded.c_str(), _(downloadedUnits).c_str(),
			total.c_str(), _(totalUnits).c_str(),
			filesDownloaded.c_str(), filesToDownload.c_str()
		)
	);
	_progressBar->setValue(progress);
	_progressBar->markAsDirty();

	if (_parent) {
		_parent->updateSaveList();
		_parent->reflowLayout();
	}
}
#endif

#ifndef DISABLE_SAVELOADCHOOSER_GRID
SaveLoadChooserType getRequestedSaveLoadDialog(const MetaEngine *metaEngine) {
	const Common::String &userConfig = ConfMan.get("gui_saveload_chooser", Common::ConfigManager::kApplicationDomain);

	// Check (and update if necessary) the theme config here. This catches
	// resolution changes, which happened after the GUI was closed. This
	// should assure that the correct GUI width/height are returned below and
	// prevent the logic from picking the grid dialog, even though it is not
	// possible to use it.
	g_gui.checkScreenChange();

	if (g_gui.getGUIWidth() >= 640 && g_gui.getGUIHeight() >= 400
		&& metaEngine->hasFeature(MetaEngine::kSavesSupportMetaInfo)
		&& metaEngine->hasFeature(MetaEngine::kSavesSupportThumbnail)
		&& userConfig.equalsIgnoreCase("grid")) {
		// In case we are 640x400 or higher, this dialog is not in save mode,
		// the user requested the grid dialog and the engines supports it we
		// try to set it up.
		return kSaveLoadDialogGrid;
	} else {
		// In all other cases we want to use the list dialog.
		return kSaveLoadDialogList;
	}
}

enum {
	kListSwitchCmd = 'LIST',
	kGridSwitchCmd = 'GRID'
};
#endif // !DISABLE_SAVELOADCHOOSER_GRID

SaveLoadChooserDialog::SaveLoadChooserDialog(const Common::String &dialogName, const bool saveMode)
	: Dialog(dialogName), _metaEngine(nullptr), _delSupport(false), _metaInfoSupport(false),
	_thumbnailSupport(false), _saveDateSupport(false), _playTimeSupport(false), _saveMode(saveMode),
	_dialogWasShown(false)
#ifndef DISABLE_SAVELOADCHOOSER_GRID
	, _listButton(nullptr), _gridButton(nullptr)
#endif // !DISABLE_SAVELOADCHOOSER_GRID
#if defined(USE_CLOUD) && defined(USE_LIBCURL)
	, _pollFrame(0), _didUpdateAfterSync(true)
#endif
	{
#ifndef DISABLE_SAVELOADCHOOSER_GRID
	addChooserButtons();
#endif // !DISABLE_SAVELOADCHOOSER_GRID
}

SaveLoadChooserDialog::SaveLoadChooserDialog(int x, int y, int w, int h, const bool saveMode)
	: Dialog(x, y, w, h), _metaEngine(nullptr), _delSupport(false), _metaInfoSupport(false),
	_thumbnailSupport(false), _saveDateSupport(false), _playTimeSupport(false), _saveMode(saveMode),
	_dialogWasShown(false)
#ifndef DISABLE_SAVELOADCHOOSER_GRID
	, _listButton(nullptr), _gridButton(nullptr)
#endif // !DISABLE_SAVELOADCHOOSER_GRID
#if defined(USE_CLOUD) && defined(USE_LIBCURL)
	, _pollFrame(0), _didUpdateAfterSync(true)
#endif
	{
#ifndef DISABLE_SAVELOADCHOOSER_GRID
	addChooserButtons();
#endif // !DISABLE_SAVELOADCHOOSER_GRID
}

SaveLoadChooserDialog::~SaveLoadChooserDialog() {
}

void SaveLoadChooserDialog::open() {
	Dialog::open();

	// So that quitting ScummVM will not cause the dialog result to say a
	// saved game was selected.
	setResult(-1);

	_dialogWasShown = false;
}

void SaveLoadChooserDialog::close() {
	Dialog::close();
}

int SaveLoadChooserDialog::run(const Common::String &target, const MetaEngine *metaEngine) {
	_metaEngine = metaEngine;
	_target = target;
	_delSupport = _metaEngine->hasFeature(MetaEngine::kSupportsDeleteSave);
	_metaInfoSupport = _metaEngine->hasFeature(MetaEngine::kSavesSupportMetaInfo);
	_thumbnailSupport = _metaInfoSupport && _metaEngine->hasFeature(MetaEngine::kSavesSupportThumbnail);
	_saveDateSupport = _metaInfoSupport && _metaEngine->hasFeature(MetaEngine::kSavesSupportCreationDate);
	_playTimeSupport = _metaInfoSupport && _metaEngine->hasFeature(MetaEngine::kSavesSupportPlayTime);

	return runIntern();
}

void SaveLoadChooserDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) {
#ifndef DISABLE_SAVELOADCHOOSER_GRID
	switch (cmd) {
	case kListSwitchCmd:
		setResult(kSwitchSaveLoadDialog);
		// We save the requested dialog type here to avoid the setting to be
		// overwritten when our reflowLayout logic selects a different dialog
		// type.
		ConfMan.set("gui_saveload_chooser", "list", Common::ConfigManager::kApplicationDomain);
		close();
		break;

	case kGridSwitchCmd:
		setResult(kSwitchSaveLoadDialog);
		// See above.
		ConfMan.set("gui_saveload_chooser", "grid", Common::ConfigManager::kApplicationDomain);
		close();
		break;

	default:
		break;
	}
#endif // !DISABLE_SAVELOADCHOOSER_GRID

	return Dialog::handleCommand(sender, cmd, data);
}

#if defined(USE_CLOUD) && defined(USE_LIBCURL)
void SaveLoadChooserDialog::runSaveSync(bool hasSavepathOverride) {
	if (!CloudMan.isSyncing()) {
		if (hasSavepathOverride) {
			CloudMan.showCloudDisabledIcon();
		} else {
			CloudMan.syncSaves();
		}
	}
}
#endif

void SaveLoadChooserDialog::handleTickle() {
#if defined(USE_CLOUD) && defined(USE_LIBCURL)
	if (!_dialogWasShown && CloudMan.isSyncing()) {
		Common::Array<Common::String> files = CloudMan.getSyncingFiles();
		if (!files.empty()) {
			{
				SaveLoadCloudSyncProgressDialog dialog(_metaEngine ? _metaEngine->hasFeature(MetaEngine::kSimpleSavesNames) : false, this);
				int result = dialog.runModal();
				if (result == kCancelSyncCmd) {
					CloudMan.cancelSync();
				}
			}
			_dialogWasShown = true;
			updateSaveList();
		}
	}

	pollCloudMan();
#endif
	Dialog::handleTickle();
}

void SaveLoadChooserDialog::reflowLayout() {
#ifndef DISABLE_SAVELOADCHOOSER_GRID
	const SaveLoadChooserType currentType = getType();
	const SaveLoadChooserType requestedType = getRequestedSaveLoadDialog(_metaEngine);

	addChooserButtons();
	if (currentType == kSaveLoadDialogList) {
		_listButton->setEnabled(false);
	}
	if (currentType == kSaveLoadDialogGrid) {
		_gridButton->setEnabled(false);
	}

	// Change the dialog type if there is any need for it.
	if (requestedType != currentType) {
		setResult(kSwitchSaveLoadDialog);
		close();
	}
#endif // !DISABLE_SAVELOADCHOOSER_GRID

	Dialog::reflowLayout();
}

void SaveLoadChooserDialog::updateSaveList() {
#if defined(USE_CLOUD) && defined(USE_LIBCURL)
	Common::Array<Common::String> files = CloudMan.getSyncingFiles(); //returns empty array if not syncing
	g_system->getSavefileManager()->updateSavefilesList(files);
#endif
	listSaves();
}

void SaveLoadChooserDialog::listSaves() {
	if (!_metaEngine) return; //very strange
	_saveList = _metaEngine->listSaves(_target.c_str(), _saveMode);

#if defined(USE_CLOUD) && defined(USE_LIBCURL)
	//if there is Cloud support, add currently synced files as "locked" saves in the list
	if (_metaEngine->hasFeature(MetaEngine::kSimpleSavesNames)) {
		Common::String pattern = _target + ".###";
		Common::Array<Common::String> files = CloudMan.getSyncingFiles(); //returns empty array if not syncing
		for (uint32 i = 0; i < files.size(); ++i) {
			if (!files[i].matchString(pattern, true))
				continue;

			//make up some slot number
			int slotNum = 0;
			for (uint32 j = (files[i].size() > 3 ? files[i].size() - 3 : 0); j < files[i].size(); ++j) { //3 last chars
				char c = files[i][j];
				if (c < '0' || c > '9')
					continue;
				slotNum = slotNum * 10 + (c - '0');
			}

			SaveStateDescriptor slot(_metaEngine, slotNum, files[i]);
			slot.setLocked(true);
			_saveList.push_back(slot);
		}

		Common::sort(_saveList.begin(), _saveList.end(), SaveStateDescriptorSlotComparator());
	}
#endif
}

void SaveLoadChooserDialog::activate(int slot, const Common::U32String &description) {
	if (!_saveList.empty() && slot < int(_saveList.size())) {
		const SaveStateDescriptor &desc = _saveList[slot];
		if (_saveMode)
			_resultString = description.empty() ? desc.getDescription() : description;
		setResult(desc.getSaveSlot());
	}
	close();
}

#ifndef DISABLE_SAVELOADCHOOSER_GRID
void SaveLoadChooserDialog::addChooserButtons() {
	if (_listButton) {
		removeWidget(_listButton);
		delete _listButton;
	}

	if (_gridButton) {
		removeWidget(_gridButton);
		delete _gridButton;
	}

	_listButton = createSwitchButton("SaveLoadChooser.ListSwitch", Common::U32String("L"), _("List view"), ThemeEngine::kImageList, kListSwitchCmd);
	_gridButton = createSwitchButton("SaveLoadChooser.GridSwitch", Common::U32String("G"), _("Grid view"), ThemeEngine::kImageGrid, kGridSwitchCmd);
	if (!_metaInfoSupport || !_thumbnailSupport || !(g_gui.getGUIWidth() >= 640 && g_gui.getGUIHeight() >= 400)) {
		_gridButton->setEnabled(false);
		_listButton->setEnabled(false);
	}
}

ButtonWidget *SaveLoadChooserDialog::createSwitchButton(const Common::String &name, const Common::U32String &desc, const Common::U32String &tooltip, const char *image, uint32 cmd) {
	ButtonWidget *button;

#ifndef DISABLE_FANCY_THEMES
	if (g_gui.xmlEval()->getVar("Globals.ShowChooserPics") == 1 && g_gui.theme()->supportsImages()) {
		button = new PicButtonWidget(this, name, tooltip, cmd);
		((PicButtonWidget *)button)->useThemeTransparency(true);
		((PicButtonWidget *)button)->setGfx(g_gui.theme()->getImageSurface(image), kPicButtonStateEnabled, false);
	} else
#endif
		button = new ButtonWidget(this, name, desc, tooltip, cmd);

	return button;
}
#endif // !DISABLE_SAVELOADCHOOSER_GRID

#if defined(USE_CLOUD) && defined(USE_LIBCURL)
void SaveLoadChooserDialog::pollCloudMan() {
	_pollFrame = (_pollFrame + 1) % 60;
	if (_pollFrame != 1)
		return;

	const bool syncing = CloudMan.isSyncing();
	const uint32 progress = (uint32)(100 * CloudMan.getSyncDownloadingProgress());

	bool update = false;
	if (syncing && progress < 100) {
		update = true;
		_didUpdateAfterSync = false;
	} else {
		if (!_didUpdateAfterSync) { // do one more update when sync is over
			update = true;
			_didUpdateAfterSync = true;
		}
	}

	if (update)
		updateSaveList();
}
#endif

// SaveLoadChooserSimple implementation

enum {
	kChooseCmd = 'CHOS',
	kDelCmd = 'DEL '
};

SaveLoadChooserSimple::SaveLoadChooserSimple(const Common::U32String &title, const Common::U32String &buttonLabel, bool saveMode)
	: SaveLoadChooserDialog("SaveLoadChooser", saveMode), _list(nullptr), _chooseButton(nullptr), _deleteButton(nullptr), _gfxWidget(nullptr),
	_container(nullptr) {
	_backgroundType = ThemeEngine::kDialogBackgroundSpecial;

	_pageTitle = new StaticTextWidget(this, "SaveLoadChooser.Title", title);

	// Add choice list
	_list = new ListWidget(this, "SaveLoadChooser.List");
	_list->setNumberingMode(kListNumberingZero);
	_list->setEditable(saveMode);

	_gfxWidget = new GraphicsWidget(this, 0, 0, 10, 10);
	_gfxWidget->useThemeTransparency(false);

	_date = new StaticTextWidget(this, 0, 0, 10, 10, _("No date saved"), Graphics::kTextAlignCenter);
	_time = new StaticTextWidget(this, 0, 0, 10, 10, _("No time saved"), Graphics::kTextAlignCenter);
	_playtime = new StaticTextWidget(this, 0, 0, 10, 10, _("No playtime saved"), Graphics::kTextAlignCenter);

	// Buttons
	new ButtonWidget(this, "SaveLoadChooser.Cancel", _("Cancel"), Common::U32String(), kCloseCmd);
	_chooseButton = new ButtonWidget(this, "SaveLoadChooser.Choose", buttonLabel, Common::U32String(), kChooseCmd);
	_chooseButton->setEnabled(false);

	_deleteButton = new ButtonWidget(this, "SaveLoadChooser.Delete", _("Delete"), Common::U32String(), kDelCmd);
	_deleteButton->setEnabled(false);

	_delSupport = _metaInfoSupport = _thumbnailSupport = false;

	addThumbnailContainer();
}

void SaveLoadChooserSimple::addThumbnailContainer() {
	// When switching layouts, create / remove the thumbnail container as needed
	if (g_gui.xmlEval()->getVar("Globals.SaveLoadChooser.ExtInfo.Visible") == 1 && !_container) {
		_container = new ContainerWidget(this, "SaveLoadChooser.Thumbnail");
	} else if (g_gui.xmlEval()->getVar("Globals.SaveLoadChooser.ExtInfo.Visible") == 0 && _container) {
		removeWidget(_container);
		delete _container;
		_container = nullptr;
	}
}

int SaveLoadChooserSimple::runIntern() {
	if (_gfxWidget)
		_gfxWidget->setGfx((Graphics::ManagedSurface *)nullptr);

	_resultString.clear();
	reflowLayout();
	updateSaveList();

	return Dialog::runModal();
}

const Common::U32String SaveLoadChooserSimple::getResultString() const {
	int selItem = _list->getSelected();
	return (selItem >= 0) ? _list->getSelectedString() : _resultString;
}

void SaveLoadChooserSimple::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) {
	int selItem = _list->getSelected();

	switch (cmd) {
	case kListItemActivatedCmd:
	case kListItemDoubleClickedCmd:
		if (selItem >= 0 && _chooseButton->isEnabled()) {
			if (_list->isEditable() || !_list->getSelectedString().empty()) {
				_list->endEditMode();
				Common::U32String description;
				if (!_saveList.empty())
					description = _list->getSelectedString();
				activate(selItem, description);
			}
		}
		break;
	case kChooseCmd:
		_list->endEditMode();
		if (selItem >= 0) {
			Common::U32String description;
			if (!_saveList.empty())
				description = _list->getSelectedString();
			activate(selItem, description);
		}
		break;
	case kListSelectionChangedCmd:
		updateSelection(true);
		break;
	case kDelCmd:
	case kListItemRemovalRequestCmd:
		if (_deleteButton->isEnabled()) {
			MessageDialog alert(_("Do you really want to delete this saved game?"),
								_("Delete"), _("Cancel"));
			if (alert.runModal() == kMessageOK) {
				_metaEngine->removeSaveState(_target.c_str(), _saveList[selItem].getSaveSlot());

				setResult(-1);
				int scrollPos = _list->getCurrentScrollPos();
				_list->setSelected(-1); // resets scroll pos
				_list->scrollTo(scrollPos);

				updateSaveList();
				updateSelection(true);
			}
		}
		break;
	case kCloseCmd:
		setResult(-1);
		// Fall through
	default:
		SaveLoadChooserDialog::handleCommand(sender, cmd, data);
	}
}

void SaveLoadChooserSimple::reflowLayout() {
	addThumbnailContainer();

	SaveLoadChooserDialog::reflowLayout();

	if (g_gui.xmlEval()->getVar("Globals.SaveLoadChooser.ExtInfo.Visible") == 1 && (_thumbnailSupport || _saveDateSupport || _playTimeSupport)) {
		int16 x, y;
		int16 w, h;

		if (!g_gui.xmlEval()->getWidgetData("SaveLoadChooser.Thumbnail", x, y, w, h))
			error("Error when loading position data for Save/Load Thumbnails");

		// Even if there is no thumbnail support, getWidgetData() will provide default thumbnail values
		int thumbW = SCALEVALUE(kThumbnailWidth);
		int thumbH = SCALEVALUE(kThumbnailHeight2);
		int thumbX = x + (w >> 1) - (thumbW >> 1);
		int thumbY = y + kLineHeight;

		int textLines = 0;
		if (_saveDateSupport)
			textLines += 2;
		if (_playTimeSupport)
			textLines++;
		if (textLines > 0)
			textLines++; // add a line of padding at the bottom

		if (_thumbnailSupport) {
			_gfxWidget->resize(thumbX, thumbY, thumbW, thumbH, false);
			_gfxWidget->setVisible(true);
		} else {
			// choose sensible values for displaying playtime and date/time when a thumbnail is not being used
			thumbH = 0;
			thumbY = y;
			h = kLineHeight;
			_gfxWidget->setVisible(false);
		}

		int height = thumbY + thumbH + kLineHeight;

		if (_saveDateSupport) {
			_date->resize(thumbX, height, thumbW, kLineHeight, false);
			height += kLineHeight;
			_time->resize(thumbX, height, thumbW, kLineHeight, false);
			height += kLineHeight;
			_date->setVisible(_saveDateSupport);
			_time->setVisible(_saveDateSupport);
		} else {
			_date->setVisible(false);
			_time->setVisible(false);
		}

		if (_playTimeSupport) {
			_playtime->resize(thumbX, height, thumbW, kLineHeight, false);
			_playtime->setVisible(_playTimeSupport);
		} else {
			_playtime->setVisible(false);
		}

		_container->resize(x, y, w, h + (kLineHeight * textLines), false);
		_container->setVisible(true);

		updateSelection(false);
	} else {
		if (_container) _container->setVisible(false);
		_gfxWidget->setVisible(false);
		_date->setVisible(false);
		_time->setVisible(false);
		_playtime->setVisible(false);
	}
}

void SaveLoadChooserSimple::updateSelection(bool redraw) {
	int selItem = _list->getSelected();

	bool isDeletable = _delSupport;
	bool isWriteProtected = false;
	bool startEditMode = _list->isEditable();
	bool isLocked = false;

	// We used to support letting the themes specify the fill color with our
	// initial theme based GUI. But this support was dropped.
	_gfxWidget->setGfx(-1, -1, 0, 0, 0);
	_date->setLabel(_("No date saved"));
	_time->setLabel(_("No time saved"));
	_playtime->setLabel(_("No playtime saved"));

	if (selItem >= 0 && _metaInfoSupport) {
		SaveStateDescriptor desc = (_saveList[selItem].getLocked() ? _saveList[selItem] : _metaEngine->querySaveMetaInfos(_target.c_str(), _saveList[selItem].getSaveSlot()));
		if (!_saveList[selItem].getLocked() && desc.getSaveSlot() >= 0 && !desc.getDescription().empty())
			_saveList[selItem] = desc;

		isDeletable = desc.getDeletableFlag() && _delSupport;
		isWriteProtected = desc.getWriteProtectedFlag() ||
			_saveList[selItem].getWriteProtectedFlag();
		isLocked = desc.getLocked();

		// Don't allow the user to change the description of write protected games
		if (isWriteProtected)
			startEditMode = false;

		if (_thumbnailSupport) {
			const Graphics::Surface *thumb = desc.getThumbnail();
			if (thumb && _gfxWidget->isVisible())
				_gfxWidget->setGfx(thumb, true);
		}

		if (_saveDateSupport) {
			const Common::U32String &saveDate = desc.getSaveDate();
			if (!saveDate.empty())
				_date->setLabel(_("Date: ") + saveDate);

			const Common::U32String &saveTime = desc.getSaveTime();
			if (!saveTime.empty())
				_time->setLabel(_("Time: ") + saveTime);
		}

		if (_playTimeSupport) {
			const Common::U32String &playTime = desc.getPlayTime();
			if (!playTime.empty())
				_playtime->setLabel(_("Playtime: ") + playTime);
		}
	}


	if (_list->isEditable()) {
		// Disable the save button if slot is locked, nothing is selected,
		// or if the selected game is write protected
		_chooseButton->setEnabled(!isLocked && selItem >= 0 && !isWriteProtected);

		if (startEditMode) {
			_list->startEditMode();

			if (_chooseButton->isEnabled() && _list->getSelectedString() == _("Untitled saved game")) {
				_list->setEditString(Common::U32String());
				_list->setEditColor(ThemeEngine::kFontColorNormal);
			}
		}
	} else {
		// Disable the load button if slot is locked, nothing is selected,
		// or if an empty list item is selected.
		_chooseButton->setEnabled(!isLocked && selItem >= 0 && !_list->getSelectedString().empty());
	}

	// Delete will always be disabled if the engine doesn't support it.
	_deleteButton->setEnabled(isDeletable && !isLocked && (selItem >= 0) && (!_list->getSelectedString().empty()));

	if (redraw) {
		_gfxWidget->markAsDirty();
		_date->markAsDirty();
		_time->markAsDirty();
		_playtime->markAsDirty();
		_chooseButton->markAsDirty();
		_deleteButton->markAsDirty();

		g_gui.scheduleTopDialogRedraw();
	}
}

void SaveLoadChooserSimple::open() {
	SaveLoadChooserDialog::open();

	// Scroll the list to the last used entry.
	_list->scrollTo(ConfMan.getInt("gui_saveload_last_pos"));
}

void SaveLoadChooserSimple::close() {
	// Save the current scroll position/used entry.
	const int result = getResult();
	if (result >= 0) {
		ConfMan.setInt("gui_saveload_last_pos", result);
	} else {
		// Use the current scroll position here.
		// TODO: This means we canceled the dialog (or switch to the grid). Do
		// we want to save this position here? Does the user want that?
		// TODO: Do we want to save the current scroll position or the
		// currently selected item here? The scroll position is what the user
		// currently sees and seems to make more sense.
		ConfMan.setInt("gui_saveload_last_pos", _list->getCurrentScrollPos());
	}

	_metaEngine = nullptr;
	_target.clear();
	_saveList.clear();
	_list->setList(Common::U32StringArray());

	SaveLoadChooserDialog::close();
}

void SaveLoadChooserSimple::updateSaveList() {
	SaveLoadChooserDialog::updateSaveList();

	int curSlot = 0;
	int saveSlot = 0;
	Common::U32StringArray saveNames;
	ThemeEngine::FontColor color = ThemeEngine::kFontColorNormal;
	for (SaveStateList::const_iterator x = _saveList.begin(); x != _saveList.end(); ++x) {
		// Handle gaps in the list of save games
		saveSlot = x->getSaveSlot();
		if (curSlot < saveSlot) {
			while (curSlot < saveSlot) {
				SaveStateDescriptor dummySave(_metaEngine, curSlot, "");
				_saveList.insert_at(curSlot, dummySave);
				saveNames.push_back(GUI::ListWidget::getThemeColor(color) + dummySave.getDescription());
				curSlot++;
			}

			// Sync the save list iterator
			for (x = _saveList.begin(); x != _saveList.end(); ++x) {
				if (x->getSaveSlot() == saveSlot)
					break;
			}
		}

		// Show "Untitled saved game" for empty/whitespace saved game descriptions
		Common::U32String description = x->getDescription();
		Common::U32String trimmedDescription = description;
		trimmedDescription.trim();
		if (trimmedDescription.empty()) {
			description = _("Untitled saved game");
			color = ThemeEngine::kFontColorAlternate;
		} else {
			color = x->getLocked() ? ThemeEngine::kFontColorAlternate : ThemeEngine::kFontColorNormal;
		}

		saveNames.push_back(GUI::ListWidget::getThemeColor(color) + description);
		curSlot++;
	}

	// Fill the rest of the save slots with empty saves

	int maximumSaveSlots = _metaEngine->getMaximumSaveSlot();

#ifdef __DS__
	// Low memory on the DS means too many save slots are impractical, so limit
	// the maximum here.
	if (maximumSaveSlots > 99) {
		maximumSaveSlots = 99;
	}
#endif

	Common::U32String emptyDesc;
	color = ThemeEngine::kFontColorNormal;
	for (int i = curSlot; i <= maximumSaveSlots; i++) {
		saveNames.push_back(emptyDesc);
		SaveStateDescriptor dummySave(_metaEngine, i, "");
		_saveList.push_back(dummySave);
	}

	int selected = _list->getSelected();
	_list->setList(saveNames);
	if (selected >= 0 && selected < (int)saveNames.size())
		_list->setSelected(selected);
	else
		_chooseButton->setEnabled(false);

	g_gui.scheduleTopDialogRedraw();
}

// SaveLoadChooserGrid implementation

#ifndef DISABLE_SAVELOADCHOOSER_GRID

enum {
	kNextCmd = 'NEXT',
	kPrevCmd = 'PREV',
	kNewSaveCmd = 'SAVE'
};

SaveLoadChooserGrid::SaveLoadChooserGrid(const Common::U32String &title, bool saveMode)
	: SaveLoadChooserDialog("SaveLoadChooser", saveMode), _lines(0), _columns(0), _entriesPerPage(0),
	_curPage(0), _newSaveContainer(nullptr), _nextFreeSaveSlot(0), _buttons() {
	_backgroundType = ThemeEngine::kDialogBackgroundSpecial;

	_pageTitle = new StaticTextWidget(this, "SaveLoadChooser.Title", title);

	// The list widget needs to be bound so it takes space in the layout
	ContainerWidget *list = new ContainerWidget(this, "SaveLoadChooser.List");
	list->setBackgroundType(ThemeEngine::kWidgetBackgroundNo);

	// Buttons
	new ButtonWidget(this, "SaveLoadChooser.Delete", _("Cancel"), Common::U32String(), kCloseCmd);
	_nextButton = new ButtonWidget(this, "SaveLoadChooser.Choose", _("Next"), Common::U32String(), kNextCmd);
	_nextButton->setEnabled(false);

	_prevButton = new ButtonWidget(this, "SaveLoadChooser.Cancel", _("Prev"), Common::U32String(), kPrevCmd);
	_prevButton->setEnabled(false);

	// Page display
	_pageDisplay = new StaticTextWidget(this, "SaveLoadChooser.PageDisplay", Common::U32String());
	_pageDisplay->setAlign(Graphics::kTextAlignEnd);
}

SaveLoadChooserGrid::~SaveLoadChooserGrid() {
	removeWidget(_pageTitle);
	delete _pageTitle;

	removeWidget(_pageDisplay);
	delete _pageDisplay;
}

const Common::U32String SaveLoadChooserGrid::getResultString() const {
	return _resultString;
}

void SaveLoadChooserGrid::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) {
	const int slot = cmd + _curPage * _entriesPerPage - 1;
	if (cmd <= _entriesPerPage && slot < (int)_saveList.size()) {
		activate(slot, Common::U32String());
	}

	switch (cmd) {
	case kNextCmd:
		++_curPage;
		updateSaves();
		g_gui.scheduleTopDialogRedraw();
		break;

	case kPrevCmd:
		--_curPage;
		updateSaves();
		g_gui.scheduleTopDialogRedraw();
		break;

	case kNewSaveCmd:
		setResult(_nextFreeSaveSlot);
		close();
		break;

	case kCloseCmd:
		setResult(-1);
		// Fall through
	default:
		SaveLoadChooserDialog::handleCommand(sender, cmd, data);
	}
}

void SaveLoadChooserGrid::handleMouseWheel(int x, int y, int direction) {
	if (direction > 0) {
		if (_nextButton->isEnabled()) {
			++_curPage;
			updateSaves();
			g_gui.scheduleTopDialogRedraw();
		}
	} else {
		if (_prevButton->isEnabled()) {
			--_curPage;
			updateSaves();
			g_gui.scheduleTopDialogRedraw();
		}
	}
}

void SaveLoadChooserGrid::updateSaveList() {
	SaveLoadChooserDialog::updateSaveList();
	updateSaves();
	g_gui.scheduleTopDialogRedraw();
}

void SaveLoadChooserGrid::open() {
	SaveLoadChooserDialog::open();

	listSaves();
	_resultString.clear();

	// Load information to restore the last page the user had open.
	assert(_entriesPerPage != 0);
	const uint lastPos = ConfMan.getInt("gui_saveload_last_pos");
	const uint listSize = _saveList.size();
	uint bestMatch = 0;
	uint diff = 0xFFFFFFFF;

	// We look for the nearest available slot, since a slot might be missing
	// due to the user deleting it via the list based chooser, by deleting
	// it by hand, etc.
	for (uint i = 0; i < listSize; ++i) {
		uint curDiff = ABS(_saveList[i].getSaveSlot() - (int)lastPos);
		if (curDiff < diff) {
			diff = curDiff;
			bestMatch = i;
		}
	}

	_curPage = bestMatch / _entriesPerPage;

	// Determine the next free save slot for save mode
	if (_saveMode) {
		int lastSlot = -1;
		_nextFreeSaveSlot = -1;
		for (SaveStateList::const_iterator x = _saveList.begin(); x != _saveList.end(); ++x) {
			const int curSlot = x->getSaveSlot();

			// In case there was a gap found use the slot.
			if (lastSlot + 1 < curSlot) {
				// Check that the save slot can be used for user saves.
				SaveStateDescriptor desc = _metaEngine->querySaveMetaInfos(_target.c_str(), lastSlot + 1);
				if (!desc.getWriteProtectedFlag()) {
					_nextFreeSaveSlot = lastSlot + 1;
					break;
				}
			}

			lastSlot = curSlot;
		}

		// Use the next available slot otherwise.
		const int maxSlot = _metaEngine->getMaximumSaveSlot();
		for (int i = lastSlot; _nextFreeSaveSlot == -1 && i < maxSlot; ++i) {
			// Check that the save slot can be used for user saves.
			SaveStateDescriptor desc = _metaEngine->querySaveMetaInfos(_target.c_str(), i + 1);
			if (!desc.getWriteProtectedFlag()) {
				_nextFreeSaveSlot = i + 1;
			}
		}
	}

	updateSaves();
}

void SaveLoadChooserGrid::reflowLayout() {
	// HACK: The page display is not available in low resolution layout. We
	// remove and readd the widget here to avoid our GUI from erroring out.
	removeWidget(_pageDisplay);
	if (g_gui.xmlEval()->getVar("Globals.ShowChooserPageDisplay") == 1) {
		_pageDisplay->init();
	}

	SaveLoadChooserDialog::reflowLayout();
	destroyButtons();

	// HACK: The whole code below really works around the fact, that we have
	// no easy way to dynamically layout widgets.
	const uint16 availableWidth = getWidth() - 20;
	int16 availableHeight;

	int16 x, y;
	int16 w;
	if (!g_gui.xmlEval()->getWidgetData("SaveLoadChooser.List", x, y, w, availableHeight))
		error("Could not load widget position for 'SaveLoadChooser.List'");

	const int16 buttonWidth = SCALEVALUE(kThumbnailWidth + 6);
	const int16 buttonHeight = SCALEVALUE(kThumbnailHeight2 + 6);

	const int16 containerFrameWidthAdd = SCALEVALUE(10);
	const int16 containerFrameHeightAdd = 0;
	const int16 containerWidth = buttonWidth + containerFrameWidthAdd;
	const int16 containerHeight = buttonHeight + kLineHeight + containerFrameHeightAdd;

	const int16 defaultSpacingHorizontal = SCALEVALUE(4);
	const int16 defaultSpacingVertical = SCALEVALUE(8);
	const int16 slotAreaWidth = containerWidth + defaultSpacingHorizontal;
	const int16 slotAreaHeight = containerHeight + defaultSpacingVertical;

	const uint oldEntriesPerPage = _entriesPerPage;
	_columns = MAX<uint>(1, availableWidth / slotAreaWidth);
	_lines = MAX<uint>(1, availableHeight / slotAreaHeight);
	_entriesPerPage = _columns * _lines;

	// In save mode the first button is always "New Save", thus we need to
	// adjust the entries per page here.
	if (_saveMode) {
		--_entriesPerPage;
	}

	// Recalculate the page number
	if (!_saveList.empty() && oldEntriesPerPage != 0) {
		if (_entriesPerPage != 0) {
			_curPage = (_curPage * oldEntriesPerPage) / _entriesPerPage;
		} else {
			_curPage = 0;
		}
	}

	const uint addX = _columns > 1 ? (availableWidth % slotAreaWidth) / (_columns - 1) : 0;
	//const uint addY = _lines > 1 ? (availableHeight % slotAreaHeight) / (_lines - 1) : 0;

	_buttons.reserve(_lines * _columns);
	y += defaultSpacingVertical / 2;
	for (uint curLine = 0; curLine < _lines; ++curLine, y += slotAreaHeight/* + addY*/) {
		for (uint curColumn = 0, curX = x + defaultSpacingHorizontal / 2; curColumn < _columns; ++curColumn, curX += slotAreaWidth + addX) {
			int dstY = containerFrameHeightAdd / 2;
			int dstX = containerFrameWidthAdd / 2;

			// In the save mode we will always create a new save button as the first button.
			if (_saveMode && curLine == 0 && curColumn == 0) {
				_newSaveContainer = new ContainerWidget(this, curX, y, containerWidth, containerHeight);
				ButtonWidget *newSave = new ButtonWidget(_newSaveContainer, dstX, dstY, buttonWidth, buttonHeight, _("New Save"), _("Create a new saved game"), kNewSaveCmd);
				// In case no more slots are free, we will disable the new save button
				if (_nextFreeSaveSlot == -1) {
					newSave->setEnabled(false);
				}
				continue;
			}

			ContainerWidget *container = new ContainerWidget(this, curX, y, containerWidth, containerHeight);
			container->setVisible(false);

			// Command 0 cannot be used, since it won't be send. Thus we will adjust
			// command number here, if required. This is only the case for load mode
			// since for save mode, the first button used is index 1 anyway.
			uint buttonCmd = curLine * _columns + curColumn;
			if (!_saveMode) {
				buttonCmd += 1;
			}

			PicButtonWidget *button = new PicButtonWidget(container, dstX, dstY, buttonWidth, buttonHeight, Common::U32String(), buttonCmd);
			button->useThemeTransparency(false);
			dstY += buttonHeight;

			StaticTextWidget *description = new StaticTextWidget(container, dstX, dstY, buttonWidth, kLineHeight, Common::String(), Graphics::kTextAlignStart);

			_buttons.push_back(SlotButton(container, button, description));
		}
	}

	if (!_target.empty())
		updateSaves();
}

void SaveLoadChooserGrid::close() {
	// Save the current page.
	const int result = getResult();
	if (result >= 0 && result != _nextFreeSaveSlot) {
		// If the user selected a slot we use that one. We ignore new slots
		// here, since otherwise the dialog would reset to page 0 when the
		// user cancels the savename dialog.
		ConfMan.setInt("gui_saveload_last_pos", result);
	} else {
		// Otherwise save the first entry on the current page.
		// This is less precise than the solution above, since the number of
		// entries shown differs between save and load version of the dialog,
		// thus it might wrap to a different page than expected.
		// Similar things happen on resolution changes.
		// TODO: Should we ignore this here? Is the user likely to be
		// interested in having this page restored when he canceled?
		ConfMan.setInt("gui_saveload_last_pos", !_saveList.empty() ? _saveList[_curPage * _entriesPerPage].getSaveSlot() : 0);
	}

	SaveLoadChooserDialog::close();
	hideButtons();
}

int SaveLoadChooserGrid::runIntern() {
	int slot;
	do {
		const SaveLoadChooserType currentType = getType();
		const SaveLoadChooserType requestedType = getRequestedSaveLoadDialog(_metaEngine);

		// Catch resolution changes when the save name dialog was open.
		if (currentType != requestedType) {
			setResult(kSwitchSaveLoadDialog);
			return kSwitchSaveLoadDialog;
		}

		slot = runModal();
	} while (_saveMode && slot >= 0 && !selectDescription());

	// Special case for new save games. We need to handle this here, since
	// we cannot handle it in close() without problems.
	if (slot == _nextFreeSaveSlot) {
		ConfMan.setInt("gui_saveload_last_pos", slot);
	}

	return slot;
}

bool SaveLoadChooserGrid::selectDescription() {
	_savenameDialog.setDescription(_resultString);
	_savenameDialog.setTargetSlot(getResult());
	if (_savenameDialog.runModal() == 0) {
		_resultString = _savenameDialog.getDescription();
		return true;
	} else {
		return false;
	}
}

void SaveLoadChooserGrid::destroyButtons() {
	if (_newSaveContainer) {
		removeWidget(_newSaveContainer);
		delete _newSaveContainer;
		_newSaveContainer = nullptr;
	}

	for (ButtonArray::iterator i = _buttons.begin(), end = _buttons.end(); i != end; ++i) {
		removeWidget(i->container);
		delete i->container;
	}

	_buttons.clear();
}

void SaveLoadChooserGrid::hideButtons() {
	for (ButtonArray::iterator i = _buttons.begin(), end = _buttons.end(); i != end; ++i) {
		i->button->setGfx((Graphics::ManagedSurface *)nullptr);
		i->setVisible(false);
	}
}

void SaveLoadChooserGrid::updateSaves() {
	hideButtons();

	for (uint i = _curPage * _entriesPerPage, curNum = 0; i < _saveList.size() && curNum < _entriesPerPage; ++i, ++curNum) {
		const uint saveSlot = _saveList[i].getSaveSlot();

		SaveStateDescriptor desc =  (_saveList[i].getLocked() ? _saveList[i] : _metaEngine->querySaveMetaInfos(_target.c_str(), saveSlot));
		if (!_saveList[i].getLocked() && desc.getSaveSlot() >= 0 && !desc.getDescription().empty())
			_saveList[i] = desc;
		SlotButton &curButton = _buttons[curNum];
		curButton.setVisible(true);
		const Graphics::Surface *thumbnail = desc.getThumbnail();
		if (thumbnail) {
			curButton.button->setGfx(desc.getThumbnail());
		} else {
			curButton.button->setGfx(kThumbnailWidth, kThumbnailHeight2, 0, 0, 0);
		}
		curButton.description->setLabel(Common::U32String(Common::String::format("%d. ", saveSlot)) + desc.getDescription());

		Common::U32String tooltip(_("Name: "));
		tooltip += desc.getDescription();

		if (_saveDateSupport) {
			const Common::U32String &saveDate = desc.getSaveDate();
			if (!saveDate.empty()) {
				tooltip += Common::U32String("\n");
				tooltip +=  _("Date: ") + saveDate;
			}

			const Common::U32String &saveTime = desc.getSaveTime();
			if (!saveTime.empty()) {
				tooltip += Common::U32String("\n");
				tooltip += _("Time: ") + saveTime;
			}
		}

		if (_playTimeSupport) {
			const Common::U32String &playTime = desc.getPlayTime();
			if (!playTime.empty()) {
				tooltip += Common::U32String("\n");
				tooltip += _("Playtime: ") + playTime;
			}
		}

		curButton.button->setTooltip(tooltip);

		// In save mode we disable the button, when it's write protected.
		// TODO: Maybe we should not display it at all then?
		// We also disable and description the button if slot is locked
		if ((_saveMode && desc.getWriteProtectedFlag()) || desc.getLocked()) {
			curButton.button->setEnabled(false);
		} else {
			curButton.button->setEnabled(true);
		}
		curButton.description->setEnabled(!desc.getLocked());
	}

	const uint numPages = (_entriesPerPage != 0 && !_saveList.empty()) ? ((_saveList.size() + _entriesPerPage - 1) / _entriesPerPage) : 1;
	_pageDisplay->setLabel(Common::String::format("%u/%u", _curPage + 1, numPages));

	if (_curPage > 0)
		_prevButton->setEnabled(true);
	else
		_prevButton->setEnabled(false);

	if ((_curPage + 1) * _entriesPerPage < _saveList.size())
		_nextButton->setEnabled(true);
	else
		_nextButton->setEnabled(false);
}

SavenameDialog::SavenameDialog()
	: Dialog("SavenameDialog") {
	_title = new StaticTextWidget(this, "SavenameDialog.DescriptionText", Common::String());

	new ButtonWidget(this, "SavenameDialog.Cancel", _("Cancel"), Common::U32String(), kCloseCmd);
	new ButtonWidget(this, "SavenameDialog.Ok", _("OK"), Common::U32String(), kOKCmd);

	_description = new EditTextWidget(this, "SavenameDialog.Description", Common::U32String(), Common::U32String(), 0, kOKCmd);

	_targetSlot = 0;
}

void SavenameDialog::setDescription(const Common::U32String &desc) {
	_description->setEditString(desc);
}

const Common::U32String &SavenameDialog::getDescription() {
	return _description->getEditString();
}

void SavenameDialog::open() {
	Dialog::open();
	setResult(-1);

	_title->setLabel(Common::U32String::format(_("Enter a description for slot %d:"), _targetSlot));
}

void SavenameDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) {
	switch (cmd) {
	case kOKCmd:
		setResult(0);
		close();
		break;

	default:
		Dialog::handleCommand(sender, cmd, data);
	}
}

#endif // !DISABLE_SAVELOADCHOOSER_GRID

} // End of namespace GUI
