<?php //$Id$
//Copyright (c) 2012-2016 Pierre Pronchery <khorben@defora.org>
//This file is part of DeforaOS Web DaPortal
//
//This program is free software: you can redistribute it and/or modify
//it under the terms of the GNU General Public License as published by
//the Free Software Foundation, version 3 of the License.
//
//This program is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU General Public License for more details.
//
//You should have received a copy of the GNU General Public License
//along with this program.  If not, see <http://www.gnu.org/licenses/>.



//HTMLFormat
class HTMLFormat extends FormatElements
{
	//protected
	//properties
	protected $doctype = FALSE;
	protected $encoding = FALSE;
	static protected $filter_class = 'HTML';


	//methods
	//essential
	//HTMLFormat::match
	protected function match(Engine $engine, $type = FALSE)
	{
		switch($type)
		{
			case 'text/html':
				return 10;
			default:
				return 0;
		}
	}


	//HTMLFormat::attach
	protected function attach(Engine $engine, $type = FALSE)
	{
		global $config;

		if(($this->encoding = $config->get('defaults', 'charset'))
				=== FALSE)
			$this->encoding = ini_get('default_charset');
		if($this->encoding == '')
			$this->encoding = FALSE;
		//configuration
		$this->javascript = $this->configGet('javascript')
			? TRUE : FALSE;
		//for escaping
		if(!defined('ENT_HTML401'))
			define('ENT_HTML401', 0);
	}


	//accessors
	//HTMLFormat::configGet
	protected function configGet($variable)
	{
		global $config;

		if(($ret = parent::configGet($variable)) !== FALSE)
			return $ret;
		return $config->get('format::html', $variable);
	}


	//HTMLFormat::getJavascript
	protected function getJavascript()
	{
		return $this->javascript;
	}


	//useful
	//escaping
	//HTMLFormat::escape
	protected function escape($text)
	{
		return htmlspecialchars($text,
				ENT_COMPAT | ENT_HTML401 | ENT_NOQUOTES,
				$this->encoding);
	}


	//HTMLFormat::escapeAttribute
	protected function escapeAttribute($text)
	{
		return htmlspecialchars($text, ENT_COMPAT | ENT_HTML401,
				$this->encoding);
	}


	//HTMLFormat::escapeComment
	protected function escapeComment($text)
	{
		$from = '-->';
		$to = '--&gt;';

		return '<!--'.str_replace($from, $to, $text).'-->';
	}


	//HTMLFormat::escapeText
	protected function escapeText($text)
	{
		$from = array('&', '<', '>', "\n", "\r");
		$to = array('&amp;', '&lt;', '&gt;', "<br />\n", '');

		return str_replace($from, $to, $text);
	}


	//HTMLFormat::escapeURI
	protected function escapeURI($text)
	{
		return rawurlencode($text);
	}


	//rendering
	//HTMLFormat::render
	public function render(Engine $engine, PageElement $page,
			$filename = FALSE)
	{
		$this->ids = array();
		$this->tags = array();
		$this->titles = array();
		$this->engine = $engine;
		$meta = array('application-name', 'author', 'description',
			'generator', 'keywords');
		//FIXME also track tags are properly closed

		if($this->doctype !== FALSE)
			print($this->doctype);
		$this->tagOpen('html');
		$this->renderTabs();
		$this->tagOpen('head');
		if($this->encoding !== FALSE)
			$this->renderMetaCharset($this->encoding);
		$this->_renderTitle($page);
		$this->_renderBase($page);
		$this->_renderTheme($page);
		$this->_renderIcontheme($page);
		$this->_renderFavicon($page);
		$this->_renderJavascript($page);
		if(($location = $page->get('location')) !== FALSE)
			$this->renderMeta('Location', $location);
		if(($refresh = $page->get('refresh')) !== FALSE
				&& is_numeric($refresh))
			$this->renderMeta('Refresh', $refresh);
		//meta-information
		foreach($meta as $m)
			if(($content = $this->configGet('meta::'.$m)) !== FALSE)
			{
				$this->renderTabs();
				$this->tag('meta', FALSE, FALSE, array(
						'name' => $m,
						'content' => $content));
			}
		//viewport
		if(($vw = $this->configGet('viewport::width')) !== FALSE)
		{
			$this->renderTabs();
			$this->tag('meta', FALSE, FALSE, array(
					'name' => 'viewport',
					'content' => "width=$vw"));
		}
		$this->renderTabs(-1);
		$this->tagClose('head');
		$this->renderTabs();
		$this->tagOpen('body');
		parent::render($engine, $page, $filename);
		$this->renderTabs(-1);
		$this->tagClose('body');
		$this->renderTabs(-1);
		$this->tagClose('html');
		$this->engine = FALSE;
	}

	private function _renderBase(PageElement $page)
	{
		$url = dirname($this->engine->getURL()).'/'; //XXX
		$this->renderTabs();
		$this->tag('base', FALSE, FALSE, array('href' => $url));
	}

	private function _renderFavicon(PageElement $page)
	{
		if(($favicon = $this->configGet('favicon')) === FALSE)
			return;
		//FIXME emit a (debugging) warning if the icon is not readable?
		$this->renderTabs();
		$this->tag('link', FALSE, FALSE, array('rel' => 'shortcut icon',
					'href' => $favicon));
	}

	private function _renderIcontheme(PageElement $page)
	{
		global $config;
		//XXX this should be set by the HTTP engine
		$cdn = $this->configGet('cdn') ?: '';
		$standalone = $this->get('standalone');

		if(($icontheme = $config->get(FALSE, 'icontheme')) === FALSE)
			return;
		//FIXME emit a (debugging) warning if the theme is not readable?
		$this->renderTabs();
		$filename = "icons/$icontheme.css";
		if(!$standalone || ($css = @file_get_contents(
				'../data/'.$filename)) === FALSE)
			$css = " @import url('$cdn$filename'); ";
		$this->tagOpen('style', FALSE, FALSE, array(
				'type' => 'text/css'));
		print($this->escapeComment($css.'//'));
		$this->tagClose('style');
	}

	private function _renderJavascript(PageElement $page)
	{
		$standalone = $this->get('standalone');

		if($this->javascript === FALSE)
			return;
		$this->_renderJavascriptScript('js/jquery.js', $standalone);
		$this->_renderJavascriptScript('js/DaPortal.js', $standalone);
	}

	private function _renderJavascriptScript($src, $standalone = FALSE)
	{
		//XXX this should be set by the HTTP engine
		$cdn = $this->configGet('cdn') ?: '';
		$tag = 'script';
		$type = 'text/javascript';

		$this->renderTabs();
		//FIXME emit a (debugging) warning if the file is not readable?
		if($standalone && ($js = @file_get_contents('../data/'.$src))
				!== FALSE)
		{
			$this->tagOpen($tag, FALSE, FALSE, array(
					'type' => $type));
			print($this->escapeComment($js.'//'));
		}
		else
			$this->tagOpen($tag, FALSE, FALSE, array(
					'type' => $type, 'src' => $cdn.$src));
		$this->tagClose($tag);
	}

	private function _renderTheme(PageElement $page)
	{
		global $config;

		if(($theme = $config->get(FALSE, 'theme')) !== FALSE)
			$this->_renderThemeCSS($theme);
		if($this->configGet('alternate_themes') == 1)
			$this->_renderThemeAlternate($page, $theme);
	}

	private function _renderThemeAlternate(PageElement $page,
			$theme = FALSE)
	{
		$themes = '../data/themes';

		if(($dir = @opendir($themes)) === FALSE)
			return;
		$themes = array();
		while(($de = readdir($dir)) !== FALSE)
			if(substr($de, -4, 4) == '.css'
					&& ($theme === FALSE
						|| $de != "$theme.css"))
				$themes[] = substr($de, 0, -4);
		sort($themes, SORT_STRING);
		foreach($themes as $theme)
			$this->_renderThemeCSS($theme, TRUE);
		closedir($dir);
	}

	private function _renderThemeCSS($theme, $alternate = FALSE)
	{
		//XXX this should be set by the HTTP engine
		$cdn = $this->configGet('cdn') ?: '';
		$rel = $alternate ? 'alternate stylesheet' : 'stylesheet';
		$standalone = $this->get('standalone');

		$this->renderTabs();
		$filename = "themes/$theme.css";
		if($standalone && ($css = @file_get_contents(
				'../data/'.$filename)) !== FALSE)
		{
			$this->tagOpen('style', FALSE, FALSE, array(
					'type' => 'text/css',
					'title' => $theme));
			print($this->escapeComment($css.'//'));
			$this->tagClose('style');
		}
		else
			$this->tag('link', FALSE, FALSE, array('rel' => $rel,
					'type' => 'text/css',
					'href' => $cdn.$filename,
					'title' => $theme));
	}

	private function _renderTitle(PageElement $e)
	{
		global $config;

		if(($title = $e->get('title')) === FALSE)
			$title = $config->get(FALSE, 'title');
		if($title !== FALSE)
		{
			$this->renderTabs();
			$this->tag('title', FALSE, $e->get('id'), FALSE,
					$title);
		}
	}


	//protected
	//methods
	//rendering
	protected function renderBlock(PageElement $e, $tag = 'div')
	{
		$class = $e->getType();

		if(($c = $e->get('class')) !== FALSE)
			$class .= ' '.$c;
		$this->renderTabs();
		$this->tagOpen($tag, $class);
		$this->renderChildren($e);
		$this->renderTabs();
		$this->tagClose($tag);
	}


	protected function renderBox(PageElement $e, $type = 'vbox')
	{
		$this->renderTabs();
		if(($class = $e->get('class')) !== FALSE)
			$class = $type.' '.$class;
		else
			$class = $type;
		$this->tagOpen('div', $class, $e->get('id'));
		$children = $e->getChildren();
		foreach($children as $c)
		{
			$this->renderTabs();
			$this->tagOpen('div', 'pack');
			$this->renderElement($c);
			if(count($c->getChildren()) > 1)
				$this->renderTabs();
			$this->tagClose('div');
		}
		$this->renderTabs();
		$this->tagClose('div');
	}


	protected function renderButton(PageElement $e)
	{
		$id = $e->get('id');
		$text = $e->get('text');
		$tooltip = $e->get('tooltip');
		$cancel = 'if(history.length > 1) { history.go(-1);'
			.' return false; }';

		if(($r = $e->get('request')) !== FALSE)
			$url = $this->engine->getURL($r, FALSE);
		else
			$url = $e->get('url');
		$class = 'button';
		if(($s = $e->get('stock')) !== FALSE)
			$class .= ' stock16 '.$s;
		if(($c = $e->get('class')) !== FALSE && $c != $s)
			$class .= ' '.$c;
		$tag = 'a';
		$args = array();
		if($tooltip !== FALSE)
			$args['title'] = $tooltip;
		switch(($type = $e->get('type')))
		{
			case 'reset':
			case 'submit':
				$tag = 'input';
				$url = FALSE;
				if($s === FALSE && $c === FALSE)
					$class .= ' stock16 '.$type;
				$args['type'] = $type;
				if(($name = $e->get('value')) !== FALSE)
					$args['name'] = $name;
				if($text !== FALSE)
					$args['value'] = $text;
				$this->tag($tag, $class, $id, $args);
				break;
			case 'button':
			case FALSE:
				if($this->javascript && $e->get('target')
						== '_cancel')
					$args['onclick'] = $cancel;
				//fallthrough
			default:
				$type = 'button';
				if($s === FALSE && $c === FALSE)
					$class .= ' stock16 '.$type;
				if($url !== FALSE)
					$args['href'] = $url;
				$this->tagOpen($tag, $class, $id, $args);
				$this->renderChildren($e);
				if($text !== FALSE)
					print($this->escape($text));
				$this->tagClose($tag);
				break;
		}
	}


	protected function renderCheckbox(PageElement $e)
	{
		$this->renderTabs();
		$this->tagOpen('div', $e->getType());
		print('<input type="checkbox"');
		if(($name = $e->get('name')) !== FALSE)
		{
			//FIXME the ID may not be unique
			print(' id="'.$this->escapeAttribute($name).'"');
			print(' name="'.$this->escapeAttribute($name).'"');
		}
		if(($value = $e->get('value')) !== FALSE)
			print(' checked="checked"');
		print('/>');
		if(($text = $e->get('text')) !== FALSE)
		{
			$l = new PageElement('label', array('text' => $text));
			if($name !== FALSE)
				$l->set('for', $name);
			$this->renderElement($l);
		}
		$this->tagClose('div');
	}


	protected function renderChildren(PageElement $e)
	{
		$children = $e->getChildren();
		foreach($children as $c)
			$this->renderElement($c);
	}


	protected function renderCombobox(PageElement $e)
	{
		$tag = 'select';
		$class = $e->get('class');
		$list = FALSE;

		$class = ($class !== FALSE) ? $class.' ' : '';
		$this->tagOpen('span', $class.$e->getType(), $e->get('id'),
				FALSE, $e->get('text'));
		$this->renderTabs();
		$name = $e->get('name');
		$value = $e->get('value');
		if($e->get('editable'))
		{
			$list = uniqid();
			$this->tag('input', FALSE, FALSE, array(
					'type' => 'text', 'list' => $list,
					'name' => $name));
			$tag = 'datalist';
		}
		$this->tagOpen($tag, FALSE, $list, array('name' => $name));
		$children = $e->getChildren();
		foreach($children as $c)
		{
			$this->renderTabs();
			$text = $c->get('text');
			if(($v = $c->get('value')) === FALSE)
				$v = $text;
			$args = array('value' => $v);
			if($value !== FALSE && $value == $v)
				$args['selected'] = 'selected';
			$this->tag('option', $c->get('class'), $c->get('id'),
					$args, $text);
		}
		$this->renderTabs();
		$this->tagClose($tag);
		$this->tagClose('span');
	}


	protected function renderData(PageElement $e)
	{
		//FIXME implement
	}


	protected function renderDialog(PageElement $e)
	{
		if(($type = $e->get('type')) === FALSE)
			$type = 'message';
		if(($title = $e->get('title')) === FALSE)
			switch($type)
			{
				case 'error':
					$title = _('Error');
					break;
				case 'question':
					$title = _('Question');
					break;
				case 'warning':
					$title = _('Warning');
					break;
				case 'info':
				case 'message':
					$title = _('Message');
					break;
			}
		$this->renderTabs();
		$this->tagOpen('div', 'dialog '.$type);
		$title = new PageElement('title', array('stock' => $type,
				'text' => $title));
		$this->renderElement($title);
		$this->renderTabs();
		$this->tagOpen('div', 'message', FALSE, FALSE, $e->get('text'));
		$this->tagClose('div');
		$this->renderTabs();
		if(count($e->getChildren()) > 0)
			$this->renderChildren($e);
		else if($this->get('close') && !$this->get('standalone'))
			//close button (if there is no child)
			$this->tag('button', 'stock16 close hidden', FALSE,
					array('type' => 'button'), _('Close'));
		$this->tagClose('div');
	}


	protected function renderEntry(PageElement $e)
	{
		$class = ($e->get('class') !== FALSE)
			? 'entry '.$e->get('class') : 'entry';

		$this->renderTabs();
		$this->tagOpen('div', $e->getType());
		if(($text = $e->get('text')) === FALSE)
			//default to the placeholder text
			$text = $e->get('placeholder');
		if($text !== FALSE)
		{
			$l = new PageElement('label', array(
					'class' => $e->get('class'),
					'text' => $text));
			$this->renderElement($l);
		}
		$name = $e->get('name');
		$value = $e->get('value');
		$type = ($e->get('hidden') === TRUE) ? 'password' : 'text';
		$attributes = array('type' => $type, 'name' => $name,
			'value' => $value);
		if(($size = $e->get('size')) !== FALSE && is_numeric($size))
			$attributes['size'] = $size;
		if(($width = $e->get('width')) !== FALSE && is_numeric($width))
			$attributes['style'] = 'width: '.$width.'ex';
		$this->tag('input', $class, $e->get('id'), $attributes);
		if($this->javascript && is_string($name)
				&& substr($name, -2) == '[]')
			$this->tag('input', 'stock16 add hidden', FALSE, array(
					'type' => 'button',
					'value' => _('More')));
		$this->tagClose('div');
	}


	protected function renderExpander(PageElement $e)
	{
		$this->renderTabs();
		$this->tagOpen('div', 'expander');
		if(($title = $e->get('title')) !== FALSE)
		{
			$this->renderTabs();
			$this->tag('div', 'title', FALSE, FALSE, $title);
		}
		$this->renderChildren($e);
		$this->renderTabs();
		$this->tagClose('div');
	}


	protected function renderFileChooser(PageElement $e)
	{
		$class = ($e->get('class') !== FALSE)
			? 'filechooser '.$e->get('class') : 'filechooser';

		$this->renderTabs();
		$this->tagOpen('div', $e->getType());
		if(($text = $e->get('text')) !== FALSE)
		{
			$l = new PageElement('label', array('text' => $text));
			$this->renderElement($l);
		}
		$name = $e->get('name');
		$type = 'file';
		$this->tag('input', $class, $e->get('id'),
				array('type' => $type, 'name' => $name));
		if($this->javascript && is_string($name)
				&& substr($name, -2) == '[]')
			$this->tag('input', 'stock16 add hidden', FALSE, array(
					'type' => 'button',
					'value' => _('More')));
		$this->tagClose('div');
	}


	protected function renderForm(PageElement $e)
	{
		$request = $e->get('request');
		$secure = $e->get('secure');
		$action = 'index.php';

		$this->renderTabs();
		if($secure && !isset($_SERVER['HTTPS']))
		{
			//XXX requires insider from HTTPEngine
			$_SERVER['HTTPS'] = 1;
			$port = isset($_SERVER['SERVER_PORT'])
				? $_SERVER['SERVER_PORT'] : FALSE;
			$_SERVER['SERVER_PORT'] = 443;
			$action = $this->engine->getURL();
			//restore the previous state
			if($port !== FALSE)
				$_SERVER['SERVER_PORT'] = $port;
			else
				unset($_SERVER['SERVER_PORT']);
			unset($_SERVER['HTTPS']);
		}
		$method = $e->get('idempotent') ? 'get' : 'post';
		$args = array('action' => $action, 'method' => $method,
			'enctype' => $this->_formEnctype($e));
		$this->tagOpen('form', $e->getType(), $e->get('id'), $args);
		if($method === 'post')
		{
			$auth = $this->engine->getAuth();
			$token = sha1(uniqid(php_uname(), TRUE));
			if(($tokens = $auth->getVariable($this->engine,
						'tokens')) === FALSE)
				$tokens = array();
			$tokens[$token] = time() + 3600;
			$auth->setVariable($this->engine, 'tokens', $tokens);
			$this->_formHidden('_token', $token);
		}
		if($request !== FALSE)
		{
			$this->_formHidden('_module', $request->getModule());
			$this->_formHidden('_action', $request->getAction());
			$this->_formHidden('_id', $request->getID());
			$this->_formHidden('_title', $request->getTitle());
			if(($args = $request->getParameters()) !== FALSE)
				foreach($args as $k => $v)
					$this->_formHidden($k, $v);
		}
		$this->renderChildren($e);
		$this->renderTabs();
		$this->tagClose('form');
	}

	protected function _formEnctype(PageElement $e)
	{
		//XXX look for any file upload field
		foreach($e->getChildren() as $c)
			if($c->getType() == 'filechooser')
				return 'multipart/form-data';
			else if(($ret = $this->_formEnctype($c)) !== FALSE)
				return $ret;
		return FALSE;
	}

	private function _formHidden($name, $value = FALSE)
	{
		if($value === FALSE)
			return;
		$this->renderTabs();
		$this->tag('input', FALSE, FALSE, array('type' => 'hidden',
					'name' => $name, 'value' => $value));
	}


	protected function renderFrame(PageElement $e)
	{
		$this->renderTabs();
		$this->tagOpen('div', 'frame');
		if(($title = $e->get('title')) !== FALSE)
		{
			$this->renderTabs();
			$this->tag('div', 'title', FALSE, FALSE, $title);
		}
		$this->renderChildren($e);
		$this->renderTabs();
		$this->tagClose('div');
	}


	protected function renderHbox(PageElement $e)
	{
		$this->renderBox($e, $e->getType());
	}


	protected function renderHtmledit(PageElement $e)
	{
		$filter_class = static::$filter_class;
		$class = $e->get('class');

		$class = ($class === FALSE) ? 'editor' : 'editor '.$class;
		$this->renderTabs();
		if(($text = $e->get('text')) !== FALSE)
		{
			$l = new PageElement('label', array('text' => $text));
			$this->renderElement($l);
		}
		if(($text = $e->get('value')) === FALSE || !is_string($text))
			$text = '';
		$text = $filter_class::filter($this->engine, $text);
		$this->tagOpen('textarea', $class, $e->get('id'), array(
				'name' => $e->get('name')));
		print($this->escape($text));
		$this->tagClose('textarea');
		if($this->javascript)
			$this->_htmleditJavascript($e, $class);
	}

	private function _htmleditJavascript(PageElement $e, $class)
	{
		$actions = array('cut' => _('Cut'), 'copy' => _('Copy'),
			'paste' => _('Paste'),
			'undo' => _('Undo'), 'redo' => _('Redo'),
			'insert-hrule' => _('Insert ruler'),
			'insert-link' => _('Insert link'),
			'insert-image' => _('Insert image'),
			'insert-table' => _('Insert table'),
			'insert-text' => _('Insert text'));
		$styles = array('' => _('Style'),
			'<h1>' => _('Heading 1'), '<h2>' => _('Heading 2'),
			'<h3>' => _('Heading 3'), '<h4>' => _('Heading 4'),
			'<h5>' => _('Heading 5'), '<h6>' => _('Heading 6'),
			'<p>' => _('Normal'), '<pre>' => _('Preformatted'),
			'<blockquote>' => _('Quotation'));
		$fonts = array('' => _('Font'),
			'cursive' => _('Cursive'),
			'fantasy' => _('Fantasy'),
			'monospace' => _('Monospace'),
			'sans-serif' => _('Sans serif'),
			'serif' => _('Serif'));
		$sizes = array('' => _('Size'),
			1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 6, 7 => 7);
		$format = array('bold' => _('Bold'),
			'italic' => _('Italic'),
			'underline' => _('Underline'),
			'strikethrough' => _('Strikethrough'),
			'subscript' => _('Subscript'),
			'superscript' => _('Superscript'),
			'remove-format' => _('Remove format'),
			'justify-left' => _('Align left'),
			'justify-center' => _('Center'),
			'justify-right' => _('Align right'),
			'justify-fill' => _('Justify'),
			'numbering' => _('Numbering'),
			'bullets' => _('Bullets'),
			'unindent' => _('Unindent'),
			'indent' => _('Indent'));

		$toolbar = $this->_htmleditToolbar($actions);
		$this->renderElement($toolbar);
		//format
		$toolbar = $this->_htmleditToolbar($format);
		//size
		$toolbar->prepend($this->_htmleditSelector('fontsize', $sizes));
		//font
		$toolbar->prepend($this->_htmleditSelector('fontname', $fonts));
		//style
		$toolbar->prepend($this->_htmleditSelector('formatblock',
					$styles));
		$this->renderElement($toolbar);
		$this->renderTabs();
		$this->tagOpen('iframe', $class, FALSE, array(
				'width' => '100%', 'height' => '400px'));
		$this->tagClose('iframe');
	}

	private function _htmleditSelector($class, $values = array())
	{
		$combobox = new PageElement('combobox', array(
				'class' => $class));

		foreach($values as $v => $t)
			$combobox->append('label', array('value' => $v,
					'text' => $t));
		return $combobox;
	}

	private function _htmleditToolbar($actions = array())
	{
		$toolbar = new PageElement('toolbar', array(
				'class' => 'editor'));

		foreach($actions as $a => $t)
		{
			$button = new PageElement('button', array('class' => $a,
				'stock' => $a, 'tooltip' => $t));
			$toolbar->append($button);
		}
		return $toolbar;
	}


	protected function renderHtmlview(PageElement $e)
	{
		$filter_class = static::$filter_class;
		$class = $e->get('class');
		$class = $e->getType().(($class !== FALSE) ? ' '.$class : '');
		$text = $e->get('text');

		if(!is_string($text))
			return;
		$this->tagOpen('div', $class, $e->get('id'), FALSE);
		if($e->get('trusted') === TRUE)
			print($text);
		else
			print($filter_class::filter($this->engine, $text));
		$this->tagClose('div');
	}


	protected function renderIconview(PageElement $e)
	{
		$e->set('view', 'icons');
		if(($columns = $e->get('columns')) === FALSE)
		{
			$columns = array('icon' => 'Icon', 'label' => 'Label');
			$e->set('columns', $columns);
		}
		$this->renderTreeview($e);
	}


	protected function renderImage(PageElement $e)
	{
		$size = 48;
		$class = FALSE;
		$attributes = array('alt' => $e->get('text'));
		$standalone = $this->get('standalone');

		if(($title = $e->get('title')) !== FALSE)
			$attributes['title'] = $title;
		if(($stock = $e->get('stock')) !== FALSE)
		{
			if(($s = $e->get('size')) !== FALSE && is_numeric($s))
				$size = $s;
			$class = "stock$size $stock";
			//XXX to display the "alt" text only when relevant
			$attributes['src'] = 'icons/generic/'.$size.'x'.$size
				.'/stock.png';
			$filename = '../data/'.$attributes['src'];
			if($standalone && ($img = file_get_contents($filename))
					!== FALSE)
				//FIXME use the actual icon
				$attributes['src'] = 'data:image/png'
					.';base64,'.base64_encode($img);
			$attributes['width'] = 0;
			$attributes['height'] = 0;
		}
		else if(($r = $e->get('request')) !== FALSE)
			$attributes['src'] = $this->engine->getURL($r, FALSE);
		else
			$attributes['src'] = $e->get('source');
		$this->tag('img', $class, $e->get('id'), $attributes);
	}


	protected function renderInline(PageElement $e)
	{
		$text = $e->get('text');

		if($e->getType() !== FALSE)
		{
			$this->tagOpen('span', $e->getType(), $e->get('id'),
					FALSE, $text);
			$this->renderChildren($e);
			$this->tagClose('span');
		}
		else if($text !== FALSE)
			print($this->escapeText($text));
	}


	protected function renderLabel(PageElement $e)
	{
		$tag = 'span';
		$class = 'label';
		$attributes = array();

		if(($c = $e->get('class')) !== FALSE)
			//FIXME apply to the respective parent and children
			$class = 'label '.$c;
		if(($for = $e->get('for')) !== FALSE)
		{
			$tag = 'label';
			$attributes['for'] = $for;
		}
		$this->tagOpen($tag, $class, $e->get('id'), $attributes,
				$e->get('text'));
		$this->tagClose($tag);
		$this->renderChildren($e);
	}


	protected function renderLink(PageElement $e)
	{
		$standalone = $this->get('standalone');
		$url = FALSE;

		$attributes = array();
		if(($a = $e->get('name')) !== FALSE)
			$attributes['name'] = $a;
		if(($r = $e->get('request')) !== FALSE)
		{
			$url = $this->engine->getURL($r, $standalone);
			$attributes['href'] = $url;
		}
		else if(($url = $e->get('url')) !== FALSE)
			$attributes['href'] = $url;
		if(($title = $e->get('title')) !== FALSE)
			$attributes['title'] = $title;
		$this->tagOpen('a', FALSE, $e->get('id'), $attributes);
		if(($stock = $e->get('stock')) !== FALSE)
			$this->tag('img', 'stock16 '.$stock, FALSE,
					array('alt' => ''));
		if(($text = $e->get('text')) === FALSE)
			$text = $url;
		if($text !== FALSE)
			print($this->escapeText($text));
		$this->renderChildren($e);
		$this->tagClose('a');
	}


	protected function renderMenu(PageElement $e, $class = FALSE)
	{
		//FIXME really implement
		if(($children = $e->getChildren()) === FALSE
				|| count($children) == 0)
			return;
		$this->tagOpen('ul', $class);
		foreach($children as $c)
			if($c->getType() == 'menuitem')
				$this->renderMenuitem($c);
		$this->renderTabs(-1);
		$this->tagClose('ul');
	}


	protected function renderMenubar(PageElement $e)
	{
		$this->renderTabs();
		$this->renderMenu($e, 'menubar');
	}


	protected function renderMenuitem(PageElement $e)
	{
		$this->renderTabs();
		$this->tagOpen('li', 'menuitem');
		if(($text = $e->get('text')) !== FALSE)
		{
			$class = $e->get('class');
			$attributes = array();
			if(($a = $e->get('name')) !== FALSE)
				$attributes['name'] = $a;
			if($e->get('important') !== FALSE)
				$class = is_string($class) ? $class.' important'
					: 'important';
			if(($r = $e->get('request')) !== FALSE)
				$attributes['href'] = $this->engine->getURL($r,
						FALSE);
			else if(($u = $e->get('url')) !== FALSE)
				$attributes['href'] = $u;
			if(count($attributes))
				$this->tagOpen('a', $class, $e->get('id'),
						$attributes);
			print($this->escapeText($text));
			if(count($attributes))
				$this->tagClose('a');
		}
		$this->renderMenu($e);
		$this->tagClose('li');
	}


	protected function renderMeta($header, $value)
	{
		$this->renderTabs();
		$this->tag('meta', FALSE, FALSE, array('http-equiv' => $header,
					'content' => $value));
	}


	protected function renderMetaCharset($charset)
	{
		$this->renderMeta('Content-Type', 'text/html; charset='
				.$charset);
	}


	protected function renderPage(PageElement $e)
	{
		$this->renderChildren($e);
	}


	protected function renderProgress(PageElement $e)
	{
		//XXX render something nicer
		$this->renderInline($e);
	}


	protected function renderRadioButton(PageElement $e)
	{
		$args = array('type' => 'radio');
		$value = $e->get('value');

		if(($children = $e->getChildren()) === FALSE)
			return;
		if(($name = $e->get('name')) !== FALSE)
			$args['name'] = $name;
		foreach($children as $c)
		{
			$a = $args;
			$class = $c->get('class');
			$id = $c->get('id');
			if(($v = $c->get('value')) !== FALSE)
			{
				$a['value'] = $v;
				if($v == $value)
					$a['checked'] = 'checked';
			}
			$this->renderTabs();
			$this->tag('input', $class, $id, $a);
			$this->renderElement($c);
		}
	}


	protected function renderStatusbar(PageElement $e)
	{
		$class = 'statusbar';

		$this->renderTabs();
		if(($c = $e->get('class')) !== FALSE)
			$class .= ' '.$c;
		$this->tagOpen('div', $class, $e->get('id'), FALSE,
				$e->get('text'));
		$this->renderChildren($e);
		$this->tagClose('div');
	}


	protected function renderTabs($more = 0)
	{
		print("\n");
		for($i = 0; $i < count($this->tags) + $more; $i++)
			print("\t");
	}


	protected function renderTextview(PageElement $e)
	{
		$this->renderTabs();
		$this->tagOpen('div', $e->getType());
		if(($text = $e->get('text')) !== FALSE)
		{
			$l = new PageElement('label', array('text' => $text));
			$this->renderElement($l);
		}
		$value = $e->get('value');
		$args = array();
		if(($name = $e->get('name')) !== FALSE)
			$args['name'] = $name;
		//text has to be escaped without any tags
		$this->tagOpen('textarea', $e->get('class'), $e->get('id'),
				$args);
		if($value !== FALSE)
			print($this->escape($value));
		$this->tagClose('textarea');
		$this->tagClose('div');
	}


	protected function renderTitle(PageElement $e)
	{
		$hcnt = count($this->titles);
		$tcnt = count($this->tags);
		$level = 1;

		/* XXX this algorithm is a bit ugly but seems to work */
		if($hcnt == 0) /* no title set */
			$this->titles[$level - 1] = $tcnt;
		else if($this->titles[$hcnt - 1] < $tcnt) //deeper level
		{
			$this->titles[$hcnt] = $tcnt;
			$level = $hcnt;
		}
		else if($this->titles[$hcnt - 1] == $tcnt) //same level
			$level = $hcnt - 1;
		else
		{
			for(; $hcnt > 0; $hcnt = count($this->titles))
			{
				$h = $this->titles[$hcnt - 1];
				if($h <= $tcnt)
					break;
				unset($this->titles[$hcnt - 1]);
			}
			$level = $hcnt - 1;
		}
		$tag = "h$level";
		if(($class = $e->get('class')) === FALSE)
			$class = '';
		if(($stock = $e->get('stock')) !== FALSE)
			switch($level)
			{
				case 1: $class = "stock48 $stock $class"; break;
				case 2: $class = "stock32 $stock $class"; break;
				case 3: $class = "stock24 $stock $class"; break;
				case 4:
				default:$class = "stock16 $stock $class"; break;
			}
		$class = rtrim($class);
		$this->renderTabs();
		$this->tagOpen($tag, $class, $e->get('id'), FALSE,
				$e->get('text'));
		$this->renderChildren($e);
		$this->tagClose($tag);
	}


	protected function renderToolbar(PageElement $e)
	{
		return $this->renderBlock($e);
	}


	protected function renderTreeview(PageElement $e)
	{
		$auth = $this->engine->getAuth();

		switch(($view = $e->get('view')))
		{
			case 'details':
			case 'preview':
				break;
			case 'icons':
			case 'list':
				if(($c = $e->get('columns')) !== FALSE)
					break;
				$c = array('icon' => '', 'label' => '');
				$e->set('columns', $c);
				break;
			case 'thumbnails':
				if(($c = $e->get('columns')) !== FALSE)
					break;
				$c = array('thumbnail' => '', 'label' => '');
				$e->set('columns', $c);
				break;
			default:
				$view = 'details';
				break;
		}
		$r = $e->get('request');
		$tag = ($r !== FALSE) ? 'form' : 'div';
		$class = "treeview $view";
		if($e->get('alternate'))
			$class .= ' alternate';
		if(($c = $e->get('class')) !== FALSE && strlen($c))
			$class .= " $c";
		$args = FALSE;
		if($r !== FALSE)
		{
			$method = $e->get('idempotent') ? 'get' : 'post';
			$args = array('action' => 'index.php',
				'method' => $method);
		}
		$this->renderTabs();
		$this->tagOpen($tag, $class, $e->get('id'), $args);
		if($r !== FALSE && $method === 'post')
		{
			$token = sha1(uniqid(php_uname(), TRUE));
			if(($tokens = $auth->getVariable($this->engine,
						'tokens')) === FALSE)
				$tokens = array();
			$tokens[$token] = time() + 3600;
			$auth->setVariable($this->engine, 'tokens', $tokens);
			$this->_renderTreeviewHidden('_token', $token);
		}
		if($r !== FALSE)
		{
			//FIXME copied from renderForm()
			$this->_renderTreeviewHidden('_module', $r->getModule());
			$this->_renderTreeviewHidden('_action', $r->getAction());
			$this->_renderTreeviewHidden('_id', $r->getID());
			$this->_renderTreeviewHidden('_title', $r->getTitle());
			if(($parameters = $r->getParameters()) !== FALSE)
				foreach($r->getParameters() as $k => $v)
					$this->_renderTreeviewHidden($k, $v);
		}
		$this->_renderTreeviewToolbar($e);
		$columns = $e->get('columns');
		if(!is_array($columns) || count($columns) == 0)
			$columns = array('title' => 'Title');
		$this->renderTabs();
		$this->tagOpen('div', 'table');
		$this->_renderTreeviewHeaders($e, $columns);
		//render rows
		$this->_renderTreeviewRows($e, $columns);
		$this->renderTabs(-1);
		$this->tagClose('div');
		$this->renderTabs(-1);
		//render the (optional) controls
		$this->_renderTreeviewControls($e);
		$this->tagClose($tag);
	}

	private function _renderTreeviewControls(PageElement $e)
	{
		if(($children = $e->getChildren()) === FALSE)
			return;
		foreach($children as $c)
			switch($c->getType())
			{
				case 'row':
				case 'toolbar':
					break;
				default:
					$this->renderElement($c);
					break;
			}
	}

	private function _renderTreeviewHeaders(PageElement $e, $columns)
	{
		$this->renderTabs();
		$this->tagOpen('div', 'header');
		if($e->get('request') !== FALSE)
			$this->tag('span', 'detail', FALSE, FALSE, '');
		foreach($columns as $c => $v)
			$this->tag('span', "detail $c", FALSE, FALSE, $v);
		$this->tagClose('div');
	}

	private function _renderTreeviewHidden($name, $value = FALSE)
	{
		//FIXME copied from _formHidden()
		if($value === FALSE)
			return;
		$this->renderTabs();
		$this->tag('input', FALSE, FALSE, array('type' => 'hidden',
					'name' => $name, 'value' => $value));
	}

	private function _renderTreeviewRows(PageElement $e, $columns)
	{
		$id = 1;

		$this->_renderTreeviewRowsDo($e, $columns, $id);
	}

	private function _renderTreeviewRowsDo(PageElement $e, $columns, &$id,
			$level = 0)
	{
		$class = 'row';
		$request = $e->get('request');

		if(($children = $e->getChildren()) === FALSE)
			return;
		if($level > 0)
			$class .= ' level level'.$level;
		foreach($children as $c)
		{
			$this->renderTabs();
			if($c->getType() != 'row')
				continue;
			$cl = $class;
			if(($p = $c->get('class')) !== FALSE)
				$cl .= ' '.$p;
			$this->tagOpen('div', $cl);
			if($request !== FALSE)
			{
				$this->tagOpen('span', 'detail');
				$name = $c->get('id');
				$args = array('type' => 'checkbox',
					'name' => $name);
				if($c->get('value'))
					$args['checked'] = 'checked';
				$this->tag('input', FALSE, '_check_'.$id,
						$args);
				$this->tagClose('span');
			}
			foreach($columns as $k => $v)
			{
				if(($v = $c->get($k)) === FALSE)
					$v = '';
				$this->tagOpen('span', "detail $k");
				if($v instanceof PageElement)
					$this->renderElement($v);
				else if(is_scalar($v))
					$this->tag('label', FALSE, FALSE,
							array('for' => '_check_'
								.$id), $v);
				$this->tagClose('span');
			}
			$this->tagClose('div');
			$id++;
			$this->_renderTreeviewRowsDo($c, $columns, $id,
					$level + 1);
		}
	}

	private function _renderTreeviewToolbar(PageElement $e)
	{
		if(($children = $e->getChildren()) === FALSE)
			return;
		foreach($children as $c)
			if($c->getType() == 'toolbar')
				$this->renderToolbar($c);
	}


	protected function renderVbox(PageElement $e)
	{
		$this->renderBox($e, $e->getType());
	}


	//tagging
	protected function tag($name, $class = FALSE, $id = FALSE,
			$attributes = FALSE, $content = FALSE)
	{
		$tag = '<'.$this->escapeAttribute($name);

		if($class !== FALSE)
			$tag .= ' class="'.$this->escapeAttribute($class).'"';
		if($id !== FALSE)
		{
			if(isset($ids[$id]))
				$this->engine->log(LOG_DEBUG, 'HTML ID '.$id
						.' is already defined');
			$ids[$id] = TRUE;
			$tag .= ' id="'.$this->escapeAttribute($id).'"';
		}
		if(is_array($attributes))
			foreach($attributes as $k => $v)
				$tag .= ' '.$this->escapeAttribute($k).'="'
					.$this->escapeAttribute($v).'"';
		if($content !== FALSE)
			$tag .= '>'.$this->escapeText($content)
				.'</'.$this->escapeAttribute($name).'>';
		else
			$tag .= '/>';
		print($tag);
	}


	protected function tagClose($name)
	{
		print('</'.$this->escapeAttribute($name).'>');
		if(array_pop($this->tags) != $name)
			$this->engine->log(LOG_DEBUG, 'Invalid tag sequence');
	}


	protected function tagOpen($name, $class = FALSE, $id = FALSE,
			$attributes = FALSE, $content = FALSE)
	{
		array_push($this->tags, $name);
		//FIXME automatically output tabs
		$tag = '<'.$this->escapeAttribute($name);
		if($class !== FALSE && strlen($class) > 0)
			$tag .= ' class="'.$this->escapeAttribute($class).'"';
		if($id !== FALSE)
		{
			if(isset($ids[$id]))
				$this->engine->log(LOG_DEBUG, 'HTML ID '.$id
						.' is already defined');
			$ids[$id] = TRUE;
			$tag .= ' id="'.$this->escapeAttribute($id).'"';
		}
		if(is_array($attributes))
			foreach($attributes as $k => $v)
				$tag .= ' '.$this->escapeAttribute($k).'="'
					.$this->escapeAttribute($v).'"';
		$tag.='>';
		print($tag);
		if($content !== FALSE)
			print($this->escapeText($content));
	}


	//private
	//properties
	private $engine = FALSE;
	private $ids;
	private $javascript = FALSE;
	private $tags;
	private $titles;
}

?>
