/*

	$.url('index.php?a=b').merge()

/**/

(function () {
	var $ = jQuery;
	
	var url = function (data, opts) {
		this.parse(typeof(data) == 'undefined' ? location.href : data, opts);
		return this;
	};

	var fn_url = function (elms, attr, opts) {
		this.$ = elms;
		this.url = {};
		this.urlAttr = attr;
		
		this.parse(opts);
		return this;
	};

	url.prototype = {

		build: function (opts) {
			return this._build(this.url, opts);
		},

		parse: function (url, opts) {
			this.url = this._parse(url, opts);
			return this;
		},

		attr: function (attr, val) {
			attr = this._attrName(attr);
			
			if (typeof(val) == 'undefined') {
				if (typeof(this.url[attr]) != 'undefined')
					return this.url[attr];
				return false;
			}
			
			if (val !== null)
				this.url[attr] = val;
			else
				delete this.url[attr];
				
			return this;
		},

		merge: function (url, opts) {
			this.url = this._merge([this.url, url], opts);
			return this;
		},

		open: function (url, opts) {
			if (typeof(url) != 'undefined')
				this.merge(url, opts);
			location.href = this.build();
			return this;
		},
		
		isRelative: function () {
			return this._isRelative(this.url);
		},
	
	};

	fn_url.prototype = {

		build: function (opts) {
			var self = this;
			this.$.each(function (k, v) {
				self.setUrl(this, self._build(self.url[k], opts));				
			});
			return this;
		},

		parse: function (opts) {
			var self = this;
			this.$.each(function (k, v) {
				self.url[k] = self._parse(self.getUrl(this), opts);
			});
			return this;
		},

		attr: function (attr, val) {
			var self = this;
			
			attr = this._attrName(attr);

			if (typeof(val) == 'undefined') {
				var ret = {};
				this.$.each(function (k, v) {
					if (typeof(self.url[k][attr]) != 'undefined')
						ret[k] = self.url[k][attr];
				});			
				return ret;
			}
			
			this.$.each(function (k, v) {
				if (val !== null)
					self.url[k][attr] = val;
				else
					delete self.url[k][attr];
			});			
				
			this.build();
			return this;
		},

		merge: function (url, opts) {
			var self = this;
			this.$.each(function (k, v) {
				self.url[k] = self._merge([self.getUrl(this), url], opts);
			});
		
			this.build();
			return this;
		},

		open: function (url, opts) {
			if (typeof(url) != 'undefined')
				this.merge(url, opts);
			location.href = self._build(self.url[0], opts);
			return this;
		},
		
		// Non-public functions
		
		setUrl: function (element, value) {
			return this._setUrl(element, value, this.urlAttr);
		},

		getUrl: function (element) {
			return this._getUrl(element, this.urlAttr);
		}
		
	};

	// Internal functions
	
	$.each({

		url: {},
		
		ports: {
			ftp: 21,
			http: 80,
			https: 443,
			mysql: 3306
		},

		_parse: function (url, opts) {
			var default_opts = {
				ports: this.ports,
				autoPort: true,
				forcePort: false,
				parseQuery: true,
				cleanPath: false,
				init: null
			};
			
			if (typeof(opts) == 'undefined')
				opts = {};
				
			for (k in default_opts)
				if (typeof(opts[k]) == 'undefined')
					opts[k] = default_opts[k];

			url = this._parseUrl(url);
			
			var init = typeof(opts.init) == 'string' ? this._parseUrl(opts.init) : opts.init;

			for (k in init)
				if (typeof(url[k]) == 'undefined')
					url[k] = init[k];

			if (typeof(url.port) == 'undefined')
				if ((opts.autoPort || opts.forcePort) && typeof(url.scheme) != 'undefined' && typeof(opts.ports[url.scheme]) != 'undefined')
					url.port = opts.ports[url.scheme];
				else
					if (opts.forcePort)
						url.port = '0';

			for (k in url)
				if (k !== 'query')
					url[k] = unescape(url[k]);

			if (opts.parseQuery && typeof(url.query) != 'undefined')
				url.query = this.parseQuery(url.query);

			if (opts.cleanPath && typeof(url.path) != 'undefined')
				url.path = this.cleanPath(url.path);
			
			return url;
		},

		_parseUrl: function (url) {
			var ret;
			
			if (parse = /^([^\:\?\#]*)(\?(.*?))?(\#(.*?))?$/i.exec(url))
				ret = {
					path: parse[1],
					query: parse[3],
					fragment: parse[5]
				};
			else if (parse = /^(([a-z]+)\:\/\/)?((.+?)(\:(.+?))?\@)?(([^\/\:\?\#]+)(\:([0-9]+))?)?(.*?)(\?(.*?))?(\#(.*?))?$/i.exec(url))
				ret = {
					scheme: parse[2],
					host: parse[8],
					port: parse[10],
					user: parse[4],
					pass: parse[6],
					path: parse[11],
					query: parse[13],
					fragment: parse[15]
				};
	
			for (var k in ret)
				if (!ret[k])
					delete ret[k];
			
			return ret;
		},

		_build: function (u, opts) {
			
			var k, url;
			
			var default_opts = {
				ports: this.ports,
				autoPort: true,
				forcePort: false,
				filter: {},
				build: true
			};
			
			if (typeof(opts) == 'undefined')
				opts = {};
				
			for (k in default_opts)
				if (typeof(opts[k]) == 'undefined')
					opts[k] = default_opts[k];
			
			url = {};
			$.each(u, function (k, v) {
				url[k] = v;
			});
			if (typeof(url.scheme) != 'undefined' && typeof(opts.ports[url.scheme]) != 'undefined') {
				if (opts.autoPort && typeof(url.port) != 'undefined' && url.port == opts.ports[url.scheme])
					delete url.port;
				if (opts.forcePort && typeof(url.port) == 'undefined')
					url.port = opts.ports[url.scheme];
			}

			if (opts.forcePort && typeof(url.port) == 'undefined')
				url.port = '0';

			for (k in opts.filter)
				delete url[opts.filter[k]];

			if (opts.build !== true)
				for (k in url)
					if (!this._inArray(opts.build, k))
						delete url[k];
			if (typeof(url.query) == 'object')
				url.query = this.buildQuery(url.query);

			$.each(url, function (k, v) {
				switch (k) {
					case 'query': case 'path':
						break;
					default:
						url[k] = escape(v);
						break;
				}
			});

			var out = '';
			
			if (typeof(url.scheme) != 'undefined' && url.scheme)
				out += url.scheme + '://';
			
			if (typeof(url.user) != 'undefined' && url.user) {
				out += url.user;
			
				if (typeof(url.pass) != 'undefined' && url.pass)
					out += ':' + url.pass;
			
				out += '@';
			}
			
			if (typeof(url.host) != 'undefined' && url.host)
				out += url.host;
				
			if (typeof(url.port) != 'undefined' && url.port)
				out += ':' + url.port;
				
			if (typeof(url.path) != 'undefined' && url.path) {
				if (out && url.path.substr(0, 1) != '/')
					out += '/';
				out += url.path.replace(/\%2F/g, '/').replace(/^\/+/, '/');
			}
				
			if (typeof(url.query) != 'undefined' && url.query)
				out += '?' + url.query;
				
			if (typeof(url.fragment) != 'undefined' && url.fragment)
				out += '#' + url.fragment;

			return out;
		},

		_merge: function (urls, opts) {
			var default_opts = {
				mergeQuery: false
			};
			
			if (typeof(opts) == 'undefined')
				opts = {};
				
			for (k in default_opts)
				if (typeof(opts[k]) == 'undefined')
					opts[k] = default_opts[k];

			var self = this, url = {};
			
			$.each(urls, function (k, v) {
				if (typeof(v) == 'string')
					v = self._parse(v, opts);
				
				v = self._clone(v);

				if (typeof(v.scheme) != 'undefined') {
					delete url.fragment;
					delete url.query;
					delete url.path;
					delete url.port;
					delete url.host;
					delete url.pass;
					delete url.user;
					delete url.scheme;
				}

				if (typeof(v.host) != 'undefined') {
					delete url.fragment;
					delete url.query;
					delete url.path;
					delete url.port;
					delete url.host;
					delete url.pass;
					delete url.user;
				}

				if (typeof(v.path) != 'undefined') {
					delete url.fragment;
					delete url.query;
					if (typeof(url.path) != 'undefined')
						v.path = self.mergePath(url.path, v.path);
					else
						delete url.path;
				}

				if (typeof(v.query) != 'undefined') {
					delete url.fragment;
					if (opts.mergeQuery && typeof(url.query) != 'undefined')
						v.query = self.mergeQuery(url.query, v.query);
					else
						delete url.query;
				}

				if (typeof(v.fragment) != 'undefined')
					delete url.fragment;

				$.each(v, function (k, v) {
					url[k] = v;
				});
					
			});

			return url;
		},

		mergePath: function () {
			var self = this, path = '';
			$.each(arguments, function (k, v) {
				path = self.cleanPath(path);
				if (!self._isRelative(v))
					path = v;
				else
					path = path.replace(/(^|\/)[^\/]+$/, '$1') + v;
			});
			
			path = this.cleanPath(path);
			return path;
		},

		cleanPath: function (path) {
			var i, parr = path.split('/');
			
			$.each(parr, function (k, v) {
				switch (v) {
					case '':
						if (k > 0)
							parr[k] = '';
						break;
					case '.':
						parr[k] = '';
						break;
					case '..':
						parr[k] = '';
						i = k - 1;
						while (i >= 0 && parr[i] == '')
							i--;
						if (i >= 0)
							parr[i] = '';
						break;
				}
			});
			
			return parr.join('/').replace(/\/+/g, '/');
		},

		mergeQuery: function () {
			var self = this, q = {}, val, keys, last, p;
			$.each(arguments, function (k, v) {
				if (typeof(v) == 'object')
					v = self.buildQuery(v);
				$.each(v.split("&"), function () {
					val = /^(.*?)(\[(.*?)\])?(\=(.*?))?$/.exec(this);
					keys = typeof(val[3]) != 'undefined' ? val[3].split('][') : [];
					keys.unshift(val[1]);
					p = q;
					$.each(keys, function () {
						p[this] = {};
						last = p;
						p = p[this];
					});
					last[keys.pop()] = typeof(val[5]) != 'undefined' ? val[5] : '';
				});
			});
			
			return q;
		},

		parseQuery: function (query) {
			var self = this, q = {}, val, keys, last, p;

			$.each(query.split("&"), function () {
				val = /^(.*?)(\[(.*?)\])?(\=(.*?))?$/.exec(this);
				keys = typeof(val[3]) != 'undefined' ? val[3].split('][') : [];
				keys.unshift(val[1]);
				p = q;
				$.each(keys, function () {
					p[this] = {};
					last = p;
					p = p[this];
				});
				last[keys.pop()] = typeof(val[5]) != 'undefined' ? val[5] : '';
			});
			
			return q;
		},

		buildQuery: function (query, key) {
			var self = this, out = '', k1 = '';
			$.each(query, function (k, v) {
				k1 = typeof(key) == 'undefined' ? k : key + '[' + k + ']';
				if (typeof(v) == 'object')
					out += self.buildQuery(v, k1);
				else
					out += '&' + escape(k1) + '=' + escape(v);
			});
			
			return typeof(key) == 'undefined' ? out.substr(1) : out;
			
		},
		
		_isRelative: function (url) {
			if (typeof(url) == 'string')
				url = this._parse(url);
			if (typeof(url.host) != 'undefined' || typeof(url.path) == 'undefined')
				return false;
			if (url.path.substr(0, 1) == '/' || url.path.substr(0, 1) == '\\')
				return false;
			return true;
		},

		_inArray: function (arr, val) {
			var i, l = arr.length;
			for (i = 0; i < l; i++)
				if(arr[i] == val)
					return true;
			return false;
		},
		
		_clone: function (data) {
			var self = this, d = {};
			$.each(data, function (k, v) {
				switch (typeof(v)) {
					case 'object':
						d[k] = self._clone(v);
						break;
					default:
						d[k] = v;
						break;
				}
			});
			return d;
		},

		_setUrl: function (element, value, attr) {
			var e = $(element);
			if (typeof(attr) == 'undefined') {
				if (e.attr('href')) {
					e.attr('href', value);
					return true;
				}
				if (e.attr('src')) {
					e.attr('src', value);
					return true;
				}
			}
			e.attr(attr, value);
		},
		
		_getUrl: function (element, attr) {
			var e = $(element);
			if (typeof(attr) == 'undefined') {
				if (ret = e.attr('href'))
					return ret;
				if (ret = e.attr('src'))
					return ret;				
			}
			return e.attr(attr);
		},

		_attrName: function (attr) {
			switch (attr) {
				case 'hash':
					attr = 'fragment';
					break;
				case 'search':
					attr = 'query';
					break;
			}
			return attr;
		}

	}, function (k, v) {
		url.prototype[k] = fn_url.prototype[k] = v;
	});
	
	$.extend({
		url: function (data, opts) { return new url(data, opts); }
	});
	
	$.fn.extend({
		url: function (attr, opts) { return new fn_url(this, attr, opts); }
	});
	
})();