/** @license MIT License (c) copyright B Cavalier & J Hann */

/**
 * wire/dom/render plugin
 * wire plugin that provides a factory for dom nodes via a simple html
 * template.
 *
 * wire is part of the cujo.js family of libraries (http://cujojs.com/)
 *
 * Licensed under the MIT License at:
 * http://www.opensource.org/licenses/mit-license.php
 */

define(['./../lib/dom/base', 'when'], function (base, when) {

	var parentTypes, parseTemplateRx, getFirstTagNameRx, isPlainTagNameRx,
		undef;

	// elements that could be used as root nodes and their natural parent type
	parentTypes = {
		'li': 'ul',
		'td': 'tr',
		'tr': 'tbody',
		'tbody': 'table',
		'thead': 'table',
		'tfoot': 'table',
		'caption': 'table',
		'col': 'table',
		'colgroup': 'table',
		'option': 'select'
	};

	parseTemplateRx = /\$\{([^}]*)\}/g;
	getFirstTagNameRx = /<\s*(\w+)/;
	isPlainTagNameRx = /^[A-Za-z]\w*$/;

	/**
	 * Constructs a DOM node and child nodes from a template string.
	 * Information contained in a hashmap is merged into the template
	 * via tokens (${name}) before rendering into DOM nodes.
	 * Nothing is done with the css parameter at this time.
	 * @param template {String} html template
	 * @param hashmap {Object} string replacements hash
	 * @param optRefNode {HTMLElement} node to replace with root node of rendered template
	 * @param optCss {Object} unused
	 * @returns {HTMLElement}
	 */
	function render (template, hashmap, optRefNode, optCss) {
		var node;

		// replace tokens (before attempting to find top tag name)
		template = replaceTokens('' + template, hashmap);

		if (isPlainTagNameRx.test(template)) {
			// just 'div' or 'a' or 'tr', for example
			node = document.createElement(template);
		}
		else {
			// create node from template
			node = createElementFromTemplate(template);
		}

		if (optRefNode) {
			node = safeReplaceElement(node, optRefNode);
		}

		return node;
	}

	render.wire$plugin = function (/*ready, destroyed, options*/) {
		return {
			factories: {
				render: domRenderFactory
			},
			proxies: [
				base.nodeProxy
			]
		};
	};

	/**
	 * Finds the first html element in a string, extracts its tag name,
	 * and looks up the natural parent element tag name for this element.
	 * @private
	 * @param template {String}
	 * @returns {String} the parent tag name, or 'div' if none was found.
	 */
	function getParentTagName (template) {
		var matches;

		// TODO: throw if no element was ever found?
		matches = template.match(getFirstTagNameRx);

		return parentTypes[matches && matches[1]] || 'div';
	}

	/**
	 * Creates an element from a text template.  This function does not
	 * support multiple elements in a template.  Leading and trailing
	 * text and/or comments are also ignored.
	 * @private
	 * @param template {String}
	 * @returns {HTMLElement} the element created from the template
	 */
	function createElementFromTemplate (template) {
		var parentTagName, parent, first, child;

		parentTagName = getParentTagName(template);
		parent = document.createElement(parentTagName);
		parent.innerHTML = template;

		// we just want to return first element (nodelists and fragments
		// are tricky), so we loop through all top-level children to ensure
		// we only have one.

		// try html5-ish API
		first = parent.firstElementChild;
		child = parent.lastElementChild;

		// old dom API
		if (!first) {
			child = parent.firstChild;
			while (child) {
				if (child.nodeType == 1) {
					if (!first) first = child;
				}
				child = child.nextSibling;
			}
		}

		if (first != child) {
			throw new Error('render: only one element per template is supported.');
		}

		return first;
	}

	/**
	 * Creates rendered dom trees for the "render" factory.
	 * @param resolver
	 * @param spec
	 * @param wire
	 */
	function domRenderFactory (resolver, spec, wire) {
		when(wire(spec.render), function (options) {
			var template;
			template = options.template || options;
			return render(template, options.replace, options.at, options.css);
		}).then(resolver.resolve, resolver.reject);
	}

	/**
	 * Replaces a dom node, while preserving important attributes
	 * of the original.
	 * @private
	 * @param oldNode {HTMLElement}
	 * @param newNode {HTMLElement}
	 * @returns {HTMLElement} newNode
	 */
	function safeReplaceElement (newNode, oldNode) {
		var i, attr, parent;

		for (i = 0; i < oldNode.attributes.length; i++) {
			attr = oldNode.attributes[i];
			if ('class' == attr.name) {
				// merge css classes
				// TODO: if we want to be smart about not duplicating classes, implement spliceClassNames from cola/dom/render
				newNode.className = (oldNode.className ? oldNode.className + ' ' : '')
					+ newNode.className;
			}
			// Note: IE6&7 don't support node.hasAttribute() so we're using node.attributes
			else if (!newNode.attributes[attr.name]) {
				newNode.setAttribute(attr.name, oldNode.getAttribute(attr.name));
			}
		}
		parent = oldNode.parentNode;
		if (parent) {
			parent.replaceChild(newNode, oldNode);
		}
		return newNode;
	}

	/**
	 * Replaces simple tokens in a string.  Tokens are in the format ${key}.
	 * Tokens are replaced by values looked up in an associated hashmap.
	 * If a token's key is not found in the hashmap, an empty string is
	 * inserted instead.
	 * @private
	 * @param template
	 * @param hashmap {Object} the names of the properties of this object
	 * are used as keys. The values replace the token in the string.
	 * @param [missing] {Function} callback that deals with missing properties
	 * @returns {String}
	 */
	function replaceTokens (template, hashmap, missing) {
		if (!hashmap) return template;
		if (!missing) missing = blankIfMissing;
		return template.replace(parseTemplateRx, function (m, token) {
			return missing(findProperty(hashmap, token));
		});
	}

	function findProperty (obj, propPath) {
		var props, prop;
		props = propPath.split('.');
		while (obj && (prop = props.shift())) {
			obj = obj[prop];
		}
		return obj;
	}

	function blankIfMissing (val) { return val == undef ? '' : val; }

	return render;

});
