/**
 * Load a global namespace static controller
 *
 * The Byng controller provides singleton object for reference
 * to the page components through a single handler
 * 
 * @author Ollie Maitland
 * @copyright Byng Systems LLP
 * 
 * @requires MooTools-1.2.1
 * @requires BaseCode-0.2.4
 */
var Byng = {

	/**
	 * Set the application to run
	 * 
	 * @param Object
	 */
	setApp 	: function ( fn ) { Byng.app = fn },
	
	/**
	 * Add a global FX instance by global handle
	 * 
	 * @param Object Fx
	 * @param String handle
	 */
	addFx  	: function ( fx, handle ) { Byng.fx[handle] = fx; },
	
	/**
	 * Get a global FX instance
	 * 
	 * @param String handle
	 */
	getFx  	: function ( handle ) { return Byng.fx[handle] },	
	
	/**
	 * Set a help controller
	 * 
	 * @param Object
	 */
	setHelp : function ( fn ) { Byng.help = fn },			
	
	/**
	 * Set the UI controller
	 * 
	 * @param Object
	 */
	setUi  	: function ( fn ) { Byng.ui = fn },	
	
	/**
	 * Set the input handler
	 * 
	 * @param Object
	 */
	setInput: function ( fn ) { Byng.input = fn },

	/**
	 * Set the transit handler
	 * 
	 * @param Object
	 */
	setTransit: function ( fn ) { Byng.transit = fn },
	
	/**
	 * Set the cache
	 * 
	 * @param Object
	 */
	 setCacher : function ( fn ) { Byng.cacher = fn },
	
	/**
	 * Holds the application object with data processing methods
	 *
	 * @see ByngApp
	 * @param ByngApp
	 */
	app 	:  	null,
	
	/**
	 * Holds the FX libraries
	 * 
	 * @param Object
	 */
	fx 		: 	{},
	
	/**
	 * Holds the help handler
	 * 
	 * @param ByngHelp
	 */
	help 	: 	null,
	
	/**
	 * Holds the UI handler
	 * 
	 * @param ByngUI
	 */
	ui 		: 	null,
	
	/**
	 * Holds the input handler
	 * 
	 * @param ByngInput
	 */
	input	:	null,
	
	/**
	 * Holds the transit handler
	 * 
	 * @param ByngTransit
	 */
	transit  : 	null,
	
	/**
	 * Holds the library store
	 * 
	 * @type Hash
	 */
	library : new Hash(),
	
	/**
	 * Holds the debug mode
	 * 
	 * @param Boolean 
	 */
	debug : false,
	
	/**
	 * Abstracted registration a library in private namespace
	 * 
	 * @param String ns
	 * @param Class fn
	 */
	register : function (ns, fn)
	{
		if (Byng.debug) Byng.log("Byng.register: "+ns);		
		if (typeOf(fn.Extends) == 'string') 	fn.Extends = Byng.source(fn.Extends, true);
		if (typeOf(fn.Implements) == 'string')	fn.Implements = Byng.source(fn.Implements, true);
		if (typeOf(fn.Implements) == 'array') {
			var tmp = [];
			// replace any Byng namespace strings with the function source
			fn.Implements.each(function(_fn){
				tmp.push ( (typeOf(_fn) == 'string' ? Byng.source(_fn, true) : _fn) );
			});
			fn.Implements = tmp;
		}
				
		Byng.library.set(ns, new Class(fn));
	},
	
	/**
	 * Initiate a library object from our private namespace
	 * 
	 * @param String ns
	 */
	init : function (ns, args0, args1, args2)
	{
		if ( Byng.library.has(ns) ) {
			if (Byng.debug) Byng.log("Byng.init: "+ns);
			var klass = Byng.library.get(ns);
			return new klass(args0, args1, args2);
		} else {
			throw "Unknown library path: {ns}".substitute({'ns' : ns});
		}
	},
	
	/**
	 * Get the class properties
	 * 
	 * @param String ns Namespace import
	 * @param Boolean silent Silently ignore missing
	 */
	source : function (ns, silent)
	{
		if (typeOf(ns) == 'array') {
			return ns.each(function(_ns) { _ns = Byng.source(_ns); });	
		} else {
			if (Byng.library.get(ns) == null && silent == null) {
				throw "Unable to load library: "+ns;
			}
			return Byng.library.get(ns);
		}
	},
	
	/**
	 * Log something
	 * 
	 * @param Mixed e
	 */
	log : function (e)
	{
		if (window.console) {
			if (['object'].contains(typeOf(e))) console.debug(e);
			else console.log(e);
		}else{
			return;
		}
	}
};

/**
 * Handles commom application events and provides
 * a base class for applications implementing ByngApp
 * 
 * ByngApp is responsible for:
 *
 *			- Client side application layer
 *			- Sending and receiving requests
 *			- Configuration of the UI
 *			- Error handling
 *			- Authentication handling
 *
 * @author Ollie Maitland
 * @copyright Byng Systems LLP
 */
var ByngApp = new Class(
{
	/**
	 * Use the Events construct to hold the stack of application events
	 * 
	 */
	Implements : Events,
	
	/**
	 * Return a reference to the HtmlGet handler
	 *
	 */ 
	htmlget : null,

	/**
	 * Return a reference to the HtmlGet handler
	 *
	 */ 
	xmlpost : null,

    /**
     * Holds a request context from ByngRequest
     * 
     */
    requestContext : null,

	/**
	 * Get the popup
	 *
	 * @return DomElement
	 */
	getPopup : function ()
	{
		return Byng.ui.dom.getInputPopup();
	},
	
	/**
	 * Handle an error in the application
	 *
	 * @param String
	 * @return Boolean
	 */
	error : function ( exception )
	{
		return false;
	},
	
	/**
	 * Start loader icon
	 * 
	 * @return DomElement
	 */
	startLoader : function ()
	{
		return Byng.ui.dom.showLoader( $('loading-icon'), Byng.app.icons.loader );
	},

    /**
     * Holds the request context sent from the backend
     * 
     * @param context
     */
    setRequestContext : function (context)
    {
        this.requestContext = context;
    }

});

/**
 * Represents a form input handler
 * 
 * @para Integer
 */
var BYNG_INPUT_FORM   = 1;

/**
 * Represents a Find-as-you-type input handler
 * 
 * @param Integer
 */
var BYNG_INPUT_FAYT = 2;

/**
 * Represents a criteria input handler
 * 
 * @type Integer
 */
var BYNG_INPUT_CRITERIA = 3;

/**
 * Handles input mechanims into the application
 *
 * ByngInput is responsible for:
 *
 *			- Raising prompts, inputs and models
 *			- Create and handling forms and events
 *			- Sanitising inputs and outputs
 *			- Clientside validation of inputs
 *
 * @author Ollie Maitland
 * @copyright Byng Systems LLP
 */
Byng.register('byng.input',
{
	/**
	 * Holds a stack of search handlers
	 * 
	 * @type Array
	 */
	inputHandlers : [],
	
	/**
	 * Holds an instance of the FormBuilder
	 * 
	 * @type FormBuilder
	 */
	builder : null,
	
	/**
	 * Holds a stack of FormBuilders for the page
	 * 
	 * @type Hash
	 */
	stack : null,
	
	/**
	 * Get a form builder instance
	 * 
	 * @return FormBuilder
	 */
	getBuilder : function ()
	{
		if (!this.builder) {
			this.builder = Byng.init("byng.forms.builder");	
		}
		return this.builder;
	},

	/**
	 * Register a form to this application
	 *
	 * @param String formName
	 * @return FormBuilder
	 */
	addForm : function (formName, options)
	{
		if (!this.stack) this.stack = $H();		
		var form = this.stack.get(formName.replace('.','_'));		
		if (form) {
			return form;
		} else {
			
			var form = this.getBuilder().clone()
							.setOptions(options)			
							.setFormName(formName);
			this.stack.set(formName, form);
			return form;
		}
	},
	
	/**
	 * Get a form from the stack
	 * 
	 * @param String formName
	 * @return FormBuilder
	 */
	getForm : function (formName)
	{
		if (!this.stack) this.stack = $H();
		var form = this.stack.get(formName.replace('.','_'));
		if ( form ) return form;
		else return Byng.input.addForm(formName.replace('.','_'),true);
	},

	/**
	 * Raise a confirm bos
	 *
	 * @param String
	 */
	confirm : function ( s )
	{
		return confirm(s);
	},

	/**
	 * Raise a prompt box
	 *
	 * @param String
	 */
	prompt : function ( q, ans )
	{
		if (!ans) ans = '';
		return prompt(q, ans);
	},
	
	/**
	 * New Find-as-you-type element
	 * 
	 * @param DomElement
	 * @param String gateway
	 * @param String redirect
	 * @return Search
	 */
	fayt : function ( element, gateway, redirect, options )
	{
		options = options || {};
		options = Object.merge(options, {'input' : element, 'gateway' : gateway, 'redirect' : redirect});
		return Byng.init('byng.ajax.search', options);
	},
	
	/**
	 * Add an input handler to the stack
	 * 
	 * @param Object handler
	 * @return Object
	 */
	addInputHandler : function ( handler, element, type )
	{
		if (!this.inputHandlers[type]) {
			this.inputHandlers[type] = new Hash();
		}
		
		this.inputHandlers[type].set(element.getProperty('accesskey'), handler);
		return handler;
	},
	
	/**
	 * Get an input handler
	 * 
	 * @param Elememt element
	 * @param Integer type
	 * @return Object
	 */
	getInputHandler : function ( element, type )
	{
		if (!type) {
			if (typeOf(element) != 'element') type = BYNG_INPUT_FAYT;
			else if (element.hasClass('fayt')) type = BYNG_INPUT_FAYT;
			else if (element.hasClass('criteria')) type = BYNG_INPUT_CRITERIA; 
		}

		if (!this.inputHandlers[type]) {
			return function(){};
		} else {
			var handle = (typeOf(element) == 'element' ? element.getProperty('accesskey') : element);
			return this.inputHandlers[type].get(handle);
		}
	},
	
	/**
	 * Set the focus for an input field
	 * 
	 * @param DomElement
	 */
	onSearchFocus : function ( input )
	{
		// clear the text
		input.value = '';
		/**
		 * @todo Set the active search request here
		 */
	},
	
	/**
	 * Create a new modal popup
	 * 
	 * @param Options options
	 */
	popup 	: function ( options, params )
	{
		// if the request object is passed directly then push into an object
		if (!options.request) options = {'request' : options};
		
		// missing required options, use defaults
		if (!options.onSuccess) 	options.onSuccess = Byng.ui.dom.showPopup;
		if (!options.target) 	 	options.target = Byng.app.getPopup();
		
		// add the popup events to the popup dom element
		if (options.onPopup)		options.target.addEvent('popup', options.onPopup)
		if (options.onCancel)		options.target.addEvent('cancel', options.onCancel);
		if (options.onSubmit)		options.target.addEvent('submit', options.onSubmit);
		
		// register the popup
		Byng.ui.dom.registerPopup ( options.target );		
		
		// retreive the HTML for the popup
		return Byng.transit.html(options, params);	
	}	

});

/**
 * Encapsulates a request to the BaseCode
 *
 * @param String screen
 * @param String module
 * @param String package
 * @param String loader
 */
var ByngRequest  = new Class(
{
	/**
	 * Of type Byng request
	 * 
	 */	
	type : 'byngrequest',

    /**
     * A chunk of string to put before requests
     *
     * @type String
     */
    urlPrefix : null,
		
	/**
	 * Create a new ByngRequest
	 * 
	 * @param String screen
	 * @param String module
	 * @param String packageName
	 * @param String loader
	 * @return ByngRequest
	 */
	initialize : function (screen, module, packageName, loader) 
	{
		
		if (!screen && !module) {		
			// If no screen is set then use the current URI
			var s = location.pathname.substr(1).split ('/');
			this.loader      = s[0];
			this.packageName = s[1];
			this.module      = s[2];
			this.screen      = s[3];
		} else {
		
			if (!loader) {
				// Reverse engineer the loader name
				s = location.pathname.split ('/'+module);
				this.loader = s[0];
			} else {
				this.loader = loader;
			}

			/**
			 * @type String
			 */
			this.screen = screen;
			
			/**
			 * @type String
			 */
			this.module = module;
			
			/**
			 * @type String
			 */
			this.packageName = packageName;
					
		}

		/**
		 * @type Hash
		 */
		this.vars = {};
		
		this.uri = "";
	},
	
	/**
	 * @param String a
	 */
	setAction : function (a)
	{
		this.action = a;
		return this;
	},
	
	/**
	 * @type String
	 */
	getAction : function ( encoded )
	{
		if (encoded == true) {
			var action = Byng.ui.makeIdTag(['module', 'package', 'action'],[this.module, this.packageName, this.action], '_');
			return action;
		} else {
			return this.action;
		}
	},
	
	/**
	 * Set the screen
	 * 
	 * @return String
	 */
	setScreen : function (screen)	
	{
		this.screen = screen;
		return this;		
	},

	/**
	 * Return the screen
	 * 
	 * @return String
	 */
	getScreen : function ()	
	{
		return this.screen;
	},
	
	/**
	 * Get the module name
	 * 
	 * @return String
	 */
	getModule : function ()
	{
		return this.module;
	},
	
	/**
	 * Set the module string
	 * 
	 * @param String
	 */
	setModule : function (module)
	{
		this.module = module;
		return this;
	},
	
	/**
	 * Get the loader
	 * 
	 * @return String
	 */
	getLoader : function ()
	{
		return this.loader;
	},
	
	/**
	 * Set the loader string
	 * 
	 * @param String
	 */
	setLoader : function (loader)
	{
		this.loader = loader;
		return this;
	},
	
	/**
	 * Get the package name
	 * 
	 * @return String
	 */
	getPackage : function ()
	{
		return this.packageName;
	},
	
	/**
	 * Set the package parameter
	 * 
	 * @param {Object} packageName
	 */
	setPackage : function (packageName)
	{
		this.packageName = packageName;
		return this;
	},

    /**
     * Set the URL prefix
     * 
     * @param array
     */
    setUrlPrefix : function (array)
    {
        this.urlPrefix = array.join('/');
        return this;
    },
	
	/**
	 * Add a parameter to the request
	 * 
	 * @param Mixed key
	 * @param String value
	 */
	addParam : function (key, value)
	{
		if (typeOf(key) == 'string') { 
			this.vars[key] = value;
		} else if (typeOf(key) == 'element') {
			// pick the parameters from the element id attribute
			new Hash(Byng.ui.fromIdTag(key.getProperty('id'))).each(function(value,key){
				this.vars[key] = value;
			}.bind(this));
		} else {
			throw "Invalid parameter supplied";
		}
		return this;		
	},
	
	/**
	 * Return a string of the GET variables
	 * 
	 * @return String
	 */
	gets : function ()
	{
		var vars = new Hash(this.vars);
		if (vars.getLength == null || vars.getLength() < 1) return '';
		
		var s = "?";
		
		vars.each(function(v,k) {	
			if (typeOf(v) == 'object' || typeOf(v) == 'array') v = new Hash(v);
			if (typeOf(v) == 'hash') {
				// recurse a level
				var _k = k;
				var recurse = function(v,k2) {
					if ( ['hash','array'].contains(typeOf(v)) ) {
						_k = _k + '['+k2+']';
						$each(v, recurse);
						_k = k;
					} else {
						s += _k+'['+k2+']='+v+'&';
					}
				};
				$each(v, recurse);
			} else {
				s += k + '=' + v  + '&';
			}
		});
		
		return s;
	},
	
	/**
	 * Return the ByngRequest as a string
	 * 
	 * @param String noQuery
	 * @return String
	 */	
	composeAsString : function (noQuery)
	{
		var uri = (this.urlPrefix ? "/" + this.urlPrefix : "");
		if (this.uri == "") {
			uri += "/" + new Array (this.getLoader(),this.getPackage(),this.getModule(),this.getScreen()).join("/");
		} else {
			if ( this.uri.indexOf("?") > 0 ) {
				uri = this.uri.substring(0, this.uri.indexOf("?"));
			} else {
				uri = this.uri;
			}
		}
		
		// condition: strip double slashes
		if (this.uri.substring(0,4) != "http") {
			uri = uri.replace("//","/");
		}
		
		if (noQuery == true) return uri;
		
		uri += this.gets();

		return uri;
	},
	
	/**
	 * Utility method to convert request to String
	 * 
	 * @todo Fix in IE (protected method)
	 * 
	 * @return String
	 */
	toString : function (noQuery)
	{
		return this.composeAsString(noQuery);
	},
	
	/**
	 * Compose variables from a string
	 * 
	 * @param String string
	 * @param Object exclude
	 */
	composeFromString : function ( string, exclude )
	{	
		var skip = false;
		string
		.split('&')
		.each(function(pair) {
			var pair = pair.split('=');
			if (!pair[0]) return;
			if (exclude) {
				exclude = new Hash(exclude);
				exclude.each(function(value,key) {
					// parameter exists and has a value
					if (key == pair[0] && value) skip = true;
				});
			}
			if (!skip) {
				// condition: matches key[]
				if(pair[0].test('([a-z*])\\[([a-z0-9*])\\]', 'i')) {
					// split out into a multi-dimensional array
					var pieces = pair[0].split('[');
					var value = pair[1];
					var key = pieces.shift();
					if (!this.vars[key]) {
						// add the new param
						this.addParam(key, new Hash());
					}
					var stored = this.vars[key];
					var k = 0;
					var i = pieces.length;
					pieces.each(function(_key) {
						i--; k = new String( _key.substring(0,(_key.length-1)) );
						if ( stored.has(k) ) {
							stored = stored.get(k);
							// move to the next one
							return;
						} else if (i > 0) {
							stored.set(k, new Hash());
							stored = stored.get(k);
						} else {
							stored.set(k, value);
						}
					});
				} else {
					// otherwise just using a key/value pair
					this.addParam(pair[0],pair[1]);
				}
			}
		}.bind(this));
		return this.vars;
	},
	
	
	/**
	 * Set the URI manually
	 * 
	 * @param String uri
	 * @return ByngRequest
	 */
	setUri : function ( uri )
	{
		uri = uri.toString();
		var q = uri.indexOf ('?');
		if (q > 0) {
			query = window.location.search.substring(1);
			/*
			vars = query.split("&");
			for (var i=0;i<vars.length;i++) {
				var pair = vars[i].split("=");
				if (!isUndefined(pair[1])) this.addParam (pair[0],pair[1]);
			}
			uri = uri.substring(0,q);*/
			this.composeFromString(query);
		}

		this.uri = uri;
		return this;		
	},
	
	/**
	 * Pass the ByngRequest to the hash
	 * 
	 * @param Object params Supplementary parameters for hash
	 * @return String
	 */
	toHash : function ( params )
	{
		if (!params) params = {};
		
		params = new Hash(params);
		
		// condition: parameters in the request object 
		if (this.vars.length > 0) {
			// merge in request parameters
			this.vars.each(function(v,k){
				if (!this[k]) this[k] = v;
			}.bind(params));
		}
		
		var s = [];
		// form the key-value pairs
		params.each(function(v,k){
			if (typeOf(v) == 'object') {
				v = new Hash(v);
				// recurse a level
				v.each(function(_v,_k) {
					_k = k + '['+_k+']';
					this.push(_k+'='+_v);
				}.bind(this));
			} else {
				this.push(k+'='+v);
			}
		}.bind(s));
		s = s.join('&');
			
		// condition: we have something to store
		if (s.length > 0) {			
			window.location.hash = s;
			return s;
		} else {
			return null;
		}
	},
	
	/**
	 * Assign variables from the #hash
	 * 
	 * @param exclude Exclude parameters from hash
	 * @return Hash
	 */
	fromHash : function ( exclude )
	{		
		return this.composeFromString(window.location.hash.substring(1), exclude);
	},
	
	/**
	 * Returns whether there is a hash for this ByngRequest
	 * 
	 * @return Boolean
	 */
	hasHash : function ()
	{
		return (window.location.hash.length > 0);
	},
	
	/**
	 * Clone this request
	 * 
	 * @return 
	 */
	clone : function (screen)
	{
		var request = new ByngRequest()
						.setScreen((screen ? screen : this.getScreen())).setModule(this.getModule())
						.setPackage(this.getPackage()).setLoader(this.getLoader())
		request.vars = this.vars;
		return request;
	}
			
});

/**
 * Byng interface class
 * 
 * Abstract class for implementing methods used by view/interface handlers
 * 
 * @author Ollie Maitland
 * @copyright Byng Systems LLP
 */
var ByngView = new Class(
{
	/**
	 * Normalise an argument
	 * 
	 * @param Mixed arg
	 * @param String property
	 */
	normalise : function ( arg, property )
	{
		if (arg == null) {
			return null;
		} else if (['string','number'].contains(typeOf(arg))) {
			return arg;
		} else {
			if (typeOf(arg) == 'event') {
				arg.stop();
				arg = arg.target;
			}
			if (typeOf(arg) == 'element') {
				arg = $(arg);
				var id = (arg.getProperty('id') ? Byng.ui.fromIdTag(arg) : null);
				if (id != null) {
					return id[property];	
				} else {
					return (arg.get('tag') != 'a' ? arg.getParent("a") : arg).getAttribute('accesskey');
				}
			}
		}
	},
	
	/**
	 * Get the ByngRequest for this view
	 * 
	 * @param String
	 * @returns ByngRequest
	 */
	getRequest : function (screen)
	{
		// condition: we have the getProperty method
		if (this.getProperty) {
			new Hash({
				// retrieve all the properties from the script URI
				'loader'	: this.getProperty('loader'),
				'package'	: this.getProperty('package'),
				'module'	: this.getProperty('module')
			}, function (v,k) {
				if (v) {
					// override the meta data if contained within the script
					this.meta.gateway[k] = v;
				}
			}.bind(this));
		}
		
		// form the request object from the class meta parameters
		var request = new ByngRequest(	(screen ? screen : (this.meta.gateway['screen'] ? this.meta.gateway['screen'] : 'default')),
					    			(this.meta.gateway['module'] 	? this.meta.gateway['module'] 	: null),
						    		(this.meta.gateway['package'] 	? this.meta.gateway['package'] 	: 'site'),
							    	(this.meta.gateway['loader']  	? this.meta.gateway['loader']  	: 'ajax'));

        // set the base request from the request object
        if (Byng.app.requestContext) {
            if (typeOf(Byng.app.requestContext.urlPrefix) == 'string') {
                request.setUrlPrefix(Byng.app.requestContext.urlPrefix.split("/"));
            }
        }

        return request;
	}
});

/**
 * Model class
 * 
 * @author Ollie Maitland
 * @copyright Byng Systems LLP
 */
var ByngEngine = new Class(
{
	

});

