define(["require", "dojo/_base/kernel", "dojo/_base/lang", "dojo/_base/declare", "dojo/_base/config",
	"dojo/_base/window", "dojo/Evented", "dojo/Deferred", "dojo/when", "dojo/has", "dojo/on", "dojo/ready",
	"dojo/dom-construct", "dojo/dom-attr", "./utils/model", "./utils/nls", "./module/lifecycle",
	"./utils/hash", "./utils/constraints", "./utils/config"],
	function(require, kernel, lang, declare, config, win, Evented, Deferred, when, has, on, ready, domConstruct, domAttr,
			 model, nls, lifecycle, hash, constraints, configUtils){

	has.add("app-log-api", (config["app"] || {}).debugApp);

	var Application = declare(Evented, {
		constructor: function(params, node){
			declare.safeMixin(this, params);
			this.params = params;
			this.id = params.id;
			this.defaultView = params.defaultView;
			this.controllers = [];
			this.children = {};
			this.loadedModels = {};
			this.loadedStores = {};
			// Create a new domNode and append to body
			// Need to bind startTransition event on application domNode,
			// Because dojox/mobile/ViewController bind startTransition event on document.body
			// Make application's root domNode id unique because this id can be visited by window namespace on Chrome 18.
			this.setDomNode(domConstruct.create("div", {
				id: this.id+"_Root",
				style: "width:100%; height:100%; overflow-y:hidden; overflow-x:hidden;"
			}));
			node.appendChild(this.domNode);
		},

		createDataStore: function(params){
			// summary:
			//		Create data store instance
			//
			// params: Object
			//		data stores configuration.

			if(params.stores){
				//create stores in the configuration.
				for(var item in params.stores){
					if(item.charAt(0) !== "_"){//skip the private properties
						var type = params.stores[item].type ? params.stores[item].type : "dojo/store/Memory";
						var config = {};
						if(params.stores[item].params){
							lang.mixin(config, params.stores[item].params);
						}
						// we assume the store is here through dependencies
						try{
							var storeCtor = require(type);
						}catch(e){
							throw new Error(type+" must be listed in the dependencies");
						}
						if(config.data && lang.isString(config.data)){
							//get the object specified by string value of data property
							//cannot assign object literal or reference to data property
							//because json.ref will generate __parent to point to its parent
							//and will cause infinitive loop when creating StatefulModel.
							config.data = lang.getObject(config.data);
						}
						if(params.stores[item].observable){
							try{
								var observableCtor = require("dojo/store/Observable");
							}catch(e){
								throw new Error("dojo/store/Observable must be listed in the dependencies");
							}
							params.stores[item].store = observableCtor(new storeCtor(config));
						}else{
							params.stores[item].store = new storeCtor(config);
						}
						this.loadedStores[item] = params.stores[item].store;							
					}
				}
			}
		},

		createControllers: function(controllers){
			// summary:
			//		Create controller instance
			//
			// controllers: Array
			//		controller configuration array.
			// returns:
			//		controllerDeferred object

			if(controllers){
				var requireItems = [];
				for(var i = 0; i < controllers.length; i++){
					requireItems.push(controllers[i]);
				}

				var def = new Deferred();
				var requireSignal;
				try{
					requireSignal = require.on ? require.on("error", function(error){
						if(def.isResolved() || def.isRejected()){
							return;
						}
						def.reject("load controllers error.");
						if(requireSignal){
							requireSignal.remove();
						}
					}) : null;
					require(requireItems, function(){
						def.resolve.call(def, arguments);
						if(requireSignal){
							requireSignal.remove();
						}
					});
				}catch(e){
					def.reject(e);
					if(requireSignal){
						requireSignal.remove();
					}
				}

				var controllerDef = new Deferred();
				when(def, lang.hitch(this, function(){
					for(var i = 0; i < arguments[0].length; i++){
						// instantiate controllers, set Application object, and perform auto binding
						this.controllers.push((new arguments[0][i](this)).bind());
					}
					controllerDef.resolve(this);
				}), function(){
					//require def error, reject loadChildDeferred
					controllerDef.reject("load controllers error.");
				});
				return controllerDef;
			}
		},

		trigger: function(event, params){
			// summary:
			//		trigger an event. Deprecated, use emit instead.
			//
			// event: String
			//		event name. The event is binded by controller.bind() method.
			// params: Object
			//		event params.
			kernel.deprecated("dojox.app.Application.trigger", "Use dojox.app.Application.emit instead", "2.0");
			this.emit(event, params);
		},

		// setup default view and Controllers and startup the default view
		start: function(){
			//
			//create application level data store
			this.createDataStore(this.params);

			// create application level data model
			var loadModelLoaderDeferred = new Deferred();
			var createPromise;
			try{
				createPromise = model(this.params.models, this, this);
			}catch(e){
				loadModelLoaderDeferred.reject(e);
				return loadModelLoaderDeferred.promise;
			}
			when(createPromise, lang.hitch(this, function(models){
				// if models is an array it comes from dojo/promise/all. Each array slot contains the same result object
				// so pick slot 0.
				this.loadedModels = lang.isArray(models)?models[0]:models;
				this.setupControllers();
				// if available load root NLS
				when(nls(this.params), lang.hitch(this, function(nls){
					if(nls){
						lang.mixin(this.nls = {}, nls);
					}
					this.startup();
				}));
			}), function(){
				loadModelLoaderDeferred.reject("load model error.")
			});
		},

		setDomNode: function(domNode){
			var oldNode = this.domNode;
			this.domNode = domNode;
			this.emit("app-domNode", {
				oldNode: oldNode,
				newNode: domNode
			});
		},

		setupControllers: function(){
			// create application controller instance
			// move set _startView operation from history module to application
			var currentHash = window.location.hash;
		//	this._startView = (((currentHash && currentHash.charAt(0) == "#") ? currentHash.substr(1) : currentHash) || this.defaultView).split('&')[0];
			this._startView = hash.getTarget(currentHash, this.defaultView);
			this._startParams = hash.getParams(currentHash);
		},

		startup: function(){
			// load controllers and views
			//
			this.selectedChildren = {};			
			var controllers = this.createControllers(this.params.controllers);
			// constraint on app
			if(this.hasOwnProperty("constraint")){
				constraints.register(this.params.constraints);
			}else{
				this.constraint = "center";
			}
			var emitLoad = function(){
				// emit "app-load" event and let controller to load view.
				this.emit("app-load", {
					viewId: this.defaultView,
					initLoad: true,
					params: this._startParams,
					callback: lang.hitch(this, function (){
						this.emit("app-transition", {
							viewId: this.defaultView,
							forceTransitionNone: true, // we want to avoid the transition on the first display for the defaultView
							opts: { params: this._startParams }
						});
						if(this.defaultView !== this._startView){
							// transition to startView. If startView==defaultView, that means initial the default view.
							this.emit("app-transition", {
								viewId: this._startView,
								opts: { params: this._startParams }
							});
						}
						this.setStatus(this.lifecycle.STARTED);
					})
				});
			};
			when(controllers, lang.hitch(this, function(){
				if(this.template){
					// emit "app-init" event so that the Load controller can initialize root view
					this.emit("app-init", {
						app: this,	// pass the app into the View so it can have easy access to app
						name: this.name,
						type: this.type,
						parent: this,
						templateString: this.templateString,
						controller: this.controller,
						callback: lang.hitch(this, function(view){
							this.setDomNode(view.domNode);
							emitLoad.call(this);
						})
					});
				}else{
					emitLoad.call(this);
				}
			}));
		}		
	});

	function generateApp(config, node){
		// summary:
		//		generate the application
		//
		// config: Object
		//		app config
		// node: domNode
		//		domNode.
		var path;

		// call configProcessHas to process any has blocks in the config
		config = configUtils.configProcessHas(config);

		if(!config.loaderConfig){
			config.loaderConfig = {};
		}
		if(!config.loaderConfig.paths){
			config.loaderConfig.paths = {};
		}
		if(!config.loaderConfig.paths["app"]){
			// Register application module path
			path = window.location.pathname;
			if(path.charAt(path.length) != "/"){
				path = path.split("/");
				path.pop();
				path = path.join("/");
			}
			config.loaderConfig.paths["app"] = path;
		}
		require(config.loaderConfig);

		if(!config.modules){
			config.modules = [];
		}
		// add dojox/app lifecycle module by default
		config.modules.push("./module/lifecycle");
		var modules = config.modules.concat(config.dependencies?config.dependencies:[]);

		if(config.template){
			path = config.template;
			if(path.indexOf("./") == 0){
				path = "app/"+path;
			}
			modules.push("dojo/text!" + path);
		}

		require(modules, function(){
			var modules = [Application];
			for(var i = 0; i < config.modules.length; i++){
				modules.push(arguments[i]);
			}

			if(config.template){
				var ext = {
					templateString: arguments[arguments.length - 1]
				}
			}
			App = declare(modules, ext);

			ready(function(){
				var app = new App(config, node || win.body());

				if(has("app-log-api")){
					app.log = function(){
						// summary:
						//		If config is set to turn on app logging, then log msg to the console
						//
						// arguments: 
						//		the message to be logged, 
						//		all but the last argument will be treated as Strings and be concatenated together, 
						//      the last argument can be an object it will be added as an argument to the console.log 						
						var msg = "";
						try{
							for(var i = 0; i < arguments.length-1; i++){
								msg = msg + arguments[i];
							}
							console.log(msg,arguments[arguments.length-1]);
						}catch(e){}
					};
				}else{
					app.log = function(){}; // noop
				}

				app.transitionToView = function(/*DomNode*/target, /*Object*/transitionOptions, /*Event?*/triggerEvent){
					// summary:
					//		A convenience function to fire the transition event to transition to the view.
					//
					// target:
					//		The DOM node that initiates the transition (for example a ListItem).
					// transitionOptions:
					//		Contains the transition options.
					// triggerEvent:
					//		The event that triggered the transition (for example a touch event on a ListItem).
					var opts = {bubbles:true, cancelable:true, detail: transitionOptions, triggerEvent: triggerEvent || null};
					on.emit(target,"startTransition", opts);
				};

				app.setStatus(app.lifecycle.STARTING);
				// Create global namespace for application.
				// The global name is application id. ie: modelApp
				var globalAppName = app.id;
				if(window[globalAppName]){
					declare.safeMixin(app, window[globalAppName]);
				}
				window[globalAppName] = app;
				app.start();
			});
		});
	}

	return function(config, node){
		if(!config){
			throw new Error("App Config Missing");
		}

		if(config.validate){
			require(["dojox/json/schema", "dojox/json/ref", "dojo/text!dojox/application/schema/application.json"], function(schema, appSchema){
				schema = dojox.json.ref.resolveJson(schema);
				if(schema.validate(config, appSchema)){
					generateApp(config, node);
				}
			});
		}else{
			generateApp(config, node);
		}
	}
});
