<?php

/*

	Copyright (c) 2009-2023 F3::Factory/Bong Cosca, All rights reserved.

	This file is part of the Fat-Free Framework (http://fatfreeframework.com).

	This is free software: you can redistribute it and/or modify it under the
	terms of the GNU General Public License as published by the Free Software
	Foundation, either version 3 of the License, or later.

	Fat-Free Framework is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
	General Public License for more details.

	You should have received a copy of the GNU General Public License along
	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.

*/

//! Factory class for single-instance objects
abstract class Prefab {

	/**
	*	Return class instance
	*	@return static
	**/
	static function instance() {
		if (!Registry::exists($class=get_called_class())) {
			$ref=new ReflectionClass($class);
			$args=func_get_args();
			Registry::set($class,
				$args?$ref->newinstanceargs($args):new $class);
		}
		return Registry::get($class);
	}

}
/**
 * @property string ALIAS
 * @property array ALIASES
 * @property string|array AUTOLOAD
 * @property bool|string CACHE
 * @property bool CASELESS
 * @property callable|\Prefab|\Psr\Container\ContainerInterface CONTAINER
 * @property array COOKIE
 * @property array GET
 * @property array POST
 * @property array REQUEST
 * @property array SESSION
 * @property array FILES
 * @property array SERVER
 * @property array ENV
 * @property array CORS
 * @property int DEBUG
 * @property array DIACRITICS
 * @property string DNSBL
 * @property array EMOJI
 * @property string ENCODING
 * @property bool ESCAPE
 * @property string EXEMPT
 * @property object EXCEPTION
 * @property string FALLBACK
 * @property array FORMATS
 * @property string FRAGMENT
 * @property bool HALT
 * @property bool HIGHLIGHT
 * @property array JAR
 * @property string LANGUAGE
 * @property string LOCALES
 * @property string|array LOGGABLE
 * @property string LOGS
 * @property mixed ONERROR
 * @property mixed ONREROUTE
 * @property string|null PACKAGE
 * @property array PARAMS
 * @property string PLUGINS
 * @property string PREFIX
 * @property string PREMAP
 * @property bool QUIET
 * @property bool RAW
 * @property array ROUTES
 * @property string SEED
 * @property string SERIALIZER
 * @property string TEMP
 * @property float TIME
 * @property bool REROUTE_TRAILING_SLASH
 * @property string TZ
 * @property string UI
 * @property callback UNLOAD
 * @property string UPLOADS
 * @property string URI
 * @property string VERB
 * @property string VERSION
 * @property string|null XFRAME
 * @property-read string AGENT
 * @property-read bool AJAX
 * @property-read string BASE
 * @property-read string BODY
 * @property-read bool CLI
 * @property-read array ERROR
 * @property-read array HEADERS
 * @property-read string HOST
 * @property-read string IP
 * @property-read string PATH
 * @property-read string PATTERN
 * @property-read int PORT
 * @property-read string QUERY
 * @property-read string REALM
 * @property-read string RESPONSE
 * @property-read string ROOT
 * @property-read string SCHEME
 */
//! Base structure
final class Base extends Prefab implements ArrayAccess {

	//@{ Framework details
	const
		PACKAGE='Fat-Free Framework',
		VERSION='3.9.1-Release';
	//@}

	//@{ HTTP status codes (RFC 2616)
	const
		HTTP_100='Continue',
		HTTP_101='Switching Protocols',
		HTTP_103='Early Hints',
		HTTP_200='OK',
		HTTP_201='Created',
		HTTP_202='Accepted',
		HTTP_203='Non-Authorative Information',
		HTTP_204='No Content',
		HTTP_205='Reset Content',
		HTTP_206='Partial Content',
		HTTP_300='Multiple Choices',
		HTTP_301='Moved Permanently',
		HTTP_302='Found',
		HTTP_303='See Other',
		HTTP_304='Not Modified',
		HTTP_305='Use Proxy',
		HTTP_307='Temporary Redirect',
		HTTP_308='Permanent Redirect',
		HTTP_400='Bad Request',
		HTTP_401='Unauthorized',
		HTTP_402='Payment Required',
		HTTP_403='Forbidden',
		HTTP_404='Not Found',
		HTTP_405='Method Not Allowed',
		HTTP_406='Not Acceptable',
		HTTP_407='Proxy Authentication Required',
		HTTP_408='Request Timeout',
		HTTP_409='Conflict',
		HTTP_410='Gone',
		HTTP_411='Length Required',
		HTTP_412='Precondition Failed',
		HTTP_413='Request Entity Too Large',
		HTTP_414='Request-URI Too Long',
		HTTP_415='Unsupported Media Type',
		HTTP_416='Requested Range Not Satisfiable',
		HTTP_417='Expectation Failed',
		HTTP_421='Misdirected Request',
		HTTP_422='Unprocessable Entity',
		HTTP_423='Locked',
		HTTP_429='Too Many Requests',
		HTTP_451='Unavailable For Legal Reasons',
		HTTP_500='Internal Server Error',
		HTTP_501='Not Implemented',
		HTTP_502='Bad Gateway',
		HTTP_503='Service Unavailable',
		HTTP_504='Gateway Timeout',
		HTTP_505='HTTP Version Not Supported',
		HTTP_507='Insufficient Storage',
		HTTP_511='Network Authentication Required';
	//@}

	const
		//! Mapped PHP globals
		GLOBALS='GET|POST|COOKIE|REQUEST|SESSION|FILES|SERVER|ENV',
		//! HTTP verbs
		VERBS='GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|OPTIONS',
		//! Default directory permissions
		MODE=0755,
		//! Syntax highlighting stylesheet
		CSS='code.css';

	//@{ Request types
	const
		REQ_SYNC=1,
		REQ_AJAX=2,
		REQ_CLI=4;
	//@}

	//@{ Error messages
	const
		E_Pattern='Invalid routing pattern: %s',
		E_Named='Named route does not exist: %s',
		E_Alias='Invalid named route alias: %s',
		E_Fatal='Fatal error: %s',
		E_Open='Unable to open %s',
		E_Routes='No routes specified',
		E_Class='Invalid class %s',
		E_Method='Invalid method %s',
		E_Hive='Invalid hive key %s';
	//@}

	private
		//! Globals
		$hive,
		//! Initial settings
		$init,
		//! Language lookup sequence
		$languages,
		//! Mutex locks
		$locks=[],
		//! Default fallback language
		$fallback='en';

	/**
	*	Sync PHP global with corresponding hive key
	*	@return array
	*	@param $key string
	**/
	function sync($key) {
		return $this->hive[$key]=&$GLOBALS['_'.$key];
	}

	/**
	*	Return the parts of specified hive key
	*	@return array
	*	@param $key string
	**/
	private function cut($key) {
		return preg_split('/\[\h*[\'"]?(.+?)[\'"]?\h*\]|(->)|\./',
			$key,-1,PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);
	}

	/**
	*	Replace tokenized URL with available token values
	*	@return string
	*	@param $url array|string
	*	@param $addParams boolean merge default PARAMS from hive into args
	*	@param $args array
	**/
	function build($url, $args=[], $addParams=TRUE) {
		if ($addParams)
			$args+=$this->recursive($this->hive['PARAMS'], function($val) {
				return implode('/', array_map('urlencode', explode('/', $val ?? '')));
			});
		if (is_array($url))
			foreach ($url as &$var) {
				$var=$this->build($var,$args, false);
				unset($var);
			}
		else {
			$i=0;
			$url=preg_replace_callback('/(\{)?@(\w+)(?(1)\})|(\*)/',
				function($match) use(&$i,$args) {
					if (isset($match[2]) &&
						array_key_exists($match[2],$args))
						return $args[$match[2]];
					if (isset($match[3]) &&
						array_key_exists($match[3],$args)) {
						if (!is_array($args[$match[3]]))
							return $args[$match[3]];
						++$i;
						return $args[$match[3]][$i-1];
					}
					return $match[0];
				},$url);
		}
		return $url;
	}

	/**
	*	Parse string containing key-value pairs
	*	@return array
	*	@param $str string
	**/
	function parse($str) {
		preg_match_all('/(\w+|\*)\h*=\h*(?:\[(.+?)\]|(.+?))(?=,|$)/',
			$str,$pairs,PREG_SET_ORDER);
		$out=[];
		foreach ($pairs as $pair)
			if ($pair[2]) {
				$out[$pair[1]]=[];
				foreach (explode(',',$pair[2]) as $val)
					array_push($out[$pair[1]],$val);
			}
			else
				$out[$pair[1]]=trim($pair[3]);
		return $out;
	}

	/**
	 * Cast string variable to PHP type or constant
	 * @param $val
	 * @return mixed
	 */
	function cast($val) {
		if ($val && preg_match('/^(?:0x[0-9a-f]+|0[0-7]+|0b[01]+)$/i',$val))
			return intval($val,0);
		if (is_numeric($val))
			return $val+0;
		$val=trim($val?:'');
		if (preg_match('/^\w+$/i',$val) && defined($val))
			return constant($val);
		return $val;
	}

	/**
	*	Convert JS-style token to PHP expression
	*	@return string
	*	@param $str string
	*	@param $evaluate bool compile expressions as well or only convert variable access
	**/
	function compile($str, $evaluate=TRUE) {
		return (!$evaluate)
			? preg_replace_callback(
				'/^@(\w+)((?:\..+|\[(?:(?:[^\[\]]*|(?R))*)\])*)/',
				function($expr) {
					$str='$'.$expr[1];
					if (isset($expr[2]))
						$str.=preg_replace_callback(
							'/\.([^.\[\]]+)|\[((?:[^\[\]\'"]*|(?R))*)\]/',
							function($sub) {
								$val=isset($sub[2]) ? $sub[2] : $sub[1];
								if (ctype_digit($val))
									$val=(int)$val;
								$out='['.$this->export($val).']';
								return $out;
							},
							$expr[2]
						);
					return $str;
				},
				$str
			)
			: preg_replace_callback(
			'/(?<!\w)@(\w+(?:(?:\->|::)\w+)?)'.
			'((?:\.\w+|\[(?:(?:[^\[\]]*|(?R))*)\]|(?:\->|::)\w+|\()*)/',
			function($expr) {
				$str='$'.$expr[1];
				if (isset($expr[2]))
					$str.=preg_replace_callback(
						'/\.(\w+)(\()?|\[((?:[^\[\]]*|(?R))*)\]/',
						function($sub) {
							if (empty($sub[2])) {
								if (ctype_digit($sub[1]))
									$sub[1]=(int)$sub[1];
								$out='['.
									(isset($sub[3])?
										$this->compile($sub[3]):
										$this->export($sub[1])).
								']';
							}
							else
								$out=function_exists($sub[1])?
									$sub[0]:
									('['.$this->export($sub[1]).']'.$sub[2]);
							return $out;
						},
						$expr[2]
					);
				return $str;
			},
			$str
		);
	}

	/**
	*	Get hive key reference/contents; Add non-existent hive keys,
	*	array elements, and object properties by default
	*	@return mixed
	*	@param $key string
	*	@param $add bool
	*	@param $var mixed
	**/
	function &ref($key,$add=TRUE,&$var=NULL) {
		$null=NULL;
		$parts=$this->cut($key);
		if ($parts[0]=='SESSION') {
			if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)
				session_start();
			$this->sync('SESSION');
		}
		elseif (!preg_match('/^\w+$/',$parts[0]))
            throw new \Exception(\sprintf(self::E_Hive,$this->stringify($key)));
		if (is_null($var)) {
			if ($add)
				$var=&$this->hive;
			else
				$var=$this->hive;
		}
		$obj=FALSE;
		foreach ($parts as $part)
			if ($part=='->')
				$obj=TRUE;
			elseif ($obj) {
				$obj=FALSE;
				if (!is_object($var))
					$var=new stdClass;
				if ($add || property_exists($var,$part))
					$var=&$var->$part;
				else {
					$var=&$null;
					break;
				}
			}
			else {
				if (!is_array($var))
					$var=[];
				if ($add || array_key_exists($part,$var))
					$var=&$var[$part];
				else {
					$var=&$null;
					break;
				}
			}
		return $var;
	}

	/**
	*	Return TRUE if hive key is set
	*	(or return timestamp and TTL if cached)
	*	@return bool
	*	@param $key string
	*	@param $val mixed
	**/
	function exists($key,&$val=NULL) {
		$val=$this->ref($key,FALSE);
		return isset($val)?
			TRUE:
			(Cache::instance()->exists($this->hash($key).'.var',$val)?:FALSE);
	}

	/**
	*	Return TRUE if hive key is empty and not cached
	*	@param $key string
	*	@param $val mixed
	*	@return bool
	**/
	function devoid($key,&$val=NULL) {
		$val=$this->ref($key,FALSE);
		return empty($val) &&
			(!Cache::instance()->exists($this->hash($key).'.var',$val) ||
				!$val);
	}

	/**
	*	Bind value to hive key
	*	@return mixed
	*	@param $key string
	*	@param $val mixed
	*	@param $ttl int
	**/
	function set($key,$val,$ttl=0) {
		$time=(int)$this->hive['TIME'];
		if (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) {
			$this->set('REQUEST'.$expr[2],$val);
			if ($expr[1]=='COOKIE') {
				$parts=$this->cut($key);
				$jar=$this->unserialize($this->serialize($this->hive['JAR']));
				unset($jar['lifetime']);
				if (version_compare(PHP_VERSION, '7.3.0') >= 0) {
					unset($jar['expire']);
					if (isset($_COOKIE[$parts[1]]))
						setcookie($parts[1],'',['expires'=>0]+$jar);
					if ($ttl)
						$jar['expires']=$time+$ttl;
					setcookie($parts[1],$val?:'',$jar);
				} else {
					unset($jar['samesite']);
					if (isset($_COOKIE[$parts[1]]))
						call_user_func_array('setcookie',
							array_merge([$parts[1],''],['expire'=>0]+$jar));
					if ($ttl)
						$jar['expire']=$time+$ttl;
					call_user_func_array('setcookie',[$parts[1],$val?:'']+$jar);
				}
				$_COOKIE[$parts[1]]=$val;
				return $val;
			}
		}
		else switch ($key) {
		case 'CACHE':
			$val=Cache::instance()->load($val);
			break;
		case 'ENCODING':
			ini_set('default_charset',$val);
			if (extension_loaded('mbstring'))
				mb_internal_encoding($val);
			break;
		case 'FALLBACK':
			$this->fallback=$val;
			$lang=$this->language($this->hive['LANGUAGE']);
		case 'LANGUAGE':
			if (!isset($lang))
				$val=$this->language($val);
			$lex=$this->lexicon($this->hive['LOCALES'],$ttl);
		case 'LOCALES':
			if (isset($lex) || $lex=$this->lexicon($val,$ttl))
				foreach ($lex as $dt=>$dd) {
					$ref=&$this->ref($this->hive['PREFIX'].$dt);
					$ref=$dd;
					unset($ref);
				}
			break;
		case 'TZ':
			date_default_timezone_set($val);
			break;
		}
		$ref=&$this->ref($key);
		$ref=$val;
		if (preg_match('/^JAR\b/',$key)) {
			if ($key=='JAR.lifetime')
				$this->set('JAR.expire',$val==0?0:
					(is_int($val)?$time+$val:strtotime($val)));
			else {
				if ($key=='JAR.expire')
					$this->hive['JAR']['lifetime']=max(0,$val-$time);
				$jar=$this->unserialize($this->serialize($this->hive['JAR']));
				unset($jar['expire']);
				if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)
					if (version_compare(PHP_VERSION, '7.3.0') >= 0)
						session_set_cookie_params($jar);
					else {
						unset($jar['samesite']);
						call_user_func_array('session_set_cookie_params',$jar);
					}
			}
		}
		if ($ttl)
			// Persist the key-value pair
			Cache::instance()->set($this->hash($key).'.var',$val,$ttl);
		return $ref;
	}

	/**
	*	Retrieve contents of hive key
	*	@return mixed
	*	@param $key string
	*	@param $args string|array
	**/
	function get($key,$args=NULL) {
		if (is_string($val=$this->ref($key,FALSE)) && !is_null($args))
			return call_user_func_array(
				[$this,'format'],
				array_merge([$val],is_array($args)?$args:[$args])
			);
		if (is_null($val)) {
			// Attempt to retrieve from cache
			if (Cache::instance()->exists($this->hash($key).'.var',$data))
				return $data;
		}
		return $val;
	}

	/**
	*	Unset hive key
	*	@param $key string
	**/
	function clear($key) {
		// Normalize array literal
		$cache=Cache::instance();
		$parts=$this->cut($key);
		if ($key=='CACHE')
			// Clear cache contents
			$cache->reset();
		elseif (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) {
			$this->clear('REQUEST'.$expr[2]);
			if ($expr[1]=='COOKIE') {
				$parts=$this->cut($key);
				$jar=$this->hive['JAR'];
				unset($jar['lifetime']);
				$jar['expire']=0;
				if (version_compare(PHP_VERSION, '7.3.0') >= 0) {
					$jar['expires']=$jar['expire'];
					unset($jar['expire']);
					setcookie($parts[1],'',$jar);
				} else {
					unset($jar['samesite']);
					call_user_func_array('setcookie',
						array_merge([$parts[1],''],$jar));
				}
				unset($_COOKIE[$parts[1]]);
			}
		}
		elseif ($parts[0]=='SESSION') {
			if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)
				session_start();
			if (empty($parts[1])) {
				// End session
				session_unset();
				session_destroy();
				$this->clear('COOKIE.'.session_name());
			}
			$this->sync('SESSION');
		}
		if (!isset($parts[1]) && array_key_exists($parts[0],$this->init))
			// Reset global to default value
			$this->hive[$parts[0]]=$this->init[$parts[0]];
		else {
			$val=preg_replace('/^(\$hive)/','$this->hive',
				$this->compile('@hive.'.$key, FALSE));
			eval('unset('.$val.');');
			if ($parts[0]=='SESSION') {
				session_commit();
				session_start();
			}
			if ($cache->exists($hash=$this->hash($key).'.var'))
				// Remove from cache
				$cache->clear($hash);
		}
	}

	/**
	*	Return TRUE if hive variable is 'on'
	*	@return bool
	*	@param $key string
	**/
	function checked($key) {
		$ref=&$this->ref($key);
		return $ref=='on';
	}

	/**
	*	Return TRUE if property has public visibility
	*	@return bool
	*	@param $obj object
	*	@param $key string
	**/
	function visible($obj,$key) {
		if (property_exists($obj,$key)) {
			$ref=new ReflectionProperty(get_class($obj),$key);
			$out=$ref->ispublic();
			unset($ref);
			return $out;
		}
		return FALSE;
	}

	/**
	*	Multi-variable assignment using associative array
	*	@param $vars array
	*	@param $prefix string
	*	@param $ttl int
	**/
	function mset(array $vars,$prefix='',$ttl=0) {
		foreach ($vars as $key=>$val)
			$this->set($prefix.$key,$val,$ttl);
	}

	/**
	*	Publish hive contents
	*	@return array
	**/
	function hive() {
		return $this->hive;
	}

	/**
	*	Copy contents of hive variable to another
	*	@return mixed
	*	@param $src string
	*	@param $dst string
	**/
	function copy($src,$dst) {
		$ref=&$this->ref($dst);
		return $ref=$this->ref($src,FALSE);
	}

	/**
	*	Concatenate string to hive string variable
	*	@return string
	*	@param $key string
	*	@param $val string
	**/
	function concat($key,$val) {
		$ref=&$this->ref($key);
		$ref.=$val;
		return $ref;
	}

	/**
	*	Swap keys and values of hive array variable
	*	@return array
	*	@param $key string
	*	@public
	**/
	function flip($key) {
		$ref=&$this->ref($key);
		return $ref=array_combine(array_values($ref),array_keys($ref));
	}

	/**
	*	Add element to the end of hive array variable
	*	@return mixed
	*	@param $key string
	*	@param $val mixed
	**/
	function push($key,$val) {
		$ref=&$this->ref($key);
		$ref[]=$val;
		return $val;
	}

	/**
	*	Remove last element of hive array variable
	*	@return mixed
	*	@param $key string
	**/
	function pop($key) {
		$ref=&$this->ref($key);
		return array_pop($ref);
	}

	/**
	*	Add element to the beginning of hive array variable
	*	@return mixed
	*	@param $key string
	*	@param $val mixed
	**/
	function unshift($key,$val) {
		$ref=&$this->ref($key);
		array_unshift($ref,$val);
		return $val;
	}

	/**
	*	Remove first element of hive array variable
	*	@return mixed
	*	@param $key string
	**/
	function shift($key) {
		$ref=&$this->ref($key);
		return array_shift($ref);
	}

	/**
	*	Merge array with hive array variable
	*	@return array
	*	@param $key string
	*	@param $src string|array
	*	@param $keep bool
	**/
	function merge($key,$src,$keep=FALSE) {
		$ref=&$this->ref($key);
		if (!$ref)
			$ref=[];
		$out=array_merge($ref,is_string($src)?$this->hive[$src]:$src);
		if ($keep)
			$ref=$out;
		return $out;
	}

	/**
	*	Extend hive array variable with default values from $src
	*	@return array
	*	@param $key string
	*	@param $src string|array
	*	@param $keep bool
	**/
	function extend($key,$src,$keep=FALSE) {
		$ref=&$this->ref($key);
		if (!$ref)
			$ref=[];
		$out=array_replace_recursive(
			is_string($src)?$this->hive[$src]:$src,$ref);
		if ($keep)
			$ref=$out;
		return $out;
	}

    /**
     * fetch a hive key result, then clear the key
     * @param string $key
     * @return mixed
     */
    function pull($key): mixed
    {
        $value = $this->get($key);
        $this->clear($key);
        return $value;
    }

	/**
	*	Convert backslashes to slashes
	*	@return string
	*	@param $str string
	**/
	function fixslashes($str) {
		return $str?strtr($str,'\\','/'):$str;
	}

	/**
	*	Split comma-, semi-colon, or pipe-separated string
	*	@return array
	*	@param $str string
	*	@param $noempty bool
	**/
	function split($str,$noempty=TRUE) {
		return array_map('trim',
			preg_split('/[,;|]/',$str?:'',0,$noempty?PREG_SPLIT_NO_EMPTY:0));
	}

	/**
	*	Convert PHP expression/value to compressed exportable string
	*	@return string
	*	@param $arg mixed
	*	@param $stack array
	**/
	function stringify($arg,?array $stack=NULL) {
		if ($stack) {
			foreach ($stack as $node)
				if ($arg===$node)
					return '*RECURSION*';
		}
		else
			$stack=[];
		switch (gettype($arg)) {
			case 'object':
				$str='';
				foreach (get_object_vars($arg) as $key=>$val)
					$str.=($str?',':'').
						$this->export($key).'=>'.
						$this->stringify($val,
							array_merge($stack,[$arg]));
				return get_class($arg).'::__set_state(['.$str.'])';
			case 'array':
				$str='';
				$num=isset($arg[0]) &&
					ctype_digit(implode('',array_keys($arg)));
				foreach ($arg as $key=>$val)
					$str.=($str?',':'').
						($num?'':($this->export($key).'=>')).
						$this->stringify($val,array_merge($stack,[$arg]));
				return '['.$str.']';
			default:
				return $this->export($arg);
		}
	}

	/**
	*	Flatten array values and return as CSV string
	*	@return string
	*	@param $args array
	**/
	function csv(array $args) {
		return implode(',',array_map('stripcslashes',
			array_map([$this,'stringify'],$args)));
	}

	/**
	*	Convert snakecase string to camelcase
	*	@return string
	*	@param $str string
	**/
	function camelcase($str) {
		return preg_replace_callback(
			'/_(\pL)/u',
			function($match) {
				return strtoupper($match[1]);
			},
			$str
		);
	}

	/**
	*	Convert camelcase string to snakecase
	*	@return string
	*	@param $str string
	**/
	function snakecase($str) {
		return strtolower(preg_replace('/(?!^)\p{Lu}/u','_\0',$str));
	}

	/**
	*	Return -1 if specified number is negative, 0 if zero,
	*	or 1 if the number is positive
	*	@return int
	*	@param $num mixed
	**/
	function sign($num) {
		return $num?($num/abs($num)):0;
	}

	/**
	*	Extract values of array whose keys start with the given prefix
	*	@return array
	*	@param $arr array
	*	@param $prefix string
	**/
	function extract($arr,$prefix) {
		$out=[];
		foreach (preg_grep('/^'.preg_quote($prefix,'/').'/',array_keys($arr))
			as $key)
			$out[substr($key,strlen($prefix))]=$arr[$key];
		return $out;
	}

	/**
	*	Convert class constants to array
	*	@return array
	*	@param $class object|string
	*	@param $prefix string
	**/
	function constants($class,$prefix='') {
		$ref=new ReflectionClass($class);
		return $this->extract($ref->getconstants(),$prefix);
	}

	/**
	*	Generate 64bit/base36 hash
	*	@return string
	*	@param $str
	**/
	function hash($str) {
		return str_pad(base_convert(
			substr(sha1($str?:''),-16),16,36),11,'0',STR_PAD_LEFT);
	}

	/**
	*	Return Base64-encoded equivalent
	*	@return string
	*	@param $data string
	*	@param $mime string
	**/
	function base64($data,$mime) {
		return 'data:'.$mime.';base64,'.base64_encode($data);
	}

	/**
	*	Convert special characters to HTML entities
	*	@return string
	*	@param $str string
	**/
	function encode($str) {
		return @htmlspecialchars($str,$this->hive['BITMASK'],
			$this->hive['ENCODING'])?:$this->scrub($str);
	}

	/**
	*	Convert HTML entities back to characters
	*	@return string
	*	@param $str string
	**/
	function decode($str) {
		return htmlspecialchars_decode($str,$this->hive['BITMASK']);
	}

	/**
	*	Invoke callback recursively for all data types
	*	@return mixed
	*	@param $arg mixed
	*	@param $func callback
	*	@param $stack array
	**/
	function recursive($arg,$func,$stack=[]) {
		if ($stack) {
			foreach ($stack as $node)
				if ($arg===$node)
					return $arg;
		}
		switch (gettype($arg)) {
			case 'object':
				$ref=new ReflectionClass($arg);
				if ($ref->iscloneable()) {
					$arg=clone($arg);
					$cast=($it=is_a($arg,'IteratorAggregate'))?
						iterator_to_array($arg):get_object_vars($arg);
					foreach ($cast as $key=>$val) {
						// skip inaccessible properties #350
						if (!$it && !isset($arg->$key))
							continue;
						$arg->$key=$this->recursive(
							$val,$func,array_merge($stack,[$arg]));
					}
				}
				return $arg;
			case 'array':
				$copy=[];
				foreach ($arg as $key=>$val)
					$copy[$key]=$this->recursive($val,$func,
						array_merge($stack,[$arg]));
				return $copy;
		}
		return $func($arg);
	}

	/**
	*	Remove HTML tags (except those enumerated) and non-printable characters
    *   NB: This method doesn't mitigate XSS/code injection attacks.
    *   @return mixed
    *	@param $arg mixed
	*	@param $tags string
	**/
	function clean($arg,$tags=NULL) {
		return $this->recursive($arg,
			function($val) use($tags) {
				if ($tags!='*')
					$val=trim(strip_tags($val??'',
						'<'.implode('><',$this->split($tags)).'>'));
				return trim(preg_replace(
					'/[\x00-\x08\x0B\x0C\x0E-\x1F]/','',$val));
			}
		);
	}

	/**
	*	Similar to clean(), except that variable is passed by reference
	*	@return mixed
	*	@param $var mixed
	*	@param $tags string
	**/
	function scrub(&$var,$tags=NULL) {
		return $var=$this->clean($var,$tags);
	}

	/**
	*	Return locale-aware formatted string
	*	@return string
	**/
	function format() {
		$args=func_get_args();
		$val=array_shift($args);
		// Get formatting rules
		$conv=localeconv();
		return preg_replace_callback(
			'/\{\s*(?P<pos>\d+)\s*(?:,\s*(?P<type>\w+)\s*'.
			'(?:,\s*(?P<mod>(?:\w+(?:\s*\{.+?\}\s*,?\s*)?)*)'.
			'(?:,\s*(?P<prop>.+?))?)?)?\s*\}/',
			function($expr) use($args,$conv) {
				/**
				 * @var string $pos
				 * @var string $mod
				 * @var string $type
				 * @var string $prop
				 */
				extract($expr);
				/**
				 * @var string $thousands_sep
				 * @var string $negative_sign
				 * @var string $positive_sign
				 * @var string $frac_digits
				 * @var string $decimal_point
				 * @var string $int_curr_symbol
				 * @var string $currency_symbol
				 */
				extract($conv);
				if (!array_key_exists($pos,$args))
					return $expr[0];
				if (isset($type)) {
					if (isset($this->hive['FORMATS'][$type]))
						return $this->call(
							$this->hive['FORMATS'][$type],
							[
								$args[$pos],
								isset($mod)?$mod:null,
								isset($prop)?$prop:null
							]
						);
					$php81=version_compare(PHP_VERSION, '8.1.0')>=0;
					switch ($type) {
						case 'plural':
							preg_match_all('/(?<tag>\w+)'.
								'(?:\s*\{\s*(?<data>.*?)\s*\})/',
								$mod,$matches,PREG_SET_ORDER);
							$ord=['zero','one','two'];
							foreach ($matches as $match) {
								/** @var string $tag */
								/** @var string $data */
								extract($match);
								if (isset($ord[$args[$pos]]) &&
									$tag==$ord[$args[$pos]] || $tag=='other')
									return str_replace('#',$args[$pos],$data);
							}
						case 'number':
							if (isset($mod))
								switch ($mod) {
									case 'integer':
										return number_format(
											$args[$pos],0,'',$thousands_sep);
									case 'currency':
										$int=$cstm=FALSE;
										if (isset($prop) &&
											$cstm=!$int=($prop=='int'))
											$currency_symbol=$prop;
										if (!$cstm &&
											function_exists('money_format') &&
											version_compare(PHP_VERSION,'7.4.0')<0)
											return money_format(
												'%'.($int?'i':'n'),$args[$pos]);
										$fmt=[
											0=>'(nc)',1=>'(n c)',
											2=>'(nc)',10=>'+nc',
											11=>'+n c',12=>'+ nc',
											20=>'nc+',21=>'n c+',
											22=>'nc +',30=>'n+c',
											31=>'n +c',32=>'n+ c',
											40=>'nc+',41=>'n c+',
											42=>'nc +',100=>'(cn)',
											101=>'(c n)',102=>'(cn)',
											110=>'+cn',111=>'+c n',
											112=>'+ cn',120=>'cn+',
											121=>'c n+',122=>'cn +',
											130=>'+cn',131=>'+c n',
											132=>'+ cn',140=>'c+n',
											141=>'c+ n',142=>'c +n'
										];
										if ($args[$pos]<0) {
											$sgn=$negative_sign;
											$pre='n';
										}
										else {
											$sgn=$positive_sign;
											$pre='p';
										}
										return str_replace(
											['+','n','c'],
											[$sgn,number_format(
												abs($args[$pos]),
												$frac_digits,
												$decimal_point,
												$thousands_sep),
												$int?$int_curr_symbol
													:$currency_symbol],
											$fmt[(int)(
												(${$pre.'_cs_precedes'}%2).
												(${$pre.'_sign_posn'}%5).
												(${$pre.'_sep_by_space'}%3)
											)]
										);
									case 'percent':
										return number_format(
											$args[$pos]*100,0,$decimal_point,
											$thousands_sep).'%';
								}
							$frac=$args[$pos]-(int)$args[$pos];
							return number_format(
								$args[$pos],
								isset($prop)?
									$prop:
									($frac?strlen($frac)-2:0),
								$decimal_point,$thousands_sep);
						case 'date':
							if ($php81) {
								$lang = $this->split($this->LANGUAGE);
								// requires intl extension
								$dateType=(empty($mod) || $mod=='short') ? IntlDateFormatter::SHORT :
									($mod=='full' ? IntlDateFormatter::FULL : IntlDateFormatter::LONG);
								$pattern = $dateType === IntlDateFormatter::SHORT
									? (($ptn=IntlDatePatternGenerator::create($lang[0]))
										? $ptn->getBestPattern('yyyyMMdd') : null) : null;
								$formatter = new IntlDateFormatter($lang[0],$dateType,
									IntlDateFormatter::NONE, null,null, $pattern);
								return $formatter->format($args[$pos]);
							} else {
								if (empty($mod) || $mod=='short')
									$prop='%x';
								elseif ($mod=='full')
									$prop='%A, %d %B %Y';
								elseif ($mod!='custom')
									$prop='%d %B %Y';
								return strftime($prop,$args[$pos]);
							}
						case 'time':
							if ($php81) {
								$lang = $this->split($this->LANGUAGE);
								// requires intl extension
								$formatter = new IntlDateFormatter($lang[0],
									IntlDateFormatter::NONE,
									(empty($mod) || $mod=='short')
										? IntlDateFormatter::SHORT :
										($mod=='full' ? IntlDateFormatter::LONG : IntlDateFormatter::MEDIUM),
									IntlTimeZone::createTimeZone($this->hive['TZ']));
								return $formatter->format($args[$pos]);
							} else {
								if (empty($mod) || $mod=='short')
									$prop='%X';
								elseif ($mod!='custom')
									$prop='%r';
								return strftime($prop,$args[$pos]);
							}
						default:
							return $expr[0];
					}
				}
				return $args[$pos];
			},
			$val
		);
	}

	/**
	*	Return string representation of expression
	*	@return string
	*	@param $expr mixed
	**/
	function export($expr) {
		return var_export($expr,TRUE);
	}

	/**
	*	Assign/auto-detect language
	*	@return string
	*	@param $code string
	**/
	function language($code) {
		$code=preg_replace('/\h+|;q=[0-9.]+/','',$code?:'');
		$code.=($code?',':'').$this->fallback;
		$this->languages=[];
		foreach (array_reverse(explode(',',$code)) as $lang)
			if (preg_match('/^(\w{2})(?:-(\w{2}))?\b/i',$lang,$parts)) {
				// Generic language
				array_unshift($this->languages,$parts[1]);
				if (isset($parts[2])) {
					// Specific language
					$parts[0]=$parts[1].'-'.($parts[2]=strtoupper($parts[2]));
					array_unshift($this->languages,$parts[0]);
				}
			}
		$this->languages=array_unique($this->languages);
		$locales=[];
		$windows=preg_match('/^win/i',PHP_OS);
		// Work around PHP's Turkish locale bug
		foreach (preg_grep('/^(?!tr)/i',$this->languages) as $locale) {
			if ($windows) {
				$parts=explode('-',$locale);
				if (!defined('ISO::LC_'.$parts[0]))
					continue;
				$locale=constant('ISO::LC_'.$parts[0]);
				if (isset($parts[1]) &&
					defined($cc='ISO::CC_'.strtolower($parts[1])))
					$locale.='-'.constant($cc);
			}
			$locale=str_replace('-','_',$locale);
			$locales[]=$locale.'.'.ini_get('default_charset');
			$locales[]=$locale;
		}
		setlocale(LC_ALL,$locales);
		return $this->hive['LANGUAGE']=implode(',',$this->languages);
	}

	/**
	*	Return lexicon entries
	*	@return array
	*	@param $path string
	*	@param $ttl int
	**/
	function lexicon($path,$ttl=0) {
		$languages=$this->languages?:explode(',',$this->fallback);
		$cache=Cache::instance();
		if ($ttl && $cache->exists(
			$hash=$this->hash(implode(',',$languages).$path).'.dic',$lex))
			return $lex;
		$lex=[];
		foreach ($languages as $lang)
			foreach ($this->split($path) as $dir)
				if ((is_file($file=($base=$dir.$lang).'.php') ||
					is_file($file=$base.'.php')) &&
					is_array($dict=require($file)))
					$lex+=$dict;
				elseif (is_file($file=$base.'.json') &&
					is_array($dict=json_decode(file_get_contents($file), true)))
					$lex+=$dict;
				elseif (is_file($file=$base.'.ini')) {
					preg_match_all(
						'/(?<=^|\n)(?:'.
							'\[(?<prefix>.+?)\]|'.
							'(?<lval>[^\h\r\n;].*?)\h*=\h*'.
							'(?<rval>(?:\\\\\h*\r?\n|.+?)*)'.
						')(?=\r?\n|$)/',
						$this->read($file),$matches,PREG_SET_ORDER);
					if ($matches) {
						$prefix='';
						foreach ($matches as $match)
							if ($match['prefix'])
								$prefix=$match['prefix'].'.';
							elseif (!array_key_exists(
								$key=$prefix.$match['lval'],$lex))
								$lex[$key]=trim(preg_replace(
									'/\\\\\h*\r?\n/',"\n",$match['rval']));
					}
				}
		if ($ttl)
			$cache->set($hash,$lex,$ttl);
		return $lex;
	}

	/**
	*	Return string representation of PHP value
	*	@return string
	*	@param $arg mixed
	**/
	function serialize($arg) {
		switch (strtolower($this->hive['SERIALIZER'])) {
			case 'igbinary':
				return igbinary_serialize($arg);
			default:
				return serialize($arg);
		}
	}

	/**
	*	Return PHP value derived from string
	*	@return mixed
	*	@param $arg mixed
	**/
	function unserialize($arg) {
		switch (strtolower($this->hive['SERIALIZER'])) {
			case 'igbinary':
				return igbinary_unserialize($arg);
			default:
				return unserialize($arg);
		}
	}

	/**
	*	Send HTTP status header; Return text equivalent of status code
	*	@return string
	*	@param $code int
	**/
	function status($code) {
		$reason=@constant('self::HTTP_'.$code);
		if (!$this->hive['CLI'] && !headers_sent())
			header($_SERVER['SERVER_PROTOCOL'].' '.$code.' '.$reason);
		return $reason;
	}

	/**
	*	Send cache metadata to HTTP client
	*	@param $secs int
	**/
	function expire($secs=0) {
		if (!$this->hive['CLI'] && !headers_sent()) {
			$secs=(int)$secs;
			if ($this->hive['PACKAGE'])
				header('X-Powered-By: '.$this->hive['PACKAGE']);
			if ($this->hive['XFRAME'])
				header('X-Frame-Options: '.$this->hive['XFRAME']);
			header('X-XSS-Protection: 1; mode=block');
			header('X-Content-Type-Options: nosniff');
			if ($this->hive['VERB']=='GET' && $secs) {
				$time=microtime(TRUE);
				header_remove('Pragma');
				header('Cache-Control: max-age='.$secs);
				header('Expires: '.gmdate('r',round($time+$secs)));
				header('Last-Modified: '.gmdate('r'));
			}
			else {
				header('Pragma: no-cache');
				header('Cache-Control: no-cache, no-store, must-revalidate');
				header('Expires: '.gmdate('r',0));
			}
		}
	}

	/**
	*	Return HTTP user agent
	*	@return string
	**/
	function agent() {
		$headers=$this->hive['HEADERS'];
		return isset($headers['X-Operamini-Phone-UA'])?
			$headers['X-Operamini-Phone-UA']:
			(isset($headers['X-Skyfire-Phone'])?
				$headers['X-Skyfire-Phone']:
				(isset($headers['User-Agent'])?
					$headers['User-Agent']:''));
	}

	/**
	*	Return TRUE if XMLHttpRequest detected
	*	@return bool
	**/
	function ajax() {
		$headers=$this->hive['HEADERS'];
		return isset($headers['X-Requested-With']) &&
			$headers['X-Requested-With']=='XMLHttpRequest';
	}

	/**
	*	Sniff IP address
	*	@return string
	**/
	function ip() {
		$headers=$this->hive['HEADERS'];
		return isset($headers['Client-IP'])?
			$headers['Client-IP']:
			(isset($headers['X-Forwarded-For'])?
				explode(',',$headers['X-Forwarded-For'])[0]:
				(isset($_SERVER['REMOTE_ADDR'])?
					$_SERVER['REMOTE_ADDR']:''));
	}

	/**
	*	Return filtered stack trace as a formatted string (or array)
	*	@return string|array
	*	@param $trace array|NULL
	*	@param $format bool
	**/
	function trace(?array $trace=NULL,$format=TRUE) {
		if (!$trace) {
			$trace=debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
			$frame=$trace[0];
			if (isset($frame['file']) && $frame['file']==__FILE__)
				array_shift($trace);
		}
		$debug=$this->hive['DEBUG'];
		$trace=array_filter(
			$trace,
			function($frame) use($debug) {
				return isset($frame['file']) &&
					($debug>1 ||
					(($frame['file']!=__FILE__ || $debug) &&
					(empty($frame['function']) ||
					!preg_match('/^(?:(?:trigger|user)_error|'.
						'__call|call_user_func)/',$frame['function']))));
			}
		);
		if (!$format)
			return $trace;
		$out='';
		$eol="\n";
		// Analyze stack trace
		foreach ($trace as $frame) {
			$line='';
			if (isset($frame['class']))
				$line.=$frame['class'].$frame['type'];
			if (isset($frame['function']))
				$line.=$frame['function'].'('.
					($debug>2 && isset($frame['args'])?
						$this->csv($frame['args']):'').')';
			$src=$this->fixslashes(str_replace($_SERVER['DOCUMENT_ROOT'].
				'/','',$frame['file'])).':'.$frame['line'];
			$out.='['.$src.'] '.$line.$eol;
		}
		return $out;
	}

	/**
	*	Log error; Execute ONERROR handler if defined, else display
	*	default error page (HTML for synchronous requests, JSON string
	*	for AJAX requests)
	*	@param $code int
	*	@param $text string
	*	@param $trace array
	*	@param $level int
	**/
	function error($code,$text='',?array $trace=NULL,$level=0) {
		$prior=$this->hive['ERROR'];
		$header=$this->status($code);
		$req=$this->hive['VERB'].' '.$this->hive['PATH'];
		if ($this->hive['QUERY'])
			$req.='?'.$this->hive['QUERY'];
		if (!$text)
			$text='HTTP '.$code.' ('.$req.')';
		$trace=$this->trace($trace);
		$loggable=$this->hive['LOGGABLE'];
		if (!is_array($loggable))
			$loggable=$this->split($loggable);
		foreach ($loggable as $status)
			if ($status=='*' ||
				preg_match('/^'.preg_replace('/\D/','\d',$status).'$/',(string) $code)) {
				error_log($text);
				foreach (explode("\n",$trace) as $nexus)
					if ($nexus)
						error_log($nexus);
				break;
			}
		if ($highlight=(!$this->hive['CLI'] && !$this->hive['AJAX'] &&
			$this->hive['HIGHLIGHT'] && is_file($css=__DIR__.'/'.self::CSS)))
			$trace=$this->highlight($trace);
		$this->hive['ERROR']=[
			'status'=>$header,
			'code'=>$code,
			'text'=>$text,
			'trace'=>$trace,
			'level'=>$level
		];
		$this->expire(-1);
		$handler=$this->hive['ONERROR'];
		$this->hive['ONERROR']=NULL;
		$eol="\n";
		if ((!$handler ||
			$this->call($handler,[$this,$this->hive['PARAMS']],
				'beforeroute,afterroute')===FALSE) &&
			!$prior && !$this->hive['QUIET']) {
			$error=array_diff_key(
				$this->hive['ERROR'],
				$this->hive['DEBUG']?
					[]:
					['trace'=>1]
			);
			if ($this->hive['CLI'])
				echo PHP_EOL.'==================================='.PHP_EOL.
					'ERROR '.$error['code'].' - '.$error['status'].PHP_EOL.
					$error['text'].PHP_EOL.PHP_EOL.(isset($error['trace']) ? $error['trace'] : '');
			else
				echo $this->hive['AJAX']?
					json_encode($error):
					('<!DOCTYPE html>'.$eol.
					'<html>'.$eol.
					'<head>'.
						'<title>'.$code.' '.$header.'</title>'.
						($highlight?
							('<style>'.$this->read($css).'</style>'):'').
					'</head>'.$eol.
					'<body>'.$eol.
						'<h1>'.$header.'</h1>'.$eol.
						'<p>'.$this->encode($text?:$req).'</p>'.$eol.
						($this->hive['DEBUG']?('<pre>'.$trace.'</pre>'.$eol):'').
					'</body>'.$eol.
					'</html>');
		}
		if ($this->hive['HALT'])
			die(1);
	}

	/**
	*	Mock HTTP request
	*	@return mixed
	*	@param $pattern string
	*	@param $args array
	*	@param $headers array
	*	@param $body string
	**/
	function mock($pattern,
		?array $args=NULL,?array $headers=NULL,$body=NULL) {
		if (!$args)
			$args=[];
		$types=['sync','ajax','cli'];
		preg_match('/([\|\w]+)\h+(?:@(\w+)(?:(\(.+?)\))*|([^\h]+))'.
			'(?:\h+\[('.implode('|',$types).')\])?/',$pattern,$parts);
		$verb=strtoupper($parts[1]);
		if ($parts[2]) {
			if (empty($this->hive['ALIASES'][$parts[2]]))
                throw new \Exception(sprintf(self::E_Named,$parts[2]));
			$parts[4]=$this->hive['ALIASES'][$parts[2]];
			$parts[4]=$this->build($parts[4],
				isset($parts[3])?$this->parse($parts[3]):[]);
		}
		if (empty($parts[4]))
            throw new \Exception(sprintf(self::E_Pattern,$pattern));
		$url=parse_url($parts[4]);
		parse_str(isset($url['query'])?$url['query']:'',$GLOBALS['_GET']);
		if (preg_match('/GET|HEAD/',$verb))
			$GLOBALS['_GET']=array_merge($GLOBALS['_GET'],$args);
		$GLOBALS['_POST']=$verb=='POST'?$args:[];
		$GLOBALS['_REQUEST']=array_merge($GLOBALS['_GET'],$GLOBALS['_POST']);
		foreach ($headers?:[] as $key=>$val)
			$_SERVER['HTTP_'.strtr(strtoupper($key),'-','_')]=$val;
		$this->hive['VERB']=$verb;
		$this->hive['PATH']=$url['path'];
		$this->hive['URI']=$this->hive['BASE'].$url['path'];
		if ($GLOBALS['_GET'])
			$this->hive['URI'].='?'.http_build_query($GLOBALS['_GET']);
		$this->hive['BODY']='';
		if (!preg_match('/GET|HEAD/',$verb))
			$this->hive['BODY']=$body?:http_build_query($args);
		$this->hive['AJAX']=isset($parts[5]) &&
			preg_match('/ajax/i',$parts[5]);
		$this->hive['CLI']=isset($parts[5]) &&
			preg_match('/cli/i',$parts[5]);
		return $this->run();
	}

	/**
	*	Assemble url from alias name
	*	@return string
	*	@param $name string
	*	@param $params array|string
	*	@param $query string|array
	*	@param $fragment string
	**/
	function alias($name,$params=[],$query=NULL,$fragment=NULL) {
		if (!is_array($params))
			$params=$this->parse($params);
		if (empty($this->hive['ALIASES'][$name]))
            throw new \Exception(sprintf(self::E_Named,$name));
		$url=$this->build($this->hive['ALIASES'][$name],$params);
		if (is_array($query))
			$query=http_build_query($query);
		return $url.($query?('?'.$query):'').($fragment?'#'.$fragment:'');
	}

	/**
	*	Bind handler to route pattern
	*	@return NULL
	*	@param $pattern string|array
	*	@param $handler callback
	*	@param $ttl int
	*	@param $kbps int
	**/
	function route($pattern,$handler,$ttl=0,$kbps=0) {
		$types=['sync','ajax','cli'];
		$alias=null;
		if (is_array($pattern)) {
			foreach ($pattern as $item)
				$this->route($item,$handler,$ttl,$kbps);
			return;
		}
		preg_match('/([\|\w]+)\h+(?:(?:@?(.+?)\h*:\h*)?(@(\w+)|[^\h]+))'.
			'(?:\h+\[('.implode('|',$types).')\])?/u',$pattern,$parts);
		if (isset($parts[2]) && $parts[2]) {
			if (!preg_match('/^\w+$/',$parts[2]))
                throw new \Exception(sprintf(self::E_Alias,$parts[2]));
			$this->hive['ALIASES'][$alias=$parts[2]]=$parts[3];
		}
		elseif (!empty($parts[4])) {
			if (empty($this->hive['ALIASES'][$parts[4]]))
                throw new \Exception(sprintf(self::E_Named,$parts[4]));
			$parts[3]=$this->hive['ALIASES'][$alias=$parts[4]];
		}
		if (empty($parts[3]))
            throw new \Exception(sprintf(self::E_Pattern,$pattern));
		$type=empty($parts[5])?0:constant('self::REQ_'.strtoupper($parts[5]));
		foreach ($this->split($parts[1]) as $verb) {
			if (!preg_match('/'.self::VERBS.'/',$verb))
				$this->error(501,$verb.' '.$this->hive['URI']);
			$this->hive['ROUTES'][$parts[3]][$type][strtoupper($verb)]=
				[is_string($handler) ? trim($handler) : $handler,$ttl,$kbps,$alias];
		}
	}

	/**
	*	Reroute to specified URI
	*	@return NULL
	*	@param $url array|string
	*	@param $permanent bool
	*	@param $die bool
	**/
	function reroute($url=NULL,$permanent=FALSE,$die=TRUE) {
		if (!$url)
			$url=$this->hive['REALM'];
		if (is_array($url))
			$url=call_user_func_array([$this,'alias'],$url);
		elseif (preg_match('/^(?:@([^\/()?#]+)(?:\((.+?)\))*(\?[^#]+)*(#.+)*)/',
			$url,$parts) && isset($this->hive['ALIASES'][$parts[1]]))
			$url=$this->build($this->hive['ALIASES'][$parts[1]],
					isset($parts[2])?$this->parse($parts[2]):[]).
				(isset($parts[3])?$parts[3]:'').(isset($parts[4])?$parts[4]:'');
		else
			$url=$this->build($url);
		if (($handler=$this->hive['ONREROUTE']) &&
			$this->call($handler,[$url,$permanent,$die])!==FALSE)
			return;
		if ($url[0]!='/' && !preg_match('/^\w+:\/\//i',$url))
			$url='/'.$url;
		if ($url[0]=='/' && (empty($url[1]) || $url[1]!='/')) {
			$port=$this->hive['PORT'];
			$port=in_array($port,[80,443])?'':(':'.$port);
			$url=$this->hive['SCHEME'].'://'.
				$this->hive['HOST'].$port.$this->hive['BASE'].$url;
		}
		if ($this->hive['CLI'])
			$this->mock('GET '.$url.' [cli]');
		else {
			header('Location: '.$url);
			$this->status($permanent?301:302);
			if ($die)
				die;
		}
	}

	/**
	*	Provide ReST interface by mapping HTTP verb to class method
	*	@return NULL
	*	@param $url string
	*	@param $class string|object
	*	@param $ttl int
	*	@param $kbps int
	**/
	function map($url,$class,$ttl=0,$kbps=0) {
		if (is_array($url)) {
			foreach ($url as $item)
				$this->map($item,$class,$ttl,$kbps);
			return;
		}
		foreach (explode('|',self::VERBS) as $method)
			$this->route($method.' '.$url,is_string($class)?
				$class.'->'.$this->hive['PREMAP'].strtolower($method):
				[$class,$this->hive['PREMAP'].strtolower($method)],
				$ttl,$kbps);
	}

	/**
	*	Redirect a route to another URL
	*	@return NULL
	*	@param $pattern string|array
	*	@param $url string
	*	@param $permanent bool
	*/
	function redirect($pattern,$url,$permanent=TRUE) {
		if (is_array($pattern)) {
			foreach ($pattern as $item)
				$this->redirect($item,$url,$permanent);
			return;
		}
		$this->route($pattern,function($fw) use($url,$permanent) {
			$fw->reroute($url,$permanent);
		});
	}

	/**
	*	Return TRUE if IPv4 address exists in DNSBL
	*	@return bool
	*	@param $ip string
	**/
	function blacklisted($ip) {
		if ($this->hive['DNSBL'] &&
			!in_array($ip,
				is_array($this->hive['EXEMPT'])?
					$this->hive['EXEMPT']:
					$this->split($this->hive['EXEMPT']))) {
			// Reverse IPv4 dotted quad
			$rev=implode('.',array_reverse(explode('.',$ip)));
			foreach (is_array($this->hive['DNSBL'])?
				$this->hive['DNSBL']:
				$this->split($this->hive['DNSBL']) as $server)
				// DNSBL lookup
				if (checkdnsrr($rev.'.'.$server,'A'))
					return TRUE;
		}
		return FALSE;
	}

	/**
	*	Applies the specified URL mask and returns parameterized matches
	*	@return $args array
	*	@param $pattern string
	*	@param $url string|NULL
	**/
	function mask($pattern,$url=NULL) {
		if (!$url)
			$url=$this->rel($this->hive['URI']);
		$case=$this->hive['CASELESS']?'i':'';
		$wild=preg_quote($pattern,'/');
		$i=0;
		while (is_int($pos=strpos($wild,'\*'))) {
			$wild=substr_replace($wild,'(?P<_'.$i.'>[^\?]*)',$pos,2);
			++$i;
		}
		preg_match('/^'.
			preg_replace(
				'/((\\\{)?@(\w+\b)(?(2)\\\}))/',
				'(?P<\3>[^\/\?]+)',
				$wild).'\/?$/'.$case.'um',$url,$args);
		foreach (array_keys($args) as $key) {
			if (preg_match('/^_\d+$/',$key)) {
				if (empty($args['*']))
					$args['*']=$args[$key];
				else {
					if (is_string($args['*']))
						$args['*']=[$args['*']];
					array_push($args['*'],$args[$key]);
				}
				unset($args[$key]);
			}
			elseif (is_numeric($key) && $key)
				unset($args[$key]);
		}
		return $args;
	}

	/**
	*	Match routes against incoming URI
	*	@return mixed
	**/
	function run() {
		if ($this->blacklisted($this->hive['IP']))
			// Spammer detected
			$this->error(403);
		if (!$this->hive['ROUTES'])
			// No routes defined
            throw new \Exception(self::E_Routes);
		// Match specific routes first
		$paths=[];
		foreach ($keys=array_keys($this->hive['ROUTES']) as $key) {
			$path=preg_replace('/@\w+/','*@',$key);
			if (substr($path,-1)!='*')
				$path.='+';
			$paths[]=$path;
		}
		$vals=array_values($this->hive['ROUTES']);
		array_multisort($paths,SORT_DESC,$keys,$vals);
		$this->hive['ROUTES']=array_combine($keys,$vals);
		// Convert to BASE-relative URL
		$req=urldecode($this->hive['PATH']);
		$preflight=FALSE;
		if ($cors=(isset($this->hive['HEADERS']['Origin']) &&
			$this->hive['CORS']['origin'])) {
			$cors=$this->hive['CORS'];
			header('Access-Control-Allow-Origin: '.$cors['origin']);
			header('Access-Control-Allow-Credentials: '.
				$this->export($cors['credentials']));
			$preflight=
				isset($this->hive['HEADERS']['Access-Control-Request-Method']);
		}
		$allowed=[];
		foreach ($this->hive['ROUTES'] as $pattern=>$routes) {
			if (!$args=$this->mask($pattern,$req))
				continue;
			ksort($args);
			$route=NULL;
			$ptr=$this->hive['CLI']?self::REQ_CLI:$this->hive['AJAX']+1;
			if (isset($routes[$ptr][$this->hive['VERB']]) ||
				($preflight && isset($routes[$ptr])) ||
				isset($routes[$ptr=0]))
				$route=$routes[$ptr];
			if (!$route)
				continue;
			if (isset($route[$this->hive['VERB']]) && !$preflight) {
				if ($this->hive['REROUTE_TRAILING_SLASH']===TRUE &&
					$this->hive['VERB']=='GET' &&
					preg_match('/.+\/$/',$this->hive['PATH']))
					$this->reroute(substr($this->hive['PATH'],0,-1).
						($this->hive['QUERY']?('?'.$this->hive['QUERY']):''));
				list($handler,$ttl,$kbps,$alias)=$route[$this->hive['VERB']];
				// Capture values of route pattern tokens
				$this->hive['PARAMS']=$args;
				// Save matching route
				$this->hive['ALIAS']=$alias;
				$this->hive['PATTERN']=$pattern;
				if ($cors && $cors['expose'])
					header('Access-Control-Expose-Headers: '.
						(is_array($cors['expose'])?
							implode(',',$cors['expose']):$cors['expose']));
				if (is_string($handler)) {
					// Replace route pattern tokens in handler if any
					$handler=preg_replace_callback('/({)?@(\w+\b)(?(1)})/',
						function($id) use($args) {
							$pid=count($id)>2?2:1;
							return isset($args[$id[$pid]])?
								$args[$id[$pid]]:
								$id[0];
						},
						$handler
					);
					if (preg_match('/(.+)\h*(?:->|::)/',$handler,$match) &&
						!class_exists($match[1]))
						$this->error(404);
				}
				// Process request
				$result=NULL;
				$body='';
				$now=microtime(TRUE);
				if (preg_match('/GET|HEAD/',$this->hive['VERB']) && $ttl) {
					// Only GET and HEAD requests are cacheable
					$headers=$this->hive['HEADERS'];
					$cache=Cache::instance();
					$cached=$cache->exists(
						$hash=$this->hash($this->hive['VERB'].' '.
							$this->hive['URI']).'.url',$data);
					if ($cached) {
						if (isset($headers['If-Modified-Since']) &&
							strtotime($headers['If-Modified-Since'])+
								$ttl>$now) {
							$this->status(304);
							die;
						}
						// Retrieve from cache backend
						list($headers,$body,$result)=$data;
						if (!$this->hive['CLI'])
							array_walk($headers,'header');
						$this->expire($cached[0]+$ttl-$now);
					}
					else
						// Expire HTTP client-cached page
						$this->expire($ttl);
				}
				else
					$this->expire(0);
				if (!strlen($body)) {
					if (!$this->hive['RAW'] && !$this->hive['BODY'])
						$this->hive['BODY']=file_get_contents('php://input');
					ob_start();
					// Call route handler
					$result=$this->call($handler,[$this,$args,$handler],
						'beforeroute,afterroute');
					$body=ob_get_clean();
					if (isset($cache) && !error_get_last()) {
						// Save to cache backend
						$cache->set($hash,[
							// Remove cookies
							preg_grep('/Set-Cookie\:/',headers_list(),
								PREG_GREP_INVERT),$body,$result],$ttl);
					}
				}
				$this->hive['RESPONSE']=$body;
				if (!$this->hive['QUIET']) {
					if ($kbps) {
						$ctr=0;
						foreach (str_split($body,1024) as $part) {
							// Throttle output
							++$ctr;
							if ($ctr/$kbps>($elapsed=microtime(TRUE)-$now) &&
								!connection_aborted())
								usleep(round(1e6*($ctr/$kbps-$elapsed)));
							echo $part;
						}
					}
					else
						echo $body;
				}
				if ($result || $this->hive['VERB']!='OPTIONS')
					return $result;
			}
			$allowed=array_merge($allowed,array_keys($route));
		}
		if (!$allowed)
			// URL doesn't match any route
			$this->error(404);
		elseif (!$this->hive['CLI']) {
			if (!preg_grep('/Allow:/',$headers_send=headers_list()))
				// Unhandled HTTP method
				header('Allow: '.implode(',',array_unique($allowed)));
			if ($cors) {
				if (!preg_grep('/Access-Control-Allow-Methods:/',$headers_send))
					header('Access-Control-Allow-Methods: OPTIONS,'.
						implode(',',$allowed));
				if ($cors['headers'] &&
					!preg_grep('/Access-Control-Allow-Headers:/',$headers_send))
					header('Access-Control-Allow-Headers: '.
						(is_array($cors['headers'])?
							implode(',',$cors['headers']):
							$cors['headers']));
				if ($cors['ttl']>0)
					header('Access-Control-Max-Age: '.$cors['ttl']);
			}
			if ($this->hive['VERB']!='OPTIONS')
				$this->error(405);
		}
		return FALSE;
	}

	/**
	*	Loop until callback returns TRUE (for long polling)
	*	@return mixed
	*	@param $func callback
	*	@param $args array
	*	@param $timeout int
	**/
	function until($func,$args=NULL,$timeout=60) {
		if (!$args)
			$args=[];
		$time=time();
		$max=ini_get('max_execution_time');
		$limit=max(0,($max?min($timeout,$max):$timeout)-1);
		$out='';
		// Turn output buffering on
		ob_start();
		// Not for the weak of heart
		while (
			// No error occurred
			!$this->hive['ERROR'] &&
			// Got time left?
			time()-$time+1<$limit &&
			// Still alive?
			!connection_aborted() &&
			// Restart session
			!headers_sent() &&
			(session_status()==PHP_SESSION_ACTIVE || session_start()) &&
			// CAUTION: Callback will kill host if it never becomes truthy!
			!$out=$this->call($func,$args)) {
			if (!$this->hive['CLI'])
				session_commit();
			// Hush down
			sleep(1);
		}
		ob_flush();
		flush();
		return $out;
	}

	/**
	*	Disconnect HTTP client;
	*	Set FcgidOutputBufferSize to zero if server uses mod_fcgid;
	*	Disable mod_deflate when rendering text/html output
	**/
	function abort() {
		if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)
			session_start();
		$out='';
		while (ob_get_level())
			$out=ob_get_clean().$out;
		if (!headers_sent()) {
			header('Content-Length: '.strlen($out));
			header('Connection: close');
		}
		session_commit();
		echo $out;
		flush();
		if (function_exists('fastcgi_finish_request'))
			fastcgi_finish_request();
	}

	/**
	*	Grab the real route handler behind the string expression
	*	@return string|array
	*	@param $func string
	*	@param $args array
	**/
	function grab($func,$args=NULL) {
		if (preg_match('/(.+)\h*(->|::)\h*(.+)/s',$func,$parts)) {
			// Convert string to executable PHP callback
			if (!class_exists($parts[1]))
                throw new \Exception(sprintf(self::E_Class,$parts[1]));
			if ($parts[2]=='->') {
				if (is_subclass_of($parts[1],'Prefab'))
					$parts[1]=call_user_func($parts[1].'::instance');
				elseif (isset($this->hive['CONTAINER'])) {
					$container=$this->hive['CONTAINER'];
					if (is_object($container) && is_callable([$container,'has'])
						&& $container->has($parts[1])) // PSR11
						$parts[1]=call_user_func([$container,'get'],$parts[1]);
					elseif (is_callable($container))
						$parts[1]=call_user_func($container,$parts[1],$args);
					elseif (is_string($container) &&
						is_subclass_of($container,'Prefab'))
						$parts[1]=call_user_func($container.'::instance')->
							get($parts[1]);
					else
                        throw new \Exception(sprintf(self::E_Class,
							$this->stringify($parts[1])));
				}
				else {
					$ref=new ReflectionClass($parts[1]);
					$parts[1]=method_exists($parts[1],'__construct') && $args?
						$ref->newinstanceargs($args):
						$ref->newinstance();
				}
			}
			$func=[$parts[1],$parts[3]];
		}
		return $func;
	}

	/**
	*	Execute callback/hooks (supports 'class->method' format)
	*	@return mixed|FALSE
	*	@param $func callback
	*	@param $args mixed
	*	@param $hooks string
	**/
	function call($func,$args=NULL,$hooks='') {
		if (!is_array($args))
			$args=[$args];
		// Grab the real handler behind the string representation
		if (is_string($func))
			$func=$this->grab($func,$args);
		// Execute function; abort if callback/hook returns FALSE
		if (!is_callable($func))
			// No route handler
			if ($hooks=='beforeroute,afterroute') {
				$allowed=[];
				if (is_array($func))
					$allowed=array_intersect(
						array_map('strtoupper',get_class_methods($func[0])),
						explode('|',self::VERBS)
					);
				header('Allow: '.implode(',',$allowed));
				$this->error(405);
			}
			else
                throw new \Exception(sprintf(self::E_Method,
					is_string($func)?$func:$this->stringify($func)));
		$obj=FALSE;
		if (is_array($func)) {
			$hooks=$this->split($hooks);
			$obj=TRUE;
		}
		// Execute pre-route hook if any
		if ($obj && $hooks && in_array($hook='beforeroute',$hooks) &&
			method_exists($func[0],$hook) &&
			call_user_func_array([$func[0],$hook],$args)===FALSE)
			return FALSE;
		// Execute callback
		$out=call_user_func_array($func,$args?:[]);
		if ($out===FALSE)
			return FALSE;
		// Execute post-route hook if any
		if ($obj && $hooks && in_array($hook='afterroute',$hooks) &&
			method_exists($func[0],$hook) &&
			call_user_func_array([$func[0],$hook],$args)===FALSE)
			return FALSE;
		return $out;
	}

	/**
	*	Execute specified callbacks in succession; Apply same arguments
	*	to all callbacks
	*	@return array
	*	@param $funcs array|string
	*	@param $args mixed
	**/
	function chain($funcs,$args=NULL) {
		$out=[];
		foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func)
			$out[]=$this->call($func,$args);
		return $out;
	}

	/**
	*	Execute specified callbacks in succession; Relay result of
	*	previous callback as argument to the next callback
	*	@return array
	*	@param $funcs array|string
	*	@param $args mixed
	**/
	function relay($funcs,$args=NULL) {
		foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func)
			$args=[$this->call($func,$args)];
		return array_shift($args);
	}

	/**
	*	Configure framework according to .ini-style file settings;
	*	If optional 2nd arg is provided, template strings are interpreted
	*	@return object
	*	@param $source string|array
	*	@param $allow bool
	**/
	function config($source,$allow=FALSE) {
		if (is_string($source))
			$source=$this->split($source);
		if ($allow)
			$preview=Preview::instance();
		foreach ($source as $file) {
			preg_match_all(
				'/(?<=^|\n)(?:'.
					'\[(?<section>.+?)\]|'.
					'(?<lval>[^\h\r\n;].*?)\h*=\h*'.
					'(?<rval>(?:\\\\\h*\r?\n|.+?)*)'.
				')(?=\r?\n|$)/',
				$this->read($file),
				$matches,PREG_SET_ORDER);
			if ($matches) {
				$sec='globals';
				$cmd=[];
				foreach ($matches as $match) {
					if ($match['section']) {
						$sec=$match['section'];
						if (preg_match(
							'/^(?!(?:global|config|route|map|redirect)s\b)'.
							'(.*?)(?:\s*[:>])/i',$sec,$msec) &&
							!$this->exists($msec[1]))
							$this->set($msec[1],NULL);
						preg_match('/^(config|route|map|redirect)s\b|'.
							'^(.+?)\s*\>\s*(.*)/i',$sec,$cmd);
						continue;
					}
					if ($allow)
						foreach (['lval','rval'] as $ndx)
							$match[$ndx]=$preview->
								resolve($match[$ndx],NULL,0,FALSE,FALSE);
					if (!empty($cmd)) {
						isset($cmd[3])?
						$this->call($cmd[3],
							[$match['lval'],$match['rval'],$cmd[2]]):
						call_user_func_array(
							[$this,$cmd[1]],
							array_merge([$match['lval']],
								str_getcsv($cmd[1]=='config'?
								$this->cast($match['rval']):
									$match['rval'],",",'"', "\\"))
						);
					}
					else {
						$rval=preg_replace(
							'/\\\\\h*(\r?\n)/','\1',$match['rval']);
						$ttl=NULL;
						if (preg_match('/^(.+)\|\h*(\d+)$/',$rval,$tmp)) {
							array_shift($tmp);
							list($rval,$ttl)=$tmp;
						}
						$args=array_map(
							function($val) {
								$val=$this->cast($val);
								if (is_string($val))
									$val=strlen($val)?
										preg_replace('/\\\\"/','"',$val):
										NULL;
								return $val;
							},
							// Mark quoted strings with 0x00 whitespace
							str_getcsv(preg_replace(
								'/(?<!\\\\)(")(.*?)\1/',
								"\\1\x00\\2\\1",trim($rval)),",",'"', "\\")
						);
						preg_match('/^(?<section>[^:]+)(?:\:(?<func>.+))?/',
							$sec,$parts);
						$func=isset($parts['func'])?$parts['func']:NULL;
						$custom=(strtolower($parts['section'])!='globals');
						if ($func)
							$args=[$this->call($func,$args)];
						if (count($args)>1)
							$args=[$args];
						if (isset($ttl))
							$args=array_merge($args,[$ttl]);
						call_user_func_array(
							[$this,'set'],
							array_merge(
								[
									($custom?($parts['section'].'.'):'').
									$match['lval']
								],
								$args
							)
						);
					}
				}
			}
		}
		return $this;
	}

	/**
	*	Create mutex, invoke callback then drop ownership when done
	*	@return mixed
	*	@param $id string
	*	@param $func callback
	*	@param $args mixed
	**/
	function mutex($id,$func,$args=NULL) {
		if (!is_dir($tmp=$this->hive['TEMP']))
			mkdir($tmp,self::MODE,TRUE);
		// Use filesystem lock
		if (is_file($lock=$tmp.
			$this->hive['SEED'].'.'.$this->hash($id).'.lock') &&
			filemtime($lock)+ini_get('max_execution_time')<microtime(TRUE))
			// Stale lock
			@unlink($lock);
		while (!($handle=@fopen($lock,'x')) && !connection_aborted())
			usleep(mt_rand(0,100));
		$this->locks[$id]=$lock;
		$out=$this->call($func,$args);
		fclose($handle);
		@unlink($lock);
		unset($this->locks[$id]);
		return $out;
	}

	/**
	*	Read file (with option to apply Unix LF as standard line ending)
	*	@return string
	*	@param $file string
	*	@param $lf bool
	**/
	function read($file, $lf = FALSE)
	{
		if (!file_exists($file)) {
			return false;
		}
		
		$out = file_get_contents($file);

		return $lf ? preg_replace('/\r\n|\r/', "\n", $out) : $out;
	}

	/**
	*	Exclusive file write
	*	@return int|FALSE
	*	@param $file string
	*	@param $data mixed
	*	@param $append bool
	**/
	function write($file,$data,$append=FALSE) {
		return file_put_contents($file,$data,$this->hive['LOCK']|($append?FILE_APPEND:0));
	}

	/**
	*	Apply syntax highlighting
	*	@return string
	*	@param $text string
	**/
	function highlight($text) {
		$out='';
		$pre=FALSE;
		$text=trim($text);
		if ($text && !preg_match('/^<\?php/',$text)) {
			$text='<?php '.$text;
			$pre=TRUE;
		}
		foreach (token_get_all($text) as $token)
			if ($pre)
				$pre=FALSE;
			else
				$out.='<span'.
					(is_array($token)?
						(' class="'.
							substr(strtolower(token_name($token[0])),2).'">'.
							$this->encode($token[1]).''):
						('>'.$this->encode($token))).
					'</span>';
		return $out?('<code>'.$out.'</code>'):$text;
	}

	/**
	*	Dump expression with syntax highlighting
	*	@param $expr mixed
	**/
	function dump($expr) {
		echo $this->highlight($this->stringify($expr));
	}

	/**
	*	Return path (and query parameters) relative to the base directory
	*	@return string
	*	@param $url string
	**/
	function rel($url) {
		return preg_replace('/^(?:https?:\/\/)?'.
			preg_quote($this->hive['BASE'],'/').'(\/.*|$)/','\1',$url);
	}

	/**
	*	Namespace-aware class autoloader
	*	@return mixed
	*	@param $class string
	**/
	protected function autoload($class) {
		$class=$this->fixslashes(ltrim($class,'\\'));
		/** @var callable $func */
		$func=NULL;
		if (is_array($path=$this->hive['AUTOLOAD']) &&
			isset($path[1]) && is_callable($path[1]))
			list($path,$func)=$path;
		foreach ($this->split($this->hive['PLUGINS'].';'.$path) as $auto)
			if (($func && is_file($file=$func($auto.$class).'.php')) ||
				is_file($file=$auto.$class.'.php') ||
				is_file($file=$auto.strtolower($class).'.php') ||
				is_file($file=strtolower($auto.$class).'.php'))
				return require($file);
	}

	/**
	*	Execute framework/application shutdown sequence
	*	@param $cwd string
	**/
	function unload($cwd) {
		chdir($cwd);
		if (!($error=error_get_last()) &&
			session_status()==PHP_SESSION_ACTIVE)
			session_commit();
		foreach ($this->locks as $lock)
			@unlink($lock);
		$handler=$this->hive['UNLOAD'];
		if ((!$handler || $this->call($handler,$this)===FALSE) &&
			$error && in_array($error['type'],
			[E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR]))
			// Fatal error detected
			$this->error(500,
				sprintf(self::E_Fatal,$error['message']),[$error]);
	}

	/**
	*	Convenience method for checking hive key
	*	@return mixed
	*	@param $key string
	**/
	#[\ReturnTypeWillChange]
	function offsetexists($key) {
		return $this->exists($key);
	}

	/**
	*	Convenience method for assigning hive value
	*	@return mixed
	*	@param $key string
	*	@param $val mixed
	**/
	#[\ReturnTypeWillChange]
	function offsetset($key,$val) {
		return $this->set($key,$val);
	}

	/**
	*	Convenience method for retrieving hive value
	*	@return mixed
	*	@param $key string
	**/
	#[\ReturnTypeWillChange]
	function &offsetget($key) {
		$val=&$this->ref($key);
		return $val;
	}

	/**
	*	Convenience method for removing hive key
	*	@param $key string
	**/
	#[\ReturnTypeWillChange]
	function offsetunset($key) {
		$this->clear($key);
	}

	/**
	*	Alias for offsetexists()
	*	@return mixed
	*	@param $key string
	**/
	function __isset($key) {
		return $this->offsetexists($key);
	}

	/**
	*	Alias for offsetset()
	*	@return mixed
	*	@param $key string
	*	@param $val mixed
	**/
	function __set($key,$val) {
		return $this->offsetset($key,$val);
	}

	/**
	*	Alias for offsetget()
	*	@return mixed
	*	@param $key string
	**/
	function &__get($key) {
		$val=&$this->offsetget($key);
		return $val;
	}

	/**
	*	Alias for offsetunset()
	*	@param $key string
	**/
	function __unset($key) {
		$this->offsetunset($key);
	}

	/**
	*	Call function identified by hive key
	*	@return mixed
	*	@param $key string
	*	@param $args array
	**/
	function __call($key,array $args) {
		if ($this->exists($key,$val))
			return call_user_func_array($val,$args);
        throw new \Exception(sprintf(self::E_Method,$key));
	}

	//! Prohibit cloning
	private function __clone() {
	}

	//! Bootstrap
	function __construct() {
		// Managed directives
		ini_set('default_charset',$charset='UTF-8');
		if (extension_loaded('mbstring'))
			mb_internal_encoding($charset);
		ini_set('display_errors',0);
		// Deprecated directives
		@ini_set('magic_quotes_gpc',0);
		@ini_set('register_globals',0);
		// Intercept errors/exceptions; PHP5.3-compatible
		if (PHP_VERSION_ID >= 80400) {
			$check = error_reporting((E_ALL) & ~(E_NOTICE | E_USER_NOTICE));
		} else {
			$check = error_reporting((E_ALL | E_STRICT) & ~(E_NOTICE | E_USER_NOTICE));
		}
		set_exception_handler(
			function($obj) {
				/** @var Exception $obj */
				$this->hive['EXCEPTION']=$obj;
				$this->error(500,
					$obj->getmessage().' '.
					'['.$obj->getFile().':'.$obj->getLine().']',
					$obj->gettrace());
			}
		);
		set_error_handler(
			function($level,$text,$file,$line) {
				if ($level & error_reporting()) {
					$trace=$this->trace(null, false);
					array_unshift($trace,['file'=>$file,'line'=>$line]);
					$this->error(500,$text,$trace,$level);
				}
			}
		);
		if (!isset($_SERVER['SERVER_NAME']) || $_SERVER['SERVER_NAME']==='')
			$_SERVER['SERVER_NAME']=gethostname();
		$headers=[];
		if ($cli=(PHP_SAPI=='cli')) {
			// Emulate HTTP request
			$_SERVER['REQUEST_METHOD']='GET';
			if (!isset($_SERVER['argv'][1])) {
				++$_SERVER['argc'];
				$_SERVER['argv'][1]='/';
			}
			$req=$query='';
			if (substr($_SERVER['argv'][1],0,1)=='/') {
				$req=$_SERVER['argv'][1];
				$query=parse_url($req,PHP_URL_QUERY);
			} else {
				foreach($_SERVER['argv'] as $i=>$arg) {
					if (!$i) continue;
					if (preg_match('/^\-(\-)?(\w+)(?:\=(.*))?$/',$arg,$m)) {
						foreach($m[1]?[$m[2]]:str_split($m[2]) as $k)
							$query.=($query?'&':'').urlencode($k).'=';
						if (isset($m[3]))
							$query.=urlencode($m[3]);
					} else
						$req.='/'.$arg;
				}
				if (!$req)
					$req='/';
				if ($query)
					$req.='?'.$query;
			}
			$_SERVER['REQUEST_URI']=$req;
			parse_str($query?:'',$GLOBALS['_GET']);
		}
		elseif (function_exists('getallheaders')) {
			foreach (getallheaders() as $key=>$val) {
				$tmp=strtoupper(strtr($key,'-','_'));
				// TODO: use ucwords delimiters for php 5.4.32+ & 5.5.16+
				$key=strtr(ucwords(strtolower(strtr($key,'-',' '))),' ','-');
				$headers[$key]=$val;
				if (isset($_SERVER['HTTP_'.$tmp]))
					$headers[$key]=&$_SERVER['HTTP_'.$tmp];
			}
		}
		else {
			if (isset($_SERVER['CONTENT_LENGTH']))
				$headers['Content-Length']=&$_SERVER['CONTENT_LENGTH'];
			if (isset($_SERVER['CONTENT_TYPE']))
				$headers['Content-Type']=&$_SERVER['CONTENT_TYPE'];
			foreach (array_keys($_SERVER) as $key)
				if (substr($key,0,5)=='HTTP_')
					$headers[strtr(ucwords(strtolower(strtr(
						substr($key,5),'_',' '))),' ','-')]=&$_SERVER[$key];
		}
		if (isset($headers['X-Http-Method-Override']))
			$_SERVER['REQUEST_METHOD']=$headers['X-Http-Method-Override'];
		elseif ($_SERVER['REQUEST_METHOD']=='POST' && isset($_POST['_method']))
			$_SERVER['REQUEST_METHOD']=strtoupper($_POST['_method']);
		$scheme=isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']=='on' ||
			isset($headers['X-Forwarded-Proto']) &&
			$headers['X-Forwarded-Proto']=='https'?'https':'http';
		// Create hive early on to expose header methods
		$this->hive=['HEADERS'=>&$headers];
		if (function_exists('apache_setenv')) {
			// Work around Apache pre-2.4 VirtualDocumentRoot bug
			$_SERVER['DOCUMENT_ROOT']=str_replace($_SERVER['SCRIPT_NAME'],'',
				$_SERVER['SCRIPT_FILENAME']);
			apache_setenv("DOCUMENT_ROOT",$_SERVER['DOCUMENT_ROOT']);
		}
		$_SERVER['DOCUMENT_ROOT']=realpath($_SERVER['DOCUMENT_ROOT']);
		$base='';
		if (!$cli)
			$base=rtrim($this->fixslashes(
				dirname($_SERVER['SCRIPT_NAME'])),'/');
		$uri=parse_url((preg_match('/^\w+:\/\//',$_SERVER['REQUEST_URI'])?'':
				$scheme.'://'.$_SERVER['SERVER_NAME']).$_SERVER['REQUEST_URI']);
		$_SERVER['REQUEST_URI']=$uri['path'].
			(isset($uri['query'])?'?'.$uri['query']:'').
			(isset($uri['fragment'])?'#'.$uri['fragment']:'');
		$path=preg_replace('/^'.preg_quote($base,'/').'/','',$uri['path']);
		$jar=[
			'expire'=>0,
			'lifetime'=>0,
			'path'=>$base?:'/',
			'domain'=>is_int(strpos($_SERVER['SERVER_NAME'],'.')) &&
				!filter_var($_SERVER['SERVER_NAME'],FILTER_VALIDATE_IP)?
				$_SERVER['SERVER_NAME']:'',
			'secure'=>($scheme=='https'),
			'httponly'=>TRUE,
			'samesite'=>'Lax',
		];
		$port=80;
		if (!empty($headers['X-Forwarded-Port']))
			$port=$headers['X-Forwarded-Port'];
		elseif (!empty($_SERVER['SERVER_PORT']))
			$port=$_SERVER['SERVER_PORT'];
		// Default configuration
		$this->hive+=[
			'AGENT'=>$this->agent(),
			'AJAX'=>$this->ajax(),
			'ALIAS'=>NULL,
			'ALIASES'=>[],
			'AUTOLOAD'=>'./',
			'BASE'=>$base,
			'BITMASK'=>ENT_COMPAT,
			'BODY'=>NULL,
			'CACHE'=>FALSE,
			'CASELESS'=>TRUE,
			'CLI'=>$cli,
			'CORS'=>[],
			'DEBUG'=>0,
			'DIACRITICS'=>[],
			'DNSBL'=>'',
			'EMOJI'=>[],
			'ENCODING'=>$charset,
			'ERROR'=>NULL,
			'ESCAPE'=>TRUE,
			'EXCEPTION'=>NULL,
			'EXEMPT'=>NULL,
			'FALLBACK'=>$this->fallback,
			'FORMATS'=>[],
			'FRAGMENT'=>isset($uri['fragment'])?$uri['fragment']:'',
			'HALT'=>TRUE,
			'HIGHLIGHT'=>FALSE,
			'HOST'=>$_SERVER['SERVER_NAME'],
			'IP'=>$this->ip(),
			'JAR'=>$jar,
			'LANGUAGE'=>isset($headers['Accept-Language'])?
				$this->language($headers['Accept-Language']):
				$this->fallback,
			'LOCALES'=>'./',
			'LOCK'=>LOCK_EX,
			'LOGGABLE'=>'*',
			'LOGS'=>'./',
			'MB'=>extension_loaded('mbstring'),
			'ONERROR'=>NULL,
			'ONREROUTE'=>NULL,
			'PACKAGE'=>self::PACKAGE,
			'PARAMS'=>[],
			'PATH'=>$path,
			'REROUTE_TRAILING_SLASH'=>TRUE,
			'PATTERN'=>NULL,
			'PLUGINS'=>$this->fixslashes(__DIR__).'/',
			'PORT'=>$port,
			'PREFIX'=>NULL,
			'PREMAP'=>'',
			'QUERY'=>isset($uri['query'])?$uri['query']:'',
			'QUIET'=>FALSE,
			'RAW'=>FALSE,
			'REALM'=>$scheme.'://'.$_SERVER['SERVER_NAME'].
				(!in_array($port,[80,443])?(':'.$port):'').
				$_SERVER['REQUEST_URI'],
			'RESPONSE'=>'',
			'ROOT'=>$_SERVER['DOCUMENT_ROOT'],
			'ROUTES'=>[],
			'SCHEME'=>$scheme,
			'SEED'=>$this->hash($_SERVER['SERVER_NAME'].$base),
			'SERIALIZER'=>extension_loaded($ext='igbinary')?$ext:'php',
			'TEMP'=>'tmp/',
			'TIME'=>&$_SERVER['REQUEST_TIME_FLOAT'],
			'TZ'=>@date_default_timezone_get(),
			'UI'=>'./',
			'UNLOAD'=>NULL,
			'UPLOADS'=>'./',
			'URI'=>&$_SERVER['REQUEST_URI'],
			'VERB'=>&$_SERVER['REQUEST_METHOD'],
			'VERSION'=>self::VERSION,
			'XFRAME'=>'SAMEORIGIN'
		];
		$this->hive['CORS']+=[
			'headers'=>'',
			'origin'=>FALSE,
			'credentials'=>FALSE,
			'expose'=>FALSE,
			'ttl'=>0
		];
		if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) {
			unset($jar['expire']);
			session_cache_limiter('');
			if (version_compare(PHP_VERSION, '7.3.0') >= 0)
				session_set_cookie_params($jar);
			else {
				unset($jar['samesite']);
				call_user_func_array('session_set_cookie_params',$jar);
			}
		}
		if (PHP_SAPI=='cli-server' &&
			preg_match('/^'.preg_quote($base,'/').'$/',$this->hive['URI']))
			$this->reroute('/');
		if (ini_get('auto_globals_jit')) {
			// Override setting
			$GLOBALS['_ENV']=$_ENV;
			$GLOBALS['_REQUEST']=$_REQUEST;
		}
		// Sync PHP globals with corresponding hive keys
		$this->init=$this->hive;
		foreach (explode('|',self::GLOBALS) as $global) {
			$sync=$this->sync($global);
			$this->init+=[
				$global=>preg_match('/SERVER|ENV/',$global)?$sync:[]
			];
		}
		if ($check && $error=error_get_last())
			// Error detected
			$this->error(500,
				sprintf(self::E_Fatal,$error['message']),[$error]);
		date_default_timezone_set($this->hive['TZ']);
		// Register framework autoloader
		spl_autoload_register([$this,'autoload']);
		// Register shutdown handler
		register_shutdown_function([$this,'unload'],getcwd());
	}

}

//! Cache engine
class Cache extends Prefab {

	protected
		//! Cache DSN
		$dsn,
		//! Prefix for cache entries
		$prefix,
		//! MemCache or Redis object
		$ref;

	/**
	*	Return timestamp and TTL of cache entry or FALSE if not found
	*	@return array|FALSE
	*	@param $key string
	*	@param $val mixed
	**/
	function exists($key,&$val=NULL) {
		$fw=Base::instance();
		if (!$this->dsn)
			return FALSE;
		$ndx=$this->prefix.'.'.$key;
		$parts=explode('=',$this->dsn,2);
		switch ($parts[0]) {
			case 'apc':
			case 'apcu':
				$raw=call_user_func($parts[0].'_fetch',$ndx);
				break;
			case 'redis':
				$raw=$this->ref->get($ndx);
				break;
			case 'memcache':
				$raw=memcache_get($this->ref,$ndx);
				break;
			case 'memcached':
				$raw=$this->ref->get($ndx);
				break;
			case 'wincache':
				$raw=wincache_ucache_get($ndx);
				break;
			case 'xcache':
				$raw=xcache_get($ndx);
				break;
			case 'folder':
				$raw=$fw->read($parts[1]
					.str_replace(['/','\\'],'',$ndx));
				break;
		}
		if (!empty($raw)) {
			list($val,$time,$ttl)=(array)$fw->unserialize($raw);
			if ($ttl===0 || $time+$ttl>microtime(TRUE))
				return [$time,$ttl];
			$val=null;
			$this->clear($key);
		}
		return FALSE;
	}

	/**
	*	Store value in cache
	*	@return mixed|FALSE
	*	@param $key string
	*	@param $val mixed
	*	@param $ttl int
	**/
	function set($key,$val,$ttl=0) {
		$fw=Base::instance();
		if (!$this->dsn)
			return TRUE;
		$ndx=$this->prefix.'.'.$key;
		if ($cached=$this->exists($key))
			$ttl=$cached[1];
		$data=$fw->serialize([$val,microtime(TRUE),$ttl]);
		$parts=explode('=',$this->dsn,2);
		switch ($parts[0]) {
			case 'apc':
			case 'apcu':
				return call_user_func($parts[0].'_store',$ndx,$data,$ttl);
			case 'redis':
				return $this->ref->set($ndx,$data,$ttl?['ex'=>$ttl]:[]);
			case 'memcache':
				return memcache_set($this->ref,$ndx,$data,0,$ttl);
			case 'memcached':
				return $this->ref->set($ndx,$data,$ttl);
			case 'wincache':
				return wincache_ucache_set($ndx,$data,$ttl);
			case 'xcache':
				return xcache_set($ndx,$data,$ttl);
			case 'folder':
				return $fw->write($parts[1].
					str_replace(['/','\\'],'',$ndx),$data);
		}
		return FALSE;
	}

	/**
	*	Retrieve value of cache entry
	*	@return mixed|FALSE
	*	@param $key string
	**/
	function get($key) {
		return $this->dsn && $this->exists($key,$data)?$data:FALSE;
	}

	/**
	*	Delete cache entry
	*	@return bool
	*	@param $key string
	**/
	function clear($key) {
		if (!$this->dsn)
			return;
		$ndx=$this->prefix.'.'.$key;
		$parts=explode('=',$this->dsn,2);
		switch ($parts[0]) {
			case 'apc':
			case 'apcu':
				return call_user_func($parts[0].'_delete',$ndx);
			case 'redis':
				return $this->ref->del($ndx);
			case 'memcache':
				return memcache_delete($this->ref,$ndx);
			case 'memcached':
				return $this->ref->delete($ndx);
			case 'wincache':
				return wincache_ucache_delete($ndx);
			case 'xcache':
				return xcache_unset($ndx);
			case 'folder':
				return @unlink($parts[1].$ndx);
		}
		return FALSE;
	}

	/**
	*	Clear contents of cache backend
	*	@return bool
	*	@param $suffix string
	**/
	function reset($suffix=NULL) {
		if (!$this->dsn)
			return TRUE;
		$regex='/'.preg_quote($this->prefix.'.','/').'.*'.
			preg_quote($suffix?:'','/').'/';
		$parts=explode('=',$this->dsn,2);
		switch ($parts[0]) {
			case 'apc':
			case 'apcu':
				$info=call_user_func($parts[0].'_cache_info',
					$parts[0]=='apcu'?FALSE:'user');
				if (!empty($info['cache_list'])) {
					$key=array_key_exists('info',
						$info['cache_list'][0])?'info':'key';
					foreach ($info['cache_list'] as $item)
						if (preg_match($regex,$item[$key]))
							call_user_func($parts[0].'_delete',$item[$key]);
				}
				return TRUE;
			case 'redis':
				$keys=$this->ref->keys($this->prefix.'.*'.$suffix);
				foreach($keys as $key)
					$this->ref->del($key);
				return TRUE;
			case 'memcache':
				foreach (memcache_get_extended_stats(
					$this->ref,'slabs') as $slabs)
					foreach (array_filter(array_keys($slabs),'is_numeric')
						as $id)
						foreach (memcache_get_extended_stats(
							$this->ref,'cachedump',$id) as $data)
							if (is_array($data))
								foreach (array_keys($data) as $key)
									if (preg_match($regex,$key))
										memcache_delete($this->ref,$key);
				return TRUE;
			case 'memcached':
				foreach ($this->ref->getallkeys()?:[] as $key)
					if (preg_match($regex,$key))
						$this->ref->delete($key);
				return TRUE;
			case 'wincache':
				$info=wincache_ucache_info();
				foreach ($info['ucache_entries'] as $item)
					if (preg_match($regex,$item['key_name']))
						wincache_ucache_delete($item['key_name']);
				return TRUE;
			case 'xcache':
				if ($suffix && !ini_get('xcache.admin.enable_auth')) {
					$cnt=xcache_count(XC_TYPE_VAR);
					for ($i=0;$i<$cnt;++$i) {
						$list=xcache_list(XC_TYPE_VAR,$i);
						foreach ($list['cache_list'] as $item)
							if (preg_match($regex,$item['name']))
								xcache_unset($item['name']);
					}
				} else
					xcache_unset_by_prefix($this->prefix.'.');
				return TRUE;
			case 'folder':
				if ($glob=@glob($parts[1].'*'))
					foreach ($glob as $file)
						if (preg_match($regex,basename($file)))
							@unlink($file);
				return TRUE;
		}
		return FALSE;
	}

	/**
	*	Load/auto-detect cache backend
	*	@return string
	*	@param $dsn bool|string
	*	@param $seed bool|string
	**/
	function load($dsn,$seed=NULL) {
		$fw=Base::instance();
		if ($dsn=trim($dsn)) {
			if (preg_match('/^redis=(.+)/',$dsn,$parts) &&
				extension_loaded('redis')) {
				list($host,$port,$db,$password)=explode(':',$parts[1])+[1=>6379,2=>NULL,3=>NULL];
				$this->ref=new Redis;
				if(!$this->ref->connect($host,$port,2))
					$this->ref=NULL;
				if(!empty($password))
					$this->ref->auth($password);
				if(isset($db))
					$this->ref->select($db);
			}
			elseif (preg_match('/^memcache=(.+)/',$dsn,$parts) &&
				extension_loaded('memcache'))
				foreach ($fw->split($parts[1]) as $server) {
					list($host,$port)=explode(':',$server)+[1=>11211];
					if (empty($this->ref))
						$this->ref=@memcache_connect($host,$port)?:NULL;
					else
						memcache_add_server($this->ref,$host,$port);
				}
			elseif (preg_match('/^memcached=(.+)/',$dsn,$parts) &&
				extension_loaded('memcached'))
				foreach ($fw->split($parts[1]) as $server) {
					list($host,$port)=explode(':',$server)+[1=>11211];
					if (empty($this->ref))
						$this->ref=new Memcached();
					$this->ref->addServer($host,$port);
				}
			if (empty($this->ref) && !preg_match('/^folder\h*=/',$dsn))
				$dsn=($grep=preg_grep('/^(apc|wincache|xcache)/',
					array_map('strtolower',get_loaded_extensions())))?
						// Auto-detect
						current($grep):
						// Use filesystem as fallback
						('folder='.$fw->TEMP.'cache/');
			if (preg_match('/^folder\h*=\h*(.+)/',$dsn,$parts) &&
				!is_dir($parts[1]))
				mkdir($parts[1],Base::MODE,TRUE);
		}
		$this->prefix=$seed?:$fw->SEED;
		return $this->dsn=$dsn;
	}

	/**
	*	Class constructor
	*	@param $dsn bool|string
	**/
	function __construct($dsn=FALSE) {
		if ($dsn)
			$this->load($dsn);
	}

}

//! View handler
class View extends Prefab {

	private
		//! Temporary hive
		$temp;

	protected
		//! Template file
		$file,
		//! Post-rendering handler
		$trigger,
		//! Nesting level
		$level=0;

	/** @var \Base Framework instance */
	protected $fw;

	function __construct() {
		$this->fw=\Base::instance();
	}

	/**
	*	Encode characters to equivalent HTML entities
	*	@return string
	*	@param $arg mixed
	**/
	function esc($arg) {
		return $this->fw->recursive($arg,
			function($val) {
				return is_string($val)?$this->fw->encode($val):$val;
			}
		);
	}

	/**
	*	Decode HTML entities to equivalent characters
	*	@return string
	*	@param $arg mixed
	**/
	function raw($arg) {
		return $this->fw->recursive($arg,
			function($val) {
				return is_string($val)?$this->fw->decode($val):$val;
			}
		);
	}

	/**
	*	Create sandbox for template execution
	*	@return string
	*	@param $hive array
	*	@param $mime string
	**/
	protected function sandbox(?array $hive=NULL,$mime=NULL) {
		$fw=$this->fw;
		$implicit=FALSE;
		if (is_null($hive)) {
			$implicit=TRUE;
			$hive=$fw->hive();
		}
		if ($this->level<1 || $implicit) {
			if (!$fw->CLI && $mime && !headers_sent() &&
				!preg_grep ('/^Content-Type:/',headers_list()))
				header('Content-Type: '.$mime.'; '.
					'charset='.$fw->ENCODING);
			if ($fw->ESCAPE && (!$mime ||
					preg_match('/^(text\/html|(application|text)\/(.+\+)?xml)$/i',$mime)))
				$hive=$this->esc($hive);
			if (isset($hive['ALIASES']))
				$hive['ALIASES']=$fw->build($hive['ALIASES']);
		}
		$this->temp=$hive;
		unset($fw,$hive,$implicit,$mime);
		extract($this->temp);
		$this->temp=NULL;
		++$this->level;
		ob_start();
		require($this->file);
		--$this->level;
		return ob_get_clean();
	}

	/**
	*	Render template
	*	@return string
	*	@param $file string
	*	@param $mime string
	*	@param $hive array
	*	@param $ttl int
	**/
	function render($file,$mime='text/html',?array $hive=NULL,$ttl=0) {
		$fw=$this->fw;
		$cache=Cache::instance();
		foreach ($fw->split($fw->UI) as $dir) {
			if ($cache->exists($hash=$fw->hash($dir.$file),$data))
				return $data;
			if (is_file($this->file=$fw->fixslashes($dir.$file))) {
				if (isset($_COOKIE[session_name()]) &&
					!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)
					session_start();
				$fw->sync('SESSION');
				$data=$this->sandbox($hive,$mime);
				if (isset($this->trigger['afterrender']))
					foreach($this->trigger['afterrender'] as $func)
						$data=$fw->call($func,[$data, $dir.$file]);
				if ($ttl)
					$cache->set($hash,$data,$ttl);
				return $data;
			}
		}
        throw new \Exception(sprintf(Base::E_Open,$file));
	}

	/**
	*	post rendering handler
	*	@param $func callback
	*/
	function afterrender($func) {
		$this->trigger['afterrender'][]=$func;
	}

}

//! Lightweight template engine
class Preview extends View {

	protected
		//! token filter
		$filter=[
			'c'=>'$this->c',
			'esc'=>'$this->esc',
			'raw'=>'$this->raw',
			'export'=>'Base::instance()->export',
			'alias'=>'Base::instance()->alias',
			'format'=>'Base::instance()->format'
		];

	protected
		//! newline interpolation
		$interpolation=true;

	/**
	 * Enable/disable markup parsing interpolation
	 * mainly used for adding appropriate newlines
	 * @param $bool bool
	 */
	function interpolation($bool) {
		$this->interpolation=$bool;
	}

	/**
	*	Return C-locale equivalent of number
	*	@return string
	*	@param $val int|float
	**/
	function c($val) {
		$locale=setlocale(LC_NUMERIC,0);
		setlocale(LC_NUMERIC,'C');
		$out=(string)(float)$val;
		$locale=setlocale(LC_NUMERIC,$locale);
		return $out;
	}

	/**
	*	Convert token to variable
	*	@return string
	*	@param $str string
	**/
	function token($str) {
		$str=trim(preg_replace('/\{\{(.+?)\}\}/s','\1',$this->fw->compile($str)));
		if (preg_match('/^(.+)(?<!\|)\|((?:\h*\w+(?:\h*[,;]?))+)$/s',
			$str,$parts)) {
			$str=trim($parts[1]);
			foreach ($this->fw->split(trim($parts[2],"\xC2\xA0")) as $func)
				$str=((empty($this->filter[$cmd=$func]) &&
					function_exists($cmd)) ||
					is_string($cmd=$this->filter($func)))?
					$cmd.'('.$str.')':
					'Base::instance()->'.
						'call($this->filter(\''.$func.'\'),['.$str.'])';
		}
		return $str;
	}

	/**
	*	Register or get (one specific or all) token filters
	*	@param string $key
	*	@param string|closure $func
	*	@return array|closure|string
	*/
	function filter($key=NULL,$func=NULL) {
		if (!$key)
			return array_keys($this->filter);
		$key=strtolower($key);
		if (!$func)
			return $this->filter[$key];
		$this->filter[$key]=$func;
	}

	/**
	*	Assemble markup
	*	@return string
	*	@param $node string
	**/
	protected function build($node) {
		return preg_replace_callback(
			'/\{~(.+?)~\}|\{\*(.+?)\*\}|\{\-(.+?)\-\}|'.
			'\{\{(.+?)\}\}((\r?\n)*)/s',
			function($expr) {
				if ($expr[1])
					$str='<?php '.$this->token($expr[1]).' ?>';
				elseif ($expr[2])
					return '';
				elseif ($expr[3])
					$str=$expr[3];
				else {
					$str='<?= ('.trim($this->token($expr[4])).')'.
						($this->interpolation?
							(!empty($expr[6])?'."'.$expr[6].'"':''):'').' ?>';
					if (isset($expr[5]))
						$str.=$expr[5];
				}
				return $str;
			},
			$node
		);
	}

	/**
	*	Render template string
	*	@return string
	*	@param $node string|array
	*	@param $hive array
	*	@param $ttl int
	*	@param $persist bool
	*	@param $escape bool
	**/
	function resolve($node,?array $hive=NULL,$ttl=0,$persist=FALSE,$escape=NULL) {
		$hash=null;
		$fw=$this->fw;
		$cache=Cache::instance();
		if ($escape!==NULL) {
			$esc=$fw->ESCAPE;
			$fw->ESCAPE=$escape;
		}
		if ($ttl || $persist)
			$hash=$fw->hash($fw->serialize($node));
		if ($ttl && $cache->exists($hash,$data))
			return $data;
		if ($persist) {
			if (!is_dir($tmp=$fw->TEMP))
				mkdir($tmp,Base::MODE,TRUE);
			if (!is_file($this->file=($tmp.
				$fw->SEED.'.'.$hash.'.php')))
				$fw->write($this->file,$this->build($node));
			if (isset($_COOKIE[session_name()]) &&
				!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)
				session_start();
			$fw->sync('SESSION');
			$data=$this->sandbox($hive);
		}
		else {
			if (!$hive)
				$hive=$fw->hive();
			if ($fw->ESCAPE)
				$hive=$this->esc($hive);
			extract($hive);
			unset($hive);
			ob_start();
			eval(' ?>'.$this->build($node).'<?php ');
			$data=ob_get_clean();
		}
		if ($ttl)
			$cache->set($hash,$data,$ttl);
		if ($escape!==NULL)
			$fw->ESCAPE=$esc;
		return $data;
	}

	/**
	 *	Parse template string
	 *	@return string
	 *	@param $text string
	 **/
	function parse($text) {
		// Remove PHP code and comments
		return preg_replace(
			'/\h*<\?(?!xml)(?:php|\s*=)?.+?\?>\h*|'.
			'\{\*.+?\*\}/is','', $text);
	}

	/**
	*	Render template
	*	@return string
	*	@param $file string
	*	@param $mime string
	*	@param $hive array
	*	@param $ttl int
	**/
	function render($file,$mime='text/html',?array $hive=NULL,$ttl=0) {
		$fw=$this->fw;
		$cache=Cache::instance();
		if (!is_dir($tmp=$fw->TEMP))
			mkdir($tmp,Base::MODE,TRUE);
		foreach ($fw->split($fw->UI) as $dir) {
			if ($cache->exists($hash=$fw->hash($dir.$file),$data))
				return $data;
			if (is_file($view=$fw->fixslashes($dir.$file))) {
				if (!is_file($this->file=($tmp.
					$fw->SEED.'.'.$fw->hash($view).'.php')) ||
					filemtime($this->file)<filemtime($view)) {
					$contents=$fw->read($view);
					if (isset($this->trigger['beforerender']))
						foreach ($this->trigger['beforerender'] as $func)
							$contents=$fw->call($func, [$contents, $view]);
					$text=$this->parse($contents);
					$fw->write($this->file,$this->build($text));
				}
				if (isset($_COOKIE[session_name()]) &&
					!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)
					session_start();
				$fw->sync('SESSION');
				$data=$this->sandbox($hive,$mime);
				if(isset($this->trigger['afterrender']))
					foreach ($this->trigger['afterrender'] as $func)
						$data=$fw->call($func, [$data, $view]);
				if ($ttl)
					$cache->set($hash,$data,$ttl);
				return $data;
			}
		}
        throw new \Exception(sprintf(Base::E_Open,$file));
	}

	/**
	 *	post rendering handler
	 *	@param $func callback
	 */
	function beforerender($func) {
		$this->trigger['beforerender'][]=$func;
	}

}

//! ISO language/country codes
class ISO extends Prefab {

	//@{ ISO 3166-1 country codes
	const
		CC_af='Afghanistan',
		CC_ax='Åland Islands',
		CC_al='Albania',
		CC_dz='Algeria',
		CC_as='American Samoa',
		CC_ad='Andorra',
		CC_ao='Angola',
		CC_ai='Anguilla',
		CC_aq='Antarctica',
		CC_ag='Antigua and Barbuda',
		CC_ar='Argentina',
		CC_am='Armenia',
		CC_aw='Aruba',
		CC_au='Australia',
		CC_at='Austria',
		CC_az='Azerbaijan',
		CC_bs='Bahamas',
		CC_bh='Bahrain',
		CC_bd='Bangladesh',
		CC_bb='Barbados',
		CC_by='Belarus',
		CC_be='Belgium',
		CC_bz='Belize',
		CC_bj='Benin',
		CC_bm='Bermuda',
		CC_bt='Bhutan',
		CC_bo='Bolivia',
		CC_bq='Bonaire, Sint Eustatius and Saba',
		CC_ba='Bosnia and Herzegovina',
		CC_bw='Botswana',
		CC_bv='Bouvet Island',
		CC_br='Brazil',
		CC_io='British Indian Ocean Territory',
		CC_bn='Brunei Darussalam',
		CC_bg='Bulgaria',
		CC_bf='Burkina Faso',
		CC_bi='Burundi',
		CC_kh='Cambodia',
		CC_cm='Cameroon',
		CC_ca='Canada',
		CC_cv='Cape Verde',
		CC_ky='Cayman Islands',
		CC_cf='Central African Republic',
		CC_td='Chad',
		CC_cl='Chile',
		CC_cn='China',
		CC_cx='Christmas Island',
		CC_cc='Cocos (Keeling) Islands',
		CC_co='Colombia',
		CC_km='Comoros',
		CC_cg='Congo',
		CC_cd='Congo, The Democratic Republic of',
		CC_ck='Cook Islands',
		CC_cr='Costa Rica',
		CC_ci='Côte d\'ivoire',
		CC_hr='Croatia',
		CC_cu='Cuba',
		CC_cw='Curaçao',
		CC_cy='Cyprus',
		CC_cz='Czech Republic',
		CC_dk='Denmark',
		CC_dj='Djibouti',
		CC_dm='Dominica',
		CC_do='Dominican Republic',
		CC_ec='Ecuador',
		CC_eg='Egypt',
		CC_sv='El Salvador',
		CC_gq='Equatorial Guinea',
		CC_er='Eritrea',
		CC_ee='Estonia',
		CC_et='Ethiopia',
		CC_fk='Falkland Islands (Malvinas)',
		CC_fo='Faroe Islands',
		CC_fj='Fiji',
		CC_fi='Finland',
		CC_fr='France',
		CC_gf='French Guiana',
		CC_pf='French Polynesia',
		CC_tf='French Southern Territories',
		CC_ga='Gabon',
		CC_gm='Gambia',
		CC_ge='Georgia',
		CC_de='Germany',
		CC_gh='Ghana',
		CC_gi='Gibraltar',
		CC_gr='Greece',
		CC_gl='Greenland',
		CC_gd='Grenada',
		CC_gp='Guadeloupe',
		CC_gu='Guam',
		CC_gt='Guatemala',
		CC_gg='Guernsey',
		CC_gn='Guinea',
		CC_gw='Guinea-Bissau',
		CC_gy='Guyana',
		CC_ht='Haiti',
		CC_hm='Heard Island and McDonald Islands',
		CC_va='Holy See (Vatican City State)',
		CC_hn='Honduras',
		CC_hk='Hong Kong',
		CC_hu='Hungary',
		CC_is='Iceland',
		CC_in='India',
		CC_id='Indonesia',
		CC_ir='Iran, Islamic Republic of',
		CC_iq='Iraq',
		CC_ie='Ireland',
		CC_im='Isle of Man',
		CC_il='Israel',
		CC_it='Italy',
		CC_jm='Jamaica',
		CC_jp='Japan',
		CC_je='Jersey',
		CC_jo='Jordan',
		CC_kz='Kazakhstan',
		CC_ke='Kenya',
		CC_ki='Kiribati',
		CC_kp='Korea, Democratic People\'s Republic of',
		CC_kr='Korea, Republic of',
		CC_kw='Kuwait',
		CC_kg='Kyrgyzstan',
		CC_la='Lao People\'s Democratic Republic',
		CC_lv='Latvia',
		CC_lb='Lebanon',
		CC_ls='Lesotho',
		CC_lr='Liberia',
		CC_ly='Libya',
		CC_li='Liechtenstein',
		CC_lt='Lithuania',
		CC_lu='Luxembourg',
		CC_mo='Macao',
		CC_mk='Macedonia, The Former Yugoslav Republic of',
		CC_mg='Madagascar',
		CC_mw='Malawi',
		CC_my='Malaysia',
		CC_mv='Maldives',
		CC_ml='Mali',
		CC_mt='Malta',
		CC_mh='Marshall Islands',
		CC_mq='Martinique',
		CC_mr='Mauritania',
		CC_mu='Mauritius',
		CC_yt='Mayotte',
		CC_mx='Mexico',
		CC_fm='Micronesia, Federated States of',
		CC_md='Moldova, Republic of',
		CC_mc='Monaco',
		CC_mn='Mongolia',
		CC_me='Montenegro',
		CC_ms='Montserrat',
		CC_ma='Morocco',
		CC_mz='Mozambique',
		CC_mm='Myanmar',
		CC_na='Namibia',
		CC_nr='Nauru',
		CC_np='Nepal',
		CC_nl='Netherlands',
		CC_nc='New Caledonia',
		CC_nz='New Zealand',
		CC_ni='Nicaragua',
		CC_ne='Niger',
		CC_ng='Nigeria',
		CC_nu='Niue',
		CC_nf='Norfolk Island',
		CC_mp='Northern Mariana Islands',
		CC_no='Norway',
		CC_om='Oman',
		CC_pk='Pakistan',
		CC_pw='Palau',
		CC_ps='Palestinian Territory, Occupied',
		CC_pa='Panama',
		CC_pg='Papua New Guinea',
		CC_py='Paraguay',
		CC_pe='Peru',
		CC_ph='Philippines',
		CC_pn='Pitcairn',
		CC_pl='Poland',
		CC_pt='Portugal',
		CC_pr='Puerto Rico',
		CC_qa='Qatar',
		CC_re='Réunion',
		CC_ro='Romania',
		CC_ru='Russian Federation',
		CC_rw='Rwanda',
		CC_bl='Saint Barthélemy',
		CC_sh='Saint Helena, Ascension and Tristan da Cunha',
		CC_kn='Saint Kitts and Nevis',
		CC_lc='Saint Lucia',
		CC_mf='Saint Martin (French Part)',
		CC_pm='Saint Pierre and Miquelon',
		CC_vc='Saint Vincent and The Grenadines',
		CC_ws='Samoa',
		CC_sm='San Marino',
		CC_st='Sao Tome and Principe',
		CC_sa='Saudi Arabia',
		CC_sn='Senegal',
		CC_rs='Serbia',
		CC_sc='Seychelles',
		CC_sl='Sierra Leone',
		CC_sg='Singapore',
		CC_sk='Slovakia',
		CC_sx='Sint Maarten (Dutch Part)',
		CC_si='Slovenia',
		CC_sb='Solomon Islands',
		CC_so='Somalia',
		CC_za='South Africa',
		CC_gs='South Georgia and The South Sandwich Islands',
		CC_ss='South Sudan',
		CC_es='Spain',
		CC_lk='Sri Lanka',
		CC_sd='Sudan',
		CC_sr='Suriname',
		CC_sj='Svalbard and Jan Mayen',
		CC_sz='Swaziland',
		CC_se='Sweden',
		CC_ch='Switzerland',
		CC_sy='Syrian Arab Republic',
		CC_tw='Taiwan, Province of China',
		CC_tj='Tajikistan',
		CC_tz='Tanzania, United Republic of',
		CC_th='Thailand',
		CC_tl='Timor-Leste',
		CC_tg='Togo',
		CC_tk='Tokelau',
		CC_to='Tonga',
		CC_tt='Trinidad and Tobago',
		CC_tn='Tunisia',
		CC_tr='Turkey',
		CC_tm='Turkmenistan',
		CC_tc='Turks and Caicos Islands',
		CC_tv='Tuvalu',
		CC_ug='Uganda',
		CC_ua='Ukraine',
		CC_ae='United Arab Emirates',
		CC_gb='United Kingdom',
		CC_us='United States',
		CC_um='United States Minor Outlying Islands',
		CC_uy='Uruguay',
		CC_uz='Uzbekistan',
		CC_vu='Vanuatu',
		CC_ve='Venezuela',
		CC_vn='Viet Nam',
		CC_vg='Virgin Islands, British',
		CC_vi='Virgin Islands, U.S.',
		CC_wf='Wallis and Futuna',
		CC_eh='Western Sahara',
		CC_ye='Yemen',
		CC_zm='Zambia',
		CC_zw='Zimbabwe';
	//@}

	//@{ ISO 639-1 language codes (Windows-compatibility subset)
	const
		LC_af='Afrikaans',
		LC_am='Amharic',
		LC_ar='Arabic',
		LC_as='Assamese',
		LC_ba='Bashkir',
		LC_be='Belarusian',
		LC_bg='Bulgarian',
		LC_bn='Bengali',
		LC_bo='Tibetan',
		LC_br='Breton',
		LC_ca='Catalan',
		LC_co='Corsican',
		LC_cs='Czech',
		LC_cy='Welsh',
		LC_da='Danish',
		LC_de='German',
		LC_dv='Divehi',
		LC_el='Greek',
		LC_en='English',
		LC_es='Spanish',
		LC_et='Estonian',
		LC_eu='Basque',
		LC_fa='Persian',
		LC_fi='Finnish',
		LC_fo='Faroese',
		LC_fr='French',
		LC_gd='Scottish Gaelic',
		LC_gl='Galician',
		LC_gu='Gujarati',
		LC_he='Hebrew',
		LC_hi='Hindi',
		LC_hr='Croatian',
		LC_hu='Hungarian',
		LC_hy='Armenian',
		LC_id='Indonesian',
		LC_ig='Igbo',
		LC_is='Icelandic',
		LC_it='Italian',
		LC_ja='Japanese',
		LC_ka='Georgian',
		LC_kk='Kazakh',
		LC_km='Khmer',
		LC_kn='Kannada',
		LC_ko='Korean',
		LC_lb='Luxembourgish',
		LC_lo='Lao',
		LC_lt='Lithuanian',
		LC_lv='Latvian',
		LC_mi='Maori',
		LC_ml='Malayalam',
		LC_mr='Marathi',
		LC_ms='Malay',
		LC_mt='Maltese',
		LC_ne='Nepali',
		LC_nl='Dutch',
		LC_no='Norwegian',
		LC_oc='Occitan',
		LC_or='Oriya',
		LC_pl='Polish',
		LC_ps='Pashto',
		LC_pt='Portuguese',
		LC_qu='Quechua',
		LC_ro='Romanian',
		LC_ru='Russian',
		LC_rw='Kinyarwanda',
		LC_sa='Sanskrit',
		LC_si='Sinhala',
		LC_sk='Slovak',
		LC_sl='Slovenian',
		LC_sq='Albanian',
		LC_sv='Swedish',
		LC_ta='Tamil',
		LC_te='Telugu',
		LC_th='Thai',
		LC_tk='Turkmen',
		LC_tr='Turkish',
		LC_tt='Tatar',
		LC_uk='Ukrainian',
		LC_ur='Urdu',
		LC_vi='Vietnamese',
		LC_wo='Wolof',
		LC_yo='Yoruba',
		LC_zh='Chinese';
	//@}

	/**
	*	Return list of languages indexed by ISO 639-1 language code
	*	@return array
	**/
	function languages() {
		return \Base::instance()->constants($this,'LC_');
	}

	/**
	*	Return list of countries indexed by ISO 3166-1 country code
	*	@return array
	**/
	function countries() {
		return \Base::instance()->constants($this,'CC_');
	}

}

//! Container for singular object instances
final class Registry {

	private static
		//! Object catalog
		$table;

	/**
	*	Return TRUE if object exists in catalog
	*	@return bool
	*	@param $key string
	**/
	static function exists($key) {
		return isset(self::$table[$key]);
	}

	/**
	*	Add object to catalog
	*	@return object
	*	@param $key string
	*	@param $obj object
	**/
	static function set($key,$obj) {
		return self::$table[$key]=$obj;
	}

	/**
	*	Retrieve object from catalog
	*	@return object
	*	@param $key string
	**/
	static function get($key) {
		return self::$table[$key];
	}

	/**
	*	Delete object from catalog
	*	@param $key string
	**/
	static function clear($key) {
		self::$table[$key]=NULL;
		unset(self::$table[$key]);
	}

	//! Prohibit cloning
	private function __clone() {
	}

	//! Prohibit instantiation
	private function __construct() {
	}

}

return Base::instance();
