define([
        "dojo/_base/lang",
    	"dojo/_base/array", // array.forEach array.map
    	"dojo/_base/declare", // declare
    	"dojo/dom", // dom.setSelectable
    	"dojo/dom-construct",
    	"dojo/dom-style",
    	"dojo/dom-class", // domClass.contains
    	"dojo/dom-attr",
    	"dojo/_base/event", // event.stop
    	"dojo/has", 
    	"dojo/string", // string.substitute
    	"dojo/_base/window", // win.doc.createTextNode
    	"dojo/on",
    	"dojo/topic",
    	"dijit/popup",
    	"obno/app/router",
    	"dojo/hash",
    	"dojo/io-query",
    	"dojo/_base/xhr",
    	"dojox/encoding/base64",
    	"dojo/Deferred",
    	"dojo/promise/all",
    	"dojo/when",
    	"dojo/cookie",
    	"dojo/_base/config",
    	"obno/core/util/bits",
    	"dijit/registry",
    	"dojo/_base/unload",
    	"obno/core/Configuration",
    	"obno/core/Debug",
    	"dijit/_Container",
	    "dojo/parser",
	    "obno/mvc/at",
	    "obno/core/resources",	    
	    "obno/security/xhr",
	    "obno/app/monkey"
       ], 
	    function(lang, array, declare, dom, domConstruct, domStyle, domClass, domAttr,
	    		 event, has, string, window, on, topic, popup, router, hash, ioQuery,
	    		 xhr, base64, Deferred, all, when, cookie, config, bits, registry, unload, Configuration, Debug, _Container) {
	
		has.add("obno-debug-app-api", (config["obno"] || {}).debugAppApi);
	
		unload.addOnUnload(function(){
			console.log("unloading app");
			//router.saveRouterState();
		});
		
		
		var obnoApp = declare("obno.model._ApplicationMixin", [_Container], {
			
		GLOBAL_TOPIC : "com.obno.application.global",
		AUTHC_TOPIC : "com.obno.security.executeLogin",
		LOGOUT_TOPIC: "com.obno.security.logout",
		SILENT_LOGOUT_TOPIC: "com.obno.security.silentLogout",
		SESSION_CHANGED_TOPIC: "com.obno.security.sessionChanged",
		BACK_TOPIC: "com.obno.application.back",
		FWD_TOPIC: "com.obno.application.forward",
		
		SESSIONID: "JSESSIONID",
		
		constructor: function(params){
			this.__activeViews = {}; //[containerID, mvc]
			this.__viewContainers = {};// [containerID, node]
			this.__appContainers = {};// [containerID, node]
			this.__activityHandlers = {}; // [activity, topic]
			this.__activityIdx = 0;
			this.__uiInitializers = [];
			this.__layoutContainers = [];
			this.__topicsToPaths = [];
			this.configuration = new Configuration();
			
			var self = this;
	
			//remember original location so that we redirect back here once successfully logged in
			var redirectedFrom = null; 
			this.__backHandle = topic.subscribe(this.BACK_TOPIC, function(event){
				router.back();
			});
			this.__fwdHandle = topic.subscribe(this.FWD_TOPIC, function(event){
				router.fwd();
			});
			this.__globalHandler = topic.subscribe(this.GLOBAL_TOPIC, function(event){
				if (event.name == "obno-authenticate")
				{
					//we may have many services in a view that trigger this, so only trigger for the first one
					var _currentHash = hash();
					var registeredLoginPath = self.__topicsToPaths["com.obno.security.login"];
					if (registeredLoginPath && registeredLoginPath == _currentHash){
						//we are already at the login location so do not fire a login route again
						return;
					}
					//we also publish a silent logout event so that any Capability based widgets get refreshed
					try
					{
						topic.publish(self.SILENT_LOGOUT_TOPIC, {});
					}
					catch (e)
					{
						self.log("error while performing silent logout. A silent logout listener failed. No problem, we are logging out anyway");
					}
					redirectedFrom = _currentHash;
					self.log(redirectedFrom + " requires login for [" + event.url + " ]. publishing login event");
					topic.publish("com.obno.security.login", {});

				}	
			});
			this.__logoutHandler = topic.subscribe(this.LOGOUT_TOPIC, function(event){
				when(xhr("GET", {
					url: self.configuration.getContext() + "/obno/service/public/logout",
					preventCache: true,
					handleAs: "json"
				}), function(results){
					topic.publish("com.obno.application.routeToLogoutLocation", {});
				},  function(error){
					//now what?
				});				
				
			});
			
			this.__silentLogoutHandler = topic.subscribe(this.SILENT_LOGOUT_TOPIC, function(event){
				when(xhr("GET", {
					url: self.configuration.getContext() + "/obno/service/public/logout",
					preventCache: true,
					handleAs: "json"
				}), function(results){
					self.log("logged out");
					//nothing - silent
				},  function(error){
					self.log("logout error");
				});				
			});
			
			this.__executeLoginHandler = topic.subscribe(this.AUTHC_TOPIC, function(event){
				//we do not want to redirect to the previous location before we authenticate
				//the user successfully. to do this we pass the authc credentials to a no-op
				//protected service so that the system authenticates the user
				
				//create authentication token
				var username, password, isRememberMe;
				if (event.eventArgs && lang.isArray(event.eventArgs)){
					var len = event.eventArgs.length;
					if (len > 0){
						username = event.eventArgs[0];
					}
					if (len > 1){
						password = event.eventArgs[1];
					}
					if (len > 2){
						isRememberMe = event.eventArgs[2];
					}
				}
				
				var authToken = (username || "") + ":" + (password || "");
				if (typeof isRememberMe === undefined || isRememberMe === null)
				{
					authToken += ":false";
				}
				else
				{
					authToken += ":" + (isRememberMe ? "true" : "false");
				}
				//convert to base64
				authToken = base64.encode(bits.s2b(authToken));
				self.log("executing login");				
				when(xhr("GET", { url: self.configuration.getContext() + "/obno/service/internal/login", 
					              handleAs: "json", 
					              preventCache: true,
					              headers: {Authorization : "basic " + authToken}}),
				function(results){
					//redirect to previous location unless we are the same location or no previous location 
					if (redirectedFrom && redirectedFrom != hash())	{
						self.log("redirecting to " + redirectedFrom + " after login");				
						hash(redirectedFrom);
						redirectedFrom = null;
					} else {
						//route to default location
						redirectedFrom = null;
						self.log("redirecting to default location after login");
						topic.publish("com.obno.application.routeToDefaultLocation", {});
					}
					topic.publish("com.obno.security.loginSuccess", {eventArgs: [username]});
				},  function(error){
					//raise login error
					self.log("redirecting to login failed");
					topic.publish("com.obno.security.loginFailed", {});
				});
			});
		},
				
		//maps to route args by position or by name
		//argNames = the named args of the route 
		getRouteArgsHandler: function(argNames)
		{
			var numArgs = argNames.length;
			var hasArgs = numArgs > 0;
			
			var argsHandler = function(args) {
				var ret = {};
				if (args){
					if (lang.isArray(args)){
						//bind by position
						array.forEach(argNames, function(arg){
							if (args.length > 0){
								ret[arg] = args.shift();
							} else {
								ret[arg] = null;
							}
						});
					} else {
						//bind by name
						array.forEach(argNames, function(arg){
							if (typeof args[arg] !== "undefined") {
								ret[arg] = args[arg];
							} else {
								ret[arg] = null;
							}
						});					
					}					
				}
				return ret;
			};
			argsHandler.numArgs = function() { return numArgs; };
			argsHandler.argNames = argNames;
			return argsHandler;
		},
		
		registerActivity : function(activityQName, path, argNames) {
			this.__topicsToPaths[activityQName] = path;
			var argsHandler = this.getRouteArgsHandler(argNames);
			var self = this;
			this.__activityHandlers['hdl' + this.__activityIdx++] = topic.subscribe(activityQName, function(args){
				self.log("triggered activity " + activityQName + "[" + path + "]");
				var activityPath = path;
				if (args && args.eventArgs) {
					var _args = argsHandler(args.eventArgs);
					var query = ioQuery.objectToQuery(_args);
					if (query && query.length > 0) {
						activityPath += "?" + query;
					}
				}
				self.log("triggered activity " + activityQName + " resolved query to [" + activityPath + "]");
				hash(activityPath);
			});
		},
		
		registerRoute: function(path, containerID, viewModule, ctrlModule, viewModelModule, argNames){
			var pathRegEx = new RegExp("^" + path + "(\\?.+)?" + "$");
			var self = this;
			var argsHandler = this.getRouteArgsHandler(argNames);
			var routeCallback = function(args){
				self.log("routed to " + args.newPath);
//				args.preventDefault();
				//store the current state in history
				var activeView = lang.getObject(containerID, false, self.__activeViews);
				var state = null;
				if (activeView && activeView.viewModel && activeView.ctrl){
					state = {};
					state.vmState = activeView.viewModel;
					state.ctrlState = activeView.ctrl.extractState();
				}
				router.pushState(args.oldPath, state);
				//look for history entry for current path
				var historicalState = router.loadState(args.newPath);
				var queryArgs = null;
				if (args.newPath.indexOf('?') > 0){
					queryArgs = ioQuery.queryToObject(args.newPath.slice(args.newPath.indexOf('?') + 1));
					//filter out any args that are not declared for this route
					var _tmpArgs = {};
					array.forEach(argNames, function(a){
						if (a in queryArgs){
							_tmpArgs[a] = queryArgs[a];
						}
					});
					queryArgs = _tmpArgs;
				}
				//now render the view(s) 
				when(self._loadMVC(viewModule, ctrlModule, viewModelModule), function(mvcModuleCtors){
					//if location has changed while we are rendering then cancel the current view render
					if (hash() != args.newPath){
						self.log("canceling render of " + args.newPath);
						return;
					}
					self.log("rendering routed to " + args.newPath);
					self.renderView(mvcModuleCtors, argsHandler, queryArgs, containerID, historicalState);
				});
			};
			router.register(pathRegEx, routeCallback);
			if (path === "/"){
				router.register("^$", routeCallback);
			}
		},
		
		registerInitializer: function(containerID, viewModule, ctrlModule, viewModelModule) {
			var self = this;
			var initializer = {
				containerID: containerID, 
				viewModule: viewModule,
				ctrlModule: ctrlModule,
				viewModelModule: viewModelModule
			};				
			this.__uiInitializers.push(initializer);
		},
		
		getTargetContainerNode : function(targetContainerID){
			if (targetContainerID)
			{
				var containerNode = lang.getObject(targetContainerID, false, this.__appContainers);
				return containerNode;
			}
			return null;
		},
				
		renderView : function(mvcModuleCtors, argsHandler, args, targetContainerID, historicalState) {
			var _args = [];
			if (argsHandler && args){
				array.forEach(argsHandler.argNames, function(arg){
					_args.push(args[arg]);
				});
			}					
			var containerNode = this.getTargetContainerNode(targetContainerID);
			if (containerNode)
			{
				var activeView = lang.getObject(targetContainerID, false, this.__activeViews);
				if (activeView){
					//check if we can tear down this view
					if (activeView.ctrl.tearDown){
						activeView.ctrl.tearDown();
					}
					if (activeView.view.destroyRecursive){
						activeView.view.destroyRecursive();
					}
				}
				var mvc = {};
				if (historicalState){
					mvc.viewModel = historicalState.vmState;
					mvc.ctrl = new mvcModuleCtors.ctrlCtor({scope: mvc.viewModel, args : _args});
					mvc.ctrl.loadState(historicalState.vmState, historicalState.ctrlState);
				} else {
					mvc.viewModel = new mvcModuleCtors.viewModelCtor();
					mvc.ctrl = new mvcModuleCtors.ctrlCtor({scope: mvc.viewModel, args : _args});
					mvc.ctrl.initialize({scope: mvc.viewModel, args : _args});
				}
				mvc.view = new mvcModuleCtors.viewCtor({viewModel : mvc.viewModel});
				containerNode.set('content', mvc.view);
				mvc.ctrl.boot();
				//resize application container(s)
				this.resizeContainers();
				lang.setObject(targetContainerID, mvc, this.__activeViews);				
			}
		},
		
		// load mvc modules on demand - we return promises with the 
		// loaded modules so that the renderning of the views can be 
		// serialized
		_loadMVC: function(v, c, vm){
			var viewModelDef = new Deferred();
			var viewDef = new Deferred();
			var ctrlDef = new Deferred();
			//make sure mvc modules are paths
			v = v.replace(/\./g, "/");
			c = c.replace(/\./g, "/");
			vm = vm.replace(/\./g, "/");
			require([v, c, vm], function(view, ctrl, viewModel){
				viewModelDef.resolve(viewModel);
				viewDef.resolve(view);
				ctrlDef.resolve(ctrl);
			});
			var mvcDeferred = new Deferred();
			when(all([viewModelDef, viewDef, ctrlDef]), function(mvc){
				var viewModelCtor = mvc[0];
				var viewCtor = mvc[1];
				var ctrlCtor = mvc[2];
				mvcDeferred.resolve({viewCtor: viewCtor, ctrlCtor: ctrlCtor, viewModelCtor: viewModelCtor});
			}, function(error){
				mvcDeferred.reject("Error loading mvc modules");
			});
			return mvcDeferred;
		},
	
		_initAppContainerConfiguration: function(){
		},
				
		_registerActivityHandlers: function()
		{
		},
		
		_registerRoutes: function()
		{
		},
		
		_unregisterActivityHandlers: function()
		{
			for(var a in this.__activityHandlers){
				this.__activityHandlers[a].remove();
				delete this.__activityHandlers[a];
			}
		},
		
		
		resizeContainers: function(){
			array.forEach(this.__layoutContainers, function(w){
				w.resize();
			});
		},
		
		_initializeApplication: function(){
			console.log("loading state");
			//router.loadRouterState();
			//load all initial mvc modules
			var self = this;
			var allMVCModules = [];
			for (var i=0; i<this.__uiInitializers.length; ++i){
				var initializer = this.__uiInitializers[i];
				allMVCModules.push(this._loadMVC(initializer.viewModule, initializer.ctrlModule, initializer.viewModelModule));				
			}
			this._routeFromQuery();
			//render views once all modules have been loaded
			when(all(allMVCModules), function(allInitializerCtors){
				for (var i=0; i<self.__uiInitializers.length; ++i){
					var initializer = self.__uiInitializers[i];
					self.renderView(allInitializerCtors[i], null, null, initializer.containerID);					
				}	
				self.log("rendered init views");
				self.resizeContainers();
				//start router once the startup views have been initialiazed. Note that, if the startup views 
				//depend on authenticated service calls, and we are currently unauthenticated, it will lead to mayhem.
				//they will be calling on the login topic but it will not have been readied yet with any listeners. 
				//SO NEVER REGISTER any views that require access to authenticated services on the init{} part of the application
				self.log("staring router");
				router.startup();
				//if we are at the root level redirect to home
				var currentLoc = hash();
				if (!currentLoc){
					hash("/");					
				}
				//if we are at the root level redirect to home

				//start session monitoring
				self._monitorSession();
			});
		},
			
		_routeFromQuery: function(){
			//before we fire up the router we need to scan the current url to see 
			//if we are being routed via an http get param. We offer this functionality
			//so that we can be routed based on server side redirects which will not contain
			//the hash portion of the url (remember the hash is only browser side).
			//for example someone can get to a route via:
			//  - /context/app.html#SomeRoute  i.e. default routing mechanism OR
			//  - /context/app.html?__route=SomeRoute i.e. transalte to /context/app.html#SomeRoute
			var routeFromQuery = null; 
			var newLocation = null;
			var hashSize = function(hash){
				var size = 0, key;
				for (key in hash) {
					if (hash.hasOwnProperty(key)) size++;
				}
				 return size;				
			};
			if (__obno.loadUri && __obno.loadUri.indexOf("?") > 0){
				var query = __obno.loadUri.substring(__obno.loadUri.indexOf("?") + 1, __obno.loadUri.length);
				//drop fragment from url query
				if (query.indexOf("#") > 0){
					query = query.substring(0, query.indexOf("#"));
				}
				var queryParams = ioQuery.queryToObject(query);
				if (queryParams && queryParams['__routedFrom']){
	    			//we have a route
	    			routeFromQuery = queryParams['__routedFrom'];
	    			//add the route params to the hash (minus the _routedFrom arg)
	    			delete queryParams['__routedFrom'];
	    			if (hashSize(queryParams) > 0){
		    			routeFromQuery += "?" + ioQuery.objectToQuery(queryParams);	   
		    		}
	    		}
			}
			if (routeFromQuery){
				hash("/" + routeFromQuery);
			}
		},
		
		_createState : function(){
		},
	
		_registerUIInitializers: function(){
			
		},
		
		log: function(msg){
			if (has("obno-debug-app-api")){
				console.log(msg);
			}
		},
		
		postCreate: function(){
			this.log("post create");
			this.inherited(arguments);
			//find all top-level layout widgets inside our template
			this._findLayoutWidgets(this.containerNode);
			this._createState();
			this._initAppContainerConfiguration();
			this._registerRoutes();
			this._registerActivityHandlers();
			this._registerUIInitializers();
			this._initializeApplication();
		},	
	
		_findLayoutWidgets: function(root){
			if (this.resize && this.layout){
				this.__layoutContainers.push(this);
				return;
			} 
			for(var node = root.firstChild; node; node = node.nextSibling){
				if(node.nodeType == 1){
					var widgetId = node.getAttribute("widgetId");
					if(widgetId){
						var w = registry.byId(widgetId);
						if (w && w.resize && w.layout){
							this.__layoutContainers.push(w);
							return;
						}
					}
					//this._getContainedWidgets(node);
				}
			}
		},
		
		//monitor our session cookie and notify interested listeners on change of this cookie
		_monitorSession: function(){
			var self = this;
			var sessionID = cookie(this.SESSIONID); //we cannot see the path of the cookie (that's the best we can do)
			//set up a timed deferred to monitor this session id every 1 second. If sessionID has changed then trigger the event		
			var _sessionMonitor = function(){
				var newSessionID = cookie(self.SESSIONID);
				if (sessionID){
					//we had a session
					if (newSessionID){
						if (sessionID != newSessionID){
							//session changed
							topic.publish(self.SESSION_CHANGED_TOPIC, {});
						}
					}else{
						//we don't have a session and we used to have one
						topic.publish(self.SESSION_CHANGED_TOPIC, {});
					}
				} else {
					//we did not have a session and now we have
					if (newSessionID){
						topic.publish(self.SESSION_CHANGED_TOPIC, {});							
					}
				}
				sessionID = newSessionID;				
			};
			var futureMonitor = new Deferred();
			var _resched = function(){
				futureMonitor.promise.then(function(){
					futureMonitor = new Deferred();
					setTimeout(function(){
						_sessionMonitor();
						futureMonitor.resolve();
					}, 1000);
					_resched();
				});
			};			
			setTimeout(function(){
				_sessionMonitor();
				futureMonitor.resolve();
			}, 1000);
			_resched();
		},
		
		destroy: function(){
			this._unregisterActivityHandlers();
			this.__globalHandler.remove();
			this.__logoutHandler.remove();
			this.__executeLoginHandler.remove();
			this.__silentLogoutHandler.remove();
			this.__backHandle.remove();
			this.__fwdHandle.remove();
			//TODO - kill seesion monitor
		}
		
	});
		
	return obnoApp;
	    
});