<?php /** @noinspection PhpUnused */

class containercontent extends baseobject {
    
    /**
     * This class manages item-in-item relationships. This is not just for containers inside containers,
     * but is also used for crystals in pins, top-level items (dewars/plates) inside shipments, and shipments 
     * at synchrotrons.
     * 
     * Some actions can only be performed by "shipment handlers" - see canHandleContainers(). THe initial implementation 
     * includes all users in the Administrators, Shippers and Technicians usergroups. Broadly speaking, any row with a 
     * null shipmentid can be managed by regular users; rows with a shipmentid are restricted to shipment handlers.
     * 
     * The intended behaviour is as follows: 
     * 
     * Fishing crystals:
     * 
     * - A pin-crystal, a puck-pin and a dewar-puck relationship are created, with a null shipment ID.
     * 
     * Adding dewars to (but not shipping) a shipment:
     * 
     * - This can only be done by shipment handlers.
     * - A shipment-dewar relationship is created, with the ID of the shipment as both parent and shipmentid.
     * 
     * Removing a dewar from a shipment:
     * 
     * - This can only be done by shipment handlers.
     * - The dewar-shipment relationship is removed, but all relationships within the dewar are preserved.
     * 
     * Shipping a shipment:
     * 
     * - This can only be done by shipment handlers.
     * - All puck-dewar, pin-puck, and crystal-pins inside the shipped dewars are updated with the shipmentid
     * - A shipment-shipmentdestination relationship is created, with the shipmentid
     * 
     * Emptying containers after a shipment:
     *  
     * - This can only be done by shipment handlers.
     * - The container and all its contents, as well as the shipment itself, are deemed to be back on site
     * - If emptying a top-level container (dewar):
     *   - The shipment-shipmentdestination relationship is removed.
     *   - The dewar-shipment relationship is removed.
     *   - If the dewar contents are not preserved (ie recursive emptying), all relevant relationship rows are deleted
     *   - Rows for any preserved relationships have the shipment ID set to null
     * 
     */
	
	protected static array $fields=array(
	    'name'=>array(validator::REQUIRED),
		'shipmentid'=>array(validator::INTEGER),
		'parent'=>array(validator::REQUIRED, validator::INTEGER),
		'child'=>array(validator::REQUIRED, validator::INTEGER),
		'position'=>array(validator::REQUIRED, validator::INTEGER),
		'iscurrent'=>array(validator::BOOLEAN),
	);
	
	protected static array $helpTexts=array(
	    'name'=>'Auto-generated name',
		'shipmentid'=>'A shipment',
		'parent'=>'The containing object',
		'child'=>'The containing object',
		'position'=>'The numbered slot within the parent',
		'iscurrent'=>'Whether the parent currently contains the child',
	);
		
	protected static bool $sharedProjectOverride=true;

	/**
	 * @param int $id
	 * @return bool
	 * @throws BadRequestException
	 * @throws NotFoundException
	 * @throws ServerException
	 */
	public static function canUpdate(int $id): bool {
	    if(static::canHandleContainers()){ return true; }
	    $cc=static::getById($id);
	    return empty($cc['shipmentid']);
	}

    /**
     * Create a record of an item being put into another item. Client is assumed to know what it is doing,
     * if it wants to put a plate type into a crystalscore this won't stop it.
     * @param array $request The details of the mapping.
     * @return array
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws NotFoundException
     * @throws ServerException
     */
	public static function create(array $request=[]): array {
	    global $exceptionParameters;
		$request['projectid']=project::getSharedProjectId();
		//Check that this position is empty in the parent
		$somethingAlreadyThere=static::getByProperties(array(
		    'parent'=>$request['parent'],
		    'position'=>$request['position']
		));
		if($somethingAlreadyThere){
		    $exceptionParameters['alreadyThere']=$somethingAlreadyThere;
		    throw new BadRequestException('There is already something at that position');
		}
		
		//Check that child is not contained in another parent
		$alreadyContained=static::getByProperties(array(
		    'child'=>$request['child']
		));
		if($alreadyContained){
		    //There SHOULD be at most one, but iterate through all...
		    foreach($alreadyContained['rows'] as $ac){
		        if(""==$ac['shipmentid']){
		            //Not in an already sent shipment
		            if(static::canHandleContainers()){
		                //can unpack anything
						usleep(1); //shut up IDE warning about empty body
		            } else if('container'==$ac['parenttype'] && 'container'==$ac['childtype']){
		                //regular users can only unpack containers from containers
						usleep(1); //shut up IDE warning about empty body
		             } else {
		                 //Regular user is attempting to unpack a dewar from a shipment, or a crystal from a pin
		                 $exceptionParameters['alreadyContained']=$alreadyContained;
		                 throw new BadRequestException('Child item is already contained in another container');
		             }
		             //Not thrown, so remove the container from its old position
		             static::delete($ac['containercontentid']);
                } else {
    		        //container content is in an already sent shipment.
    		        //Only admin/tech/shipper can unpack... and we assume the shipment has been returned. 
    		        if(static::canHandleContainers()){
                        containercontent::delete($ac['containercontentid']);
    		              $shipment=shipment::getById($ac['shipmentid']);
        		        if(empty($shipment['datereturned'])){
        		            shipment::update($ac['shipmentid'], array('datereturned'=>gmdate('Y-m-d')));
        		        }
    		        }
    		    }
		    }
		}
		
		$parentId=$request['parent'];
		$childId=$request['child'];
		$parentBaseObject=baseobject::getById($parentId);
		$childBaseObject=baseobject::getById($childId);
		$parentType=$parentBaseObject['objecttype'];
		$childType=$childBaseObject['objecttype'];
		
		//Special handling for crystal fishing actions
		if(("crystal"==$childType || "welldrop"==$childType) && "container"==$parentType){
	    
		    $created=array();
		    $container=container::getById($request['parent']);
    		$category=strtolower($container['containercategoryname']);
    		
    		//If crystal not supplied, create one at dummy coordinates in the welldrop
    		if('welldrop'==$childType){
    		    
    		    $wellDrop=welldrop::getById($childId);
    		    $crystals=welldrop::getcrystals($childId);
    		    if(!$crystals){ $crystals=array(); }
    		    
    		    //calculate position of the dummy crosshair for this crystal, and the crystal's number in the drop
    		    $offset=50; //pixels in from top left corner, for first crosshair
    		    $spacing=100; //pixels between rows, columns of crosshairs
    		    $crystalsPerRow=6; 
    		    $existingCoords=array();
    		    $numberInDrop=0;
    		    $x=$offset;
    		    $y=$offset;
    		    if(isset($crystals['rows'])){
        		    foreach($crystals['rows'] as $c){
        		        $existingCoords[]=$c['pixelx'].'_'.$c['pixely'];
        		        $numberInDrop=max($numberInDrop, 1*$c['numberindrop']);
        		    }
    		    }
    		    $numberInDrop++;
    		    $foundSpace=false;
    		    while(!$foundSpace){
    		        if(!in_array($x.'_'.$y, $existingCoords)){
    		            $foundSpace=true;
    		        } else {
    		            $x+=$spacing;
    		            if($x>$spacing*$crystalsPerRow){
    		                $x=$offset;
    		                $y+=$spacing;
    		            }
    		        }
    		    }
				$wasAdmin=session::isAdmin();
    		    session::becomeAdmin();
    		    $crystal=crystal::create(array(
    		        'isfished'=>1,
    		        'pixelx'=>$x,
    		        'pixely'=>$y,
    		        'numberindrop'=>$numberInDrop,
    		        'projectid'=>$wellDrop['projectid'],
    		        'welldropid'=>$wellDrop['id'],
    		        'isdummycoordinate'=>1,
    		        'allowcreatedummyinspection'=>'yes',
    		        'dropimageid'=>-1, //crystal::create will find right image or create dummy inspection
    		        'prefix'=>'dummy', //correct name generated in crystal create
    		        'name'=>'dummy', //correct name generated in crystal create
    		    ));
    		    session::set('isAdmin',$wasAdmin);

    		    $crystal=$crystal['created'];
    		    $crystal['objecttype']='crystal';
    		    $created[]=$crystal;
    		} else {
    		    $crystal=crystal::getById($childId);
				$wasAdmin=session::isAdmin();
				session::becomeAdmin();
    		    crystal::update($childId, array("isfished"=>1));
				session::set('isAdmin',$wasAdmin);
    		}

    		//If fishing straight into the puck, create dummy pin, otherwise use the pin the user chose
    		if("puck"==$category){
    		    
    		    //Become admin to create the pin in Shared project
    		    $wasAdmin=session::isAdmin();
    		    session::set("isAdmin",true);
    		    $pin=container::create(array(
    		        'name'=>'dummypin',
    		        'containertypeid'=>-1,
    		        'projectid'=>-1
    		    ));
    		    session::set("isAdmin",$wasAdmin); //return to regular permissions
    		    $pin=$pin['created'];
    		    $pin['objecttype']='container';
    		    $created[]=$pin;

    		    //Put the pin into the puck
    		    $pinInPuck=parent::create(array(
    		        'name'=>'cc_'.microtime(), //will be replaced at end of method
    		        'projectid'=>$request['projectid'],
    		        'parent'=>$container['id'],
    		        'child'=>$pin['id'],
    		        'position'=>$request['position']
    		    ));
    		    $pinInPuck=$pinInPuck['created'];
    		    $pinInPuck['objecttype']='containercontent';
    		    $created[]=$pinInPuck;
    		    database::query('UPDATE containercontent SET name=:name WHERE id=:id', array(
    		        ':name'=>'containercontent_'.$pinInPuck['id'],
    		        ':id'=>$pinInPuck['id']
    		    ));

    		} else {
    		    $pin=container::getById($parentId);
    		}

    		//Whether the crystal and pin existed before or not, the crystal needs to go in the pin
    		$crystalInPin=parent::create(array(
    		    'name'=>'cc_'.microtime(), //will be replaced at end of method
    		    'projectid'=>$request['projectid'],
    		    'parent'=>$pin['id'],
    		    'child'=>$crystal['id'],
    		    'position'=>1
    		));
    		$crystalInPin=$crystalInPin['created'];
    		$crystalInPin['objecttype']='containercontent';
    		$created[]=$crystalInPin;
    		database::query('UPDATE containercontent SET name=:name WHERE id=:id', array(
    		    ':name'=>'containercontent_'.$crystalInPin['id'],
    		    ':id'=>$crystalInPin['id']
    		));
    		
		    $response=array();
		    if(1==count($created)){
		        $response['type']=$created[0]['objecttype'];
		        $response['created']=$created[0];
		    } else {
		        $response['type']='multiple';
		        $response['created']=$created;
		    }
		
		    //End special handling for crystal fishing actions
		} else {

			session::becomeAdmin(); //Because the user needs to create in the Shared project, which they can't do
			$request['name']='cc_'.microtime(); //will be replaced at end of method
		    $response=parent::createByClassName($request,'containercontent');
    		if($response['created']){
    			database::query('UPDATE containercontent SET name=:name WHERE id=:id', array(
    				':name'=>'containercontent_'.$response['created']['id'],
    				':id'=>$response['created']['id']
    			));
    		}
			session::revertAdmin();
		}
		
		return $response;
	}

	/**
	 * @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(!static::canUpdate($id)){
	        throw new ForbiddenException('Container seems to be in a shipment. Only admins, shippers and technicians can unpack it.');
	    }
	    return parent::update($id, $request);
	}

	/**
	 * @param int $id
	 * @return bool
	 * @throws BadRequestException
	 * @throws NotFoundException
	 * @throws ServerException
	 */
	public static function canDelete(int $id): bool {
	    $cc=static::getById($id);
	    if(!$cc){ return false; }
	    if(empty($cc['shipmentid'])){ return true; }
	    return static::canHandleContainers();
	}

	/**
	 * @param int $containerId
	 * @return array
	 * @throws BadRequestException
	 * @throws ForbiddenException
	 * @throws NotFoundException
	 * @throws ServerException
	 */
	public static function deleteContainerContentsRecursively(int $containerId): array {
 	    $container=container::getById($containerId);
 	    if(!$containerId){ throw new NotFoundException('No container with id '.$containerId); }
 	    $contents=static::getByProperty('parent', $containerId);
 	    if(empty($contents)){ throw new NotFoundException('Container '.$container['name'].' appears to be empty'); }
 	    $deletedIds=array();
 	    foreach($contents['rows'] as $cc){
    	    if(!static::canDelete($cc['id'])){
    	        throw new ForbiddenException('Container seems to be in a shipment. Only admins, shippers and technicians can unpack it.');
    	    }
    	    $childIds=static::doDelete($cc['id'], true);
    	    if(is_array($childIds)){ $deletedIds=array_merge($deletedIds,$childIds); }
 	    }
 	    return array('deleted'=>$deletedIds);
	}

    /**
     * @param $id
     * @return array
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws NotFoundException
     * @throws ServerException
     */
	public static function delete(int $id): array {
	    global $parameters;
	    $isRecursive=false;
	    if(isset($parameters['recursive']) && 1 == (int)($parameters['recursive'])){
	        $isRecursive=true;
	    }
	    if(!static::canDelete($id)){
	        throw new ForbiddenException('Container seems to be in a shipment. Only admins, shippers and technicians can unpack it.');
	    }
	    $cc=static::getById($id);
	    if(!empty($cc['shipmentid'])){
	        //Already checked for permission to do this.
	        //This container was in a shipment, so let shipment take care of removing containers from itself, and itself from destination
	        $shipment=shipment::getById($cc['shipmentid']);
	        if(!empty($shipment['dateshipped'])){
    	        shipment::update($cc['shipmentid'], array('datereturned'=>gmdate('Y-m-d')));
	        }
	    }
	    $deletedIds=static::doDelete($id, $isRecursive);
	    return(array("deleted"=>$deletedIds));
	}

	/**
	 * @param int $id
	 * @param bool $isRecursive
	 * @param array $deletedIds
	 * @return array
	 * @throws BadRequestException
	 * @throws ForbiddenException
	 * @throws NotFoundException
	 * @throws ServerException
	 */
	private static function doDelete(int $id, bool $isRecursive, array $deletedIds=[]): array {
	    $record=static::getById($id);
	    if(!empty($record['shipmentid']) && !static::canHandleContainers()){
	        throw new ForbiddenException('Container seems to be in a shipment. Only admins, shippers and technicians can unpack it.');
	    }
	    if($isRecursive){
	        //call this method on all child IDs
	        $shipmentId=$record['shipmentid'];
	        if(empty($shipmentId)){ 
	            $shipmentId=database::$nullValue; 
	        } else if(!static::canHandleContainers()){
	            throw new ForbiddenException('At least one child container seems to be in an active shipment.');
	        }
	        $children=static::getByProperties(array('shipmentid'=>$shipmentId, 'parent'=>$record['child']));
	        if(isset($children['rows'])){
    	        foreach($children['rows'] as $c){
    	            $deletedIds=static::doDelete($c['id'], true, $deletedIds);
    	        }
	        }
	    }
	    parent::delete($id);
	    $deletedIds[]=$id;
	    return $deletedIds;
	}

    /**
     * @param int $id
     * @return array|mixed
     * @throws BadRequestException
     * @throws ServerException
     * @throws NotFoundException
     */
	public static function getById(int $id): ?array {
		$record=parent::getById($id);
		if(!$record){ return $record; }
		return static::getParentChildDetails($record);
	}

	/**
	 * @param string $name
	 * @return array|void
	 * @throws BadRequestException
	 * @throws NotFoundException
	 * @throws ServerException
	 */
	public static function getByName(string $name): ?array {
		$record=parent::getByName($name);
		if(!$record){ return $record; }
		return static::getParentChildDetails($record);
	}

	/**
	 * @param array $request
	 * @return array|null
	 * @throws BadRequestException
	 * @throws NotFoundException
	 * @throws ServerException
	 */
	public static function getAll(array $request=[]): ?array {
		$contents=parent::getAll($request);
		if(!$contents){ return $contents; }
		$contents['rows']=static::getParentChildDetails($contents['rows']);
		return $contents;
	}

	/**
	 * @param string $key
	 * @param string $value
	 * @param array $request
	 * @return array|null
	 * @throws BadRequestException
	 * @throws NotFoundException
	 * @throws ServerException
	 */
	public static function getByProperty(string $key, string $value, array $request=[]): ?array {
		$contents=parent::getByProperty($key, $value, $request);
			if(!$contents){ return $contents; }
		$contents['rows']=static::getParentChildDetails($contents['rows']);
		return $contents;
	}

	/**
	 * @param array $keyValuePairs
	 * @param array $request
	 * @return array|null
	 * @throws BadRequestException
	 * @throws NotFoundException
	 * @throws ServerException
	 */
	public static function getByProperties(array $keyValuePairs, array $request=[]): ?array {
		$contents=parent::getByProperties($keyValuePairs, $request);
		if(!$contents){ return $contents; }
		$contents['rows']=static::getParentChildDetails($contents['rows']);
		return $contents;
	}

	/**
	 * @param array $contentRecords
	 * @return array
	 * @throws BadRequestException
	 * @throws NotFoundException
	 * @throws ServerException
	 */
	private static function getParentChildDetails(array $contentRecords): array {
		$ids=array();
		$wasSingleRecord=false;
		if(isset($contentRecords['child'])){
		    //single object was passed
		    $wasSingleRecord=true;
		    $contentRecords=array($contentRecords);
		}
		if(empty($contentRecords)){ return $contentRecords; }
		foreach($contentRecords as $record){
			$parentId=$record['parent'];
			$childId=$record['child'];
			$ids[]=(int)$parentId;
			$ids[]=(int)$childId;
		}
		$ids=array_unique($ids);
		$objects=array();
		$foundContents=database::queryGetAll('SELECT id,objecttype,projectid FROM baseobject WHERE id IN('.implode(',',$ids).') ');
		$foundContents=$foundContents['rows'];
		$readableProjects=session::getReadProjects();
		$canHandleContainers=static::canHandleContainers();
		foreach($foundContents as &$fc){
			if(!$canHandleContainers && !in_array($fc['projectid'], $readableProjects)){
				$fc['record']=null;
            } else if($canHandleContainers) {
			    session::becomeAdmin();
                $fc['record']=baseobject::callOrThrow($fc['objecttype'], 'getById', array($fc['id']));
                session::revertAdmin();
            } else {
                $fc['record']=baseobject::callOrThrow($fc['objecttype'], 'getById', array($fc['id']));
			}
			$objects['id'.$fc['id']]=$fc;
		}
		foreach($contentRecords as &$record){
			$parentId=$record['parent'];
			$childId=$record['child'];
			$record['containercontentid']=$record['id'];
			$record['parentobject']=$objects['id'.$parentId];
			$record['childobject']=$objects['id'.$childId];
			$record['parenttype']=$record['parentobject']['objecttype'];
			$record['childtype']=$record['childobject']['objecttype'];
			if(empty($record['parentobject'])){ $record['parentname']='(hidden)'; } else { $record['parentname']=$record['parentobject']['record']['name']; }
			if(empty($record['childobject'])){ 
			    $record['childname']='(hidden)'; 
			} else { 
			    $record['childname']=$record['childobject']['record']['name']; 
			}
			if('crystal'==$record['childobject']['objecttype']){
			    if(empty($record['childobject'])){
			        $wasAdmin=session::isAdmin();
			        session::set("isAdmin",true);
			        $crystal=crystal::getById($record['childobject']['record']['id']);
			        session::set("isAdmin",$wasAdmin);
			        $record['childobject']['record']['hasacronym']=(''!=$crystal['proteinacronym']) ? 1 : 0;
			    } else {
			        $record['childobject']['record']['hasacronym']=(''!=$record['childobject']['record']['proteinacronym']) ? 1 : 0;
			    }
			}
		}
		if($wasSingleRecord){
		    return $contentRecords[0];
		}
		return $contentRecords;
	}

    /**
     * Whether the current user is able to handle containers, outside crystal fishing. For example, emptying after a shipment.
     * @return boolean
     * @throws NotFoundException
     */
	private static function canHandleContainers(): bool {
	    return session::isAdmin() || session::isTechnician() || session::isShipper();
	}
	
}
