// Copyright (c) 2012- PPSSPP Project.

// 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, version 2.0 or later versions.

// 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 2.0 for more details.

// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/

// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.

#ifdef __MINGW32__
#define _POSIX_THREAD_SAFE_FUNCTIONS 200112L
#endif

#include <thread>

#include "base/stringutil.h"
#include "i18n/i18n.h"
#include "thread/threadutil.h"

#include "Common/FileUtil.h"
#include "Common/Serialize/Serializer.h"
#include "Common/Serialize/SerializeFuncs.h"
#include "Core/FileSystems/MetaFileSystem.h"
#include "Core/Util/PPGeDraw.h"
#include "Core/HLE/sceCtrl.h"
#include "Core/HLE/sceUtility.h"
#include "Core/MemMapHelpers.h"
#include "Core/Config.h"
#include "Core/Reporting.h"
#include "Core/HW/MemoryStick.h"
#include "Core/Dialog/PSPSaveDialog.h"

const static float FONT_SCALE = 0.55f;

// These are rough, it seems to take at least 100ms or so to init, and shutdown depends on threads.
// Some games seem to required slightly longer delays to work, so we try 200ms as a compromise.
const static int SAVEDATA_INIT_DELAY_US = 200000;
const static int SAVEDATA_SHUTDOWN_DELAY_US = 2000;

// These are the only sizes which are allowed.
// TODO: We should test what the different behavior is for each.
const static int SAVEDATA_DIALOG_SIZE_V1 = 1480;
const static int SAVEDATA_DIALOG_SIZE_V2 = 1500;
const static int SAVEDATA_DIALOG_SIZE_V3 = 1536;


PSPSaveDialog::PSPSaveDialog()
	: PSPDialog()
	, display(DS_NONE)
	, currentSelectedSave(0)
	, ioThread(0)
{
	param.SetPspParam(0);
}

PSPSaveDialog::~PSPSaveDialog() {
}

int PSPSaveDialog::Init(int paramAddr)
{
	// Ignore if already running
	if (GetStatus() != SCE_UTILITY_STATUS_NONE) {
		ERROR_LOG_REPORT(SCEUTILITY, "A save request is already running, not starting a new one");
		return SCE_ERROR_UTILITY_INVALID_STATUS;
	}

	JoinIOThread();
	ioThreadStatus = SAVEIO_NONE;

	requestAddr = paramAddr;
	int size = Memory::Read_U32(requestAddr);
	memset(&request, 0, sizeof(request));
	// Only copy the right size to support different save request format
	if (size != SAVEDATA_DIALOG_SIZE_V1 && size != SAVEDATA_DIALOG_SIZE_V2 && size != SAVEDATA_DIALOG_SIZE_V3) {
		ERROR_LOG_REPORT(SCEUTILITY, "sceUtilitySavedataInitStart: invalid size %d", size);
		return SCE_ERROR_UTILITY_INVALID_PARAM_SIZE;
	}
	Memory::Memcpy(&request, requestAddr, size);
	Memory::Memcpy(&originalRequest, requestAddr, size);

	int retval = param.SetPspParam(&request);

	const u32 mode = (u32)param.GetPspParam()->mode;
	const char *modeName = mode < ARRAY_SIZE(utilitySavedataTypeNames) ? utilitySavedataTypeNames[mode] : "UNKNOWN";
	INFO_LOG(SCEUTILITY,"sceUtilitySavedataInitStart(%08x) - %s (%d)", paramAddr, modeName, mode);
	INFO_LOG(SCEUTILITY,"sceUtilitySavedataInitStart(%08x) : Game key (hex): %s", paramAddr, param.GetKey(param.GetPspParam()).c_str());

	yesnoChoice = 1;
	switch ((SceUtilitySavedataFocus)(u32)param.GetPspParam()->focus)
	{
	case SCE_UTILITY_SAVEDATA_FOCUS_NAME:
		currentSelectedSave = param.GetSaveNameIndex(param.GetPspParam());
		break;
	case SCE_UTILITY_SAVEDATA_FOCUS_FIRSTLIST:
		currentSelectedSave = param.GetFirstListSave();
		break;
	case SCE_UTILITY_SAVEDATA_FOCUS_LASTLIST:
		currentSelectedSave = param.GetLastListSave();
		break;
	case SCE_UTILITY_SAVEDATA_FOCUS_LATEST:
		currentSelectedSave = param.GetLatestSave();
		break;
	case SCE_UTILITY_SAVEDATA_FOCUS_OLDEST:
		currentSelectedSave = param.GetOldestSave();
		break;
	case SCE_UTILITY_SAVEDATA_FOCUS_FIRSTDATA:
		currentSelectedSave = param.GetFirstDataSave();
		break;
	case SCE_UTILITY_SAVEDATA_FOCUS_LASTDATA:
		currentSelectedSave = param.GetLastDataSave();
		break;
	case SCE_UTILITY_SAVEDATA_FOCUS_FIRSTEMPTY:
		currentSelectedSave = param.GetFirstEmptySave();
		break;
	case SCE_UTILITY_SAVEDATA_FOCUS_LASTEMPTY:
		currentSelectedSave = param.GetLastEmptySave();
		break;
	default:
		WARN_LOG(SCEUTILITY, "Unknown save list focus option: %d", param.GetPspParam()->focus);
		currentSelectedSave = 0;
		break;
	}

	switch ((SceUtilitySavedataType)(u32)param.GetPspParam()->mode)
	{
		case SCE_UTILITY_SAVEDATA_TYPE_LOAD:
			DEBUG_LOG(SCEUTILITY, "Loading. Title: %s Save: %s File: %s", param.GetGameName(param.GetPspParam()).c_str(), param.GetSaveName(param.GetPspParam()).c_str(), param.GetFileName(param.GetPspParam()).c_str());
			if (param.GetFileInfo(0).size != 0)
				display = DS_LOAD_CONFIRM;
			else
				display = DS_LOAD_NODATA;
			break;
		case SCE_UTILITY_SAVEDATA_TYPE_AUTOLOAD:
			DEBUG_LOG(SCEUTILITY, "Loading. Title: %s Save: %s File: %s", param.GetGameName(param.GetPspParam()).c_str(), param.GetSaveName(param.GetPspParam()).c_str(), param.GetFileName(param.GetPspParam()).c_str());
			display = DS_NONE;
			// Is this necessary?
			// currentSelectedSave = param.GetSelectedSave();
			break;
		case SCE_UTILITY_SAVEDATA_TYPE_LISTLOAD:
			DEBUG_LOG(SCEUTILITY, "Loading. Title: %s Save: %s File: %s", param.GetGameName(param.GetPspParam()).c_str(), param.GetGameName(param.GetPspParam()).c_str(), param.GetFileName(param.GetPspParam()).c_str());
			if(param.GetFilenameCount() == 0)
				display = DS_LOAD_NODATA;
			else
				display = DS_LOAD_LIST_CHOICE;
			break;
		case SCE_UTILITY_SAVEDATA_TYPE_SAVE:
			DEBUG_LOG(SCEUTILITY, "Saving. Title: %s Save: %s File: %s", param.GetGameName(param.GetPspParam()).c_str(), param.GetGameName(param.GetPspParam()).c_str(), param.GetFileName(param.GetPspParam()).c_str());
			if (param.GetFileInfo(0).size != 0)
			{
				yesnoChoice = 0;
				display = DS_SAVE_CONFIRM_OVERWRITE;
			}
			else
				display = DS_SAVE_CONFIRM;
			break;
		case SCE_UTILITY_SAVEDATA_TYPE_AUTOSAVE:
			DEBUG_LOG(SCEUTILITY, "Saving. Title: %s Save: %s File: %s", param.GetGameName(param.GetPspParam()).c_str(), param.GetGameName(param.GetPspParam()).c_str(), param.GetFileName(param.GetPspParam()).c_str());
			display = DS_NONE;
			// Is this necessary?
			// currentSelectedSave = param.GetSelectedSave();
			break;
		case SCE_UTILITY_SAVEDATA_TYPE_LISTSAVE:
			DEBUG_LOG(SCEUTILITY, "Saving. Title: %s Save: %s File: %s", param.GetGameName(param.GetPspParam()).c_str(), param.GetGameName(param.GetPspParam()).c_str(), param.GetFileName(param.GetPspParam()).c_str());
			display = DS_SAVE_LIST_CHOICE;
			break;
		case SCE_UTILITY_SAVEDATA_TYPE_LISTDELETE:
			DEBUG_LOG(SCEUTILITY, "Delete. Title: %s Save: %s File: %s", param.GetGameName(param.GetPspParam()).c_str(), param.GetGameName(param.GetPspParam()).c_str(), param.GetFileName(param.GetPspParam()).c_str());
			if(param.GetFilenameCount() == 0)
				display = DS_DELETE_NODATA;
			else
				display = DS_DELETE_LIST_CHOICE;
			break;
		case SCE_UTILITY_SAVEDATA_TYPE_SIZES:
		case SCE_UTILITY_SAVEDATA_TYPE_LIST:
		case SCE_UTILITY_SAVEDATA_TYPE_FILES:
		case SCE_UTILITY_SAVEDATA_TYPE_GETSIZE:
		case SCE_UTILITY_SAVEDATA_TYPE_SINGLEDELETE:
		case SCE_UTILITY_SAVEDATA_TYPE_MAKEDATASECURE:
		case SCE_UTILITY_SAVEDATA_TYPE_MAKEDATA:
		case SCE_UTILITY_SAVEDATA_TYPE_WRITEDATASECURE:
		case SCE_UTILITY_SAVEDATA_TYPE_WRITEDATA:
		case SCE_UTILITY_SAVEDATA_TYPE_READDATASECURE:
		case SCE_UTILITY_SAVEDATA_TYPE_READDATA:
		case SCE_UTILITY_SAVEDATA_TYPE_DELETEDATA:
			display = DS_NONE;
			break;

		case SCE_UTILITY_SAVEDATA_TYPE_DELETE: // When run on a PSP, displays a list of all saves on the PSP. Weird. (Not really, it's to let you free up space)
			display = DS_DELETE_LIST_CHOICE;
			break;
		default:
		{
			ERROR_LOG_REPORT(SCEUTILITY, "Load/Save function %d not coded. Title: %s Save: %s File: %s", (SceUtilitySavedataType)(u32)param.GetPspParam()->mode, param.GetGameName(param.GetPspParam()).c_str(), param.GetGameName(param.GetPspParam()).c_str(), param.GetFileName(param.GetPspParam()).c_str());
			param.GetPspParam()->common.result = 0;
			ChangeStatusInit(SAVEDATA_INIT_DELAY_US);
			display = DS_NONE;
			return 0; // Return 0 should allow the game to continue, but missing function must be implemented and returning the right value or the game can block.
		}
		break;
	}

	if (retval < 0) {
		ChangeStatusShutdown(SAVEDATA_SHUTDOWN_DELAY_US);
	} else {
		ChangeStatusInit(SAVEDATA_INIT_DELAY_US);
	}

	UpdateButtons();
	StartFade(true);

	/*INFO_LOG(SCEUTILITY,"Dump Param :");
	INFO_LOG(SCEUTILITY,"size : %d",param.GetPspParam()->common.size);
	INFO_LOG(SCEUTILITY,"language : %d",param.GetPspParam()->common.language);
	INFO_LOG(SCEUTILITY,"buttonSwap : %d",param.GetPspParam()->common.buttonSwap);
	INFO_LOG(SCEUTILITY,"result : %d",param.GetPspParam()->common.result);
	INFO_LOG(SCEUTILITY,"mode : %d",param.GetPspParam()->mode);
	INFO_LOG(SCEUTILITY,"bind : %d",param.GetPspParam()->bind);
	INFO_LOG(SCEUTILITY,"overwriteMode : %d",param.GetPspParam()->overwriteMode);
	INFO_LOG(SCEUTILITY,"gameName : %s",param.GetGameName(param.GetPspParam()).c_str());
	INFO_LOG(SCEUTILITY,"saveName : %s",param.GetPspParam()->saveName);
	INFO_LOG(SCEUTILITY,"saveNameList : %08x",*((unsigned int*)&param.GetPspParam()->saveNameList));
	INFO_LOG(SCEUTILITY,"fileName : %s",param.GetPspParam()->fileName);
	INFO_LOG(SCEUTILITY,"dataBuf : %08x",*((unsigned int*)&param.GetPspParam()->dataBuf));
	INFO_LOG(SCEUTILITY,"dataBufSize : %u",param.GetPspParam()->dataBufSize);
	INFO_LOG(SCEUTILITY,"dataSize : %u",param.GetPspParam()->dataSize);

	INFO_LOG(SCEUTILITY,"sfo title : %s",param.GetPspParam()->sfoParam.title);
	INFO_LOG(SCEUTILITY,"sfo savedataTitle : %s",param.GetPspParam()->sfoParam.savedataTitle);
	INFO_LOG(SCEUTILITY,"sfo detail : %s",param.GetPspParam()->sfoParam.detail);

	INFO_LOG(SCEUTILITY,"icon0 data : %08x",*((unsigned int*)&param.GetPspParam()->icon0FileData.buf));
	INFO_LOG(SCEUTILITY,"icon0 size : %u",param.GetPspParam()->icon0FileData.bufSize);

	INFO_LOG(SCEUTILITY,"icon1 data : %08x",*((unsigned int*)&param.GetPspParam()->icon1FileData.buf));
	INFO_LOG(SCEUTILITY,"icon1 size : %u",param.GetPspParam()->icon1FileData.bufSize);

	INFO_LOG(SCEUTILITY,"pic1 data : %08x",*((unsigned int*)&param.GetPspParam()->pic1FileData.buf));
	INFO_LOG(SCEUTILITY,"pic1 size : %u",param.GetPspParam()->pic1FileData.bufSize);

	INFO_LOG(SCEUTILITY,"snd0 data : %08x",*((unsigned int*)&param.GetPspParam()->snd0FileData.buf));
	INFO_LOG(SCEUTILITY,"snd0 size : %u",param.GetPspParam()->snd0FileData.bufSize);*/
	return retval;
}

const std::string PSPSaveDialog::GetSelectedSaveDirName() const
{
	switch ((SceUtilitySavedataType)(u32)param.GetPspParam()->mode)
	{
	case SCE_UTILITY_SAVEDATA_TYPE_LOAD:
	case SCE_UTILITY_SAVEDATA_TYPE_AUTOLOAD:
	case SCE_UTILITY_SAVEDATA_TYPE_SAVE:
	case SCE_UTILITY_SAVEDATA_TYPE_AUTOSAVE:
		return param.GetSaveDirName(param.GetPspParam());

	case SCE_UTILITY_SAVEDATA_TYPE_MAKEDATASECURE:
	case SCE_UTILITY_SAVEDATA_TYPE_MAKEDATA:
	case SCE_UTILITY_SAVEDATA_TYPE_READDATASECURE:
	case SCE_UTILITY_SAVEDATA_TYPE_READDATA:
	case SCE_UTILITY_SAVEDATA_TYPE_WRITEDATASECURE:
	case SCE_UTILITY_SAVEDATA_TYPE_WRITEDATA:
	case SCE_UTILITY_SAVEDATA_TYPE_ERASESECURE:
	case SCE_UTILITY_SAVEDATA_TYPE_ERASE:
	case SCE_UTILITY_SAVEDATA_TYPE_DELETEDATA:
		return param.GetSaveDirName(param.GetPspParam());

	// TODO: Maybe also SINGLEDELETE/etc?

	// SIZES ignores saveName it seems.

	default:
		return param.GetSaveDirName(param.GetPspParam(), currentSelectedSave);
		break;
	}
}

void PSPSaveDialog::DisplayBanner(int which)
{
	auto di = GetI18NCategory("Dialog");
	PPGeDrawRect(0, 0, 480, 23, CalcFadedColor(0x65636358));

	PPGeStyle textStyle = FadedStyle(PPGeAlign::BOX_VCENTER, 0.6f);
	textStyle.hasShadow = false;

	const char *title;
	switch (which)
	{
	case DB_SAVE:
		title = di->T("Save");
		break;
	case DB_LOAD:
		title = di->T("Load");
		break;
	case DB_DELETE:
		title = di->T("Delete");
		break;
	default:
		title = "";
		break;
	}
	// TODO: Draw a hexagon icon
	PPGeDrawImage(10, 6, 12.0f, 12.0f, 1, 10, 1, 10, 10, 10, textStyle.color);
	PPGeDrawText(title, 30, 11, textStyle);
}

void PSPSaveDialog::DisplaySaveList(bool canMove) {
	std::lock_guard<std::mutex> guard(paramLock);
	static int upFramesHeld = 0;
	static int downFramesHeld = 0;

	for (int displayCount = 0; displayCount < param.GetFilenameCount(); displayCount++)
	{
		int textureColor = 0xFFFFFFFF;
		auto fileInfo = param.GetFileInfo(displayCount);

		if (fileInfo.size == 0 && fileInfo.texture != NULL)
			textureColor = 0xFF777777;

		// Calc save image position on screen
		float w, h , x, b;
		float y = 97;
		if (displayCount != currentSelectedSave) {
			w = 81;
			h = 45;
			x = 58.5f;
		} else {
			w = 144;
			h = 80;
			x = 27;
			b = 1.2f;
			PPGeDrawRect(x-b, y-b, x+w+b, y, CalcFadedColor(0xD0FFFFFF)); // top border
			PPGeDrawRect(x-b, y, x, y+h, CalcFadedColor(0xD0FFFFFF)); // left border
			PPGeDrawRect(x-b, y+h, x+w+b, y+h+b, CalcFadedColor(0xD0FFFFFF)); //bottom border
			PPGeDrawRect(x+w, y, x+w+b, y+h, CalcFadedColor(0xD0FFFFFF)); //right border
		}
		if (displayCount < currentSelectedSave)
			y -= 13 + 45 * (currentSelectedSave - displayCount);
		else if (displayCount > currentSelectedSave)
			y += 48 + 45 * (displayCount - currentSelectedSave);

		// Skip if it's well outside the screen.
		if (y > 472.0f || y < -200.0f)
			continue;

		int tw = 256;
		int th = 256;
		if (fileInfo.texture != NULL) {
			fileInfo.texture->SetTexture();
			tw = fileInfo.texture->Width();
			th = fileInfo.texture->Height();
			PPGeDrawImage(x, y, w, h, 0, 0, 1, 1, tw, th, textureColor);
		}
		PPGeSetDefaultTexture();
	}

	if (canMove) {
		if ( (IsButtonPressed(CTRL_UP) || IsButtonHeld(CTRL_UP, upFramesHeld)) && currentSelectedSave > 0)
			currentSelectedSave--;

		else if ( (IsButtonPressed(CTRL_DOWN) || IsButtonHeld(CTRL_DOWN, downFramesHeld)) && currentSelectedSave < (param.GetFilenameCount() - 1))
			currentSelectedSave++;
	}
}

void PSPSaveDialog::DisplaySaveIcon(bool checkExists)
{
	std::lock_guard<std::mutex> guard(paramLock);
	int textureColor = CalcFadedColor(0xFFFFFFFF);
	auto curSave = param.GetFileInfo(currentSelectedSave);

	if (curSave.size == 0 && checkExists)
		textureColor = CalcFadedColor(0xFF777777);

	// Calc save image position on screen
	float w = 144;
	float h = 80;
	float x = 27;
	float y = 97;

	int tw = 256;
	int th = 256;
	if (curSave.texture != NULL) {
		curSave.texture->SetTexture();
		tw = curSave.texture->Width();
		th = curSave.texture->Height();
	} else {
		PPGeDisableTexture();
	}
	PPGeDrawImage(x, y, w, h, 0, 0, 1, 1, tw, th, textureColor);
	PPGeSetDefaultTexture();
}

static void FormatSaveHourMin(char *hour_time, size_t sz, const tm &t) {
	const char *am_pm = "AM";
	int hour = t.tm_hour;
	switch (g_Config.iTimeFormat) {
	case 1:
		if (hour == 12) {
			am_pm = "PM";
		} else if (hour > 12) {
			am_pm = "PM";
			hour -= 12;
		} else if (hour == 0) {
			hour = 12;
		}
		snprintf(hour_time, sz, "%02d:%02d %s", hour, t.tm_min, am_pm);
		break;
	case 0:
	default:
		snprintf(hour_time, sz, "%02d:%02d", hour, t.tm_min);
		break;
	}
}

static void FormatSaveDate(char *date, size_t sz, const tm &t) {
	int year = t.tm_year + 1900;
	int month = t.tm_mon + 1;
	switch (g_Config.iDateFormat) {
	case 1:
		snprintf(date, sz, "%02d/%02d/%04d", month, t.tm_mday, year);
		break;
	case 2:
		snprintf(date, sz, "%02d/%02d/%04d", t.tm_mday, month, year);
		break;
	case 0:
	default:
		snprintf(date, sz, "%04d/%02d/%02d", year, month, t.tm_mday);
		break;
	}
}

void PSPSaveDialog::DisplaySaveDataInfo1() {
	std::lock_guard<std::mutex> guard(paramLock);
	const SaveFileInfo &saveInfo = param.GetFileInfo(currentSelectedSave);

	if (saveInfo.size == 0) {
		auto di = GetI18NCategory("Dialog");
		PPGeStyle textStyle = FadedStyle(PPGeAlign::BOX_VCENTER, 0.6f);
		PPGeDrawText(di->T("NEW DATA"), 180, 136, textStyle);
	} else {
		char hour_time[32];
		FormatSaveHourMin(hour_time, sizeof(hour_time), saveInfo.modif_time);

		char date_year[32];
		FormatSaveDate(date_year, sizeof(date_year), saveInfo.modif_time);

		s64 sizeK = saveInfo.size / 1024;

		PPGeDrawRect(180, 136, 480, 137, CalcFadedColor(0xFFFFFFFF));
		std::string titleTxt = saveInfo.title;
		std::string timeTxt = StringFromFormat("%s   %s  %lld KB", date_year, hour_time, sizeK);
		std::string saveTitleTxt = saveInfo.saveTitle;
		std::string saveDetailTxt = saveInfo.saveDetail;

		PPGeStyle titleStyle = FadedStyle(PPGeAlign::BOX_BOTTOM, 0.6f);
		PPGeStyle saveTitleStyle = FadedStyle(PPGeAlign::BOX_LEFT, 0.55f);
		titleStyle.color = CalcFadedColor(0xFFC0C0C0);
		PPGeStyle textStyle = FadedStyle(PPGeAlign::BOX_LEFT, 0.5f);

		PPGeDrawText(titleTxt.c_str(), 180, 136, titleStyle);
		PPGeDrawText(timeTxt.c_str(), 180, 137, textStyle);
		PPGeDrawText(saveTitleTxt.c_str(), 175, 159, saveTitleStyle);
		PPGeDrawTextWrapped(saveDetailTxt.c_str(), 175, 181, 480 - 175, 250 - 181, textStyle);
	}
}

void PSPSaveDialog::DisplaySaveDataInfo2(bool showNewData) {
	std::lock_guard<std::mutex> guard(paramLock);

	tm modif_time;
	const char *save_title;
	u32 data_size;

	if (showNewData || param.GetFileInfo(currentSelectedSave).size == 0) {
		time_t t;
		time(&t);
		localtime_r(&t, &modif_time);
		save_title = param.GetPspParam()->sfoParam.savedataTitle;
		// TODO: Account for icon, etc., etc.
		data_size = param.GetPspParam()->dataSize;
	} else {
		modif_time = param.GetFileInfo(currentSelectedSave).modif_time;
		save_title = param.GetFileInfo(currentSelectedSave).saveTitle;
		data_size = param.GetFileInfo(currentSelectedSave).size;
	}

	char hour_time[32];
	FormatSaveHourMin(hour_time, sizeof(hour_time), modif_time);

	char date_year[32];
	FormatSaveDate(date_year, sizeof(date_year), modif_time);

	s64 sizeK = data_size / 1024;

	PPGeStyle textStyle = FadedStyle(PPGeAlign::BOX_LEFT, 0.5f);
	std::string saveinfoTxt = StringFromFormat("%.128s\n%s  %s\n%lld KB", save_title, date_year, hour_time, sizeK);
	PPGeDrawText(saveinfoTxt.c_str(), 8, 200, textStyle);
}

void PSPSaveDialog::DisplayMessage(std::string text, bool hasYesNo)
{
	PPGeStyle textStyle = FadedStyle(PPGeAlign::BOX_CENTER, FONT_SCALE);

	const float WRAP_WIDTH = 254.0f;
	float y = 136.0f, h;
	PPGeMeasureText(nullptr, &h, text.c_str(), FONT_SCALE, PPGE_LINE_WRAP_WORD, WRAP_WIDTH);
	float h2 = h / 2.0f;
	if (hasYesNo)
	{
		auto di = GetI18NCategory("Dialog");
		const char *choiceText;
		float x, w;
		if (yesnoChoice == 1) {
			choiceText = di->T("Yes");
			x = 302.0f;
		}
		else {
			choiceText = di->T("No");
			x = 366.0f;
		}
		PPGeMeasureText(&w, &h, choiceText, FONT_SCALE);
		w = w / 2.0f + 5.5f;
		h /= 2.0f;
		float y2 = y + h2 + 4.0f;
		h2 += h + 4.0f;
		y = 132.0f - h;
		PPGeDrawRect(x - w, y2 - h, x + w, y2 + h, CalcFadedColor(0x40C0C0C0));
		PPGeDrawText(di->T("Yes"), 302.0f, y2, textStyle);
		PPGeDrawText(di->T("No"), 366.0f, y2, textStyle);
		if (IsButtonPressed(CTRL_LEFT) && yesnoChoice == 0) {
			yesnoChoice = 1;
		}
		else if (IsButtonPressed(CTRL_RIGHT) && yesnoChoice == 1) {
			yesnoChoice = 0;
		}
	}
	PPGeDrawTextWrapped(text.c_str(), 334.0f, y, WRAP_WIDTH, 0, textStyle);
	float sy = 122.0f - h2, ey = 150.0f + h2;
	PPGeDrawRect(202.0f, sy, 466.0f, sy + 1.0f, CalcFadedColor(0xFFFFFFFF));
	PPGeDrawRect(202.0f, ey, 466.0f, ey + 1.0f, CalcFadedColor(0xFFFFFFFF));
}

int PSPSaveDialog::Update(int animSpeed)
{
	if (GetStatus() != SCE_UTILITY_STATUS_RUNNING)
		return SCE_ERROR_UTILITY_INVALID_STATUS;

	if (!param.GetPspParam()) {
		ChangeStatusShutdown(SAVEDATA_SHUTDOWN_DELAY_US);
		return 0;
	}

	if (pendingStatus != SCE_UTILITY_STATUS_RUNNING) {
		// We're actually done, we're just waiting to tell the game that.
		return 0;
	}

	// The struct may have been updated by the game.  This happens in "Where Is My Heart?"
	// Check if it has changed, reload it.
	// TODO: Cut down on preloading?  This rebuilds the list from scratch.
	int size = Memory::Read_U32(requestAddr);
	if (memcmp(Memory::GetPointer(requestAddr), &originalRequest, size) != 0) {
		memset(&request, 0, sizeof(request));
		Memory::Memcpy(&request, requestAddr, size);
		Memory::Memcpy(&originalRequest, requestAddr, size);
		std::lock_guard<std::mutex> guard(paramLock);
		param.SetPspParam(&request);
	}

	UpdateButtons();
	UpdateFade(animSpeed);

	okButtonImg = ImageID("I_CIRCLE");
	cancelButtonImg = ImageID("I_CROSS");
	okButtonFlag = CTRL_CIRCLE;
	cancelButtonFlag = CTRL_CROSS;
	if (param.GetPspParam()->common.buttonSwap == 1) {
		okButtonImg = ImageID("I_CROSS");
		cancelButtonImg = ImageID("I_CIRCLE");
		okButtonFlag = CTRL_CROSS;
		cancelButtonFlag = CTRL_CIRCLE;
	}

	auto di = GetI18NCategory("Dialog");

	switch (display)
	{
		case DS_SAVE_LIST_CHOICE:
			StartDraw();

			DisplaySaveList();
			DisplaySaveDataInfo1();

			DisplayButtons(DS_BUTTON_OK | DS_BUTTON_CANCEL);
			DisplayBanner(DB_SAVE);

			if (IsButtonPressed(cancelButtonFlag)) {
				param.GetPspParam()->common.result = SCE_UTILITY_DIALOG_RESULT_CANCEL;
				StartFade(false);
			} else if (IsButtonPressed(okButtonFlag)) {
				// Save exist, ask user confirm
				if (param.GetFileInfo(currentSelectedSave).size > 0) {
					yesnoChoice = 0;
					display = DS_SAVE_CONFIRM_OVERWRITE;
				} else {
					display = DS_SAVE_SAVING;
					StartIOThread();
				}
			}
			EndDraw();
		break;
		case DS_SAVE_CONFIRM:
			StartDraw();

			DisplaySaveIcon(false);
			DisplaySaveDataInfo2(true);

			DisplayMessage(di->T("Confirm Save", "Do you want to save this data?"), true);

			DisplayButtons(DS_BUTTON_OK | DS_BUTTON_CANCEL);
			DisplayBanner(DB_SAVE);

			if (IsButtonPressed(cancelButtonFlag) || (IsButtonPressed(okButtonFlag) && yesnoChoice == 0)) {
				param.GetPspParam()->common.result = SCE_UTILITY_DIALOG_RESULT_CANCEL;
				StartFade(false);
			} else if (IsButtonPressed(okButtonFlag)) {
				display = DS_SAVE_SAVING;
				StartIOThread();
			}

			EndDraw();
		break;
		case DS_SAVE_CONFIRM_OVERWRITE:
			StartDraw();

			DisplaySaveIcon(true);
			DisplaySaveDataInfo2();

			DisplayMessage(di->T("Confirm Overwrite","Do you want to overwrite the data?"), true);

			DisplayButtons(DS_BUTTON_OK | DS_BUTTON_CANCEL);
			DisplayBanner(DB_SAVE);

			if (IsButtonPressed(cancelButtonFlag) || (IsButtonPressed(okButtonFlag) && yesnoChoice == 0)) {
				if (param.GetPspParam()->mode != SCE_UTILITY_SAVEDATA_TYPE_SAVE)
					display = DS_SAVE_LIST_CHOICE;
				else {
					param.GetPspParam()->common.result = SCE_UTILITY_DIALOG_RESULT_CANCEL;
					StartFade(false);
				}
			} else if (IsButtonPressed(okButtonFlag)) {
				display = DS_SAVE_SAVING;
				StartIOThread();
			}

			EndDraw();
		break;
		case DS_SAVE_SAVING:
			if (ioThreadStatus != SAVEIO_PENDING) {
				JoinIOThread();
			}

			StartDraw();

			DisplaySaveIcon(true);
			DisplaySaveDataInfo2(true);

			DisplayMessage(di->T("Saving","Saving\nPlease Wait..."));

			DisplayBanner(DB_SAVE);

			EndDraw();
		break;
		case DS_SAVE_FAILED:
			JoinIOThread();
			StartDraw();

			DisplaySaveIcon(true);
			DisplaySaveDataInfo2(true);

			DisplayMessage(di->T("SavingFailed", "Unable to save data."));

			DisplayButtons(DS_BUTTON_CANCEL);
			DisplayBanner(DB_SAVE);

			if (IsButtonPressed(cancelButtonFlag)) {
				// Go back to the list so they can try again.
				if (param.GetPspParam()->mode != SCE_UTILITY_SAVEDATA_TYPE_SAVE) {
					display = DS_SAVE_LIST_CHOICE;
				} else {
					param.GetPspParam()->common.result = SCE_UTILITY_DIALOG_RESULT_CANCEL;
					StartFade(false);
				}
			}

			EndDraw();
		break;
		case DS_SAVE_DONE:
			if (ioThread) {
				JoinIOThread();
				param.SetPspParam(param.GetPspParam());
			}
			StartDraw();

			DisplaySaveIcon(true);
			DisplaySaveDataInfo2(true);

			DisplayMessage(di->T("Save completed"));

			DisplayButtons(DS_BUTTON_CANCEL);
			DisplayBanner(DB_SAVE);

			if (IsButtonPressed(cancelButtonFlag)) {
				param.GetPspParam()->common.result = SCE_UTILITY_DIALOG_RESULT_SUCCESS;
				// Set the save to use for autosave and autoload
				param.SetSelectedSave(param.GetFileInfo(currentSelectedSave).idx);
				StartFade(false);
			}

			EndDraw();
		break;

		case DS_LOAD_LIST_CHOICE:
			StartDraw();
			
			DisplaySaveList();
			DisplaySaveDataInfo1();

			DisplayButtons(DS_BUTTON_OK | DS_BUTTON_CANCEL);
			DisplayBanner(DB_LOAD);

			if (IsButtonPressed(cancelButtonFlag)) {
				param.GetPspParam()->common.result = SCE_UTILITY_DIALOG_RESULT_CANCEL;
				StartFade(false);
			} else if (IsButtonPressed(okButtonFlag)) {
				display = DS_LOAD_LOADING;
				StartIOThread();
			}

			EndDraw();
		break;
		case DS_LOAD_CONFIRM:
			StartDraw();

			DisplaySaveIcon(true);
			DisplaySaveDataInfo2();

			DisplayMessage(di->T("ConfirmLoad", "Load this data?"), true);

			DisplayButtons(DS_BUTTON_OK | DS_BUTTON_CANCEL);
			DisplayBanner(DB_LOAD);

			if (IsButtonPressed(cancelButtonFlag) || (IsButtonPressed(okButtonFlag) && yesnoChoice == 0)) {
				param.GetPspParam()->common.result = SCE_UTILITY_DIALOG_RESULT_CANCEL;
				StartFade(false);
			} else if (IsButtonPressed(okButtonFlag)) {
				display = DS_LOAD_LOADING;
				StartIOThread();
			}

			EndDraw();
		break;
		case DS_LOAD_LOADING:
			if (ioThreadStatus != SAVEIO_PENDING) {
				JoinIOThread();
			}

			StartDraw();

			DisplaySaveIcon(true);
			DisplaySaveDataInfo2();

			DisplayMessage(di->T("Loading","Loading\nPlease Wait..."));

			DisplayBanner(DB_LOAD);

			EndDraw();
		break;
		case DS_LOAD_FAILED:
			JoinIOThread();
			StartDraw();

			DisplaySaveIcon(true);
			DisplaySaveDataInfo2();

			DisplayMessage(di->T("LoadingFailed", "Unable to load data."));

			DisplayButtons(DS_BUTTON_CANCEL);
			DisplayBanner(DB_LOAD);

			if (IsButtonPressed(cancelButtonFlag)) {
				// Go back to the list so they can try again.
				if (param.GetPspParam()->mode != SCE_UTILITY_SAVEDATA_TYPE_LOAD) {
					display = DS_LOAD_LIST_CHOICE;
				} else {
					param.GetPspParam()->common.result = SCE_UTILITY_DIALOG_RESULT_CANCEL;
					StartFade(false);
				}
			}

			EndDraw();
		break;
		case DS_LOAD_DONE:
			JoinIOThread();
			StartDraw();
			
			DisplaySaveIcon(true);
			DisplaySaveDataInfo2();

			DisplayMessage(di->T("Load completed"));

			DisplayButtons(DS_BUTTON_CANCEL);
			DisplayBanner(DB_LOAD);

			// Allow OK to be pressed as well to confirm the save.
			// The PSP only allows cancel, but that's generally not great UX.
			// Allowing this here makes it quicker for most users to get into the actual game.
			if (IsButtonPressed(cancelButtonFlag) || IsButtonPressed(okButtonFlag)) {
				param.GetPspParam()->common.result = SCE_UTILITY_DIALOG_RESULT_SUCCESS;
				// Set the save to use for autosave and autoload
				param.SetSelectedSave(param.GetFileInfo(currentSelectedSave).idx);
				StartFade(false);
			}

			EndDraw();
		break;
		case DS_LOAD_NODATA:
			StartDraw();

			DisplayMessage(di->T("There is no data"));

			DisplayButtons(DS_BUTTON_CANCEL);
			DisplayBanner(DB_LOAD);

			if (IsButtonPressed(cancelButtonFlag)) {
				param.GetPspParam()->common.result = SCE_UTILITY_SAVEDATA_ERROR_LOAD_NO_DATA;
				StartFade(false);
			}

			EndDraw();
		break;

		case DS_DELETE_LIST_CHOICE:
			StartDraw();
			
			DisplaySaveList();
			DisplaySaveDataInfo1();

			DisplayButtons(DS_BUTTON_OK | DS_BUTTON_CANCEL);
			DisplayBanner(DB_DELETE);

			if (IsButtonPressed(cancelButtonFlag)) {
				param.GetPspParam()->common.result = SCE_UTILITY_DIALOG_RESULT_CANCEL;
				StartFade(false);
			} else if (IsButtonPressed(okButtonFlag)) {
				yesnoChoice = 0;
				display = DS_DELETE_CONFIRM;
			}

			EndDraw();
		break;
		case DS_DELETE_CONFIRM:
			StartDraw();

			DisplaySaveIcon(true);
			DisplaySaveDataInfo2();

			DisplayMessage(di->T("DeleteConfirm", 
						"This save data will be deleted.\nAre you sure you want to continue?"), 
						true);

			DisplayButtons(DS_BUTTON_OK | DS_BUTTON_CANCEL);
			DisplayBanner(DB_DELETE);

			if (IsButtonPressed(cancelButtonFlag))
				display = DS_DELETE_LIST_CHOICE;
			else if (IsButtonPressed(okButtonFlag)) {
				if (yesnoChoice == 0)
					display = DS_DELETE_LIST_CHOICE;
				else {
					display = DS_DELETE_DELETING;
					StartIOThread();
				}
			}

			EndDraw();
		break;
		case DS_DELETE_DELETING:
			if (ioThreadStatus != SAVEIO_PENDING) {
				JoinIOThread();
			}

			StartDraw();

			DisplayMessage(di->T("Deleting","Deleting\nPlease Wait..."));

			DisplayBanner(DB_DELETE);

			EndDraw();
		break;
		case DS_DELETE_FAILED:
			JoinIOThread();
			StartDraw();

			DisplayMessage(di->T("DeleteFailed", "Unable to delete data."));

			DisplayButtons(DS_BUTTON_CANCEL);
			DisplayBanner(DB_DELETE);

			if (IsButtonPressed(cancelButtonFlag)) {
				display = DS_DELETE_LIST_CHOICE;
			}

			EndDraw();
		break;
		case DS_DELETE_DONE:
			if (ioThread) {
				JoinIOThread();
				param.SetPspParam(param.GetPspParam());
			}
			StartDraw();
			
			DisplayMessage(di->T("Delete completed"));

			DisplayButtons(DS_BUTTON_CANCEL);
			DisplayBanner(DB_DELETE);

			if (IsButtonPressed(cancelButtonFlag)) {
				if (param.GetFilenameCount() == 0)
					display = DS_DELETE_NODATA;
				else
					display = DS_DELETE_LIST_CHOICE;
			}

			EndDraw();
		break;
		case DS_DELETE_NODATA:
			StartDraw();
			
			DisplayMessage(di->T("There is no data"));

			DisplayButtons(DS_BUTTON_CANCEL);
			DisplayBanner(DB_DELETE);

			if (IsButtonPressed(cancelButtonFlag)) {
				param.GetPspParam()->common.result = SCE_UTILITY_SAVEDATA_ERROR_DELETE_NO_DATA;
				StartFade(false);
			}

			EndDraw();
		break;

		case DS_NONE: // For action which display nothing
			switch (ioThreadStatus) {
			case SAVEIO_NONE:
				StartIOThread();
				break;
			case SAVEIO_PENDING:
			case SAVEIO_DONE:
				// To make sure there aren't any timing variations, we sync the next frame.
				JoinIOThread();
				ChangeStatus(SCE_UTILITY_STATUS_FINISHED, 0);
				break;
			}
		break;

		default:
			ChangeStatus(SCE_UTILITY_STATUS_FINISHED, 0);
		break;
	}

	if (status == SCE_UTILITY_STATUS_FINISHED || pendingStatus == SCE_UTILITY_STATUS_FINISHED)
		Memory::Memcpy(requestAddr, &request, request.common.size);
	
	return 0;
}

void PSPSaveDialog::ExecuteIOAction() {
	std::lock_guard<std::mutex> guard(paramLock);
	switch (display) {
	case DS_LOAD_LOADING:
		if (param.Load(param.GetPspParam(), GetSelectedSaveDirName(), currentSelectedSave) == 0) {
			display = DS_LOAD_DONE;
		} else {
			display = DS_LOAD_FAILED;
		}
		break;
	case DS_SAVE_SAVING:
		if (param.Save(param.GetPspParam(), GetSelectedSaveDirName()) == 0) {
			display = DS_SAVE_DONE;
		} else {
			display = DS_SAVE_FAILED;
		}
		break;
	case DS_DELETE_DELETING:
		if (param.Delete(param.GetPspParam(),currentSelectedSave)) {
			display = DS_DELETE_DONE;
		} else {
			display = DS_DELETE_FAILED;
		}
		break;
	case DS_NONE:
		ExecuteNotVisibleIOAction();
		break;

	default:
		// Nothing to do here.
		break;
	}

	ioThreadStatus = SAVEIO_DONE;
}

void PSPSaveDialog::ExecuteNotVisibleIOAction() {
	auto &result = param.GetPspParam()->common.result;

	switch ((SceUtilitySavedataType)(u32)param.GetPspParam()->mode) {
	case SCE_UTILITY_SAVEDATA_TYPE_LOAD: // Only load and exit
	case SCE_UTILITY_SAVEDATA_TYPE_AUTOLOAD:
		result = param.Load(param.GetPspParam(), GetSelectedSaveDirName(), currentSelectedSave);
		break;
	case SCE_UTILITY_SAVEDATA_TYPE_SAVE: // Only save and exit
	case SCE_UTILITY_SAVEDATA_TYPE_AUTOSAVE:
		result = param.Save(param.GetPspParam(), GetSelectedSaveDirName());
		break;
	case SCE_UTILITY_SAVEDATA_TYPE_SIZES:
		result = param.GetSizes(param.GetPspParam());
		break;
	case SCE_UTILITY_SAVEDATA_TYPE_LIST:
		param.GetList(param.GetPspParam());
		result = 0;
		break;
	case SCE_UTILITY_SAVEDATA_TYPE_FILES:
		result = param.GetFilesList(param.GetPspParam());
		break;
	case SCE_UTILITY_SAVEDATA_TYPE_GETSIZE:
		{
			bool sizeResult = param.GetSize(param.GetPspParam());
			// TODO: According to JPCSP, should test/verify this part but seems edge casey.
			if (MemoryStick_State() != PSP_MEMORYSTICK_STATE_INSERTED) {
				result = SCE_UTILITY_SAVEDATA_ERROR_RW_NO_MEMSTICK;
			} else if (sizeResult) {
				result = 0;
			} else {
				result = SCE_UTILITY_SAVEDATA_ERROR_RW_NO_DATA;
			}
		}
		break;
	case SCE_UTILITY_SAVEDATA_TYPE_DELETEDATA:
		DEBUG_LOG(SCEUTILITY, "sceUtilitySavedata DELETEDATA: %s", param.GetPspParam()->saveName);
		result = param.DeleteData(param.GetPspParam());
		break;
	//case SCE_UTILITY_SAVEDATA_TYPE_AUTODELETE:
	case SCE_UTILITY_SAVEDATA_TYPE_SINGLEDELETE:
		if (param.Delete(param.GetPspParam(), param.GetSelectedSave())) {
			result = 0;
		} else {
			result = SCE_UTILITY_SAVEDATA_ERROR_DELETE_NO_DATA;
		}
		break;
	// TODO: Should reset the directory's other files.
	case SCE_UTILITY_SAVEDATA_TYPE_MAKEDATA:
	case SCE_UTILITY_SAVEDATA_TYPE_MAKEDATASECURE:
		result = param.Save(param.GetPspParam(), GetSelectedSaveDirName(), param.GetPspParam()->mode == SCE_UTILITY_SAVEDATA_TYPE_MAKEDATASECURE);
		if (result == SCE_UTILITY_SAVEDATA_ERROR_SAVE_MS_NOSPACE) {
			result = SCE_UTILITY_SAVEDATA_ERROR_RW_MEMSTICK_FULL;
		}
		break;
	case SCE_UTILITY_SAVEDATA_TYPE_WRITEDATA:
	case SCE_UTILITY_SAVEDATA_TYPE_WRITEDATASECURE:
		result = param.Save(param.GetPspParam(), GetSelectedSaveDirName(), param.GetPspParam()->mode == SCE_UTILITY_SAVEDATA_TYPE_WRITEDATASECURE);
		break;
	case SCE_UTILITY_SAVEDATA_TYPE_READDATA:
	case SCE_UTILITY_SAVEDATA_TYPE_READDATASECURE:
		if (!param.IsSaveDirectoryExist(param.GetPspParam())){
			result = SCE_UTILITY_SAVEDATA_ERROR_RW_NO_DATA;
		} else if (!param.IsSfoFileExist(param.GetPspParam())) {
			result = SCE_UTILITY_SAVEDATA_ERROR_RW_DATA_BROKEN;
		} else if (param.Load(param.GetPspParam(), GetSelectedSaveDirName(), currentSelectedSave, param.GetPspParam()->mode == SCE_UTILITY_SAVEDATA_TYPE_READDATASECURE) == 0) {
			result = 0;
		} else {
			result = SCE_UTILITY_SAVEDATA_ERROR_RW_FILE_NOT_FOUND;
		}
		break;
	default:
		break;
	}
}

void PSPSaveDialog::JoinIOThread() {
	if (ioThread) {
		ioThread->join();
		delete ioThread;
		ioThread = 0;
	}
}

static void DoExecuteIOAction(PSPSaveDialog *dialog) {
	setCurrentThreadName("SaveIO");
	dialog->ExecuteIOAction();
}

void PSPSaveDialog::StartIOThread() {
	if (ioThread) {
		WARN_LOG_REPORT(SCEUTILITY, "Starting a save io thread when one already pending, uh oh.");
		JoinIOThread();
	}

	ioThreadStatus = SAVEIO_PENDING;
	ioThread = new std::thread(&DoExecuteIOAction, this);
}

int PSPSaveDialog::Shutdown(bool force) {
	if (GetStatus() != SCE_UTILITY_STATUS_FINISHED && !force)
		return SCE_ERROR_UTILITY_INVALID_STATUS;

	JoinIOThread();
	ioThreadStatus = SAVEIO_NONE;

	PSPDialog::Shutdown(force);
	if (!force) {
		ChangeStatusShutdown(SAVEDATA_SHUTDOWN_DELAY_US);
	}
	param.SetPspParam(0);

	return 0;
}

void PSPSaveDialog::DoState(PointerWrap &p) {
	JoinIOThread();
	PSPDialog::DoState(p);

	auto s = p.Section("PSPSaveDialog", 1, 2);
	if (!s) {
		return;
	}

	Do(p, display);
	param.DoState(p);
	Do(p, request);
	// Just reset it.
	bool hasParam = param.GetPspParam() != NULL;
	Do(p, hasParam);
	if (hasParam) {
		param.SetPspParam(&request);
	}
	Do(p, requestAddr);
	Do(p, currentSelectedSave);
	Do(p, yesnoChoice);
	if (s > 2) {
		Do(p, ioThreadStatus);
	} else {
		ioThreadStatus = SAVEIO_NONE;
	}
}

pspUtilityDialogCommon *PSPSaveDialog::GetCommonParam() {
	return &param.GetPspParam()->common;
}