window.SynchwebShippingHandler={

	synchrotronApiDescription:{

		"apiRoot":null,
		"authHeader":"Authorization: Bearer {{TOKEN}}",

		"getProposals":{
			"uri":"proposal?page=1&per_page=1000",
			"method":"get",
			"path":["data"],
			"properties":{
				"type":"PROPOSALCODE",
				"number":"PROPOSALNUMBER",
				"title":"TITLE",
				"id":"PROPOSALID"
			}
		},

		"getSessions":{
			"uri":"proposal/visits?per_page=9999&prop={{PROPOSALCODE}}{{PROPOSALNUMBER}}",
			"method":"get",
			"path":["data"],
			"properties":{
				"session":"VISIT",
				"beamline":"BL",
				"startDate":"ST",
				"endDate":"EN"
			}
		},

		"getProteins":{
			"uri":"sample/proteins?per_page=9999&prop={{PROPOSALCODE}}{{PROPOSALNUMBER}}",
			"method":"get",
			"path":["data"],
			"properties":{
				"acronym":"ACRONYM"
			}
		},

		"getLabContacts":{
			"uri":"contact?per_page=9999&prop={{PROPOSALCODE}}{{PROPOSALNUMBER}}",
			"method":"get",
			"path":["data"],
			"properties":{
				"emailAddress":"EMAILADDRESS",
				"phoneNumber":"PHONENUMBER",
				"familyName":"FAMILYNAME",
				"givenName":"GIVENNAME",
				"labName":"LABNAME"
			}
		}

	},

	/**
	 * The base URI of the SynchWeb instance.
	 * Populated from the shipmentdestination's apibaseuri property.
	 */
	apiBaseUri:null,

	allowCreatePlaceholderShipment:true,

	/**
	 * The authentication token to be passed to the synchrotron with each request after authentication.
	 * Received from the synchrotron on authentication.
	 */
	token:null,

	safetyLevels:[
		{ "code":"Green", "risk":"Low", "background":"#cfc", "color":"#060"},
		{ "code":"Yellow", "risk":"Medium", "background":"#ffc", "color":"#660"},
		{ "code":"Red", "risk":"High", "background":"#fcc", "color":"#600"}
	],

	/*
     * These are set and used during shipment submission
     */
	shipmentIdAtFacility:"",
	safetyLevel:null,
	labContact:null,
	proteins:null,
	proposal:null,
	visit:null,

	MAIL_IN:"Mail-in",

	/**
	 * Returns the URL of this shipment at the remote synchrotron, or false if the shipment has not been submitted.
	 * Appends /shipment/sid/[REMOTE ID] to base client URI.
	 */
	getShipmentUrlAtFacility:function(shipmentDestination, shipmentIdAtFacility){
		if(!shipmentIdAtFacility &&!data.idatremotefacility){
			return false;
		} else if(!shipmentIdAtFacility){
			shipmentIdAtFacility=data.idatremotefacility;
		}
		let clientBaseUri=shipmentDestination["clientbaseuri"];
		if(!clientBaseUri){
			alert("Cannot get shipment URL. No shipping API base URI set for "+shipmentDestination.name+".");
			return false;
		}
		clientBaseUri=clientBaseUri.replace(/\/$/, "");
		return clientBaseUri+"/shipments/sid/"+shipmentIdAtFacility;
	},

	/**
	 * Returns the URL of this crystal at the remote synchrotron, or false if the crystal does not have a remote ID.
	 * Appends /samples/sid/[REMOTE ID] to client base URI.
	 *
	 */
	getCrystalUrlAtFacility:function(shipmentDestination, crystalIdAtFacility){
		if(!crystalIdAtFacility || ""===crystalIdAtFacility){
			return false;
		}
		let clientBaseUri=shipmentDestination["clientbaseuri"];
		if(!clientBaseUri){
			alert("Cannot get crystal URL. No shipping API base URI set for "+shipmentDestination.name+".");
			return false;
		}
		clientBaseUri=clientBaseUri.replace(/\/$/, "");
		return clientBaseUri+"/samples/sid/"+crystalIdAtFacility;
	},


	/**
	 * Begins the process of authenticating, choosing a proposal/session, and submitting to
	 * the synchrotron.
	 *
	 * It is assumed that local validation of the shipment has already been done! This includes
	 * basic checks such as: at least one dewar; each dewar has at least one puck; all crystals
	 * have protein acronyms. Some validation can only be done at the synchrotron, but we should
	 * not be sending them shipments that fail these basic checks.
	 */
	begin:function(){
		if(data["dateshipped"]){
			alert("Shipment has already been sent.");
			return false;
		} else if(data["idatremotefacility"]){
			Shipment.populatingPlaceholder=true;
		}
		if(!window.shipmentDestination){
			alert("No shipment destination set - cannot determine shipping API location.");
			return false;
		}
		let apiBaseUri=window.shipmentDestination["apibaseuri"];
		if(!apiBaseUri){
			alert("No shipping API base URI set for "+window.shipmentDestination.name+".");
			return false;
		}
		SynchwebShippingHandler.synchrotronApiDescription.apiRoot=window.shipmentDestination["apibaseuri"];
		SynchwebShippingHandler.apiBaseUri=apiBaseUri.replace(/\/+$/g,"")+"/";
		Shipment.synchrotronApiDescription=SynchwebShippingHandler.synchrotronApiDescription;
		//Check still logged into IceBear, reset session clock - don't want to fail at the end.
		if(Shipment.populatingPlaceholder && !confirm("Really add the container and sample details to the placeholder shipment at "+window.shipmentDestination.name+"?")){
			return false;
		}
		new AjaxUtils.Request("/api/homepagebrick", {
			"method":"get",
			"onSuccess":SynchwebShippingHandler.openShippingDialog,
			"onFailure":function(transport){
				if(401===transport.status){
					//IceBear session timed out. Reload page to show login form.
					ui.forceReload();
				} else {
					ui.keepAlive();
					SynchwebShippingHandler.openShippingDialog();
				}
			}
		});

	},

	openShippingDialog:function(){
		if(Shipment.populatingPlaceholder){
			ui.modalBox({ title:"Send shipment details to "+window.shipmentDestination.name, content:"Adding sample details to the placeholder shipment at "+window.shipmentDestination.name+"..." });
			SynchwebShippingHandler.populatePlaceholderShipment();
		} else {
			ui.modalBox({ title:"Send shipment to "+window.shipmentDestination.name, content:"Getting list of proposals from "+window.shipmentDestination.name+"..." });
			SynchwebShippingHandler.getProposals();
		}
	},

	populatePlaceholderShipment:function (){
		if(!SynchwebShippingHandler.token){
			SynchwebShippingHandler.showLoginForm(SynchwebShippingHandler.populatePlaceholderShipment);
			return false;
		}
		SynchwebShippingHandler.sendShipment();
	},

	/**
	 * Renders the login form into the modal box.
	 */
	showLoginForm:function(afterAuthenticate){
		let mb=document.getElementById("modalBox");
		mb.querySelector(".boxbody").innerHTML="";
		ui.setModalBoxTitle("Authenticate at "+window.shipmentDestination.name);
		let f=mb.form({
			action:SynchwebShippingHandler.apiBaseUri+"authenticate",
			method:"post",
		});
		f.afterAuthenticate=afterAuthenticate;
		f.style.maxWidth="800px";
		let h=f.formField({
			label:"Authenticate with your "+window.shipmentDestination.name+" credentials", content:'&nbsp;'
		});
		h.classList.add("radiohead");
		f.textField({
			name:'remoteusername',
			label:window.shipmentDestination.name+" username",
			value:""
		});
		f.passwordField({
			name:'remotepassword',
			label:window.shipmentDestination.name+" password",
			value:""
		});
		fieldValidations.remoteusername="required";
		fieldValidations.remotepassword="required";

		f.submitButton({ label:"Authenticate" });
		ui.infoMessageBar("IceBear doesn't store your "+window.shipmentDestination.name+" credentials.", f);
		f.onsubmit=function(){ SynchwebShippingHandler.authenticate(); return false; };
		document.getElementById("remoteusername").focus();
	},

	/**
	 * Validates that both username and password are present, then submits them to the remote API.
	 * Expects a token in return.
	 */
	authenticate:function(){
		if(document.getElementById("autherror")){
			document.getElementById("autherror").remove();
		}
		let frm=document.getElementById("modalBox").querySelector(".boxbody form");
		let isValid=true;
		frm.querySelectorAll("input").forEach(function(f){
			if(!validator.validate(f)){ isValid=false; }
		});
		if(!isValid){ return false; }
		frm.querySelector("input[type=submit]").closest("label").classList.add("updating");
		AjaxUtils.remoteAjax(
			SynchwebShippingHandler.apiBaseUri+"authenticate",
			'post',
			JSON.stringify({ "login":frm.remoteusername.value, "password":frm.remotepassword.value }), //Note SynchWeb wants JSON as a string, not an object
			SynchwebShippingHandler.authenticate_onSuccess,
			SynchwebShippingHandler.authenticate_onFailure
		);
		return false;
	},
	/**
	 * Success handler for remote synchrotron authentication. Calls getProposals().
	 * Note that Synchweb returns a 200 OK with the "correct" response code in a status value (i.e.,
	 * transport.responseJSON.status instead of transport["status"], which is always 200). We check for
	 * an "error" value and handle anything with it present as a failure.
	 * @param {XMLHttpRequest} transport The response object.
	 */
	authenticate_onSuccess:function(transport){
		if(!transport.responseJSON || !transport.responseJSON["jwt"]){
			if(transport.responseJSON["error"]){
				SynchwebShippingHandler.authenticate_onFailure(transport);
			} else {
				SynchwebShippingHandler.showErrorAtAuthenticate("Bad response from remote server. Cannot authenticate");
			}
		} else {
			SynchwebShippingHandler.token=transport.responseJSON["jwt"];
			let frm=document.getElementById("modalBox").querySelector(".boxbody form");
			window.setTimeout(frm.afterAuthenticate,50);
		}
	},
	/**
	 * Failure handler for remote synchrotron authentication.
	 * @param transport
	 */
	authenticate_onFailure:function(transport){
		let msg="Could not log you in. The remote server gave no further information.";
		if(transport.responseJSON && transport.responseJSON["error"]){
			msg=window.shipmentDestination.name+" said: "+transport.responseJSON["error"];
		}
		SynchwebShippingHandler.showErrorAtAuthenticate(msg);
	},
	/**
	 * Renders the authentication error message below the form.
	 * @param {String} msg The error message.
	 */
	showErrorAtAuthenticate:function(msg){
		let frm=document.getElementById("modalBox").querySelector(".boxbody").querySelector("form");
		frm.querySelector("input[type=submit]").closest("label").classList.remove("updating");
		let ae=document.getElementById("autherror");
		if(!ae){
			let ae=ui.errorMessageBar(msg, frm);
			ae.id="autherror";
		}
	},

	attemptRecoveryFromInvalidToken: function(onSuccess, onFailure){
		AjaxUtils.remoteAjax(
			"https://ispyb.diamond.ac.uk/api/authenticate/check",
			'get',
			'',
			function(xhr){
				if(xhr.responseJSON["jwt"]){
					Shipment.synchrotronApiDescription["token"]=xhr.responseJSON["jwt"];
					SynchwebShippingHandler.token=xhr.responseJSON["jwt"];
					OIDC.token=xhr.responseJSON["jwt"];
					onSuccess();
				} else {
					alert("Auth token was invalid and could not exchange it for a new one.")
				}
			},
			onFailure,
			{
				'X-IceBear-Remote-Cookie':'synchweb-auth='+SynchwebShippingHandler.token
			}
		);
	},

	getProposals:function (){
		if(!SynchwebShippingHandler.token){
			SynchwebShippingHandler.showLoginForm(SynchwebShippingHandler.getProposals);
			return false;
		}
		Shipment.synchrotronApiDescription["token"]=SynchwebShippingHandler.token;
		Shipment.submission.getProposals();
	},

	/**
	 * Returns the definition of an unattended/mail-in session. This is only available at Diamond, so
	 * this returns false for other synchrotrons.
	 */
	getUnattendedSession: function (){
		if(-1===window.shipmentDestination.name.toLowerCase().indexOf("diamond")){ return false; }
		return {
			"VISIT":SynchwebShippingHandler.MAIL_IN,
			"SESSIONID":"",
			"BL":"", "ST":"When available", "EN":"",
			"ACTIVE":"1"
		}
	},

	/**
	 * Returns a dummy session object for use when there are no active sessions. This is only available
	 * at MAXIV, so this Synchweb implementation always returns false.
	 */
	getNullSession: function (){
		return false;
	},

	/**
	 * Whether the synchrotron for the current shipment can accept a shipment with no session. This is
	 * only available at MAX-IV, so this Synchweb implementation always returns false.
	 * @returns {boolean}
	 */
	canShipWithoutSession: function (){
		return false;
	},

	/**
	 * Returns the HTML for a "send shipment" button.
	 * @param {Object} record A visit object from Synchweb.
	 */
	getSendButton:function(record){
		if(SynchwebShippingHandler.MAIL_IN===record["VISIT"]){
			return '<input type="button" value="Unattended collection" onclick="SynchwebShippingHandler.chooseLabContact(this)" />';
		} else if(1===parseInt(record["ACTIVE"])){
			return '<input type="button" value="Use this visit" onclick="SynchwebShippingHandler.chooseLabContact(this)" />';
		}
		//Inactive visits - currently allow and let synchrotron barf if unhappy.
		return '<input type="button" value="Use this visit" onclick="SynchwebShippingHandler.chooseLabContact(this)" />';
	},

	sendShipment:function(){
		if(!Shipment.populatingPlaceholder){
			if(!confirm("Really submit shipment to "+window.shipmentDestination.name+"?")){ return false; }
		}
		ui.setModalBoxTitle("Submitting your shipment to "+window.shipmentDestination.name+"...");
		document.getElementById("modalBox").querySelector(".boxbody").innerHTML="";
		ui.logToDialog('Beginning to send shipment...');
		if(Shipment.populatingPlaceholder){
			SynchwebShippingHandler.getProposalNameForShipment();
		} else {
			SynchwebShippingHandler.getProteins();
		}
	},

	getProposalNameForShipment: function (){
		AjaxUtils.remoteAjax(
			SynchwebShippingHandler.apiBaseUri+"proposal/lookup?SHIPPINGID="+data["idatremotefacility"],
			'get',
			'',
			function (xhr){
				if(!xhr.responseJSON || !xhr.responseJSON["PROP"]){
					ui.logToDialog("Could not determine the proposal number for this shipment","error");
					return false;
				}
				ui.logToDialog("Proposal for this shipment is "+xhr.responseJSON["PROP"]);
				SynchwebShippingHandler.proposal={"PROPOSAL":xhr.responseJSON["PROP"]};
				SynchwebShippingHandler.getProteins();
			},
			function(){
				ui.logToDialog("Could not determine the proposal number for this shipment","error");
			},
			{ 'Authorization':'Bearer '+SynchwebShippingHandler.token }
		);

	},

	getProteins:function(){
		let proposal=SynchwebShippingHandler.proposal;
		AjaxUtils.remoteAjax(
			SynchwebShippingHandler.apiBaseUri+"sample/proteins?page=1&per_page=1000&all=1&prop="+proposal["PROPOSAL"],
			'get',
			'',
			SynchwebShippingHandler.doPreflight,
			SynchwebShippingHandler.getProteins_onFailure,
			{ 'Authorization':'Bearer '+SynchwebShippingHandler.token }
		);
	},
	getProteins_onFailure:function(transport){
		ui.setModalBoxTitle("Could not send shipment");
		document.getElementById("modalBox").querySelector(".boxbody").innerHTML='';
		if(!transport.responseJSON){
			ui.logToDialog('Could not get list of proteins from '+window.shipmentDestination.name+
				', so could not validate shipment before sending.<br/><br/>Please try again later.','error');
		} else if(transport.responseJSON.message){
			ui.logToDialog('Could not get list of proteins from '+window.shipmentDestination.name+
				', so could not validate shipment before sending.<br/>The remote server said:<br/><br/>'+
				transport.responseJSON.message+'<br/><br/>Please try again later.','error');
		} else if(0===parseInt(transport.responseJSON.total)){
			ui.logToDialog('Proposal  '+SynchwebShippingHandler.proposal["PROPOSAL"]+
				' has no proteins. Therefore, none of your samples have a protein acronym that appears in the proposal, '+
				'and the shipment cannot be sent.<br/><br/>Create the proteins in the '+window.shipmentDestination.name+' ISPyB system.','error');
		}
	},

	doPreflight:function(transport){
		if(!transport.responseJSON || 0===parseInt(transport.responseJSON.total) || 400<=transport.responseJSON.status){
			return SynchwebShippingHandler.getProteins_onFailure(transport);
		}
		let box=document.getElementById("modalBox").querySelector(".boxbody");
		SynchwebShippingHandler.proteins=transport.responseJSON.data;
		let acronyms=[];
		transport.responseJSON.data.forEach(function(protein){
			acronyms.push(protein["ACRONYM"]);
		});
		if(!Shipment.creatingPlaceholder){
			let errors=Shipment.getShipmentErrors(acronyms);
			if(0<errors.length){
				box.innerHTML="Could not send shipment. Please fix the following:<br/><ul><li>"+errors.join("</li><li>")+"</ul>";
				return false;
			}
			let dewars=document.querySelectorAll(".containertab");
			dewars.forEach(function(tab){
				let dewarName=tab.dataset.containername;
				let nameFormat=new RegExp('^DLS-MX-\d\d\d\d$');
				// if(!nameFormat.test(dewarName)){
				//      errors.push('Dewar name does not match Diamond barcode format DLS-MX-nnnn');
				// }
				tab.classList.add("noRemoteDewar");
			});
		}
		SynchwebShippingHandler.remoteErrors=[];
		if(Shipment.creatingPlaceholder){
			ui.logToDialog("Pre-flight validation complete. Creating placeholder shipment in ISPyB...");
			SynchwebShippingHandler.createShipment(true);
		} else if(Shipment.populatingPlaceholder){
			ui.logToDialog("Pre-flight validation complete. Adding containers and samples to placeholder shipment in ISPyB...");
			SynchwebShippingHandler.getDewarObjectsForShipment();
		} else { //Normal full shipment creation
			ui.logToDialog("Pre-flight validation complete. Creating full shipment in ISPyB...");
			SynchwebShippingHandler.createShipment(true);
		}
	},

	remoteErrors:[],
	logRemoteError:function(err){
		SynchwebShippingHandler.remoteErrors.push(err);
	},
	showRemoteErrors:function(){
		ui.logToDialog('The following errors occurred:<br/><br/>','error');
		ui.logToDialog(SynchwebShippingHandler.remoteErrors.join('<br/>'));
	},

	/**
	 * Creates a shipment at the synchrotron, with the empty dewars attached.
	 * Dewars that are not registered at the synchrotron are automatically created.
	 * We only receive a shipment ID in return, so the next stage retrieves all dewars for the shipment.
	 */
	createShipment:function(withEmptyDewars){
		ui.logToDialog("Submitting basic shipment details");
		let safetyLevel=SynchwebShippingHandler.safetyLevel;
		let proposal=SynchwebShippingHandler.proposal["PROPOSAL"];
		let visitId=SynchwebShippingHandler.session["SESSIONID"];
		let labContactId=SynchwebShippingHandler.labContact["LABCONTACTID"];
		let shipmentName=document.getElementById("shipmentdetails_form").querySelector("#name").value;

		let numBarcodes=0;
		let dewarBarcodes='""';
		if(withEmptyDewars){
			let dewars=document.querySelectorAll(".containertab");
			dewarBarcodes=[];
			dewars.forEach(function(tab){
				dewarBarcodes.push(tab.dataset.containername);
			});
			numBarcodes=dewarBarcodes.length;
			dewarBarcodes='["'+dewarBarcodes.join('","')+'"]';
		}

		let parameters='{"SHIPPINGNAME":"'+shipmentName+'","DEWARS":"'+numBarcodes+'","FCODES":'+dewarBarcodes;
		if(SynchwebShippingHandler.MAIL_IN===SynchwebShippingHandler.session["VISIT"]){
			parameters+=',"FIRSTEXPERIMENTID":null,"noexp":"on",';
		} else {
			parameters+=',"FIRSTEXPERIMENTID":"'+visitId+'","noexp":null,';
		}
		parameters+='"SAFETYLEVEL":"'+safetyLevel+'","COMMENTS":"Created by IceBear","SENDINGLABCONTACTID":"'+labContactId+'","RETURNLABCONTACTID":"'+labContactId+'",'+
			'"DELIVERYAGENT_SHIPPINGDATE":"","PHYSICALLOCATION":"","READYBYTIME":"","CLOSETIME":"","DELIVERYAGENT_DELIVERYDATE":"",'+
			'"ENCLOSEDHARDDRIVE": false,"ENCLOSEDTOOLS": false,"DYNAMIC": false,'+
			'"DELIVERYAGENT_AGENTNAME":"","DELIVERYAGENT_AGENTCODE":"","prop":"'+proposal+'"'+
			'}';
		AjaxUtils.remoteAjax(
			SynchwebShippingHandler.apiBaseUri+"shipment/shipments?prop="+proposal,
			'post',
			parameters,
			SynchwebShippingHandler.createShipment_onSuccess,
			SynchwebShippingHandler.createShipment_onFailure,
			{ 'Authorization':'Bearer '+SynchwebShippingHandler.token }
		);
	},
	createShipment_onSuccess:function(transport){
		if(transport.responseJSON && transport.responseJSON["SHIPPINGID"]){
			SynchwebShippingHandler.shipmentIdAtFacility=transport.responseJSON["SHIPPINGID"];
			if(Shipment.creatingPlaceholder){
				window.setTimeout(SynchwebShippingHandler.completeShipment, 50);
			} else {
				window.setTimeout(SynchwebShippingHandler.getDewarObjectsForShipment, 50);
			}
		} else {
			SynchwebShippingHandler.logRemoteError("No shipment ID returned from shipment create attempt.");
			return SynchwebShippingHandler.createShipment_onFailure(transport);
		}
	},
	createShipment_onFailure:function(){
		SynchwebShippingHandler.logRemoteError("Could not create shipment.");
		SynchwebShippingHandler.showRemoteErrors();
	},



	/**
	 * Retrieves dewar objects for this shipment. On success, we attempt to populate them with pucks.
	 * We created the shipment above, along with empty dewars (which may or mat not have been registered at the synchrotron beforehand). However,
	 * we only got a shipment ID in return and have no idea what the dewar IDs are. We need those for the next step. So here , we retrieve all
	 * dewar objects for this shipment, and we attach them to the relevant tab in our shipping UI.
	 */
	getDewarObjectsForShipment:function(){
		ui.logToDialog("Getting list of dewars in remote shipment...");
		let shipmentId=SynchwebShippingHandler.shipmentIdAtFacility;
		if(!shipmentId){ shipmentId=data["idatremotefacility"]; }
		let proposalName=SynchwebShippingHandler.proposal["PROPOSAL"];
		AjaxUtils.remoteAjax(
			SynchwebShippingHandler.apiBaseUri+"shipment/dewars/sid/"+shipmentId+"?prop="+proposalName,
			'get',
			{},
			SynchwebShippingHandler.getDewarObjectsForShipment_onSuccess,
			SynchwebShippingHandler.getDewarObjectsForShipment_onFailure,
			{ 'Authorization':'Bearer '+SynchwebShippingHandler.token }
		);
	},

	getDewarObjectsForShipment_onSuccess:function(transport){
		if(!transport.responseJSON){
			return SynchwebShippingHandler.getDewarObjectsForShipment_onFailure(transport);
		}
		if(!transport.responseJSON.total || !transport.responseJSON.data){
			if(Shipment.populatingPlaceholder){
				ui.logToDialog("...none found. This is expected for a placeholder shipment.");
			} else {
				return SynchwebShippingHandler.getDewarObjectsForShipment_onFailure(transport);
			}
		} else {
			ui.logToDialog("...succeeded.");
			transport.responseJSON.data.forEach(function(remoteDewar){
				if(remoteDewar["FACILITYCODE"]){
					let dewarTab=document.getElementById(remoteDewar["FACILITYCODE"]);
					if(!dewarTab){
						SynchwebShippingHandler.logRemoteError("Barcode mismatch - could not find local dewar tab for remote dewar "+remoteDewar["FACILITYCODE"]+".");
						return;
					}
					dewarTab.remoteDewar=remoteDewar;
					dewarTab.dataset.remoteid=remoteDewar["DEWARID"];
					dewarTab.classList.remove("noRemoteDewar");
				}
			});
		}
		if(0!==SynchwebShippingHandler.remoteErrors.length){
			return SynchwebShippingHandler.showRemoteErrors();
		}
		let unFoundDewars=document.querySelectorAll(".noRemoteDewar");
		if(0!==unFoundDewars.length){
			if(Shipment.populatingPlaceholder){
				let dewarBarcodes=[];
				unFoundDewars.forEach(function (tab){
					dewarBarcodes.push(tab.dataset.containername);
				});
				window.setTimeout(SynchwebShippingHandler.createDewarsInPlaceholderShipment, 50, dewarBarcodes);
				return;
			} else {
				SynchwebShippingHandler.logRemoteError("Not all dewars in local shipment are in the remote shipment.");
			}
		}
		if(0!==SynchwebShippingHandler.remoteErrors.length){
			return SynchwebShippingHandler.showRemoteErrors();
		}
		SynchwebShippingHandler.addPucksToDewars();
	},

	getDewarObjectsForShipment_onFailure:function(){
		SynchwebShippingHandler.logRemoteError("Could not get list of dewars in shipment, or list was empty.");
		SynchwebShippingHandler.showRemoteErrors();
	},

	createDewarsInPlaceholderShipment:function (dewarBarcodes){
		if(!dewarBarcodes.length){
			ui.logToDialog("All dewars created.");
			if(Shipment.creatingPlaceholder){
				ui.logToDialog("Placeholder shipment and dewars created","success");
				ui.logToDialog("Reloading the page, please wait a moment...");
				document.location.reload();
				return;
			}
			window.setTimeout(SynchwebShippingHandler.addPucksToDewars, 50);
			return;
		}
		let proposalName=SynchwebShippingHandler.proposal["PROPOSAL"];
		let dewarBarcode=dewarBarcodes.pop();
		ui.logToDialog("Creating dewar "+dewarBarcode+"...");
		AjaxUtils.remoteAjax(
			SynchwebShippingHandler.apiBaseUri+"shipment/dewars?prop="+proposalName,
			'post',
			{
				// "prop":proposalName,
				// "CODE":dewarBarcode,
				// "FACILITYCODE":dewarBarcode,
				// "SHIPPINGID":data["idatremotefacility"],
				// "FIRSTEXPERIMENTID":"",
				// "new":"true"
				"rawPostBody":'{"prop":"'+proposalName+'", "CODE":"'+dewarBarcode+'", "FACILITYCODE":"'+dewarBarcode+'", "SHIPPINGID":"'+data["idatremotefacility"]+'"}'
			},
			function (xhr){
				if(xhr.responseJSON.status && xhr.responseJSON.message){
					SynchwebShippingHandler.logRemoteError("ISPyB reported: "+xhr.responseJSON.message+" ("+xhr.responseJSON.status+")");
					return SynchwebShippingHandler.showRemoteErrors();
				}
				document.getElementById(dewarBarcode).dataset.remoteid=xhr.responseJSON["DEWARID"];
				ui.logToDialog("...dewar "+dewarBarcode+" created.");
				window.setTimeout(SynchwebShippingHandler.createDewarsInPlaceholderShipment, 50, dewarBarcodes);
			},
			function (xhr){
				SynchwebShippingHandler.logRemoteError("Could not create dewar "+dewarBarcode+".");
				SynchwebShippingHandler.showRemoteErrors();
			},
			{
				'Referer':SynchwebShippingHandler.apiBaseUri.replace("/api","")+"shipment/"+data["idatremotefacility"],
				'Authorization':'Bearer '+SynchwebShippingHandler.token
			}
		);
	},

	/**
	 * Populate all dewars with their child pucks, at the synchrotron.
	 */
	addPucksToDewars:function(){
		ui.logToDialog("Adding pucks to dewars...");
		document.querySelectorAll(".containertab").forEach(function(localDewarTab){
			SynchwebShippingHandler.addPucksToDewar(localDewarTab);
		});
	},

	/**
	 * Populate a dewar with its child pucks, at the synchrotron.
	 */
	addPucksToDewar:function(localDewarTab){
		ui.logToDialog("Adding pucks to dewar "+localDewarTab.id+"...");
		let tabBody=localDewarTab.nextElementSibling;
		let pucks=tabBody.querySelectorAll(".treeitem");
		pucks.forEach(function(p){
			let localPuck=p.record;
			p.id=p.record.name;
			SynchwebShippingHandler.addPuckToDewar(localPuck, localDewarTab);
		});
	},

	/**
	 * Add a puck to a dewar within the shipment, at the synchrotron.
	 */
	addPuckToDewar:function(localPuck, localDewarTab){
		ui.logToDialog("Adding puck "+localPuck.name+" to "+localDewarTab.id+"...");
		let remoteDewarId=localDewarTab.dataset.remoteid;
		let proposalName=SynchwebShippingHandler.proposal["PROPOSAL"];
		AjaxUtils.remoteAjax(
			SynchwebShippingHandler.apiBaseUri+"shipment/containers?prop="+proposalName,
			'post',
			'{ "NAME":"'+localPuck.name+'", "CONTAINERTYPE":"Puck", "DEWARID":"'+remoteDewarId+'","CAPACITY":"'+localPuck.positions+'","prop":"'+proposalName+'"  }',
			function(transport){ SynchwebShippingHandler.addPuckToDewar_onSuccess(transport, localPuck) },
			function(transport){ SynchwebShippingHandler.addPuckToDewar_onFailure(transport, localPuck) },
			{ 'Authorization':'Bearer '+SynchwebShippingHandler.token }
		);
	},
	addPuckToDewar_onSuccess:function(transport, localPuck){
		if(transport.responseJSON["msg"] && -1!==transport.responseJSON["msg"].indexOf("Duplicate entry")){
			ui.logToDialog("Puck "+localPuck.name+" already exists. Trying another way...");
			SynchwebShippingHandler.findExistingPuck(localPuck);
			return;
		}
		localPuck.remoteId=transport.responseJSON["CONTAINERID"];
		ui.logToDialog("...added "+localPuck.name+" to dewar.");
		SynchwebShippingHandler.addSamplesToPuck(localPuck);
	},
	addPuckToDewar_onFailure:function(){
		SynchwebShippingHandler.logRemoteError("Could not add puck to dewar.");
		SynchwebShippingHandler.showRemoteErrors();
	},

	/**
	 * If addPuckToDewar reported "duplicate entry" so puck exists. Get it, and continue.
	 */
	findExistingPuck:function(localPuck){
		ui.logToDialog("Finding existing puck "+localPuck.name+"...");
		let proposalName=SynchwebShippingHandler.proposal["PROPOSAL"];
		AjaxUtils.remoteAjax(
			SynchwebShippingHandler.apiBaseUri+"shipment/containers?per_page=100000&page=1&prop="+proposalName,
			'get',
			'{}',
			function(transport){ SynchwebShippingHandler.findExistingPuck_onSuccess(transport, localPuck) },
			function(transport){ SynchwebShippingHandler.findExistingPuck_onFailure(transport, localPuck) },
			{ 'Authorization':'Bearer '+SynchwebShippingHandler.token }
		);
	},
	findExistingPuck_onSuccess:function(transport, localPuck){
		let puckBarcode=localPuck.name;
		let remotePuckId=null;
		transport.responseJSON.data.forEach(function(container){
			if(container["NAME"]===puckBarcode){
				remotePuckId=container["CONTAINERID"];
			}
		});
		if(!remotePuckId){
			SynchwebShippingHandler.logRemoteError("Could not retrieve list of existing containers.");
			SynchwebShippingHandler.showRemoteErrors();
		}
		localPuck.remoteId=remotePuckId;
		let localDewarTab=ui.previousSibling(document.getElementById(localPuck.name).closest(".tabbody"));
		SynchwebShippingHandler.addExistingPuckToDewar(localPuck, localDewarTab);
	},
	findExistingPuck_onFailure:function(){
		SynchwebShippingHandler.logRemoteError("Could not retrieve list of existing containers.");
		SynchwebShippingHandler.showRemoteErrors();
	},


	/**
	 * Add an existing puck to a dewar within the shipment, at the synchrotron.
	 */
	addExistingPuckToDewar:function(localPuck, localDewarTab){
		ui.logToDialog("Adding existing puck "+localPuck.name+" to "+localDewarTab.id+"...");
		let remoteDewarId=localDewarTab.dataset.remoteid;
		let proposalName=SynchwebShippingHandler.proposal["PROPOSAL"];
		AjaxUtils.remoteAjax(
			SynchwebShippingHandler.apiBaseUri+"shipment/containers/move?cid="+localPuck.remoteId+"&did="+remoteDewarId+"&prop="+proposalName,
			'get', //yes, really
			'{   }',
			function(transport){ SynchwebShippingHandler.addExistingPuckToDewar_onSuccess(transport, localPuck) },
			function(transport){ SynchwebShippingHandler.addExistingPuckToDewar_onFailure(transport, localPuck) },
			{ 'Authorization':'Bearer '+SynchwebShippingHandler.token }
		);
	},
	addExistingPuckToDewar_onSuccess:function(transport, localPuck){
		ui.logToDialog("...added "+localPuck.name+" to dewar.");
		SynchwebShippingHandler.addSamplesToPuck(localPuck);
	},
	addExistingPuckToDewar_onFailure:function(){
		SynchwebShippingHandler.logRemoteError("Could not add puck to dewar.");
		SynchwebShippingHandler.showRemoteErrors();
	},


	addSamplesToPuck:function(localPuck){
		let puckElement=document.getElementById(localPuck.name);
		let positions=puckElement.querySelectorAll("tr.datarow");
		positions.forEach(function(tr){
			if(!tr.rowData || 1===parseInt(tr.rowData.isEmpty)){ return; }
			tr.classList.add("unsavedPin");
		});
		positions.forEach(function(tr){
			if(!tr.rowData || (tr.rowData.isEmpty && 1===parseInt(tr.rowData.isEmpty))){ return; }
			SynchwebShippingHandler.addSampleToPuck(localPuck, tr);
		});
	},
	addSampleToPuck:function(localPuck, pinTr){
		let proposalName=SynchwebShippingHandler.proposal["PROPOSAL"];
		let pin=pinTr.rowData;
		let position=pin.position;
		let pinBarcode=pin.name;
		if(0===pinBarcode.indexOf("dummypin")){
			pinBarcode="";
		}
		let crystal=pin.childitems[1]; //0 is dummy
		let proteinId=null;
		SynchwebShippingHandler.proteins.forEach(function(protein){
			if(protein["ACRONYM"]===crystal.proteinacronym){
				proteinId=protein["PROTEINID"];
			}
		});
		if(null==proteinId){
			SynchwebShippingHandler.logRemoteError("Could not find protein ID for crystal.");
			SynchwebShippingHandler.showRemoteErrors();
			return false;
		}
		let sampleName=crystal.name.replace(" ","_"); //Don't send sample names with spaces
		let experimentType=crystal["diffractiontype"];
		if(!experimentType){ experimentType=""; }
		let spaceGroup=crystal.spacegroup;
		if(!spaceGroup){ spaceGroup=""; }
		let shippingComment=crystal.shippingcomment.split('"').join('');
		ui.logToDialog("Adding pin with barcode ["+pinBarcode+"] to puck "+localPuck.name+", position "+position+"...");

		AjaxUtils.remoteAjax(
			SynchwebShippingHandler.apiBaseUri+"sample",
			'post',
			' [{"LOCATION":"'+position+'","PROTEINID":"'+proteinId+'","CRYSTALID":-1,"new":true,"NAME":"'+sampleName+'","CODE":"'+pinBarcode+'", '+
			' "COMMENTS":"'+shippingComment+'","SPACEGROUP":"'+spaceGroup+'","REQUIREDRESOLUTION":"","ANOMALOUSSCATTERER":"","CELL_A":"'+crystal.unitcella+'","CELL_B":"'+crystal.unitcellb+'", '+
			' "CELL_C":"'+crystal.unitcellc+'","CELL_ALPHA":"'+crystal.unitcellalpha+'","CELL_BETA":"'+crystal.unitcellbeta+'","CELL_GAMMA":"'+crystal.unitcellgamma+'","VOLUME":"","ABUNDANCE":"","SYMBOL":null, '+
			' "PACKINGFRACTION":"","EXPERIMENTALDENSITY":"","COMPOSITION":"","LOOPTYPE":"","DIMENSION1":"","DIMENSION2":"", '+
			' "DIMENSION3":"","SHAPE":"","components":[],"EXPERIMENTKINDNAME":"","isSelected":false,"STATUS":"", '+
			' "CENTRINGMETHOD":"","EXPERIMENTKIND":"'+experimentType+'","CONTAINERID":'+localPuck.remoteId+',"COMPONENTID":"","ANOM_NO":"","prop":"'+proposalName+'" '+
			' }]',
			function(transport){ SynchwebShippingHandler.addSampleToPuck_onSuccess(transport, pinTr) },
			function(transport){ SynchwebShippingHandler.addSampleToPuck_onFailure(transport, pinTr) },
			{ 'Authorization':'Bearer '+SynchwebShippingHandler.token }
		);
	},
	addSampleToPuck_onSuccess:function(transport,pinTr){
		/*
         [{"prop":"mx4025","PROTEINID":"42242","CONTAINERID":138441,"LOCATION":"1","CODE":"HA00AV9785","NAME"
                :"LMTIM_9098A01d1c1","COMMENTS":"","SPACEGROUP":"P1","BLSAMPLEID":2185329}]
         */
		let remoteCrystalId=transport.responseJSON[0]["BLSAMPLEID"];
		let diffractionRequestId=pinTr.rowData.childitems[1]["diffractionrequestid"];
		SynchwebShippingHandler.saveRemoteCrystalId(diffractionRequestId, remoteCrystalId, pinTr);
		SynchwebShippingHandler.addShippedNote(pinTr.rowData.childitems[1]);
	},
	addSampleToPuck_onFailure:function(transport,pinTr){
		ui.logToDialog("<strong>Adding sample "+pinTr.name+"to puck failed.</strong>","error");
	},

	addShippedNote:function(localCrystal){
		//TODO Move this to server-side, shipment::update - eliminate duplication in future handlers
		let noteText='Crystal shipped to '+window.shipmentDestination.name;
		new AjaxUtils.Request('/api/note',{
			method:'post',
			parameters:{
				csrfToken:csrfToken,
				parentid:localCrystal.id,
				text:noteText
			},
			onSuccess:function(transport){ if(successCallback){ successCallback(transport.responseJSON); } },
			onFailure:AjaxUtils.checkResponse
		});
	},

	saveRemoteCrystalId:function(diffractionRequestId, remoteCrystalId, pinTr){
		new AjaxUtils.Request('/api/diffractionrequest/'+diffractionRequestId,{
			'method':'patch',
			'parameters':{
				'csrfToken':csrfToken,
				'shipmentid':data.id,
				'crystalidatremotefacility':remoteCrystalId,
				'crystalurlatremotefacility':SynchwebShippingHandler.getCrystalUrlAtFacility(window.shipmentDestination, remoteCrystalId)
			},
			onSuccess:function(){
				pinTr.classList.remove("unsavedPin");
				let unsavedPins=document.querySelectorAll(".unsavedPin");
				if(!unsavedPins || 0===unsavedPins.length){
					window.setTimeout(SynchwebShippingHandler.completeShipment, 100);
				}
			},
			onFailure:function(){
				pinTr.classList.remove("unsavedPin");
				let unsavedPins=document.querySelectorAll(".unsavedPin");
				if(!unsavedPins || 0===unsavedPins.length){
					window.setTimeout(SynchwebShippingHandler.completeShipment, 100);
				}
			},
		});

	},

	completeShipment:function(){
		if(Shipment.creatingPlaceholder) {
			ui.logToDialog("Placeholder shipment was created at "+window.shipmentDestination.name+".","success");
			ui.logToDialog("Updating IceBear...");
		} else {
			ui.logToDialog("Shipment was submitted to "+window.shipmentDestination.name+".","success");
			ui.logToDialog("Updating IceBear and creating shipment manifest...");
		}
		let parameters={
			'csrfToken':csrfToken,
			'idatremotefacility':SynchwebShippingHandler.shipmentIdAtFacility,
			'urlatremotefacility':SynchwebShippingHandler.getShipmentUrlAtFacility(window.shipmentDestination, SynchwebShippingHandler.shipmentIdAtFacility)
		};
		//proposalname and sessionname will only be available on full send or on creating placeholder - not on populating placeholder.
		if(SynchwebShippingHandler.proposal && SynchwebShippingHandler.proposal["PROPOSAL"]){
			parameters["proposalname"]=SynchwebShippingHandler.proposal["PROPOSAL"];
		}
		if(SynchwebShippingHandler.session && SynchwebShippingHandler.session["VISIT"]){
			parameters["sessionname"]=SynchwebShippingHandler.session["VISIT"];
		}
		if(!Shipment.creatingPlaceholder){
			parameters["dateshipped"]=new Date().toISOString().split("T")[0];
		}
		new AjaxUtils.Request('/api/shipment/'+data.id,{
			'method':'patch',
			'parameters':parameters,
			onSuccess:SynchwebShippingHandler.completeShipment_onSuccess,
			onFailure:SynchwebShippingHandler.completeShipment_onFailure,
		});
	},
	completeShipment_onSuccess:function(){
		ui.logToDialog("IceBear updated successfully.","success");
		ui.logToDialog("The page will reload in a moment. Please wait...");
		window.setTimeout(ui.forceReload, 2500, true);
	},
	completeShipment_onFailure:function(transport){
		ui.logToDialog("The shipment submission to "+window.shipmentDestination.name+" succeeded, but IceBear was not updated ","error");
		if(transport.responseJSON && transport.responseJSON.error){
			ui.logToDialog(transport.responseJSON.error, "error");
		}
		ui.logToDialog("The page will reload in a moment. Please wait...");
		window.setTimeout(ui.forceReload, 5000, true);
	},

	DatasetRetrieval: {

		begin: function(){
			ui.modalBox({
				"title":"Dataset retrieval",
				content:"...",
				onclose:Shipment.Results.renderTab
			});
			let apiBaseUri=window.shipmentDestination["apibaseuri"];
			if(!apiBaseUri){
				alert("No shipping API base URI set for "+window.shipmentDestination.name+".");
				return false;
			}
			SynchwebShippingHandler.apiBaseUri=apiBaseUri;
			SynchwebShippingHandler.DatasetRetrieval.checkCanSeeProposal();
		},

		checkCanSeeProposal:function (){
			let mb=document.getElementById("modalBox").querySelector(".boxbody");
			mb.innerHTML="";
			if(!SynchwebShippingHandler.token){
				return SynchwebShippingHandler.showLoginForm(SynchwebShippingHandler.DatasetRetrieval.checkCanSeeProposal);
			}
			ui.setModalBoxTitle("Dataset retrieval");
			ui.logToDialog("Checking that you are on the proposal...");
			let proposal=data["proposalname"];
			AjaxUtils.remoteAjax(
				SynchwebShippingHandler.apiBaseUri+"proposal/"+proposal+"?prop="+proposal,
				"get",
				"",
				SynchwebShippingHandler.DatasetRetrieval.checkCanSeeProposal_onSuccess,
				SynchwebShippingHandler.DatasetRetrieval.checkCanSeeProposal_onFailure,
				{ 'Authorization':'Bearer '+SynchwebShippingHandler.token }
			);
		},
		checkCanSeeProposal_onSuccess:function (transport){
			if(400<=1*transport.responseJSON["status"]){
				//because why would we return HTTP 400 when we can return 200 OK and put 400 in the body?
				return SynchwebShippingHandler.DatasetRetrieval.checkCanSeeProposal_onFailure(transport);
			}
			SynchwebShippingHandler.DatasetRetrieval.getDatasetsForShipment();
		},
		checkCanSeeProposal_onFailure:function (transport){
			ui.logToDialog("Could not get details of proposal ("+data["proposalname"]+").","error");
			if(transport.responseJSON["message"]){
				ui.logToDialog("The synchrotron said: "+transport.responseJSON["message"]);
				ui.logToDialog("Check whether you are on this proposal.","error");
			} else if(transport.responseJSON["error"]){
				ui.logToDialog("The error message was: "+transport.responseJSON["error"]);
			}
		},

		getDatasetsForShipment:function(){
			SynchwebShippingHandler.DatasetRetrieval.remoteCrystalIds=Shipment.DatasetRetrieval.getRemoteCrystalIdsFromManifest(data["manifest"]);
			SynchwebShippingHandler.DatasetRetrieval.processedRemoteCrystalIds=[];
			Shipment.DatasetRetrieval.datasets=[];

			SynchwebShippingHandler.DatasetRetrieval.remoteCrystalIds.forEach(function (remoteCrystalId){
				let test=false;
				if(test){
					remoteCrystalId=3590625;
					data["proposalname"]="mx26794";
				}

				ui.logToDialog("Getting datasets for crystal with remote ID "+remoteCrystalId+"...");
				AjaxUtils.remoteAjax(
					SynchwebShippingHandler.apiBaseUri+"dc?page=1&per_page=100&total_pages=1&total_entries=100&sid="+remoteCrystalId+"&pp=5&prop="+data["proposalname"],
					"get",
					"",
					function (transport){
						SynchwebShippingHandler.DatasetRetrieval.processedRemoteCrystalIds.push(remoteCrystalId);
						let response=transport.responseJSON;
						if(!response){
							ui.logToDialog("Error getting datasets for crystal with remote ID "+remoteCrystalId+" (Bad response, not JSON)", "error");
						} else if(2!==transport.responseJSON.length || 0===transport.responseJSON[1].length){
							ui.logToDialog("No datasets for crystal with remote ID "+remoteCrystalId);
						} else {
							let datasets=transport.responseJSON[1];
							datasets.forEach(function(remoteDataset){
								if("data"===remoteDataset["TYPE"]){
									if(test){
										remoteDataset["BLSAMPLEID"]=3335810;
									}
									Shipment.DatasetRetrieval.datasets.push(remoteDataset);
								}
							});
						}
					},
					function (transport){
						SynchwebShippingHandler.DatasetRetrieval.processedRemoteCrystalIds.push(remoteCrystalId);
						if(404===transport.status){
							ui.logToDialog("No datasets for crystal with remote ID "+remoteCrystalId);
						} else {
							ui.logToDialog("Error getting datasets for crystal with remote ID "+remoteCrystalId+" (HTTP status "+transport.status+")");
						}
					},
					{ 'Authorization':'Bearer '+SynchwebShippingHandler.token }
				)
			});
			SynchwebShippingHandler.DatasetRetrieval.waitForDatasetsFromAllCrystals();
		},

		remoteCrystalIds:[],
		processedRemoteCrystalIds:[],
		waitForDatasetsFromAllCrystals:function (){
			let diff=SynchwebShippingHandler.DatasetRetrieval.remoteCrystalIds.length-SynchwebShippingHandler.DatasetRetrieval.processedRemoteCrystalIds.length;
			if(0!==diff){
				window.setTimeout(SynchwebShippingHandler.DatasetRetrieval.waitForDatasetsFromAllCrystals, 1000);
				if(1===diff){
					ui.logToDialog("Waiting for datasets for 1 crystal");
				} else {
					ui.logToDialog("Waiting for datasets for "+diff+" crystals");
				}
				return;
			}
			ui.logToDialog("Retrieved datasets for all crystals. Found "+Shipment.DatasetRetrieval.datasets.length+" datasets.");
			if(0===Shipment.DatasetRetrieval.datasets.length){
				ui.logToDialog("Found no datasets for any crystal in this shipment.", "error");
				return;
			}
			ui.logToDialog("Beginning to process the datasets...");
			window.setTimeout(function () {
				document.querySelector("#modalBox .boxbody").innerHTML="";
				Shipment.DatasetRetrieval.renderProgressNumbers();
				SynchwebShippingHandler.DatasetRetrieval.gatherBeamlineDetails();
				SynchwebShippingHandler.DatasetRetrieval.processFoundDatasets();
			}, 500);
		},

		processFoundDatasets:function (){
			if(!Shipment.DatasetRetrieval.datasets || !Shipment.DatasetRetrieval.datasets.length){
				ui.logToDialog("No datasets found for this shipment.");
				return false;
			}
			Shipment.DatasetRetrieval.datasets.forEach(function (remoteDataset){
				if("datacollection successful"!==remoteDataset["RUNSTATUS"].toLowerCase()){
					Shipment.DatasetRetrieval.incrementFailedCount('Dataset run status is not "DataCollection Successful"');
				} else if(!remoteDataset["BLSAMPLEID"]){
					Shipment.DatasetRetrieval.incrementFailedCount("Dataset does not contain a remote crystal ID (BLSAMPLEID)");
				} else {
					new AjaxUtils.Request("/api/diffractionrequest/shipmentid/"+data["id"]+"/crystalidatremotefacility/"+remoteDataset["BLSAMPLEID"],{
						method:"get",
						onSuccess:function(transport){
							if(1===transport.responseJSON["rows"].length){
								SynchwebShippingHandler.DatasetRetrieval.processFoundDataset(remoteDataset, transport.responseJSON["rows"][0]);
							} else {
								Shipment.DatasetRetrieval.incrementFailedCount("More than one IceBear crystal in this shipment with remote ID "+remoteDataset["BLSAMPLEID"]);
							}
						}, onFailure:function (transport){
							if(404===transport.status){
								Shipment.DatasetRetrieval.incrementFailedCount("No IceBear crystal in this shipment with remote ID "+remoteDataset["BLSAMPLEID"]);
							} else {
								Shipment.DatasetRetrieval.incrementFailedCount("Error getting IceBear crystal(s) with remote ID "+remoteDataset["BLSAMPLEID"]+" (HTTP "+transport.status+")");
							}
						}
					});
				}
			});
		},

		processFoundDataset:function (remoteDataset, diffractionRequest){
			let remoteDatasetId=remoteDataset["ID"];
			if(!remoteDatasetId){
				Shipment.DatasetRetrieval.incrementFailedCount("Dataset has no ID");
				return;
			}
			new AjaxUtils.Request('/api/dataset/diffractionrequestid/'+diffractionRequest["id"]+'/remotedatasetid/'+remoteDatasetId, {
				method: 'get',
				onSuccess:function (transport){
					if(transport.responseJSON.rows && 1===transport.responseJSON.rows.length){
						let localDataset=transport.responseJSON.rows[0];
						SynchwebShippingHandler.DatasetRetrieval.createOrUpdateLocalDataset(diffractionRequest, remoteDataset, localDataset);
					} else {
						Shipment.DatasetRetrieval.incrementFailedCount("Got more than one local dataset, should not happen");
					}
				},
				onFailure:function (transport){
					if(404===transport.status){
						SynchwebShippingHandler.DatasetRetrieval.createOrUpdateLocalDataset(diffractionRequest, remoteDataset, null);
					} else {
						Shipment.DatasetRetrieval.incrementFailedCount("Error on checking for existing dataset, HTTP "+transport.status);
					}
				},
			});
		},

		createOrUpdateLocalDataset:function (diffractionRequest, remoteDataset, localDataset){
			let beamlineName=remoteDataset["BL"];
			if(!beamlineName){
				Shipment.DatasetRetrieval.incrementFailedCount("No beamline name for this dataset - undefined");
				return false;
			}
			if(undefined===Shipment.DatasetRetrieval.beamlineNameToId[beamlineName]){
				//Need the beamline ID
				ui.logToDialog("Waiting for beamline ID");
				window.setTimeout(function (){
					SynchwebShippingHandler.DatasetRetrieval.createOrUpdateLocalDataset(diffractionRequest, remoteDataset,localDataset);
				},500);
				return false;
			}
			let beamlineId=Shipment.DatasetRetrieval.beamlineNameToId[beamlineName];
			let method="post";
			let uri="/api/dataset/";
			if(localDataset){
				method="patch";
				uri+=localDataset["id"];
			}
			let detectorManufacturer=remoteDataset["DETECTORMANUFACTURER"];
			let detectorModel=remoteDataset["DETECTORMODEL"];
			let detectorMode=remoteDataset["DETECTORMODE"];
			let detectorType=remoteDataset["DETECTORTYPE"];
			//As of March 2021, Diamond returns null, "" or "1", which are useless, and doesn't provide detector type at all.
			if(!detectorManufacturer || 2>detectorManufacturer.length){ detectorManufacturer=""; }
			if(!detectorModel || 2>detectorModel.length){ detectorModel=""; }
			if(!detectorMode || 2>detectorMode.length){ detectorMode=""; }
			if(!detectorType || 2>detectorType.length){ detectorType=""; }
			new AjaxUtils.Request(uri, {
				method: method,
				parameters: {
					"csrfToken": csrfToken,
					"diffractionrequestid": diffractionRequest["id"],
					"crystalid": diffractionRequest["crystalid"],
					"beamlineid": beamlineId,
					"remotedatasetid": remoteDataset["ID"],
					"collecteddatetime": remoteDataset["STA"],
					"remotedatasetobject": JSON.stringify(remoteDataset),
					"datalocation": window.shipmentDestination.name + ": " + remoteDataset["DIRFULL"],
					"description": "Run "+remoteDataset["RUN"]+". "+remoteDataset["COMMENTS"],
					"detectormanufacturer": detectorManufacturer,
					"detectormodel": detectorModel,
					"detectortype": detectorType,
					"detectormode": detectorMode,
					"wavelength":remoteDataset["WAVELENGTH"]
				},
				onSuccess: function () {
					Shipment.DatasetRetrieval.incrementSucceededCount("Created/updated IceBear dataset OK");
					SynchwebShippingHandler.DatasetRetrieval.getAutoProcessingResultsForDataset(remoteDataset, localDataset);
				},
				onFailure: function () {
					Shipment.DatasetRetrieval.incrementFailedCount("Failed creating/updating IceBear dataset");
				}
			});
		},

		getAutoProcessingResultsForDataset:function(remoteDataset, localDataset) {
			let proposalName=data.proposalname;
			AjaxUtils.remoteAjax(SynchwebShippingHandler.apiBaseUri+"processing/"+remoteDataset["ID"]+"?prop="+proposalName,
				'get',
				{},
				function (transport){
					if(!transport.responseJSON){
						return ui.logToDialog("Bad (non-JSON) response getting autoprocessing data for dataset (local ID "+localDataset["id"]+", remote ID "+localDataset["remotedatasetid"]+")", "error");
					}
					if(0===1*transport.responseJSON[0]){
						return ui.logToDialog("No autoprocessing data for dataset (local ID "+localDataset["id"]+", remote ID "+localDataset["remotedatasetid"]+")");
					}
					SynchwebShippingHandler.DatasetRetrieval.saveAutoProcessingResultsForDataset(localDataset, transport.responseJSON[1]);
				},
				function (transport){
					if(404===transport.status){
						ui.logToDialog("No autoprocessing data for dataset (local ID "+localDataset["id"]+", remote ID "+localDataset["remotedatasetid"]+")");
					} else {
						ui.logToDialog("HTTP "+transport.status+"error getting autoprocessing data for dataset (local ID "+localDataset["id"]+", remote ID "+localDataset["remotedatasetid"]+")", "error");
					}
				},
				{ 'Authorization':'Bearer '+SynchwebShippingHandler.token }
			);
		},

		saveAutoProcessingResultsForDataset:function(localDataset, autoProcessingResults){
			Object.keys(autoProcessingResults).forEach(function(resultId){
				let result=autoProcessingResults[resultId];
				if(1!==1*result["PROCESSINGSTATUS"]){
					return;
				}
				let parameters={
					"anomalousPhasing": 0,
					"spaceGroup": result["SG"].replaceAll(" ","")
				};
				let iceBearResult={
					"projectid":localDataset['projectid'],
					"datasetid":localDataset["id"],
					"remoteid":result["AID"], //Remote ID of the autoprocessing result
					"isanomalous":0,
					"spacegroup":result["SG"].replaceAll(" ",""),
					"rmerge":result["SHELLS"]["outerShell"]["RMERGE"],
					"ioversigma":result["SHELLS"]["outerShell"]["ISIGI"],
					"cchalf":result["SHELLS"]["outerShell"]["CCHALF"],
					"pipelinename":result["TYPE"],
					"remoteobject":JSON.stringify(result)
				}
				Object.keys(result["CELL"]).forEach(function (dimension){
					let iceBearName=SynchwebShippingHandler.DatasetRetrieval.dataProcessingParameterNames[dimension];
					parameters[iceBearName]=result["CELL"][dimension];
				});
				Object.keys(result["SHELLS"]).forEach(function (shell){
					let shellParameters=result["SHELLS"][shell];
					let shellName=shell.replaceAll("Shell",""); //innerShell -> inner
					if(null!==shellParameters["ANOMALOUS"]){
						parameters["anomalousPhasing"]=1;
						parameters["anomalousPhasing_"+shellName]=1;
						iceBearResult["isanomalous"]=1;
						iceBearResult["pipelinename"]=result["TYPE"]+"_anomalous";
					}
					["ANOMCOMPLETENESS","RHIGH","RLOW","ISIGI","ANOMMULTIPLICITY","CCHALF","COMPLETENESS",
						"MULTIPLICITY","RMERGE","NTOBS","NUOBS"].forEach(function(key){
						let iceBearName=SynchwebShippingHandler.DatasetRetrieval.dataProcessingParameterNames[key];
						parameters[iceBearName+"_"+shellName]=shellParameters[key];
						if("outer"===shellName){
							if("COMPLETENESS"===key){
								iceBearResult["completeness"]=shellParameters[key];
							} else if("RHIGH"===key){
								iceBearResult["bestresolution"]=shellParameters[key];
							}
						}
					});
				});
				iceBearResult["csrfToken"]=csrfToken;
				iceBearResult["parameters"]=JSON.stringify(parameters);
				AjaxUtils.request("/api/autoprocessingresult",{
					method:"post",
					parameters:iceBearResult,
					onSuccess:function (transport){},
					onFailure:function (transport){}
				});
			});
		},

		//ISPyB:IceBear
		dataProcessingParameterNames:{
			"TYPE":"programs",
			"PROCESSINGSTATUS":"processingStatus",

			"CELL_A":"cellA",
			"CELL_B":"cellB",
			"CELL_C":"cellC",
			"CELL_AL":"cellAlpha",
			"CELL_BE":"cellBeta",
			"CELL_GA":"cellGamma",
			"SG":"spaceGroup",

			"RMEAS":"rMeasAllIPlusIMinus",

			"scalingStatisticsType":"scalingStatisticsType",
			//These all have three values separated by ", ", being overall, inner shell, outer shell.
			//IceBear will store these as three separate values, suffixed with _overall|_inner|_outer.
			"ANOMCOMPLETENESS":"anomalousCompleteness",
			"RHIGH":"resolutionLimitHigh",
			"RLOW":"resolutionLimitLow",
			"ISIGI":"meanIOverSigma",
			"ANOMMULTIPLICITY":"anomalousMultiplicity",
			"CCHALF":"ccHalf",
			"COMPLETENESS":"completeness",
			"MULTIPLICITY":"multiplicity",
			"RMERGE":"rMerge",
			"NTOBS":"totalObservations",
			"NUOBS":"uniqueObservations"
		},


		gatherBeamlineDetails:function () {
			let beamlines = [];
			let namesDone = [];
			Shipment.DatasetRetrieval.datasets.forEach(function (ds) {
				let beamlineName = ds["BL"];
				if (undefined !== beamlineName && -1 === namesDone.indexOf(beamlineName)) {
					let detectorManufacturer=ds["DETECTORMANUFACTURER"];
					let detectorModel=ds["DETECTORMODEL"];
					let detectorMode=ds["DETECTORMODE"];
					let detectorType=ds["DETECTORTYPE"];
					//As of March 2021, Diamond returns null, "" or "1", which are useless, and doesn't provide detector type at all.
					if(!detectorManufacturer || 2>detectorManufacturer.length){ detectorManufacturer=""; }
					if(!detectorModel || 2>detectorModel.length){ detectorModel=""; }
					if(!detectorMode || 2>detectorMode.length){ detectorMode=""; }
					if(!detectorType || 2>detectorType.length){ detectorType=""; }
					let beamline={
						'name': beamlineName,
						'shipmentdestinationid': data['shipmentdestinationid'],
						"detectormanufacturer": detectorManufacturer,
						"detectormodel": detectorModel,
						"detectortype": detectorType,
						"detectormode": detectorMode
					};
					beamlines.push(beamline);
					namesDone.push(beamlineName);
				}
			});
			Shipment.DatasetRetrieval.updateIceBearBeamlines(beamlines);
		},

	}, //end DatasetRetrieval

	CsvExport:{

		shipment:{},

		columns:{
			"diamond.ac.uk":"proposalCode,proposalNumber,visitNumber,shippingName,dewarCode,containerCode,preObsResolution,"+
				"neededResolution,oscillationRange,proteinAcronym,proteinName,spaceGroup,sampleBarcode,sampleName,"+
				"samplePosition,sampleComments,cell_a,cell_b,cell_c,cell_alpha,cell_beta,cell_gamma,subLocation,"+
				"loopType,requiredResolution,centringMethod,experimentKind,radiationSensitivity,energy,userPath,"+
				"screenAndCollectRecipe,screenAndCollectNValue,sampleGroup"
		},

		synchrotronDomain:null,

		writeCsvExportButton: function(shipmentDestination){
			let show=UserConfig.get(Shipment.SHOW_CSV_EXPORT, false);
			if(!show){ return; }
			let buttonText="Export";
			let buttonOnClick=null;

			Object.keys(SynchwebShippingHandler.CsvExport.columns).forEach(function(uri){
				if(-1!==shipmentDestination['clientbaseuri'].indexOf(uri)) {
					SynchwebShippingHandler.CsvExport.synchrotronDomain=uri;
					buttonText = "Export as "+window.shipmentDestination.name+" shipment CSV";
					buttonOnClick = SynchwebShippingHandler.CsvExport.beginCsvExport;
				}
			});
			if(!SynchwebShippingHandler.CsvExport.synchrotronDomain){ return false; }
			let ff=document.getElementById("shipmentactions_form").buttonField({ "label":buttonText, "id":"csvExport", "onclick":buttonOnClick });
			ff.insertAdjacentHTML('afterbegin','<h3>CSV export</h3><hr/>');
		},

		beginCsvExport:function (){
			if(!Shipment.validate()){
				//No dewars, or no pucks in a dewar, or no pins in a puck
				return false;
			}
			let frm=ui.modalBox({"title":"CSV export: Basic proposal details"}).form({ "autosubmit":false });
			ui.warningMessageBar("If you submit your shipment as CSV, IceBear won't have the ISPyB sample IDs. It "+
				"won't be able to retrieve dataset and autoprocessing information.<br/>Close this box and click the \"Send "+
				"shipment...\" button to submit your shipment to "+window.shipmentDestination.name+" and exchange "+
				"sample IDs.",frm);
			frm.textField({"name":"shippingName", "label":"Shipment name", "value":data["name"], readonly:"true"});
			frm.textField({"name":"proposalCode", "label":"Proposal code", "value":"mx"});
			frm.textField({"name":"proposalNumber", "label":"Proposal number", "value":""});
			frm.textField({"name":"visitNumber", "label":"Visit number", "value":""});
			frm.buttonField({"label":"Next", "onclick": SynchwebShippingHandler.CsvExport.validateBasicDetails });
		},

		validateBasicDetails:function (){
			let frm=document.getElementById("modalBox").querySelector("form");
			let validations=[
				["proposalCode","^[a-z][a-z]$", "Proposal code is required, and must be two letters (typically \"mx\")"],
				["proposalNumber","^[0-9]+$", "Proposal number is required, and must be numeric (no \"mx\" prefix)"],
				["visitNumber","^[0-9]+$", "Visit number is required, and must be numeric"],
			];
			let errors=[];
			validations.forEach(function(validation){
				let ff=frm.querySelector("[name="+validation[0]+"]")
				if(new RegExp(validation[1]).test(ff.value)){
					ff.closest("label").classList.remove("invalidfield");
					SynchwebShippingHandler.CsvExport.shipment[validation[0]]=ff.value;
				} else {
					ff.closest("label").classList.add("invalidfield");
					errors.push(validation[2]);
				}
			});
			if(errors.length){
				window.setTimeout(function (){
					return alert(errors.join("\n"));
				},50);
				return;
			}
			SynchwebShippingHandler.CsvExport.populateSamplesFromDom();
			SynchwebShippingHandler.CsvExport.writePerProteinForm();
		},

		samples:[],
		populateSamplesFromDom:function (){
			let dewars=document.querySelectorAll(".containertab");
			dewars.forEach(function(tab){
				let dewarBarcode=tab.dataset.containername;
				let pucks=tab.nextElementSibling.querySelectorAll('.treeitem');
				pucks.forEach(function(puck){
					if(!puck.record){ return; }
					let puckBarcode=puck.record.name;
					let pins=puck.querySelectorAll("tr.datarow");
					pins.forEach(function (pin){
						pin=pin.rowData;
						if(1*pin.isEmpty){ return; }
						pin=pin.childitems[1];
						SynchwebShippingHandler.CsvExport.samples.push({
							"proposalCode":"",
							"proposalNumber":"",
							"visitNumber":"",
							"shippingName":"",
							"dewarCode":dewarBarcode,
							"containerCode":puckBarcode,
							"proteinName":pin["proteinname"],
							"proteinAcronym":pin["proteinacronym"]
						})
					});
				});
			});
			this.samples[0]["proposalCode"]=this.shipment.proposalCode;
			this.samples[0]["proposalNumber"]=this.shipment.proposalNumber;
			this.samples[0]["visitNumber"]=this.shipment.visitNumber;
			this.samples[0]["shippingName"]=data["name"];

		},

		writePerProteinForm:function (){
			let columns={
				"proteinAcronym":{"label":"Protein acronym", "fieldWidthEm":5 },
				"requiredResolution":{"label":"Required resolution", "fieldWidthEm":6 },
				"centringMethod":{"label":"Centring method", "options":["diffraction","optical"], "fieldWidthEm":8 },
				"experimentKind":{"label":"Experiment kind", "options":["native","ligand","phasing","stepped"], "fieldWidthEm":8 },
				"energy":{"label":"Energy", "fieldWidthEm":6 },
				"userPath":{"label":"User path", "fieldWidthEm":8 },
			};
			let proteins=[];
			ui.setModalBoxTitle("CSV export: Additional sample details, by protein");
			let frm=document.getElementById("modalBox").querySelector("form");
			frm.innerHTML="";
			ui.infoMessageBar("Changes made here will go into the CSV, but won't be updated in IceBear.",frm);
			frm.textField({"name":"shippingName", "label":"Shipment name", "value":data["name"], readonly:"true"});
			frm.textField({"name":"proposal", "label":"Proposal and visit", "value":""+
					SynchwebShippingHandler.CsvExport.shipment["proposalCode"]+
					SynchwebShippingHandler.CsvExport.shipment["proposalNumber"]+"-"+
					SynchwebShippingHandler.CsvExport.shipment["visitNumber"],
				readonly:"true"});
			this.samples.forEach(function (sample){
				let proteinNames=Object.keys(proteins);
				if(-1===proteinNames.indexOf(sample.proteinName)){
					proteins[sample.proteinName]={
						"proteinName":sample.proteinName,
						"proteinAcronym":sample.proteinAcronym,
					}
				} else {

				}
			});
			frm.classList.add("hastable");
			let headers=['Protein name'];
			let cellTemplates=["{{proteinName}}"];
			Object.keys(columns).forEach(function (key){
				let obj=columns[key];
				headers.push(obj["label"]);
				if(obj.options){
					let sel='<select data-fieldname="'+key+'" style="width:'+Math.max(7, obj["fieldWidthEm"])+'em" />';
					obj.options.forEach(function (opt){
						sel+='<option value="'+opt+'">'+opt+'</option>';
					});
					sel+='</select>';
					cellTemplates.push(sel);
				} else {
					cellTemplates.push('<input type="text" data-fieldname="'+key+'" style="width:'+Math.max(7, obj["fieldWidthEm"])+'em" />');
				}
			});
			let tbl=ui.table({
				headers:headers,
				cellTemplates:cellTemplates,
			}, Object.values(proteins));

			SynchwebShippingHandler.CsvExport.insertChangeAllRow(tbl,columns);

			tbl.style.marginTop="1.5em";
			tbl.style.marginBottom="1.5em";
			tbl.querySelectorAll("input[type=text]").forEach(function (inp){
				inp.style.border="1px solid #ccc";
			});
			tbl.querySelectorAll("tr.datarow").forEach(function (tr){
				tr.querySelectorAll("input,select").forEach(function (field){
					field.dataset.proteinname=tr.rowData.proteinName;
				});
			});
			frm.appendChild(tbl);
			let f1=frm.buttonField({"label":"Download CSV", onclick:SynchwebShippingHandler.CsvExport.downloadDiamondCsv });
			let f2=frm.buttonField({"label":"Copy CSV to clipboard", onclick:SynchwebShippingHandler.CsvExport.copyDiamondCsvToClipboard });
			f1.querySelector("input").style.marginRight=".5em";
			f1.appendChild(f2.querySelector("input"));
			f2.remove();
			SynchwebShippingHandler.CsvExport.setProteinAcronymFields(tbl);
		},

		insertChangeAllRow:function (tbl,columns){
			let shipmentDestinationId=data["shipmentdestinationid"];
			let defaultsPrefix="shipmentdestination"+shipmentDestinationId+"_default_";
			let allRow=tbl.insertRow(1);
			allRow.insertCell();
			Object.keys(columns).forEach(function (key){
				if("proteinAcronym"===key){
					allRow.insertCell().innerHTML="";
					return;
				}
				let obj=columns[key];
				let field;
				if(obj.options){
					field='<select data-fieldname="'+key+'" style="width:'+Math.max(7, obj["fieldWidthEm"])+'em" />';
					obj.options.forEach(function (opt){
						let selected='';
						let defaultValue=UserConfig.get('shipmentdestination'+data["shipmentdestinationid"]+"_default_"+key, "");
						if(defaultValue===opt){
							selected='selected="selected"';
						}
						field+='<option '+selected+' value="'+opt+'">'+opt+'</option>';
					});
					field+='</select>';
				} else {
					field='<input type="text" name="'+key+'_all " data-fieldname="'+key+'" value="'+UserConfig.get(defaultsPrefix+key,"")+'" />';
				}
				field+='<br/><input type="button" name="'+key+'_setAll" value="Copy to all" />'
				field+='<br/><input type="button" name="'+key+'_setDefault" value="Set as default" />'
				let td=allRow.insertCell();
				td.innerHTML=field;
				td.style.paddingBottom="0.5em";
				td.querySelectorAll("input").forEach(function (inp){
					inp.style.width=Math.max(7, obj["fieldWidthEm"])+"em";
					inp.style.cssFloat="none";
				});
				td.querySelector("[name="+key+"_setDefault]").addEventListener("click", SynchwebShippingHandler.CsvExport.setFieldDefault);
				td.querySelector("[name="+key+"_setAll]").addEventListener("click", SynchwebShippingHandler.CsvExport.setToAll);
				td.querySelector("[name="+key+"_setAll]").click();
			});

		},

		setProteinAcronymFields:function(tbl){
			SynchwebShippingHandler.CsvExport.samples.forEach(function (sample){
				let inp=tbl.querySelector("[data-proteinname="+sample["proteinAcronym"]+"][data-fieldname=proteinAcronym]");
				if(""===inp.value){
					inp.value=sample["proteinAcronym"];
				}
			});
			tbl.querySelectorAll("[data-fieldname=proteinAcronym]").forEach(function (inp){
				if(""===inp.value){
					inp.value=inp.dataset.proteinname;
				}
			});
		},

		setToAll:function (evt){
			let btn=evt.target;
			let td=btn.closest("td");
			let inp=td.querySelector("input[type=text],select");
			let fieldName=inp.dataset.fieldname;
			let fieldValue=inp.value;
			let tbl=td.closest("table");
			tbl.querySelectorAll("input[data-fieldname="+fieldName+"],select[data-fieldname="+fieldName+"]").forEach(function (inp){
				inp.value=fieldValue;
			});
		},

		setFieldDefault:function(evt){
			let btn=evt.target;
			let td=btn.closest("td");
			td.classList.add("updating");
			let inp=td.querySelector("input[type=text],select");
			let fieldName=inp.dataset.fieldname;
			let fieldValue=inp.value;
			let shipmentDestinationId=data["shipmentdestinationid"];
			fieldName="shipmentdestination"+shipmentDestinationId+"_default_"+fieldName;
			UserConfig.set(fieldName, fieldValue);
			window.setTimeout(function (){
				td.classList.remove("updating");
			}, 500);
		},

		downloadDiamondCsv:function(evt){
			let csv=SynchwebShippingHandler.CsvExport.generateDiamondCsv(evt);
			ui.downloadAsFile(csv, data["name"].replaceAll(" ","")+".csv");
		},

		copyDiamondCsvToClipboard:function(evt){
			let csv=SynchwebShippingHandler.CsvExport.generateDiamondCsv(evt);
			ui.copyToClipboard(csv);
		},

		generateDiamondCsv: function(evt){
			let btn=evt.target;
			let proteinRows=btn.closest(".boxbody").querySelectorAll("tr.datarow");
			proteinRows.forEach(function (proteinRow){
				let fields=[];
				let proteinName=proteinRow.rowData.proteinName;
				proteinRow.querySelectorAll("input[type=text],select").forEach(function (field){
					fields[field.dataset.fieldname]=field.value;
				});
				SynchwebShippingHandler.CsvExport.samples.forEach(function (sample){
					if(sample.proteinName!==proteinName){ return; }
					Object.keys(fields).forEach(function(field){
						sample[field]=fields[field];
					})
				});
			})
			let csvRows=[];
			let domain=SynchwebShippingHandler.CsvExport.synchrotronDomain;
			let columns=SynchwebShippingHandler.CsvExport.columns[domain].split(",");
			csvRows.push(columns);
			SynchwebShippingHandler.CsvExport.samples.forEach(function (sample) {
				let row=[];
				columns.forEach(function (column){
					if(undefined===sample[column]){
						row.push("");
					} else {
						let txt=sample[column];
						let needsQuotes=false;
						if(-1!==txt.indexOf('"')){
							needsQuotes=true;
							txt=txt.replaceAll('"','""');
						}
						if(-1!==txt.indexOf(',')){
							needsQuotes=true;
						}
						if(needsQuotes){
							txt='"'+txt+'"';
						}
						row.push(txt);
					}
				});
				csvRows.push(row.join(","));
			});
			return csvRows.join("\n");

		},

	}

};
