Source: util.js

/**
 * @module 	util
 * @author  Flavio De Stefano <flavio.destefano@caffeinalab.com>
 */

/**
 * Require a module, or return a null object
 * @param  {String} name
 * @return {Object}
 */
exports.requireOrNull = function(name) {
	try {
		return require(name) || null;
	} catch (ex) {
		return null;
	}
};

/**
 * Try to open the URL with `Ti.Platform.openURL`, catching errors.
 *
 * If can't open the primary argument (url), open the fallback.
 *
 * If can't open the fallback, and `error` is set, show the error dialog.
 *
 * @param  {String} url The URL to open
 * @param  {String|Function} [fallback] If is a string, try to open the URL. If is a functions, call it.
 * @param  {String} [error]    The error to show
 */
exports.openURL = function(url, fallback, error) {
	var doFallback = function() {
		if (fallback != null) {
			if (_.isFunction(fallback)) {
				fallback();
			} else if (_.isString(fallback)) {
				Ti.Platform.openURL(fallback);
			}
		} else if (error != null) {
			exports.errorAlert(error);
		}
	};

	if (OS_IOS) {
		if (Ti.Platform.canOpenURL(url)) {
			Ti.Platform.openURL(url);
		} else {
			doFallback();
		}
	} else if (OS_ANDROID) {
		try {
			Ti.Platform.openURL(url);
		} catch (err) {
			doFallback();
		}
	}
};

/**
 * @param  {String} url
 */
exports.openHTTPLink = function(url) {
	if (OS_IOS) {

		var SD = exports.requireOrNull('ti.safaridialog');

		if (SD != null && SD.isSupported()) {
			SD.open({ url:url });

			return SD;
		} else {
			require('T/dialog').confirmYes(L('confirm_openlink_leave_app', 'Leave application?'), L('confirm_openlink_browser_alert', 'The link will be open in the browser'), function() {
				Ti.Platform.openURL(url);
			}, L('yes', 'Yes'));
		}

	} else {
		Ti.Platform.openURL(url);
	}
};

/**
 * Try to open all URLs in the array
 * @param  {Array} urls
 * @return {Boolean} `true` if at least one url has been opened.
 */
exports.tryOpenURLs = function(urls) {
	for (var i = 0; i < urls.length; i++) {
		try {
			if (OS_IOS) {
				if (Ti.Platform.canOpenURL(urls[i])) {
					Ti.Platform.openURL(urls[i]);
				} else {
					throw new Error();
				}
			} else if (OS_ANDROID) {
				if (!Ti.Platform.openURL(urls[i])) throw new Error();
			}

			return true;
		} catch (err) {}
	}

	return false;
};

/**
 * Valid only on Android, start the activity catching any possible errors.
 *
 * If `error` is provided, show the error dialog with this message.
 *
 * @param  {Object} opt   		Options for `createIntent(...)`
 * @param  {String} [error] 	Error message
 */
exports.startActivity = function(opt, error) {
	try {
		Ti.Android.currentActivity.startActivity(Ti.Android.createIntent(opt));
	} catch (ex) {
		if (error != null) {
			exports.errorAlert(error);
		}
	}
};

/**
 * Open a Facebook profile in the Facebook application
 * @param  {String} fb_id 	Facebook ID
 */
exports.openFacebookProfile = function(fb_id) {
	if (!/^\d+$/.test(fb_id)) {
		Ti.API.warn('Util: openFacebookProfile needs a numeric ID, not the username');
	}

	return exports.tryOpenURLs([
		'fb://profile/' + fb_id,
		'https://www.facebook.com/' + fb_id
	]);
};

/**
 * Open a Twitter profile in the Twitter application
 * @param  {String} tw_username 	Twitter screen name
 */
exports.openTwitterProfile = function(tw_username) {
	if (OS_IOS) {
		return exports.tryOpenURLs([
			'tweetbot:///user_profile/' + tw_username,
			'twitter://user?screen_name=' + tw_username,
			'http://www.twitter.com/' + tw_username
		]);
	} else {
		// There's a bug in the Twitter app for Android that blocks requests if the app is still closed
		return Ti.Platform.openURL('http://www.twitter.com/' + tw_username);
	}
};

/**
 * Open a Twitter status in the Twitter application
 * @param  {String} tw_username   	The user id
 * @param  {String} status_id 		The status id
 */
exports.openTwitterStatus = function(tw_username, status_id) {
	return exports.tryOpenURLs([
		'twitter://status?id=' + status_id,
		'http://www.twitter.com/' + tw_username + '/statuses/' + status_id
	]);
};

/**
 * Open a Youtube profile in the Yotube application
 * @param  {String} ytid 	Youtube ID
 */
exports.openYoutubeProfile = function(ytid) {
	return Ti.Platform.openURL('https://www.youtube.com/user/' + ytid);
};

/**
 * Open an Instagram profile in the Instagram application
 * @param  {String} ig_username 	The user's id
 */
exports.openInstagramProfile = function(ig_username) {
	return exports.tryOpenURLs([
		'instagram://user?username=' + ig_username,
		'http://www.instagram.com/' + ig_username
	]);
};

/**
 * Get the Facebook avatar from the graph
 *
 * @param  {String} fbid Facebook ID
 * @param  {Number} [w]    Width
 * @param  {Number} [h]    Height
 * @return {String}      	The open graph url pointing to the image
 */
exports.getFacebookAvatar = function(fbid, w, h) {
	return 'http://graph.facebook.com/' + fbid + '/picture/?width=' + (w || 150) + '&height=' + (h || 150);
};

/**
 * Open the iTunes Store or Google Play Store of specified appid
 * @property appid The appid
 */
exports.openInStore = function(appid) {
	if (OS_IOS) {
		Ti.Platform.openURL('https://itunes.apple.com/app/id' + appid);
	} else if (OS_ANDROID) {
		Ti.Platform.openURL('https://play.google.com/store/apps/details?id=' + appid);
	}
};

/**
 * Return the clean domain of an URL
 *
 * @param  {String} url The URL to parse
 * @return {String}     Clean domain
 */
exports.getDomainFromURL = function(url) {
	var matches = url.match(/^.+\:\/\/([^\/]+)/);
	if (matches == null || matches[1] == null) return '';
	return matches[1];
};

/**
 * Returns the build type. It differs from Ti.Platform.deployType, as it returns "development" both for "test" and "development" builds.
 * @return {String}
 */
exports.getDeployType = function() {
	return Ti.App.deployType === 'production' ? 'production' : 'development';
};

/**
 * Returns the OS name. It differs from Ti.Platform.osname, as it returns "ios" both for iPhone and iPad.
 * @return {String}
 */
exports.getOS = function() {
	var name = Ti.Platform.osname;
	return (name == 'iphone' || name == 'ipad') ? 'ios' : name;
};

/**
 * Return the iOS major version
 * @return {Number}
 */
exports.getIOSVersion = function() {
	if (!OS_IOS) return 0;
	return Ti.Platform.version.split('.')[0] >> 0;
};

/**
 * Check if is iOS 6
 * @return {Boolean}
 */
exports.isIOS6 = function() {
	return exports.getIOSVersion() === 6;
};

/**
 * Check if is iOS 7
 * @return {Boolean}
 */
exports.isIOS7 = function() {
	return exports.getIOSVersion() === 7;
};

/**
 * Check if is iOS 8
 * @return {Boolean}
 */
exports.isIOS8 = function() {
	return exports.getIOSVersion() === 8;
};

/**
 * Check if is iOS 9
 * @return {Boolean}
 */
exports.isIOS9 = function() {
	return exports.getIOSVersion() === 9;
};

/**
 * Parse the initial arguments URL schema
 *
 * @return {String}
 */
exports.parseSchema = function() {
	if (OS_IOS) {
		var cmd = Ti.App.getArguments();
		if (cmd.url != null) return cmd.url;
	} else if (OS_ANDROID) {
		var url = Ti.Android.currentActivity.intent.data;
		if (url != null) return url;
	}
	return null;
};

/**
 * Get the UNIX timestamp.
 *
 * @param  {Object} [arg]  The date to parse.
 * @return {Number}
 */
exports.timestamp = function(arg) {
	if (arg == null) return exports.now();
	return (new Date(arg).getTime() / 1000) >> 0;
};

/**
 * Get the current UNIX timestamp.
 * @return {Number}
 */
exports.now = function() {
	return (Date.now() / 1000) >> 0;
};

/**
 * Get the UNIX timestamp from now with delay expressed in seconds.
 *
 * @param  {Number} [t]  Seconds from now.
 * @return {Number}
 */
exports.fromNow = function(t) {
	return exports.timestamp(Date.now() + t*1000);
};

/**
 * Return in human readable format a timestamp
 * @param  {Number} ts The timestamp
 * @return {String}
 */
exports.timestampForHumans = function(ts) {
	return require('alloy/moment')(ts*1000).format();
};

/**
 * Try to parse a JSON, and silently fail on error, returning a `null` in this case.
 *
 * @param  {String} json 		The JSON to parse.
 * @return {Object}
 */
exports.parseJSON = function(json) {
	try {
		return JSON.parse(json) || null;
	} catch (ex) {
		return null;
	}
};

/**
 * Generate URL-encoded query string.
 *
 * @param {Object} obj 			Object key-value to parse.
 * @param {String} prepend 	The prepended char
 * @return {String}
 */
exports.buildQuery = function(obj, prepend) {
	if (_.isEmpty(obj)) return '';

	var q = [];
	var builder = function(value, key) {
		if (value === null || value === undefined) return;

		if (_.isArray(value)) {
			_.each(value, function(v) { builder(v, key+'[]'); });
		} else if (_.isObject(value)) {
			_.each(value, function(v, k) { builder(v, key+'['+k+']'); });
		} else {
			q.push( encodeURIComponent(key) + '=' + encodeURIComponent(value) );
		}
	};

	_.each(obj, builder);
	return q.length === 0 ? '' : ((prepend != null ? prepend : '?') + q.join('&'));
};

var APPDATA_DIRECTORY = null;

/**
 * Return the app-data directory.
 *
 * @return {String}
 */
exports.getAppDataDirectory = function() {
	if (APPDATA_DIRECTORY === null) {
		if (OS_IOS) {
			APPDATA_DIRECTORY = Ti.Filesystem.applicationSupportDirectory;
		} else if (OS_ANDROID) {
			APPDATA_DIRECTORY = Ti.Filesystem.getFile(Ti.Filesystem[ Ti.Filesystem.isExternalStoragePresent() ? 'externalStorageDirectory' : 'applicationDataDirectory' ]).nativePath + "/";
		} else {
			APPDATA_DIRECTORY = Ti.Filesystem.applicationDataDirectory;
		}
		// Why this?
		// Because sometimes this directory doesn't exists,
		// so with this wrap we are sure that the directory will exists.
		try { Ti.Filesystem.getFile(APPDATA_DIRECTORY).createDirectory(); } catch (err) {}
	}
	return APPDATA_DIRECTORY;
};

/**
 * Dial a number.
 *
 * @param  {String} tel The number to call.
 */
exports.dial = function(tel) {
	var telString = tel.match(/[0-9]/g).join('');
	var errString = String.format(L('unable_to_call', 'Unable to call %s'), tel);
	if (OS_IOS) {
		exports.openURL('tel:' + telString, null, errString);
	} else if (OS_ANDROID) {
		exports.startActivity({
			action: Ti.Android.ACTION_CALL,
			data: 'tel:' + telString
		}, errString);
	}
};

var XCU = {
	key: ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'anchor'],
	parser: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
	queryParser: function(params) {
		var obj = {};

		_.each(params.replace(/\+/g, ' ').split('&'), function (v,j) {
			var param = v.split('=');
			var key = decodeURIComponent(param[0]);
			var val, cur = obj, i = 0;

			var keys = key.split(']['), keys_last = keys.length - 1;

			if (/\[/.test(keys[0]) && /\]$/.test(keys[keys_last])) {
				keys[ keys_last ] = keys[ keys_last ].replace( /\]$/, '' );
				keys = keys.shift().split('[').concat( keys );
				keys_last = keys.length - 1;
			} else {
				keys_last = 0;
			}

			if (param.length === 2) {
				val = decodeURIComponent(param[1]);

				if (keys_last) {
					for (; i <= keys_last; i++) {
						key = keys[i] === '' ? cur.length : keys[i];
						cur = cur[key] = i < keys_last ? cur[key] || ( keys[i+1] && isNaN( keys[i+1] ) ? {} : [] ) : val;
					}
				} else {
					if (_.isArray(obj[key])) {
						obj[key].push( val );
					} else if ({}.hasOwnProperty.call(obj, key)) {
						obj[key] = [ obj[key], val ];
					} else {
						obj[key] = val;
					}
				}
			} else if (key) {
				obj[key] = '';
			}
		});

		return obj;
	}
};


/**
 * @param  {String} 	url  The URL to parse
 * @return {XCallbackURL}
 */
exports.parseAsXCallbackURL = function(str) {
	var uri = {};

	var m = XCU.parser.exec(str);
	var i = XCU.key.length;
	while (i--) uri[XCU.key[i]] = m[i] || '';

	uri.queryKey = XCU.queryParser(uri.query);

	return uri;
};

// The next two methods are taken from https://gist.github.com/CatTail/4174511
// Many thanks to CatTail!
/**
 * Decode html entities into text
 * @param  {String} str The string to decode
 * @return {String} 	The decoded string
 */
exports.decodeHtmlEntity = function(str) {
	return str.replace(/&#(\d+);/g, function(match, dec) {
		return String.fromCharCode(dec);
	});
};

/**
 * Encode a string into html entities
 * @param  {String} str The string to encode
 * @return {String} 	The encoded string
 */
exports.encodeHtmlEntity = function(str) {
	var buf = [];
	for (var i=str.length-1;i>=0;i--) {
		buf.unshift(['&#', str[i].charCodeAt(), ';'].join(''));
	}
	return buf.join('');
};

/**
 * Return the seralized representation of any JS object.
 * @param  {Object} obj
 * @return {String} The hash
 */
exports.hashJavascriptObject = function(obj) {
	if (obj == null) return 'null';
	if (_.isArray(obj) || _.isObject(obj)) return JSON.stringify(obj);
	return obj.toString();
};

/**
 * An error parser that parse a String/Object
 */
exports.getErrorMessage = function(obj, def) {
	if (_.isObject(obj)) {
		if (_.isString(obj.message)) {
			return obj.message;
		} else if (_.isObject(obj.error) && _.isString(obj.error.message)) {
			return obj.error.message;
		} else if (_.isString(obj.error)) {
			return obj.error;
		}
	} else if (!_.isEmpty(obj)) {
		return obj.toString();
	}

	if (def != null) return def;
	
	return L('unexpected_error', 'Unexpected error');
};

/**
 * @param  {Object}   err      		The object error
 * @param  {Function} [callback] 	The callback
 */
exports.errorAlert = function(err, callback) {
	require('T/dialog').alert(L('error', 'Error'), exports.getErrorMessage(err), callback);
};

/**
 * @method alertError
 * @see {@link errorAlert}
 */
exports.alertError = exports.errorAlert;


/**
 * Get a human representation of bytes
 * @param  {Number} bytes
 * @return {String}
 */
exports.bytesForHumans = function(bytes) {
	var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
	if (bytes === 0) return 'n/a';
	var i = parseInt(Math.floor(Math.log(bytes)/Math.log(1024)));
	return Math.round(bytes/Math.pow(1024,i),2) + ' ' + sizes[i];
};

var DATABASE_DIRECTORY = null;

/**
 * Get the private documents directory
 * @return {String}
 */
exports.getDatabaseDirectoryName = exports.getDatabaseDirectory = function() {
	if (DATABASE_DIRECTORY === null) {
		if (OS_IOS) {
			var db = require('T/db').open('test');
			var path = db.file.resolve().split('/'); path.pop();
			db.close();
			DATABASE_DIRECTORY = path.join('/') + '/';
		} else if (OS_ANDROID) {
			DATABASE_DIRECTORY = Ti.Filesystem[ Ti.Filesystem.isExternalStoragePresent() ? 'externalStorageDirectory' : 'applicationDataDirectory' ] + '/databases';
			try { Ti.Filesystem.getFile(DATABASE_DIRECTORY).createDirectory(); } catch (err) {}
		}
	}
	return DATABASE_DIRECTORY;
};

/**
 * Get the resources directory path
 * @return {String}
 */
exports.getResourcesDirectory = function() {
	if (OS_IOS) {
		if (Ti.Shadow) {
			return Ti.Filesystem.applicationDataDirectory + Ti.App.name + '/iphone/';
		} else {
			return Ti.Filesystem.resourcesDirectory;
		}
	} else {
		return Ti.Filesystem.getFile(Ti.Filesystem.resourcesDirectory, "").nativePath + (Ti.Shadow ? "/" : "");
	}
};


/**
 * Compare two app versions
 * @param  {String} a
 * @param  {String} b
 * @return {Number}
 */
exports.compareVersions = function(a, b) {
	if (a == null || b == null) return 0;

	a = a.split('.');
	b = b.split('.');
	for (var i = 0; i < Math.max(a.length, b.length); i++) {
		var _a = +a[i] || 0, _b = +b[i] || 0;
		if (_a > _b) return 1;
		else if (_a < _b) return -1;
	}
	return 0;
};

/**
 * Add leading zeros
 * @param  {String} num  The number
 * @param  {Number} size The final size
 * @return {String}
 */
exports.zeroPad = function(num, size) {
	if (num == null) return num;

	var result = num.toString();
	while (result.length < (size || 2)) result = '0' + result;
	return result;
};

/**
 * Get a UUID
 * @return {String}
 */
exports.guid = function() {
	return Ti.Platform.createUUID();
};

/**
 * Get the full platform name
 * @return {String}
 */
exports.getPlatformFullName = function() {
	return Ti.Platform.model + ' - ' + Ti.Platform.osname + ' ' + Ti.Platform.version + ' (' + Ti.Platform.ostype + ') - ' + Ti.Platform.locale;
};

/**
 * Get the rot13 of a string
 * @param  {String} s
 */
exports.rot13 = function(s) {
	return s.replace(/[a-zA-Z]/g,function(c){return String.fromCharCode((c<="Z"?90:122)>=(c=c.charCodeAt(0)+13)?c:c-26);});
};