/**
 * channel_playlist_navigator.js
 * 
 * Depends on:
 *  history.js
 *  ellipsis.js
 *  googutil.js
 *  ...
 */

function $(id) { return document.getElementById(id); }
function $$(tagName, className, el) {
	return getElementsByTagNameAndClass(tagName, className, el);
}

/**
 * Namespace for Playlist Navigator's public methods.
 */
var playnav = function() {};
playnav.onPlayerLoadedFunctions = [];
playnav.mostRecentInitFunction;
playnav.mostRecentSelectViewFunction;
playnav.mostRecentSelectTabFunction;
playnav.mostRecentPlayVideoFunction;

/**
 * Playlist Navigator implementation.
 */
(function() {
	var OVERSCROLL = 500;
	var AUTOSKIP_ERROR_TIMEOUT = 3000;

	var PlayState = {
		UNSTARTED: -1,
		ENDED: 0,
		PLAYING: 1,
		PAUSED: 2,
		BUFFERING: 3,
		CUED: 4
	};

	/**
	 * Are we in autoskip mode? If so, the player will automatically roll over
	 * to the next video when done playing the current if it encounters an
	 * error. This gets set when a video ends.
	 */
	var autoskip = false;

	/**
	 * Are we in autoplay mode? If so, the player will automatically roll over
	 * to the next video when done playing the current, regardless of error.
	 */
	var autoplay = true;

	/**
	 * Set true when video playback has been requested, but the player has
	 * not yet responded by indicating the PLAYING state.
	 */
	var playRequested = false;

	// Hook into Boxes JS. 
	var box_id = 'user_playlist_navigator';	
	var backend;
	var box_info = {'name' : box_id, 'x_position' : 0};

	var player;
	var currentPlayState = PlayState.UNSTARTED;

	// Reference to currently selected Tab: Uploads, Favorites, etc. 
	var currentTab;
	var currentTabName;

	var invalidatedTabs = {};

	// Reference to currently selected View: Player, Grid
	var currentView;
	var currentViewName;

	var currentVideoId;
	var currentVideoIndexId;
	var currentPlaylistId;
	var currentSearchQuery;
	var currentSelection;
	var currentPanelName;
	var currentSortName;

	// Updated when video playback is paused, and the user selects a new
	// playlist.
	var savedLocationHash;

	var handlingLocationHashUpdate = false;
	var skipping = false;

	// Queue of videos to play
	var videoQueue = [];

	var isLoaded = false;
	var isInit = false;
	var viewUpdateRequired = false;
	var ageVerificationRequired = false;
	var w = window;

	var scrollableItemSetupFunction = null;

	function setupScrollableItems(callback) {
		scrollableItemSetupFunction = callback;

		if (callback) {
			var currentScrollbox = $(getCurrentScrollboxId);
			callback(currentScrollbox);
		}
	}

	function getCurrentScrollboxId() {
		return ['playnav', currentViewName, currentPlaylistId, 'scrollbox'].join('-');
	}

	function setBoxInfo(_box_id) {
		box_id = _box_id;
		box_info = {'name' : box_id, 'x_position' : 0};
	}

	function executeIf(func) {
		if (func) {
			func();
		}
	}

	function setViewElementStyle(elementName, key, value) {
		var element = $(elementName);
		if (element) {
			element.style[key] = value;
		}
	}

	function removeLoadingClasses(elementName) {
		var element = $(elementName);
		removeClass(element, 'playnav-visible');
		removeClass(element, 'playnav-hidden');
		removeClass(element, 'playnav-show');
		removeClass(element, 'playnav-hide');
	}

	/*
	 * Some browser JavaScript implementations seem to occasional call functions out of order.
	 * This protects against such behavior while providing a firebug log to help in refactoring
	 * the code to remove the invalid call if possible.
	 */
	function navigatorNotReady() {
		if (!isLoaded) {
			if (window.console && window.console['warn']) {
				window.console['warn']('Function called before navigator initialized.');
				window.console['trace']();
			}
			return true;
		}
		return false;
	}

	function onNavigatorLoaded(viewName, tabName) {
		currentViewName = '';
		currentTabName = tabName;
		currentTab =  $('playnav-navbar-tab-' + tabName);
		if (currentTab) {
			addClass(currentTab, 'navbar-tab-selected');
		}

		currentPanelName = 'info';
		currentSortName = 'default';

		backend = get_channel_backend();
		box_info = get_channel_box_info(box_id);

		initView(viewName);
		updateViewOnly();

		// In some cases, certain browsers use the CSS class style over
		// style set in javascript, so we clear those here.
		removeLoadingClasses('playnav-player');
		removeLoadingClasses('playnav-playview');
		removeLoadingClasses('playnav-gridview');

		resizeView();
		isLoaded = true;

		executeIf(playnav.mostRecentInitFunction);
		playnav.mostRecentInitFunction = null;

		executeIf(playnav.mostRecentSelectViewFunction);
		playnav.mostRecentSelectViewFunction = null;

		executeIf(playnav.mostRecentSelectTabFunction);
		playnav.mostRecentSelectTabFunction = null;

		handleLocationHashUpdate(window.location.hash);
	}

	function init(playerId, tabName) {
		if (isInit) {
			return;
		}

		if (!isLoaded) {
			playnav.mostRecentInitFunction = init.bind(null, playerId, tabName);
			return;
		}
		isInit = true;

		player = $(playerId);
		player.addEventListener('onStateChange', 'playnav.onPlayerStateChange');
		player.addEventListener('onError', 'playnav.onPlayerError');

		while (playnav.onPlayerLoadedFunctions.length) {
			playnav.onPlayerLoadedFunctions.shift()();
		}
		executeIf(playnav.mostRecentPlayVideoFunction);
		playnav.mostRecentPlayVideoFunction = null;

		if (currentViewName == 'play') {
			setViewElementStyle('playnav-player', 'visibility', 'visible');
		}
	}

	function unInit() {
		isInit = false;
	}

	function isInitted() {
		return isInit;
	}

	function handleLocationHashUpdate(h) {
		handlingLocationHashUpdate = true;

		if (h.charAt(0) == '#') {
			h = h.substr(1);
		}

		if (h != '') {
			var parts = h.split('/');
			parts = Iter(parts).collect(function(str) {
				return decodeURIComponent(str);
			});

			var view = parts[0];
			if (view != 'grid' && view != 'play') {
				parts.unshift('');
			} else {
				selectView(view);
			}

			switch(parts.length) {
				case 2: // format: #<viewname>/<playlistname>
					selectPlaylist(parts[1]);
					break;
				case 3: // format: #<viewname>/user/<playlistId>
					selectPlaylist(parts[1], parts[2]);
					break;
				case 4: // format: #<viewname>/<playlistname>/<videoIndex>/<videoId>
					selectPlaylist(parts[1]); // Force tab and playlist switch.
					playVideo(parts[1], parts[2], parts[3]);
					break;
				case 5: // format: #<viewname>/user/<playlistId>/<videoIndex>/<videoId>	
					selectPlaylist(parts[1], parts[2]);
					playVideo(parts[2], parts[3], parts[4]);
					break;
			}
		}

		handlingLocationHashUpdate = false;
	}

	/**
	 * Highlight/unhighlight a single view button.
	 */
	function highlightViewButton(buttonName, isHighlighted) {
		var button = $(buttonName);
		if (!button) {
			return;
		}
		if (isHighlighted) {
			addClass(button, 'view-button-selected');
		} else {
			removeClass(button, 'view-button-selected');
		}
	}

	/**
	 * Highlight the current view button and turn off other button highlights.
	 */
	function highlightViewButtons(viewName) {
		highlightViewButton('playview-icon', viewName == 'play');
		highlightViewButton('gridview-icon', viewName == 'grid');
	}

	/**
	 * Initializes 'play' view or 'grid' view.
	 */
	function initView(viewName) {
		currentViewName = viewName;
		highlightViewButtons(viewName);
	}

	/**
	 * Selects 'play' view or 'grid' view.
	 */
	function selectView(viewName) {
		if (!isLoaded) {
			highlightViewButtons(viewName);
			playnav.mostRecentSelectViewFunction = selectView.bind(null, viewName);
			return;
		}

		if (currentViewName != viewName) {
			initView(viewName);
			if (!viewUpdateRequired) {
				viewUpdateRequired = true;
				setTimeout(updateView, 0);
			}
		}
	}

	/**
	 * Selects 'uploads', 'favorites', 'shows', 'playlists', 'topics', 'recent' or 'all' tab.
	 */
	function selectTab(tabName, opt_suppressUpdate) {
		if (!isLoaded) {
			playnav.mostRecentSelectTabFunction = selectTab.bind(null, tabName, opt_suppressUpdate);
			return;
		}

		var workingTabName;
		arranger.destruct();

		if (tabName == 'user') {
			workingTabName = 'playlists';
		} else if (tabName == 'search') {
			workingTabName = 'uploads';
		} else {
			workingTabName = tabName;
		} 

		var tab = $('playnav-navbar-tab-' + workingTabName);
		if (currentTab) {
			removeClass(currentTab, 'navbar-tab-selected');
		}
		if (tab) {
			addClass(tab, 'navbar-tab-selected');
		}

		currentTab = tab;
		currentTabName = tabName;

		if (!opt_suppressUpdate && !viewUpdateRequired) {
			viewUpdateRequired = true;
			setTimeout(updateView, 0);
		}
	}

	function selectPlaylist(playlistName, opt_playlistId, opt_searchQuery) {
		currentTabName = playlistName;
		currentPlaylistId = opt_playlistId;
		currentSearchQuery = opt_searchQuery;

		if (!viewUpdateRequired) {
			viewUpdateRequired = true;
			setTimeout(updateView, 0);
		}
	}

	function selectPanel(name, opt_params, opt_dont_hide) {
		if (navigatorNotReady()) {
			return;
		}

		// If name contains 'info' or 'favorite', autoplay is true.
		autoplay = (name.search(/info|favorite/) >= 0);

		var panel = $('playnav-panel-' + name);
		var panelTab = $('playnav-panel-tab-' + name);
		if (!panel || !panelTab) return;

		removeClass($('playnav-panel-tab-' + currentPanelName), 'panel-tab-selected');
		addClass(panelTab, 'panel-tab-selected');

		if (!opt_dont_hide) {
			$('playnav-panel-' + currentPanelName).style.display = 'none';
		}
		removePoppedElements();

		panel.style.display = 'block';
		currentPanelName = name;

		// Load panel contents.
		if (!opt_dont_hide) {
			panel.innerHTML = $('playnav-spinny-graphic').innerHTML;
		}

		var callback = function(data) {
			panel.innerHTML = data.html ? data.html : data;

			var scrollable = hasClass(panel, 'scrollable');
			$('playnav-video-panel-inner').style.overflow = (scrollable ? 'auto' : 'hidden');

			if (data.css) {
				var styleElement = document.createElement('style');
				styleElement.setAttribute("type", "text/css");
				if (styleElement.styleSheet) {	 	// IE
					styleElement.styleSheet.cssText = data.css;
				} else {													// rest of the world
					styleElement.appendChild(document.createTextNode(data.css));
				}
				document.getElementsByTagName('head')[0].appendChild(styleElement);
			}
			if (data.js) {
				var scriptElement = document.createElement('script');
				scriptElement.text = data.js;
				document.getElementsByTagName('head')[0].appendChild(scriptElement);
			}
			if (data.js_exec) {
				// Run it now!
				eval(data.js_exec);
			}
			window.setTimeout(function() { run_scripts_in_el('playnav-panel-' + name);}, 0);
		};

		var params = {
			'video_id' : currentVideoId,
			'playlist_id' : currentPlaylistId,
			'playlist_name' : currentTabName
		};

		// Apply panel-specific JS params to pass through.
		switch(name) {
			case 'info':
				params['video_index'] = currentVideoIndexId;
				break;
		}

		if (currentSelection) {
			var _tmp = $('ID2POST-' + currentSelection.id);
			if (_tmp) {
				params['comment'] = _tmp.attributes['name'].value;
			}
		}

		if (opt_params) {
			for (n in opt_params) {
				params[n] = opt_params[n];
			}
		}
		backend.call_box_method(box_info, params, 'load_popup_' + name, callback);
	}

	function updateViewOnly() {
		viewUpdateRequired = false;

		if (currentViewName == 'play') {
			setViewElementStyle('playnav-player', 'visibility', 'visible');
			setViewElementStyle('playnav-playview', 'display', 'block');
			setViewElementStyle('playnav-gridview', 'display', 'none');
		} else {
			setViewElementStyle('playnav-player', 'visibility', 'hidden');
			setViewElementStyle('playnav-playview', 'display', 'none');
			setViewElementStyle('playnav-gridview', 'display', 'block');
		}
	}

	function updateTab() {
		switch(currentTabName) {
			case 'uploads':
				clearSearchQueryFields();
				loadPlaylist(currentTabName);
				break;
			case 'favorites': case 'all': case 'recent': case 'playlists': case 'topics': case 'shows':
				loadPlaylist(currentTabName);
				break;
			case 'user':
				loadPlaylist(currentTabName, currentPlaylistId)
				break;
			case 'search':
				loadPlaylist(currentTabName, null, currentSearchQuery);
				break;
		}
	}

	function updateView() {
		if (viewUpdateRequired) {
			updateViewOnly();
		}
		updateTab();
	}

	function clearSearchQueryFields() {
		var base = 'upload_search_query-';
		Iter(['grid', 'play']).each(function(post) {
			try {
				$(base + post).value = '';
			} catch(e) {}
		});
	}

	
	var updateScrollbox = function(id) {
		var box = $(id);		
		if (!box) return;

		if (!hasClass(box, 'outer-scrollbox')) {

			// Technically this will work even if box is null, if the above $()
			// lookup didn't work, but this (getByClass) will then search the entire
			// DOM, which would be bad.
			box = $$('div', 'outer-scrollbox', box)[0];
			if (!box) return;
		}

		var top = box.scrollTop;
		var bottom = top + box.offsetHeight;
		
		top -= OVERSCROLL;
		bottom += OVERSCROLL;
		
		var pages = $$('div', 'scrollbox-page', box);
		
		var len = pages.length;
		for (var i = 0; i < len; i++) {
			var page = pages[i];

			var pageTop = page.offsetTop;
			var pageBottom = pageTop + page.offsetHeight;			
			
			if ((pageTop > top && pageTop < bottom)
					|| (pageBottom > top && pageBottom < bottom)) {
				// Show pages which are visible.
				page.style.visibility = 'visible';
				
				// Fetch pages which aren't loaded.
				if (page.className.indexOf('loaded') < 0
						&& page.className.indexOf('loading') < 0) {
					var pageNum = parseInt(page.id.split('-').pop());
					addClass(page, 'loading');
					loadPlaylistPage(currentTabName, pageNum, currentPlaylistId, currentSearchQuery);
				}
			} else {
				// Hide pages which aren't visible.
				page.style.visibility = 'hidden';
			}
		}	
	};

	updateScrollbox = Thread.bind(updateScrollbox, 'updatePlaynavScrollbox');

	var userAgent = navigator.userAgent.toLowerCase();
	var isIE6 = userAgent.indexOf('msie 6') != -1 && userAgent.indexOf('opera') == -1;

	/**
	 * Calculate the size of a scrollbox which contains a header portion of variable size
	 * (which does not scroll) and a body portion which does. The body will take the remaining
	 * space left over after positioning the header.
	 * 
	 * Also scrolls the box to the top. 
	 */
	var resizeScrollbox = function(sb) {
		var scrollBoxes = $$('div', 'scrollbox-content', sb);
		for (var i = 0; i < scrollBoxes.length; i++) {
			// Get content height (height of entire scrollbox).
			var sbContentHeight;
			var sbContent = scrollBoxes[i];
			if (!sbContent) return;
			if (isIE6) {
				sbContentHeight = 585;
				sbContent.style.height = sbContentHeight + 'px'; 
			} else {
				sbContentHeight = sbContent.offsetHeight;
			}

			// Get header height.
			var sbHeaderHeight;
			var sbHeader = $$('div', 'scrollbox-header', sbContent)[0];
			displayHeader = !(sbHeader.innerHTML.search(/^[ \n\t]*$/) >= 0);
			if (displayHeader) {
				sbHeader.style.display = '';
				sbHeaderHeight = sbHeader.offsetHeight;
			} else {
				sbHeader.style.display = 'none';
				sbHeaderHeight = 0;
			}

			// Get body element.
			var sbBody = $$('div', 'scrollbox-body', sbContent)[0];
			if (!sbBody) return;

			var newHeight = (sbContentHeight - sbHeaderHeight) + 'px';
			sbBody.style.height = newHeight;
			sbBody.style.zoom = '1';  // Force IE6 repaint.
		}
	};

	var resizePlayview = function() {
		var container = $('playnav-play-panel');
		var content = $('playnav-play-content');
		if (container.style.display == 'none' || content.style.display == 'none') return;

		var fudgeFactor = 4;  // Account for padding and stuff that we're not yet pulling into the calculation...
		var topBoxes = [
			$('watch-longform-ad'),
			$('watch-channel-brand-div')
		];
		var totalHeight = container.offsetHeight;
		var topBoxHeight = 0;

		Iter(topBoxes).each(function(box) {
			try {
				topBoxHeight += box.offsetHeight;
			} catch(e) {}
		});

		try {
			content.style.height = (totalHeight - topBoxHeight - fudgeFactor) + 'px';
		} catch(e) {}

		resizeScrollbox(content);
	}

	var resizeView = function() {
		if (currentViewName == 'play') {
			resizePlayview();
		} else {
			var gridContent = $('playnav-grid-content');
			resizeScrollbox(gridContent);
		}
	}

	resizeScrollbox = Thread.bind(resizeScrollbox, 'resizePlaynavScrollbox');	

	function sort(sortName) {
		currentSortName = sortName;
		invalidateTab(currentTabName);
		selectPlaylist(currentTabName);
	}

	function loadPlaylistPage(playlistName, pageNum, opt_playlistId, opt_searchQuery) {
		if (navigatorNotReady()) {
			return;
		}

		var method = 'load_playlist_page';
		var playlistId = (playlistName == 'user') ? opt_playlistId : playlistName;

		var params = {
			'playlist_name': playlistName,
			'encrypted_playlist_id': currentPlaylistId || "",
			'query': currentSearchQuery || "",
			'page_num': pageNum,
			'view': currentViewName,
			'playlist_sort': currentSortName
		};

		var sortEl = $([playlistId, 'sort'].join('-'));
		var sort = sortEl && sortEl.innerHTML || '';
		
		backend.call_box_method(box_info, params, method,
			onPlaylistPageLoaded.bind(this, currentViewName, playlistId, pageNum)
		);
	}

	/**
	 * When a playlist page is loaded, inject it into the proper place.
	 */	
	function onPlaylistPageLoaded(viewName, playlistId, pageNum, html) {
		var id = ['playnav', viewName, playlistId, 'page', pageNum].join('-');
		var page = $(id);
		if (page) {
			page.innerHTML = html;
			updateEllipses(page);
			removeClass(page, 'loading');
			addClass(page, 'loaded');
			selectCurrentVideo();
			
			if (scrollableItemSetupFunction) {
				scrollableItemSetupFunction(page);
			}
		}
	}

	function hideCachedPages() {
		var viewNode = $('playnav-' + currentViewName + '-content');
		var otherEls = $$('div', 'playnav-playlist-holder', viewNode);
		
		var len = otherEls.length;
		for (var i = 0; i < len; i++) {
			var el = otherEls[i];
			try {
				el.style.display = 'none';
			} catch(e) {}
		}
	}
	
	function invalidateTab(tabName) {
		invalidatedTabs[tabName] = {'play': true, 'grid': true};
	}
	
	var elementsToDelete = [];
	
	function loadPlaylist(playlistName, opt_playlistId, opt_searchQuery, opt_forceReload) {
		if (navigatorNotReady()) {
			return;
		}

		currentPlaylistId = opt_playlistId || playlistName;

		selectTab(playlistName, true);

		if (!handlingLocationHashUpdate && !playRequested) {
			var parts = [currentViewName, playlistName];
			if (opt_playlistId) {
				parts.push(opt_playlistId);
			}
			History.add(parts.join('/'));
		}
		
		var cachedEl = $(['playnav', currentViewName, 'playlist', currentPlaylistId, 'holder'].join('-'));

		var isInvalidated = invalidatedTabs[playlistName] && invalidatedTabs[playlistName][currentViewName];
		if (opt_forceReload || isInvalidated) {
			if (isInvalidated) {							
				delete invalidatedTabs[playlistName][currentViewName];
			}
			
			//TODO(michaeljh) fix this.
			//$('playnav-curvideo-controls').style.visibility = 'hidden';
			
			if (cachedEl) {				
				// Don't delete this element right away, since that'll cause
				// the "covered" thing to disappear. Postpone it until the
				// fetch of the new thing is done.
				elementsToDelete.push(cachedEl);
				cachedEl = null;
			}
		}		
		if (cachedEl) {
			hideCachedPages();
			
			// Set current page equal to element in cache.
			cachedEl.style.display = 'block';
			resizeScrollbox(cachedEl);

			var scrollArea = $$('div', 'outer-scrollbox', cachedEl)[0];
			if (scrollArea) scrollArea.scrollTop = 0;			
			
			updateEllipses(cachedEl);
			selectCurrentVideo();

		} else {
			
			// Request missing playlist via AJAX.		
			var method = 'load_playlist';
			var params = {};
			var logging = "&playlistName=" + playlistName;
			switch (playlistName) {
				case 'uploads':
					logging += "&sort=" + currentSortName;
					break;
				case 'favorites':
				case 'playlists':
				case 'topics':
					break;
				case 'all':
				case 'recent':
					method = 'load_playlist_videos_multi';
					break;
				case 'user':					
					params['encrypted_playlist_id'] = opt_playlistId;					
					break;
				case 'search':
					params['query'] = opt_searchQuery || "";
					currentSearchQuery = opt_searchQuery;
					break;
			}
	
			params['playlist_name'] = playlistName;
			params['view'] = currentViewName;
			params['playlist_sort'] = currentSortName;
			var view = currentViewName;
			backend.call_box_method(box_info, params, method,
					onPlaylistLoaded.bind(this, view, currentPlaylistId), logging);
					
			setViewLoading(currentViewName, true);
		}
	}

	function onPlaylistLoaded(viewName, playlistId, html) {
		hideCachedPages();
		Iter(elementsToDelete).each(function(el) {
			el.parentNode.removeChild(el);
		});
		elementsToDelete = [];

		var viewNode = $(['playnav', viewName, 'content'].join('-'));
		var node = document.createElement('div');

		node.className = 'playnav-playlist-holder';
		node.id = ['playnav', viewName, 'playlist', playlistId, 'holder'].join('-');
		node.innerHTML = html;
		viewNode.appendChild(node);
		resizeScrollbox(node);
		selectCurrentVideo();
		setViewLoading(viewName, false);
	}

	function selectCurrentVideo() {
		selectVideo(currentSelection);
	}

	function setViewLoading(view, isLoading) {
		$('playnav-' + view + '-loading').style.display = isLoading ? 'block': 'none';
	}

	function setVideoId(videoId) {
		currentVideoId = videoId;
	}

	function setPlaylistId(playlistId) {
		currentTab = 'user';
		currentPlaylistId = playlistId;
	}

	function goToWatchPage() {
		window.location.href = "/watch?v=" + currentVideoId;
	}

	function selectVideo(selection) {
		var targets;
		if (currentSelection) {
			targets = [
				['playnav-video-play', currentSelection.p, currentSelection.i, currentSelection.v].join('-'),
				['playnav-video-grid', currentSelection.p, currentSelection.i, currentSelection.v].join('-'),
				['playnav-video-play', currentSelection.p + '-all', currentSelection.i, currentSelection.v].join('-'),
				['playnav-video-grid', currentSelection.p + '-all', currentSelection.i, currentSelection.v].join('-')
			];
		
			Iter(targets).each(function(id) {
				try {
					removeClass($(id), 'playnav-item-selected');
				} catch(e) {}
			});
		}
		
		currentSelection = selection;
		
		if (currentSelection) {	
			targets = [
				['playnav-video-play', currentSelection.p, currentSelection.i, currentSelection.v].join('-'),
				['playnav-video-grid', currentSelection.p, currentSelection.i, currentSelection.v].join('-'),
				['playnav-video-play', currentSelection.p + '-all', currentSelection.i, currentSelection.v].join('-'),
				['playnav-video-grid', currentSelection.p + '-all', currentSelection.i, currentSelection.v].join('-')
			];
			
			Iter(targets).each(function(id) {
				try {
					addClass($(id), 'playnav-item-selected');
				} catch(e) {}
			});
		}
	}
	

	/**
	 * When a video is clicked, start it playing!
	 */
	function playVideo(playlistId, videoIndexId, videoId, opt_startSecs, opt_postId, opt_onLoad) {
		if (!isInitted()) {
			// Set up the video to play. This stores a single call since it could be time consuming to
			// cycle through multiple video plays when the last one is the only one of importance.
			playnav.mostRecentPlayVideoFunction = playVideo.bind(null, playlistId, videoIndexId, videoId, opt_startSecs, opt_postId, opt_onLoad);
			// Force into play view to make sure the player gets initialized.
			if (currentViewName != 'play') {
				selectView('play');
			}
			return;
		}

		var id = null;
		ageVerificationRequired = false;

		$('playnav-player-racy-hider').style.visibility = 'visible';
		$('playnav-player-racy').style.display = 'none';

		if (!videoIndexId && opt_postId) {
			var _tmp = $('POST2ID-' + opt_postId);
			if (_tmp) {
				id = _tmp.attributes['name'].value;
			}
		}
		if (!id) {
			id = [currentViewName, playlistId, videoIndexId, videoId].join('-');
		}

		if (!opt_onLoad && !handlingLocationHashUpdate && !skipping) {
			var parts = ['play', currentTabName];
			if (currentTabName != playlistId) {
				parts.push(playlistId);
			}

			parts.push(videoIndexId);
			parts.push(videoId);

			var hash = parts.join('/');
			History.add(hash);
			savedLocationHash = hash;
		}

		closePopup();
		if (currentViewName == 'grid' && !skipping) {
			selectView('play');
		}

		selectVideo({p: playlistId.replace('-all', ''), i: videoIndexId, v: videoId, id: id});

		currentPlaylistId = playlistId;
		currentVideoIndexId = videoIndexId;
		currentVideoId = videoId;

		if (!opt_onLoad) {
			playRequested = true;
			// $('playnav-watch-link').href = '/watch?v=' + videoId;
			var opt_startSecs = opt_startSecs && parseInt(opt_startSecs) || 0;

			// Prevent onStateChange() from thinking we need to play the next video because
			// the current video ended (the "ended" event is fired before the "playing" event).
			currentPlayState = PlayState.UNSTARTED;

			// Hide the ad on page if its showing
			hideDiv('watch-channel-brand-div');
			hideDiv('watch-longform-ad');
			resizePlayview();

			// Use player API to play video.
			player.loadVideoById(videoId, opt_startSecs);
		}

		if (window.groupname) {
			selectPanel('discussion');
		} else {
			selectPanel('info');
		}

		if (videoIndexId != null) {
			// $('playnav-curplaylist-index').innerHTML = parseInt(videoIndexId) + 1;
			try {
				$('playnav-curplaylist-count').innerHTML = $('playnav-playlist-' + playlistId + '-count').value;
				$('playnav-curplaylist-title').innerHTML = $('playnav-playlist-' + playlistId + '-title').innerHTML;
			} catch(e) {}
		}

		if (videoIndexId == null) {
			if ($('playnav-curvideo-controls')) {
				$('playnav-curvideo-controls').style.visibility = 'hidden';
			}
		} else if (currentSelection) {
			//$('playnav-curvideo-controls').style.visibility = (playlistId.indexOf('-all') > 0) ? 'hidden' : 'visible';
		}
	}

	function getNext(id) {
		var ind = id.lastIndexOf('-');
		var ind2 = id.lastIndexOf('-', ind-1);
		return id.slice(0, ind2+1) + (parseInt(id.slice(ind2+1, ind)) + 1);
	}

	function getPrev(id) {
		var ind = id.lastIndexOf('-');
		var ind2 = id.lastIndexOf('-', ind-1);
		return id.slice(0, ind2+1) + Math.max(parseInt(id.slice(ind2+1, ind)) - 1, 0);
	}

	function skip(increment) {
		skipping = true;
		var currentIndex = parseInt(currentSelection.i); 
		var newIndex = currentIndex + increment;
		if (newIndex < 0) return;
			
		// In order to skip, user must have transitioned to Player view
		// at some point.
		var el = $('playnav-video-play-' + currentSelection.p + '-' + newIndex);
		if (!el) return;
		
		var videoId = el.innerHTML;

		playVideo(currentSelection.p, newIndex, videoId);
		skipping = false;
	}

	function skipNext() {
		skip(1);
	}
	
	function skipPrev() {
		skip(-1);
	}
	
	function playAll(playlistId, firstVideoId) {
		playVideo(playlistId, 0, firstVideoId, 0, true);

		selectPlaylist('user', playlistId);
	}
	
	var currentBottomPopup = null;
	var currentPopupArrow = null;
	
	function openBottomPopup(name, opt_params) {
		if (navigatorNotReady()) {
			return;
		}

		//closePopup();
		var popup = $(name + '-popup');
		popup.style.display = '';
		//var arrow = $(name + '-popup-arrow');
		arrow.style.display = '';
		//arrow.style.left = ($(name + '-bottom-link').offsetLeft + 15) + 'px';
		//currentPopupArrow = arrow;
		$(name + '-popup-inner').innerHTML = $('playnav-spinny-graphic').innerHTML;

		var callback = function(html) {
			$(name + '-popup-inner').innerHTML = html;
			window.setTimeout(function() {run_scripts_in_el('playnav-panel-' + name)}, 0);
		};
		var params = {'video_id' : currentVideoId};
		if (opt_params) {
			for (n in opt_params) {
				params[n] = opt_params[n];
			}
		}
		backend.call_box_method(box_info, params, 'load_popup_' + name, callback);
		currentBottomPopup = popup;
	}

	function closePopup() {
		if (currentBottomPopup) {
			currentBottomPopup.style.display = 'none';
			currentPopupArrow.style.display = 'none';
			var flag_floatie = _gel('popup_flagging_menu');
			if (flag_floatie) {
				removeNode(flag_floatie);
			}
		}
	}
	
	function searchChannel(elementId) {
		var el = $(elementId);
		var query = el.value;

		if (query) {
			currentTabName = 'search';
			invalidateTab('search');
			selectPlaylist('search', null, query);
		}
	}
	
	function clearFirstTime(inp) {
		if (!inp.__touched) {
			inp.__stored_value = inp.value;
			inp.__stored_color = inp.style.color;
			inp.value='';
			inp.style.color='#333';
			inp.__touched=true;
			if (!inp.onblur) {
				inp.onblur = function() {
					if (!inp.value) {
						inp.value = inp.__stored_value;
						inp.style.color = inp.__stored_color;
						inp.__touched = false;
					}
				};
			}
		}
	}
	
	
	function onPlayerStateChange(newState) {
		switch(newState) {
			case PlayState.ENDED:
				if (currentPlayState == PlayState.PLAYING && autoplay) {
					currentPlayState = PlayState.UNSTARTED;

					// Only autoplay in user playlists.
					if (currentSelection.p && currentSelection.p.search(/uploads|favorites|search/) < 0) {
						autoskip = true; // It's okay to skip the next video if it's unplayable.
						skipNext();
					}
				}
				break;
				
			case PlayState.PLAYING:
				if (ageVerificationRequired) {
					player.stopVideo();
				}
			
				// If the location hash was modified during the paused state,
				// then when playback resumes, restore the hash.
				if (!playRequested && savedLocationHash && savedLocationHash != window.location.hash && !handlingLocationHashUpdate) {
					History.add(savedLocationHash);
				}
				currentPlayState = newState;
				playRequested = false;
				break; 
			case PlayState.PAUSED:
				currentPlayState = newState;
				playRequested = false;
				break;
		}
	}

	function onPlayerError() {
		if (autoskip) {
			setTimeout(function() {
				// Check autoskip again, since the user might have clicked
				// on something in between the error and the timeout firing.
				if (autoskip) {
					skipNext();
				}
			}, AUTOSKIP_ERROR_TIMEOUT)
		}
				
		currentPlayState = PlayState.UNSTARTED;
		playRequested = false;
	}

	function toggleFullVideoDescription(state) {
		var display = state ? 'block' : 'none';
		var displayNot = state ? 'none' : 'block';
	
		$('playnav-curvideo-description-more-holder').style.display = (state ? 'none' : 'block');
		$('playnav-curvideo-description-less').style.display = (state ? 'inline' : 'none');
		
		$('playnav-curvideo-description-container').style.height = state ? 'auto' : '28px';
	}
	
	
	function getWatchUrl() {
		return 'http://www.youtube.com/watch?v=' + currentVideoId;
	}
	
	function verifyAge(verifyAgeUrl) {
		ageVerificationRequired = true;
		player.stopVideo();
		$('playnav-player-racy-hider').style.visibility = 'hidden';
		$('playnav-player-racy').style.display = 'block';
		$('playnav-player-racy-link').href = getWatchUrl();
	}


	/**
	 * Video arranger.
	 */

	var arranger = function() {};

	// Namespace for video arranger.
	(function () {
		var visible = false;

		var bodyEl;
		
		var featuredContainerEl = null;
		var rows = 6;
		var clearBothEl = $ce('div', {'style': 'clear:both'});
		
		var draggingEl;
		var draggingPoolEl;
		var hoveredPoolEl;
		var hoveredFeatured;
				
		var dragOffset;
		var baseOffset;
		
		var scrollboxEl;
		
		var mousePos;
		
		var arrangerEl;

		function toggle(playlistId) {
			arrangerEl = $('playnav-arranger-' + playlistId);
			if (arrangerEl.style.display == 'none') {
				construct(arrangerEl);
			} else {
				destruct(arrangerEl);
			}
		}
		
		var countSelector;
		
		// Array of featured FeaturedBox items.
		var featuredBoxes;
		
		// Last box; used to hold items temporarily moved off the grid
		// when an item is inserted, before it is dropped.		
		var tempBox;
		
		
		// Index of itemId => item, used for storing fake items (items which
		// were used to pre-populate the featured boxes.
		var fakeItemIndex;
		
		// "Real" items (items in the scrollbox) which are queued up to be
		// checked for whether a fake already exists, in which case we replace
		// the fake with the real. 
		var fakeCheckItems;
		
		// Flag that says that fake items have been loaded and are ready for
		// checking. This'll be set to true when handleGetArrangedItemResponse() runs.
		var fakeItemsReady;

		
		function construct(arrangerEl) {
			fakeCheckItems = [];
			fakeItemIndex = {};
			fakeItemsReady = false;
			draggingEl = null;
			draggingPoolEl = null;
			hoveredFeatured = null;
			dragOffset = {x: 0, y: 0};
			mousePos = {x: 0, y: 0};
			
			unsetBusy();

			selectVideo(null);
			getArrangedItems();

			bodyEl = $('playnav-body');
			bodyOffset = getClientPosition(bodyEl);

			scrollboxEl = $(getCurrentScrollboxId());
		
			arrangerEl.style.display = 'block';
			
			countSelector = $$('select', 'count-selector', arrangerEl)[0];
			countSelector.value = '6';
			
			
			featuredBoxes = [];
			featuredContainerEl = $$('div', 'featured', arrangerEl)[0];			

			constructFeaturedBoxes(6, 1);			
			tempBox = new FeaturedBox(0);
			featuredBoxes[5].nextBox = tempBox;
			
			setupScrollableItems(makeContainedItemsArrangeable);

			document.onmousemove = handleMousemove;
			document.onmouseup = handleMouseup;

			document.onmousedown = handleMousedown;
			
			setTimeout(moveDropzones, 0);

			resizeScrollbox(scrollboxEl);
		}
		
		
		function constructFeaturedBoxes(count, startingNumber) {
			var prevBox = null;

			for (var i = 0; i < count; i++) {
				var box = new FeaturedBox(i + startingNumber);
				if (prevBox) {
					prevBox.nextBox = box;
				}
				prevBox = box;
				featuredContainerEl.appendChild(box.holderEl);
				featuredBoxes.push(box);
			}
			
			featuredContainerEl.appendChild(clearBothEl.cloneNode(false));
		}
		
		
		function grow() {
			// Yay for hardcoded numbers.
			if (featuredBoxes.length == 6) {
				constructFeaturedBoxes(6, 7);
				featuredBoxes[5].nextBox = featuredBoxes[6];
				featuredBoxes[11].nextBox = tempBox;
			}
			
			countSelector.value = '12';
			resizeScrollbox(scrollboxEl);
			moveDropzones();
		}
		
		function shrink() {
			if (featuredBoxes.length == 12) {
				for (var i = 0; i < 6; i++) {
					var box = featuredBoxes.pop();					
					box.destruct();
				}
				featuredBoxes[5].nextBox = tempBox;
			}
			
			countSelector.value = '6';
			resizeScrollbox(scrollboxEl);
		}
		
		function destruct() {
			if (!arrangerEl) return;
		
			arrangerEl.style.display = 'none';
			featuredContainerEl.innerHTML = '';
			for (var i = 0, len = featuredBoxes.length; i < len; i++) {
				featuredBoxes[i].destruct();
			}
			
			tempBox.destruct();
			
			featuredBoxes = null;
			
			// This will affect all scrollable items in current view immediately.
			setupScrollableItems(undoMakeContainedItemsArrangeable);
			setupScrollableItems(null);
			
			document.onmousemove = null;
			document.onmouseup = null;
			document.onmousedown = null;
		
			resizeScrollbox(scrollboxEl);
			
			scrollboxEl = null;
			arrangerEl = null;
		}
		
		function setBusy() {
			try {
				$$('div', 'loading', arrangerEl)[0].style.display = 'block';
			} catch(e) {}
		}
		
		function unsetBusy() {
			try {
				$$('div', 'loading', arrangerEl)[0].style.display = 'none';
			} catch(e) {}
		}
		
		
		function getFirstEmptyFeaturedBox() {
			for (var i = 0, len = featuredBoxes.length; i < len; i++) {
				if (!featuredBoxes[i].isFilled()) return featuredBoxes[i];
			} 		
		}
		
		FeaturedBox.focused = null;
		
		function FeaturedBox(number) {
			this.number = number;
			
			// A top-layer cover which gets mouse events.
			this.handleEl = $ce('div', {'c': 'handle dropzone'});

			this.targetEl =  $ce('div', {'c': 'target'}, [
				$ce('div', {'c': 'number'}, $ctn(number)),
				this.handleEl
			]);
			
			this.holderEl = $ce('div', {'c': 'target-holder'}, this.targetEl);

			addListener(this.handleEl, 'mouseover', handleFeaturedMouseover);
			addListener(this.handleEl, 'mouseout', handleFeaturedMouseout);

			this.handleEl.cls = this;
			
			return this;
		}
		
		FeaturedBox.prototype.destruct = function() {
			removeListener(this.handleEl, 'mouseover', handleFeaturedMouseover);
			removeListener(this.handleEl, 'mouseout', handleFeaturedMouseout);
			
			removeNode(this.handleEl);
			removeNode(this.holderEl);
		}
		
		// This is needed for IE, since it always thinks the ghost
		// element is above the cover, if the cover is inside a container
		// which is of lower zIndex than that the ghost. Instead we move
		// the drop detection zones up into the document, so they can have
		// higher zIndex than anything. 
		function moveDropzones() {
			var zones = $$('div', 'dropzone', featuredContainerEl);
			for (var i = 0, len = zones.length; i < len; i++) {
				moveDropzone(zones[i]);
			}
		}
		
		function moveDropzone(zone) {
			clientPosition = getClientPosition(zone);
			var width = zone.offsetWidth;
			var height = zone.offsetHeight;
			
			zone.style.width = zone.offsetWidth + 'px';
			zone.style.height = zone.offsetHeight + 'px';
			zone.style.top = clientPosition.y + 'px';
			zone.style.left = clientPosition.x + 'px';


			zone.parentNode.insertBefore($ce('div', {'c': 'handle'}), zone);
			document.body.appendChild(zone);
		}
		
		FeaturedBox.prototype.isFilled = function() {
			return !!this.contentEl;
		}
		
		FeaturedBox.prototype.setFocus = function() {
			FeaturedBox.unsetFocus();
			FeaturedBox.focused = this;
			addClass(this.holderEl, 'focused');
		};
		
		FeaturedBox.prototype.setMouseover = function() {
			FeaturedBox.unsetMouseover();
			FeaturedBox.mouseover = this;
		};

		
		// Static function
		FeaturedBox.unsetFocus = function() {
			var f = FeaturedBox.focused;
			if (!f) return;
			removeClass(f.holderEl, 'focused');
			FeaturedBox.focused = null;
		};
		
		FeaturedBox.unsetMouseover = function() {
			FeaturedBox.mouseover = null;
		};
		
		
		FeaturedBox.prototype.fill = function(poolItem, opt_existingCopy) {
			this.containedItem = poolItem;
			
			var copy = opt_existingCopy || this.containedItem.getCopy();
			
			this.containedItem.makeUndraggable();
			

			this.contentEl = copy;

			addClass(this.holderEl, 'draggable');
			addClass(this.targetEl, 'target-filled');

			this.targetEl.appendChild(copy);
			
			addClass(this.handleEl, 'dropzone-filled');

			this.containedItem.setFeatured(this);

		};
		
		FeaturedBox.prototype.empty = function() {
			var oldContentEl = this.contentEl;
		
			removeNode(this.contentEl);
			this.contentEl = null;
			
			removeClass(this.holderEl, 'draggable');
			removeClass(this.targetEl, 'target-filled');

			this.containedItem.unsetFeatured();
			this.containedItem = null;
			
			removeClass(this.handleEl, 'dropzone-filled');
			
			return oldContentEl;
		};
				
		FeaturedBox.prototype.makeSpace = function(opt_poolItem, opt_existingCopy) {
			var item = this.containedItem;
			var copy = null;

			if (item) {
				copy = this.empty();
			}
			
			if (opt_poolItem) {
				this.fill(opt_poolItem, opt_existingCopy);
			}
						
			if (item && this.nextBox) {
				this.nextBox.makeSpace(item, copy);
			}
		};
		
		FeaturedBox.prototype.closeHole = function(priorItem) {
			var item = this.containedItem;
		
			if (priorItem) {
				if (item) {
					var copy = this.empty();
					priorItem.fill(item, copy);
				}
			} else if (item) {
				this.empty();
			}
			
			if (this.nextBox) {
				this.nextBox.closeHole(this);
			}
		};
		
		FeaturedBox.prototype.getItemId = function() {			 
			if (!this.isFilled()) return;
			var className = (currentTabName == 'playlists' ? 'encryptedPlaylistId' : 'encryptedVideoId');
			try {
				return $$('div', className, this.contentEl)[0].innerHTML;
			} catch(e) {
				return '';
			}
		};
		
		PoolItem.focused = null;
		PoolItem.dragging = null;
		
		function PoolItem(item) {			
			item.cls = this;
			
			this.contentEl = item;
			
			this.handleEl = $ce('div', {'c': 'handle'});
			this.handleEl.cls = this;
			
			addListener(this.handleEl, 'mouseover', handlePoolMouseover);
			addListener(this.handleEl, 'mouseout', handlePoolMouseout);
			
			this.makeDraggable();
			this.contentEl.appendChild(this.handleEl);
		}
		
		PoolItem.prototype.destruct = function() {
			this.contentEl.cls = null;

			removeListener(this.handleEl, 'mouseover', handlePoolMouseover);
			removeListener(this.handleEl, 'mouseout', handlePoolMouseout);
			
			if (this.inFeaturedBox) {
				this.unsetFeatured();
			}
			
			removeNode(this.handleEl);
			delete this.handleEl;
			removeClass(this.contentEl, 'draggable');
			removeClass(this.contentEl, 'in-featured');
		};

		PoolItem.prototype.getItemId = function() {			 
			var className = (currentTabName == 'playlists' ? 'encryptedPlaylistId' : 'encryptedVideoId');
			try {
				return $$('div', className, this.contentEl)[0].innerHTML;
			} catch(e) {
				return '';
			}
		};

		
		PoolItem.prototype.setFocus = function() {
			PoolItem.focused = this;			
			addClass(this.contentEl, 'focused');
		};
		
		PoolItem.prototype.setMouseover = function() {
			PoolItem.mouseover = this;
		};
		
		
		PoolItem.unsetFocus = function() {
			var f = PoolItem.focused;
			if (!f) return;
			
			removeClass(f.contentEl, 'focused');
			PoolItem.focused = null;
		};
		
		PoolItem.unsetMouseover = function() {
			var f = PoolItem.mouseover;
			if (!f) return;
			PoolItem.mouseover = null;
		}
		
		
		function hideUntilContainedImagesAreLoaded(el, firstOnly) {
			var imgs = el.getElementsByTagName('img');
			if (!imgs.length) return;
			var img = imgs[0];
			if (img.complete) return;
			el.style.display = 'none';
			addListener(img, 'load', function() { el.style.display = ''; });
		}
		
		
		PoolItem.prototype.getDraggableCopy = function() {
			var copy = this.contentEl.cloneNode(true);
			var wrapper = $ce('div', {'c': 'dragging'}, copy);

			hideUntilContainedImagesAreLoaded(wrapper);
			
			addClass(wrapper, 'inner-box-colors');

			wrapper.contentEl = copy;
			
			return wrapper;
		};
		
		PoolItem.prototype.getCopy = function() {
			var temp = this.handleEl;
			removeNode(this.handleEl);
			
			var copy = this.contentEl.cloneNode(true);
			
			hideUntilContainedImagesAreLoaded(copy);
			
			this.contentEl.appendChild(temp);
			return copy;
		};
		
		PoolItem.prototype.makeDraggable = function() {
			addClass(this.contentEl, 'draggable');
			this.draggable = true;
		};
		
		PoolItem.prototype.makeUndraggable = function() {
			removeClass(this.contentEl, 'draggable');
			this.draggable = false;			
		};
		
		PoolItem.prototype.setFeatured = function(featuredBox) {
			this.inFeaturedBox = featuredBox;
			this.makeUndraggable();
			addClass(this.contentEl, 'in-featured');
			this.numberEl = $ce('div', {'c': 'number'}, $ctn(featuredBox.number))
			
			this.actualContentEl = $$('div', 'content', this.contentEl)[0];
			
			this.actualContentEl.appendChild(this.numberEl);
		};
		
		PoolItem.prototype.unsetFeatured = function() {
			this.inFeaturedBox = null;
			this.makeDraggable();
			removeClass(this.contentEl, 'in-featured');
			
			removeNode(this.numberEl);
			
		};
		
		function makeContainedItemsArrangeable(container) {
			var items = $$('div', 'playnav-item', container);
			var poolItems = [];
			for (var i = 0, len = items.length; i < len; i++) {
				var item = new PoolItem(items[i])
				poolItems.push(item);
				fakeCheckItems.push(item);
			}
			
			checkFakes();
			return poolItems;
		}
		
		function undoMakeContainedItemsArrangeable(container) {
			var items = $$('div', 'playnav-item', container);
			for (var i = 0, len = items.length; i < len; i++) {
				var item = items[i];
				if (item && item.cls) {
					item.cls.destruct();
				}
			}
		}
		
		
		function getMouseCoords(e) {
			var pos = {x: 0, y: 0};
			if (!e) var e = window.event;
			if (e.pageX || e.pageY) {
				pos = {x: e.pageX, y: e.pageY};
			}
			else if (e.clientX || e.clientY) 	{
				pos = {
					x: e.clientX + document.body.scrollLeft +
						document.documentElement.scrollLeft,
					y: e.clientY + document.body.scrollTop +
						document.documentElement.scrollTop
				};
			}
			return pos;
		}
		
		/**
		 * Get the cumulative offset of an element relative to the
		 * page, not just its immediate container.
		 */
		function getClientPosition(el) {
			var offset = {x: 0, y: 0};
			for (; el; el = el.offsetParent) {
				offset.x += (el.offsetLeft - el.scrollLeft);
				offset.y += (el.offsetTop - el.scrollTop);
			}
			return offset;
		}
		
		function handleMousemove(e) {
			mousePos = getMouseCoords(e);
			
			if (draggingEl) {
				updateDragPosition();
			}
			return false;
		}
		
		
		function handleMouseup(e) {
			if (PoolItem.dragging && FeaturedBox.focused) {
				var box = getFirstEmptyFeaturedBox();
				box.fill(PoolItem.dragging);
				if (box != FeaturedBox.focused &&
						!FeaturedBox.focused.isFilled()) {
					FeaturedBox.unsetFocus()
				}
				
				if (tempBox.isFilled()) {
					tempBox.empty();
				}
			}

			if (draggingEl) {
				removeNode(draggingEl);
				draggingEl = null;
				PoolItem.unsetFocus();

				if (PoolItem.dragging) {
					if (!PoolItem.dragging.inFeaturedBox) {
						PoolItem.dragging.makeDraggable();
					}
					PoolItem.dragging = null;
				}
				
				if (PoolItem.mouseover) {
					PoolItem.mouseover.setFocus();
				}
			}
			
		}

		function handleFeaturedMouseover(e) {
			var featured = this.cls;
			
			if (draggingEl) {
				if (featured.isFilled()) {
					featured.makeSpace();
				}
			
				featured.setFocus();
				addClass(draggingEl, 'generictheme');
			} else if (this.cls.isFilled()) {
				featured.setFocus();
			}
			
			featured.setMouseover();
		}
		
		function handleFeaturedMouseout(e) {
			var featured = this.cls;
		
			if (draggingEl) {
				removeClass(draggingEl, 'generictheme');
				if (!featured.isFilled()) {
					featured.closeHole();
				}				
			}
			
			FeaturedBox.unsetFocus();
		}

		function handlePoolMouseover(evt) {
			this.cls.setMouseover();
			if (!draggingEl) {
				this.cls.setFocus();
			}
		}
		
		function handlePoolMouseout(evt) {
			PoolItem.unsetMouseover();
			if (!draggingEl) {
				PoolItem.unsetFocus();
			}
		}

		function updateDragPosition() {
			draggingEl.style.left = mousePos.x - bodyOffset.x - dragOffset.x + 'px';
			draggingEl.style.top = mousePos.y - bodyOffset.y - dragOffset.y + 'px';
		}
		
		function handleMousedown(evt) {
			if (PoolItem.focused) {
				var f = PoolItem.focused;
				
				if (!f.draggable) {
					return false;
				}
				
				var contentOffset = getClientPosition(f.contentEl);
				PoolItem.dragging = f;

				f.makeUndraggable();

				draggingEl = f.getDraggableCopy();
								
				bodyEl.appendChild(draggingEl);
				
				dragOffset = {x: mousePos.x - contentOffset.x, y: mousePos.y - contentOffset.y};
				
				updateDragPosition();
				return false;
			} else if (FeaturedBox.focused) {
				var f = FeaturedBox.focused;
				var contentOffset = getClientPosition(f.targetEl);

				PoolItem.dragging = f.containedItem;
				PoolItem.dragging.setFocus();
				
				f.empty();
				
				draggingEl = PoolItem.dragging.getDraggableCopy();
				addClass(draggingEl, 'generictheme');
				PoolItem.dragging.makeUndraggable();

				bodyEl.appendChild(draggingEl);
				
				dragOffset = {x: mousePos.x - contentOffset.x, y: mousePos.y - contentOffset.y};
				
				updateDragPosition();
				
				return false;
			} else if (FeaturedBox.mouseover) {
				// In this case, the user is hovering over a disabled box.
				// Return false to prevent highlighting text.
				return false;
			}
		}

		function save() {
			if (navigatorNotReady()) {
				return;
			}

			setBusy();
			var videoIdList = [];
			var playlistIdList = [];

			idList = Iter(featuredBoxes).collect(function(box) {
				if (box.isFilled()) {
					return box.getItemId();
				}
			});

			if (currentTabName == 'playlists') {
				playlistIdList = idList;
			} else {
				videoIdList = idList;
			} 

			params = {
				playlist_name: currentTabName,
				video_id_list: videoIdList,
				playlist_id_list: playlistIdList
			};

			backend.call_box_method(box_info, params, 'save_arranged_items', saveResponse);

			if (currentTabName == 'uploads') {
				sort('default');
			}
		}

		function saveResponse(data) {
			unsetBusy();
			destruct();
			invalidateTab('all');
			invalidateTab(currentTabName);
			selectTab(currentTabName);
		}
		
		function cancel() {
			destruct();
		}
		
		// Load the user's saved item arrangement for this playlist.
		function getArrangedItems() {
			if (navigatorNotReady()) {
				return;
			}

			setBusy();
			params = { playlist_name: currentTabName };
			backend.call_box_method(box_info, params, 'get_arranged_items', handleGetArrangedItemsResponse);
		}
		
		// Once the top items are loaded, use the retrieved HTML to instantiate		
		// objects representing items in the pool, and use these to fill in the
		// featured boxes. We call these objects "fake" items, though, because
		// the "real" item exists in the pool. We use fakes because the
		// scrollbox page which contains the "real" item might not yet have been
		// loaded.
		function handleGetArrangedItemsResponse(data) {
			
			var el = $ce('div');
			el.innerHTML = data;
			var fakeItemList = makeContainedItemsArrangeable(el);
			
			
			if (fakeItemList.length > featuredBoxes.length) {
				grow();
			}
			
			// Index the fake items for easy searching, and fill boxes.		
			for (var i = 0; i < fakeItemList.length; i++) {
				var fake = fakeItemList[i];
				fakeItemIndex[fake.getItemId()] = fake;
				featuredBoxes[i].fill(fakeItemList[i]);
			}
			
			// Ok, now the fake items are ready for cross-checking.
			fakeItemsReady = true;
			
						
			unsetBusy();
			
			// Check whether these fake items already exist as real items
			// on the first (or already-populated) scrollbox pages.
			checkFakes();
			
		}
		
		// Check whether fake items exist for any of the items in the
		// "fakeCheckItems" array. If so, then take the fake item out of the
		// featured box and replace it with the real item which should go there.  
		function checkFakes() {
			if (!fakeItemsReady) return;
			while (fakeCheckItems.length) {
			
				// Get a real item; check if a fake version of it is in the hopper.
				// If yes, take out the fake and put in the genuine goods.
				var realItem = fakeCheckItems.pop();
				var fakeItem;
				if (fakeItem = fakeItemIndex[realItem.getItemId()]) {
					if (fakeItem.inFeaturedBox) {
						var featuredBox = fakeItem.inFeaturedBox;
						featuredBox.empty();
						featuredBox.fill(realItem);
					}
				}
			}
		}

		
		function updateItemCount(selectEl) {
			switch(parseInt(selectEl.value)) {
				default:
					return;
				case 6:
					if (featuredBoxes.length == 6) return;
					shrink();
					break;
				case 12:
					if (featuredBoxes.length == 12) return;
					grow();
					break;
			}
		}
		
		// Export public arranger stuff into parent namespace.
		arranger['toggle'] = toggle;
		arranger['updateItemCount'] = updateItemCount;
		arranger['destruct'] = destruct;
		arranger['save'] = save;
		arranger['cancel'] = cancel;
	})();

	/**
	 * Wrap a function intended for user consumption with setup and teardown
	 * code.
	 */
	function makeUserAction(fref) {
		return function() {
			autoskip = false;
			autoplay = true;
			fref.apply(this, arguments);
		};
	}

	// Export public variables and functions.
	playnav['getPlayer'] = function() { return player; };
	playnav['getPlaylistId'] = function() { return currentPlaylistId; };
	playnav['onNavigatorLoaded'] = onNavigatorLoaded;
	playnav['init'] = init;
	playnav['unInit'] = unInit;
	playnav['isInitted'] = isInitted;
	playnav['invalidateTab'] = invalidateTab;
	playnav['setBoxInfo'] = setBoxInfo;
	playnav['selectTab'] = selectTab;
	playnav['selectView'] = selectView;
	playnav['openBottomPopup'] = openBottomPopup;
	playnav['closePopup'] = closePopup;
	playnav['setVideoId'] = setVideoId;
	playnav['setPlaylistId'] = setPlaylistId;
	playnav['searchChannel'] = searchChannel;
	playnav['goToWatchPage'] = goToWatchPage;
	playnav['updateScrollbox'] = updateScrollbox;
	playnav['clearFirstTime'] = clearFirstTime;
	playnav['resizePlayview'] = resizePlayview;
	playnav['verifyAge'] = verifyAge;

	playnav['onPlayerStateChange'] = onPlayerStateChange;
	playnav['onPlayerError'] = onPlayerError;
	playnav['handleLocationHashUpdate'] = handleLocationHashUpdate;

	playnav['toggleFullVideoDescription'] = toggleFullVideoDescription;

	playnav['hideCachedPages'] = hideCachedPages;
	playnav['arranger'] = arranger;

	playnav['playVideo'] = makeUserAction(playVideo);
	playnav['loadPlaylist'] = loadPlaylist;
	playnav['selectPanel'] = selectPanel;

	playnav['playAll'] = playAll;
	playnav['skipNext'] = makeUserAction(skipNext);
	playnav['skipPrev'] = makeUserAction(skipPrev);
	playnav['sort'] = makeUserAction(sort);

})();



function removeElementById(id) {
	var el = $(id);
	if (el) {
		removeElement(el);
	}
}

function removeElement(el) {
	el.parentNode.removeChild(el);
}

window.poppedElements = [];

function removePoppedElements() {
	Iter(window.poppedElements).each(function(el) {
		el.parentNode.removeChild(el);
	});
	
	window.poppedElements = [];
}

function popDivToTop(el) {
	el = _gel(el);
	if (!el.__popped) {
		poppedElements.push(el);
		var pos = ani.getPosition(el);
		el.style.position = 'absolute';
		el.style.top = pos.y + 'px';
		el.style.left = pos.x + 'px';
		document.body.appendChild(el);
		el.__popped = true;
	}
}

function onChannelPlayerReady() {
	if (!window.scripts_are_loaded) {
		window.setTimeout(onChannelPlayerReady, 10);
	} else {
		playnav.init('movie_player', window.defaultPlaylistName);
		if (window.defaultPlaylistId) {
			playnav.setPlaylistId(window.defaultPlaylistId);
		}
	}
}


// For add-to-playlist popup
function submitToPlaylist() {
	var form = document.forms['addfavsform'];

	if (!form.playlist_id.value) {
		return;
	}
	if (!_gel('playlist_comment').__touched) {
		_gel('playlist_comment').value = '';
	}

	//self.disabled = true;
	var successCallback = function(xhr) {
		playnav.selectPanel('playlists', {'success': true});
	};
	var errorCallback = function(xhr) {
		playnav.selectPanel('playlists', {'error': true});
	};

	if (form.playlist_id.value == 'N') {
		// We can't duplicate playlist names, so we look for duplicate names
		// modulo a trailing space, and use that rather than failing to create
		// a new duplicate playlist.
		var z = form.new_playlist_name.value.toLowerCase();
		var y = -1;
		var x = '';
		for (var i = 0; i < form.playlist_id.options.length; i++) {
			x = form.playlist_id.options[i].text;
			y = x.lastIndexOf('(') - z.length;
			y = y < 0 ? 2 : y;
			if ((x.slice(0, z.length).toLowerCase() == z) && (y < 2)) {
				form.playlist_id.selectedIndex = i;
				break;
			}
		}
	}

	ajaxRequest(form.action, {
		postBody: extractFormData(form),
		onComplete: successCallback,
		onException: errorCallback 
	});
}


function addToPlaylistClose() {
	showDiv('popup_playlist_result');
	window.setTimeout(playnav.closePopup, 3000);
}

function handleAddToPlaylistsChange(el) {
	if (el.value == 'N') {
		_gel('new_playlist_area').style.display='block';
	} else {
		_gel('new_playlist_area').style.display='none';
	}
}


function update_featured(input) {
	_gel("featured_content").style.visibility = "visible";
	var feature_option = _gel("feature_" + input.value);
	if (input.checked) {
		feature_option.style.display = "";
		feature_option.disabled = false;
	} else {
		feature_option.style.display = "none";
		feature_option.disabled = true;
		if (feature_option.selected || has_selected_child(feature_option)) {
			var select = feature_option.parentNode;
			if (select.tagName.toLowerCase() != "select") select = select.parentNode;
			if (!pick_first_option(select)) {
				_gel("featured_content").style.visibility = "hidden";
			}
		}
	}
	if (input.value == 'playlists') {
		_gel("arrange_playlists").style.display = input.checked ? '' : "none";
	}
	var playlists_available = 0;
	var feature_playlists = _gel('feature_playlists');
	feature_playlists.style.display = 'none';
	feature_playlists.disabled = true;
	var all_playlists_option = null;
	for (var i = 1; i < feature_playlists.childNodes.length; i++) {
		if (feature_playlists.childNodes[i].value == "playlists") {
			all_playlists_option = feature_playlists.childNodes[i];
		} else if (feature_playlists.childNodes[i].style && feature_playlists.childNodes[i].style.display == '') {
			playlists_available++;
		}
	}
	if (playlists_available > 0) {
		feature_playlists.style.display = '';
		feature_playlists.disabled = false;
		if (all_playlists_option) {
			all_playlists_option.style.display = (playlists_available > 1) ? '' : 'none';
			all_playlists_option.disabled = (playlists_available < 2);
		}
	}
		
	var num_displayed = 0;
	if (_gel("display_uploads").checked) num_displayed++;
	if (_gel("display_favorites").checked) num_displayed++;
	if (_gel("display_playlists").checked && playlists_available > 0) num_displayed++;
	if (num_displayed > 1) {
		_gel('display_all').disabled = false;
		removeClass(_gel('display_all_container'), 'opacity50');
	} else {
		_gel('display_all').disabled = true;
		addClass(_gel('display_all_container'), 'opacity50');
	}
}
window.update_featured = Thread.bind(update_featured);

function pick_first_option(select) {
	for (var i = 0; i < select.options.length; i++ ) {
		var option = select.options[i];
		if (option.style.display == '' && option.parentNode.style.display == '') {
			option.selected = true;
			return true;
		}
	}
	return false;
}

function has_selected_child(optgroup) {
	if (optgroup.tagName.toLowerCase() != "optgroup") {
		return false;
	}
	for (var i = 0; i < optgroup.childNodes.length; i++) {
		if (optgroup.childNodes[i].selected) {
			return true;
		}
	}
	return false;
}

function handleAdLoaded() {
	setTimeout(function() {
		playnav.resizePlayview();
	}, 10);
}

window.handleAdLoaded = handleAdLoaded;
