<?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
*/

/**
* Base class for View components.
*
*  Detached ==> Attached ==> Initalized ==> Rendering
*
* Wact Views are designed to be rendered in two stages.  First, a view heirarchy is built.
* Then, bindScope is called once for each node in the heirarchy.
*
* In the second stage, the render method is called.  The render method for a node in the view
* Tree may be called multiple times during the course of rendering the view tree.
*/
class Wact_View_Partial implements ArrayAccess {

    /**
    * View Id of this component, often 
    * corresponding to it's Id attribute in the template
    * @var string
    */
    protected $id;

    /**
    * Refers to the Root component for the view tree, which always defines
    * a model.
    * @var object Wact_View_Root
    */
    public $root;

    /**
    * Parent component - "parent" refers to nesting in template
    * not to class hierarchy.
    * @var object Wact_View_Partial
    */
    public $parent;

    /**
    * Array of child components.  
    * Keys in the array are the lower case view Id's of the child elements.
    * @var array of Wact_View_Partial
    */
    public $children = array();

    /**
    * Can children be added or removed from this node?
    */
    private $canChangeChildren = TRUE;

    /**
    * Refers to the next enclosing view that has a model.
    * @var object Wact_View_Partial
    */
    public $myScope;

    /**
    * @var object Wact_View_Partial
    */
    public $parentScope;

    /**
    * Refers to the top level scope in the view.
    * @var object Wact_View_Partial
    */
    public $rootScope;

    /**
    * A controller object associated with this view.
    * @var object Wact_Controller_Base
    */
    public $controller;

    /*
    * @var array A list of listeners for data binding events
    */
    protected $preRenderListeners;


    /**
    * Construct this object.
    * @see Wact_View_Partial.setUp
    */
    public function __construct() {
        $this->setUp();
    }

    /**
    * Setup is a template method that is called during object construction.  No
    * non-final class in the framework will override the setUp method.  There is
    * no need inside a setup method to call the parent setUp.
    * A typeical task performed in the setUp method might be to add child
    * Views.
    */
    public function setUp() {
    }

    /**
    * @return string the ID of this Wact_View_Partial
    */
    public function getViewId() {
        return $this->id;
    }

    /**
    * Can this View accept child views?
    */
    public function canChangeChildren() {
        return $this->canChangeChildren;
    }

    /**
    * Do not allow further changes to child views
    */
    public function lockChildren() {
        $this->canChangeChildren = FALSE;
    }
    
    /**
    * Assign a ViewId to this component.  If one is not provided, a
    * random unique id will be used instead.
    *
    * joinViewTree may not be called after a view has been added to its parent.
    *
    * @param string id
    * @return string the actual view id assigned.
    */
    public function joinViewTree($parent, $viewId = NULL) {
        if (!empty($viewId)) {
            if (isset($this->parent)) {
                throw new Wact_View_Exception(
                    'Cannot reassign view id of view from {OldViewId} to {NewViewId} after adding to parent view.',
                    $this->id, $viewId);
            }
            $this->id = strtolower($viewId);
        }
        
        // Generate a random ViewId if we don't have one already.
        if (empty($this->id)) {
            // this benchmarks twice as fast as uniqid()
            static $genid = 1;
            $this->id = strtolower(get_class($this) . $genid);
            $genid++;
        }

        if (isset($parent->children[$this->id])) {
            throw new Wact_View_Exception(
                'Cannot add component with the duplicate view id "{ViewId}"', $this->id);
        }
        $this->parent = $parent;

        return $this->id;
    }

    /**
    * Appends a child view to the end of this view.
    *
    * @param object child component
    * @param string value for ID attribute
    * @throws Wact_View_Exception if the view id already exists
    */
    public function appendChild($child, $viewId = NULL) {
        if (!$this->canChangeChildren()) {
            throw new Wact_View_Exception('This View cannot accept child views');
        }

        $cannonicalName = $child->joinViewTree($this, $viewId);
        $this->children[$cannonicalName] = $child;

        // trying to add a new child after bindScope has already been called?
        if (isset($this->root)) {
            $child->bindScope($this->root, $this->myScope);
        }
    }

    /**
    * Inserts a child view before all other children.
    *
    * @param object child component
    * @param string value for ID attribute
    * @throws Wact_View_Exception if the view id already exists
    */
    public function prependChild($child, $viewId = NULL) {
        if (!$this->canChangeChildren()) {
            throw new Wact_View_Exception('This View cannot accept child views');
        }

        $cannonicalName = $child->joinViewTree($this, $viewId);
        $this->children = array($cannonicalName => $child) + $this->children;

        // trying to add a new child after bindScope has already been called?
        if (isset($this->root)) {
            $child->bindScope($this->root, $this->myScope);
        }
    }

    /**
    * Called on a node that has been removed from the tree
    */
    public function leaveViewTree() {
        unset($this->parent);
    }

    /**
    * Remove all the children of this view
    */
    public function removeAllChildren() {
        foreach($this->children as $child) {
            $child->leaveViewTree();
        }
        $this->children = array();
    }
    
    /**
    * set the controller asssociated with this view component.
    * @param object Controller
    */
    public function setController($controller) {
        $this->controller = $controller;

        // We set all of our children controllers because we are assuming that 
        // Views have not been composed yet.  However the proper relationship
        // between views and controllers may not be this simple.  Especially
        // if there are many controllers and a single view.
        foreach($this->children as $child) {
            $child->setController($controller);
        }
    }

    /*
    * set the data source of a child component, or raise an error
    * if the child is not found.
    * @param string path
    * @param object a model
    */
    // Remove this method as unecessisary as we get tighter integration
    // with controllers which can automatically bind the model to the view
    public function setChildModel($path, $model) {
        $child = $this->getChild($path);
        $child->setModel($model);
    }
 
    /**
    * set the data source of a child component, or raise an error
    * if the child is not found.
    * @param string path
    * @param object an iterator
    */
    // Remove this method as unecessisary as we get tighter integration
    // with controllers which can automatically bind the model to the view
    public function setChildIterator($path, $iterator) {
        $child = $this->getChild($path);
        $child->setIterator($iterator);
    }

    /**
    * Returns a child component given it's ID.<br />
    * Note this is a potentially expensive operation if dealing with
    * many components, as it calls the findChild method of children
    * based on alphanumeric order: strcasecmp(). Attempt to call it via
    * the nearest known component to the required child.
    * @param string id
    * @return mixed child component object or FALSE if not found
    */
    public function findChild($viewId) {
        if (!strcasecmp($this->id, $viewId)) {
            return $this;
        }
        foreach($this->children as $child) {
            $result = $child->findChild($viewId);
            if ($result) {
                return $result;
            }
        }
        return FALSE;
    }

    /**
    * Same as find child, except raises error if child is not found
    * @param string id
    * @return object refernce to child component object
    * @throws Wact_View_Exception if the specified child does not exist.
    */
    public function getChild($viewId) {
        $result = $this->findChild($viewId);
        if ($result === FALSE) {
            throw new Wact_View_Exception(
                'Could not find child view named "{ViewId}"', $viewId);
        }
        return $result;
    }

    /**
    * Returns the first parent component matching the supplied PHP class name.
    * @param string view class name
    * @return mixed parent view object or FALSE if not found
    */
    function findParentByType($type) {
        $parent = $this->parent;
        while ($parent !== NULL && !is_a($parent, $type)) {
            $parent = $parent->parent;
        }
        return $parent;
    }

    /**
    * Checks the existence of a child view.
    *
    * @param string $offset view id of the child to check
    * @return boolean
    */
    public function offsetExists($offset) {
        return isset($this->children[strtolower($offset)]);
    }

    /**
    * implements ArrayAccess::offsetGet()
    *
    * Returns the child with the view id "$offset"
    *
    * Called automatically by PHP when using $view['ChildViewId']
    *
    * @param string $offset view id of the child to get
    * @return Wact_View_Partial
    * @throws Wact_View_Exception if the specified child does not exist.
    */
    public function offsetGet($offset) {
        $cannonicalName = strtolower($offset);
        if (isset($this->children[$cannonicalName])) {
            return $this->children[$cannonicalName];
        } else {
            throw new Wact_View_Exception(
                'Could not find child view named "{ViewId}"', $offset);
        }
    }

    /**
    * implements ArrayAccess::offsetSet()
    *
    * Adds child "$value" with the view id "$offset" to the internal children array.
    *
    * Called automatically by PHP when adding children to the object like
    * $view[] = new Wact_View_Partial();
    * $view['MyViewId'] = new Wact_View_Partial();
    *
    * @param string $offset view id for the child
    * @param Wact_View_Partial $value The child object
    * @throws Wact_View_Exception if the view id already exists
    * @return void
    */
    public function offsetSet($offset, $value) {
        return $this->appendChild($value, $offset);
    }

    /**
    * Child views cannot be removed from the tree.
    * If this were allowed, a view could only be used once.
    *
    * In the future, this restriction may be relaxed when we have a way to
    * detect when it is safe to remvoe a child view and when it is not.
    *
    * @param string $offset
    */
    public function offsetUnset($offset) {
        throw new Wact_View_Exception('Child views cannot be removed.');
    }

    /**
    * Default property implmentation for base class.
    */
    public function __get($name) {
        throw new Wact_Property_Exception_PropertyNotFound($name);
    }

    /**
    * Default property implmentation for base class.
    */
    public function __set($name, $value) {
        throw new Wact_Property_Exception_PropertyNotFound($name);
    }

    /**
    * Default property implmentation for base class.
    */
    public function __unset($name) {
        throw new Wact_Property_Exception_PropertyNotFound($name);
    }
    
    /**
    * Default property implmentation for base class.
    */
    public function __isset($name) {
        return FALSE;
    }

    /*
    * register a listener to receive data Binding events
    * A typical tasks that a preRenderingListener might perform would be to set a
    * model, set an iterator, change attribute values, or initialize properties
    * used in rendering.
    * @param callback listener to receive  notification when rendering is about to occur.
    */
    public function onPreRenderDo() {
        if (empty($this->preRenderListeners)) {
            $this->preRenderListeners = new Wact_Event_Notifier();
        }
        $listener = func_get_args();
        $this->preRenderListeners->addListener($listener);
    }

    /**
    * Binds this view to other views in the composite tree of views.
    * @param object Wact_View_Root The root component for the tree containing this View.
    * @param object Wact_View_Scoped the view holding the parent model.
    */
    public function bindScope($root, $myScope) {
        $this->root = $root;
        $this->rootScope = $root;
        $this->myScope = $myScope;

        if ($myScope->parent) {
            $this->parentScope = $myScope->parent->myScope;
        } else {
            $this->parentScope = $myScope;
        }

        foreach($this->children as $child) {
            $child->bindScope($root, $myScope);
        }
    }

    /**
    * Render the children of this View
    */
    protected function renderChildren() {
        foreach($this->children as $child) {
            $child->render();
        }
    }

    /**
    * Override this template method to paint this view.
    * You must call render() on each child of this view in order for that
    * child to render in the correct position,  or call renderChildren() to
    * render all children at a single position.
    */
    protected function paint() {
        $this->renderChildren();
    }

    /**
    * Outputs this component, rendering any child components as well.
    * Render should never be called unless bindScope is called first.
    */
    public function render() {
        if (isset($this->preRenderListeners)) {
            $this->preRenderListeners->notify($this);
        }
        $this->paint();
    }

}

?>