Source: flow.js

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

/**
 * @property config
 * @property [config.antiBounce=true] 	Add an anti bounce to avoid double triggering of open windows
 * @property [config.log=false]			Log the opening of controllers
 */
exports.config = _.extend({
	antiBounce: true,
	log: false
}, Alloy.CFG.T ? Alloy.CFG.T.flow : {});

var Event = require('T/event');

var Navigator = null;
var windowsStack = [];
var currentController = null;

var controllerBouncing = {};

/**
 * Current controller name
 * @type {Object}
 */
exports.currentControllerName = null;

/**
 * Current controller arguments
 * @type {Object}
 */
exports.currentControllerArgs = null;

/**
 * Attach event to current module
 */
exports.event = function(name, cb) {
	Event.on('flow.' + name, cb);
};

function track($window, route) {
	// Track screen with GA
	require('T/ga').trackScreen(route);

	// Track timing with GA
	var startFocusTime = null;

	$window.addEventListener('focus', function(){
		startFocusTime = Date.now();
	});

	$window.addEventListener('blur', function(){
		if (startFocusTime === null) return;
		require('T/ga').trackTiming(route, Date.now() - startFocusTime);
	});
}

function open(name, args, openArgs, route, useNav) {
	args = args || {};
	openArgs = openArgs || {};
	route = route || name;

	if (exports.config.log) {
		Ti.API.trace("Flow: opening controller <" + name + ">");
	}

	if (name in controllerBouncing) {
		Ti.API.warn("Flow: Trying to open twice the controller <" + name + "> - avoided by anti bounce system");
		return;
	}

	controllerBouncing[ name ] = true;
	// Avoid that, if the listener is never called, the antibounce system will block all future opens
	setTimeout(function() {
		delete controllerBouncing[ name ];
	}, 500);

	var controller = Alloy.createController(name, args);
	exports.setCurrentController(controller, name, args);

	// Get the main window to track focus/blur
	var $window = controller.getView();

	// Check if the getView is a NavigationController, in that case, $window is the subwindow
	if ($window.window) $window = $window.window;

	exports.setCurrentWindow($window, route);

	// Clean up controller on window close
	$window.addEventListener('close', function() {
		controller.trigger('close');

		controller.off(); // Turn off Backbone Events
		controller.destroy(); // Destroy by Kroll Bridge

		if (_.isFunction(controller.cleanup)) {
			controller.cleanup();
		}

		controller = null;
		$window = null;
	});

	$window.addEventListener('open', function() {
		delete controllerBouncing[ name ];
		controller.trigger('open');
	});

	// Open the window
	if (useNav) {

		if (/NavigationWindow/.test(Navigator.apiName)) {
			Navigator.openWindow($window, openArgs);
		} else if (Navigator.apiName === "Ti.UI.Tab") {
			Navigator.open($window, openArgs);
		} else {
			throw new Error('Flow: incompatible navigator');
		}

	} else {
		if (_.isFunction(controller.open)) {
			controller.open(openArgs);
		} else {
			$window.open(openArgs);
		}
	}

	return controller;
}

function onWindowClose(e) {
	var route = e.source._route;
	Event.trigger('flow.close', {
		route: route,
	});

	var index = -1;
	for (var k in windowsStack) {
		if (windowsStack[k]._route === route) {
			windowsStack.splice(+k, 1);
			return;
		}
	}
}

/**
 * Set, in a single way, the global Navigator (and open it), the Index-Controller and the Index-Window
 *
 * This method, is typically called on startup, on the index.js, like this:
 *
 * ```
 * Flow.startup($, $.nav, $.win, 'index', {});
 * ```
 *
 * @param  {Alloy.Controller} 				controller
 * @param  {Ti.UI.iOS.NavigationWindow} 	nav
 * @param  {Ti.UI.Window} 						win
 * @param  {String}								controllerName
 * @param  {String}								controllerArgs
 */
exports.startup = function(controller, nav, win, controllerName, controllerArgs) {
	exports.setCurrentWindow(win, '/' + controllerName);
	exports.setCurrentController(controller, controllerName, controllerArgs);
	exports.setNavigationController(nav, true);

	// Reset variables
	windowsStack = [];
};


/**
 * Require an Alloy.Controller without passing it to the Navigator
 *
 * This is anyway tracked with Google Analitycs
 *
 * @param  {String} 	name 				The name of the controller
 * @param  {Object} 	[args]     		The args passed to the controller
 * @param  {String} 	[route]				Optional route that identify this controller
 * @return {Alloy.Controller} 		The controller instance
 */
exports.openDirect = function(name, args, openArgs, route) {
	return open(name, args, openArgs, route, false);
};

/**
 * Require an Alloy.Controller and open its main `View` in the Navigator.
 *
 * A `close` event is automatically attached to the main window to call sequentially
 * `Controller.cleanup` (if defined) and `Controller.destroy`
 *
 * This is tracked with Google Analitycs
 *
 * @param  {String} 	name 				The name of the controller
 * @param  {Object} 	[args]       	The arguments passed to the controller
 * @param  {Object} 	[openArgs]     The arguments passed to the `Navigator.openWindow` or `Tab.open`
 * @param  {String} 	[route]			Optional route that identify this controller
 * @return {Alloy.Controller}    	The controller instance
 */
exports.open = function(name, args, openArgs, route) {
	if (Navigator === null) {
		throw new Error('Flow: A Navigator is not defined yet. You have to call Flow.setNavigationController upfront.');
	}

	return open(name, args, openArgs, route, true);
};

/**
 * Close current Navigatgor and all windows associated with it
 */
exports.close = function() {
	if (Navigator === null) return;

	windowsStack = [];
	Navigator.close();
	Navigator = null;
};

/**
 * Set current controller
 * @param {Alloy.Controller} 	controller
 * @param {String} 				[name]
 * @param {Object}				[args]
 */
exports.setCurrentController = function(controller, name, args) {
	currentController = controller;
	exports.currentControllerName = name;
	exports.currentControllerArgs = args;
};

/**
 * Return current controller
 * @return {Alloy.Controller}
 */
exports.getCurrentController = function() {
	return currentController;
};

/**
 * Set current Window and push in the windows stack
 * @param {Ti.UI.Window} $window
 * @param {String} route
 */
exports.setCurrentWindow = function($window, route) {
	$window._route = route;
	windowsStack.push($window);

	// Track with GA
	track($window, route);

	// Add listener
	$window.addEventListener('close', onWindowClose);
};

/**
 * Return the windows stacks
 * @return {Array}
 */
exports.getWindows = function() {
	return windowsStack;
};

/**
 * Get current Window
 * @return {Ti.UI.Window}
 */
exports.getCurrentWindow = function() {
	return _.last(windowsStack);
};


/**
 * Set the Navigator used to open the windows
 *
 * ** This is required before opening windows **
 *
 * @param {Object} 	nav 			An instance of `NavigationWindow` or a `TabGroup`
 * @param {Boolean} 	[openNow] 	Specify if call instantly the open on the navigator
 */
exports.setNavigationController = function(nav, openNow) {
	Navigator = nav;
	if (openNow === true) {
		Navigator.open();
	}
};


/**
 * Return the instance set of Navigator
 * @return {Object}
 */
exports.getNavigationController = function() {
	return Navigator;
};


/**
 * Close all windows, except first.
 */
exports.closeAllWindowsExceptFirst = function() {
	if (windowsStack.length < 2) return;

	var wins = _.clone(windowsStack);

	for (var i = wins.length-2; i > 0; i--) {
		wins[i].close({ animated: false });
	}
	wins[wins.length-1].close();
};