/**
 * Pop namespace/module/singleton
 *
 * Used to access functionality for Pop front ends.
 * Some methods of this module were taken from Douglas Crockford's website.
 */
var Pop = function () {
	
	// private properties
	var m_isReady = false;
	var me = {};
	
	// private methods
	
	// constructor
	
	// public:
	me.tools = {};	
	
	/**
	 * Call from the "ready" event from your favorite JS framework.
	 * @param options
	 *  An object containing any options
	 */
	me.ready = function(options) {
		for (var prefix in this.tools) {
			try {
				if (('popOptions' in this.tools[prefix])
				 && ('ready' in this.tools[prefix].popOptions)
				 && !this.tools[prefix].popOptions.ready) {
					continue;
				}
				if (!('ready' in this.tools[prefix])) {
					throw new Exception(
						"Tool with prefix " + prefix 
						+ "lacks ready(options) function."
					);
				}
				this.tools[prefix].ready(options);
			} catch (e) {
				// don't let errors escape, but log them
				if (typeof(console) != 'undefined') {
					console.warn(e);
				}
			}
		}
		_isReady = true;
	}
	
	/**
	 * Returns whether Pop.ready() has been called
	 */ 
	me.isReady = function() {
		return m_isReady;
	}
	
	/**
	 * Extends a string or object to be used with AJAX
	 * @param what
	 *  If a string, then treats it as a URL and
	 *  appends ajax fields to the end of the querystring.
	 *  If an object, then adds properties to it.
	 * @param slots
	 *  If a string, expects a comma-separated list of slots
	 *  If an object, converts it to a comma-separated list
	 */
	me.ajaxExtend = function(what, slots) {
		if (typeof(slots) == 'string') {
			var slots2 = slots;
		} else {
			var slots2 = "";
			for (var k in slots) {
				if (slots2 > "")
					slots2 += ",";
				slots2 += k;
			}
		}
		var timestamp = (new Date()).getTime();
		if (typeof(what) == 'string') {
			if (what.indexOf('?') < 0) {
				what2 = what + '?';
			} else {
				what2 = what;
			}
			what2 += encodeURI('&_pop[ajax]=JSON')
			 + encodeURI('&_pop[timestamp]=') + encodeURIComponent(timestamp)
			 + encodeURI('&_pop[slotNames]=') + encodeURIComponent(slots2);
		} else {
			// assume it's an object
			what2 = {};
			for (k in what) {
				what2[k] =  what[k];
			}
			what2._pop = {
				"ajax": "JSON",
				"timestamp": timestamp,
				"requestId": requestId,
				"slotNames": slots2
			};
		}
		return what2;
	}
	
	/**
	 * Replaces an element in the DOM with some other content.
	 * The replaced element will be completely mangled and
	 * removed from the DOM, so don't use it afterward.
	 * 
	 * @param String|DOMNode|FBDOMNode element
	 *  This can either be an HTML or FBML node, or 
	 *  If this is a string, the element is obtained via 
	 *  document.getElementById on that string.
	 *
	 * @param String|DomNode|FBMLString|FBDOMNode replacement
	 *  The HTML or FBML string -- obtained, for example, from an AJAX call.
	 *  If this DOM Node is already in the DOM, then this function
	 *  will remove it from the DOM before replacing the target.
	 */
	me.replace = function(element, replacement) {
		if (typeof(element) == 'string') {
			element = document.getElementById(element);
		}
		if ('getParentNode' in element) {
			// element is an FBML node
			var eParent = element.getParentNode();
			var eNext = element.getNextSibling();
			element.setId('avoid_conflict_'+(new Date().getTime()));
			if (typeof(replacement) == 'object'
			 && ('getParentNode' in replacement)) {
				// replacement is an FBMLNode
				eParent.insertBefore(replacement, element);
				eParent.removeChild(element);
			} else {
				// assume replacement is an FBMLString
				element.setInnerFBML(replacement);
				var eChild = element.getFirstChild();
				var eRef = element;
				while (eChild) {
					eNextChild = eChild.getNextSibling();
					eRef = eRef.getNextSibling();
					eParent.insertBefore(eChild, eRef);
					eChild = eNextChild;
				}
				eParent.removeChild(element);
			}
		} else {
			if (typeof(element) !== 'string') {
				// assume replacement is an HTML node
				eParent.insertBefore(replacement, element);
				eParent.removeChild(element);
			} else {
				// replacement is a string
				if ('html' in element
				 && typeof(element.html) == 'function') {
					element.html(replacement); // jquery
				} else {
					element.innerHTML = replacement; // otherwise
				}
				var eChild = element.firstChild;
				var eRef = element;
				while (eChild) {
					eNextChild = eChild.nextSibling;
					eRef = eRef.nextSibling;
					eParent.insertBefore(eChild, eRef);
					eChild = eNextChild;
				}
			}
			eParent = element.parentNode;
			eParent.replaceChild(replacement, element);
		}
	};
	
	/**
	 * Clones an existing object, creating a new object
	 * which you can extend.
	 */
	me.clone = function (original) {
		function F() {};
		F.prototype = original;
		return new F();
	};
	
	/**
	 * Returns the type of a value
	 * @param value
	 *  
	 * return 
	 */
	me.typeOf = function (value) {
	    var s = typeof value;
	    if (s === 'object') {
			if (value === null) {
				return 'null';
			}
            if (typeof value.length === 'number' 
			 && !(value.propertyIsEnumerable('length'))
			 && typeof value.splice === 'function') {
                s = 'array';
			} else if (typeof(value.typename) != 'undefined' ) {
				return value.typename;
            } else if (typeof(value.constructor) != 'undefined'
			 && typeof(value.constructor.name) != 'undefined') {
				return value.constructor.name;
			} else {
				return 'object';
			}
	    }
	    return s;
	};
	
	/**
	 * Binds a method to an object, so "this" inside the method
	 * refers to that object when it is called.
	 * @param method
	 *  A reference to the function to call
	 * @param obj
	 *  The object to bind to
	 * @param options
	 *  Optional. If supplied, binds these options and passes
	 *  them during invocation.
	 */
	me.bind = function (method, obj, options) {
		if (options) {
			return function () {
				return method.apply(obj, arguments, options);
			}
		} else {
			return function () {
				return method.apply(obj, arguments);
			}
		}
	};
	
	/**
	 * Tests whether an object is empty
	 * @param o
	 *  The object to test.
	 */
	me.isEmpty = function (o) {
		if (!o) {
			return false;
		}
	    var i, v;
	    if (Pop.typeOf(o) === 'object') {
	        for (i in o) {
	            v = o[i];
	            if (v !== undefined && Pop.typeOf(v) !== 'function') {
	                return false;
	            }
	        }
	    }
	    return true;
	};
	
	return me;
}();

/**
 * Pop.Tool Class
 *
 * All JS classes for tools should extend this class using
 * Pop.Tool.apply(this, arguments)
 */
Pop.Tool = function(prefix, options, popOptions) {

	// fix up some things
	if (!popOptions) {
		popOptions = {};
	}
	if (!options) {
		options = {};
	}
	if (Pop.typeOf(popOptions) == 'array'
	 && popOptions.length == 0) {
		popOptions = {};
	}

	// private properties
	var m_tool_children = {"_": {}};
	var me = this;
	
	// private methods
	
	// constructor
	if (prefix in Pop.tools) {
		// remove the tool, notifying it
		Pop.tools[prefix].remove(this);
	}
	Pop.tools[prefix] = this;
	
	// public:
	me.typename = 'Pop.Tool';
	me.prefix = prefix;
	me.popOptions = popOptions;
	
	/**
	 * Gets the children of a particular tool
	 * based on the prefix of the tool.
	 */
	me.children = function () {
		var result = {};
		for (key in Pop.tools) {
			if (key.substr(0, prefix.length) == this.prefix) {
				result[key] = Pop.tools[key];
			}
		}
		return result;
	}
	
	me.setParent = function (newParentPrefix) {
		// remove from previous parent
		if ('parentPrefix' in this.popOptions) {
			if (this.popOptions.parentPrefix in m_tool_children) {
				var pp = this.popOptions.parentPrefix;
				if (this.prefix in m_tool_children[pp]) {
					delete m_tool_children[pp][this.prefix];
				}
			}
		}
		// move to new parent
		this.popOptions.parentPrefix = newParentPrefix;
		if (!newParentPrefix || newParentPrefix == '_') {
			m_tool_children["_"][this.prefix] = this;
		} else {
			if (! (newParentPrefix in m_tool_children))
				m_tool_children[newParentPrefix] = {};
			m_tool_children[newParentPrefix][this.prefix] = this;
		}
	}
	
	me.getParent = function () {
		if ('parentPrefix' in this.popOptions) {
			if (this.popOptions.parentPrefix in Pop.tools) {
				return Pop.tools[this.popOptions.parentPrefix];
			}
		}
		return null;
	}
	
	/**
	 * Called when a tool instance is removed, possibly
	 * being replaced by another.
	 * Typically happens after an AJAX call which returns
	 * markup for the new instance tool.
	 * Also can be used for removing a tool instance
	 * and all of its children.
	 * Calls onRemove before replacing.
	 *
	 * @param Pop.Tool newTool
	 *  The tool that is supposed to be replacing it.
	 *  If null, the original tool is just removed.
	 */
	me.remove = function (newTool) {
		
		if (newTool 
		&& ('prefix' in newTool) 
		&& newTool.prefix == this.prefix) {
			// We are just "replacing the tool with itself",
			// so we should do nothing.
			// The real replacing happened during the construction
			// of the new instance of the tool, and onRemove
			// was called on the old instance from within Pop.Tool().
			return false;
		}

		var children = this.children();
		for (key in children) {
			children[key].remove();
		}
		if ('onRemove' in this) {
			// Handle this event
			if ('onRemove' in this) {
				this.onRemove({"newTool": newTool});
			}
		}
		
		if (this.prefix in Pop.tools) {
			delete Pop.tools[this.prefix];
		}
		if ('parentPrefix' in this.popOptions) {
			var pp = this.popOptions.parentPrefix;
			delete m_tool_children[pp][this.prefix];
		}
			
		return true;
	}
};

Pop.Session = function() {
	// TODO: Set a timer for when session expires?
	return {};
}


/**
 * Extend some built-in prototypes
 */

String.prototype.htmlentities = function () {
    return this.replace(/&/g, "&amp;").replace(/</g,
        "&lt;").replace(/>/g, "&gt;");
};

String.prototype.quote = function () {
    var c, i, l = this.length, o = '"';
    for (i = 0; i < l; i += 1) {
        c = this.charAt(i);
        if (c >= ' ') {
            if (c === '\\' || c === '"') {
                o += '\\';
            }
            o += c;
        } else {
            switch (c) {
            case '\b':
                o += '\\b';
                break;
            case '\f':
                o += '\\f';
                break;
            case '\n':
                o += '\\n';
                break;
            case '\r':
                o += '\\r';
                break;
            case '\t':
                o += '\\t';
                break;
            default:
                c = c.charCodeAt();
                o += '\\u00' + Math.floor(c / 16).toString(16) +
                    (c % 16).toString(16);
            }
        }
    }
    return o + '"';
};

String.prototype.supplant = function (o) {
    return this.replace(/\{([^{}]*)\}/g,
        function (a, b) {
            var r = o[b];
            return typeof r === 'string' || typeof r === 'number' ? r : a;
        }
    );
};

String.prototype.trim = function () {
    return this.replace(/^\s+|\s+$/g, "");
};
