/**
 * Copyright (c) 2006-2018, JGraph Ltd
 * Copyright (c) 2006-2018, Gaudenz Alder
 * 
 * Realtime collaboration for any file.
 */
DrawioFileSync = function(file)
{
	mxEventSource.call(this);

	this.lastActivity = new Date();
	this.clientId = Editor.guid();
	this.ui = file.ui;
	this.file = file;

    // Listens to online state changes
	this.onlineListener = mxUtils.bind(this, function()
	{
		this.updateOnlineState();

		if (this.isConnected())
		{
			this.fileChangedNotify();
		}
	});
    
	mxEvent.addListener(window, 'online', this.onlineListener);
	
    // Listens to visible state changes
	this.visibleListener = mxUtils.bind(this, function()
	{
		if (document.visibilityState == 'hidden')
		{
			if (this.isConnected())
			{
				this.stop();
			}
		}
		else
		{
			this.start();
		}
	});
    
	mxEvent.addListener(document, 'visibilitychange', this.visibleListener);
	
    // Listens to visible state changes
	this.activityListener = mxUtils.bind(this, function(evt)
	{
		this.lastActivity = new Date();
		this.start();
	});

	mxEvent.addListener(document, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', this.activityListener);
	mxEvent.addListener(document, 'keypress', this.activityListener);
	mxEvent.addListener(window, 'focus', this.activityListener);
	
	if (!mxClient.IS_POINTER && mxClient.IS_TOUCH)
	{
		mxEvent.addListener(document, 'touchstart', this.activityListener);
		mxEvent.addListener(document, 'touchmove', this.activityListener);	
	}

	// Listens to errors in the pusher API
	this.pusherErrorListener = mxUtils.bind(this, function(err)
	{
		if (err.error != null && err.error.data != null &&
			err.error.data.code === 4004)
		{
			EditorUi.logError('Error: Pusher Limit', null, this.file.getId());
		}
	});

    // Listens to connection state changes
	this.connectionListener = mxUtils.bind(this, function()
	{
		this.updateOnlineState();
		this.updateStatus();
		
		if (this.isConnected())
		{
			if (!this.announced)
			{
				var user = this.file.getCurrentUser();
				var join = {a: 'join'};
				
				if (user != null)
				{
					join.name = user.displayName;
					join.uid = user.id;
				}

				mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() +
					'&msg=' + encodeURIComponent(this.objectToString(
					this.createMessage(join))));
				this.file.stats.msgSent++;
				this.announced = true;
			}

			// Catchup on any lost edits
			this.fileChangedNotify();
		}
	});
	
	// Listens to remove messages
	this.changeListener = mxUtils.bind(this, function(data)
	{
		this.file.stats.msgReceived++;
		this.lastActivity = new Date();

		if (this.enabled && !this.file.inConflictState &&
			!this.file.redirectDialogShowing)
		{
			try
			{
				var msg = this.stringToObject(data);
				
				if (msg != null)
				{
					EditorUi.debug('Sync.message', [this], msg, data.length, 'bytes');

					// Handles protocol mismatch
					if (msg.v > DrawioFileSync.PROTOCOL)
					{
						this.file.redirectToNewApp(mxUtils.bind(this, function()
						{
							// Callback adds cancel option
						}));
					}
					else if (msg.v === DrawioFileSync.PROTOCOL && msg.d != null)
					{
						this.handleMessageData(msg.d);
					}
				}
			}
			catch (e)
			{
				if (window.console != null && urlParams['test'] == '1')
				{
					console.log(e);
				}
			}
		}
	});
};

/**
 * Protocol version to be added to all communcations and diffs to check
 * if a client is out of date and force a refresh. Note that this must
 * be incremented if new messages are added or the format is changed.
 * This must be numeric to compare older vs newer protocol versions.
 */
DrawioFileSync.PROTOCOL = 6;

//Extends mxEventSource
mxUtils.extend(DrawioFileSync, mxEventSource);

/**
 * Maximum size in bytes for cache values.
 */
DrawioFileSync.prototype.maxCacheEntrySize = 1000000;

/**
 * Specifies if notifications should be sent and received for changes.
 */
DrawioFileSync.prototype.enabled = true;

/**
 * True if a change event is fired for a remote change.
 */
DrawioFileSync.prototype.updateStatusInterval = 10000;

/**
 * Holds the channel ID for sending and receiving change notifications.
 */
DrawioFileSync.prototype.channelId = null;

/**
 * Holds the channel ID for sending and receiving change notifications.
 */
DrawioFileSync.prototype.channel = null;

/**
 * Specifies if descriptor change events should be ignored.
 */
DrawioFileSync.prototype.catchupRetryCount = 0;

/**
 * Specifies if descriptor change events should be ignored.
 */
DrawioFileSync.prototype.maxCatchupRetries = 15;

/**
 * Specifies if descriptor change events should be ignored.
 */
DrawioFileSync.prototype.maxCacheReadyRetries = 2;

/**
 * Specifies if descriptor change events should be ignored.
 */
DrawioFileSync.prototype.cacheReadyDelay = 500;

/**
 * Inactivity timeout is 30 minutes.
 */
DrawioFileSync.prototype.inactivityTimeoutSeconds = 1800;

/**
 * Specifies if notifications should be sent and received for changes.
 */
DrawioFileSync.prototype.lastActivity = null;

/**
 * Adds all listeners.
 */
DrawioFileSync.prototype.start = function()
{
	if (this.channelId == null)
	{
		this.channelId = this.file.getChannelId();
	}
	
	if (this.key == null)
	{
		this.key = this.file.getChannelKey();
	}
	
	if (this.pusher == null && this.channelId != null &&
		document.visibilityState != 'hidden') 
	{
		this.pusher = this.ui.getPusher();
		
		if (this.pusher != null)
		{
			try
			{
				// Error listener must be installed before trying to create channel
				if (this.pusher.connection != null)
				{
					this.pusher.connection.bind('error', this.pusherErrorListener);
				}
			}
			catch (e)
			{
				// ignore
			}
			
			try
			{
				this.pusher.connect();
				this.channel = this.pusher.subscribe(this.channelId);
				EditorUi.debug('Sync.start', [this]);
			}
			catch (e)
			{
				// ignore
			}

			this.installListeners();
		}

		window.setTimeout(mxUtils.bind(this, function()
		{
			this.lastModified = this.file.getLastModifiedDate();
			this.lastActivity = new Date();
			this.resetUpdateStatusThread();
			this.updateOnlineState();
			this.updateStatus();
		}, 0));
	}
};

/**
 * Draw function for the collaborator list.
 */
DrawioFileSync.prototype.isConnected = function()
{
	if (this.pusher != null && this.pusher.connection != null)
	{
		return this.pusher.connection.state == 'connected';
	}
	else
	{
		return false;
	}
};

/**
 * Draw function for the collaborator list.
 */
DrawioFileSync.prototype.updateOnlineState = function()
{
	var addClickHandler = mxUtils.bind(this, function(elt)
	{
		mxEvent.addListener(elt, 'click', mxUtils.bind(this, function(evt)
		{
			this.enabled = !this.enabled;
			this.ui.updateButtonContainer();
			this.resetUpdateStatusThread();
			this.updateOnlineState();
			this.updateStatus();
			
			if (!this.file.inConflictState && this.enabled)
			{
				this.fileChangedNotify();
			}
		}));
	});

	if (uiTheme == 'min' && this.ui.buttonContainer != null)
	{
		if (this.collaboratorsElement == null)
		{
			var elt = document.createElement('a');
    		elt.className = 'geToolbarButton';
			elt.style.cssText = 'display:inline-block;position:relative;box-sizing:border-box;margin-right:4px;cursor:pointer;float:left;';
    		elt.style.backgroundPosition = 'center center';
        	elt.style.backgroundRepeat = 'no-repeat';
        	elt.style.backgroundSize = '24px 24px';
        	elt.style.height = '24px';
        	elt.style.width = '24px';
        	
        	addClickHandler(elt);
        	this.ui.buttonContainer.appendChild(elt);
        	this.collaboratorsElement = elt;
		}
	}
	else if (this.ui.toolbarContainer != null)
	{
		if (this.collaboratorsElement == null)
		{
			var elt = document.createElement('a');
			elt.className = 'geButton';
			elt.style.position = 'absolute';
			elt.style.display = 'inline-block';
			elt.style.verticalAlign = 'bottom';
			elt.style.color = '#666';
			elt.style.top = '5px';
			elt.style.right = (uiTheme == 'atlas') ? '42px' : '60px';
			elt.style.padding = '2px';
			elt.style.fontSize = '8pt';
			elt.style.verticalAlign = 'middle';
			elt.style.textDecoration = 'none';
	    	elt.style.backgroundPosition = 'center center';
	    	elt.style.backgroundRepeat = 'no-repeat';
	    	elt.style.backgroundSize = '16px 16px';
			elt.style.width = '16px';
			elt.style.height = '16px';
	    	mxUtils.setOpacity(elt, 60);
	    	
			if (uiTheme == 'dark')
			{
				elt.style.filter = 'invert(100%)';
			}
			
			// Prevents focus
		    mxEvent.addListener(elt, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown',
	        	mxUtils.bind(this, function(evt)
	    	{
				evt.preventDefault();
			}));
			
			addClickHandler(elt);
			this.ui.toolbarContainer.appendChild(elt);
			this.collaboratorsElement = elt;
		}
	}
	
	if (this.collaboratorsElement != null)
	{
		var status = '';
		
		if (!this.enabled)
		{
			status = mxResources.get('disconnected');
		}
		else if (this.file.invalidChecksum)
		{
			status = mxResources.get('error') + ': ' + mxResources.get('checksum');
		}
		else if (this.ui.isOffline() || !this.isConnected())
		{
			status = mxResources.get('offline');
		}
		else
		{
			status = mxResources.get('online');
		}
		
		this.collaboratorsElement.setAttribute('title', status);
		this.collaboratorsElement.style.backgroundImage = 'url(' + ((!this.enabled) ? Editor.syncDisabledImage :
			((!this.ui.isOffline() && this.isConnected() && !this.file.invalidChecksum) ?
			Editor.syncImage : Editor.syncProblemImage)) + ')';
	}
};


/**
 * Updates the status bar with the latest change.
 */
DrawioFileSync.prototype.updateStatus = function()
{
	if (this.isConnected() && this.lastActivity != null &&
		(new Date().getTime() - this.lastActivity.getTime()) / 1000 >
		this.inactivityTimeoutSeconds)
	{
		this.stop();
	}
	
	if (!this.file.isModified() && !this.file.inConflictState &&
		this.file.autosaveThread == null && !this.file.savingFile &&
		!this.file.redirectDialogShowing)
	{
		if (this.enabled && this.ui.statusContainer != null)
		{
			// LATER: Write out modified date for more than 2 weeks ago
			var str = this.ui.timeSince(new Date(this.lastModified));
			
			if (str == null)
			{
				str = mxResources.get('lessThanAMinute');
			}
			
			var history = this.file.isRevisionHistorySupported();

			// Consumed and displays last message
			var msg = this.lastMessage;
			this.lastMessage = null;
			
			if (msg != null && msg.length > 40)
			{
				msg = msg.substring(0, 40) + '...';
			}

			var label = mxResources.get('lastChange', [str]);
			
			this.ui.editor.setStatus('<div title="'+ mxUtils.htmlEntities(label) +
				'" style="display:inline-block;">' + mxUtils.htmlEntities(label)  + '</div>' +
				((msg != null) ? ' <span style="opacity:0;" title="' + mxUtils.htmlEntities(msg) +
				'">(' + mxUtils.htmlEntities(msg) + ')</span>' : '') +
				(this.file.isEditable() ? '' : '<div class="geStatusAlert" style="margin-left:8px;display:inline-block;">' +
					mxUtils.htmlEntities(mxResources.get('readOnly')) + '</div>') +
				(this.isConnected() ? '' : '<div class="geStatusAlert geBlink" style="margin-left:8px;display:inline-block;">' +
					mxUtils.htmlEntities(mxResources.get('disconnected')) + '</div>'));
			var links = this.ui.statusContainer.getElementsByTagName('div');
			
			if (links.length > 0 && history)
			{
				links[0].style.cursor = 'pointer';
				links[0].style.textDecoration = 'underline';
				
				mxEvent.addListener(links[0], 'click', mxUtils.bind(this, function()
				{
					this.ui.actions.get('revisionHistory').funct();
				}));
			}
			
			// Fades in/out last message
			var spans = this.ui.statusContainer.getElementsByTagName('span');
			
			if (spans.length > 0)
			{
				var temp = spans[0];
				mxUtils.setPrefixedStyle(temp.style, 'transition', 'all 0.2s ease');
				
				window.setTimeout(mxUtils.bind(this, function()
				{
					mxUtils.setOpacity(temp, 100);
					mxUtils.setPrefixedStyle(temp.style, 'transition', 'all 1s ease');
					
					window.setTimeout(mxUtils.bind(this, function()
					{
						mxUtils.setOpacity(temp, 0);
					}), this.updateStatusInterval / 2);
				}), 0);
			}
			
			this.resetUpdateStatusThread();
		}
		else
		{
			this.file.addAllSavedStatus();
		}
	}
};

/**
 * Resets the thread to update the status.
 */
DrawioFileSync.prototype.resetUpdateStatusThread = function()
{
	if (this.updateStatusThread != null)
	{
		window.clearInterval(this.updateStatusThread);
	}
	
	if (this.channel != null)
	{
		this.updateStatusThread = window.setInterval(mxUtils.bind(this, function()
		{
			this.updateStatus();
		}), this.updateStatusInterval);
	}
};

/**
 * Installs all required listeners for syncing the current file.
 */
DrawioFileSync.prototype.installListeners = function()
{
	if (this.pusher != null && this.pusher.connection != null)
	{
		this.pusher.connection.bind('state_change', this.connectionListener);
	}
    
	if (this.channel != null)
    {
    	this.channel.bind('changed', this.changeListener);
    }
};

/**
 * Adds the listener for automatically saving the diagram for local changes.
 */
DrawioFileSync.prototype.handleMessageData = function(data)
{
	if (data.a == 'desc')
	{
		if (!this.file.savingFile)
		{
			this.reloadDescriptor();
		}
	}
	else if (data.a == 'join' || data.a == 'leave')
	{
		if (data.a == 'join')
		{
			this.file.stats.joined++;
		}
		
		if (data.name != null)
		{
			this.lastMessage = mxResources.get((data.a == 'join') ?
				'userJoined' : 'userLeft', [data.name]);
			this.resetUpdateStatusThread();
			this.updateStatus();
		}
	}
	else if (data.m != null)
	{
		var mod = new Date(data.m);
		
		// Ignores obsolete messages
		if (this.lastMessageModified == null || this.lastMessageModified < mod)
		{
			this.lastMessageModified = mod;
			this.fileChangedNotify();
		}
	}
};

/**
 * Adds the listener for automatically saving the diagram for local changes.
 */
DrawioFileSync.prototype.isValidState = function()
{
	return this.ui.getCurrentFile() == this.file &&
		this.file.sync == this && !this.file.invalidChecksum &&
		!this.file.redirectDialogShowing;
};

/**
 * Adds the listener for automatically saving the diagram for local changes.
 */
DrawioFileSync.prototype.fileChangedNotify = function()
{
	if (this.isValidState())
	{
		if (this.file.savingFile)
		{
			this.remoteFileChanged = true;
		}
		else
		{
			// It's possible that a request never returns so override
			// existing requests and abort them when they are active
			var thread = this.fileChanged(mxUtils.bind(this, function(err)
			{
				this.updateStatus();
			}),
				mxUtils.bind(this, function(err)
			{
				this.file.handleFileError(err);
			}), mxUtils.bind(this, function()
			{
				return !this.file.savingFile && this.notifyThread != thread;
			}));
		}
	}
};

/**
 * Adds the listener for automatically saving the diagram for local changes.
 */
DrawioFileSync.prototype.fileChanged = function(success, error, abort)
{
	var thread = window.setTimeout(mxUtils.bind(this, function()
	{
		if (abort == null || !abort())
		{
			if (!this.isValidState())
			{
				if (error != null)
				{
					error();
				}
			}
			else
			{
				this.file.loadPatchDescriptor(mxUtils.bind(this, function(desc)
				{
					if (abort == null || !abort())
					{
						if (!this.isValidState())
						{
							if (error != null)
							{
								error();
							}
						}
						else
						{
							this.catchup(this.file.getDescriptorEtag(desc),
								this.file.getDescriptorSecret(desc),
								success, error, abort);
						}
					}
				}), error);
			}
		}
	}), 0);
	
	this.notifyThread = thread;
	
	return thread;
};

/**
 * Adds the listener for automatically saving the diagram for local changes.
 */
DrawioFileSync.prototype.reloadDescriptor = function()
{
	this.file.loadDescriptor(mxUtils.bind(this, function(desc)
	{
		if (desc != null)
		{
			// Forces data to be updated
			this.file.setDescriptorEtag(desc, this.file.getCurrentEtag());
			this.updateDescriptor(desc);
			this.fileChangedNotify();
		}
		else
		{
			this.file.inConflictState = true;
			this.file.handleFileError();
		}
	}), mxUtils.bind(this, function(err)
	{
		this.file.inConflictState = true;
		this.file.handleFileError(err);
	}));
};

/**
 * Adds the listener for automatically saving the diagram for local changes.
 */
DrawioFileSync.prototype.updateDescriptor = function(desc)
{
	this.file.setDescriptor(desc);
	this.file.descriptorChanged();
	this.start();
};

/**
 * Adds the listener for automatically saving the diagram for local changes.
 */
DrawioFileSync.prototype.catchup = function(etag, secret, success, error, abort)
{
	if (abort == null || !abort())
	{
		var current = this.file.getCurrentEtag();
	
		if (current == etag)
		{
			if (success != null)
			{
				success();
			}
		}
		else if (!this.isValidState())
		{
			if (error != null)
			{
				error();
			}
		}
		else
		{
			// Cache entry may not have been uploaded to cache before new
			// etag is visible to client so retry once after cache miss
			var cacheReadyRetryCount = 0;
			var failed = false;
			
			var doCatchup = mxUtils.bind(this, function()
			{
				if (abort == null || !abort())
				{
					// Ignores patch if shadow has changed
					if (current != this.file.getCurrentEtag())
					{
						if (success != null)
						{
							success();
						}
					}
					else if (!this.isValidState())
					{
						if (error != null)
						{
							error();
						}
					}
					else
					{
						var acceptResponse = true;
						
						var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
						{
							acceptResponse = false;
							this.reload(success, error, abort);
						}), this.ui.timeout);
						
						mxUtils.get(EditorUi.cacheUrl + '?id=' + encodeURIComponent(this.channelId) +
							'&from=' + encodeURIComponent(current) + '&to=' + encodeURIComponent(etag) +
							((secret != null) ? '&secret=' + encodeURIComponent(secret) : ''),
							mxUtils.bind(this, function(req)
						{
							this.file.stats.bytesReceived += req.getText().length;	
							window.clearTimeout(timeoutThread);
							
							if (acceptResponse && (abort == null || !abort()))
							{
								// Ignores patch if shadow has changed
								if (current != this.file.getCurrentEtag())
								{
									if (success != null)
									{
										success();
									}
								}
								else if (!this.isValidState())
								{
									if (error != null)
									{
										error();
									}
								}
								else
								{
									var checksum = null;
									var temp = [];
							
									if (req.getStatus() >= 200 && req.getStatus() <= 299 &&
										req.getText().length > 0)
									{
										try
										{
											var result = JSON.parse(req.getText());
											
											if (result != null && result.length > 0)
											{
												for (var i = 0; i < result.length; i++)
												{
													var value = this.stringToObject(result[i]);
													
													if (value.v > DrawioFileSync.PROTOCOL)
													{
														failed = true;
														temp = [];
														break;
													}
													else if (value.v === DrawioFileSync.PROTOCOL &&
														value.d != null)
													{
														checksum = value.d.checksum;
														temp.push(value.d.patch);
													}
													else
													{
														failed = true;
														temp = [];
														break;
													}
												}
											}
										}
										catch (e)
										{
											temp = [];
											
											if (window.console != null && urlParams['test'] == '1')
											{
												console.log(e);
											}
										}
									}
									
									try
									{
										if (temp.length > 0)
										{
											this.file.stats.cacheHits++;
											this.merge(temp, checksum, etag, success, error, abort);
										}
										// Retries if cache entry was not yet there
										else if (cacheReadyRetryCount <= this.maxCacheReadyRetries &&
											!failed && req.getStatus() != 401)
										{
											cacheReadyRetryCount++;
											this.file.stats.cacheMiss++;
											window.setTimeout(doCatchup, (cacheReadyRetryCount + 1) * this.cacheReadyDelay);
										}
										else
										{
											this.file.stats.cacheFail++;
											this.reload(success, error, abort);
										}
									}
									catch (e)
									{
										if (error != null)
										{
											error(e);
										}
									}
								}
							}
						}));
					}
				}
			});
			
			window.setTimeout(doCatchup, this.cacheReadyDelay);
		}
	}
};

/**
 * Adds the listener for automatically saving the diagram for local changes.
 */
DrawioFileSync.prototype.reload = function(success, error, abort, shadow)
{
	this.file.updateFile(mxUtils.bind(this, function()
	{
		this.lastModified = this.file.getLastModifiedDate();
		this.updateStatus();
		this.start();
		
		if (success != null)
		{
			success();
		}
	}), mxUtils.bind(this, function(err)
	{
		if (error != null)
		{
			error(err);
		}
	}), abort, shadow);
};

/**
 * Adds the listener for automatically saving the diagram for local changes.
 */
DrawioFileSync.prototype.merge = function(patches, checksum, etag, success, error, abort)
{
	try
	{
		this.file.stats.merged++;
		this.lastModified = new Date();
		this.file.shadowPages = (this.file.shadowPages != null) ?
			this.file.shadowPages : this.ui.getPagesForNode(
			mxUtils.parseXml(this.file.shadowData).documentElement)

		// Creates a patch for backup if the checksum fails
		this.file.backupPatch = (this.file.isModified()) ?
			this.ui.diffPages(this.file.shadowPages,
			this.ui.pages) : null;
		var ignored = this.file.ignorePatches(patches);

		if (!ignored)
		{
			// Patches the shadow document
			for (var i = 0; i < patches.length; i++)
			{
				this.file.shadowPages = this.ui.patchPages(this.file.shadowPages, patches[i]);
			}
			
			var current = (checksum != null) ? this.ui.getHashValueForPages(this.file.shadowPages) : null;
			
			if (urlParams['test'] == '1')
			{
				EditorUi.debug('Sync.merge', [this],
					'from', this.file.getCurrentEtag(), 'to', etag,
					'backup', this.file.backupPatch,
					'attempt', this.catchupRetryCount,
					'patches', patches,
					'checksum', checksum == current, checksum);
			}
			
			// Compares the checksum
			if (checksum != null && checksum != current)
			{
				var from = this.ui.hashValue(this.file.getCurrentEtag());
				var to = this.ui.hashValue(etag);
				
				this.file.checksumError(error, patches, 'From: ' + from + '\nTo: ' + to +
					'\nChecksum: ' + checksum + '\nCurrent: ' + current, etag, 'merge');

				// Uses current state as shadow to compute diff since
				// shadowPages has been modified in-place above
				// LATER: Check if fallback to reload is possible
//				this.reload(success, error, abort, this.ui.pages);
				
				// Abnormal termination
				return;
			}
			else
			{
				// Patches the current document
				this.file.patch(patches,
					(DrawioFile.LAST_WRITE_WINS) ?
					this.file.backupPatch : null);
			}
		}

		this.file.invalidChecksum = false;
		this.file.inConflictState = false;
		this.file.setCurrentEtag(etag);
		this.file.backupPatch = null;
		
		if (success != null)
		{
			success();
		}
	}
	catch (e)
	{
		this.file.inConflictState = true;
		this.file.invalidChecksum = true;
		this.file.descriptorChanged();
		
		if (error != null)
		{
			error(e);
		}
		
		try
		{
			if (this.file.errorReportsEnabled)
			{
				var from = this.ui.hashValue(this.file.getCurrentEtag());
				var to = this.ui.hashValue(etag);
				
				this.file.sendErrorReport('Error in merge',
					'From: ' + from + '\nTo: ' + to +
					'\nChecksum: ' + checksum +
					'\nPatches:\n' + this.file.compressReportData(
						JSON.stringify(patches, null, 2)), e);
			}
			else
			{
				var user = this.file.getCurrentUser();
				var uid = (user != null) ? user.id : 'unknown';
				
				EditorUi.logError('Error in merge', null,
					this.file.getMode() + '.' +
					this.file.getId(), uid, e);
			}
		}
		catch (e2)
		{
			// ignore
		}
	}
};

/**
 * Invokes after a file was saved to add cache entry (which in turn notifies
 * collaborators).
 */
DrawioFileSync.prototype.descriptorChanged = function(etag)
{
	this.lastModified = this.file.getLastModifiedDate();
	
	if (this.channelId != null)
	{
		var msg = this.objectToString(this.createMessage({a: 'desc',
			m: this.lastModified.getTime()}));
		var current = this.file.getCurrentEtag();
		var data = this.objectToString({});

		mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() +
			'&from=' + encodeURIComponent(etag) + '&to=' + encodeURIComponent(current) +
			'&msg=' + encodeURIComponent(msg) + '&data=' + encodeURIComponent(data));
		this.file.stats.bytesSent += data.length;
		this.file.stats.msgSent++;
	}
	
	this.updateStatus();
};

/**
 * Invokes after a file was saved to add cache entry (which in turn notifies
 * collaborators).
 */
DrawioFileSync.prototype.objectToString = function(obj)
{
	var data = this.ui.editor.graph.compress(JSON.stringify(obj));
	
	if (this.key != null && typeof CryptoJS !== 'undefined')
	{
		data = CryptoJS.AES.encrypt(data, this.key).toString();
	}
	
	return data;
};

/**
 * Invokes after a file was saved to add cache entry (which in turn notifies
 * collaborators).
 */
DrawioFileSync.prototype.stringToObject = function(data)
{
	if (this.key != null && typeof CryptoJS !== 'undefined')
	{
		data = CryptoJS.AES.decrypt(data, this.key).toString(CryptoJS.enc.Utf8);
	}
	
	return JSON.parse(this.ui.editor.graph.decompress(data));
};

/**
 * Invokes after a file was saved to add cache entry (which in turn notifies
 * collaborators).
 */
DrawioFileSync.prototype.fileSaved = function(pages, lastDesc, success, error)
{
	this.lastModified = this.file.getLastModifiedDate();
	this.resetUpdateStatusThread();
	this.catchupRetryCount = 0;
	
	if (!this.ui.isOffline() && !this.file.inConflictState && !this.file.redirectDialogShowing)
	{
		this.start();

		if (this.channelId != null && this.isConnected())
		{
			// Computes diff and checksum
			var shadow = (this.file.shadowPages != null) ?
				this.file.shadowPages : this.ui.getPagesForNode(
				mxUtils.parseXml(this.file.shadowData).documentElement)
			var checksum = this.ui.getHashValueForPages(pages);
			var diff = this.ui.diffPages(shadow, pages);
			
			// Data is stored in cache and message is sent to all listeners
			var etag = this.file.getDescriptorEtag(lastDesc);
			var current = this.file.getCurrentEtag();
			
			var data = this.objectToString(this.createMessage({patch: diff, checksum: checksum}));
			var msg = this.objectToString(this.createMessage({m: this.lastModified.getTime()}));
			var secret = this.file.getDescriptorSecret(this.file.getDescriptor());

			this.file.stats.bytesSent += data.length;
			this.file.stats.msgSent++;
	
			mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() +
				'&from=' + encodeURIComponent(etag) + '&to=' + encodeURIComponent(current) +
				'&msg=' + encodeURIComponent(msg) + ((secret != null) ? '&secret=' + encodeURIComponent(secret) : '') +
				((data.length < this.maxCacheEntrySize) ? '&data=' + encodeURIComponent(data) : ''),
				mxUtils.bind(this, function(req)
			{
				// Ignores response
			}));
	
			if (urlParams['test'] == '1')
			{
				EditorUi.debug('Sync.fileSaved', [this],
					'from', etag, 'to', current, data.length,
					'bytes', 'diff', diff, 'checksum', checksum);
			}
		}
	}
	
	this.file.shadowPages = pages;
	
	if (success != null)
	{
		success();
	}
};

/**
 * Creates the properties for the file descriptor.
 */
DrawioFileSync.prototype.getIdParameters = function()
{
	var result = 'id=' + this.channelId;
	
	if (this.pusher != null && this.pusher.connection != null)
	{
		result += '&sid=' + this.pusher.connection.socket_id;
	}
	
	return result;
};

/**
 * Creates the properties for the file descriptor.
 */
DrawioFileSync.prototype.createMessage = function(data)
{
	return {v: DrawioFileSync.PROTOCOL, d: data, c: this.clientId};
};

/**
 * Creates the properties for the file descriptor.
 */
DrawioFileSync.prototype.fileConflict = function(desc, success, error)
{
	this.catchupRetryCount++;
	
	if (this.catchupRetryCount < this.maxCatchupRetries)
	{
		this.file.stats.conflicts++;
		
		if (desc != null)
		{
			var etag = this.file.getDescriptorEtag(desc);
			var secret = this.file.getDescriptorSecret(desc);
			this.catchup(etag, secret, success, error);
		}
		else
		{
			this.fileChanged(success, error);
		}
	}
	else
	{
		this.file.stats.timeouts++;
		this.catchupRetryCount = 0;
		
		if (error != null)
		{
			error({message: mxResources.get('timeout')});
		}
	}
};

/**
 * Adds the listener for automatically saving the diagram for local changes.
 */
DrawioFileSync.prototype.stop = function()
{
	if (this.pusher != null)
	{
		EditorUi.debug('Sync.stop', [this]);
	
		if (this.pusher.connection != null)
		{
			this.pusher.connection.unbind('state_change', this.connectionListener);
			this.pusher.connection.unbind('error', this.pusherErrorListener);
		}
	
		if (this.channel != null) 
		{
			this.channel.unbind('changed', this.changeListener);
			
			// See https://github.com/pusher/pusher-js/issues/75
			// this.pusher.unsubscribe(this.channelId);
			this.channel = null;
		}
		
		this.pusher.disconnect();
		this.pusher = null;
	}
	
	this.updateOnlineState();
	this.updateStatus();
};

/**
 * Adds the listener for automatically saving the diagram for local changes.
 */
DrawioFileSync.prototype.destroy = function()
{
	if (this.channelId != null)
	{
		var user = this.file.getCurrentUser();
		var leave = {a: 'leave'};
		
		if (user != null)
		{
			leave.name = user.displayName;
			leave.uid = user.id;
		}
		
		mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() +
			'&msg=' + encodeURIComponent(this.objectToString(
			this.createMessage(leave))));
		this.file.stats.msgSent++;
	}
	
	this.stop();

	if (this.updateStatusThread != null)
	{
		window.clearInterval(this.updateStatusThread);
		this.updateStatusThread = null;
	}
	
	if (this.onlineListener != null)
	{
		mxEvent.removeListener(window, 'online', this.onlineListener);
		this.onlineListener = null;
	}

	if (this.visibleListener != null)
	{
		mxEvent.removeListener(document, 'visibilitychange', this.visibleListener);
		this.visibleListener = null;
	}

	if (this.activityListener != null)
	{
		mxEvent.removeListener(document, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', this.activityListener);
		mxEvent.removeListener(document, 'keypress', this.activityListener);
		mxEvent.removeListener(window, 'focus', this.activityListener);
		
		if (!mxClient.IS_POINTER && mxClient.IS_TOUCH)
		{
			mxEvent.removeListener(document, 'touchstart', this.activityListener);
			mxEvent.removeListener(document, 'touchmove', this.activityListener);	
		}
		
		this.activityListener = null;
	}
	
	if (this.collaboratorsElement != null)
	{
		this.collaboratorsElement.parentNode.removeChild(this.collaboratorsElement);
		this.collaboratorsElement = null;
	}
};
