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

/**
* Acts on the Wact_Template_Compiler_Node_Root in response to events within the Wact_Template_Compiler_Parser_Source
*
* When adding an open tag to the tree, call pushTag().  When closing
* a tag, call popTag(), which ensures the tree is balanced.
*
* These methods do not actually add nodes to the tree, as tags and nodes
* do not necessarily match up.
*
* To add a node to the tree, you have the following choices.  To add a node
* which can have children, use pushNode().  To add a terminal node use addNode(),
* or addTextNode().
*/
class Wact_Template_Compiler_Parser_TreeBuilder {

    /**
    * Current Component
    * @var Wact_Template_Compiler_Node
    */
    private $component;

    /**
    * The last child added since the last pushNode
    * @var CompilerCompononent
    */
    private $lastAddedChild;

    /**
    * Used to locate position within document
    */
    private $locator;

    /**
    * Stack of tags pushed onto the tree builder, may also contain components
    * @see pushTag
    * @see popTag
    * @var array of array($tagname, $info) or array($component)
    */
    private $expectedTags = array();

    /**
    * @var Wact_Template_Compiler_Context
    */
    private $compilingContext;

    /**
    * @param Wact_Template_Compiler_Context $compilingContext
    */
    public function __construct($compilingContext) {
        $this->compilingContext = $compilingContext;
    }

    /**
    * @return Wact_Template_Compiler_Context
    */
    public function getCompilingContext() {
        return $this->compilingContext;
    }

    /**
    * Sets the Document Locator
    */
    function setDocumentLocator($locator) {
        $this->locator = $locator;
    }

    /**
    * Returns the current component
    * @return Wact_Template_Compiler_Node
    */
    function getCursor() {
        return $this->component;
    }

    /**
    * Sets the cursor (the current working component) of the tree builder
    * @param Wact_Template_Compiler_Node
    * @return void
    */
    function setCursor($component) {
        $this->component = $component;
        unset($this->lastAddedChild);
    }

    /**
    * Begins a component's build phase in relation to the component tree.
    * Adds a component to the tree, then makes that component the 'cursor'.
    * Return the result of the component's preParse() method which is the
    * parsing instruction FORBID_PARSING or REQUIRE_PARSING.
    * @param Wact_Template_Compiler_Node
    * @return void
    */
    function pushNode($newComponent) {
        array_push($this->expectedTags, array(
            'tag' => $newComponent->getTag(), 
            'count' => 1, 
            'component' => $this->component));

        $this->component->addChild($newComponent);
        $this->setCursor($newComponent);

        // scheduled for removal from component: taginfo to contain nesting level info
        $this->component->checkNestingLevel();

        return $this->component->preParse($this);
    }

    /**
    * Adds a component to the tree, without descending into it.
    * This begins and finishes the component's composition
    * @param Wact_Template_Compiler_Node
    * @return void
    */
    function addNode($childComponent) {
        $this->component->addChild($childComponent);
        $this->lastAddedChild = $childComponent;
        
        return $childComponent->preParse($this);
    }

    /**
    * Helper method to add a TextNode, given plain text
    * @param string text to create node for
    * @return void
    */
    function addTextNode($text) {
        if (isset($this->lastAddedChild) && is_a($this->lastAddedChild, 'Wact_Template_Compiler_Node_Text')) {
            $this->lastAddedChild->append($text);
        } else {
            $textNode = new Wact_Template_Compiler_Node_Text(NULL, NULL, NULL, $text);
            $this->addNode($textNode);
        }
    }

    /**
    * Ends a component's build phase in relation to the tree.
    * Checks child view ids and moves the 'cursor' up the tree to the parent
    * component.
    * @return void
    */
    function popNode() {
        $this->checkViewIds();
        $this->setCursor($this->component->parent);
    }

    /**
    * Expects the passed tag.  Optionally $info may be passed which is info
    * about that tag.  The parser state that calls TreeBuilder may use this info
    * to differentiate, say, plain vs. component tags.
    * @param string tag name
    * @return void
    */
    function pushTag($tag) {
        $item = end($this->expectedTags);
        if ($item && strcasecmp($item['tag'], $tag) == 0) {
            $expectedTagItem = array_pop($this->expectedTags);
            $expectedTagItem['count']++;
            array_push($this->expectedTags, $expectedTagItem);
        }
    }

    /**
    * Tests the passed tag against what is expected.  Returns any info that
    * was kept about the expected tag.
    * If the item in the tag stack is a component, then the cursor is
    * restored to that, and popTag is called again.
    * @param string tag name
    * @return mixed info
    */
    function popTag($tag) {
        $item = end($this->expectedTags);
        if ($item && strcasecmp($item['tag'], $tag) == 0) {
            $expectedTagItem = array_pop($this->expectedTags);
            $expectedTagItem['count']--;
            if ($expectedTagItem['count']) {
                // Need more closing tags to close this component
                array_push($this->expectedTags, $expectedTagItem);
                return FALSE;
            }
            $this->checkViewIds();
            $this->setCursor($expectedTagItem['component']);
            return TRUE;
        } else {
            // Not a tag we care about
            return FALSE;
        }
    }

    /**
    * Builds a component, adding attributes
    * @param Wact_Template_Compiler_Tag_Info
    * @param string XML tag name of component
    * @param array attributes for tag
    * @param boolean whether the tag has contents
    * @return Wact_Template_Compiler_Node
    */
    function buildComponent($tagInfo, $tag, $attrs, $closing) {

        $class = $tagInfo->getName();
        $sourceLocation = new Wact_Template_Compiler_Location(
            $this->locator->getPublicId(),
            $this->locator->getLineNumber());

        $component = new $class($tag, $tagInfo, $sourceLocation);

        $component->setClosing($closing);

        foreach ( $attrs as $name => $value ) {
            $listener = new Wact_Template_Compiler_Parser_AttributeListener($component, $name);
            $parser = new Wact_Template_Compiler_Expression_Parser($this->compilingContext);
            $parser->parseFragments($listener, $value);
        }

        return $component;
    }

    /**
    * Builds content node(s), adding it (them) to the component tree.
    * A single piece of content may actually be a mix of terminal nodes
    * (Wact_Template_Compiler_Node_Texts and Expressions)
    * @param string content of tag
    * @return void
    */
    function addContent($text) {
        $listener = new Wact_Template_Compiler_Parser_ContentListener($this, $this->locator);
        $parser = new Wact_Template_Compiler_Expression_Parser($this->compilingContext);
        $parser->parseFragments($listener, $text, 'Wact_Template_Compiler_Expression_Node_Html');
    }

    /**
    * Checks that each immediate child of the current component has a unique ID
    * amongst its siblings.
    * @param object Wact_Template_Compiler_Node
    * @return void
    */
    function checkViewIds() {
        $childViewIds = array();
        $children = $this->component->getChildren();
        foreach($children as $child) {
            $id = $child->getViewId();
            if (in_array($id, $childViewIds)) {
                throw new Wact_Template_Compiler_Exception(
                    'Duplicate id "{ViewId}" for tag "{Tag}" in template file "{Filename}" on line {Lineno}',
                    $id,
                    $child->getTag(),
                    $child->getSourceLocation()->file,
                    $child->getSourceLocation()->line
                );
            } else {
                $childViewIds[] = $id;
            }
        }
    }

    /**
    * @param string $source
    * @param Wact_Template_Compiler_Node $tree
    */
    public function parseSource($source, $tree, $publicId) {
        $tagCountBeforeParse = count($this->expectedTags);
        
        $sfp = new Wact_Template_Compiler_Parser_Source($source, $this);

        $this->setCursor($tree);

        $sfp->parse($tree, $publicId);

        $tagCount = count($this->expectedTags);
        if ($tagCountBeforeParse < $tagCount) {
            $item = array_pop($this->expectedTags);
            throw new Wact_Template_Compiler_Exception(
                'Closing tag not found for tag "{Tag}" in template file "{Filename}" on line {Lineno}',
                $item['tag'],
                $tree->getSourceLocation()->file,
                $tree->getSourceLocation()->line
            );
        }

        if ($tagCountBeforeParse > $tagCount) {
            throw new Wact_Template_Compiler_Exception(
                'Unbalanced Tree in template file "{Filename}" on line {Lineno}',
                $tree->getSourceLocation()->file,
                $tree->getSourceLocation()->line
            );
        }

    }

}
?>
