window.IspybRestShippingHandler={

		synchrotronApiDescription: {
			"apiRoot":null,

			"getProposals":{
				"uri":"proposal/list",
				"method":"get",
				"properties":{
					"type":"Proposal_proposalCode",
					"number":"Proposal_proposalNumber",
					"title":"Proposal_title",
				}
			},

			"getSessions":{
				"uri":"proposal/{{Proposal_proposalCode}}{{Proposal_proposalNumber}}/session/list",
				"method":"get",
				"properties":{
					"beamline":"beamLineName",
					"startDate":"BLSession_startDate",
					"endDate":"BLSession_endDate"
				}
			},

			"getProteins":{
				"uri":"proposal/{{Proposal_proposalCode}}{{Proposal_proposalNumber}}/mx/protein/list",
				"method":"get",
				"properties":{
					"acronym":"acronym"
				}
			},

			"getLabContacts":{
				"uri":"proposal/{{Proposal_proposalCode}}{{Proposal_proposalNumber}}/info/get",
				"method":"get",
				"path":[0,"labcontacts"],
				"properties":{
					"id":"labContactId",
					"familyName":["personVO","familyName"],
					"givenName":["personVO","givenName"],
					"emailAddress":["personVO","emailAddress"],
					"phoneNumber":["personVO","phoneNumber"],
					"labName":["personVO","laboratoryVO","name"]
				}
			}

		},

		/**
		 * The base URI of the EXI instance.
		 * Populated from the shipment destination's baseuri property.
		 */
		apiBaseUri:null, //TODO use apibaseuri - then do the same for SW handler

		/**
		 * When authenticating with EXI, a "site" parameter is sent along with username and password.
		 * This object is a mapping of partial URI to the site parameter value. It will break if the synchrotron's
		 * ISPyB URI is changed - but checks in authenticate() below will catch this.
		 */
		sites:{
			'maxiv.lu.se':'MAXIV',
			'esrf.fr':'ESRF',
			'cells.es':'ALBA'
		},

		/**
		 * Some EXI sites may not require a valid session on the proposal. Currently, this is only MAX-IV.
		 * Note that the values here are the IceBear-managed shipmentdestination name, NOT the "site" parameter above.
		 */
		sitesNotNeedingSession:['MAX-IV'],

		/**
		 * While all known EXI sites enforce the requirement that the protein acronym exist in ISPyB, not all of them
		 * import protein information from their user office backend. Instead, they allow proteins to be created in
		 * ISPyB. Currently, this is only MAX-IV.
		 */
		sitesAllowingProteinCreation:['MAX-IV'],

		/**
		 * Something changed in May/June 2025 causing retrieval of sessions for proposals, and shipment creation, to fail.
		 * It seems related to proxying via IceBear to overcome a - now fixed, it seems - lack of CORS headers. For these
		 * synchrotrons, we request directly unless explicitly overridden at the AjaxUtils.remoteAjax call.
		 */
		sitesBypassingIceBearProxy:['ESRF','MAX-IV'],

		bypassProxy:false,

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

		/*
		 * These are set and used during shipment submission
		 */
		shipmentIdAtFacility:"",
		safetyLevel:null,
		labContact:null,
		proteins:null,
		proposal:null,
		proposalName:null,
		session:null,
		proteinAcronymsInShipment:[],
		shipmentErrors:[],
		missingAcronyms:[],
		acronymToRemoteProteinId:{},

		SAMPLE_COMMENT_MAXLENGTH:250,

		/**
		 * Returns the URL of this shipment at the remote synchrotron, or false if the shipment has not been submitted.
		 */
		getShipmentUrlAtFacility:function(shipmentDestination, shipmentIdAtFacility){
			if(!shipmentIdAtFacility &&!data.idatremotefacility){
				return false;
			} else if(!shipmentIdAtFacility){
				shipmentIdAtFacility=data.idatremotefacility;
			}
			let clientBaseUri=shipmentDestination["clientbaseuri"].trim("/");
			if(!clientBaseUri){
				alert("Cannot get shipment URL. No shipping API base URI set for "+shipmentDestination.name+".");
				return false;
			}
			clientBaseUri=clientBaseUri.replace(/\/$/, "");
			return clientBaseUri+"/mx/index.html#/shipping/"+shipmentIdAtFacility+"/main";
		},
		
		/**
		 * Returns the URL of this crystal at the remote synchrotron, or false if the crystal does not have a remote ID.
		 * Assumes base API in the form
		 * http(s)//ispyb.wherever.net/ispyb-ws/rest/
		 * and crystal URL is
		 * http(s)//exi.wherever.net/mx/index/html#/proposal/MXnnnn/datacollection/sample/SAMPLE_ID/main
		 *
		 */
		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+"/mx/index.html#/mx/proposal/"+IspybRestShippingHandler.proposalName+"/datacollection/sample/"+crystalIdAtFacility+"/main";
		},

		getAutoProcessingResultsUrlAtFacility:function(shipmentDestination, dataCollectionIdAtFacility){
			if(!dataCollectionIdAtFacility || ""===dataCollectionIdAtFacility){
				return false;
			}
			let clientBaseUri=shipmentDestination["clientbaseuri"];
			if(!clientBaseUri){
				alert("Cannot get autoprocessing data URL. No shipping API base URI set for "+shipmentDestination.name+".");
				return false;
			}
			clientBaseUri=clientBaseUri.replace(/\/$/, "");
			return clientBaseUri+"/mx/index.html#/mx/proposal/"+IspybRestShippingHandler.proposalName+"/datacollection/"+dataCollectionIdAtFacility+"/view";
		},

		isInited: false,

		init: function(afterInit){
			if(IspybRestShippingHandler.isInited){ return afterInit(); }
			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;
			}
			if(-1!==IspybRestShippingHandler.sitesBypassingIceBearProxy.indexOf(window.shipmentDestination.name)){
				IspybRestShippingHandler.bypassProxy=true;
			}
			IspybRestShippingHandler.apiBaseUri=apiBaseUri.replace(/\/+$/g,"")+"/";
			//Check still logged into IceBear, reset session clock - don't want to fail at the end.
			IspybRestShippingHandler.setProteinAcronymsInShipment();
			new AjaxUtils.Request("/api/homepagebrick", {
				"method":"get",
				"onSuccess":afterInit,
				"onFailure":function(transport){
					if(401===transport.status){
						//IceBear session timed out. Reload page to show login form.
						ui.forceReload();
					} else {
						ui.keepAlive();
						IspybRestShippingHandler.isInited=true;
						afterInit();
					}
				}
			});
		},

		/**
		 * 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["remoteidatfacility"]){
				alert("Shipment has already been sent.");
				return false;
			}
			let baseUri=window.shipmentDestination["apibaseuri"];
			if(!baseUri.endsWith("/")){ baseUri+="/"; }
			baseUri+="{{TOKEN}}";
			IspybRestShippingHandler.synchrotronApiDescription.apiRoot=baseUri;
			Shipment.synchrotronApiDescription=IspybRestShippingHandler.synchrotronApiDescription;
			if(window.shipmentDestination && window.shipmentDestination.name==="ESRF"){
				IspybRestShippingHandler.init(EsrfCsvMaker.begin);
			} else {
				IspybRestShippingHandler.init(IspybRestShippingHandler.openShippingDialog);
			}
		},

		/**
		 * Iterates through all dewars, pucks, pins and adds the protein acronyms to IspybRestShippingHandler.proteinAcronymsInShipment.
		 * Note that this does no validation, not even for non-existent acronyms.
		 */
		setProteinAcronymsInShipment:function(){
			let dewars = document.querySelectorAll(".containertab+.tabbody");
			dewars.forEach(function (d) {
				let pucks = d.querySelectorAll(".treeitem");
				pucks.forEach(function (p) {
					let slots = p.querySelectorAll("tr.datarow");
					slots.forEach(function (tr) {
						if(!tr.rowData || !tr.rowData.childitems || !tr.rowData.childitems.length){
							return;
						}
						let pinAcronym=tr.rowData.childitems[1].proteinacronym;
						if(-1===IspybRestShippingHandler.proteinAcronymsInShipment.indexOf(pinAcronym)){
							IspybRestShippingHandler.proteinAcronymsInShipment.push(pinAcronym);
						}
					});
				});
			});
		},

		openShippingDialog:function(){
			ui.modalBox({ title:"Send shipment to "+window.shipmentDestination.name, content:"Getting list of proposals from "+window.shipmentDestination.name+"..." });
			IspybRestShippingHandler.getProposals();
		},

		/**
		 * 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:IspybRestShippingHandler.apiBaseUri+"authenticate",
				method:"post",
			});
			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(){ IspybRestShippingHandler.authenticate(afterAuthenticate); 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(afterAuthenticate){
			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; }
			let site=false;
			Object.keys(IspybRestShippingHandler.sites).forEach(function (key){
				if(-1!==IspybRestShippingHandler.apiBaseUri.indexOf(key)){
					site=IspybRestShippingHandler.sites[key];
				}
			});
			if(!site){
				alert("No entry in sites lookup table for "+window.shipmentDestination.name+"\n\nCannot authenticate. See your administrator.");
				return false;
			}

			let username = frm.remoteusername.value;
			let password = frm.remotepassword.value;
			frm.querySelector("input[type=submit]").closest("label").classList.add("updating");
			AjaxUtils.remoteAjax(
					IspybRestShippingHandler.apiBaseUri+"authenticate?site="+site,
					'post',
					'login='+username+'&password='+encodeURIComponent(password),
					function(transport){
						IspybRestShippingHandler.authenticate_onSuccess(transport, afterAuthenticate);
					},
					function(transport){
						IspybRestShippingHandler.authenticate_onFailure(transport, afterAuthenticate);
					},
					null,
					false
			);
			return false;
		},
		/**
		 * Success handler for remote synchrotron authentication. Calls getProposals().
		 *
		 * Typical success response:
		 *   {"token":"8e993500f813f8fdb5b6caa75cf23772936289bb","roles":["User"]}
		 *
		 * A bad login appears to return a Java error:
		 *   JBAS011843: Failed instantiate InitialContextFactory com.sun.jndi.ldap.LdapCtxFactory from classloader
		 *   ModuleClassLoader for Module "deployment.ispyb.ear.ispyb-ws.war:main" from Service Module Loader
		 *
		 * @param {XMLHttpRequest} transport The response object.
		 * @param {function} afterAuthenticate A function to call after successful authentication
		 */
		authenticate_onSuccess:function(transport,afterAuthenticate){
			if(!transport.responseJSON || !transport.responseJSON["token"]){
				if(transport.responseJSON && transport.responseJSON["error"]){
					IspybRestShippingHandler.authenticate_onFailure(transport);
				} else {
					IspybRestShippingHandler.showErrorAtAuthenticate("Could not log you in. Check your username and password.");
				}
			} else {
				IspybRestShippingHandler.token=transport.responseJSON["token"];
				window.setTimeout(afterAuthenticate,50);
			}
		},
		/**
		 * Failure handler for remote synchrotron authentication.
		 * @param transport
		 */
		authenticate_onFailure:function(transport){
			let msg="Could not log you in. Check your username and password.";
			if(transport.responseJSON && transport.responseJSON.error){
				msg=window.shipmentDestination.name+" said: "+transport.responseJSON.error;
			}
			IspybRestShippingHandler.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";
			}
		},

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

		/**
		 * Returns the definition of an unattended/mail-in session. This is only available at Diamond, so
		 * this EXI-only implementation returns false;
		 * @returns {boolean}
		 */
		getUnattendedSession: function (){
			return false;
		},

		/**
		 * Returns a dummy session object for use when there are no active sessions. This is only available
		 * at MAXIV.
		 */
		getNullSession: function (){
			if(!IspybRestShippingHandler.canShipWithoutSession()){ return false; }
			return { "sessionId":null };
		},

		/**
		 * Whether the synchrotron for the current shipment can accept a shipment with no session. This is
		 * only available at MAX-IV.
		 * @returns {boolean}
		 */
		canShipWithoutSession: function (){
			return(-1!==IspybRestShippingHandler.sitesNotNeedingSession.indexOf(window.shipmentDestination.name));
		},

		sendShipment:function(){
			IspybRestShippingHandler.shipmentErrors=[];
			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...');
			IspybRestShippingHandler.markAllPinsUnsaved();
			IspybRestShippingHandler.watchForAllPinsSaved();
			IspybRestShippingHandler.doPreflight();
		},

		markAllPinsUnsaved:function(){
			document.querySelectorAll("tr.datarow").forEach(function(tr){
				if(tr.rowData && 1!==parseInt(tr.rowData.isEmpty)) {
					tr.classList.add("unsavedPin");
				}
			});

		},

		doPreflight:function(){
			let box=document.getElementById("modalBox").querySelector(".boxbody");
			let errors=Shipment.getShipmentErrors(); //verify nothing silly like an empty dewar

			if(0<errors.length){
				box.innerHTML="Could not send shipment. Please fix the following:<br/><ul><li>"+errors.join("</li><li>")+"</ul>";
				return false;
			}

			ui.logToDialog("Pre-flight validation complete. Creating shipment in ISPyB...");
			IspybRestShippingHandler.remoteErrors=[];
			IspybRestShippingHandler.createEmptyShipment();
		},

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

		/**
		 * Creates a shipment at the synchrotron.
		 */
		createEmptyShipment:function(){
			ui.logToDialog("Submitting basic shipment details");
			let labContactId=IspybRestShippingHandler["labContact"]["labContactId"];
			let shipmentName=document.getElementById("shipmentdetails_form").querySelector("#name").value;

			AjaxUtils.remoteAjax(
				IspybRestShippingHandler.apiBaseUri+IspybRestShippingHandler.token+"/proposal/"+IspybRestShippingHandler.proposalName+"/shipping/save",
				'post',
				{
					"name": shipmentName,
					"sendingLabContactId": labContactId,
					"returnLabContactId": -1, //same as outbound
					"comments": "IceBear: "+document.location.href,
					returnCourier:"-1",
					courierAccount:"",
					billingReference:"",
					dewarAvgCustomsValue:"0",
					dewarAvgTransportValue:"0",
					sendersID:data.id,
					sendersURL:document.location.href,
					sessionId:IspybRestShippingHandler.session["sessionId"]
				},
				IspybRestShippingHandler.createEmptyShipment_onSuccess,
				IspybRestShippingHandler.createEmptyShipment_onFailure,
				null,
				IspybRestShippingHandler.bypassProxy
			);
		},
		createEmptyShipment_onSuccess:function(transport){
			if(transport.responseJSON && transport.responseJSON["shippingId"]){
				IspybRestShippingHandler.shipmentIdAtFacility=transport.responseJSON["shippingId"];
				window.setTimeout(IspybRestShippingHandler.createDewars, 50);
			} else {
				IspybRestShippingHandler.logRemoteError("No shipment ID returned from shipment create attempt.");
				return IspybRestShippingHandler.createEmptyShipment_onFailure(transport);
			}
		},
		createEmptyShipment_onFailure:function(transport){
			IspybRestShippingHandler.logRemoteError("Could not create shipment.");
			if(transport.responseJSON && transport.responseJSON["error"]){
				IspybRestShippingHandler.logRemoteError(transport.responseJSON["error"]);
			} else {
				IspybRestShippingHandler.logRemoteError(transport.responseText);
			}
			IspybRestShippingHandler.showRemoteErrors();
		},

		createDewars:function(){
			ui.logToDialog("Creating dewars in remote shipment...");
			let dewarTabs=document.querySelectorAll(".containertab");
			dewarTabs.forEach(function (dt) {
				IspybRestShippingHandler.createDewar(dt.dataset.containername);
			});
		},
		createDewar:function(dewarName){
			if(!dewarName || ""===dewarName){
				IspybRestShippingHandler.logRemoteError("Could not create shipment.");
				IspybRestShippingHandler.showRemoteErrors();
			}
			ui.logToDialog("Creating dewar "+dewarName+" in remote shipment...");
			AjaxUtils.remoteAjax(IspybRestShippingHandler.apiBaseUri+IspybRestShippingHandler.token+"/proposal/"+
										IspybRestShippingHandler.proposalName+"/shipping/"+
										IspybRestShippingHandler.shipmentIdAtFacility+"/dewar/save",
				"post",
				{
					code:dewarName,
					transportValue:"",
					storageLocation:"N/A",
					comments:"",
					shippingId: IspybRestShippingHandler.shipmentIdAtFacility
				},
				function(transport){ IspybRestShippingHandler.createDewar_onSuccess(transport, dewarName); },
				IspybRestShippingHandler.createDewar_onFailure,
				null,
				IspybRestShippingHandler.bypassProxy
			);
		},
		createDewar_onSuccess:function(transport, dewarName){
			// Returned JSON should represent the entire shipment
			// {
			//  "shippingId":604,
			//  ...
			// 	"dewarVOs":[
			// 		{
			// 			"dewarId":2218,"code":"DEWAR1","comments":"","storageLocation":"N/A","dewarStatus":null,
			// 			"timeStamp":null,"isStorageDewar":null,"barCode":"MAXIV02218","customsValue":null,
			// 			"transportValue":0,"trackingNumberToSynchrotron":null,"trackingNumberFromSynchrotron":null,
			// 			"facilityCode":null,"type":"Dewar","isReimbursed":false,
			// 			"containerVOs":[
			//
			// 			],
			// 			"sessionVO":null
			// 		}
			// 	],
			// 	"sessions":[]
			// 	}
			if(!transport.responseJSON || !transport.responseJSON["dewarVOs"] || 0===transport.responseJSON["dewarVOs"].length){
				return IspybRestShippingHandler.createDewar_onFailure(dewarName);
			}
			let dewarCreated=false;
			transport.responseJSON["dewarVOs"].forEach(function(remoteDewar){
				if(remoteDewar["code"]===dewarName){
					let dewarTab=document.getElementById(dewarName);
					dewarTab.remoteDewar=remoteDewar;
					dewarTab.dataset.remoteid=remoteDewar["dewarId"];
					dewarTab.classList.remove("noRemoteDewar");
					dewarCreated=true;
				}
			});
			if(!dewarCreated){
				return IspybRestShippingHandler.createDewar_onFailure(dewarName);
			}
			ui.logToDialog("...added "+dewarName+" to shipment.");
			window.setTimeout(function(){ IspybRestShippingHandler.addPucksToDewar(dewarName) }, 50);
		},
		createDewar_onFailure:function(dewarName){
			IspybRestShippingHandler.logRemoteError("Could not create dewar "+dewarName+" in remote shipment.");
			IspybRestShippingHandler.showRemoteErrors();
		},

		/**
		 * Populate a dewar with its child pucks, at the synchrotron.
		 */
		addPucksToDewar:function(dewarName){
			ui.logToDialog("Adding pucks to dewar "+dewarName+"...");
			let tabBody=document.getElementById(dewarName+"_body");
			let pucks=tabBody.querySelectorAll(".treeitem");
			pucks.forEach(function(p){
				let localPuck=p.record;
				p.id=p.record.name;
				IspybRestShippingHandler.addPuckToDewar(localPuck);
			});
		},
		
		/**
		 * Add a puck to a dewar within the shipment, at the synchrotron.
		 */
		addPuckToDewar:function(localPuck){
			let localDewarTab=document.getElementById(localPuck["name"]).closest(".tabbody").previousElementSibling;
			let dewarName=localDewarTab.id;
			ui.logToDialog("Adding puck "+localPuck.name+" to "+dewarName+"...");
			let remoteDewarId=localDewarTab.dataset.remoteid;
			AjaxUtils.remoteAjax(
				IspybRestShippingHandler.apiBaseUri+IspybRestShippingHandler.token+"/proposal/"+IspybRestShippingHandler.proposalName+
						"/shipping/"+IspybRestShippingHandler.shipmentIdAtFacility+"/dewar/"+remoteDewarId+
						"/containerType/"+localPuck["containertypename"]+"/capacity/"+localPuck["positions"]+"/container/add",
				'get',
				{},
				function(transport){ IspybRestShippingHandler.addPuckToDewar_onSuccess(transport, localPuck) },
				function(transport){ IspybRestShippingHandler.addPuckToDewar_onFailure(transport, localPuck) },
				null,
				IspybRestShippingHandler.bypassProxy
			);
		},
		addPuckToDewar_onSuccess:function(transport, localPuck){
			if(!transport.responseJSON || !transport.responseJSON["containerId"]){
				return IspybRestShippingHandler.addPuckToDewar_onFailure();
			}
			ui.logToDialog("...added "+localPuck.name+" to dewar.");
			localPuck.remoteId=transport.responseJSON["containerId"];
			IspybRestShippingHandler.addSamplesToPuck(localPuck);			
		},
		addPuckToDewar_onFailure:function(){
			IspybRestShippingHandler.logRemoteError("Could not add puck to dewar.");
			IspybRestShippingHandler.showRemoteErrors();
		},

		addSamplesToPuck:function(localPuck){
			ui.logToDialog("Adding samples to puck "+localPuck.name+"...");
			let puckElement=document.getElementById(localPuck.name);
			let remotePuckId=localPuck.remoteId;
			let remoteDewarId=puckElement.closest(".tabbody").previousElementSibling["remoteDewar"]["dewarId"];
			let positions=puckElement.querySelectorAll("tr.datarow");

			let remotePuck= {
				"containerId": remotePuckId, "code": localPuck["name"],
				"containerType": localPuck["containertypename"], "capacity": localPuck["positions"],
				"beamlineLocation": IspybRestShippingHandler.session["beamLineName"], "sampleChangerLocation": null,
				"containerStatus": null, "barcode": null,
				"sampleVOs":[]
			};

			positions.forEach(function(tr){
				if(tr.rowData && 1!==parseInt(tr.rowData.isEmpty)) {
					let pin = tr.rowData;
					let proteinName=pin["proteinname"];
					let proteinAcronym=pin["proteinacronym"];
					let remoteProteinId=IspybRestShippingHandler.acronymToRemoteProteinId[proteinAcronym];
					let position=pin.position;
					let pinBarcode=pin.name;
					if(0===pinBarcode.indexOf("dummypin")){
						pinBarcode="";
					}
					let crystal=pin.childitems[1]; //0 is dummy
					let sampleName=crystal.name.replace(" ","_"); //Don't send sample names with spaces
					let shippingComment=crystal.shippingcomment.split('"').join('');

					let joiner=" -IceBear: ";
					let localUrl=window.document.location.protocol+"//"+window.document.location.hostname+"/crystal/"+crystal.id;
					if(shippingComment.length+joiner.length+localUrl.length <= IspybRestShippingHandler.SAMPLE_COMMENT_MAXLENGTH){
						shippingComment+=localUrl;
					}

					let remoteSample={
						"name":sampleName,"BLSample_code":pinBarcode,"location":position,"comments":shippingComment,
						"crystalVO":{
							"proteinVO":{
								"proteinId":remoteProteinId,"name":proteinName,"acronym":proteinAcronym,"safetyLevel":null,"molecularMass":null,
								"proteinType":null,"sequence":null,"personId":null,"timeStamp":null,
								"isCreatedBySampleSheet":null,"externalId":null
							},
							"spaceGroup":"P1","cellA":0,"cellB":0,"cellC":0,"cellAlpha":0,"cellBeta":0,"cellGamma":0
						},
						"diffractionPlanVO":{
							"radiationSensitivity":null,"requiredCompleteness":null,"requiredMultiplicity":null,
							"requiredResolution":null, "observedResolution":null,"preferredBeamDiameter":null,
							"numberOfPositions":null,"experimentKind":"Default"
						},
						"sendersID":crystal.id,
						"sendersURL":localUrl
					};
					if(""!==crystal.spacegroup){ remoteSample["crystalVO"]["spaceGroup"]=crystal.spacegroup.replace("_",""); }
					if(""!==crystal.unitcella){ remoteSample["crystalVO"]["cellA"]=1*crystal.unitcella; }
					if(""!==crystal.unitcellb){ remoteSample["crystalVO"]["cellB"]=1*crystal.unitcellb; }
					if(""!==crystal.unitcellc){ remoteSample["crystalVO"]["cellC"]=1*crystal.unitcellc; }
					if(""!==crystal.unitcellalpha){ remoteSample["crystalVO"]["cellAlpha"]=1*crystal.unitcellalpha; }
					if(""!==crystal.unitcellbeta) { remoteSample["crystalVO"]["cellBeta"] =1*crystal.unitcellbeta;  }
					if(""!==crystal.unitcellgamma){ remoteSample["crystalVO"]["cellGamma"]=1*crystal.unitcellgamma; }

					remotePuck.sampleVOs.push(remoteSample);
				}
			});
			AjaxUtils.remoteAjax(IspybRestShippingHandler.apiBaseUri+IspybRestShippingHandler.token+"/proposal/"+
									IspybRestShippingHandler.proposalName+"/shipping/"+IspybRestShippingHandler.shipmentIdAtFacility+
									"/dewar/"+remoteDewarId+"/puck/"+remotePuckId+"/save",
				"post",
				{
					puck:JSON.stringify(remotePuck)
				},
				IspybRestShippingHandler.addSamplesToPuck_onSuccess,
				IspybRestShippingHandler.addSamplesToPuck_onFailure,
				null,
				IspybRestShippingHandler.bypassProxy
			);
		},
		addSamplesToPuck_onSuccess:function(transport){
			if(!transport.responseJSON || !transport.responseJSON["containerId"]){
				return IspybRestShippingHandler.addSamplesToPuck_onFailure(transport);
			}
			let puckElement=document.getElementById(transport.responseJSON["code"]);
			transport.responseJSON["sampleVOs"].forEach(function(remoteSample){
				//IceBear puck table may have empty rows. ISPyB puck response doesn't.
				//So we can't just match on array positions and have to iterate.
				let positions=puckElement.querySelectorAll("tr.unsavedPin");
				positions.forEach(function (tr) {
					if(tr.rowData && 1!==parseInt(tr.rowData.isEmpty) && tr.rowData.samplename===remoteSample["name"]) {
						let pin = tr.rowData;
						let diffractionRequestId=pin.childitems[1]["diffractionrequestid"];
						IspybRestShippingHandler.addShippedNote(pin.childitems[1]);

						let remoteCrystalId=remoteSample["blSampleId"];
						//Or do we want the crystalVO ID?
						//let remoteCrystalId=remoteSample["crystalVO"]["crystalId"];
						if(remoteCrystalId){
							IspybRestShippingHandler.saveRemoteCrystalId(diffractionRequestId, remoteCrystalId, tr);
						} else {
							tr.classList.remove("unsavedPin");
						}
					}
				});
			});


		},
		addSamplesToPuck_onFailure:function(){
			//TODO Puck name
			ui.logToDialog("<strong>Adding samples 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':IspybRestShippingHandler.getCrystalUrlAtFacility(window.shipmentDestination, remoteCrystalId)
				},
				onSuccess:function(){
					pinTr.classList.remove("unsavedPin");
				},
				onFailure:function(){
					pinTr.classList.remove("unsavedPin");
				},
			});
			
		},

		watchForAllPinsSaved:function(){
			if(document.querySelector(".unsavedPin")){
				window.setTimeout(IspybRestShippingHandler.watchForAllPinsSaved, 500);
				return;
			}
			window.setTimeout(IspybRestShippingHandler.completeShipment,50);
		},
		
		completeShipment:function(){
			ui.logToDialog("Shipment was submitted to "+window.shipmentDestination.name+".","success");			
			ui.logToDialog("Updating IceBear and creating shipment manifest...");
			let shippedDate=new Date().toISOString().split("T")[0];
			new AjaxUtils.Request('/api/shipment/'+data.id,{
				'method':'patch',
				'parameters':{
					'csrfToken':csrfToken,
					'dateshipped':shippedDate,
					'idatremotefacility':IspybRestShippingHandler.shipmentIdAtFacility,
					'urlatremotefacility':IspybRestShippingHandler.getShipmentUrlAtFacility(window.shipmentDestination, IspybRestShippingHandler.shipmentIdAtFacility),
					'proposalname':IspybRestShippingHandler.proposalName,
				},
				onSuccess:IspybRestShippingHandler.completeShipment_onSuccess,
				onFailure:IspybRestShippingHandler.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);
		},
		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);
		},

	DatasetRetrieval:{

		counts:[],
		datasets:[],
		beamlines:{},
		beamlineNameToId:{},
		sessions:{},
		IMAGE_COUNT_CUTOFF:100, //A dataset with more images than this is deemed to be a real collection, not a grid scan, etc.

		begin:function (){
			IspybRestShippingHandler.init(IspybRestShippingHandler.DatasetRetrieval.openRetrievalDialog);
		},

		openRetrievalDialog:function (){
			ui.modalBox({
				title:"Getting datasets from "+window.shipmentDestination.name,
				content:"Getting datasets from "+window.shipmentDestination.name+"...",
				onclose:Shipment.Results.renderTab
			});
			IspybRestShippingHandler.DatasetRetrieval.beginRetrieval();
		},

		beginRetrieval:function (){
			ui.setModalBoxTitle("Getting datasets...");
			let mb=document.getElementById("modalBox").querySelector(".boxbody");
			if(!mb){ alert('Call begin() not getDatasets()'); return false; }
			if(!IspybRestShippingHandler.apiBaseUri){
				alert("No base URI supplied. Call begin() first, with base URI.");
				return false;
			}
			if(!IspybRestShippingHandler.token){
				IspybRestShippingHandler.showLoginForm(IspybRestShippingHandler.DatasetRetrieval.beginRetrieval);
				return false;
			}

			//Parameter names map is ISPyB:IceBear. Generate reverse, IceBear:ISPyB
			// https://stackoverflow.com/questions/23013573/swap-key-with-value-in-object
			IspybRestShippingHandler.DatasetRetrieval.iceBearDataProcessingParameterNames=Object.entries(IspybRestShippingHandler.DatasetRetrieval.dataProcessingParameterNames).reduce((acc, [key, value]) => (acc[value] = key, acc), {});

			mb.innerHTML="";
			mb.classList.add("hastable");
			let manifest=data['manifest']['rows'];
			let tbl=document.createElement("table");
			mb.appendChild(tbl);
			let countsTable=document.createElement("table");
			countsTable.id="countsTable";
			countsTable.style.width="40%";
			countsTable.style.marginTop="2em";
			mb.appendChild(countsTable);
			manifest.forEach(function(dewar){
				let dewarRow=document.createElement("tr");
				let dewarCell=document.createElement("th");
				dewarCell.innerHTML=dewar.name;
				dewarRow.appendChild(dewarCell);
				tbl.appendChild(dewarRow);
				dewar["childitems"].forEach(function (puck){
					if(typeof puck==="string"){ return; }
					dewarCell.colSpan=puck.length; // -1 for dummy, +1 for puck name
					let puckRow=document.createElement("tr");
					let puckCell=document.createElement("th");
					puckCell.innerHTML=puck.name;
					puckRow.appendChild(puckCell);
					puckCell.puck=puck;
					puck["childitems"].forEach(function (slot){
					 	if("dummy"===slot){ return; }
					 	let cell=document.createElement("td");
						cell.style.textAlign="center";
					 	cell.innerHTML=slot["position"]+"";
						cell.id=puck.name+"_slot_"+slot["position"];
						if(0===1*slot["isEmpty"]){
							cell.crystal=slot["childitems"][1];
							cell.classList.add("hasCrystal");
							cell.classList.add("updating");
							cell.classList.add("processing");
							cell.dataset.localCrystalId=cell.crystal["id"];
							cell.dataset.remoteCrystalId=cell.crystal["crystalidatremotefacility"];
							cell.dataset.diffractionRequestId=cell.crystal["diffractionrequestid"];
							IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Crystals",cell.crystal["id"]);
						} else {
							cell.style.opacity="0.8";
							cell.style.backgroundColor="#ccc";
							cell.title="Empty position";
							console.log(cell.id.replaceAll("_"," ")+" is empty");
						}
					 	puckRow.appendChild(cell);
					});
					tbl.appendChild(puckRow);
				});
			});

			window.setTimeout(IspybRestShippingHandler.DatasetRetrieval.getAllDatasets, 50);
		},

		incrementCounter:function (label, itemsToCount){
			let key=label.toLowerCase().replaceAll(/[^a-z0-9_-]/g,"");
			let td=document.getElementById(key);
			if(!td){
				let countsTable=document.getElementById("countsTable");
				let tr=document.createElement("tr");
				let td1=document.createElement("td");
				td1.innerHTML=label;
				let td2=document.createElement("td");
				td2.id=key;
				tr.appendChild(td1);
				tr.appendChild(td2);
				countsTable.appendChild(tr);
				td2.items=[];
				window.setInterval(IspybRestShippingHandler.DatasetRetrieval.updateCounter, 100, td2);
			}
			td=document.getElementById(key);
			if(Array.isArray(itemsToCount)){
				itemsToCount.forEach(function(item){
					td.items.push(itemsToCount);
				});
			} else if(itemsToCount){
				td.items.push(itemsToCount);
			}
			IspybRestShippingHandler.DatasetRetrieval.updateCounter(td);
		},

		updateCounter:function (td){
			td.innerHTML=td.items.length;
		},

		stillUpdating:function (){
			return document.querySelector("#modalBox td.updating")
		},

		getAllDatasets:function(){
			IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Crystals completed successfully");
			IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Crystals completed with errors");
			IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Datasets found");
			IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Datasets saved");
			document.querySelectorAll("#modalBox td.hasCrystal").forEach(function(td){
				console.log(td.id.replaceAll("_"," ")+": Getting datasets");
				td.classList.add("updating");
				IspybRestShippingHandler.DatasetRetrieval.getDatasetsForCrystal(td);
			});
			IspybRestShippingHandler.DatasetRetrieval.updateIceBearBeamlines();
		},

		getDatasetsForCrystal:function(td){
			if(!td.crystal){ return IspybRestShippingHandler.DatasetRetrieval.fail(td, "No crystal"); }
			if(!td.dataset.remoteCrystalId){ return IspybRestShippingHandler.DatasetRetrieval.fail(td, "No remote ID for crystal"); }
			td.classList.add("updating");
			td.title="Getting datasets...";
			AjaxUtils.remoteAjax(IspybRestShippingHandler.apiBaseUri+IspybRestShippingHandler.token+"/proposal/"+
				data.proposalname+"/mx/datacollection/sample/"+td.dataset.remoteCrystalId+"/list",
				'get',{},
				function(transport) {
					let datasets=transport.responseJSON;
					if(!datasets || !datasets.length){
						return IspybRestShippingHandler.DatasetRetrieval.succeed(td, "Complete; no datasets for crystal");
					}
					IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Datasets found", datasets);
					td.title="Found datasets: "+datasets.length;
					td.datasets=datasets;
					td.classList.remove("updating");
				},
				function (transport){
					if(404===transport.status){
						return IspybRestShippingHandler.DatasetRetrieval.succeed(td, "Complete; no datasets for crystal");
					}
					IspybRestShippingHandler.DatasetRetrieval.fail(td, "Could not get datasets for crystal. Error code: "+transport.status);
				},
				null,
				IspybRestShippingHandler.bypassProxy
			);
		},

		updateIceBearBeamlines:function (){
			if(IspybRestShippingHandler.DatasetRetrieval.stillUpdating()){
				return window.setTimeout(IspybRestShippingHandler.DatasetRetrieval.updateIceBearBeamlines, 100);
			}
			ui.setModalBoxTitle("Updating IceBear beamline(s)...");
			document.querySelectorAll("#modalBox td.hasCrystal").forEach(function(td) {
				if(td.classList.contains("complete")){ return; }
				td.datasets.forEach(function (ds){
					let beamlineName=ds["Container_beamlineLocation"];
					if(!IspybRestShippingHandler.DatasetRetrieval.beamlines[beamlineName]){
						let manufacturer="";
						let model="";
						let type="";
						if(ds["Detector_detectorManufacturer"]){ manufacturer=ds["Detector_detectorManufacturer"]; }
						if(ds["Detector_detectorModel"]){ model=ds["Detector_detectorModel"]; }
						if(ds["Detector_detectorType"]){ type=ds["Detector_detectorType"]; }
						IspybRestShippingHandler.DatasetRetrieval.beamlines[beamlineName]={
							"name":ds["Container_beamlineLocation"],
							"shipmentdestinationid":data["shipmentdestinationid"],
							"detectormanufacturer":manufacturer,
							"detectormodel":model,
							"detectortype":type
						};
					}
				});
			});
			IspybRestShippingHandler.DatasetRetrieval.waitForBeamlinesUpdated();
			IspybRestShippingHandler.DatasetRetrieval.createOrUpdateIceBearBeamlines();
		},

		createOrUpdateIceBearBeamlines:function (){
			let shipmentDestinationId=data['shipmentdestinationid'];
			Object.keys(IspybRestShippingHandler.DatasetRetrieval.beamlines).forEach(function (beamlineName){
				new AjaxUtils.Request("/api/beamline/shipmentdestinationid/"+shipmentDestinationId+"/name/"+beamlineName,{
					method:"get",
					onSuccess:function (transport){
						if(transport.responseJSON["rows"]){
							transport.responseJSON=transport.responseJSON["rows"][0];
						}
						IspybRestShippingHandler.DatasetRetrieval.beamlines[beamlineName]["id"]=transport.responseJSON["id"];
					},
					onFailure:function (transport){
						if(404!==transport.status){
							IspybRestShippingHandler.DatasetRetrieval.beamlines={};
							IspybRestShippingHandler.DatasetRetrieval.failAll("Could not get IceBear beamline record. Error: "+transport.status);
							return;
						}
						IspybRestShippingHandler.DatasetRetrieval.createOrUpdateIceBearBeamline(IspybRestShippingHandler.DatasetRetrieval.beamlines[beamlineName]);
					}
				});
			});
		},

		createOrUpdateIceBearBeamline:function (beamline){
			beamline["csrfToken"]=csrfToken;
			let uri="/api/beamline/";
			let method="post";
			let action="created";
			if(beamline["id"]){
				uri+=beamline["id"];
				method="patch";
				action="updated"
			}
			new AjaxUtils.Request(uri,{
				parameters:beamline,
				method:method,
				onSuccess:function (transport){
					let result=transport.responseJSON[action];
					IspybRestShippingHandler.DatasetRetrieval.beamlines[beamline["name"]]["id"]=result["id"];
					console.log("Setting "+[beamline["name"]]+" ID to "+result["id"])
				},
					onFailure:function (transport){
					IspybRestShippingHandler.DatasetRetrieval.beamlines={};
					IspybRestShippingHandler.DatasetRetrieval.failAll("Could not create/update IceBear beamline record. Error: "+transport.status);
				}
			});
		},

		waitForBeamlinesUpdated:function () {
			let allUpdated = true;
			if(IspybRestShippingHandler.DatasetRetrieval.beamlines){
				Object.values(IspybRestShippingHandler.DatasetRetrieval.beamlines).forEach(function (beamline) {
					if (!beamline["id"]) {
						console.log(beamline["name"]+" has no id")
						allUpdated = false;
					}
				});
			}
			if(!allUpdated){
				console.log("Waiting for IceBear beamlines to update");
				return window.setTimeout(IspybRestShippingHandler.DatasetRetrieval.waitForBeamlinesUpdated, 250);
			}
			console.log("IceBear beamlines updated");
			IspybRestShippingHandler.DatasetRetrieval.processDatasetsForAllCrystals();
		},

		processDatasetsForAllCrystals:function (){
			ui.setModalBoxTitle("Saving datasets...");
			document.querySelectorAll("#modalBox td.hasCrystal").forEach(function (td){
				if(td.classList.contains("complete")){ return; }
				td.classList.add("updating");
				IspybRestShippingHandler.DatasetRetrieval.getOrCreateIceBearDataset(td);
			});
		},

		/**
		 * For the first dataset in td.datasets, get an existing dataset. If none exists (as expected), create it.
		 * If no datasets left, succeed.
		 * @param td
		 */
		getOrCreateIceBearDataset:function (td){
			let localCrystalId=td.crystal.id;
			let remoteCrystalId=td.dataset.remoteCrystalId;
			let diffractionRequestId=td.crystal.diffractionrequestid;
			let ds=td.datasets.pop();
			if(!ds){
				IspybRestShippingHandler.DatasetRetrieval.succeed(td, "All datasets processed OK");
				return;
			}
			td.remoteDataset=ds;
			let remoteDatasetId=ds["DataCollection_dataCollectionId"];
			console.log("Got a dataset for remote crystal "+remoteCrystalId+", its datacollectiongroupid is "+ds["DataCollectionGroup_dataCollectionGroupId"]);
			let runStatus=ds["DataCollection_runStatus"];
			if(runStatus.toLowerCase().indexOf("success")===-1){
				IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Datasets ignored - status: "+runStatus, remoteDatasetId);
				console.log("Ignoring dataset with datacollectiongroupid "+ds["DataCollectionGroup_dataCollectionGroupId"]+"; run status is: "+runStatus);
				window.setTimeout(IspybRestShippingHandler.DatasetRetrieval.getOrCreateIceBearDataset, 50, td);
				return;
			}
			let imageCount=ds["totalNumberOfImages"];
			if(imageCount<=IspybRestShippingHandler.DatasetRetrieval.IMAGE_COUNT_CUTOFF){
				IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Datasets ignored - not enough images, likely a test", remoteDatasetId);
				console.log("Ignoring dataset with datacollectiongroupid "+ds["DataCollectionGroup_dataCollectionGroupId"]+"; image count is: "+imageCount+", which is below the threshold ("+IspybRestShippingHandler.DatasetRetrieval.IMAGE_COUNT_CUTOFF+")");
				window.setTimeout(IspybRestShippingHandler.DatasetRetrieval.getOrCreateIceBearDataset, 50, td);
				return;
			}
			new AjaxUtils.Request("/api/dataset/remotedatasetid/"+remoteDatasetId+"/diffractionrequestid/"+diffractionRequestId,{
				method:"get",
				onSuccess:function (transport){
					if(!transport.responseJSON){
						IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Datasets not saved - IceBear error", remoteDatasetId);
						return IspybRestShippingHandler.DatasetRetrieval.fail(td, "Error on checking for existing IceBear dataset");
					} else if(1!==transport.responseJSON["rows"].length){
						IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Datasets not saved - IceBear error", remoteDatasetId);
						return IspybRestShippingHandler.DatasetRetrieval.fail(td, "Error on checking for existing IceBear dataset - multiple datasets returned");
					}
					td.localDatasetId=transport.responseJSON["rows"][0]["id"];
					IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Datasets saved", remoteDatasetId);
					console.log("IceBear dataset with remote ID "+remoteDatasetId+" exists (IceBear ID "+td.localDatasetId+")");
					window.setTimeout(IspybRestShippingHandler.DatasetRetrieval.getAutoProcessingResults, 50, td);
				},
				onFailure:function (transport){
					if(404!==transport.status){
						return IspybRestShippingHandler.DatasetRetrieval.fail(td, "Error on checking for existing IceBear dataset");
					}
					console.log("IceBear dataset with remote ID "+remoteDatasetId+" does not exist, creating it");
					let beamline=IspybRestShippingHandler.DatasetRetrieval.beamlines[ds["Container_beamlineLocation"]];
					console.log("beamline is: "+beamline.toString());
					let parameters={
						"csrfToken":csrfToken,
						"crystalid":localCrystalId,
						"diffractionrequestid":diffractionRequestId,
						"remotedatasetid":remoteDatasetId,
						"remotedatasetobject":JSON.stringify(ds),
						"datalocation":ds["DataCollection_imageDirectory"],
						"description":"(no description)",
						"beamlineid":beamline["id"],
						"detectormanufacturer":beamline["detectormanufacturer"],
						"detectormodel":beamline["detectormodel"],
						"detectortype":beamline["detectortype"],
						"wavelength":ds["DataCollection_wavelength"]
					};
					let datetime=new Date(ds["DataCollection_startTime"]);
					if(datetime){
						datetime=datetime.toISOString().substring(0,19).replace("T"," ");
						parameters['collecteddatetime']=datetime;
					}

					new AjaxUtils.Request("/api/dataset",{
						method:"post",
						parameters:parameters,
						onSuccess:function (transport){
							td.localDatasetId=transport.responseJSON["created"]["id"];
							IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Datasets saved", remoteDatasetId);
							console.log("IceBear dataset with remote ID "+remoteDatasetId+" created (IceBear ID "+td.localDatasetId+")");
							window.setTimeout(IspybRestShippingHandler.DatasetRetrieval.getAutoProcessingResults, 50, td);
						},
						onFailure:function (transport){
							IspybRestShippingHandler.DatasetRetrieval.fail(td, "Could not create IceBear dataset record. Error code: "+transport.status);
						}
					});
				}

			});
		},

		getAutoProcessingResults:function (td){
			ui.setModalBoxTitle("Getting autoprocessing results...");
			let ds=td.remoteDataset;
			td.remoteDatasetId=ds["DataCollection_dataCollectionId"];
			AjaxUtils.remoteAjax(IspybRestShippingHandler.apiBaseUri+IspybRestShippingHandler.token+"/proposal/"+
				data.proposalname+"/mx/autoprocintegration/datacollection/"+td.remoteDatasetId+"/view",
//				data.proposalname+"/mx/autoprocintegration/datacollection/51051/view",
				'get',{},
				function(transport) {
					let results=transport.responseJSON;
					if(!results || !results.length || !results[0].length){
						//There are no autoprocessing results for the current dataset. Move on to the next.
						IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Datasets with no autoprocessing results", td.remoteDatasetId);
						window.setTimeout(IspybRestShippingHandler.DatasetRetrieval.getOrCreateIceBearDataset,50, td);
						return;
					}
					td.autoprocessingResults=results[0];
					IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Autoprocessing results found", td.autoprocessingResults);
					td.title="Found autoprocessing results: "+td.autoprocessingResults.length;
					window.setTimeout(IspybRestShippingHandler.DatasetRetrieval.createIceBearAutoProcessingResult, 50, td);
				},
				function (transport){
					if(404===transport.status){
						//There are no autoprocessing results for the current dataset. Move on to the next.
						IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Datasets with no autoprocessing results", td.remoteDatasetId);
						window.setTimeout(IspybRestShippingHandler.DatasetRetrieval.getOrCreateIceBearDataset,50, td);
						return;
					}
					IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Datasets where retrieving autoprocessing results failed", td.remoteDatasetId);
					IspybRestShippingHandler.DatasetRetrieval.fail(td, "Could not get autoprocessing results. Error code: "+transport.status);
				},
				null,
				IspybRestShippingHandler.bypassProxy
			);
		},

		createIceBearAutoProcessingResult:function (td){
			let results=td.autoprocessingResults;
			if(!results || !results.length){
				//No more autoprocessing results from this dataset. Go back and handle the next remote dataset.
				console.log(td.id.replaceAll("_"," ")+":All results processed for this dataset");
				window.setTimeout(IspybRestShippingHandler.DatasetRetrieval.getOrCreateIceBearDataset, 50, td);
				return;
			}
			let result=results.pop();
			let runStatus=result["v_datacollection_processingStatus"];
			let remoteResultId=result["v_datacollection_summary_phasing_autoProcIntegrationId"];
			if("SUCCESS"!==runStatus){
				IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Autoprocessing results ignored - status: "+runStatus, remoteResultId);
				console.log(td.id.replaceAll("_"," ")+":Ignoring autoprocessing result with status "+runStatus);
				window.setTimeout(IspybRestShippingHandler.DatasetRetrieval.createIceBearAutoProcessingResult, 50, td);
				return;
			}
			if(!result["scalingStatisticsType"]){
				IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Autoprocessing results ignored - no statistics ", remoteResultId);
				console.log(td.id.replaceAll("_"," ")+":Ignoring autoprocessing result with no statistics");
				window.setTimeout(IspybRestShippingHandler.DatasetRetrieval.createIceBearAutoProcessingResult, 50, td);
				return;
			}
			console.log(td.id.replaceAll("_"," ")+":Creating autoprocessing result with remote ID "+remoteResultId);
			let projectid=td.crystal.projectid;
			let shells=result["scalingStatisticsType"].toLowerCase().replaceAll("shell","").split(","); // likely ["overall","inner","outer"]
			let outerShellIndex=shells.indexOf("outer");
			if(-1===outerShellIndex){
				IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Autoprocessing results ignored - bad shells", remoteResultId);
				console.log(td.id.replaceAll("_"," ")+": Ignoring autoprocessing result with bad shells (no 'outerShell')");
			}
			if(3!==shells.length){
				IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Autoprocessing results ignored - bad shells", remoteResultId);
				console.log(td.id.replaceAll("_"," ")+": Ignoring autoprocessing result with bad shells (not 3)");
			} else {
				let parameters={
					"anomalousPhasing": 0,
					"spaceGroup":result["v_datacollection_summary_phasing_autoproc_space_group"].replaceAll(" ",""),
				};
				let iceBearResult={
					"projectid":projectid,
					"datasetid":td.localDatasetId,
					"isanomalous":0,
					"spacegroup":result["v_datacollection_summary_phasing_autoproc_space_group"].replaceAll(" ","_"),
					"rmerge":result["rMerge"].split(",")[outerShellIndex],
					"ioversigma":result["meanIOverSigI"].split(",")[outerShellIndex],
					"cchalf":result["ccHalf"].split(",")[outerShellIndex],
					"pipelinename":result["v_datacollection_processingPrograms"],
					"remoteid":result["v_datacollection_summary_phasing_autoProcIntegrationId"],
					"remoteobject":JSON.stringify(result)
				};
				let parameterNames=IspybRestShippingHandler.DatasetRetrieval.dataProcessingParameterNames;
				["a","b","c","alpha","beta","gamma"].forEach(function(dimension){
					let ispybName="v_datacollection_summary_phasing_cell_"+dimension;
					let iceBearName=parameterNames[ispybName];
					parameters[iceBearName]=result[ispybName];
				});

				if(result["v_datacollection_summary_phasing_anomalous"]){
					parameters["anomalousPhasing"]=1;
					iceBearResult["isanomalous"]=1;
				}

				//Get parameters that are split into shells
				shells.forEach(function(shell){
					let shellIndex=shells.indexOf(shell);
					["anomalousCompleteness","resolutionLimitHigh","resolutionLimitLow","meanIOverSigma","anomalousMultiplicity",
							"ccHalf","rMeasAllIPlusIMinus","rPimAllIPlusIMinus","rMerge",
							"completeness","multiplicity","totalObservations","uniqueObservations"].forEach(function (key){
						let ispybKey=IspybRestShippingHandler.DatasetRetrieval.iceBearDataProcessingParameterNames[key];
						let val=result[ispybKey];
						if(val===null){ return; }
						val=val.split(",")[shellIndex];
						parameters[key+"_"+shell]=val;
						if(shell==="outer"){
							if(key==="completeness"){
								iceBearResult[key]=val;
							} else if(key==="resolutionLimitHigh"){
								iceBearResult["bestresolution"]=val;
							}
						}
					});
				});

				iceBearResult["csrfToken"]=csrfToken;
				iceBearResult["parameters"]=JSON.stringify(parameters);
				AjaxUtils.request("/api/autoprocessingresult",{
					method:"post",
					parameters:iceBearResult,
					onSuccess:function (){
						IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Autoprocessing results saved", remoteResultId);
						window.setTimeout(IspybRestShippingHandler.DatasetRetrieval.createIceBearAutoProcessingResult, 50, td);
					},
					onFailure:function (transport){
						IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Autoprocessing results save failed", remoteResultId);
						IspybRestShippingHandler.DatasetRetrieval.fail(td,"Could not save autoprocessing result. Error code: "+transport.status);
					}
				});
			}
		},

		succeed:function(td, message){
			IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Crystals completed successfully", td.crystal["id"]);
			console.log(td.id.replaceAll("_"," ")+": SUCCESS: "+message);
			IspybRestShippingHandler.DatasetRetrieval.finish(td, message, "#9f9", "/images/icons/yes.gif");
		},

		fail:function(td, message){
			IspybRestShippingHandler.DatasetRetrieval.incrementCounter("Crystals completed with errors", td.crystal["id"]);
			console.log(td.id.replaceAll("_"," ")+": FAILED: "+message);
			IspybRestShippingHandler.DatasetRetrieval.finish(td, message, "#f99", "/images/icons/no.gif");
		},

		failAll:function(message){
			document.querySelectorAll("#modalBox td.hasCrystal").forEach(function (td){
				IspybRestShippingHandler.DatasetRetrieval.fail(td, message);
			});
		},

		finish:function(td, message, color, image){
			td.style.backgroundColor=color;
			td.title=message;
			td.innerHTML="";
			let img=document.createElement("img");
			img.src=image;
			td.appendChild(img);
			td.classList.remove("updating");
			td.classList.remove("processing");
			td.classList.add("complete");
			if(!document.querySelector("#modalBox td.processing")){
				window.setTimeout(function (){
					ui.setModalBoxTitle("Finished saving datasets and autoprocessing data");
				},250);
				let tr=document.createElement("tr");
				let th=document.createElement("th");
				th.innerHTML="Finished saving datasets and autoprocessing data";
				th.colSpan=2;
				tr.appendChild(th);
				document.getElementById("countsTable").appendChild(tr);
			}
		},

		//ISPyB:IceBear
		dataProcessingParameterNames:{
			"v_datacollection_processingPrograms":"programs",
			"v_datacollection_processingStatus":"processingStatus",
			"v_datacollection_processingStartTime":"processingStartTime",
			"v_datacollection_processingEndTime":"processingEndTime",

			"v_datacollection_summary_phasing_cell_a":"cellA",
			"v_datacollection_summary_phasing_cell_b":"cellB",
			"v_datacollection_summary_phasing_cell_c":"cellC",
			"v_datacollection_summary_phasing_cell_alpha":"cellAlpha",
			"v_datacollection_summary_phasing_cell_beta":"cellBeta",
			"v_datacollection_summary_phasing_cell_gamma":"cellGamma",
			"v_datacollection_summary_phasing_autoproc_space_group":"spaceGroup",

			"rMeasAllIPlusIMinus":"rMeasAllIPlusIMinus",
			"rMeasWithinIPlusIMinus":"rMeasWithinIPlusIMinus",
			"rPimAllIPlusIMinus":"rPimAllIPlusIMinus",
			"rPimWithinIPlusIMinus":"rPimWithinIPlusIMinus",

			"v_datacollection_summary_phasing_anomalous":"anomalousPhasing",

			"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.
			"anomalousCompleteness":"anomalousCompleteness",
			"resolutionLimitHigh":"resolutionLimitHigh",
			"resolutionLimitLow":"resolutionLimitLow",
			"meanIOverSigI":"meanIOverSigma",
			"anomalousMultiplicity":"anomalousMultiplicity",
			"ccHalf":"ccHalf",
			"completeness":"completeness",
			"multiplicity":"multiplicity",
			"rMerge":"rMerge",
			"anomalous":"anomalous",
			"nTotalObservations":"totalObservations",
			"nTotalUniqueObservations":"uniqueObservations"
		},

		//IceBear:ISPyB
		iceBearDataProcessingParameterNames:{}

    } //end DatasetRetrieval

}; //end IspybRestShippingHandler

let EsrfCsvMaker={

	csvColumns:'Parcel*,Storage condition*,Type - containerType,Name - containerName,Sample position,Type*,Short name,Name*,Description,Comments,Exp. Type,Min. Rot. Angle,Tot. Rot. Angle,Initial resolution,Data Collection resolution,Mesh Beam diameter,Collection Beam diameter,Radiation-sensitivity,Number of positions,Collect second high dose,High dose factor,Force Space Group,Unit cell - unit_cell,Required completeness,Required multiplicity,MR from Cell,Search models,Smiles,Cutoffs,Force S.G,Unit cell - proc_unit_cell,SAD,Reference reflection,UniProt IDs',

	begin:function() {
		let mb = ui.modalBox({title: "Send shipment to " + window.shipmentDestination.name, content: ""});
		mb.innerHTML = "<p>IceBear can generate a CSV file describing the samples in this shipment, which you can then upload to the ESRF Data Portal.</p>";
		mb.innerHTML += '<p><a target="_blank" href="https://confluence.esrf.fr/display/DATAPOLWK/Parcel">Help on uploading shipment CSV files to the ESRF Data Portal</a></p>';
		ui.buttonField({
			"onclick": EsrfCsvMaker.createCsvFile,
			"label": "Create CSV file"
		}, mb);
	},

	createCsvFile:function(){
		let mb=document.getElementById("modalBox").querySelector(".boxbody");
		let button=mb.querySelector("input");
		button.disabled=true;
		button.value="Creating CSV file...";
		let csvHeader=EsrfCsvMaker.csvColumns.split(",");
		let csvRows=[];
		let dewarTabs=document.querySelectorAll(".containertab");
		dewarTabs.forEach(function (dt) {
			let dewarName=dt.dataset.containername;
			let tabBody=document.getElementById(dewarName+"_body");
			let pucks=tabBody.querySelectorAll(".treeitem");
			pucks.forEach(function(p){
				let puck=p.record;
				let puckName=puck.name;
				let pinRows=p.querySelectorAll("tr.datarow");
				pinRows.forEach(function(pinRow){
					let pin=pinRow.rowData;
					if("dummy"===pin || 1===1*pin.isEmpty){ return; }
					let pinName=pin.name;
					if(pinName.startsWith("dummypin")){ pinName=""; }
					let crystal=pin.childitems[1];
					let localUrl=window.document.location.protocol+"//"+window.document.location.hostname+"/crystal/"+crystal.id;
					csvRows.push({
						"Parcel*":dewarName,
						"Storage condition*":"At room temperature", //The dewar, not its contents
						"Type - containerType":"UNIPUCK",
						"Name - containerName":puckName,
						"Sample position":pin.position,
						"Type*":"SAMPLE",
						"Short name":crystal.proteinacronym,
						"Name*":crystal.name,
						"Comments":"IceBear "+localUrl
					});
				})
			});
		});
		let filename="Shipment_"+new Date().toISOString().replaceAll(":","-").replaceAll("T","_").substring(0,19)+".csv";
		new Ajax.Request("/api/file",{
			method:"post",
			parameters:{
				"csrfToken":csrfToken,
				"parentid":data["id"],
				"description":"Sample information CSV",
				"filename":filename,
				"csvHeaders":EsrfCsvMaker.csvColumns,
				"csvRows":JSON.stringify(csvRows)
			},
			onSuccess: EsrfCsvMaker.onSuccess,
			onFailure: EsrfCsvMaker.onFailure
		});
	},

	onSuccess: function(xhr){
		let mb=document.getElementById("modalBox").querySelector(".boxbody");
		let button=mb.querySelector("input");
		button.remove();
		let file=xhr.responseJSON["created"];
		let downloadUrl="/api/file/"+file["id"]+"/"+file["filename"];
		ui.logToDialog("Shipment CSV file created","success");
		ui.logToDialog('You can <a href="'+downloadUrl+'">download the file here</a>, or find it in the Files tab.');
		document.getElementById("files").refresh();
	},

	onFailure: function(xhr){
		let mb=document.getElementById("modalBox").querySelector(".boxbody");
		let button=mb.querySelector("input");
		button.remove();
		let err=xhr.responseText;
		if(xhr.responseJSON && xhr.responseJSON.error){
			err=xhr.responseJSON.error;
		}
		ui.logToDialog("Could not create shipment CSV file","error");
		ui.logToDialog("The server said: "+err);
	}

}