<?php 
/**
 * This is the base project class. It contains functionality for access control.
 * All application-specific project methods should go in classes/model/project.php. Do not modify this class unless adding generic access-related functionality.
 * 
 * A project is essentially a container for application-specific records, serving as a way to apply common access rights to them all.
 * One user is designated the "project owner". The project owner can perform certain actions such as archiving the project or updating its details. They may also have some application-specific privileges.
 * Users are members of usergroups. The project owner can grant those groups read, update, create and delete permissions "on the project" - actually on the records within the project.
 * 
 */
class baseproject implements crudable {

	const SHARED='Shared'; //The shared project for stuff that anyone might need, for example rooms, shared equipment...
	const DEFAULTPROJECT='Default Project'; //The default for unknown items
	const SELECT_STATEMENT='SELECT project.*,user.fullname AS ownername 
				FROM project, user WHERE user.id=project.owner ';

	public static string $defaultSortOrder='issystem DESC, UPPER(project.name) ASC';



	/**
	 * This is the base project class, which handles only general access-related functions.
	 * If you want to add methods that are specific to your application, you should do these in
	 * classes/model/project.class.php instead. 
	 */
	
	protected static array $fields=array(
			'name'=>validator::REQUIRED,
			'description'=>validator::REQUIRED,
			'owner'=>array(validator::INTEGER, validator::REQUIRED),
			'isarchived'=>validator::BOOLEAN,
			'issystem'=>validator::BOOLEAN
	);
	
	protected static array $helpTexts=array(
			'name'=>'A unique name for the project',
			'description'=>'A short description of why the project exists',
			'owner'=>'The user in charge of this project',
			'isarchived'=>'If archived, the project cannot be changed',
			'issystem'=>'Whether this is a system project'
	);

    /**
     * @return array
     */
	public static function getFieldValidations(): array {
		return self::$fields;
	}

    /**
     * @return array
     */
	public static function getFieldHelpTexts(): array {
		return self::$helpTexts;
	}

	/**
	 * @param int $id
	 * @return array|null
	 * @throws BadRequestException
	 * @throws ServerException
	 */
	public static function getById(int $id): ?array {
		$sqlStatement=static::SELECT_STATEMENT.' AND project.id=:id ';
		$readProjects=session::getReadProjects();
		if(empty($readProjects)){
			$readProjects=array();
			$readProjects[]=0;
		}
		$sqlStatement.=' AND project.id IN('.implode(',', $readProjects).')';
		return database::queryGetOne($sqlStatement, array(':id'=>$id));
	}

	/**
	 * @param string $name
	 * @return array|null
	 * @throws BadRequestException
	 * @throws ServerException
	 */
	public static function getByName(string $name): ?array {
		$sqlStatement=static::SELECT_STATEMENT.' AND project.name=:name ';
		$readProjects=session::getReadProjects();
		if(empty($readProjects)){
			$readProjects=[0];
		}
		$sqlStatement.=' AND project.id IN('.implode(',', $readProjects).')';
		return database::queryGetOne($sqlStatement, array(':name'=>$name));
	}

	/**
	 * @param string $key
	 * @param string $value
	 * @param array $request
	 * @return array|null
	 * @throws BadRequestException
	 * @throws ServerException
	 */
	public static function getByProperty(string $key,string $value,array $request=[]): ?array {
		$keys=array_keys(self::getFieldValidations());
		if(!in_array($key, $keys)){
			throw new BadRequestException('Property '.$key.' not recognised on project');
		}
		$sqlStatement=static::SELECT_STATEMENT.' AND project.'.$key.'=:val ';
		$readProjects=session::getReadProjects();
		if(empty($readProjects)){
			$readProjects=[0];
		}
		$sqlStatement.=' AND project.id IN('.implode(',', $readProjects).')';
		$sqlStatement.=database::getOrderClause($request, 'project');
		$sqlStatement.=database::getLimitClause($request);
		return database::queryGetAll($sqlStatement, array(':val'=>$value));
	}

	/**
	 * @throws ServerException
	 * @throws BadRequestException
	 */
	public static function getByProperties(array $keyValuePairs, array $request=[]): ?array {
		$sqlStatement=static::SELECT_STATEMENT;
		$params=[];
		$keys=array_keys(self::getFieldValidations());
		foreach ($keyValuePairs as $key=>$value){
			if(!in_array($key, $keys)){
				throw new BadRequestException('Property '.$key.' not recognised on project');
			}
			$sqlStatement.=" AND project.$key=:$key ";
			$params[":$key"]=$value;
		}
		$readProjects=session::getReadProjects();
		if(empty($readProjects)){
			$readProjects=[0];
		}
		$sqlStatement.=' AND project.id IN('.implode(',', $readProjects).')';
		$sqlStatement.=database::getOrderClause($request, 'project');
		$sqlStatement.=database::getLimitClause($request);
		return database::queryGetAll($sqlStatement, $params);
	}

	/**
	 * @param array $request
	 * @return array|null
	 * @throws BadRequestException
	 * @throws ServerException
	 */
	public static function getAll(array $request=[]): ?array {
		$sqlStatement=static::SELECT_STATEMENT;
		$readProjects=session::getReadProjects();
		if(empty($readProjects)){
			$readProjects=array();
			$readProjects[]=0;
		}
		$sqlStatement.=' AND project.id IN('.implode(',', $readProjects).')';
		$sqlStatement.=database::getOrderClause($request, 'project');
		$sqlStatement.=database::getLimitClause($request);
		return database::queryGetAll($sqlStatement);
	}

    /**
     * @param array $request
     * @return array
     * @throws BadRequestException
     * @throws ForbiddenException
	 * @throws ServerException
     */
	public static function create(array $request=[]): array {
		if(!session::isAdmin() &&!session::canCreateProjects()){
			throw new ForbiddenException('You do not have permission to create projects.');
		}
		$keys=array();
		foreach (static::$fields as $fieldName=>$validations){
			if(isset($request[$fieldName])){
				validator::validate($fieldName, $request[$fieldName], $validations);
				$keys[]=$fieldName;
			} else {
				validator::validate($fieldName, null, $validations);
			}
		}

		$wasAdmin=session::isAdmin();

		baseobject::beforeCreateHook($request, 'project');

		$fields=implode(',', $keys);
		$values=':'.implode(',:', $keys);
        /** @noinspection SqlInsertValues */
        $sqlStatement='INSERT INTO project('.$fields.') VALUES('.$values.')';
		$params=array();
		foreach($keys as $k){
			if(isset($request[$k])){
				$params[':'.$k]=$request[$k];
			}
		}
		database::query($sqlStatement,$params);
		$newProjectId=database::getLastInsertId();
		$sqlStatement=static::SELECT_STATEMENT.' AND project.id=:id ';
		$created=database::queryGetOne($sqlStatement, array(':id'=>$newProjectId));
		session::refreshProjectPermissions();
		session::set('isAdmin',$wasAdmin);

		baseobject::afterCreateHook($created, 'project');

		return array('type'=>'project', 'created'=>$created);
	}

	/**
	 * @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['showArchivedProjects'])){
			return basesession::setShowArchivedProjects($request['showArchivedProjects']);
		}
		$project=self::getById($id);
		if(!session::isAdmin() && $project['owner']!=session::getUserId()){
			throw new ForbiddenException('You do not have permission to update this project');
		}
		if(isset($request['isarchived'])){
			if(1===1*$request['isarchived']){
				return baseproject::archive($id, $request);
			} else {
				return baseproject::unArchive($id, $request);
			}
		}
		foreach($request as $k=>$v){
			if(1===$project['isarchived'] && 'isarchived'!==$k){
				throw new ForbiddenException('You cannot update an archived project.');
			}
		}
		baseobject::beforeUpdateHook($id,$request,'project');
		foreach($request as $k=>$v){
			if(in_array($k, array_keys(self::$fields))){
				validator::validate($k, $v, self::$fields[$k]);
				database::query('UPDATE project SET '.$k.'=:val WHERE id=:id', array(':id'=>$id, ':val'=>$v));
			}
		}
		baseobject::afterUpdateHook($id,'project');
		return array('updated'=>self::getById($id));
	}

	/**
	 * Archives the project. Sets isarchived=1 and creates a read permission for the Archive Viewers usergroup.
	 * @throws ForbiddenException
	 * @throws BadRequestException
	 * @throws ServerException
	 */
	public static function archive(int $id, array $request=[]): array {
		$project=self::getById($id);
		if(1*$project['issystem']) {
			throw new BadRequestException('You cannot archive a system project');
		}
		if(!session::isAdmin() && 1*$project['owner']!==session::getUserId()){
			throw new ForbiddenException('Only administrators and the project owner can archive a project');
		}
		baseobject::beforeUpdateHook($id,$request,'project');
		database::query('UPDATE project SET isarchived=1 WHERE id=:id', array(':id'=>$id));
		baseobject::afterUpdateHook($id,'project');
		return array('updated'=>self::getById($id));
	}

	/**
	 * Un-archives the project.
	 * @param int $id
	 * @param array $request
	 * @return array
	 * @throws BadRequestException
	 * @throws ForbiddenException
	 * @throws ServerException
	 */
	public static function unArchive(int $id, array $request=[]): array {
		$project=self::getById($id);
		if(1*$project['issystem']) {
			throw new BadRequestException('You cannot archive or un-archive a system project');
		}
		if(!session::isAdmin()){
			throw new ForbiddenException('Only administrators can un-archive a project');
		}
		baseobject::beforeUpdateHook($id,$request,'project');
		database::query('UPDATE project SET isarchived=0 WHERE id=:id', array(':id'=>$id));
		baseobject::afterUpdateHook($id,'project');
		return array('updated'=>self::getById($id));
	}


	/**
	 * @param int $id
	 * @return array
	 * @throws BadRequestException
	 */
	public static function delete(int $id): array {
		throw new BadRequestException('Projects cannot be deleted');
	}

    /**
     * @return bool
     */
	public static function canCreate(): bool {
		return session::isAdmin() || session::canCreateProjects();
	}

	/**
	 * @throws ServerException
	 * @throws BadRequestException
	 */
	public static function canReadProject(int $id): bool {
		return in_array($id, session::getReadProjects()) || session::isAdmin();
	}

	/**
	 * @throws ServerException
	 * @throws BadRequestException
	 */
	public static function canCreateInProject(int $id): bool {
		return in_array($id, session::getCreateProjects()) || session::isAdmin();
	}

	/**
	 * @throws ServerException
	 * @throws BadRequestException
	 */
	public static function canUpdateInProject(int $id): bool {
		return in_array($id, session::getUpdateProjects()) || session::isAdmin();
	}

	/**
	 * @throws ServerException
	 * @throws BadRequestException
	 */
	public static function canDeleteInProject(int $id): bool {
		return static::canUpdateInProject($id);
	}

	/**
	 * @param int $id
	 * @return bool
	 * @throws BadRequestException
	 * @throws NotFoundException
	 * @throws ServerException
	 */
	public static function canUpdate(int $id): bool {
		if(session::isAdmin()){ return true; }
		$project=project::getById($id);
		if(!$project){
			throw new NotFoundException('Project does not exist, or you do not have permission to see it');
		}
		if($project['owner']==session::getUserId()){ return true; }
			return false;
	}

	/**
	 * @param int $id
	 * @param array $request
	 * @return array
	 * @throws BadRequestException
	 * @throws ServerException
	 */
	public static function getpermissions(int $id,array $request=[]): array {
		$sqlStatement='SELECT id,name, "usergroup" AS objecttype FROM usergroup WHERE 1=1 ';
		if(!session::isAdmin()){
			$readables=session::getVisibleGroupIds();
			if(empty($readables)){ $readables=array('0'); }
			$sqlStatement.=' AND id IN('.implode(',', $readables).')';
		}
		$sqlStatement.=' ORDER BY issystem DESC, name ASC';
		//TODO Limit clause
		$result=database::queryGetAll($sqlStatement);
		$groups=array();
		foreach($result['rows'] as $r){
			$r['permissions']=array('read'=>false, 'update'=>false, 'create'=>false, 'delete'=>false);
			$groups['grp'.$r['id']]=$r;
		}
		$groupIds=array_column($groups,'id');
		$sqlStatement='SELECT id, usergroupid, type FROM permission WHERE projectid=:id AND usergroupid IN('.implode(',',$groupIds).')';
		$perms=database::queryGetAll($sqlStatement, array(':id'=>$id));
		if(!empty($perms)){
			foreach($perms['rows'] as $p){
				$groups['grp'.$p['usergroupid']]['permissions'][$p['type']]=$p['id'];
			}
		}
		return array(
				'total'=>count($groups), //TODO more than this if LIMIT clause
				'rows'=>array_values($groups)
		);
	}

	/**
	 * Returns the ID of the shared project.
	 * @return int
	 * @throws BadRequestException
	 * @throws ServerException
	 */
	public static function getSharedProjectId(): int {
		$project=database::queryGetOne('SELECT id FROM project WHERE name=:name', array(':name'=>self::SHARED));
		if(!$project){ throw new ServerException('Shared project not found'); }
		return 1*$project['id'];
	}

	/**
	 * Returns the ID of the default project.
	 * @return int
	 * @throws BadRequestException
	 * @throws ServerException
	 * @noinspection PhpUnused
	 */
	public static function getDefaultProjectId(): int {
		$project=database::queryGetOne('SELECT id FROM project WHERE name=:name', array(':name'=>self::DEFAULTPROJECT));
		if(!$project){ throw new ServerException('Default project not found'); }
		return 1*$project['id'];
	}

	/**
	 * @param int $id
	 * @param array $request
	 * @return array|null
	 * @throws BadRequestException
	 * @throws ServerException
	 */
	public static function getNotes(int $id, array $request): ?array {
        return note::getByProperties(array(
            'parentid'=>database::$nullValue,
            'projectid'=>$id
        ), $request);
    }

	/**
	 * @param int $id
	 * @param array $request
	 * @return array|null
	 * @throws BadRequestException
	 * @throws ServerException
	 */
    public static function getFiles(int $id, array $request): ?array {
        return file::getByProperties(array(
            'parentid'=>database::$nullValue,
            'projectid'=>$id
        ), $request);
    }

}