/**
 * Copyright (c) 2006-2024, JGraph Holdings Ltd
 * Copyright (c) 2006-2024, draw.io AG
 */

//Add a closure to hide the class private variables without changing the code a lot
(function ()
{

var _token = null;
var _tokenSP = null;
var _pickerToken = null;
let _editorUi = null;
let _filePickedCallback = null;
let _pickerPort = null;
let _pickerWindow = null;
let _pickerMessageListener = null;
let _pickerCloseTimer = null;

window.OneDriveClient = function(editorUi, isExtAuth, inlinePicker, noLogout, sharepointMode)
{
	this.sharepointMode = sharepointMode === true;
	_editorUi = editorUi;

	if (isExtAuth == null && window.urlParams != null && window.urlParams['extAuth'] == '1')
	{
		isExtAuth = true;
	}
	
	if (inlinePicker == null) //Use inline picker as default
	{
		inlinePicker = window.Editor != null? Editor.oneDriveInlinePicker : true;
	}
	
	if (noLogout == null && window.urlParams != null && window.urlParams['noLogoutOD'] == '1')
	{
		noLogout = true;
	}
	
	DrawioClient.call(this, editorUi, isExtAuth? 'oneDriveExtAuthInfo' : 
		(this.sharepointMode? 'ms365AuthInfo': 'oneDriveAuthInfo'));
	
	this.isExtAuth = isExtAuth;
	this.inlinePicker = inlinePicker;
	this.noLogout = noLogout;
	var authInfo = JSON.parse(this.token);
	
	if (authInfo != null)
	{
		this.endpointHint = authInfo.endpointHint != null ? authInfo.endpointHint.replace('/Documents', '/_layouts/15/onedrive.aspx') : authInfo.endpointHint;
	}

	if (this.sharepointMode)
	{
		this.redirectUri = window.DRAWIO_SERVER_URL + 'ms365';
	}
};

// Extends DrawioClient
mxUtils.extend(OneDriveClient, DrawioClient);

/**
 * Specifies if thumbnails should be enabled. Default is true.
 * LATER: If thumbnails are disabled, make sure to replace the
 * existing thumbnail with the placeholder only once.
 */
OneDriveClient.prototype.clientId = window.DRAWIO_MSGRAPH_CLIENT_ID || ((window.location.hostname == 'test.draw.io') ?
		'95e4b4ed-ed5c-4a05-935b-b411b4562ef2' : '24b129a6-117b-4394-bdc8-3b9955e5cdef');

OneDriveClient.prototype.clientId = window.location.hostname == 'app.diagrams.net' ?
		'b5ff67d6-3155-4fca-965a-59a3655c4476' : OneDriveClient.prototype.clientId;

OneDriveClient.prototype.clientId = window.location.hostname == 'viewer.diagrams.net' ?
		'417a451a-a343-4788-b6c1-901e63182565' : OneDriveClient.prototype.clientId;
/**
 * OAuth 2.0 scopes for installing Drive Apps.
 */
OneDriveClient.prototype.scopes = 'user.read files.readwrite.all sites.read.all';
OneDriveClient.prototype.scopesSP = 'https://microsoft.sharepoint.com/AllSites.Write https://microsoft.sharepoint.com/MyFiles.Write';

/**
 * OAuth 2.0 scopes for installing Drive Apps.
 */
OneDriveClient.prototype.redirectUri = window.DRAWIO_SERVER_URL + 'microsoft';
OneDriveClient.prototype.pickerRedirectUri = window.DRAWIO_SERVER_URL + 'onedrive3.html';

/**
 * This is the default endpoint for personal accounts
 */
OneDriveClient.prototype.defEndpointHint = 'api.onedrive.com'; 
OneDriveClient.prototype.endpointHint = OneDriveClient.prototype.defEndpointHint;

/**
 * Value for the root folder.
 */
OneDriveClient.prototype.rootId = {id: 'root', name: 'root', parentReference: {driveId: 'me'}};

/**
 * Executes the first step for connecting to Google Drive.
 */
OneDriveClient.prototype.extension = '.drawio';

/**
 * Executes the first step for connecting to Google Drive.
 */
OneDriveClient.prototype.baseUrl = 'https://graph.microsoft.com/v1.0';

OneDriveClient.prototype.authUrl = 'https://login.microsoftonline.com/' + (window.DRAWIO_MSGRAPH_TENANT_ID || 'common');

OneDriveClient.prototype.PICKER_SDK_VERSION = "8.0";
OneDriveClient.prototype.FILE_EXTENSIONS = [".drawio", ".xml", ".svg", ".png", ".pdf", ".vsdx"];
OneDriveClient.prototype.sharepointMode = false;

/**
 * Empty function used when no callback is needed
 */
OneDriveClient.prototype.emptyFn = function(){};

/**
 * Authorizes the client, gets the userId and calls <open>.
 */
OneDriveClient.prototype.invalidFilenameRegExs = [
	/[~"#%\*:<>\?\/\\{\|}]/,
	/^\.lock$/i,
	/^CON$/i,
	/^PRN$/i,
	/^AUX$/i,
	/^NUL$/i,
	/^COM\d$/i,
	/^LPT\d$/i,
	/^desktop\.ini$/i,
	/_vti_/i
];

/**
 * Check if the file/folder name is valid
 */
OneDriveClient.prototype.isValidFilename = function(filename)
{
	if (filename == null || filename === '') return false;
	
	for (var i = 0; i < this.invalidFilenameRegExs.length; i++)
	{
		if (this.invalidFilenameRegExs[i].test(filename)) return false;
	}
	
	return true;
};


/**
 * Checks if the client is authorized and calls the next step.
 */
OneDriveClient.prototype.get = function(url, onload, onerror)
{
	var req = new mxXmlRequest(url, null, 'GET');
	
	req.setRequestHeaders = mxUtils.bind(this, function(request, params)
	{
		request.setRequestHeader('Authorization', 'Bearer ' + _token);
	});
	
	req.send(onload, onerror);
	
	return req;
};

/**
 * Checks if the client is authorized and calls the next step.
 */
OneDriveClient.prototype.updateUser = function(success, error, failOnAuth)
{
	var acceptResponse = true;
	
	var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
	{
		acceptResponse = false;
		error({code: App.ERROR_TIMEOUT});
	}), this.ui.timeout);
	
	this.get(this.baseUrl + '/me', mxUtils.bind(this, function(req)
	{
		window.clearTimeout(timeoutThread);
		
		if (acceptResponse)
		{
			if (req.getStatus() < 200 || req.getStatus() >= 300)
			{
				if (!failOnAuth)
				{
					this.logout();
					
					this.authenticate(mxUtils.bind(this, function()
					{
						this.updateUser(success, error, true);
					}), error, null, this.sharepointMode);
				}
				else
				{
					error({message: mxResources.get('accessDenied')});
				}
			}
			else
			{
				try
				{
					var data = JSON.parse(req.getText());
					this.setUser(new DrawioUser(data.id, data.mail, data.displayName));
					success();
				}
				catch (e)
				{
					error(e);
				}
			}
		}
	}), mxUtils.bind(this, function(err)
	{
		window.clearTimeout(timeoutThread);
			    	
		if (acceptResponse)
		{
			error(err);
		}
	}));
};

/**
 * Authorizes the client, gets the userId and calls <open>.
 */
OneDriveClient.prototype.resetTokenRefresh = function(expires_in)
{
	if (this.tokenRefreshThread != null)
	{
		window.clearTimeout(this.tokenRefreshThread);
		this.tokenRefreshThread = null;
	}

	// Starts timer to refresh token before it expires
	if (expires_in > 0)
	{
		this.tokenRefreshInterval = expires_in * 1000;
		
		this.tokenRefreshThread = window.setTimeout(mxUtils.bind(this, function()
		{
			//Get a new fresh accessToken
			this.authenticate(this.emptyFn, this.emptyFn, true, this.sharepointMode);
		}), expires_in * 900);
	}
};

/**
 * Authorizes the client, gets the userId and calls <open>.
 */
OneDriveClient.prototype.authenticate = function(success, error, failOnAuth, isSP, forceUserUpdate)
{
	if (this.isExtAuth)
	{
		var oneDriveAuth = window.oneDriveAuth? window.oneDriveAuth : window.parent.oneDriveAuth;
		oneDriveAuth(mxUtils.bind(this, function(newAuthInfo)
		{
			this.updateAuthInfo(newAuthInfo, true, this.endpointHint == null, success, error);
		}), error, window.urlParams != null && urlParams['odAuthCancellable'] == '1');
		return;
	}
	
	var req = new mxXmlRequest(this.redirectUri + '?getState=1', null, 'GET');
	
	req.send(mxUtils.bind(this, function(req)
	{
		if (req.getStatus() >= 200 && req.getStatus() <= 299)
		{
			this.authenticateStep2(req.getText(), success, error, failOnAuth, isSP, forceUserUpdate);
		}
		else if (error != null)
		{
			error({message: mxResources.get('error') + ' ' + req.getStatus()});
		}
	}), error);
};

/**
 * Authorizes the client, gets the userId and calls <open>.
 */
OneDriveClient.prototype.updateAuthInfo = function(newAuthInfo, remember, forceUserUpdate, success, error, isSP)
{
	if (forceUserUpdate)
	{
		this.setUser(null);
	}
	
	if (isSP)
	{
		_pickerToken = newAuthInfo.access_token;
	}
	else
	{
		_token = newAuthInfo.access_token;

		if (this.sharepointMode)
		{
			_tokenSP = newAuthInfo.access_token;
		}
	}

	delete newAuthInfo.access_token; //Don't store access token
	newAuthInfo.expiresOn = Date.now() + newAuthInfo.expires_in * 1000;
	this.tokenExpiresOn = newAuthInfo.expiresOn;

	newAuthInfo.remember = remember;
	this.setPersistentToken(JSON.stringify(newAuthInfo), !remember);
	this.resetTokenRefresh(newAuthInfo.expires_in);

	if (forceUserUpdate)
	{
		//Find out the type of the account + endpoint
		this.getAccountTypeAndEndpoint(mxUtils.bind(this, function()
		{
			success();
		}), error);	
	}
	else
	{
		success();
	}
};

/**
 * Authorizes the client, gets the userId and calls <open>.
 */
OneDriveClient.prototype.authenticateStep2 = function(state, success, error, failOnAuth, isSP, forceUserUpdate)
{
	if (window.onOneDriveCallback == null)
	{
		var auth = mxUtils.bind(this, function()
		{
			var acceptAuthResponse = true;
			
			//Retry request with refreshed token
			var authInfo = JSON.parse(this.getPersistentToken(true));
			
			if (authInfo != null)
			{
				var req = new mxXmlRequest(this.redirectUri + '?state=' + encodeURIComponent('cId=' + this.clientId +
					'&domain=' + window.location.host + '&token=' + state) +
					'&scopes=' + encodeURIComponent(isSP? this.scopesSP : this.scopes), null, 'GET'); // To identify which app/domain is used
				
				req.send(mxUtils.bind(this, function(req)
				{
					if (req.getStatus() >= 200 && req.getStatus() <= 299)
					{
						try
						{
							this.updateAuthInfo(JSON.parse(req.getText()), authInfo.remember, forceUserUpdate, mxUtils.bind(this, function()
							{
								if (isSP)
								{
									this.authenticate(success, error, failOnAuth, false);
								}
								else
								{
									success();
								}
							}), error, isSP);
						}
						catch (e)
						{
							error({message: mxResources.get('authFailed'), retry: auth});
						}
					}
					else 
					{
						this.clearPersistentToken();
						this.setUser(null);
						_token = null;

						if (this.sharepointMode)
						{
							_tokenSP = null;
						}

 						// (Unauthorized) [e.g, invalid refresh token] or bad request
						if ((req.getStatus() == 401 || req.getStatus() == 400) && !failOnAuth)
						{
							auth();
						}
						else
						{
							error({message: mxResources.get('accessDenied'), retry: auth});
						}
					}
				}), error);
			}
			else
			{
				this.ui.showAuthDialog(this, true, mxUtils.bind(this, function(remember, authSuccess)
				{
					var url = this.authUrl + '/oauth2/v2.0/authorize' +
						'?client_id=' + this.clientId + '&response_type=code&prompt=select_account' +
						'&redirect_uri=' + encodeURIComponent(this.redirectUri) +
						'&scope=' + encodeURIComponent((isSP? this.scopesSP : this.scopes) + (remember? ' offline_access' : '')) +
						'&state=' + encodeURIComponent('cId=' + this.clientId + '&domain=' + window.location.host + '&token=' + state); //To identify which app/domain is used
	
					var width = 525,
						height = 525,
						screenX = window.screenX,
						screenY = window.screenY,
						outerWidth = window.outerWidth,
						outerHeight = window.outerHeight;
					
					var left = screenX + Math.max(outerWidth - width, 0) / 2;
					var top = screenY + Math.max(outerHeight - height, 0) / 2;
					
					var features = ['width=' + width, 'height=' + height,
					                'top=' + top, 'left=' + left,
					                'status=no', 'resizable=yes',
					                'toolbar=no', 'menubar=no',
					                'scrollbars=yes'];
					var popup = window.open(url, 'odauth', features.join(','));
					
					if (popup != null)
					{
						window.onOneDriveCallback = mxUtils.bind(this, function(authInfo, authWindow)
						{
							if (acceptAuthResponse)
							{
								window.onOneDriveCallback = null;
								acceptAuthResponse = false;
								
								try
								{
									if (authInfo == null)
									{
										error({message: mxResources.get('accessDenied'), retry: auth});
									}
									else
									{
										if (authSuccess != null)
										{
											authSuccess();
										}
										
										this.updateAuthInfo(authInfo, remember, !isSP, mxUtils.bind(this, function()
										{
											if (isSP)
											{
												this.authenticate(success, error, failOnAuth, false, true);
											}
											else
											{
												success();
											}
										}), error, isSP);
									}
								}
								catch (e)
								{
									error(e);
								}
								finally
								{
									if (authWindow != null)
									{
										authWindow.close();
									}
								}
							}
							else if (authWindow != null)
							{
								authWindow.close();
							}
						});
					
						popup.focus();
					}
				}), mxUtils.bind(this, function()
				{
					if (acceptAuthResponse)
					{
						window.onOneDriveCallback = null;
						acceptAuthResponse = false;
						error({message: mxResources.get('accessDenied'), retry: auth});
					}
				}));
			}
		});
		
		auth();
	}
	else
	{
		error({code: App.ERROR_BUSY});
	}
};

/**
 * Authorizes the client, gets the userId and calls <open>.
 */
OneDriveClient.prototype.getAccountTypeAndEndpoint = function(success, error)
{
	this.get(this.baseUrl + '/me/drive/root', mxUtils.bind(this, function(req)
	{
		try
		{
			if (req.getStatus() >= 200 && req.getStatus() <= 299)
			{
				var resp = JSON.parse(req.getText());
				
				if (resp.webUrl.indexOf('.sharepoint.com') > 0) 
			 	{
					//TODO Confirm this works with all sharepoint sites
					this.endpointHint = resp.webUrl.replace('/Documents', '/_layouts/15/onedrive.aspx');
				}
				else
				{
					this.endpointHint = this.defEndpointHint;
				}
				
			 	//Update authInfo with endpointHint
			 	var authInfo = JSON.parse(this.getPersistentToken(true));
			 	
			 	if (authInfo != null)
		 		{
				 	authInfo.endpointHint = this.endpointHint;
				 	this.setPersistentToken(JSON.stringify(authInfo), !authInfo.remember);
		 		}
			 	
				success();
				return;
			}
		}
		catch(e) {}
		//It is expected to work as this call immediately follows getting a fresh access token
		error({message: mxResources.get('unknownError') + ' (Code: ' + req.getStatus() + ')'});
		
	}), error);
};

/**
 * Checks if the client is authorized and calls the next step.
 */
OneDriveClient.prototype.executeRequest = function(url, success, error)
{
	if (this.sharepointMode)
	{
		_token = _tokenSP;
	}

	var doExecute = mxUtils.bind(this, function(failOnAuth)
	{
		var acceptResponse = true;
		
		var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
		{
			acceptResponse = false;
			error({code: App.ERROR_TIMEOUT, retry: doExecute});
		}), this.ui.timeout);

		this.get(url, mxUtils.bind(this, function(req)
		{
			window.clearTimeout(timeoutThread);
			
			if (acceptResponse)
			{
				// 404 (file not found) is a valid response for checkExists
				if ((req.getStatus() >= 200 && req.getStatus() <= 299) || req.getStatus() == 404)
				{
					if (this.user == null)
					{
						this.updateUser(this.emptyFn, this.emptyFn, true);
					}
					
					success(req);
				}
				// 400 is returns if wrong user for this file
				else if (!failOnAuth && (req.getStatus() === 401 || req.getStatus() === 400))
				{
					//Authorize again using the refresh token
					this.authenticate(function()
					{
						doExecute(true);
					}, error, failOnAuth, this.sharepointMode);
				}
				else
				{
					error(this.parseRequestText(req));
				}
			}
		}), mxUtils.bind(this, function(err)
		{
			window.clearTimeout(timeoutThread);
				    	
			if (acceptResponse)
			{
				error(err);
			}
		}));
	});
	
	if (_token == null || this.tokenExpiresOn - Date.now() < 60000) //60 sec tolerance window
	{
		this.authenticate(function()
		{
			doExecute(true);
		}, error, null, this.sharepointMode);
	}
	else
	{
		doExecute(false);
	}
};

/**
 * Checks if the client is authorized and calls the next step.
 */
OneDriveClient.prototype.checkToken = function(fn, error)
{
	if (this.sharepointMode)
	{
		_token = _tokenSP;
	}

	if (_token == null || this.tokenRefreshThread == null || this.tokenExpiresOn - Date.now() < 60000)
	{
		this.authenticate(fn, (error != null) ? error : this.emptyFn, null, this.sharepointMode);
	}
	else
	{
		fn();
	}
};

/**
 * Authorizes the client, gets the userId and calls <open>.
 */
OneDriveClient.prototype.getItemRef = function(id)
{
	var idParts = id.split('/');
	
	if (idParts.length > 1)
	{
		return {driveId: idParts[0], id: idParts[1]};
	}
	else
	{
		return {id: id};
	}
};

/**
 * Authorizes the client, gets the userId and calls <open>.
 */
OneDriveClient.prototype.getItemURL = function(id, relative)
{
	var idParts = id.split('/');
	
	if (idParts.length > 1)
	{
		var driveId = idParts[0];
		var itemId = idParts[1];
		return (relative? '' : this.baseUrl) + '/drives/' + driveId + (itemId == 'root' ? '/root' : '/items/' + itemId);
	}
	else
	{
		return (relative? '' : this.baseUrl) + '/me/drive/items/' + id;
	}
};

/**
 * Checks if the client is authorized and calls the next step.
 */
OneDriveClient.prototype.getLibrary = function(id, success, error)
{
	this.getFile(id, success, error, false, true);
};

/**
 * Workaround for added content to HTML files in Sharepoint.
 */
OneDriveClient.prototype.removeExtraHtmlContent = function(data)
{
	var idx = data.lastIndexOf('<html><head><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8"><meta name="Robots" ');

	if (idx > 0)
	{
		data = data.substring(0, idx);
	}
	
	return data;
};

/**
 * Checks if the client is authorized and calls the next step.
 */
OneDriveClient.prototype.getFile = function(id, success, error, denyConvert, asLibrary)
{
	asLibrary = (asLibrary != null) ? asLibrary : false;

	this.executeRequest(this.getItemURL(id), mxUtils.bind(this, function(req)
	{
		if (req.getStatus() >= 200 && req.getStatus() <= 299)
		{
			var meta = JSON.parse(req.getText());
			var binary = /\.png$/i.test(meta.name);
			
			// Handles .vsdx, Gliffy and PNG+XML files by creating a temporary file
			if (/\.v(dx|sdx?)$/i.test(meta.name) || /\.gliffy$/i.test(meta.name) ||
				/\.pdf$/i.test(meta.name) || (!Editor.useCanvasForExport && binary))
			{
				var mimeType = (meta.file != null) ? meta.file.mimeType : null;
				this.ui.convertFile(meta['@microsoft.graph.downloadUrl'], meta.name, mimeType,
					this.extension, success, error);
			}
			else
			{
				var acceptResponse = true;
				
				var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
				{
					acceptResponse = false;
					error({code: App.ERROR_TIMEOUT})
				}), this.ui.timeout);
				
				this.ui.editor.loadUrl(meta['@microsoft.graph.downloadUrl'], mxUtils.bind(this, function(data)
				{
					try
					{
						window.clearTimeout(timeoutThread);
			    	
				    	if (acceptResponse)
				    	{
				    		if (/\.html$/i.test(meta.name))
				    		{
				    			data = this.removeExtraHtmlContent(data);
				    		}
				    		
							var index = (binary) ? data.lastIndexOf(',') : -1;
							var file = null;
	
							if (index > 0)
							{
								var xml = this.ui.extractGraphModelFromPng(data);
								
								if (xml != null && xml.length > 0)
								{
									data = xml;
								}
								else
								{
									// Imports as PNG image
									file = new LocalFile(this.ui, data, meta.name, true);
								}
							}
							// Checks for base64 encoded mxfile
							else if (data.substring(0, 32) == 'data:image/png;base64,PG14ZmlsZS')
							{
								var temp = data.substring(22);
								data = (window.atob && !mxClient.IS_SF) ? atob(temp) : Base64.decode(temp);
							}
							
							if (Graph.fileSupport && new XMLHttpRequest().upload && this.ui.isRemoteFileFormat(data, meta['@microsoft.graph.downloadUrl']))
							{
								this.ui.parseFileData(data, mxUtils.bind(this, function(xhr)
								{
									try
									{
										if (xhr.readyState == 4)
										{
											if (xhr.status >= 200 && xhr.status <= 299)
											{
												success(new LocalFile(this.ui, xhr.responseText, meta.name + this.extension, true));
											}
											else if (error != null)
											{
												error({message: mxResources.get('errorLoadingFile')});
											}
										}
									}
									catch (e)
									{
										if (error != null)
										{
											error(e);
										}
										else
										{
											throw e;
										}
									}
								}), meta.name);
							}
							else
							{
								if (file != null)
								{
									success(file);
								}
								else if (asLibrary)
								{
									success(new OneDriveLibrary(this.ui, data, meta, this.sharepointMode));
								}
								else
								{
									success(new OneDriveFile(this.ui, data, meta, this.sharepointMode));
								}
							}
				    	}
					}
					catch (e)
					{
						if (error != null)
						{
							error(e);
						}
						else
						{
							throw e;
						}
					}
    			}), mxUtils.bind(this, function(req)
				{
					window.clearTimeout(timeoutThread);
			    	
			    	if (acceptResponse)
			    	{
			    		error(this.parseRequestText(req));
			    	}
				}), binary || (meta.file != null && meta.file.mimeType != null &&
					((meta.file.mimeType.substring(0, 6) == 'image/' &&
					meta.file.mimeType.substring(0, 9) != 'image/svg') ||
					meta.file.mimeType == 'application/pdf')));
			}
		}
		else
		{
			if (this.isExtAuth)
			{
				error({message: mxResources.get('fileNotFoundOrDenied'),
						ownerEmail: window.urlParams != null? urlParams['ownerEml'] : null});
			}
			else
			{
				error(this.parseRequestText(req));				
			}
		}
	}), function(err)
	{
		if (error != null)
		{
			// Replaces ObjectHandle is Invalid error message
			if (err != null && err.error != null &&
				err.error.message === 'ObjectHandle is Invalid')
			{
				err.error.message = mxResources.get('fileNotFoundOrDenied');
			}
			
			error(err);
		}
	});
};

/**
 * Translates this point by the given vector.
 * 
 * @param {number} dx X-coordinate of the translation.
 * @param {number} dy Y-coordinate of the translation.
 */
OneDriveClient.prototype.renameFile = function(file, filename, success, error)
{
	if (file != null && filename != null)
	{
		if (!this.isValidFilename(filename))
		{
			error({message: this.invalidFilenameRegExs[0].test(filename) ?
					mxResources.get('oneDriveCharsNotAllowed') : mxResources.get('oneDriveInvalidDeviceName')});
			return;
		}
		
		// TODO: How to force overwrite file with same name?
		this.checkExists(file.getParentId(), filename, false, mxUtils.bind(this, function(checked)
		{
			if (checked)
			{
				this.writeFile(this.getItemURL(file.getId()), JSON.stringify({name: filename}), 'PATCH', 'application/json', success, error);
			}
			else
			{
				error();
			}
		}));
	}
};

/**
 * Translates this point by the given vector.
 * 
 * @param {number} dx X-coordinate of the translation.
 * @param {number} dy Y-coordinate of the translation.
 */
OneDriveClient.prototype.moveFile = function(id, folderId, success, error)
{
	//check that the source and destination are on the same drive
	var folderInfo = this.getItemRef(folderId);
	var fileInfo = this.getItemRef(id);
	
	if (folderInfo.driveId != fileInfo.driveId)
	{
		error({message: mxResources.get('cannotMoveOneDrive', null, 'Moving a file between accounts is not supported yet.')});
	}
	else 
	{
		this.writeFile(this.getItemURL(id), JSON.stringify({parentReference: folderInfo}), 'PATCH', 'application/json', success, error);
	}
};

/**
 * Translates this point by the given vector.
 * 
 * @param {number} dx X-coordinate of the translation.
 * @param {number} dy Y-coordinate of the translation.
 */
OneDriveClient.prototype.insertLibrary = function(filename, data, success, error, folderId)
{
	this.insertFile(filename, data, success, error, true, folderId);
};

/**
 * Translates this point by the given vector.
 * 
 * @param {number} dx X-coordinate of the translation.
 * @param {number} dy Y-coordinate of the translation.
 */
OneDriveClient.prototype.insertFile = function(filename, data, success, error, asLibrary, folderId)
{
	if (!this.isValidFilename(filename))
	{
		error({message: this.invalidFilenameRegExs[0].test(filename) ?
				mxResources.get('oneDriveCharsNotAllowed') : mxResources.get('oneDriveInvalidDeviceName')});
		return;
	}

	asLibrary = (asLibrary != null) ? asLibrary : false;
	
	this.checkExists(folderId, filename, true, mxUtils.bind(this, function(checked)
	{
		if (checked)
		{
			var folder = '/me/drive/root';
			
			if (folderId != null)
			{
				folder = this.getItemURL(folderId, true);
			}
			
			var insertSuccess = mxUtils.bind(this, function(meta)
			{
				if (asLibrary)
				{
					success(new OneDriveLibrary(this.ui, data, meta, this.sharepointMode));
				}
				else
				{
					success(new OneDriveFile(this.ui, data, meta, this.sharepointMode));
				}
			});

			var url = this.baseUrl + folder + '/children/' + encodeURIComponent(filename) + '/content';
			
			//OneDrive has a limit on PUT API of 4MB, larger files needs to use the upload session method
			if (data.length >= 4000000 /*4MB*/)
			{
				//Create empty file first then upload. TODO Can we get an upload session for non-existing files?
				this.writeFile(url, '', 'PUT', null, mxUtils.bind(this, function(meta)
				{
					this.writeLargeFile(this.getItemURL(meta.id), data, insertSuccess, error);
				}), error);
			}
			else
			{
				this.writeFile(url, data, 'PUT', null, insertSuccess, error);
			}
		}
		else
		{
			error();
		}
	}))
};

/**
 * Translates this point by the given vector.
 * 
 * @param {number} dx X-coordinate of the translation.
 * @param {number} dy Y-coordinate of the translation.
 */
OneDriveClient.prototype.checkExists = function(parentId, filename, askReplace, fn)
{
	var folder = '/me/drive/root';
	
	if (parentId != null) 
	{
		folder = this.getItemURL(parentId, true);
	}
	
	this.executeRequest(this.baseUrl + folder + '/children/' + encodeURIComponent(filename), mxUtils.bind(this, function(req)
	{
		if (req.getStatus() == 404)
		{
			fn(true);
		}
		else
		{
			if (askReplace)
			{
				this.ui.spinner.stop();
				
				this.ui.confirm(mxResources.get('replaceIt', [filename]), function()
				{
					fn(true);
				}, function()
				{
					fn(false);
				});
			}
			else
			{
				this.ui.spinner.stop();
				
				this.ui.showError(mxResources.get('error'), mxResources.get('fileExists'), mxResources.get('ok'), function()
				{
					fn(false);						
				});
			}
		}
	}), function(req)
	{
		fn(false);
	}, true);
};

/**
 * Translates this point by the given vector.
 * 
 * @param {number} dx X-coordinate of the translation.
 * @param {number} dy Y-coordinate of the translation.
 */
OneDriveClient.prototype.saveFile = function(file, success, error, etag)
{
	try
	{
		var savedData = file.getData();
		
		var fn = mxUtils.bind(this, function(data)
		{
			var saveSuccess = mxUtils.bind(this, function(resp)
			{
				success(resp, savedData);
			});

			var url = this.getItemURL(file.getId());
			
			//OneDrive has a limit on PUT API of 4MB, larger files needs to use the upload session method
			if (data.length >= 4000000 /*4MB*/)
			{
				this.writeLargeFile(url, data, saveSuccess, error, etag);
			}
			else
			{
				this.writeFile(url + '/content/', data, 'PUT', null, saveSuccess, error, etag);
			}
		});
		
		if (Editor.useCanvasForExport && /(\.png)$/i.test(file.meta.name))
		{
			var p = this.ui.getPngFileProperties(this.ui.fileNode);
			
			this.ui.getEmbeddedPng(mxUtils.bind(this, function(data)
			{
				fn(this.ui.base64ToBlob(data, 'image/png'));
			}), error, (this.ui.getCurrentFile() != file) ?
				savedData : null, p.scale, p.border);
		}
		else
		{
			fn(savedData);
		}
	}
	catch (e)
	{
		error(e);
	}
};

OneDriveClient.prototype.writeLargeFile = function(url, data, success, error, etag)
{
	if (this.sharepointMode)
	{
		_token = _tokenSP;
	}

	try
	{
		var chunkSize = 4 * 1024 * 1024; //4MB chunk;
		
		if (data != null)
		{
			var dataByteLength = (new TextEncoder().encode(data)).length;

			var uploadPart = mxUtils.bind(this, function(uploadUrl, index, byteIndex, retryCount)
			{
				try
				{
					retryCount = retryCount || 0;
					var acceptResponse = true;
					var timeoutThread = null;
						
					timeoutThread = window.setTimeout(mxUtils.bind(this, function()
					{
						acceptResponse = false;
						error({code: App.ERROR_TIMEOUT});
					}), this.ui.timeout);
	
					var part = data.substr(index, chunkSize);
					var partByteLength = (new TextEncoder().encode(part)).length;
					var req = new mxXmlRequest(uploadUrl, part, 'PUT');

					req.setRequestHeaders = mxUtils.bind(this, function(request, params)
					{
						request.setRequestHeader('Content-Range', 'bytes ' + byteIndex + '-' + 
							(byteIndex + partByteLength - 1) + '/' + dataByteLength);
					});

					req.send(mxUtils.bind(this, function(req)
					{
				    	window.clearTimeout(timeoutThread);
				    	
				    	if (acceptResponse)
				    	{
							var status = req.getStatus();
					    	if (status >= 200 && status <= 299)
							{
								var nextByte = index + part.length;
								
								if (nextByte == data.length)
								{
									success(JSON.parse(req.getText()));
								}
								else
								{
									uploadPart(uploadUrl, nextByte, byteIndex + partByteLength, retryCount);
								}
							}
							else if (status >= 500 && status <= 599 && retryCount < 2) //Retry on server errors
							{
								retryCount++;
								uploadPart(uploadUrl, index, byteIndex, retryCount);
							}
							else
							{
								error(this.parseRequestText(req), req);
							}
				    	}
					}), mxUtils.bind(this, function(req)
					{
				    	window.clearTimeout(timeoutThread);
				    	
				    	if (acceptResponse)
				    	{
							error(this.parseRequestText(req));
				    	}
					}));
				}
				catch (e)
				{
					error(e);
				}
			});
			
			var doExecute = mxUtils.bind(this, function(failOnAuth)
			{
				try
				{
					var acceptResponse = true;
					var timeoutThread = null;
					
					try
					{
						timeoutThread = window.setTimeout(mxUtils.bind(this, function()
						{
							acceptResponse = false;
							error({code: App.ERROR_TIMEOUT});
						}), this.ui.timeout);
					}
					catch (e)
					{
						// Ignore window closed
					}
					
					var req = new mxXmlRequest(url + '/createUploadSession', '{}', 'POST');
					
					req.setRequestHeaders = mxUtils.bind(this, function(request, params)
					{
						request.setRequestHeader('Content-Type', 'application/json');
						request.setRequestHeader('Authorization', 'Bearer ' + _token);
						
						if (etag != null)
						{
							request.setRequestHeader('If-Match', etag);
						}
					});
					
					req.send(mxUtils.bind(this, function(req)
					{
				    	window.clearTimeout(timeoutThread);
				    	
				    	if (acceptResponse)
				    	{
					    	if (req.getStatus() >= 200 && req.getStatus() <= 299)
							{
								var resp = JSON.parse(req.getText());
					    		uploadPart(resp.uploadUrl, 0, 0);
							}
							else if (!failOnAuth && req.getStatus() === 401)
							{
								this.authenticate(function()
								{
									doExecute(true);
								}, error, failOnAuth, this.sharepointMode);
							}
							else
							{
								error(this.parseRequestText(req), req);
							}
				    	}
					}), mxUtils.bind(this, function(req)
					{
				    	window.clearTimeout(timeoutThread);
				    	
				    	if (acceptResponse)
				    	{
							error(this.parseRequestText(req));
				    	}
					}));
				}
				catch (e)
				{
					error(e);
				}
			});
			
			if (_token == null || this.tokenExpiresOn - Date.now() < 60000) //60 sec tolerance window
			{
				this.authenticate(function()
				{
					doExecute(true);
				}, error, null, this.sharepointMode);
			}
			else
			{
				doExecute(false);
			}
		}
		else
		{
			error({message: mxResources.get('unknownError')});
		}
	}
	catch (e)
	{
		error(e);
	}
};

/**
 * Translates this point by the given vector.
 * 
 * @param {number} dx X-coordinate of the translation.
 * @param {number} dy Y-coordinate of the translation.
 */
OneDriveClient.prototype.writeFile = function(url, data, method, contentType, success, error, etag)
{
	if (this.sharepointMode)
	{
		_token = _tokenSP;
	}

	try
	{
		if (url != null && data != null)
		{
			var doExecute = mxUtils.bind(this, function(failOnAuth)
			{
				try
				{
					var acceptResponse = true;
					var timeoutThread = null;
					
					try
					{
						timeoutThread = window.setTimeout(mxUtils.bind(this, function()
						{
							acceptResponse = false;
							error({code: App.ERROR_TIMEOUT});
						}), this.ui.timeout);
					}
					catch (e)
					{
						// Ignore window closed
					}
					
					var req = new mxXmlRequest(url, data, method);
					
					req.setRequestHeaders = mxUtils.bind(this, function(request, params)
					{
						// Space deletes content type header. Specification says "text/plain"
						// should work but returns an 415 Unsupported Media Type error
						request.setRequestHeader('Content-Type', contentType || ' ');
						//TODO This header is needed for moving a file between two different drives. 
						//		Note: the response is empty when this header is used, also the server may take some time to really execute the request (i.e. async) 
						//request.setRequestHeader('Prefer', 'respond-async');
						request.setRequestHeader('Authorization', 'Bearer ' + _token);
						
						if (etag != null)
						{
							request.setRequestHeader('If-Match', etag);
						}
					});
					
					req.send(mxUtils.bind(this, function(req)
					{
				    	window.clearTimeout(timeoutThread);
				    	
				    	if (acceptResponse)
				    	{
					    	if (req.getStatus() >= 200 && req.getStatus() <= 299)
							{
					    		if (this.user == null)
								{
									this.updateUser(this.emptyFn, this.emptyFn, true);
								}
					    		
								success(JSON.parse(req.getText()));
							}
							else if (!failOnAuth && req.getStatus() === 401)
							{
								this.authenticate(function()
								{
									doExecute(true);
								}, error, failOnAuth, this.sharepointMode);
							}
							else
							{
								error(this.parseRequestText(req), req);
							}
				    	}
					}), mxUtils.bind(this, function(req)
					{
				    	window.clearTimeout(timeoutThread);
				    	
				    	if (acceptResponse)
				    	{
							error(this.parseRequestText(req));
				    	}
					}));
				}
				catch (e)
				{
					error(e);
				}
			});
			
			if (_token == null || this.tokenExpiresOn - Date.now() < 60000) //60 sec tolerance window
			{
				this.authenticate(function()
				{
					doExecute(true);
				}, error, null, this.sharepointMode);
			}
			else
			{
				doExecute(false);
			}
		}
		else
		{
			error({message: mxResources.get('unknownError')});
		}
	}
	catch (e)
	{
		error(e);
	}
};

/**
 * Checks if the client is authorized and calls the next step.
 */
OneDriveClient.prototype.parseRequestText = function(req)
{
	var result = {message: mxResources.get('unknownError')};
	
	try
	{
		result = JSON.parse(req.getText());
		result.status = req.getStatus();
		
		if (result.error)
		{
			result.error.status = result.status;
			result.error.code = result.status;
		}
	}
	catch (e)
	{
		// ignore
	}
	
	return result;
};

/**
 * Checks if the client is authorized and calls the next step.
 */
OneDriveClient.prototype.pickLibrary = function(fn)
{
	this.pickFile(function(id)
	{
		// Ignores second argument
		fn(id);
	});
};

/**
 * Checks if the client is authorized and calls the next step.
 */
OneDriveClient.prototype.createInlinePicker = function(fn, foldersOnly, acceptAllFiles)
{
	return mxUtils.bind(this, function()
	{
		var odPicker = null;
		var div = document.createElement('div');
		div.style.position = 'relative';
		
		var dlg = new CustomDialog(this.ui, div, mxUtils.bind(this, function()
		{
			var item = odPicker.getSelectedItem();
			
			if (item != null)
			{
				if (foldersOnly && typeof item.folder == 'object')
				{
					fn({value: [item]});
				}
				else if (!item.folder && this.ui.spinner.spin(document.body, mxResources.get('loading')))
				{
					var id = OneDriveFile.prototype.getIdOf(item);

					this.executeRequest(this.getItemURL(id), mxUtils.bind(this, function(req)
					{
						this.ui.spinner.stop();

						if (req.getStatus() >= 200 && req.getStatus() <= 299)
						{
							var meta = JSON.parse(req.getText());
							fn(id, {value: [meta]});
						}
						else
						{
							this.ui.handleError({code: req.getStatus()});
						}
					}), mxUtils.bind(this, function(req)
					{
						this.ui.spinner.stop();
						this.ui.handleError(req);
					}));
				}

				return;
			}
			
			return mxResources.get('invalidSel', null, 'Invalid selection');
		}), null, mxResources.get(foldersOnly? 'select' :'open'), null, null, null, null, true);
		
		this.ui.showDialog(dlg.container, 550, 500, true, true);
		//Set width/height of the picker container
		div.style.width = (parseInt(dlg.container.parentNode.style.width) - 60) + 'px';
		div.style.height = (parseInt(dlg.container.parentNode.style.height) - 120) + 'px';
		
		odPicker = new mxODPicker(div, null, mxUtils.bind(this, function(url, success, error, isAbsUrl)
		{
			this.executeRequest(isAbsUrl? url : this.baseUrl + url, function(req)
			{
				success(JSON.parse(req.getText()));
			}, error);
		}), mxUtils.bind(this, function(id, driveId, success, error)
		{
			this.executeRequest(this.baseUrl + '/drives/' + driveId + '/items/' + id, function(req)
			{
				success(JSON.parse(req.getText()));
			}, error);
		}), null, null, function(item)
		{
			if (foldersOnly) //Currently this is not called when in foldersOnly mode
			{
				fn({
					value: [item]
				});
			}
			else
			{
				fn(OneDriveFile.prototype.getIdOf(item));
			}
		},
		mxUtils.bind(this, function(err)
		{
			this.ui.showError(mxResources.get('error'), err);
		}), foldersOnly, null, null, null, null, acceptAllFiles); 
	});
};

const cleanupPickerResources = function () {
	if (_pickerCloseTimer != null) {
		clearInterval(_pickerCloseTimer);
		_pickerCloseTimer = null;
	}

	if (_pickerMessageListener != null) {
		try { window.removeEventListener('message', _pickerMessageListener); } catch (e) { /* ignore */ }
		_pickerMessageListener = null;
	}

	if (_pickerPort != null) {
		try { _pickerPort.close && _pickerPort.close(); } catch (e) { /* ignore */ }
		_pickerPort = null;
	}

	if (_pickerWindow != null) {
		try { if (!_pickerWindow.closed) { _pickerWindow.close(); } } catch (e) { /* ignore */ }
	}

	_pickerWindow = null;
};

const closePickerDialog = function () {
	cleanupPickerResources();
};

const extractDriveId = function (itemObj, parent) {
	return ((itemObj.parentReference != null && itemObj.parentReference.driveId != null) ? itemObj.parentReference.driveId + '/' : '') +
	((parent != null) ? itemObj.parentReference.id : (itemObj.id + (itemObj.folder && itemObj.folder.isRoot ? '/root' : '')));
};

const generateUuid = () =>
	'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
		const r = Math.random() * 16 | 0;
		const v = c === 'x' ? r : (r & 0x3 | 0x8);
		return v.toString(16);
});
OneDriveClient.prototype.createPickerConfig = function (mode, webUrl) {
	return {
		sdk: this.PICKER_SDK_VERSION,
		entry: {
			sharePoint: {
				byPath: {
					web: webUrl
				}
			}
		},
		authentication: {},
		messaging: {
			origin: location.origin,
			channelId: generateUuid()
		},
		typesAndSources: {
			mode,
			...(mode === "files" && { filters: this.FILE_EXTENSIONS }),
			pivots: {
			oneDrive: true,
			recent: true,
			shared: false,
			sharedLibraries: true,
			myOrganization: false,
			site: true
			}
		},
		selection: {
			mode: "single"
		},
		theme: Editor.isDarkMode()? 'dark' : 'default'
	};
};

OneDriveClient.prototype.setupPickerMessageListener = function (channelId, messageHandler) {
	if (_pickerMessageListener != null) {
		try { window.removeEventListener('message', _pickerMessageListener); } catch (e) { /* ignore */ }
		_pickerMessageListener = null;
	}

	const handleMessage = (event) => {
		if (event.source && event.source === _pickerWindow) {
			const message = event.data;

			if (message.type === "initialize" && message.channelId === channelId) {
				_pickerPort = event.ports[0];
				_pickerPort.addEventListener("message", messageHandler);
				_pickerPort.start();
				_pickerPort.postMessage({ type: "activate" });
			}
		}
	};

	_pickerMessageListener = handleMessage;
	window.addEventListener('message', handleMessage);
};

const handlePickerCommandMessage = async function (data, onPick) {
	_pickerPort.postMessage({ type: 'acknowledge', id: data.id });

	const command = data.data;

	try {
		switch (command.command) {
		case 'authenticate':
			const token = _pickerToken;

			if (token) {
				_pickerPort.postMessage({
					type: 'result',
					id: data.id,
					data: { result: 'token', token }
				});
			}
			else {
				throw new Error('Could not get auth token for command: ' + JSON.stringify(command));
			}
			break;

		case 'close':
			closePickerDialog();
			break;

		case 'pick':
			_pickerPort.postMessage({
				type: 'result',
				id: data.id,
				data: { result: 'success' }
			});

			closePickerDialog();
			onPick(command);
			break;

		default:
			console.warn('Unsupported command: ' + JSON.stringify(command));
			_pickerPort.postMessage({
				type: 'result',
				id: data.id,
				data: {
				result: 'error',
				error: {
					code: 'unsupportedCommand',
					message: command.command
				},
				isExpected: true
				}
			});
		}
	}
	catch (err) {
		console.error('Error handling picker command:', err);
		_pickerPort.postMessage({
			type: 'result',
			id: data.id,
			data: {
			result: 'error',
			error: { code: 'commandError', message: err.message },
			isExpected: true
			}
		});
	}
};

OneDriveClient.prototype.handleFolderPickerMessage = async function (message) {
	const { data } = message;

	switch (data.type) {
	case "notification":
		break;

	case "command":
		await handlePickerCommandMessage(data, function (command) {
			_filePickedCallback({ value: command.items });
		});
		break;
	}
};

OneDriveClient.prototype.handleFilePickerMessage = async (message) => {
	const { data } = message;

	switch (data.type) {
	case "command":
		await handlePickerCommandMessage(data, function (command) {
			const selectedFileId = extractDriveId(command.items[0]);
			_filePickedCallback(selectedFileId);
		});
		break;
	}
};

OneDriveClient.prototype.pickFile = function (fn, acceptAllFiles) 
{
	this.sharepointMode? this.pickFileSP(fn, acceptAllFiles) :
								this.pickFileOD(fn, acceptAllFiles)
};

OneDriveClient.prototype.pickFolder = async function (fn, direct) 
{
	this.sharepointMode? this.pickFolderSP(fn, direct) :
								this.pickFolderOD(fn, direct);
};

OneDriveClient.prototype.pickFileSP = function (fn, acceptAllFiles) {
	fn = fn ?? mxUtils.bind(this, function (id) {
		this.ui.loadFile('M' + encodeURIComponent(id));
	});
	this.launchPicker(fn, 'files');
};

OneDriveClient.prototype.pickFolderSP = async function (fn, direct) {
	this.launchPicker(fn, 'folders');
};

OneDriveClient.prototype.launchPicker = async function (fn, mode)
{
	try
	{
		_filePickedCallback = fn;
		cleanupPickerResources();

		this.executeRequest(this.baseUrl + '/me/drive', mxUtils.bind(this, function(req)
		{
			if (req.getStatus() >= 200 && req.getStatus() <= 299)
			{
				var info = JSON.parse(req.getText());
				var baseUrl = info.webUrl;

				if (baseUrl != null)
				{
					// Remove the last segment from the baseUrl which is /Documents or similar
					var lastSlash = baseUrl.lastIndexOf('/');
					if (lastSlash > 0) {
						baseUrl = baseUrl.substring(0, lastSlash);
					}
				}

				const pickerConfig = this.createPickerConfig(mode, baseUrl);
				const queryString = new URLSearchParams({
					filePicker: JSON.stringify(pickerConfig)
				});

				// Use popup window instead of iframe to avoid SharePoint frame-ancestors CSP restrictions
				var popupWidth = 1080, popupHeight = 680;
				var left = (screen.width - popupWidth) / 2;
				var top = (screen.height - popupHeight) / 2;
				_pickerWindow = window.open('', 'SharePointPicker',
					'width=' + popupWidth + ',height=' + popupHeight + ',left=' + left + ',top=' + top +
					',menubar=no,toolbar=no,location=no,status=no');

				if (_pickerWindow == null)
				{
					this.ui.showError(mxResources.get('error'), 'Popup blocked. Please allow popups for this site.');
					cleanupPickerResources();
					return;
				}

				// Load FilePicker via GET in popup — access_token is optional for popups
				// and avoids SharePoint CSP issues with POSTed token pages.
				// The picker will request tokens via MessagePort authenticate commands.
				const url = baseUrl + '/_layouts/15/FilePicker.aspx?' + queryString;

				this.setupPickerMessageListener(pickerConfig.messaging.channelId, mode === 'files' ? this.handleFilePickerMessage : this.handleFolderPickerMessage);

				_pickerWindow.location.href = url;

				// Monitor popup close — if user closes the popup manually, clean up
				_pickerCloseTimer = setInterval(function()
				{
					if (_pickerWindow == null || _pickerWindow.closed)
					{
						cleanupPickerResources();
					}
				}, 500);
			}
			else
			{
				this.ui.handleError({code: req.getStatus()});
			}
		}), mxUtils.bind(this, function(req)
		{
			this.ui.handleError(req);
		}));
	}
	catch (e)
	{
		cleanupPickerResources();
		this.ui.showError(mxResources.get('error'), e && e.message ? e.message : e);
	}
};

/**
 * Checks if the client is authorized and calls the next step.
 */
OneDriveClient.prototype.pickFolderOD = function(fn, direct)
{
	var errorFn = mxUtils.bind(this, function(e)
	{
		this.ui.showError(mxResources.get('error'), e && e.message? e.message : e);
	});
	
	var odSaveDlg = mxUtils.bind(this, function(direct)
	{
		var openSaveDlg = this.inlinePicker ?
			this.createInlinePicker(fn, true) :
			mxUtils.bind(this, function()
		{
			OneDrive.save(
			{
				clientId: this.clientId,
				action: 'query',
				openInNewWindow: true,
				advanced:
				{
					'endpointHint': this.endpointHint,
					'redirectUri': this.pickerRedirectUri,
					'queryParameters': 'select=id,name,parentReference',
					'accessToken': _token,
					isConsumerAccount: false
				},
				success: mxUtils.bind(this, function(files)
				{
					fn(files);
				}),
				cancel: mxUtils.bind(this, function()
				{
					// do nothing
				}),
				error: errorFn
			});
		});
		
		if (direct)
		{
			openSaveDlg();
		}
		else
		{
			this.ui.confirm(mxResources.get('useRootFolder'), mxUtils.bind(this, function()
			{
				fn({value: [this.rootId]});
				
			}), openSaveDlg, mxResources.get('yes'), mxResources.get('noPickFolder') + '...', true);
		}
		
		if (this.user == null)
		{
			this.updateUser(this.emptyFn, this.emptyFn, true);
		}
	});
	
	if (_token == null || this.tokenExpiresOn - Date.now() < 60000) //60 sec tolerance window
	{
		this.authenticate(mxUtils.bind(this, function()
		{
			// Direct only possible within user event
			odSaveDlg(this.inlinePicker && direct);
		}), errorFn);
	}
	else
	{
		odSaveDlg(direct);
	}
};

/**
 * Checks if the client is authorized and calls the next step.
 */
OneDriveClient.prototype.pickFileOD = function(fn, acceptAllFiles)
{
	fn = (fn != null) ? fn : mxUtils.bind(this, function(id)
	{
		this.ui.loadFile('W' + encodeURIComponent(id));
	});
	
	var errorFn = mxUtils.bind(this, function(e)
	{
		this.ui.showError(mxResources.get('authFailed'), e && e.message? e.message : e);
	});
	
	var odOpenDlg = this.inlinePicker? this.createInlinePicker(fn, null, acceptAllFiles) :
							mxUtils.bind(this, function()
	{
		OneDrive.open(
		{
			clientId: this.clientId,
			action: 'query',
			multiSelect: false,
			advanced:
			{
				'endpointHint': this.endpointHint,
				'redirectUri': this.pickerRedirectUri,
				'queryParameters': 'select=id,name,parentReference,webUrl', //We can also get @microsoft.graph.downloadUrl within this request but it will break the normal process
				'accessToken': _token,
				isConsumerAccount: false
			},
			success: mxUtils.bind(this, function(files)
			{
				if (files != null && files.value != null && files.value.length > 0)
				{
					fn(OneDriveFile.prototype.getIdOf(files.value[0]), files);
				}
			}),
			cancel: mxUtils.bind(this, function()
			{
				// do nothing
			}),
			error: errorFn
		});
		
		if (this.user == null)
		{
			this.updateUser(this.emptyFn, this.emptyFn, true);
		}
	});
	
	if (_token == null || this.tokenExpiresOn - Date.now() < 60000) //60 sec tolerance window
	{
		this.authenticate(mxUtils.bind(this, function()
		{
			if (this.inlinePicker)
			{
				odOpenDlg();
			}
			else
			{
				this.ui.showDialog(new BtnDialog(this.ui, this, mxResources.get('open'), mxUtils.bind(this, function()
				{
					this.ui.hideDialog();
					odOpenDlg();							
				})).container, 300, 140, true, true);
			}
		}), errorFn)
	}
	else
	{
		odOpenDlg();
	}
};

/**
 * Checks if the client is authorized and calls the next step.
 */
OneDriveClient.prototype.logout = function()
{
	if (isLocalStorage)
	{
		var check = localStorage.getItem('odpickerv7cache');
		
		if (check != null && check.substring(0, 19) == '{"odsdkLoginHint":{')
		{
			localStorage.removeItem('odpickerv7cache');	
		}
	}

	window.open(this.authUrl + '/oauth2/v2.0/logout', 'logout', 'width=525,height=525,status=no,resizable=yes,toolbar=no,menubar=no,scrollbars=yes');
	//Send to server to clear refresh token cookie
	this.ui.editor.loadUrl(this.redirectUri + '?doLogout=1&state=' + encodeURIComponent('cId=' + this.clientId + '&domain=' + window.location.host));
	this.clearPersistentToken();
	this.setUser(null);
	_token = null;
	
	if (this.sharepointMode)
	{
		_pickerToken = null;
		_tokenSP = null;
	}
};

})();
