<?php class screen extends baseobject {
	
	protected static array $fields=array(
			'name'=>validator::REQUIRED,
			'manufacturer'=>validator::ANY,
			'catalognumber'=>validator::ANY,
			'isstandard'=>validator::BOOLEAN,
            'isavailable'=>validator::BOOLEAN
	);
	
	protected static array $helpTexts=array(
			'name'=>'A unique name for this screen',
			'manufacturer'=>'The manufacturer of the screen',
			'catalognumber'=>'Manufacturer\'s catalogue number',
			'isstandard'=>'Whether this is a standard screen (not an optimization)',
            'isavailable'=>'Whether this screen is available for use (affects standard screens only)'
	);

	public static string $defaultSortOrder='screen.isstandard DESC, name ASC';
	
	private static array $rowLabels=array('-','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z');
	
	private static array $csvMimetypes = array(
	    'text/csv',
	    'text/plain',
	    'application/csv',
	    'text/comma-separated-values',
	    'application/excel',
	    'application/vnd.ms-excel',
	    'application/vnd.msexcel',
	    'text/anytext',
	    'application/octet-stream',
	    'application/txt',
	);

    /**
     * Whether the current user can create standard screens.
     * @return bool true if the current user can create standard screens, otherwise false.
     * @throws BadRequestException
     * @throws NotFoundException
     * @throws ServerException
     */
	public static function canCreateStandardScreens(): bool {
		$projects=session::getCreateProjects();
		if(empty($projects)){ return false; }
		$sharedProject=project::getByName(baseproject::SHARED);
		if(!$sharedProject){
			throw new NotFoundException('Could not read shared project');
		}
		return in_array($sharedProject['id'], $projects);
	}

    /**
     * Whether this user can update this screen.
     * @param int $id The ID of the screen.
     * @return bool Whether this user can update screens.
     * @throws BadRequestException
     * @throws NotFoundException
     * @throws ServerException
     */
	public static function canUpdate(int $id): bool {
		 if(parent::canUpdate($id)){ return true; }
		 $groupId=usergroup::getByName(usergroup::TECHNICIANS);
		 return usergroup::userisingroup($groupId);
	}

    /**
     * @param array $request
     * @return array
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws NotFoundException
     * @throws NotModifiedException
     * @throws ServerException
     */
	public static function create(array $request=[]): array {

        //Is this a standard screen or an optimization screen?
        if (isset($request['projectid'])) {
            $project=project::getById($request['projectid']);
			if(!$project){
				throw new NotFoundException('Project does not exist, or you do not have permission to see it');
			}
			$request['isstandard'] = false;
            if(isset($request['plateid'])){
                $plate=plate::getById($request['plateid']);
                if(!$plate){
                    throw new BadRequestException('No plate with ID '.$request['plateid']);
                }
                $barcode=$plate['name'];
                $screenName='Optimization '.$barcode;
                $existing=screen::getByName($screenName);
                if($existing){
                    screen::delete($existing['id']);
                }
                $request['name']=$screenName;
            }
        } else {
            $project = project::getByName(baseproject::SHARED);
			if(!$project){
				throw new NotFoundException('Could not read shared project');
			}
            $request['projectid'] = $project['id'];
            $request['isstandard'] = true;
        }
        if (isset($_FILES['file'])) {
            //A file was uploaded
            $filename = $_FILES['file']['name'];
            $tempName = $_FILES['file']['tmp_name'];
            $request['tempfile'] = rtrim(config::get('core_filestore'), '/') . '/' . $filename;
            if (!@move_uploaded_file($tempName, $request['tempfile'])) {
                throw new ServerException('Could not move file into temporary storage');
            }
        }
        if (!isset($request['tempfile'])) {
            //There is no file with dimensions and conditions
            $parsedFile=static::makeDummyScreenConditions($request['rows'], $request['cols'], 'Unknown');
        } else {
            //Either a file was uploaded ($_FILES['file'] above) or a file was already on disk, for example from
            //the Formulatrix screen library in screenfromrockmaker::create.
            $parsedFile=static::parseScreenFile($request['tempfile'], $request);
        }

        foreach(array('name','rows','cols') as $key){
            if(isset($parsedFile[$key]) && !isset($request[$key])){
                $request[$key]=$parsedFile[$key];
            }
        }

        //Create the screen...
        $screen=parent::createByClassName($request,'screen');
        $screenId=$screen['created']['id'];

        //...and its conditions...
        static::createConditionsFromPlateArray($parsedFile['conditions'], $screenId);

        if(isset($request['plateid'])){
            plate::update($request['plateid'], array('screenid'=>$screenId));
        }

        //Create DB record of file and save it
        if(!empty($request['tempfile'])){
            $tempFile=$request['tempfile'];
            file::createFromTempFile(array(
                'description'=>'Screen definition file',
                'parentid'=>$screenId,
                'filename'=>pathinfo($tempFile, PATHINFO_FILENAME).'.'.pathinfo($tempFile, PATHINFO_EXTENSION),
                'mimetype'=>mime_content_type($tempFile),
                'tempfile'=>$tempFile
            ));
        }
        return $screen;
    }

	/**
	 * @param string $filePath
	 * @param array $request
	 * @return array
	 * @throws BadRequestException
	 * @throws ServerException
	 */
    private static function parseScreenFile(string $filePath, array $request): array {

        $fileContent=@file_get_contents($filePath);
        if(!$fileContent){
            throw new ServerException('Could not open temporary file at '.$filePath.' to parse conditions');
        }
        if(str_contains(strtolower($fileContent), 'rock maker xml')){
            //Formulatrix Rock Maker XML file
            $ret=static::parseRockMakerXml($fileContent);
        } else if(str_contains($fileContent, 'CrysScreen')){
            //Tecan CrysScreen Word document (ugh)
            $ret=static::parseTecanCrysScreenWordFile($fileContent);
        } else if(str_contains($fileContent, 'crystaltrak ')){
            //Rigaku CrystalTrak XML file
            $ret=static::parseCrystalTrakXml($fileContent);
        } else if(in_array($_FILES['file']['type'], static::$csvMimetypes)) {
            //Hopefully a CSV file. But if so, what kind? Could be Mimer, or simple 2-column condition list
            $ret = static::parseCsvFile($fileContent);
        } else {
            $rows=1*$request['rows'];
            $cols=1*$request['cols'];
            if(1>$rows || 1>$cols){
                throw new ServerException('Cannot parse uploaded file. Cannot create dummy conditions - bad rows/columns numbers');
            }
            $ret=static::makeDummyScreenConditions($rows, $cols, 'See screen definition file');
        }
        if(!isset($ret['name'])){ $ret['name']=pathinfo($filePath, PATHINFO_FILENAME); }
        if(!isset($ret['rows'])){ $ret['rows']=$request['rows']; }
        if(!isset($ret['cols'])){ $ret['cols']=$request['cols']; }
	    return $ret;
    }


	/**
	 * @param int $id
	 * @param array $request
	 * @return array
	 * @throws BadRequestException
	 * @throws ForbiddenException
	 * @throws NotFoundException
	 * @throws ServerException
	 */
	public static function update(int $id, array $request=[]): array {
		if(isset($request['isstandard']) && $request['isstandard']){
			$sharedProject=project::getByName(baseproject::SHARED);
			if(!$sharedProject){
				throw new NotFoundException('Could not read shared project');
			}
			$parameters=array(':projectid'=>$sharedProject['id'], ':id'=>$id);
			database::query(
					'UPDATE baseobject set projectid=:projectid WHERE id=:id', 
					$parameters
			);
			database::query(
					'UPDATE screen set projectid=:projectid WHERE id=:id', 
					$parameters
			);
			database::query(
					'UPDATE screencondition set projectid=:projectid WHERE screenid=:id', 
					$parameters
			);
		}
		return parent::update($id,$request);
	}

	/**
	 * @param int $id
	 * @param array $request
	 * @return array
	 * @throws BadRequestException
	 * @throws ServerException
	 */
	public static function getscreenconditions(int $id, array $request=[]): array {
		$ret=screencondition::getByProperty('screenid', $id, $request);
		$screen=static::getById($id);
		$ret['screen']=$screen;
		return $ret;
	}

	/**
	 * @param int $id
	 * @param array $request
	 * @return array|null
	 * @throws BadRequestException
	 * @throws ServerException
	 */
    public static function getplates(int $id, array $request=[]): ?array {
	    return plate::getByProperty('screenid', $id, $request);
    }

	/**
	 * Deletes a screen. This will fail if any plate is using the screen; unset the screen from the plate(s) first.
	 * @param int $id
	 * @return array
	 * @throws BadRequestException
	 * @throws ForbiddenException
	 * @throws NotFoundException
	 * @throws ServerException
	 */
    public static function delete(int $id): array {
        $screen=static::getById($id);
        if(!$screen){
            throw new BadRequestException('No screen with ID '.$id.', cannot delete');
        }
        $platesUsing=static::getplates($id);
        if($platesUsing){
            throw new ForbiddenException('This screen is used by at least one plate. It cannot be deleted.');
        }
        if(1===(int)($screen['isstandard'])) {
            //Standard screen
            if(!session::isAdmin() && !session::isTechnician()){
                //Only admins and technicians can delete
                throw new ForbiddenException('Only admins and technicians can delete standard screens');
            }
        } else {
            //Optimization screen
            if(!screen::canUpdate($id)){
                //Optimization screens are created with the parent plate's project ID, so we can still check permissions here,
                //even though the parent plate must have been deleted before we got here.
                throw new ForbiddenException('You do not have permission to update the plate associated with this optimization screen.');
            }
        }
        session::becomeAdmin();
        $deleted=array();
        $deleted[]=$id;
        $conditions=static::getscreenconditions($id);
        foreach ($conditions['rows'] as $condition){
            screencondition::delete($condition['id']);
            $deleted[]=$condition["id"];
        }
        $files=static::getFiles($id);
        if($files){
            foreach($files['rows'] as $file){
                try {
                    file::delete($file['id']);
                    $deleted[]=$file['id'];
                } catch (NotFoundException){
                    //do nothing, already gone
                }
            }
        }
        baseobject::delete($id);
        session::revertAdmin();
        return array("deleted"=>$deleted);
    }

	/**
	 * @param string $fileContent
	 * @return array
	 * @throws BadRequestException
	 */
    private static function parseCsvFile(string $fileContent): array {
        $delimiter=static::determineCsvDelimiter($fileContent);
        $fileContent=preg_split("/\r\n|\n|\r/", $fileContent);
        $firstRow=str_getcsv($fileContent[0], $delimiter);
        $thirdRow=str_getcsv($fileContent[2], $delimiter);

        if(2==count($thirdRow)){
            //Simple 2-column "well number, condition" CSV
            $ret=static::parseWellNameAndConditionCsv($fileContent);
        } else if(2<count($thirdRow)){
            //Hopefully a Mimer CSV...
            $ret=static::parseMimerCsv($fileContent);
            if(!isset($ret['name'])){
                if(empty($firstRow[0])){
                    throw new BadRequestException('No name supplied, and could not find one in cell A1');
                }
                $ret['name']=$firstRow[0];
            }
        } else {
            //Only one column, if it's a CSV it doesn't look like it
            throw new BadRequestException('File does not look like a screen - not enough columns');
        }

        return $ret;
    }

	/**
	 * @param string $fileContent
	 * @return array
	 * @throws BadRequestException
	 */
	private static function parseCrystalTrakXml(string $fileContent): array {
		$xml=simplexml_load_string($fileContent);
		if(!$xml){ throw new BadRequestException('Detected CrystalTrak XML file but it was not valid XML'); }
		if(!empty($xml->reservoir_design) && !empty($xml->reservoir_design->format->attributes())){
			$format=$xml->reservoir_design->format->attributes();
			$wells=$xml->reservoir_design->children();
			$screenName=$xml->reservoir_design->attributes()['name'];
		} else if(!empty($xml->screen) && !Empty($xml->screen->format->attributes())){
			$format=$xml->screen->format->attributes();
			$wells=$xml->screen->children();
			$screenName=$xml->screen->attributes()['name'];
		} else {
			throw new BadRequestException('File is valid XML but does not appear to be a CrystalTrak XML file.');
		}
		if(empty($format['rows']) || empty($format['cols'])){
			throw new BadRequestException('Rows and columns not defined in screen file');
		}
		$rows=(int)$format['rows'];
		$cols=(int)$format['cols'];

		$conditions=array();
		for($r=1;$r<=$rows;$r++){
			$conditions[]=array();
			for($c=1;$c<=$cols;$c++){
				$conditions[$r-1][]='';
			}
		}
		
		foreach($wells as $well){
			if($well->getName()!="well"){ 
				continue; //format and comments elements are of no interest here
			}
			$description=array();
			foreach($well->children() as $item){
				$attrs=$item->attributes();
				$desc=$attrs['class'].' '.$attrs['conc'].$attrs['units'].' '.$attrs['name'];
				if(!empty($attrs['ph'])){ $desc.=', pH '.$attrs['ph']; }
				$description[]=$desc;
			}
			$description=implode('; ', $description);

			$wellNumber=$well->attributes()['number'];
			$wellNumber=(int)$wellNumber;
			$colNumber=$wellNumber%$cols;
			if(0==$colNumber){ $colNumber=$cols; }
			$rowNumber=ceil($wellNumber/$cols);
			
			$conditions[$rowNumber-1][$colNumber-1]=$description;

		}
		return array(
		    'name'=>$screenName,
		    'rows'=>$rows,
		    'cols'=>$cols,
		    'conditions'=>$conditions
        );
	}

    /**
     * Parses a RockMaker XML screen file.
     * Schema is at http://help.formulatrix.com/rock-maker/3.10/Content/RMXML_Schema.htm as of March 2020; it has been
     * at a different URL which is now 404, so it may also disappear from here. It's not terribly difficult to figure
     * out what is needed for our purposes, though.
     *
     * Note that there is no explicit ordering in the RMXML file. We assume that the conditions are in order A1-12, B1-B12,
     * etc.
     *
     * See https://formulatrix.com/protein-crystallization-systems/rock-maker-crystallization-software/resources/ for
     * a large number of RMXML screen files.
     *
     * @param string $fileContent The content of the XML file
     * @return array An 8x12 array of strings, each representing the condition in one well
     * @throws BadRequestException if the file does not look like an RMXML file, or if it does not contain exactly 96 conditions
     * @throws ServerException if the simpleXML PHP extension is not available
     */
	private static function parseRockMakerXml(string $fileContent): array {
	    if(!function_exists('simplexml_load_string')){
	        throw new ServerException('Cannot process XML file because simplexml is not available. Your administrator needs to fix this.');
        }
        $xml=simplexml_load_string($fileContent);
        if(!$xml){ throw new BadRequestException('Detected RockMaker XML file but it was not valid XML'); }
        if(empty($xml) || empty($xml->ingredients) || empty($xml->conditions)){
            throw new BadRequestException('Detected RockMaker XML file but it was not valid XML - 2');
        }

        //First pull all the ingredients into an array, identified by their LocalID.
        //We will need to guess the screen manufacturer from the ingredient manufacturer!
        $screenManufacturer='';
        $ingredients=array();
        foreach($xml->ingredients->children() as $i){
            $stocks=$i->stocks->children();
            foreach ($stocks as $stock){
                $id=$stock->localID;
                $unit=$stock->units;
                $chemical=$i->name.'';
                $pH='';
                if($stock->pH){
                    $pH=$stock->pH.'';
                }
                $ingredients[1*$id]=array(
                    'chemical'=>$chemical,
                    'unit'=>$unit,
                    'pH'=>$pH
                );

                //There is no "screen manufacturer" field in RMXML. The best we can do is hope that the manufacturer of all
                //the stocks is the same. If so, we set that as the screen manufacturer, otherwise (unknown).
                //At least one screen (from Kerafast, see screens link in function header) contains stock from two or
                //more manufacturers.
                $stockManufacturer=$stock->vendorName.'';
                if(''==$screenManufacturer){
                    $screenManufacturer=$stockManufacturer;
                } else if($screenManufacturer!=$stockManufacturer){
                    $screenManufacturer='';
                }
            }
        }

        $rows=8;
        $cols=12;
        $conditions=array();
        for($r=1;$r<=$rows;$r++){
            $conditions[]=array();
            for($c=1;$c<=$cols;$c++){
                $conditions[$r-1][]='';
            }
        }
        if(96!=$xml->conditions->children()->count()){
            throw new BadRequestException('This screen does not have 96 conditions, so it cannot be read.');
        }
        $count=0;
        foreach ($xml->conditions->children() as $condition){
            $parts=array();
            $roles=array(
                'A'=>array(),
                'B'=>array(),
                'P'=>array(),
                'other'=>array()
            );
            foreach($condition->children() as $conditionIngredient){
                $ingredientId=$conditionIngredient->stockLocalID.'';
                $ingredient=$ingredients[1*$ingredientId];
                $concentration=$conditionIngredient->concentration.'';
                $chemical=$ingredient['chemical'];
                $unit=$ingredient['unit'];
                $pH='';
                if($conditionIngredient->pH){
                    $pH=', pH '.$conditionIngredient->pH;
                }
                if('M'==$unit && 1>1*$concentration){
                    $unit='mM';
                    $concentration=1000*$concentration;
                }
                $role=$conditionIngredient->type.'';
                $role=substr($role,0, 1);
                if(in_array($role, array_keys($roles), $role)){
                    $roles[$role][]=($role.': '.$concentration.' '.$unit.' '.$chemical.$pH);
                } else {
                    $roles['other'][]=$concentration.' '.$unit.' '.$chemical.$pH;
                }
            }
            foreach (array('B','P','A','other') as $part){
                if(!empty($roles[$part])){
                    $parts[]=implode('; ', $roles[$part]);
                }
            }
            $row=floor($count/12);
            $col=($count%12);
            $conditions[$row][$col]=implode('; ',$parts);
            $count++;
        }
        return array(
            'rows'=>$rows,
            'cols'=>$cols,
            'manufacturer'=>$screenManufacturer,
            'conditions'=>$conditions
        );
	}

	/**
	 * @param string|array $fileContent
	 * @return array
	 * @throws BadRequestException
	 */
	private static function parseMimerCsv(string|array $fileContent): array {
		/* Example CSV format: note that the column order may be different and row 1 may not have the numbers.
		Incomplete factorial,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
		Reagent number,PEG 8000,[PEG 8000],[PEG 8000] units,pH_PEG 8000,calcium acetate,[calcium acetate],[calcium acetate] units,pH_calcium acetate,calcium acetate,[calcium acetate],[calcium acetate] units,pH_calcium acetate,"bistris propane, pH7","[bistris propane, pH7]","[bistris propane, pH7] units","pH_bistris propane, pH7","cacodylate, pH 6.5","[cacodylate, pH 6.5]","[cacodylate, pH 6.5] units","pH_cacodylate, pH 6.5",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
		A1,PEG 8000,8,%,,calcium acetate,0.15,M,,,,,,"bistris propane, pH7",0.1,M,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
		A2,PEG 8000,8,%,,calcium acetate,0.2,M,,,,,,"bistris propane, pH7",0.1,M,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
		A3,PEG 8000,8,%,,calcium acetate,0.25,M,,,,,,"bistris propane, pH7",0.1,M,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
		A4,PEG 8000,8,%,,,,,,calcium acetate,0.15,M,,,,,,"cacodylate, pH 6.5",0.1,M,6.5,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
		A5,PEG 8000,8,%,,,,,,calcium acetate,0.2,M,,,,,,"cacodylate, pH 6.5",0.1,M,6.5,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
		A6,PEG 8000,8,%,,,,,,calcium acetate,0.25,M,,,,,,"cacodylate, pH 6.5",0.1,M,6.5,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
		B1,PEG 8000,10,%,,calcium acetate,0.15,M,,,,,,"bistris propane, pH7",0.1,M,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
		*/
		if(!is_array($fileContent)){
			$fileContent=preg_split("/\r\n|\n|\r/", $fileContent);
		}
		$delimiter=static::determineCsvDelimiter($fileContent);

		$rows=0;
		$cols=0;
		/* Step 1. Parse 2nd row headers for components and their properties.
		 * We aim to create an array where key is name/role and value is an array containing the column numbers for name, amount, units and optionally pH.
		 * Later we use this to know which columns to mash together in what order.
		 * This part is made unnecessarily filthy by the wonderful world of almost-the-same-but-not-quite-just-for-the-hell-of-it screen formats. 
		 */
		$secondRow=str_getcsv($fileContent[1], $delimiter);
		$parts=array();
		//1a. First extract all the unbracketed labels that are not "pH" - these are assumed to be the chemical names/roles
		for($i=1;$i<100;$i++){ //1 because we know 0 is "reagent number" or similar
			if(!isset($secondRow[$i])){ break; }
			$label=trim($secondRow[$i]);
			if(empty($label)){ break; }
			if(str_contains($label, '[') || 'pH'==$label || str_starts_with($label, 'pH_')){ continue; }
			$parts[$label]=array('name'=>$i);
		}
		//1b. Now iterate through the headers again, trying to find the column numbers for each component's pH, amount, units 
		for($i=1;$i<100;$i++){ //1 because we know 0 is "reagent number" or similar
			if(!isset($secondRow[$i])){ break; }
			$label=trim($secondRow[$i]);
			if(empty($label)){ break; }
			if(str_contains($label, '[')){
				if(str_ends_with($label, '] units')){
					$parts[substr($label,1,-7)]['unit']=$i;
				} else if(str_ends_with($label, ']')){
					$parts[substr($label,1,-1)]['amount']=$i;
				} 
			} else if(str_contains($label, 'pH_')){
				$parts[substr($label,3)]['pH']=$i;
			} else if('pH'==$label){
				//This is a pain. The label says nothing about what this is the pH *of*.
				//Work backward from this column to find something that's definitely a name/role
				for($j=0;$j<$i;$j++){
					$otherCol=$i-$j;
					$otherLabel=trim($secondRow[$otherCol]);
					if(str_ends_with($otherLabel, ']')){
						$parts[substr($otherLabel,1,-1)]['pH']=$i;
						break;
					}
				}
			}
		}
		/*
		 * Step 2. Generate human-friendly representations of the conditions 
		 * Now we know which column represents what, go through each row in the file and concatenate columns in the right order.
		 */
		$fileContent=array_slice($fileContent,2); //Skip the first two rows, they are headers
		$rowLabels=implode('', static::$rowLabels);
		$wells=array();
		foreach($fileContent as $row){
            if(""==trim($row)){ continue; }
			$row=str_getcsv($row, $delimiter);
			//Row and column
			$wellLabel=trim($row[0]);
			if(empty($wellLabel)){ continue; }
			$rowLabel=substr($wellLabel,0,1);
			$rowNumber=stripos($rowLabels,$rowLabel);
			$rows=max($rows,$rowNumber);
			$columnNumber=(int)(substr($wellLabel,1));
			$cols=max($cols,$columnNumber);
			//Text description
			$labelParts=array();
			foreach($parts as $k=>$v){
				if(!isset($v['amount'])){
					throw new BadRequestException('Badly-formed CSV. Does not look like simple 2-column file. Failed to parse as a MIMER file.');
				}
				if(!isset($row[$v['amount']]) || !isset($row[$v['unit']]) || !isset($row[$v['name']])){
					continue;
				}
				$str=trim($row[$v['amount']].$row[$v['unit']].' '.$row[$v['name']]);
				if(!empty($str)){
					if(isset($v['pH']) && !empty($row[$v['pH']]) && !str_contains($str, 'pH')){
						$str.=', pH '.$row[$v['pH']];
					}
					$labelParts[]=$str;
				}
			}
			$label=implode('; ',$labelParts);
			$well=array(
				'description'=>$label,
				'row'=>$rowNumber,
				'col'=>$columnNumber
			);
			$wells[]=$well;
		}
		
		$conditions=array();
		for($r=1;$r<=$rows;$r++){
			$conditions[]=array();
			for($c=1;$c<=$cols;$c++){
				$conditions[$r-1][]='';
			}
		}
		
		foreach($wells as $w){
			$r=$w['row'];
			$c=$w['col'];
			$conditions[$r-1][$c-1]=$w['description'];
		}
		
		return array(
		    'rows'=>$rows,
		    'cols'=>$cols,
		    'conditions'=>$conditions
        );
	}

	/**
	 * Parses a Tecan CrysScreen Word document's well conditions.
	 * Assumes that the document has a table with B:(buffer) P:(precipitant) S:(salt) in each cell, and that the wells appear
	 * in the document in A1-A12, B1-B12, etc., order. 
	 * @param string|array $fileContent The contents of the file
	 * @return array A 2D array of screen conditions
	 * @throws BadRequestException if file does not contain exactly 96 conditions
	 */
	private static function parseTecanCrysScreenWordFile(string|array $fileContent): array {
	
		if(!is_array($fileContent)){
			$fileContent=preg_split("/\r\n|\n|\r/", $fileContent);
		}

		$conditions=array(
				array('', '', '', '', '', '', '', '', '', '', '', ''),
				array('', '', '', '', '', '', '', '', '', '', '', ''),
				array('', '', '', '', '', '', '', '', '', '', '', ''),
				array('', '', '', '', '', '', '', '', '', '', '', ''),
				array('', '', '', '', '', '', '', '', '', '', '', ''),
				array('', '', '', '', '', '', '', '', '', '', '', ''),
				array('', '', '', '', '', '', '', '', '', '', '', ''),
				array('', '', '', '', '', '', '', '', '', '', '', ''),
		);
		
		$wholeScreen='';
		foreach($fileContent as $row){
			$row=trim($row);
			if(!preg_match("/^[BPSA]:/", $row)){ continue; }
			$wholeScreen.=$row.' ';
		}
		$wholeScreen=trim($wholeScreen); //remove stray whitespace at the end
		$wholeScreen=preg_replace('/ pH:/', ', pH ', $wholeScreen);
        $wholeScreen=substr($wholeScreen, 0, -2); //lose the double separator at the end
        $wholeScreen=preg_replace('/\x07/', '[[[SEP]]]', $wholeScreen); //Table rows delimited by double separator, change to single
        $wholeScreen=str_replace('[[[SEP]]][[[SEP]]]', '[[[SEP]]]', $wholeScreen); //Table rows delimited by double separator, change to single
		$parts=explode('[[[SEP]]]', $wholeScreen);
		if(count($parts)!=96){
			throw new BadRequestException('Could not parse CrysScreen file. Expected 96 conditions, but found '.count($parts).'.');
		}

		for($i=0;$i<count($parts); $i++){
			$condition=preg_replace(
                array('/\sB:\s/','/\sP:\s/','/\sS:\s/','/\sA:\s/',),
                array('; B: ','; P: ','; S: ','; A: '),
                trim($parts[$i])
            );
			$conditionNumber=$i;
			$row=floor($conditionNumber/12);
			$col=$conditionNumber%12;
			$condition=trim($condition);
			if(''==$condition){ throw new BadRequestException('Condition '.(1+$i).' was empty'); }
			$conditions[$row][$col]=$condition;
		}
		
		return array(
		    'rows'=>8,
		    'cols'=>12,
		    'conditions'=>$conditions
        );
	}

	/**
	 * Parses a CSV file with just well and condition
	 * A1,Condition
	 * A01,Condition
	 * 1,Condition - assumes numbering A1-A12,B1-B12, etc.
	 * @param string|array $fileContent
	 * @return array
	 * @throws BadRequestException if file does not contain exactly 96 conditions or any well ID is outside 12x8
	 */
	private static function parseWellNameAndConditionCsv(string|array $fileContent): array {

		if(!is_array($fileContent)){
			$fileContent=preg_split("/\r\n|\n|\r/", $fileContent);
		}
		
		$delimiter=static::determineCsvDelimiter($fileContent);
		$firstRow=str_getcsv($fileContent[0], $delimiter);
		if(2!==count($firstRow)){
            throw new BadRequestException('Expected only two columns: Well identifier, and condition');
		}

		//First pass: Determine row and column dimensions of screen (usually 8x12)
		$maxRowNumber=0;
		$maxColNumber=0;
		foreach($fileContent as $row){
		    $row=str_getcsv($row, $delimiter);
		    if(empty($row) || empty($row[0])){ continue; } //blank line at end of file
			$wellLabel=trim($row[0]);
            if(!preg_match('/^[A-Z]\d\d?$/',$wellLabel)){ continue; } //header row

            //Row and column
			$rowNumber=array_search(substr($wellLabel,0,1), static::$rowLabels);
			$colNumber=(int)(substr($wellLabel,1));
			$maxRowNumber=max($maxRowNumber, $rowNumber);
			$maxColNumber=max($maxColNumber, $colNumber);
		}


		//Make a 2d array matching the screen dimensions, all cells containing ''
		$conditions=array_fill(0,$maxRowNumber,array_fill(0,$maxColNumber,'-'));
		
		//Second pass: Fill the 2d array with conditions
        $rows=0;
        $cols=0;
		foreach($fileContent as $row){
			$row=str_getcsv($row, $delimiter);
			if(empty($row) || empty($row[0])){ continue; } //blank line at end of file
            if(!preg_match('/^[A-Z]?\d\d?$/',$row[0])){ continue; } //header row

            //Row and column
			$wellLabel=trim($row[0]);
			$condition=trim($row[1]);
			if(preg_match('/^[A-Z]\d\d?$/',$wellLabel)){
				$rowLabel=substr($wellLabel, 0, 1);
				$colNumber=(int)(substr($wellLabel,1));
				$rowNumber=array_search($rowLabel, static::$rowLabels);
			} else {
				$wellNumber=(int)$wellLabel;
				$colNumber=$wellNumber%12;
				if(0==$colNumber){ $colNumber=12; }
				$rowNumber=ceil($wellNumber/12);
			}
			if($rowNumber<1 || $rowNumber>8 || $colNumber<1 || $colNumber>12){
				throw new BadRequestException('Well identifier ('.$wellLabel.') not recognised or out of bounds');
			}
			$conditions[$rowNumber-1][$colNumber-1]=$condition;
			$rows=max($rows,$rowNumber);
			$cols=max($cols,$colNumber);
		}
		return array(
		    'rows'=>$rows,
		    'cols'=>$cols,
		    'conditions'=>$conditions
        );
	}

	/**
	 * @param int $rows
	 * @param int $cols
	 * @param string $dummyText
	 * @return array
	 * @throws BadRequestException
	 */
	private static function makeDummyScreenConditions(int $rows, int $cols, string $dummyText): array {
	    if(1>$rows || 1>$cols){
	        throw new BadRequestException('Bad row/column count in makeDummyScreenConditions');
        }
		$conditions=array();
		$separator='|---|';
		$rowText=str_repeat($dummyText.$separator, $cols-1) . $dummyText;
		for($r=1;$r<=$rows;$r++){
			$conditions[]=explode($separator, $rowText);
		}
        return array(
            'rows'=>$rows,
            'cols'=>$cols,
            'conditions'=>$conditions
        );
	}


    /**
     * Takes an 8x12 array of screen conditions and a screen ID, and sets the screen conditions against the screen.
     * @param array $plateArray
     * @param int|string $screenId
     * @throws BadRequestException
     * @throws NotFoundException
     * @throws ServerException
     * @throws ForbiddenException
     */
	public static function createConditionsFromPlateArray(array $plateArray, int|string $screenId): void {
		//public because installer uses it
		$screen=screen::getById($screenId);
		if(!$screen){ throw new ServerException('screen::createConditionsFromPlateArray got bad screen ID'); }
		$projectId=$screen['projectid'];
		$rowCount=count($plateArray);
		$colCount=count($plateArray[0]);
		for($r=1;$r<=$rowCount;$r++){
			if($colCount!=count($plateArray[$r-1])){
				throw new ServerException('screen::createConditionsFromPlateArray found inconsistent column count in plate array');
			}
			for($c=1;$c<=$colCount;$c++){
				$description=$plateArray[$r-1][$c-1];
				if(empty($description)){
					throw new BadRequestException('No condition found for well '.self::$rowLabels[$r].$c.'. Are plate and screen the same size?');
				}
				$wellNumber=($colCount*($r-1))+$c;
				$condition=array(
						'name'=>'screen'.$screenId.'_well'.str_pad($wellNumber, 3, '0', STR_PAD_LEFT),
						'description'=>$description,
						'wellnumber'=>$wellNumber,
						'screenid'=>$screenId,
						'projectid'=>$projectId,
						'row'=>$r,
						'col'=>$c
				);
				screencondition::create($condition);
			}
		}
	}

    /**
     * Determines whether a screen CSV file is delimited by comma, semicolon or tab, and returns that delimiter.
     * Note that this implementation is rather naive, assuming that the first column is a well identifier (A1 or A01)
     * or well number (1-96), immediately followed by the delimiter.
     * @param $fileContent string|array The content of the file
     * @return string The delimiter
     * @throws BadRequestException if the delimiter cannot be determined, or is not comma, semicolon, or tab
     */
	private static function determineCsvDelimiter(string|array $fileContent): string {
		if(!is_array($fileContent)){
			$fileContent=preg_split("/\r\n|\n|\r/", $fileContent);
		}

		//We examine the fifth row of the file, to make sure we're well past header rows. MIMER, at least, has two
        //header rows. Anything that doesn't have five rows (or couldn't be split into more than that above) isn't
        //something we want to mess with.
		if(5>count($fileContent)){
			throw new BadRequestException('Supplied CSV is too short');
		}
		$fifthRow=$fileContent[4];

		//Iterate through the possible delimiters, looking for a row beginning with a well number/identifier
        //immediately followed by each delimiter. Return the first match.
		$delimiters=array(",", ";", "\t");
        foreach($delimiters as $d) {
            if (preg_match('/^[A-Z]?\d+'.$d.'/', $fifthRow)) {
                return $d;
            }
        }
		throw new BadRequestException('Could not determine delimiter of CSV file - tried comma, semicolon and tab');
	}
	
}