/*	VERSION:	2.5

NOTE:		For best performance, turn on "Enable External Interface" in Zinc when you compile an EXE.
2.5		Files are saved using "unicode" instead of ASCII  (Zinc uses clean UTF-8  (no BOM byte-order-mark)  (Flash can read this seamlessly)
2.3		Aborting does not hang the main promise-chain.  Instead, all the main steps check for abort and "return" prematurely, doing absolutely nothing, including the done() and error() steps.
2.2b	TEST:  Added a delay between saving the last chunk and the confirm-check
2.2a	TEST:  If save-confirmation fails, a very verbose explanation will be dispayed, and save-confirmation will be attempted again if possible.
2.1		More descriptive errors mention the fileName that failed.
2.0		Added a "type" property to failure objects, to describe the type of failure.  External code may want to ignore errors caused by aborts they deliberately caused.
1.9		Bugfix:  abort() now returns a promise.
1.8		Added "current" and "total" to onProgress() callback.
1.7		abort() returns a promise.  Both abort() and errors return an object containing un-damaged data instead of a string-message, so re-saving is possible.  The object contains:  fileIsDamaged,  abortedPath,  abortedData
1.6		Added:  abort()  &  intelligent file-rescue to confirmSave()
1.5		Changed:  zincSave = function(  =>  function zincSave(
1.4		Re-organized the save sequence into a single promise chain, with added pauses to improve asychronous performance
1.3		Added an initial delay between cerating the file & writing its first chunk, to improve performance


USAGE:
	#include "functions/VOW.as"
	#include "functions/VOW/zincSave.as"
	zincSave( "test.txt", test_data ).then( saveDone, saveFail );
	
	function saveDone(){
		trace("saveDone()");
	}// saveDone()
	
	function saveFail( reason ){
		trace(reason);
	}// saveFail()
	
	
	// save the file without backup or confirmation
	zincSave( "test.txt", test_data, false, false, false, 34 ).then( saveDone, saveFail );
	
	
DESCRIPTION:
	This asyncronously saves a file using Zinc,
	optionally backs up the saved file,
	and optionally confirms the saved file.
	
	By default, it'll backup any existing file before saving, confirm the saved file, and then delete the backup if confirmation succeeds
	
	You can recieve save-progress updates by defining an onProgress() function within its returned promise object, like so:
		save_promise = zincSave( "test.txt", test_data );
		save_promise.onProgress = function(perc, obj){
			progress_txt.text = perc+"%";
			progress_txt.text = obj.current + "/" + obj.total;
		}// onProgress()
		save_promise.then( saveDone, saveFail );
		
		
ERROR / ABORT DATA:
	abort() returns a promise.  Both abort() and errors send this object:
	{
		message: "Oh noez!",
		type: "abort" / "fail",
		fileIsDamaged: !fileWasRestored,				// Whether or not the file on the harddrive is damaged  (In most cases, the file is automatically renamed to:  fileName.ext.damaged)
		abortedPath: filePath,									// The filePath it was originally attempting to save
		abortedData: data_str										// The file data it was originally attempting to save  (This data will be intact even if the file was damaged, so a re-save is possible)
	}
*/
function zincSave( filePath, data_str, backupFile, useConfirmCheck, keepBackup, saveDelay ){
	var	saveChunkSize = 8192,
			vow = VOW.make(),
			saveIndex = 0,
			alreadyExists = false,
			chunk_vow,
			chunk_interval = null,
			isAborted = false,
			newFileCreated = false;
	var createBackup,
			createFile,
			saveNextChunk,
			confirmSave,
			removeBackup,
			done,
			error;
	if(backupFile==undefined)	backupFile = true;
	if(useConfirmCheck==undefined)		useConfirmCheck = true;
	if(keepBackup==undefined)	keepBackup = false;
	if(saveDelay == undefined)		saveDelay = 34;
	
	
	
	function once( func ){
		var done = false;
		
		return function () {
			return done ? void 0 : ((done = true), func.apply(this, arguments));
		}
	}// once()
	
	
	
	createBackup = function(){
		if(isAborted === true)		return;
		
		if(backupFile){
			alreadyExists = mdm.FileSystem.fileExists( filePath );
			if(alreadyExists)
				mdm.FileSystem.copyFile(filePath, filePath+'.bak');
		}// if:  backupFile
	}// createBackup()
	
	
	
	createFile = function(){
		if(isAborted === true)		return;
		
		mdm.FileSystem.saveFileUnicode( filePath, "" );
		newFileCreated = true;
	}// createFile()
	
	
	
	// gradually append each block of data until saving is complete
	chunk_vow = VOW.make();
	saveNextChunk = function(){
		if(isAborted === true)		return;
		
		var endAt,
				chunk;
		
		// announce file-save progress, via an externally-defined function attached to the promise
		// promise.onProgress()
		if(vow.promise.onProgress){
			var percent = (saveIndex /data_str.length) *100;
			percent = Math.floor(percent);
			if(percent > 100)		percent = 100;
			
			var thisChunk = saveIndex / saveChunkSize;
			var totalChunks = Math.ceil( data_str.length / saveChunkSize );
			
			vow.promise.onProgress( percent, {current:thisChunk, total:totalChunks} );
		}// if:  something is listening for progress
		
		
		if( saveIndex < data_str.length ){
			endAt = saveIndex +saveChunkSize;
			if(endAt > data_str.length)		endAt = data_str.length;
			chunk = data_str.substring( saveIndex, endAt );
			// save this chunk to the file
			mdm.FileSystem.appendFileUnicode(filePath, chunk);
			// prepare to save the next chunk
			saveIndex += saveChunkSize;
			// wait, then save the next chunk
			chunk_interval = setTimeout( function(){ saveNextChunk(); },saveDelay);
		}// if:  NOT done saving
		else
		{// if:  done saving
			chunk_interval = null;
			chunk_vow.keep();
		}// if:  done saving
		
		return chunk_vow.promise;
	}// saveNextChunk()
	//setTimeout( function(){ saveNextChunk(); },saveDelay);
	
	
	
	confirmSave = function(){
		if(isAborted === true)		return;
		
		if(useConfirmCheck == true)
		{
			var fileLoader,
					confirm_vow = VOW.make(),
					loadFile_vow = VOW.make();
			var checkFile;
			
			if( saveIndex < data_str.length ){
				var msg = 'confirmSave() was called pre-maturely';
				mdm.Dialogs.prompt(msg);
			}
			
			// load the file
			XML.prototype.ignoreWhite = true;
			fileLoader = new XML();
			fileLoader.onData = function( file_str ){
				if(file_str !== undefined){
					loadFile_vow.keep(file_str);
				}else{
					/*
					var errorData = {
						message: getFileName(filePath) + " cannot be read!",
						type: "fail",
						fileIsDamaged: true,
						abortedPath: filePath,
						abortedData: data_str
					}
					loadFile_vow.doBreak( errorData );
					*/
					
					var msg = "";
					msg += "save-confirm failed. "+getFileName(filePath)+" couldn't be read.\n";
					msg += "File path:  "+filePath+"\n";
					var savedFileExists = mdm.FileSystem.fileExists( filePath );
					if(savedFileExists){
						msg += "But the file does exist on the disk, so...\n";
						msg += "... Let's just wait a moment and then try loading it again.";
					}else{
						msg += "The file is completely missing!\n";
						msg += "So I guess the save really did fail.";
					}
					mdm.Dialogs.prompt(msg);
					
					if(savedFileExists)
					{// if:  file exists
						setTimeout(function(){
							fileLoader.load( filePath );
						}, 100);
					}// if:  file exists
					else
					{// if:  file is missing
						var errorData = {
							message: getFileName(filePath) + " cannot be read!",
							type: "fail",
							fileIsDamaged: true,
							abortedPath: filePath,
							abortedData: data_str
						}
						loadFile_vow.doBreak( errorData );
					}// if:  file is missing
					
				}// if:  file cannot be read
			}// onData()
			
			// compare its contents with the desired data
			checkFile = function( file_str ){
				// announce whether it matches
				if(file_str.length >= data_str.length-1){
					confirm_vow.keep();
					//vow.keep();
				}else{
					
					// try to restore the file using the backup file
					var fileWasRestored = restoreFromBackup();
					
					if( fileWasRestored == false )
					{// if:  the broken file was NOT restored  (no backup)
						var damagedPath = renameDamagedFile();
						var error_str = "Save failed!  "+getFileName(filePath)+" has been damaged!";
						if(damagedPath){
							var damagedName = getFileName(damagedPath);
							error_str += "  It has been renamed to:  "+damagedName;
						}
						var errorData = {
							message: error_str,
							type: "fail",
							fileIsDamaged: true,
							abortedPath: filePath,
							abortedData: data_str
						}
						confirm_vow.doBreak( errorData );
					}// if:  the broken file was NOT restored  (no backup)
					else
					{// if:  the broken file was restored
						removeBackup();
						var errorData = {
							message: getFileName(filePath)+" could NOT be saved!  It remains unchanged.",
							type: "fail",
							fileIsDamaged: false,
							abortedPath: filePath,
							abortedData: data_str
						}
						confirm_vow.doBreak( errorData );
					}// if:  the broken file was restored
				}
			}// checkFile()
			
			loadFile_vow.promise.then( checkFile, vow.doBreak );
			fileLoader.load( filePath );
			return confirm_vow.promise;
		}// if: useConfirmCheck
	}// confirmSave()
	
	
	
	removeBackup = function( ignoreAbort ){
		if(!ignoreAbort && isAborted === true)		return;
		
		if(keepBackup == false){
			if( mdm.FileSystem.fileExists( filePath+'.bak') ){
				mdm.FileSystem.deleteFile( filePath+'.bak' );
			}// if:  backup file exists
		}//if:  not keeping the backup file
	}// removeBackup()
	
	
	
	// the entire save process is complete
	done = function(){
		if(isAborted === true)		return;
		
		vow.keep();
	}// done()
	done = once( done );
	
	
	
	// something went wrong
	error = function( reason ){
		if(isAborted === true)		return;
		
		vow.doBreak( reason );
	}// error()
	error = once( error );
	
	
	function restoreFromBackup(){
		var success = false;
		
		var backupFilePath = filePath+'.bak';
		var backupExists = mdm.FileSystem.fileExists( backupFilePath );
		if(backupExists)
		{// if:  a backup file exists
			// delete the incomplete file
			if( mdm.FileSystem.fileExists( filePath) ){
				mdm.FileSystem.deleteFile( filePath );
			}// if:  incomplete file exists
			// restore the backup file  (overwrite the aborted file with the backup file)
			mdm.FileSystem.copyFile( backupFilePath, filePath );
			success = true;
		}// if:  a backup file exists
		
		return success;
	}// restoreFromBackup()
	
	
	
	function renameDamagedFile(){
		if( mdm.FileSystem.fileExists( filePath) ){
			// rename the broken file to indicate that it is broken
			var brokenFilePath = filePath + ".damaged";
			mdm.FileSystem.copyFile( filePath, brokenFilePath );
			mdm.FileSystem.deleteFile( filePath );
			return brokenFilePath;
		}// if:  incomplete file exists
	}// renameDamagedFile()
	
	
	
	// allow saving to be aborted pre-maturely
	vow.promise.abort = once( function(){
		mdm.Exception.DebugWindow.trace("zincSave abort()");
		var abort_vow = VOW.make();
		var damagedName = null;
		// tell everything to halt
		isAborted = true;
		// cancel saving the next chunk
		mdm.Exception.DebugWindow.trace("  chunk_interval: " + chunk_interval);
		if(chunk_interval !== null){
			clearTimeout( chunk_interval );
			// wait for the file to be un-locked
			setTimeout( function(){
				resumeAbort();
			}, saveDelay||34 );
		}// if:  file chunks are being saved
		else
		{// if:  NO file chunks are being saved
			resumeAbort();
		}// if:  NO file chunks are being saved
		
		function resumeAbort()
		{
			mdm.Exception.DebugWindow.trace("resumeAbort()");
			var fileWasRestored = true;
			
			if(newFileCreated == false)
			{// if:  file writing didn't start yet
				mdm.Exception.DebugWindow.trace("  file writing didn't start yet.  Deleting backup file.");
				removeBackup();
				
				// announce that the save was aborted
				var error_str = "The save process was aborted.";
				error_str += "  Nothing has been saved.";
			}// if:  file writing didn't start yet
			
			else
			
			{// if:  file writing was interrupted
				mdm.Exception.DebugWindow.trace("  file writing was interrupted");
				// try to restore the file using the backup file
				fileWasRestored = restoreFromBackup();
				mdm.Exception.DebugWindow.trace("  fileWasRestored: " + fileWasRestored);
				
				if( fileWasRestored == false )
				{// if:  The damaged file could NOT restored  (no backup)
					damagedPath = renameDamagedFile();
				}// if:  The damaged file could NOT restored  (no backup)
				else
				{// if:  the broken file was restored
					removeBackup( ignoreAbort );
				}// if:  the broken file was restored
				
				// announce that the save was aborted
				var error_str = "The save process was aborted.";
				if(fileWasRestored == false)
				{// if:  file was damaged
					error_str += "  "+getFileName(filePath)+" has been damaged!";
					if(damagedPath){
						var damagedName = getFileName(damagedPath);
						error_str += "  It has been renamed to:  "+damagedName;
					}
				}// if:  file was damaged
				else
				{// if:  file was rescued
					error_str += "  "+getFileName(filePath)+" remains unchanged.";
				}// if:  file was rescued
			}// if:  file writing was interrupted
			
			var errorData = {
				message: error_str,
				type: "abort",
				fileIsDamaged: !fileWasRestored,
				abortedData: data_str
			}
			vow.doBreak( errorData );
			abort_vow.keep( errorData );
		}// resumeAbort()
		return abort_vow.promise;
	});// abort()  (once)
	
	
	
	function getFileName(filePath){
		var startAt = filePath.lastIndexOf("\\");
		return filePath.substr( startAt+1 );
	}// getFileName()
	
	
	
	// start the saving process
	VOW.firstWait(saveDelay)
	.then( createBackup )
//	.then( VOW.wait(saveDelay) )
	.then( createFile )
//	.then( VOW.wait(saveDelay) )
	.then( saveNextChunk )
//	.then( VOW.wait(saveDelay) )
	.then( confirmSave )
	.then( removeBackup )
//	.then( VOW.wait(saveDelay) )
	.then( done, error );
	
	
	
	return vow.promise;
}// zincSave()