<?php
/**
 * Web Application Component Toolkit
 *
 * @link http://www.phpwact.org/
 *
 * @author Wact Development Team
 * @link http://www.phpwact.org/team
 *
 * @copyright Copyright 2006, Jeff Moore
 * @license http://opensource.org/licenses/mit-license.php MIT
 */

/**
 * DSL for creating instances of types
 */
class Wact_Config_Registry_Factory {

    /**
     * The beginning state of the DSL
     */
    const MODE_DEFAULT = 0;
    
    /**
     * We're defining a class
     */
    const MODE_CLASS = 1;
    
    /**
     * We're defining an alias
     */
    const MODE_TYPE = 2;

    /**
     * We're defining a simple value
     */
    const MODE_VALUE = 3;
    
    /**
     * Internal type description designation meaning, use default type 
     */
    const USE_DEFAULT_TYPE = 1;

    /**
     * Current state of the DSL
     * @var MODE_CLASS|MODE_TYPE|MODE_DEFAULT
     */
    protected $mode = self::MODE_DEFAULT;
    
    /**
     * @var string
     */
    protected $class = 'StdClass';

    /**
     * @var string
     */
    protected $alias;

    /**
     * @var mixed
     */
    protected $value;
    
    /**
     * @var Wact_Reflection_Class
     */
    protected $classReflection;

    /**
     * @var array of Wact_Reflection_Parameter
     */
    protected $parameterDefs;
    
    /**
     * @var array
     */
    protected $parameterValues = array();

    /**
     * @var array
     */
    protected $parameterTypes = array();

    /**
     * @var array of Wact_Reflection_Parameter
     */
    protected $setterDefs;
    
    /**
     * @var array
     */
    protected $setterValues = array();

    /**
     * @var array
     */
    protected $setterTypes = array();

    /**
     * @var array of Wact_Reflection_Parameter
     */
    protected $propertyDefs;
    
    /**
     * @var array
     */
    protected $propertyValues = array();

    /**
     * @var array
     */
    protected $propertyTypes = array();
    
    /**
     * Are changes allowed to the definition of this factory?
     * @var boolean
     */
    protected $locked = FALSE;
    
    /**
     * Lock this factory definition from further changes.
     * @return Wact_Config_Registry_Factory for fluent interface
     */
    public function lock() {
        $this->locked = TRUE;
        return $this; // Stay fluent
    }

    /**
     * Protect against changing a locked definition
     */
    protected function requireClassModeForChange() {
        if ($this->mode !== self::MODE_CLASS) {
            throw new Wact_Config_Exception('Cannot describe class construction on a non-class factory');
        }
        if ($this->locked) {
            throw new Wact_Config_Exception('Attempt to change locked factory definition');
        }
    }

    /**
     * Protect against changing a locked definition
     */
    protected function requireDefaultModeForChange() {
        if ($this->mode !== self::MODE_DEFAULT) {
            throw new Wact_Config_Exception('Cannot redefine factory');
        }
        if ($this->locked) {
            throw new Wact_Config_Exception('Attempt to change locked factory definition');
        }
    }
    
    /**
     * Set the class that this factory will create.
     * @param string $class
     * @return Wact_Config_Registry_Factory for fluent interface
     */
    public function asClass($class) {
        $this->requireDefaultModeForChange();
        $this->class = $class;
        $this->mode = self::MODE_CLASS;
        return $this; // Stay fluent
    }

    /**
     * Set an alias to a different type
     * @param string $type
     * @return Wact_Config_Registry_Factory for fluent interface
     */
    public function asAliasToType($type) {
        $this->requireDefaultModeForChange();
        $this->alias = $type;
        $this->mode = self::MODE_TYPE;
        return $this; // Stay fluent
    }

    /**
     * Use a specific value
     * @param string $type
     * @return Wact_Config_Registry_Factory for fluent interface
     */
    public function asValue($value) {
        $this->requireDefaultModeForChange();
        $this->value = $value;
        $this->mode = self::MODE_VALUE;
        return $this; // Stay fluent
    }
    
    /**
     * Define a value to be injected for a specific constructor parameter
     *
     * @param string $name
     * @param mixed $value
     * @return Wact_Config_Registry_Factory for fluent interface
     */
    public function parameterValue($name, $value) {
        $this->requireClassModeForChange();
        $this->parameterValues[$name] = $value;
        unset($this->parameterTypes[$name]);
        return $this; // Stay fluent
    }
    
    /**
     * define the type of a specific constructor parameter
     *
     * @param string $name
     * @param string $type
     * @return Wact_Config_Registry_Factory for fluent interface
     */
    public function parameterType($name, $type) {
        $this->requireClassModeForChange();
        $this->parameterTypes[$name] = (string) $type;
        unset($this->parameterValues[$name]);
        return $this; // Stay fluent
    }
    
    /**
     * Indicate that injecting a specific parameter is required 
     *
     * @param string $name
     * @return Wact_Config_Registry_Factory for fluent interface
     */
    public function parameterRequired($name) {
        $this->requireClassModeForChange();
        $this->parameterTypes[$name] = self::USE_DEFAULT_TYPE;
        unset($this->parameterValues[$name]);
        return $this; // Stay fluent
    }
    
    /**
     * Define a value to be injected for a specific property
     *
     * @param string $name
     * @param mixed $value
     * @return Wact_Config_Registry_Factory for fluent interface
     */
    public function propertyValue($name, $value) {
        $this->requireClassModeForChange();
        $this->propertyValues[$name] = $value;
        unset($this->propertyTypes[$name]);
        return $this; // Stay fluent
    }
    
    /**
     * define the type of a specific property
     *
     * @param string $name
     * @param string $type
     * @return Wact_Config_Registry_Factory for fluent interface
     */
    public function propertyType($name, $type) {
        $this->requireClassModeForChange();
        $this->propertyTypes[$name] = (string) $type;
        unset($this->propertyValues[$name]);
        return $this; // Stay fluent
    }

    /**
     * Indicate that injecting a specific property is required 
     *
     * @param string $name
     * @return Wact_Config_Registry_Factory for fluent interface
     */
    public function propertyRequired($name) {
        $this->requireClassModeForChange();
        $this->propertyTypes[$name] = self::USE_DEFAULT_TYPE;
        unset($this->propertyValues[$name]);
        return $this; // Stay fluent
    }
    
    /**
     * Define a value to be injected for a specific setter
     *
     * @param string $name
     * @param mixed $value
     * @return Wact_Config_Registry_Factory for fluent interface
     */
    public function setterValue($name, $value) {
        $this->requireClassModeForChange();
        $this->setterValues[$name] = $value;
        unset($this->setterTypes[$name]);
        return $this; // Stay fluent
    }
    
    /**
     * define the type of a specific setter parameter
     *
     * @param string $name
     * @param string $type
     * @return Wact_Config_Registry_Factory for fluent interface
     */
    public function setterType($name, $type) {
        $this->requireClassModeForChange();
        $this->setterTypes[$name] = (string) $type;
        unset($this->setterValues[$name]);
        return $this; // Stay fluent
    }

    /**
     * define the type of a specific setter parameter
     *
     * @param string $name
     * @return Wact_Config_Registry_Factory for fluent interface
     */
    public function setterRequired($name) {
        $this->requireClassModeForChange();
        $this->setterTypes[$name] = self::USE_DEFAULT_TYPE;
        unset($this->setterValues[$name]);
        return $this; // Stay fluent
    }
    
    /**
     * define a setter injection value
     *
     * @param string $method
     * @param array $args
     * @return Wact_Config_Registry_Factory for fluent interface
     */
    public function __call($method, $args) {
        if (count($args) > 1) {
            throw new Wact_Config_Exception('Only one parameter is allowed per setter');
        }
        if (empty($args)) {
            $this->setterRequired($method);
        } else {
            $this->setterValue($method, array_shift($args));
        }
        return $this; // Stay fluent
    }
    
    /**
     * Lazy load and cache a Wact_Reflection_Class object
     * @return Wact_Reflection_Class
     */
    protected function getClassReflection() {

        // Lazy loading allows us to create factories without unecessarily
        // invoking reflection API.
        if (empty($this->classReflection)) {
            $this->classReflection = new Wact_Reflection_Class($this->class);
        }

        return $this->classReflection;
    }

    /**
     * Lazy load and cache a list of constructor parameters definitions
     * @return array
     */
    protected function getParameterDefinitions() {
        if (!isset($this->parameterDefs)) {
            $constructorInfo = $this->getClassReflection()->getConstructor();
            if ($constructorInfo) {
                $this->parameterDefs = $constructorInfo->getParameters();
            } else {
                $this->parameterDefs = array();
            }
        }
        return $this->parameterDefs;
    }
    
    /**
     * Accept a list of constructor arguments and extend it out by
     * injecting required parameters that were not specified.
     * @param Wact_Config_Registry $context
     * @param array $args a list of supplied arguments
     * @return array a full list of argument values to call
     */
    protected function buildParameters($context, $args) {
        $info = $this->getParameterDefinitions();

        // Only inject unspecified constructor parameters
        for( $i = count($args); $i < count($info); $i++) {
            $name = $info[$i]->getName();
            if (isset($this->parameterValues[$i])) {
                $args[$i] = isset($this->parameterValues[$i]);
                continue;
            } else if (isset($this->parameterValues[$name])) {
                $args[$i] = $this->parameterValues[$name];
                continue;
            } else if (isset($this->parameterTypes[$i])) {
                $type = $this->parameterTypes[$i];
            } else if (isset($this->parameterTypes[$name])) {
                $type = $this->parameterTypes[$name];
            } else if ($info[$i]->isOptional()) {
                continue;
            } else {
                $type = self::USE_DEFAULT_TYPE;
            }
            if ($type === self::USE_DEFAULT_TYPE) {
                $type = $info[$i]->getType();
            }
            if ($type) {
                $args[$i] = $context->get($type);
            } else {
                throw new Wact_Config_Exception(
                    'Cannot determine value to inject for "{class}" constructor parameter {parameter}',
                    $this->class,
                    $info[$i]->getName());
            }
        }
        return $args;
    }
    
    /**
     * Inject properties into an object instance
     *
     * @param Wact_Config_Registry $context
     * @param object $obj
     */
    protected function injectProperties($context, $obj) {
        foreach($this->propertyValues as $property => $value) {
            $obj->$property = $value;
        }
        if (empty($this->propertyTypes)) {
            return;
        }
        foreach($this->propertyTypes as $property => $type) {
            if ($type === self::USE_DEFAULT_TYPE) {
                $type = $this->getClassReflection()->getProperty($property)->getType();
                $this->propertyTypes[$property] = $type; // Cache it
            }
            if ($type) {
                $obj->$property = $context->get($type);
            } else {
                throw new Wact_Config_Exception(
                    'Cannot determine value to inject for "{class}" property {property}',
                    $this->class,
                    $property);
            }
        }        
    }

    /**
     * Inject Values with Setters into an object instance
     *
     * @param Wact_Config_Registry $context
     * @param object $obj
     */
    protected function injectSetters($context, $obj) {
        foreach($this->setterValues as $setter => $value) {
            $obj->$setter($value);
        }
        if (empty($this->setterTypes)) {
            return;
        }
        foreach($this->setterTypes as $setter => $type) {
            if ($type === self::USE_DEFAULT_TYPE) {
                $method = $this->getClassReflection()->getMethod($setter);
                if ($method && ($params = $method->getParameters()) && ($param = array_shift($params)) && $param) {
                    $type = $param->getType(); 
                } else {
                    $type = FALSE;
                }
                $this->setterTypes[$setter] = $type; // Cache it
            }
            if ($type) {
                $obj->$setter($context->get($type));
            } else {
                throw new Wact_Config_Exception(
                    'Cannot determine value to inject for "{class}" setter {setter}',
                    $this->class,
                    $setter);
            }
        }        
    }
    
    /**
     * Create an instance of an object using the parent registry to complete
     * missing constructor parameters.
     * @param Wact_Config_Registry $context
     * @param array $parameters a list of partially specified constructor params
     * @return mixed
     */
    public function createInstance($context, $parameters = array()) {
        $this->locked = TRUE;
        if ($this->mode == self::MODE_TYPE) {
            return $context->get($this->alias, $parameters);
        }
        if ($this->mode == self::MODE_VALUE) {
            return $this->value;
        }
        if (!class_exists($this->class)) {
            throw new Wact_Config_Exception('Class "{type}" does not exist', $this->class);
        }

        if ($this->getClassReflection()->isAbstract()) {
            throw new Wact_Config_Exception('Cannot instantiate abstract type "{type}"', $this->class);
        }

        $args = $this->buildParameters($context, $parameters);
        if (empty($args)) {
            $obj = $this->getClassReflection()->newInstance();
        } else {
            $obj = $this->getClassReflection()->newInstanceArgs($args);
        }
        
        $this->injectProperties($context, $obj);
        $this->injectSetters($context, $obj);
        
        return $obj;
    }

}

?>