/* -*- js-indent-level: 8 -*- */
/*
 * L.Clipboard is used to abstract our storage and management of
 * local & remote clipboard data.
 */
/* global app _ vex brandProductName isAnyVexDialogActive $ */

// Get all interesting clipboard related events here, and handle
// download logic in one place ...
// We keep track of the current selection content if it is simple
// So we can do synchronous copy/paste in the callback if possible.
L.Clipboard = L.Class.extend({
	initialize: function(map) {
		this._map = map;
		this._selectionContent = '';
		this._selectionType = null;
		this._accessKey = [ '', '' ];
		this._clipboardSerial = 0; // incremented on each operation
		this._failedTimer = null;
		this._dummyDivName = 'copy-paste-container';
		this._unoCommandForCopyCutPaste = null;

		var div = document.createElement('div');
		this._dummyDiv = div;

		div.setAttribute('id', this._dummyDivName);
		div.setAttribute('style', 'user-select: text !important');
		div.style.opacity = '0';
		div.setAttribute('contenteditable', 'true');
		div.setAttribute('type', 'text');
		div.setAttribute('style', 'position: fixed; left: 0px; top: -200px; width: 15000px; height: 200px; ' +
				 'overflow: hidden; z-index: -1000, -webkit-user-select: text !important; display: block; ' +
				 'font-size: 6pt">');

		// so we get events to where we want them.
		var parent = document.getElementById('map');
		parent.appendChild(div);

		// sensible default content.
		this._resetDiv();

		var that = this;
		var beforeSelect = function(ev) { return that._beforeSelect(ev); };

		document.oncut = function(ev)   { return that.cut(ev); };
		document.oncopy = function(ev)  { return that.copy(ev); };
		document.onpaste = function(ev) { return that.paste(ev); };
		document.onbeforecut = beforeSelect;
		document.onbeforecopy = beforeSelect;
		document.onbeforepaste = beforeSelect;
	},

	// We can do a much better job when we fetch text/plain too.
	stripHTML: function(html) {
		var tmp = document.createElement('div');
		tmp.innerHTML = html;
		// attempt to cleanup unwanted elements
		var styles = tmp.querySelectorAll('style');
		for (var i = 0; i < styles.length; i++) {
			styles[i].parentNode.removeChild(styles[i]);
		}
		return tmp.textContent.trim() || tmp.innerText.trim() || '';
	},

	setKey: function(key) {
		if (this._accessKey[0] === key)
			return;
		this._accessKey[1] = this._accessKey[0];
		this._accessKey[0] = key;
	},

	getMetaBase: function() {
		return window.makeHttpUrl('');
	},

	getMetaPath: function(idx) {
		if (!idx)
			idx = 0;
		return '/cool/clipboard?WOPISrc=' + encodeURIComponent(this._map.options.doc) +
			'&ServerId=' + app.socket.WSDServer.Id +
			'&ViewId=' + this._map._docLayer._viewId +
			'&Tag=' + this._accessKey[idx];
	},

	getMetaURL: function(idx) {
		return this.getMetaBase() + this.getMetaPath(idx);
	},

	// Returns the marker used to identify stub messages.
	_getHtmlStubMarker: function() {
		return '<title>Stub HTML Message</title>';
	},

	// Returns true if the argument is a stub html.
	_isStubHtml: function(text) {
		return text.indexOf(this._getHtmlStubMarker()) > 0;
	},

	// wrap some content with our stub magic
	_originWrapBody: function(body, isStub) {
		var encodedOrigin = encodeURIComponent(this.getMetaURL());
		var text =  '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\n' +
		            '<html>\n' +
		            '  <head>\n';
		if (isStub)
			text += '    ' + this._getHtmlStubMarker() + '\n';
		text +=     '    <meta http-equiv="content-type" content="text/html; charset=utf-8"/>\n' +
			    '    <meta name="origin" content="' + encodedOrigin + '"/>\n' +
			    '  </head>\n'
			    + body +
			'</html>';
		return text;
	},

	// what an empty clipboard has on it
	_getStubHtml: function() {
		var lang = 'en_US'; // FIXME: l10n
		return this._substProductName(this._originWrapBody(
		    '  <body lang="' + lang + '" dir="ltr">\n' +
		    '    <p>' + _('To paste outside %productName, please first click the \'download\' button') + '</p>\n' +
		    '  </body>\n', true
		));
	},

	// used for DisableCopy mode to fill the clipboard
	_getDisabledCopyStubHtml: function() {
		var lang = 'en_US'; // FIXME: l10n
		return this._substProductName(this._originWrapBody(
		    '  <body lang="' + lang + '" dir="ltr">\n' +
		    '    <p>' + _('Copying from the document disabled') + '</p>\n' +
		    '  </body>\n', true
		));
	},

	_getMetaOrigin: function (html) {
		var match = '<meta name="origin" content="';
		var start = html.indexOf(match);
		if (start < 0) {
			return '';
		}
		var end = html.indexOf('"', start + match.length);
		if (end < 0) {
			return '';
		}
		var meta = html.substring(start + match.length, end);

		// quick sanity checks that it one of ours.
		if (meta.indexOf('%2Fclipboard%3FWOPISrc%3D') >= 0 &&
		    meta.indexOf('%26ServerId%3D') > 0 &&
		    meta.indexOf('%26ViewId%3D') > 0 &&
		    meta.indexOf('%26Tag%3D') > 0)
			return decodeURIComponent(meta);
		else
			console.log('Mis-understood foreign origin: "' + meta + '"');
		return '';
	},

	_encodeHtmlToBlob: function(text) {
		var content = [];
		var data = new Blob([text]);
		content.push('text/html\n');
		content.push(data.size.toString(16) + '\n');
		content.push(data);
		content.push('\n');
		return new Blob(content);
	},

	_readContentSyncToBlob: function(dataTransfer) {
		var content = [];
		var types = dataTransfer.types;
		for (var t = 0; t < types.length; ++t) {
			if (types[t] === 'Files')
				continue; // images handled elsewhere.
			var dataStr = dataTransfer.getData(types[t]);
			// Avoid types that has no content.
			if (!dataStr.length)
				continue;
			var data = new Blob([dataStr]);
			console.log('type ' + types[t] + ' length ' + data.size +
				    ' -> 0x' + data.size.toString(16) + '\n');
			content.push((types[t] === 'text' ? 'text/plain' : types[t]) + '\n');
			content.push(data.size.toString(16) + '\n');
			content.push(data);
			content.push('\n');
		}
		if (content.length > 0)
			return new Blob(content, {type : 'application/octet-stream', endings: 'transparent'});
		else
			return null;
	},

	// Abstract async post & download for our progress wrappers
	// type: GET or POST
	// url:  where to get / send the data
	// optionalFormData: used for POST for form data
	// forClipboard: a boolean telling if we need the "Confirm copy to clipboard" link in the end
	// completeFn: called on completion - with response.
	// progressFn: allows splitting the progress bar up.
	_doAsyncDownload: function(type,url,optionalFormData,forClipboard,completeFn,progressFn,onErrorFn) {
		try {
			var that = this;
			var request = new XMLHttpRequest();

			// avoid to invoke the following code if the download widget depends on user interaction
			if (!that._downloadProgress || !that._downloadProgress.isVisible()) {
				that._startProgress();
				that._downloadProgress.startProgressMode();
			}
			request.onload = function() {
				that._downloadProgress._onComplete();
				if (!forClipboard) {
					that._downloadProgress._onClose();
				}

				// For some reason 400 error from the server doesn't
				// invoke onerror callback, but we do get here with
				// size==0, which signifies no response from the server.
				// So we check the status code instead.
				if (this.status == 200) {
					completeFn(this.response);
				} else if (onErrorFn) {
					onErrorFn(this.response);
				}
			};
			request.onerror = function() {
				if (onErrorFn)
					onErrorFn();
				that._downloadProgress._onComplete();
				that._downloadProgress._onClose();
			};

			request.upload.addEventListener('progress', function (e) {
				if (e.lengthComputable) {
					var percent = progressFn(e.loaded / e.total * 100);
					var progress = { statusType: 'setvalue', value: percent };
					that._downloadProgress._onUpdateProgress(progress);
				}
			}, false);
			request.open(type, url, true /* isAsync */);
			request.timeout = 20 * 1000; // 20 secs ...
			request.responseType = 'blob';
			if (optionalFormData !== null)
				request.send(optionalFormData);
			else
				request.send();
		} catch (error) {
			if (onErrorFn)
				onErrorFn();
		}
	},

	// Suck the data from one server to another asynchronously ...
	_dataTransferDownloadAndPasteAsync: function(src, dest, fallbackHtml) {
		var that = this;
		// FIXME: add a timestamp in the links (?) ignore old / un-responsive servers (?)
		that._doAsyncDownload(
			'GET', src, null, false,
			function(response) {
				console.log('download done - response ' + response);
				var formData = new FormData();
				formData.append('data', response, 'clipboard');
				that._doAsyncDownload(
					'POST', dest, formData, false,
					function() {
						if (this.pasteSpecialVex && this.pasteSpecialVex.isOpen) {
							vex.close(this.pasteSpecialVex);
							console.log('up-load done, now paste special');
							app.socket.sendMessage('uno .uno:PasteSpecial');
						} else {
							console.log('up-load done, now paste');
							app.socket.sendMessage('uno .uno:Paste');
						}

					},
					function(progress) { return 50 + progress/2; }
				);
			},
			function(progress) { return progress/2; },
			function() {
				console.log('failed to download clipboard using fallback html');

				// If it's the stub, avoid pasting.
				if (that._isStubHtml(fallbackHtml))
				{
					// Let the user know they haven't really copied document content.
					vex.dialog.alert({
						message: _('Failed to download clipboard, please re-copy'),
						callback: function () {
							that._map.focus();
						}
					});
					return;
				}

				var formData = new FormData();
				formData.append('data', new Blob([fallbackHtml]), 'clipboard');
				that._doAsyncDownload(
					'POST', dest, formData, false,
					function() {
						if (this.pasteSpecialVex && this.pasteSpecialVex.isOpen) {
							vex.close(this.pasteSpecialVex);
							console.log('up-load of fallback done, now paste special');
							app.socket.sendMessage('uno .uno:PasteSpecial');
						} else {
							console.log('up-load of fallback done, now paste');
							app.socket.sendMessage('uno .uno:Paste');
						}

					},
					function(progress) { return 50 + progress/2; },
					function() {
						that.dataTransferToDocumentFallback(null, fallbackHtml);
					}
				);
			}
		);
	},

	_onFileLoadFunc: function(file) {
		var socket = app.socket;
		return function(e) {
			var blob = new Blob(['paste mimetype=' + file.type + '\n', e.target.result]);
			socket.sendMessage(blob);
		};
	},

	_asyncReadPasteImage: function(file) {
		if (file.type.match(/image.*/)) {
			var reader = new FileReader();
			reader.onload = this._onFileLoadFunc(file);
			reader.readAsArrayBuffer(file);
			return true;
		}
		return false;
	},

	// Returns true if it finished synchronously, and false if it have started an async operation
	// that will likely end at a later time (required to avoid closing progress bar in paste(ev))
	dataTransferToDocument: function (dataTransfer, preferInternal, htmlText, usePasteKeyEvent) {
		// Look for our HTML meta magic.
		//   cf. ClientSession.cpp /textselectioncontent:/

		var meta = this._getMetaOrigin(htmlText);
		var id = this.getMetaPath(0);
		var idOld = this.getMetaPath(1);

		// for the paste, we always prefer the internal LOK's copy/paste
		if (preferInternal === true &&
		    (meta.indexOf(id) >= 0 || meta.indexOf(idOld) >= 0))
		{
			// Home from home: short-circuit internally.
			console.log('short-circuit, internal paste');
			this._doInternalPaste(this._map, usePasteKeyEvent);
			return true;
		}

		// Do we have a remote Online we can suck rich data from ?
		if (meta !== '')
		{
			console.log('Transfer between servers\n\t"' + meta + '" vs. \n\t"' + id + '"');
			this._dataTransferDownloadAndPasteAsync(meta, this.getMetaURL(), htmlText);
			return false; // just started async operation - did not finish yet
		}

		// Fallback.
		this.dataTransferToDocumentFallback(dataTransfer, htmlText, usePasteKeyEvent);
		return true;
	},

	dataTransferToDocumentFallback: function(dataTransfer, htmlText, usePasteKeyEvent) {

		var content;
		if (dataTransfer) {
			// Suck HTML content out of dataTransfer now while it feels like working.
			content = this._readContentSyncToBlob(dataTransfer);
		}

		// Fallback on the html.
		if (!content && htmlText != '') {
			content = this._encodeHtmlToBlob(htmlText);
		}

		// FIXME: do we want this section ?

		// Images get a look in only if we have no content and are async
		var htmlImage = htmlText.substring(0, 4) === '<img';
		if (((content == null && htmlText === '') || htmlImage) && dataTransfer != null)
		{
			var types = dataTransfer.types;

			console.log('Attempting to paste image(s)');

			// first try to transfer images
			// TODO if we have both Files and a normal mimetype, should we handle
			// both, or prefer one or the other?
			for (var t = 0; t < types.length; ++t) {
				console.log('\ttype' + types[t]);
				if (types[t] === 'Files') {
					var files = dataTransfer.files;
					if (files !== null)
					{
						for (var f = 0; f < files.length; ++f)
							this._asyncReadPasteImage(files[f]);
					}
					else // IE / Edge
						this._asyncReadPasteImage(dataTransfer.items[t].getAsFile());
				}
			}
			return;
		}

		if (content != null) {
			console.log('Normal HTML, so smart paste not possible');

			var formData = new FormData();
			formData.append('file', content);

			var that = this;
			this._doAsyncDownload('POST', this.getMetaURL(), formData, false,
				function() {
					console.log('Posted ' + content.size + ' bytes successfully');
					that._doInternalPaste(that._map, usePasteKeyEvent);
				},
				function(progress) { return progress; }
					    );
		} else {
			console.log('Nothing we can paste on the clipboard');
		}
	},

	_checkSelection: function() {
		var checkSelect = document.getSelection();
		if (checkSelect && checkSelect.isCollapsed)
			console.log('Error: collapsed selection - cannot copy/paste');
	},

	_getHtmlForClipboard: function() {
		var text;

		if ($('.ui-edit').is(':focus'))
			return $('.ui-edit').value();

		if ($('.w2ui-input').is(':focus'))
			return $('.w2ui-input').value();

		if (this._selectionType === 'complex' ||
		    this._map._docLayer.hasGraphicSelection()) {
			console.log('Copy/Cut with complex/graphical selection');
			if (this._selectionType === 'text' && this._selectionContent !== '')
			{ // back here again having downloaded it ...
				text = this._selectionContent;
				console.log('Use downloaded selection.');
			}
			else
			{
				console.log('Downloaded that selection.');
				text = this._getStubHtml();
				this._onDownloadOnLargeCopyPaste();
				this._downloadProgress.setURI( // richer, bigger HTML ...
					this.getMetaURL() + '&MimeType=text/html');
			}
		} else if (this._selectionType === null) {
			console.log('Copy/Cut with no selection!');
			text = this._getStubHtml();
		} else {
			console.log('Copy/Cut with simple text selection');
			text = this._selectionContent;
		}
		return text;
	},

	// returns whether we shold stop processing the event
	populateClipboard: function(ev) {
		this._checkSelection();

		var text = this._getHtmlForClipboard();
		//this._stopHideDownload(); - this confuses the browser ruins copy/cut on iOS

		var plainText = this.stripHTML(text);
		if (ev.clipboardData) { // Standard
			if (this._unoCommandForCopyCutPaste === '.uno:CopyHyperlinkLocation') {
				var ess = 's';
				var re = new RegExp('^(.*)(<a href=")([^"]+)(">.*</a>)(</p>\n</body>\n</html>)$', ess);
				var match = re.exec(text);
				if (match !== null && match.length === 6) {
					text = match[1] + match[3] + match[5];
					plainText = this.stripHTML(text);
				}
			}
			// if copied content is graphical then plainText is null and it does not work on mobile.
			ev.clipboardData.setData('text/plain', plainText ? plainText: ' ');
			ev.clipboardData.setData('text/html', text);
			console.log('Put "' + text + '" on the clipboard');
			this._clipboardSerial++;
		}

		return true; // prevent default
	},

	_isAnyInputFieldSelected: function() {
		if ($('#search-input').is(':focus'))
			return true;

		if ($('.ui-edit').is(':focus'))
			return true;

		if ($('.w2ui-input').is(':focus'))
			return true;

		if (isAnyVexDialogActive() && !(this.pasteSpecialVex && this.pasteSpecialVex.isOpen))
			return true;

		if ($('.annotation-active').length)
		    return true;

		return false;
	},

	// Does the selection of text before an event comes in
	_beforeSelect: function(ev) {
		console.log('Got event ' + ev.type + ' setting up selection');

		if (this._isAnyInputFieldSelected())
			return;

		this._beforeSelectImpl();
	},

	_beforeSelectImpl: function() {
		// We need some spaces in there ...
		this._resetDiv();

		var sel = document.getSelection();
		if (!sel)
			return;

		var selected = false;
		var selectRange;

		if (!selected)
		{
			sel.removeAllRanges();
			selectRange = document.createRange();
			selectRange.selectNodeContents(this._dummyDiv);
			sel.addRange(selectRange);

			var checkSelect = document.getSelection();
			if (checkSelect.isCollapsed)
				console.log('Error: failed to select - cannot copy/paste');
		}

		return false;
	},

	_resetDiv: function() {
		// cleanup the content:
		this._dummyDiv.innerHTML =
			'<b style="font-weight:normal; background-color: transparent; color: transparent;"><span>&nbsp;&nbsp;</span></b>';
	},

	// Try-harder fallbacks for emitting cut/copy/paste events.
	_execOnElement: function(operation) {
		var serial = this._clipboardSerial;

		this._resetDiv();

		var success = false;
		var active = null;

		// selection can change focus.
		active = document.activeElement;

		success = (document.execCommand(operation) &&
			   serial !== this._clipboardSerial);

		// try to restore focus if we need to.
		if (active !== null && active !== document.activeElement)
			active.focus();

		console.log('fallback ' + operation + ' ' + (success?'success':'fail'));

		return success;
	},

	// Encourage browser(s) to actually execute the command
	_execCopyCutPaste: function(operation, cmd) {
		var serial = this._clipboardSerial;

		this._unoCommandForCopyCutPaste = cmd;
		if (document.execCommand(operation) &&
		    serial !== this._clipboardSerial) {
			console.log('copied successfully');
			this._unoCommandForCopyCutPaste = null;
			return;
		}
		this._unoCommandForCopyCutPaste = null;

		// try a hidden div
		if (this._execOnElement(operation)) {
			console.log('copied on element successfully');
			return;
		}

		// see if we have help for paste
		if (operation === 'paste')
		{
			try {
				console.warn('Asked parent for a paste event');
				this._map.fire('postMessage', {msgId: 'UI_Paste'});
			} catch (error) {
				console.warn('Failed to post-message: ' + error);
			}
		}

		// wait and see if we get some help
		var that = this;
		clearTimeout(this._failedTimer);
		setTimeout(function() {
			if (that._clipboardSerial !== serial)
			{
				console.log('successful ' + operation);
				if (operation === 'paste')
					that._stopHideDownload();
			}
			else
			{
				console.log('help did not arrive for ' + operation);
				that._warnCopyPaste();
			}
		}, 150 /* ms */);
	},

	// Pull UNO clipboard commands out from menus and normal user input.
	// We try to massage and re-emit these, to get good security event / credentials.
	filterExecCopyPaste: function(cmd) {
		if (window.ThisIsAMobileApp) {
			// We do native copy/paste in the iOS and Android cases
			return false;
		}

		if (this._map['wopi'].DisableCopy && (cmd === '.uno:Copy' || cmd === '.uno:Cut')) {
			// perform internal operations
			app.socket.sendMessage('uno ' + cmd);
			return true;
		}

		if (cmd === '.uno:Copy' || (L.Browser.mobile && L.Browser.safari && cmd === '.uno:CopyHyperlinkLocation')) {
			this._execCopyCutPaste('copy', cmd);
		} else if (cmd === '.uno:Cut') {
			this._execCopyCutPaste('cut', cmd);
		} else if (cmd === '.uno:Paste') {
			this._execCopyCutPaste('paste', cmd);
		} else {
			return false;
		}
		console.log('filtered uno command ' + cmd);
		return true;
	},

	_doCopyCut: function(ev, unoName) {
		console.log(unoName);

		if (this._isAnyInputFieldSelected())
			return;

		if (this._downloadProgressStatus() === 'downloadButton')
			this._stopHideDownload(); // Terminate pending confirmation

		var preventDefault = true;

		if (this._map['wopi'].DisableCopy === true)
		{
			var text = this._getDisabledCopyStubHtml();
			var plainText = this.stripHTML(text);
			if (ev.clipboardData) {
				console.log('Copying disabled: put stub message on the clipboard');
				ev.clipboardData.setData('text/plain', plainText ? plainText: ' ');
				ev.clipboardData.setData('text/html', text);
				this._clipboardSerial++;
			}
		} else {
			preventDefault = this.populateClipboard(ev);
		}

		app.socket.sendMessage('uno .uno:' + unoName);
		if (preventDefault) {
			ev.preventDefault();
			return false;
		}
	},

	_doInternalPaste: function(map, usePasteKeyEvent) {
		if (usePasteKeyEvent) {
			// paste into dialog
			var KEY_PASTE = 1299;
			map._textInput._sendKeyEvent(0, KEY_PASTE);
		} else if (this.pasteSpecialVex && this.pasteSpecialVex.isOpen) {
			this.pasteSpecialVex.close();
			app.socket.sendMessage('uno .uno:PasteSpecial');
		} else {
			// paste into document
			app.socket.sendMessage('uno .uno:Paste');
		}
	},

	cut:  function(ev) { return this._doCopyCut(ev, 'Cut'); },

	copy: function(ev) { return this._doCopyCut(ev, 'Copy'); },

	paste: function(ev) {
		console.log('Paste');

		if (this._isAnyInputFieldSelected())
			return;

		// If the focus is in the search box, paste there.
		if (this._map.isSearching())
			return;

		if (this._downloadProgressStatus() === 'downloadButton')
			this._stopHideDownload(); // Terminate pending confirmation

		if (this._map._activeDialog)
			ev.usePasteKeyEvent = true;

		if (ev.clipboardData) {
			ev.preventDefault();
			var usePasteKeyEvent = ev.usePasteKeyEvent;
			// Always capture the html content separate as we may lose it when we
			// pass the clipboard data to a different context (async calls, f.e.).
			var htmlText = ev.clipboardData.getData('text/html');
			var hasFinished = this.dataTransferToDocument(ev.clipboardData, /* preferInternal = */ true, htmlText, usePasteKeyEvent);
			this._map._textInput._abortComposition(ev);
			this._clipboardSerial++;
			if (hasFinished)
				this._stopHideDownload();
		}
		return false;
	},

	clearSelection: function() {
		this._selectionContent = '';
		this._selectionType = null;
		this._scheduleHideDownload(15);
	},

	// textselectioncontent: message
	setTextSelectionHTML: function(html) {
		this._selectionType = 'text';
		this._selectionContent = html;
		if (L.Browser.cypressTest) {
			this._dummyDiv.innerHTML = html;
		}
		this._scheduleHideDownload(15);
	},

	// sets the selection to some (cell formula) text)
	setTextSelectionText: function(text) {
		this._selectionType = 'text';
		this._selectionContent = this._originWrapBody(
			'<body>' + text + '</body>');
		this._scheduleHideDownload(15);
	},

	// complexselection: message
	onComplexSelection: function (/*text*/) {
		// Mark this selection as complex.
		this._selectionType = 'complex';
		this._scheduleHideDownload(15);
	},

	_startProgress: function() {
		if (!this._downloadProgress) {
			this._downloadProgress = L.control.downloadProgress();
		}
		if (!this._downloadProgress.isVisible()) {
			this._downloadProgress.addTo(this._map);
		}
		this._downloadProgress.show();
	},

	_onDownloadOnLargeCopyPaste: function () {
		if (!this._downloadProgress || this._downloadProgress.isClosed()) {
			this._warnFirstLargeCopyPaste();
			this._startProgress();
		}
		else if (this._downloadProgress.isStarted()) {
			// Need to show this only when a download is really in progress and we block it.
			// Otherwise, it's easier to flash the widget or something.
			this._warnLargeCopyPasteAlreadyStarted();
		}
	},

	_downloadProgressStatus: function() {
		if (this._downloadProgress && this._downloadProgress.isVisible())
			return this._downloadProgress.currentStatus();
	},

	// Download button is still shown after selection changed -> user has changed their mind...
	_scheduleHideDownload: function(s) {
		if (!this._downloadProgress || !this._downloadProgress.isVisible())
			return;

		// If no other copy/paste things occurred then ...
		var that = this;
		var serial = this._clipboardSerial;
		if (!this._hideDownloadTimer)
			this._hideDownloadTimer = setTimeout(function() {
				that._hideDownloadTimer = null;
				if (serial == that._clipboardSerial && that._downloadProgressStatus() === 'downloadButton')
					that._stopHideDownload();
			}, 1000 * s);
	},

	// useful if we did an internal paste already and don't want that.
	_stopHideDownload: function() {
		clearTimeout(this._hideDownloadTimer);
		this._hideDownloadTimer = null;

		if (!this._downloadProgress ||
		    !this._downloadProgress.isVisible() ||
		    this._downloadProgress.isClosed())
			return;
		this._downloadProgress._onClose();
	},

	_userAlreadyWarned: function (warning) {
		var itemKey = warning;
		var storage = localStorage;
		if (storage && !storage.getItem(itemKey)) {
			storage.setItem(itemKey, '1');
			return false;
		} else if (!storage)
			return false;

		return true;
	},

	_warnCopyPaste: function() {
		var self = this;
		var msg;
		if (window.mode.isMobile() || window.mode.isTablet()) {
			msg = _('<p>Please use the copy/paste buttons on your on-screen keyboard.</p>');
		} else {
			msg = _('<p>Your browser has very limited access to the clipboard, so use these keyboard shortcuts:</p><table class="warn-copy-paste"><tr><td><kbd>Ctrl</kbd><span class="kbd--plus">+</span><kbd>C</kbd></td><td><kbd>Ctrl</kbd><span class="kbd--plus">+</span><kbd>X</kbd></td><td><kbd>Ctrl</kbd><span class="kbd--plus">+</span><kbd>V</kbd></td></tr><tr><td>Copy</td><td>Cut</td><td>Paste</td></tr></table>');
			msg = L.Util.replaceCtrlAltInMac(msg);
		}
		vex.dialog.alert({
			unsafeMessage: msg,
			callback: function () {
				self._map.focus();
			}
		});
	},

	_substProductName: function (msg) {
		var productName = (typeof brandProductName !== 'undefined') ? brandProductName : 'Collabora Online Development Edition';
		return msg.replace('%productName', productName);
	},

	_warnFirstLargeCopyPaste: function () {
		if (this._userAlreadyWarned('warnedAboutLargeCopy'))
			return;

		var self = this;
		var msg = _('<p>If you would like to share larger elements of your document with other applications ' +
			    'it is necessary to first download them onto your device. To do that press the ' +
			    '"Start download" button below, and when complete click "Confirm copy to clipboard".</p>' +
			    '<p>If you are copy and pasting between documents inside %productName, ' +
			    'there is no need to download.</p>');
		vex.dialog.alert({
			unsafeMessage: this._substProductName(msg),
			callback: function () {
				self._map.focus();
			}
		});
	},

	_warnLargeCopyPasteAlreadyStarted: function () {
		var self = this;
		vex.dialog.alert({
			unsafeMessage: _('<p>A download due to a large copy/paste operation has already started. ' +
				   'Please, wait for the current download or cancel it before starting a new one</p>'),
			callback: function () {
				self._map.focus();
			}
		});
	},

});

L.clipboard = function(map) {
	if (window.ThisIsAMobileApp)
		console.log('======> Assertion failed!? No L.Clipboard object should be needed in a mobile app');
	return new L.Clipboard(map);
};
