App = new function() {
	var searchQuery = null, searchIds = null, searchIndex,
		searchPattern = null, highlights = null, highlight, highlightIndex;

	function clearHighlights() {
		if (highlights) {
			var parents = [];
			highlights.each(function(highlight) {
				parents.push(highlight.getParent());
				highlight.getChildNodes().insertBefore(highlight);
				highlight.remove();
			});
			// Flatten text:
			parents.each(function(parent) {
				parent.setHtml(parent.getHtml());
			});
			highlights = null;
		}
	}

	function showResult(dir) {
		var next = highlightIndex + dir;
		if (highlights && next >= 0 && next < highlights.length) {
			if (highlight)
				highlight.removeClass('active');
			highlight = highlights[highlightIndex = next];
			highlight.addClass('active');
			var bounds = highlight.getBounds(), y = $document.getScrollOffset().y,
				height = $document.getSize().height, diff = 18;
			if (bounds.top + diff >= y + height || bounds.bottom - diff < y)
				App.scroll.start(0, bounds.top > y
					? bounds.bottom - height + diff
					: bounds.top - diff);
				
		} else {
			clearHighlights();
			var next = searchIndex + dir;
			if (next < 0)
				next = searchIds.length - 1;
			else if (searchIds && next == searchIds.length)
				next = 0;
			var id = searchIds && searchIds[searchIndex = next];
			var entry = id && $(id);
			if (entry) {
				entry.open(true, {
					forceLayout: true,
					handler: function() {
						clearHighlights();
						$$('h3, p, li', entry).each(function(text) {
							// Replace tags first, then place them back after
							// hightlight replacement, to avoid destruction of tags.
							var tags = [];
							text.setHtml(
								text.getHtml().replace(/<([^>]*)>/gi, function(tag) {
									return '###' + (tags.push(tag) - 1) + '###';
								})
								.replace(searchPattern, '<span class="highlight">$1</span>')
								.replace(/###(\d*)###/gi, function(placeholder, index) {
									return tags[index];
								})
							);
						});
						highlights = $$('span.highlight', entry);
						highlightIndex = dir > 0 ? -1 : highlights.length;
						showResult(dir);
					}
				});
			}
		}
	}

	function search(value) {
		if (value && value.length >= 2) {
			if (value != searchQuery) {
				clearHighlights();
				searchQuery = value;
				searchPattern = new RegExp('(' + value.split(/\s/).map(function(term) {
					return term.escapeRegExp();
				}).join('|') + ')', 'gi');
				searchIds = highlights = null;
				searchIndex = -1;
				App.request.send('/search', { data: { query: value } }, function(result) {
					searchIds = Json.decode(result);
					showResult(1);
				});
			} else if (searchIds) {
				showResult(1);
			}
		} else {
			clearHighlights();
		}
		$('.search-results').modifyClass('hidden', !value);
	}

	$document.addEvents({
		domready: function() {
			App.entries = $$('div.entry');
			$$('#search-query').addEvents({
				search: function() {
					search(this.getValue());
				},
				keydown: function(event) {
					if (event.key == 'enter')
						event.stop();
				}
			});
			$$('.search-button').addEvents({
				mousedown: function(event) {
					event.stop();
				},

				mouseup: function(event) {
					showResult(this.getId() == 'search-next' ? 1 : -1);
					event.stop();
				}
			});
			$$('#credits-link').addEvents({
				click: function(event) {
					var credits = $('#credits');
					if (credits)
						credits.open(!credits.opened);
					event.stop();
				}
			});
		},

		beforeupdate: function() {
			// Store old states of entries and menus:
			App.opened = App.entries.each(function(entry) {
				this[entry.getId()] = entry.opened;
			}, {});
		},

		afterupdate: function() {
			// Now restore states
			App.entries.each(function(entry) {
				entry.open(App.opened[entry.getId()]);
			});
			// Recreate request object
			App.request = new Request(App.request.options);
		},

		mousedown: function(event) {
			// Do not allow dragging of images
			if (event.target.match('img'))
				event.stop();
		}
	});

	return {
		entries: null,
		selected: null,
		request: new Request({ method: 'get', iframe: true, link: 'chain' }),
		scroll: new Fx.Scroll($document, { duration: 500 })
	};
};

ListEntry = HtmlElement.extend({
	_class: 'entry',
	_lazy: true,

	initialize: function() {
		this.button = $('a.button', this);
		var id = this.getId(), that = this;
		if (this.button) {
			this.buttonOver = $('#' + id + '-over', this.button);
			this.buttonOut = $('#' + id + '-out', this.button);
			this.button.addEvents({
				click: this.onClick.bind(this),
				mouseover: function() {
					that.over = true;
					that.updateButton();
				},
				mouseout: function() {
					that.over = false;
					that.updateButton();
				}
			});
		}
		this.content = $('div.content', this);
		this.loaded = !this.content.hasClass('hidden');
		// Close first to get closedHeight
		this.open(false);
		this.closedHeight = this.getHeight();
		if (this.loaded)
			this.open(true);
		if (this.loaded) {
			this.setup(null, {});
			$window.setScrollOffset(this.getOffset());
		}
	},

	updateButton: function() {
		this.buttonOver.modifyClass('hidden', !this.over);
		this.buttonOut.modifyClass('hidden', this.over);
	},

	setup: function(html, param) {
		if (html)
			this.content.setHtml(html);
		// y: 1 to hide rulers
		new Fx.SmoothScroll({ duration: 500, offset: { x: 0, y: 1 }}, this);
		this.title = $('h1', this.content);
		this.title.addEvent('click', this.onClick.bind(this));
		this.index = $('.index', this.content);
		// Only make columns for first block right away since it is the visible
		// one. Then process the others with delay, so the user does not feel
		// a lag when opening the entry.
		var cols = $$('div.columns', this.content), i = 0, that = this;
		function layout() {
			if (i < cols.length) {
				if (that.opened) {
					var obj = cols[i++].columns({ count: 2, gap: 32 });
					if (param.forceLayout)
						layout();
					else
						layout.delay(1);
				} else { // TODO: Find better way!
					layout.delay(250);
				}
			}
		}
		if (!this.hasClass('print'))
			layout();
	},

	open: function(open, param) {
		param = param || {};
		var screenHeight = $window.getSize().height,
			// -30 to take padding and margins into account (20 / -10)
			verticalOffset = -30;
		if (open && !this.loaded) {
			App.request.send('/' + this.getId() + '/content', (function(html) {
				this.loaded = true;
				// Need to be open for layout
				this.open(true, param);
				this.setup(html, param);
				// Scroll back in place, as set by open
				if (this.offset !== undefined)
					$window.setScrollOffset(0, this.getOffset().y + this.offset);
			}).bind(this));
		} else {
			this.opened = open;
			if (!open && !param.dontScroll) {
				var bottom1 = $window.getScrollOffset().y + screenHeight,
					bottom2 = $window.getScrollSize().height;
				if (this.closedHeight !== undefined)
				 	bottom2 -= this.getHeight() - this.closedHeight;
				if (bottom1 > bottom2) {
					return App.scroll.start(0, bottom2 - screenHeight + verticalOffset).chain((function() {
						this.open(false, { dontScroll: true });
					}).bind(this));
				}
			}
			this.offset = $document.getScrollOffset().y - this.getOffset().y;
			this.modifyClass('open', open);
			if (this.button)
				this.button.modifyClass('hidden', open);
			this.content.modifyClass('hidden', !open);
			if (open) {
				// Scroll into view if invisible
				var offset = this.getOffset().y, scroll = $window.getScrollOffset().y;
				if (offset + this.closedHeight < scroll || offset >= scroll + screenHeight + verticalOffset) {
					// Delay in order to give setup time to flow content in.
					App.scroll.toElement.delay(1, App.scroll, [this]);
				}
				if (App.selected && App.selected != this)
					App.selected.open(false, { dontScroll: true });
				App.selected = this;
			}
			if (param.handler)
				param.handler.delay(1, this);
		}
	},

	toggle: function() {
		var open = !this.opened;
		this.open(open);
		return open;
	},

	onClick: function(event) {
		this.toggle();
		event.stop();
	}
});

Index = HtmlElement.extend({
	_class: 'index',
	_lazy: true,

	initialize: function() {
		// Shorten each list entry to fit one line only
		$$('li', this).each(function(line) {
			var abstract = $('.abstract', line);
			if (abstract) {
				var text = abstract.getFirstNode();
				var str = text.getText();
				// Remove trailing white space, since we're matching non-white
				// in backward loop.
				var pos = str.length - str.match(/([\s]*)$/)[1].length;
				// Depending on the prediction, either one or the other of
				// the following loops is needed.
				// Now first go backwards until the height fits.
				while (line.getHeight() > 20) {
					str = str.substring(0, pos).trim(' .,');
					text.setText(str + '\u2026');
					// Find the previous word using regexp, including whitespace.
					var word = (str.match(/([\s]*[^\s]*)$/) || [])[1];
					if (!word)
						break;
					pos -= word.length;
				}
			}
		});
	}
});
