<?php

/**
 * Handles plate/image push from Weizmann CRIMS to IceBear.
 */
class WisCrimsImport extends Device {

    const USERNAME='wiscrims';
    const USERFULLNAME='WIS CRIMS';

    const SCORING_SYSTEM_NAME='CRIMS';

    const CRIMS_SCORES=array(
        array('index'=>0, 'color'=>'7fff00', 'name'=>'Clear drop'),
        array('index'=>1, 'color'=>'0e1ef5', 'name'=>'Heavy Precipitate'),
        array('index'=>2, 'color'=>'7ab9d4', 'name'=>'Light Precipitate'),
        array('index'=>8, 'color'=>'8b4513', 'name'=>'No Diffraction'),
        array('index'=>9, 'color'=>'8b4513', 'name'=>'Low Resolution'),
        array('index'=>7, 'color'=>'8b4513', 'name'=>'Salt')
    );

	/**
	 * @param array $params
	 * @return array[]
	 * @throws BadRequestException
	 * @throws ForbiddenException
	 * @throws NotFoundException
	 * @throws ServerException
	 */
    public static function createImages(array $params): array {
        static::require('images',$params);
        $createdImages=[];
        foreach ($params['images'] as $image){
            $createdImages[]=static::createImage($image);
        }
        return array('created'=>$createdImages);
    }

    /**
     * Creates an image record, or updates an existing one, with the supplied UUencoded image and thumbnail.
     * @param $image
     * @return array|array[]
     * @throws BadRequestException
     * @throws ServerException
     * @throws NotFoundException
     * @throws ForbiddenException
     */
    public static function createImage(array $image): array {
        if(isset($image['images'])){
            return static::createImages($image);
        }
        static::require(['plateBarcode','rowNumber','columnNumber','dropNumber','image','thumbnail'],$image);
        session::becomeAdmin();
        $barcode=$image['plateBarcode'];
        $row=1*$image['rowNumber'];
        $col=1*$image['columnNumber'];
        $sub=1*$image['dropNumber'];
        if(!$row){
            $rowLabels=platetype::$rowLabels;
            $row=array_search($image['rowNumber'], $rowLabels);
        }
        $limsPlate=plate::getByName($barcode);
        if(!$limsPlate){
            throw new NotFoundException('No plate with barcode '.$barcode);
        }
        $wellDrops=plate::getwelldrops($limsPlate['id']);
        $limsPlate['welldrops']=$wellDrops['rows'];

        $imageName=platewell::getWellLabel($row, $col).'_'.$sub.'.tmp';
        $dummyInspection=static::getDummyPlateInspectionForPlate($limsPlate);
        $inspectionId=$dummyInspection['id'];
        foreach($limsPlate['welldrops'] as $drop){
            if($drop['row']==$row && $drop['col']==$col && $drop['dropnumber']==$sub){
                $wellDropId=$drop['id'];
                break;
            }
        }
        if(!isset($wellDropId)){
            throw new NotFoundException("Plate $barcode has no drop at row $row, column $col, drop $sub");
        }
        $inspectionImage=dropimage::getByProperties(array(
            'welldropid'=>$wellDropId,
            'imagingsessionid'=>$inspectionId
        ));
        if(isset($inspectionImage['rows']) && 1==count($inspectionImage['rows'])){
            $inspectionImage=$inspectionImage['rows'][0];
        } else {
            $inspectionImage=static::createDummyImage($inspectionId, $wellDropId);
        }
        if(!$inspectionImage){
            throw new NotFoundException("No image with inspection ID $inspectionId and welldrop ID $wellDropId. Could not create dummy image.");
        }

        $inspectionImageId=$inspectionImage['id'];

        $imageStore=rtrim(config::get('core_imagestore'),'/').'/';
        $imagePath=substr($barcode,0,2).'/'.substr($barcode,0,3).'/'.$barcode.'/';
        $imagePath=$imageStore.$imagePath.'imagingsession'.$inspectionId.'/';
        $thumbPath=$imagePath.'thumbs/';
        @unlink($imagePath.platewell::getWellLabel($row, $col).'_'.$sub.'.jpg');
        @unlink($thumbPath.platewell::getWellLabel($row, $col).'_'.$sub.'.jpg');

        try {
            if(!file_exists($thumbPath) && !@mkdir($thumbPath, 0755, true)){
                throw new ServerException('Could not create directory for image storage');
            }

            $imagePath.=$imageName;
            $thumbPath.=$imageName;

            if(!@file_put_contents($imagePath, convert_uudecode($image['image'])) ){
                throw new ServerException('Could not write image to image store');
            }
            if(!@file_put_contents($thumbPath, convert_uudecode($image['thumbnail'])) ){
                throw new ServerException('Could not write thumbnail to image store');
            }
            if(strlen(@file_get_contents($imagePath))<10){
                //probably a 2-byte garbage file caused by UUencode header/footer
                $imageBytes=static::stripUUencodingHeaderAndFooterLines($image['image']);
                $imageBytes=convert_uudecode($imageBytes);
                if(!@file_put_contents($imagePath, $imageBytes) ){
                    throw new ServerException('Could not write image to image store');
                }
            }
            if(strlen(@file_get_contents($thumbPath))<10){
                //probably a 2-byte garbage file caused by UUencode header/footer
                $thumbBytes=static::stripUUencodingHeaderAndFooterLines($image['thumbnail']);
                $thumbBytes=convert_uudecode($thumbBytes);
                if(!@file_put_contents($thumbPath, $thumbBytes) ){
                    throw new ServerException('Could not write thumbnail to image store');
                }
            }

            $imageProperties=@getimagesize($imagePath);
            $thumbProperties=@getimagesize($thumbPath);
            if(!$imageProperties || !$imageProperties[0] || !$thumbProperties || !$thumbProperties[0]){
                throw new ServerException('Could not determine size of image and/or thumbnail. Is it corrupt?');
            }
            if(IMAGETYPE_JPEG==$imageProperties[2]){
                $newImagePath=substr($imagePath, 0, -4).'.jpg';
            } else if(IMAGETYPE_PNG==$imageProperties[2]){
                $newImagePath=substr($imagePath, 0, -4).'.png';
            } else {
                throw new ServerException('Image must be PNG or JPEG');
            }
            if(IMAGETYPE_JPEG==$thumbProperties[2]){
                $newThumbPath=substr($thumbPath, 0, -4).'.jpg';
            } else if(IMAGETYPE_PNG==$thumbProperties[2]){
                $newThumbPath=substr($thumbPath, 0, -4).'.png';
            } else {
                throw new ServerException('Thumbnail must be PNG or JPEG');
            }
            if(!@rename($imagePath, $newImagePath) || !@rename($thumbPath, $newThumbPath)){
                throw new ServerException('Could not save image/thumb in final store');
            }
            $width=$imageProperties[0];
            $height=$imageProperties[1];

        } catch (ServerException $e){
            @unlink($imagePath);
            @unlink($thumbPath);
            throw $e;
        }

        dropimage::update($inspectionImageId, array(
            'pixelheight'=>$height,
            'pixelwidth'=>$width,
            'imagepath'=>str_replace($imageStore,'',$newImagePath),
            'thumbnailpath'=>str_replace($imageStore,'',$newThumbPath),
        ));

		return dropimage::getById($inspectionImageId);
    }

    private static function stripUUencodingHeaderAndFooterLines(string $rawString): string {
        $parts=explode("\n", $rawString, 2);
        $headerlessString=$parts[1];
        $parts=explode("\nend", $headerlessString);
		return $parts[0];
    }

	/**
	 * Creates multiple plates.
	 * @param array $params
	 * @return array[]
	 * @throws BadRequestException
	 * @throws ForbiddenException
	 * @throws NotFoundException
	 * @throws NotModifiedException
	 * @throws ServerException
	 */
    public static function createPlates(array $params): array {
        if(isset($params['plateType'])){
            //This is a single plate
            return static::createPlate($params);
        }
        static::require('plates',$params);
        $createdPlates=[];
        foreach($params['plates'] as $plate){
            $createdPlates[]=static::createPlate($plate);
        }
        return array('created'=>$createdPlates);
    }

    /**
     * Creates a plate.
     * @param $plate array The plate - requires scoringSystemName, plateType, barcode, owner
     * @return array
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws NotFoundException
     * @throws ServerException
     * @throws NotModifiedException
     */
    public static function createPlate(array $plate): array {
        session::becomeAdmin();
        if(isset($plate['plates'])){ return static::createPlates($plate); }
        static::require(['scoringSystemName','plateType','barcode','owner'],$plate);
        if(static::SCORING_SYSTEM_NAME!==$plate['scoringSystemName']){
            throw new BadRequestException('Plate scoringSystemName must be "'.static::SCORING_SYSTEM_NAME.'"');
        }

        $barcode=$plate['barcode'];
        $defaultProjectId=static::getDefaultProjectId();
        $plateType=static::createPlateType($plate['plateType']);
        $scoringSystem=static::getScoringSystem();
        $owner=static::createUser($plate['owner']);

        $limsPlate=plate::getByName($plate['barcode']);
        if(!$limsPlate){
            $limsPlate=plate::create(array(
                'name'=>$barcode,
                'platetypeid'=>$plateType['id'],
                'crystalscoringsystemid'=>$scoringSystem['id'],
                'ownerid'=>$owner['id'],
                'projectid'=>$defaultProjectId
            ));
            $limsPlate=$limsPlate['created'];
        }
        $wellDrops=plate::getwelldrops($limsPlate['id']);
        $limsPlate['welldrops']=$wellDrops['rows'];

        $limsInspection=static::getDummyPlateInspectionForPlate($limsPlate);

        if(isset($plate['proteinConstruct'])) {
            static::createProteinAndConstruct($limsPlate['id'], $plate['proteinConstruct'], $owner);
        }

        if(isset($plate['interestingDrops'])){
            foreach ($plate['interestingDrops'] as $drop){
                static::handleInterestingDrop($drop, $limsPlate, $limsInspection);
            }
        }

        unset($limsPlate['welldrops']);
        return $limsPlate;
    }

    /**
     * Creates project, protein and construct if needed, and sets the construct on the plate.
     * @param $limsPlateId
     * @param $proteinConstruct
     * @param $owner
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws NotFoundException
     * @throws NotModifiedException
     * @throws ServerException
     */
    public static function createProteinAndConstruct(int $limsPlateId, array $proteinConstruct, array $owner): void {
        static::require('proteinName',$proteinConstruct);
        if(!isset($proteinConstruct['proteinAcronym'])) {
            $proteinConstruct['proteinAcronym']='';
        }
        if(!isset($proteinConstruct['constructName'])){
            $proteinConstruct['constructName']='Construct 1';
        }
        $proteinName=$proteinConstruct['proteinName'];
        $constructName=$proteinConstruct['constructName'];

		$project=project::getByName($proteinName);
		if(!$project){
            $project=project::create(array(
                'name'=>$proteinName,
                'description'=>$proteinName,
                'owner'=>session::getUserId()
            ));
            $project=$project['created'];
        }
        $projectId=$project['id'];

        $proteins=project::getproteins($projectId);
        if(!empty($proteins)){
            foreach ($proteins['rows'] as $p){
                if($p['name']==$proteinName){
                    $protein=$p;
                    break;
                }
            }
        }
        if(!isset($protein)){
            $protein=protein::create(array(
                'name'=>$proteinName,
                'description'=>$proteinName,
                'projectid'=>$projectId,
                'proteinacronym'=>$proteinConstruct['proteinAcronym']
            ))['created'];
        }
        $proteinId=$protein['id'];

        $constructs=protein::getconstructs($proteinId);
        foreach ($constructs['rows'] as $c){
            if($c['name']==$constructName){
                $construct=$c;
                break;
            }
        }
        if(!isset($construct)){
            $construct=construct::create(array(
                'proteinid'=>$proteinId,
                'projectid'=>$projectId,
                'name'=>$constructName,
                'description'=>$constructName
            ))['created'];
            $sequences=[];
            if(isset($proteinConstruct['sequences'])){
                $sequences=$proteinConstruct['sequences'];
            } else if(isset($proteinConstruct['dnaSequences'])) {
                $sequences=$proteinConstruct['dnaSequences'];
            }
            if(!empty($sequences)){
                $count=1;
                foreach($sequences as $sequence){
                    $dnaSequence='';
                    $proteinSequence='';
                    $sequence=preg_replace('[^A-Z]','',strtoupper($sequence));
                    if(preg_match('/[^ACGT]/',$sequence)){
                        //it's a protein sequence
                        $proteinSequence=$sequence;
                    } else {
                        //it's a DNA sequence
                        $dnaSequence=$sequence;
                        $converted=sequence::dnaToProtein($sequence);
                        if($converted){
                            $proteinSequence=$converted;
                        }
                    }
                    sequence::create(array(
                        'constructid'=>$construct['id'],
                        'projectid'=>$projectId,
                        'name'=>'Sequence '.$count,
                        'description'=>'Sequence '.$count,
                        'dnasequence'=>$dnaSequence,
                        'proteinsequence'=>$proteinSequence
                    ));
                    $count++;
                }
            }
        }
        $constructId=$construct['id'];

        plate::update($limsPlateId, array(
            'constructid'=>$constructId
        ));

        project::update($project['id'],array(
            'owner'=>$owner['id']
        ));

    }

    /**
     * Creates the user, or returns the existing one.
     * @param $user array The user to create.
     * @return array The user - requires name, username, emailAddress
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws NotFoundException
     * @throws ServerException
     */
    public static function createUser(array $user): array {
        Log::debug('In WisCrimsImport::createUser');
        static::require(['name','username','emailAddress'],$user);
        $user=UserManagement::createUser($user);
        Log::debug('Returning from WisCrimsImport::createUser');
        return $user;
    }

    /**
     * Creates the plate type, or returns the existing one
     * @param $plateType array The plate type - requires name,rows,columns,subPositions
     * @return array
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws NotFoundException
     * @throws ServerException
     */
    public static function createPlateType(array $plateType): array {
        Log::debug('In WisCrimsImport::createPlateType');
        $plateType=PlateImport::createPlateType($plateType);
        Log::debug('Returning from WisCrimsImport::createPlateType');
        return $plateType;
    }

    /**
     * Returns the CRIMS scoring system, creating it and its scores if not found.
     * @return array The scoring system
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws NotFoundException
     * @throws ServerException
     */
    private static function getScoringSystem(): array {
        $crimsSS=crystalscoringsystem::getByName('CRIMS');
        if(!empty($crimsSS)){
            return $crimsSS;
        }
        $sharedProjectId=static::getSharedProjectId();
        $crimsSS=crystalscoringsystem::create(array(
            'projectid'=>$sharedProjectId,
            'name'=>static::SCORING_SYSTEM_NAME,
            'iscurrent'=>1
        ));
        $crimsSS=$crimsSS['created'];
        foreach(self::CRIMS_SCORES as $s){
            crystalscore::create(array(
                'projectid'=>$sharedProjectId,
                'crystalscoringsystemid'=>$crimsSS['id'],
                'label'=>$s['name'],
                'name'=>static::SCORING_SYSTEM_NAME.'_'.$s['name'],
                'color'=>$s['color'],
                'scoreindex'=>$s['index'],
                'hotkey'=>$s['index']
            ));
        }
        return $crimsSS;
    }

	/**
	 * Returns a dummy inspection for the plate, creating it if necessary.
	 * @param $plate array The plate - requires name, id
	 * @return array
	 * @throws BadRequestException
	 * @throws ForbiddenException
	 * @throws NotFoundException
	 * @throws ServerException
	 */
    private static function getDummyPlateInspectionForPlate(array $plate): array {
        $plateBarcode=$plate['name'];
        $dummyInspection=imagingsession::getByName($plateBarcode.'_dummy');
        if(!$dummyInspection){
            $dummyImager=imager::getByName('+20 Microscope');
            if(!$dummyImager){
                throw new NotFoundException('No imager in IceBear called +20 Microscope');
            }
            $dummyInspection=imagingsession::create(array(
                'name'=>$plateBarcode.'_dummy',
                'manufacturerdatabaseid'=>0,
                'plateid'=>$plate['id'],
                'imagerid'=>$dummyImager['id'],
                'imageddatetime'=>gmdate('Y-m-d H:i:s')
            ));
            $dummyInspection=$dummyInspection['created'];
        }
        return $dummyInspection;
    }

    /**
     * @param $drop
     * @param $limsPlate
     * @param $limsInspection
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws NotFoundException
     * @throws ServerException
     */
    private static function handleInterestingDrop(array $drop, array $limsPlate, array $limsInspection): void {
        static::require(['rowNumber','columnNumber','dropNumber'],$drop);
        $row=1*$drop['rowNumber'];
        $col=1*$drop['columnNumber'];
        $sub=1*$drop['dropNumber'];
        if(!$row){
            $rowLabels=platetype::$rowLabels;
            $row=array_search($drop['rowNumber'], $rowLabels);
        }
        foreach($limsPlate['welldrops'] as $welldrop){
            if($row==$welldrop['row'] && $col==$welldrop['col'] && $sub==$welldrop['dropnumber']){
                $limsDrop=$welldrop;
            }
        }
        if(!isset($limsDrop)){
            throw new NotFoundException("handleInterestingDrop: No drop with row $row, col $col, sub $sub");
        }
        $dropId=$limsDrop['id'];
        $inspectionId=$limsInspection['id'];
        $limsDropImage=static::createDummyImage($inspectionId, $dropId);
        if(isset($drop['crystals'])){
            foreach ($drop['crystals'] as $crystal){
                static::handleCrystal($crystal, $limsDropImage);
            }
        }
    }

	/**
	 * @param int $limsInspectionId
	 * @param int $wellDropId
	 * @return array
	 * @throws BadRequestException
	 * @throws ForbiddenException
	 * @throws NotFoundException
	 * @throws ServerException
	 */
    private static function createDummyImage(int $limsInspectionId, int $wellDropId): array {
        $wellDrop=welldrop::getById($wellDropId);
        if(!$wellDropId){ throw new NotFoundException("No well drop with ID $wellDropId"); }
        $plateWell=platewell::getById($wellDrop['platewellid']);
        $plate=plate::getById($plateWell['plateid']);
        $barcode=$plate['name'];
        $row=$plateWell['row'];
        $col=$plateWell['col'];
        $sub=$wellDrop['dropnumber'];
        $imageStore=rtrim(config::get('core_imagestore'),'/').'/';
        $imagePath='dummyimage.jpg';
        $thumbPath='dummythumb.jpg';
        $dummyFilename=platewell::getWellLabel($row, $col).'_'.$sub.'.jpg';
        $midPath=substr($barcode, 0, 2).'/'.substr($barcode,0,3).'/'.$barcode.'/imagingsession'.$limsInspectionId.'/';
        if(!file_exists($imageStore.$midPath.'thumbs') && !@mkdir($imageStore.$midPath.'thumbs', 0755, true)){
            throw new ServerException('Could not create image/thumbnail path: '.$imageStore.$midPath.'thumbs');
        }
        if(!@copy($imageStore.$imagePath, $imageStore.$midPath.$dummyFilename)){
            throw new ServerException('Could not copy dummy image into place');
        }
        if(!@copy($imageStore.$thumbPath, $imageStore.$midPath.'thumbs/'.$dummyFilename)){
            throw new ServerException('Could not copy dummy image into place');
        }

        $imageName=$plate['name'].'_'.platewell::getWellLabel($row, $col).'.'.$sub.'_is'.$limsInspectionId;
        $limsDropImage=dropimage::getByName($imageName);
        if(!$limsDropImage){
            $limsDropImage=dropimage::create(array(
                'name'=>$imageName,
                'projectid'=>$wellDrop['projectid'],
                'imagingsessionid'=>$limsInspectionId,
                'welldropid'=>$wellDropId,
                'imagestorepath'=>$imageStore,
                'imagepath'=>$imagePath,
                'thumbnailpath'=>$thumbPath
            ))['created'];
        }
        return $limsDropImage;
    }

	/**
	 * @param array $crystal
	 * @param array $limsDropImage
	 * @return array
	 * @throws BadRequestException
	 * @throws ForbiddenException
	 * @throws NotFoundException
	 * @throws ServerException
	 */
    private static function handleCrystal(array $crystal, array $limsDropImage): array {
        static::require(['sampleName','numberInDrop','sendersImage'],$crystal);
        $image=$crystal['sendersImage'];
        static::require(['micronsPerPixel','marks'],$image);
        if(1!==count($image['marks'])){ throw new BadRequestException('Exactly one mark per crystal'); }
        $mark=$image['marks'][0];
        if('point'!==$mark['regionType']){ throw new BadRequestException('Only point marks are supported'); }
        static::require(['x','y'],$mark);
        $pixelX=$mark['x'];
        $pixelY=$mark['y'];
        $limsDropImageId=$limsDropImage['id'];
        $limsWellDrop=welldrop::getbyId($limsDropImage['welldropid']);
        $existing=crystal::getByProperties(array(
            'dropimageid'=>$limsDropImageId,
            'pixelx'=>$pixelX,
            'pixely'=>$pixelY
        ));
        if(!empty($existing)){ return $existing['rows'][0]; }
        $numCrystalsInDrop=0;
        $crystalsInDrop=welldrop::getcrystals($limsDropImage['welldropid']);
        if(isset($crystalsInDrop['rows'])){
            $numCrystalsInDrop=count($crystalsInDrop['rows']);
        }
        $numberInDrop=1+$numCrystalsInDrop;
        $crystalName=str_replace('.','d',$limsWellDrop['name']).'c'.$numberInDrop;

        $fields=array(
            'prefix'=>$crystalName,
            'name'=>$crystalName,
            'numberindrop'=>$numberInDrop,
            'dropimageid'=>$limsDropImageId,
            'welldropid'=>$limsWellDrop['id'],
            'projectid'=>$limsWellDrop['projectid'],
            'pixelx'=>$pixelX,
            'pixely'=>$pixelY
        );
        foreach(array("spaceGroup","unitCellA","unitCellB","unitCellC","unitCellAlpha","unitCellBeta","unitCellGamma") as $field){
            if(!empty ($crystal[$field])){
                $fields[strtolower($field)]=$crystal[$field];
            }
        }
        $limsCrystal=crystal::create($fields);

        if(isset($image['micronsperpixel'])){
            dropimage::update($limsDropImageId, array(
                'micronsperpixelx'=>$image['micronsperpixel'],
                'micronsperpixely'=>$image['micronsperpixel'],
            ));
        }
        return $limsCrystal;
    }

}
