/*	

hg_core.js v1.0

mercury (Hg) core class
part of the mercury (Hg) client-side script library
more information/downloads available at: http://mercury.cashmusic.org

revisions:
• 1.0: initial release

requires:
• mootools v 1.2

distributed under the MIT license, terms:
copyright (c) 2008 CASH Music and dutchmoney

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

*/

var HgDebug = new Class({
	/*
	Class HgDebug
	
	Adds additional debug functionality to existing classes.
		
	*/
	debug: 0,
	hgCore: false,
	
	setHgCore: function(hgObj) {
		/*
		Function setHgCore(object hgObj)
		
		Sets a pointer to the main Hg Core object
		
		*/
		this.hgCore = hgObj;
		this.debug = this.hgCore.debug;
	},
	
	debugMsg: function(type,msg) {
		if (this.debug) {
			var moduleName = '',
				msgTypes;
			type = type + 0;
			if (this.hgCore && this.name) {moduleName = '[' + this.name + '] ';}
			msgTypes = ['Hg Error: ','Hg Warning: ','Hg Notice: '];
			console.log(msgTypes[type] + moduleName + msg);
		}
	},
	
	debugLoadMsg: function() {
		if (this.debug) {
			var logStr = 'Hg Module Loaded: ' + this.name + ' (v' + this.version + ')',
				allOptions = this.listAllOptions();
			if (allOptions) {logStr += '\nOptions: ' + allOptions;}
			console.log(logStr);
		}
	},
	
	listAllOptions: function() {
		/*
		Function listAllOptions()
		
		Lists all options passed to a Moo class as a plain text string, returns false 
		in the absence of any options.
		
		*/
		if (!this.donotdebugoptions) {
			var optionString = '',
				leadingComma = '',
				addQuote = '';
			$H(this.options).each(function(optionValue,optionName){
				if (typeof(optionValue) == 'string') {addQuote = '\'';}
				optionString += leadingComma + optionName + ' = ' + addQuote + optionValue + addQuote;
				leadingComma = ', ';
				addQuote = '';
			});
			if (optionString) {
				return optionString;
			} else {
				return false;
			}
		} else {
			return false;
		}
	}
});

var HgCore = new Class({
	/*
	Class HgCore
	
	The Hg Core is a dynamic application manager capable of loading, managing, and
	interacting with multiple objects across multiple external javascript files —
	even cross domain. Each object, or module, can be defined on-the-fly or as part
	of the core library, with dependencies and CSS selector conditions to trigger
	an automatic launch. Modules can also be loaded by request, with every load
	firing a moduleLoad event when successfully initiated. Those events, combined 
	with a primary bootComplete event fired at the completion of all auto-loading 
	modules, provide event-based application management that ensures an object will 
	only be requested after it is fully ready.
	

	OPTIONS:
	• debug (0)
	  if true, debug information will be sent to the console
	  
	• autoBoot (0)
	  if true, Hg will scan the page for specified selectors and auto-launch
	  appropriate modules
	  
	• timeout (16)
	  the number of seconds a module is permitted to load before it is declared
	  not loade (by error)

	EVENTS:
	• moduleLoad (string moduleName)
	  fires when a module is loaded, returning the name of the module
	  
	• bootComplete (bool)
	  fires after bootstrap() has been called and all modules are loaded or 
	  have timed out. returns true if all modules have been loaded, false if
	  fired on error/timeout
	  
	• htmlChanged (mixed (string elementName) OR (element el))
	  fires when an element's html property has been changed. relies on module
	  to callback, so not absolute.
	  
	KEY METHODS:
	• storeModule(string filePath,
				  string moduleName,
				  mixed (string dependencies) OR (false), 
				  string autoLaunchBySelectors, 
				  bool attachToLaunchSelectors,
				  bool relativePath)
	  stores module for loading/retrieving by name. dependencies should be set 
	  either to false, or a comma separated list of dependent module names. 
	  autoLaunchBySelectors should be a comma separated list of CSS selectors 
	  whose appearance in the DOM trigger the module. if attachToLaunchSelectors
	  is true, the module should call its attachToElement() method for each
	  selector. relativePath specifies if the filePath is relative to the current 
	  HgCore.js file, or if it is fully qualified. 
	  
	• loadModule(string moduleName)
	  loads a module by name
	  
	• getModule(string moduleName)
	  attempts to get a module by name, returning either a pointer object for the
	  initiated module, or null if the module has not been loaded
	  
	• getModuleOptions(string moduleName)
	  returns a module's options property
	  
	• setModuleOptions(string moduleName, object optionsObj)
	  sets a simple options object to pass to the initialization of a new module
	  object, allowing a module to be called with the specified options
	  
	• clearAutoLoad(string moduleName)
	  clears a module's autoLaunchBySelectors property
	  
	• addToAutoLoad(string moduleName, string selectorString)
	  accepts a comma separated list of CSS selectors to add to a module's
	  autoLaunchBySelectors property
	  
	• bootstrap()
	  auto-launches all appropriate modules for the current page, firing the
	  bootComplete event when finished (successfully or not)
	
	*/
	Implements: [Options, Events, HgDebug],
	
	name: 'Hg (mercury) Javascript Library',
	version: 1.0,
	
	options: {
		debug: 0,
		autoBoot: 0,
		timeout: 16
	},
	
	initialize: function(options){
		this.setOptions(options);
		this.modules = $H();
		this.commonCache = $H(); // common memory space for all Hg modules
		this.injectedFiles = [];
		this.debug = 0;
		this.timeout = this.options.timeout * 1000;
		this.documenthead = $$('head')[0];
		// determine file location as hg library path
		if ($('hg_core')) {this.libpath = $('hg_core').getProperty('src').replace('hg_core.js','');}
		this.defineLibrary();
		if (this.options.autoBoot) {this.bootstrap();}
		
		if (this.options.debug && typeof(console) != 'undefined') {
			this.debug = 1;
			var allOptions = this.listAllOptions();
			if (this.libpath) {
				console.log('Hg Library (v' + this.version + ') loaded.\nPath: \'' + this.libpath + '\'' + '\nMooTools version: ' + MooTools.version + '\nOptions: ' + allOptions);
			} else {
				console.log('Hg Library loaded with errors. Version: ' + this.hgVersion + ' Path unknown. Please add id="hg_core" to script declaration' + '\nOptions: ' + allOptions);
			}
		}
	},

	injectScript: function(scripturl,asHg) {
		/*
		Function injectScript(string scripturl, bool asHg)
		
		Injects a <script> element into the DOM head. If asHg is true then scripturl will
		be treated as relative to this.libpath
		
		*/
		var injected;
		if (asHg) {scripturl = this.libpath + scripturl;}
		if (this.injectedFiles.indexOf(scripturl) == -1) {
			injected = new Element('script', {
				'type': 'text/javascript',
				'src': scripturl
			}).injectInside(this.documenthead);
			this.injectedFiles.push(scripturl);
		}
	},
	
	htmlContentChanged: function(inElement) {
		/*
		Function htmlContentChanged(element or string inElement)
	
		Treats elements that have new content via javascript as if they had just been 
		loaded in the DOM — auto-attaching Hg modules, etc. 
	
		*/
		this.allModulesAutoAttach(inElement);
		this.fireEvent ('htmlChanged',inElement);
	},
	
	storeModule: function(filePath,moduleName,dependencies,autoLaunchBySelectors,attachToSelectors,relativePath) {
		/*
		Function storeModule(string filePath,string moduleName,
			mixed (string dependencies) OR (false), string autoLaunch, bool attachToSelectors,
			bool relativePath)
	
		Store information about Hg modules in this.modules, allowing for loading, auto-
		loading of dependencies, and auto-attaching to elements by selector.
		
		Dependencies should be comma separated, and dependent scripts *must* include 
		a moduleCallback() call
		
		Be careful not to loop dependencies.
		
		Paths should be relative to the libpath.
		
		attachToSelectors presumes the module refers to a returnable object containing an 
		addToElement(element) function that can accept the results of $$(autoLaunchBySelectors)
		
		*/
		var moduleObj = {
		   path: filePath,
		   dependencies: dependencies,
		   autoLaunch: autoLaunchBySelectors,
		   attach: attachToSelectors,
		   relativePath: relativePath,
		   pointer: null,
		   options: null
		};
		this.modules.set(moduleName, moduleObj);
	},
	
	loadModule: function(moduleName) {
		/*
		Function loadModule(string moduleName)
		
		Checks/inserts dependencies, waits for callbacks (if necessary), then inserts the
		requested module .
					
		*/
		var theModule = this.modules.get(moduleName),
			allDependencies;
		if (theModule) {
			if (theModule.pointer === null) {
				if (theModule.dependencies) {
					allDependencies = $A(theModule.dependencies.split(','));
					allDependencies.each(function(argument){
						if (this.modules.get(argument).pointer === null) {this.loadModule(argument);}
					}.bind(this));
					this.loadAfterDependencies(moduleName,allDependencies,0);
				} else {
					this.injectScript(theModule.path,theModule.relativePath);
				}
			} else {
				this.debugMsg(2,'requested module (\'' + moduleName + '\') already loaded');
				return false;
			}
		} else {
			this.debugMsg(0,'requested module (\'' + moduleName + '\') is not defined, cannot load');
			return false;
		}
	},
	
	loadAfterDependencies: function(moduleName,dependencies,iteration) {
		/*
		Function loadAfterDependencies(string moduleName, array dependencies, integer iteration)
		
		Checks status of dependencies, respawns if necessary, launches module
		once they are all loaded.
					
		*/
		var totalDependencies = dependencies.length,
			totalLoaded = 0,
			newiteration,
			newargs,
			theModule;
		dependencies.each(function(argument){
			if (this.modules.get(argument).pointer) {totalLoaded++;}
		}.bind(this));
		if (totalLoaded < totalDependencies) {
			if (iteration < this.timeout) {
				newiteration = iteration+100;
				newargs = [moduleName,dependencies,newiteration];
				this.loadAfterDependencies.delay(100,this,newargs);
			} else {
				this.debugMsg(0,'requested module (\'' + moduleName + 
					'\') could not load, dependency loading exceeded timeout');
			}
		} else {
			theModule = this.modules.get(moduleName);
			this.injectScript(theModule.path,theModule.relativePath);
		}
	},
	
	getModule: function(moduleName) {
		/*
		Function getModule(string moduleName)
		
		Wrapper function returns object pointer or null if not set.
					
		*/
		var objPointer = this.modules.get(moduleName);
		if (objPointer) {
			return objPointer.pointer;
		} else {
			return null;
		}
	},
	
	getModuleOptions: function(moduleName) {
		/*
		Function getModuleOptions(string moduleName)
		
		Wrapper function returns options object or null if not set.
					
		*/
		var objPointer = this.modules.get(moduleName);
		if (objPointer) {
			return objPointer.options;
		} else {
			return null;
		}
	},
	
	setModuleOptions: function(moduleName,optionsObj) {
		/*
		Function setModuleOptions(string moduleName, object optionsObj)
	
		Simple function copies current module object and returns it with specified options.
					
		*/
		var moduleObj = this.modules.get(moduleName);
		if (moduleObj) {moduleObj.options = optionsObj;}
		this.modules.set(moduleName,moduleObj);
	},
	
	setModulePointer: function(moduleName,objPointer) {
		/*
		Function setModulePointer(string moduleName, object objPointer)
	
		Simple function copies current module object and returns it with specified pointer.
					
		*/
		var moduleObj = this.modules.get(moduleName);
		if (moduleObj) {
			moduleObj.pointer = objPointer;
		}
		this.modules.set(moduleName,moduleObj);
	},
	
	moduleCallback: function(theModule) {
		/*
		Function moduleCallback(object or string theModule)
		
		Call-back function for Hg modules keeps main object aware of status and allows for 
		easier direct calls to each object where necessary
		
		Call-back expects an object with defined 'name' and 'version' properties, ideally 
		with hg_prototypes implemented. 
		
		*/
		if (typeof(theModule) == 'object') {
			if (this.modules.has(theModule.name)) {
				this.setModulePointer(theModule.name,theModule);
			} else {
				this.storeModule(0,theModule.name,0);
				this.setModulePointer(theModule.name,theModule);
			}
			this.fireEvent ('moduleLoad',theModule.name);
			theModule.debugLoadMsg();
			this.moduleAutoAttach(theModule.name);
		} else {
			this.debugMsg(0,'moduleCallback() must be provided with an object');
		}
	},
	
	moduleAutoAttach: function (moduleName,inElement) {
		/*
		Function moduleAutoAttach(string moduleName[, element or string inElement])
		
		Calls module.attachToElement() for a module's auto-attach elements. If 
		inElement is specified then only that element will be scanned for 
		auto-attach elements
		
		*/
		var moduleObj = this.modules.get(moduleName),
			allSelectors,
			allElementMatches;
		if (moduleObj.autoLaunch && moduleObj.attach && typeof(moduleObj.pointer.attachToElement) == 'function') {
			allSelectors = $A(moduleObj.autoLaunch.split(','));
			allElementMatches = $$(allSelectors);
			if (allElementMatches.length > 0) {
				if (!inElement) {
					$$(allSelectors).each(function(element){
						moduleObj.pointer.attachToElement(element);
					});
				} else {
					allSelectors.each(function(selector){
						$(inElement).getElements(selector).each(function(element){
							moduleObj.pointer.attachToElement(element);
						});
					});
				}
			}
		}
	},
	
	allModulesAutoAttach: function (inElement) {
		/*
		Function allModulesAutoAttach([element or string inElement])
		
		Calls allModulesAutoAttach() for all loaded modules.
		
		*/
		this.modules.each(function(moduleObj,moduleName){
			if (moduleObj.autoLaunch && moduleObj.pointer) {
				if (!inElement) {
					this.moduleAutoAttach(moduleName);
				} else {
					this.moduleAutoAttach(moduleName,inElement);
				}
			}
		}.bind(this));
	},
	
	registerModule: function(ClassType,className) {
		/*
		Function registerModule(class classType, string className)
		
		Called from the module's script file, this function provides a single-line method
		of implementing Hg classes into the module class, checking for and setting options,
		and initiating the module call-back script.
		
		*/
		ClassType.implement(new HgDebug());
		var optionsCheck = this.getModuleOptions(className),
			moduleObject;
		if (optionsCheck) {
			moduleObject = new ClassType(optionsCheck);
		} else {
			moduleObject = new ClassType();
		}
		moduleObject.setHgCore(this);
		this.moduleCallback(moduleObject);	
	},
	
	bootstrap: function() {
		/*
		Function bootstrap()
		
		Searches through all defined modules for auto-boot selectors. If elements
		matching those CSS selectors are found, the module is loaded.
		
		*/
		var bootingModules = [],
			allSelectors;
		this.modules.each(function(moduleObj,moduleName){
			if (moduleObj.autoLaunch && !moduleObj.pointer) {
				allSelectors = $A(moduleObj.autoLaunch.split(','));
				if ($$(allSelectors).length > 0) {
					this.loadModule(moduleName);
					bootingModules.push(moduleName);
				}
			}
		}.bind(this));
		if (bootingModules.length > 0) {
			this.debugMsg(2,'boot started, attempting to load necessary modules (' +
				bootingModules.join(', ') + ')');
		}
		this.checkBootStatus(bootingModules,0);
	},
	
	checkBootStatus: function(bootModules, iteration) {
		/*
		Function bootstrap(array bootModules, integer iteration)
		
		Takes an array of all required boot modules (based on auto-load selectors)
		and checks their status. Calls in a loop and fires the bootComplete event
		on successful load completion (first true) or on timeout (+2 seconds, fires false.) 
		
		*/
		if (bootModules.length == 0) {
			this.fireEvent ('bootComplete',true);
		} else {
			var totalModules = bootModules.length,
				totalLoaded = 0,
				newiteration,
				newargs;
			bootModules.each(function(argument){
				if (this.modules.get(argument).pointer) {totalLoaded++;}
			}.bind(this));
			if (totalLoaded < totalModules) {
				if (iteration < (this.timeout + 2000)) {
					newiteration = iteration+100;
					newargs = [bootModules,newiteration];
					this.checkBootStatus.delay(100,this,newargs);
				} else {
					this.fireEvent ('bootComplete',false);
					this.debugMsg(1,'boot failure, could not load all modules');
				}
			} else {
				this.fireEvent ('bootComplete',true);
				this.debugMsg(2,'boot successfully completed');
			}
		}
	},
	
	defineLibrary: function() {
		/*
		Function defineLibrary()
		
		Utility function that registers all default Library module information.
		
		Multiple instances of one path are allowed, to define multiple modules
		in one file — the file will only be injected once, with multiple call-
		backs made and all module pointers set.
		
		For reference: this.storeModule(string filePath,
										string moduleName,
										mixed (string dependencies) OR (false), 
										string autoLaunchBySelectors, 
										bool attachToLaunchSelectors,
										bool relativePath)
		
		*/		
		this.storeModule('enhancements/hg_anchor.js','linkexternal',0,'a.external',1,1);
		this.storeModule('enhancements/hg_anchor.js','linkpopup',0,'a.popup',1,1);
		this.storeModule('enhancements/hg_anchor.js','linkinside',0,'a.hg_linkinside',1,1);
		this.storeModule('enhancements/hg_anchor.js','drawer',0,'a.hg_drawertoggle',1,1);
		this.storeModule('utility/hg_utility.js','utility',0,0,0,1);
		this.storeModule('media/hg_overlay.js','overlay',0,0,0,1);
		this.storeModule('media/hg_imagebox.js','imagebox','utility,overlay','a.hg_imagebox,div.hg_imagebox',1,1);
		this.storeModule('media/hg_moviebox.js','moviebox','utility,overlay','a[href$=.mov],a[href$=.mp4],a[href$=.MOV],a[href$=.MP4],a[href^=http://www.youtube.com/watch?v],a[href^=http://youtube.com/watch?v],a[href^=http://vimeo.com/],a[href^=http://www.vimeo.com/],a[href^=http://video.google.com/videoplay?docid],a[href^=http://myspacetv.com/index.cfm?fuseaction=vids.individual&videoid],a[href^=http://vids.myspace.com/index.cfm?fuseaction=vids.individual&videoid]',1,1);
	},
	
	clearAutoLoad: function(moduleName) {
		/*
		Function clearAutoLoad(string moduleName)
		
		Removes all autoLoad selectors from a module. Must be called before 
		bootstrap().
		
		*/
		var moduleObj = this.modules.get(moduleName);
		moduleObj.autoLaunch = 0;
	},
	
	addToAutoLoad: function(moduleName,selectorString) {
		/*
		Function addToAutoLoad(string moduleName, string selectorString)
		
		Adds selectorString to the autoLoad selectors of a module. Accepts a
		comma separated string of CSS selectors must be called before bootstrap().
		
		*/
		var moduleObj = this.modules.get(moduleName);
		if (moduleObj.autoLaunch) {
			moduleObj.autoLaunch += (',' + selectorString);
		} else {
			moduleObj.autoLaunch = selectorString;
		}
	}
});