    <?php
    /**
     * This importer copies plate inspections and their associated images from a Rigaku imager
     * into the LIMS for onward processing. It creates associated records in the LIMS, such as
     * imagers and plate types, where needed.
     *
     * Command-line arguments:
     *
     * -h Show help
     * -c - Commissioning run. Import all plates and inspections, committing after each plate and inspection
     * -b909s - (Re)import images for this barcode
     * -l1 - Set logging level. 1=debug, 2=info, 3=warn, 4=error. (Default 2)
     *
     * General approach:
     *
     * We do not connect to the Rigaku database, instead relying on XML and the filesystem state.
     *
     * Stage 1: Parse all recently-modified plate XML files to ensure that the plate, plate type,
     * plate owner, project, (screen), etc., exist - creating them if not.
     *
     * If a plate has a screen that is already attached to another plate in IceBear (i.e., there is
     * another plate whose screen has the same name), that screen is promoted to "standard screen" in
     * IceBear. Conditions are assumed to be the same if the screen name is the same.
     *
     * Stage 2: We look in the image store for plate directories modified in the last x hours,
     * creating new plate inspection and image records as required.
     *
     * This approach depends on filesystem last-modified dates and is therefore not watertight.
     * It also involves parsing entire directories at one subdirectory per plate, and will be
     * PAINFULLY SLOW if not COMPLETELY UNWORKABLE on an imaging system seeing even moderate use
     * over an extended period. This is intended as a first attempt only, in order to get one
     * lightly-used Rigaku installation tied into IceBear. The correct approach is to connect to
     * and use the (rather intimidating) Oracle database used by CrystalTrak.
     *
     * It is assumed that there is exactly one imager, called "Minstrel", and that all plates are
     * at +20. If a plate XML specifies a different temperature, import will fail.
     */

	use JetBrains\PhpStorm\NoReturn;

	set_time_limit(0);
    setUpClassAutoLoader();
    date_default_timezone_set('UTC');

    //Plates and images with a filesystem last-modified time more than this many hours ago will not be (re-)imported.
    //Does not affect by-barcode import - plate and images for the specified barcode are always imported.
    //Zero means "always import, regardless of last-modified time". Think carefully before setting this in a production environment!
    $lastModifiedHoursCutoffPlates=24;
    $lastModifiedHoursCutoffImages=72;

    $isCommissioning=false;

    const IMAGINGSESSIONS_PER_RUN = 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';
    const IMAGER_NAME = 'Minstrel';
    const IMAGER_TEMP = 4;

    const MINIMUM_INSPECTION_AGE_MINS = 20;  //Don't attempt to parse inspections younger than this. Images are likely not processed yet.

    const TEMPERATURE_FILENAME = 'IceBear_temperature';


    //define('LOGLEVEL_DEBUG', 1);
    //define('LOGLEVEL_INFO', 2);
    //define('LOGLEVEL_WARN', 3);
    //define('LOGLEVEL_ERROR', 4);
    $logLevel=Log::LOGLEVEL_INFO; //may be overridden by argument

    //Mapping of image type code (found in filenames) to name and light path.
    //Where multiple images are taken of a drop, the inspection will be split into several IceBear inspections.
    //The order of those inspections is the reverse of this array. If adding to this, keep the default 00/Visible FIRST.
    $imagingTypes=array(
        '00'=>array('name'=>'Normal', 'light'=>'Visible'), //This entry FIRST
        'E0'=>array('name'=>'Enhanced', 'light'=>'Visible'),
        'HF'=>array('name'=>'High Frequency', 'light'=>'Visible'),
        'U0'=>array('name'=>'UV', 'light'=>'UV'),
    );

    $shouldntOwnPlates=array('Administrator');
    $limitStart=0;
    $limitTotal=IMAGINGSESSIONS_PER_RUN;
    $fromDate=null;
    $imager=null;
    $barcode=null;
    $importMode="normal";

    for($i=1;$i<count($argv);$i++){
        $arg=$argv[$i];
        if($arg=='-h'){
            showHelp();
        } else if($arg=='-c'){
            $isCommissioning=true;
            $importMode="commissioning";
        } 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('/^-i.+$/',$arg)){
            $importMode="imager";
            $imager=substr($arg, 2);
        } else if(preg_match('/^-b.+$/',$arg)){
            $importMode="barcode";
            $barcode=substr($arg, 2);
        } else if(preg_match('/^-d\d\d\d\d-\d\d-\d\d+$/',$arg)){
            $importMode="date";
            $fromDate=substr($arg,2);
        }
    }

    try {
        Log::init($logLevel);

        Log::write(Log::LOGLEVEL_INFO, 'Importer started');

        if ($isCommissioning) {
            Log::write(Log::LOGLEVEL_INFO, 'Commissioning import specified');
        } else if (null != $fromDate) {
            Log::write(Log::LOGLEVEL_INFO, 'Start date specified: ' . $fromDate);
        } else {
            Log::write(Log::LOGLEVEL_INFO, 'Normal import - not from-date or commissioning');
        }


        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... ');
        try {
            session::init(new DummySession());
        } catch (BadRequestException $e) {
            Log::write(Log::LOGLEVEL_ERROR, '...session::init threw BadRequestException');
            exit();
        } catch (NotFoundException $e) {
            Log::write(Log::LOGLEVEL_ERROR, '...session::init threw NotFoundException');
            exit();
        } catch (ServerException $e) {
            Log::write(Log::LOGLEVEL_ERROR, '...session::init threw ServerException');
            exit();
        }
        session::set('isAdmin', true);
        Log::write(Log::LOGLEVEL_DEBUG, '...got LIMS session');
    } catch (ServerException $e){
        session::set('isAdmin', false);
        echo 'Exception thrown, and further exception thrown when logging error. Cannot continue.';
        exit();
    }

    $sharedProjectId=null;

    try {

        database::begin();

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

        $hasRigakuImagers=config::get('rigaku_hasimagers');
        if(!(int)$hasRigakuImagers){
            throw new Exception("IceBear is not configured for import from Rigaku imagers");
        }

        //Config items
        $plateStore=config::get('rigaku_platepath');
        $imageStore=config::get('rigaku_imagepath');
        $thumbStore=config::get('rigaku_thumbpath');
        define('IMAGER_MICRONS_PER_PIXEL',1*config::get('rigaku_micronsperpixel'));
        if(empty($plateStore) || empty($thumbStore) ||  empty($imageStore) ||empty(IMAGER_MICRONS_PER_PIXEL)){
            throw new Exception('Config items rigaku_platepath, rigaku_imagepath, rigaku_thumbpath, rigaku_micronsperpixel must all be defined');
        }
        $plateStore=rtrim($plateStore,'/').'/';
        $imageStore=rtrim($imageStore,'/').'/';
        $thumbStore=rtrim($thumbStore,'/').'/';


        $limsImageStore=config::get('core_imagestore');
        $limsImageStore=rtrim($limsImageStore,'/\\').'/';
        if(!file_exists($limsImageStore)){
            Log::write(Log::LOGLEVEL_ERROR, 'Cannot find LIMS image store: '.$limsImageStore);
            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');
        }

        //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'];

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

        $imager=getImager(IMAGER_NAME);
        Log::write(Log::LOGLEVEL_DEBUG, 'Finding IceBear imagingparametersversion for each imaging type...');
        foreach($imagingTypes as &$it){
            //If it isn't in IceBear, create it.
            //Should really check for and support multiple versions of each type, but we're
            //making dummy ones anyway - no real need for versioning, unlike Formulatrix.
            Log::write(Log::LOGLEVEL_DEBUG, 'Imaging type name is '.$it['name'].', looking for imagingparametersversion '.$it['name'].'_v1...');
            $limsParametersVersion=imagingparametersversion::getByName($it['name'].'_v1');
            if(!$limsParametersVersion){
                Log::write(Log::LOGLEVEL_DEBUG, '...not found, creating imagingparameters first...');
                $ip=imagingparameters::create(array(
                        'name'=>$it['name'],
                        'manufacturer'=>'Rigaku',
                        'manufacturerdatabaseid'=>0,
                        'projectid'=>$sharedProjectId
                ));
                $ip=$ip['created'];
                Log::write(Log::LOGLEVEL_DEBUG, '...done. creating imagingparametersversion...');
                $ipv=imagingparametersversion::create(array(
                        'name'=>$it['name'].'_v1',
                        'manufacturerdatabaseid'=>0,
                        'projectid'=>$sharedProjectId,
                        'imagingparametersid'=>$ip['id']
                ));
                $ipv=$ipv['created'];
                imagingparameters::update($ip['id'], array(
                        'currentversionid'=>$ipv['id']
                ));
                Log::write(Log::LOGLEVEL_DEBUG, '...created.');
                $limsParametersVersion=imagingparametersversion::getByName($it['name'].'_v1');
            }
            Log::write(Log::LOGLEVEL_DEBUG, 'imagingparametersversion '.$it['name'].'_v1... exists');
            $it['limsParametersVersion']=$limsParametersVersion;
        }
        database::commit();

        //Decide what to import
        $plateBarcodesToImport=array();
        $plateFilesToImport=array();
        $imagesToImport=array();

        $now=time();
//        $lastModifiedTimestampCutoffPlates=0;
//        $lastModifiedTimestampCutoffImages=0;
        if(0==$lastModifiedHoursCutoffPlates){
            $lastModifiedTimestampCutoffPlates=0;
        } else {
            $lastModifiedTimestampCutoffPlates=$now-(3600*$lastModifiedHoursCutoffPlates);
        }
        if(0==$lastModifiedHoursCutoffImages){
            $lastModifiedTimestampCutoffImages=0;
        } else {
            $lastModifiedTimestampCutoffImages=$now-(3600*$lastModifiedHoursCutoffImages);
        }

        if($isCommissioning){

            Log::write(Log::LOGLEVEL_INFO, 'Commissioning import specified');
            Log::write(Log::LOGLEVEL_INFO, 'Checking plate store...');
            $dir=dir($plateStore);
            $filename=$dir->read();
            while(!empty($filename)){
                if('.'!=$filename && '..'!=$filename){
                    $plateFilesToImport[]=$filename;
                }
                $filename=$dir->read();
            }
            Log::write(Log::LOGLEVEL_INFO, 'Done checking plate XML store. Found '.count($plateFilesToImport).' plate files to import.');

        } else if(null!=$barcode){

            //By-plate import
            Log::write(Log::LOGLEVEL_INFO, 'Single-plate import specified, barcode is '.$barcode);
            $lastModifiedTimestampCutoffPlates=0; //override for full plate history
            $lastModifiedTimestampCutoffImages=0; //override for full plate history
            $plateBarcodesToImport[]=$barcode;

        } else {

            Log::write(Log::LOGLEVEL_INFO, 'Checking plate store for files modified in last '.$lastModifiedHoursCutoffPlates.'hrs...');
            $dir=dir($plateStore);
            $filename=$dir->read();
            while(!empty($filename)){
                if('.'!=$filename && '..'!=$filename){
                    if(0==$lastModifiedTimestampCutoffPlates){
                        Log::write(Log::LOGLEVEL_DEBUG, 'No cutoff set, Importing:  '.$filename);
                        $plateFilesToImport[]=$filename;
                    } else if(filemtime($plateStore.$filename)<$lastModifiedTimestampCutoffPlates){
                        Log::write(Log::LOGLEVEL_DEBUG, 'File too old, ignoring:  '.$filename);
                    } else {
                        Log::write(Log::LOGLEVEL_DEBUG, 'File changed, importing:: '.$filename);
                        $plateFilesToImport[]=$filename;
                    }
                }
                $filename=$dir->read();
            }
            Log::write(Log::LOGLEVEL_INFO, 'Done checking plate XML store. Found '.count($plateFilesToImport).' plate files to import.');

        }

        //Nothing to do? Check the plate reimport queue.
        if(empty($plateBarcodesToImport) && empty($plateFilesToImport)){
            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 will be deleted.');
                } else {
                    $barcode=$plate['name'];
                    Log::write(Log::LOGLEVEL_INFO, 'Plate barcode is '.$barcode);
                    if(strlen($barcode<8)){
                        Log::write(Log::LOGLEVEL_WARN, 'Barcode is too short to be from Rigaku. Formulatrix plate?');
                        Log::write(Log::LOGLEVEL_ERROR, 'Queue entries for that plate ID will be deleted.');
                    } else {
                        Log::write(Log::LOGLEVEL_INFO, 'Adding to list of plates to import');
                        $plateBarcodesToImport[]=$barcode;
                    }
                }
                //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));
                Log::write(Log::LOGLEVEL_ERROR, "Queue entry for plate ID $plateId deleted.");
            }
        }


        Log::write(Log::LOGLEVEL_INFO, 'Beginning plate import');
        Log::write(Log::LOGLEVEL_INFO, '-------------------------------------------------');
        foreach($plateBarcodesToImport as $barcode){
            Log::write(Log::LOGLEVEL_DEBUG, 'Adding '.$barcode.' to barcodes list for image import');
            $imagesToImport[]=$barcode;
            database::begin();
            importPlateByBarcode($barcode, $isCommissioning);
            database::commit();
        }
        foreach($plateFilesToImport as $filename){
            Log::write(Log::LOGLEVEL_DEBUG, 'Beginning import of plate file '.$filename);
            database::begin();
            $plate=importPlateByFilename($filename, $isCommissioning);
            if(!$plate){
                Log::write(Log::LOGLEVEL_WARN, 'No plate barcode found in '.$filename);
                Log::write(Log::LOGLEVEL_WARN, 'Not importing from this file.');
            } else {
                $barcode=$plate['name'];
                Log::write(Log::LOGLEVEL_INFO, 'Plate barcode found in file is '.$barcode);
                Log::write(Log::LOGLEVEL_DEBUG, 'Adding '.$barcode.' to barcodes list for image import');
                $imagesToImport[]=$barcode;
            }
            database::commit();
            Log::write(Log::LOGLEVEL_DEBUG, 'Finished importing plate file '.$filename);
        }
        Log::write(Log::LOGLEVEL_INFO, 'Finished plate import');
        Log::write(Log::LOGLEVEL_INFO, 'Beginning inspection/image import');
        Log::write(Log::LOGLEVEL_INFO, '-------------------------------------------------');
        foreach($imagesToImport as $barcode){
            if(empty(trim($barcode))){
                Log::write(Log::LOGLEVEL_WARN, 'No plate barcode specified, not importing images for unknown plate');
                continue;
            }
            database::begin();
            importInspectionsForPlateBarcode($barcode,$isCommissioning);
            database::commit();
        }
        Log::write(Log::LOGLEVEL_INFO, 'Finished inspection/image import');


    } catch(Exception $e){
        try {
            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']);
                }
            }
            Log::write(Log::LOGLEVEL_ERROR, 'Import aborted due to exception');
        } catch (ServerException $loggingException){
            echo 'Exception thrown during import, and further exception thrown on logging that exception';
        }
    } finally {
        database::abort();
    }

    try {
        Log::write(Log::LOGLEVEL_INFO, 'Importer finished.');
        Log::write(Log::LOGLEVEL_INFO,'=================================================');
    } catch (ServerException $e) {
        echo 'Error on logging end of importer run';
        exit();
    }

    /**
     * @param string $barcode
     * @return string
     * @throws Exception
     */
    function getXmlFilenameForPlateBarcode(string $barcode):string {
        global $plateStore;
        Log::write(Log::LOGLEVEL_DEBUG, 'In getXmlFilenameForPlateBarcode, barcode="'.$barcode.'"...');
        $filenameToImport='';
        Log::write(Log::LOGLEVEL_DEBUG, 'Filenames do not contain plate barcodes, checking each file for barcode="'.$barcode.'"...');
        $dir=dir($plateStore);
        $filename=$dir->read();
        while(!empty($filename)){
            if('.'!=$filename && '..'!=$filename){
                Log::write(Log::LOGLEVEL_DEBUG, 'Checking file with name '.$filename);
                $contents=file_get_contents($plateStore.$filename);
                if(stripos($contents, ' barcode="'.$barcode.'" ')!==false){
                    $filenameToImport=$filename;
                    Log::write(Log::LOGLEVEL_DEBUG, 'Filename for barcode="'.$barcode.'" is '.$filenameToImport);
                    break;
                }
            }
            $filename=$dir->read();
        }
        if(empty($filenameToImport)){
            throw new Exception('No plate XML found containing barcode="'.$barcode.'" - cannot import this plate by barcode.');
        }
        Log::write(Log::LOGLEVEL_DEBUG, 'Returning from getXmlFilenameForPlateBarcode');
        return $filenameToImport;
    }

    /**
     * @param string $barcode
     * @param bool $isCommissioning
     * @return bool
     * @throws BadRequestException
     * @throws NotFoundException
     * @throws ServerException
     * @throws ForbiddenException
     */
    function importInspectionsForPlateBarcode(string $barcode, bool $isCommissioning=false): bool {
        global $imageStore, $thumbStore, $lastModifiedTimestampCutoffImages;
        Log::write(Log::LOGLEVEL_DEBUG, 'In importInspectionsForPlateBarcode, barcode="'.$barcode.'"...');
        if(empty(trim($barcode))){
            Log::write(Log::LOGLEVEL_WARN, 'Empty plate barcode - cannot determine plate');
            Log::write(Log::LOGLEVEL_DEBUG, 'Returning from importInspectionsForPlateBarcode');
            return false;
        }
        if(!file_exists($imageStore.'/'.$barcode)){
            Log::write(Log::LOGLEVEL_WARN, 'No images directory for '.$barcode.' - not importing inspections');
            Log::write(Log::LOGLEVEL_DEBUG, 'Returning from importInspectionsForPlateBarcode');
            return false;
        }
        if(!file_exists($thumbStore.'/'.$barcode)){
            Log::write(Log::LOGLEVEL_WARN, 'No thumbnails directory for '.$barcode.' - not importing inspections');
            Log::write(Log::LOGLEVEL_DEBUG, 'Returning from importInspectionsForPlateBarcode');
            return false;
        }
        $limsPlate=plate::getByName($barcode);
        if(!$limsPlate){
            Log::write(Log::LOGLEVEL_ERROR, 'No plate in IceBear with barcode '.$barcode.', cannot import images');
            throw new ServerException('No plate in IceBear with barcode '.$barcode.', cannot import images');
        }
        $filenamesToImport=array();
        $dir=dir(rtrim($imageStore,'/').'/'.$barcode);
        $filename=$dir->read();
        while($filename){
            if(str_ends_with($filename, '.xml')){
                if($isCommissioning || filemtime($dir->path.'/'.$filename)>$lastModifiedTimestampCutoffImages){
                    $filenamesToImport[]=$filename;
                }
            }
            $filename=$dir->read();
        }
        if(empty($filenamesToImport)){
            Log::write(Log::LOGLEVEL_WARN, 'No inspections for '.$barcode.' need importing');
        } else {
            $doImport=true;
            if($isCommissioning){
                $numImagerInspections=count($filenamesToImport);
                $limsInspections=imagingsession::getbyproperty('plateid',$limsPlate['id']);
                $numLimsInspections=1*($limsInspections['total']);
                Log::write(Log::LOGLEVEL_DEBUG, 'Commissioning import found '.$numLimsInspections.' in IceBear, '.$numImagerInspections.' in Rigaku');
                if($numImagerInspections<=$numLimsInspections){
                   Log::write(Log::LOGLEVEL_WARN, 'Not re-importing inspections for this plate');
                   $doImport=false;
                }
            }
            if($doImport){
                foreach($filenamesToImport as $filename){
                    importInspectionFromXml($barcode, $filename);
                }
            }
        }
        Log::write(Log::LOGLEVEL_DEBUG, 'Returning from importInspectionsForPlateBarcode');
        return true;
    }

    /**
     * Imports a group of images described in an XML file, as one or more IceBear plate inspections.
     *
     * Approach:
     *
     * Iterate through XML image elements, building an array of image paths for each imaging type (UV, Enhanced, etc.) found.
     * When all image paths have been determined, and the images found to exist, create one IceBear imagingsession for
     * each imaging type and either
     * - copy the images and thumbnails to the IceBear image store (if rigaku_imagepath does not begin with core_imagestore)
     * - point the database at the existing images in the Rigaku store
     *
     * Only composite ("extended focus" in Formulatrix terms) or best-slice images are imported, with composite being
     * preferred. Should any image be newer than the minimum age, import of the entire XML file is aborted; composite images
     * may not have finished generating.
     *
     * The XML files do not directly describe the composite image, so we have to munge the "best slice" image name to find it.
     *
     * @param string $barcode The plate barcode.
     * @param string $filename The name of the XML file describing an inspection.
     * @return bool
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws NotFoundException
     * @throws ServerException
     */
    function importInspectionFromXml(string $barcode, string $filename): bool {
        global $imageStore, $thumbStore, $limsImageStore, $imagingTypes;
        Log::write(Log::LOGLEVEL_DEBUG, 'In importInspectionFromXml');
        Log::write(Log::LOGLEVEL_DEBUG, 'barcode='.$barcode.', filename='.$filename);
        if(empty(trim($barcode))){
            Log::write(Log::LOGLEVEL_WARN, 'Empty plate barcode - cannot determine plate');
            Log::write(Log::LOGLEVEL_DEBUG, 'Returning from importInspectionFromXml');
            return false;
        }
        if(!file_exists($imageStore.'/'.$barcode.'/'.$filename)){
            throw new ServerException('No inspection XML '.$filename.' for '.$barcode);
        }
        $limsPlate=plate::getByName($barcode);
        if(!$limsPlate){
            throw new ServerException('No plate in IceBear with barcode '.$barcode.', cannot import images');
        }

        $temperatureFileContents=@file_get_contents($imageStore.'/'.$barcode.'/'.TEMPERATURE_FILENAME);
        if(false===$temperatureFileContents){
            //Helsinki, use hard-coded temperature
            $imager=getImager(IMAGER_NAME, IMAGER_TEMP);
        } else {
            //Lund - two imagers, reading temperature file written when parsing plate XML
            $imagerTemperature=(int)$temperatureFileContents;
            $imagerName=$temperatureFileContents.' imager';
            if($imagerTemperature>0){
                $imagerName='+'.$imagerTemperature.' imager';
            }
            $imager=getImager($imagerName, $imagerTemperature);
        }

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

        $now=time();
        $tooNew=false;
        $xml=simplexml_load_file($imageStore.'/'.$barcode.'/'.$filename);
        $images=$xml->children('rs',true)->data->children('z',true);
        $inspectionNumber=null;
        $inspections=array(); //will hold a list of images against their imaging type
        $lastModifiedTime=null;
        foreach($images as $i){
            $imageName=$i->attributes()->DROP_IMAGE_NAME;
            $row=$i->attributes()->ROW; //Zero-based!
            $col=$i->attributes()->COL; //Zero-based!
            $dropNumber=$i->attributes()->SUBWELL;
            $imagePlateBarcode=$i->attributes()->BARCODE_ID;
            Log::write(Log::LOGLEVEL_DEBUG, 'Image name is '.$imageName);
            if($barcode!=$imagePlateBarcode){
                throw new ServerException('Image with name '.$imageName.' is not for plate '.$barcode);
            }
            if(null==$inspectionNumber){
                $inspectionNumber=1*($i->attributes()->INSPECT_NUMBER);
            } else if($inspectionNumber!=$i->attributes()->INSPECT_NUMBER){
                throw new ServerException('Image with name '.$imageName.' is not for inspection '.$inspectionNumber);
            }
            $inspectionImageStore=$imageStore.$barcode.'/'.$inspectionNumber.'/';
            $inspectionThumbStore=$thumbStore.$barcode.'/'.$inspectionNumber.'/Thumbnails/160/';

            //RM01_03_000_0213_Proj1_Clon1_MC_0000MC000518_002_150730_01_01_02_00_99_031_001_RAI
            //Discard everything up to and including the barcode and its trailing underscore: 002_150730_01_01_02_00_99_031_001_RAI
            //Explode on underscores.
            //002 - Second inspection on this plate?
            //150730 - Date of inspection in YYMMDD format. If you're reading this in the year 2100, sorry. Wasn't me.
            //01 - Row (1-based)
            //01 - Col (1-based)
            //02 - Drop (1-based)
            //00 - Imaging profile, see $imagingTypes near top of this file
            //99 - Slice number. 99 is (a copy of) the best slice, 00 is a composite ("Extended-focus image" in Formulatrix) and the one we want.
            //031 - Unknown
            //001 - Unknown
            //RAI - Unknown
            $filenameParts=explode('_', explode($barcode.'_', $imageName)[1]);

            //Interested in composite images, but those aren't described in the XML.
            //Find the best slice and modify its filename to get that of the composite.
            if("99"!=$filenameParts[6]){
                Log::write(Log::LOGLEVEL_DEBUG, 'Image is not composite or best slice, not importing');
                continue;
            }
            Log::write(Log::LOGLEVEL_DEBUG, 'Image is composite or best slice, importing');

            if(!array_key_exists('profile'.$filenameParts[5], $inspections)){
                $inspections['profile'.$filenameParts[5]]=array();
                $inspectionArray=&$inspections['profile'.$filenameParts[5]];
                $inspectionArray['images']=array();
                $inspectionArray['profile']=$filenameParts[5];
            }
            //If the key DID exist, we still need to set it. Got away with this at Helsinki because the images were always
            //in inspection order. Lund, not so, resulting in images going to wrong imaging profile.
            $inspectionArray=&$inspections['profile'.$filenameParts[5]];

            //First we look for the "composite" / "extended focus" image, by changing the _99_ in the best-slice image name to _00_
            $compositeImageName=str_replace('_99_', '_00_', $imageName);
            $imagePath=$inspectionImageStore.$compositeImageName.'.jpg';
            $thumbPath=$inspectionThumbStore.$compositeImageName.'_160.jpg';

            //For some profiles, the imager doesn't generate a composite image, only the best slice is available.
            //If either the full-size or thumbnail composite is missing, fall back to the best-slice filenames
            if(!file_exists($imagePath) || !file_exists($thumbPath)) {
                Log::write(Log::LOGLEVEL_DEBUG, 'Did not find image and thumbnail for composite. Falling back to best slice.');
                $imagePath=$inspectionImageStore.$imageName.'.jpg';
                $thumbPath=$inspectionThumbStore.$imageName.'_160.jpg';
            }

            //If neither composite nor best-slice has both full-size and thumbnail, warn and don't import.
            if(!file_exists($imagePath) || !file_exists($thumbPath)){
                Log::write(Log::LOGLEVEL_WARN, 'Could not find both image and thumbnail for composite, or for best slice. Not importing image.');
                Log::write(Log::LOGLEVEL_DEBUG, 'Best-slice image path: '.$inspectionImageStore.$imageName.'.jpg');
                Log::write(Log::LOGLEVEL_DEBUG, 'Best-slice image exists: '.(file_exists($inspectionImageStore.$imageName.'.jpg')?'Yes':'No'));
                Log::write(Log::LOGLEVEL_DEBUG, 'Best-slice thumbnail path: '.$inspectionThumbStore.$imageName.'_160.jpg');
                Log::write(Log::LOGLEVEL_DEBUG, 'Best-slice thumbnail exists: '.(file_exists($inspectionThumbStore.$imageName.'_160.jpg')?'Yes':'No'));
                Log::write(Log::LOGLEVEL_DEBUG, 'Composite image path: '.$inspectionImageStore.$compositeImageName.'.jpg');
                Log::write(Log::LOGLEVEL_DEBUG, 'Composite image exists: '.(file_exists($inspectionImageStore.$compositeImageName.'.jpg')?'Yes':'No'));
                Log::write(Log::LOGLEVEL_DEBUG, 'Composite thumbnail path: '.$inspectionThumbStore.$compositeImageName.'_160.jpg');
                Log::write(Log::LOGLEVEL_DEBUG, 'Composite thumbnail exists: '.(file_exists($inspectionThumbStore.$compositeImageName.'_160.jpg')?'Yes':'No'));
                continue;
            }

            Log::write(Log::LOGLEVEL_DEBUG, 'Found image and thumbnail');
            Log::write(Log::LOGLEVEL_DEBUG, 'Image: '.$imagePath);
            Log::write(Log::LOGLEVEL_DEBUG, 'Thumb: '.$thumbPath);
            $inspectionArray['images'][]=array(
                    'image'=>$imagePath,
                    'thumb'=>$thumbPath,
                    'row'=>1+(int)$row,
                    'col'=>1+(int)$col,
                    'sub'=>(int)$dropNumber,
            );

            $lastModifiedTime=filemtime($imagePath);
            if($now-(60*MINIMUM_INSPECTION_AGE_MINS)<$lastModifiedTime){
                $tooNew=true;
                break;
            }
            if(!isset($inspectionArray['datetime'])){
                $datetime=gmdate('Y-m-d h:i:s', $lastModifiedTime);
                $inspectionArray['datetime']=$datetime;
            }
        }

        if($tooNew){
            Log::write(Log::LOGLEVEL_WARN, 'Not importing this inspection yet. Found image less than '.MINIMUM_INSPECTION_AGE_MINS.' minutes old.');
            Log::write(Log::LOGLEVEL_DEBUG, 'Returning from importInspectionFromXml');
            Log::write(Log::LOGLEVEL_INFO, '-------------------------------------------------');
            return false;
        }
        $imagedTime=$lastModifiedTime;
        foreach($imagingTypes as $label=>$type){
            Log::write(Log::LOGLEVEL_DEBUG, $label);
            if(!isset($inspections['profile'.$label])){ continue; }
            $found=$inspections['profile'.$label];
            if(empty($found['images'])){ continue; }
            Log::write(Log::LOGLEVEL_DEBUG, count($found['images']).' images found for '.$label);

            //Create the imagingsession
            $date=gmdate('Y-m-d H:i:s',$imagedTime);
            $imagedTime--; //Import the next imaging profile with timestamp a second before this one

            $imagingSessionName=$barcode.'_'.$date.'_profile'.$type['limsParametersVersion']['id'];
            $limsImagingSession=imagingsession::getByName($imagingSessionName);
            if(!empty($limsImagingSession)){
                Log::write(Log::LOGLEVEL_DEBUG, 'LIMS imagingsession already exists');
            } else {
                Log::write(Log::LOGLEVEL_DEBUG, 'LIMS imagingsession was not found, creating it');
                $params=array(
                        'name'=>$imagingSessionName,
                        'manufacturerdatabaseid'=>0,
                        'imagerid'=>$imager['id'],
                        'plateid'=>$limsPlate['id'],
                        'imageddatetime'=>$date,
                        'lighttype'=>$type['light'],
                        'imagingparametersversionid'=>$type['limsParametersVersion']['id']
                );
                imagingsession::create($params);
                $limsImagingSession=imagingsession::getByName($imagingSessionName);
            }
            if(!$limsImagingSession){
                throw new ServerException('Could not find imagingsession in LIMS after create');
            }
            $limsImagingSessionId=$limsImagingSession['id'];

            $copyImagesToStore=true;
            $destinationdir=substr($barcode,0,4).'/'.substr($barcode,0,6).'/'.$barcode.'/imagingsession'.$limsImagingSession['id'].'/';

            if(!empty(trim($limsImageStore, '/')) && stripos($imageStore, $limsImageStore)===0){
                Log::write(Log::LOGLEVEL_DEBUG, 'LIMS and Rigaku image stores appear common, will use Rigaku images in place (no copy)');
                $copyImagesToStore=false;
            } else {
                Log::write(Log::LOGLEVEL_DEBUG, 'LIMS and Rigaku image stores appear different, will copy Rigaku images into LIMS image store');
                //Create the directory of images from this imagingsession if it does not exist
                //Should be at image_store/MC00/MC0005/MC000518/imagingsession1234/thumbs/
                Log::write(Log::LOGLEVEL_DEBUG, 'Checking for imagingsession directory in LIMS image store...');
                @mkdir($limsImageStore.$destinationdir.'thumbs',0755,true);
                if(!file_exists($limsImageStore.$destinationdir.'thumbs')){
                    Log::write(Log::LOGLEVEL_ERROR, 'Path does not exist: '.$limsImageStore.$destinationdir.'thumbs');
                    throw new ServerException('Could not create destination directory in LIMS image store');
                }
                Log::write(Log::LOGLEVEL_DEBUG, '...exists.');
            }


            foreach($found['images'] as $i){

                if($copyImagesToStore){
                    Log::write(Log::LOGLEVEL_DEBUG, 'Copying images to LIMS store');
                    $imageDestination=$destinationdir.platetype::$rowLabels[(int)($i['row'])].str_pad($i['col'],2,'0',STR_PAD_LEFT).'.'.$i['sub'].'.jpg';
                    $thumbDestination=$destinationdir.'thumbs/'.platetype::$rowLabels[(int)($i['row'])].str_pad($i['col'],2,'0',STR_PAD_LEFT).'.'.$i['sub'].'.jpg';
                    copy($i['image'], $limsImageStore.$imageDestination);
                    copy($i['thumb'], $limsImageStore.$thumbDestination);
                    if(!file_exists($limsImageStore.$imageDestination)){
                        Log::write(Log::LOGLEVEL_ERROR, 'Compound image not copied: '.$limsImageStore.$imageDestination);
                        throw new ServerException('Could not create compound image in LIMS image store');
                    }
                    if(!file_exists($limsImageStore.$thumbDestination)){
                        Log::write(Log::LOGLEVEL_ERROR, 'Thumbnail image not copied: '.$limsImageStore.$thumbDestination);
                        throw new ServerException('Could not create thumbnail in LIMS image store');
                    }
                } else {
                    //If Helsinki breaks after updating, it's because we added this block for Lund.
                    $compareLength=min(strlen($imageStore), strlen($thumbStore));
                    for($j=1;$j<$compareLength;$j++){
                        if(substr($imageStore,0,$j)!=substr($thumbStore,0,$j)){ break; }
                        $limsImageStore=substr($imageStore,0,$j);
                    }

                    $imageDestination=str_replace($limsImageStore, '', $i['image']);
                    $thumbDestination=str_replace($limsImageStore, '', $i['thumb']);
                }

                $imageSize=getimagesize($i['image']);
                if(!$imageSize){
                    Log::write(Log::LOGLEVEL_WARN, ''.$i['image'].' may be corrupt, could not get pixel dimensions');
                    Log::write(Log::LOGLEVEL_WARN, 'Not linking image '.$i['image'].' in LIMS database');
                } else {
                    $imageWidth=$imageSize[0];
                    $imageHeight=$imageSize[1];
                    Log::write(Log::LOGLEVEL_DEBUG, 'Creating database record of image...');
                    $imageDbName=$barcode.'_'.platetype::$rowLabels[(int)($i['row'])].str_pad($i['col'],2,'0',STR_PAD_LEFT).'.'.$i['sub'].'_is'.$limsImagingSession['id'];

                    $preExisting=dropimage::getByName($imageDbName);
                    if(!$preExisting){
                        $welldropName=$barcode.'_'.platetype::$rowLabels[(int)($i['row'])].str_pad($i['col'],2,'0',STR_PAD_LEFT).'.'.$i['sub'];
                        $welldrop=welldrop::getByName($welldropName);
                        if(!$welldrop){
                            throw new ServerException('welldrop not found in LIMS for '.$welldropName);
                        }
                        dropimage::create(array(
                                'name'=>$imageDbName,
                                'projectid'=>$projectId,
                                'imagingsessionid'=>$limsImagingSessionId,
                                'welldropid'=>$welldrop['id'],
                                'pixelheight'=>$imageHeight,
                                'pixelwidth'=>$imageWidth,
                                'micronsperpixelx'=>IMAGER_MICRONS_PER_PIXEL,
                                'micronsperpixely'=>IMAGER_MICRONS_PER_PIXEL,
                                'imagestorepath'=>$limsImageStore,
                                'imagepath'=>$imageDestination,
                                'thumbnailpath'=>$thumbDestination
                        ));
                        //No - done in dropimage::create
                        //carryForwardScores($welldrop['id']);
                    }
                }
            }
        }
        Log::write(Log::LOGLEVEL_DEBUG, 'Returning from importInspectionFromXml');
        Log::write(Log::LOGLEVEL_INFO, '-------------------------------------------------');
        return true;
    }


    /**
     * Creates a record in the LIMS of the imager with the specified serial/name.
     * Capacity calculations may be somewhat complicated by the existence in the Rigaku DB of a second hotel that is not installed.
     * Note that IceBear determines an imagingsession temperature from that of the imager, so a twin +4/+20 Rigaku with a shared
     * camera will need to be treated as two separate imagers. This is NOT SUPPORTED at present. @TODO Support multiple temperatures.
     * @param string $imagerName The imager name.
     * @param int $temperature
     * @return array
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws NotFoundException
     * @throws ServerException
     */
    function getImager(string $imagerName, int $temperature=20): array {
        Log::write(Log::LOGLEVEL_DEBUG, 'In getImager, imagerName='.$imagerName);
        $imager=imager::getByName($imagerName);
        if(!$imager){
            Log::write(Log::LOGLEVEL_WARN, 'Imager "'.$imagerName.'" does not exist, creating it');
            $capacity=500;
            //TODO Calculate capacity from table structure
            //Looks like Hotels are swappable, Helsinki's has Linbro and SBS?
            $imager=imager::create(array(
                    'name'=>$imagerName,
                    'friendlyname'=>$imagerName,
                    'manufacturer'=>'Rigaku',
                    'temperature'=>$temperature,
                    'platecapacity'=>$capacity,
                    'alertlevel'=>floor($capacity*IMAGERLOAD_ALERT_THRESHOLD_PERCENT/100),
                    'warninglevel'=>floor($capacity*IMAGERLOAD_WARNING_THRESHOLD_PERCENT/100)
            ));
            $imager=$imager['created'];
        }
        Log::write(Log::LOGLEVEL_DEBUG, 'Returning from getImager');
        return $imager;
    }


    /**
     * Creates a record in the LIMS of the plate with the specified barcode.
     * If the plate type does not exist in IceBear, it is created.
     * If the plate owner does not exist in IceBear, the user is created.
     * If screen information is present in the XML, a screen is created, or an existing one is associated with the plate.
     * @param string $barcode The plate barcode.
     * @param bool $isCommissioning
     * @return array the plate
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws NotFoundException
     * @throws NotModifiedException
     * @throws ServerException
     * @throws Exception
     */
    function importPlateByBarcode(string $barcode, bool $isCommissioning=false): array {
        Log::write(Log::LOGLEVEL_DEBUG, 'In importPlateByBarcode, barcode='.$barcode);
        $filename=getXmlFilenameForPlateBarcode($barcode);
        $plate=importPlateByFilename($filename, $isCommissioning);
        Log::write(Log::LOGLEVEL_DEBUG, 'Returning from importPlateByBarcode');
        return $plate;
    }

    /**
     * Creates a record in the LIMS of a plate, by parsing the specified Rigaku plate XML.
     * If the plate type does not exist in IceBear, it is created.
     * If the plate owner does not exist in IceBear, the user is created.
     * If screen information is present in the XML, a screen is created, or an existing one is associated with the plate.
     * @param string $filename
     * @param bool $isCommissioning
     * @return array|boolean the plate, or false if the file does not look like plate XML.
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws NotFoundException
     * @throws NotModifiedException
     * @throws ServerException
     */
    function importPlateByFilename(string $filename, bool $isCommissioning=false): array {
        global $lastModifiedTimestampCutoffPlates, $plateStore, $imageStore, $shouldntOwnPlates, $scoringSystem, $importMode;
        Log::write(Log::LOGLEVEL_DEBUG, 'In importPlateByFilename, filename='.$filename);
        if(!file_exists($plateStore.$filename)){
            throw new ServerException('Plate filename '.$filename.' not found in plate XML store');
        }
        if(stripos($filename, 'Plate_')!==0){
            Log::write(Log::LOGLEVEL_WARN, 'Not attempting to import file: '.$filename);
            Log::write(Log::LOGLEVEL_WARN, 'File does not look like a plate');
            return false;
        }
        if(!$isCommissioning && 0!=$lastModifiedTimestampCutoffPlates && filemtime($plateStore.$filename)<$lastModifiedTimestampCutoffPlates){
            Log::write(Log::LOGLEVEL_WARN, 'File was not modified recently enough. Ignoring.');
            Log::write(Log::LOGLEVEL_DEBUG, 'Returning from importPlateByFilename, nothing to do');
            return false;
        }
        $xml=@simplexml_load_file($plateStore.$filename);
        if(false===$xml){
            throw new ServerException('Could not parse plate XML '.$filename);
        }
        $xmlPlate=$xml->plate->attributes();
        $barcode=$xmlPlate['barcode'];
        $xmlPlateType=$xml->plate->format->attributes();

        if(''==$xmlPlate->temperature){
            Log::write(Log::LOGLEVEL_WARN, 'Plate temperature: not specified. Assuming '.IMAGER_TEMP);
        } else if(IMAGER_TEMP!=$xmlPlate->temperature){
            Log::write(Log::LOGLEVEL_WARN, 'Plate is at wrong temperature: '.$xmlPlate->temperature.' - should be '.IMAGER_TEMP);
            //throw new Exception('Plate is at wrong temperature: '.$xmlPlate->temperature.' - should be '.IMAGER_TEMP);
        }

        if(''==trim($barcode)){
            Log::write(Log::LOGLEVEL_WARN, 'No plate barcode in XML file. Not importing plate from XML.');
            Log::write(Log::LOGLEVEL_DEBUG, 'Returning from importPlateByFilename');
            return false;
        }

        $plate=plate::getByName($barcode);
        if($plate){

            Log::write(Log::LOGLEVEL_DEBUG, 'Plate '.$barcode.' exists in IceBear with ID '.$plate['id']);
            //We can assume that plate type and user already exist in IceBear
            $projectId=$plate['projectid'];

        } else {
            Log::write(Log::LOGLEVEL_INFO, 'Plate '.$barcode.' does not exist in IceBear, need to create it');

            //check for plate type
            $plateType=getPlateType($xmlPlateType->name, $xmlPlateType->rows, $xmlPlateType->cols, $xmlPlateType->subs);
            platetype::update($plateType['id'], array('defaultdropsize'=>$xmlPlateType->def_drop_vol.'uL'));

            //check for user
            $username=trim($xmlPlate->user);
            if(empty($username) || in_array($username, $shouldntOwnPlates)){
                Log::write(Log::LOGLEVEL_WARN, 'Username '.$username.' should not own plates, finding first username in IceBear');
                //First user ought to be system administrator - username may vary
                $user=user::getFirstAdmin();
                $username=$user['name'];
                Log::write(Log::LOGLEVEL_WARN, 'Setting plate '.$barcode.' owner to '.$username);
            }
            $plateOwner=getUser($username);

            //check for project
			$project=project::getByName($xmlPlate->project);
            if(!$project){
                try {
                    $project=project::create(array(
                            'name'=>$xmlPlate->project->__toString(),
                            'owner'=>$plateOwner['id'],
                            'description'=>'Created automatically by Rigaku importer'
                    ));
                    $project=$project['created'];
                } catch(Exception){
                    session::set('isAdmin',true);
                    $project=project::getByName($xmlPlate->project->__toString());
                }
            }
            $projectId=$project['id'];

            //This is a hack because the importer failed in HEL after upgrade to 1.5.1. I assume this is something to do
            //with the user being created above, somehow. session::isAdmin had unset to false, so force it to true, as it
            //should be throughout this importer run.
            session::set('isAdmin',true);

            //create the plate
            $plate=plate::create(array(
                    //'screenid'=>whatever,
                    'name'=>$barcode,
                    'description'=>$xmlPlate->name,
                    'ownerid'=>$plateOwner['id'],
                    'projectid'=>$projectId,
                    'platetypeid'=>$plateType['id'],
                    'crystalscoringsystemid'=>$scoringSystem['id']
            ));
            $plate=$plate['created'];
            Log::write(Log::LOGLEVEL_INFO, 'Plate created with ID '.$plate['id']);
        }

        $xmlScreen=$xml->screen;
        $screenName='';
        if(empty($xmlScreen)){
            Log::write(Log::LOGLEVEL_DEBUG, 'XML has no screen element');
            Log::write(Log::LOGLEVEL_INFO,'No screen info attached to plate. Screen will need to be set manually.');
        } else {
            Log::write(Log::LOGLEVEL_DEBUG, 'XML has screen element, parsing');
            $screenName=$xmlScreen->attributes()->name;
            $limsScreen=screen::getByName($screenName);
            if(!empty($limsScreen)){

                Log::write(Log::LOGLEVEL_INFO,'Screen with name '.$screenName.' exists in LIMS. Using existing screen.');

            } else {

                //Have not seen the screen before. Assume optimization screen.
                //Parse screen XML, create screen and conditions.
                Log::write(Log::LOGLEVEL_INFO,'Screen with name '.$screenName.' does not exist in LIMS. Parsing screen XML.');

                $screenRows=$xmlScreen->format->attributes()->rows;
                $screenCols=$xmlScreen->format->attributes()->cols;

                $limsScreen=screen::create(array(
                        'projectid'=>$projectId,
                        'name'=>$screenName,
                        'rows'=>$screenRows,
                        'cols'=>$screenCols
                ));
                $limsScreen=$limsScreen['created'];

                $conditionsFound=0;
                foreach($xmlScreen->children() as $c){
                    if("well"!=$c->getName()){ continue; } //not the format or comments element
                    $wellNumber=$c->attributes()->number;
                    $conditionParts=array();
                    $parts=$c->children();
                    foreach($parts as $p){
                        $part=$p->attributes()->class.': '. $p->attributes()->conc . $p->attributes()->units.' '.$p->attributes()->name;
                        $pH=$p->attributes()->ph;
                        if(!empty($pH) && ""!=$pH){ $part.=', pH '.$pH; }
                        $conditionParts[]=$part;
                    }
                    $condition=implode('; ', $conditionParts);
                    $row=floor(($wellNumber-1)/$screenCols)+1;
                    $col=floor(($wellNumber-1)%$screenCols)+1;
                    Log::write(Log::LOGLEVEL_DEBUG, 'Condition in well '.$wellNumber.' is '.$condition);
                    Log::write(Log::LOGLEVEL_DEBUG, 'Updating dummy condition in row '.$row.' col '.$col.'...');
                    $existingCondition=screencondition::getByName('screen'.$limsScreen['id'].'_well'.str_pad($wellNumber, 3, '0', STR_PAD_LEFT));
                    screencondition::update($existingCondition['id'], array(
                            'description'=>$condition
                    ));
                    Log::write(Log::LOGLEVEL_DEBUG, '...created');
                    $conditionsFound++;
                }
                if($conditionsFound!=$screenCols*$screenRows){
                    Log::write(Log::LOGLEVEL_WARN, 'Screen has '.$conditionsFound.' conditions, expected '.$screenCols*$screenRows.' ('.$screenCols.'x'.$screenRows.')');
                }
                Log::write(Log::LOGLEVEL_DEBUG, 'Finished creating screen');
            }

            //Lastly, attach the screen to the plate.
            Log::write(Log::LOGLEVEL_DEBUG, 'Attaching screen '.$limsScreen['id'].' to plate '.$plate['id'].'...');
            plate::update($plate['id'], array('screenid'=>$limsScreen['id']));
            Log::write(Log::LOGLEVEL_DEBUG, 'Finished creating screen');
        }

        //If more than one plate has the screen found, promote it to a standard screen
        //(If the screen is newly created, this almost certainly won't happen.)
        if(!empty($limsScreen)){
            $platesWithScreen=plate::getByProperty('screenid', $limsScreen['id']);
            if(!empty($platesWithScreen) && isset($platesWithScreen['rows']) && count($platesWithScreen['rows'])>1){
                Log::write(Log::LOGLEVEL_INFO,'More than one plate has screen '.$limsScreen['name'].'.');
                Log::write(Log::LOGLEVEL_INFO,'Promoting '.$screenName.' to standard screen.');
                screen::update($limsScreen['id'], array(
                        'isstandard'=>true,
                ));
            } else {
                //Leave as optimization screen
                Log::write(Log::LOGLEVEL_INFO,'First time seeing screen '.$limsScreen['name'].'.');
                Log::write(Log::LOGLEVEL_INFO,'Assuming optimization screen. Will promote to standard screen if seen again.');
            }
        }

        // The plate XML must be saved manually from CrystalTrak. If this is done late, some inspections may be older than the age cutoff.
        // If we are importing this plate, it's likely the first time we've seen the plate XML, but there may be several older inspections.
        //
        // To get around this, we "touch" all the inspection XML files. This likely means that none will be imported on this run, due to the
        // minimum age requirement, but they will be imported as soon as they appear old enough.
        //
        // Note that this is only done if (1) this is a normal import, and (2) there are 2 or more inspection directories. We assume that the
        // maximum age cutoff is high enough, and the plate is being imaged often enough at the start of its schedule, that the first inspection
        // will not age out before the second is written.

        $dir=dir(rtrim($imageStore,'/').'/'.$barcode);
        if("normal"===$importMode && file_exists($dir->path.'/1') && file_exists($dir->path.'/2')){
            Log::write(Log::LOGLEVEL_WARN, 'Newly-found plate has multiple inspections');
            Log::write(Log::LOGLEVEL_WARN, 'Touching all inspection XML files to override age limits');
            $filename=$dir->read();
            while($filename){
                if(str_ends_with($filename, '.xml')){
                    Log::write(Log::LOGLEVEL_WARN, 'Touching '.$dir->path.'/'.$filename);
                    touch($dir->path.'/'.$filename);
                }
                $filename=$dir->read();
            }

        }

        Log::write(Log::LOGLEVEL_DEBUG, 'Returning from importPlateByFilename');
        return $plate;
    }

    /**
     * Creates the Rigaku scoring system in the LIMS, if it does not exist
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws NotFoundException
     * @throws ServerException
     * @throws Exception
     */
    function importScoringSystem(){
        Log::write(Log::LOGLEVEL_DEBUG, 'In importScoringSystem');

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

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

        $scores=array(
                array('0','0','Clear','ffffff'),
                array('1','9','Crystals','ff0000'),
                array('2','8','Micro-crystals','ffff00'),
                array('3','7','Crystalline','999999'),
                array('4','6','Clusters','999999'),
                array('5','5','Spherulites','999999'),
                array('6','4','Precipitation','999999'),
                array('7','3','Heavy precipitation','999999'),
                array('8','2','Phase separation','999999'),
                array('9','1','Matter','999999'),
        );
        $index=0;
        foreach($scores as $s){
            $request=array(
                    'crystalscoringsystemid'=>$system['id'],
                    'hotkey'=>$s[0],
                    'scoreindex'=>$s[1],
                    'color'=>$s[3],
                    'label'=>$s[2],
                    'name'=>'Rigaku_'.$s[2],
            );
            crystalscore::create($request);
            $index++;
        }
        Log::write(Log::LOGLEVEL_DEBUG, 'returning from importScoringSystem');
        return $system;
    }


    /**
     * Retrieves the IceBear platetype with the specified name, or creates and returns it.
     * @param $name
     * @param $rows
     * @param $cols
     * @param $subs
     * @return array|mixed
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws NotFoundException
     * @throws ServerException
     * @throws Exception
     */
    function getPlateType($name, $rows, $cols, $subs){
        Log::write(Log::LOGLEVEL_DEBUG, 'In getPlateType, name='.$name);
        $plateType=platetype::getByName($name);
        if(!$plateType){
            //create it
            Log::write(Log::LOGLEVEL_WARN, 'Plate type '.$name.' does not exist in LIMS, creating it');
            $sharedProject=project::getByName('Shared');
			if(!$sharedProject){
				throw new NotFoundException('Could not read shared project');
			}
			$dropMapping='1,R';
            if($subs>1){
                $top='';
                $bottom='';
                for($i=1;$i<=$subs;$i++){
                    $top.=$i;
                    $bottom.='R';
                }
                $dropMapping=$top.','.$bottom;
            }
            $created=platetype::create(array(
                'name'=>$name,
                'rows'=>$rows,
                'cols'=>$cols,
                'subs'=>$subs,
                'dropmapping'=>$dropMapping,
                'projectid'=>$sharedProject['id'],
            ));
            $plateType=$created['created'];
            Log::write(Log::LOGLEVEL_DEBUG, 'Plate type '.$name.' created in LIMS with ID '.$plateType['id']);
        }
        if($rows!=$plateType['rows'] || $cols!=$plateType['cols'] || $subs!=$plateType['subs']){
            $msg='Plate type geometry mismatch.';
            $msg.=' IceBear: '.$plateType['rows'].'x'.$plateType['cols'].'x'.$plateType['subs'];
            $msg.=' Plate XML: '.$rows.'x'.$cols.'x'.$subs;
            throw new Exception($msg);
        }
        Log::write(Log::LOGLEVEL_DEBUG, 'Returning from getPlateType');
        return $plateType;
    }


    /**
     * Returns the LIMS user with the specified username, or creates and returns it.
     * The user is created in an inactive state, with a bogus email address and name, and will be unable to log in
     * until an administrator activates their account.
     * @param string $username The user's Rigaku username
     * @return array|mixed
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws NotFoundException
     * @throws ServerException
     */
    function getUser(string $username): array{
        Log::write(Log::LOGLEVEL_DEBUG, 'In getUser, username='.$username);
        if(str_contains($username, ' ')){
            Log::write(Log::LOGLEVEL_WARN, 'Username='.$username.' contains spaces, which will be stripped');
            Log::write(Log::LOGLEVEL_WARN, 'IceBear usernames cannot contain spaces');
            $username=str_replace(' ', '', $username);
            Log::write(Log::LOGLEVEL_WARN, 'Username is now '.$username);
        }
        $user=user::getByName($username);
        if(!$user){
            $lowerCaseUsername=strtolower($username);
            $lowerCaseUser=user::getByName($lowerCaseUsername);
            if($lowerCaseUser){ $user=$lowerCaseUser; }
        }
        if(!$user){
            Log::write(Log::LOGLEVEL_WARN, 'User '.$username.' does not exist in LIMS, creating it');
            $user=user::create(array(
                    'name'=>$username,
                    'fullname'=>$username,
                    'email'=>$username.'@bogus.bogus',
                    'password'=>'USELESSPASSWORD',
                    'isactive'=>0
            ));
            $user=$user['created'];
        } else {
            Log::write(Log::LOGLEVEL_DEBUG, 'User exists in IceBear');
        }
        Log::write(Log::LOGLEVEL_DEBUG, 'Returning from getUser');
        return $user;
    }

    function setUpClassAutoLoader(){
        spl_autoload_register(function($className){
            $paths=array(
                    '../classes/',
                    '../classes/core/',
                    '../classes/core/exception/',
                    '../classes/core/authentication/',
                    '../classes/core/interface/',
                    '../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 (re-)import all images and plates created or modified within the last 24 hours.\n";
        echo "\nThe following arguments can be supplied to modify this behaviour:\n";
        echo "\n -h Show help";
        echo "\n -c Commissioning import - import ALL found plates and images";
        echo "\n -bBARCODE - (Re)import all inspections and images for this barcode - not compatible with other options";
        exit;
    }
