/**
 * 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;

	// Hash settings.
	var initialLocationHash;

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

	// Reference to currently selected Tab: Uploads, Favorites, etc. 
	var currentTabName;
	var currentPlaylistId;
	var currentSearchQuery;
	var currentSortName;
	var invalidatedTabs = {};

	// Reference to video and video information
	var currentSelection;
	var currentVideoId;
	var currentVideoIndexId;
	var currentPanelName;
	var skipping = false;

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

	// Initialization flags
	var isLoaded = false;
	var isInit = false;

	// Limit calls to viewUpdate
	var viewUpdateRequested = false;
	var viewUpdatePending = 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 executeAll(funcs) {
		while (funcs.length > 0) {
			funcs.shift()();
		}
	}

	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');
	}

	/**
	 * Finish initial loading process. Execute any delayed player
	 * initialization and/or delayed UI interaction.
	 */
	function finishLoading() {
		isLoaded = true;

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

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

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

	/**
	 * Strip the standard prefix from a location hash if it exists.
	 */
	function stripLocationHash(locationHash) {
		if (locationHash.charAt(0) == '#') {
			return locationHash.substr(1);
		}
		return locationHash;
	}

	/**
	 * Get the current window location hash, strip # prefix if it exists.
	 */
	function getLocationHash() {
		return stripLocationHash(window.location.hash);
	}

	/**
	 * Parse the window location hash into an object with:
	 *   { length, view, playlistName, playlistId, videoIndex, videoId }
	 */
	function parseLocationHash(windowLocationHash) {
		var locationHash = {
			length: 0,
			view: '',
			playlistName: '',
			playlistId: '',
			videoIndex: '',
			videoId: ''
		};

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

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

			locationHash.view = view;
			locationHash.length = parts.length;
			if (locationHash.length > 1) {
				locationHash.playlistName = parts[1];
			}

			switch(locationHash.length) {
				case 3: // format: #<viewname>/user/<playlistId>
					locationHash.playlistId = parts[2];
					break;
				case 4: // format: #<viewname>/<playlistname>/<videoIndex>/<videoId>
					locationHash.videoIndex = parts[2];
					locationHash.videoId = parts[3];
					break;
				case 5: // format: #<viewname>/user/<playlistId>/<videoIndex>/<videoId>	
					locationHash.playlistId = parts[2];
					locationHash.videoIndex = parts[3];
					locationHash.videoId = parts[4];
					break;
			}
		}

		return locationHash;
	}

	/**
	 * Not valid until parseLocationHash has been called.
	 */
	function hasLocationHash() {
		return (initialLocationHash.view != '') ||
			(initialLocationHash.playlistName != '');
	}

	function _checkPermission(playlistid, videoid, ok_callback) {
		var params = {'playlist_name': playlistid, 'video_id': videoid};
		var callback = function(result) {
			if (result['success'] == true) {
				ok_callback();
			} else if (result['errors']) {
				// do something with the errors...
				alert(result['errors']);
			}
		};

		backend.call_box_method(box_info, params, 'check_permission_ajax', callback);
	}

	/**
	 * Update navigator state based on location hash object.
	 */
	function handleLocationHash(locationHash) {
		if (locationHash.view != '') {
			selectView(locationHash.view);
		}
		var handlerFunctions = [];
		var playlist = locationHash.playlistName;
		if (locationHash.playlistId != '') {
			playlist = locationHash.playlistId;
			handlerFunctions.push(selectPlaylist.bind(null,
				locationHash.playlistName,
				locationHash.playlistId));
		} else if (locationHash.playlistName != '') {
			handlerFunctions.push(selectPlaylist.bind(null,
				locationHash.playlistName));
		}
		if (locationHash.videoIndex != '') {
			handlerFunctions.push(playVideo.bind(null, playlist,
				locationHash.videoIndex, locationHash.videoId));
			_checkPermission(playlist, locationHash.videoId,
				executeAll.bind(null, handlerFunctions));
		} else {
			executeAll(handlerFunctions);
		}
	}

	/**
	 * Handle history calls.
	 */
	function handleLocationHashUpdate(windowLocationHash) {
		if (navigatorNotReady()) {
			return;
		}

		var locationHash = stripLocationHash(windowLocationHash);
		handleLocationHash(parseLocationHash(locationHash));
	}

	/**
	 * Delay hash update briefly to allow some browsers to finish first
	 * javascript execution pass.
	 */
	function updateHashLater() {
		return function() {
			handleLocationHash(initialLocationHash);
			finishLoading();
		};
	}

	/**
	 * Update location history, but only if the new history is different.
	 * Also, skip the update if the new history is just a subset of the
	 * current location hash.
	 */
	function updateHistory(viewName, tabName, playlistId, opt_videoIndex, opt_videoId) {
		parts = [viewName, tabName];
		if (playlistId != tabName) {
			parts.push(playlistId);
		}
		if (opt_videoIndex && opt_videoId) {
			parts.push(opt_videoIndex);
			parts.push(opt_videoId);
		}
		var windowLocationHash = getLocationHash();
		var locationHash = parts.join('/');
		// Only proceed if the first part of the current location
		// hash is not the same as the new location hash.
		if (windowLocationHash.indexOf(locationHash) != 0) {
			History.add(locationHash);
		}
	}

	/*
	 * 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;
	}

	/**
	 * This should be called after the playlist navigator DOM is loaded
	 * but before any other actions are performed. This initializes the
	 * javascript state and performs any other initial setup that does
	 * not require the player to be loaded.
	 */
	function onNavigatorLoaded(viewName, tabName) {
		initialLocationHash = parseLocationHash(getLocationHash());

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

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

		currentViewName = '';
		initTab(tabName);
		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();

		if (hasLocationHash()) {
			// Do not bother if hash does not change the state loaded.
			var viewMatches = (initialLocationHash.view == '') ||
				(initialLocationHash.view == viewName);
			var tabMatches =
				(initialLocationHash.playlistName == '') ||
				(initialLocationHash.playlistName == tabName);
			if (!viewMatches || !tabMatches ||
				(initialLocationHash.length > 2)) {
				setTimeout(updateHashLater(), 0);
				return;
			}
		}

		finishLoading();
	}

	/**
	 * This should be called after the player is loaded and ready. It
	 * finishes any player setup and sets and javascript state that
	 * depends on the player.
	 */
	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');

		if (currentViewName == 'play') {
			if (!hasLocationHash()) {
				executeAll(playnav.onPlayerLoadedFunctions);
				executeIf(playnav.mostRecentPlayVideoFunction);
				playnav.mostRecentPlayVideoFunction = null;
			}

			if (initialLocationHash.view == '') {
				setViewElementStyle('playnav-player', 'visibility', 'visible');
			}
		}
	}

	function unInit() {
		isInit = false;
	}

	function isInitted() {
		return isInit;
	}

	/**
	 * 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;
				requestViewUpdate();
			}
		}
	}

	/**
	 * Some of the tab names map to the same class name for the tab
	 * indicator, this function maps the tab name to the corresponding
	 * class name.
	 */
	function tabNameToClassName(name) {
		switch (name) {
			case 'user':
				return 'playlists';
			case 'search':
				return 'uploads';
		}
		return name;
	}

	/**
	 * Highlight a single tab selection.
	 */
	function highlightTab(name) {
		var tabBar = $('playnav-navbar');
		if (tabBar) {
			var tabs = $$('a', 'navbar-tab', tabBar);
			var numberOfTabs = tabs.length;
			for (var i = 0; i < numberOfTabs; i++) {
				removeClass(tabs[i], 'navbar-tab-selected');
			}
		}
		var tab = $('playnav-navbar-tab-' + tabNameToClassName(name));
		if (tab) {
			addClass(tab, 'navbar-tab-selected');
		}
	}

	function initTab(name) {
		currentTabName = name;
		highlightTab(currentTabName);
	}

	/**
	 * Selects a new tab and highlights the tab indicator. The tab content
	 * is not shown until the next view update.
	 */
	function selectTab(tabName, opt_suppressUpdate) {
		if (!isLoaded) {
			highlightTab(tabName);
			playnav.mostRecentSelectTabFunction = selectTab.bind(null, tabName, opt_suppressUpdate);
			return;
		}

		arranger.destruct();
		initTab(tabName);

		if (!opt_suppressUpdate && !viewUpdateRequired) {
			viewUpdateRequired = true;
			requestViewUpdate();
		}
	}

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

	function runPanelScriptsLater(name) {
		return function() { run_scripts_in_el('playnav-panel-' + name); };
	}

	function handlePanelLoaded(name, panel) {
		return 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(runPanelScriptsLater(name), 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 params = {
			'video_id' : currentVideoId,
			'playlist_id' : currentPlaylistId,
			'playlist_name' : currentTabName
		};

		// Apply panel-specific JS params to pass through.
		if (name == 'info') {
			params['video_index'] = currentVideoIndexId;
		}
		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,
			handlePanelLoaded(name, panel));
	}

	function updateViewOnly() {
		viewUpdateRequired = false;

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

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

	function updateView() {
		if (viewUpdateRequired) {
			updateViewOnly();
		}
		updateTab();
		setTimeout(resizeViewLater(), 10);

		if (viewUpdateRequested) {
			viewUpdateRequested = false;
			viewUpdatePending = true;
			setTimeout(updateViewLater(), 100);
		} else {
			viewUpdatePending = false;
		}
	}

	function updateViewLater() {
		return function() { updateView(); };
	}

	function requestViewUpdate() {
		if (!viewUpdatePending) {
			viewUpdatePending = true;
			setTimeout(updateViewLater(), 100);
		} else {
			viewUpdateRequested = true;
		}
	}

	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');

	/**
	 * Special handling for IE6.
	 */
	var userAgent = navigator.userAgent.toLowerCase();
	var isIE6 = userAgent.indexOf('msie 6') != -1 && userAgent.indexOf('opera') == -1;
	var forceLayoutLater = function(element) {
		return function() { element.style.zoom = '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.
	 */
	var resizeScrollbox = function(content, containerHeight) {
		if (!content) return;
		var body = $$('div', 'scrollbox-body', content)[0];
		if (body) {
			if (isIE6) {
				content.style.zoom = '0';
				containerHeight = 595;
			}
			var padding = 5;  // bottom only
			var outerScrollbox = $$('div', 'outer-scrollbox', body)[0];

			var height = containerHeight - outerScrollbox.offsetTop - padding;
			body.style.height = height + 'px';
			body.style.zoom = '1';

			if (isIE6) {
				content.style.height = height + 'px';
				setTimeout(forceLayoutLater(content), 0);
			}
		}
	};

	/**
	 * Apply resize scrollbox to all scrollbox-content class divs in the container.
	 * Use the container height as the height for all scroll boxes.
	 */
	var resizeScrollboxes = function(container) {
		var containerHeight = container.offsetHeight;
		var scrollboxes = $$('div', 'scrollbox-content', container);
		for (var i = 0; i < scrollboxes.length; i++) {
			resizeScrollbox(scrollboxes[i], containerHeight);
		}
	}

	var resizePlayviewWrapper = function() {
		setTimeout(function() { resizePlayview(); }, 10);
	}

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

		content.style.height = (container.offsetHeight - content.offsetTop) + 'px';
		resizeScrollboxes(content);
	}

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

	var resizeViewLater = function() {
		return function() { resizeView(); };
	}

	resizeScrollboxes = Thread.bind(resizeScrollboxes, '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(opt_viewName) {
		if (!opt_viewName) {
			opt_viewName = currentViewName;
		}
		var viewNode = $('playnav-' + opt_viewName + '-content');
		if (!viewNode) {
			return;
		}
		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);
		updateHistory(currentViewName, currentTabName, currentPlaylistId);

		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';
			resizeScrollboxes(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);

		resizeScrollboxes(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) {
		selectPlaylist('user', playlistId);
	}

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

	function selectionTargets() {
		return [['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('-')];
	}

	function selectVideoClass(classFunction) {
		return function(id) {
			var element = $(id);
			if (element) {
				classFunction(element, 'playnav-item-selected');
			}
		};
	}

	function selectVideo(selection) {
		if (currentSelection) {
			Iter(selectionTargets()).each(selectVideoClass(removeClass));
		}
		currentSelection = selection;
		if (currentSelection) {
			Iter(selectionTargets()).each(selectVideoClass(addClass));
		}
	}

	/**
	 * 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 && !skipping) {
			updateHistory('play', currentTabName, playlistId, videoIndexId, videoId);
		}

		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;
			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) {
			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) {
					updateHistory(currentViewName, currentTabName, currentPlaylistId,
						currentVideoIndexId, currentVideoId);
				}
				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' : '56px';
	}

	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();
	}

	/**
	 * 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'] = resizePlayviewWrapper;
	playnav['verifyAge'] = verifyAge;

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

	playnav['toggleFullVideoDescription'] = toggleFullVideoDescription;

	playnav['hideCachedPages'] = hideCachedPages;

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

	playnav['playAll'] = playAll;
	playnav['skipNext'] = makeUserAction(skipNext);
	playnav['skipPrev'] = makeUserAction(skipPrev);
	playnav['sort'] = makeUserAction(sort);
	playnav['setupScrollableItems'] = setupScrollableItems;
	
	playnav['resizeScrollbox'] = resizeScrollboxes;
		
	playnav['getBoxInfo'] = function() {
		return box_info;
	};
	
	playnav['getCurrentTabName'] = function() { return currentTabName; };
	
	playnav['getCurrentScrollboxId'] = getCurrentScrollboxId;

})();



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() {
	playnav.resizePlayview();
}

window.handleAdLoaded = handleAdLoaded;

