/**
 * jQuery ajax extend with bruteCache (tm) functionality
 */
(function(){
	var _ajax = jQuery.ajax;
	var _cache = [];
	
	jQuery.extend({
 		ajax: function(options){
			// cache is enabled by default:
			jQuery.extend(options, {
 				cache: options.cache !== false,
				bruteCache: options.bruteCache !== false
			});

			
			if (options && options.url && typeof pageTracker !== 'undefined') {
				pageTracker._trackPageview(options.url); // Google Analytics
			}
			if (options.bruteCache) {
				var key = '' + options.url + options.data + options.type;
				if (_cache[key]) {
					if (options.success) {
						options.success.apply(this, _cache[key]);
					}
					if (options.complete) {
						options.complete.apply(this, _cache[key]);
					}
					// do not make any calls
					return;
				} else {
					if (options.success) {
						var _success = options.success;
						options.success = function(){
							_cache[key] = arguments;
							_success.apply(this, arguments);
						}
					}
					if (options.complete) {
						var _complete = options.complete;
						options.complete = function(){
							_cache[key] = arguments;
							_complete.apply(this, arguments);
						}
					}
				}
			}
			_ajax(options);
		}
	});
})();

/**
 * Garp global namespace. For Garp functionality
 */
var Garp = typeof Garp !='object' ? {} : Garp;

/**
 * Garp BBInstance 
 */

Garp.BrowseboxInstance = {};

/**
 * @class Browsebox
 * @author Peet & Daaf - Grrr
 * 
 * Usage: var myBrowsebox = new Browsebox(config);
 * 
 * @param {Object} config
 * @param {config} url
 * @param {config} id
 * @param {config} speed
 * @param {config} autoCycle
 * @param {config} cycle
 * @param {config} lastPage
 * @param {config} page
 * @param {config} etc...
 */

Garp.Browsebox = function(cfg){

	var bb = {

		init: function(cfg){
			this.cfg = cfg;
			this.setCfg(cfg);
						
			var initialHtml = document.getElementById(cfg.id+'_content').innerHTML, //this.container.html(), 
				loaderBg = $('<div class="'+this.loaderBgClass+'"></div>'),
				loader = $('<div class="loader" style="z-index:11;"></div>'),
				container = this.container;

			container.html('');
			container.css('position', 'relative');

			for (var i = 0; i < 2; i++) {
				if (this.fade) {
					this.layer[i] = $('<div id="BB-' + this.elID + '-' + i + '" style="position:absolute;z-index:10;top:0;left:0;opacity:' + (1 - i) + ';" />').appendTo(container);
				} else {
					this.layer[i] = $('<div id="BB-' + this.elID + '-' + i + '" style="position:absolute;z-index:10;top:0;left:0;" />').appendTo(container);
				}
			}
			this.layer[0].html(initialHtml);
			var h = this.layer[0].height();
			container.css('height',h);

			
			if (this.fade) {
				loaderBg.css({
					'width': container.width(),
					'height': h
				}).fadeTo(0, 0, function(){
					loaderBg.hide();
					loaderBg.appendTo(container);
				});
				loader.css({
					'left': container.width() / 2,
					'top': h / 2 //container.height() / 2
				}).fadeTo(0, 0, function(){
					loader.hide();
					loader.appendTo(container);
				});
			} else {
				loaderBg.css({
					'width': container.width(),
					'height': h
				}).hide().appendTo(container);
				loader.css({
					'left': container.width() / 2,
					'top': h / 2 //container.height() / 2
				}).hide().appendTo(container);
			}

			this.loader=loader;
			this.loaderBg=loaderBg;
			this.addHandlers();
			this.hideNonApplicableButtons();
			
			if (this.rememberPage === true) {
				this.getDejaVu();
			} else {
				this.loadPage(this.onchange);
				this.onchange();
			}
		},

		setCfg: function(cfg){			
		
			this.page = cfg.page || 1;
			this.speed = cfg.speed || 200;
			this.autoCycle = typeof cfg.autoCycle !== 'undefined' ? cfg.autoCycle : false;
			this.cycle = typeof cfg.cycle  !== 'undefined' ? cfg.cycle : false;
			this.hideNavButtons = typeof cfg.hideNavButtons !== 'undefined' ? cfg.hideNavButtons : false;
			this.fade = typeof cfg.fade !== 'undefined' ? cfg.fade : true;
			this.url = cfg.url;
			this.elID = cfg.id;
			this.pageSize = cfg.pageSize;
			this.queryParams = typeof cfg.queryParams!='undefined' ? '/query_params:'+cfg.queryParams : '';
			this.pages=cfg.pages; // page Cache
			this.chunk = cfg.chunk;
			this.chunkSize = cfg.chunkSize;
			this.lastPage = cfg.lastPage;
			this.layer = [];
			this.firstPage = 1;
			this.loaderBgClass= cfg.loaderBgClass ? cfg.loaderBgClass : 'loaderBg';
			this.container = $('div.browsebox#' + cfg.id + ' div.browsebox_content');
			this.interval = null;
			this.isLoading = false;
			this.onchange = cfg.onchange;
			this.rememberPage = cfg.rememberPage;
		},

		getDejaVu: function(){
			var page = document.location.hash;
			var key = '#bb' + this.elID + '=';
			if(page.indexOf(key) != -1){
				page = page.substr(key.length, page.length);
				if (page) {
					this.page = page;
					this.loadPage(this.onchange);
				}
			}
		},
		
		setDejaVu: function(){
			document.location.hash = '#bb' + this.elID + '=' + this.page;
		},
		
		addHandlers: function(){
			$('div.browsebox#' + this.elID + ' div.navbar ul li.prev').click(function(){
				bb.prev();
			});
			$('div.browsebox#' + this.elID + ' div.navbar ul li.next').click(function(){
				bb.next();
			});
		},

		hideNonApplicableButtons: function(){
			var prevButton = $('div.browsebox#' + this.elID + ' div.navbar ul li.prev'), 
				nextButton = $('div.browsebox#' + this.elID + ' div.navbar ul li.next');

			if (!this.autoCycle || !this.cycle) {

				if (this.page == this.firstPage) {
					prevButton.addClass('disabled');
				} else {
					prevButton.removeClass('disabled');
				}
				if (this.page == this.lastPage) {
					nextButton.addClass('disabled');
				} else {
					nextButton.removeClass('disabled');
				}
				
				if(this.page > this.firstPage){
					nextButton.removeClass('noprevious');
				} else {
					nextButton.addClass('noprevious');
				}
				if(this.page < this.lastPage){
					prevButton.removeClass('nonext');
				} else{
					prevButton.addClass('nonext');	
				}
				
			} else if(this.hideNavButtons){
				prevButton.addClass('disabled');
				nextButton.addClass('disabled');
			}
			
			if ($('div.browsebox#' + this.elID + ' div.navbar ul .disabled').length === 2) {
				$('div.browsebox#' + this.elID + ' div.navbar').css({visibility:'hidden'});
			}
			
		},

		prev: function(){
			if (this.isLoading) {
				return;
			}
			
			var button = $('div.browsebox#' + this.elID + ' div.navbar ul li.prev'), 
				s = this.speed;
			this.page--;
			if(this.cycle && this.page < this.firstPage){
				this.page = this.lastPage;
			} else if(this.page < this.firstPage){
				this.page = this.firstPage;
			}
			if (this.fade) {
				button.fadeTo(s, 0);
				this.loadPage(function(){
					button.fadeTo(s, 1, function() {
						this.style.display = ''; // jQuery sets style="display: list-item". We don't need that
					});
				});
			} else {
				button.hide();
				this.loadPage(function(){
					button.show();
				});
			}
			
			if (this.autoCycle) {
				clearInterval(this.interval);
			}
		},

		next: function(){
			if (this.isLoading) {
				return;
			}

			var button = $('div.browsebox#' + this.elID + ' div.navbar ul li.next'), 
				s = this.speed;
			this.page++;
			if(this.cycle && this.page > this.lastPage){
				this.page = this.firstPage;
			} else if(this.page > this.lastPage){
				this.page = this.lastPage;
			}
			if (this.fade) {
				button.fadeTo(s, 0);
				this.loadPage(function(){
					button.fadeTo(s, 1, function() {
						this.style.display = ''; // jQuery sets style="display: list-item". We don't need that
					});
				});
			} else {
				button.hide();
				this.loadPage(function(){
					button.show();
				});
			}
			
			if (this.autoCycle) {
				clearInterval(this.interval);
			}
		},

		setPage: function(layer, page, cback){
			if(this.rememberPage === true){
				this.setDejaVu();
			}
			
			var cb = function(){ // defer the callback function by 100msecs
				setTimeout(cback, 100);
			};
			
			if (!cback) {
				throw "No callback for browsebox.setPage() specified.";
			}
			
			if (this.pages[page]) {
				$(layer).html(this.pages[page].html);
				
				var imgs = $('div.browsebox#' + this.elID + ' div.browsebox_content img');
				
				if (imgs && imgs.length && imgs.length > 0) {
					var counter = imgs.length;
					var img = [];
					$(imgs).each(function(i){
					
						var status = false, src = $(this).attr('src');
						img[i] = new Image();
						
						$(img[i]).error(function(){
							counter--;
							if (!status && counter == 0) {
								status = true;
								cb();
							}
							
						}).load(function(){
							counter--;
							if (!status && counter == 0) {
								status = true;
								cb();
							}
						});
						
						img[i].src = src;
						
					});
					
				} else {
					cb();
				}
			}
		},

		resetHeight: function(h){
			console.log('resetHeight', h);
			if (!h) {
				var h = $('*', this.layer[1]).outerHeight() || $('*', this.layer[0]).outerHeight();
			}
			this.loader.animate({
				'top': (h / 2)
			});
			this.container.css({
				height: h
			});
			console.log('resetHeight end', h);
		},

		loadPage: function(pageLoadedCb){			
			if (this.isLoading) {
				return;
			}
			this.isLoading = true;
			this.toggle = !this.toggle;
			var scope = this,
			s = this.speed, 
			url = this.url + this.elID + '/' + this.page, 
			a = this.layer[this.toggle *1], 
			b = this.layer[!this.toggle *1],
			c = this.container, 
			elID = this.elID, 
			loader = this.loader, 
			loaderBg = this.loaderBg,
			autoCycle = this.autoCycle, 
			onchange = this.onchange,
			fadeBack = function(){
				b.show();
				scope.isLoading = false;
				
				if (scope.fade) {
					b.fadeTo(s, 1, function(){
						scope.isLoading = false;
					});
				}
				var h = $('*',b).outerHeight() || $('*',a).outerHeight();
				
				if (onchange) {
					onchange();
				}
				
				if (scope.fade) {
					loader.animate({
						'opacity': 0,
						'top': (h / 2)
					}, s, null, function(){
						loader.hide();
						loaderBg.hide();
					});
					loaderBg.animate({
						'opacity': 0
					}, s);
				} else {
					loader.hide();
					loaderBg.hide();
					
				}
				if(typeof stopLoading === 'function'){
					stopLoading();
				}
				c.animate({
					//'height': h
				}, s);
			}; 
			this.hideNonApplicableButtons();		

			if (!this.autoCycle) {
				loader.css({
					'left': this.container.width() / 2,
					'top': this.container.height() / 2
				});
				loaderBg.css({
					'width': this.container.width(),
					'height': this.container.height()
				});
				if (this.fade) {
					loader.fadeTo(s, 1);
					loaderBg.fadeTo(s, 0.8);
				} else {
					loader.show();
					loaderBg.show();
				}
				if(typeof animateLoading === 'function'){
					animateLoading();
				}
			}
			
			var visible = $(this.container).css('display') !== 'hidden';
			
			var scope = this;
			if (typeof this.pages[this.page] == 'undefined' || this.filter || !visible) { // not in cache, so make subsequent call
				this.chunk = Math.ceil(this.page / this.chunkSize);
				
				if (this.filter) {
					var qp = '/query_params:' + Garp.URLEncode(this.filter);
				} else if (this.queryParams) {
					var qp = this.queryParams;
				} else {
					var qp = '';
				}
				
				$.getJSON(this.url + this.elID + '/' + this.chunk + qp, function(json){
					for (var p in json) {
						scope.pages[p] = json[p];
					}
					scope.setPage(b, scope.page, function(){
						fadeBack();
						if (scope.fade) {
							a.fadeTo(s, 0, function(){
								a.html('');
								if (pageLoadedCb) {
									pageLoadedCb();
								}
							});
						} else {
							a.hide().html('');
							if (pageLoadedCb) {
								pageLoadedCb();
							}
						}
					});
				});
			} else {
				this.setPage(b, this.page, function(){
					if (scope.fade) {
						fadeBack();
						a.fadeTo(s, 0, function(){
							a.html('');
							if (pageLoadedCb) {
								pageLoadedCb();
							}
						});
					} else {
						fadeBack();
						a.hide().html('');
						if (pageLoadedCb) {
							pageLoadedCb();
						}
					}
				});
			}
		},
		
		query:function(filter){			
			this.page = 1;
			this.filter = JSON.encode(filter);
			this.isLoading = false;
		
			this.loadPage();
		},

		rndPage: function(){
			var r = Math.ceil(Math.random() * this.lastPage);
			return r;
		},

		autoLoad: function(){
			if (this.lastPage > 1) {
				do {
					var rnd = this.rndPage();
				} while (this.page == rnd);
				this.page = rnd;
				this.loadPage();
			}
		}
	};
	
	$(window).load(function(){
		bb.init(cfg);
		if (bb.autoCycle && bb.autoCycle > 0) {
			bb.interval = setInterval(function(){
				bb.autoLoad();
			}, bb.autoCycle);
		}
	});
	
	return bb;
};

Garp.flashMessage = function(msg) {
	$('#flashMessage').remove();
	// if it's not a string and not an array, it's gotta be an object
	if (typeof msg !== 'string' && !('push' in msg)) {
		var _msg = '';
		for (var i in msg) {
			_msg += msg[i]+' ';
		}
	} else {
		var _msg = msg;
	}
	var theClass = '';
	if ('flashMessageHtml' in Garp) {
		_msg = Garp.flashMessageHtml.open+_msg+Garp.flashMessageHtml.close;
		theClass = Garp.flashMessageHtml.cls;
	}
	$('body').prepend('<div class="'+theClass+'" id="flashMessage">'+_msg+'</div>');
	Garp.animateFlashMessage();
};

Garp.animateFlashMessage = function() {
	var fm = $('#flashMessage');
	if (fm.length) {
		fm.animate({
			'top': 0
		}, 150, 'swing', function(){
			fm.animate({
				'top': -30
			}, 150, 'swing', function(){
				fm.animate({
					'top': 0
				}, 150, 'swing', function(){
					fm.animate({
						'top': -10
					}, 150, 'swing', function(){
						var fn = function(){
							fm.animate({
								'top': 0
							}, 200, 'swing', function(){
								fm.animate({
									'top': -200
								}, 220, 'swing', function(){
									fm.remove();
								});
							});
						}
						fm.click(fn);
						setTimeout(fn, 5000);
					});
				});
			});
		});
	}
};
Garp.animateFlashMessage();

/**
 * Validator object
 * @version 1.1
 */
Garp.Validator = (function() {
	/**
	 * Private methods
	 */
	// validation functions. The key is the className that triggers the function
	var rules = {
		required: function(elm) {
			if (!elm.val()) {
				Garp.Validator.triggerError(elm.attr('id'), __('%s is een verplicht veld.'));
			}
		},
		noBMP: function(elm) {
			if (elm.val()) {
				var e = elm.val();
				e = e.substring(e.length-4, e.length);
				e = e.toUpperCase();
				if (e === '.BMP') {
					Garp.Validator.triggerError(elm.attr('id'), __('Geen geldig bestandsformaat.'));
				}
			}
		},
		email: function(elm) {
			var email = /([\w]+)(\.[\w]+)*@([\w\-]+\.){1,5}([A-Za-z]){2,4}$/;
			if (!email.test(elm.val())) {
				Garp.Validator.triggerError(elm.attr('id'), __('%s is geen geldig e-mailadres.'));
			}
		},
		password: function(elm) {
		},
		repeatPassword: function(elm) {
			if (elm.attr('rel') && $('#'+elm.attr('rel'))) {
				var theOtherPwdField = $('#'+elm.attr('rel'));
				if (theOtherPwdField.length) {
					if (theOtherPwdField.val() !== elm.val()) {
						Garp.Validator.triggerError(elm.attr('id'), __('De wachtwoorden komen niet overeen.'));
					}
				}
			}
		},
		requiredIf: function(elm) {
			if (elm.attr('rel') && $('#'+elm.attr('rel'))) {
				var otherField = $('#'+elm.attr('rel'));
				var otherFieldFilled = false;
				if (otherField.attr('type') == 'checkbox') {
					otherFieldFilled = otherField.is(':checked');
				} else {
					otherFieldFilled = otherField.val();
				}
				if (otherFieldFilled && !elm.val()) {
					var verb = otherField.attr('type') === 'checkbox' ? 'aangevinkt' : 'ingevuld';
					var str = __('Als ### is '+verb+', is %s verplicht.');
					str = str.replace('###', $('label[for="'+otherField.attr('id')+'"]').text());
					Garp.Validator.triggerError(elm.attr('id'), str);
				}
			}
		}
	};
	
	/**
	 * Public methods
	 */
	return {
		// Validate the form according to the rules above
		validateForm: function(formId) {
			// loop thru all the different input types
			var fields = $('#'+formId+' input, #'+formId+' select, #'+formId+' textarea');
			$('#'+formId).submit(function(e) {
				// reset errorMessages to an empty array
				Garp.Validator.errorMessages = {};
				fields.each(function() {
					var self = $(this);
					for (var i in rules) {
						if (self.hasClass(i)) {
							rules[i](self);
						}
					}
				});
				// if the form is valid, encrypt the password fields
				var valid = true;
				for (var i in Garp.Validator.errorMessages) {
					valid = false;
					break;
				}
				if (valid) {
					Garp.Authenticator.encryptPasswords(formId);
					return true;
				} else {
					for (var j in Garp.Validator.errorMessages) {
						Garp.flashMessage(Garp.Validator.errorMessages[j]);
						break;
					}
					e.preventDefault();
					return false;
				}
			});
		},
		// add custom rules, with custom functions if required
		pushRule: function(rule, fn) {
			rules.push(rule);
			if (fn) {
				validationFunctions.push(fn);
			}
		},
		// add errors
		triggerError: function(id, msg) {
			var label = $('label[for='+id+']');
			Garp.Validator.errorMessages[id] = msg.replace('%s', label.text());
		}
	};
})();

Garp.SearchBox = function(selector, callbacks, res) {

	var elm = selector + ' input.text', 
		btn = selector + ' input.button' +  ',' + selector + ' button[type="submit"]', 
		res = typeof res != 'undefined' ? res : selector + ' div#searchresult', 
		frm = selector + ' form',
		SPEED = 300,
		MINCHARS = 3,
		MINCHARMSG = '<p>'+__('Je zoekopdracht is te kort...')+'</p>';
	
	
	
	function toggleResultView(state){
		if (state) {
			$(res).slideDown(SPEED);
		} else {
			$(res).slideUp(SPEED);
		}
	}
	
	function submit(e) {
		if ($(elm).val() && $(elm).val().length >= MINCHARS) {
			$(res).html('');
			$(btn).addClass('loading');
			// toggleResultView(true);
			$(btn).ajaxError(function(){
				$(btn).removeClass('loading');
				toggleResultView(false);
			});
			$.getJSON(BASE + 'garp/search/index/q:' + $(elm).val() +'/.json', function(response) {
				$(btn).removeClass('loading');
				$(res).slideUp({
					duration: 1,
					complete: function(){
						$(res).html(response.html);
						if (typeof callbacks !== 'undefined' && callbacks && 'afterComplete' in callbacks) {
							callbacks.afterComplete($(res));
						}
						toggleResultView(true);
						$(document.body).one('click',function(){
							toggleResultView(false);
							return true;
						});
					}
				});
			});
		} else {
			if ($(elm).val() && $(elm).val().length <= MINCHARS) {
				setTimeout(function(){
					$(document.body).one('click', function(){
						toggleResultView(false);
						return true;
					});
				},300);

				$(res).html(MINCHARMSG);
				toggleResultView(true);
			} else {
				toggleResultView(false);
				$(elm).focus();
				$(elm).val('');
			}
		}
		
		if (e && typeof e.preventDefault === 'function') {
			e.preventDefault();
		}
		return false;
	}
	
	$(frm).submit(submit);
	$(btn).click(submit);
	
	if (!$(res).length) {
		$(selector).append('<div style="display:none;" id="searchresult"></div>');
	}

};

/**
 * Authenticator object, hashes a password before submitting a form.
 * @version 2.0
 */
Garp.Authenticator = (function () {
	var encryption = 'md5';
	return {
		encryptPasswords: function(formId) {
			$('#'+formId+' input[type="password"]').each(function() {
				var self = $(this);
				if (self.val()) {
					self.parent().append(
						'<input type="hidden" name="'+
						self.attr('name')+
						'" value="'+
						Garp.Authenticator.encryptPassword(self.val(), encryption)+
						'">'
					);
					self.attr({
						name: ''
					});
					self.val('');
				}
			});
		},
		encryptPassword: function(plainPassword, encryptionMethod) {
			switch (encryptionMethod) {
				case 'sha1':
					return hex_sha1(plainPassword);
				case 'md5':
					return hex_md5(plainPassword);
				case 'none':
					return plainPassword;
				default:
					throw "No encryption method specified";
			}
		}
	};
})();

/**
 * Garp.CountDownArea
 * twitter-style countdown for textareas
 * @param {string} formSelector
 * @param {bool} allowBlank allow no comment (do not disable form submit)
 */
Garp.CountDownArea = function(formSelector, allowBlank, callback) {
		var maxCharacters = 140,
			textarea = $('textarea', formSelector),
			submit = $('input[type="submit"], button', formSelector);
		
		allowBlank = allowBlank || false;
		
		function updateCounter() {
			var val = maxCharacters - textarea.val().length;
			$('.counter span', formSelector).html(val + '');
			if (typeof callback === 'function') {
				callback(val);
			}
		}

		function checkLength() {
			if(typeof textarea.val() === 'undefined') return;
			// timeout construct: buffer this check. It might get called very often; that might cause slugish behavior:
			
			if(this.buffer){
				clearTimeout(this.buffer);
			}
			this.buffer = setTimeout(function(){
				var len = textarea.val().length;
				if (len > maxCharacters) {
					submit.attr({
						'disabled': 'disabled'
					});
					if (len > 0) {
						$('.counter', formSelector).addClass('surplus');
					}
				} else {
					submit.removeAttr('disabled');
					$('.counter', formSelector).removeClass('surplus');
				}
				if (!allowBlank && len == 0) {
					submit.attr({
						'disabled': 'disabled'
					});
				}
				updateCounter();
			}, 50);
		}
		
		textarea.keyup(checkLength)
				.keypress(checkLength)
				.blur(checkLength)
				.click(checkLength);
		if (!$('.counter', formSelector).length) {
			textarea.before('<p class="counter" />');
		}
		
		checkLength();
};

/**
 * Generic hijackin' of requests
 * @author Harmen Janssen
 */
Garp.Hijax = {
	/**
	 * Callback method
	 * @var Array
	 */
	callbacks: [],
	/**
	 * Initialize behavior
	 * @return Object this
	 */
	init: function() {
		var _self = this;
		$(document).ready(function() {
			$('a.hijax').click(function(e) {
				var theElement = this;
				var theUrl = $(theElement).attr('href');
				theUrl += theUrl[theUrl.length-1] != '/' ? '/' : '';
				theUrl += 'ajax:1';
				$.getJSON(theUrl, {}, function(data) {
					if (_self.callbackExists(theElement)) {
						_self.executeCallback(theElement, data);
					} else {
						if (!data.message) {
							throw 'Key "message" required in returned data.';
						}
						Garp.flashMessage(data.message);
					}
				});
				e.preventDefault();
			});
		});
		return this;
	},
	/**
	 * Check if custom callback function exists
	 * @param HTMLElement $element The element
	 * @return Boolean
	 */
	callbackExists: function(element) {
		for (var i = 0; i < this.callbacks.length; i++) {
			if (this.callbacks[i].element == element) {
				return true;
			}
		}
		return false;
	},
	/**
	 * Execute custom callback function
	 * @param HTMLElement $element The element
	 * @param Object $data The data returned from the AJAX call
	 * @return Void
	 */
	executeCallback: function(element, data) {
		for (var i = 0; i < this.callbacks.length; i++) {
			if (this.callbacks[i].element == element) {
				this.callbacks[i].fn(data);
			}
		}
	},
	/**
	 * If something other than the display of a flash message needs to happen
	 * when a link is clicked, use this method to register a custom callback function.
	 * @param HTMLElement $element The element
	 * @param Function $fn The callback function
	 * @return Object this
	 */
	registerCallback: function(element, fn) {
		this.callbacks.push({
			element: element,
			fn: fn
		});
		return this;
	}
}.init();

Garp.makeBlockLinks = function(e) {
	e.live('click', function() {
		window.location = $(this).find('a:first').attr('href');
	}).live('mouseover', function() {
		window.status = $(this).find('a:first').attr('href');
		$(this).addClass('hover');
	}).live('mouseout', function() {
		window.status = '';
		$(this).removeClass('hover');
	});
};

/**
 * Inline label module. For labels that look as if they are the value of an input field
 */
Garp.inlineLabels = {
	/**
	 * Find correct labels on the page and display 'em 'inline'
	 * @param Mixed $elements Optional elements, if none, "label.inline" will be used.
	 */
	init: function(elements) {
		var self = this;
		elements = elements || 'label.inline';
		$(elements).each(function() {
			var thisLabel = $(this);
			var input = $('#'+thisLabel.attr('for'));
			input.focus(function() {
				self.focus.call(input, thisLabel);
			}).blur(function() {
				self.blur.call(input, thisLabel);
			});
			
			// 'cause browsers remember certain form values, there needs to be one manual check.
			setTimeout(function() {
				if ($(input).val()) {
					self.focus.call(input, thisLabel);
				}
			}, 1000);
		});
	},
	/**
	 * Focus event handler on inputs
	 */
	focus: function(theLabel) {
		theLabel.addClass('hidden');
	},
	/**
	 * Blur event handler on inputs
	 */
	blur: function(theLabel) {
		if (!$(this).val()) {
			theLabel.removeClass('hidden');
		}
	}
};
Garp.inlineLabels.init();

/**
 * Utility function. Binds receiverObj's properties to senderObj's properties 
 * @param {Object} receiverObj
 * @param {Object} senderObj
 * @return {Object} receiverObj
 */
Garp.apply = function(receiverObj, senderObj){
	for (var i in senderObj) {
		receiverObj[i] = senderObj[i];
	}
	return receiverObj;
};

/**
 * Utility function. Binds receiverObj's properties to senderObj's properties if not already present
 * @param {Object} receiverObj
 * @param {Object} senderObj
 * @return {Object} receiverObj
 */
Garp.applyIf = function(receiverObj, senderObj){
	for (var i in senderObj) {
		if (!receiverObj.hasOwnProperty(i)) {
			receiverObj[i] = senderObj[i];
		}
	}
	return receiverObj;
}

/**
 * Utility string function: use a simple tpl string to format multiple arguments
 * 
 * example:
 * var html = Garp.format('<a href="${1}">${2}</a>"', 'http://www.grrr.nl/', 'Grrr Homepage');
 * 
 * @param {String} tpl  template
 * @param {String} ...n input string(s)
 * @return {String}
 */
Garp.format = function(tpl){
	for (var res = tpl, i = 1, l = arguments.length; i < l; i++) {
		res = res.replace(new RegExp("\\${" + i +"\\}","g"), arguments[i]);
	}
	return res;
};

/**
 * Utility function each
 * Calls fn for each ownProperty of obj. fn(property, iterator, obj)
 * 
 * @param {Object} obj
 * @param {Function} fn 
 */
Garp.each = function(obj, fn){
	for(var i in obj){
		if (obj.hasOwnProperty(i)) {
			fn(obj[i], i, obj);
		}
	}
}

/**
 * 
 */
Garp.relativeDate = function(oldest, newest){
	oldest = new Date(oldest + '');
	newest = new Date(newest + '');
	
	var elapsed = Math.abs(oldest.getTime() - newest.getTime()); // milliseconds
	
	if(isNaN(elapsed)){
		return '';
	}
	
	var elapsed = elapsed / 60000; // minutes
	var result = '';
	
	switch (true) {
		case (elapsed < 1):
			result = __('less than a minute');
			break;
			
		case (elapsed < (60)):
			var minutes = Math.round(elapsed);
			result = minutes + ' ' + (minutes == 1 ? __('minute') : __('minutes'));
			break;
			
		case (elapsed < (60 * 24)):
		
			var hours = Math.round(elapsed / 60);
			result = hours + ' ' + (hours == 1 ? __('hour') : __('hours'));
			break;
			
		case (elapsed < (60 * 24 * 7)):
			var days = Math.round(elapsed / (60 * 24));
			result = days + ' ' + (days == 1 ? __('day') : __('days'));
			break;
			
		case (elapsed < (60 * 24 * 7 * 30)):
			var weeks = Math.round(elapsed / (60 * 24 * 7));
			result = weeks + ' ' + (weeks == 1 ? __('week') : __('weeks'));
			break;
			
		case (elapsed < (60 * 24 * 7 * 30 * 12)):
			var months = Math.round(elapsed / (60 * 24 * 7 * 30));
			result = months + ' ' + (months == 1 ? __('month') : __('months'));
			break;
			
		default:
			var years = Math.round(elapsed / (60 * 24 * 7 * 30 * 365));
			result = years + ' ' + (years == 1 ? __('year') : __('years'));
			break;
			
	}
	return result + ' ' +__('ago');
}

/***
 * Class Twitter
 * @param {Object} config
 * 
 * example usage:
 * var twitter = new Garp.Twitter({
 *		elm: $('#tweets'),
 *		afterFetch: function(result){
 *			this.elm.prepend(result);
 *		},
 *		beforeFetch: function(){
 *			this.elm.empty();
 *		}
 *	});
 *	twitter.search('garp');
 */
Garp.Twitter = function(config){

	// Default config: //
	Garp.apply(this, {
		query: '',
		resultsPerPage: 25, // max 100
		resultsAsArray: false,
		searchTpl: '<img src="${1}" alt="${2}">${2}: ${3}<hr>',
		listTpl: '<img src="${1}" alt="${2}">${2}: ${3}<hr>',
		beforeFetch: jQuery.noop,
		afterFetch: jQuery.noop,
		onError: jQuery.noop // Gets called when no results found or an error occurred
	});
	
	// Override config: //
	Garp.apply(this, config);
	
	/**
	 * Searches Twitter for query and caches the query string for later re-use
	 * @param {query} (optional) query
	 */
	this.search = function(query){
		if (query) {
			this.query = query;
		} else {
			query = this.query;
		}
		query = encodeURIComponent(query);
		var scope = this;
		scope.beforeFetch.call(this);
		$.getJSON(Garp.format('http://search.twitter.com/search.json?q=${1}&rpp=${2}&callback=?', query, this.resultsPerPage), function(response){
			scope.parseResponse.call(scope, response);
		});
	};
	
	
	/**
	 * Gets Twitter Lists
	 * @param {String} user
	 * @param {String} listId
	 */
	this.getList = function(user, listId){
		var scope = this;
		scope.beforeFetch.call(this);
		$.getJSON(Garp.format('http://api.twitter.com/1/${1}/lists/${2}/statuses.json?callback=?', user, listId), function(response){
			scope.parseResponse.call(scope, response);
		});
	}
	
	// private //
	this.parseResponse = function(response){
		var result = [];
		if (response) {
			if (response.results) { // search results
				for (var item in response.results) {
					result.push(Garp.format(this.searchTpl, response.results[item].profile_image_url, response.results[item].from_user, response.results[item].text.replace(new RegExp('(http://[^ ]+)',"g"),'<a target="_blank" href="$1">$1</a>'), Garp.relativeDate(response.results[item].created_at, new Date()), response.results[item].from_user));
				}
			} else { // list results
				var c = 1;
				for (var item in response) {
					c++;
					result.push(Garp.format(this.listTpl, response[item].user.profile_image_url, response[item].user.name, response[item].text.replace(new RegExp('(http://[^ ]+)',"g"),'<a href="$1">$1</a>'), Garp.relativeDate(response[item].created_at, new Date()), response[item].user.screen_name));
					if(c > this.resultsPerPage){
						break;
					}
				}
			}
			if (result.length) {
				this.afterFetch(this.resultsAsArray ? result : result.join(''));
			} else {
				this.onError(response);
			}
		} else {
			this.onError(response);
		}
	};
	
	return this;
};

/**
 * Spinner
 */
(function() {
	Garp.spinner = {};
	var spinner = document.createElement('div');
	var timer = null;
	
	$('body').append(spinner);
	$(spinner).css({
		'position':'absolute',
		'top':0,
		'z-index': 65535,
		'left':0,
		'width':40,
		'height':40,
		'background':'url('+BASE+'css/img/icons/fancy_progress.png)'
	}).hide();
	
	showSpinner = function(position){
		if(position){
			$(spinner).css({
				top: position.top - 20,
				left: position.left - 20
			});
		}
		if (!timer) {
			timer = setInterval(function(){
				if (!this.i || this.i > 480) {
					this.i = 0;
				}
				$(spinner).css('background-position', '0 ' + (480 - this.i) + 'px');
				this.i += 40;
			}, 90);
		}
		$(spinner).show();
	}
	
	hideSpinner = function(){
		$(spinner).hide();
		clearInterval(timer);
		timer = null;
	}
	
	Garp.spinner = {
		show: showSpinner,
		hide: hideSpinner
	};
})();


/**
 * i18n module
 */
Garp.i18n = {};

function __(identifier){
	if (typeof LANGUAGE !== 'undefined' && LANGUAGE in Garp.i18n && identifier in Garp.i18n[LANGUAGE]) {
		return Garp.i18n[LANGUAGE][identifier];
	}
	return identifier;
}

