// Install some useful jQuery extensions that we use a lot

$.support.touch = 'ontouchstart' in window;

$.extend($.fn, {
	orNull: function() {
		return this.length > 0 ? this : null;
	},

	findAndSelf: function(selector) {
		return this.find(selector).add(this.filter(selector));
	}
});

// Little Helpers

function hyphenate(str) {
	return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}

function isVisible(el) {
	if (el.is(':hidden'))
		return false;
	var viewTop = $(window).scrollTop();
	var viewBottom = viewTop + $(window).height();
	var top = el.offset().top;
	var bottom = top + el.height();
	return top >= viewTop && bottom <= viewBottom 
			|| top <= viewTop && bottom >= viewTop 
			|| top <= viewBottom && bottom >= viewBottom; 
}

function smoothScrollTo(el, callback) {
	$('html, body').animate({
		scrollTop: el.offset().top
	}, 250, callback);
}

var behaviors = {};

behaviors.hiDPI = function() {
	// Turn off hiDPI for all touch devices for now, until the site is built
	// true to scale.
	if ($.support.touch)
		$('canvas').attr('hidpi', 'off');
};

behaviors.sections = function() {
	var toc = $('.toc');
	var checks = [];
	var active;

	function update() {
		$.each(checks, function() {
			if (this())
				return false;
		});
	}

	$(document).scroll(update);
	$(window).resize(update);
	setTimeout(update, 0);

	$('article section').each(function() {
		var section = $(this);
		var anchor = $('a', section);
		// Move content until next section inside section
		section.append(section.nextUntil('section'));
		var title = anchor.attr('title') || $('h1,h2', section).first().text();
		var id = section.attr('id');
		if (!id) {
			id = hyphenate(title)
				.replace(/\s+/g, '-')
				.replace(/^#/, '')
				.replace(/[!"#$%&'\()*+,.\/:;<=>?@\[\\\]\^_`{|}~]+/g, '-')
				.replace(/-+/g, '-');
			section.attr('id', id);
			anchor.attr('name', id);
		}

		function activate() {
			if (active)
				active.removeClass('active');
			selector.addClass('active');
			active = selector;
		}

		// Create table of contents on the fly
		if (toc) {
			var selector = $('<li class="entry selector"><a href="#' + id + '">'
					+ title + '</a></li>').appendTo(toc);
			if (section.is('.spacer'))
				selector.addClass('spacer');
			$('a', selector).click(function() {
				smoothScrollTo(section, function() {
					window.location.hash = id;
				});
				return false;
			});

			checks.push(function() {
				var visible = isVisible(section);
				if (visible)
					activate();
				return visible;
			});
		}
	});

	// Adjust height of last section so that the last anchor aligns perfectly
	// with the top of the browser window.
	var lastSection = $('article section:last');
	var lastAnchor = $('a[name]', lastSection);

	function resize() {
		lastSection.height('auto');
		var bottom = $(document).height() - lastAnchor.offset().top - $(window).height();
		if (bottom < 0)
			lastSection.height(lastSection.height() - bottom);
	}

	if (lastSection.length && lastAnchor.length) {
		$(window).on({
			load: resize,
			resize: resize
		});
		resize();
	}
};

behaviors.sticky = function() {
	$('.sticky').each(function() {
		me = $(this);
		container = $('<div/>').append(me.contents()).appendTo(me);
		// Insert a div wrapper of which the fixed class is modified depending on position
		$(window).scroll(function() {
			if (container.is(':visible'))
			  container.toggleClass('fixed', me.offset().top - $(this).scrollTop() <= 0);
		});
	});
};

behaviors.hash = function() {
	var hash = unescape(window.location.hash);
	if (hash) {
		// First see if there's a class member to open
		var target = $(hash);
		if (target.length) {
			if (target.hasClass('member'))
				toggleMember(target);
			smoothScrollTo(target);
		}
	}
};

behaviors.thumbnails = function() {
	var thumbnails = $('.thumbnail');
	var height = 0;
	thumbnails.each(function() {
		height = Math.max(height, $(this).height());
	});
	$('.thumbnail').height(height);
};

behaviors.expandableLists = function() {
	$('.expandable-list').each(function() {
		var list = $(this);
		$('<a href="#" class="arrow"/>')
			.prependTo(list)
			.click(function() {
				list.toggleClass('expanded');
			});
	});
};

behaviors.referenceClass = function() {
	var classes = $('.reference-classes');
	if (classes.length) {
		// Mark currently selected class as active. Do it client-sided
		// since the menu is generated by jsdocs.
		var path = window.location.pathname.toLowerCase();
		$('a[href="' + path + '"]', classes).addClass('active');
	}
};

behaviors.hover = function() {
	$('.hover').hover(function() {
		$('.normal', this).toggleClass('hidden');
		$('.over', this).toggleClass('hidden');
	});
};

behaviors.code = function() {
	$('.code:visible, pre:visible code').each(function() {
		createCode($(this));
	});
};

behaviors.paperscript = function() {
	// Ignore all paperscripts in the automatic load event, and load them
	// separately in createPaperScript() when needed.
	$('script[type="text/paperscript"]').attr('ignore', 'true');
	$('.paperscript:visible').each(function() {
		createPaperScript($(this));
	});
};

function createCodeMirror(place, options, source) {
	return new CodeMirror(place, $.extend({}, {
		mode: 'javascript',
		lineNumbers: true,
		matchBrackets: true,
		tabSize: 4,
		indentUnit: 4,
		indentWithTabs: true,
		tabMode: 'shift',
		value: source.text().match(
			// Remove first & last empty line
			/^\s*?[\n\r]?([\u0000-\uffff]*?)[\n\r]?\s*?$/)[1]
	}, options));
}

function createCode(element) {
	if (element.data('initialized'))
		return;
	var start = element.attr('start');
	var highlight = element.attr('highlight');
	var editor = createCodeMirror(function(el) {
		element.replaceWith(el);
	}, {
		lineNumbers: !element.parent('.resource-text').length,
		firstLineNumber: parseInt(start || 1, 10),
		mode: element.attr('mode') || 'javascript',
		readOnly: true
	}, element);
	if (highlight) {
		var highlights = highlight.split(',');
		for (var i = 0, l = highlights.length; i < l; i++) {
			var highlight = highlights[i].split('-');
			var hlStart = parseInt(highlight[0], 10) - 1;
			var hlEnd = highlight.length == 2
					? parseInt(highlight[1], 10) - 1 : hlStart;
			if (start) {
				hlStart -= start - 1;
				hlEnd -= start - 1;
			}
			for (var j = hlStart; j <= hlEnd; j++) {
				editor.setLineClass(j, 'highlight');
			}
		}
	}
	element.data('initialized', true);
}

function createPaperScript(element) {
	if (element.data('initialized'))
		return;

	var script = $('script', element).orNull(),
		runButton = $('.button.run', element).orNull();
	if (!script)
		return;
	// Now load / parse / execute the script
	script.removeAttr('ignore');
	var scope = paper.PaperScript.load(script[0]);
	if (!runButton)
		return;

	var canvas = $('canvas', element),
		hasResize = canvas.attr('resize'),
		showSplit = element.hasClass('split'),
		sourceFirst = element.hasClass('source'),
		editor = null,
		hasBorders = true,
		edited = false,
		animateExplain,
		explain = $('.explain', element).orNull(),
		source = $('<div class="source hidden"/>').insertBefore(script);

	if (explain) {
		explain.addClass('hidden');
		var text = explain.html().replace(/http:\/\/([\w.]+)/g, function(url, domain) {
			return '<a href="' + url + '">' + domain + '</a>';
		}).trim();
		// Add explanation bubbles to tickle the visitor's fancy
		var explanations = [{
			index: 0,
			list: [
				[text ? 4 : 3, text || ''],
				[1, ''],
				[4, '<b>Note:</b> You can view and even edit<br>the source right here in the browser'],
				[1, ''],
				[3, 'To do so, simply press the <b>Source</b> button &rarr;']
			]
		}, {
			index: 0,
			indexIfEdited: 3, // Skip first sentence if user has already edited code
			list: [
				[4, ''],
				[3, 'Why don\'t you try editing the code?'],
				[1, ''],
				[4, 'To run it again, simply press press <b>Run</b> &rarr;']
			]
		}];
		var timer,
			mode;
		animateExplain = function(clearPrevious) {
			if (timer)
				timer = clearTimeout(timer);
			// Set previous mode's index to the end?
			if (mode && clearPrevious)
				mode.index = mode.list.length;
			mode = explanations[source.hasClass('hidden') ? 0 : 1];
			if (edited && mode.index < mode.indexIfEdited)
				mode.index = mode.indexIfEdited;
			var entry = mode.list[mode.index];
			if (entry) {
				explain.removeClass('hidden');
				explain.html(entry[1]);
				timer = setTimeout(function() {
					// Only increase once we're stepping, not in animate()
					// itself, as entering & leaving would continuosly step
					mode.index++;
					animateExplain();
				}, entry[0] * 1000);
			}
			if (!entry || !entry[1])
				explain.addClass('hidden');
		};
		element
			.mouseover(function() {
				if (!timer)
					animateExplain();
			})
			.mouseout(function() {
				// Check the effect of :hover on button to see if we need
				// to turn off...
				// TODO: make mouseenter / mouseleave events work again
				if (timer && runButton.css('display') == 'none') {
					timer = clearTimeout(timer);
					explain.addClass('hidden');
				}
			});
	}

	function showSource(show) {
		source.toggleClass('hidden', !show);
		runButton.text(show ? 'Run' : 'Source');
		if (explain)
			animateExplain(true);
		if (show && !editor) {
			editor = createCodeMirror(source[0], {
				onKeyEvent: function(editor, event) {
					edited = true;
				}
			}, script);
		}
	}

	function runScript() {
		// Update script to edited version
		var code = editor.getValue();
		script.text(code);
		// Keep a reference to the used canvas, since we're going to
		// fully clear the scope and initialize again with this canvas.
		// Support both old and new versions of paper.js for now:
		var element = scope.view.element;
		// Clear scope first, then evaluate a new script.
		scope.clear();
		scope.initialize(script[0]);
		scope.setup(element);
		scope.execute(code);
	}

	function resize() {
		source
			.width(element.width() - (hasBorders ? 2 : 1))
			.height(element.height() - (hasBorders ? 2 : 0));
		if (editor)
			editor.refresh();
	}

	function toggleView() {
		var show = source.hasClass('hidden');
		resize();
		canvas.toggleClass('hidden', show);
		showSource(show);
		if (!show)
			runScript();
		// Add extra margin if there is scrolling
		var scrollHeight = $('.CodeMirror .CodeMirror-scroll', source).height();
		runButton.css('margin-right', scrollHeight > element.height() ? 25 : 8);
	}

	if (hasResize) {
		paper.view.on('resize', resize);
		hasBorders = false;
		source.css('border-width', '0 0 0 1px');
	}

	if (showSplit) {
		showSource(true);
	} else if (sourceFirst) {
		toggleView();
	}

	runButton
		.click(function() {
			if (showSplit) {
				runScript();
			} else {
				toggleView();
			}
			return false;
		})
		.mousedown(function() {
			return false;
		});

	element.data('initialized', true);
}

// Reference (before behaviors)

var lastMember = null;
function toggleMember(member, dontScroll, offsetElement) {
	var link = $('.member-link:first', member);
	if (!link.length)
		return true;
	var desc = $('.member-description', member);
	var visible = !link.hasClass('hidden');
	// Retrieve y-offset before any changes, so we can correct scrolling after
	var offset = (offsetElement || member).offset().top;
	if (lastMember && !lastMember.is(member)) {
		var prev = lastMember;
		lastMember = null;
		toggleMember(prev, true);
	}
	lastMember = visible && member;
	link.toggleClass('hidden', visible);
	desc.toggleClass('hidden', !visible);
	if (!dontScroll) {
		// Correct scrolling relatively to where we are, by checking the amount
		// the element has shifted due to the above toggleMember call, and
		// correcting by 11px offset, caused by 1px border and 10px padding.
		var scroll = $(document).scrollTop();
		// Only change hash if we're not in frames, since there are redrawing
		// issues with that on Chrome.
		if (parent === self)
			window.location.hash = visible ? member.attr('id') : '';
		$(document).scrollTop(scroll
				+ (visible ? desc : link).offset().top - offset
				+ 11 * (visible ? 1 : -1));
	}
	if (!member.data('initialized') && visible) {
		behaviors.code();
		behaviors.paperscript();
		member.data('initialized', true);
	}
	return false;
}

$(function() {
	$('.reference .member').each(function() {
		var member = $(this);
		var link = $('.member-link', member);
		// Add header to description, with link and closing button
		var header = $('<div class="member-header"/>').prependTo($('.member-description', member));
		// Clone link, but remove name, id and change href
		link.clone().removeAttr('name').removeAttr('id').attr('href', '#').appendTo(header);
		// Add closing button.
		header.append('<div class="member-close"><input type="button" value="Close"></div>');
	});

	// Give open / close buttons behavior
	$('.reference')
		.on('click', '.member-link, .member-close', function() {
			return toggleMember($(this).parents('.member'));
		})
		.on('click', '.member-text a', function() {
			if (this.href.match(/^(.*?)\/?#|$/)[1] === document.location.href.match(/^(.*?)\/?(?:#|$)/)[1]) {
				toggleMember($(this.href.match(/(#.*)$/)[1]), false, $(this));
				return false;
			}
		});
});

// DOM-Ready

$(function() {
	for (var i in behaviors)
		behaviors[i]();
});
