/**
 * ElementController, Site, Page, and Component
 * This is my baby.
 *
 * @author Stephen Rushing
 * @version 2.8a (2011-06-21)
 *
 **/
 
 


(function(){
	var $ = jQuery,
		undefined;
   
	
	var ElementController = window.ElementController = ResigClass.extend({    
	    classPath:"",
	    className:"ElementController",
	    debug:false,
	    construct:function(container, options){
            var $container = $(container).first(),
				classKeyName = this.getFullClassName(true);
			
			//Check for an existing instance and set the container
			assert(!$container.data(classKeyName), $container.selector + " is already attached to an instance of " + this.getFullClassName() +".");			
			$container.data(classKeyName, this);
			
			this.el = {
				$container: $container
			};			
			//Merge the options
			this.options = $.extend(true, {}, this.options, options); 
			
			//this.createDispatcher("setContainer", "beforeSetContainer", "afterSetContainer").createDo("setContainer", "_setContainer");
    		//this.createDispatcher("init", "beforeInit", "afterInit").createDo("init", "_init").createDo("beforeInit", "_beforeInit").createDo("afterInit", "_afterInit");
    		this.createDispatcher({init:"_init", beforeInit:"_beforeInit", afterInit:"_afterInit"});
    	},
		initialized:false,
		el:null,
		options:null,
		bind: function () {
			arguments[0] = this._nameSpacify(arguments[0]);			
			//$.fn.bind.apply(this.el.$container, arguments);
			
			$.fn.bind.apply(this.el.$container, arguments);
			return this;
		},
		unbind: function(){
			arguments[0] = this._nameSpacify(arguments[0]);
			//$.fn.unbind.apply(this.el.$container, arguments);
			
			$.fn.unbind.apply(this.el.$container, arguments);
			return this;
		},
		trigger: function () {
			//Using jQuery.trigger() causes an infinite loop...using jQuery.triggerHandler() until I figure out why.
			arguments[0] = this._nameSpacify(arguments[0]);
			
			//this.$I.triggerHandler.apply(this.el.$container, arguments);
			$.fn.trigger.apply(this.el.$container, arguments);
			return this;
		},
		createDispatcher:function(dispatch){
		    
		    var primary=dispatch,
		        before=null,
		        after=null,
		        i=0;
		        
            if(typeof primary !== "string"){
    		    for(var name in dispatch){
    		        switch(i){
    		            case 0:
        		            primary = name;
        		            break;
        	            case 1:
        	                before = name;
        	                break;
                        case 2:
                            after = name;
                            break;
    		        }
    		        //Create the do
    		        var doName = dispatch[name];
    		        if(typeof doName === "string"){
    		            //this.log(this.getFullClassName(), name, doName);
    		            this.createDo(name, doName);
    		        }
    		        i++;
    		    }
		    }
		    
		    if(assert(!$.isFunction(this[primary]), this.getFullClassName() +" already has a \""+primary+"\" dispatcher.")){
		        
		        //Create the dispatcher
				this[primary] = function(){
					assert(this.el.$container && this.el.$container.length, this.getFullClassName() +" requires a container to dispatch events.");
					
					var evtBefore,
						evtPrimary,
						evtAfter;
						
					if(before){
						evtBefore = $.Event(this._nameSpacify(before));
						this.trigger(evtBefore, arguments);					
						if(evtBefore.result === false){
							return this;
						}
					}
					//Unless the "before" event already returned false, always run the primary event
					
					evtPrimary = $.Event(this._nameSpacify(primary));
					if(evtBefore != undefined) evtPrimary.before = evtBefore;
					this.trigger(evtPrimary, arguments);
					if(evtPrimary.result === false){
						return this;
					}
					
					//If "before" and "primary" events didn't return false, run the after event
					if(after){
						evtAfter = $.Event(this._nameSpacify(after));
						evtAfter.primary = evtPrimary;
						this.trigger(evtAfter, arguments);
					}
					return this;
				};
			}
			
			return this;		    
		},
		//A "do" is the primary handler of the current object's event. We create a proxy function for it so that it can be replaced with object._whatever = function(){} at any time and still maintain its position in the event handler stack.
		createDo:function(eventName, methodName){
		    var I = this;
			I.bind(eventName, function(evt){
			    //I.log(I.getFullClassName(), eventName, methodName);
    		    if($.isFunction(I[methodName])){
				   result = I[methodName].apply(I, arguments);
    		       return result; 
    		    }
		    });
		    return this;
		},
		_nameSpacify:function(name, isBasic){
			if($.type(name)=="array"){ 
				name = name[0]+"."+name[1];
			}
			else if($.type(name) == "string" && name.indexOf(".") < 0){
				name = name + "." + ((isBasic!==true) ? this.getFullClassName(true) : this.className);
			}
			return name;
		},
		
		_afterInit:function(){
			this.initialized = true;
		},
		
		populate:function(data, context, selector, getKey){
		    if(!(context instanceof $)) context = this.el.$container;
		    if(!selector) selector = "[data-key]";
		    if(!getKey) getKey = "data-key";
		    var I = this;
		    //this.log(arguments);
		    context.find(selector).each(function(){
                var $el = $(this),
                    key = (typeof getKey === "string") ? $el.attr(getKey) : getKey.call(this, this),
                    keyData,
                    $input;
                
                if (key in data){
                    keyData = data[key];
                }
                //Look for an input within the context, if keyData does not contain it
                else if(($input = I.el.$inputs.filter("[name='"+key+"']")).length && $.trim($input.val()).length){
                    keyData = $input.val();
                }else{
                    I.log("No data found in populate() for key \""+key+"\"", this, I);
                    return;
                }
                
                //Set the element content                
                if($el.is(":input")){
                    //TODO: Add logic for checkboxes and multi selects
                    $el.val(keyData);
                }else{
                    $el.html(keyData);
                }
                    
                //this.log($el, key, $input);
            });
            return this;
		},
		
		//This is a proxy for the ContextString class. For example, pass the link <a id="#link" href="#Site._something(#Component.getSomething())"> with an alias map of var obj = {Site:this, Component:this.comp} with the event and attribute you want to bind. 
		//Usage Examples: 
		//    bindContextAttr("click", "#selector", "href", {window:window}, "#", true)
		//    bindContextAttr("click", "button", "data-call", {window:window}, "$", false)
		bindContextAttr:function(eventName, elements, attr, aliases, prefix, cancelEvent){	
		
            var I = this;
            $(elements).bind(eventName, function(evt){
                var $el = $(this),
                    contextStr = ContextString($el.attr(attr), aliases, prefix);

                contextStr.eval();
                
                //Cancel the link, if not told otherwise
                if(cancelEvent === true){
                    return false;
                }
            });

		    return this;
		},	
		destroy:function(){
		    //Need to unbind events and stuff here, but for now we just detach the instance from the element
			this.el.$container.data(this.getFullClassName(), null);
			this.el = {};
		},
		getFullClassName:function(isKeySafe){
		    var name = this.className;
		    if(typeof this.classPath === "string" && this.classPath.length){
		        name = this.classPath+"."+this.className;
		    }
		    if(isKeySafe){
		        name = name.replace(/\./g, "_");
		    }
		    return name;
		},
		toString:function(){
			return "Object [["+this.getFullClassName()+"]]";
		},
		log:function(){
		    if(this.debug){
		        console.log.apply(console, arguments);
		    }
		    return this;
		}
	});
	
	ElementController.get = function(controllerClass, el, options, autoInit, initArgs){
	    var ControllerClass = $.isFunction(controllerClass) ? controllerClass : eval(controllerClass),
	        CClassName = ControllerClass.prototype.getFullClassName(true),
	        instance = $(el).data(CClassName);
	    
	    //console.log(ControllerClass, CClassName, el, options, autoInit, initArgs, instance, $(el).data());
	    if(!instance){
	        //Create the new instance
	        instance = new ControllerClass(el, options);
	    }else if(options){
	        //Extend the options of an existing instance
	        $.extend(instance.options, options);
	    }
	    if(instance && autoInit !== false && !instance.initialized){
	        //Initialize instance with initialize args
	        instance.init.apply(instance, initArgs||[]);
	    }
	    return instance;
	};
	
	
	
	//Create the contollers and controller jQuery plugins for ElementController
	$.fn.controllers = function(controllerClass, options, autoInit, initArgs){
	    
	    var controllers = [];
	    this.each(function(e, el){
	        var controller = ElementController.get(controllerClass, el, options, autoInit, initArgs);
	        if(controller){
	            controllers.push(controller);
	        }
	    });
	    return controllers;
	};
	
	$.fn.controller = function(controllerClass, options, autoInit, initArgs){	    
	    if(this.length){
    	    var controller = ElementController.get(controllerClass, this[0], options, autoInit, initArgs);;    	    
    	    return controller;
	    }
	    return null;
	};
	
	
	
	var Site = window.Site = ElementController.extend({
    	className:"Site",
    	construct: function(container){			
			if(!container) container = arguments[0] = document.documentElement;
			this._super.apply(this, arguments);
    	    
    		var I = this;
    		
    		//Create an empty object for the pages and components of this instance
    		I.page = {};
    		I.component = {};
			
			//I.setContainer(document.documentElement);
    		
    		//Create event dispatchers and "do" methods
    		I.createDispatcher({ initPages:"_initPages", beforeInitPages:"_beforeInitPages", afterInitPages:"_afterInitPages"});
    		I.createDispatcher({ initComponents:"_initComponents", beforeInitComponents:"_beforeInitComponents", afterInitComponents:"_afterInitComponents"});
    		
			return I;
    	},	
		initialized:false,
		_init:function(evt){
			
		},
		
		_afterInit:function(evt){
		    this.initialized = true;
		    //Initialize the pages
			this.initPages(this.page);
		},
		
		_initPages:function(evt, pageControllers){
			for(var pageName in pageControllers){
				var pageClass = pageControllers[pageName];
				
				if((pageClass.prototype.test === true || ($.isFunction(pageClass.prototype.test) && pageClass.prototype.test() === true))){
					var pageController = $("body").controller(pageClass, null, true);
				}
			}
		},
		
		_afterInitPages:function(evt, pages){
		    //Find and initialize the components
			
			var components = this.findComponents();
			this.initComponents(components);
		},
		
		page:null,		
		
		_initComponents:function(evt, componentInstances){
			for(var c=0; c < componentInstances.length; c++){
				var component = componentInstances[c];
				this.log("initComponents", component);
				if(!component.initialized){
					component.init();
				}
			}
		},
		
		componentAttr:"data-component",
		componentOptionsAttr:"data-component-options",
		
		findComponents:function(context){
			var I = this,
				instances = [];
			if(!context) context = I.el.$container;
			
			if(!(context instanceof $)) context = $(context);
			
			context.find("["+I.componentAttr+"]").each(function(e, el){
				var $el = $(el),
					components = $el.attr(I.componentAttr).split(/\s*,\s*/g),
					options = $el.attr(I.componentOptionsAttr);

                if(options && $.trim(options).length){
			        try{
			            options = eval("["+options+"]");
			        }catch(err){
			            this.log(el, options, err.message);
			            options = null;
			        }
			    }
				for(var c=0; c<components.length; c++){
					var componentName = components[c],
						componentType = I.component[componentName],
						instance;
					
					if(componentType){
					    var opts = null;
					    if(options && options.length && options[c]){
					        opts = eval(options[c]);
					    }
						instance = $el.controller(componentType, opts, false);
						I.log("findComponents", instance, opts);
						instances.push(instance);
					}else{
					    I.log(componentName + " component does not exist in this site.");
					}
				}			
				
			});
			return instances;
		},
		
		component:null
		
	});

	
	


	var Page = Site.Page = ElementController.extend({
    	classPath:"Site",
    	className:"Page",
    	construct:function(container){
    	    if(!container) container = arguments[0] = "body";
			this._super.apply(this, arguments);
			
    	    var I = this;
			
			//I.setContainer("body");

    	},
    	test:function(){ return false; },
		_beforeInit:function(){},
		_init:function(){}
	});
	
	
	
	
	var Component = Site.Component = ElementController.extend({
    	classPath:"Site",
    	className:"Component",
    	construct:function(container){
    		this._super.apply(this, arguments);
    		
    		var I = this;
    	
    	},    	
		_init:function(evt){}
	});
	
	
	
})();
