<?php

namespace XF\Util;

class File
{
	protected static $tempFiles = [];
	protected static $tempCleanUpRegistered = false;
	protected static $defaultFilePermissions = null;

	public static function getCodeCachePath()
	{
		$config = \XF::app()->config();

		return self::canonicalizePath(
			sprintf($config['codeCachePath'], $config['internalDataPath'])
		);
	}

	public static function getTempDir()
	{
		$config = \XF::app()->config();
		$path = self::canonicalizePath(
			sprintf($config['tempDataPath'], $config['internalDataPath'])
		);
		if (!is_dir($path))
		{
			self::createDirectory($path);
		}

		return $path;
	}

	public static function getTempFile()
	{
		$dir = self::getTempDir();
		$file = tempnam($dir, 'xf');

		self::registerTempFile($file);

		return $file;
	}

	public static function getNamedTempFile($fileName)
	{
		$file = self::getTempDir() . '/' . $fileName;

		self::registerTempFile($file);

		return $file;
	}

	protected static function registerTempFile($file)
	{
		self::$tempFiles[] = $file;

		if (!self::$tempCleanUpRegistered)
		{
			register_shutdown_function([__CLASS__, 'cleanUpTempFiles']);
			self::$tempCleanUpRegistered = true;
		}
	}

	public static function cleanUpTempFiles()
	{
		foreach (self::$tempFiles AS $file)
		{
			try
			{
				@unlink($file);
			}
			catch (\Exception $e) {}
		}

		self::$tempFiles = [];
	}

	public static function copyStreamToTempFile($sourceResource)
	{
		$tempFile = self::getTempFile();

		$tempResource = fopen($tempFile, 'w');
		stream_copy_to_stream($sourceResource, $tempResource);
		fclose($tempResource);

		return $tempFile;
	}

	public static function copyAbstractedPathToTempFile($abstractedPath)
	{
		$stream = \XF::app()->fs()->readStream($abstractedPath);
		$tempFile = self::copyStreamToTempFile($stream);
		@fclose($stream); // some streams may close after reading

		return $tempFile;
	}

	public static function copyFileToAbstractedPath($file, $abstractedPath, array $config = [])
	{
		$fp = fopen($file, 'r');
		$result = \XF::app()->fs()->putStream($abstractedPath, $fp, $config);
		fclose($fp);

		return $result;
	}

	public static function writeToAbstractedPath($abstractedPath, $contents, array $config = [], $allowOverwrite = true)
	{
		$fs = \XF::app()->fs();

		if ($allowOverwrite)
		{
			return $fs->put($abstractedPath, $contents, $config);
		}
		else
		{
			return $fs->write($abstractedPath, $contents, $config);
		}
	}

	public static function deleteFromAbstractedPath($abstractedPath)
	{
		try
		{
			\XF::app()->fs()->delete($abstractedPath);
		}
		catch (\League\Flysystem\FileNotFoundException $e) {}
	}

	public static function deleteAbstractedDirectory($abstractedDir)
	{
		try
		{
			\XF::app()->fs()->deleteDir($abstractedDir);
		}
		catch (\League\Flysystem\FileNotFoundException $e) {}
	}

	public static function abstractedPathExists($abstractedPath)
	{
		return \XF::app()->fs()->has($abstractedPath);
	}

	public static function createDirectory($path, $createIndexHtml = true)
	{
		if (file_exists($path))
		{
			if (is_dir($path))
			{
				return true;
			}

			throw new \LogicException("Attempting to created directory $path but a non-directory already exists at $path");
		}

		$path = str_replace('\\', '/', $path);
		$path = rtrim($path, '/');
		$parts = explode('/', $path);
		$pathPartCount = count($parts);
		$partialPath = '';

		$rootDir = str_replace('\\', '/', \XF::getRootDirectory());

		// find the "lowest" part that exists (and is a dir)...
		for ($i = $pathPartCount - 1; $i >= 0; $i--)
		{
			$partialPath = implode('/', array_slice($parts, 0, $i + 1));

			if (file_exists($partialPath))
			{
				if (!is_dir($partialPath))
				{
					throw new \LogicException("Attempting to created directory $path but a non-directory already exists at $partialPath");
				}
				else
				{
					break;
				}
			}

			if ($partialPath == $rootDir)
			{
				return false; // can't go above the root dir
			}
		}
		if ($i < 0)
		{
			return false;
		}

		$i++; // skip over the last entry (as it exists)

		// ... now create directories for anything below it
		for (; $i < $pathPartCount; $i++)
		{
			$partialPath .= '/' . $parts[$i];
			if (!mkdir($partialPath))
			{
				return false;
			}

			self::makeWritableByFtpUser($partialPath);

			if ($createIndexHtml)
			{
				if (@file_put_contents($partialPath . '/index.html', ' '))
				{
					self::makeWritableByFtpUser($partialPath . '/index.html');
				}
			}
		}

		return true;
	}

	public static function writeFile($file, $data, $createIndexHtml = true)
	{
		$dir = dirname($file);
		if (self::createDirectory($dir, $createIndexHtml))
		{
			if (file_put_contents($file, $data))
			{
				self::makeWritableByFtpUser($file);
				return true;
			}
			else
			{
				return false;
			}
		}
		else
		{
			return false;
		}
	}

	public static function copyFile($source, $destination, $createIndexHtml = true)
	{
		$dir = dirname($destination);
		if (self::createDirectory($dir, $createIndexHtml))
		{
			if (copy($source, $destination))
			{
				self::makeWritableByFtpUser($destination);
				return true;
			}
			else
			{
				return false;
			}
		}
		else
		{
			return false;
		}
	}

	/**
	 * Performs a cross-stream/device safe rename by falling back
	 * to a copy+delete if needed.
	 *
	 * @param string $source
	 * @param string $destination
	 * @param bool $createIndexHtml
	 *
	 * @return bool
	 */
	public static function renameFile($source, $destination, $createIndexHtml = true)
	{
		$dir = dirname($destination);
		if (!self::createDirectory($dir, $createIndexHtml))
		{
			return false;
		}

		try
		{
			$success = rename($source, $destination);
		}
		catch (\Exception $e)
		{
			// possibly a perm problem, but may be an issue moving across streams, so copy+delete instead
			$success = false;
		}

		if (!$success)
		{
			$success = copy($source, $destination);
			if ($success)
			{
				@unlink($source);
			}
		}

		if ($success)
		{
			self::makeWritableByFtpUser($destination);
		}

		return $success;
	}

	public static function getDefaultFilePermissions()
	{
		if (self::$defaultFilePermissions)
		{
			return self::$defaultFilePermissions;
		}

		$chmod = \XF::app()->config('chmodWritableValue');
		if (!$chmod)
		{
			$selfWritable = null;

			if (PHP_SAPI == 'cli' && function_exists('posix_getuid'))
			{
				$lock = self::canonicalizePath(\XF::app()->config('internalDataPath') . '/install-lock.php');
				$uid = @posix_getuid();
				if (file_exists($lock))
				{
					// if we're running as who created the lock file,
					// then the web access is likely running with the same user
					$selfWritable = ($uid == fileowner($lock));
				}
				else if ($uid == 0)
				{
					// if we're root, just force w+w
					$selfWritable = false;
				}
			}

			if ($selfWritable === null)
			{
				$selfWritable = @is_writable(__FILE__);
			}

			if ($selfWritable)
			{
				// writable - probably owned by ftp user already
				$chmod = 0644;
			}
			else
			{
				// not writable, so file is probably owned by "nobody", need to w+w it
				$chmod = 0666;
			}
		}

		self::$defaultFilePermissions = [
			'file' => $chmod,
			'dir' => $chmod | 0111
		];

		return self::$defaultFilePermissions;
	}

	/**
	 * Ensures that the specified file can be written to by the FTP user.
	 * This generally doesn't need to do anything if PHP is running via
	 * some form of suexec. It's primarily needed when running as apache.
	 *
	 * @param string $file
	 * @return boolean
	 */
	public static function makeWritableByFtpUser($file)
	{
		if (@is_readable($file))
		{
			$permissions = self::getDefaultFilePermissions();
			$chmod = @is_file($file) ? $permissions['file'] : $permissions['dir'];
			return @chmod($file, $chmod);
		}
		else
		{
			return false;
		}
	}

	public static function deleteDirectory($path)
	{
		$path = self::canonicalizePath($path);
		if (!is_readable($path) || !is_dir($path))
		{
			throw new \InvalidArgumentException("$path is not readable or not a directory");
		}

		if (!is_writable($path))
		{
			throw new \InvalidArgumentException("$path is not writable");
		}

		$files = new \RecursiveIteratorIterator(
			new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS),
			\RecursiveIteratorIterator::CHILD_FIRST
		);
		foreach ($files AS $file)
		{
			/** @var $file \SplFileInfo */
			if ($file->isDir())
			{
				rmdir($file->getRealPath());
			}
			else
			{
				unlink($file->getRealPath());
			}
		}

		rmdir($path);
	}

	public static function canonicalizePath($path, $root = null)
	{
		if (preg_match('#^(/|\\\\|[a-z0-9]+:)#i', $path))
		{
			return $path; // absolute already
		}

		if ($root === null)
		{
			$root = \XF::getRootDirectory();
		}

		return $root . DIRECTORY_SEPARATOR . $path;
	}

	public static function stripRootPathPrefix($path, $root = null)
	{
		if ($root === null)
		{
			$root = \XF::getRootDirectory();
		}
		$root = rtrim($root, '/\\') . DIRECTORY_SEPARATOR;

		$realPath = realpath($path);
		if ($realPath)
		{
			$path = realpath($path);
		}

		if (strpos($path, $root) === 0)
		{
			return strval(substr($path, strlen($root)));
		}
		else
		{
			return $path;
		}
	}

	/**
	 * Method for writing out a file log.
	 *
	 * @param string $logName 'foo' will write to {tempDataPath}/foo.log
	 * @param string $logEntry The string to write into the log. Line break not required.
	 * @param boolean $append Append the log entry to the end of the existing log. Otherwise, start again.
	 *
	 * @return boolean True on successful log write
	 */
	public static function log($logName, $logEntry, $append = true)
	{
		$logName = preg_replace('/[^a-z0-9._-]/i', '', $logName);
		$path = self::getTempDir();
		$file = "$path/$logName.log";

		if ($fp = @fopen($file, ($append ? 'a' : 'w')))
		{
			fwrite($fp, date('Y-m-d H:i:s') . ' ' . $logEntry . "\n");
			fclose($fp);

			return true;
		}

		return false;
	}

	public static function writeInstallLock()
	{
		$contents = '<?php header(\'Location: ../index.php\'); /* Installed: ' . date(DATE_RFC822) . ' */';
		\XF::fs()->write('internal-data://install-lock.php', $contents);
	}

	public static function installLockExists()
	{
		try
		{
			// If this path doesn't exist, then this will throw an exception. We need to handle this elsewhere.
			return \XF::fs()->has('internal-data://install-lock.php');
		}
		catch (\Exception $e)
		{
			return false;
		}
	}

	/**
	 * Gets a file's extension in lower case. This only includes the last
	 * extension (eg, x.tar.gz -> gz).
	 *
	 * @param string $filename
	 *
	 * @return string
	 */
	public static function getFileExtension($filename)
	{
		return strtolower(pathinfo($filename, PATHINFO_EXTENSION));
	}
}