/**
 * Autocompleter
 *
 * http://digitarald.de/project/autocompleter/
 *
 * @version		1.1.2
 *
 * @license		MIT-style license
 * @author		Harald Kirschner <mail [at] digitarald.de>
 * @copyright	Author
 */

var Autocompleter = new Class({

	Implements: [Options, Events],

	options: {/*
		onOver: $empty,
		onSelect: $empty,
		onSelection: $empty,
		onShow: $empty,
		onHide: $empty,
		onBlur: $empty,
		onFocus: $empty,*/
		minLength: 1,
		markQuery: true,
		width: 'inherit',
		maxChoices: 10,
		injectChoice: null,
		customChoices: null,
		emptyChoices: null,
		visibleChoices: true,
		className: 'menu',
		zIndex: 42,
		delay: 400,
		observerOptions: {},
		fxOptions: {},

		autoSubmit: false,
		overflow: false,
		overflowMargin: 25,
		selectFirst: false,
		filter: null,
		filterCase: false,
		filterSubset: false,
		forceSelect: false,
		selectMode: true,
		choicesMatch: null,

		multiple: false,
		separator: ', ',
		separatorSplit: /\s*[,;]\s*/,
		autoTrim: false,
		allowDupes: false,

		cache: true,
		relative: false
	},

	initialize: function(element, options){
		this.element = $(element);
		this.setOptions(options);
		this.build();
		this.observer = new Observer(this.element, this.prefetch.bind(this), $merge({
			'delay': this.options.delay
		}, this.options.observerOptions));
		this.queryValue = null;
		if (this.options.filter) this.filter = this.options.filter.bind(this);
		var mode = this.options.selectMode;
		this.typeAhead = (mode == 'type-ahead');
		this.selectMode = (mode === true) ? 'selection' : mode;
		this.cached = [];
	},

	build: function(){
		if ($(this.options.customChoices)){
			this.choices = this.options.customChoices;
		} else {
			this.choices = new Element('ul', {
				'class': this.options.className,
				'styles': {
					'zIndex': this.options.zIndex
				}
			}).inject(document.body);
			this.relative = false;
			if (this.options.relative){
				this.choices.inject(this.element, 'after');
				this.relative = this.element.getOffsetParent();
			}
			this.fix = new OverlayFix(this.choices);
		}
		if (!this.options.separator.test(this.options.separatorSplit)){
			this.options.separatorSplit = this.options.separator;
		}
		this.fx = (!this.options.fxOptions) ? null : new Fx.Tween(this.choices, $merge({
			'property': 'opacity',
			'link': 'cancel',
			'duration': 200
		}, this.options.fxOptions)).addEvent('onStart', Chain.prototype.clearChain).set(0);
		this.element.setProperty('autocomplete', 'off')
			.addEvent((Browser.Engine.trident || Browser.Engine.webkit) ? 'keydown' : 'keypress', this.onCommand.bind(this))
			.addEvent('click', this.onCommand.bind(this, [false]))
			.addEvent('focus', this.toggleFocus.create({bind: this, arguments: true, delay: 100}))
			.addEvent('blur', this.toggleFocus.create({bind: this, arguments: false, delay: 100}));
	},

	destroy: function(){
		if (this.fix) this.fix.destroy();
		this.choices = this.selected = this.choices.destroy();
	},

	toggleFocus: function(state){
		this.focussed = state;
		if (!state) this.hideChoices(true);
		this.fireEvent((state) ? 'onFocus' : 'onBlur', [this.element]);
	},

	onCommand: function(e){
		if (!e && this.focussed) return this.prefetch();
		if (e && e.key && !e.shift){
			switch (e.key){
				case 'enter':
					if (this.element.value != this.opted) return true;
					if (this.selected && this.visible){
						this.choiceSelect(this.selected);
						return !!(this.options.autoSubmit);
					}
					break;
				case 'up': case 'down':
					if (!this.prefetch() && this.queryValue !== null){
						var up = (e.key == 'up');
						this.choiceOver((this.selected || this.choices)[
							(this.selected) ? ((up) ? 'getPrevious' : 'getNext') : ((up) ? 'getLast' : 'getFirst')
						](this.options.choicesMatch), true);
					}
					return false;
				case 'esc': case 'tab':
					this.hideChoices(true);
					break;
			}
		}
		return true;
	},

	setSelection: function(finish){
		var input = this.selected.inputValue, value = input;
		var start = this.queryValue.length, end = input.length;
		if (input.substr(0, start).toLowerCase() != this.queryValue.toLowerCase()) start = 0;
		if (this.options.multiple){
			var split = this.options.separatorSplit;
			value = this.element.value;
			start += this.queryIndex;
			end += this.queryIndex;
			var old = value.substr(this.queryIndex).split(split, 1)[0];
			value = value.substr(0, this.queryIndex) + input + value.substr(this.queryIndex + old.length);
			if (finish){
				var tokens = value.split(this.options.separatorSplit).filter(function(entry){
					return this.test(entry);
				}, /[^\s,]+/);
				if (!this.options.allowDupes) tokens = [].combine(tokens);
				var sep = this.options.separator;
				value = tokens.join(sep) + sep;
				end = value.length;
			}
		}
		this.observer.setValue(value);
		this.opted = value;
		if (finish || this.selectMode == 'pick') start = end;
		this.element.selectRange(start, end);
		this.fireEvent('onSelection', [this.element, this.selected, value, input]);
	},

	showChoices: function(){
		var match = this.options.choicesMatch, first = this.choices.getFirst(match);
		this.selected = this.selectedValue = null;
		if (this.fix){
			var pos = this.element.getCoordinates(this.relative), width = this.options.width || 'auto';
			this.choices.setStyles({
				'left': pos.left,
				'top': pos.bottom,
				'width': (width === true || width == 'inherit') ? pos.width : width
			});
		}
		if (!first) return;
		if (!this.visible){
			this.visible = true;
			this.choices.setStyle('display', '');
			if (this.fx) this.fx.start(1);
			this.fireEvent('onShow', [this.element, this.choices]);
		}
		if (this.options.selectFirst || this.typeAhead || first.inputValue == this.queryValue) this.choiceOver(first, this.typeAhead);
		var items = this.choices.getChildren(match), max = this.options.maxChoices;
		var styles = {'overflowY': 'hidden', 'height': ''};
		this.overflown = false;
		if (items.length > max){
			var item = items[max - 1];
			styles.overflowY = 'scroll';
			styles.height = item.getCoordinates(this.choices).bottom;
			this.overflown = true;
		};
		this.choices.setStyles(styles);
		this.fix.show();
		if (this.options.visibleChoices){
			var scroll = document.getScroll(),
			size = document.getSize(),
			coords = this.choices.getCoordinates();
			if (coords.right > scroll.x + size.x) scroll.x = coords.right - size.x;
			if (coords.bottom > scroll.y + size.y) scroll.y = coords.bottom - size.y;
			window.scrollTo(Math.min(scroll.x, coords.left), Math.min(scroll.y, coords.top));
		}
	},

	hideChoices: function(clear){
		if (clear){
			var value = this.element.value;
			if (this.options.forceSelect) value = this.opted;
			if (this.options.autoTrim){
				value = value.split(this.options.separatorSplit).filter($arguments(0)).join(this.options.separator);
			}
			this.observer.setValue(value);
		}
		if (!this.visible) return;
		this.visible = false;
		if (this.selected) this.selected.removeClass('autocompleter-selected');
		this.observer.clear();
		var hide = function(){
			this.choices.setStyle('display', 'none');
			this.fix.hide();
		}.bind(this);
		if (this.fx) this.fx.start(0).chain(hide);
		else hide();
		this.fireEvent('onHide', [this.element, this.choices]);
	},

	prefetch: function(){
		var value = this.element.value, query = value;
		if (this.options.multiple){
			var split = this.options.separatorSplit;
			var values = value.split(split);
			var index = this.element.getSelectedRange().start;
			var toIndex = value.substr(0, index).split(split);
			var last = toIndex.length - 1;
			index -= toIndex[last].length;
			query = values[last];
		}
		if (query.length < this.options.minLength){
			this.hideChoices();
		} else {
			if (query === this.queryValue || (this.visible && query == this.selectedValue)){
				if (this.visible) return false;
				this.showChoices();
			} else {
				this.queryValue = query;
				this.queryIndex = index;
				if (!this.fetchCached()) this.query();
			}
		}
		return true;
	},

	fetchCached: function(){
		return false;
		if (!this.options.cache
			|| !this.cached
			|| !this.cached.length
			|| this.cached.length >= this.options.maxChoices
			|| this.queryValue) return false;
		this.update(this.filter(this.cached));
		return true;
	},

	update: function(tokens){
		this.choices.empty();
		this.cached = tokens;
		var type = tokens && $type(tokens);
		if (!type || (type == 'array' && !tokens.length) || (type == 'hash' && !tokens.getLength())){
			(this.options.emptyChoices || this.hideChoices).call(this);
		} else {
			if (this.options.maxChoices < tokens.length && !this.options.overflow) tokens.length = this.options.maxChoices;
			tokens.each(this.options.injectChoice || function(token){
				var choice = new Element('li', {'html': this.markQueryValue(token)});
				choice.inputValue = token;
				this.addChoiceEvents(choice).inject(this.choices);
			}, this);
			this.showChoices();
		}
	},

	choiceOver: function(choice, selection){
		if (!choice || choice == this.selected) return;
		if (this.selected) this.selected.removeClass('autocompleter-selected');
		this.selected = choice.addClass('autocompleter-selected');
		this.fireEvent('onSelect', [this.element, this.selected, selection]);
		if (!this.selectMode) this.opted = this.element.value;
		if (!selection) return;
		this.selectedValue = this.selected.inputValue;
		if (this.overflown){
			var coords = this.selected.getCoordinates(this.choices), margin = this.options.overflowMargin,
				top = this.choices.scrollTop, height = this.choices.offsetHeight, bottom = top + height;
			if (coords.top - margin < top && top) this.choices.scrollTop = Math.max(coords.top - margin, 0);
			else if (coords.bottom + margin > bottom) this.choices.scrollTop = Math.min(coords.bottom - height + margin, bottom);
		}
		if (this.selectMode) this.setSelection();
	},

	choiceSelect: function(choice){
		if (choice) this.choiceOver(choice);
		this.setSelection(true);
		this.queryValue = false;
		this.hideChoices();
	},

	filter: function(tokens){
		return (tokens || this.tokens).filter(function(token){
			return this.test(token);
		}, new RegExp(((this.options.filterSubset) ? '' : '^') + this.queryValue.escapeRegExp(), (this.options.filterCase) ? '' : 'i'));
	},

	markQueryValue: function(str){
		return (!this.options.markQuery || !this.queryValue) ? str
			: str.replace(new RegExp('(' + ((this.options.filterSubset) ? '' : '^') + this.queryValue.escapeRegExp() + ')', (this.options.filterCase) ? '' : 'i'), '<span class="autocompleter-queried">$1</span>');
	},

	addChoiceEvents: function(el){
		return el.addEvents({
			'mouseover': this.choiceOver.bind(this, [el]),
			'click': this.choiceSelect.bind(this, [el])
		});
	}
});

var OverlayFix = new Class({

	initialize: function(el){
		if (Browser.Engine.trident){
			this.element = $(el);
			this.relative = this.element.getOffsetParent();
			this.fix = new Element('iframe', {
				'frameborder': '0',
				'scrolling': 'no',
				'src': 'javascript:false;',
				'styles': {
					'position': 'absolute',
					'border': 'none',
					'display': 'none',
					'filter': 'progid:DXImageTransform.Microsoft.Alpha(opacity=0)'
				}
			}).inject(this.element, 'after');
		}
	},

	show: function(){
		if (this.fix){
			var coords = this.element.getCoordinates(this.relative);
			delete coords.right;
			delete coords.bottom;
			this.fix.setStyles($extend(coords, {
				'display': '',
				'zIndex': (this.element.getStyle('zIndex') || 1) - 1
			}));
		}
		return this;
	},

	hide: function(){
		if (this.fix) this.fix.setStyle('display', 'none');
		return this;
	},

	destroy: function(){
		if (this.fix) this.fix = this.fix.destroy();
	}

});

Element.implement({

	getSelectedRange: function(){
		if (!Browser.Engine.trident) return {start: this.selectionStart, end: this.selectionEnd};
		var pos = {start: 0, end: 0};
		var range = this.getDocument().selection.createRange();
		if (!range || range.parentElement() != this) return pos;
		var dup = range.duplicate();
		if (this.type == 'text'){
			pos.start = 0 - dup.moveStart('character', -100000);
			pos.end = pos.start + range.text.length;
		} else {
			var value = this.value;
			var offset = value.length - value.match(/[\n\r]*$/)[0].length;
			dup.moveToElementText(this);
			dup.setEndPoint('StartToEnd', range);
			pos.end = offset - dup.text.length;
			dup.setEndPoint('StartToStart', range);
			pos.start = offset - dup.text.length;
		}
		return pos;
	},

	selectRange: function(start, end){
		if (Browser.Engine.trident){
			var diff = this.value.substr(start, end - start).replace(/\r/g, '').length;
			start = this.value.substr(0, start).replace(/\r/g, '').length;
			var range = this.createTextRange();
			range.collapse(true);
			range.moveEnd('character', start + diff);
			range.moveStart('character', start);
			range.select();
		} else {
			this.focus();
			this.setSelectionRange(start, end);
		}
		return this;
	}

});

Autocompleter.Local = new Class({

	Extends: Autocompleter,

	options: {
		minLength: 0,
		delay: 200
	},

	initialize: function(element, tokens, options){
		this.parent(element, options);
		this.tokens = tokens;
	},

	query: function(){
		this.update(this.filter());
	}

});

Autocompleter.Request = new Class({

	Extends: Autocompleter,

	options: {/*
		indicator: null,
		indicatorClass: null,
		onRequest: $empty,
		onComplete: $empty,*/
		postData: {},
		ajaxOptions: {},
		postVar: 'value'

	},

	query: function(){
		var data = $unlink(this.options.postData) || {};
		data[this.options.postVar] = this.queryValue;
		var indicator = $(this.options.indicator);
		if (indicator) indicator.setStyle('display', '');
		var cls = this.options.indicatorClass;
		if (cls) this.element.addClass(cls);
		this.fireEvent('onRequest', [this.element, this.request, data, this.queryValue]);
		this.request.send({'data': data});
	},

	queryResponse: function(){
		var indicator = $(this.options.indicator);
		if (indicator) indicator.setStyle('display', 'none');
		var cls = this.options.indicatorClass;
		if (cls) this.element.removeClass(cls);
		return this.fireEvent('onComplete', [this.element, this.request]);
	}

});

Autocompleter.Request.JSON = new Class({

	Extends: Autocompleter.Request,

	initialize: function(el, url, options){
		this.parent(el, options);
		this.request = new Request.JSON($merge({
			'url': url,
			'link': 'cancel'
		}, this.options.ajaxOptions)).addEvent('onComplete', this.queryResponse.bind(this));
	},

	queryResponse: function(response){
		this.parent();
		this.update(response);
	}

});

Autocompleter.Request.HTML = new Class({

	Extends: Autocompleter.Request,

	initialize: function(el, url, options){
		this.parent(el, options);
		this.request = new Request.HTML($merge({
			'url': url,
			'link': 'cancel',
			'update': this.choices
		}, this.options.ajaxOptions)).addEvent('onComplete', this.queryResponse.bind(this));
	},

	queryResponse: function(tree, elements){
		this.parent();
		if (!elements || !elements.length){
			this.hideChoices();
		} else {
			this.choices.getChildren(this.options.choicesMatch).each(this.options.injectChoice || function(choice){
				var value = choice.innerHTML;
				choice.inputValue = value;
				this.addChoiceEvents(choice.set('html', this.markQueryValue(value)));
			}, this);
			this.showChoices();
		}

	}

});

var Observer = new Class({

	Implements: [Options, Events],

	options: {
		periodical: false,
		delay: 1000
	},

	initialize: function(el, onFired, options){
		this.element = $(el) || $$(el);
		this.addEvent('onFired', onFired);
		this.setOptions(options);
		this.bound = this.changed.bind(this);
		this.resume();
	},

	changed: function(){
		var value = this.element.get('value');
		if ($equals(this.value, value)) return;
		this.clear();
		this.value = value;
		this.timeout = this.onFired.delay(this.options.delay, this);
	},

	setValue: function(value){
		this.value = value;
		this.element.set('value', value);
		return this.clear();
	},

	onFired: function(){
		this.fireEvent('onFired', [this.value, this.element]);
	},

	clear: function(){
		$clear(this.timeout || null);
		return this;
	},

	pause: function(){
		if (this.timer) $clear(this.timer);
		else this.element.removeEvent('keyup', this.bound);
		return this.clear();
	},

	resume: function(){
		this.value = this.element.get('value');
		if (this.options.periodical) this.timer = this.changed.periodical(this.options.periodical, this);
		else this.element.addEvent('keyup', this.bound);
		return this;
	}

});

var $equals = function(obj1, obj2){
	return (obj1 == obj2 || JSON.encode(obj1) == JSON.encode(obj2));
};/*
Class: Cropper
  Simplified image cropping class
*/
var Cropper = new Class({
	
	Implements: [Events, Options],
	
	options: {
		min: 220,
		base: 'cropper',
		onChange: $empty
	},
	
	initialize: function(container, source, options){
		this.setOptions(options);
		this.scroller = new Element('div', { 'class': this.options.base + '-scroller' });
		this.slider = new Element('div', { 'class': this.options.base + '-slider' }).adopt(new Element('div')).hide(true);
		this.min = this.options.min;
		this.properties = [];
		this.previous = 0;
		
		$(container).adopt(this.scroller, this.slider);
		
		this.image = new Element('img', {
			src: source,
			events: { load: this.setup.bind(this) },
			styles: { visibility: 'hidden' }
		}).inject(this.scroller);
	},
	
	setup: function(){
	  this.image.setStyle('margin', 0);
		var image = this.image, width = image.width, height = image.height;
		if (width > height){
			this.properties = ['width', 'height', 'marginTop'];
			this.initial = width;
		} else {
			this.properties = ['height', 'width', 'marginLeft'];
			this.initial = height;
		}
		if (this.initial > this.min) this.createSlider();
		else image[this.properties[0]] = this.min;
		this.createScroller();
		this.image.show();
		this.fireEvent('onLoad', [this, this.image, this.scroller]);
	},
	
	createSlider: function(){
		this.slider.show();
		var args = [this, this.image, this.scroller];
		var slider = new Slider(this.slider, this.slider.getFirst(), {
			steps: this.initial - this.min,
			initialize: function(){
				this.image[this.properties[0]] = this.min;
				this.scroller.scrollTop = (this.image[this.properties[1]] - this.min) / 2;
				this.scroller.scrollLeft = (this.image[this.properties[0]] - this.min) / 2;
				var margin = (this.options.min - this.image[this.properties[1]]) / 2;
				this.image.style[this.properties[2]] = margin.max(0) + 'px';
			}.bind(this),
			onChange: function(step){
				this.image[this.properties[0]] = step + this.min;
				var change = ((step - this.previous) / 2).toInt();
				this.scroller.scrollLeft += change;
				this.scroller.scrollTop += change;
				var margin = (this.options.min - this.image[this.properties[1]]) / 2;
				this.image.style[this.properties[2]] = margin.max(0) + 'px';
				this.previous = step;
				this.fireEvent('onChange', args);
			}.bind(this),
			onComplete: function(){ this.fireEvent('onComplete', args); }.bind(this)
		});
	},
	
	createScroller: function(){
		var args = [this, this.image, this.scroller];
		new Drag(this.scroller, {
			style: false,
			invert: true,
			modifiers: {x: 'scrollLeft', y: 'scrollTop'},
			onDrag: function(){ this.fireEvent('onChange', args); }.bind(this),
			onComplete: function(){ this.fireEvent('onComplete', args); }.bind(this)
		});
	},
	
	serialize: function(){
		return {
			top: this.scroller.scrollTop - this.image.style.marginTop.toInt(),
			left: this.scroller.scrollLeft - this.image.style.marginLeft.toInt(),
			width: this.image.width,
			height: this.image.height,
			scale: this.image[this.properties[0]] / this.initial
		};
	}
	
});/*
Class: InlineEditor
  Simple inline editor
*/
var InlineEditor = new Class({
  
  Implements: [Events, Options],
  
  options: {
    rows: false,
    className: 'inline-edit',
    saveText: 'Save',
    cancelText: 'Cancel',
    replaceTags: {}
  },
  
  initialize: function(element, options){
    this.setOptions(options);
    this.element = $(element).setStyle('cursor', 'pointer').addEvent('mousedown', this.edit.bind(this));
    this.textarea = this.options.rows && this.options.rows > 1;
    this.create();
  },
  
  create: function(){
    var styles = this.element.getStyles('color', 'font-size', 'font-weight', 'line-height');
    this.input = this.textarea ? new Element('textarea', { rows: this.options.rows }) : new Element('input', { type: 'text' });
    this.input.setStyles(styles);
    if (!this.textarea) this.input.addEvent('keyup', function(event){
      if (event.key == 'enter') this.respond(null, 'Save');
    }.bind(this));
    
    this.saveElement = new Element('input', {
      type: 'button',
      value: this.options.saveText
    }).addEvent('click', this.respond.bindWithEvent(this, 'Save'));
    
    this.cancelElement = new Element('a', {
      href: "#",
      text: this.options.cancelText
    }).addEvent('click', this.respond.bindWithEvent(this, 'Cancel'));
    
    this.container = new Element('div', { 'class': this.options.className }).inject(this.element, 'after').hide(true);
    this.container.adopt(this.input, this.saveElement, new Element('span', { text: 'or' }), this.cancelElement);
  },
  
  edit: function(){
    this.input.value = this.element.get('text');
    this.toggle(true);
  },
  
  respond: function(event, action){
    if (event) event.stop();
    this.fireEvent('on' + action, [this.element, this.input]);
    this.toggle(false);
  },
  
  toggle: function(state){
    this.element.toggle(true, !state);
    this.container.toggle(true, state);
    if (state){
      if (!this.textarea) this.input.select();
      this.input.highlight();
      this.input.focus();
    }
  }
  
});

/*
Class: InlineEditor.Request
  Adds a request object to the InlineEditor class
*/
InlineEditor.Request = new Class({
  
  Extends: InlineEditor,
  
  options: {
    url: '',
    method: 'post'
  },
  
  initialize: function(element, options){
    this.parent(element, options);
    this.request = new Request({
      url: this.options.url,
      method: this.options.method
    });
  }

});/*
Class: Slideshow
  Simple sliding slideshow with page links for featured items on the homepage
*/

var Slideshow = new Class({
  
  Implements: [Events, Options],
  
  options: {/*
    onShow: $empty,
    onStop: $empty,
    onStart: $empty,*/
    links: false,
    duration: 10000
  },
  
  initialize: function(element, options){
    this.setOptions(options);
    this.element = $(element);
    this.pages = this.element.getChildren();
    this.numPages = this.pages.length;
    this.links = $$(this.options.links);
    
    this.element.addEvents({
      mouseenter: this.stop.bind(this, true),
      mouseleave: this.start.bind(this)
    });
    
    this.pages.each(function(page, index){
      page.setStyles({ left: index * 100 + '%', visibility: 'visible' });
    });
    
    this.links.each(function(link, index){
      link.addEvent('click', function(event){
        event.stop();
        this.stop();
        this.show(index);
      }.bind(this));
    }, this);
    
    this.scroll = new Fx.Scroll(this.element, { link:'cancel' });
    this.show(0, true).start();
  },
  
  show: function(page, set){
    page = (page >= this.numPages ? 0 : (page < 0 ? this.numPages - 1 : page));
    set = set || Math.abs(this.page - page) > 2;
    
    var pos = this.pages[page].getPosition(this.element);
    this.scroll[set ? 'set' : 'start'](pos.x, pos.y);
    this.page = page;
    
    if (this.options.links) this.links.removeClass('active')[page].addClass('active');
    return this.fireEvent('onShow');
  },
  
  next: function(){
    return this.show(this.page + 1);
  },
  
  prev: function(){
    return this.show(this.page - 1);
  },
  
  start: function(){
    $clear(this.timer);
    this.timer = this.next.periodical(this.options.duration, this);
    return this.fireEvent('onStart');
  },
  
  stop: function(){
    this.timer = $clear(this.timer);
    return this.fireEvent('onStop');
  },
  
  toggle: function(){
    return this[this.timer ? 'stop' : 'start']();
  }
  
});


/*
Class: Carousel
  Simple Carousel class for the homepage
*/
var Carousel = new Class({
  
  Implements: Options,
  
  options: {
    visible: 2,
    duration: 5000,
    property: 'top'
  },
  
  initialize: function(element, options){
    this.setOptions(options);
    this.element = $(element);
    this.elements = this.element.getChildren();
    
    this.index = 0;
    this.numElements = this.elements.length;
    this.numPages = (this.numElements / this.options.visible).ceil();
    this.scroll = new Fx.Scroll(this.element, {});
    this.property = this.options.property == 'top' ? 'scrollTop' : 'scrollLeft';
    
    this.element.addEvents({
      mouseenter: function(){ $clear(this.timer); }.bind(this),
      mouseleave: function(){ this.timer = this.next.periodical(this.options.duration, this); }.bind(this)
    }).fireEvent('mouseleave')[this.property] = 0;
  },
  
  next: function(){
    if (++this.index >= this.numPages) this.index = 0;
    var self = this, next = this.index * this.options.visible;
    this.scroll.toElement(this.elements[next]).chain(function(){
      if (next == 0) next = self.numElements;
      self.options.visible.times(function(i){
        self.elements[next - i - 1].inject(self.element);
      });
      self.element[self.property] = 0;
    });
  }
  
});


/*
Class: TabContainer
	Simple tabbed container
*/
var TabContainer = new Class({
	
	Implements: [Events, Options],

	options: {
		initial: 0,
		activeClass: 'active',
		tabSelector: 'ul.tabs li a',
		paneSelector: 'div.tab-panes div.pane'
	},

	initialize: function(container, options){
		this.setOptions(options);
		
		this.tabs = $(container).getElements(this.options.tabSelector);
		this.panes = $(container).getElements(this.options.paneSelector);
		
		this.tabs.each(function(tab, index){
			tab.addEvents({
				click: function(event){ event.stop(); },
				mousedown: this.swap.bind(this, index)
			});
		}, this);
		
		this.swap(this.options.initial || 0);
	},
	
	swap: function(index){
		if (this.active == index) return;
		this.tabs.removeClass(this.options.activeClass)[index].addClass(this.options.activeClass);
		this.panes.each(function(pane, i){ pane[index == i ? 'show' : 'hide'](true); }.bind(this));
		
		if (this.active != index) this.fireEvent('onSwap', [this.tabs[index], index]);
		this.active = index;
	}
	
});

/*
Class: MaxLength
  Creates a max length input or textarea
*/
var MaxLength = new Class({
  
  Implements: [Events, Options],
  
  options: {
    max: false,
    onChange: $empty
  },
  
  initialize: function(input, options){
    this.setOptions(options);
    this.input = $(input);
    this.max = this.options.max || this.input.get('limit').toInt();
    this.input.addEvent('keyup', this.check.bind(this));
  },
  
  check: function(){
    var length = this.input.value.length;
    if (length >= this.max) this.input.value = this.input.value.substr(0, this.max);
    this.fireEvent('onChange', [this.input, length]);
  }
  
});

/*
Class: Selectables
	Class which allows elements to be selected and serialized
*/
var Selectables = new Class({
	
	Implements: [Events, Options],

	options: {
		min: false,
		max: false,
		selected: [],
		selectedClass: 'selected',
		onChange: $empty
	},

	initialize: function(selector, options){
		this.setOptions(options);
		this.items = $$(selector);
		this.items.each(this.add.bind(this));
		
		this.selected = [];
		var selected = this.options.selected;
		if ($type(selected) == 'number') selected = [this.items[selected]];
		if (this.options.max && selected.length > this.options.max) selected.length = this.options.max;
		selected.each(this.select.bind(this));
	},
	
	add: function(item){
		item.addEvents({
			'click': function(event){ event.stop(); },
			'mousedown': function(event){
				this[(item.hasClass(this.options.selectedClass) ? 'de' : '') + 'select'](item);
				event.stop();
			}.bind(this)
		});
	},
	
	select: function(item){
		if (item && (this.options.max && this.selected.length >= this.options.max)) this.deselect();
		item.addClass(this.options.selectedClass);
		this.selected.push(item);
		this.fireEvent('onChange', [this.selected, item]);
	},
	
	deselect: function(item){
		if (item && (this.options.min && this.selected.length <= this.options.min)) return;
		item = item || this.selected[0];
		item.removeClass(this.options.selectedClass);
		this.selected.erase(item);
		this.fireEvent('onChange', [this.selected, item]);
	},
	
	selectAll: function(items){
		(items || this.items).each(this.select.bind(this));
	},
	
	deselectAll: function(items){
		(items || this.items).each(this.deselect.bind(this));
	},
	
	serialize: function(modifier){
		return this.selected.map(modifier || function(item){
			return item.id;
		});
	}
	
});

/*
Class: Select
	Class which emulates a select html element optionally from an existing one
*/
var Select = new Class({

	Implements: [Events, Options],
	
	options: {
		className: 'select'
	},
	
	initialize: function(element, items, options){
		element = $(element);
		if (!element) return;
		else if (element.get('tag') == 'select'){
			var select = element;
			options = items;
			items = {};
			select.getChildren().each(function(option, index){
				items[option.value || index] = option.innerHTML;
			});
			element = new Element('input', { type: 'text', readonly: 'readonly', value: select.options[select.selectedIndex].text });
			element.replaces(select);
		}
		this.setOptions(options);
		
		this.element = element.addEvents({
			mousedown: this.showMenu.bind(this)
		}).addClass(this.options.className);
		
		this.menu = new Menu(items, {
			onSelect: function(value, key, element){
				this.menu.hide();
				this.element.value = value;
				this.fireEvent('onSelect', [value, key, element]);
			}.bind(this)
		});
		
		document.addEvent('mousedown', function(event){
			if (event.target !== this.element) this.menu.hide();
		}.bind(this));
	},
	
	showMenu: function(){
		var coords = this.element.getCoordinates();
		this.menu.element.setStyles({
			top: coords.bottom,
			left: coords.left,
			width: coords.width - 2
		});
		this.menu.show();
	}
	
});

/*
Class: Menu
	Class to create an interactive menu
*/
var Menu = new Class({
	
	Implements: [Events, Options],
	
	options: {
		className: 'menu',
		selectedClass: 'selected',
		fxOptions: {
			link: 'cancel',
			duration: 300
		}
	},
	
	initialize: function(items, options){
		this.setOptions(options);
		this.items = [];
		this.enabled = false;
		this.selected = null;
		this.element = new Element('ul', {
			'class': this.options.className,
			'styles': {
				right: '0px',
				position: 'absolute',
				visibility: 'hidden'
			}
		}).set('tween', this.options.fxOptions).fade('hide').inject(document.body, 'top');
		
		$each(items, function(item, key){
			this.addItem(item, key);
		}, this);
		
		window.addEvent('keydown', function(event){
			if (!this.enabled || !$chk(this.selected)) return;
			switch(event.key){
				case 'up': this.prevItem(); break;
				case 'down': this.nextItem(); break;
				case 'tab': case 'enter': this.selectItem(); break;
			}
		}.bind(this));
	},
	
	show: function(set){
		this.enabled = true;
		this.element.fade(set ? 'show' : 'in');
		return this;
	},
	
	hide: function(set){
		this.enabled = false;
		this.element.fade(set ? 'hide' : 'out');
		return this;
	},
	
	blur: function(item){
		this.enabled = false;
		this.selected = null;
		this.items.each(function(item){
			item.element.removeClass(this.options.selectedClass);
		}, this);
	},
	
	addItem: function(value, key){
		var item = {
			key: key,
			value: value,
			element: new Element('li').set('html', value)
		}, index = this.items.length;
		
		item.element.addEvents({
			mousedown: this.selectItem.bind(this, index),
			mouseenter: this.highlightItem.bind(this, index)
		});
		
		this.items.push(item);
		this.element.adopt(item.element);
	},
	
	highlightItem: function(index){
		if (index == this.selected || index < 0 || index >= this.items.length) return;
		this.items[this.selected || 0].element.removeClass(this.options.selectedClass);
		this.items[index].element.addClass(this.options.selectedClass);
		this.selected = index;
	},
	
	nextItem: function(){
		this.highlightItem(this.selected + 1);
	},
	
	prevItem: function(){
		this.highlightItem(this.selected - 1);
	},
	
	selectItem: function(item){
		if (!item) item = this.selected;
		if ($type(item) == 'number') item = this.items[item];
		this.fireEvent('onSelect', [item.value, item.key, item.element]);
	}

});

/*
Class: Mask
	Class for creating a modal mask over an element
*/
var Mask = new Class({
	
	Implements: [Events, Options],

	options: {
		styles: { },
		opacity: 0.8,
		container: 'mask',
		className: 'mask',
		properties: ['top', 'height'],
		fx: { duration: 500, transition: 'quint:out', link: 'cancel' }
	},
	
	initialize: function(element, options){
		this.setOptions(options);
		this.element = $(element);
		this.mask = new Element('div', {
			'class'  : this.options.className,
			'styles' : this.options.styles
		}).inject(this.options.container);
		
		this.effect = new Fx.Morph(this.mask, this.options.fx);
		this.hide(true);
	},
	
	toggle: function(set){
		this[this.visible ? 'hide' : 'show'](set);
	},
	
	show: function(set, opacity){
		if (this.visible && !opacity) return;
		this.visible = true;
		
		var coords = this.element.getCoordinates(),
		    target = { opacity: (opacity || this.options.opacity) },
		    initial = { opacity: 0 };
		
		this.options.properties.each(function(property){ initial[property] = coords[property]; });
		this.effect.cancel().set(initial)[set ? 'set' : 'start'](target);
	},
	
	hide: function(set){
		if (!this.visible) return;
		this.visible = false;
		
		this.effect[set ? 'set' : 'start']({ opacity: 0 });
	}
	
});

Element.Properties.mask = {

	set: function(options){
		return this.store('mask', new Mask(this, options));
	},

	get: function(options){
		if (options || !this.retrieve('mask')) this.set('mask', options);
		return this.retrieve('mask');
	}

};

Element.implement({

	mask: function(set, opacity){
		this.get('mask').show(set, opacity);
		return this;
	},

	unmask: function(set){
		this.get('mask').hide(set);
		return this;
	}

});var Roar = new Class({

	Implements: [Options, Events],

	options: {
		duration: 5000,
		position: 'bottomLeft',
		relativeTo: document,
		reposFx: {},
		itemFx: { transition: Fx.Transitions.Back.easeOut },
		offset: {x: 0, y: 0},
		itemOffset: 10,
		className: 'roar',
		title: null,
		container: false
	},

	initialize: function(options){
		this.setOptions(options);
		this.align = this.options.position;
		if ($type(this.align) == 'string'){
			var align = {x: 'center', y: 'center'};
			this.itemAlign = {x: 'left', y: 'top'};
			if ((/left|west/i).test(this.align)) align.x = 'left';
			else if ((/right|east/i).test(this.align)) this.itemAlign.x = align.x = 'right';
			if ((/upper|top|north/i).test(this.align)) align.y = 'top';
			else if ((/bottom|lower|south/i).test(this.align)) this.itemAlign.y = align.y = 'bottom';
			this.align = align;
		};
		this.rel = $(this.options.relativeTo);
	},

	alert: function(title, message, event){
		if (!this.wrap) this.build();
		var item = new Element('div', {
			'class': this.options.className
		}).adopt(
			new Element('div', {
				'class': 'roar-bg',
				'styles': {opacity: 0.7}
			}),
			new Element('h3').set('html', title),
			new Element('p').set('html', message)
		).setStyle(this.itemAlign.x, 0).setStyle(this.itemAlign.y, (this.current || 0));
		this.fxs.elements.push(item);
		if (this.options.duration) this.remove.delay(this.options.duration, this, item);
		var fx = new Fx.Tween(item, { property: 'opacity', duration: 200 }).set(0);
		if (event) item.addEvent('click', event);
		item.addEvent('click', this.remove.pass(item, this)).inject(this.wrap);
		fx.start(1);
		this.update();
	},

	persist: function(title, message, event){
		if (!this.prev) this.prev = {title: title, message: message};
		else if (this.prev.title == title && this.prev.message == message) return;
		
		var duration = this.options.duration;
		this.options.duration = 300000;
		this.alert(title, message, event || function(){
		  this.prev = null;
		}.bind(this));
		this.options.duration = duration;
	},

	remove: function(item){
		var index = this.fxs.elements.indexOf(item);
		if (index == -1) return;
		this.fxs.cancel().elements.splice(index, 1);
		new Fx.Tween(item.removeEvents('click'), { property: 'opacity' }).start(1, 0).chain(item.destroy.bind(item));
		this.update();
	},

	update: function(){
		this.current = 0;
		if (this.fxs.elements.length) this.fxs.start(this.fxs.elements.reduce(function(obj, el, i){
			obj[i] = {};
			obj[i][this.itemAlign.y] = this.current;
			this.current += el.offsetHeight + this.options.itemOffset;
			return obj;
		}.bind(this), {}));
	},

	build: function(){
		this.wrap = new Element('div', {'class': 'roar-wrap'}).inject(this.options.container || document.body);
		this.moveTo = this.wrap.setStyles.bind(this.wrap);
		this.repos();
		if (this.options.reposFx){
			this.reposFx = new Fx.Morph(this.wrap, $merge({
				unit: 'px',
				wait: false,
				transition: Fx.Transitions.Circ.easeOut
			}, this.options.reposFx));
			this.moveTo = this.reposFx.start.bind(this.reposFx);
		}
		window.addEvents({
			scroll: this.repos.bind(this),
			resize: this.repos.bind(this)
		});
		this.fxs = new Fx.Elements([], $merge({
			unit: 'px',
			wait: false,
			transition: Fx.Transitions.Circ.easeOut
		}, this.options.itemFx));
	},

	repos: function(){
		var scroll = document.getScroll();
		var size = document.getSize();
		scroll = {
			top: scroll.y,
			left: scroll.x,
			right: scroll.x + size.x,
			bottom: scroll.y + size.y
		};
		var rel = ($type(this.rel) == 'element') ? this.rel.getCoordinates() : scroll;
		var offset = this.options.offset;
		this.moveTo({
			left: (this.align.x == 'left')
				? Math.max(rel.left, scroll.left) + offset.x
				: Math.min(rel.right, scroll.right) - offset.x,
			top: (this.align.y == 'top')
				? Math.max(rel.top, scroll.top) + offset.y
				: Math.min(rel.bottom, scroll.bottom) - offset.y
		});
	}

});

Roar.Queue = new Class({ });/* Swiff.Uploader - Flash FileReference Control
 * @version		1.2
 * @license		MIT License
 * @author		Harald Kirschner <mail [at] digitarald [dot] de>
 * @copyright	Authors
 */

Swiff.Uploader = new Class({

	Extends: Swiff,

	Implements: Events,

	options: {
		path: 'Swiff.Uploader.swf',
		multiple: true,
		queued: true,
		typeFilter: null,
		url: null,
		method: 'post',
		data: null,
		fieldName: 'Filedata',
		target: null,
		height: '100%',
		width: '100%',
		callBacks: null,
		styles: {
			top: '0px',
			left: '0px',
			height: '16px',
			position: 'absolute'
		}
	},

	initialize: function(options){
		if (Browser.Plugins.Flash.version < 9) return false;
		this.setOptions(options);

		var callBacks = this.options.callBacks || this;
		if (callBacks.onLoad) this.addEvent('onLoad', callBacks.onLoad);
		if (!callBacks.onBrowse) {
			callBacks.onBrowse = function() {
				return this.options.typeFilter;
			};
		}

		var prepare = {}, self = this;
		['onBrowse', 'onSelect', 'onAllSelect', 'onCancel', 'onBeforeOpen', 'onOpen', 'onProgress', 'onComplete', 'onError', 'onAllComplete'].each(function(index) {
			var fn = callBacks[index] || $empty;
			prepare[index] = function() {
				self.fireEvent(index, arguments, 10);
				return fn.apply(self, arguments);
			};
		});

		prepare.onLoad = this.load.create({delay: 10, bind: this});
		this.options.callBacks = prepare;

		var path = this.options.path;
		if (!path.contains('?')) path += '?noCache=' + $time(); // quick fix

		this.parent(path);

		var styles = this.options.styles;
		for (var style in styles) this.object.style[style] = styles[style];

		this.inject($(this.options.container) || document.body);

		return this;
	},

	load: function(){
		this.remote('register', this.instance, this.options.multiple, this.options.queued);
		this.fireEvent('onLoad');

		this.target = $(this.options.target);
		if (Browser.Plugins.Flash.version >= 9 && this.target) {
			this.reposition();
			window.addEvent('resize', this.reposition.bind(this));
		}
	},

	reposition: function() {
		var pos = this.target.getCoordinates(this.box.getOffsetParent());
		this.box.setStyles(pos);
	},

	browse: function(typeFilter){
		this.options.typeFilter = $pick(typeFilter, this.options.typeFilter);
		return this.remote('browse');
	},

	upload: function(options){
		var current = this.options;
		options = $extend({data: current.data, url: current.url, method: current.method, fieldName: current.fieldName}, options);
		if ($type(options.data) == 'element') options.data = $(options.data).toQueryString();
		return this.remote('upload', options);
	},

	removeFile: function(file){
		if (file) file = {name: file.name, size: file.size};
		return this.remote('removeFile', file);
	},

	getFileList: function(){
		return this.remote('getFileList');
	}

});




/* FancyUpload - Flash meets Ajax for powerful and elegant uploads.
 * @version		2.1
 * @license		MIT License
 * @author		Harald Kirschner <mail [at] digitarald [dot] de>
 * @copyright	Authors
 */

var FancyUpload2 = new Class({

	Extends: Swiff.Uploader,

	options: {
		limitSize: false,
		limitFiles: 5,
		instantStart: false,
		allowDuplicates: false,
		validateFile: $lambda(true), // provide a function that returns true for valid and false for invalid files.
		debug: false,

		fileInvalid: null, // called for invalid files with error stack as 2nd argument
		fileCreate: null, // creates file element after select
		fileUpload: null, // called when file is opened for upload, allows to modify the upload options (2nd argument) for every upload
		fileComplete: null, // updates the file element to completed state and gets the response (2nd argument)
		fileRemove: null // removes the element
		/**
		 * Events:
		 * onBrowse, onSelect, onAllSelect, onCancel, onBeforeOpen, onOpen, onProgress, onComplete, onError, onAllComplete
		 */
	},

	initialize: function(status, list, options) {
		this.status = $(status);
		this.list = $(list);

		this.files = [];

		if (options.callBacks) {
			this.addEvents(options.callBacks);
			options.callBacks = null;
		}
		
		this.parent(options);
		this.render();
	},

	render: function() {
		this.overallTitle = this.status.getElement('.overall-title');
		this.currentTitle = this.status.getElement('.current-title');
		this.currentText = this.status.getElement('.current-text');

		var progress = this.status.getElement('.overall-progress');
		this.overallProgress = new Fx.ProgressBar(progress, {
			text: new Element('span', {'class': 'progress-text'}).inject(progress, 'after')
		});
		progress = this.status.getElement('.current-progress');
		this.currentProgress = new Fx.ProgressBar(progress, {
			text: new Element('span', {'class': 'progress-text'}).inject(progress, 'after')
		});
	},

	onLoad: function() {
		this.log('Uploader ready!');
	},

	onBeforeOpen: function(file, options) {
		this.log('Initialize upload for "{name}".', file);
		var fn = this.options.fileUpload;
		var obj = (fn) ? fn.call(this, this.getFile(file), options) : options;
		return obj;
	},

	onOpen: function(file, overall) {
		this.log('Starting upload "{name}".', file);
		file = this.getFile(file);
		file.element.addClass('file-uploading');
		this.currentProgress.cancel().set(0);
		this.currentTitle.set('html', 'File Progress "{name}"'.substitute(file) );
	},

	onProgress: function(file, current, overall) {
		this.overallProgress.start(overall.bytesLoaded, overall.bytesTotal);
		this.currentText.set('html', 'Upload with {rate}/s. Time left: ~{timeLeft}'.substitute({
			rate: (current.rate) ? this.sizeToKB(current.rate) : '- B',
			timeLeft: Date.fancyDuration(current.timeLeft || 0)
		}));
		this.currentProgress.start(current.bytesLoaded, current.bytesTotal);
	},

	onSelect: function(file, index, length) {
		var errors = [];
		if (this.options.limitSize && (file.size > this.options.limitSize)) errors.push('size');
		if (this.options.limitFiles && (this.countFiles() >= this.options.limitFiles)) errors.push('length');
		if (!this.options.allowDuplicates && this.getFile(file)) errors.push('duplicate');
		if (!this.options.validateFile.call(this, file, errors)) errors.push('custom');
		if (errors.length) {
			var fn = this.options.fileInvalid;
			if (fn) fn.call(this, file, errors);
			return false;
		}
		(this.options.fileCreate || this.fileCreate).call(this, file);
		this.files.push(file);
		return true;
	},

	onAllSelect: function(files, current, overall) {
		this.log('Added ' + files.length + ' files, now we have (' + current.bytesTotal + ' bytes).', arguments);
		this.updateOverall(current.bytesTotal);
		this.status.removeClass('status-browsing');
		if (this.files.length && this.options.instantStart) this.upload.delay(10, this);
	},

	onComplete: function(file, response) {
		this.log('Completed upload "' + file.name + '".', arguments);
		this.currentText.set('html', 'Upload complete!');
		this.currentProgress.start(100);
		(this.options.fileComplete || this.fileComplete).call(this, this.finishFile(file), response);
	},

	onError: function(file, error, info) {
		this.log('Upload "' + file.name + '" failed. "{1}": "{2}".', arguments);
		(this.options.fileError || this.fileError).call(this, this.finishFile(file), error, info);
	},

	onCancel: function() {
		this.log('Filebrowser cancelled.', arguments);
		this.status.removeClass('file-browsing');
	},

	onAllComplete: function(current) {
		this.log('Completed all files, ' + current.bytesTotal + ' bytes.', arguments);
		this.updateOverall(current.bytesTotal);
		this.overallProgress.start(100);
		this.status.removeClass('file-uploading');
	},

	browse: function(fileList) {
		var ret = this.parent(fileList);
		if (ret !== true){
			if (ret) this.log('An error occured: ' + ret);
			else this.log('Browse in progress.');
		} else {
			this.log('Browse started.');
			this.status.addClass('file-browsing');
		}
	},

	upload: function(options) {
		var ret = this.parent(options);
		if (ret !== true) {
			this.log('Upload in progress or nothing to upload.');
			if (ret) alert(ret);
		} else {
			this.log('Upload started.');
			this.status.addClass('file-uploading');
			this.overallProgress.set(0);
		}
	},

	removeFile: function(file) {
		var remove = this.options.fileRemove || this.fileRemove;
		if (!file) {
			this.files.each(remove, this);
			this.files.empty();
			this.updateOverall(0);
		} else {
			if (!file.element) file = this.getFile(file);
			this.files.erase(file);
			remove.call(this, file);
			this.updateOverall(this.bytesTotal - file.size);
		}
		this.parent(file);
	},

	getFile: function(file) {
		var ret = null;
		this.files.some(function(value) {
			if ((value.name != file.name) || (value.size != file.size)) return false;
			ret = value;
			return true;
		});
		return ret;
	},

	countFiles: function() {
		var ret = 0;
		for (var i = 0, j = this.files.length; i < j; i++) {
			if (!this.files[i].finished) ret++;
		}
		return ret;
	},

	updateOverall: function(bytesTotal) {
		this.bytesTotal = bytesTotal;
		this.overallTitle.set('html', 'Overall Progress (' + this.sizeToKB(bytesTotal) + ')');
	},

	finishFile: function(file) {
		file = this.getFile(file);
		file.element.removeClass('file-uploading');
		file.finished = true;
		return file;
	},

	fileCreate: function(file) {
		file.info = new Element('span', {'class': 'file-info'});
		file.element = new Element('li', {'class': 'file'}).adopt(
			new Element('span', {'class': 'file-size', 'html': this.sizeToKB(file.size)}),
			new Element('a', {
				'class': 'file-remove',
				'href': '#',
				'html': 'Remove',
				'events': {
					'click': function() {
						this.removeFile(file);
						return false;
					}.bind(this)
				}
			}),
			new Element('span', {'class': 'file-name', 'html': file.name}),
			file.info
		).inject(this.list);
	},

	fileComplete: function(file, response) {
		this.options.processResponse || this;
		var json = $H(JSON.decode(response, true));
		if (json.get('result') == 'success') {
			file.element.addClass('file-success');
			file.info.set('html', json.get('size'));
		} else {
			file.element.addClass('file-failed');
			file.info.set('html', json.get('error') || response);
		}
	},

	fileError: function(file, error, info) {
		file.element.addClass('file-failed');
		file.info.set('html', '<strong>' + error + '</strong><br />' + info);
	},

	fileRemove: function(file) {
		file.element.fade('out').retrieve('tween').chain(Element.destroy.bind(Element, file.element));
	},

	sizeToKB: function(size) {
		var unit = 'B';
		if ((size / 1048576) > 1) {
			unit = 'MB';
			size /= 1048576;
		} else if ((size / 1024) > 1) {
			unit = 'kB';
			size /= 1024;
		}
		return size.round(1) + ' ' + unit;
	},

	log: function(text, args) {
		if (this.options.debug && window.console) console.log(text.substitute(args || {}));
	}

});

/**
 * @todo Clean-up, into Date.js
 */
Date.parseDuration = function(sec) {
	var units = {}, conv = Date.durations;
	for (var unit in conv) {
		var value = Math.floor(sec / conv[unit]);
		if (value) {
			units[unit] = value;
			if (!(sec -= value * conv[unit])) break;
		}
	}
	return units;
};

Date.fancyDuration = function(sec) {
	var ret = [], units = Date.parseDuration(sec);
	for (var unit in units) ret.push(units[unit] + Date.durationsAbbr[unit]);
	return ret.join(', ');
};

Date.durations = {years: 31556926, months: 2629743.83, days: 86400, hours: 3600, minutes: 60, seconds: 1, milliseconds: 0.001};
Date.durationsAbbr = {
	years: 'j',
	months: 'm',
	days: 'd',
	hours: 'h',
	minutes: 'min',
	seconds: 'sec',
	milliseconds: 'ms'
};
