/*
 * Utility Functions
 */
function hideModalDialog() {
	var e = getElement('cms_dialog');
	clearInterval(e.interval);
	document.body.removeChild(getElement('cms_shadow'));
	document.body.removeChild(e);
}

function updateModalDialog() {
	var d = document.getElementById('cms_dialog');
	var e = document.getElementById('cms_shadow');
	var w = window.innerWidth || (document.documentElement ? document.documentElement.clientWidth : 0) || (document.body ? document.body.clientWidth : 0);
	var h = window.innerHeight || (document.documentElement ? document.documentElement.clientHeight : 0) || (document.body ? document.body.clientHeight : 0);
	var x = window.pageXOffset || (document.documentElement ? document.documentElement.scrollLeft : 0) || (document.body ? document.body.scrollLeft : 0);
	var y = window.pageYOffset || (document.documentElement ? document.documentElement.scrollTop : 0) || (document.body ? document.body.scrollTop : 0);
	/* Resize shadow */
	e.style.width = w+x+'px';
	e.style.height = h+y+'px';
	/* Move dialog to center */
	d.style.top = y+(h-d.offsetHeight)/2+'px';
	d.style.left = x+(w-d.offsetWidth)/2+'px';
}

function showModalDialog(html) {
	/* Return if an alert exists */
	if (getElement('cms_dialog')) return false;
	/* Create curtain */
	var e = document.createElement('div');
	e.id = 'cms_shadow';
	e.className = 'select-free';
	e.innerHTML = '<!--[if lte IE 6.5]><iframe></iframe><![endif]-->';
	with (e.style) {
		position = 'absolute';
		top = '0px';
		left = '0px';
		background = '#000';
		filter = 'alpha(opacity=20)';
		opacity = '.20';
		zIndex = '2000';
	}
	document.body.appendChild(e);
	/* Create dialog */
	var d = document.createElement('div');
	d.id = 'cms_dialog';
	d.className = 'dialog';
	d.innerHTML = html;
	with (d.style) {
		position = 'absolute';
		width = '320px';
		zIndex = '2001';
	}
	document.body.appendChild(d);
	updateModalDialog();
	d.interval = setInterval(updateModalDialog,200);
}

function showAlert(text) {
	showModalDialog("<table style='width:100%'><tr><td style='text-align:center; padding:0px 10px'><img src='images/icon_warning_medium.gif' alt=''/><br /><b>"+getConstant('_WARNING')+"</b></td><td style='width:100%'>"+text.replace(/\n/g,"<br />")+"</td></tr><tr><td colspan='2' style='text-align:center; padding-top:5px'><button type='button' onclick='hideModalDialog()'>"+getConstant('_OK')+"</button></td></tr></table>");
}

function GMapAvailable() {
	return typeof(GBrowserIsCompatible) != "undefined" && GBrowserIsCompatible();
}

function dummy() {}

function trim(str) {
	str = String(str);
	str = str.replace(/^\s+(.*?)$/,'$1');
	str = str.replace(/^(.*?)\s+$/,'$1');
	return str;
}

function isEmpty(obj,allow_zero) {
	if (obj == undefined) return true;
	var value = (typeof(obj) == "string" || obj instanceof String) ? obj : obj.value;
	return allow_zero ? /^\s*$/.test(value) : /^[0\s]*$/.test(value);
}

function getElement(id) {
	return typeof(id) == 'object' ? id : document.getElementById(id);
}

function getValue(id) {
	var e = getElement(id);
	return e ? e.value : null;
}

function setValue(id,value) {
	var e = getElement(id);
	if (e) e.value = value;
	return (e ? true : false);
}

function isVisible(id) {
	var e = getElement(id);
	if (!e) return null;
	while (e) {
		if (e.style && e.style.display == "none") return false;
		e = e.parentNode;
	}
	return true;
}

function setVisible(id,status) {
	var e = getElement(id);
	if (e) e.style.display = status ? '' : 'none';
}

function enable(id,status) {
	var e = getElement(id);
	if (e) e.disabled = !status;
}

function setImage(id,src) {
	var e = getElement(id);
	if (e) e.src = 'images/'+src;
}

function toggleDisplay(id) {
	var e = getElement(id);
	if (e) e.style.display = e.style.display == 'none' ? '' : 'none';
}

function createUID() {
	return new Date().getTime()+Math.round(Math.random()*32768);
}

function addEvent(element,eventtype,callback,useCapture) {
	if (element.addEventListener){
		element.addEventListener(eventtype,callback,useCapture);
		return true;
	} else if (element.attachEvent){
		var result = element.attachEvent("on"+eventtype,callback);
		return result;
	} else return false;
}

function stopEvent(event) {
	if (window.event) event = window.event;
	event.cancelBubble = true;
	if (event.stopPropagation) event.stopPropagation();
}

function createHidden(name,value) {
	var element = document.createElement('input');
	element.type = 'hidden';
	element.id = name;
	element.name = name;
	if (value) element.value = value;
	return element;
}

function getRadioValue(name) {
	var list = document.getElementsByTagName('input');
	for (var i = 0; i < list.length; i++) if (list[i].name == name && list[i].checked) return list[i].value;
	return null;
}

function createOption(value,innerHTML) {
	var e = document.createElement('option');
	e.value = value ? value : '';
	e.innerHTML = e.text = innerHTML ? innerHTML : '';
	return e;
}

function cloneOptions(src,dest) {
	src = getElement(src);
	dest = getElement(dest);
	dest.options.length = 0;
	for (var i=0; i<src.length; i++) {
		var e = document.createElement("option");
		e.value = src.options[i].value;
		e.innerHTML = src.options[i].innerHTML;
		dest.appendChild(e);
	}
}

function getOptionByValue(select,value,index_only) {
	var list = getElement(select).options;
	for (var i = 0; i < list.length; i++) if (list[i].value == value) return index_only ? i : list[i];
	return false;
}

function getOptionText(select,value) {
	var e = getOptionByValue(select,value);
	return (e ? e.text : false);
}

function selectOptionByValue(select,value,defaultFirst) {
	var e = getElement(select);
	var index = getOptionByValue(e,value,true);
	e.selectedIndex = -1;
	e.selectedIndex = (index !== false ? index : (defaultFirst ? 0 : -1));
}

function selectFirstRadio(name) {
	var list = document.getElementsByTagName('input');
	for (var i = 0; i < list.length; i++) if (list[i].name == name) {
		list[i].checked = 'checked';
		break;
	}
}

function encodeEntities(s) {
	var result = '';
	if (s) for (var i = 0; i < s.length; i++) {
		var c = s.charAt(i);
		result += {'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;',"'":'&#039;'}[c] || c;
	}
	return result;
}

function decodeEntities(s) {
	var ents = {'&amp;':'&','&lt;':'<','&gt;':'>','&quot;':'"','&#039;':"'"};
	for (var i in ents) s = s.replace(new RegExp(i,"g"),ents[i]);
	return s;
}

function generatePassword(length) {
	var pwd = "";
	var list = "abcdefghijkmnopqrstuvyz0123456789";
	if (!length) length = 7;
	for (var i = 0; i < length; i++) {
		var index = Math.floor(Math.random()*list.length);
		pwd += list.charAt(index);
	}
	return pwd;
}

function insertFirstChild(parent,child) {
	parent = getElement(parent);
	child = getElement(child);
	var e = parent.firstChild;
	if (e) parent.insertBefore(child,e); else parent.appendChild(child);
	return child;
}

function insertAfter(sibling,child) {
	sibling = getElement(sibling);
	child = getElement(child);
	var p = sibling.parentNode;
	if (sibling.nextSibling) p.insertBefore(child,sibling.nextSibling); else p.appendChild(child);
	return child;
}

function parseDate(date,format) {
	if (!testDate(date,false,format)) return false;
	var data = {};
	date = date.split(/[-\s\.\/]/);
	format = format.split("/");
	for (var i = 0; i < format.length; i++) data[format[i]] = parseInt(date[i]);
	return {month:data.m,day:data.d,year:data.Y};
}

function activateRow(name) {
	var id = getRadioValue(name);
	var len = name.length;
	var list = document.getElementsByTagName('tr');
	for (var i = 0, n = list.length; i < n; i++) if (list[i].id && list[i].id.substr(0,len) == name) {
		list[i].className = trim((' '+list[i].className+' ').replace(' selected ','')+(list[i].id.substr(len+1) == id ? ' selected' : ''));
	}
}

/*
 * Cookie Functions
 */
function createCookie(name,value,days) {
	if (days) {
		var date = new Date();
		date.setTime(date.getTime()+(days*24*60*60*1000));
		var expires = "; expires="+date.toGMTString();
	} else var expires = "";
	document.cookie = name+"="+value+expires+"; path=/";
}

function readCookie(name) {
	var nameEQ = name + "=";
	var ca = document.cookie.split(';');
	for(var i=0;i < ca.length;i++) {
		var c = ca[i];
		while (c.charAt(0)==' ') c = c.substring(1,c.length);
		if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
	}
	return null;
}

function eraseCookie(name) {
	createCookie(name,"",-1);
}

/*
 * Validation Functions
 */
function validateClearIcons(prefix) {
	prefix = prefix ? prefix+"_" : "";
	var list = document.getElementsByTagName("img");
	for (var i = 0; i < list.length; i++) {
		if (list[i].id && list[i].id.substr(0,4) == "vic_"+prefix) list[i].src = 'images/icon_blank.gif';
	}
}

function validateSetIcon(id,valid) {
	var e = getElement('vic_'+id);
	if (!e) return false;
	e.src = 'images/'+(valid ? 'icon_blank.gif' : 'icon_warning.gif');
	if (!valid && (e = getElement(id))) e.focus();
}

function validateNotEmpty(id,allow_zero) {
	var e = getElement(id);
	var v = !isEmpty(e,allow_zero);
	validateSetIcon(e.id,v);
	return v;
}

function validate(id,allow_empty,callback,call_obj) {
	var e = getElement(id);
	var v = (allow_empty && isEmpty(e.value)) || callback((call_obj || !e) ? e : e.value);
	validateSetIcon(e ? e.id : id,v);
	return v;
}

/*
 * Test Functions
 */
function testImageType(value) {
	var index = value.lastIndexOf(".");
	if (index == -1) return false;
	var ext = value.substring(index+1).toLowerCase();
	return ext == "png" || ext == "gif" || ext == "jpg" || ext == "jpeg";
}

function testPhone(value) {
	return /^\d{3,3}-\d{7,7}$/.test(value);
}

function testEmail(value) {
	return /^[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value);
}

function testURL(value) {
	return /^((http|https):\/\/|)[A-Z0-9.-]+\.[A-Z]{2,4}(|\/|\/([\w#!:.,~?+=&%@!\-\/]+))$/i.test(value);
}

function testDate(value,partial,format) {
	var ps = "([-\\s\\.\\/])";
	var py = "(19|20)\\d\\d";
	var pm = "([1-9]|0[1-9]|1[012])";
	var pd = "([1-9]|0[1-9]|[12]\\d|3[01])";
	if (partial && RegExp("^~?\\s*("+py+"|"+pm+ps+py+"|"+py+ps+pm+")\\s*\\??$").test(value)) return true;
	switch (format) {
		case "m/d/Y": return RegExp("^~?\\s*"+pm+ps+pd+"\\2"+py+"\\s*\\??$").test(value);
		case "d/m/Y": return RegExp("^~?\\s*"+pd+ps+pm+"\\2"+py+"\\s*\\??$").test(value);
		default: return RegExp("^~?\\s*"+py+ps+pm+"\\2"+pd+"\\s*\\??$").test(value);
	}
}

function testTime(value,partial) {
	if (partial && /^~?\s*(00|\d|[01]\d|2[0-4])\s*\??$/.test(value)) return true;
	return /^~?\s*(00|\d|[01]\d|2[0-4])[:.](\d|[0-5]\d)\s*\??$/.test(value);
}

function testInteger(value,signed) {
	if (signed) return /^-?\d+$/.test(value);
	return /^\d+$/.test(value);
}

function testDecimal(value,signed) {
	if (signed) return /^-?(\d+|\d+\.\d*|\.\d+)$/.test(value);
	return /^(\d+|\d+\.\d*|\.\d+)?$/.test(value);
}

function testScientific(value,signed) {
	if (signed) return /^-?(\d+|\d+\.\d*|\.\d+)([eE][-+]?\d+)?$/.test(value);
	return /^(\d+|\d+\.\d*|\.\d+)([eE][-+]?\d+)?$/.test(value);
}

function testFuzzyInteger(value,signed,unknown) {
	if (unknown && /^\s*\?\s*$/.test(value)) return true;
	if (signed) return /^([><~]?\s*|\-?\d+\s*\-\s*)\-?\d+\s*\??$/.test(value);
	return /^([><~]?\s*|\d+\s*\-\s*)\d+\s*\??$/.test(value);
}

function testFuzzyDecimal(value,signed,unknown) {
	if (unknown && /^\s*\?\s*$/.test(value)) return true;
	if (signed) return /^([><~]?\s*|\-?(\d+|\.\d+|\d+\.\d*)\s*\-\s*)\-?(\d+|\.\d+|\d+\.\d*)\s*\??$/.test(value);
	return /^([><~]?\s*|(\d+|\.\d+|\d+\.\d*)\s*\-\s*)(\d+|\.\d+|\d+\.\d*)\s*\??$/.test(value);
}

function testFuzzyScientific(value,signed,unknown) {
	if (unknown && /^\s*\?\s*$/.test(value)) return true;
	if (signed) return /^([><~]?\s*|\-?(\d+|\.\d+|\d+\.\d*)([eE][-+]?\d+)?\s*\-\s*)\-?(\d+|\.\d+|\d+\.\d*)([eE][-+]?\d+)?\s*\??$/.test(value);
	return /^([><~]?\s*|(\d+|\.\d+|\d+\.\d*)([eE][-+]?\d+)?\s*\-\s*)(\d+|\.\d+|\d+\.\d*)([eE][-+]?\d+)?\s*\??$/.test(value);
}

/*
 * Input Limiting Functions
 */
function limitEnter(event,callback) {
	if (window.event) event = window.event;
	return event.keyCode == 13 ? (callback ? callback() : false) : true;
}

function limitInput(event,regex) {
	if (window.event) event = window.event;
	var key = event.which ? event.which : event.keyCode;
	if (event.ctrlKey == true || event.charCode === 0 || key == 8 || key == 9 || key == 13 || key == 27) return true;
	var value = (event.target ? event.target : event.srcElement).value+String.fromCharCode(key);
	return regex.test(value);
}

function limitInputPhone(event) {
	return limitInput(event,/^[\d\-]*$/);
}

function limitInputDate(event) {
	return limitInput(event,/^[\d\-\s/\.~\?]*$/);
}

function limitInputTime(event) {
	return limitInput(event,/^[\d:.~\?]*$/);
}

function limitInputInteger(event,signed) {
	return limitInput(event,signed ? /^-?\d*$/ : /^\d*$/);
}

function limitInputDecimal(event,signed) {
	return limitInput(event,signed ? /^-?\d*\.?\d*$/ : /^\d*\.?\d*$/);
}

function limitInputScientific(event,signed) {
	return limitInput(event,signed ? /^-?\d*\.?\d*[eE]?[-+]?\d*$/ : /^\d*\.?\d*[eE]?[-+]?\d*$/);
}

function limitInputFuzzyInteger(event,signed) {
	return limitInput(event,signed ? /^([><~]?\s*|\-?\d*\s*\-?\s*)\-?\d*\s*\??$/ : /^([><~]?\s*|\d*\s*\-?\s*)\d*\s*\??$/);
}

function limitInputFuzzyDecimal(event,signed) {
	return limitInput(event,signed ? /^([><~]?\s*|\-?\d*\.?\d*\s*\-?\s*)\-?\d*\.?\d*\s*\??$/ : /^([><~]?\s*|\d*\.?\d*\s*\-?\s*)\d*\.?\d*\s*\??$/);
}

function limitInputFuzzyScientific(event,signed) {
	return limitInput(event,signed ? /^([><~]?\s*|\-?\d*\.?\d*[eE]?[-+]?\d*\s*\-?\s*)\-?\d*\.?\d*[eE]?[-+]?\d*\s*\??$/ : /^([><~]?\s*|\d*\.?\d*[eE]?[-+]?\d*\s*\-?\s*)\d*\.?\d*[eE]?[-+]?\d*\s*\??$/);
}

/*
 * User Interface Functions
 */
var CMSConstant = {};

function getConstant(name) {
	return !CMSConstant[name] ? name : CMSConstant[name];
}

function setConstant(name,value) {
	CMSConstant[name] = value;
}

function setConstants(list) {
	for (var name in list) CMSConstant[name] = list[name];
}

/*
 * Form with Data Propagation
 */
function CMSForm(m_id) {
	var m_form, m_data;

	/* Initialize object */
	m_data = getElement('cms_data');
	m_form = getElement(m_id ? m_id : 'form');
	if (!m_form) {
		m_form = document.createElement('form');
		document.body.appendChild(m_form);
	}

	function _submit(url) {
		var form = document.createElement('form');
		document.body.appendChild(form);
		form.appendChild(createHidden('data',m_data.value));
		form.appendChild(createHidden('url',url));
		form.method = 'get';
		form.action = 'redirect_process.php';
		form.submit();
	}

	function _save() {
		if (typeof(tinyMCE) != 'undefined') tinyMCE.triggerSave();
		var tags = ['input','textarea'];
		for (var i in tags) {
			var list = document.getElementsByTagName(tags[i]);
			for (var j = 0, n = list.length; j < n; j++) if (list[j].ml_input) list[j].ml_input.save();
		}
	}

	this.getElement = function() {
		return m_form;
	}

	this.validate = function(show_message) {
		validateClearIcons();
		var list = this.onValidate(m_form);
		if (list && show_message && list.length) showAlert(list.join("\n"));
		return list && list.length ? false : true;
	}

	this.submit = function() {
		_save();
		if (!this.validate(true)) return false;
		this.onSubmit(m_form);
		m_form.appendChild(createHidden('process_form','true'));
		if (m_data) m_form.appendChild(m_data);
		m_form.submit();
	}

	this.propagate = function(url) {
		_save();
		this.onSubmit(m_form,true);
		m_form.appendChild(createHidden('cms_url',url));
		if (m_data) m_form.appendChild(m_data);
		m_form.method = 'get';
		m_form.action = 'redirect_propagate.php';
		m_form.submit();
	}

	this.cancel = function(url,back) {
		if (m_data) _submit('cms_task'); else
		if (back) {
			if (history.length) history.back(); else
			location.assign(url);
		} else {
			if (url) location.assign(url);
			history.back();
		}
	}

	this.turnback = function(name,value,url) {
		if (m_data) url = 'cms_task';
		if (name) url += (url.indexOf('?') != -1 ? '&' : '?')+name+'='+value;
		if (m_data) _submit(url); else this.propagate(url);
	}

	this.onValidate = function(form) {
	}

	this.onSubmit = function(form,propagate) {
	}
}

/*
 * Text Input with Listbox
 */
function CMSTextInputWithListbox(m_name,m_id) {
	var m_self = this;
	var m_container, m_select, m_input, m_icon;

	CMSTextInputWithListbox();

	function CMSTextInputWithListbox() {
		if (m_id == undefined) m_id = m_name;
		m_container = document.createElement("span");
		m_input = document.createElement("input");
		m_input.type = "text";
		m_input.name = m_name;
		m_input.id = m_id;
		m_input.style.width = "100px";
		m_select = document.createElement("select");
		m_select.onchange = function() { m_input.value = m_select.value; };
		m_select.style.width = m_input.style.width;
		m_container.appendChild(m_select);
		m_container.appendChild(m_input);
		m_icon = document.createElement("img");
		m_icon.style.marginLeft = "3px";
		m_icon.style.cursor = "pointer";
		m_icon.onclick = function() { m_self.setInputType(m_self.getInputType() == "text" ? "listbox" : "text"); };
		m_container.appendChild(m_icon);
		var state = readCookie(m_id+"_state");
		setInputType(state ? state : "listbox");
	}

	function setInputType(type) {
		setVisible(m_select,type == "listbox");
		setVisible(m_input,type == "text");
		if (type == "listbox") selectOptionByValue(m_select,m_input.value,true);
		m_input.value = m_select.value;
		m_icon.src = 'images/'+(type == 'text' ? 'icon_find.gif' : 'icon_edit.gif');
		createCookie(m_id+"_state",type,365);
	}

	this.setWidth = function(width) {
		m_input.style.width = width;
		m_select.style.width = width;
	}

	this.setInputType = function(type) {
		setInputType(type);
	}

	this.getInputType = function() {
		return m_input.style.display == '' ? 'text' : 'listbox';
	}

	this.setValue = function(value) {
		m_input.value = value;
		selectOptionByValue(m_select,value,true);
	}

	this.getValue = function() {
		return m_input.value;
	}

	this.setOptions = function() {
		m_select.options.length = 0;
		for (var i = 0; i < arguments.length; i++) m_select.appendChild(createOption(arguments[i],arguments[i]));
		m_input.value = m_select.value;
	}

	this.setOptionsWithValue = function(list) {
		m_select.options.length = 0;
		for (var i in list) m_select.appendChild(createOption(i,list[i]));
		m_input.value = m_select.value;
	}

	this.getElement = function() {
		return m_container;
	}

	this.appendToParent = function(parent_id) {
		insertFirstChild(getElement(parent_id),m_container);
	}

	this.update = function() {
		if (this.getInputType() == 'listbox') m_input.value = m_select.value;
	}
}

/*
 * Note
 */
function CMSNote(title,note) {
	this.setTitle = function(title) {
		m_title.innerHTML = title ? title : "";
		(title ? th1 : th2).appendChild(m_icon);
		(title ? td1 : td2).appendChild(m_note);
		setVisible(tr1,title);
		setVisible(tr2,title);
		setVisible(tr3,!title);
	}

	this.setNote = function(note) {
		m_note.innerHTML = note ? note : "";
	}

	this.set = function(title,note) {
		this.setTitle(title);
		this.setNote(note);
	}

	this.getElement = function() {
		return m_table;
	}

	this.appendToParent = function(parent_id) {
		insertFirstChild(getElement(parent_id),m_table);
	}

	this.setWidth = function(width) {
		m_table.style.width = width;
	}

	this.setHeight = function(height) {
		m_note.style.height = height;
	}

	this.setStyle = function(style) {
		m_table.className = style == "table" ? "list" : "note";
	}

	this.getStyle = function(style) {
		return m_table.className == "list" ? "table" : "note";
	}

	this.isVisible = function() {
		return isVisible(m_table);
	}

	this.setVisible = function(status) {
		setVisible(m_table,status);
	}

	var m_table = document.createElement("table");
	m_table.style.width = "100%";
	m_table.style.margin = "5px 0px 5px 0px";
	m_table.style.display = "none";
	m_table.className = "note";
	var tbody = m_table.appendChild(document.createElement("tbody"));
	var tr1 = tbody.appendChild(document.createElement("tr"));
	var m_title = tr1.appendChild(document.createElement("th"));
	m_title.style.width = "100%";
	var th1 = tr1.appendChild(document.createElement("th"));
	var tr2 = tbody.appendChild(document.createElement("tr"));
	var td1 = tr2.appendChild(document.createElement("td"));
	td1.colSpan = 2;
	var tr3 = tbody.appendChild(document.createElement("tr"));
	var td2 = tr3.appendChild(document.createElement("td"));
	td2.style.width = "100%";
	var th2 = tr3.appendChild(document.createElement("td"));
	var m_icon = document.createElement("img");
	m_icon.src = 'images/icon_close.gif';
	m_icon.style.cursor = "pointer";
	m_icon.onclick = function() { m_table.style.display = "none"; };
	var m_note = document.createElement("div");
	m_note.style.width = "100%";
	m_note.style.overflowY = "auto";
	m_note.style.margin = "2px 0px 2px 0px";
	this.set(title,note);
}

/**
 * JavaScript printf/sprintf functions.
 *
 * This code is unrestricted: you are free to use it however you like.
 *
 * The functions should work as expected, performing left or right alignment,
 * truncating strings, outputting numbers with a required precision etc.
 *
 * For complex cases, these functions follow the Perl implementations of
 * (s)printf, allowing arguments to be passed out-of-order, and to set the
 * precision or length of the output based on arguments instead of fixed
 * numbers.
 *
 * See http://perldoc.perl.org/functions/sprintf.html for more information.
 *
 * Implemented:
 * - zero and space-padding
 * - right and left-alignment,
 * - base X prefix (binary, octal and hex)
 * - positive number prefix
 * - (minimum) width
 * - precision / truncation / maximum width
 * - out of order arguments
 *
 * Not implemented (yet):
 * - vector flag
 * - size (bytes, words, long-words etc.)
 *
 * Will not implement:
 * - %n or %p (no pass-by-reference in JavaScript)
 *
 * @version 2007.04.27
 * @author Ash Searle
 */
function sprintf() {
	function pad(str, len, chr, leftJustify) {
		var padding = (str.length >= len) ? '' : Array(1 + len - str.length >>> 0).join(chr);
		return leftJustify ? str + padding : padding + str;
	}

	function justify(value, prefix, leftJustify, minWidth, zeroPad) {
		var diff = minWidth - value.length;
		if (diff > 0) {
			if (leftJustify || !zeroPad) {
				value = pad(value, minWidth, ' ', leftJustify);
			} else {
				value = value.slice(0, prefix.length) + pad('', diff, '0', true) + value.slice(prefix.length);
			}
		}
		return value;
	}

	function formatBaseX(value, base, prefix, leftJustify, minWidth, precision, zeroPad) {
		// Note: casts negative numbers to positive ones
		var number = value >>> 0;
		prefix = prefix && number && {'2': '0b', '8': '0', '16': '0x'}[base] || '';
		value = prefix + pad(number.toString(base), precision || 0, '0', false);
		return justify(value, prefix, leftJustify, minWidth, zeroPad);
	}

	function formatString(value, leftJustify, minWidth, precision, zeroPad) {
		if (precision != null) {
			value = value.slice(0, precision);
		}
		return justify(value, '', leftJustify, minWidth, zeroPad);
	}

	var a = arguments, i = 0, format = a[i++];
	return format.replace(sprintf.regex, function(substring, valueIndex, flags, minWidth, _, precision, type) {
		if (substring == '%%') return '%';

		// parse flags
		var leftJustify = false, positivePrefix = '', zeroPad = false, prefixBaseX = false;
		for (var j = 0; flags && j < flags.length; j++) switch (flags.charAt(j)) {
			case ' ': positivePrefix = ' '; break;
			case '+': positivePrefix = '+'; break;
			case '-': leftJustify = true; break;
			case '0': zeroPad = true; break;
			case '#': prefixBaseX = true; break;
		}

		// parameters may be null, undefined, empty-string or real valued
		// we want to ignore null, undefined and empty-string values

		if (!minWidth) {
			minWidth = 0;
		} else if (minWidth == '*') {
			minWidth = +a[i++];
		} else if (minWidth.charAt(0) == '*') {
			minWidth = +a[minWidth.slice(1, -1)];
		} else {
			minWidth = +minWidth;
		}

		// Note: undocumented perl feature:
		if (minWidth < 0) {
			minWidth = -minWidth;
			leftJustify = true;
		}

		if (!isFinite(minWidth)) {
			throw new Error('sprintf: (minimum-)width must be finite');
		}

		if (!precision) {
			precision = 'fFeE'.indexOf(type) > -1 ? 6 : (type == 'd') ? 0 : void(0);
		} else if (precision == '*') {
			precision = +a[i++];
		} else if (precision.charAt(0) == '*') {
			precision = +a[precision.slice(1, -1)];
		} else {
			precision = +precision;
		}

		// grab value using valueIndex if required?
		var value = valueIndex ? a[valueIndex.slice(0, -1)] : a[i++];

		switch (type) {
		case 's': return formatString(String(value), leftJustify, minWidth, precision, zeroPad);
		case 'c': return formatString(String.fromCharCode(+value), leftJustify, minWidth, precision, zeroPad);
		case 'b': return formatBaseX(value, 2, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
		case 'o': return formatBaseX(value, 8, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
		case 'x': return formatBaseX(value, 16, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
		case 'X': return formatBaseX(value, 16, prefixBaseX, leftJustify, minWidth, precision, zeroPad).toUpperCase();
		case 'u': return formatBaseX(value, 10, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
		case 'i':
		case 'd': {
						var number = parseInt(+value);
						var prefix = number < 0 ? '-' : positivePrefix;
						value = prefix + pad(String(Math.abs(number)), precision, '0', false);
						return justify(value, prefix, leftJustify, minWidth, zeroPad);
				}
		case 'e':
		case 'E':
		case 'f':
		case 'F':
		case 'g':
		case 'G':
							{
						var number = +value;
						var prefix = number < 0 ? '-' : positivePrefix;
						var method = ['toExponential', 'toFixed', 'toPrecision']['efg'.indexOf(type.toLowerCase())];
						var textTransform = ['toString', 'toUpperCase']['eEfFgG'.indexOf(type) % 2];
						value = prefix + Math.abs(number)[method](precision);
						return justify(value, prefix, leftJustify, minWidth, zeroPad)[textTransform]();
				}
		default: return substring;
			}
				});
}
sprintf.regex = /%%|%(\d+\$)?([-+#0 ]*)(\*\d+\$|\*|\d+)?(\.(\*\d+\$|\*|\d+))?([scboxXuidfegEG])/g;
