<?php
/**
 * A class for rendering a full page.
 *
 * This class provides a way to render a full page composed of three templates
 * (action, layout and shell). It uses the view class with the composite
 * renderer.
 */
class controller_logic_render extends controller_logic
{
    /**
     * @var integer The dynamic template variables options.
     */
    const BASE = 1;
    const LINK = 2;

    /**
     * @var array The cascading directory list.
     */
    protected $cascade;
    
    /**
     * @var array The inner directory list.
     */
    protected $template, $action, $layout, $shell, $skin;
    
    /**
     * @var array The default template variables.
     */
    protected $data;
    
    /**
     * @var array The template variables to be dynamically created at rendering
     * time.
     */
    protected $dynamic;
    
    ////////////////////////////////////////////////////////////////////////////
    // Construct and destruct functions.

    /**
     * @param string/array (optional) $directory The cascading directory list.
     * @param string (optional) $template The template inner directory.
     * @param string (optional) $action The action inner directory.
     * @param string (optional) $layout The layout inner directory.
     * @param string (optional) $shell The shell inner directory.
     * @param string (optional) $skin The skin inner directory.
     * @param array (optional) $data The default template variables.
     * @param integer/array (optional) $dynamic The dynamic template variables
     * to include.
     */
    public function __construct($directory = '', $template = '', $action = '', $layout = '', $shell = '', $skin = '', array $data = array(), $dynamic = array())
    {
        if (!is_array($directory))
        {
            $directory = array($directory);
        }
        
        $this->cascade = $directory;
        $this->template = $template;
        $this->action = $action;
        $this->layout = $layout;
        $this->shell = $shell;
        $this->skin = $skin;
        $this->data = $data;
        
        if (!is_array($dynamic))
        {
            $dynamic = array($dynamic);
        }
        
        $this->dynamic = $dynamic;
    }

    ////////////////////////////////////////////////////////////////////////////
    // Overload functions.

    ////////////////////////////////////////////////////////////////////////////
    // Interface functions.

    /**
     * Sets a full rendered page to the response.
     * 
     * @param string (optional) $action The action template filename.
     * @param string (optional) $layout The layout template filename.
     * @param string (optional) $shell The shell template filename.
     * @param array (optional) $data The template variables.
     * @throws logicexception If any template files could not be found.
     * @throws logicexception If a 'request' template variable is given.
     * @return controller A fluent interface.
     */
    public function execute($action = null, $layout = null, $shell = null, array $data = array())
    {
        $template = array('action' => '', 'layout' => '', 'shell' => '');
        
        foreach (array_keys($template) as $t)
        {
            if (is_array($$t))
            {
                $data = $$t;
                $$t = null;
                break;
            }
        }
        
        $directory = $this->cascade;
        
        if ($this->directory !== null)
        {
            array_unshift($directory, $this->directory);
        }
        
        $extension = '';
        
        foreach (array_keys($template) as $t)
        {
            if (is_file($$t))
            {
                $template[$t] = $$t;
                continue;
            }
            
            if (preg_match('~^.+\.' . (($extension) ? $extension : '[a-z0-9]+') . '$~', $$t))
            {
                $extension = pathinfo($$t, PATHINFO_EXTENSION);
                $$t = pathinfo($$t, PATHINFO_FILENAME);
            }
            
            if (!$$t)
            {
                $$t = $this->$t . $t;
            }
            
            $file = $this->find($this->template . $$t . (($extension) ? '.' . $extension : '') . '.php', $directory);
            
            if (!$file)
            {
                throw new logicexception(ucfirst($t) . ' template file could not be found.');
            }
            
            $template[$t] = $file;
        }
        
        $data = self::merge($this->data, $data);
        
        if (array_key_exists('request', $data))
        {
            throw new logicexception('Template variable \'request\' is a reserved variable.');
        }
        
        $data['request'] = $this->request;
        
        if (in_array(self::BASE, $this->dynamic, true))
        {
            $data['base'] = array('href' => $this->request->base());
        }
        
        if (in_array(self::LINK, $this->dynamic, true))
        {
            foreach ($directory as $d)
            {
                if (preg_match('~^' . $d . '~', $template['action']))
                {
                    $data['link'][] = array('rel' => 'stylesheet', 'type' => 'text/css', 'media' => 'screen', 'href' => $d . $this->skin . $action . '.css');
                    break;
                }
            }
        }
        
        $view = new view();
        $body = $view->render(array_reverse($template), $data);
        $this->response->set($body);
        return $this;
    }

    ////////////////////////////////////////////////////////////////////////////
    // Internal functions.

    /**
     * Finds a file in the cascading directory list.
     * 
     * @param string $file The filename.
     * @param array (optional) $directory The cascading directory list.
     * @return string The filename if found, else an empty string.
     */
    protected function find($file, array $directory = null)
    {
        if ($directory === null)
        {
            $directory = $this->cascade;
        }
        
        foreach ($directory as $d)
        {
            $f = $d . $file;
            
            if (is_file($f))
            {
                return $f;
            }
        }
    }

    /**
     * Merges recursively two arrays (the recursion only occurs when both keys
     * are arrays themselves, otherwise it overwrites).
     *
     * @param array $first The initial array. 
     * @param array $second The array to merge.
     * @return array The merged array.
     */
    protected static function merge(array &$first, array &$second)
    {
        $merged = $first;
        
        foreach ($second as $k => $v)
        {
            if (array_key_exists($k, $merged) && is_array($merged[$k]) && is_array($v))
            {
                $merged[$k] = self::merge($merged[$k], $v);
            }
            else
            {
                $merged[$k] = $v;
            }
        }
        
        return $merged;
    }
}
