<?php
/**
 * This importer copies plate inspections and their associated images from Formulatrix' RockMaker
 * into the LIMS for onward processing. It creates associated records in the LIMS, such as
 * imagers and plate types, where needed.
 *
 * This importer is NOT suitable for Formulatrix sites without RockMaker. At such sites, the LIMS
 * must replace RockMaker completely, including plate scheduling, etc.
 *
 * A Formulatrix imaging task can have several images per drop, with different imaging settings or
 * even completely different cameras (UV versus visible, for example). In order to simplify things
 * on the LIMS side, the Formulatrix imaging task is split into one or more imagingsessions that
 * are separated by a time interval (in the order of seconds); this ensures that no drop
 * has more than one image at a given time in the LIMS.
 *
 * Command-line arguments:
 *
 * -h Show help
 * -d2016-02-28 - Begin import from this date (default date of last imported inspection)
 * -iRI54-0039 - Import only for this imager (default all imagers)
 * -b909s - (Re)import images for this barcode
 * -s5 - Skip this many inspections at the beginning (Default 0)
 * -m20 - Import at most this many inspections (default 10)
 * -l1 - Set logging level. 1=debug, 2=info, 3=warn, 4=error. (Default 2)
 *
 * A note on time and time zones:
 *
 * It appears that RockMaker's database holds LOCAL, not UTC, times in DateImaged, with no offset
 * information.  These times come, as I understand it, not from the database server but from the
 * controller PC for the relevant imager. (This allows the imager controller PC to buffer images locally
 * in event of a network outage, writing the correct time into the database when the network comes back.)
 *
 * The controller PC may or may not be in the same time zone as the server (Oulu's RI2 controller was
 * installed on New York time!) and may or may not observe DST; if it does observe DST, it may or may not
 * switch at the same time as the server, which also may or may not observe it. Even if they all play by
 * the same rules, their clocks may or may not be synchronised. By default, Windows synchronises clocks
 * only ONCE PER WEEK, during which we have seen imagers drift by as much as 10 minutes.
 *
 * All in all, it does make life difficult for this importer. Realistically, the best you can do under the
 * circumstances is to calculate the server's local GMT offset and apply that to DateImaged, hoping
 * blindly that all the Formulatrix boxes have roughly the same idea of what time it is. For historical
 * data this may be an hour (possibly two) adrift depending on how much fun DST happened to be that day.
 * The normal operating scenario is to import imaging tasks shortly after they happen, so they will be
 * mostly right (except right around the DST change, if there is one). In theory the DST change mean some
 * missed inspections once per year; under the circumstances, that's not the worst outcome. Re-importing
 * the affected plate(s) with the -b option should overcome it.
 *
 * The approach taken by this imager during normal operation is to import any inspections after the time
 * of the latest one in IceBear. However, with clock drift, DST, possible time zone inaccuracy, etc., this
 * can result in inspections getting missed - Imager A's clock is 10 minutes slow and images a plate 5 minutes
 * after Imager B, for example. We therefore apply a fudge factor (180 minutes at time of writing), checking
 * this far back from the most recent IceBear inspection for un- or partially-imported inspections.
 *
 * An alternative approach would be to import per imager, but this would require occasional checks to ensure
 * that no new imagers had been added. The fudge is easier and probably cleaner to implement.
 */

use JetBrains\PhpStorm\NoReturn;

set_time_limit(0);
setUpClassAutoLoader();

//Shut up warnings at UTU
error_reporting(E_ERROR | E_PARSE);

const CLOCK_DRIFT_FUDGE_MINUTES = 180;

const IMAGINGSESSIONS_PER_RUN = 5;
const MINIMUM_INSPECTION_AGE_MINS = 10;

//These are just defaults. Imagers are imported rarely, and these numbers can be changed later,
//so no need for a separate config item.
const IMAGERLOAD_ALERT_THRESHOLD_PERCENT = 75;
const IMAGERLOAD_WARNING_THRESHOLD_PERCENT = 90;

const IMAGER_NAME_UNKNOWN = 'Unknown Imager';

$logLevel=Log::LOGLEVEL_INFO; //may be overridden by argument

//Formulatrix: 0=visible, 1=UV. Probably additional values for SONICC, etc. Not defined in DB.
$lightPaths=array('Visible','UV');

$limitStart=0;
$limitTotal=IMAGINGSESSIONS_PER_RUN;
$fromDate=null;
$imager=null;
$barcode=null;
for($i=1;$i<count($argv);$i++){
	$arg=$argv[$i];
	if($arg == '-h'){
		showHelp();
	} else if(preg_match('/^-l[1-4]$/',$arg)){
		$logLevel=(int)substr($arg, 2);
	} else if(preg_match('/^-s\d+$/',$arg)){
		$limitStart=(int)substr($arg, 2);
	} else if(preg_match('/^-m\d+$/',$arg)){
		$limitTotal=(int)substr($arg, 2);
	} else if(preg_match('/^-i.+$/',$arg)){
		$imager=substr($arg, 2);
	} else if(preg_match('/^-b.+$/',$arg)){
		$barcode=substr($arg, 2);
		$limitTotal=10000;
	} else if(preg_match('/^-d\d\d\d\d-\d\d-\d\d+$/',$arg)){
		$fromDate=substr($arg,2);
	}
}


ini_set('mssql.secure_connection', 'on');

//Constants for Formulatrix' various imaging states. These are not defined in the RI or RM databases, you just have to know them.
const IMAGING_STATE_SCHEDULED = 1;    //"Not completed" - planned but not yet started, may or may not be overdue
const IMAGING_STATE_SKIPPED = 2;        //"Skipped" - RockImager decided not to do it
const IMAGING_STATE_PENDING = 3;        //"Pending" - Imaging task is overdue and will be done next, as soon as imager is free
const IMAGING_STATE_QUEUED = 4;        //"Queued" - Imaging task is about to begin (plate is probably moving to camera)
const IMAGING_STATE_IMAGING = 5;        //"Imaging" - Imaging task in progress, the plate is being imaged
const IMAGING_STATE_COMPLETED = 6;    //"Completed" - Imaging task has been done. Last image has been taken, but plate may not be back in the hotel yet.
const IMAGING_STATE_CANCELLED = 7;    //"Cancelled" - Imaging task was cancelled manually while in progress.

Log::init($logLevel);
Log::write(Log::LOGLEVEL_INFO,'Importer started');

Log::write(Log::LOGLEVEL_DEBUG,'Attempting to get LIMS DB connection... ');
	database::connect();
Log::write(Log::LOGLEVEL_DEBUG,'...got LIMS DB connection');

Log::write(Log::LOGLEVEL_DEBUG,'Attempting to get LIMS session... ');
	session::init(new DummySession());
	session::set('isAdmin',true);
Log::write(Log::LOGLEVEL_DEBUG,'...got LIMS session');

$sharedProjectId=null;


try {

	database::begin();

    $limsStorage=config::get('core_imagestore');
    $limsStorage=rtrim($limsStorage,'/\\').'/';
    $lockFile=rtrim(config::get('core_filestore'),'/').'/fx_importer_running';
    $lockFileRemoveAfterSeconds=7200; // 2 hours
    if(file_exists($lockFile)){
        Log::write(Log::LOGLEVEL_WARN, 'Formulatrix importer lock file exists at '.$lockFile);
        if(time()-filemtime($lockFile) > $lockFileRemoveAfterSeconds){ //Old lockfile. Toast it and try again.
            Log::write(Log::LOGLEVEL_WARN, 'Lock file is more than '. $lockFileRemoveAfterSeconds.' seconds old.');
            Log::write(Log::LOGLEVEL_WARN, 'Removing the lock file and continuing.');
            if(!@unlink($lockFile)){
                throw new Exception('Could not delete lock file.');
            }
        } else { //New lockfile. Respect it, and abort.
            Log::write(Log::LOGLEVEL_WARN, 'Lock file is less than '. $lockFileRemoveAfterSeconds.' seconds old.');
            Log::write(Log::LOGLEVEL_WARN, 'Another importer process may be running. Aborting.');
            Log::write(Log::LOGLEVEL_INFO,'=================================================');
            Log::end();
            exit();
        }
    }
    if(!@touch($lockFile)){
        throw new Exception('Could not create lock file.');
    }

    //Check for existence of a database backup lock file.
    //If the database is locked for backup, importing won't work.
    if(file_exists($limsStorage.'dbbackuprunning')){
        Log::write(Log::LOGLEVEL_WARN, 'Database backup lock file exists at '.$limsStorage.'dbbackuprunning');
        Log::write(Log::LOGLEVEL_WARN, 'If the database is locked for backup, there is no point trying to import.');
        Log::write(Log::LOGLEVEL_WARN, 'If this condition persists, investigate possible database backup failure.');
        throw new Exception('Not importing due to database backup lock file');
    }

    $fxRoot=config::get('fx_imagestoremountpoint');
	if(null==$fxRoot){
        $fxRoot='/mnt/Formulatrix/';
        Log::write(Log::LOGLEVEL_WARN, 'Config item fx_imagestoremountpoint does not exist.');
        Log::write(Log::LOGLEVEL_WARN, 'Assuming /mnt/Formulatrix/ and setting a config item.');
        Log::write(Log::LOGLEVEL_WARN, 'Import will fail if this is not the correct mount point.');
        Log::write(Log::LOGLEVEL_WARN, 'Typically image stores should be mounted at, for example, /mnt/Formulatrix/RockMakerStorage.');
        Log::write(Log::LOGLEVEL_WARN, 'This config item should be set to the directory above RockMakerStorage on the IceBear filesystem.');
        Log::write(Log::LOGLEVEL_WARN, 'You can change this at http(s)//your.icebear/config on the Imagers tab.');
        Log::write(Log::LOGLEVEL_INFO, 'Adding config item...');
        database::query('INSERT INTO config(`name`,`description`,`type`,`defaultvalue`,`value`) VALUES (:name,:description,:type,:defaultvalue,:value)',
            array(
                ':name'=>"fx_imagestoremountpoint",
                ':description'=>"Where Formulatrix image stores are mounted on the IceBear server",
                ':type'=>"text",
                ':defaultvalue'=>$fxRoot,
                ':value'=>$fxRoot
            )
        );
        Log::write(Log::LOGLEVEL_INFO, '...done.');
        Log::write(Log::LOGLEVEL_DEBUG, 'Committing database transaction...');
        database::commit();
        Log::write(Log::LOGLEVEL_DEBUG, '...done. Beginning new database transaction...');
        database::begin();
        Log::write(Log::LOGLEVEL_DEBUG, '...done.');
    }
	$fxRoot=rtrim($fxRoot,'/').'/';


    Log::write(Log::LOGLEVEL_DEBUG, 'Checking for config item fx_copytoicebearstore...');
    $copyToIceBearStore=config::get('fx_copytoicebearstore');
    if(null===$copyToIceBearStore){
        Log::write(Log::LOGLEVEL_WARN, 'Config item fx_copytoicebearstore does not exist.');
        Log::write(Log::LOGLEVEL_WARN, 'Importer cannot determine whether to copy images to the IceBear store.');
        Log::write(Log::LOGLEVEL_WARN, 'Will add fx_copytoicebearstore to the config table, set to TRUE (1). The importer will then stop.');
        Log::write(Log::LOGLEVEL_WARN, 'If you do NOT want to copy images to the IceBear store, change this value to FALSE (0) before running the importer.');
        Log::write(Log::LOGLEVEL_WARN, 'You can do this at http(s)//your.icebear/config on the Imagers tab.');
        Log::write(Log::LOGLEVEL_INFO, 'Adding config item...');
        database::query('INSERT INTO config(`name`,`description`,`type`,`defaultvalue`,`value`) VALUES ("fx_copytoicebearstore","Copy images to IceBear store","boolean","1","1")');
        Log::write(Log::LOGLEVEL_INFO, '...done.');
        Log::write(Log::LOGLEVEL_DEBUG, 'Committing database transaction...');
        database::commit();
        Log::write(Log::LOGLEVEL_DEBUG, '...done. Beginning new database transaction...');
        database::begin();
        Log::write(Log::LOGLEVEL_DEBUG, '...done.');

        Log::write(Log::LOGLEVEL_ERROR, 'Importer will stop. You can safely ignore this and following errors.');
        throw new Exception('Created config item fx_copytoicebearstore, aborting.');
    }
    $copyToIceBearStore=(int)$copyToIceBearStore;
    Log::write(Log::LOGLEVEL_DEBUG, 'Done. fx_copytoicebearstore is '.$copyToIceBearStore);


    $user=user::getByName('formulatriximporter');
	if(empty($user)){
		$user=user::create(array(
				'name'=>'formulatriximporter',
				'fullname'=>'Formulatrix Importer',
				'email'=>'fximport@null.null',
				'password'=>'USELESSPASSWORD',
				'isactive'=>0
		));
		$user=$user['created'];
	}
	session::init(new DummySession());
	session::set('userId', $user['id']);
	session::set('isAdmin', true);

	//Force all projects into session, despite admin status. Issues at UTU discovered 9/2025.
	$projectIds=database::queryGetAll('SELECT id FROM project');
	if(empty($projectIds)){
		throw new Exception('No project IDs found');
	}
	$projectIds=array_column($projectIds['rows'], 'id');
	session::set('projectPermissions',[
		'read'=>$projectIds,
		'readOne'=>$projectIds,
		'create'=>$projectIds,
		'update'=>$projectIds,
		'delete'=>$projectIds,
	]);

	$importerStarted=config::get('fx_importerrunning');
	if(null!==$importerStarted && !$importerStarted){
		throw new Exception('Not importing because importer paused. Set "Importer running" in IceBear config.');
	}


	$hostname=trim(config::get('fx_dbhost'));
	$port=trim(config::get('fx_dbport'));
	$username=trim(config::get('fx_dbuser'));
	$pw=trim(config::get('fx_dbpass'));
	$rmdbname=trim(config::get('fx_rmdbname'));
	$ridbname=trim(config::get('fx_ridbname'));

	//Check in the config for a start date from which to import
	if(!$fromDate && !empty(config::get('fx_importfromdate'))){
		$fromDate=config::get('fx_importfromdate');
	}

	//Connect to RockImager and RockMaker databases
// 	Log::write(Log::LOGLEVEL_DEBUG,'Attempting Formulatrix DB connections... ');
// 	$rm=null;
// 	$rm = new PDO ("sqlsrv:Server=$hostname, $port; Database=$rmdbname","$username","$pw");
// 	$rm->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
// 	Log::write(Log::LOGLEVEL_DEBUG,'...got DB connection');
// 	Log::write(Log::LOGLEVEL_DEBUG,'Attempting Formulatrix RockImager DB connection... ');
// 	$ri=null;
// 	$ri = new PDO ("sqlsrv:Server=$hostname, $port; Database=$ridbname","$username","$pw");
// 	$ri->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
// 	Log::write(Log::LOGLEVEL_DEBUG,'...got DB connection');

	$rm=null;
	$ri=null;
	$drivers=PDO::getAvailableDrivers();
	Log::write(Log::LOGLEVEL_DEBUG, 'Available PDO drivers: '.implode(',',$drivers));
	$message='';
	if(in_array('dblib', $drivers)){
		try {
		    Log::write(Log::LOGLEVEL_DEBUG, 'Connecting to Formulatrix with dblib...');
			$rm=new PDO("dblib:host=$hostname:$port;dbname=RockMaker","$username","$pw");
			$rm->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
			$ri=new PDO("dblib:host=$hostname:$port;dbname=RockImager","$username","$pw");
			$ri->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
		} catch(Exception $e){
			//dblib: attempt failed
			$message.=PHP_EOL."Attempting with dblib gave: ".$e->getMessage();
		}
	} else if(in_array('sqlsrv', $drivers)){
		try {
		    Log::write(Log::LOGLEVEL_DEBUG, 'Connecting to Formulatrix with sqlsrv...');
			$rm = new PDO ("sqlsrv:Server=$hostname,$port; Database=$rmdbname",$username,$pw);
			$rm->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
			$ri = new PDO ("sqlsrv:Server=$hostname,$port;Database=$ridbname",$username,$pw);
			$ri->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
		} catch(Exception $e){
			//sqlsrv: attempt failed
			$message.=PHP_EOL."Attempting with sqlsrv gave: ".$e->getMessage();
		}
	} else {
		$message.=PHP_EOL.'Neither dblib nor sqlsrv PDO drivers available';
	}
    if((!$rm || !$ri) && in_array('dblib', $drivers)){
        //Fix for issue at UTU, required TDS version to be specified
        try {
            Log::write(Log::LOGLEVEL_DEBUG, 'Connecting to Formulatrix with dblib...');
            $rm=new PDO("dblib:version=8.0;host=$hostname:$port;dbname=RockMaker","$username","$pw");
            $rm->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
            $ri=new PDO("dblib:version=8.0;host=$hostname:$port;dbname=RockImager","$username","$pw");
            $ri->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
        } catch(Exception $e){
            //dblib: attempt failed
            $message.=PHP_EOL."Attempting with dblib and TDS 8.0 gave: ".$e->getMessage();
        }
    }
    if(!$rm || !$ri){
		throw new Exception('Could not connect to Formulatrix databases. '.$message);
	}


	Log::write(Log::LOGLEVEL_DEBUG,'...got Formulatrix DB connections');



	//Find some common prerequisites

	$sharedProject=project::getByName('Shared');
	if(empty($sharedProject)){ throw new Exception('Could not find "Shared" project in LIMS'); }
	$sharedProjectId=$sharedProject['id'];

	$defaultProject=project::getByName('Default Project');
	if(empty($defaultProject)){ throw new Exception('Could not find "Default Project" in LIMS'); }
	$defaultProjectId=$defaultProject['id'];

	//$unknownUsernames is a comma-separated list of RockMaker usernames that are known to be defaults.
	//They should not be plate owners, but often are because users forget to select themselves.
	//We assign their plates to username $defaultPlateOwner in the LIMS, and an admin has to sort it out.
	$unknownUsernames='_*** CHANGE THIS!!!! ***_, Default User, Administrator, FormulatrixAdmin';
	$defaultPlateOwnerUsername='unknownuser';
	$unknownUsernames=explode(',',$unknownUsernames);
	foreach($unknownUsernames as &$u){
		$u=trim($u);
	}
	$defaultPlateOwner=user::getByName($defaultPlateOwnerUsername);
	if(empty($defaultPlateOwner)){
		$defaultPlateOwner=user::create(array(
			'name'=>$defaultPlateOwnerUsername,
			'fullname'=>'Unknown User',
			'email'=>'unknownuser@null.null',
			'password'=>'USELESSPASSWORD',
			'isactive'=>0
		));
		$defaultPlateOwner=$defaultPlateOwner['created'];
	}
	if(empty($defaultPlateOwner)){
		throw new Exception('Could not find or create default user ['.$defaultPlateOwnerUsername.']');
	}
	$defaultPlateOwnerId=$defaultPlateOwner['id'];

	$scoringSystem=crystalscoringsystem::getByName('Custom');
	if(!$scoringSystem){
		$scoringSystem=crystalscoringsystem::getByName('Formulatrix');
		if(!$scoringSystem){
			$scoringSystem=importScoringSystem();
		}
	}
	if(!$scoringSystem){
		throw new Exception("Could not find or import Formulatrix scoring system");
	}

	database::commit();

	//Decide what to import
    $lastImportDateTime=getTimeOfNewestLimsImagingSession();

	if(null!=$barcode){

		$imagingTasks=getFormulatrixImagingTasksForPlateBarcode($barcode);

	} else if (null!=$fromDate){

		//get the ImagingTasks
		//Time zone: See header comment for info on local versus GMT times in RockMaker.
		//TODO $fromDate is not corrected for GMT
		//Just interested in preventing very recent inspections from importing, so comparing (local) DateImaged to (local) GETDATE() is good enough.
		$stmt=$rm->prepare('SELECT * FROM ImagingTask WHERE DateImaged>=:startdate AND DateImaged<=DATEADD(minute, -5, GETDATE()) ORDER BY DateImaged ASC');
		$stmt->execute(array(':startdate'=>$fromDate));
		$imagingTasks=$stmt->fetchAll(PDO::FETCH_ASSOC);

	} else if (null!=$imager){

		throw new Exception('Per-imager import not implemented yet');

	} else {

		if(null==$lastImportDateTime){
			Log::write(Log::LOGLEVEL_WARN, 'Cannot find latest inspection in LIMS.');
			Log::write(Log::LOGLEVEL_WARN, 'Re-run with "from date" option, e.g, "-d2015-12-31".');
			$imagingTasks=array();
		} else {
		    Log::write(Log::LOGLEVEL_DEBUG, 'Newest inspection in LIMS is at '.$lastImportDateTime);
		    Log::write(Log::LOGLEVEL_DEBUG, 'Looking '.CLOCK_DRIFT_FUDGE_MINUTES.' minutes before then, allowing for clock drift.');
		    $lastImportDateTime=new DateTime($lastImportDateTime, new DateTimeZone('UTC')); //LIMS is assumed to use UTC.
		    $lastImportDateTime->sub(new DateInterval('PT'.CLOCK_DRIFT_FUDGE_MINUTES.'M'));
		    $lastImportDateTime=$lastImportDateTime->format('Y-m-d H:i:s');
		    Log::write(Log::LOGLEVEL_DEBUG, 'Looking for inspections after '.$lastImportDateTime);
		    //Time zone: For preventing very recent inspections from importing, comparing (local) DateImaged to (local) GETDATE() is good enough.
			//However, we need to convert the GMT last imaged date in the LIMS to RockMaker database local time when deciding what to import.
			$stmt=$rm->prepare('SELECT * FROM ImagingTask 
					WHERE DATEADD(second, DATEDIFF(second, GETDATE(), GETUTCDATE()), DateImaged)>=:dateimaged 
					AND DateImaged<=DATEADD(minute, -5, GETDATE()) ORDER BY DateImaged ASC');
			$stmt->execute(array(':dateimaged'=>$lastImportDateTime));
			$imagingTasks=$stmt->fetchAll(PDO::FETCH_ASSOC);
		}
	}


	/*
	 * MSSQL "limit" is exceedingly messy. Take the performance hit, pull them all in above and deal with it in PHP.
	 * In regular operation it is likely that far fewer than 10 rows will be returned anyway. This will
	 * only hurt when importing a lot of historical inspections.
	 */
	$imagingTasks=array_splice($imagingTasks, $limitStart,$limitTotal);

    $numInspectionsImported=0;
	if(empty($imagingTasks)){
		Log::write(Log::LOGLEVEL_WARN, 'No imaging tasks to import');
	} else {
	    Log::write(Log::LOGLEVEL_DEBUG, 'Found '.count($imagingTasks).' imaging tasks to import');
		foreach($imagingTasks as $it){
			database::begin();
			$imported=importImagingTask($it);
			if($imported){
			    $numInspectionsImported++;
			}
			database::commit();
			Log::write(Log::LOGLEVEL_INFO,'-------------------------------------------------');

			if($numInspectionsImported>=$limitTotal){
			    Log::write(Log::LOGLEVEL_DEBUG, $numInspectionsImported.'>='.$limitTotal);
			    break;
			}
		}

        if(config::get('fx_importfromdate')){
		    config::set('fx_importfromdate','');
        }
	}

    database::begin();
    updateImagerInventories();
    database::commit();

    Log::write(Log::LOGLEVEL_INFO,'-------------------------------------------------');
    Log::write(Log::LOGLEVEL_INFO, 'Checking the plate re-import queue');
    $queueItem=database::queryGetOne('SELECT id, plateid FROM platereimportqueue ORDER BY queuedtime ASC');
    if(!$queueItem){
        Log::write(Log::LOGLEVEL_INFO, 'Plate re-import queue is empty.');
    } else {
        $plateId=$queueItem['plateid'];
        Log::write(Log::LOGLEVEL_INFO, 'Found plate in re-import queue. Plate ID is '.$plateId);
        $plate=plate::getById($plateId);
        if(!$plate){
            Log::write(Log::LOGLEVEL_ERROR, 'Plate with that ID does not exist in LIMS');
            Log::write(Log::LOGLEVEL_ERROR, 'Queue entries for that plate ID deleted.');
        } else {
            $barcode=$plate['name'];
            Log::write(Log::LOGLEVEL_INFO, 'Plate barcode is '.$barcode);
            $imagingTasks=getFormulatrixImagingTasksForPlateBarcode($barcode); //will not return super-recent, don't need to filter
            if(empty($imagingTasks)){
                Log::write(Log::LOGLEVEL_INFO, 'No imaging tasks for re-import plate '.$barcode);
            } else {
                Log::write(Log::LOGLEVEL_INFO, count($imagingTasks).' imaging tasks for re-import plate '.$barcode);
                foreach ($imagingTasks as $it){
                    database::begin();
                    $imported=importImagingTask($it);
                    if($imported){
                        $numInspectionsImported++;
                    }
                    database::commit();
                    Log::write(Log::LOGLEVEL_INFO,'-------------------------------------------------');
                }
            }

        }
        //Whether the plate exists or not, we remove the entry from the queue. Only try re-importing once.
        database::query('DELETE FROM platereimportqueue WHERE plateid=:pid', array(':pid'=>$plateId));
    }

	unset($rm); unset($ri); unset($stmt);

	Log::write(Log::LOGLEVEL_INFO,'Importer finished.');
    Log::write(Log::LOGLEVEL_DEBUG,'Removing lock file...');
    if(!@unlink($lockFile)){
        throw new Exception('Could not delete lock file.');
    }
    Log::write(Log::LOGLEVEL_DEBUG,'...done.');
    Log::write(Log::LOGLEVEL_INFO,'=================================================');
	Log::end();

} catch(Exception $e){
	Log::write(Log::LOGLEVEL_ERROR, get_class($e).' caught: '.$e->getMessage());
	$trace=$e->getTrace();
	foreach($trace as $t){
		Log::write(Log::LOGLEVEL_ERROR, ': '.$t['file']);
		if(isset($t['type']) && isset($t['class'])){
			Log::write(Log::LOGLEVEL_ERROR, ':    Line '.$t['line'].', Function '.$t['class'].$t['type'].$t['function']);
		} else {
			Log::write(Log::LOGLEVEL_ERROR, ':    Line '.$t['line'].', Function '.$t['function']);
		}
		//TODO $t['args']
	}
	database::abort();
	Log::write(Log::LOGLEVEL_ERROR, 'Import aborted due to exception');
	Log::write(Log::LOGLEVEL_INFO,'=================================================');
	Log::end();
}


/**
 * Creates one or more plate inspections in the LIMS, from the Formulatrix/RockMaker ImagingTask with the specified ID.
 * If the ImagingTask has more than one CaptureProfile (for example, both visible and UV images), one LIMS plate inspection
 * will be created for each, with the inspections slightly separated in time. This ensures that there is only one image per
 * drop per time point in the LIMS.
 * @param array $imagingTask The RockMaker ImagingTask
 * @return bool
 * @throws BadRequestException
 * @throws ForbiddenException
 * @throws NotFoundException
 * @throws ServerException
 */
function importImagingTask(array $imagingTask): bool {

	global $rm, $ri, $lightPaths, $copyToIceBearStore, $fxRoot;

    $state=$imagingTask['State'];
    $taskId=$imagingTask['ID'];
    if($state==IMAGING_STATE_SKIPPED){
        Log::write(Log::LOGLEVEL_WARN, 'Imaging task '.$taskId.' was Skipped, nothing to import');
        return false;
    } else if($state==IMAGING_STATE_CANCELLED){
        Log::write(Log::LOGLEVEL_WARN, 'Imaging task '.$taskId.' was cancelled while in progress. It may or may not have images.');
    } else if($state!=IMAGING_STATE_COMPLETED){
        Log::write(Log::LOGLEVEL_INFO, 'Imaging task '.$taskId.' is not marked Completed, checking for later tasks on same imager');
        $barcode=getPlateBarcodeForImagingTask($taskId);
        $imagerName=getImagerNameForImagingTask($taskId, $barcode);
        if(IMAGER_NAME_UNKNOWN==$imagerName){
            Log::write(Log::LOGLEVEL_WARN, 'Could not determine imager for this task.');
        } else {
            Log::write(Log::LOGLEVEL_INFO, 'Imager for this task was '.$imagerName);
        }
    }

	Log::write(Log::LOGLEVEL_INFO, 'Importing imaging task, ID '.$taskId);

	$limsImageCount=0;
	$limsTask=imagingsession::getByProperty('manufacturerdatabaseid', $taskId);
	if($limsTask){
		//This is a re-import, assume the metadata is OK and just look for new images
	    Log::write(Log::LOGLEVEL_INFO, 'Imaging task with Formulatrix ID '.$taskId.' already imported, will check for new images');
	    Log::write(Log::LOGLEVEL_DEBUG, 'Imaging task with Formulatrix ID '.$taskId.' is '.count($limsTask['rows']).' imagingsession(s) in LIMS');
	    foreach($limsTask['rows'] as $lt){
	        $limsTaskId=$lt['id'];
	        $limsTaskImages=dropimage::getByProperty('imagingsessionid', $limsTaskId, array('all'=>1));
	        if($limsTaskImages){
	            Log::write(Log::LOGLEVEL_DEBUG, 'LIMS imagingsession with ID '.$limsTaskId.' has '.count($limsTaskImages['rows']).' images');
	            $limsImageCount+=count($limsTaskImages['rows']);
	        } else {
	            Log::write(Log::LOGLEVEL_DEBUG, 'LIMS imagingsession with ID '.$limsTaskId.' has no images');
	        }
	    }
	    Log::write(Log::LOGLEVEL_INFO, 'Total images in LIMS for Formulatrix task '.$taskId.': '.$limsImageCount);
	}

	/*
	 * Get imaging task information.
	 * See header comment for info on local vs GMT in RockMaker.
	 */

	//$stmt=$rm->prepare('SELECT CONVERT(VARCHAR(19), ImagingTask.DateImaged, 126) AS DateImaged,
	$stmt=$rm->prepare('SELECT CONVERT(VARCHAR(19), ImagingTask.DateImaged, 126) AS DateImagedLocal, 
			CONVERT(VARCHAR(19), DATEADD(second, DATEDIFF(second, GETDATE(), GETUTCDATE()), DateImaged), 126) AS DateImagedUtc,
			ImageBatch.ID AS ImageBatchID, Plate.ID AS PlateID, Plate.Barcode AS Barcode
			FROM ImagingTask, ImageBatch, ExperimentPlate, Plate 
			WHERE ExperimentPlate.ID=ImagingTask.ExperimentPlateID 
			AND ImageBatch.ImagingTaskID=ImagingTask.ID
			AND ExperimentPlate.PlateID=Plate.ID
			AND ImagingTask.ID=:taskid
	'); //CONVERT to varchar(19) to lop off the milliseconds. Date looks like 2011-12-13T12:42:55 after conversion.
	$stmt->execute(array(':taskid'=>$taskId));
	$imagingTask=$stmt->fetch(PDO::FETCH_ASSOC);
	if(!$imagingTask){
		Log::write(Log::LOGLEVEL_WARN, 'No matching ImageBatch for ImagingTask '.$taskId.', so cannot locate images. Not importing this ImagingTask.');
		return false;
	}

	/*
	 * Are there any images for this ImagingTask? It appears that there are some, even marked as Completed, that do not have any. I don't know all the reasons
	 * why this can happen, but certainly in older versions of RockImager, if the USB stick containing the MIL (Matrox Imaging Library) license works loose
	 * you'll see a plate get to the camera, a license exception thrown, and the ImagingTask marked as Completed.
	 *
	 * We should check this first, before importing pre-requisites that may not be needed, and abort import of this ImagingTask if need be.
	 */
	$stmt=$rm->prepare('SELECT ID FROM ImageType WHERE ShortName=\'ef\'');
	$stmt->execute(array());
	$result=$stmt->fetch(PDO::FETCH_ASSOC);
	$imageTypeId=$result['ID'];
	Log::write(Log::LOGLEVEL_DEBUG, 'Image type ID for extended-focus images: '.$imageTypeId);
	$stmt=$rm->prepare('SELECT ID FROM RegionType WHERE Name=\'Drop\'');
	$stmt->execute(array());
	$result=$stmt->fetch(PDO::FETCH_ASSOC);
	$regionTypeId=$result['ID'];
	Log::write(Log::LOGLEVEL_DEBUG, 'Region type ID for whole-drop images: '.$regionTypeId);
	$stmt=$rm->prepare('SELECT ImageStore.BasePath, ImageStore.Name AS ImageStoreName, ImageBatch.ID AS ImageBatchID, CaptureResult.ID AS CaptureResultID,
			CaptureProfileVersion.CaptureProfileID AS CaptureProfileID, CaptureProfileVersion.ID AS CaptureProfileVersionID,
			Region.ID AS RegionID, Plate.Barcode, Well.PlateID, Well.WellNumber, Well.RowLetter, Well.ColumnNumber, WellDrop.DropNumber,
			Image.PixelSize, CaptureProfile.Name AS CaptureProfileName 
		FROM Image, ImageStore, CaptureResult, CaptureProfile, CaptureProfileVersion, ImageBatch, ImagingTask, Region, WellDrop, Well, Plate
		WHERE Image.ImageTypeID=:imagetypeid
			AND Image.ImageStoreID=ImageStore.ID
			AND Image.CaptureResultID=CaptureResult.ID
			AND CaptureResult.ImageBatchID=ImageBatch.ID
			AND CaptureResult.RegionID=Region.ID
			AND Region.RegionTypeID=:regiontypeid
			AND CaptureResult.CaptureProfileVersionID=CaptureProfileVersion.ID
		    AND CaptureProfileVersion.CaptureProfileID=CaptureProfile.ID 
			AND Region.WellDropID=WellDrop.ID
			AND WellDrop.WellID=Well.ID
			AND Well.PlateID=Plate.ID
			AND ImageBatch.ImagingTaskID=ImagingTask.ID
			AND ImagingTask.ID=:taskid
		ORDER BY CaptureProfileID, Well.ID ASC
	');
	$stmt->execute(array(':taskid'=>$taskId, ':imagetypeid'=>$imageTypeId, ':regiontypeid'=>$regionTypeId));
	$images=$stmt->fetchAll(PDO::FETCH_ASSOC);

	if(empty($images)){
		Log::write(Log::LOGLEVEL_WARN, 'No images found for ImagingTask ID  '.$taskId.', not importing it');
		return false;
	}
	Log::write(Log::LOGLEVEL_INFO, count($images).' images found for ImagingTask ID  '.$taskId);

	if(count($images)<=$limsImageCount){
	    Log::write(Log::LOGLEVEL_INFO, 'LIMS imagingsessions for this ImagingTask have at least as many images as the ImagingTask');
	    Log::write(Log::LOGLEVEL_INFO, 'Looks like all images are already imported. Not importing this ImagingTask.');
	    return false;
	}

	$fxPlateId=$imagingTask['PlateID'];
	$imageBatchId=$imagingTask['ImageBatchID'];
	$barcode=$imagingTask['Barcode'];
	$imagedTime=str_replace('T', ' ', $imagingTask['DateImagedUtc']);

	//Check whether the LIMS has the plate. If not, import it (and its plate type if needed).
	$limsPlate=plate::getByName($barcode);
	Log::write(Log::LOGLEVEL_INFO, 'Plate barcode is '.$barcode.', time imaged '.$imagedTime);
	if($limsPlate){
		Log::write(Log::LOGLEVEL_DEBUG, 'Plate '.$barcode.' exists in LIMS');
	} else {
		Log::write(Log::LOGLEVEL_INFO, 'Plate '.$barcode.' does not exist in LIMS, creating it');
		importPlate($barcode);
	}

	//If plate STILL doesn't exist in the LIMS, bail.
	$limsPlate=plate::getByName($barcode);
	if(!$limsPlate){
		throw new Exception('Plate '.$barcode.' does not exist in LIMS after calling importPlate.');
	}

	//All records under this plate need to share its project ID. Imagers, plate types, etc., go under the Shared project.
	$projectId=$limsPlate['projectid'];

	$imagerName=getImagerNameForImagingTask($taskId, $barcode);

	$limsImager=imager::getbyName($imagerName);
	if(!$limsImager){
		Log::write(Log::LOGLEVEL_INFO, 'Imager '.$imagerName.' does not exist in LIMS, creating it.');
		importImager($imagerName);
		$limsImager=imager::getbyName($imagerName);
	}
	if(!$limsImager){
		throw new Exception('Imager '.$imagerName.' does not exist in LIMS after calling importImager.');
	}


	//We want to split the Formulatrix ImagingTask into one LIMS imagingsession per capture profile, but do not want them to have the same
	//timestamp in the LIMS. We subtract $CaptureProfileId seconds from the Formulatrix time when setting the LIMS imagingsession time -
	//but NOT for the lowest profile ID. If we did the same with that ID, then an "import all ImagingTasks after the last imagingsession time"
	//job would re-import the most recent ImagingTask.
	$minimumProfileId=999999;
	foreach($images as $img){
		if($img['CaptureProfileID']<$minimumProfileId){
			$minimumProfileId=$img['CaptureProfileID'];
		}
	}

	$limsImagingSessions=array();
	$verifiedImageStores=array();
	$existingProfiles=array();
	$existingProfileVersions=array();

	/*
	 * Verify that the LIMS image store exists
	 * Always do this, even if not copying to it, otherwise PHP warnings of undefined $limsStorage
	 */
    $limsStorage=config::get('core_imagestore');
    $limsStorage=rtrim($limsStorage,'/\\').'/';
    if(!in_array($limsStorage, $verifiedImageStores)){
        if(!file_exists($limsStorage)){
            Log::write(Log::LOGLEVEL_ERROR, 'Cannot find LIMS image store: '.$limsStorage);
            Log::write(Log::LOGLEVEL_ERROR, 'Check that the drive is mounted and has the correct permissions');
            throw new Exception('Aborting because LIMS image store cannot be read');
        }
        $verifiedImageStores[]=$limsStorage;
    }

	/*
	 * Verify that the LIMS image store backup exists, if specified
	 */
    if(!$copyToIceBearStore){
        Log::write(Log::LOGLEVEL_DEBUG, 'Not copying images, so not verifying existence of LIMS image store');
    } else {
        $limsStorageBackup=config::get('core_imagestorebackup');
        if(!empty($limsStorageBackup)){
            $limsStorageBackup=rtrim($limsStorageBackup,'/\\').'/';
            if(!in_array($limsStorageBackup, $verifiedImageStores)){
                if(!file_exists($limsStorageBackup)){
                    Log::write(Log::LOGLEVEL_ERROR, 'Cannot find LIMS image store backup: '.$limsStorageBackup);
                    Log::write(Log::LOGLEVEL_ERROR, 'Check that the drive is mounted and has the correct permissions');
                    throw new Exception('Aborting because LIMS image store backup cannot be read');
                }
                $verifiedImageStores[]=$limsStorageBackup;
            }
        }
    }

	//If we don't find the path in the expected location while iterating through images, we warn. But doing it 300 times clutters up the logs.
	$warnedPaths=array();

	/*
	 * Process each image in turn - set up pre-requisites, copy files across, create database record in LIMS
	 */
	foreach($images as $img){
		Log::write(Log::LOGLEVEL_DEBUG, 'Found image details for '.$img['RowLetter'].$img['ColumnNumber'].'.'.$img['DropNumber'].' CaptureProfile '.$img['CaptureProfileID']);
		$basePath=$img['BasePath'];
		$profileId=$img['CaptureProfileID'];
		$profileVersionId=$img['CaptureProfileVersionID'];
		$fxPlateId=$img['PlateID'];
		$barcode=$img['Barcode'];
		$imageBatchId=$img['ImageBatchID'];
		$path='WellImages/'.($fxPlateId%1000).'/plateID_'.$fxPlateId.'/batchID_'.$imageBatchId;
        $profileName=$img['CaptureProfileName'];

		/*
		 * Verify that the Formulatrix image store for this image exists
		 */
		$parts=explode("\\",$basePath);
		$lastPart=$parts[count($parts)-1];
		$basePath=$fxRoot.$lastPart.'/';


		if(!in_array($basePath, $verifiedImageStores)){
			Log::write(Log::LOGLEVEL_DEBUG, 'Checking Formulatrix image store exists...');
			if(!file_exists($basePath)){
			    $warned=true;
			    if(!in_array($basePath, $warnedPaths)){
			        $warnedPaths[]=$basePath;
			        $warned=false;
			        Log::write(Log::LOGLEVEL_WARN, 'Formulatrix image store not mounted at expected Linux location '.$basePath);
			    }
				if(strtoupper(substr(php_uname(),0,3))=='WIN'){
				    if(!$warned){
					   Log::write(Log::LOGLEVEL_WARN, 'Checking at Windows location '.$img['BasePath']);
				    }
					$basePath=$img['BasePath'];
				}
			}
			if(!file_exists($basePath)){
				Log::write(Log::LOGLEVEL_ERROR, 'Cannot find image store: '.$img['BasePath']);
				Log::write(Log::LOGLEVEL_ERROR, 'Check that the drive is mounted and has the correct permissions');
				Log::write(Log::LOGLEVEL_ERROR, 'Drive should be mounted at '.$basePath);
				throw new Exception('Aborting because Formulatrix image store cannot be read');
			} else if(!file_exists($basePath.'WellImages')){
                Log::write(Log::LOGLEVEL_ERROR, 'Found image store: '.$img['BasePath'].' but no WellImages directory');
                Log::write(Log::LOGLEVEL_ERROR, 'Check that the drive is mounted and has the correct permissions');
                Log::write(Log::LOGLEVEL_ERROR, 'If the server name is not fully qualified, try it fully qualified, or its IP.');
                Log::write(Log::LOGLEVEL_ERROR, 'Drive should be mounted at '.$basePath);
                throw new Exception('Aborting because there was no WellImages directory under '.$basePath);
            }
			$verifiedImageStores[]=$basePath;
			Log::write(Log::LOGLEVEL_DEBUG, '...Formulatrix image store exists.');
		} else {
			Log::write(Log::LOGLEVEL_DEBUG, 'Formulatrix image store already known to exist');
		}


		/*
		 * Create the CaptureProfile / imagingparameters in the LIMS if it doesn't exist
		 */
		if(!isset($existingProfiles['fx'.$profileId])){
			Log::write(Log::LOGLEVEL_DEBUG, 'Checking whether CaptureProfile exists in LIMS...');
			$limsProfile=imagingparameters::getByProperty('manufacturerdatabaseid', $profileId);
			if(!$limsProfile){
				importCaptureProfile($profileId);
				$limsProfile=imagingparameters::getByProperty('manufacturerdatabaseid', $profileId);
			}
			$limsProfile=$limsProfile['rows'][0];
			$existingProfiles['fx'.$profileId]=$limsProfile;
			Log::write(Log::LOGLEVEL_DEBUG, '...CaptureProfile exists.');
		} else {
			Log::write(Log::LOGLEVEL_DEBUG, 'CaptureProfile already known to exist in LIMS...');
		}

		Log::write(Log::LOGLEVEL_DEBUG, 'Checking whether CaptureProfileVersion exists in LIMS...');
		/*
		 * Create the CaptureProfileVersion / imagingparametersversion in the LIMS if it doesn't exist
		 */
		if(isset($existingProfileVersions['fx'.$profileVersionId])){
			$limsProfileVersion=$existingProfileVersions['fx'.$profileVersionId];
		} else {
			$limsProfileVersion=imagingparametersversion::getByProperty('manufacturerdatabaseid', $profileVersionId);
			if(!$limsProfileVersion){
				importCaptureProfileVersion($profileVersionId);
				$limsProfileVersion=imagingparametersversion::getByProperty('manufacturerdatabaseid', $profileVersionId);
			}
			$limsProfileVersion=$limsProfileVersion['rows'][0];
			$imagerSettings=imagingparametersversion::getimagersettings($limsProfileVersion['id']);

			/*
			 * Determine the light type for this imaging task
			 */
			Log::write(Log::LOGLEVEL_DEBUG, 'Determining light type for this imaging session');
			$lightPathId=0; //Visible
			$lightPathSettings=imagingsetting::getByProperty('settingname', 'LightPath');
			if(0===stripos($profileName, 'UV ') || 0===stripos($profileName, 'UV-')){
                Log::write(Log::LOGLEVEL_DEBUG, 'Capture profile name starts with UV, setting light type to UV');
			    $lightPathId=1; //UV
            } else if(empty($lightPathSettings) || empty($lightPathSettings['rows'])) {
                Log::write(Log::LOGLEVEL_DEBUG, 'No light path settings in LIMS database');
			} else {
				Log::write(Log::LOGLEVEL_DEBUG, 'Found light path settings in LIMS database, iterating');
				foreach($lightPathSettings['rows'] as $s){
					$settingValue=$s['settingvalue'];
					Log::write(Log::LOGLEVEL_DEBUG, 'Setting has light path '.$settingValue);
					if($limsProfileVersion['id']==$s['imagingparametersversionid']){
						if($limsImager['id']==$s['imagerid']){
						    if(-1==$settingValue){
						        Log::write(Log::LOGLEVEL_DEBUG, 'Matched Robot, CaptureProfileVersion: LightPath '.$settingValue.' [unknown]');
						    } else {
						        Log::write(Log::LOGLEVEL_DEBUG, 'Matched Robot, CaptureProfileVersion: LightPath '.$settingValue.' ['.$lightPaths[$settingValue].']');
						    }
							Log::write(Log::LOGLEVEL_DEBUG, 'Breaking out of iteration');
							$lightPathId=$settingValue;
							break;
						} else if(IMAGER_NAME_UNKNOWN==$limsImager['name'] && $settingValue>=0){
							Log::write(Log::LOGLEVEL_DEBUG, 'Matched (unknown imager), CaptureProfileVersion: LightPath '.$settingValue.' ['.$lightPaths[$settingValue].']');
							Log::write(Log::LOGLEVEL_DEBUG, 'Breaking out of iteration');
							$lightPathId=$settingValue;
							break;
						}
					} else {
						Log::write(Log::LOGLEVEL_DEBUG, 'Setting did not match CaptureProfileVersion, irrelevant');
					}
				}
			}

			if($lightPathId>=count($lightPaths)){
				throw new Exception('Unknown light path ID value'.$lightPathId.' is out of range.');
			} else if($lightPathId<0){
				Log::write(Log::LOGLEVEL_WARN, 'Imager is not capable of light path specified in CaptureProfile. Assuming visible.');
				$lightPathId=0; //Visible;
			}
			$lightPath=$lightPaths[$lightPathId];
			Log::write(Log::LOGLEVEL_DEBUG, 'Light type was determined to be '.$lightPathId.' ['.$lightPaths[$lightPathId].']');

			$limsProfileVersion['lightpath']=$lightPath;
			$existingProfileVersions['fx'.$profileVersionId]=$limsProfileVersion;
		}
		Log::write(Log::LOGLEVEL_DEBUG, '...CaptureProfileVersion exists.');

		/*
		 * Create the LIMS imagingsession if it doesn't exist already
		 */
		$limsImagingSession=null;
		if(isset($limsImagingSessions['profile'.$img['CaptureProfileID']])){
			$limsImagingSession=$limsImagingSessions['profile'.$img['CaptureProfileID']];
		} else {
			Log::write(Log::LOGLEVEL_DEBUG, 'LIMS imagingsession for this image has not been found yet, looking for it');
			$captureProfileId=$img['CaptureProfileID'];
			$date=DateTime::createFromFormat('Y-m-d H:i:s',$imagedTime);
			if($minimumProfileId!=$captureProfileId){
				//subtract (captureProfileId) seconds from time, but not for lowest CaptureProfileId.
				$date=$date->sub(date_interval_create_from_date_string($captureProfileId.' sec'));
			}
			$imagingSessionName=$barcode.'_'.date_format($date, 'Y-m-d_h:i:s').'_profile'.$captureProfileId;
			$limsImagingSession=imagingsession::getByName($imagingSessionName);
			if(!empty($limsImagingSession)){
				Log::write(Log::LOGLEVEL_DEBUG, 'LIMS imagingsession for this image was found');
			} else {
				Log::write(Log::LOGLEVEL_DEBUG, 'LIMS imagingsession for this image was not found, creating it');
				$params=array(
						'name'=>$imagingSessionName,
						'manufacturerdatabaseid'=>$taskId,
						'imagerid'=>$limsImager['id'],
						'plateid'=>$limsPlate['id'],
						'imageddatetime'=>date_format($date, 'Y-m-d H:i:s'),
						'lighttype'=>$limsProfileVersion['lightpath'],
						'imagingparametersversionid'=>$limsProfileVersion['id']
				);
				imagingsession::create($params);
				$limsImagingSession=imagingsession::getByName($imagingSessionName);
			}
			if(!$limsImagingSession){
				throw new Exception('Could not find imagingsession in LIMS after create');
			}
			$limsImagingSessions['profile'.$captureProfileId]=$limsImagingSession;
			Log::write(Log::LOGLEVEL_DEBUG, 'LIMS imagingsession created');
		}
		if(!$limsImagingSession){
			throw new Exception('Could not find LIMS imaging session for ImagingTask '.$taskId.' CaptureProfile '.$img['CaptureProfileID']);
		}

		/*
		 * Check that the extended-focus image and its thumbnail exist in the Formulatrix image store
		 */
		Log::write(Log::LOGLEVEL_DEBUG, 'Checking for existence of EF and thumbnail in Formulatrix store');
		$fxBaseName=$basePath.$path.'/wellNum_'.$img['WellNumber'].'/profileID_'.$img['CaptureProfileID'].'/d'.$img['DropNumber'].'_r'.$img['RegionID'];
		$efImageFileExtension='.jpg';
		$thImageFileExtension='.jpg';
		$fxExtendedFocusPath=$fxBaseName.'_ef'.$efImageFileExtension;
		$fxThumbnailPath=$fxBaseName.'_th'.$thImageFileExtension;
		Log::write(Log::LOGLEVEL_DEBUG, 'Looking for extended-focus image...');
		if(!file_exists($fxExtendedFocusPath)){
			Log::write(Log::LOGLEVEL_DEBUG,'Extended-focus image not found at '.$fxExtendedFocusPath.', looking for PNG instead');
			$efImageFileExtension='.png';
			$fxExtendedFocusPath=$fxBaseName.'_ef'.$efImageFileExtension;
		}
		if(!file_exists($fxExtendedFocusPath)){
			Log::write(Log::LOGLEVEL_WARN,'Extended-focus image not found at '.$fxExtendedFocusPath.', not importing image');
			continue;
		}
		Log::write(Log::LOGLEVEL_DEBUG, '...EF image exists.');
		Log::write(Log::LOGLEVEL_DEBUG, 'Looking for thumbnail image...');

		if(!file_exists($fxThumbnailPath)){
			Log::write(Log::LOGLEVEL_DEBUG,'Thumbnail image not found at '.$fxThumbnailPath.', looking for PNG instead');
			$thImageFileExtension='.png';
			$fxThumbnailPath=$fxBaseName.'_th'.$thImageFileExtension;
		}
		if(!file_exists($fxThumbnailPath)){
			Log::write(Log::LOGLEVEL_WARN,'Thumbnail image not found at '.$fxThumbnailPath.', not importing image');
			continue;
		}
		Log::write(Log::LOGLEVEL_DEBUG, '...Thumbnail exists.');

		/*
		 * Create the directory of images from this imagingsession if it does not exist
		 */
        if(!$copyToIceBearStore){
            Log::write(Log::LOGLEVEL_DEBUG, 'Not checking for imagingsession directory in LIMS image store');
        } else {
            Log::write(Log::LOGLEVEL_DEBUG, 'Checking for imagingsession directory in LIMS image store...');
            $destinationdir=substr($barcode,0,2).'/'.substr($barcode,0,3).'/'.$barcode.'/imagingsession'.$limsImagingSession['id'].'/';
            @mkdir($limsStorage.$destinationdir.'thumbs',0755,true);
            if(!file_exists($limsStorage.$destinationdir.'thumbs')){
                Log::write(Log::LOGLEVEL_ERROR, 'Path does not exist: '.$destinationdir.'thumbs');
                throw new Exception('Could not create destination directory in LIMS image store');
            }
            Log::write(Log::LOGLEVEL_DEBUG, '...exists.');
        }

        $imageWidth=0;
        $imageHeight=0;
        $imageStore=$basePath;
        $imagePath=str_replace($basePath,'',$fxExtendedFocusPath);
        $thumbPath=$fxThumbnailPath;

        if(!$copyToIceBearStore){
            //Create dropimage record with $fxExtendedFocusPath and $fxThumbnailPath
            $imageSize=getimagesize($fxExtendedFocusPath);
            if(!$imageSize) {
                Log::write(Log::LOGLEVEL_WARN, $fxExtendedFocusPath . ' may be corrupt, could not get pixel dimensions');
                Log::write(Log::LOGLEVEL_WARN, 'Not linking image ' . $fxExtendedFocusPath . ' in LIMS database');
            } else {
                $imageWidth=$imageSize[0];
                $imageHeight=$imageSize[1];
                Log::write(Log::LOGLEVEL_DEBUG, 'Linked Formulatrix image and thumbnail in LIMS database');
            }
        } else {
            /*
             * Copy the extended-focus image and its thumbnail to the LIMS image store
             */
            Log::write(Log::LOGLEVEL_DEBUG, 'Copying images to LIMS store');
            $efDestination=$destinationdir.$img['RowLetter'].str_pad($img['ColumnNumber'],2,'0',STR_PAD_LEFT).'_'.$img['DropNumber'].$efImageFileExtension;
            $thDestination=$destinationdir.'thumbs/'.$img['RowLetter'].str_pad($img['ColumnNumber'],2,'0',STR_PAD_LEFT).'_'.$img['DropNumber'].$thImageFileExtension;
            copy($fxExtendedFocusPath, $limsStorage.$efDestination);
            copy($fxThumbnailPath, $limsStorage.$thDestination);
            if(!file_exists($limsStorage.$efDestination)){
                Log::write(Log::LOGLEVEL_ERROR, 'EF image not copied: '.$limsStorage.$efDestination);
                throw new Exception('Could not create EF image in LIMS image store');
            }
            if(!file_exists($limsStorage.$thDestination)){
                Log::write(Log::LOGLEVEL_ERROR, 'Thumbnail image not copied: '.$limsStorage.$thDestination);
                throw new Exception('Could not create thumbnail in LIMS image store');
            }
            $imageSize=getimagesize($limsStorage.$efDestination);
            if(!$imageSize){
                Log::write(Log::LOGLEVEL_WARN, $limsStorage .$efDestination.' may be corrupt, could not get pixel dimensions');
                Log::write(Log::LOGLEVEL_WARN, 'Not linking image '.$limsStorage.$efDestination.' in LIMS database');
            } else {
                $imageWidth=$imageSize[0];
                $imageHeight=$imageSize[1];
                $imageStore=$limsStorage;
                $imagePath=$efDestination;
                $thumbPath=$thDestination;
                Log::write(Log::LOGLEVEL_DEBUG, 'Copied image and thumbnail to LIMS store');
            }
        }

		if(0!==$imageHeight){
            Log::write(Log::LOGLEVEL_DEBUG, 'Creating database record of image...');
            $imageDbName=$barcode.'_'.$img['RowLetter'].str_pad($img['ColumnNumber'],2,'0',STR_PAD_LEFT).'.'.$img['DropNumber'].'_is'.$limsImagingSession['id'];
            $preExistingDropImage=dropimage::getByName($imageDbName);
            $welldropName=$barcode.'_'.$img['RowLetter'].str_pad($img['ColumnNumber'],2,'0',STR_PAD_LEFT).'.'.$img['DropNumber'];
            $welldrop=welldrop::getByName($welldropName);
            if(!$preExistingDropImage){
                if(!$welldrop){
                    throw new Exception('welldrop not found in LIMS for '.$welldropName);
                }
                $limsDropImage=dropimage::create(array(
                    'name'=>$imageDbName,
                    'projectid'=>$projectId,
                    'imagingsessionid'=>$limsImagingSession['id'],
                    'welldropid'=>$welldrop['id'],
                    'pixelheight'=>$imageHeight,
                    'pixelwidth'=>$imageWidth,
                    'micronsperpixelx'=>$img['PixelSize'],
                    'micronsperpixely'=>$img['PixelSize'],
                    'imagestorepath'=>$imageStore,
                    'imagepath'=>$imagePath,
                    'thumbnailpath'=>$thumbPath
                ));
                //No - done in dropimage::create
                //carryForwardScores($welldrop['id']);
                Log::write(Log::LOGLEVEL_DEBUG, '...done');
            }
        }

		if($copyToIceBearStore && !empty($limsStorageBackup) && '/'!=$limsStorageBackup){
			Log::write(Log::LOGLEVEL_DEBUG, 'Copying image and thumbnail to LIMS store backup');
			@mkdir($limsStorageBackup.$destinationdir.'thumbs',0755,true);
			if(!file_exists($limsStorageBackup.$destinationdir.'thumbs')){
				Log::write(Log::LOGLEVEL_ERROR, 'Path does not exist in backup store: '.$destinationdir.'thumbs');
				throw new Exception('Could not create destination directory in LIMS backup image store');
			}
			copy($limsStorage.$efDestination, $limsStorageBackup.$efDestination);
			copy($limsStorage.$thDestination, $limsStorageBackup.$thDestination);
			if(!file_exists($limsStorageBackup.$efDestination)){
				Log::write(Log::LOGLEVEL_ERROR, 'EF backup image not copied: '.$limsStorageBackup.$efDestination);
				throw new Exception('Could not create EF image in LIMS backup image store');
			}
			if(!file_exists($limsStorageBackup.$thDestination)){
				Log::write(Log::LOGLEVEL_ERROR, 'Thumbnail backup image not copied: '.$limsStorageBackup.$thDestination);
				throw new Exception('Could not create thumbnail in backup LIMS image store');
			}
			Log::write(Log::LOGLEVEL_DEBUG, 'Copied image and thumbnail to LIMS store backup');
		}

	} //End of images loop

	Log::write(Log::LOGLEVEL_INFO, 'Imaging task imported');
	return true;

}

/**
 * Gets all ImagingTasks in the RockMaker database for the specified plate barcode.
 *
 * @param string $barcode The plate barcode
 * @return array The ImagingTasks
 * @throws ServerException
 */
function getFormulatrixImagingTasksForPlateBarcode(string $barcode): array {
    Log::write(Log::LOGLEVEL_DEBUG, 'In getFormulatrixImagingTasksForPlateBarcode, $barcode='.$barcode);
    global $rm;

    //get plate
    $stmt=$rm->prepare('SELECT * FROM Plate WHERE Barcode=:barcode');
    $stmt->execute(array(':barcode'=>$barcode));
    $result=$stmt->fetch(PDO::FETCH_ASSOC);
    if(!$result){ throw new Exception('No plate in RockMaker database with barcode '.$barcode); }
    $fxPlateId=$result['ID'];
    Log::write(Log::LOGLEVEL_DEBUG, 'RockMaker Plate.ID='.$fxPlateId);

    //get its ExperimentPlate
    $stmt=$rm->prepare('SELECT * FROM ExperimentPlate WHERE PlateId=:plateid');
    $stmt->execute(array(':plateid'=>$fxPlateId));
    $result=$stmt->fetch(PDO::FETCH_ASSOC);
    if(!$result){ throw new Exception('No ExperimentPlate in RockMaker database for Plate with barcode '.$barcode); }
    $explateId=$result['ID'];
    Log::write(Log::LOGLEVEL_DEBUG, 'RockMaker ExperimentPlate.ID='.$explateId);

    //get the ImagingTasks
    //Time zone: Just interested in preventing very recent inspections from importing, so comparing (local) DateImaged to (local) GETDATE() is good enough.
    $stmt=$rm->prepare('SELECT * FROM ImagingTask WHERE ExperimentPlateId=:explateid AND DateImaged<=DATEADD(minute, -5, GETDATE()) ORDER BY DateImaged ASC');
    $stmt->execute(array(':explateid'=>$explateId));
    $imagingTasks=$stmt->fetchAll(PDO::FETCH_ASSOC);

    Log::write(Log::LOGLEVEL_DEBUG, 'Returning from getFormulatrixImagingTasksForPlateBarcode');
    return $imagingTasks;
}

/**
 * Gets the date/time of the latest imaging session in the LIMS. Should be in YYYY-MM-DD HH:mm:ss format.
 * Returns null if the there are no imaging sessions in the LIMS.
 * @return NULL|string
 * @throws BadRequestException
 * @throws ServerException
 */
function getTimeOfNewestLimsImagingSession(){
    Log::write(Log::LOGLEVEL_DEBUG, 'In getTimeOfNewestLimsImagingSession()');
    $lastImportDateTime=null;
    $limsSession=imagingsession::getByProperty('imagermanufacturer','Formulatrix',array(
        'sortby'=>'imageddatetime',
        'sortdescending'=>'yes',
        'all'=>1
    ));
    if(isset($limsSession['rows']) && !empty($limsSession['rows'])){
        $limsSession=$limsSession['rows'][0];
        $lastImportDateTime=$limsSession['imageddatetime'];
    }
    Log::write(Log::LOGLEVEL_DEBUG, '$lastImportDateTime='.$lastImportDateTime);
    Log::write(Log::LOGLEVEL_DEBUG, 'Returning from getTimeOfNewestLimsImagingSession()');
    return $lastImportDateTime;
}



/**
 * Creates a record in the LIMS of the imager with the specified serial/name.
 * For imagers above RI2, the licensed capacity may be significantly lower than the nominal capacity. Fortunately,
 * it seems that only licensed hotel slots appear in the RockImager database, so we can count those to determine
 * the licensed capacity. RI54 is relatively simple, all hotel slots are the load port. RI182 and RI1000 have internal
 * storage plus load port. UPDATE 2017-02-13: It's not that simple. Running this importer against a newly-installed
 * RI54 returned a single-digit plate capacity. It seems that plate slots may not exist in the RI database until they
 * have been used at least once. Therefore, we simply parse the serial number and obtain the default capacity;
 * down-licensed imagers will need to have their capacity adjusted manually in IceBear after import.
 * @param string $imagerName The Formulatrix serial, e.g., RI1000-1234.
 * @throws Exception
 */
function importImager(string $imagerName): void {
	Log::write(Log::LOGLEVEL_DEBUG, 'In importImager, imagerName='.$imagerName);
	global $ri, $sharedProjectId;
	$imagerType=explode('-', $imagerName)[0];
	if(str_starts_with($imagerType, 'RI')){
		$capacity=substr($imagerType, 2);
		$capacity=(int)$capacity;
	} else {
		Log::write(Log::LOGLEVEL_WARN, 'Unrecognised Formulatrix imager type, serial was '.$imagerName.'. Assuming capacity 10000, set correct value manually in the LIMS');
		$capacity=10000;
	}
	imager::create(array(
			'name'=>$imagerName,
			'friendlyname'=>$imagerName,
			'manufacturer'=>'Formulatrix',
			'temperature'=>20,
			'platecapacity'=>$capacity,
			'alertlevel'=>floor($capacity*IMAGERLOAD_ALERT_THRESHOLD_PERCENT/100),
			'warninglevel'=>floor($capacity*IMAGERLOAD_WARNING_THRESHOLD_PERCENT/100)
	));
	Log::write(Log::LOGLEVEL_DEBUG, 'Returning from importImager');

}


/**
 * Creates an imagingparametersversion record in the LIMS of a Formulatrix CaptureProfileVersion, and
 * updates the parent imagingparameters to reflect the current version in the Formulatrix system.
 * WARNING This could fail if two versions of a CaptureProfile are created and used in quick succession. Importing the first causes
 * the second to be set as current on the LIMS' copy of the CaptureProfile, likely causing a DB constraint violation. Realistically, if
 * the importer runs every 5-10 minutes and if it takes that long to image a plate, this situation should not arise. Historical data may cause issues though.
 * @param int $captureProfileVersionId
 * @throws BadRequestException
 * @throws ForbiddenException
 * @throws NotFoundException
 * @throws ServerException
 */
function importCaptureProfileVersion(int $captureProfileVersionId): void {
	Log::write(Log::LOGLEVEL_DEBUG, 'In importCaptureProfileVersion, id='.$captureProfileVersionId);
	global $rm, $sharedProjectId;
	$stmt=$rm->prepare('SELECT CaptureProfileVersion.ID, CaptureProfileVersion.CaptureProfileID, CaptureProfile.CurrentCaptureProfileVersionID, CaptureProfile.Name
			FROM CaptureProfileVersion, CaptureProfile
			WHERE CaptureProfile.ID=CaptureProfileVersion.CaptureProfileID
			AND CaptureProfileVersion.ID=:id
	');
	$stmt->execute(array(':id'=>$captureProfileVersionId));
	$fxVersion=$stmt->fetch(PDO::FETCH_ASSOC);
	if(!$fxVersion){
		throw new Exception('No CaptureProfileVersion in Formulatrix with ID '.$captureProfileVersionId);
	}

	$limsVersion=imagingparametersversion::getByProperty('manufacturerdatabaseid', $fxVersion['ID']);
	if($limsVersion){
		Log::write(Log::LOGLEVEL_WARN, 'Called importCaptureProfileVersion but it already exists');
		return;
	}

	$limsProfile=imagingparameters::getByProperty('manufacturerdatabaseid', $fxVersion['CaptureProfileID']);
	if(!$limsProfile){
		importCaptureProfile($fxVersion['CaptureProfileID']);
	}
	$limsProfile=imagingparameters::getByProperty('manufacturerdatabaseid', $fxVersion['CaptureProfileID']);
	if(!$limsProfile){ throw new Exception('imagingparameters with manufacturerdatabaseid '.$fxVersion['CaptureProfileID'].' does not exist in LIMS'); }
	$limsProfile=$limsProfile['rows'][0];

	imagingparametersversion::create(array(
			'name'=>$fxVersion['Name'].'_v'.$fxVersion['CurrentCaptureProfileVersionID'],
			'imagingparametersid'=>$limsProfile['id'],
			'manufacturerdatabaseid'=>$captureProfileVersionId,
			'projectid'=>$sharedProjectId,
	));

	$limsVersion=imagingparametersversion::getByProperty('manufacturerdatabaseid', $fxVersion['ID']);
	if(!$limsVersion){
		throw new Exception('Could not create imagingparametersversion in LIMS');
	}
	$limsVersion=$limsVersion['rows'][0];

	//Update the parent imagingparameters with the current version (whether it's this version or not) - this is the part that may fail, see warning in function docs
	imagingparameters::update($limsVersion['id'], array(
			'currentversionid'=>$limsVersion['id']
	));

	//import the imager settings for this version of the profile
	$stmt=$rm->prepare('SELECT CaptureProperty.CaptureProfileVersionID, CaptureProperty.RobotID, CaptureProperty.Name AS Name, CaptureProperty.Value AS Value, Imager.Name AS RobotName
			FROM CaptureProperty, Imager
			WHERE CaptureProperty.RobotId=Imager.ID 
			AND CaptureProperty.CaptureProfileVersionID=:id
	');
	$stmt->execute(array(':id'=>$captureProfileVersionId));
	$settings=$stmt->fetchAll(PDO::FETCH_ASSOC);
	foreach($settings as $s){
		$imager=imager::getByName($s['RobotName']);
		if(!$imager){
			importImager($s['RobotName']);
			$imager=imager::getByName($s['RobotName']);
		}
		if(!$imager){
			throw new Exception('Could not find imager '.$s['RobotName'].' in LIMS');
		}
		imagingsetting::create(array(
				'settingname'=>$s['Name'],
				'settingvalue'=>$s['Value'],
				'imagerid'=>$imager['id'],
				'projectid'=>$sharedProjectId,
				'imagingparametersversionid'=>$limsVersion['id'],
				'name'=>'v'.$limsVersion['id'].'_robot'.$imager['id'].'_'.$s['Name']
		));
	}

	Log::write(Log::LOGLEVEL_DEBUG, 'Returning from importCaptureProfileVersion');
}


/**
 * Creates an imagingparameters record in the LIMS of a Formulatrix CaptureProfile
 * @param int $captureProfileId
 * @throws BadRequestException
 * @throws ForbiddenException
 * @throws NotFoundException
 * @throws ServerException
 */
function importCaptureProfile(int $captureProfileId): void {
	Log::write(Log::LOGLEVEL_DEBUG, 'In importCaptureProfile, id='.$captureProfileId);
	global $rm, $sharedProjectId;
	$stmt=$rm->prepare('SELECT Name, CurrentCaptureProfileVersionID
			FROM CaptureProfile
			WHERE ID=:id
		');
	$stmt->execute(array(':id'=>$captureProfileId));
	$fxProfile=$stmt->fetch(PDO::FETCH_ASSOC);
	if(empty($fxProfile)){
		throw new Exception('RockMaker CaptureProfile with ID '.$captureProfileId.' does not exist');
	}

	$limsProfile=imagingparameters::getByName($fxProfile['Name']);
	if($limsProfile){
		Log::write(Log::LOGLEVEL_WARN, 'Called importCaptureProfile but it already exists');
		return;
	}
	imagingparameters::create(array(
			'name'=>$fxProfile['Name'],
			'projectid'=>$sharedProjectId,
			'manufacturer'=>'Formulatrix',
			'manufacturerdatabaseid'=>$captureProfileId
	));
	$limsProfile=imagingparameters::getByName($fxProfile['Name']);
	if(!$limsProfile){
		throw new Exception('Could not create imagingparameters in LIMS');
	}
	Log::write(Log::LOGLEVEL_DEBUG, 'Returning from importCaptureProfile');
}

/**
 * Creates a record in the LIMS of the plate with the specified barcode.
 * If the plate type does not exist, importPlateType is called to create it.
 * @param string $barcode The plate barcode.
 * @return void
 * @throws BadRequestException
 * @throws ForbiddenException
 * @throws NotFoundException
 * @throws ServerException
 */
function importPlate(string $barcode): void {
	Log::write(Log::LOGLEVEL_DEBUG, 'In importPlate, barcode='.$barcode);
	global $rm, $scoringSystem, $unknownUsernames, $defaultPlateOwnerUsername;
	$stmt=$rm->prepare('SELECT Plate.ID, Containers.Name AS PlateTypeName, Users.Name AS UserName, Users.EmailAddress
			FROM Plate, Experiment, Containers, Users
			WHERE Plate.ExperimentID=Experiment.ID 
				AND Users.ID=Experiment.UserID 
				AND Containers.ID=ContainerID 
				AND Plate.Barcode=:barcode
		');
	$stmt->execute(array(':barcode'=>$barcode));
	$fxPlate=$stmt->fetch(PDO::FETCH_ASSOC);
	$plateTypeName=$fxPlate['PlateTypeName'];
	Log::write(Log::LOGLEVEL_DEBUG, 'Plate '.$barcode.' has plate type: '.$plateTypeName);
	$limsPlateType=platetype::getByName($plateTypeName);
	if($limsPlateType){
		Log::write(Log::LOGLEVEL_DEBUG, 'Plate type '.$plateTypeName.' exists in LIMS');
	} else {
		Log::write(Log::LOGLEVEL_INFO, 'Plate type '.$plateTypeName.' does not exist in LIMS, creating it');
		importPlateType($plateTypeName);
		$limsPlateType=platetype::getByName($plateTypeName);
	}

	$project=project::getByName("Default Project");
	if(!$project){
		throw new Exception('Project "Default Project" does not exist.');
	}


	if(in_array($fxPlate['UserName'], $unknownUsernames)){
		Log::write(Log::LOGLEVEL_WARN, 'Plate owner [' .$fxPlate['UserName']. '] should not own plates. Setting ['.$defaultPlateOwnerUsername.'] instead.');
		$plateOwner=user::getByName($defaultPlateOwnerUsername);
	} else {
		$plateOwner=user::getByProperty('email', $fxPlate['EmailAddress']);
        if(!$plateOwner){
            Log::write(Log::LOGLEVEL_INFO, 'Plate owner [' .$fxPlate['EmailAddress']. '] does not exist in LIMS. Forcing lowercase...');
            $plateOwner=user::getByProperty('email', strtolower($fxPlate['EmailAddress']));
        }
		if(!$plateOwner){
			Log::write(Log::LOGLEVEL_INFO, 'Plate owner [' .$fxPlate['EmailAddress']. '] does not exist in LIMS. Creating.');
			$plateOwner=importUser($fxPlate['EmailAddress']);
			if(!$plateOwner){
				throw new Exception('Could not create user in LIMS');
			}
		} else {
			$plateOwner=$plateOwner['rows'][0];
		}
	}
	$request=array(
			'name'=>$barcode,
			'projectid'=>$project['id'],
			'platetypeid'=>$limsPlateType['id'],
			'ownerid'=>$plateOwner['id'],
			'crystalscoringsystemid'=>$scoringSystem['id']
	);
	plate::create($request);
	$limsPlate=plate::getByName($barcode);

	Log::write(Log::LOGLEVEL_DEBUG, 'Returning from importPlate after create');
}

/**
 * Creates the Formulatrix scoring system in the LIMS, if it does not exist
 */
function importScoringSystem(){
	global $rm, $sharedProjectId;
	Log::write(Log::LOGLEVEL_DEBUG, 'In importScoringSystem');

	$existing=crystalscoringsystem::getByName('Formulatrix');
	if($existing){ return $existing; }

	$system=crystalscoringsystem::create(array(
		'name'=>'Formulatrix'
	));
	if(!$system){ throw new Exception('Could not create scoring system'); }
	$system=$system['created'];

	global $rm, $ri, $sharedProjectId, $lightPaths;

	$stmt=$rm->prepare('SELECT WellDropScore.*
			FROM WellDropScore, WellDropScoreGroup
			WHERE WellDropScore.WellDropScoreGroupID=WellDropScoreGroup.ID
			AND WellDropScoreGroup.Name=:name
			ORDER BY OrderNumber ASC
	');
	$stmt->execute(array(':name'=>'Default'));
	$scores=$stmt->fetchAll(PDO::FETCH_ASSOC);
	$index=0;
	foreach($scores as $s){
		$color=substr("000000".dechex(1*$s['Color']),-6);
		$request=array(
				'crystalscoringsystemid'=>$system['id'],
				'scoreindex'=>$index,
				'hotkey'=>$index,
				'color'=>$color,
				'label'=>$s['Name'],
				'name'=>'Formulatrix_'.$s['Name'],
		);
		crystalscore::create($request);
		$index++;
	}
	Log::write(Log::LOGLEVEL_DEBUG, 'returning from importScoringSystem');
	return $system;
}


/**
 * Creates a record of the plate type in the LIMS, from the RockMaker Containers entry with the same name.
 * @param string $plateTypeName The name of the plate type
 * @throws BadRequestException
 * @throws ForbiddenException
 * @throws NotFoundException
 * @throws ServerException
 */
function importPlateType(string $plateTypeName): void {
	Log::write(Log::LOGLEVEL_DEBUG, 'In importPlateType, plateTypeName='.$plateTypeName);
	global $rm;

	$stmt=$rm->prepare('SELECT NumRows, NumColumns, MaxNumDrops
			FROM Containers WHERE Name=:name');
	$stmt->execute(array(':name'=>$plateTypeName));
	$plateType=$stmt->fetch(PDO::FETCH_ASSOC);

	//All plate types belong in the Shared project
	$sharedProject=project::getByName('Shared');

	//Make a default drop mapping with the drops along the top of the well (e.g., 123,RRR).
	//TODO Try to determine from RockImager calibration data
	$dropMapping=array('','');
	for($i=1;$i<=$plateType['MaxNumDrops'];$i++){
		$dropMapping[0].=$i;
		$dropMapping[1].='R';
	}
	$dropMapping=implode(',', $dropMapping);

	//Create the plate type in the LIMS
	$created=platetype::create(array(
		'name'=>$plateTypeName,
		'rows'=>$plateType['NumRows'],
		'cols'=>$plateType['NumColumns'],
		'subs'=>$plateType['MaxNumDrops'],
		'dropmapping'=>$dropMapping,
		'projectid'=>$sharedProject['id'],
	));
	Log::write(Log::LOGLEVEL_DEBUG, 'Returning from importPlateType');
}


/**
 * Creates a record of a Formulatrix/RockMaker user in the LIMS.
 * The user is created in an inactive state, with a default username, and will be unable to log in
 * until an administrator activates their account.
 * @param string $emailAddress The user's email address, registered in the Formulatrix database
 * @return mixed
 * @throws BadRequestException
 * @throws ForbiddenException
 * @throws NotFoundException
 * @throws ServerException
 */
function importUser(string $emailAddress): mixed {
	global $rm;
	$stmt=$rm->prepare('SELECT Name FROM Users WHERE EmailAddress=:email');
	$stmt->execute(array(':email'=>$emailAddress));
	$user=$stmt->fetch(PDO::FETCH_ASSOC);
	if(!$user){
		throw new Exception('Could not find RockMaker user with email address '.$emailAddress);
	}
	$userFullName=$user['Name'];
	$temporaryUsername='AUTO'.rand(10000000,99999999);
	while(user::getByName($temporaryUsername)){
		Log::write(Log::LOGLEVEL_DEBUG, 'Tried temporary username '.$temporaryUsername.' but it exists');
		$temporaryUsername='AUTO'.rand(10000000,99999999);
	}
	Log::write(Log::LOGLEVEL_DEBUG, 'Using temporary username '.$temporaryUsername);
	$user=user::create(array(
			'name'=>$temporaryUsername,
			'fullname'=>$userFullName,
			'email'=>strtolower($emailAddress),
			'password'=>'ChangeMe!',
			'isactive'=>0
	));
	return $user['created'];
}


function getPlateBarcodeForImagingTask($taskId){
	global $rm;
	$stmt=$rm->prepare('SELECT Plate.Barcode AS Barcode 
			FROM ImagingTask, ExperimentPlate, Plate
			WHERE ImagingTask.ExperimentPlateID=ExperimentPlate.ID
			AND ExperimentPlate.PlateID=Plate.ID
			AND ImagingTask.ID=:taskid');
	$stmt->execute(array(':taskid'=>$taskId));
	$task=$stmt->fetch(PDO::FETCH_ASSOC);
	if($task){
		return $task['Barcode'];
	}
	return null;
}

function getImagerNameForImagingTask($taskId, $barcode){
	global $ri,$sharedProject;

	$imagerName=null;
	//TODO Is there only one imager? Use it.

	//Check RockImager's imaging log for this specific imaging task
	if(!$imagerName){
		$stmt=$ri->prepare('SELECT Robot.Name AS RobotName
				FROM ImagingLog, Robot
				WHERE ImagingLog.RobotID=Robot.ID
					AND ImagingLog.PlateBarcode=:barcode
					AND ImagingLog.InspectionId=:taskid
		');
		$stmt->execute(array(':barcode'=>$barcode, ':taskid'=>$taskId));
		$logEntry=$stmt->fetch(PDO::FETCH_ASSOC);
		if($logEntry){
			$imagerName=$logEntry['RobotName'];
		}
	}
	//Then see if the plate is currently in an imager
	//TODO prioritise bigger imagers, since RI2 inventory can be out of date
	if(!$imagerName){
		$stmt=$ri->prepare('SELECT Robot.Name AS RobotName
				FROM Plate, Address, PlateAddress, Robot
				WHERE Address.RobotID=Robot.ID
					AND PlateAddress.AddressID=Address.ID
					AND PlateAddress.PlateID=Plate.ID
					AND Plate.Barcode=:barcode
		');
		$stmt->execute(array(':barcode'=>$barcode));
		$address=$stmt->fetch(PDO::FETCH_ASSOC);
		if($address){
			$imagerName=$address['RobotName'];
		}
	}
	//Has the plate been imaged before? Assume the same imager. Check the imaging log again,
	//without asking for a specific task ID, and take the most recent log entry.
	if(!$imagerName){
		$stmt=$ri->prepare('SELECT Robot.Name AS RobotName
			FROM ImagingLog, Robot
			WHERE ImagingLog.RobotID=Robot.ID
				AND ImagingLog.PlateBarcode=:barcode
			ORDER BY ImagingLog.InspectionID DESC
		');
		$stmt->execute(array(':barcode'=>$barcode));
		$logEntry=$stmt->fetch(PDO::FETCH_ASSOC);
		if($logEntry){
			$imagerName=$logEntry['RobotName'];
		}

	}
	//Use a default imager and let the user sort it out later. If the default doesn't exist, we create it.
	if(!$imagerName){
		Log::write(Log::LOGLEVEL_WARN, 'Could not determine imager for ImagingTask '.$taskId.' on plate '.$barcode.', using default');
		$imagerName=IMAGER_NAME_UNKNOWN;
		$limsImager=imager::getByName($imagerName);
		if(!$limsImager){
			Log::write(Log::LOGLEVEL_WARN, 'Default imager does not exist in LIMS, creating it');
			$sharedProject=project::getByName('Shared');
			imager::create(array(
					'name'=>$imagerName,
					'friendlyname'=>'Correct imager unknown',
					'manufacturer'=>'Unknown',
					'temperature'=>20,
					'platecapacity'=>100000,
					'alertlevel'=>100000,
					'warninglevel'=>100000,
					'projectid'=>$sharedProject['id']
			));
		}
	}
	Log::write(Log::LOGLEVEL_DEBUG, 'Imager name is '.$imagerName);
	return $imagerName;
}


function updateImagerInventories(){
	Log::write(Log::LOGLEVEL_DEBUG, 'Begin updating imager inventories');
	$imagers=imager::getAll();
	if($imagers && isset($imagers['rows'])){
		foreach($imagers['rows'] as $i){
			updateImagerInventory($i);
		}
	}
	Log::write(Log::LOGLEVEL_DEBUG, 'End updating imager inventories');
}

function updateImagerInventory($imager){
	global $sharedProjectId;
	$imagerName=$imager['name'];
	Log::write(Log::LOGLEVEL_DEBUG, 'Begin updating imager inventory, imager name is '.$imagerName);
	global $ri, $rm;
	/**
	 * @TODO The actual inventory. This just updates the active/expired load.
	 */
	if(0!==stripos($imagerName,'RI')){
		Log::write(Log::LOGLEVEL_DEBUG, 'Imager name '.$imagerName.' does not look like a Formulatrix imager, not updating');
		return;
	} else if(0===stripos($imagerName,'RI1-')){
		Log::write(Log::LOGLEVEL_DEBUG, 'Not updating RI1 imager '.$imagerName.' inventory');
		return;
	} else if(0===stripos($imagerName,'RI2-')){
		Log::write(Log::LOGLEVEL_DEBUG, 'Not updating RI2 imager '.$imagerName.' inventory');
		return;
	}

	$stmt=$ri->prepare('SELECT ID FROM Robot WHERE Name=:name');
	$stmt->execute(array(':name'=>$imagerName));
	$robot=$stmt->fetch(PDO::FETCH_ASSOC);
	if(!$robot){ return; }
	$robotId=$robot['ID'];

	$stmt=$ri->prepare('SELECT riPlate.Barcode AS Barcode, 
		Users.Name AS Owner, 
		CONVERT(varchar(25), MIN(ImagingTask.DateToImage), 120) AS NextInspection, 
		CONVERT(varchar(25), MAX(ImagingTask.DateToImage), 120) AS FinalInspection, 
		COUNT(ImagingTask.DateToImage) AS InspectionsRemaining 
		FROM RockImager.dbo.Plate AS riPlate, 
		RockImager.dbo.PlateAddress AS PlateAddress, 
		RockMaker.dbo.Plate AS rmPlate, 
		RockMaker.dbo.Experiment AS Experiment, 
		RockMaker.dbo.Users AS Users, 
		RockMaker.dbo.ExperimentPlate AS ExperimentPlate 
		LEFT OUTER JOIN RockMaker.dbo.ImagingTask AS ImagingTask 
		ON ImagingTask.ExperimentPlateID=ExperimentPlate.ID '.
		' AND ImagingTask.State<>1 '. //Not the Skipped ones
		' AND ImagingTask.State<>6 '. //Not the Completed ones
		' AND ImagingTask.State<>7 '. //Not the Cancelled ones
			'WHERE riPlate.ID=PlateAddress.PlateId AND rmPlate.Barcode=riPlate.Barcode 
			AND rmPlate.ID=ExperimentPlate.PlateID AND rmPlate.ExperimentId=Experiment.ID 
			AND Experiment.UserID=Users.ID AND riPlate.RobotID=:robotid
			GROUP BY riPlate.Barcode, Users.Name 
			ORDER BY NextInspection ASC');
	$stmt->execute(array(':robotid'=>$robotId));
	$platesInImager=$stmt->fetchAll(PDO::FETCH_ASSOC);
	$active=array();
	$expired=array();

	//set all plates with imager id to null locationid
	database::query('UPDATE plate SET locationid=NULL, nextinspectiontime=NULL, finalinspectiontime=NULL, inspectionsremaining=NULL
			WHERE locationid=:imagerid', array(':imagerid'=>$imager['id']));

	foreach($platesInImager as $p){
		if(0==$p['InspectionsRemaining']){
			$expired[]=$p;
		} else {
			$active[]=$p;
		}
		$plate=plate::getByName($p['Barcode']);
		if(!$plate){
			importPlate($p['Barcode']);
			$plate=plate::getByName($p['Barcode']);
		}
		Log::write(Log::LOGLEVEL_DEBUG, 'Plate '.$p['Barcode'].', ID '.$plate['id'].' is in imager '.$imager['name'].', '.(0==$p['InspectionsRemaining']?'expired':'active'));
		$parameters=array(
				'locationid'=>$imager['id'],
				'nextinspectiontime'=>$p['NextInspection'],
				'finalinspectiontime'=>$p['FinalInspection'],
				'inspectionsremaining'=>$p['InspectionsRemaining'],
		);
		plate::update($plate['id'],$parameters);
	}

	imager::update($imager['id'], array(
			'platesactive'=>count($active),
			'platesexpired'=>count($expired)
	));

	$date=gmdate("Y-m-d");
	$request=array(
			'projectid'=>$sharedProjectId,
			'imagerid'=>$imager['id'],
			'loadingdate'=>$date,
			'platesactive'=>count($active),
			'platesexpired'=>count($expired),
	);
	imagerloadlog::log($request);


	Log::write(Log::LOGLEVEL_INFO, 'Imager '.$imagerName.' has '.count($active).' active and '.count($expired).' expired plates');
	Log::write(Log::LOGLEVEL_DEBUG, 'Finished updating imager inventory for '.$imagerName);

}

/**
 * Echoes a log message to the output, prefixed by a level indicator and date/time.
 * @param int $level The log level, see top of this file for definitions
 * @param string $message The log message
 */

function setUpClassAutoLoader(){
	spl_autoload_register(function($className){
		$dir=rtrim(__DIR__,'/').'/';
		$paths=array(
				$dir.'../classes/',
				$dir.'../classes/core/',
				$dir.'../classes/core/exception/',
				$dir.'../classes/core/authentication/',
				$dir.'../classes/core/interface/',
				$dir.'../classes/model/',
		);
		foreach($paths as $path){
			if(file_exists($path.$className.'.class.php')){
				include_once($path.$className.'.class.php');
			}
		}
	});
}


#[NoReturn] function showHelp(){
	echo "\nImporter help\n\n";
	echo "By default this will import the ".IMAGINGSESSIONS_PER_RUN." imaging sessions after the most recent in the LIMS database, in date/time order. If none are present in the LIMS, the first ones in the Formulatrix database will be imported.\n";
	echo "\nRockImagerProcessor can lag some way behind the imager itself, meaning that JPEG images may not be available for some minutes after the inspection is shown as complete in RockImager. For this reason, no inspections younger than ".MINIMUM_INSPECTION_AGE_MINS." minutes old are imported.\n";
	echo "\nThe following arguments can be supplied to modify this behaviour:\n";
	echo "\n -h Show help";
	echo "\n -s5 - Skip this many inspections at the beginning (default 0)";
	echo "\n -m20 - Import at most this many inspections (default 10)";
	echo "\n -d2016-02-28 - Begin import from this date (default date of last imported inspection) - not compatible with other options";
//	echo "\n -iRI54-0039 - Import only for this imager (default all imagers) - not compatible with other options";
	echo "\n -b909s - (Re)import all inspections and images for this barcode - not compatible with other options";
	echo '\n\nNote that the default behaviour is to import from the latest previously-imported imaging session. If the importer is running some way behind the imagers, using some of these options may mean that some imaging sessions are NEVER imported automatically.';
	exit;
}

