<?php

/**
 * WP_Route
 *
 * A simple class for binding
 * complex routes to functions
 * methods or WP_AJAX actions.
 *
 * @author     Anthony Budd
 */

final class WP_Route{

	private $hooked = FALSE;
	private $routes = array(
		'ANY' 		=> array(),
		'GET' 		=> array(),
		'POST' 		=> array(),
		'HEAD' 		=> array(),
		'PUT' 		=> array(),
		'DELETE' 	=> array(),
	);

	private function __construct(){}

	public static function instance(){
		static $instance = NULL;

		if($instance === NULL){
			$instance = new self();
			$instance->hook();
		}

		return $instance;
	}


	// -----------------------------------------------------
	// CREATE ROUTE METHODS
	// -----------------------------------------------------
	public static function any($route, $callable){
		$r = self::instance();
		$r->addRoute('ANY', $route, $callable);
	}

	public static function get($route, $callable){
		$r = self::instance();
		$r->addRoute('GET', $route, $callable);
	}

	public static function post($route, $callable){
		$r = self::instance();
		$r->addRoute('POST', $route, $callable);
	}

	public static function head($route, $callable){
		$r = self::instance();
		$r->addRoute('HEAD', $route, $callable);
	}

	public static function put($route, $callable){
		$r = self::instance();
		$r->addRoute('PUT', $route, $callable);
	}

	public static function delete($route, $callable){
		$r = self::instance();
		$r->addRoute('DELETE', $route, $callable);
	}

	public static function match($methods, $route, $callable){
		if(!is_array($methods)){
			throw new Exception("\$methods must be an array");
		}

		$r = self::instance();
		foreach($methods as $method){
			if(!in_array(strtoupper($method), array_keys($this->routes))){
				throw new Exception("Unknown method {$method}");
			}

			$r->addRoute(strtoupper($method), $route, $callable);
		}
	}

	public static function redirect($route, $redirect, $code = 301){
		$r = self::instance();
		$r->addRoute('ANY', $route, $redirect, array(
			'code'     => $code,
			'redirect' => $redirect,
		));
	}


	// -----------------------------------------------------
	// INTERNAL UTILITY METHODS
	// -----------------------------------------------------
	private function addRoute($method, $route, $callable, $options = array()){
		$this->routes[$method][] = (object) array_merge(array(
			'route' 	=>  ltrim($route, '/'),
			'callable'  =>  $callable,
		), $options);
	}

	private function hook(){
		if(!$this->hooked){
			add_filter('init', array('WP_Route', 'onInit'), 1, 0);
			$this->hooked = TRUE;
		}
	}

	public static function onInit(){
		$r = self::instance();
		$r->handle();
	}

	private function getRouteParams($route){
		$tokenizedRoute		 = $this->tokenize($route);
		$tokenizedRequestURI = $this->tokenize($this->requestURI());
		preg_match_all('/\{\s*.+?\s*\}/', $route, $matches);

		$return = array();
		foreach($matches[0] as $key => $match){
			$search = array_search($match, $tokenizedRoute);
			if($search !== FALSE){
				$return[]  = $tokenizedRequestURI[$search];
			}
		}

		return $return;
	}


	// -----------------------------------------------------
	// GENERAL UTILITY METHODS
	// -----------------------------------------------------
	public static function routes(){
		$r = self::instance();
		return $r->routes;
	}

	public function tokenize($url){
		return array_filter(explode('/', ltrim($url, '/')));
	}

	public function requestURI(){
		return ltrim($_SERVER['REQUEST_URI'], '/');
	}

	// -----------------------------------------------------
	// handle()
	// -----------------------------------------------------
	public function handle(){
		$method              = strtoupper($_SERVER['REQUEST_METHOD']);
		$routes              = array_merge($this->routes[$method], $this->routes['ANY']);
		$requestURI 		 = $this->requestURI();
		$tokenizedRequestURI = $this->tokenize($requestURI);

		foreach($routes as $key => $route){
			// First, filter routes that do not have equal tokenized lengths
			if(count($this->tokenize($route->route)) !== count($tokenizedRequestURI)){
				unset($routes[$key]);
				continue;
			}

			// Add more filtering here as routing gets more complex.
		}

		$routes = array_values($routes);
		if(isset($routes[0])){
			$route = $routes[0];

			if(is_string($route->callable) &&
				class_exists($route->callable) &&
				is_subclass_of($route->callable, 'WP_AJAX')){

				$callable   = $route->callable;
				$controller = new $callable;
				call_user_func_array(array($controller, 'boot'), $this->getRouteParams($route->route));

			}elseif(isset($routes[0]->redirect)){

				$redirect = $routes[0]->redirect;
				header("Location: {$redirect}", TRUE, $routes[0]->code);
				die;

			}else{

				call_user_func_array($route->callable, $this->getRouteParams($route->route));

			}
		}
	}
}