/*!
 * jsUri
 * https://github.com/derek-watson/jsUri
 *
 * Copyright 2013, Derek Watson
 * Released under the MIT license.
 *
 * Includes parseUri regular expressions
 * http://blog.stevenlevithan.com/archives/parseuri
 * Copyright 2007, Steven Levithan
 * Released under the MIT license.
 */

/*globals define, module */

(function (global) {

	var re = {
		starts_with_slashes: /^\/+/,
		ends_with_slashes: /\/+$/,
		pluses: /\+/g,
		query_separator: /[&;]/,
		//Разбирающий regexp. Пример: http://regex101.com/r/jL5bN1/1
		uri_parser: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
	};

	/**
	 * unescape a query param value
	 * @param  {string} s encoded value
	 * @return {string}   decoded value
	 */
	function decode(s) {
		if (s) {
			s = decodeURIComponent(s);
			s = s.replace(re.pluses, ' ');
		}
		return s;
	}

	/**
	 * Breaks a uri string down into its individual parts
	 * @param  {string} str uri
	 * @return {object}     parts
	 */
	function parseUri(str) {
		var parser = re.uri_parser;
		var parserKeys = ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "relative", "path", "directory", "file", "query", "anchor"];
		var m = parser.exec(str || '');
		var parts = {};

		parserKeys.forEach(function (key, i) {
			parts[key] = m[i] || '';
		});

		return parts;
	}

	/**
	 * Breaks a query string down into an array of key/value pairs
	 * @param  {string} str query
	 * @return {array}      array of arrays (key/value pairs)
	 */
	function parseQuery(str) {
		var i, ps, p, n, k, v;
		var pairs = [];

		if (typeof(str) === 'undefined' || str === null || str === '') {
			return pairs;
		}

		if (str.indexOf('?') === 0) {
			str = str.substring(1);
		}

		ps = str.toString().split(re.query_separator);

		for (i = 0; i < ps.length; i++) {
			p = ps[i];
			n = p.indexOf('=');

			if (n !== 0) {
				k = decodeURIComponent(p.substring(0, n));
				v = decodeURIComponent(p.substring(n + 1));
				pairs.push(n === -1 ? [p, null] : [k, v]);
			}

		}
		return pairs;
	}

	/**
	 * Creates a new Uri object
	 * @constructor
	 * @param {string} str
	 */
	function Uri(str) {
		this.uriParts = parseUri(str);
		this.queryPairs = parseQuery(this.uriParts.query);
		this.hasAuthorityPrefixUserPref = null;
	}

	/**
	 * Define getter/setter methods
	 */
	['protocol', 'userInfo', 'host', 'port', 'path', 'anchor'].forEach(function (key) {
		Uri.prototype[key] = function (val) {
			if (typeof val !== 'undefined') {
				this.uriParts[key] = val;
			}
			return this.uriParts[key];
		};
	});

	/**
	 * if there is no protocol, the leading // can be enabled or disabled
	 * @param  {Boolean}  val
	 * @return {Boolean}
	 */
	Uri.prototype.hasAuthorityPrefix = function (val) {
		if (typeof val !== 'undefined') {
			this.hasAuthorityPrefixUserPref = val;
		}

		if (this.hasAuthorityPrefixUserPref === null) {
			return (this.uriParts.source.indexOf('//') !== -1);
		} else {
			return this.hasAuthorityPrefixUserPref;
		}
	};

	/**
	 * Serializes the internal state of the query pairs
	 * @param  {string} [val]   set a new query string
	 * @return {string}         query string
	 */
	Uri.prototype.query = function (val) {
		var s = '', i, param;

		if (typeof val !== 'undefined') {
			this.queryPairs = parseQuery(val);
		}

		for (i = 0; i < this.queryPairs.length; i++) {
			param = this.queryPairs[i];
			if (s.length > 0) {
				s += '&';
			}
			if (param[1] === null) {
				s += param[0];
			} else {
				s += param[0];
				s += '=';
				if (param[1]) {
					s += encodeURIComponent(param[1]);
				}
			}
		}
		return s.length > 0 ? '?' + s : s;
	};

	/**
	 * returns the first query param value found for the key
	 * @param  {string} key query key
	 * @return {string}     first value found for key
	 */
	Uri.prototype.getQueryParamValue = function (key) {
		var param, i;
		for (i = 0; i < this.queryPairs.length; i++) {
			param = this.queryPairs[i];
			if (key === param[0]) {
				return param[1];
			}
		}
	};

	/**
	 * returns an array of query param values for the key
	 * @param  {string} key query key
	 * @return {array}      array of values
	 */
	Uri.prototype.getQueryParamValues = function (key) {
		var arr = [], i, param;
		for (i = 0; i < this.queryPairs.length; i++) {
			param = this.queryPairs[i];
			if (key === param[0]) {
				arr.push(param[1]);
			}
		}
		return arr;
	};

	/**
	 * removes query parameters
	 * @param  {string} key     remove values for key
	 * @param  {val}    [val]   remove a specific value, otherwise removes all
	 * @return {Uri}            returns self for fluent chaining
	 */
	Uri.prototype.deleteQueryParam = function (key, val) {
		var arr = [], i, param, keyMatchesFilter, valMatchesFilter;

		for (i = 0; i < this.queryPairs.length; i++) {

			param = this.queryPairs[i];
			keyMatchesFilter = decode(param[0]) === decode(key);
			valMatchesFilter = param[1] === val;

			if ((arguments.length === 1 && !keyMatchesFilter) || (arguments.length === 2 && (!keyMatchesFilter || !valMatchesFilter))) {
				arr.push(param);
			}
		}

		this.queryPairs = arr;

		return this;
	};

	/**
	 * adds a query parameter
	 * @param  {string}  key        add values for key
	 * @param  {string}  val        value to add
	 * @param  {integer} [index]    specific index to add the value at
	 * @return {Uri}                returns self for fluent chaining
	 */
	Uri.prototype.addQueryParam = function (key, val, index) {
		if (arguments.length === 3 && index !== -1) {
			index = Math.min(index, this.queryPairs.length);
			this.queryPairs.splice(index, 0, [key, val]);
		} else if (arguments.length > 0) {
			this.queryPairs.push([key, val]);
		}
		return this;
	};

	/**
	 * replaces query param values
	 * @param  {string} key         key to replace value for
	 * @param  {string} newVal      new value
	 * @param  {string} [oldVal]    replace only one specific value (otherwise replaces all)
	 * @return {Uri}                returns self for fluent chaining
	 */
	Uri.prototype.replaceQueryParam = function (key, newVal, oldVal) {
		var index = -1, i, param;

		if (arguments.length === 3) {
			for (i = 0; i < this.queryPairs.length; i++) {
				param = this.queryPairs[i];
				if (decode(param[0]) === decode(key) && decodeURIComponent(param[1]) === decode(oldVal)) {
					index = i;
					break;
				}
			}
			this.deleteQueryParam(key, oldVal).addQueryParam(key, newVal, index);
		} else {
			for (i = 0; i < this.queryPairs.length; i++) {
				param = this.queryPairs[i];
				if (decode(param[0]) === decode(key)) {
					index = i;
					break;
				}
			}
			this.deleteQueryParam(key);
			this.addQueryParam(key, newVal, index);
		}
		return this;
	};

	/**
	 * Define fluent setter methods (setProtocol, setHasAuthorityPrefix, etc)
	 */
	['protocol', 'hasAuthorityPrefix', 'userInfo', 'host', 'port', 'path', 'query', 'anchor'].forEach(function (key) {
		var method = 'set' + key.charAt(0).toUpperCase() + key.slice(1);
		Uri.prototype[method] = function (val) {
			this[key](val);
			return this;
		};
	});

	/**
	 * Scheme name, colon and doubleslash, as required
	 * @return {string} http:// or possibly just //
	 */
	Uri.prototype.scheme = function () {
		var s = '';

		if (this.protocol()) {
			s += this.protocol();
			if (this.protocol().indexOf(':') !== this.protocol().length - 1) {
				s += ':';
			}
			s += '//';
		} else {
			if (this.hasAuthorityPrefix() && this.host()) {
				s += '//';
			}
		}

		return s;
	};

	/**
	 * Same as Mozilla nsIURI.prePath
	 * @return {string} scheme://user:password@host:port
	 * @see  https://developer.mozilla.org/en/nsIURI
	 */
	Uri.prototype.origin = function () {
		var s = this.scheme();

		if (s === 'file://') {
			return s + this.uriParts.authority;
		}

		if (this.userInfo() && this.host()) {
			s += this.userInfo();
			if (this.userInfo().indexOf('@') !== this.userInfo().length - 1) {
				s += '@';
			}
		}

		if (this.host()) {
			s += this.host();
			if (this.port()) {
				s += ':' + this.port();
			}
		}

		return s;
	};

	/**
	 * Adds a trailing slash to the path
	 */
	Uri.prototype.addTrailingSlash = function () {
		var path = this.path() || '';

		if (path.substr(-1) !== '/') {
			this.path(path + '/');
		}

		return this;
	};

	/**
	 * Serializes the internal state of the Uri object
	 * @return {string}
	 */
	Uri.prototype.toString = function () {
		var path = this.path(), query = this.query().toString(), anchor = this.anchor(), s = this.origin();

		if (path) {
			if (!(re.ends_with_slashes.test(s) || re.starts_with_slashes.test(path))) {
				s += '/';
			} else {
				if (s) {
					s.replace(re.ends_with_slashes, '/');
				}
				path = path.replace(re.starts_with_slashes, '/');
			}
			s += path;
		} else {
			if (this.host() && (query || this.anchor())) {
				s += '/';
			}
		}
		if (query) {
			if (query.indexOf('?') !== 0) {
				s += '?';
			}
			s += query;
		}

		if (anchor) {
			if (anchor.indexOf('#') !== 0) {
				s += '#';
			}
			s += anchor;
		}

		return s;
	};

	/**
	 * Clone a Uri object
	 * @return {Uri} duplicate copy of the Uri
	 */
	Uri.prototype.clone = function () {
		return new Uri(this.toString());
	};

	/**
	 * export via AMD or CommonJS, otherwise leak a global
	 */
	if (typeof define === 'function' && define.amd) {
		define(function () {
			return Uri;
		});
	} else if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
		module.exports = Uri;
	} else {
		global.Uri = Uri;
	}
}(this));