<?php

class NitroFiles {
	
	private $root;
	private $start;
	private $batch;
	private $ext;
	private $result;
	private $total_size;
	private $rules;

	/*
		Init and clean variables

		$config = array(
			'root' => './', 						// (Optional) Some directory - regarded as root of the search. Resolved to the real directory on the server. If omitted the directory of the executing file __FILE__ will be used.
			'start' => 'dir_1/dir_2/file_1.txt', 	// (Optional) Some file or directory which serves to calculate the start point of the iteration. Note that results will be all files AFTER this file. The path of this start file is relative to the root dir. If omitted or not existing or is a dir, the returning of the files will begin from the root dir.
			'batch' => 2000, 						// (Optional) How many files should we return. Default value is 0, which means "all files"
			'ext' => array('jpg', 'jpeg', 'css'), 	// (Optional) Extensions to filter files
		);	
	*/

	public function __construct($config = array()) {
		// Init root
		$this->root = !empty($config['root']) ? realpath($config['root']) : dirname(__FILE__);

		// Init ext
		$this->ext = !empty($config['ext']) && is_array($config['ext']) ? $config['ext'] : array();

		// Init start
		$start_rel_path = !empty($config['start']) ? ltrim($config['start'], DIRECTORY_SEPARATOR) : '';
		$start_path = realpath($this->root . DIRECTORY_SEPARATOR . $start_rel_path);
		$this->start = !is_file($start_path) ? $this->root . DIRECTORY_SEPARATOR : $start_path;
		
		// Init batch
		$this->batch = is_numeric($config['batch']) ? (int)$config['batch'] : 0;

		// Init result
		$this->result = array();

		// Init total_size
		$this->total_size = 0;

		// Init rules
		$this->rules = !empty($config['rules']) && is_array($config['rules']) ? $config['rules'] : array();
	}

	public function clearStatCache() {
		clearstatcache();
	}

	public function find() {
		$this->findRec($this->root, $this->start);

		return $this->getResult();
	}

	public function getResult() {
		if (!$this->resultInBounds()) {
			$this->result = array_slice($this->result, 0, $this->batch);
		}

		return $this->result;
	}

	public function totalSize() {
		$this->totalSizeRec($this->root, $this->start);

		return $this->getTotalSize();
	}

	public function getTotalSize() {
		return $this->total_size;
	}

	// Magic happens here
	private function findRec($var_root, $var_start) {
		$scope = $this->getScope($var_root, $var_start); // an array with folders and files after the root

		do {
			$offset = array_pop($scope);

			$rel_root = $var_root . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $scope);

			$matches = $this->items($rel_root, $offset); // Match the items in the current scope

			foreach ($matches as $match) {
				$item = $match;

				if ($this->isValidFile($item)) {
					$this->addFileToResult($item);
				} else if (is_dir($item)) {
					$this->findRec($item, $item);
				}
			}

		} while (!empty($scope) && $this->resultInBounds());
	}

	private function totalSizeRec($var_root, $var_start) {
		$scope = $this->getScope($var_root, $var_start); // an array with folders and files after the root

		do {
			$offset = array_pop($scope);

			$rel_root = $var_root . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $scope);

			$matches = $this->items($rel_root, $offset); // Match the items in the current scope

			foreach ($matches as $match) {
				$item = $match;

				if ($this->isValidFile($item)) {
					$this->addFilesizeToTotalSize($item);
				} else if (is_dir($item)) {
					$this->totalSizeRec($item, $item);
				}
			}

		} while (!empty($scope));
	}

	private function addFileToResult($item) { 
		if ($this->resultInBounds()) {
			$this->result[] = array(
				'full_path' => $item,
				'rel_path' => implode(DIRECTORY_SEPARATOR, $this->getScope($this->root, $item)),
				'size' => filesize($item)
			);
		}
	}

	private function addFilesizeToTotalSize($item) {
		$this->total_size = $this->getTotalSize() + filesize($item);
	}

	private function isValidFile($item) {
		if (!is_file($item)) {
			return false;
		}

		$extension = strtolower(pathinfo($item, PATHINFO_EXTENSION));
		$rel_item = implode(DIRECTORY_SEPARATOR, $this->getScope($this->root, $item));

		if (empty($extension)) {
			return false;
		} else {
			$validates_rules = true;

			if (!empty($this->rules)) {
				foreach ($this->rules as $rule) {
					if (
						(!empty($rule['ext']) && is_array($rule['ext']) && in_array($extension, $rule['ext'])) ||
						empty($rule['ext'])
					) {
						if (isset($rule['match']) && $rule['match'] == false) {
							$validates_rule = !preg_match($rule['rule'], $rel_item);
						} else {
							$validates_rule = preg_match($rule['rule'], $rel_item);
						}

						$validates_rules = $validates_rules && $validates_rule;
					}
				}
			}

			return $validates_rules && (!empty($this->ext) ? in_array($extension, $this->ext) : true);
		}
	}

	private function resultInBounds() {
		if ($this->batch == 0 || count($this->result) < $this->batch) {
			return true;
		} else {
			$this->result = array_slice($this->result, 0, $this->batch);
			return false;
		}
	}

	private function getScope($var_root, $var_start) {
		$start = explode(DIRECTORY_SEPARATOR, $var_start);
		$root = explode(DIRECTORY_SEPARATOR, $var_root);

		return array_slice($start, count($root));
	}

	private function items($path, $offset = '') {
		$path = realpath($path) . DIRECTORY_SEPARATOR;
		$values = array_filter(scandir($path), array($this, 'filterDots'));

		sort($values);

		foreach ($values as $key => $value) {
			if ($value <= $offset) unset($values[$key]);
		}

		array_walk($values, array($this, 'applyPath'), $path);

		return $values;
	}

	private function filterDots($var) {
		return !in_array($var, array('.', '..'));
	}

	private function applyPath(&$var, $index, $path) {
		$var = $path . $var;
	}
}