<?php

namespace Engine;

class File
{

	/**
	 * Returns the extension of the file
	 * @param String $file
	 * @return String file extension.
	 */
	public static function getExtension($file)
	{
		$fullName=self::getNameFull($file);
		// first check that the file has an extension (we do this by searching '.' within the file name)
		$periodPos=strrpos($fullName, '.');
		if ($periodPos===false)
			return false;

		return substr($fullName,$periodPos+1);
	}

	/**
	 * Returns the name of the file (no path and extension)
	 * @param object $file
	 * @return String file name.
	 */
	public static function getName($file)
	{
		if(!$file) return false;

		$fullName=self::getNameFull($file);
		if(!$fullName) return false;
		$name="";
		// REMOVE PATH
		$filePos=strrpos($fullName,'/');
		if ($filePos) $name=substr($fullName,$filePos+1);
		else $name=$fullName;

		if (!$name)
			return;

		// REMOVE EXTENSION
		if (strrpos($name, '.')!==false)
			return substr($name,0,strrpos($name,'.'));
		else
			return $name;
	}

	/**
	 * Returns the name of the file with extension (no path)
	 * @param object $file
	 * @return string The file name.
	 */
	public static function getNameFull($file)
	{
		$file = cleanPath((string)$file);
		$filePathArr = explode("/", (string)$file);

		$lastEl=end($filePathArr);
		// if last el is empty, it means last char in filepath was "/" (a directory)
		if(!$lastEl) return false;
		// if path is a directory return false
		elseif(is_dir($file)) return false;

		return self::basename($file);
	}

	/**
	 * Workaround implementation of the basename function
	 * The core function is locale-aware, and cannot handle correctly 'تذكرة تشيرانجي (002).pdf'
	 * This specific code is the fastest one, as tested in the below uTest
	 * @see uTests/Example1/LanguageBenchmarks/basename/
	 * @param String $file
	 * @return String
	 */
	public static function basename($path)
	{
		if (!$path) {
			return '';
		}
		$pos = strrpos($path, '/');
    	return substr($path, ($pos?$pos+1:0));
	}

	/**
	 * Returns only the path of the file
	 * @param object $file
	 * @return string The file path.
	 */
	public static function getPath($file)
	{
		$file = (string)cleanPath($file);
		$filePathArr=explode("/", $file);

		$lastEl=end($filePathArr);
		// if last el is empty, it means last char in filepath was "/" (a directory)
		if(!$lastEl) return $file;
		// if path is a directory and it doesn't end with a "/", add it.
		elseif(is_dir($file)) return $file."/";

		return dirname($file)."/";
	}

	/**
	 * Validate a browser-supplied relative path against a base directory and build a safe, normalized full path.
	 * It's supposed to support both files & folders
	 * @param string $baseDir
	 * @param string $pathReceivedFromBrowser
	 * @return string|bool
	 */
	public static function buildSafePath(string $baseDir, string $pathReceivedFromBrowser) :string|bool
	{
		$pathReceivedFromBrowser = trim($pathReceivedFromBrowser);
		if ($pathReceivedFromBrowser === '') {
			return false;
		}

		// disallow absolute paths
		if (!$baseDir && in_array($pathReceivedFromBrowser[0], ['/', '\\'], true)) {
			return false;
		}

		// normalize the pieces
		$relative = cleanPath($pathReceivedFromBrowser);

		// disallow directory traversal
		if (strpos($relative, '..') !== false) {
			return false;
		}

		$fullPath = cleanPath($baseDir.$relative);

		return $fullPath;
	}

	/**
	 * Checks the file path for available name. If name is not available, it assigns an index at the end of the file name.
	 * @param object $file
	 * @return string The available name of the file
	 */
	public static function getNameAvailable($file)
	{
		$path=self::getPath($file);
		$fullName=self::getNameFull($file);

		if (!$fullName) {
			throw new \Exception("'{$file}' is a directory. Please supply a complete filename");
		}

		// if the name is taken, try to find the next free file name
		if (is_file($file)) {
			$prefix=self::getName($file);
			$ext=self::getExtension($file);
			$i=0;
			do{
				$i++;
				$fullName=$prefix."_$i.".$ext;
			} while(file_exists($path.$fullName));
		}
		// if the name is not taken, assign the original name
		return $fullName;
	}

	/**
	 * Returns the raw contents of a file
	 * @param object $file
	 * @return String file contents
	 */
	public static function getContents($file)
	{

		if (!self::check($file)) {
			return false;
		}

		// return the file contents
		return file_get_contents($file);

	}

	/**
	 * Cleans the name of the file for forbidden chars.
	 * @param object $file
	 * @return string The cleaned name of the file
	 */
	public static function cleanName($file)
	{
		$file=cleanPath($file);
		$fullName=self::getNameFull($file);
		//return preg_replace('/[^\`\'\"]/', '', $fullName);
		return preg_replace('/[\/\?\:\*\|\\\\<>\"\']/', '', $fullName);
	}

	/**
	 * Removes a file from disk
	 * @param object $file
	 * @return boolean true on success, false otherwise
	 */
	public static function delete($file)
	{
		// REVIEW: prone to race conditions, @see TT-805, @ee AS Travel/server log 1067873 (and many more with 'Could not update the permissions for...')


		if(!self::check($file)) return false;
		return unlink($file);

	}

	/**
	 * Checks that file path exists and is readable/writable. Same goes for the file itself. The path is cleaned for further use (reference).
	 * @param object $file
	 * @return boolean true on success, false otherwise
	 */
	public static function check($file)
	{

		if(!$file) return false;
		$file=cleanPath($file);

		if(!file_exists($file) or is_dir($file)) return false;

		if(!is_writable($file) or !is_readable($file)) return false;

		return true;
	}

	/**
	 * Creates a file in its path, checking it first.
	 * @param string $file
	 * @param string $contents [optional]
	 * @param string $mode [optional] (overwrite|append|rename)
	 * @return mixed false on fail, the path of the file on success
	 */
	public static function create($file, $contents="", $mode="overwrite", $permissions=0777)
	{

		if($mode!="overwrite" and $mode!="append" and $mode!="rename") throw new \Exception('The \Engine\File::create mode is not valid.');

		$filePath=self::getPath($file);
		$fileNameFull=self::cleanName($file);
		$file=$filePath.$fileNameFull;

		// if file is a directory, return false
		if(is_dir($file))return false;

		// if file exists, do not change access rights
		$chmod=false;
		if (!file_exists($file)) $chmod=true;

		// check and create path
		if (!\Engine\Dir::check($filePath)) {
			\Log::getInstance()->error(sprintf("Could not create folder structure path on disk: '%s'", $filePath), [
				'error_get_last' => error_get_last(),
				'file' => $file,
			]);
			return false;
		}

		// check file name is available
		if($mode=="rename"){
			$fileNameFull=self::getNameAvailable($file);
			$file=$filePath.$fileNameFull;
		}

		// open handle
		$fMode = ($mode=="append") ? 'a' : 'w';
		$fp=fopen($file,$fMode);
		if(!$fp){
			\Log::getInstance()->error(sprintf("Could not open file handle for '%s'", $file), [
				'error_get_last' => error_get_last(),
				'file' => $file,
			]);
			return false;
		}

		// write to file
		if (fwrite($fp, $contents)===false) {
			\Log::getInstance()->error(sprintf("Could not write to file '%s'", $file), [
				'error_get_last' => error_get_last(),
				'file' => $file,
			]);
			fclose($fp);
			return false;
		}
		fclose($fp);

		// change file permission
		if ($chmod) {
			if (!chmod($file, $permissions)) {
				\Log::getInstance()->error(sprintf("[%s] Could not update the permissions for %s' to '%s'", __METHOD__, $file, $permissions), [
					'error_get_last' => new \Engine\Logger\Context\ArrayAssoc(error_get_last()),
					'file' => $file,
					'permissions' => $permissions,
				]);
				return false;
			}
		}

		return $file;
	}

	/**
	 * Copies a file from source to destination. Checks source file and destination folder.
	 * If destination param doesn't contain a file name, the source file name is used.
	 * @param object $sourceFile
	 * @param object $destinationFile
	 * @param String $permissions
	 * @return
	 */
	public static function copy($sourceFile, $destinationFile, $permissions=0777)
	{

		// check source file
		if(!self::check($sourceFile)){
			return false;
		}

		$destinationPath=self::getPath($destinationFile);
		$destinationFileNameFull=self::getNameFull($destinationFile);
		// if name of file is not provided in the destination file, get it from source
		if(!$destinationFileNameFull){
			$destinationFileNameFull=self::getNameFull($sourceFile);
		}

		// check destination file path
		if(!\Engine\Dir::create($destinationPath)) return false;

		// the actual copy
		if (!copy($sourceFile,$destinationPath.$destinationFileNameFull)) return false;
		if (!chmod($destinationPath.$destinationFileNameFull, $permissions)) return false;

		return true;
	}

	/**
	 * Move a file from source to destination. Checks source file and destination folder.
	 * If destination param doesn't contain a file name, the source file name is used.
	 * @param object $sourceFile
	 * @param object $destinationFile
	 * @return
	 */
	public static function move($sourceFile, $destinationFile)
	{

		// check source file
		if(!self::check($sourceFile)){
			return false;
		}

		$destinationPath=self::getPath($destinationFile);
		$destinationFileNameFull=self::cleanName($destinationFile);
		// if name of file is not provided in the destination file, get it from source
		if(!$destinationFileNameFull){
			$destinationFileNameFull=self::getNameFull($sourceFile);
		}

		// check destination file path
		if(!\Engine\Dir::create($destinationPath)){
			return false;
		}

		// move file
		$ret = rename($sourceFile,$destinationPath.$destinationFileNameFull);

		// and change access level
		if (!is_writable($destinationPath.$destinationFileNameFull)) {
			chmod($destinationPath.$destinationFileNameFull, 0777);
		}

		return $ret;

	}

	/**
	 * Convert bytes for display
	 * @param int $bytes
	 * @return string
	 */
	public static function formatBytes(?int $bytes=null, int $decimals=2)
	{
		if ($bytes > 1024*1024*1024) return round($bytes / (1024*1024*1024), $decimals)."GB";
		else if ($bytes > 1024*1024) return round($bytes / (1024*1024), $decimals)."MB";	// changed from 1 so it can be reused  @JIRA TD-9152
		else if ($bytes > 1024) return round($bytes / 1024, $decimals)."KB";				// changed from 1 so it can be reused  @JIRA TD-9152
		else return ($bytes)."B";
	}

	/**
	 * Create a temporary file
	 * @see http://php.net/manual/en/function.tempnam.php
	 * @param string $baseFilename
	 * @return string
	 */
	public static function tempnam($baseFilename)
	{
		global $CFG;
		$path = APP_REAL_PATH . $CFG['paths']['temporary'];

		// regexp from http://stackoverflow.com/a/2727693/11301
		$baseFilename = preg_replace(['/\s/', '/\.[\.]+/', '/[^\w_\.\-]/'], ['_', '.', '_'], $baseFilename);

		// check if the filename is really unique
		$baseFilename = static::getNameAvailable($path.$baseFilename);

		// create the file, to avoid race conditions
		fclose(fopen($path.$baseFilename, "w"));


		return $path.$baseFilename;
	}

	/**
	 * Set default permissions for a file
	 * @param string $file
	 * @return boolean
	 */
	public static function setDefaultPermissions($file)
	{
		if (!chmod($file, 0777)) {
			\Log::getInstance()->error(sprintf("[%s] Could not update the default permissions for %s'.", __METHOD__, $file), [
				'error_get_last' => error_get_last(),
				'file' => $file
			]);

			return false;
		}

		return true;
	}
}
