/**
* Copyright 1998-2013 Epic Games, Inc. All Rights Reserved.
*/

/**
* This class reads a set of files from Live/NP servers and uses it to
* update the game.
*/
class IniLocPatcher extends Object
	native
	config(Engine);

/** Holds the list of files to download and their download state */
struct native IniLocFileEntry
{
	/** The file to read from the online service */
	var string Filename;
	/** Unique name of file only used for downloading */
	var string DLName;
	/** Hash to verify integrity of file against cached version */
	var string HashCode;
	/** Whether the file should be treated as unicode or not */
	var bool bIsUnicode;
	/** The state of that read */
	var EOnlineEnumerationReadState ReadState;
};

/** The list of files to request from the online service */
var config array<IniLocFileEntry> Files;

/** if TRUE Then file list is downloaded from EMS otherwise it comes from config */
var config bool bRequestEmsFileList;
/** Max age in seconds to keep cached EMS files */
var config int MaxCachedFileAge;

/** Interface for downloading files */
var transient OnlineTitleFileInterface TitleFileInterface;
/** Interface for caching files to/from disk */
var transient OnlineTitleFileCacheInterface TitleFileCacheInterface;

/** list of delegates that get called whenever a file is downloaded or loaded from cache */
var array<delegate<OnReadTitleFileComplete> > ReadTitleFileCompleteDelegates;
// @ZOMBIE_FOXTROT_BEGIN - ccooper 1/9/2014 - Adding delegate container for OnAllTitleFilesCompleted
var array<delegate<OnAllTitleFilesCompleted> > AllTitleFilesCompletedDelegates;
// @ZOMBIE_FOXTROT_END
/**
* Delegate fired when a file read from the network platform's title specific storage is complete
*
* @param bWasSuccessful whether the file read was successful or not
* @param FileName the name of the file this was for
*/
delegate OnReadTitleFileComplete(bool bWasSuccessful,string FileName);

/**
* Delegate fired when all title files have completed processing
*/
delegate OnAllTitleFilesCompleted();

/**
* Initializes the patcher, sets delegates, vars, etc.
*/
function Init()
{
	local OnlineSubsystem OnlineSub;
	local int Index;

	OnlineSub = class'GameEngine'.static.GetOnlineSubsystem();
	if (OnlineSub != None)
	{
		TitleFileInterface = OnlineSub.TitleFileInterface;
		if (TitleFileInterface != None)
		{
			// Set the callback for notifications of files completing download
			TitleFileInterface.AddReadTitleFileCompleteDelegate(OnDownloadFileComplete);
		}
		else
		{
			// Mark all as failed to be read since there is no way to read them
			for (Index = 0; Index < Files.Length; Index++)
			{
				Files[Index].ReadState = OERS_Failed;
			}
		}
		// Hook up the caching interface
		TitleFileCacheInterface = OnlineSub.TitleFileCacheInterface;
		if (TitleFileCacheInterface != None)
		{
			// Set the callback for notifications of files completing a load
			TitleFileCacheInterface.AddLoadTitleFileCompleteDelegate(OnFileCacheLoadComplete);
			TitleFileCacheInterface.AddSaveTitleFileCompleteDelegate(OnFileCacheSaveComplete);
		}
	}
}

/**
* Reads the set of files from the online service
*/
function DownloadFiles()
{
	local int FileIdx;

	if (bRequestEmsFileList)
	{
		// Delete stale files
		if (MaxCachedFileAge > 0 &&
			TitleFileCacheInterface != None)
		{
			TitleFileCacheInterface.DeleteTitleFiles(MaxCachedFileAge);
		}
		// Get a list of files that should be downloaded
		TitleFileInterface.AddRequestTitleFileListCompleteDelegate(OnRequestTitleFileListComplete);
		TitleFileInterface.RequestTitleFileList();
	}
	else
	{
		// config DLNames and Filenames are expected to match unless requested
		for (FileIdx = 0; FileIdx < Files.Length; FileIdx++)
		{
			Files[FileIdx].DLName = Files[FileIdx].Filename;
		}
		StartLoadingFiles();
	}
}

/**
* Delegate fired when the request for a list of files completes
*
* @param bWasSuccessful whether the request completed successfully
* @param ResultStr contains the list of files and associated meta data
*/
function OnRequestTitleFileListComplete(bool bWasSuccessful, array<string> ResultStr)
{
	local JsonObject Root;
	local int JsonObjectIdx;
	local IniLocFileEntry RequestFileEntry;
	local int Index;

	TitleFileInterface.ClearRequestTitleFileListCompleteDelegate(OnRequestTitleFileListComplete);
	if (bWasSuccessful)
	{
		Root = class'JsonObject'.static.DecodeJson(ResultStr[0]);
		if (Root != None)
		{
			Files.Length = 0;
			// fill in the list of RequestFiles
			for (JsonObjectIdx=0; JsonObjectIdx < Root.ObjectArray.Length; JsonObjectIdx++)
			{
				RequestFileEntry.Filename = Root.ObjectArray[JsonObjectIdx].GetStringValue("file_name");
				RequestFileEntry.DLName = Root.ObjectArray[JsonObjectIdx].GetStringValue("dl_name");
				RequestFileEntry.HashCode = Root.ObjectArray[JsonObjectIdx].GetStringValue("hash_code");
				// Any file that is not INT or INI should be Unicode
				RequestFileEntry.bIsUnicode = InStr(RequestFileEntry.FileName,".ini",,true) == INDEX_NONE && InStr(RequestFileEntry.FileName,".int",,true) == INDEX_NONE;
				Files.AddItem(RequestFileEntry);
			}
			// Determine which files can be loaded from the cache that don't require downloading
			StartLoadingFiles();
		}
		else
		{
			`log(`location @ "Download of file list failed. Bad json."
				@"ResultStr="$ResultStr[0]);

			// @ZOMBIE_FOXTROT_BEGIN - ccooper 1/13/2014 - Changed code to call the completed delegate if the title list fails since the client still wants to know about it
			for(Index = 0; Index < AllTitleFilesCompletedDelegates.Length; ++Index)
			{
				if(AllTitleFilesCompletedDelegates[Index] != none)
				{
					OnAllTitleFilesCompleted = AllTitleFilesCompletedDelegates[Index];
					OnAllTitleFilesCompleted();
					OnAllTitleFilesCompleted = none;
				}
			}
			// @ZOMBIE_FOXTROT_END
		}

	}
	else
	{
		`log(`location@"Download of file list failed.",, 'DevConfig');

		// @ZOMBIE_FOXTROT_BEGIN - ccooper 1/13/2014 - Changed code to call the completed delegate if the title list fails since the client still wants to know about it
		for(Index = 0; Index < AllTitleFilesCompletedDelegates.Length; ++Index)
		{
			if(AllTitleFilesCompletedDelegates[Index] != none)
			{
				OnAllTitleFilesCompleted = AllTitleFilesCompletedDelegates[Index];
				OnAllTitleFilesCompleted();
				OnAllTitleFilesCompleted = none;
			}
		}
		// @ZOMBIE_FOXTROT_END
	}
}

/**
* Kick off the cache/download requests
*/
function StartLoadingFiles()
{
	local int Index;

	// If there is online interface, then try to download the files
	if (bRequestEmsFileList && TitleFileCacheInterface != None)
	{
		`log(`location@"if (bRequestEmsFileList && TitleFileCacheInterface != None)"@`showvar(Files.Length),, 'DevConfig');
		// Iterate through files trying to download them
		for (Index = 0; Index < Files.Length; Index++)
		{
			// Kick off the read of that file if not already started or failed
			if (Files[Index].ReadState == OERS_NotStarted)
			{
				Files[Index].ReadState = OERS_InProgress;
				if (!TitleFileCacheInterface.LoadTitleFile(Files[Index].DLName))
				{
					Files[Index].ReadState = OERS_Failed;
				}
			}
		}
	}
	else if (TitleFileInterface != None)
	{
		`log(`location@"else if (TitleFileInterface != None)"@`showvar(Files.Length),, 'DevConfig');
		// Iterate through files trying to download them
		for (Index = 0; Index < Files.Length; Index++)
		{
			// Kick off the read of that file if not already started or failed
			if (Files[Index].ReadState == OERS_NotStarted)
			{
				// If this is a loc file name, make sure we are getting the right language
				Files[Index].Filename = UpdateLocFileName(Files[Index].Filename);
				// @ZOMBIE_FOXTROT_BEGIN - vspencer 11/26/2014 - The file to download should be localized also
				Files[Index].DLName = UpdateLocFileName(Files[Index].DLName);
				// @ZOMBIE_FOXTROT_END
				if (TitleFileInterface.ReadTitleFile(Files[Index].DLName))
				{
					Files[Index].ReadState = OERS_InProgress;
				}
				else
				{
					Files[Index].ReadState = OERS_Failed;
				}
			}
		}
	}
}

/**
* Notifies us when the download of a file is complete
*
* @param bWasSuccessful true if the download completed ok, false otherwise
* @param FileName the file that was downloaded (or failed to)
*/
function OnDownloadFileComplete(bool bWasSuccessful,string FileName)
{
	local bool bSuccessLoad;
	local int Index;
	local array<byte> FileData;

	// Iterate through files to verify that this is one that we requested
	for (Index = 0; Index < Files.Length; Index++)
	{
		if (Files[Index].DLName == FileName)
		{
			if (bWasSuccessful)
			{
				// Read the contents so that they can be processed
				if (TitleFileInterface.GetTitleFileContents(FileName,FileData) &&
					FileData.Length > 0)
				{
					bSuccessLoad = true;
					Files[Index].ReadState = OERS_Done;
					// Cache it so that the file doesn't need to be downloaded next time
					if (bRequestEmsFileList &&
						TitleFileCacheInterface != None)
					{
						TitleFileCacheInterface.SaveTitleFile(Files[Index].DLName,Files[Index].Filename,FileData);
					}
					// Clear memory copy of file from title file downloader 
					TitleFileInterface.ClearDownloadedFile(FileName);
					// patch loc and class
					ProcessIniLocFile(Files[Index].Filename,Files[Index].bIsUnicode,FileData);
				}
				else
				{
					Files[Index].ReadState = OERS_Failed;
				}
			}
			else
			{
				`Log("Failed to download the file from system interface."
					@"DLName="$Files[Index].DLName
					@"Filename="$Files[Index].Filename);
				Files[Index].ReadState = OERS_Failed;
			}
			break;
		}		
	}
	// Notify that download complete
	TriggerDownloadCompleteDelegates(bSuccessLoad,FileName);
}

/**
* Delegate fired when a file read from the local cache is complete
*
* @param bWasSuccessful whether the file read was successful or not
* @param FileName the name of the file this was for
*/
function OnFileCacheLoadComplete(bool bWasSuccessful,string FileName)
{
	local int Index;
	local array<byte> FileData;
	local bool bRequiresDownload;

	bRequiresDownload = true;
	// Iterate through files to verify that this is one that we requested
	for (Index = 0; Index < Files.Length; Index++)
	{
		if (Files[Index].DLName == FileName)
		{
			if (bWasSuccessful)
			{
				// Compare requested hash vs the one from the cache
				if (TitleFileCacheInterface.GetTitleFileHash(FileName) == Files[Index].HashCode)
				{
					// Read the contents so that they can be processed
					if (TitleFileCacheInterface.GetTitleFileContents(FileName,FileData) &&
						FileData.Length > 0)
					{
						Files[Index].ReadState = OERS_Done;
						bRequiresDownload = false;
						// patch loc and class
						ProcessIniLocFile(Files[Index].Filename,Files[Index].bIsUnicode,FileData);
						// Clear memory copy of file from title file downloader 
						TitleFileCacheInterface.ClearCachedFile(FileName);
					}
				}
				else
				{
					`Log("Hash for file cache entry not valid."
						@"DLName="$Files[Index].DLName
						@"Filename="$Files[Index].Filename);
				}
			}
			break;
		}
	}
	// File could not be loaded from cache or hash was invalid. so, download it
	if (bRequiresDownload)
	{
		// File was invalid so delete it
		TitleFileCacheInterface.DeleteTitleFile(FileName);
		// Start the download
		if (TitleFileInterface != None &&
			TitleFileInterface.ReadTitleFile(FileName))
		{
			Files[Index].ReadState = OERS_InProgress;
		}
		else
		{
			Files[Index].ReadState = OERS_Failed;
		}
	}
	else
	{
		// Notify that load completed
		TriggerDownloadCompleteDelegates(true,FileName);
	}
}

/**
* Delegate fired when a file save to the local cache is complete
*
* @param bWasSuccessful whether the file read was successful or not
* @param FileName the name of the file this was for
*/
function OnFileCacheSaveComplete(bool bWasSuccessful,string FileName)
{
	// clear the memory for the entry that was saved
	TitleFileCacheInterface.ClearCachedFile(FileName);
}

/**
* Triggers list of delegates whenever a file has been loaded
*/
function TriggerDownloadCompleteDelegates(bool bSuccess,string FileName)
{
	local int Index;
	local delegate<OnReadTitleFileComplete> OnReadTitleFileComplete;

	// Call delegates for file completion
	for (Index=0; Index < ReadTitleFileCompleteDelegates.Length; Index++)
	{
		if (ReadTitleFileCompleteDelegates[Index] != None)
		{
			OnReadTitleFileComplete = ReadTitleFileCompleteDelegates[Index];
			OnReadTitleFileComplete(bSuccess,FileName);
		}
	}
	// See if all files are done
	CheckForAllFilesComplete();
}

/**
* Triggers the final delegate if all files have been processed
*/
function CheckForAllFilesComplete()
{
	local int Index;
	local bool bAllFilesComplete;

	bAllFilesComplete = true;
	for (Index = 0; Index < Files.Length; Index++)
	{
		if (Files[Index].ReadState == OERS_NotStarted ||
			Files[Index].ReadState == OERS_InProgress)
		{
			bAllFilesComplete = false;
		}
	}
	if (bAllFilesComplete)
	{
		// @ZOMBIE_FOXTROT_BEGIN - ccooper 1/9/2014 - Changed delegate call to delegate container calls
		for(Index = 0; Index < AllTitleFilesCompletedDelegates.Length; ++Index)
		{
			if(AllTitleFilesCompletedDelegates[Index] != none)
			{
				OnAllTitleFilesCompleted = AllTitleFilesCompletedDelegates[Index];
				OnAllTitleFilesCompleted();
				OnAllTitleFilesCompleted = none;
			}
		}
		// @ZOMBIE_FOXTROT_END
	}
}

/**
* Takes the data, merges with the INI/Loc system, and then reloads the config for the
* affected objects
*
* @param FileName the name of the file being merged
* @param bIsUnicode whether the file should be treated as unicode or not
* @param FileData the file data to merge with the config cache
*/
native function ProcessIniLocFile(string FileName,bool bIsUnicode,const out array<byte> FileData);

/**
* Adds a loc/ini file to download
*
* @param FileName the file to download
*/
function AddFileToDownload(string FileName)
{
	local int FileIndex;

	FileIndex = Files.Find('FileName',FileName);
	// Don't add more than once
	if (FileIndex == INDEX_NONE)
	{
		// Add a new entry which will default to not started
		FileIndex = Files.Length;
		Files.Length = FileIndex + 1;
		Files[FileIndex].FileName = FileName;
		Files[FileIndex].DLName = FileName;
		// Any file that is not INT or INI should be Unicode
		Files[FileIndex].bIsUnicode = InStr(FileName,".ini",,true) == INDEX_NONE && InStr(FileName,".int",,true) == INDEX_NONE;
		//`log("Files["$FileIndex$"].bIsUnicode = "$Files[FileIndex].bIsUnicode,, 'DevConfig');
	}
	else
	{
		Files[FileIndex].ReadState = OERS_NotStarted;
	}
	// Kick off the download
	DownloadFiles();
}

/**
* Adds the specified delegate to the registered downloader. Since the file read can come from
* different objects, this method hides that detail, but still lets callers get notifications
*
* @param ReadTitleFileCompleteDelegate the delegate to set
*/
function AddReadFileDelegate(delegate<OnReadTitleFileComplete> ReadTitleFileCompleteDelegate)
{
	// Add the delegate if not None and not found
	if (ReadTitleFileCompleteDelegate != None &&
		ReadTitleFileCompleteDelegates.Find(ReadTitleFileCompleteDelegate) == INDEX_NONE)
	{
		ReadTitleFileCompleteDelegates.AddItem(ReadTitleFileCompleteDelegate);
	}
}

/**
* Clears the specified delegate from any registered downloaders
*
* @param ReadTitleFileCompleteDelegate the delegate to remove from the downloader
*/
function ClearReadFileDelegate(delegate<OnReadTitleFileComplete> ReadTitleFileCompleteDelegate)
{
	local int RemoveIndex;

	RemoveIndex = ReadTitleFileCompleteDelegates.Find(ReadTitleFileCompleteDelegate);
	if (RemoveIndex != INDEX_NONE)
	{
		ReadTitleFileCompleteDelegates.Remove(RemoveIndex,1);
	}
}

// @ZOMBIE_FOXTROT_BEGIN - ccooper 1/9/2014 - Adding function to add delegates for all files downloads and patching complete
function AddAllTitleFilesCompletedDelegate(delegate<OnAllTitleFilesCompleted> InDelegate)
{
	if (AllTitleFilesCompletedDelegates.Find(InDelegate) == INDEX_NONE)
	{
		AllTitleFilesCompletedDelegates[AllTitleFilesCompletedDelegates.Length] = InDelegate;
	}
}
function ClearAllTitleFilesCompletedDelegate(delegate<OnAllTitleFilesCompleted> InDelegate)
{
	local int RemoveIndex;
	RemoveIndex = AllTitleFilesCompletedDelegates.Find(InDelegate);
	if (RemoveIndex != INDEX_NONE)
	{
		AllTitleFilesCompletedDelegates.Remove(RemoveIndex,1);
	}
}
// @ZOMBIE_FOXTROT_END

/**
* Tells any subclasses to clear their cached file data
*/
function ClearCachedFiles()
{
	local int Index;

	// Iterate through files trying to download them
	for (Index = 0; Index < Files.Length; Index++)
	{
		// Reset their status
		Files[Index].ReadState = OERS_NotStarted;
	}
	if (TitleFileInterface != None)
	{
		// memory copy of downloaded files
		TitleFileInterface.ClearDownloadedFiles();
	}
	if (TitleFileCacheInterface != None)
	{
		// memory copy of cached files
		TitleFileCacheInterface.ClearCachedFiles();
	}
}

/**
* Gets the proper language extension for the loc file
*
* @param FileName the file name being modified
*
* @return the modified file name for this language setting
*/
native function string UpdateLocFileName(string FileName);