<?php
// last edit: 02.11.2018
/*

Краткое содерщание функционала:

	redirect([$url])
	route([$path_or_name, $method, $params])
	is_function($callback)

	

	Route::$function_finich     // as function
	Route::getUserURL()
	Route::getUserMethod()
	Route::getUserIP([$default_value])

	Route::def([$callback])
	Route::match($method, $urls [,$callback])
	Route::any($url [,$callback])
	Route::get($url [,$callback])
	Route::post($url [,$callback])
	Route::put($url [,$callback])
	Route::delete($url [,$callback])
	Route::patch($url [,$callback])
	Route::options($url [,$callback])

	Route::auto_route()
	Route::redirect([$url])
	Route::findUrl($name [,$methods])
	Route::routes([$path_or_name, $method, $params])

	<objectUri> -> name($set_name)
	<objectUri> -> isName($name)
	<objectUri> -> getUrl()
	<objectUri> -> cmp($Uri)
	<objectUri> -> getParams()
*/

trait UriFunctions{
	/**
	* Наименование ссылки для роутинга
	* @var string
	*/
	public $nameRoute;
	

	/**
	* Установка наименования маршрута (текущего объекта URL-строки)
	* @param string $name
	* @return $this
	*/
	function name($name){
		$this->nameRoute = $name;
		return $this;
	}

	/**
	* Возвращает TRUE если искомое имя совпадает с именем данного объекта
	* @param string $name
	* @return boolean
	*/
	function isName($name){
		return $this->nameRoute == $name;
	}

}


class RouteUri{ // Класс объектов маршрута или правил проверки URL-строки
	use UriFunctions;

	/**
	* разбитый массив URL-строки, для сравнения
	* @var array
	*/
	private $url;

	/**
	* массим параметров из URL-строки, получаемый согласно описанному правилу
	* @var array
	*/
	private $params;

	private $psParams = 999;
	private $isParams = false;
	private $isManyDir = false;

	function __construct($url){
		$url = explode('?', $url);
		$this->url = explode('/', $url[0]);
		$this->psParams = count($this->url);
		if(isset($url[1])){
			$last_str = $this->url[count($this->url)-1];
			if($url[1] == '}' && $last_str != '' && $last_str[strlen($last_str)-1] == '{'){
				$this->isParams = true;
				$this->url[count($this->url)-1] = substr($last_str, 0, strlen($last_str)-1);
			} else foreach(explode('&', $url[1]) as $key_value){
				$key_value = explode('=',$key_value);
				$this->url[] = $key_value[0];
				if(isset($key_value[1]))
					$this->url[] = $key_value[1];
			}
		}

		$this->params = [];

		foreach($this->url as $num => $urlDir)
			if($urlDir && $urlDir[0] == '{' && (substr($urlDir,-1) == '}' || substr($urlDir,-2) == '}{')){
				$i = strrpos($urlDir, '}');
				$key = substr($urlDir, 1, $i-1);
				$this->params[$key] = '';
				$this->params[$num] = $key;
			}elseif($urlDir == '**')
				$this->isManyDir = true;

		if($this->isManyDir)
			$this->params['_dirs_'] = [];
		$this->nameRoute = '';
	}

	function getUrl(){
		return $this->url;
	}


	/**
	* Сравнивает переданную URL-строку или Объект URL-маршрута с текущим правилом. Возвращает TRUE в случае успеха
	* @param string|object $url
	* @return boolean
	*/
	function cmp($url){
		if(is_string($url))
			$url = new RouteUrl($url);

		//var_dump($this->url);echo '<br>';
		//var_dump($url->url);echo '<hr>';
		if(($this->isParams && $this->psParams == $url->psParams) || (!$this->isParams && count($this->url) == count($url->url)) || $this->isManyDir){
			$urlList = $this->isParams ? array_slice($url->url, 0, $url->psParams) : $url->url;
			$temp = -1;
			foreach($urlList as $num => $urlDir)
				if(isset($this->params[$num]))
					$this->params[$this->params[$num]] = $urlDir;
				elseif($this->url[$num] == '**'){
					$temp = $num;
					break;
				}elseif(strtolower($this->url[$num]) != strtolower($urlDir))
					return false;

			if($temp >= 0)
				$this->params['_dirs_'] = $this->isParams ? array_slice($url->url, $temp, $url->psParams - $temp) : array_slice($url->url, $temp);

			if($this->isParams){
				$urlList = array_slice($url->url, $url->psParams);
				$isKey = true;
				$key = '';
				while(count($urlList) > 0){
					$temp = array_shift($urlList);
					if($isKey)
						$key = $temp;
					else 
						$this->params[$key] = $temp;
					$isKey = !$isKey;
				}
				if(!$isKey)
					$this->params[$key] = '';
			}
			return true;
		} else return false;
	}
	
	/**
	* Возвращает полученный набор параметров, выдранных из запроса при сравнении с текущим правилом.
	* @return array
	*/
	function getParams(){
		$temp = [];
		foreach ($this->params as $param => $value) 
			if(!is_numeric($param))
				$temp[$param] = $value;

		return $temp;
	}
}

class multiRouteUri{
	use UriFunctions;

	private $urls;

	/**
	* массим параметров из URL-строки, получаемый согласно описанному правилу
	* @var array
	*/
	private $params;

	function __construct($urls){
		$this->urls = array_map(function($url){ return new RouteUri($url);}, $urls);
		$this->nameRoute = '';
	}

	function cmp($url_find){
		if(is_string($url_find))
			$url_find = new RouteUrl($url_find);

		//var_dump($url_find->getUrl());echo '<br>';
		//var_dump(array_map(function($url){return $url->getUrl();},$this->urls));
		
		foreach ($this->urls as $num => $url) 
			if($url->cmp($url_find)){
				$this->params = $url->getParams();
				return true;
			}

		return false;
	}

	/**
	* Возвращает полученный набор параметров, выдранных из запроса при сравнении с текущим правилом.
	* @return array
	*/
	function getParams(){
		return isset($this->params) ? $this->params : [];
	}
}

class Route { // Класс роутинга

	/**
	* Переменная для настройки чтения URL-строки из окружения среды.
	* @var string
	*/
	static $read_url = 'HTTP_X_ORIGINAL_URL';
	static $read_url2 = 'REQUEST_URI';

	/**
	* Базовый путь к контроллерам-файлом (если таковые будут), в котором они должны находиться
	* @var string
	*/
	static $basePath = './';

	/**
	* Набор правил, по которым следует осуществлять роутинг
	* @var array
	*/
	static $listUrls = ['get'=>[], 'post'=>[], 'put'=>[], 'delete'=>[], 'patch'=>[], 'options'=>[] ];

	/**
	* @var string $original_method
	* Переменная, которая сохраняет оригинальный метод запроса браузера.
	*
	* @var array $hash_obj
	* Данная переменная хранит все созданные экземпляры контроллеров, для вызова их нестатических методов
	*/
	private static $original_method = '', $hash_obj = [], $origin_url, $defRoute = false;


	static $function_finish = false;

	static function getUserURL(){
		return self::$origin_url;
	}

	static function getUserMethod(){
		return self::$original_method;
	}

	static function getUserIP($default = 'NO IP'){
		$all_ips = [
			isset($_SERVER["HTTP_X_FORWARDED_FOR"]) ? $_SERVER["HTTP_X_FORWARDED_FOR"] : getenv('HTTP_X_FORWARDED_FOR'),
	    	isset($_SERVER["HTTP_CLIENT_IP"]) ? $_SERVER["HTTP_CLIENT_IP"] : getenv('HTTP_CLIENT_IP'),
	    	isset($_SERVER["HTTP_CF_CONNECTING_IP"]) ? $_SERVER["HTTP_CF_CONNECTING_IP"] : getenv('HTTP_CF_CONNECTING_IP'),
	    	isset($_SERVER["REMOTE_ADDR"]) ? $_SERVER["REMOTE_ADDR"] : getenv('REMOTE_ADDR')
	    ];

	    foreach ($all_ips as $ip) 
	        if ($ip = filter_var($ip, FILTER_VALIDATE_IP))
	            break;
	    return $ip ?: $default;
	}

	static function def($callback = null){
		self::$defRoute = self::__method_detected(false, $callback);
		return static::class;
	}

	static private function __method_detected($urls, $callback = null){
		if($urls !== false){
			if(is_array($urls))
				$r_url = new multiRouteUri($urls);
			else $r_url = new RouteUri($urls);
		}else $r_url = false;

		$is_str = is_string($callback);

		if(!isset($callback))
			return [$r_url, 'default'];
		elseif($is_str && strpos($callback, '::')){
			$arr2 = explode('::', $callback);
			return [$r_url, 'classMethod', $arr2[0], $arr2[1]];
		}elseif($is_str && strpos($callback, '@')){
			$arr2 = explode('@', $callback);
			return [$r_url, 'objectMethod', $arr2[0], $arr2[1]];
		}elseif(is_function($callback))
			return [$r_url, 'function', $callback];
		else return [$r_url, 'include', $callback];
	}

	/**
	* Позволяет настроить правило для нескольких методов вызова.
	* @param array|string    $method    метод или методы запроса, которые могут обрабатываться по указанному URL
	* @param string          $url       правило URL-запроса, по которому будет вызван указанный контроллер
	* @param function|string $callback  функция, пара "class::method" или "object@method", или наименование файла, которые являются контроллерами
	* @param object
	*/
	static function match($method, $urls, $callback = null){
		$temp = self::__method_detected($urls, $callback);

		if(is_array($method))
			foreach ($method as $mtd) 
				self::$listUrls[$mtd][] = $temp;
		else self::$listUrls[$method][] = $temp;

		return $temp[0];
	}

	/**
	* Позволяет установить правило для всех методов вызова.
	* @param string          $url       правило URL-запроса, по которому будет вызван указанный контроллер
	* @param function|string $callback  функция, пара "class::method" или "object@method", или наименование файла, которые являются контроллерами
	* @param object
	*/
	static function any($url, $callback = null){
		return self::match(array_keys(self::$listUrls), $url, $callback);
	}

	/**
	* Позволяет установить правило для метода вызова GET.
	* @param string          $url       правило URL-запроса, по которому будет вызван указанный контроллер
	* @param function|string $callback  функция, пара "class::method" или "object@method", или наименование файла, которые являются контроллерами
	* @param object
	*/
	static function get($url, $callback = null){
		return self::match('get', $url, $callback);
	}

	/**
	* Позволяет установить правило для метода вызова POST.
	* @param string          $url       правило URL-запроса, по которому будет вызван указанный контроллер
	* @param function|string $callback  функция, пара "class::method" или "object@method", или наименование файла, которые являются контроллерами
	* @param object
	*/
	static function post($url, $callback = null){
		return self::match('post', $url, $callback);
	}

	/**
	* Позволяет установить правило для дополнительного метода вызова PUT.
	* @param string          $url       правило URL-запроса, по которому будет вызван указанный контроллер
	* @param function|string $callback  функция, пара "class::method" или "object@method", или наименование файла, которые являются контроллерами
	* @param object
	*/
	static function put($url, $callback = null){
		return self::match('put', $url, $callback);
	}

	/**
	* Позволяет установить правило для дополнительного метода вызова DELETE.
	* @param string          $url       правило URL-запроса, по которому будет вызван указанный контроллер
	* @param function|string $callback  функция, пара "class::method" или "object@method", или наименование файла, которые являются контроллерами
	* @param object
	*/
	static function delete($url, $callback = null){
		return self::match('delete', $url, $callback);
	}

	/**
	* Позволяет установить правило для дополнительного метода вызова PATCH.
	* @param string          $url       правило URL-запроса, по которому будет вызван указанный контроллер
	* @param function|string $callback  функция, пара "class::method" или "object@method", или наименование файла, которые являются контроллерами
	* @param object
	*/
	static function patch($url, $callback = null){
		return self::match('patch', $url, $callback);
	}

	/**
	* Позволяет установить правило для дополнительного метода вызова OPTIONS.
	* @param string          $url       правило URL-запроса, по которому будет вызван указанный контроллер
	* @param function|string $callback  функция, пара "class::method" или "object@method", или наименование файла, которые являются контроллерами
	* @param object
	*/
	static function options($url, $callback = null){
		return self::match('options', $url, $callback);
	}


	/**
	* Вспомогательный внутренний метод, который осуществляет вызов контроллера.
	* @param array $optUrl  массив настроек правила вызова контроллера, где индексы:
	*	0:object          - объект-правило, которое вернуло TRUE при сравнении соответствия правилу запроса
	*   1:string          - тип контроллера
	*   2:function|string - функция, класс или файл контроллера
	*  [3:string]         - наименование статического метода класса или метода экземпляра класса контроллера
	* @param array $param   набор параметров, которые необходимо передать в контроллер
	* @return boolean
	*/
	private static function __run_route($optUrl,$params){
		$temp = $optUrl;
		switch($optUrl[1]){
			case 'function':    
				$res = $optUrl[2];
				$res = $res($params);
				break;
			case 'classMethod': 
				if(method_exists($optUrl[2],$optUrl[3]))
					$res = $optUrl[2]::{$optUrl[3]}($params);
				else $res = false;
				break;
			case 'objectMethod':
				if(!isset($hash_obj[$optUrl[2]])){
					$obj = new $optUrl[2]();
					$hash_obj[$optUrl[2]] = $obj;
				} else $obj = $hash_obj[$optUrl[2]];
				if(method_exists($obj,$optUrl[3]))
					$res = $obj->{$optUrl[3]}($params);
				else $res = false;
				break;
			case 'include': $res = include(self::$basePath . $optUrl[2] . '.php'); break;

			case 'default': defView($params); $res = true; break;
		}
		return $res === false ? false : true;
	}

	/**
	* Вспомогательный внутренний метод для поиска и применения маршрутизации
	* @param object|string $url     URL-запроса, по которому будет производиться поиск
	* @param string        $method  метод вызова, по которому необходимо производить поиск
	* @param array         $params  параметры, которые необходимо передать контроллеру (дополняют параметры которые были получены из URL)
	* @return boolean
	*/
	private static function __route($url, $method, $params = []){
		if($method != 'get'){
			header('Cache-Control: no-store, no-cache, must-revalidate');
			header("Expires: " . date("r"));
		}
		
		foreach(self::$listUrls[$method] as $optUrl)
			if($optUrl[0]->cmp($url))
				return self::__run_route($optUrl, array_merge($params, $optUrl[0]->getParams()));

		if(self::$defRoute)
			return self::__run_route(self::$defRoute, $params);
		return false;
	}


	/**
	* Делает начальный поиск по текущему URL-запросу и метода вызова
	* @return boolean
	*/
	static function auto_route(){
		if(isset($_SERVER[self::$read_url]))
			$url = $_SERVER[self::$read_url];
		elseif(isset($_SERVER[self::$read_url2]))
			$url = $_SERVER[self::$read_url2];
		else $url = '/';
		self::$origin_url = $url;
		
		$url = new RouteUri($url);

		
		if($_SERVER['REQUEST_METHOD'] == 'GET')
			$key = 'get';
		elseif($_SERVER['REQUEST_METHOD'] == 'POST'){
			if(isset($_REQUEST['_method']))
				$key = strtolower($_REQUEST['_method']);
			else $key = 'post';
		}else $key = strtolower($_SERVER['REQUEST_METHOD']);

		self::$original_method = $key;

		$res = self::__route($url, $key);

		if(is_function(self::$function_finish)){
			$func =  self::$function_finish;
			$func();
		}

		return $res;
	}

	/**
	* Позволяет сообщить браузеру, что необходимо перейти по указанной ссылки. Данный метод останавливает выполнение PHP-скрипта.
	* @param string $path  путь от доменного имени (или внешний адрес целиком), на который необходимо перенаправить браузер
	*/
	static function redirect($path = '/'){
		if(is_function(self::$function_finish)){
			$func = self::$function_finish;
			$func();
		}
		header("Location: $path");
		exit();
	}

	/**
	* Производит поиск правила контроллера по его имени
	* @param string       $name    наименование искомого правила
	* @param string|array $methods метод вызова или массив методов вызова по которому необходимо искать, или 'all', если нужно искать по всем.
	* @return boolean
	*/
	private static function findUrl($name, $methods = 'all'){
		if($methods == 'all')
			$methods = array_keys(self::$listUrls);
		else $methods = [$methods];

		foreach ($methods as $method)
			foreach (self::$listUrls[$method] as $optUrl) 
				if($optUrl[0]->isName($name))
					return $optUrl;
		return false;
	}

	/**
	* Делает перенаправление адреса, без сообщения браузера и перезапуска скрипта.
	* @param string       $path_name   имя правила или адрес, по которому это правило будет искаться
	* @param string|false $method      метод по которому будет производиться поиск (по умолчанию 'all'). Так же можно опустить, передав вместо него следующий параметр
	* @param array        $params      параметры, которые необходимо передать контроллеру (дополняют параметры которые были получены из URL)
	* @return boolean
	*/
	static function routes($path_name = '/', $method = false, $params = []){
		
		if(is_array($method)){
			$params = $method;
			$method = false;
		}

		
		$method = $method ? strtolower($method) : self::$original_method;


		if(strpos($path_name,'/')===0){
			$url = new RouteUri($path_name);
			return self::__route($url, $method, $params);
		}

		$url = self::findUrl($path_name, $method ?: 'all');
		if(!$url) return false;

		return self::__run_route($url, $params);
	}

}


/**
* аналог вызова Route::redirect($path)
*/
function redirect($path = '/'){
	Route::redirect($path);
}

/**
* аналог вызова Route::routes($path_name, $method, $params)
*/
function route($path_name = '/', $method = false, $params = []){
	return Route::routes($path_name, $method, $params);
}


function is_function($callback){
	return isset($callback) && ((is_object($callback) && $callback instanceof Closure) || is_callable($callback));
}