<?php
//Configuration. Do not change anything else.
const MINIMUM_AGE_MINUTES = 15;
const MAXIMUM_AGE_MINUTES = 400;
const IMAGES_ROOT = 'C:/Rigaku example plate/Pub/Images/'; // <<<MUST HAVE TRAILING SLASH
const PLATES_ROOT = 'C:/Rigaku example plate/Pub/PlateXML/'; // <<<MUST HAVE TRAILING SLASH
const TEMPERATURE_FILENAME = 'IceBear_temperature';

/**
 * This script generates XML describing a plate inspection done by a Rigaku imager.
 *
 * The IceBear Rigaku/CrystalTrak importer, as written for use in Helsinki, depends on both plate and inspection XML
 * files. The plate files must be generated manually by exporting from CrystalTrak after plate setup. The inspection
 * XML files may or may not be generated; it appears that this is non-configurable and dependent entirely on the
 * version of the imager control/image processing software running on the hardware itself.
 *
 * In order to make the importer usable quickly, without the risk of introducing bugs that break image import at
 * existing sites, we leave the importer itself alone and generate the inspection XML that it expects. A better
 * approach may be to ignore this XML entirely and have the importer try to gain the relevant info from the filesystem.
 *
 * Strategy:
 * - scan the plates directory for recently-modified plate directories (between MINIMUM_AGE_MINUTES and
 *        MAXIMUM_AGE_MINUTES minutes old)
 * - for each plates directory found,
 * - - for each inspection images directory found,
 * - - if last-modified time is between MINIMUM_AGE_MINUTES and MAXIMUM_AGE_MINUTES minutes old, and XML file does not
 *          exist, generate XML.
 *
 * We wait for the subdirectory to remain unmodified for MINIMUM_AGE_MINUTES minutes because we have no other way of
 * knowing that the images have stopped coming. Note that the importer itself imposes a minimum age on the XML for the
 * same reason, and these minimum ages are cumulative.
 */

chdir(__DIR__);
set_time_limit(0);
setUpClassAutoLoader();
date_default_timezone_set('UTC');
$now=time();
define('MINIMUM_AGE_CUTOFF_TIMESTAMP', $now-(60*MINIMUM_AGE_MINUTES));
define('MAXIMUM_AGE_CUTOFF_TIMESTAMP', $now-(60*MAXIMUM_AGE_MINUTES));
const PLATE_BARCODE_REGEX = '|^[A-Z][A-Z]?[0-9]{6}$|';

try {
    Log::init(Log::LOGLEVEL_DEBUG);

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

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

    if(!file_exists(IMAGES_ROOT)){
        throw new Exception('Images root directory not found. Is the drive mounted? '.IMAGES_ROOT);
    }

    Log::info('Generating XML for any new plate inspections');
    Log::debug('Timestamp now: '.$now);
    Log::debug('Minimum age: '.MINIMUM_AGE_MINUTES.' minutes. Cutoff timestamp: '.MINIMUM_AGE_CUTOFF_TIMESTAMP);
    Log::debug('Maximum age: '.MAXIMUM_AGE_MINUTES.' minutes. Cutoff timestamp: '.MAXIMUM_AGE_CUTOFF_TIMESTAMP);
    Log::info('Looking for plate directories found that were modified between '.MINIMUM_AGE_MINUTES.' and '.MAXIMUM_AGE_MINUTES.' minutes ago');

    $barcode=null;
    for($i=1;$i<count($argv);$i++){
        $arg=$argv[$i];
        if(preg_match('/^-b.+$/',$arg)){
            $barcode=substr($arg, 2);
        }
    }
    if(empty($barcode)){
        Log::info('No barcode specified, searching for plates modified within time cutoffs');
        $modifiedPlates=findBarcodesOfModifiedPlates();
    } else {
        Log::info("Plate barcode $barcode specified, ignoring time cutoffs");
        $modifiedPlates=[];
        $modifiedPlates[]=$barcode;
    }

    if(empty($modifiedPlates)) {
        Log::info('No matching plate directories found');
    } else {
        Log::info(count($modifiedPlates).' recently-modified plate directories found');
        Log::info('-----------------------------');
        foreach ($modifiedPlates as $plateBarcode) {

            if(!file_exists(IMAGES_ROOT).$plateBarcode.'/'.TEMPERATURE_FILENAME){
                writeTemperatureFiles();
            }

            $modifiedInspections = findModifiedInspectionsWithoutXml($plateBarcode);
            if (empty($modifiedInspections)) {
                Log::info("Plate $plateBarcode was modified within time but has no new inspections without XML.");
                Log::info("This might be because we wrote XML on a previous run.");
            } else {
                $numInspections = count($modifiedInspections);
                Log::info("Plate $plateBarcode has $numInspections new inspection(s).");
                foreach ($modifiedInspections as $inspectionNumber) {
                    generateInspectionXML($plateBarcode, $inspectionNumber);
                    touchPlateXML($plateBarcode);
                }
            }
        }
        Log::info('-----------------------------');
    }
} catch (Exception $exception){
    try {
        Log::error('Exception was thrown');
        Log::error('Message: '.$exception->getMessage());
        Log::error('Stack trace:');
        $trace=$exception->getTraceAsString();
        $trace=explode("\n",$trace);
        foreach ($trace as $line) {
            Log::error('  '.$line);
        }
    } catch (ServerException $e) {
        echo 'Exception was thrown, and logging threw exception. ';
        echo 'Original exception message: '.$exception->getMessage();
        echo 'Logging exception message: '.$e->getMessage();
    }
} finally {
    try {
        Log::info(' ');
        Log::info('Run complete.');
        Log::info('=============================');
    } catch (ServerException $e) {
        echo 'Logging threw exception. ';
        echo 'Logging exception message: '.$e->getMessage();
    }
}

/**
 * Iterates through all the plate XML files in PLATES_ROOT, writing a temperature file into the plate's directory in
 * IMAGES_ROOT.
 * @return void
 * @throws Exception
 */
function writeTemperatureFiles(){
    $plates=dir(PLATES_ROOT);
    $filename=$plates->read();
    while(!empty($filename)){
        if('.'!==$filename && '..'!==$filename && str_ends_with($filename, '.xml')){
            $xml=simplexml_load_file(PLATES_ROOT.$filename);
            if(false===$xml){
                throw new Exception('Could not parse plate XML '.$filename);
            }
            $xmlPlate=$xml->plate->attributes();
            $barcode=$xmlPlate['barcode'];
            $temperature=$xmlPlate['temperature'];
            if(""==$temperature){ $temperature="4"; } // +4 plate imaged ad-hoc in +20 imager (Lund)
            $temperatureFilePath=IMAGES_ROOT.$barcode.'/'.TEMPERATURE_FILENAME;
            if(!@file_exists($temperatureFilePath) && !@file_put_contents($temperatureFilePath, $temperature)){
                    throw new Exception('Could not write temperature file for plate: '.$barcode);
            }
        }
        $filename=$plates->read();
    }

}

/**
 * Attempts to touch the plate XML file to update its modification time. This prevents some odd behaviour where
 * inspections are only imported if the plate is younger than the plate modification cutoff.
 * @param $plateBarcode
 * @return void
 * @throws BadRequestException
 * @throws NotFoundException
 * @throws ServerException
 */
function touchPlateXml($plateBarcode){
    Log::info("Touching plate file for plate $plateBarcode to update its modification time...");
    $plate=plate::getByName($plateBarcode);
    if(!$plate){
        Log::warning("Plate with barcode $plateBarcode not found in IceBear. Might not be imported yet.");
        Log::warning("Cannot determine filename of plate XML file, so cannot touch it");
        return;
    }
    $description=$plate['description'];
    if(!preg_match('/^[A-Za-z0-9_-]+$/', $description)){
        Log::warning('Not touching plate file because plate description is not alphanumeric+underscore+dash');
        return;
    }
    $filePath=PLATES_ROOT.'Plate_'.$description.'.xml';
    if(!file_exists($filePath)){
        Log::warning('Not touching plate file because it is not found at the expected location.');
        Log::warning('It should be at '.$filePath);
        Log::warning('If the description was changed after import, that would cause this to happen.');
        return;
    }
    if(!@touch($filePath)) {
        Log::warning('Unable to touch plate description file.');
        return;
    }
    Log::info('Finished touching plate file');
}

/**
 * Returns an array of plate barcodes (directory names are plate barcodes) whose last-modified time falls between
 * the minimum and maximum constraints. If none are found, returns an empty array.
 * @return array
 * @throws ServerException
 */
function findBarcodesOfModifiedPlates(): array {
    Log::debug('In findBarcodesOfModifiedPlates');
    $barcodes=[];
    $plates=dir(IMAGES_ROOT);
    $barcode=$plates->read();
    while(!empty($barcode)){
        if('.'!==$barcode && '..'!==$barcode  && preg_match(PLATE_BARCODE_REGEX, $barcode)){
            $modifiedTime=filemtime(IMAGES_ROOT.$barcode);
            if($modifiedTime>MINIMUM_AGE_CUTOFF_TIMESTAMP){
                Log::debug('Plate '.$barcode.' has been modified, but too recently - inspection may be in progress');
            } else if($modifiedTime<MAXIMUM_AGE_CUTOFF_TIMESTAMP){
                Log::debug('Plate '.$barcode.' has not been modified recently enough, not adding to list');
            } else {
                Log::debug('Plate '.$barcode.' was modified within the cut-off period, adding to list');
                $barcodes[]=$barcode;
            }
        } else {
            Log::debug('String "'.$barcode.'" does not look like a Rigaku barcode');
        }
        $barcode=$plates->read();
    }
    Log::debug('Returning from findBarcodesOfModifiedPlates with '.count($barcodes).' barcodes');
    return $barcodes;
}

/**
 * Returns an array of the directory names modified between MINIMUM_AGE_MINUTES and MAXIMUM_AGE_MINUTES and with
 * no generated XML, or an empty array if none are found. These directory names will be integers (1-based) with
 * the highest numbers being the most recent inspections.
 * @param $plateBarcode
 * @return array
 * @throws ServerException
 * @throws BadRequestException
 */
function findModifiedInspectionsWithoutXml($plateBarcode): array {
    Log::debug('In findModifiedInspectionsWithoutXml, plateBarcode='.$plateBarcode);
    $plateDirectory=IMAGES_ROOT.$plateBarcode.'/';
    $contents=dir($plateDirectory);
    $fileName=$contents->read();
    $inspectionNumbers=array();
    while(!empty($fileName)){
        if((int)$fileName>0 && is_dir($plateDirectory.$fileName)){
            Log::debug('Checking directory "'.$fileName.'"');
                $hasXml=file_exists($plateDirectory.getInspectionXmlFilename($plateBarcode, $fileName));
                if($hasXml){
                    Log::debug('Inspection XML file already exists');
                } else {
                    Log::debug('Inspection XML file does not exist. Adding inspection number to return');
                    $inspectionNumbers[]=$fileName;
                }
        } else {
            Log::debug('Ignoring '.$fileName);
        }
        $fileName=$contents->read();
    }
    Log::debug('Returning '.count($inspectionNumbers).' inspections that need XML');
    return $inspectionNumbers;
}

/**
 * @param $plateBarcode
 * @param $inspectionNumber
 * @return string
 * @throws BadRequestException
 */
function getInspectionXmlFilename($plateBarcode, $inspectionNumber): string {
    $inspectionNumber=(int)$inspectionNumber;
    if(!preg_match(PLATE_BARCODE_REGEX, $plateBarcode)){
        throw new BadRequestException("Bad plate barcode $plateBarcode got to getInspectionXmlFilename");
    }
    return "IceBear_$plateBarcode"."_Inspection_$inspectionNumber.xml";
}

/**
 * @param $plateBarcode
 * @param $inspectionNumber
 * @throws ServerException
 * @throws Exception
 */
function generateInspectionXML($plateBarcode, $inspectionNumber){
    Log::debug("In generateInspectionXML, plateBarcode=$plateBarcode, inspectionNumber=$inspectionNumber");
    $inspectionNumber=(int)$inspectionNumber;
    $plateDirectory=IMAGES_ROOT.$plateBarcode.'/';
    $inspectionDirectory=$plateDirectory.$inspectionNumber;
    $xmlFileName=getInspectionXmlFilename($plateBarcode, $inspectionNumber);
    $fileFullPath=$plateDirectory.$xmlFileName;
    if(!@file_exists($plateDirectory)){
        throw new Exception('Plate directory does not exist: '.$plateDirectory);
    } else if(!@file_exists($inspectionDirectory)){
        throw new Exception('Inspection directory does not exist: '.$inspectionDirectory);
    } else if(@file_exists($fileFullPath)){
        Log::warning('generateInspectionXml called but XML already exists:');
        Log::warning($fileFullPath);
        return;
    }
    $images=dir($inspectionDirectory);
    if(empty($images)){
        Log::warning('Inspection directory has no images: '.$inspectionDirectory);
        return;
    }
    Log::info('Writing XML to '.$fileFullPath);
    $handle=@fopen($fileFullPath,'w');
    if(!$handle){
        throw new Exception('Could not open file for writing: '.$fileFullPath);
    }
    /** @noinspection XmlUnusedNamespaceDeclaration */
    @fwrite($handle,'<xml xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema">
    <s:Schema id="RowsetSchema">
        <s:ElementType name="row" content="eltOnly" rs:updatable="true">
            <s:AttributeType name="BARCODE_ID" rs:number="1" rs:baseCatalog="'.$plateBarcode.'" rs:baseTable="WellData" rs:keycolumn="False" rs:autoincrement="False">
                <s:datatype dt:type="" dt:maxlength="-1" rs:maybenull="True" />
            </s:AttributeType>
            <s:AttributeType name="INSPECT_NUMBER" rs:number="2" rs:baseCatalog="'.$plateBarcode.'" rs:baseTable="WellData" rs:keycolumn="False" rs:autoincrement="False">
                <s:datatype dt:type="" dt:maxlength="-1" rs:maybenull="True" />
            </s:AttributeType>
            <s:AttributeType name="ROW" rs:number="3" rs:baseCatalog="'.$plateBarcode.'" rs:baseTable="WellData" rs:keycolumn="False" rs:autoincrement="False">
                <s:datatype dt:type="" dt:maxlength="-1" rs:maybenull="True" />
            </s:AttributeType>
            <s:AttributeType name="COL" rs:number="4" rs:baseCatalog="'.$plateBarcode.'" rs:baseTable="WellData" rs:keycolumn="False" rs:autoincrement="False">
                <s:datatype dt:type="" dt:maxlength="-1" rs:maybenull="True" />
            </s:AttributeType>
            <s:AttributeType name="SUBWELL" rs:number="5" rs:baseCatalog="'.$plateBarcode.'" rs:baseTable="WellData" rs:keycolumn="False" rs:autoincrement="False">
                <s:datatype dt:type="" dt:maxlength="-1" rs:maybenull="True" />
            </s:AttributeType>
            <s:AttributeType name="WELL_IMAGE_NAME" rs:number="6" rs:baseCatalog="'.$plateBarcode.'" rs:baseTable="WellData" rs:keycolumn="False" rs:autoincrement="False">
                <s:datatype dt:type="" dt:maxlength="-1" rs:maybenull="True" />
            </s:AttributeType>
            <s:AttributeType name="DROP_IMAGE_NAME" rs:number="7" rs:baseCatalog="'.$plateBarcode.'" rs:baseTable="WellData" rs:keycolumn="False" rs:autoincrement="False">
                <s:datatype dt:type="" dt:maxlength="-1" rs:maybenull="True" />
            </s:AttributeType>
        </s:ElementType>
    </s:Schema>
    <rs:data>');

    while (false !== ($imageFileName = $images->read())) {
        if('.'===$imageFileName || '..'===$imageFileName){ continue; }
        /*
		//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 - Col (1-based)
		//01 - Row (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('.',$imageFileName);
        $imageFileName=$filenameParts[0]; //part 1 is file extension
        $extension=$filenameParts[1];
        $filenameParts=explode($plateBarcode.'_', $imageFileName);
        if(2!==count($filenameParts)){
            Log::warning("Filename $imageFileName.$extension does not contain the plate barcode. Not a Rigaku image. Ignoring.");
            continue;
        }
        $filenameParts=explode('_', $filenameParts[1]);
        if(10!==count($filenameParts)){
            Log::warning("Filename $imageFileName.$extension does not look like a Rigaku image. Ignoring.");
            continue;
        }
        if('99'!=$filenameParts['6']){
            //Importer will take best-slice image and replace _99_ with _00_ to check for composite image.
            //So we don't need composite images in the XML, only best-slice.
            Log::debug("Skipping $imageFileName.$extension - not best-slice.");
            continue;
        }

        $row=(1*$filenameParts[3])-1; //Row is 1-based in filename, 0-based in XML
        $col=(1*$filenameParts[2])-1; //Column is 1-based in filename, 0-based in XML
        $sub=(1*$filenameParts[4]);  //Subposition is 1-based in both

        @fwrite($handle, '      <z:row BARCODE_ID="'.$plateBarcode.'" INSPECT_NUMBER="'.$inspectionNumber.'" ROW="'.$row.'" COL="'.$col.'" SUBWELL="'.$sub.'" WELL_IMAGE_NAME="" DROP_IMAGE_NAME="'.$imageFileName.'" />');
    }

    @fwrite($handle,'    </rs:data>');
    @fwrite($handle,'</xml>');
    Log::debug('File handle closed');
}

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');
            }
        }
    });
}
