/*
	Stuff found on the Internet
*/

function getRandomNumber() {
	return 4; 	// chosen by fair dice roll.
					// guaranteed to be random.
}

(function () { // shamefully stolen from somewhere and then lost the link
	var methods = {
		defaultValueActsAsHint: function (element) {
			element = $(element);
			element._default = element.value;
			return element.observe('focus', function () {
				if (element._default != element.value) {
					return;
				}
				element.removeClassName('hint').value = '';
				element.setStyle({color: '#333'});
			}).observe('blur', function () {
				if (element.value.strip() !== '') {return;}
				element.addClassName('hint').value = element._default;
				element.setStyle({color: '#888'});
			}).addClassName('hint');
		}
	};
	$w('input textarea').each(function (tag) {Element.addMethods(tag, methods);});
})();


/* My code starts here!

Animebox Universe: http://www.animebox.eu/universe
Git repo: git://github.com/KazeNoKoe/Animebox-Universe.git

Copyright 2009 Walter Palagi <lepaca@animebox.eu>. All Rights Reserved.
This work is distributed under the GPLv3 Software License [1] in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

[1] http://www.gnu.org/copyleft/gpl.html
*/


Array.prototype.sortInsert = function (item, prop) {
/* 	Insert an element in an array keeping it sorted.
	item is the element to add, prop is the property
	that should be compared between different items.
*/
	for (var i = 0, l = this.length, didit = false; i < l; ++i) {
		if (this[i][prop] > item[prop]) {
			this.splice(i, 0, item);
			didit = true;
			break;
		}
	}
	if (!didit) {
		this.push(item);
	}
	return i;
};

Array.prototype.getRandomItem = function () {
	return this[Math.floor(Math.random()*this.length)];
}


/*	---------------------------
	Configuration
	---------------------------
*/

var Config = {
/* Application configuration
*/
	aspectLabel: ['n/a', 'standard', 'wide', 'SCAN'],
	dimensionLabel: ['1024px wide or more', '1280px wide or more', '1680px wide or more', '1920px wide or more'],
	sourceLabel: ['konachan', 'danbooru', 'imouto'], /*'animebox',*/
	sortLabel: ['none', 'dimension', 'aspect', 'source'],
	spinner: 'ajax_loader.gif',
	backgrounds: [
		{fname:'abox-bg', align:'bottom right'},
		{fname:'akira01', align:'bottom right'},
		{fname:'appleseed01', align:'bottom right'},
		{fname:'azumanga01', align:'bottom left'},
		{fname:'azumanga02', align:'bottom left'},
		{fname:'bebop02', align:'bottom right'},
		{fname:'beck', align:'bottom right'},
		{fname:'blacklagoon01', align:'bottom left'},
		{fname:'bladeimm', align:'bottom left'},
		{fname:'blame01', align:'bottom left'},
		{fname:'blame02', align:'bottom right'},
		{fname:'boogiepop01', align:'bottom left'},
		{fname:'souleater01', align:'bottom left'},
		{fname:'tenjoutenge01', align:'bottom left'},
		{fname:'tenjoutenge02', align:'bottom right'},
		{fname:'vagrant01', align:'bottom right'},
		{fname:'vagrant02', align:'bottom left'},
		{fname:'vagrant03', align:'bottom right'},
		{fname:'vagrant04', align:'bottom right'}
	],
	inspect: function () {
		return 'This object contains the configuration of this webapp.'
	}
};


/*	-------------------------------
	Core
	-------------------------------
*/

var Core = (function () {

	var ElementInterface = {
	/* 	Essential methods that should be added to any object that controls a DOM element.
	*/
		fxParms: {
			Appear: {duration: 1},
			Fade: {duration: 1}
		},
		preInsert: function () {
			return;
		},  // pre- and post-insert: functions called before and after the elm has been created
		postInsert: function () {
			return;
		},
		makeAndInsert: function (tgt, where) {
		/*	Create the element and insert it somewhere.
		*/
			this.preInsert();
			if (!this.hidden) {
				this.hidden = $('hidden');
				if (!this.hidden) {
					$(document.body).insert('<div style="display: none;" id="hidden"></div>');
					this.hidden = $('hidden');
				}
			}
			if (this.html.evaluate) {
				this.hidden.update(this.html.evaluate(this));
			} else {
				this.hidden.update(this.html);
			}
			this.elm = this.hidden.down();
			//this.elm.store('obj', this);
			this.insert(tgt, where);
			this.postInsert();
		},
		insert: function (tgt, where) {
		// Insert the element somewhere
			var insertion = {};
			if (where) {
				insertion[where] = this.elm;
			} else {
				insertion.bottom = this.elm;
			}
			$(tgt).insert(insertion);
		},
		show: function () {
			if (this.elm) {
				this.elm.show();
			}
		},
		hide: function () {
			if (this.elm) {
				this.elm.hide();
			}
		},
		fx: function (fxName) {
		// Start an effect with preset params
			return new Effect[fxName](this.elm, this.fxParms[fxName]);
		},
		die: function () {
		// Kill the elm and clear listeners
			if (this.elm) {
				this.elm.stopObserving();
				this.elm.remove();
			}
		},
		inspect: function () {
			return 'ElementInterface: this is a mixin that stores functions that every object that controls a DOM element should have.';
		}
	};

	var mix = function () {
	/* 	Merge the properties of all the objects given as arguments
	into the first object, making sure only the first object is modified.
	Of all the properties with the same name belonging to different
	objects, the one belonging to the rightmost object wins; that
	is, precedence goes right to left.
	*/
		var args = $A(arguments);
		if (!args[0]) {
			args[0] = {};
		}
		for (l = args.length, i = 1; i < l; ++i) {
			Object.extend(args[0], args[i]);
		}
	};

	return {
		ElementInterface: ElementInterface,
		mix: mix
	};
}());


/* ----------------------------------
	INTERFACE ELEMENTS
   ----------------------------------
*/

var Interface = (function () {

	var Trigger = function () {
		Core.mix(this, {
			event: 'click',
			callback: null,
			html: '<div class="trigger">Click me!!!</div>'
		}, arguments[0] || null);
	}

	Core.mix(Trigger.prototype, Core.ElementInterface, {
		postInsert: function () {
			if (this.event && this.callback) {
				this.elm.observe(this.event, this.callback);
			}
		},
		inspect: function () {
			return 'Trigger: a simple element that triggers an action when clicked.'
		}
	});

	var InputBox = function () {
		Core.mix(this,{
			defaultValue: 'Search!',
			extraClassNames: ['searchBox'],
			callback: this.onKeyUpEvt.bind(this)
		}, arguments[0] || null);
		this.html = '<div class="inputBox"><input class="inputBox' + (this.extraClassNames && this.extraClassNames.length ? ' ' + this.extraClassNames.join(' ') : '') + '" type="text" value="' + this.defaultValue + '" /><img src="/universe/assets/img/clear.png" /></div>';
	};

	Core.mix(InputBox.prototype, Core.ElementInterface, {
		postInsert: function () {
			this.elm.down('input').defaultValueActsAsHint();
			this.elm.down('input').observe('keyup', this.callback);
			this.elm.down('img').observe('click', this.clear.bind(this));
		},
		onKeyUpEvt: function (evt) {
			if (evt.keyCode === 13 && this.elm.down('input').value.length > 3) {
				universe.search(this.elm.down('input').value);
			}
		},
		clear: function () {
			this.elm.down('input').value = '';
			this.elm.down('input').focus();
		},
		inspect: function () {
			return 'InputBox: a text box that calls a function when enter is pressed.'
		}
	});

	var ActiveText = function () {
		Core.mix(this, {
			content: '[ change ]',
			callback: false
		}, arguments[0] || null);
		this.html = '<span class="activeWord' + (this.callback ? ' clickable' : '') + '">' + this.content + '</span>';
	};

	Core.mix(ActiveText.prototype, Core.ElementInterface, {
		postInsert: function () {
			if (this.callback) {
				this.elm.observe('click', this.callback);
			}
		},
		inspect: function () {
			return 'ActiveText: a word that may trigger an action when clicked. Will eventually be merged with Trigger.'
		}
	});

	var ActiveSentence = function () {
		Core.mix(this, {
			tokens: [
				/*{content: 'Don\'t filter anything!'},
				{content: '[ change ]', callback: function () {}}*/
			]
		}, arguments[0] || null);
		this.html = '<span class="activeSentence"></span>';
	}

	Core.mix(ActiveSentence.prototype, Core.ElementInterface, {
		postInsert: function () {
			this.tokens.each((function (token) {
				(new ActiveText(token)).makeAndInsert(this.elm);
			}).bind(this));
		},
		inspect: function () {
			return 'ActiveSentence: a container for many ActiveText instances.'
		}
	});
	
	var FilteringRule = function () {
		Core.mix(this, {
			parent: null
		}, arguments[0] || null);
		this.html = '<p class="filteringRule"></p>';
		this.initialSentence = new ActiveSentence({
			tokens: [
				{content: '<span class="disabled">&bull;</span> Filter the wallpapers [ '},
				{content: 'by size', callback: this.buildLevel2.bind(this, 'dimension')},
				{content: ' | '},
				{content: 'by aspect ratio', callback: this.buildLevel2.bind(this, 'aspect')},
				{content: ' | '},
				{content: 'by source', callback: this.buildLevel2.bind(this, 'source')},
				{content: ' ]'},
				{content: ' [ remove ]', callback: this.remove.bind(this)}
			]
		});
		this.sentence = this.initialSentence;
	}

	Core.mix(FilteringRule.prototype, Core.ElementInterface, {
		postInsert: function () {
			this.sentence.makeAndInsert(this.elm);
		},
		buildLevel2: function (method) {
			var pre = method == 'aspect' ? 'with a ' : (method == 'source' ? 'coming from ' : 'that are '),
			level2tokens = [
				{content: '<span class="disabled">&bull;</span> Show me <span style="color: #55FF55;">only</span> wallpapers ' + pre},
				{content: '[ '}
			];
			Config[method + 'Label'].each((function (label, i) {
				level2tokens.push({
					content: (method == 'source' ? label.capitalize() : label),
					callback: this.set.bind(this, method, i)
				});
				level2tokens.push({content: ' | '});
			}).bind(this));
			level2tokens.pop();
			level2tokens.push({content: ' ]'});
			if (method == 'aspect') {
				level2tokens.push({content: ' aspect ratio'});
			}
			level2tokens.push({content: ' [ back ]', callback: this.reset.bind(this)});
			level2tokens.push({content: ' [ remove ]', callback: this.remove.bind(this)});
			this.sentence = new ActiveSentence({
				tokens: level2tokens
			});
			this.elm.update('');
			this.sentence.makeAndInsert(this.elm);
		},
		set: function (method, i) {
			var pre = method == 'aspect' ? 'with a ' : (method == 'source' ? 'coming from ' : 'that are ');
			this.method = method;
			this.rule = i;
			universe.interface.filter.buildFilterMap();
			universe.agents.filter.refilter();
			var tokens = [];
			tokens.push({content: '<span class="enabled">&bull;</span> Show me <span style="color: #55FF55;">only</span> wallpapers ' + pre + '<span style="color: #55FF55;">' + (method == 'source' ? Config.sourceLabel[i].capitalize() : Config[method + 'Label'][i]) + '</span>'});
			if (this.method == 'aspect') {
				tokens.push({content: ' aspect ratio'});
			}
			tokens.push({content: ' [ back ]', callback: (function () {
					this.reset();
					this.buildLevel2(method);
			}).bind(this)});
			tokens.push({content: ' [ change ]', callback: this.reset.bind(this)});
			tokens.push({content: ' [ remove ]', callback: this.remove.bind(this)});
			this.sentence = new ActiveSentence({
				tokens: tokens
			});
			this.elm.update('');
			this.sentence.makeAndInsert(this.elm);
		},
		reset: function () {
			this.method = null;
			this.rule = null;
			this.sentence = this.initialSentence;
			universe.interface.filter.buildFilterMap();
			universe.agents.filter.refilter();
			this.elm.update('');
			this.sentence.makeAndInsert(this.elm);
		},
		remove: function () {
			this.parent.removeRule(this);
		},
		inspect: function () {
			return 'FilteringRule: a control interface that sets a rule for Universe\'s filter.';
		}
	});


	var Filter = function () {
		this.html = '<div id="filter" style="display: none; left: ' + ((document.viewport.getWidth() - 850) / 2) + 'px"><img class="ok clickable" src="/universe/assets/img/ok.png" style="width: 30px;" /></div>';
		this.rules = [];
	}

	Core.mix(Filter.prototype, Core.ElementInterface, {
		postInsert: function () {
			this.elm.down('.ok').observe('click', this.hide.bind(this));
			this.addelm = new ActiveSentence({
				tokens: [
					{content: ' [ add a rule ]', callback: this.addRule.bind(this)}
				]
			});
			this.addelm.makeAndInsert(this.elm);
		},
		addRule: function () {
			var newRule = new FilteringRule({parent: this});
			newRule.makeAndInsert(this.elm);
			this.rules.push(newRule);
			this.rules.each((function(rule){
				if (rule && rule.insert) {
					rule.insert(this.elm);
				}
			}).bind(this));
			this.addelm.insert(this.elm);
		},
		removeRule: function (rule) {
			rule.die();
			this.rules[this.rules.indexOf(rule)] = null;
			this.buildFilterMap();
			universe.agents.filter.refilter();
		},
		buildFilterMap: function () {
			var map = {};
			this.rules.each(function (rule) {
				if (rule && rule.method) {
					if (!map[rule.method]) { 
						map[rule.method] = Config[rule.method + 'Label'].collect(function(){return false;});
					}
					map[rule.method][rule.rule] = true;
				}
			});
			this.map = map;
			universe.agents.filter.set(map);
		},
		inspect: function () {
			return 'Interface.Filter: main interface for Universe\'s filter and provider of the data  Agent.Filter needs to operate.';
		}
	});

	var Counter = function () {
		this.html = '<div id="counter"><span class="ready count"><span class="sourceName">Not yet on your screen:&nbsp;</span><span class="num">0</span></span>' + Config.sourceLabel.collect(function (source, i) {
			return '<span class="' + source + ' count"><span class="sourceName">' + (i === 0 ? 'From ' : '') + source.capitalize() + ': </span><span class="num">0</span> <span class="filtered"></span></span>';
		}).join('') + '<span class="total count"><span class="sourceName">On your screen: </span><span class="num">0</span> <span class="filtered"></span></span></div>';
	};

	Core.mix(Counter.prototype, Core.ElementInterface, {
		postInsert: function () {
			Config.sourceLabel.each((function (source) {
				this[source + 'Counter'] = {
					sName: this.elm.down('.' + source).down('.sourceName'),
					shown: this.elm.down('.' + source).down('.num'),
					filtered: this.elm.down('.' + source).down('.filtered')
				};
			}).bind(this));
			this.totalCounter = {
				shown: this.elm.down('.total').down('.num'),
				filtered: this.elm.down('.total').down('.filtered')
			};
			this.readyCounter = {
				shown: this.elm.down('.ready').down('.num'),
				filtered: {innerHTML: ''}
			};
 			document.observe('engine:search', this.fx.bind(this));
		},
		set: function (source, values) {
			this[source + 'Counter'].shown.innerHTML = values.shown;
			if (values.filtered) {
				this[source + 'Counter'].filtered.innerHTML = '(' + values.filtered + ')';
			}
		},
		reset: function () {
			$$('#counter .num').each(function (elm) {
				elm.innerHTML = 0;
			});
			$$('#counter .filtered').each(function (elm) {
				elm.innerHTML = '';
			});
		},
		inspect: function () {
			return 'Interface.Counter: controls the counters at the bottom of the page.'
		},
		fx: function (evt) { // memo.mode == 'start' || 'stop', memo.source = source
			this[evt.memo.source + 'Counter'].fx = true;
			this[evt.memo.mode + 'fx'](evt.memo.source);
		},
		startfx: function (source) {
			if (this[source + 'Counter'].fx) {
				new Effect.Morph(this[source + 'Counter'].sName, {
					style: {color: '#0f0'},
					duration: 2,
					fps: 10,
					afterFinish: this.revertfx.bind(this, source)
				});
			}
		},
		revertfx: function (source) {
			new Effect.Morph(this[source + 'Counter'].sName, {
				style: {color: '#fff'},
				duration: 2,
				fps: 10,
				afterFinish: this.startfx.bind(this, source)
			});
		},
		stopfx: function (source) {
			this[source + 'Counter'].fx = false;
		}
	});

	var Notifier = function () {
		Core.mix(this, {
			max: 5, // max number of notifications shown
			timeout: 8, // a notification disappears after this number of seconds
			html: '<div id="notifier" style="display: none;"></div>'
		}, arguments[0] || null);
	};

	Core.mix(Notifier.prototype, Core.ElementInterface, {
		notify: function (msg) {
			if (this.elm) {
				var notes = this.elm.getElementsBySelector('.notification');
				if (notes.length >= this.max) {
					notes.last().remove();
				}
				this.elm.insert({top: '<div class="notification" style="display: none;">' + msg + '</div>'});
				if (!this.elm.visible()) {
					this.fx('Appear');
				}
				var newNote = this.elm.down('div');
				new Effect.Appear(newNote, {duration: 1});
				this.removeNotification.bind(this).delay(this.timeout, newNote);
			}
		},
		removeNotification: function (note) {
			note.remove();
			var notes = this.elm.getElementsBySelector('.notification');
			if (notes.length === 0) {
				this.elm.hide();
			}
		}
	});

	
	
	return {
		Trigger: Trigger,
		InputBox: InputBox,
		/*ActiveText: ActiveText,
		ActiveSentence: ActiveSentence,
		FilteringRule: FilteringRule,*/
		Filter: Filter,
		Counter: Counter,
		Notifier: Notifier
	};

}());



/* 	------------------------------
	Content Elements
	------------------------------
*/

var Content = (function () {
	var Wallpaper = function () {
		Core.mix(this, {
			thumbUrl: '',
			view: '',
			width: 0,
			height: 0,
			source: ''
		}, arguments[0] || null);
		this.aspect = this.width / this.height;
		switch (this.aspect) {
		case 1.6:
			this.aspect = 2;
			break;
		case 16 / 9:
			this.aspect = 2;
			break;
		case 5 / 4:
			this.aspect = 1;
			break;
		case 4 / 3:
			this.aspect = 1;
			break;
		default:
			if (this.width > 3000) {
				this.aspect = 3;
			}
			else {
				this.aspect = 0;
			}
		}
		this.dimension = this.width < 1280 ? 0 : (this.width < 1680 ? 1 : (this.width < 1920 ? 2 : 3 ));
		this.hr_aspect = Config.aspectLabel[this.aspect];
		this.hr_source = Config.sourceLabel[this.source].capitalize();
		this.view = this.view.replace(/\?g2_GALLERYSID.*/, '');
		this.thumbUrl = this.thumbUrl.replace(/\?g2_GALLERYSID.*/, '');
		this.html = new Template('<div class="source">#{hr_source}</div><a class="thumb" target="_BLANK" href="#{view}"><img class="thumb" src="#{thumbUrl}" /></a><div class="info"><div class="resolution">#{width}x#{height}</div><div class="aspect">#{hr_aspect}</div></div>');
	};

	Core.mix(Wallpaper.prototype, Core.ElementInterface, {
		fxParms: null,
		getElement: function () {
			this.elm = document.createElement('div');
			this.elm.className="wallpaper";
			this.elm.innerHTML = this.html.evaluate(this);
			return this.elm;
		},
		postInsert: function () {
			if (this.thumbHeight) {
				var x = this.thumbHeight * 200 / this.thumbWidth;
				this.elm.down('img.thumb').setStyle('margin: ' + ((200 - x) / 2) + 'px 0 0;');
			}
			delete this.html;
		},
		toJSON: function () {
			return Object.toJSON({
				thumbUrl: this.thumbUrl,
				view: this.view,
				width: this.width,
				height: this.height,
				source: this.hr_source,
				aspect: this.hr_aspect
			});
		},
		inspect: function () {
			return 'Content.Wallpaper: not a movie nor a song, this is a wallpaper! Yay!';
		}
	});

	return {Wallpaper: Wallpaper};
}());



/*	-----------------------------------
	Search Engines
	-----------------------------------
*/

var SearchEngine = (function () {

	var Konachan = function () {
		this.sourceName = 'konachan';
		this.site = 'konachan.com';
	};

	var Danbooru = function () {
		this.sourceName = 'danbooru';
		this.site = 'danbooru.donmai.us';
	};

	var Imouto = function () {
		this.sourceName = 'imouto';
		this.site = 'moe.imouto.org';
	};

	Konachan.prototype = {
		baseURL: '/universe/index.php/danbooru/search/',
		tagURL: '/universe/index.php/danbooru/get_tags/',
		search: function (searchTerm) {
			document.fire('engine:search', {mode: 'start', source: this.sourceName});
			this.total = 0;
			this.finished = false;
			this.searchTerms = searchTerm.replace(':', '_');
			this.searchTerms = this.searchTerms.split(' ');
			this.searchTerms.unshift(searchTerm.replace(/[: ]/g, '_'));
			universe.agents.scheduler.add({
				url: this.tagURL,
				parms: {
					method: 'post',
					parameters: {pattern: this.searchTerms.shift().toLowerCase(), site: this.site}
				},
				callback: this.processTagData.bind(this)
			});
		},
		processTagData: function (response) {
			if (response && response.responseText) {
				var tagList = response.responseText.evalJSON(), tag, bestTags = [], finalTagList = [], me = this;
				if (tagList && tagList.length > 0) {
					this.searchTerms = this.searchTerms.collect(function (term) {
						return new RegExp('.*' + term + '.*', 'i');
					});
					//search for tags that are matched by any of the other search terms
					tagList.each(function (toCheck) {
						me.searchTerms.each(function (term) {
							if (toCheck.name.match(term)) {
								bestTags.push(toCheck);
							}
						});
					});
					//If we got one or more tags, we select the one with more results; if no tags were matched by the other search terms, we do the same operation on the original list of tags the server gave us
					if (bestTags.length > 0) {
						finalTagList = bestTags;
					} else {
						finalTagList = tagList;
					}
					tag = finalTagList[0];
					for (var i = 1, l = finalTagList.length; i < l; ++i) {
						if (finalTagList[i].count > tag.count) {
							tag = finalTagList[i];
						}
					}
					//Now start the page-fetching loop.
					this.requestTagPage(tag, 0);
				} else {
					if (this.searchTerms.length) {
						universe.agents.scheduler.add({
							url: this.tagURL,
							parms: {
								method: 'post',
								parameters: {pattern: this.searchTerms.shift().toLowerCase(), site: this.site}
							},
							callback: this.processTagData.bind(this)
						});
					} else {
						universe.interface.notifier.notify(this.sourceName.capitalize() + ': no results found');
						document.fire('engine:search', {mode: 'stop', source: this.sourceName});
					}
				}
			} else {
				universe.interface.notifier.notify(this.sourceName.capitalize() + ': communication failure');
				document.fire('engine:search', {mode: 'stop', source: this.sourceName});
			}
		},
		requestTagPage: function (tag, page) {
			var toSearch = tag.name.toLowerCase(), myreq;
			++page;
			universe.agents.scheduler.add({
				url: this.baseURL,
				parms: {
					method: 'post',
					parameters: {tag: toSearch, page: page, site: this.site}
				},
				callback: this.buildWallpaperList.bind(this, tag, page)
			});
		},
		buildWallpaperList: function (tag, page, response) {
			if (response.responseText) {
				var walls, wp, matches, doesItMatch;
				try {
					walls = response.responseText.evalJSON();
				} catch(err) {
					universe.interface.notifier.notify(this.sourceName.capitalize() + ': communication failure');
					document.fire('engine:search', {mode: 'stop', source: this.sourceName});
				}
				doesItMatch = function (term) {
					if (wp.tags.match(term)) {
						matches = true;
					}
				};
				for (var i = 0, l = walls.length; i < l; ++i) {
					wp = walls[i];
					//If there are any other search terms, only show wallpapers that match at least one of the other search terms; if there are NO other search terms, show everything.
					if (this.searchTerms.length > 0) {
						this.searchTerms.each(doesItMatch);
					} else {
						matches = true;
					}
					if (matches && wp.rating == 's' && wp.height < wp.width && wp.width >= 1024) {
						this.addWallpaper(wp);
						this.total++;
					}
					matches = false;
				}
				//If we got at least one wallpaper, ask for the next ones. The loop stops only after we receive an empty response or an invalid one.
				if (walls.length > 0) {
					this.requestTagPage(tag, page);
				} else {
					document.fire('engine:search', {mode: 'stop', source: this.sourceName});
				}
			} else {
				document.fire('engine:search', {mode: 'stop', source: this.sourceName});
			}
		},
		addWallpaper: function (wp) {
			universe.newWallpapers.push(new Content.Wallpaper({
				thumbUrl: wp.preview_url,
				view: 'http://' + this.site + '/post/show?md5=' + wp.md5,
				width: wp.width,
				height: wp.height,
				thumbWidth: wp.preview_width,
				thumbHeight: wp.preview_height,
				source: Config.sourceLabel.indexOf(this.sourceName)
			}));
		},
		inspect: function () {
			return 'SearchEngine.Konachan: searches Konachan and parses the results.';
		}
	};

	Core.mix(Imouto.prototype, Konachan.prototype, {
		inspect: function () {
			return 'SearchEngine.Imouto: searches Imouto and parses the results.';
		}
	});

	Core.mix(Danbooru.prototype, Konachan.prototype, {
		addWallpaper: function (wp) {
			universe.newWallpapers.push(new Content.Wallpaper({
				thumbUrl: wp.preview_url,
				view: 'http://' + this.site + '/post/show/' + wp.id,
				width: wp.width,
				height: wp.height,
				thumbWidth: wp.preview_width,
				thumbHeight: wp.preview_height,
				source: Config.sourceLabel.indexOf(this.sourceName)
			}));
		},
		inspect: function () {
			return 'SearchEngine.Danbooru: searches Danbooru and parses the results.';
		}
	});

	return {
		Danbooru: Danbooru,
		Imouto: Imouto,
		Konachan: Konachan
	};
}());

/*	-------------------------------
	Agents
	-------------------------------
*/

var Agent = (function () {

	var RequestScheduler = function () {
		this.requestList = [];
		this.currentRequests = [];
		this.running = 0;
	}

	RequestScheduler.prototype = {
		add: function (req) {
			this.requestList.push(req);
			if (!this.running) {
				this.next();
			}
		},
		next: function () {
			if (this.requestList.length > 0) {
				this.running++;
				var req = this.requestList.shift();
				this.currentRequests.push(
					new Ajax.Request(
						req.url,
						Object.extend(
							(req.parms ? req.parms : {}),
							{onComplete: this.processData.bind(this, req)}
						)
					)
				);
				if (this.running < 2 && this.requestList.length > 0) {
					this.next();
				}
			} else {
				this.running = false;
			}
		},
		processData: function (req, response) {
			if (req.callback) {
				req.callback(response);
				this.running--;
			}
			this.currentRequests.shift();
			this.next();
		}
	}

	var Counter = function () {
		this.counters = {};
		this.zero();
	};

	Counter.prototype = {
		zero: function () {
			Config.sourceLabel.each((function (source) {
				this.counters[source] = {
					shown: 0,
					filtered: 0
				};
			}).bind(this));
			this.counters.total = {shown: 0, filtered: 0};
			this.counters.ready = {shown: 0, filtered: 0};
		},
		reset: function () {
			this.zero();
			universe.interface.counter.reset()
		},
		increment: function (source, isVisible, silent) {
			this.counters.ready.shown = universe.newWallpapers.length;
			++this.counters[Config.sourceLabel[source]][isVisible ? 'shown' : 'filtered'];
			++this.counters.total[isVisible ? 'shown' : 'filtered'];
			if (!silent) {
				this.updateInterface()
			}
		},
		updateInterface: function () {
			Config.sourceLabel.each((function (source) {
				universe.interface.counter.set(source, this.counters[source]);
			}).bind(this));
			universe.interface.counter.set('total', this.counters.total);
			universe.interface.counter.set('ready', this.counters.ready);
		},
		inspect: function () {
			return 'Agent.Counter: keeps a count of the wallpapers we got so far.';
		}
	};
	

	var Adder = function () {
		this.decay = 0.3;
		this.interval = 1.0;
		this.addWallpaper.bind(this).delay(this.interval);
	};

	Adder.prototype = {
		addWallpaper: function () {
			if (universe.newWallpapers.length > 0) {
				this.interval = 1.0;
				var frag = document.createDocumentFragment();
				for (var i = 0, l = universe.newWallpapers.length; i < l; ++i) {
					var toAdd = universe.newWallpapers.shift();
					if (!toAdd) {break;}
					universe.wallpapers.push(toAdd);
					frag.appendChild($(toAdd.getElement()));
					toAdd.postInsert();
					if (universe.agents.filter.test(toAdd)) {
						universe.agents.counter.increment(toAdd.source, true, true);
					} else {
						toAdd.hide();
						universe.agents.counter.increment(toAdd.source, false, true);
					}
				}
				universe.agents.counter.updateInterface();
				$('end').up().insertBefore(frag, $('end'));
			} else {
				this.interval = this.interval < 4 ? this.interval + this.decay : 4;
			}
			this.addWallpaper.bind(this).delay(this.interval);
		},
		inspect: function () {
			return 'Agent.Adder: adds wallpapers to the main page. Needs every other agent.';
		}
	};

	var Sorter = function () {
		this.method = 'none'; // none | dimension | aspect | source
		this.matrix = [];
	};

	Sorter.prototype = {
		sort: function () {
			if (this.method === 'none') {
				return;
			}
			var matrix = Config[this.method + 'Label'].collect(function () {
				return [];
			}), otherMethod = this.method === 'aspect' ? 'dimension' : 'aspect', wp, insertWallpaper;
			$('album').down('.list').getElementsBySelector('div.sortLabel').each(function (elm) {
				elm.remove();
			});
			for (var i = 0, l = universe.wallpapers.length; i < l; ++i) {
				matrix[(wp = universe.wallpapers[i])[this.method]].sortInsert(wp, otherMethod);
			}
			insertWallpaper = (function (wp) {
				wp.insert($('album').down('.list'));
			}).bind(this);
			for (var i = 0, l = matrix.length; i < l; ++i) {
				$('album').down('.list').insert('<div class="sortLabel"><div class="label">' + Config[this.method + 'Label'][i] + '</div></div>');
				if (matrix[i]) {
					matrix[i].each(insertWallpaper(wp));
				}
			}
			this.matrix = matrix;
			this.otherMethod = otherMethod;
		},
		getInsertionPoint: function (wp) {
			//return the element after which the wallpaper should be inserted
			if (this.method === 'none') {
				return $('album').down('.list');
			} else {
				var position = this.matrix[wp[this.method]].sortInsert(wp, this.otherMethod);
				return position > 0 ? $$('.sortLabel')[wp[this.method]].next(position - 1) : $$('.sortLabel')[wp[this.method]];
			}
		},
		inspect: function () {
			return 'Agent.Sorter: deprecated sorter.';
		}
	};

	var Filter = function () {
		this.dimensionFilters = [' > 1024', ' >= 1280', ' >= 1680', ' >= 1920'].collect(function (test) {
			return {
				testExp: test,
				enabled: false
			};
		});
		this.dimensionFilters.prop = 'width';
		this.aspectFilters = Config.aspectLabel.collect(function (label, i) {
			return {
				testExp: ' === ' + i,
				enabled: false
			};
		});
		this.aspectFilters.prop = 'aspect';
		this.sourceFilters = Config.sourceLabel.collect(function (label, i) {
			return {
				testExp: ' === ' + i,
				enabled: false
			};
		});
		this.sourceFilters.prop = 'source';
	};

	Filter.prototype = {
		set: function (filterMap) {
		// filterMap example: {source: [false, true, false], aspect: ['true', 'true', false]} etc
			var enable = false;
			['dimension', 'source', 'aspect'].each((function (type) {
				if (filterMap[type]) {
					this[type + 'Filters'].each(function (filter, i) {
						filter.enabled = filterMap[type][i];
						if (filter.enabled) {
							enable = true; // if at least a link in the chain is enabled, enable the chain
						}
					});
				}
				this[type + 'Filters'].enabled = enable;
				enable = false;
			}).bind(this));
		},
		test: function (wp) {
			var testResults = {};
			['dimension', 'source', 'aspect'].each((function (type) {
				var filterList = this[type + 'Filters']
				if (filterList.enabled) {
					testResults[type] = false;
					for (var i = 0, l = filterList.length; i < l; ++i) {
						if (filterList[i].enabled && !testResults[type]) {
							testResults[type] = eval(wp[filterList.prop] + filterList[i].testExp);
							if (testResults[type]) {
								break;
							}
						}
					}
				} else {
					testResults[type] = true;
				}
			}).bind(this));
			return testResults.dimension && testResults.source && testResults.aspect;
		},
		refilter: function () {
			universe.agents.counter.reset();
			var album = $('album');
			album.remove();
			universe.wallpapers.each((function (wp) {
				if (this.test(wp)) {
					wp.show();
					universe.agents.counter.increment(wp.source, true);
				} else {
					wp.hide();
					universe.agents.counter.increment(wp.source, false);
				}
			}).bind(this));
			$('toolbar').insert({after: album});
		},
		inspect: function () {
			return 'Agent.Filter: tests the attributes of a wallpaper and decides if it should appear on the page or stay hidden.';
		}
	};
	

	return {
		RequestScheduler: RequestScheduler,
		Adder: Adder,
		Counter: Counter,
		Filter: Filter,
		Sorter: Sorter
	};

}());


/* -------------------------------
	Universe
   -------------------------------
*/

var Universe = function () {
	this.html = '<div id="main"><div id="toolbar"></div><div id="album"><div class="list"></div></div><div style="clear:both;"></div></div>';
	this.wallpapers = [];
	this.newWallpapers = [];
	this.engines = {};
	Config.sourceLabel.each((function (source) {
		this.engines[source] = new SearchEngine[source.capitalize()]();
	}).bind(this));
	this.interface = {
		counter: new Interface.Counter(),
		filter: new Interface.Filter(),
		searchBox: new Interface.InputBox(),
		notifier: new Interface.Notifier()
	};
	this.agents = {
		scheduler: new Agent.RequestScheduler(),
		counter: new Agent.Counter(),
		filter: new Agent.Filter(),
		sorter: new Agent.Sorter(),
		adder: new Agent.Adder()
	};
};

Core.mix(Universe.prototype, Core.ElementInterface, {
	postInsert: function () {
		this.interface.counter.makeAndInsert(document.body);
		this.interface.filter.makeAndInsert(document.body);
		$('toolbar').insert('<div id="toolbar-inner"></div>');
		this.interface.searchBox.makeAndInsert('toolbar-inner');
		this.interface.notifier.makeAndInsert(document.body);
		
		(new Interface.Trigger({
			event: 'click',
			callback: function () {
				if (universe.wallpapers.length > 0) {
					new Ajax.Request(
						'http://www.animebox.eu/universe/index.php/export', 
						{
							method: 'post', 
							parameters: {
								wallpapers: universe.wallpapers.toJSON()
							}, 
							onComplete: function (response) {
								$$('iframe')[0].src = 'http://www.animebox.eu/universe/index.php/export/download/' + response.responseText + '/' + universe.currentSearchTerms.replace(/ /g, '%20');
							}
						}
					);
				} else {
					universe.interface.notifier.notify('There are no results to save yet!')
				}
			},
			html: '<div id="saveResults" class="button">Save results</div>'
		})).makeAndInsert('toolbar-inner');

		(new Interface.Trigger({
			event: 'click',
			callback: this.interface.filter.show.bind(this.interface.filter),
			html: '<div id="filterButton" class="button">Filtering rules</div>'
		})).makeAndInsert('toolbar-inner');

		var bg = Config.backgrounds.getRandomItem();
		$(document.body).insert('<div id="background"></div>');
		$('background').setStyle({background:'white url("/assets/img/bg/' + bg.fname + '_normal.jpg") ' + bg.align + ' no-repeat'});
	},
	search: function (searchTerms) {
		if ($('background')) {
			new Effect.Fade('background', {
				duration: 5, 
				afterFinish: function () {
					$('background').remove();
				}
			});
		}
		$('album').down('.list').update('');
		$('album').down('.list').insert('<div id="end" style="display: none;"></div>');
		this.interface.notifier.notify('Animebox Universe loves you, please wait for the results to show up!');
		this.wallpapers = [];
		this.wallpapers.toJSON = function () {
			var tmp = [];
			this.each(function (wp) {
				if (wp.elm.visible()) {
					tmp.push(wp.toJSON());
				}
			});
			return '[' + tmp.join(', ') + ']';
		}
		this.newWallpapers = [];
		this.agents.counter.reset();
		this.currentSearchTerms = searchTerms;
		Config.sourceLabel.each((function (source) {
			this.engines[source].search(searchTerms);
		}).bind(this));
	}
});

var startup = function () {
	if ($('searchterm') && $('searchterm').innerHTML !== '') {
		var searchterm = $('searchterm').innerHTML;
	}
	window.universe = new Universe();
	universe.makeAndInsert(document.body);
	if (searchterm) {
		var inputelm = $$('input.inputBox')[0];
		inputelm.value = searchterm;
		inputelm.setStyle({color: 'black'});
		universe.search(searchterm);
	}
};

document.observe('dom:loaded', startup);
