#include <algorithm>
#include <vector>

#include "Common/File/Path.h"
#include "Common/File/FileUtil.h"
#include "Common/File/DirListing.h"
#include "Common/System/System.h"
#include "Common/Log.h"
#include "Common/TimeUtil.h"
#include "Common/StringUtils.h"
#include "Common/Data/Text/I18n.h"
#include "Common/Data/Text/Parsers.h"

#include "Core/Util/MemStick.h"
#include "Core/Config.h"
#include "Core/System.h"
#include "Core/Reporting.h"

bool FolderSeemsToBeUsed(const Path &newMemstickFolder) {
	// Inspect the potential new folder, quickly.
	if (File::Exists(newMemstickFolder / "PSP/SAVEDATA") || File::Exists(newMemstickFolder / "SAVEDATA")) {
		// Does seem likely. We could add more criteria like checking for actual savegames or something.
		return true;
	} else {
		return false;
	}
}

bool SwitchMemstickFolderTo(Path newMemstickFolder) {
	// Doesn't already exist, create.
	// Should this ever happen?
	if (newMemstickFolder.Type() == PathType::NATIVE) {
		if (!File::Exists(newMemstickFolder)) {
			File::CreateFullPath(newMemstickFolder);
		}
		Path testWriteFile = newMemstickFolder / ".write_verify_file";
		if (!File::WriteDataToFile(true, "1", 1, testWriteFile)) {
			return false;
		}
		File::Delete(testWriteFile);
	} else {
		// TODO: Do the same but with scoped storage? Not really necessary, right? If it came from a browse
		// for folder, we can assume it exists and is writable, barring wacky race conditions like the user
		// being connected by USB and deleting it.
	}

	Path memStickDirFile = g_Config.internalDataDirectory / "memstick_dir.txt";
#if PPSSPP_PLATFORM(UWP)
	File::Delete(memStickDirFile);
	if (newMemstickFolder != g_Config.internalDataDirectory) {
#endif

		std::string str = newMemstickFolder.ToString();
		if (!File::WriteDataToFile(true, str.c_str(), str.size(), memStickDirFile)) {
			ERROR_LOG(Log::System, "Failed to write memstick path '%s' to '%s'", newMemstickFolder.c_str(), memStickDirFile.c_str());
			// Not sure what to do if this file can't be written.  Disk full?
		}

#if PPSSPP_PLATFORM(UWP)
	}
#endif

	// Save so the settings, at least, are transferred.
	g_Config.memStickDirectory = newMemstickFolder;
	g_Config.SetSearchPath(GetSysDirectory(DIRECTORY_SYSTEM));
	g_Config.UpdateIniLocation();
	return true;
}

// Keep the size with the file, so we can skip overly large ones in the move.
// The user will have to take care of them afterwards, it'll just take too long probably.
struct FileSuffix {
	std::string suffix;
	u64 fileSize;
};

static bool ListFileSuffixesRecursively(const Path &root, const Path &folder, std::vector<std::string> &dirSuffixes, std::vector<FileSuffix> &fileSuffixes, MoveProgressReporter &progressReporter) {
	std::vector<File::FileInfo> files;
	if (!File::GetFilesInDir(folder, &files)) {
		return false;
	}

	for (auto &file : files) {
		if (file.isDirectory) {
			std::string dirSuffix;
			if (root.ComputePathTo(file.fullName, dirSuffix)) {
				if (!dirSuffix.empty()) {
					dirSuffixes.push_back(dirSuffix);
					ListFileSuffixesRecursively(root, folder / file.name, dirSuffixes, fileSuffixes, progressReporter);
					progressReporter.SetProgress(file.name, fileSuffixes.size(), (size_t)-1);
				}
			} else {
				ERROR_LOG_REPORT(Log::System, "Failed to compute PathTo from '%s' to '%s'", root.c_str(), folder.c_str());
			}
		} else {
			std::string fileSuffix;
			if (root.ComputePathTo(file.fullName, fileSuffix)) {
				if (!fileSuffix.empty()) {
					fileSuffixes.push_back(FileSuffix{ fileSuffix, file.size });
				}
			}
		}
	}

	return true;
}

bool MoveChildrenFast(const Path &moveSrc, const Path &moveDest, MoveProgressReporter &progressReporter) {
	std::vector<File::FileInfo> files;
	if (!File::GetFilesInDir(moveSrc, &files)) {
		return false;
	}

	for (size_t i = 0; i < files.size(); i++) {
		auto &file = files[i];
		// Construct destination path
		Path fileSrc = file.fullName;
		Path fileDest = moveDest / file.name;
		progressReporter.SetProgress(file.name, i, files.size());
		INFO_LOG(Log::System, "Fast-moving PSP data from '%s' to '%s' (%d/%d)", fileSrc.c_str(), fileDest.c_str(), (int)i + 1, (int)files.size());
		bool result = File::MoveIfFast(fileSrc, fileDest);
		if (!result) {
			// TODO: Should we try to move back anything that succeeded before this one?
			INFO_LOG(Log::System, "Failed to fast-move '%s' to '%s'", fileSrc.c_str(), fileDest.c_str());
			return false;
		}
	}
	INFO_LOG(Log::System, "Done with fast-move.");
	return true;
}

std::string MoveProgressReporter::Format() {
	std::string str;
	{
		std::lock_guard<std::mutex> guard(mutex_);
		if (max_ > 0) {
			str = StringFromFormat("(%d/%d) ", count_ + 1, max_);
		} else if (max_ < 0) {
			str = StringFromFormat("(%d/?) ", count_ + 1);
		}
		str += progress_;
	}
	return str;
}

MoveResult *MoveDirectoryContentsSafe(Path moveSrc, Path moveDest, MoveProgressReporter &progressReporter) {
	auto ms = GetI18NCategory(I18NCat::MEMSTICK);
	if (moveSrc.GetFilename() != "PSP") {
		moveSrc = moveSrc / "PSP";
	}
	if (moveDest.GetFilename() != "PSP") {
		moveDest = moveDest / "PSP";
		File::CreateDir(moveDest);
	}

	INFO_LOG(Log::System, "About to move PSP data from '%s' to '%s'", moveSrc.c_str(), moveDest.c_str());

	Instant moveStart = Instant::Now();

	// First, we try the cheapest and safest way to move: Can we move files directly within the same device?
	// We loop through the files/dirs in the source directory and just try to move them, it should work.
	if (MoveChildrenFast(moveSrc, moveDest, progressReporter)) {
		INFO_LOG(Log::System, "Quick-move succeeded after %0.1f ms", moveStart.ElapsedMs());
		progressReporter.SetProgress(StringFromFormat("%s (%0.3f)", ms->T_cstr("Done!"), moveStart.ElapsedSeconds()));
		return new MoveResult{ true, "" };
	} else {
		INFO_LOG(Log::System, "Quick move denied after %0.1f ms, falling back to slow move.", moveStart.ElapsedMs());
	}

	// If this doesn't work, we'll fall back on a recursive *copy* (disk space is less of a concern when
	// moving from device to device, other than that everything fits on the destination).
	// Then we verify the results before we delete the originals.

	// Search through recursively, listing the files to move and also summing their sizes.
	std::vector<FileSuffix> fileSuffixesToMove;
	std::vector<std::string> directorySuffixesToCreate;

	// NOTE: It's correct to pass moveSrc twice here, it's to keep the root in the recursion.
	if (!ListFileSuffixesRecursively(moveSrc, moveSrc, directorySuffixesToCreate, fileSuffixesToMove, progressReporter)) {
		// TODO: Handle failure listing files.
		const char *error = "Failed to read old directory";
		INFO_LOG(Log::System, "%s", error);
		progressReporter.SetProgress(ms->T(error));
		return new MoveResult{ false, error };
	}

	bool dryRun = false;  // Useful for debugging. Probably want to remove the MoveChildrenFast path above to test it.

	size_t failedFileCount = 0;

	// We're not moving huge files like ISOs during this process, unless
	// they can be directly moved, without rewriting the file.
	const uint64_t BIG_FILE_THRESHOLD = 24 * 1024 * 1024;

	if (moveSrc.empty()) {
		// Shouldn't happen.
		return new MoveResult{ true, "", };
	}

	// Better not interrupt the app while this is happening!

	// Create all the necessary directories.
	for (size_t i = 0; i < directorySuffixesToCreate.size(); i++) {
		const auto &dirSuffix = directorySuffixesToCreate[i];
		Path dir = moveDest / dirSuffix;
		if (dryRun) {
			INFO_LOG(Log::System, "dry run: Would have created dir '%s'", dir.c_str());
		} else {
			INFO_LOG(Log::System, "Creating dir '%s'", dir.c_str());
			progressReporter.SetProgress(dirSuffix);
			// Just ignore already-exists errors.
			File::CreateDir(dir);
		}
	}

	for (size_t i = 0; i < fileSuffixesToMove.size(); i++) {
		const auto &fileSuffix = fileSuffixesToMove[i];
		progressReporter.SetProgress(StringFromFormat("%s (%s)", fileSuffix.suffix.c_str(), NiceSizeFormat(fileSuffix.fileSize).c_str()),
			i, fileSuffixesToMove.size());

		Path from = moveSrc / fileSuffix.suffix;
		Path to = moveDest / fileSuffix.suffix;

		if (dryRun) {
			INFO_LOG(Log::System, "dry run: Would have moved '%s' to '%s' (%d bytes)", from.c_str(), to.c_str(), (int)fileSuffix.fileSize);
		} else {
			// Remove the "from" prefix from the path.
			// We have to drop down to string operations for this.
			if (File::Exists(to)) {
				WARN_LOG(Log::System, "Target file '%s' already exists. Will be overwritten", to.c_str());
			}

			if (!File::Copy(from, to)) {
				ERROR_LOG(Log::System, "Failed to copy file '%s' to '%s'", from.c_str(), to.c_str());
				failedFileCount++;
				// Should probably just bail?
			} else {
				INFO_LOG(Log::System, "Copied file '%s' to '%s' (size: %d)", from.c_str(), to.c_str(), (int)fileSuffix.fileSize);
			}
		}
	}

	if (failedFileCount) {
		ERROR_LOG(Log::System, "Copy failed (%d files failed)", (int)failedFileCount);
		return new MoveResult{ false, "", failedFileCount };
	}

	INFO_LOG(Log::System, "Done copying. Moving to verification.");

	// After the whole move, verify that all the files arrived correctly.
	// If there's a single error, we do not delete the source data.
	bool ok = true;
	for (size_t i = 0; i < fileSuffixesToMove.size(); i++) {
		const auto &fileSuffix = fileSuffixesToMove[i];
		progressReporter.SetProgress(ms->T("Checking..."), i, fileSuffixesToMove.size());

		Path to = moveDest / fileSuffix.suffix;

		File::FileInfo info;
		if (!File::GetFileInfo(to, &info)) {
			ok = false;
			break;
		}

		if (!info.exists) {
			ERROR_LOG(Log::System, "File '%s' missing at target location '%s'.", fileSuffix.suffix.c_str(), to.c_str());
			ok = false;
			failedFileCount++;
			break;
		}

		// More lenient handling for .nomedia files.
		if (fileSuffix.fileSize != info.size && !endsWithNoCase(fileSuffix.suffix, ".nomedia")) {
			ERROR_LOG(Log::System, "Mismatched size in target file %s (expected %d, was %d bytes). Verification failed.", fileSuffix.suffix.c_str(), (int)fileSuffix.fileSize, (int)info.size);
			ok = false;
			failedFileCount++;
			break;
		}
	}

	if (!ok) {
		return new MoveResult{ false, "", failedFileCount };
	}

	INFO_LOG(Log::System, "Verification complete");

	// Delete all the old, now hopefully empty, directories.
	// Hopefully DeleteDir actually fails if it contains a file...
	// Reverse the order to delete subfolders before parents.
	std::reverse(directorySuffixesToCreate.begin(), directorySuffixesToCreate.end());
	for (size_t i = 0; i < directorySuffixesToCreate.size(); i++) {
		const auto &dirSuffix = directorySuffixesToCreate[i];
		Path dir = moveSrc / dirSuffix;
		if (dryRun) {
			INFO_LOG(Log::System, "dry run: Would have deleted dir '%s'", dir.c_str());
		} else {
			INFO_LOG(Log::System, "Deleting dir '%s'", dir.c_str());
			progressReporter.SetProgress(dirSuffix, i, directorySuffixesToCreate.size());
			if (File::Exists(dir)) {
				File::DeleteDir(dir);
			}
		}
	}
	return new MoveResult{ true, "", 0 };
}
