<?php
	namespace Juju;
	require_once('base.abstract.class.php');
	require_once('App/router.class.php');
	require_once('App/exception.class.php');
	require_once('App/controller.abstract.class.php');
	use \Juju\{App\Controller, App\Exception, App\Router, Http\Response, Http\Request};

	class App extends Base {
		use Events;
		private static $apps = [];
		private $domains;
		private $routers;
		private $router;
		public $_onerror;
		public $request = null;
		public $response;
		public function __construct(string $name, callable $fn = null, array $events = null){
			$this->name = $name;
			$this->router = new Router();
			$this->response = new Response();
			$this->routers = [];
			$this->domains = [];
			$this->_onerror = function($req, $res, $error){
				$o = json_encode([
					'message'=>$error->getMessage(),
					'code'=>$error->getCode(),
					'file'=>$error->getFile(),
					'line'=>$error->getLine(),
					'traceback'=>$error->getTrace()
				]);
				$res->code(404);
				$res->header('Content-Type', 'application/json');
				$res->end($o);
				$res->shutdown();
			};
			if(!is_null($events)){
				foreach($events as $name => $handler){
					$this->on($name, $handler);
				}
			}
			static::$apps[] = $this;
			if(is_callable($fn)){
				$fn($this);
			}
		}
		public function __destruct(){
			$this->routers = [];
			$index = array_search($this, static::$apps);
			if($index !== false){
				array_splice(static::$apps, $index, 1);
			}
			if(is_callable('parent::__destruct')){
				parent::__destruct();
			}
		}
		public static function import_all(string $dirpath){
			foreach(scandir($dirpath) as $file){
				$path = "{$dirpath}/{$file}";
				if(is_file($path) && pathinfo($path, PATHINFO_EXTENSION) == "php"){
					require_once($path);
				}
			}
		}
		public static function shutdown(){
			$verb = Request::get_verb();
			$url = Request::get_url();
			$data = Request::get_body();
			foreach(static::$apps as $k => $app){
				if($app instanceof App){
					$app->handle($verb, $url, $data)->shutdown();
				}
			}
			if(is_callable('parent::__destruct')){
				parent::__destruct();
			}
		}
		public static function shutdown_error($error){
			foreach(static::$apps as $k => $app){
				if(is_null($app->request)){
					$app->request = new Request(Request::get_verb(), Request::get_url(), Request::get_headers(), Request::get_body());
				}
				$app->onerror($app->request, $app->response, $error);
				$app->response->shutdown();
			}
		}
		public function handle(string $verb, array $url, string $data, array $headers = null){
			if(is_null($headers)){
				$headers = Request::get_headers();
			}
			$res = $this->response;
			$this->request = $req = new Request($verb, $url, $headers, $data);
			if($this->fire('handle', $req, $res)){
				$self = $this;
				$onerror = function($res, $error) use($self){
					$self->onerror($req, $res, $error);
				};
				$handled = false;
				// Domain routers
				foreach($this->domains as $host => $router){
					if($url['host'] == $host){
						while(is_string($router)){
							$router = $this->domains[$router];
						}
						$router->handle($url["path"], $req, $res, null, $onerror);
						$handled = $handled || $router->handled;
					}
				}
				// Prefixed path routers
				foreach($this->routers as $prefix => $router){
					$router->handle($url["path"], $req, $res, null, $onerror);
					$handled = $handled || $router->handled;
				}
				// Base router for non-prefixed paths
				$this->router->handle($url["path"], $req, $res, function($req, $res) use($handled, $self, $url){
					if(!$handled){
						$self->onerror($req, $res, new Exception("{$url['scheme']}://{$url['host']}{$url["path"]} Not Found", 404));
					}
				}, $onerror);
				$this->fire('afterhandle', $req, $res);
			}
			return $res;
		}
		public function error(callable $fn){
			$this->_onerror = $fn;
			return $this;
		}
		public function route(string $path, callable $fn){
			$this->router->path($path, $fn);
			return $this;
		}
		public function prefix(string $prefix, callable $fn){
			if(!$this->routers[$prefix]){
				$this->routers[$prefix] = new Router($prefix);
			}
			$fn($this->routers[$prefix]);
			return $this;
		}
		public function domain(string $host, callable $fn){
			if(!isset($this->domains[$host])){
				$this->domains[$host] = new Router();
			}
			$fn($this->domains[$host]);
			return $this;
		}
		public function map_domain(string $host, $handle){
			if(!is_array($handle)){
				$handle = [$handle];
			}
			foreach($handle as $host2){
				$this->domains[$host2] = $host;
			}
			return $this;
		}
		public function bind(string $hosts, callable $fn = null){
			$hosts = array_map(function($item){
				return trim($item);
			}, explode(',', $hosts));
			$host = array_pop($hosts);
			if(is_null($fn)){
				$fn = function(Router $router){
					Controller::handle_all($router);
				};
			}
			return $this->map_domain($host, $hosts)->domain($host, $fn);
		}
		public function onerror(Request $req, Response $res, $error){
			$this->fire('error', $error);
			$fn = $this->_onerror;
			if(is_callable($fn)){
				$fn($req, $res, $error);
			}
		}
	}
	set_error_handler(function($errno, $errstr, $errfile, $errline){
		App::shutdown_error(new App\Exception($errstr, $errno, null, $errfile, $errline, debug_backtrace()));
	}, E_ALL);
	register_shutdown_function(function(){
		error_reporting(E_ALL);
		ini_set('display_errors', 'On');
		try{
			App::shutdown();
		}catch(Exception $error){
			App::shutdown_error(new App\Exception($error->getMessage(), $error->getCode(), $error, $error->getFile(), $error->getLine(), $error->getTrace()));
		}
	});
?>