<?php namespace Intellex\Filesystem;

use Intellex\Filesystem\Exception\NotADirectoryException;
use Intellex\Filesystem\Exception\NotAFileException;

/**
 * Handles the basic filesystem operations.
 *
 * @package Intellex\Filesystem
 */
abstract class Path {

	/** @var string The path to the file or directory. */
	private $path;

	/** const string  */
	const DS = DIRECTORY_SEPARATOR;

	/**
	 * Initialize file or directory.
	 *
	 * @param string $path The path to the file or directory.
	 */
	public function __construct($path) {
		$this->init($path);
	}

	/**
	 * Initialize the complete path from the scratch.
	 *
	 * @param string $path The path to the file or directory.
	 *
	 * @return Path The instance of itself.
	 */
	public function init($path) {
		$this->path = static::realPath($path);
		return $this;
	}

	/**
	 * Get the name of the path.
	 *
	 * @return string The name of the path.
	 */
	public function getName() {
		$info = pathinfo($this->getPath());
		return $info['basename'];
	}

	/**
	 * Get the path to the file or directory.
	 *
	 * @return string The path to the file or directory.
	 */
	public function getPath() {
		return $this->path;
	}

	/**
	 * Check if the path is a file.
	 *
	 * @return bool True if supplied path is a file.
	 * @throws NotAFileException
	 * @throws NotADirectoryException
	 */
	public function isFile() {
		$this->typeMatches();
		return $this instanceof File;
	}

	/**
	 * Check if the path is a directory.
	 *
	 * @return bool True if supplied path is a directory.
	 * @throws NotAFileException
	 * @throws NotADirectoryException
	 */
	public function isDir() {
		$this->typeMatches();
		return $this instanceof Dir;
	}

	/**
	 * Get the parent directory.
	 *
	 * @return Dir|null The parent directory, or null if called on the filesystem root.
	 */
	public function getParent() {
		if ($this instanceof Dir && $this->isRoot()) {
			return null;
		}

		return new Dir(dirname($this->path));
	}

	/**
	 * Check if this path is absolute or relative.
	 *
	 * @param Path|string $path The path to check.
	 *
	 * @return bool True this path is absolute.
	 */
	public static function isAbsolute($path) {
		return preg_match('~^([A-Z]:\\\\|/)~', $path);
	}

	/**
	 * Get the cleaned up path.
	 *
	 * @param Path|string $path The to get the real path from.
	 *
	 * @return string The true and absolute path.
	 */
	public static function realPath($path) {

		// Make it an absolute path
		$path = !static::isAbsolute($path)
			? __DIR__ . static::DS . $path
			: $path;

		// Clear back links
		$path = str_replace(DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR, '', $path);

		// Clean up double DS
		return preg_replace('~' . DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR . '+~', DIRECTORY_SEPARATOR, $path);
	}

	/**
	 * Check if declared type matches the filesystem
	 *
	 * @throws NotAFileException On miss match.
	 * @throws NotADirectoryException
	 */
	public function typeMatches() {

		// File on system, but not declared as such
		if ($this instanceof File && file_exists($this->path) && !is_file($this->path)) {
			throw new NotAFileException($this);
		}

		// Directory on system but not declared as such
		if ($this instanceof Dir && file_exists($this->path) && !is_dir($this->path)) {
			throw new NotADirectoryException($this);
		}
	}

	/**
	 * Check if path is readable.
	 *
	 * @return bool True if path is readable.
	 * @throws NotADirectoryException
	 * @throws NotAFileException
	 */
	public function isReadable() {
		$this->typeMatches();
		return is_readable($this->getPath());
	}

	/** @return string The path. */
	public function __toString() {
		return $this->getPath();
	}

	/**
	 * Check if the path exists on the system.
	 *
	 * @return bool True if found on filesystem.
	 */
	abstract public function exists();

	/**
	 * Check if path is writable.
	 *
	 * @return bool True if path is writable.
	 */
	abstract public function isWritable();

	/**
	 * Touch a path, or create it if not exists.
	 *
	 * @return Path Itself, for chaining purposes.
	 */
	abstract public function touch();

	/**
	 * Delete a path.
	 */
	abstract public function delete();

}
