<?php
/**
 * PHPTAL templating engine
 *
 * PHP Version 5
 *
 * @category HTML
 * @package  PHPTAL
 * @author   Laurent Bedubourg <lbedubourg@motion-twin.com>
 * @author   Kornel LesiƄski <kornel@aardvarkmedia.co.uk>
 * @license  http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
 * @version  SVN: $Id$
 * @link     http://phptal.org/
 */
/**
 * Helps generate php representation of a template.
 *
 * @package PHPTAL
 * @subpackage Php
 * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
 */
class PHPTAL_Php_CodeWriter
{
    /**
     * max id of variable to give as temp
     */
    private $temp_var_counter=0;
    /**
     * stack with free'd variables
     */
    private $temp_recycling=array();

    /**
     * keeps track of seen functions for function_exists
     */
    private $known_functions = array();


    public function __construct(PHPTAL_Php_State $state)
    {
        $this->_state = $state;
    }

    public function createTempVariable()
    {
        if (count($this->temp_recycling)) return array_shift($this->temp_recycling);
        return '$_tmp_'.(++$this->temp_var_counter);
    }

    public function recycleTempVariable($var)
    {
        if (substr($var, 0, 6)!=='$_tmp_') throw new PHPTAL_Exception("Invalid variable recycled");
        $this->temp_recycling[] = $var;
    }

    public function getCacheFilesBaseName()
    {
        return $this->_state->getCacheFilesBaseName();
    }

    public function getResult()
    {
        $this->flush();
        if (version_compare(PHP_VERSION, '5.3', '>=') && __NAMESPACE__) {
            return '<?php use '.'PHPTALNAMESPACE as P; ?>'.trim($this->_result);
        } else {
            return trim($this->_result);
        }
    }

    /**
     * set full '<!DOCTYPE...>' string to output later
     *
     * @param string $dt
     *
     * @return void
     */
    public function setDocType($dt)
    {
        $this->_doctype = $dt;
    }

    /**
     * set full '<?xml ?>' string to output later
     *
     * @param string $dt
     *
     * @return void
     */
    public function setXmlDeclaration($dt)
    {
        $this->_xmldeclaration = $dt;
    }

    /**
     * functions later generated and checked for existence will have this prefix added
     * (poor man's namespace)
     *
     * @param string $prefix
     *
     * @return void
     */
    public function setFunctionPrefix($prefix)
    {
        $this->_functionPrefix = $prefix;
    }

    /**
     * @return string
     */
    public function getFunctionPrefix()
    {
        return $this->_functionPrefix;
    }

    /**
     * @see PHPTAL_Php_State::setTalesMode()
     *
     * @param string $mode
     *
     * @return string
     */
    public function setTalesMode($mode)
    {
        return $this->_state->setTalesMode($mode);
    }

    public function splitExpression($src)
    {
        preg_match_all('/(?:[^;]+|;;)+/sm', $src, $array);
        $array = $array[0];
        foreach ($array as &$a) $a = str_replace(';;', ';', $a);
        return $array;
    }

    public function evaluateExpression($src)
    {
        return $this->_state->evaluateExpression($src);
    }

    public function indent()
    {
        $this->_indentation ++;
    }

    public function unindent()
    {
        $this->_indentation --;
    }

    public function flush()
    {
        $this->flushCode();
        $this->flushHtml();
    }

    public function noThrow($bool)
    {
        if ($bool) {
            $this->pushCode('$ctx->noThrow(true)');
        } else {
            $this->pushCode('$ctx->noThrow(false)');
        }
    }

    public function flushCode()
    {
        if (count($this->_codeBuffer) == 0) return;

        // special treatment for one code line
        if (count($this->_codeBuffer) == 1) {
            $codeLine = $this->_codeBuffer[0];
            // avoid adding ; after } and {
            if (!preg_match('/\}\s*$|\{\s*$/', $codeLine))
                $this->_result .= '<?php '.$codeLine."; ?>\n"; // PHP consumes newline
            else
                $this->_result .= '<?php '.$codeLine." ?>\n"; // PHP consumes newline
            $this->_codeBuffer = array();
            return;
        }

        $this->_result .= '<?php '."\n";
        foreach ($this->_codeBuffer as $codeLine) {
            // avoid adding ; after } and {
            if (!preg_match('/[{};]\s*$/', $codeLine)) {
                $codeLine .= ' ;'."\n";
            }
            $this->_result .= $codeLine;
        }
        $this->_result .= "?>\n";// PHP consumes newline
        $this->_codeBuffer = array();
    }

    public function flushHtml()
    {
        if (count($this->_htmlBuffer) == 0) return;

        $this->_result .= implode('', $this->_htmlBuffer);
        $this->_htmlBuffer = array();
    }

    /**
     * Generate code for setting DOCTYPE
     *
     * @param bool $called_from_macro for error checking: unbuffered output doesn't support that
     */
    public function doDoctype($called_from_macro = false)
    {
        if ($this->_doctype) {
            $code = '$ctx->setDocType('.$this->str($this->_doctype).','.($called_from_macro?'true':'false').')';
            $this->pushCode($code);
        }
    }

    /**
     * Generate XML declaration
     *
     * @param bool $called_from_macro for error checking: unbuffered output doesn't support that
     */
    public function doXmlDeclaration($called_from_macro = false)
    {
        if ($this->_xmldeclaration && $this->getOutputMode() !== PHPTAL::HTML5) {
            $code = '$ctx->setXmlDeclaration('.$this->str($this->_xmldeclaration).','.($called_from_macro?'true':'false').')';
            $this->pushCode($code);
        }
    }

    public function functionExists($name)
    {
        return isset($this->known_functions[$this->_functionPrefix . $name]);
    }

    public function doTemplateFile($functionName, PHPTAL_Dom_Element $treeGen)
    {
        $this->doComment("\n*** DO NOT EDIT THIS FILE ***\n\nGenerated by PHPTAL from ".$treeGen->getSourceFile()." (edit that file instead)");
        $this->doFunction($functionName, 'PHPTAL $tpl, PHPTAL_Context $ctx');
        $this->setFunctionPrefix($functionName . "_");
        $this->doSetVar('$_thistpl', '$tpl');
        $this->doInitTranslator();
        $treeGen->generateCode($this);
        $this->doComment("end");
        $this->doEnd('function');
    }

    public function doFunction($name, $params)
    {
        $name = $this->_functionPrefix . $name;
        $this->known_functions[$name] = true;

        $this->pushCodeWriterContext();
        $this->pushCode("function $name($params) {\n");
        $this->indent();
        $this->_segments[] =  'function';
    }

    public function doComment($comment)
    {
        $comment = str_replace('*/', '* /', $comment);
        $this->pushCode("/* $comment */");
    }

    public function doInitTranslator()
    {
        if ($this->_state->isTranslationOn()) {
            $this->doSetVar('$_translator', '$tpl->getTranslator()');
        }
    }

    public function getTranslatorReference()
    {
        if (!$this->_state->isTranslationOn()) {
            throw new PHPTAL_ConfigurationException("i18n used, but Translator has not been set");
        }
        return '$_translator';
    }

    public function doEval($code)
    {
        $this->pushCode($code);
    }

    public function doForeach($out, $source)
    {
        $this->_segments[] =  'foreach';
        $this->pushCode("foreach ($source as $out):");
        $this->indent();
    }

    public function doEnd($expects = null)
    {
        if (!count($this->_segments)) {
            if (!$expects) $expects = 'anything';
            throw new PHPTAL_Exception("Bug: CodeWriter generated end of block without $expects open");
        }

        $segment = array_pop($this->_segments);
        if ($expects !== null && $segment !== $expects) {
            throw new PHPTAL_Exception("Bug: CodeWriter generated end of $expects, but needs to close $segment");
        }

        $this->unindent();
        if ($segment == 'function') {
            $this->pushCode("\n}\n\n");
            $this->flush();
            $functionCode = $this->_result;
            $this->popCodeWriterContext();
            $this->_result = $functionCode . $this->_result;
        } elseif ($segment == 'try')
            $this->pushCode('}');
        elseif ($segment == 'catch')
            $this->pushCode('}');
        else
            $this->pushCode("end$segment");
    }

    public function doTry()
    {
        $this->_segments[] =  'try';
        $this->pushCode('try {');
        $this->indent();
    }

    public function doSetVar($varname, $code)
    {
        $this->pushCode($varname.' = '.$code);
    }

    public function doCatch($catch)
    {
        $this->doEnd('try');
        $this->_segments[] =  'catch';
        $this->pushCode('catch('.$catch.') {');
        $this->indent();
    }

    public function doIf($condition)
    {
        $this->_segments[] =  'if';
        $this->pushCode('if ('.$condition.'): ');
        $this->indent();
    }

    public function doElseIf($condition)
    {
        if (end($this->_segments) !== 'if') {
            throw new PHPTAL_Exception("Bug: CodeWriter generated elseif without if");
        }
        $this->unindent();
        $this->pushCode('elseif ('.$condition.'): ');
        $this->indent();
    }

    public function doElse()
    {
        if (end($this->_segments) !== 'if') {
            throw new PHPTAL_Exception("Bug: CodeWriter generated else without if");
        }
        $this->unindent();
        $this->pushCode('else: ');
        $this->indent();
    }

    public function doEcho($code)
    {
        if ($code === "''") return;
        $this->flush();
        $this->pushCode('echo '.$this->escapeCode($code));
    }

    public function doEchoRaw($code)
    {
        if ($code === "''") return;
        $this->pushCode('echo '.$this->stringifyCode($code));
    }

    public function interpolateHTML($html)
    {
        return $this->_state->interpolateTalesVarsInHtml($html);
    }

    public function interpolateCDATA($str)
    {
        return $this->_state->interpolateTalesVarsInCDATA($str);
    }

    public function pushHTML($html)
    {
        if ($html === "") return;
        $this->flushCode();
        $this->_htmlBuffer[] =  $html;
    }

    public function pushCode($codeLine)
    {
        $this->flushHtml();
        $codeLine = $this->indentSpaces() . $codeLine;
        $this->_codeBuffer[] =  $codeLine;
    }

    /**
     * php string with escaped text
     */
    public function str($string)
    {
        return "'".strtr($string,array("'"=>'\\\'','\\'=>'\\\\'))."'";
    }

    public function escapeCode($code)
    {
        return $this->_state->htmlchars($code);
    }

    public function stringifyCode($code)
    {
        return $this->_state->stringify($code);
    }

    public function getEncoding()
    {
        return $this->_state->getEncoding();
    }

    public function interpolateTalesVarsInString($src)
    {
        return $this->_state->interpolateTalesVarsInString($src);
    }

    public function setDebug($bool)
    {
        return $this->_state->setDebug($bool);
    }

    public function isDebugOn()
    {
        return $this->_state->isDebugOn();
    }

    public function getOutputMode()
    {
        return $this->_state->getOutputMode();
    }

    public function quoteAttributeValue($value)
    {
        // FIXME: interpolation is done _after_ that function, so ${} must be forbidden for now

        if ($this->getEncoding() == 'UTF-8') // HTML 5: 8.1.2.3 Attributes ; http://code.google.com/p/html5lib/issues/detail?id=93
        {
            // regex excludes unicode control characters, all kinds of whitespace and unsafe characters
            // and trailing / to avoid confusion with self-closing syntax
            $unsafe_attr_regex = '/^$|[&=\'"><\s`\pM\pC\pZ\p{Pc}\p{Sk}]|\/$|\${/u';
        } else {
            $unsafe_attr_regex = '/^$|[&=\'"><\s`\0177-\377]|\/$|\${/';
        }

        if ($this->getOutputMode() == PHPTAL::HTML5 && !preg_match($unsafe_attr_regex, $value)) {
            return $value;
        } else {
            return '"'.$value.'"';
        }
    }

    public function pushContext()
    {
        $this->doSetVar('$ctx', '$tpl->pushContext()');
    }

    public function popContext()
    {
        $this->doSetVar('$ctx', '$tpl->popContext()');
    }

    // ~~~~~ Private members ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    private function indentSpaces()
    {
        return str_repeat("\t", $this->_indentation);
    }

    private function pushCodeWriterContext()
    {
        $this->_contexts[] =  clone $this;
        $this->_result = "";
        $this->_indentation = 0;
        $this->_codeBuffer = array();
        $this->_htmlBuffer = array();
        $this->_segments = array();
    }

    private function popCodeWriterContext()
    {
        $oldContext = array_pop($this->_contexts);
        $this->_result = $oldContext->_result;
        $this->_indentation = $oldContext->_indentation;
        $this->_codeBuffer = $oldContext->_codeBuffer;
        $this->_htmlBuffer = $oldContext->_htmlBuffer;
        $this->_segments = $oldContext->_segments;
    }

    private $_state;
    private $_result = "";
    private $_indentation = 0;
    private $_codeBuffer = array();
    private $_htmlBuffer = array();
    private $_segments = array();
    private $_contexts = array();
    private $_functionPrefix = "";
    private $_doctype = "";
    private $_xmldeclaration = "";
}