<?php

/**
 * $header
 * @copyright Copyright 2007, Peter Goodman
 * @package valkyrie
 * @subpackage Template.parser
 * @author Peter Goodman
 */

define ('VK_TPL_TAGS_DIR', dirname(__FILE__) .'/tags/');

/**
 * Parse a document for specific XML tags.
 * @see Response
 */
class TemplateParser {
    
    /**
     * Get a tag handler class name. If the tag handler doesn't exist, then
     * use the UnknownNode tag handler.
     * @param string $tag_name The tag name of the tag handler to look for.
     * @return string The class name of the tag handler to use.
     * @see UnknownNode, ClosingNode, NonClosingNode
     * @internal
     */
    private function getTagHandler($prefix = '', $tag_name)
    {
        // default to the unknown node tag handler
        $ret = 'UnknownNode';
                
        $file = VK_TPL_TAGS_DIR .'/'. $prefix .'/'. $tag_name .'.php';
        if(file_exists($file)) {
            require_once $file;
            
            $class = ucfirst($prefix) . camelize($tag_name) .'Node';
            
            if(class_exists($class, FALSE)) {
                $ret = $class;
            }
        }
        
        return new $ret($prefix, $tag_name);
    }
    
    /**
     * Parse the contents of a file and return the parsed text.
     * @param string $file_name The absolute path to the file to parse.
     * @return string The parsed file contents.
     * @see TemplateParser::parseTags(), TemplateParser::parseVariables()
     */
    public function parse($file_name = '')
    {
        $buffer = '';
        
        if(file_exists($file_name)) {
            $buffer = file_get_contents($file_name);
            $buffer = $this->parseTags($buffer);        
            $buffer = $this->parseVariables($buffer);
        }
                
        return $buffer;
    }
    
    /**
     * Parse out the template tags.
     * @param string $buffer The text to parse.
     * @return string The parsed text.
     * @internal
     */
    private function parseTags($buffer = '')
    {
        // split up the buffer into tags and text
        $parts = preg_split("~<(/?)([a-z0-9_]+)\:([a-z0-9_]+)((?: [^>]*)?)>~i", $buffer, -1, PREG_SPLIT_DELIM_CAPTURE);
        
        // create a node stack, this will keep track of
        // non-text nodes, and push our root node onto
        // it as the first node.
        $stack = array();
        $stack[] = new RootNode('');
                
        // loop through the split up parts of the buffer and
        // also increment a tracker
        $i = -1;
        while(isset($parts[++$i])) {

            // get the last node added to the stack
            $parent = end($stack);
                        
            // 0 for text, the 1-4 for tag info
            $key = $i % 5;
            
            // stuff before
            if($key == 0) {
                $parent->buffer($parts[$i]);
            }
            
            // do we have a tag?
            if(isset($parts[$i+4])) {
                
                // get the tag info
                $closing = trim($parts[$i+1]) == '/';
                $prefix = trim($parts[$i+2]);
                $tag_name = trim($parts[$i+3]);
                $attribs = trim($parts[$i+4]);
                $non_closing = FALSE;
                
                // this tag is non-closing
                if($attribs != '' && $attribs{strlen($attribs)-1} == '/') {
                    $non_closing = TRUE;
                    $attribs = trim(substr($attribs, 0, -1));
                }
                
                // closing tag
                if($closing) {
                    $tag = array_pop($stack);
                    $parent = end($stack);
                    
                    // right closing tag
                    if($tag_name == $tag->getName()) {
                        $parent->buffer($tag->parseTag());
                    
                    // wrong closing tag
                    } else {
                        $parent->buffer('<!-- BAD CLOSING TAG FOR ['. $prefix .':'. $tag_name .'] -->');
                    }
                
                // opening tag
                } else {
                    
                    // get the tag handler
                    $tag = $this->getTagHandler($prefix, $tag_name);
                    
                    // parse out the attributes
                    if($attribs != '') {
                        preg_match_all('~(?P<attrib>[a-z]+)="(?P<value>[^"]*)"~i', $attribs, $attributes);
                        $attributes = $this->parseTagAttributes($attributes);
                        
                        if(!empty($attributes)) {
                            $tag->addAttributes($attributes);
                        }
                    }
                    
                    // non-closing node, parse it right away and add it to the parent
                    // nodes buffer
                    if($non_closing) {
                        $parent->buffer($tag->parseTag());
                    
                    // opening tag of a closing node, add it to the stack
                    } else {
                        $stack[] = $tag;
                    }
                }
            }
            
            $i+= 4;
        }
        
        // parse out any tags that somehow weren't properly parsed. If there were
        // any tags that weren't properly closed, this will catch them.
        $parsed_buffer = '';
        $temp_buffer = '';
        while($node = array_pop($stack)) {
        
            // is this the root node?
            if($node instanceof RootNode) {
                // add any text back into it from malformed
                // tags then parse it.
                $node->buffer($temp_buffer);
                
                // parse the root node
                $parsed_buffer = $node->parseTag();
            }
            
            // extract the buffers from the malformed tags
            // to put them back into the root node later.
            else {
                $temp_buffer .= $node->buffer();
            }
        }
        
        return $parsed_buffer;
    }
    
    /**
     * Parse the tag attributes.
     * @param array $parts The tag attributes.
     * @return array An associative array of attributes => value
     * @internal
     */
    private function parseTagAttributes(array $parts = array())
    {       
        $attributes = array();
            
        foreach($parts['attrib'] as $i => $attrib) {
            $attributes[strtolower($attrib)] = $parts['value'][$i];
        }
        
        return $attributes;
    }
    
    /**
     * Handle matched template variables.
     * @param array $matches An array returned from a preg_replace_callback
     * @return string A compiled version of the template variable.
     * @see TemplateParser::parseVariables, preg_replace_callback
     * @internal
     */
    private function handleVariable($matches) {
                
        $matches[2] = trim($matches[2]);
        $buffer = '';
        
        // template variable
        if($matches[1] == '$') {
            
            if($matches[2] != '') {
                $parts = explode('|', $matches[2]);
                $varname = array_shift($parts);
                
                $buffer = '$scope["'. $varname .'"]';
                
                foreach($parts as $callback) {
        
                    // call a callback if it exists
                    if($callback !== NULL) {
                        $callback_args = explode(":", $callback);
                        $callback = array_shift($callback_args);
                    
                        if(function_exists($callback)) {
                            $callback_args = !empty($callback_args) ? ','. implode(",", $callback_args) : '';
                            $buffer = $callback .'('. $buffer . $callback_args .')';
                        }
                    }
                }
                
                if($matches[1] == '$') {
                    $buffer = '<?='. $buffer .';?>';
                }
            }
        }
        
        // url / path variable
        else if($matches[1] == '/') {
            if($matches[0] == '{/}') {
                $matches[2] = '/';
            }
            
            $buffer = vk_url(vk_path($matches[2]));
            
            // need to put $ in subpattern so that handleVariable accepts it properly :P
            $buffer = preg_replace_callback("~(\\$)([^/]+)~", array($this, 'handleVariable'), $buffer);
        }
        
        // get text
        else {
            
            $buffer = '';
        }
        
        return $buffer;
    }
    
    /**
     * Find and parse variables in the text.
     * @param string $buffer The text to parse.
     * @return string The text with all matched variables compiled.
     * @see TemplateParser::parseVariables, preg_replace_callback
     * @internal
     */
    private function parseVariables($buffer)
    {
        $buffer = preg_replace_callback('~{(\$|\@|\/)([^}]*)}~', array($this, 'handleVariable'), $buffer);      
        return $buffer;
    }
}

/**
 * Exception for template nodes, gets called for missing attributes.
 */
class TemplateNodeException extends Exception {
    
}

/**
 * A template node, this is any xml tag that uses a namespace,
 * for example: <tpl:something>
 */
abstract class TemplateNode implements ArrayAccess {
    
    /**
     * The text in-between the opening and closing nodes od this tag.
     * @internal
     */
    private $buffer = '';
    
    /**
     * This tags name.
     * @internal
     */
    protected $name = '',
            $prefix = '';
    
    /**
     * An associative array of this tags attributes.
     * @internal
     */
    private $attributes = array();
    
    /**
     * Constructor, pass in this tags name.
     * @param string $name The tag name.
     */
    public function __construct($prefix = '', $name = '') {
        $this->prefix = $prefix;
        $this->name = $name;
    }
    
    /**
     * Add the tags attributes to the attributes array.
     * @param array $array An associative array of attribute=>value.
     */
    public function addAttributes(array $array = array()) {
        $this->attributes = array_merge($this->attributes, $array);
    }
    
    /**
     * Add text to this tags buffer.
     * @param string $buffer The text to append to the buffer.
     * @see ClosingNode::$buffer
     * @internal
     */
    public function buffer($buffer = FALSE) {
        if($buffer !== FALSE) {
            $this->buffer .= $buffer;
        } else {
            return $this->buffer;
        }
    }
    
    /**
     * Get the name of this tag.
     * @return string The tag name.
     */
    public function getName() {
        return $this->name;
    }
    
    /**
     * Require that this tag be passed certain arguments.
     * <code>
     * $this->requireAttributes('href', 'title', ...);
     * </code>
     */
    public function requireAttributes() {
        foreach(func_get_args() as $attrib) {
            if(!array_key_exists($attrib, $this->attributes)) {
                throw new TemplateNodeException("Missing attribute [$attrib] for tag [{$this->name}].");
            }
        }
    }
    
    /**
     * Require at least one out of a number of attributes to exist.
     * <code>
     * $this->requireOne('href', 'src');
     * </code>
     * @return boolean The first present attribute from the set found.
     */
    public function requireOne() {
        $ret = FALSE;
        $attribs = func_get_args();
        foreach($attribs as $attrib) {
            if(array_key_exists($attrib, $this->attributes)) {
                $ret = $attrib;
                break;
            }
        }
        
        if($ret === FALSE) {
            throw new TemplateNodeException("Missing at least one required attribute of set [". implode(',', $attribs) ."] for tag [{$this->name}].");
        }
        
        return $ret;
    }
    
    /**
     * Get an attribute.
     * @param string $key The attribute name.
     * @return mixed The value of the attribute.
     * @see ArrayAccess
     */
    public function offsetGet($key) {
        return $this->offsetExists($key) ? $this->attributes[$key] : NULL;
    }
    
    /**
     * Set an attribute.
     * @param string $key The attribute name.
     * @param mixed $val The value of the attribute to set.
     * @see ArrayAccess
     */
    public function offsetSet($key, $val) {
        $this->attributes[$key] = $val;
    }
    
    /**
     * Check if an attribute exists.
     * @param string $key The attribute name.
     * @return boolean Whether or not the attribute exists.
     * @see ArrayAccess
     */
    public function offsetExists($key) {
        return isset($this->attributes[$key]);
    }
    
    /**
     * Unset an attribute.
     * @param string $key The attribute name.
     * @see ArrayAccess
     */
    public function offsetUnset($key) {
        unset($this->attributes[$key]);
    }
    
    /**
     * Parse the tag.
     */
    abstract public function parseTag();
}

/**
 * Class to handle the root node in the template parser stack.
 */
class RootNode extends TemplateNode {
    public function parseTag() {
        return $this->buffer();
    }
}

/**
 * Class to handle nodes that the parser doesn't recognize.
 */
class UnknownNode extends TemplateNode {
    public function parseTag() {
        
        $buffer = "<". $this->prefix .":". $this->name .">\n";
        $buffer .= $this->buffer();
        $buffer .= "\n</". $this->prefix .':'. $this->name .'>';
        
        return $buffer;
    }
}

