<?php namespace Illuminate\Routing;

class ResourceRegistrar {

	/**
	 * Create a new resource registrar.
	 *
	 * @param  \Illuminate\Routing\Router
	 */
	protected $router;

	/**
	 * The default actions for a resourceful controller.
	 *
	 * @var array
	 */
	protected $resourceDefaults = array('index', 'create', 'store', 'show', 'edit', 'update', 'destroy');

	/**
	 * Create a new resource registrar instance.
	 *
	 * @param  \Illuminate\Routing\Router  $router
	 * @return void
	 */
	public function __construct(Router $router)
	{
		$this->router = $router;
	}

	/**
	 * Route a resource to a controller.
	 *
	 * @param  string  $name
	 * @param  string  $controller
	 * @param  array   $options
	 * @return void
	 */
	public function register($name, $controller, array $options = array())
	{
		// If the resource name contains a slash, we will assume the developer wishes to
		// register these resource routes with a prefix so we will set that up out of
		// the box so they don't have to mess with it. Otherwise, we will continue.
		if (str_contains($name, '/'))
		{
			$this->prefixedResource($name, $controller, $options);

			return;
		}

		// We need to extract the base resource from the resource name. Nested resources
		// are supported in the framework, but we need to know what name to use for a
		// place-holder on the route wildcards, which should be the base resources.
		$base = $this->getResourceWildcard(last(explode('.', $name)));

		$defaults = $this->resourceDefaults;

		foreach ($this->getResourceMethods($defaults, $options) as $m)
		{
			$this->{'addResource'.ucfirst($m)}($name, $base, $controller, $options);
		}
	}

	/**
	 * Build a set of prefixed resource routes.
	 *
	 * @param  string  $name
	 * @param  string  $controller
	 * @param  array   $options
	 * @return void
	 */
	protected function prefixedResource($name, $controller, array $options)
	{
		list($name, $prefix) = $this->getResourcePrefix($name);

		// We need to extract the base resource from the resource name. Nested resources
		// are supported in the framework, but we need to know what name to use for a
		// place-holder on the route wildcards, which should be the base resources.
		$callback = function($me) use ($name, $controller, $options)
		{
			$me->resource($name, $controller, $options);
		};

		return $this->router->group(compact('prefix'), $callback);
	}

	/**
	 * Extract the resource and prefix from a resource name.
	 *
	 * @param  string  $name
	 * @return array
	 */
	protected function getResourcePrefix($name)
	{
		$segments = explode('/', $name);

		// To get the prefix, we will take all of the name segments and implode them on
		// a slash. This will generate a proper URI prefix for us. Then we take this
		// last segment, which will be considered the final resources name we use.
		$prefix = implode('/', array_slice($segments, 0, -1));

		return array(end($segments), $prefix);
	}

	/**
	 * Get the applicable resource methods.
	 *
	 * @param  array  $defaults
	 * @param  array  $options
	 * @return array
	 */
	protected function getResourceMethods($defaults, $options)
	{
		if (isset($options['only']))
		{
			return array_intersect($defaults, (array) $options['only']);
		}
		elseif (isset($options['except']))
		{
			return array_diff($defaults, (array) $options['except']);
		}

		return $defaults;
	}

	/**
	 * Get the base resource URI for a given resource.
	 *
	 * @param  string  $resource
	 * @return string
	 */
	public function getResourceUri($resource)
	{
		if ( ! str_contains($resource, '.')) return $resource;

		// Once we have built the base URI, we'll remove the wildcard holder for this
		// base resource name so that the individual route adders can suffix these
		// paths however they need to, as some do not have any wildcards at all.
		$segments = explode('.', $resource);

		$uri = $this->getNestedResourceUri($segments);

		return str_replace('/{'.$this->getResourceWildcard(last($segments)).'}', '', $uri);
	}

	/**
	 * Get the URI for a nested resource segment array.
	 *
	 * @param  array   $segments
	 * @return string
	 */
	protected function getNestedResourceUri(array $segments)
	{
		// We will spin through the segments and create a place-holder for each of the
		// resource segments, as well as the resource itself. Then we should get an
		// entire string for the resource URI that contains all nested resources.
		return implode('/', array_map(function($s)
		{
			return $s.'/{'.$this->getResourceWildcard($s).'}';

		}, $segments));
	}

	/**
	 * Get the action array for a resource route.
	 *
	 * @param  string  $resource
	 * @param  string  $controller
	 * @param  string  $method
	 * @param  array   $options
	 * @return array
	 */
	protected function getResourceAction($resource, $controller, $method, $options)
	{
		$name = $this->getResourceName($resource, $method, $options);

		return array('as' => $name, 'uses' => $controller.'@'.$method);
	}

	/**
	 * Get the name for a given resource.
	 *
	 * @param  string  $resource
	 * @param  string  $method
	 * @param  array   $options
	 * @return string
	 */
	protected function getResourceName($resource, $method, $options)
	{
		if (isset($options['names'][$method])) return $options['names'][$method];

		// If a global prefix has been assigned to all names for this resource, we will
		// grab that so we can prepend it onto the name when we create this name for
		// the resource action. Otherwise we'll just use an empty string for here.
		$prefix = isset($options['as']) ? $options['as'].'.' : '';

		if ( ! $this->router->hasGroupStack())
		{
			return $prefix.$resource.'.'.$method;
		}

		return $this->getGroupResourceName($prefix, $resource, $method);
	}

	/**
	 * Get the resource name for a grouped resource.
	 *
	 * @param  string  $prefix
	 * @param  string  $resource
	 * @param  string  $method
	 * @return string
	 */
	protected function getGroupResourceName($prefix, $resource, $method)
	{
		$group = trim(str_replace('/', '.', $this->router->getLastGroupPrefix()), '.');

		if (empty($group))
		{
			return trim("{$prefix}{$resource}.{$method}", '.');
		}

		return trim("{$prefix}{$group}.{$resource}.{$method}", '.');
	}

	/**
	 * Format a resource wildcard for usage.
	 *
	 * @param  string  $value
	 * @return string
	 */
	public function getResourceWildcard($value)
	{
		return str_replace('-', '_', $value);
	}

	/**
	 * Add the index method for a resourceful route.
	 *
	 * @param  string  $name
	 * @param  string  $base
	 * @param  string  $controller
	 * @param  array   $options
	 * @return \Illuminate\Routing\Route
	 */
	protected function addResourceIndex($name, $base, $controller, $options)
	{
		$uri = $this->getResourceUri($name);

		$action = $this->getResourceAction($name, $controller, 'index', $options);

		return $this->router->get($uri, $action);
	}

	/**
	 * Add the create method for a resourceful route.
	 *
	 * @param  string  $name
	 * @param  string  $base
	 * @param  string  $controller
	 * @param  array   $options
	 * @return \Illuminate\Routing\Route
	 */
	protected function addResourceCreate($name, $base, $controller, $options)
	{
		$uri = $this->getResourceUri($name).'/create';

		$action = $this->getResourceAction($name, $controller, 'create', $options);

		return $this->router->get($uri, $action);
	}

	/**
	 * Add the store method for a resourceful route.
	 *
	 * @param  string  $name
	 * @param  string  $base
	 * @param  string  $controller
	 * @param  array   $options
	 * @return \Illuminate\Routing\Route
	 */
	protected function addResourceStore($name, $base, $controller, $options)
	{
		$uri = $this->getResourceUri($name);

		$action = $this->getResourceAction($name, $controller, 'store', $options);

		return $this->router->post($uri, $action);
	}

	/**
	 * Add the show method for a resourceful route.
	 *
	 * @param  string  $name
	 * @param  string  $base
	 * @param  string  $controller
	 * @param  array   $options
	 * @return \Illuminate\Routing\Route
	 */
	protected function addResourceShow($name, $base, $controller, $options)
	{
		$uri = $this->getResourceUri($name).'/{'.$base.'}';

		$action = $this->getResourceAction($name, $controller, 'show', $options);

		return $this->router->get($uri, $action);
	}

	/**
	 * Add the edit method for a resourceful route.
	 *
	 * @param  string  $name
	 * @param  string  $base
	 * @param  string  $controller
	 * @param  array   $options
	 * @return \Illuminate\Routing\Route
	 */
	protected function addResourceEdit($name, $base, $controller, $options)
	{
		$uri = $this->getResourceUri($name).'/{'.$base.'}/edit';

		$action = $this->getResourceAction($name, $controller, 'edit', $options);

		return $this->router->get($uri, $action);
	}

	/**
	 * Add the update method for a resourceful route.
	 *
	 * @param  string  $name
	 * @param  string  $base
	 * @param  string  $controller
	 * @param  array   $options
	 * @return void
	 */
	protected function addResourceUpdate($name, $base, $controller, $options)
	{
		$this->addPutResourceUpdate($name, $base, $controller, $options);

		return $this->addPatchResourceUpdate($name, $base, $controller);
	}

	/**
	 * Add the update method for a resourceful route.
	 *
	 * @param  string  $name
	 * @param  string  $base
	 * @param  string  $controller
	 * @param  array   $options
	 * @return \Illuminate\Routing\Route
	 */
	protected function addPutResourceUpdate($name, $base, $controller, $options)
	{
		$uri = $this->getResourceUri($name).'/{'.$base.'}';

		$action = $this->getResourceAction($name, $controller, 'update', $options);

		return $this->router->put($uri, $action);
	}

	/**
	 * Add the update method for a resourceful route.
	 *
	 * @param  string  $name
	 * @param  string  $base
	 * @param  string  $controller
	 * @return void
	 */
	protected function addPatchResourceUpdate($name, $base, $controller)
	{
		$uri = $this->getResourceUri($name).'/{'.$base.'}';

		$this->router->patch($uri, $controller.'@update');
	}

	/**
	 * Add the destroy method for a resourceful route.
	 *
	 * @param  string  $name
	 * @param  string  $base
	 * @param  string  $controller
	 * @param  array   $options
	 * @return \Illuminate\Routing\Route
	 */
	protected function addResourceDestroy($name, $base, $controller, $options)
	{
		$uri = $this->getResourceUri($name).'/{'.$base.'}';

		$action = $this->getResourceAction($name, $controller, 'destroy', $options);

		return $this->router->delete($uri, $action);
	}

}
