<?php

require_once FRAMEWORK_PATH . '/src/SilverStripe/Framework/Injector/Factory.php';

require_once __DIR__ . '/InjectionCreator.php';
require_once __DIR__ . '/SilverStripeInjectionCreator.php';
require_once __DIR__ . '/ServiceConfigurationLocator.php';
require_once __DIR__ . '/SilverStripeServiceConfigurationLocator.php';

use SilverStripe\Framework\Injector\Factory;

/**
 * A simple injection manager that manages creating objects and injecting
 * dependencies between them. It borrows quite a lot from ideas taken from
 * Spring's configuration, but is adapted to the stateless PHP way of doing
 * things. 
 * 
 * In its simplest form, the dependency injector can be used as a mechanism to
 * instantiate objects. Simply call
 * 
 * Injector::inst()->get('ClassName')
 * 
 * and a new instance of ClassName will be created and returned to you. 
 * 
 * Classes can have specific configuration defined for them to 
 * indicate dependencies that should be injected. This takes the form of 
 * a static variable $dependencies defined in the class (or configuration),
 * which indicates the name of a property that should be set. 
 * 
 * eg 
 * 
 * <code>
 * class MyController extends Controller {
 * 
 *		public $permissions;
 *		public $defaultText;
 * 
 *		static $dependencies = array(
 *			'defaultText'		=> 'Override in configuration',
 *			'permissions'		=> '%$PermissionService',
 *		);
 * }
 * </code>
 * 
 * will result in an object of type MyController having the defaultText property
 * set to 'Override in configuration', and an object identified
 * as PermissionService set into the property called 'permissions'. The %$ 
 * syntax tells the injector to look the provided name up as an item to be created
 * by the Injector itself. 
 * 
 * A key concept of the injector is whether to manage the object as
 * 
 * * A pseudo-singleton, in that only one item will be created for a particular
 *   identifier (but the same class could be used for multiple identifiers)
 * * A prototype, where the same configuration is used, but a new object is
 *   created each time
 * * unmanaged, in which case a new object is created and injected, but no 
 *   information about its state is managed.
 * 
 * Additional configuration of items managed by the injector can be done by 
 * providing configuration for the types, either by manually loading in an 
 * array describing the configuration, or by specifying the configuration
 * for a type via SilverStripe's configuration mechanism. 
 *
 * Specify a configuration array of the format
 *
 * array(
 *		array(
 *			'id'			=> 'BeanId',					// the name to be used if diff from the filename
 *			'priority'		=> 1,							// priority. If another bean is defined with the same ID, 
 *															// but has a lower priority, it is NOT overridden
 *			'class'			=> 'ClassName',					// the name of the PHP class
 *			'src'			=> '/path/to/file'				// the location of the class
 *			'type'			=> 'singleton|prototype'		// if you want prototype object generation, set it as the
 *			                                                // type
 *															// By default, singleton is assumed
 *
 *			'factory' => 'FactoryService'					// A factory service to use to create instances.
 *			'construct'		=> array(						// properties to set at construction
 *				'scalar',									
 *				'%$BeanId',
 *			)
 *			'properties'	=> array(
 *				'name' => 'value'							// scalar value
 *				'name' => '%$BeanId',						// a reference to another bean
 *				'name' => array(
 *					'scalar',
 *					'%$BeanId'
 *				)
 *			)
 *		)
 *		// alternatively
 *		'MyBean'		=> array(
 *			'class'			=> 'ClassName',
 *		)
 *		// or simply
 *		'OtherBean'		=> 'SomeClass',
 * )
 *
 * In addition to specifying the bindings directly in the configuration,
 * you can simply create a publicly accessible property on the target
 * class which will automatically be injected if the autoScanProperties
 * option is set to true. This means a class defined as
 *
 * <code>
 * class MyController extends Controller {
 *
 *		private $permissionService;
 *
 *		public setPermissionService($p) {
 *			$this->permissionService = $p;
 *		}
 * }
 * </code>
 *
 * will have setPermissionService called if
 *
 * * Injector::inst()->setAutoScanProperties(true) is called and
 * * A service named 'PermissionService' has been configured
 *
 * @author marcus@silverstripe.com.au
 * @package framework
 * @subpackage injector
 * @license BSD License http://silverstripe.org/bsd-license/
 */
class Injector {

	/**
	 * Local store of all services
	 *
	 * @var array
	 */
	private $serviceCache;

	/**
	 * Cache of items that need to be mapped for each service that gets injected
	 *
	 * @var array
	 */
	private $injectMap;

	/**
	 * A store of all the service configurations that have been defined.
	 *
	 * @var array
	 */
	private $specs;
	
	/**
	 * A map of all the properties that should be automagically set on all 
	 * objects instantiated by the injector
	 */
	private $autoProperties;

	/**
	 * A singleton if you want to use it that way
	 *
	 * @var Injector
	 */
	private static $instance;
	
	/**
	 * Indicates whether or not to automatically scan properties in injected objects to auto inject
	 * stuff, similar to the way grails does things. 
	 * 
	 * @var boolean
	 */
	private $autoScanProperties = false;

	/**
	 * The default factory used to create new instances.
	 *
	 * The {@link InjectionCreator} is used by default, which simply directly
	 * creates objects. This can be changed to use a different default creation
	 * method if desired.
	 *
	 * Each individual component can also specify a custom factory to use by
	 * using the `factory` parameter.
	 *
	 * @var Factory
	 */
	protected $objectCreator;

	/**
	 * Create a new injector. 
	 *
	 * @param array $config
	 *				Service configuration
	 */
	public function __construct($config = null) {
		$this->injectMap = array();
		$this->serviceCache = array(
			'Injector'		=> $this,
		);
		$this->specs = array(
			'Injector'		=> array('class' => 'Injector')
		);
		
		$this->autoProperties = array();


		$creatorClass = isset($config['creator']) ? $config['creator'] : 'InjectionCreator';
		$locatorClass = isset($config['locator']) ? $config['locator'] : 'ServiceConfigurationLocator';
		
		$this->objectCreator = new $creatorClass;
		$this->configLocator = new $locatorClass;
		
		if ($config) {
			$this->load($config);
		}
	}

	/**
	 * If a user wants to use the injector as a static reference
	 *
	 * @param array $config
	 * @return Injector
	 */
	public static function inst($config=null) {
		if (!self::$instance) {
			self::$instance = new Injector($config);
		}
		return self::$instance;
	}

	/**
	 * Sets the default global injector instance.
	 *
	 * @param Injector $instance
	 */
	public static function set_inst(Injector $instance) {
		self::$instance = $instance;
	}

	/**
	 * Indicate whether we auto scan injected objects for properties to set. 
	 *
	 * @param boolean $val
	 */
	public function setAutoScanProperties($val) {
		$this->autoScanProperties = $val;
	}
	
	/**
	 * Sets the default factory to use for creating new objects.
	 *
	 * @param Factory $obj
	 */
	public function setObjectCreator(Factory $obj) {
		$this->objectCreator = $obj;
	}
	
	/**
	 * @return Factory
	 */
	public function getObjectCreator() {
		return $this->objectCreator;
	}
	
	/**
	 * Set the configuration locator 
	 * @param ServiceConfigurationLocator $configLocator 
	 */
	public function setConfigLocator($configLocator) {
		$this->configLocator = $configLocator;
	}
	
	/**
	 * Retrieve the configuration locator 
	 * @return ServiceConfigurationLocator 
	 */
	public function getConfigLocator() {
		return $this->configLocator;
	}
	
	/**
	 * Add in a specific mapping that should be catered for on a type. 
	 * This allows configuration of what should occur when an object
	 * of a particular type is injected, and what items should be injected
	 * for those properties / methods.
	 *
	 * @param type $class
	 *					The class to set a mapping for
	 * @param type $property
	 *					The property to set the mapping for
	 * @param type $injectType 
	 *					The registered type that will be injected
	 * @param string $injectVia
	 *					Whether to inject by setting a property or calling a setter
	 */
	public function setInjectMapping($class, $property, $toInject, $injectVia = 'property') {
		$mapping = isset($this->injectMap[$class]) ? $this->injectMap[$class] : array();
		
		$mapping[$property] = array('name' => $toInject, 'type' => $injectVia);
		
		$this->injectMap[$class] = $mapping;
	}
	
	/**
	 * Add an object that should be automatically set on managed objects
	 *
	 * This allows you to specify, for example, that EVERY managed object
	 * will be automatically inject with a log object by the following
	 *
	 * $injector->addAutoProperty('log', new Logger());
	 *
	 * @param string $property
	 *				the name of the property
	 * @param object $object
	 *				the object to be set
	 */
	public function addAutoProperty($property, $object) {
		$this->autoProperties[$property] = $object;
		return $this;
	}

	/**
	 * Load services using the passed in configuration for those services
	 *
	 * @param array $config
	 */
	public function load($config = array()) {
		$services = array();

		foreach ($config as $specId => $spec) {
			if (is_string($spec)) {
				$spec = array('class' => $spec);
			}

			$file = isset($spec['src']) ? $spec['src'] : null; 
			$name = null;

			if (file_exists($file)) {
				$filename = basename($file);
				$name = substr($filename, 0, strrpos($filename, '.'));
			}

			// class is whatever's explicitly set, 
			$class = isset($spec['class']) ? $spec['class'] : $name;
			
			// or the specid if nothing else available.
			if (!$class && is_string($specId)) {
				$class = $specId;
			}
			
			// make sure the class is set...
			$spec['class'] = $class;

			$id = is_string($specId) ? $specId : (isset($spec['id']) ? $spec['id'] : $class); 
			
			$priority = isset($spec['priority']) ? $spec['priority'] : 1;
			
			// see if we already have this defined. If so, check priority weighting
			if (isset($this->specs[$id]) && isset($this->specs[$id]['priority'])) {
				if ($this->specs[$id]['priority'] > $priority) {
					return;
				}
			}

			// okay, actually include it now we know we're going to use it
			if (file_exists($file)) {
				require_once $file;
			}

			// make sure to set the id for later when instantiating
			// to ensure we get cached
			$spec['id'] = $id;

//			We've removed this check because new functionality means that the 'class' field doesn't need to refer
//			specifically to a class anymore - it could be a compound statement, ala SilverStripe's old Object::create
//			functionality
//			
//			if (!class_exists($class)) {
//				throw new Exception("Failed to load '$class' from $file");
//			}

			// store the specs for now - we lazy load on demand later on. 
			$this->specs[$id] = $spec;

			// EXCEPT when there's already an existing instance at this id.
			// if so, we need to instantiate and replace immediately
			if (isset($this->serviceCache[$id])) {
				$this->updateSpecConstructor($spec);
				$this->instantiate($spec, $id);
			}
		}

		return $this;
	}
	
	/**
	 * Update the configuration of an already defined service
	 * 
	 * Use this if you don't want to register a complete new config, just append
	 * to an existing configuration. Helpful to avoid overwriting someone else's changes
	 * 
	 * updateSpec('RequestProcessor', 'filters', '%$MyFilter')
	 *
	 * @param string $id
	 *				The name of the service to update the definition for
	 * @param string $property
	 *				The name of the property to update. 
	 * @param mixed $value 
	 *				The value to set
	 * @param boolean $append
	 *				Whether to append (the default) when the property is an array
	 */
	public function updateSpec($id, $property, $value, $append = true) {
		if (isset($this->specs[$id]['properties'][$property])) {
			// by ref so we're updating the actual value
			$current = &$this->specs[$id]['properties'][$property];
			if (is_array($current) && $append) {
				$current[] = $value;
			} else {
				$this->specs[$id]['properties'][$property] = $value;
			}
			
			// and reload the object; existing bindings don't get
			// updated though! (for now...) 
			if (isset($this->serviceCache[$id])) {
				$this->instantiate(array('class'=>$id), $id);
			}
		}
	}
	
	/**
	 * Update a class specification to convert constructor configuration information if needed
	 * 
	 * We do this as a separate process to avoid unneeded calls to convertServiceProperty 
	 * 
	 * @param array $spec
	 *			The class specification to update
	 */
	protected function updateSpecConstructor(&$spec) {
		if (isset($spec['constructor'])) {
			$spec['constructor'] = $this->convertServiceProperty($spec['constructor']);
		}
	}

	/**
	 * Recursively convert a value into its proper representation with service references
	 * resolved to actual objects
	 *
	 * @param string $value 
	 */
	public function convertServiceProperty($value) {
		if (is_array($value)) {
			$newVal = array();
			foreach ($value as $k => $v) {
				$newVal[$k] = $this->convertServiceProperty($v);
			}
			return $newVal;
		}
		
		if (is_string($value) && strpos($value, '%$') === 0) {
			$id = substr($value, 2);
			return $this->get($id);
		}
		return $value;
	}

	/**
	 * Instantiate a managed object
	 *
	 * Given a specification of the form
	 *
	 * array(
	 *		'class' => 'ClassName',
	 *		'properties' => array('property' => 'scalar', 'other' => '%$BeanRef')
	 *		'id' => 'ServiceId',
	 *		'type' => 'singleton|prototype'
	 * )
	 *
	 * will create a new object, store it in the service registry, and
	 * set any relevant properties
	 *
	 * Optionally, you can pass a class name directly for creation
	 * 
	 * To access this from the outside, you should call ->get('Name') to ensure
	 * the appropriate checks are made on the specific type. 
	 * 
	 *
	 * @param array $spec
	 *				The specification of the class to instantiate
	 * @param string $id
	 *				The name of the object being created. If not supplied, then the id will be inferred from the
	 *				object being created
	 * @param string $type
	 *				Whether to create as a singleton or prototype object. Allows code to be explicit as to how it
	 *				wants the object to be returned
	 */
	protected function instantiate($spec, $id=null, $type = null) {
		if (is_string($spec)) {
			$spec = array('class' => $spec);
		}
		$class = $spec['class'];

		// create the object, using any constructor bindings
		$constructorParams = array();
		if (isset($spec['constructor']) && is_array($spec['constructor'])) {
			$constructorParams = $spec['constructor'];
		}

		$factory = isset($spec['factory']) ? $this->get($spec['factory']) : $this->getObjectCreator();
		$object = $factory->create($class, $constructorParams);

		// figure out if we have a specific id set or not. In some cases, we might be instantiating objects
		// that we don't manage directly; we don't want to store these in the service cache below
		if (!$id) {
			$id = isset($spec['id']) ? $spec['id'] : null;
		}

		// now set the service in place if needbe. This is NOT done for prototype beans, as they're
		// created anew each time
		if (!$type) {
			$type = isset($spec['type']) ? $spec['type'] : null; 
		}
		
		if ($id && (!$type || $type != 'prototype')) {
			// this ABSOLUTELY must be set before the object is injected.
			// This prevents circular reference errors down the line
			$this->serviceCache[$id] = $object;
		}

		// now inject safely
		$this->inject($object, $id);

		return $object;
	}

	/**
	 * Inject $object with available objects from the service cache
	 * 
	 * @todo Track all the existing objects that have had a service bound
	 * into them, so we can update that binding at a later point if needbe (ie
	 * if the managed service changes)
	 *
	 * @param object $object
	 *				The object to inject
	 * @param string $asType
	 *				The ID this item was loaded as. This is so that the property configuration
	 *				for a type is referenced correctly in case $object is no longer the same
	 *				type as the loaded config specification had it as. 
	 */
	public function inject($object, $asType=null) {
		$objtype = $asType ? $asType : get_class($object);
		$mapping = isset($this->injectMap[$objtype]) ? $this->injectMap[$objtype] : null;
		
		// first off, set any properties defined in the service specification for this
		// object type
		if (isset($this->specs[$objtype]) && isset($this->specs[$objtype]['properties'])) {
			foreach ($this->specs[$objtype]['properties'] as $key => $value) {
				$val = $this->convertServiceProperty($value);
				$this->setObjectProperty($object, $key, $val);
			}
		}

		// now, use any cached information about what properties this object type has
		// and set based on name resolution
		if (!$mapping) {
			if ($this->autoScanProperties) {
				// we use an object to prevent array copies if/when passed around
				$mapping = new ArrayObject();

				// This performs public variable based injection
				$robj = new ReflectionObject($object);
				$properties = $robj->getProperties();

				foreach ($properties as $propertyObject) {
					/* @var $propertyObject ReflectionProperty */
					if ($propertyObject->isPublic() && !$propertyObject->getValue($object)) {
						$origName = $propertyObject->getName();
						$name = ucfirst($origName);
						if ($this->hasService($name)) {
							// Pull the name out of the registry
							$value = $this->get($name);
							$propertyObject->setValue($object, $value);
							$mapping[$origName] = array('name' => $name, 'type' => 'property');
						}
					}
				}

				// and this performs setter based injection
				$methods = $robj->getMethods(ReflectionMethod::IS_PUBLIC);

				foreach ($methods as $methodObj) {
					/* @var $methodObj ReflectionMethod */
					$methName = $methodObj->getName();
					if (strpos($methName, 'set') === 0) {
						$pname = substr($methName, 3);
						if ($this->hasService($pname)) {
							// Pull the name out of the registry
							$value = $this->get($pname);
							$methodObj->invoke($object, $value);
							$mapping[$methName] = array('name' => $pname, 'type' => 'method');
						}
					}
				}

				// we store the information about what needs to be injected for objects of this
				// type here
				$this->injectMap[get_class($object)] = $mapping;
			}
		} else {
			foreach ($mapping as $prop => $spec) {
				if ($spec['type'] == 'property') {
					$value = $this->get($spec['name']);
					$object->$prop = $value;
				} else {
					$method = $prop;
					$value = $this->get($spec['name']);
					$object->$method($value);
				}
			}
		}

		$injections = Config::inst()->get(get_class($object), 'dependencies');
		// If the type defines some injections, set them here
		if ($injections && count($injections)) {
			foreach ($injections as $property => $value) {
				// we're checking isset in case it already has a property at this name
				// this doesn't catch privately set things, but they will only be set by a setter method, 
				// which should be responsible for preventing further setting if it doesn't want it. 
				if (!isset($object->$property)) {
					$value = $this->convertServiceProperty($value);
					$this->setObjectProperty($object, $property, $value);
				}
			}
		}

		foreach ($this->autoProperties as $property => $value) {
			if (!isset($object->$property)) {
				$value = $this->convertServiceProperty($value);
				$this->setObjectProperty($object, $property, $value);
			}
		}

		// Call the 'injected' method if it exists
		if (method_exists($object, 'injected')) {
			$object->injected();
		}
	}

	/**
	 * Helper to set a property's value
	 *
	 * @param object $object
	 *					Set an object's property to a specific value
	 * @param string $name
	 *					The name of the property to set
	 * @param mixed $value 
	 *					The value to set
	 */
	protected function setObjectProperty($object, $name, $value) {
		if (method_exists($object, 'set'.$name)) {
			$object->{'set'.$name}($value);
		} else {
			$object->$name = $value;
		}
	}

	/**
	 * Does the given service exist, and if so, what's the stored name for it?
	 * 
	 * We do a special check here for services that are using compound names. For example, 
	 * we might want to say that a property should be injected with Log.File or Log.Memory,
	 * but have only registered a 'Log' service, we'll instead return that. 
	 * 
	 * Will recursively call hasService for each depth of dotting
	 * 
	 * @return string 
	 *				The name of the service (as it might be different from the one passed in)
	 */
	public function hasService($name) {
		// common case, get it overwith first
		if (isset($this->specs[$name])) {
			return $name;
		}
		
		// okay, check whether we've got a compound name - don't worry about 0 index, cause that's an 
		// invalid name
		if (!strpos($name, '.')) {
			return null;
		}
		
		return $this->hasService(substr($name, 0, strrpos($name, '.')));
	}

	/**
	 * Register a service object with an optional name to register it as the
	 * service for
	 * 
	 * @param stdClass $service
	 *					The object to register
	 * @param string $replace
	 *					The name of the object to replace (if different to the 
	 *					class name of the object to register)
	 * 
	 */
	public function registerService($service, $replace = null) {
		$registerAt = get_class($service);
		if ($replace != null) {
			$registerAt = $replace;
		}
		
		$this->specs[$registerAt] = array('class' => get_class($service));
		$this->serviceCache[$registerAt] = $service;
		$this->inject($service);
	}
	
	/**
	 * Register a service with an explicit name
	 * 
	 * @deprecated since 3.1.1
	 */
	public function registerNamedService($name, $service) {
		return $this->registerService($service, $name);
	}
	
	/**
	 * Removes a named object from the cached list of objects managed
	 * by the inject
	 * 
	 * @param type $name 
	 *				The name to unregister
	 */
	public function unregisterNamedObject($name) {
		unset($this->serviceCache[$name]);
	}

	/**
	 * Clear out all objects that are managed by the injetor. 
	 */
	public function unregisterAllObjects() {
		$this->serviceCache = array('Injector' => $this);
	}
	
	/**
	 * Get a named managed object
	 * 
	 * Will first check to see if the item has been registered as a configured service/bean
	 * and return that if so. 
	 * 
	 * Next, will check to see if there's any registered configuration for the given type
	 * and will then try and load that
	 * 
	 * Failing all of that, will just return a new instance of the 
	 * specificied object.
	 * 
	 * @param string $name 
	 *				the name of the service to retrieve. If not a registered 
	 *				service, then a class of the given name is instantiated
	 * @param boolean $asSingleton
	 *				Whether to register the created object as a singleton
	 *				if no other configuration is found
	 * @param array $constructorArgs
	 *				Optional set of arguments to pass as constructor arguments
	 *				if this object is to be created from scratch 
	 *				(ie asSingleton = false)
	 * 
	 */
	public function get($name, $asSingleton = true, $constructorArgs = null) {
		// reassign the name as it might actually be a compound name
		if ($serviceName = $this->hasService($name)) {
			// check to see what the type of bean is. If it's a prototype,
			// we don't want to return the singleton version of it.
			$spec = $this->specs[$serviceName];
			$type = isset($spec['type']) ? $spec['type'] : null;
			
			// if we're explicitly a prototype OR we're not wanting a singleton
			if (($type && $type == 'prototype') || !$asSingleton) {
				if ($spec && $constructorArgs) {
					$spec['constructor'] = $constructorArgs;
				} else {
					// convert any _configured_ constructor args. 
					// we don't call this for get() calls where someone passes in 
					// constructor args, otherwise we end up calling convertServiceParams
					// way too often
					$this->updateSpecConstructor($spec);
				}
				return $this->instantiate($spec, $serviceName, !$type ? 'prototype' : $type);
			} else {
				if (!isset($this->serviceCache[$serviceName])) {
					$this->updateSpecConstructor($spec);
					$this->instantiate($spec, $serviceName);
				}
				return $this->serviceCache[$serviceName];
			}
		}
		
		$config = $this->configLocator->locateConfigFor($name);
		if ($config) {
			$this->load(array($name => $config));
			if (isset($this->specs[$name])) {
				$spec = $this->specs[$name];
				$this->updateSpecConstructor($spec);
				if ($constructorArgs) {
					$spec['constructor'] = $constructorArgs;
				}
				return $this->instantiate($spec, $name);
			}
		}

		// If we've got this far, we're dealing with a case of a user wanting 
		// to create an object based on its name. So, we need to fake its config
		// if the user wants it managed as a singleton service style object
		$spec = array('class' => $name, 'constructor' => $constructorArgs);
		if ($asSingleton) {
			// need to load the spec in; it'll be given the singleton type by default
			$this->load(array($name => $spec));
			return $this->instantiate($spec, $name);
		}

		return $this->instantiate($spec);
	}
	
	/**
	 * Magic method to return an item directly
	 * 
	 * @param string $name
	 *				The named object to retrieve
	 * @return mixed
	 */
	public function __get($name) {
		return $this->get($name);
	}

	/**
	 * Similar to get() but always returns a new object of the given type
	 * 
	 * Additional parameters are passed through as 
	 * 
	 * @param type $name 
	 */
	public function create($name) {
		$constructorArgs = func_get_args();
		array_shift($constructorArgs);
		return $this->get($name, false, count($constructorArgs) ? $constructorArgs : null);
	}
	
	/**
	 * Creates an object with the supplied argument array
	 *   
	 * @param string $name
	 *				Name of the class to create an object of
	 * @param array $args
	 *				Arguments to pass to the constructor
	 * @return mixed
	 */
	public function createWithArgs($name, $constructorArgs) {
		return $this->get($name, false, $constructorArgs);
	}
}