<?
class Akyn_Action
{
    protected $obj;
    protected $name;
    protected $parms;

    public function __construct($name, $parms, $obj)
    {
        $this->name = $name;
        $this->parms = $parms;
        $this->obj = $obj;
    }
    
    public function __toString()
    {
        $messages = isset($this->obj->messages[$this->name]) ? $this->obj->messages[$this->name] : array();

        ob_start();
        $obj = 'RT_out_action_' . $this->name;
        $obj = new $obj;

        if (call_user_func(array($obj, 'main'), $this->obj, $this->parms, $messages) & RT_NOTFOUND)
        $_ENV[404] = true;

        return ob_get_clean();
    }
}

class Akyn_Var
{
    protected $obj;
    protected $name;
    protected $values;
    protected $len;
    
    public function __construct($obj, $name, $values = null)
    {
        $this->obj = $obj;
        $this->name = $name;
        $this->values = $values;
        $this->len = sizeof($this->values);
    }
    
    public function __toString()
    {
        if ($this->values === null) return (string) $this->obj->vars[$this->name];
    
        return htmlspecialchars($this->values[$this->obj->vars[$this->name] % $this->len], ENT_COMPAT, 'UTF-8');
    }
}

class Akyn_Node
{
    const TYPE_CHUNK     = '';
    const TYPE_SECTION   = '(';
    const TYPE_CONDITION = '?';
    
    public $type;
    public $vars;
    public $sections;
    public $includes;
    public $actions;
    public $messages;    
    public $basepath;

    protected $rawbody;
    protected $body;
    protected $search;
    protected $replace;
    
    public function __construct($type = self::TYPE_CHUNK)
    {
        $this->basepath = null;
    
        $this->type = $type;
        $this->search = $this->replace = array();
        
        foreach (array('vars', 'includes', 'actions', 'messages', 'sections') as $what)
        $this->$what = new ArrayObject();
    }

    public function __clone()
    {
        $this->type = null;
        $this->search = $this->replace = array();
        $this->body = $this->rawbody = '';
    }

    public function open($template, $section = false)
    {   
        $template = $this->read($template, $section);

        $template = preg_replace_callback(
            '!<\+([^\s>]+)(?:\s+"((?:\\\\.|[^"])+)")?>!suS',
            array($this, 'includeCallback'),
            $template
        );

        $template = preg_replace('!<(\$[^>]+)>!suS', '{$1}', $template);
        
        for ($i = strcspn($template, '{<'), $len = strlen($template); $i<$len;) {
            $sign = substr($template, $i, 2);
        
            switch ($sign)
            {
                case '{$': // переменная
                    $i+=2; // пропускаем {$            
                    $endof = strpos($template, '}', $i);

                    if ($endof !== false) {
                        $varbody = substr($template, $i, $endof-$i);
                        
                        $i = $endof + 1;
                        
                        if ($varbody[0] == '!') { // отрицание переменнной
                            $varname = substr($varbody, 1);
                            $values = array('', "\0");
                        } elseif (strpos($varbody, '?') !== false) { // переменные с альтернативой
                            $values = explode('?', $varbody);
                            $varname = array_shift($values);
                            
                            $values = sizeof($values) > 1 ?
                                array_map(array($this, 'space2nul'), $values) :
                                array("\0", '');
                        } else { // просто переменная
                            $varname = $varbody;
                            $values  = null;
                        }

                        $key = '{$' . $varbody . '}';
                        
                        for (;;) {
                            if (!array_key_exists($varname, $this->vars)) {
                                $this->vars[$varname] = null;
                            } else {
                                // если такая переменная уже есть, надо проверить нет ли её в то же форме, если
                                // есть, то не создавать ещё раз

                                if ($this->alreadyInSearch($key))
                                break;
                            }

                            $this->addReplace($key, new Akyn_Var($this, $varname, $values));
                            break;
                        }
                    } else {
                        $i++;
                    }
                    
                    break;
                case '<:': // секция
                
                    $i += 2;
                    $endof = strpos($template, '>', $i);
                    if ($endof === false) break;
                    
                    $name    = substr($template, $i, $endof - $i);
                    $endName = '</:'  . $name . '>';
                    $end     = strpos($template, $endName, $endof + 1);
             
                    if ($end === false)
                    throw new Exception('Cannot find end of section "' . $name . '"');
                    
                    $sectionInner = 'str://' . substr($template, $endof + 1, $end - $endof - 1);
                    $sectionLen = $end + strlen($endName) - $i + 2; 
                    
                    $child = clone $this;
                    $child->type = self::TYPE_SECTION;
                    $child->open($sectionInner);

                    $key = '{$:' . $name . ':' . $i . '}';

                    $template = substr_replace($template, $key, $i - 2, $sectionLen);
                    $len = strlen($template);

                    $this->search[]  = $key;
                    $this->replace[] = $child;

                    if (isset($this->sections[$name])) {
                        $this->sections[$name][] = $child;
                    } else {
                        $this->sections[$name] = array($child);
                    }
                
                    break;
                    
                case '<&': // action
                
                    $i += 2;
                    // имя action
                    if (preg_match('!\G[^\W>]+!sSu', $template, $matches, null, $i)) {
                        $name  = $matches[0];
                        $parms = array();
                        $shift = $i + strlen($name);
                        
                        if ($stopFound = $template[$shift] == '>') {
                            $shift++;
                        } else
                        while (preg_match('!\G\s+(\w+="(?:\\\\.|[^"])+")(>)?!suS', $template, $matches, null, $shift)) {
                            $shift += strlen($matches[0]);
                            list($var, $value) = explode('=', $matches[1], 2);
                            $parms[$var] = stripslashes(substr($value, 1, -1)); // убираем кавычки
                            
                            if (isset($matches[2])) { // выход — после параметра встретилась >
                                $stopFound = true;
                                break;
                            }

                        }

                        
                        if ($stopFound) {
                            ksort($parms);
                            $key = '{$&' . md5(serialize(array($name, $parms))) . '}';
                            
                            $i -= 2;
                            
                            $template = substr_replace($template, $key, $i, $shift - $i);
                            $len = strlen($template);
                            
                            if (isset($this->messages[$name])) {
                                $messages =  $this->messages[$name];
                                unset($this->messages[$name]);
                            } else {
                                $messages = false;
                            }
                            
                            $this->addReplace($key, $action = new Akyn_Action($name, $parms, $this));
                            
                            $this->actions[$name] = $action;
                            
                            $i += strlen($key);
                        }        
                    }
                    break;
                    
                default:
                    $i++;
                    break;
                    
            }
            
            $i += strcspn($template, '{<', $i);
        }

        $this->rawbody = $template;
        $this->body = '';
    }
    
    protected function getSection($name, $text)
    {
        $start = '<!--'.$name. '-->';
        $end   = '<!--/'.$name. '-->';

        if (($idx = strpos($text, $start)) !== false)
        $text = substr($text, $start + strlen($start));

        if (($idx = strpos($text, $end)) !== false)
        $text = substr($text, 0, $end);
        
        return $text;
    }

    protected function alreadyInSearch($name)
    {
        return array_search($name, $this->search, true) !== false;
    }

    protected function addReplace($name, $value)
    {
        $this->search[]  = $name;
        $this->replace[] = $value;
    }
    
    protected function space2nul($val)
    {
        return $val === '' ? "\0" : $val;
    }
    
    protected function read($filename, $section = false)
    {
        if (strlen($filename) < 7 || substr_compare($filename, 'str://', 0, 6)) {
            if ($filename[0] <> '/' && $this->basepath !== null) {
                $filename = $this->basepath . $filename;
            }

            $filename .= '.html';

            $this->includes[] = $filename;
            $template = file_get_contents($filename);
        } else {
            $template = substr($filename, 6);
        }
    
        if ($section !== false) return $this->getSection($section, $template);
        return $template;
    }
    
    protected function includeCallback($m)
    {
        if (isset($m[2]))
        return $this->read($m[1], stripslashes($m[2])); else
        return $this->read($m[1]);
    }

    public function exists($sectionname)
    {
        return isset($this->sections[$sectionname]);
    }

    public function html($name, $value)
    {
        if ($value === null || $value === false) {
            $this->vars[$name] = "\0";
        } else {
            $this->vars[$name] = $value;
        }        
    }
    
    public function __set($name, $value)
    {
        $this->html($name, is_string($value) ? htmlspecialchars($value, ENT_COMPAT, 'UTF-8') : $value);
    }

    public function __isset($name)
    {
        return isset($this->vars[$name]) && $this->vars[$name] !== null && $this->vars[$name] != "\0";
    }

    public function __unset($name)
    {
        $this->__set($name, null);
    }
    
    public function __toString()
    {
        if ($this->type == self::TYPE_SECTION) {
            $toRet = $this->body;
            $this->body = '';
            
            return $toRet;
        } else {
            return $this->runRawBody();
        }
    }
    
    protected function runRawBody()
    {
        $str = str_replace($this->search, $this->replace, $this->rawbody);
        $zeroCnt = substr_count($str, "\0");
            
        if ($zeroCnt) {
            return preg_replace('!</?\0[^>]*>|\s*[a-zA-Z][a-zA-Z0-9]*="\0"|\0!s', '', $str, $zeroCnt);
        } else {
            return $str;
        }
    }

    public function show($name)
    {
        return $this->iterate($name);
    }
    
    public function iterate($name = null)
    {
        if ($name === null) {
            if ($this->type == self::TYPE_SECTION) {
                $this->body .= $this->runRawBody();
                return true;
            } else {
                return null;
            }
        }

        if (isset($this->sections[$name])) {
            foreach ($this->sections[$name] as $section) {
                $section->iterate();
            }
        } else {
            return null;
        }
    }
    
    public function message($action, $name, $value, $append = false)
    {
        if (!isset($this->messages[$action]))
        $this->messages[$action] = array();
    
        if ($append)
        {
            if (!isset($this->messages[$action][$name]))
            $this->messages[$action][$name] = array();

            $this->messages[$action][$name][] = $value;
        }
        else
        {
            $this->messages[$action][$name] = $value;
        }
    }
}