<?php
/** @file
 * QueryPath templates. See QPTPL.
 */
/**
 * QPTPL is a template library for QueryPath.
 *
 * The QPTPL extension provides template tools that can be used in
 * conjunction with QueryPath.
 *
 * There are two basic modes in which this tool operates. Both merge data into
 * a pure HTML template. Both base their insertions on classes and IDs in the
 * HTML data. Where they differ is in the kind of data merged into the template.
 *
 * One mode takes array data and does a deep (recursive) merge into the template.
 * It can be used for simple substitutions, but it can also be used to loop through
 * "rows" of data and create tables.
 *
 * The second mode takes a classed object and introspects that object to find out
 * what CSS classes it is capable of filling. This is one way of bridging an object
 * model and QueryPath data.
 *
 * The unit tests are a good place for documentation, as is the QueryPath webste.
 *
 * @author M Butcher <matt@aleph-null.tv>
 * @license http://opensource.org/licenses/lgpl-2.1.php LGPL or MIT-like license.
 * @see QueryPathExtension
 * @see QueryPathExtensionRegistry::extend()
 * @see https://fedorahosted.org/querypath/wiki/QueryPathTemplate
 * @ingroup querypath_extensions
 */
class QPTPL implements QueryPathExtension {
	protected $qp;
	public function __construct(QueryPath $qp) {
		$this->qp = $qp;
	}

	/**
	 * Apply a template to an object and then insert the results.
	 *
	 * This takes a template (an arbitrary fragment of XML/HTML) and an object
	 * or array and inserts the contents of the object into the template. The
	 * template is then appended to all of the nodes in the current list.
	 *
	 * Note that the data in the object is *not* escaped before it is merged
	 * into the template. For that reason, an object can return markup (as
	 * long as it is well-formed).
	 *
	 * @param mixed $template
	 *  The template. It can be of any of the types that {@link qp()} supports
	 *  natively. Typically it is a string of XML/HTML.
	 * @param mixed $object
	 *  Either an object or an associative array.
	 *  - In the case where the parameter
	 *  is an object, this will introspect the object, looking for getters (a la
	 *  Java bean behavior). It will then search the document for CSS classes
	 *  that match the method name. The function is then executed and its contents
	 *  inserted into the document. (If the function returns NULL, nothing is
	 *  inserted.)
	 *  - In the case where the paramter is an associative array, the function will
	 *  look through the template for CSS classes that match the keys of the
	 *  array. When an array key is found, the array value is inserted into the
	 *  DOM as a child of the currently matched element(s).
	 * @param array $options
	 *  The options for this function. Valid options are:
	 *  - <None defined yet>
	 * @return QueryPath
	 *  Returns a QueryPath object with all of the changes from the template
	 *  applied into the QueryPath elements.
	 * @see QueryPath::append()
	 */
	public function tpl($template, $object, $options = array()) {
		// Handle default options here.

		//$tqp = ($template instanceof QueryPath) ? clone $template: qp($template);
		$tqp = qp($template);

		if (is_array($object) || $object instanceof Traversable) {
			$this->tplArrayR($tqp, $object, $options);
			return $this->qp->append($tqp->top());
		}
		elseif (is_object($object)) {
			$this->tplObject($tqp, $object, $options);
		}

		return $this->qp->append($tqp->top());
	}

	/**
	 * Given one template, do substitutions for all objects.
	 *
	 * Using this method, one template can be populated from a variety of
	 * sources. That one template is then appended to the QueryPath object.
	 * @see tpl()
	 * @param mixed $template
	 *  The template. It can be of any of the types that {@link qp()} supports
	 *  natively. Typically it is a string of XML/HTML.
	 * @param array $objects
	 *  An indexed array containing a list of objects or arrays (See {@link tpl()})
	 *  that will be merged into the template.
	 * @param array $options
	 *  An array of options. See {@link tpl()} for a list.
	 * @return QueryPath
	 *  Returns the QueryPath object.
	 */
	public function tplAll($template, $objects, $options = array()) {
		$tqp = qp($template, ':root');
		foreach ($objects as $object) {
			if (is_array($object))
			$tqp = $this->tplArrayR($tqp, $object, $options);
			elseif (is_object($object))
			$tqp = $this->tplObject($tqp, $object, $options);
		}
		return $this->qp->append($tqp->top());
	}

	/*
	 protected function tplArray($tqp, $array, $options = array()) {

	 // If we find something that's not an array, we try to handle it.
	 if (!is_array($array)) {
	 is_object($array) ? $this->tplObject($tqp, $array, $options) : $tqp->append($array);
	 }
	 // An assoc array means we have mappings of classes to content.
	 elseif ($this->isAssoc($array)) {
	 print 'Assoc array found.' . PHP_EOL;
	 foreach ($array as $key => $value) {
	 $first = substr($key,0,1);

	 // We allow classes and IDs if explicit. Otherwise we assume
	 // a class.
	 if ($first != '.' && $first != '#') $key = '.' . $key;

	 if ($tqp->top()->find($key)->size() > 0) {
	 print "Value: " . $value . PHP_EOL;
	 if (is_array($value)) {
	 //$newqp = qp($tqp)->cloneAll();
	 print $tqp->xml();
	 $this->tplArray($tqp, $value, $options);
	 print "Finished recursion\n";
	 }
	 else {
	 print 'QP is ' . $tqp->size() . " inserting value: " . $value . PHP_EOL;

	 $tqp->append($value);
	 }
	 }
	 }
	 }
	 // An indexed array means we have multiple instances of class->content matches.
	 // We copy the portion of the template and then call repeatedly.
	 else {
	 print "Array of arrays found..\n";
	 foreach ($array as $array2) {
	 $clone = qp($tqp->xml());
	 $this->tplArray($clone, $array2, $options);
	 print "Now appending clone.\n" . $clone->xml();
	 $tqp->append($clone->parent());
	 }
	 }


	 //return $tqp->top();
	 return $tqp;
	 }
	 */

	/**
	 * Introspect objects to map their functions to CSS classes in a template.
	 */
	protected function tplObject($tqp, $object, $options = array()) {
		$ref = new ReflectionObject($object);
		$methods = $ref->getMethods();
		foreach ($methods as $method) {
			if (strpos($method->getName(), 'get') === 0) {
				$cssClass = $this->method2class($method->getName());
				if ($tqp->top()->find($cssClass)->size() > 0) {
					$tqp->append($method->invoke($object));
				}
				else {
					// Revert to the find() that found something.
					$tqp->end();
				}
			}
		}
		//return $tqp->top();
		return $tqp;
	}

	/**
	 * Recursively merge array data into a template.
	 */
	public function tplArrayR($qp, $array, $options = NULL) {
		// If the value looks primitive, append it.
		if (!is_array($array) && !($array instanceof Traversable)) {
			$qp->append($array);
		}
		// If we are dealing with an associative array, traverse it
		// and merge as we go.
		elseif ($this->isAssoc($array)) {
			// Do key/value substitutions
			foreach ($array as $k => $v) {

				// If no dot or hash, assume class.
				$first = substr($k,0,1);
				if ($first != '.' && $first != '#') $k = '.' . $k;

				// If value is an array, recurse.
				if (is_array($v)) {
					// XXX: Not totally sure that starting at the
					// top is right. Perhaps it should start
					// at some other context?
					$this->tplArrayR($qp->top($k), $v, $options);
				}
				// Otherwise, try to append value.
				else {
					$qp->branch()->children($k)->append($v);
				}
			}
		}
		// Otherwise we have an indexed array, and we iterate through
		// it.
		else {
			// Get a copy of the current template and then recurse.
			foreach ($array as $entry) {
				$eles = $qp->get();
				$template = array();

				// We manually deep clone the template.
				foreach ($eles as $ele) {
					$template = $ele->cloneNode(TRUE);
				}
				$tpl = qp($template);
				$tpl = $this->tplArrayR($tpl, $entry, $options);
				$qp->before($tpl);
			}
			// Remove the original template without loosing a handle to the
			// newly injected one.
			$dead = $qp->branch();
			$qp->parent();
			$dead->remove();
			unset($dead);
		}
		return $qp;
	}

	/**
	 * Check whether an array is associative.
	 * If the keys of the array are not consecutive integers starting with 0,
	 * this will return false.
	 *
	 * @param array $array
	 *  The array to test.
	 * @return Boolean
	 *  TRUE if this is an associative array, FALSE otherwise.
	 */
	public function isAssoc($array) {
		$i = 0;
		foreach ($array as $k => $v) if ($k !== $i++) return TRUE;
		// If we get here, all keys passed.
		return FALSE;
	}

	/**
	 * Convert a function name to a CSS class selector (e.g. myFunc becomes '.myFunc').
	 * @param string $mname
	 *  Method name.
	 * @return string
	 *  CSS 3 Class Selector.
	 */
	protected function method2class($mname) {
		return '.' . substr($mname, 3);
	}
}
QueryPathExtensionRegistry::extend('QPTPL');