SCRMobileSyncingController
--------------------------

SCRMobileSyncingController provides the main sync routines for both Mac and iOS. The following shows an example of how it might be used, along with comments on its usage.

On the Mac, the syncing code should be called in the manually-invoked sync action, and also when the project is opened, just after all of the data has been read and set up (immediately after calling -setContents: on SCRBinderController in -makeWindowControllers).

On iOS, syncing should be called when Sync is invoked in the main project screen.

- (BOOL)mySampleSyncMethod
{
	NSString *scrivProjectPath = ... // Path to the .scriv project.

	// First, check to see if the project contains any mobile data (i.e. a /Mobile folder):
	if ([SCRMobileSyncController projectAtPathContainsMobileData:scrivProjectPath] == NO)
	{
		// There is nothing to do, as the project contains no mobile data. (If the iOS version
		// is opening a project with no mobile data, it opens the project from the desktop data -
		// the .scrivx file etc.)

		return YES;
	}

	// Mac-only: back up the project if necessary.

	// Create an instance of the syncing controller. We must create a new instance for each sync (we do not keep it around).
	SCRMobileSyncingController *syncController = [[SCRMobileSyncingController alloc] init];

	// Tell the syncing controller where the project is located.
	[syncController setProjectPath:scrivPackagePath];

	// Set the delegates. There are two delegates, one for data handling, and the other for showing progress bars.
	// You must implement both.
	// The iOS version needs only to implement the -mobileSyncController:saveMobileProjectInfo... and
	// -mobileSyncController:shouldChoosePriority... data handling delegate methods. It should implement all
	// progress delegate methods, though.
	[syncController setDelegate:...];	// Handles saving data, choosing the device to take priority and more. Must be implemented.
	[syncController setProgressDelegate:...]; // Handles showing progress bars. Should be implemented.

	// SHOW PROGRESS
	// Show a progress panel/overlay along with an indeterminate progress spinner and a message saying, "Syncing...".
	// Then update this as necessary using the progressDelegate methods, and hide it again once this sync method is complete.

	// This method just does a little preliminary cleaning up - it removes any duplicate binder.mob files inside
	// the /Mobile folder, keeping only the most recent.
	[syncController removeDuplicateMobileBinderFiles];

	// Now grab the binder file paths - note that this *must* only be done *after* calling
	// -removeDuplicateMobileBinderFiles above.
	NSString *mobileXMLPath = [syncController mobileBinderFilepath];	// May be nil.
	NSString *desktopXMLPath = [syncController mostCurrentDesktopBinderFilepath]; // May be nil.

	if (desktopXMLPath == nil && mobileXMLPath == nil)	// No data
	{
		// iOS: Throw warning and return - this is not a valid project, we cannot open it.
    		// OS X: Return without doing anything - we have no mobile data to sync with, and
    		// if there's no desktop .scrivx file anyway, that's a big problem that will be dealt with elsewhere.
		// Hide progress bar.
		return NO;
	}

	// Tell the sync controller whether to use 2.x's file system (/Files/Docs with numerical IDs for the documents)
	// or 3.x's (/Files/Data with UUIDs for the documents). This setting affects where the sync controller will look
	// in the main .scriv project for desktop files. This should be called with YES if the project is a 2.x project,
	// or NO if the project is a 3.x project. The best way of determining this is to look at the the "Version" attribute
	// of the "ScrivenerProject" element in the main XML file. If "Version" is less than "2.0" (i.e. "1.0" or "1.5"),
	// usesLegacyFileSystem should be set to YES; if "Version" is "2.0" or higher, usesLegacyFileSystem should be set to NO.
	[syncController setUsesLegacyFileSystem:...]; // Note: Mac version can use [SCRMobileSyncingController shouldUseLegacyFileSystem] helper method (won't work on iOS).

	// Tell the sync controller that we will convert script files, using Markdown formatting for paragraphs. This is a temporary
	// measure until we are able to fully support script formatting on iOS. 
	[syncController setConvertsScriptFiles:YES];

	// Now build the data from the mobile XML path. This is a crucial part and must be implemented independently on each platform,
	// since it requires the binder.mob XML file to be read. What you need to do is read the XML at mobileXMLPath and populate an
	// NSDictionary with data for all the SCRMobileSync... keys listed at the top of the SCRMobileSyncingController header file -
	// please see the comments in the header file for information on the keys. (Note that SCRMobileSyncBinder should be an array
	// containing the top-level items in the binder - those items will have -children arrays containing the rest of the binder
	// objects, of course; that is, it should not be a flat list of all binder documents, but just the top-level objects from which
	// the structure can be read.)
	NSDictionary *mobileDataInfo = ... // May be nil, but only if something has gone wrong.
	[syncController setMobileProjectInfo:mobileDataInfo];

	// This next method looks for the .checksums file in the /Mobile/Data folder (based on the "Checksums" UUID in the binder.mob
	// file), and if it exists, checks the checksums against the files existing in the /Mobile/Data folder to ensure that they match.
	// If they don't match, that indicates that something has gone wrong - most likely that not all files have finished download from
	// or syncing with the Dropbox servers.
	if ([syncController mobileContentMatchesChecksums] == NO)
	{
		// Show a warning telling the user that not all files seem to have downloaded, and that if he or she continues, it's possible
		// that they may lose data or the project may not appear as expected. Offer the options of cancelling or continuing.
		// Abort opening the project if user chooses to cancel.
		if (cancelled)
		{
			// Hide progress bar.
			return NO;
		}
	}

	// This method looks through the binder.mob file and through the /Mobile/Data folder looking for any discrepancies between them,
	// and it cleans up any problems as necessary (inserting any orphaned files into the mobile binder and so on).
	[syncController resolveMobileConflictsIfNecessary];	// Does nothing if mobileProjectInfo is nil or empty.

	// Now read the desktop data from the main .scrivx XML file, so that we can check for any conflicts between the desktop version
	// of the project and the mobile version of the project. (The aim here is to ensure that the mobile version is always the most
	// up to date, so the binder.mob file may need updating if the user has somehow edited the desktop version of the project without
	// syncing.)
	// This dictionary is populated in exactly the same way as the mobileDataInfo dictionary, using the SCRMobileSync... keys, but reading
	// the data from the main .scriv/...scrivx file rather than from the /Mobile/binder.mob file.
	NSDictionary *desktopDataInfo = // Build data from desktop XML path. (This may be nil if the project has never been opened on the desktop.)
	[syncController setDesktopProjectInfo:desktopDataInfo];

	// Now that the sync controller has been fed both the desktop and mobile data (the internal version of which may have been updated to
	// resolve internal mobile con flits already), we now ask it to resolve any problems it finds between the desktop and mobile versions
	// of the project. (This will update the -mobileProjectInfo dictionary as necessary.) The delegate must implement -mobileSyncController:
	// shouldChooseBinderPriorityBetweenDesktopDevice:andMobileDevice:... for this method to work properly. (The delegate method just handles
	// offering the user the choice of which version of the project to prioritise when a conflict is found, returning the necessary result.)
	BOOL canCancel = ... // YES on the desktop if the user has just called sync and isn't opening a project. Always set to NO on iOS.
	SCRMobileSyncConflictResolutionResult result = [syncController resolveDesktopAndMobileConflictsWithCancel:canCancel];

	if (result == SCRMobileSyncConflictResolutionError)
	{
		// Show an error message saying, "An unexpected error occurred during sync. Sync could not be completed." or some such.
		// Hide progress bar.
		return NO;
	}
	else if (result == SCRMobileSyncConflictResolutionCancel)
	{
		// (This result should be impossible on iOS if canCancel was set to NO.)
		// Hide progress bar.
		return NO;
	}
	// We don't need to worry about other results, as the only other possible results are that no conflicts were found, or that they
	// were found and resolved.

	// Save binder.mob file if necessary - does nothing if no changes were made by any of the above methods, and deletes the
	// binder.mob if it is empty or corrupt.
	// NOTE: This shouldn't be necessary on the Mac, which will want to remove the  mobile binder file at the end of the sync
	// anyway, but it is vital on iOS.
	// NOTE: The actual saving is done in the mandatory -mobileSyncController:saveMobileProjectInfo:toPath: delegate method, but you
	// should call this method and do the saving in that delegate method to ensure that everything is cleaned up properly. The delegate
	// method should take the projectInfo parameter and write a new binder.mob XML file to the binderSyncPath parameter, populating the
	// XML solely from the information contained in the projectInfo dictionary (which represents an updated version of the project data,
	// incorporating all conflict resolutions).
	[syncController saveMobileBinderFileIfNecessary];

	// Syncing is now complete on the mobile version. The mobile version should now try to open the project, first looking for the /Mobile/binder.mob
	// file, and if that doesn't exist or couldn't be opened, by falling back on the desktopXMLPath.

	// *** MOBILE VERSION ONLY *****

	if (syncController.checksumsNeedRegenerating)
		// Regenerate checksums!
   
	// NOTE:
	// +checksumsStringForMobileDataInProjectAtPath:existingChecksumInfo:validBinderDocumentUUIDs: can be used by the mobile version to
	// generate an updated .checksums file when necessary. See the SCRMobileSyncingController header file for information on using this
	// method, which is intended as a helper method for the iOS version. This method can also be used by the mobile version whenever it
	// needs to generate the .checksums file. For instance, the mobile version could maintain a list of valid binder UUIDs (UUIDs for
	// documents that exist in the binder). (It may do this already - for speed, for instance, the desktop version maintains a dictionary
	// of SCRBinderDocument objects stored against their UUID as the key. This way, Scrivener can instantly find any binder document object
	// by its UUID.) And it could read in the original .checksums file upon file open, storing it in a dictionary created using
	// +checksumInfoForChecksumsFile:. It could then remove any entries from this dictionary whenever it updates a document (thus invalidating
	// its old checksums). Then, whenever the mobile version needs to regenerate the checksums file, it could call this method passing in
	// the list of valid UUIDs along with the dictionary of checksum info for files it knows still match the checksums in that dictionary,
	// and this method will generated checksums for all other files that match a valid binder UUID. The mobile version can then write that
	// string to disk.
    
    // Push to Dropbox if files have changed?
    // On the mobile version, if the result returned above was SCRMobileSyncConflictResolutionResolvedConflicts, at this point
    // you should push the updated /Mobile folder to Dropbox, since files will have been changed. This particular sync with
    // Dropbox can happen in the background, though.

	// **** END MOBILE ONLY ****

	// Everything else from hereon in pertains *only* to the desktop version, and should *not* be implemented in the iOS version - the iOS
	// version's sync method ends here.


	// **** DESKTOP ONLY ****

	// The following section applies only to the desktop version and should not be done in the mobile version.

	NSArray *deletedDocs, *updatedUUIDs;
	NSError *error;
	if ([syncController updateDesktopDataFromMobileDataWithObsoleteDocuments:&deletedDocs updatedUUIDs:&updatedUUIDs error:&error] == NO)
	{
		// Handle error if there is one. (Otherwise, the project just didn't need updating.)
		if (error)
			// Show error.
	}
	else
	{
		// If we updated the desktop data, we need to save the updated data and reload the project.

		// Delete documents as necessary.
		[mainDoc deleteBinderDocumentsCompletely:deletedDocs forMobileSync:YES];

		if (syncController.convertsScriptFiles)
		{
			NSDictionary *elementStyles = // Get script element styles from main document.
			[syncController copyAndConvertScriptFilesToMobileFolderWithScriptElementStyles:elementStyles];
		}

		// If we are opening the project, we just change the necessary information and carry on with the open.
		// (Nothing in the interface should have been set up yet.) Otherwise, we have to reload the project.
		if (isOpeningProject)
		{
			// NOTE: Be sure to check for nil values here!
			[[[mainDoc mainWindowController] binderController] setContents:desktopProjectInfo[SCRMobileSyncBinder]];

			[mainDoc setLabelList:syncController.desktopProjectInfo[SCRMobileSyncLabelList]];
			[mainDoc setLabelTitle:syncController.desktopProjectInfo[SCRMobileSyncLabelTitle]];
			[mainDoc setDefaultLabelTag:[desktopProjectInfo[SCRMobileSyncLabelDefaultTag] integerValue]];

			[mainDoc setStatusList:syncController.desktopProjectInfo[SCRMobileSyncStatusList]];
			[mainDoc setStatusTitle:syncController.desktopProjectInfo[SCRMobileSyncStatusTitle]];
			[mainDoc setDefaultStatusTag:[syncController.desktopProjectInfo[SCRMobileStatusDefaultTag integerValue]];

			[mainDoc setTemplateBinderDocumentsFolderUUID:desktopProjectInfo[SCRMobileSyncTemplateFolderUUID]];
        
        		// Update the search indexes and save the Quick Look preview...

			[mainDoc setHasChanges:YES];	// Ensure the changes are saved.
        
        		if (syncController.didAddFilesToConflictsFolder)
            			// Show warning telling the user that we've created a "Sync Conflicts" folder.
		}
		else
		{
			// Save the updated data.
			NSData *data = [mainDoc scrivenerProjectXMLDataFromMobileInfo:syncController.desktopProjectInfo];
			if (data == nil)
			{
			// Error!
			}
			else if ([data writeToFile:desktopXMLPath atomically:YES] == NO)
			{
			// Error!
			}
			else
			{
            			if (syncController.didAddFilesToConflictsFolder)
                		// Show warning telling the user that we've created a "Sync Conflicts" folder.

				// Reload the project!
				[[SCRDocumentController sharedDocumentController] closeAndReloadProjectForMobileSync:mainDoc updatedUUIDs:updatedUUIDs];
            
            			// Reload the search indexes for all documents in updatedUUIDs, and resave Quick Look previews.
			}
		}
	}

	// **** END DESKTOP ONLY ****
}