<?php class file extends baseobject {

	protected static array $fields=array(
			'name'=>validator::ANY,
			'filename'=>validator::ANY,
			'mimetype'=>validator::ANY,
			'description'=>validator::ANY,
			'bytes'=>validator::INTEGER,
			'parentid'=>array(validator::REQUIRED, validator::INTEGER),
	);
	
	protected static array $helpTexts=array(
	);

	protected static string $adminSelect= 'SELECT file.*, baseobject.createtime AS uploaddatetime FROM baseobject,file,project WHERE project.id=file.projectid AND file.id=baseobject.id';
	protected static string $normalSelect='SELECT file.*, baseobject.createtime AS uploaddatetime FROM baseobject,file,project WHERE project.id=file.projectid AND file.id=baseobject.id';

	/**
	 * @param int $parentId
	 * @throws BadRequestException
	 * @throws ServerException
	 */
	public static function setProjectIdsToMatchParent(int $parentId): void {
	    $parent=baseobject::getById($parentId);
        if(!$parent){ throw new ServerException('Attempted to set project IDs on files for object with ID '.$parentId.' but it does not exist'); }
        $projectId=$parent['projectid'];
        $stmt='UPDATE file set projectid=:projectid WHERE parentid=:parentid';
        database::query($stmt,array(':projectid'=>$projectId, ':parentid'=>$parentId));
    }

    /**
     * @param int $id
     * @return array|void
     * @throws BadRequestException
     * @throws NotFoundException
     * @throws ServerException
     */
	public static function getById(int $id): ?array {
		$file=parent::getById($id);
		$path=static::getPathToFile($file);
		if(!$file || !file_exists($path)){
			throw new NotFoundException('The file does not exist, or you do not have permission to see it');
		}
		header('Content-Type: '.$file['mimetype']);
		$handle=@fopen($path, 'r');
		@fpassthru($handle);
		@fclose($handle);
		exit;
	}

	/**
	 * Required for interface compatibility but no implementation. Throws a BadRequestException.
	 * @param string $name
	 * @return array|void
	 * @throws BadRequestException
	 */
	public static function getByName(string $name): ?array {
		throw new BadRequestException('getByName not implemented in file class');		
	}

    /**
     * @param array $request
     * @return array
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws NotFoundException
     * @throws ServerException
     */
	public static function create(array $request=[]): array {
		if(isset($request["csvRows"])){
			return static::createCsvFile($request);
		}
	    if(empty($_FILES) || empty($_FILES['file'])){
	        throw new BadRequestException('file::create called but no file was supplied');
	    }
	    $file=$_FILES['file'];
	    $request['name']='file'.microtime();
	    $request['filename']=$file['name'];
	    $request['mimetype']=$file['type'];
	    $request['bytes']=$file['size'];
	    $canUpdate=session::isAdmin();
	    if(database::$nullValue===$request['parentid']){
	        //File is to be attached to a project
            $canUpdate= in_array($request['projectid'], session::getUpdateProjects());
        } else {
            //File is to be attached to a regular object
            $parentObject=baseobject::getById($request['parentid']);
            $request['projectid']=$parentObject['projectid'];
            if(in_array($parentObject['projectid'], session::getUpdateProjects())){
                //Can update because object is in user's updatable projects
                $canUpdate=true;
            } else {
                //Object isn't in user's updatable projects - check its class
                $parentType=$parentObject['objecttype'];
                $canUpdate=forward_static_call_array(array($parentType, 'canUpdate'), array($request['parentid']));
            }
        }
        if(!$canUpdate){
            throw new ForbiddenException('You need "Update" permission on this project to attach files');
        }
	    $record=parent::create($request);
	    $record=$record['created'];
	    $fileDir=static::getFileParentDirectory($record);
	    if(!file_exists($fileDir)){
	        if(!@mkdir($fileDir, 0700, true)){
	            throw new ServerException('Could not save uploaded file into file storage area - could not make project directory');
	        }
	    }
	    $newPath=static::getPathToFile($record);
	    if(!@move_uploaded_file($file['tmp_name'], $newPath)){
	        throw new ServerException('Could not save uploaded file into file storage area. Ask your administrator to check the file store path and permissions, as well as the upload size limits.');
	    }
	    
	    $params=array('name'=>'file'.$record['id'], 'id'=>$record['id']);
	    database::query('UPDATE file SET name=:name WHERE id=:id', $params);
	    
	    return array("created"=>parent::getById($record['id']) );
	}

    /**
     * @param array $request
     * @return array
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws NotFoundException
     * @throws ServerException
     */
	public static function createFromTempFile(array $request=[]): array {
	    $request['name']='file'.microtime();
	    if(!$request['tempfile']){
	        throw new BadRequestException('file::createFromTempFile called but no file was supplied');
	    }
        $request['bytes']=@filesize($request['tempfile']);
	    $parentObject=baseobject::getById($request['parentid']);
	    $request['projectid']=$parentObject['projectid'];
	    if(!in_array($parentObject['projectid'], session::getUpdateProjects())){
            $parentType=$parentObject['objecttype'];
            $canUpdate=session::isAdmin() || forward_static_call_array(array($parentType, 'canUpdate'), array($request['parentid']));
            if(!$canUpdate){
                throw new ForbiddenException('You need "Update" permission on this project to attach files');
            }
	    }
	    $record=parent::create($request);
	    $record=$record['created'];
	    $fileDir=static::getFileParentDirectory($record);
	    if(!file_exists($fileDir)){
	        if(!@mkdir($fileDir, 0700, true)){
	            throw new ServerException('Could not save uploaded file into file storage area - could not make project directory');
	        }
	    }
	    $newPath=static::getPathToFile($record);
	    if(!@copy($request['tempfile'],$newPath)){
	        throw new ServerException('Could not save uploaded file into file storage area');
	    }
        @unlink($request['tempfile']);	    
	    $params=array('name'=>'file'.$record['id'], 'size'=>@filesize($newPath), 'id'=>$record['id']);
	    database::query('UPDATE file SET name=:name, bytes=:size WHERE id=:id', $params);
	    
	    return array("created"=>parent::getById($record['id']) );
	}

	public static function delete(int $id): array {
	    $file=parent::getById($id);
        if(!$file){ throw new BadRequestException('No file with ID '.$id.', cannot delete'); }
	    if(!isset($file['mimetype'])){
            throw new ServerException('Tried to delete file with ID '.$id.', but that ID is not a file');
        }
	    if(!baseobject::canDelete($file['parentid'])){
	        throw new ForbiddenException('You do not have permission to delete the file');
        }
	    $path=static::getPathToFile($file);
	    if(file_exists($path) && !@unlink($path)){
	        throw new ServerException('Could not delete the file from the server filestore');
        }
	    parent::delete($id);
	    return array('deleted'=>$id);
    }
	
	private static function getFileParentDirectory(array $fileRecord): string {
		return rtrim(config::get('core_filestore'),'/').'/files/project'.$fileRecord['projectid'];
	}
	public static function getPathToFile(array $fileRecord): string {
		return static::getFileParentDirectory($fileRecord).'/file'.$fileRecord['id'];
	}

    /**
     * Returns the pixels-per-unit information encoded in a PNG, otherwise null.
     * @param $filePath string The full path to the PNG file on disk
     * @return array|null If no scale information is found, null is returned. If the PNG contains scale information, an
     * array is returned with the following keys:
     *  - pixelsPerUnitX
     *  - pixelsPerUnitY
     *  - unit
     * The unit can only be "metres" or "" (unknown), per the PNG spec. If unknown, the relative values of the other
	 * two keys define pixel aspect ratio.
     * @throws ServerException if the file does not exist or cannot be opened for reading
     * @throws BadRequestException if the file is not a valid PNG
     * @see http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.tEXt for the PNG specification
     */
	public static function getPngPixelsPerUnit(string $filePath): ?array {

	    if(!file_exists($filePath)){
	        throw new ServerException('Tried to parse PNG headers but file does not exist');
        }
	    $handle=@fopen($filePath, 'r');
	    if(!$handle){
	        throw new ServerException('Could not open PNG file for reading');
        }
        $header=@fread($handle, 8);
        if($header!="\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"){
            throw new BadRequestException('Image is not a valid PNG (bad header bytes)');
        }
        $physHeader=null;
        $chunkHeader=@fread($handle,8);
        while ($chunkHeader){
            $chunk=@unpack('Nsize/a4type', $chunkHeader);
            $type=$chunk['type'];
            if("pHYs"==$type){
                $offset=ftell($handle);
                $size=$chunk['size'];
                if(9!=$size){
                    throw new BadRequestException('Invalid PNG file (pHYs header is not 9 bytes');
                }
                fseek($handle, $offset);
                $bytes=fread($handle, 4);
                $perPixelX=unpack("N",$bytes); //unsigned long, 32 bits (4 bytes)
                $perPixelX=array_pop($perPixelX);
                $bytes=fread($handle, 4);
                $perPixelY=unpack("N",$bytes); //unsigned long, 32 bits (4 bytes)
                $perPixelY=array_pop($perPixelY);
                $bytes=fread($handle, 1);
                $units=unpack("C",$bytes); //unsigned char
                $units=array_pop($units);
                if(1==$units){
                    $units='metres';
                } else {
                    $units='';
                }
                $physHeader=array();
                $physHeader['pixelsPerUnitX']=$perPixelX;
                $physHeader['pixelsPerUnitY']=$perPixelY;
                $physHeader['units']=$units;
                break;
            }
            // Skip to next chunk (over body and CRC)
            fseek($handle, $chunk['size'] + 4, SEEK_CUR);
            // Read next chunk header
            $chunkHeader = fread($handle, 8);
        }
        @fclose($handle);
        return $physHeader;
    }

	/**
	 * Create a CSV file from the supplied information.
	 * @param array $request An array containing:
	 * - parentid The ID of the file's parent object
	 * - filename The name of the CSV file (should end in .csv)
	 * - description Optional description of the file
	 * - csvHeaders Comma-separated list or JSON-encoded array of headers
	 * - csvRows JSON-encoded array of objects, keys as in csvHeaders. Empty fields can be left out.
	 * @throws BadRequestException
	 * @throws ForbiddenException
	 * @throws ServerException
	 * @throws NotFoundException
	 */
	private static function createCsvFile(array $request): array {
		if(!isset($request['parentid'])){ throw new BadRequestException('No parentid specified'); }
		$parent=baseobject::getById($request['parentid']);
		if(!$parent){ throw new NotFoundException('Parent object does not exist, or you do not have permission to see it'); }

		if(!isset($request['filename'])){ throw new BadRequestException('No filename specified'); }
		if(!preg_match('/^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+$/', $request['filename'])){ throw new BadRequestException('Invalid filename'); }

		$description=$request['description'];
		if(empty($description)){ $description=''; }

		if(!isset($request['csvHeaders'])){ throw new BadRequestException('No CSV headers specified'); }
		$csvHeaders=json_decode($request['csvHeaders'],true);
		if(!$csvHeaders){ $csvHeaders=explode(",",$request['csvHeaders']); }

		if(!isset($request['csvRows'])){ throw new BadRequestException('No CSV rows specified'); }
		$csvRows=json_decode($request['csvRows'],true);
		if(!$csvRows){ throw new BadRequestException('Invalid CSV rows'); }

		//check that all keys for all rows are in csv headers and assemble CSV rows
		$fileRows=[$csvHeaders];
		$rowLength=count($csvHeaders);
		foreach ($csvRows as $row){
			$fileRow=array_pad([], $rowLength, '');
			$keys=array_keys($row);
			foreach ($keys as $key){
				$index=array_search($key, $csvHeaders);
				if(false===$index){
					throw new BadRequestException('CSV header found in row does not exist in headers: '.$key);
				}
				$fileRow[$index]=$row[$key];
			}
			$fileRows[]=$fileRow;
		}

		$tempFile = rtrim(config::get('core_filestore'), '/') . '/'.$request['filename'];
		$file=@fopen($tempFile, 'w');
		if(!$file){ throw new ServerException('Could not open temp file for writing'); }
		foreach ($fileRows as $fileRow){
			if(!@fputcsv($file, $fileRow, ',', '"', '')){
				throw new ServerException('Could not write temp file');
			}
		}
		@fclose($file);

		//Create DB record of file and save it
		return file::createFromTempFile(array(
			'description'=>$description,
			'parentid'=>$request['parentid'],
			'filename'=>pathinfo($tempFile, PATHINFO_FILENAME).'.'.pathinfo($tempFile, PATHINFO_EXTENSION),
			'mimetype'=>'text/csv',
			'tempfile'=>$tempFile
		));

	}

}