<?php namespace Intellex\Filesystem;

use Intellex\Filesystem\Exception\PathExistsException;
use Intellex\Filesystem\Exception\PathNotReadableException;
use Intellex\Filesystem\Exception\PathNotWritableException;
use Mimey\MimeTypes;

/**
 * Class File
 * Represents a file on the filesystem.
 *
 * @package Intellex\Filesystem
 */
class File extends Path {

	/** @var int The full name of the file, with the extension. */
	private $basename;

	/** @var int The name of the file, without the extension. */
	private $filename;

	/** @var int The file size, in bytes. */
	private $size;

	/** @var string|null The extension. */
	private $extension;

	/** @var string MIME type of the file. */
	private $mimetype;

	/** @var string The extension as parsed from the mime type of a file. */
	private $mimeExtension;

	/** @inheritdoc */
	public function init($path) {
		parent::init($path);

		// Clear all known info
		$this->basename = null;
		$this->filename = null;
		$this->size = null;
		$this->extension = null;
		$this->mimetype = null;
		$this->mimeExtension = null;
		return $this;
	}

	/**
	 * Read from the file.
	 *
	 * @return mixed The content of the file.
	 * @throws Exception\NotADirectoryException
	 * @throws Exception\NotAFileException
	 * @throws PathNotReadableException
	 */
	public function read() {

		// Make sure it is readable
		if (!$this->isReadable()) {
			throw new PathNotReadableException($this);
		}

		return file_get_contents($this->getPath());
	}

	/**
	 * Write arbitrary data to the file.
	 *
	 * @param mixed $data   The data to write.
	 * @param bool  $append True to append to existing data, false to overwrite.
	 *
	 * @throws PathNotWritableException
	 */
	public function write($data, $append = false) {
		$this->touch();
		if (!$this->isWritable()) {
			throw new PathNotWritableException($this);
		}

		file_put_contents($this->getPath(), $data, $append ? FILE_APPEND : 0);
	}

	/**
	 * Copy to another path.
	 *
	 * @param File|string $destination The destination directory.
	 * @param bool        $overwrite   True the existing file.
	 *
	 * @return Path Itself, for chaining purposes.
	 * @throws Exception\NotADirectoryException
	 * @throws Exception\NotAFileException
	 * @throws PathExistsException
	 * @throws PathNotReadableException
	 * @throws PathNotWritableException
	 */
	public function copyTo($destination, $overwrite = false) {
		if (is_string($destination)) {
			$destination = new File($destination);
		}

		// If file exists
		if ($destination->exists() && !$overwrite) {
			throw new PathExistsException($destination);
		}

		// Validate source
		if (!$this->isReadable()) {
			throw new PathNotReadableException($this);
		}

		// Validate destination
		$destination->getParent()->touch();
		if (!$destination->isWritable()) {
			throw new PathNotWritableException($destination);
		}

		// Write
		$destination->write($this->read());

		// Reinitialize the file
		$this->init($destination->getPath());
		return $this;
	}

	/**
	 * Move to another path.
	 *
	 * @param File|string $destination The destination directory.
	 *
	 * @return Path Itself, for chaining purposes.
	 * @throws Exception\NotADirectoryException
	 * @throws Exception\NotAFileException
	 * @throws PathExistsException
	 * @throws PathNotReadableException
	 * @throws PathNotWritableException
	 */
	public function moveTo($destination) {
		if (is_string($destination)) {
			$destination = new File($destination);
		}

		// If file exists
		if ($destination->exists()) {
			throw new PathExistsException($destination);
		}

		// Validate source
		if (!$this->isReadable()) {
			throw new PathNotReadableException($this);
		}
		if (!$this->isWritable()) {
			throw new PathNotWritableException($this);
		}

		// Validate destination
		$destination->getParent()->touch();
		if (!$destination->isWritable()) {
			throw new PathNotWritableException($destination);
		}

		// Move the file
		rename($this->getPath(), $destination->getPath());

		// Reinitialize the file
		$this->init($destination->getPath());
		return $this;
	}

	/**
	 * Initialize the file info.
	 *
	 * @return $this
	 */
	private function load() {

		// Load only once
		if ($this->basename === null || $this->filename === null || $this->extension === null) {
			$info = pathinfo($this->getPath());
			$this->basename = $info['basename'];
			$this->filename = $info['filename'];
			$this->extension = key_exists('extension', $info) ? $info['extension'] : null;
		}

		// Only load additional info for existing files
		if ($this->exists() && ($this->mimetype === null || $this->mimeExtension === null || empty($this->size))) {
			clearstatcache();
			$this->size = filesize($this->getPath());
			$this->mimetype = mime_content_type($this->getPath());
			$this->mimeExtension = static::validateMimeExtension($this->mimetype, $this->extension);
		}

		return $this;
	}

	/**
	 * Make sure that the MIME type checked did not faulty concluded a different extension.
	 *
	 * @param string $mimeType  The MIME type of the file.
	 * @param string $extension The named extension of the file.
	 *
	 * @return string The extension to use.
	 */
	public static function validateMimeExtension($mimeType, $extension) {
		$mimeExtension = (new MimeTypes)->getExtension($mimeType);

		// Exceptions
		$exceptions = [
			'svg' => [ 'html' ]
		];

		// Allow the list in exceptions
		$ext = strtolower($extension);
		if (key_exists($ext, $exceptions)) {

			// If exception is found, return the original extension
			if (in_array(strtolower($mimeExtension), $exceptions[$ext])) {
				return $extension;
			}
		}

		// Return the found MIME extension
		return $mimeExtension;
	}

	/**
	 * Get the name of the file.
	 *
	 * @return string The name of the path.
	 */
	public function getName() {
		return $this->load()->basename;
	}

	/** @return string The name of the file, without the extension. */
	public function getFilename() {
		return $this->load()->filename;
	}

	/** @return int The file size, in bytes. */
	public function getSize() {
		return $this->load()->size;
	}

	/**
	 * Get the extension for the file.
	 *
	 * @param bool $fromMimeType False to get extension from filename, true to try to parse it from
	 *                           the detected mime type.
	 *
	 * @return string|null The extension.
	 */
	public function getExtension($fromMimeType = false) {
		$var = $fromMimeType ? 'mimeExtension' : 'extension';
		return $this->load()->$var;
	}

	/** @return string Mimetype of the file. */
	public function getMimetype() {
		return $this->load()->mimetype;
	}

	/** @inheritdoc */
	public function exists() {
		$this->typeMatches();
		return is_file($this->getPath());
	}

	/** @inheritdoc */
	public function isWritable() {
		$this->typeMatches();
		return is_writable($this->getPath()) || (!$this->exists() && $this->getParent()->isWritable());
	}

	/** @inheritdoc */
	public function touch() {
		$this->getParent()->touch();
		touch($this->getPath());
	}

	/** @inheritdoc */
	public function delete() {
		if ($this->exists() && $this->isWritable()) {
			unlink($this->getPath());
		} else {
			throw new PathNotWritableException($this);
		}
	}

}
