
// Get the namespace.
window.EXANIMO = window.EXANIMO || {};




/**
 *
 * Adapted from the YAHOO Global Object.
 * <http://developer.yahoo.com/yui/docs/yahoo/YAHOO.html>
 *
 *  Returns the namespace specified and creates it if it doesn't exist
 *
 *     @param ns    the name of the namespace
 *     @return      a reference to the namespace object
 *
 */
EXANIMO.namespace = function(ns)
{
    if (!ns || !ns.length)
        return;

    var levels = ns.split('.');
    var nsobj = EXANIMO;

    // EXANIMO is implied, so it is ignored if it is included
    for (var i = (levels[0] == 'EXANIMO') ? 1 : 0; i < levels.length; i++)
    {
        nsobj[levels[i]] = nsobj[levels[i]] || {};
        nsobj = nsobj[levels[i]];
    }

    return nsobj;
}




/**
 *
 * EXANIMO.utils.getBrowserInfo
 *
 *     Stupid crappy browser sniffing crap.
 *
 *     @author     matthew at exanimo dot com
 *     @version    2006.07.25
 *
 */

    EXANIMO.namespace('EXANIMO.utils');

    /**
     *
     * Gets the browser information.
     *
     *     @return    an object with a browser and a version property
     *
     */
    EXANIMO.utils.getBrowserInfo = function()
    {
        if (navigator.appName.indexOf('Microsoft Internet') != -1)
            return {browser: 'MSIE'};
        if (navigator.userAgent.indexOf('Safari') != -1)
            return {browser: 'SAFARI'};
        else
            return {browser: 'OTHER'};
    }




/**
 *
 * EXANIMO.utils.getSWFObject
 *
 *     From <http://www.permadi.com/tutorial/flashjscommand/>
 *
 */

    EXANIMO.namespace('EXANIMO.utils');

    /**
     *
     * Gets a SWF from the DOM.
     *
     *     @param id    the name/id of the movie
     *     @return      the swf object
     *
     */
    EXANIMO.utils.getSWFObject = function(id)
    {
        if (window.document[id])
            return window.document[id];
        if (navigator.appName.indexOf('Microsoft Internet') == -1)
            if (document.embeds && document.embeds[id])
                return document.embeds[id];
        else
            return document.getElementById(id);
    }




/**
 *
 * StateManager
 *
 *     Adds state management (back button and deep-linking) to your single-page
 *     web application.
 *
 *     For usage instructions, see
 *     <http://exanimo.com/javascript/using-the-statemanager-in-ajax-apps>.
 *
 *     copyright 2006 matthew john tretter.  available under the MIT License.
 *     (<http://www.opensource.org/licenses/mit-license.php>)
 *
 *     todo:
 *         - pick one of _cancelLoad or _refreshPage
 *         - clean up code
 *             - look at all the stuff that hasn't been looked at since July
 *               (_updateIFrame, etc.) and make sure it's the best way.
 *         - OO changes
 *             - reduce redundancy (less "EXANIMO.managers.StateManager")
 *             - use Function instead of Object.
 *
 *     version history:
 *         2006.11.16
 *             - fixed bug introduced in last version that broke deeplinks for
 *               Safari.
 *         2006.11.12
 *             - added AJAX autoInit
 *             - logState is now setState
 *             - added new events 'stateRegistered', 'stateSet'
 *         2006.11.10
 *             - major updates. loadState is now logState. there was no reason
 *               to load a state without logging it -- that's the same as
 *               calling the stateChange handler.
 *         2006.11.09
 *             - stateChange event now tells you if the state was changed
 *               directly or indirectly (by back, forward, or a deeplink)
 *         2006.11.08
 *             - fixed a bug that caused safari to load states twice
 *         2006.11.07
 *             - some misc. bugs fixes / tons of cleanup
 *             - registerState is gone in favor of setTitle
 *         2006.11.06
 *             - renamed to StateManager
 *             - now supports AJAX
 *         2006.11.05
 *             - FULL Safari support!!!
 *         2006.10.31
 *             - reworked hash method to eliminate FP9 bugs.
 *             - defineState is now registerState
 *         2006.10.30
 *             - added defineState to fix a problem with deep linking
 *         2006.09.25
 *             - added initialize function
 *             - now uses object literal
 *         2006.09.14
 *             - BACK BUTTON FIXED FOR SAFARI!! unfortunately, hash no longer
 *               updates, though you can still deep-link if you know the hash.
 *         2006.09.13
 *             - if movie is not provided, checks for variable filled by
 *               JSInterface's locateSelf.
 *         2006.07.27
 *             - renamed from BB5K
 *             - got rid of #lookup= in hash.
 *         2006.07.20
 *
 *     @author     matthew at exanimo dot com
 *     @version    2006.11.12
 *
 */

EXANIMO.namespace('EXANIMO.managers');
if (!EXANIMO.managers.StateManager)
{
    EXANIMO.managers.StateManager = {


        CHECK_RATE: 100,
        stateChangeHandler: null,
        event: null,

        _initialized: false,
        _autoInitInterval: setInterval(
            function()
            {
                if (document.body && !EXANIMO.managers.StateManager._initialized)
                {
                    EXANIMO.managers.StateManager.initialize();
                }
            },
            10
        ),
        _checkInterval: null,
        _method: null,
        _swf: null,

        // Browser specific variables.
        _oldStateID: null,
        _refreshPage: null,
        _cancelLoad: false,


        /**
         *
         * Does all the behind-the-scenes startup work for the StateManager.
         *
         *     @param swf    (optional) the SWF to add state management to. if
         *                   not present, the function will use the swf in
         *                   EXANIMO.utils.JSInterface.swfObject. if that is
         *                   also empty, it goes into AJAX mode.
         *
         */
        initialize: function(swf)
        {
            // Stop trying to auto-initialize and reset StateManager.
            clearInterval(EXANIMO.managers.StateManager._autoInitInterval);
            clearInterval(EXANIMO.managers.StateManager._checkInterval);
            EXANIMO.managers.StateManager._initialized = true;

            // If a SWF was provided, remember it.
            if (typeof swf != 'undefined')
            {
                EXANIMO.managers.StateManager._swf = typeof(swf) == 'string' ? EXANIMO.utils.getSWFObject(swf) : swf || null;
            }

            // Decide which method to use by (blech!) browser sniffing.
            var browserInfo = EXANIMO.utils.getBrowserInfo();
            switch(browserInfo.browser)
            {
                case 'MSIE':
                    EXANIMO.managers.StateManager._method = 'IFRAME';
                    break;
                case 'SAFARI':
                    EXANIMO.managers.StateManager._method = 'LINK';
                    break;
                default:
                    EXANIMO.managers.StateManager._method = 'HASH';
                    break;
            }

            switch(EXANIMO.managers.StateManager._method)
            {
                case 'HASH':

                    var initialState = EXANIMO.managers.StateManager._getStateID();
                    EXANIMO.managers.StateManager._oldStateID = initialState == 'home' ? 'home' : null;

                    // Watch to see if the hash changes.
                    var checkForHashChange = function()
                    {
                        var stateID = EXANIMO.managers.StateManager._getStateID();

                        if (stateID != EXANIMO.managers.StateManager._oldStateID)
                        {
                            EXANIMO.managers.StateManager._oldStateID = stateID;
                            EXANIMO.managers.StateManager._dispatchEvent(stateID)
                        }
                    }

                    EXANIMO.managers.StateManager._checkInterval = setInterval(checkForHashChange, EXANIMO.managers.StateManager.CHECK_RATE);

                    break;

                //
                // Do the actual initialization, based on the method determined
                // above.
                //
                case 'IFRAME':

                    //
                    // Make sure the iframe knows that, when it loads, it should
                    // not refresh this page.
                    //
                    EXANIMO.managers.StateManager._refreshPage = false;

                    // Create and attach the iframe.
                    var iframe = document.createElement('iframe');
                    iframe.setAttribute('src', 'about:blank');
                    iframe.setAttribute('name', 'EXANIMO-managers-StateManager-iFrame');
                    iframe.setAttribute('id', 'EXANIMO-managers-StateManager-iFrame');
                    iframe.style.visibility = 'hidden';
                    iframe.style.width = '0';
                    iframe.style.height = '0';
                    iframe.style.position = 'absolute';
                    iframe.style.overflow = 'hidden';
                    document.body.appendChild(iframe);

                    // If a state id is already present in the hash, go to it.
                    var stateID = EXANIMO.managers.StateManager._getStateID();
                    if (stateID != 'home')
                    {
                        setTimeout(
                            function()
                            {
                                EXANIMO.managers.StateManager._dispatchEvent(stateID)
                            },
                            0
                        );
                    }

                    // Update the page and hash from the iframe.
                    frames['EXANIMO-managers-StateManager-iFrame'].document.open();
                    if (stateID)
                        frames['EXANIMO-managers-StateManager-iFrame'].document.write('<script>parent.document.location.hash = "' + (stateID == 'home' ? '' : stateID) + '"; parent.EXANIMO.managers.StateManager._updateIFrame("' + stateID + '");</script>');
                    else
                        frames['EXANIMO-managers-StateManager-iFrame'].document.write('<script>parent.document.location.hash = ""; parent.EXANIMO.managers.StateManager._updateIFrame();</script>');
                    frames['EXANIMO-managers-StateManager-iFrame'].document.close();

                    break;

                case 'LINK':

                    document.location.EXANIMO = document.location.EXANIMO || {};
                    document.location.EXANIMO.managers = document.location.EXANIMO.managers || {};
                    document.location.EXANIMO.managers.StateManager = document.location.EXANIMO.managers.StateManager || {};

                    var loc = document.location.EXANIMO.managers.StateManager;

                    // Make sure the last state is loaded when you come back to
                    // this page after navigating away.
                    window.onunload = function()
                    {
                        loc.oldHistoryLength = -1;
                    }

                    if (loc.deepLink && loc.deepLink != 'home')
                    {
                    	loc.oldHistoryLength = -1;
						loc.deepLink = null;
                    }

                    // Create a list of the states we click through.
                    if (typeof loc.stateList == 'undefined')
                    {
                        loc.stateList = [EXANIMO.managers.StateManager._getStateID() || 'home'];
                        loc.deepLink = loc.stateList[0];

                        loc.offset = history.length - 1;
                        while (loc.offset)
                        {
                            loc.stateList.unshift(null);
                            loc.offset--;
                        }
                        delete loc.offset;

                        loc.oldHistoryLength = document.location.hash ? -1 : history.length;

                    }

                    // Watch to see if the length of the history object changes.
                    var checkForHistoryLengthChange = function()
                    {
                        var loc = document.location.EXANIMO.managers.StateManager;

                        if (EXANIMO.managers.StateManager._cancelLoad)
                        {
                            EXANIMO.managers.StateManager._cancelLoad = false;
                            loc.oldHistoryLength = history.length;
                            return;
                        }

                        if (history.length != loc.oldHistoryLength)
                        {
                            var stateID = loc.stateList[history.length - 1];

                            EXANIMO.managers.StateManager._dispatchEvent(stateID)
                            loc.oldHistoryLength = history.length;
                        }
                    }

                    EXANIMO.managers.StateManager._checkInterval = setInterval(checkForHistoryLengthChange, EXANIMO.managers.StateManager.CHECK_RATE);

                    break;

            }
        },


        /**
         *
         * Adds an entry to the history.
         *
         *     @param stateID       a unique identifier corresponding to a
         *                          specific state
         *
         */
        setState: function(stateID, title)
        {
            // Set the title.
            if (title)
            {
                EXANIMO.managers.StateManager.setTitle(title);
            }

            // Block infinite loops.
            if (EXANIMO.managers.StateManager.event) return;

            switch(EXANIMO.managers.StateManager._method)
            {
                case 'HASH':

                    document.location.hash = stateID == 'home' ? '#' : stateID;
                    EXANIMO.managers.StateManager._oldStateID = stateID;

                    break;

                case 'IFRAME':

                    EXANIMO.managers.StateManager._setIFrame(stateID);

                    break;

                case 'LINK':

                    EXANIMO.managers.StateManager._cancelLoad = true;

                    var a = document.createElement('a');
                    a.setAttribute('href', stateID == 'home' ? '#' : '#' + stateID);

                    var evt = document.createEvent('MouseEvents');
                    evt.initEvent('click', true, true);
                    a.dispatchEvent(evt);

                    document.location.EXANIMO.managers.StateManager.stateList.push(stateID);

                    break;

            }

            // Dispatch the stateChange events
            EXANIMO.managers.StateManager._dispatchEvent(stateID, true);

        },


        /**
         *
         * Sets the window's title.
         *
         *     @param title    the string to put in the browser's title bar
         *
         */
        setTitle: function(title)
        {
            window.document.title = title || ' ';
        },


        /**
         *
         * Dispatches a stateChange event.
         *
         *     @param stateID    the id of the state to load
         *     @param manual     was the stateChange manual? manual changes
         *                       are the result of calls to setState. (automatic
         *                       changes are the result of the back / forward
         *                       buttons or deep-linking.)
         *
         */
        _dispatchEvent: function(stateID, manual)
        {
            stateID = stateID || 'home';

            // If a SWF reference has been stored using the JSInterface's
            // locateSelf (and the StateManager wasn't initialized with a swf
            // argument), use that.
            if ((typeof EXANIMO.utils.JSInterface != 'undefined') && (EXANIMO.utils.JSInterface.swfObject))
            {
                EXANIMO.managers.StateManager._swf = EXANIMO.managers.StateManager._swf || EXANIMO.utils.JSInterface.swfObject;
            }

            // Flash: call the SWF's loadState function..
            if (EXANIMO.managers.StateManager._swf)
            {
                // ..but only if
                //     1) it hasn't been caused by navigating within flash. (if
                //        it has, we don't want to navigate again.)
                //     2) a handler has been set
                if (!manual && EXANIMO.managers.StateManager._swf.dispatchStateChangeEvents)
                {
                    EXANIMO.managers.StateManager._swf.dispatchStateChangeEvents(stateID);
                }
            }

            // AJAX: dispatch a stateChange event.
            else
            {
                EXANIMO.managers.StateManager.event = {id: stateID};

                // If a handler is set, call it.
                if (EXANIMO.managers.StateManager.stateChangeHandler)
                {
                    EXANIMO.managers.StateManager.stateChangeHandler(EXANIMO.managers.StateManager.event);
                }

                if (manual)
                {
                    if (EXANIMO.managers.StateManager.stateSetHandler)
                    {
                        EXANIMO.managers.StateManager.stateSetHandler(EXANIMO.managers.StateManager.event);
                    }
                }
                else
                {
                    if (EXANIMO.managers.StateManager.stateRevisitedHandler)
                    {
                        EXANIMO.managers.StateManager.stateRevisitedHandler(EXANIMO.managers.StateManager.event);
                    }
                }

                EXANIMO.managers.StateManager.event = null;
            }
         },


        /**
         *
         * Retrieves state ID.
         *
         *     @return    the id of the current state
         *
         */
        _getStateID: function()
        {
            return document.location.href.split('#')[1] || 'home';
        },


        /**
         *
         * Sets the iframe to a specific lookup.
         *
         *     @param lookup    the lookup to use
         *
         */
        _setIFrame: function(stateID)
        {
            EXANIMO.managers.StateManager._refreshPage = false;
            var iframe = document.getElementById('EXANIMO-managers-StateManager-iFrame');

            switch(EXANIMO.managers.StateManager._method)
            {
                case 'IFRAME':

                    frames['EXANIMO-managers-StateManager-iFrame'].document.open();
                    frames['EXANIMO-managers-StateManager-iFrame'].document.write('<script>parent.document.location.hash = "' + (stateID == 'home' ? '#' : stateID) + '"; /* Wait for IE to impose its title before setting ours. */ setTimeout( function(){ parent.EXANIMO.managers.StateManager._updateIFrame("' + stateID + '"); }, 0);</script>');
                    frames['EXANIMO-managers-StateManager-iFrame'].document.close();

                    break;
            }
        },


        /**
         *
         * Updates the iframe
         *
         *     @param stateID    the id of the state to record
         *
         */
        _updateIFrame: function(stateID)
        {
            if (EXANIMO.managers.StateManager._refreshPage)
            {
                EXANIMO.managers.StateManager._dispatchEvent(stateID);
            }
            else
            {
                EXANIMO.managers.StateManager._refreshPage = true;
            }
        }




    }




}




// Copyright (c) 2006 matthew john tretter.  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// ---------------------------------------------
//                Sample playing
// ---------------------------------------------
STOPPED = 1;
LOADING = 2;
PLAYING = 3;
ERROR = 4;

var gHomePage = 'http://www.pandora.com/';
var gCurrentSample = null;
var gCurrentName = null;
var gSampleState = STOPPED;
var gTimeoutID = null;
var gTimeoutStep = -1;
var gTokenList = [];
var gCurrentDisc = 0;

// This function assumes the samples have no unique "name" appended to the tokens
function playAllSamples(/*int*/ discNum, /*string[]*/ tokenList) {
	stopCurrentSample(false);
	gTokenList = tokenList;
	gCurrentDisc = discNum;
	var img = getDiv("sampleAllStop" + gCurrentDisc);
	if (img) {
		img.style.display = "inline";
	}
	img = getDiv("sampleAllPlay" + gCurrentDisc);
	if (img) {
		img.style.display = "none";
		img.src = gHomePage + 'images/sample-all.gif'
	}
	playNextSample();
}

function playNextSample(/*string*/ name) {
	if (gTokenList.length > 0) {
		var token = gTokenList.shift();
		playSampleAudio(token, "", true, true);
	} else {
		stopCurrentSample(false);
	}
}

// Play a 30-second clip for the specified token.  The "name" arg is a unique
// string that is appended to the token to come up with the DIV names for the DHTML.
// This is to allow the same sample to appear on a page in more than one location
// without interference (i.e. the profile page)
function playSampleAudio(/*string*/ token, /*string*/ name, /*boolean*/ leaveTokenList, /*boolean*/ allowExplicit) {
	stopCurrentSample(leaveTokenList);

	gCurrentSample = token;
	gCurrentName = name;
	setSampleDisplay(token, name, LOADING);

	flashVars = ["musicId=" + token + "&uniqueName=" + name + "&sampleURL=" + escape(gHomePage + "favorites/getSample.jsp?token=" + token + "&allowExplicit=" + allowExplicit)];
	getAudioDiv().innerHTML = [
			'<OBJECT classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"',
			'		codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0"',
			'		WIDTH="1"',
			'		HEIGHT="1"',
			'       id="SamplePlayer">',
			'	<PARAM NAME=movie VALUE="' + gHomePage + 'SamplePlayer.swf">',
			'	<PARAM NAME=quality VALUE=high>',
			'	<PARAM NAME=bgcolor VALUE=#FFFFFF>',
			'	<PARAM NAME=allowscriptaccess VALUE=always>',
			'	<PARAM NAME=menu VALUE=false>',
			' <PARAM NAME="FlashVars" VALUE="' + flashVars + '">',
			'<EMBED src="' + gHomePage + 'SamplePlayer.swf"',
			'       quality=high',
			'       bgcolor=#FFFFFF',
			'       allowscriptaccess="always"',
			'       WIDTH="1"',
			'       HEIGHT="1"',
			'       MENU="false"',
			'       NAME="SamplePlayer" ALIGN="" TYPE="application/x-shockwave-flash"',
			'       PLUGINSPAGE="http://www.macromedia.com/go/getflashplayer"',
			'       FlashVars="' + flashVars + '">',
			'</EMBED>',
			'</OBJECT>'
			].join("");
}

// stop the current sample, unless it errored (we want errored load to stay
// displayed as errors)
function stopCurrentSample(/*boolean*/ leaveTokenList) {
	if (gCurrentSample != null && gSampleState != ERROR) {
		stopSampleAudio(gCurrentSample, gCurrentName, leaveTokenList);
	}
}

function stopSampleAudio(/*string*/ token, /*string*/ name, /*boolean*/ leaveTokenList) {
    if ($(".landing_sample_player_slider_container").length > 0){
        onLandingSampleComplete();
    }
    
	getAudioDiv().innerHTML = "";
	setSampleDisplay(token, name, STOPPED);
	if (!leaveTokenList) {
		gTokenList = [];
		var img = getDiv("sampleAllStop" + gCurrentDisc);
		if (img) {
			img.style.display = "none";
		}
		img = getDiv("sampleAllPlay" + gCurrentDisc);
		if (img) {
			img.style.display = "inline";
		}
	}
}

function onLandingSampleLoad(/*string*/ token, /*string*/ name, /*boolean*/ success) {
    var startTime = (new Date()).getTime();
    var travelDistance = 390;

    window['landingSampleInterval'] = setInterval(function(){
        var landing_sample_player_middle = $("#landing_sample_player_middle" + token + name);
        var landing_sample_player_right = $("#landing_sample_player_right" + token + name);
        var landing_sample_player_time = $("#landing_sample_player_time" + token + name);

        var msElapsed = ((new Date()).getTime() - startTime);
        var percentDone = msElapsed / 30000; // assumes a 30 seconds sample

        if (percentDone > 1){
            clearTimeout(window['landingSampleInterval']);
            landing_sample_player_time.text(":30");
        }else{
            landing_sample_player_middle.css("width", travelDistance * percentDone);
            landing_sample_player_right.css("left", 67 + travelDistance * percentDone);
            
            var secondsElapsed = Math.floor(msElapsed / 1000);
            landing_sample_player_time.text(":" + (secondsElapsed < 10 ? "0" : "") + secondsElapsed);
        }
    }, 50);
}

function onLandingSampleComplete(){
    clearTimeout(window['landingSampleInterval']);
    var landing_sample_player_middle = $(".landing_sample_player_middle");
    var landing_sample_player_right = $(".landing_sample_player_right");
    var landing_sample_player_time = $(".landing_sample_player_time");

    landing_sample_player_middle.css("width", 1);
    landing_sample_player_right.css("left", 67);
    landing_sample_player_time.text(":00");
}

function onSampleLoad(/*string*/ token, /*string*/ name, /*boolean*/ success) {
    if ($(".landing_sample_player_slider_container").length > 0){
        onLandingSampleLoad(token, name, success);
    }
    
    if (success) {
        setSampleDisplay(token, name, PLAYING);
    } else {
        setSampleDisplay(token, name, ERROR);
        // If "playing all", continue on to next sample (RADIO-6764)
        playNextSample();
    }
}

function onSampleComplete(/*string*/ token, /*string*/ name) {
	setSampleDisplay(token, name, STOPPED);
	playNextSample();
}

function getAudioDiv() {
	var div = getDiv("audioDiv");
	if (div == null) {
		div = document.createElement("DIV");
		div.id = "audioDiv";
		document.documentElement.appendChild(div);
	}
	return div;
}

function setSampleDisplay(token, name, state) {
	gSampleState = state;

	setDisplay("sampleStopped" + token + name, state == STOPPED);
	setDisplay("sampleLoading" + token + name, state == LOADING);
	setDisplay("samplePlaying" + token + name, state == PLAYING);
	setDisplay("sampleError" + token + name, state == ERROR);
	if (state == LOADING) {
		// workaround for Safari which doesn't send onmouseout when the stopped
		// div is hidden
		document.getElementById("sampleStoppedImg" + token + name).src = gHomePage + "images/30-play.gif";
	}

	// update the title text flashing except when we're transitioning from
	// LOADING to PLAYING (in that case, we want to keep flashing at regular
	// intervals rather than throwing of the rhythm)
	if (state != PLAYING) {
		if (gTimeoutID != null) clearTimeout(gTimeoutID);
		updateStep();
	}
}

function updateStep() {
	gTimeoutStep++;

	if (gTimeoutStep % 2 == 0) {
		setTitleDisplay(gCurrentSample, gCurrentName, gSampleState);
	} else {
		setTitleDisplay(gCurrentSample, gCurrentName, STOPPED);
	}

	if (gSampleState == LOADING || gSampleState == PLAYING) {
		gTimeoutID = setTimeout(updateStep, 1500);
	} else {
		gTimeoutStep = -1;
		gTimeoutID = null;
	}
}

function setTitleDisplay(token, name, state) {
	setVisibility("titleStopped" + token + name, state == STOPPED || state == ERROR);
	setVisibility("titleLoading" + token + name, state == LOADING);
	setVisibility("titlePlaying" + token + name, state == PLAYING);
}

function setDisplay(id, displayed) {
	if (document.getElementById(id) != null) {
		document.getElementById(id).style.display = displayed ? "" : "none";
	}
}

function setVisibility(id, displayed) {
	if (document.getElementById(id) != null) {
		document.getElementById(id).style.visibility = displayed ? "visible" : "hidden";
	}
}

function landingPageHTML(token, uniqueName) {
    if (window.ipBlocked) {
        return "&nbsp;";	// Don't allow 30-second samples for blocked IPs
    }
    var allowExplicit = false;
    var s = "";

    s += '<div>';
    s += '<a id="sampleStopped' + token + uniqueName + '" title="Play sample" href="javascript:playSampleAudio(\'' + token + '\', \'' + uniqueName + '\', false, ' + allowExplicit + ');">';
    s += '<img id="sampleStoppedImg' + token + uniqueName + '" src="' + gHomePage + 'images/landing/play_sample_button.gif" ';
    s += 'border=0 alt="play sample" onmouseover="this.src=\'' + gHomePage + 'images/landing/play_sample_button_hover.gif\'" onmouseout="this.src=\'' + gHomePage + 'images/landing/play_sample_button.gif\'">';
    s += '</a>';

    // Loading icon
    s += '<span id="sampleLoading' + token + uniqueName + '" style="display:none">';
    s += '<img src="' + gHomePage + 'images/landing/play_sample_button_hover.gif" alt="play sample"></span>';

    // Stop icon
    s += '<a id="samplePlaying' + token + uniqueName + '" href="javascript:stopSampleAudio(\'' + token + '\', \'' + uniqueName + '\');" style="display:none">';
    s += '<img src="' + gHomePage + 'images/landing/stop_sample_button.gif" ';
    s += 'border=0 alt="stop sample" onmouseover="this.src=\'' + gHomePage + 'images/landing/stop_sample_button_hover.gif\'" onmouseout="this.src=\'' + gHomePage + 'images/landing/stop_sample_button.gif\'">';
    s += '</a>';

    // Error icon
    s += '<span id="sampleError' + token + uniqueName + '" style="display:none">';
    s += '<img src="' + gHomePage + 'images/landing/play_sample_button.gif" alt="sample not available"></span>';



    s += '<div class="landing_sample_player_slider_container">';
    s += '<div class="landing_sample_player_sliderbar" id="landing_sample_player_sliderbar' + token + uniqueName + '"></div>';
    s += '<div class="landing_sample_player_left" id="landing_sample_player_left' + token + uniqueName + '"></div>';
    s += '<div class="landing_sample_player_middle" id="landing_sample_player_middle' + token + uniqueName + '"></div>';
    s += '<div class="landing_sample_player_right" id="landing_sample_player_right' + token + uniqueName + '"></div>';
    s += '<div class="landing_sample_player_time" id="landing_sample_player_time' + token + uniqueName + '">:00</div>';
    s += '</div>';
    s += '</div>';

    return s;
}

function sampleHTML(token, uniqueName, allowExplicit) {
	if (window.ipBlocked) {
		return "&nbsp;";	// Don't allow 30-second samples for blocked IPs
	}
	var s = "";
	if (allowExplicit == null) {
		allowExplicit = false;
	}

	s += '<a id="sampleStopped' + token + uniqueName + '" title="Play sample" href="javascript:playSampleAudio(\'' + token + '\', \'' + uniqueName + '\', false, ' + allowExplicit + ');">';
	s += '<img id="sampleStoppedImg' + token + uniqueName + '" src="' + gHomePage + 'images/30-play.gif" ';
	s += 'border=0 width="14" height="15" alt="play sample" onmouseover="this.src=\'' + gHomePage + 'images/30-play-hover.gif\'" onmouseout="this.src=\'' + gHomePage + 'images/30-play.gif\'"></a>';
	// Loading icon
	s += '<span id="sampleLoading' + token + uniqueName + '" style="display:none">';
	s += '<img src="' + gHomePage + 'images/30-play-hover.gif" width="14" height="15" alt="play sample"></span>';
	// Stop icon
	s += '<a id="samplePlaying' + token + uniqueName + '" href="javascript:stopSampleAudio(\'' + token + '\', \'' + uniqueName + '\');" style="display:none">';
	s += '<img src="' + gHomePage + 'images/30-play-stop.gif" border=0 width="14" height="15" alt="stop"></a>';
	// Error icon
	s += '<span id="sampleError' + token + uniqueName + '" style="display:none">';
	s += '<img src="' + gHomePage + 'images/30-play-error.gif" width="14" height="15" alt="sample not available"></span>';

	return s;
}

function trackHTML(token, title, linkToDetail, textClass, playingClass){
	if (window.ipBlocked && title == "Play Sample") {
		return "&nbsp;";	// Don't allow 30-second samples for blocked IPs
	}
	var s = "";

	// Loading icon
	s += '<span id="titleLoading' + token + '" class="' + playingClass + '">Loading Sample</span>';
	// Play indicator
	s += '<span id="titlePlaying' + token + '" class="' + playingClass + '">Playing Sample</span>';
	// Song title
	if (linkToDetail) {
		s += '<a href="' + gHomePage + 'music/song/' + token + '" class="' + textClass + '" title="Song details" id="titleStopped' + token + '">' + title + '</a>';
	} else {
		s += '<span id="titleStopped' + token + '" class="' + textClass + '">' + title + '</span>';
	}

	return s;
}

// Used by #sampleLink macro to render 30-second sample UI using javascript to reduce size of HTML
function insertSample() {
	var jqThis = $(this);

	var token = jqThis.attr('token');
	var uniqueName = jqThis.attr('uniqueName');
	var allowExplicit = jqThis.attr('allowExplicit');

	var s = sampleHTML(token, uniqueName, allowExplicit);

	$(this).html(s);
}


// Used by #trackTitle macro to render 30-second sample track title using javascript to reduce size of HTML
// NOTE: title must be HTML-escaped
function insertTrackTitle() {
	var jqThis = $(this);

	var token = jqThis.attr('token');
	var title = jqThis.attr('trackTitle');
	var linkToDetail = jqThis.attr('linkToDetail');
	var textClass = jqThis.attr('textClass');
	var playingClass = jqThis.attr('playingClass');

	var s = trackHTML(token, title, linkToDetail, textClass, playingClass);

	$(this).html(s);
}

function drawLandingPageSample(token, uniqueName, hasCleanAudio) {
    if (hasCleanAudio){
        document.write(landingPageHTML(token, uniqueName));
    } else {
        document.write('<img src="/images/clearspacer.gif" width="14" height="15">');
    }
}

// Used by #sampleLink macro to render 30-second sample UI using javascript to reduce size of HTML
// NOTE: podcasts call this method with two args (second one usually empty) so take care in
// modifying the number, order, or meaning of the arguments
function drawSample(token, uniqueName, hasCleanAudio, allowExplicit) {
	if (arguments.length <= 2) {
		// 2-arg support for backwards compatibility with old podcast entries (RADIO-6783)
		document.write(sampleHTML(token, uniqueName, true));
	} else if (hasCleanAudio || allowExplicit) {
		document.write(sampleHTML(token, uniqueName, allowExplicit));
	} else {
		document.write('<img src="/images/clearspacer.gif" width="14" height="15">');
	}
}


// Used by #trackTitle macro to render 30-second sample track title using javascript to reduce size of HTML
// NOTE: title must be HTML-escaped
// NOTE2: podcasts call this method with two args (second one usually empty) so take care in
// modifying the number, order, or meaning of the arguments
function trackTitle(token, title, linkToDetail, textClass, playingClass) {
	var s = trackHTML(token, title, linkToDetail, textClass, playingClass);

	document.write(s);
}
/**
 *  The CustomEvent class provides a simple event framework.  Code can subscribe to
 *  an event instance, and when the event is fired, all subscriber functions are called
 */
var CustomEvent = function() {
	this._arrSubscribers = [];
}
CustomEvent.prototype = {

/**
 * Call all subscribers functions.
 * param obj:  an option argument to be passed to the subscriber functions
 */
	fire: function(obj) {
		for (var i = 0; i < this._arrSubscribers.length; i++) {
			var function_subscriber = this._arrSubscribers[i];

			function_subscriber(obj);
		}
	},

/**
 * Subscribe to this event.  eventHandler function will be called when the event is fired
 */
	subscribe: function(function_eventHandler) {
		this._arrSubscribers.push(function_eventHandler);
	}
}


/**
 * State is change when the user clicks the BACK or FORWARD browser button
 * subscribers to STATE_CHANGED_EVENT are responsible to inspecting the new state
 * and updating the UI, if necessary
 */
var STATE_CHANGED_EVENT = new CustomEvent("state changed event");
EXANIMO.managers.StateManager.stateChangeHandler = function(e)
{
	STATE_CHANGED_EVENT.fire(e);
}

/**
 * Disable the auto initialization of the state manager
 * we want to initialize ourselves after everything else has been initialized
 */
clearInterval(EXANIMO.managers.StateManager._autoInitInterval);

/**
 * The PAGE_INITIALIZED_EVENT is fired after everything on the page has been initialized.
 * subscribe to this event if you to initialize a service that requires other services to be initialized
 */
var PAGE_INITIALIZED_EVENT = new CustomEvent("page initialized event");

/**
 * initialize the state manager after everything else has been initialized.
 * we need to allow the pandora pagers to subscribe to the STATE_CHANGED_EVENT
 * prior to initializing the state manager
 */
PAGE_INITIALIZED_EVENT.subscribe(function(e)
{
	EXANIMO.managers.StateManager.initialize();
});


/**
 * The USER_INTERACTION_EVENT is fired whenever the user interacts with a backstage page:
 *	- when the page through a table
 *  - when they add a bookmark
 */
var USER_INTERACTION_EVENT = new CustomEvent("USER_INTERACTION_EVENT");

USER_INTERACTION_EVENT.subscribe(function(e)
{
	Patron.refreshAds();
});


function expandBio(expand) {
	if (expand) {
		$("#bio_end").show();
		$("#see_all_link").hide();
		$("#shorten_link").show();
	} else {
		$("#bio_end").hide();
		$("#see_all_link").show();
		$("#shorten_link").hide();
	}
}

function expandAlbumGrid(expand) {
	if (expand) {
		$("#album_grid_end").show();
		$("#album_grid_see_all_link").hide();
		$("#album_grid_shorten_link").show();
	} else {
		$("#album_grid_end").hide();
		$("#album_grid_see_all_link").show();
		$("#album_grid_shorten_link").hide();
	}
}

function showStationDescriptionForm(expand) {
	if (expand) {
		$("#station_description_form").show();
		$("#show_description_form").hide();
		$("#community_section_title_description").hide();
		$("#station_details").hide();
		$("#hide_description_form").show();
		$("#hide_station_name_form").show();
		$("#station_description_text").hide();
	} else {
		$("#station_description_form").hide();
		$("#show_description_form").show();
		$("#community_section_title_description").show();
		$("#station_details").show();
		$("#hide_description_form").hide();
		$("#hide_station_name_form").hide();
		$("#station_description_text").show();
	}
}

function showCheckboxesFriend() {
	$('.profile_friend_checkbox').show();
	$('#remove_checked_friends').show();
	return false;
}

function hideCheckboxesFriend() {
	$('.profile_friend_checkbox').hide();
	$('#remove_checked_friends').hide();
	return false;
}

function showCheckboxesTrack() {
	$('.profile_track_buy').hide();
	$('.profile_track_checkbox').show();
	$('#remove_checked_tracks').show();
	return false;
}

function hideCheckboxesTrack() {
	$('.profile_track_buy').show();
	$('.profile_track_checkbox').hide();
	$('#remove_checked_tracks').hide();
	return false;
}

function showCheckboxesArtist() {
	$('.profile_artist_checkbox').show();
	$('#remove_checked_artists').show();
	return false;
}

function hideCheckboxesArtist() {
	$('.profile_artist_checkbox').hide();
	$('#remove_checked_artists').hide();
	return false;
}

function showQuickAddForm(showing, hiding) {
	$(showing).show();
	$(hiding).hide();
}

function quickAdd_onclick(htmlAnchor, function_callback)
{
	var imgQuickAdd_array = $("img", htmlAnchor);
	if (imgQuickAdd_array && imgQuickAdd_array.length == 1)
	{
		imgQuickAdd_array[0].src = "/images/searching_anim_70.gif";
		imgQuickAdd_array[0].onmouseout = null;
		imgQuickAdd_array[0].onmouseover = null;


		if (window.setTimeout)
		{
			window.setTimeout(function() {
				function_callback();
			}, 100);
		}
		else
		{
			function_callback();
		}
	}
	else
	{
		function_callback();
	}

}

function expandHiddenRows(expand, visibleRows, tableSelector, seeAllSelector, shortenSelector) {
	var rows = $(tableSelector + " tr");
	for (var i = visibleRows + 1; i < rows.size(); i++) {
		if (expand) {
			$(rows.get(i)).show();
		} else {
			$(rows.get(i)).hide();
		}
	}
	if (expand) {
		$(seeAllSelector).hide();
		$(shortenSelector).show();
	} else {
		$(seeAllSelector).show();
		$(shortenSelector).hide();
	}
	return false;
}

function prefGlobalEmailChange(checkbox) {
	if (checkbox.checked) {
		getDiv("emailCheckBoxes").className = "";
		getDiv("emailOptIn").checked = true;
		getDiv("notifyOnNote").checked = true;
		getDiv("emailOptIn").disabled = false;
		getDiv("notifyOnNote").disabled = false;
	} else {
		getDiv("emailCheckBoxes").className = "emailDisabled";
		getDiv("emailOptIn").checked = false;
		getDiv("notifyOnNote").checked = false;
		getDiv("emailOptIn").disabled = true;
		getDiv("notifyOnNote").disabled = true;
	}
}

function noteTextChanged(textarea, max, noteCharCount, noteCharCountDiv, classTooLong) {
	var count = textarea.value.length;
	getDiv(noteCharCount).value = count;
	if (count > max) {
		getDiv(noteCharCountDiv).className = classTooLong;
	} else {
		getDiv(noteCharCountDiv).className = "";
	}
}


// code to watch notes for character count changes
var listOfNoteChangeCheckers = {}

var NoteTextChangeChecker = function(objToWatch, maxNoteSize) {
    this.objToWatch = objToWatch;
	this.maxNoteSize = maxNoteSize;
	listOfNoteChangeCheckers[objToWatch.id] = this;
}

NoteTextChangeChecker.prototype = {
	isReady : false,
	check : function() {
		if (this.isReady) {
		   noteTextChanged(this.objToWatch, this.maxNoteSize, 'noteCharCount', 'noteCharCountDiv', 'noteTooLong');
		   var thisObj = this;
		   setTimeout(function() {thisObj.check()},300);
		}
	}
}

function startCheckNoteTextChanged(obj){
	var thisChecker = listOfNoteChangeCheckers[obj.id];
    if (typeof (thisChecker) == "undefined") {
		thisChecker = new NoteTextChangeChecker(obj);
    }
	thisChecker.isReady = true;
	thisChecker.check();
}

function stopCheckNoteTextChanged(obj){
	var thisChecker = listOfNoteChangeCheckers[obj.id];
	thisChecker.isReady = false;
}
// -- END / code to watch notes for character count changes


// Render some HTML rows in a "fans of" table, taking data from a JS object.
// The object is created by the #fansOf macro and has the format:
// ( (isOnline, "webname", "fullname", ("artist1", "artist2", ...)), ... )
function renderFansOf(tableSelector, fans, numRows) {
	var count = 0;
	var picked = [];
	while (count < Math.min(fans.length, numRows)) {
		var r = Math.floor(Math.random() * fans.length);
		if (picked[r]) continue;
		count++;
		picked[r] = true;
		var fan = fans[r];
		var row = '<tr>';
		row += '<td align="left" valign="top">';
		if (fan[0]) {
			row += '<img src="/images/online_status_on.gif" width="24" height="18" alt="currently online" title="currently online">';
		} else {
			row += '<img src="/images/online_status_off.gif" width="24" height="18" alt="currently offline" title="currently offline">';
		}
		row += '</td>'
		row += '<td align="left" valign="top">';
		row += '<div style="width:144px; word-wrap:break-word">';
		// Without this DIV, word-wrapping does not work in Safari
		row += '<b><a href="/people/' + fan[1] + '">' + fan[2] + '</a></b><br />';
		if (fan[3].length > 0) {
			row += '(also listening to:	';
			for (var i = 0; i < fan[3].length; i++) {
				row += fan[3][i];
				if (i < fan[3].length - 1) {
					row += ", ";
				}
			}
			row += ')';
		}
		row += '</div>';
		row += '</td>';
		$(tableSelector).append(row);
	}
}

// Create an artist link using special Pandora tag syntax, using the currently
// selected text as an artist name (if any) and prompting for any other information.
function createArtistLink(textAreaSelector) {
	var element = $(textAreaSelector).get(0);
	var artist = getSelectionValue(element);
	var context = createModalContext(element);
	if (artist == "") {
		showModalDialog("Please enter the name of the artist:", createArtistLinkFromText, restoreSelection, context);
	} else {
		createArtistLinkFromText(artist, context);
	}
}

// Create an artist link in the text selection for the specified artist
function createArtistLinkFromText(artist, context) {
	if (artist && artist.length > 0) {
		replaceSelectionWith(context.element, "[artist]" + artist + "[/artist]", context.range);
	} else {
		restoreSelection();
	}
}

// Create a profile link using special Pandora tag syntax, using the currently
// selected text as a profile name (if any) and prompting for any other information.
function createProfileLink(textAreaSelector) {
	var element = $(textAreaSelector).get(0);
	var user = getSelectionValue(element);
	var context = createModalContext(element);
	if (user == "") {
		showModalDialog("Please enter the email address or profile URL of the Pandora user:", createProfileLinkFromText, restoreSelection, context);
		//showModalDialog("Please enter the email address or profile URL of the Pandora user.", createProfileLinkFromText, restoreSelection, context);
	} else {
		createProfileLinkFromText(user, context);
	}
}

// Create a profile link in the text selection for the specified user
function createProfileLinkFromText(user, context) {
	if (user && user.length > 0) {
		var re = new RegExp("/people/(.*)");
		var match = re.exec(user);
		if (match && match.length > 1) {
			user = match[1];
		}
		replaceSelectionWith(context.element, "[profile]" + user + "[/profile]", context.range);
	} else {
		restoreSelection();
	}
}

// Create an album link using special Pandora tag syntax, using the currently
// selected text as an album name (if any) and prompting for any other information.
function createAlbumLink(textAreaSelector) {
	var element = $(textAreaSelector).get(0);
	var album = getSelectionValue(element);
	var context = createModalContext(element);
	if (album == "") {
		showModalDialog("Please enter the name of the album:", createAlbumLinkFromText, restoreSelection, context);
	} else {
		context.album = album;
		showModalDialog("To make sure we have the correct '" + album + "', please tell us the artist:", createAlbumLinkFromText, restoreSelection, context);
	}
}

// Create an album link in the text selection with the specified text
function createAlbumLinkFromText(text, context) {
	if (text && text.length > 0) {
		if (context.album == null) {
			// We only have album, get artist with another dialog
			context.album = text;
			showModalDialog("To make sure we have the correct '" + text + "', please tell us the artist:", createAlbumLinkFromText, restoreSelection, context);
		} else {
			// We have album and artist
			replaceSelectionWith(context.element, '[album artist="' + text + '"]' + context.album + "[/album]", context.range);
		}

	} else {
		restoreSelection();
	}
}

// Create a song link using special Pandora tag syntax, using the currently
// selected text as a song name (if any) and prompting for any other information.
function createSongLink(textAreaSelector) {
	var element = $(textAreaSelector).get(0);
	var song = getSelectionValue(element);
	var context = createModalContext(element);
	if (song == "") {
		showModalDialog("Please enter the name of the song:", createSongLinkFromText, restoreSelection, context);
	} else {
		context.song = song;
		showModalDialog("To make sure we have the correct '" + song + "', please tell us the artist:", createSongLinkFromText, restoreSelection, context);
	}
}

// Create a song link in the text selection with the specified text
function createSongLinkFromText(text, context) {
	if (text && text.length > 0) {
		if (context.song == null) {
			// We only have song, get artist with another dialog
			context.song = text;
			showModalDialog("To make sure we have the correct '" + text + "', please tell us the artist:", createSongLinkFromText, restoreSelection, context);
		} else {
			// We have song and artist
			replaceSelectionWith(context.element, '[song artist="' + text + '"]' + context.song + "[/song]", context.range);
		}
	} else {
		restoreSelection();
	}
}

// Create a context object to use with the modal dialog callbacks
function createModalContext(element) {
	var context = { element : element };
	if (document.selection) {
		// Save range in context because modal dialog wipes it out somehow (IE only)
		element.focus();
		range = document.selection.createRange();
		context.range = range;
	}
	return context;
}

// Restore the text selection's cursor position for the given modal context
function restoreSelection(context) {
	if (context.range != null) {	// IE only
		var dummyRange = context.range.duplicate();
		dummyRange.moveToElementText(context.element);
		dummyRange.setEndPoint("EndToEnd", context.range);
		var start = dummyRange.text.length - context.range.text.length;
		if (context.range.parentElement() == context.element) {
			var finalRange = context.element.createTextRange();
			finalRange.move("character", start);
			finalRange.select();
		}
	}
}

// Get the value of the currently-selected text for the element (i.e. textarea)
// Returns the text as a string, or empty string if nothing is selected.
function getSelectionValue(element) {
	element.focus();
	if (element.selectionStart >= 0) {
		var start = element.selectionStart;
		var end = element.selectionEnd;
		return element.value.substring(start, end);
	} else if (document.selection) {
		element.focus();
		var range = document.selection.createRange();
		if (range.parentElement() == element) {
			return range.text;
		}
	}
	return "";
}

// Replace the current text selection with the supplied text.  If nothing is selected,
// the new text will be inserted at the current cursor position.
function replaceSelectionWith(element, text, range) {
	element.focus();
	if (element.selectionStart >= 0) {			// Mozilla, Firefox, Safari and Opera
		var start = element.selectionStart;
		var end = element.selectionEnd;
		var value = element.value;
		element.value = value.substr(0, start) + text + value.substr(end, value.length);
		element.focus();
		element.setSelectionRange(start + text.length, start + text.length);

	} else if (document.selection) {			// Internet Explorer
		if (range == null) {
			range = document.selection.createRange();
		}
		var dummyRange = range.duplicate();
		dummyRange.moveToElementText(element);
		dummyRange.setEndPoint("EndToEnd", range);
		var start = dummyRange.text.length - range.text.length;

		// If selection range is not in the expected element, abort
		if (range.parentElement() != element) {
			return false;
		}

		// Set the range with the new text
		range.text = text;

		// Set carat to end of selection
		var finalRange = element.createTextRange();
		finalRange.move("character", start + text.length);
		finalRange.select();

	} else {
		// Fallback - just append it to the end
		element.value += text;
	}

	// Restore focus to element (only works reliably using setTimeout)
	setTimeout(function() {
		element.focus();
	}, 1);

}

// Escape HTML entities in a string
function escapeHtml(html) {
	html = html.replace(/&/g, '&amp;');
	html = html.replace(/</g, '&lt;');
	html = html.replace(/>/g, '&gt;');
	html = html.replace(/"/g, '&quot;');
	return html;
}

// Decode a URL-encoded string
function urlDecode(str) {
	return unescape(str.replace(/\+/g, " "));
}

// Prevent dragging of links into textarea (see RADIO-4066)
function preventDragDrop(selector) {
	$(selector).each(function() {
		if (this.addEventListener) {
			this.addEventListener("dragdrop", function(e) {
				e.stopPropagation();
				e.preventDefault();
			}, true);
		}
	});
}

// Display an error message in a floating div, with a dismiss button
function showErrorMessage(msg) {
	if (document.createElement) {
		msg = msg.replace(/\n/g, "<br>");
		var div = document.createElement("div");
		$(div).addClass("error").html(msg + "<br><br>");
		var img = document.createElement("img");
		img.src = "/images/continue_button.gif";
		img.width = 100;
		img.height = 22;
		img.style.cursor = "pointer";
		$(div).append(img);
		$(img).mouseover(function() {
			this.src = "/images/continue_button_hover.gif";
		});
		$(img).mouseout(function() {
			this.src = "/images/continue_button.gif";
		});
		$(img).click(function() {
			$(div).hide();
		})
		$(document.body).append(div);
	} else {
		alert(msg);
	}
}

var gModalDialogSubmitCallback = null;
var gModalDialogCancelCallback = null;
var gModalDialogContext = null;

// Show a modal dialog that simulates a javascript prompt() call.  If the user hits
// ENTER or submits the dialog, the submitCallback is called with the user's text and
// context as arguments.  Otherwise the cancelCallback (if any) is called with the context.
function showModalDialog(text, submitCallback, cancelCallback, context) {
	gModalDialogSubmitCallback = submitCallback;
	gModalDialogCancelCallback = cancelCallback;
	gModalDialogContext = context;
	$('#modalDialog').modalContent({opacity: '.40', background: '#fff'}, "fadeIn", "slow", function() {
		$('#modalContent input').get(0).focus();
	});
	$('#modalContent span').html(text);
}

// Internal function used by the modal dialog box
function modalDialogSubmit() {
	var text = $('#modalContent input').val();
	$('#modalDialog').unmodalContent();
	if (gModalDialogSubmitCallback) {
		var tmpCallback = gModalDialogSubmitCallback;
		var tmpContext = gModalDialogContext;
		setTimeout(function() {
			tmpCallback(text, tmpContext);
		}, 1);
	}
	gModalDialogSubmitCallback = null;
	gModalDialogCancelCallback = null;
	gModalDialogContext = null;
}

// Internal function used by the modal dialog box
function modalDialogCancel() {
	$('#modalDialog').unmodalContent();
	if (gModalDialogCancelCallback) {
		var tmpCallback = gModalDialogCancelCallback;
		var tmpContext = gModalDialogContext;
		setTimeout(function() {
			tmpCallback(tmpContext);
		}, 1);
	}
	gModalDialogSubmitCallback = null;
	gModalDialogCancelCallback = null;
	gModalDialogContext = null;
}

function reloadWindow() {
	if ($.browser.safari
			&& window.location.hash != null
			&& window.location.hash.length > 0) {
		// Safari will not reload the window if there is a hash value in the url
		// if the url has a hash value and we are in safari, reconstruct the url
		// without the hash.  Also, safari requires that we tack a unique query string onto the url
		var url = window.location.protocol + "//" + window.location.host + window.location.pathname + "?deleteHash=" + new Date().getTime();
		window.location.href = url;
	} else {
		window.location.reload();
	}
}

function getKeyCode(event) {
	if (event.which) {
		return event.which;
	} else {
		return event.keyCode;
	}
}

function executeClickEvent(jqueryAnchor) {
	var onClick = jqueryAnchor.attr("onclick");
	var strHref = jqueryAnchor.attr("href");

	if ($.browser.msie) {
		//TODO:  get rid of these IE hacks when we upgrade jquery (RADIO-4172)
		if (typeof(onClick) == "function") {
			//the anchor click event fails on ie6, so call the function a bit more manually
			onClick.call();
		} else if (strHref != null && strHref.length > 0) {
			//Marc is really going to like this one...
			if (strHref.indexOf("javascript:") == 0) {
				eval(strHref.substring("javascript:".length));
			} else {
				window.location.href = strHref;
			}
		}
	} else {
		if (onClick != null && onClick.length > 0) {
			jqueryAnchor.click();
		} else if (strHref != null && strHref.length > 0) {
			window.location.href = strHref;
		}
	}
}


/**
 * PandoraPagerManager implements the table paging functionality.  This class is married to jquery.pandoraPager.js
 *
 */
(function() {
PandoraPagerManager = {
/**
 * NOTE:  very important.  PandoraPagerManager should never carry state.  A page can have multiple tables
 * and there is only the one instance of PandoraPagerManager
 */
	init: function(domProxy, settings, optionalSettings, function_onStateChange) {
		//normalize the settings
		if (!optionalSettings) {
			optionalSettings = {};
		}

		if (!isNaN(parseInt(optionalSettings.totalItemCount))) {
			domProxy.setTotalItemCountAttr(optionalSettings.totalItemCount);
		}

		if (!isNaN(parseInt(optionalSettings.currentPage))) {
			domProxy.setCurrentPageNumberAttr(optionalSettings.currentPage);
		}

		if (optionalSettings.showall) {
			domProxy.setShowAllAttr(true);
		}

		if (settings.navContainers == null || settings.navContainers.length == 0) {
			settings.navContainers = domProxy.createNavContainers(settings.table_id + 'nav_container');
		}

		if (settings.url_rowprovider != null
				&& settings.url_rowprovider.length > 0
				&& settings.url_rowprovider.indexOf("?") < 0) {
			settings.url_rowprovider += "?";
		}

		if (isNaN(settings.batchSize)) {
			settings.batchSize = 5;
		}

		if (isNaN(settings.pageSize)) {
			settings.pageSize = 10;
		}

		// if show_index is supplied, set currentPage so that show_index will be displayed
		if (!isNaN(settings.show_index)) {
			var p = Math.floor(settings.show_index / settings.pageSize) + 1;
			domProxy.setCurrentPageNumberAttr(p);
		}

		PPM.executeFunction(settings.onDataDownload);

		if (domProxy.getPageCount() > 1) {
			if (!domProxy.isPagingControlBuilt()) {
				var blnAllowSkipToEnd = settings.allowSkipToEnd
						&& (settings.maxItemsToAllowSkipToEnd == null || isNaN(settings.maxItemsToAllowSkipToEnd) || domProxy.getTotalItemCount() < settings.maxItemsToAllowSkipToEnd);
				
				domProxy.makeNav(blnAllowSkipToEnd );
			}

			if (!domProxy.isBoundToStateChangedEvent()) {
				//if we haven't already bound to the STATE_CHANGED_EVENT, bind to it
				STATE_CHANGED_EVENT.subscribe(function_onStateChange);
				domProxy.setIsBoundToStateChangedEvent();
			}

			domProxy.setPage(domProxy.getShowAll() ? PandoraPagerManager.PAGE_NUMBER_SHOW_ALL : domProxy.getCurrentPageNumber());
		} else {
			if (domProxy.hasPagingControl()) {
				//if we have a paging control, but now we don't have any pages, that means that this table used to page
				//but now it doesn't, so we have to hide the paging control
				domProxy.setNavItemVisibility([]);
			}
		}

		domProxy.applyToggleAllVisiblity();

		// Show the table body, in case it was hidden
		domProxy.showTableBody();
	},

/**
 * show and hide the table rows according to our current page.  If the table doesn't contain the requested page
 * this will attempt to download the required table rows
 */
	applyTableRowVisibility: function (domProxy, settings, blnReturnFromFetch) {
		var countRowsOnBrowser = domProxy.getRowCount();
		var totalItemCount = domProxy.getTotalItemCount();
		var currentPage = domProxy.getCurrentPageNumber();
		var blnShowAll = domProxy.getShowAll();

		if (countRowsOnBrowser > totalItemCount) {
			//if we have more rows on the browser than the total item count,
			//we must have added some rows.  update the total item count accordingly
			totalItemCount = countRowsOnBrowser;
		}

		var startIndex_currentPage;
		var lastIndex_currentPage;

		if (blnShowAll) {
			startIndex_currentPage = 0;
			lastIndex_currentPage = totalItemCount;
		} else {
			startIndex_currentPage = (currentPage - 1) * settings.pageSize;
			lastIndex_currentPage = startIndex_currentPage + settings.pageSize;
		}

		if (startIndex_currentPage >= totalItemCount) {
			//if we're trying to jump to a page that doesn't exist, go back to page 1
			domProxy.setPage(1);

			return;
		}

		var blnNeedMoreRecords = lastIndex_currentPage > countRowsOnBrowser;
		var blnAllRecordsFetched = totalItemCount == countRowsOnBrowser;
		var blnHasDataProvider = settings.url_rowprovider != null && settings.url_rowprovider.length > 0;


		if (!blnReturnFromFetch && blnHasDataProvider && blnNeedMoreRecords && !blnAllRecordsFetched) {
			//if we don't have the necessary rows, we have to download them
			var countRowsNeeded = Math.min(countRowsOnBrowser + (settings.batchSize * settings.pageSize), totalItemCount);
			countRowsNeeded = Math.max(lastIndex_currentPage, countRowsNeeded);

			domProxy.setNavItemVisibility([PandoraPagerManager.NAV_CONTAINER_FETCHING_DATA]);

			domProxy.fetchRows(settings.url_rowprovider, countRowsOnBrowser, countRowsNeeded, function(html) {
				domProxy.appendTableRowHTML(html);

				PPM.executeFunction(settings.onDataDownload);

				PPM.applyTableRowVisibility(domProxy, settings, true);
			});
		} else {
			var strWindowTitle = PPM.buildWindowTitle(domProxy.getWindowTitle(), settings.friendlyName, blnShowAll ? PandoraPagerManager.PAGE_NUMBER_SHOW_ALL : currentPage);

			domProxy.setWindowTitle(strWindowTitle);

			var pageCount = domProxy.getPageCount();

			domProxy.enableNavPrev(currentPage > 1);
			domProxy.enableNavNext(currentPage < pageCount);
			domProxy.setNavPageDesc(currentPage + " of " + pageCount);

			// Hide all rows
			domProxy.showTableRows(false);

			// Show rows for the current page
			if (blnShowAll) {
				domProxy.setNavItemVisibility([PandoraPagerManager.NAV_CONTAINER_DONOT_SHOWALL]);

				domProxy.showTableRows(true);
			} else {
				domProxy.setNavItemVisibility([PandoraPagerManager.NAV_CONTAINER_PAGING, PandoraPagerManager.NAV_CONTAINER_SHOWALL]);

				domProxy.showTableRowsByIndex(startIndex_currentPage, settings.pageSize);
			}

			domProxy.showTableBody();
		}
	},

	buildWindowTitle: function (currentWindowTitle, tableFriendlyName, page) {
		var pageTitlePrefix = " || ";

		//if we previously added a "<table> <page>" suffix to the title, remove it
		var indexOfPageTitle = currentWindowTitle.indexOf(pageTitlePrefix);
		if (indexOfPageTitle >= 0) {
			currentWindowTitle = currentWindowTitle.substr(0, indexOfPageTitle);
		}

		//if our page number is greater than 1, add a page number suffix
		if (page == PandoraPagerManager.PAGE_NUMBER_SHOW_ALL) {
			currentWindowTitle += pageTitlePrefix + "All " + tableFriendlyName;
		} else if (page > 1) {
			currentWindowTitle += pageTitlePrefix + tableFriendlyName + " page " + page;
		}

		return currentWindowTitle;
	},

	eventHandler_onPage: function(domProxy, navButton) {
		USER_INTERACTION_EVENT.fire();

		var pageCount = domProxy.getPageCount();
		var currentPage = domProxy.getCurrentPageNumber();

		if (navButton == PandoraPagerManager.NAV_BUTTON_NEXT) {
			if (currentPage < pageCount) {
				currentPage++;
			}
		} else if (navButton == PandoraPagerManager.NAV_BUTTON_PREVIOUS) {
			if (currentPage > 1) {
				currentPage--;
			}
		} else if (navButton == PandoraPagerManager.NAV_BUTTON_FIRST) {
			currentPage = 1;
		} else if (navButton == PandoraPagerManager.NAV_BUTTON_LAST) {
			currentPage = pageCount;
		}

		domProxy.setPage(currentPage);

		return false;
	},

	eventHandler_onShowAll: function(domProxy) {
		USER_INTERACTION_EVENT.fire();

		domProxy.setPage(PandoraPagerManager.PAGE_NUMBER_SHOW_ALL);

		return false;
	},

	eventHandler_onDoNotShowAll: function (domProxy) {
		USER_INTERACTION_EVENT.fire();

		domProxy.scrollToWindowTop();

		domProxy.setPage(domProxy.getCurrentPageNumber());

		return false;
	},

/**
 * executed when the user clicks the browser's back or forward button, or when they reload the page
 * The stateEvent parameter describes the desired current state.  This method inspects this value
 * and updates its table appropriately
 */
	eventHandler_onStateChange: function(proxy, settings, stateEvent) {
		var tableID_PageNum = stateEvent.id.split(",");

		if (settings.table_id == tableID_PageNum[0]) {
			var showAll_fromEvent = tableID_PageNum[1] == PandoraPagerManager.PAGE_NUMBER_SHOW_ALL;
			var currentPage_fromEvent = parseInt(tableID_PageNum[1]);


			proxy.setShowAllAttr(showAll_fromEvent);

			if (!isNaN(currentPage_fromEvent)) {
				proxy.setCurrentPageNumberAttr(currentPage_fromEvent);
			}

			PPM.applyTableRowVisibility(proxy, settings);
			proxy.applyToggleAllVisiblity();
		}
	},

/**
 * Excecutes a javascript function.  The param function_unknownType can be a
 * function pointer or a String.  If it is a function point, the function will
 * be called directly.  If it is a string, the string is eval'd
 */
	executeFunction: function (function_unknownType) {
		if (function_unknownType != null) {

			if (typeof function_unknownType == "function") {

				function_unknownType();

			} else if (typeof function_unknownType == "string") {

				if (function_unknownType.length > 0) {
					eval(function_unknownType);
				}

			}
		}
	}
}
var PPM = PandoraPagerManager;

/**
 * PandoraPagerDomProxy is responsible for marshalling calls to the ui compents.
 * The unit tests will supply a fake version of this class to intercept these calls
 * and assert conditions
 *
 * This is the bit of the pager that is not tested
 */
PandoraPagerDomProxy = function(jqTable, settings) {
	this._jqTable = jqTable;
	this._settings = settings;
}

PandoraPagerDomProxy.prototype = {
	private_getTBody: function () {
		return $(this._jqTable).children("tbody");
	},

	private_getTHead: function () {
		return $(this._jqTable).children("thead");
	},

	private_getRows: function () {
		return this.private_getTBody().children("tr");
	},

	private_getPagingControl: function () {
		return $("." + this._settings.table_id + PandoraPagerManager.NAV_CONTAINER_PAGING, this._settings.navContainers);
	},

	hasPagingControl: function () {
		return this.private_getPagingControl().length > 0;
	},

	getRowCount: function() {
		return  this.private_getRows().size();
	},

	isPagingControlBuilt: function() {
		return this.private_getPagingControl().length > 0
	},


	isBoundToStateChangedEvent: function() {
		return $(this._jqTable).attr("boundToEvent") == "true";
	},
	setIsBoundToStateChangedEvent: function() {
		$(this._jqTable).attr("boundToEvent", "true");
	},


	showTableBody: function() {
		this.private_getTBody().show();
	},

	showTableRows: function(blnShow) {
		if (blnShow) {
			this.private_getRows().show();
		} else {
			this.private_getRows().hide();
		}
	},

	showTableRowsByIndex: function(startIndex_currentPage, pageSize) {
		this.private_getRows().gt(startIndex_currentPage - 1).lt(pageSize).show();
	},

	setNavItemVisibility: function(arrVisibleButtons) {
		for (var i = 0; i < PandoraPagerManager.ALL_NAV_CONTAINERS.length; i++) {
			$("." + this._settings.table_id + PandoraPagerManager.ALL_NAV_CONTAINERS[i], this._settings.navContainers).hide();
		}

		for (var i = 0; i < arrVisibleButtons.length; i++) {
			$("." + this._settings.table_id + arrVisibleButtons[i], this._settings.navContainers).show();
		}
	},

	enableNavPrev: function(enabled) {
		var pagingControl = this.private_getPagingControl();

		if (enabled) {
			$(".navPrevDimmed", pagingControl).hide();
			$(".navPrev", pagingControl).css("display", "");
		} else {
			$(".navPrevDimmed", pagingControl).css("display", "");
			$(".navPrev", pagingControl).hide();
		}
	},

	enableNavNext: function(enabled) {
		var pagingControl = this.private_getPagingControl();

		if (enabled) {
			$(".navNextDimmed", pagingControl).hide();
			$(".navNext", pagingControl).css("display", "");
		} else {
			$(".navNextDimmed", pagingControl).css("display", "");
			$(".navNext", pagingControl).hide();
		}
	},

	setNavPageDesc: function(desc) {
		var pagingControl = this.private_getPagingControl();

		$(".navPageNum", pagingControl).html(desc);
	},

/**
 * returns the total number of items on the server
 */
	getTotalItemCount: function () {
		return parseInt($(this._jqTable).attr("totalitemcount"));
	},
	setTotalItemCountAttr: function (itemCount) {
		$(this._jqTable).attr("totalitemcount", itemCount);
	},

/**
 * return the current page index.  1 based
 */
	getCurrentPageNumber: function () {
		var p = parseInt($(this._jqTable).attr("currentpage"));
		return isNaN(p) ? 1 : p;
	},
	setCurrentPageNumberAttr: function (pageNum) {
		$(this._jqTable).attr("currentpage", pageNum);
	},

/**
 * if showAll == true, the paging controls will be hidden and all items will be visible
 */
	getShowAll: function () {
		return $(this._jqTable).attr("showall") == "true";
	},
	setShowAllAttr: function (blnShowAll) {
		$(this._jqTable).attr("showall", blnShowAll ? "true" : false);
	},


	createNavContainers: function(navContainersClassName) {
		var nc = $("." + navContainersClassName, $(this._jqTable).parent());

		if (nc == null || nc.length == 0) {
			//we haven't already created it.  create it now
			$(this._jqTable).parent().append('<span class="left ' + navContainersClassName + '" nowrap="true" />');
			nc = $("." + navContainersClassName, $(this._jqTable).parent());
		}

		return nc;
	},

/**
 * fetch the number of total number pages in the table
 */
	getPageCount: function () {
		return Math.max(Math.ceil(this.getRowCount() / this._settings.pageSize), Math.ceil(this.getTotalItemCount() / this._settings.pageSize));
	},


/**
 * if the current page has less than two visible rows, hide
 * the toggle all checkbox, if there is one.
 *
 * This method assumes that the toggle all checkbox lives
 * in the thead, and it is the only checkbox in the thead
 *
 * todo:  this feels like it breaks encapsulation a little bit.  consider
 *        moving this bit of functionality out of the pager.
 */
	applyToggleAllVisiblity: function () {
		if (this.private_getRows().length > 1) {
			this.private_getTHead().find("input[@type=checkbox]").show();
		} else {
			this.private_getTHead().find("input[@type=checkbox]").hide();
		}
	},

	appendTableRowHTML: function(html) {
		this.private_getTBody().append(html);
	},

	getWindowTitle: function() {
		return window.document.title;
	},

	setWindowTitle: function(strTitle) {
		window.document.title = strTitle;
	},

	scrollToWindowTop: function() {
		if (window.scrollTo) {
			var tableTop = $(this._jqTable).offset().top;

			if (tableTop < document.body.scrollTop) {
				//only scroll if we need to - if we already see the table top, no need to scroll to it
				window.scrollTo(0, tableTop);
			}
		}
	},

	setPage: function(page) {
		var windowTitle = PandoraPagerManager.buildWindowTitle(this.getWindowTitle(), this._settings.friendlyName, page);

		EXANIMO.managers.StateManager.setState(this._settings.table_id + "," + page, windowTitle);
	},

	fetchRows: function(url_rowprovider, countRowsOnBrowser, countRowsNeeded, function_callBack) {
		var url = url_rowprovider + "&countRowsOnBrowser=" + countRowsOnBrowser + "&countRowsNeeded=" + countRowsNeeded;

		$.ajax({
			type: "get",
			url: url,
			dataType: "html",
			processData: false,
			success: function_callBack
		});
	},


/**
 * build the navigation controls
 */
	makeNav: function (blnAllowSkipToEnd ) {
		var me_domProxy = this;
		var str = '';

		str += '<table cellpadding="0" cellspacing="0" border="0" style="width: 168px"><tr>';


		str += '<td class="' + this._settings.table_id + PandoraPagerManager.NAV_CONTAINER_PAGING + '" align="left" nowrap="true" style="border: none;">';
		str += '<span class="navPrevDimmed">';
		str += '<img src="/images/first_dim.gif" height="16" width="16" border="0">';
		str += '<img src="/images/prev_dim.gif" height="16" width="16" border="0">';
		str += '</span>';

		str += '<span class="navPrev" style="display:none">';
		str += '<a href="#"><img src="/images/first.gif" height="16" width="16" border="0"></a>';
		str += '<a href="#"><img src="/images/prev.gif" height="16" width="16" border="0"></a>';
		str += '</span>';

		str += '<span class="navPageNum" style="font-size:10px;">' + this.getCurrentPageNumber() + ' of ' + this.getPageCount() + '</span>';

		str += '<span class="navNextDimmed" style="display:none">';
		str += '<img src="/images/next_dim.gif" height="16" width="16" border="0">';
		if (blnAllowSkipToEnd) {
			str += '<img src="/images/last_dim.gif" height="16" width="16" border="0">';
		}
		str += '</span>';

		str += '<span class="navNext">';
		str += '<a href="#"><img src="/images/next.gif" height="16" width="16" border="0"></a>';
		if (blnAllowSkipToEnd) {
			str += '<a href="#"><img src="/images/last.gif" height="16" width="16" border="0"></a>';
		}
		str += '</span>';

		str += '</td>';

		if (blnAllowSkipToEnd) {
			str += '<td class="' + this._settings.table_id + PandoraPagerManager.NAV_CONTAINER_SHOWALL + '" align="right" style="border: none; padding-top: 4px; font-size:10px; ">';
			str += '<a href="#" class="navShowAll"><img src="/images/show_all_button.gif" width="50" height="12" border="0" alt="show all" onMouseOver="this.src=\'/images/show_all_button_hover.gif\';" onMouseOut="this.src=\'/images/show_all_button.gif\';"></a>';
			str += '</td>';
		}


		str += '<td class="' + this._settings.table_id + PandoraPagerManager.NAV_CONTAINER_DONOT_SHOWALL + '" style="font-size: 10x; padding: 6px; margin-left: 6px; display: none; border: none;">';
		str += '<a href="#" class="navReturnToPagedView"><img src="/images/dont_show_all_button.gif" width="81" height="12" border="0" alt="don\'t show all" onMouseOver="this.src=\'/images/dont_show_all_button_hover.gif\';" onMouseOut="this.src=\'/images/dont_show_all_button.gif\';"></a>';
		str += '</td>';

		str += '<td class="' + this._settings.table_id + PandoraPagerManager.NAV_CONTAINER_FETCHING_DATA + '" style="font-size: 10x; padding: 3px 7px 0px 7px; margin-left: 5px; display: none; border: none;">';
		str += 'fetching data...';
		str += '</td>';

		str += '</tr></table>';

		for (var i = 0; i < this._settings.navContainers.length; i++) {
			var nc = $(this._settings.navContainers[i]);
			nc.children().remove();
		}

		this._settings.navContainers.append(str);
		this._settings.navContainers.after('<div style="clear: both" />');

		$("." + this._settings.table_id + PandoraPagerManager.NAV_CONTAINER_DONOT_SHOWALL, this._settings.navContainers).find("a").click(function() {
			PandoraPagerManager.eventHandler_onDoNotShowAll(me_domProxy);

			return false;
		});

		$("." + this._settings.table_id + PandoraPagerManager.NAV_CONTAINER_SHOWALL, this._settings.navContainers).find("a").click(function() {
			PandoraPagerManager.eventHandler_onShowAll(me_domProxy);

			return false;
		});

		$("." + this._settings.table_id + PandoraPagerManager.NAV_CONTAINER_PAGING, this._settings.navContainers).find("a").click(function() {
			this.blur();

			// get rid of dotted outline around link
			var src = $(this).children().eq(0).attr("src");
			var image = src.substring(src.lastIndexOf("/") + 1);

			var navButton;
			if (image == 'next.gif') {
				navButton = PandoraPagerManager.NAV_BUTTON_NEXT;
			} else if (image == 'prev.gif') {
				navButton = PandoraPagerManager.NAV_BUTTON_PREVIOUS;
			} else if (image == 'first.gif') {
				navButton = PandoraPagerManager.NAV_BUTTON_FIRST;
			} else if (image == 'last.gif') {
				navButton = PandoraPagerManager.NAV_BUTTON_LAST;
			}

			PandoraPagerManager.eventHandler_onPage(me_domProxy, navButton)

			return false;
		});
	}
}

PandoraPagerManager.PAGE_NUMBER_SHOW_ALL = "all";

PandoraPagerManager.NAV_BUTTON_FIRST = "first";
PandoraPagerManager.NAV_BUTTON_PREVIOUS = "previous";
PandoraPagerManager.NAV_BUTTON_NEXT = "next";
PandoraPagerManager.NAV_BUTTON_LAST = "last";

PandoraPagerManager.NAV_CONTAINER_SHOWALL = "nav_container_showall";
PandoraPagerManager.NAV_CONTAINER_DONOT_SHOWALL = "nav_container_donot_showall";
PandoraPagerManager.NAV_CONTAINER_PAGING = "nav_container_paging";
PandoraPagerManager.NAV_CONTAINER_FETCHING_DATA = "nav_container_fetching_data";

PandoraPagerManager.ALL_NAV_CONTAINERS = [
		PandoraPagerManager.NAV_CONTAINER_SHOWALL,
		PandoraPagerManager.NAV_CONTAINER_DONOT_SHOWALL,
		PandoraPagerManager.NAV_CONTAINER_PAGING,
		PandoraPagerManager.NAV_CONTAINER_FETCHING_DATA
		];
})();
/*
 *
 * Pandora Pager - jQuery plugin for displaying paged content on Backstage pages.  It works
 * by selectively hiding and showing rows of a table.
 *
 * The pandora pager is able to incrementally download subsets of the table's contents.
 * If the user requests a page of data that has not yet been download, the pager will attempt
 * to fetch the required data.  To minimize data fetches. the pager will also attempt to fetch
 * a number of pages of data a head of what the user requested.  The number of pages fetched
 * at a time is specified by the batchSize setting.
 *
 * 	To use, simply attach it to a table:   $('#tableId').pandoraPager();
 *
 * Configurable Settings:
 * ---------------------
 * table_id: 		DOM id of the table
 * friendlyName: 	User readable name of the data contents.  Will be appended to the page's title up on page navigation
 * url_rowprovider: The URL of the page that serves up the table's row's HTML.
 *					see /favorites/profile_tablerows_artist.vm for an example
 * allowSkipToEnd: 	if true, a "skip to end" control and a "show all" control will be available in the navigation controls
 * navContainers: 	a jQuery list of elements to use as containers for navigation controls - if omitted
 *                	then navigation controls will be inserted in a DIV immediately following the table
 * showall:     	the initial view of the table is to show all records
 * currentPage: 	the initial page to display, once the control is initialized
 * show_index:      if specified, currentpage is ignored and the pager will make sure that the specified index is displayed
 * pageSize: 		number of rows to display per page
 * maxitemstoallowskiptoend: The max number of items before hiding the 'showall' and 'pagelast' button.
 * batchSize: 		the size (in pages) of each batch of data retrieved from the server
 * totalItemCount: 	Total number of items in the table
 * onDataDownload: 	Javascript method called after a batch of table rows have been downloaded
 * onPage: 			Javascript method called upon page navigation
 *
 *
 * These settings may be specified either when the pager is initialized
 *
 *		jqSearchResultsTable.pandoraPager({
 *			currentPage: 1,
 *			totalItemCount: searchResults.searchResultItems.length
 *		});
 *
 * or in the table's html attributes
 *
 *		<table id="tbl_artists_table" cellpadding="2" cellspacing="0" border="0" width="100%"
 *			   friendlyname="Artists"
 *			   totalitemcount="$all_artist_bookmarks.size()"
 *			   urlrowprovider="/favorites/profile_tablerows_artist.vm?webname=$profile.webName">
 *
 *
 */
(function($) {
$.fn.pandoraPager = function(options) {
	if (this.length == 0) {
		return false;
	}

	var settings = {
		table_id: this.attr("id"),
		friendlyName: this.attr("friendlyname"),

		url_rowprovider: this.attr("urlrowprovider"),

		allowSkipToEnd: this.attr("allowskiptoend") != "false",

		maxItemsToAllowSkipToEnd: parseInt(this.attr("maxitemstoallowskiptoend")),

		navContainers: null,

		show_index: parseInt(this.attr("show_index")),

		pageSize: parseInt(this.attr("pagesize")),

		batchSize: parseInt(this.attr("batchsize")),

		onDataDownload: this.attr("ondatadownload"),

		onPage: this.attr("onpage")
	}

	if (options) $.extend(settings, options);

	return this.each(function () {
		var jqTable = $(this);

		var pandoraPagerDomProxy = new PandoraPagerDomProxy(jqTable, settings);

		PandoraPagerManager.init(pandoraPagerDomProxy, settings, options,
				function (stateEvent) {
					PandoraPagerManager.eventHandler_onStateChange(pandoraPagerDomProxy, settings, stateEvent);
				});
	});
}
})(jQuery);
 /**
 *
 * The CommentManager class implements our Artist, Song, and Listener comment
 * functionality.
 *
 * depends on 3rd-party/jquery/jquery.jsonext.js   
 *
 **/
var CommentManager =
{
	MAX_NOTE_LENGTH: 500,
	
//the values of these constants must correspond to their values in
//com.savagebeast.radio.comment.CommentTypeHelper
	COMMENT_TYPE_PROFILE: "profile",
	COMMENT_TYPE_STATION: "station",
	COMMENT_TYPE_MUSIC:   "music",

	COMMENT_MODE_ADD: "add",

//DOM proxy returned when commentType is COMMENT_TYPE_TEST.
	commentTestDomProxy: null,

	METHOD_NAME_EDIT_COMMENT: "comment.editComment",
	METHOD_NAME_ADD_COMMENT: "comment.addComment",
	METHOD_NAME_DELETE_COMMENT: "comment.deleteComment",
	METHOD_NAME_BAN_COMMENT: "comment.banListenerFromCommenting",

	onPageLoad: function() {
		var actionData = AuthenticationManager.getCurrentActionData();

		//if we have an action token, that means that we are returning to this page after
		//a successfull login, and we want to execute the command that the user was trying to
		//execute prior to logging in
		if (actionData != null) {
			if (actionData.token == CommentManager.METHOD_NAME_ADD_COMMENT) {
				var data = actionData.data;
				AuthenticationManager.clearCurrentActionData();

				//re-attempt the comment add
				var commentDomProxy = CM.privateGetCommentDomProxy();
				$.getJSONExt(CommentManager.METHOD_NAME_ADD_COMMENT,
						data,
						function(json) {
							commentDomProxy.incrementTotalItemCount();
							commentDomProxy.reloadCommentList();
						},

						function(json) {
							CommentManager.private_handleError(commentDomProxy, json);
						});
			}
		}

		$(".sample_link	").each(insertSample);
	},

	init: function(strCommentType, userIsObjectOwner) {
		CM.commentType = strCommentType;
		CM.userIsObjectOwner = userIsObjectOwner;

		this.privateGetCommentDomProxy().initDomProxy();
	},

	onCommentDownload: function(){
		CM.privateGetCommentDomProxy().onCommentDownload();
	},

	privateGetCommentDomProxy: function() {
		if (CM.commentTestDomProxy != null) {
			return CM.commentTestDomProxy;

		} else if (CM.commentType == CM.COMMENT_TYPE_PROFILE) {
			return CommentDomProxyProfile;

		} else if (CM.commentType == CM.COMMENT_TYPE_STATION) {
			return CommentDomProxyProfile;

		} else if (CM.commentType == CM.COMMENT_TYPE_MUSIC) {
			return CommentDomProxyMusic;

		}
	},

	loadComments: function(){
		CM.privateGetCommentDomProxy().reloadCommentList();

	},

	addComment: function(strCommentMode, commentTargetID, buttonElementID)
	{
		var commentDomProxy = CM.privateGetCommentDomProxy();
		commentDomProxy.clearMessages();

		USER_INTERACTION_EVENT.fire();

		var commentText = commentDomProxy.getCommentText();
		var isprivate = commentDomProxy.getIsPrivate();

		if (commentText == null || commentText.length == 0){
			//if no comment text supplied (the user clicked submit without
			// supplying any text), just get out of dodge.
			// perhaps we should show a message, but I think that might be too much UI clutter
			return false;
		}
		commentDomProxy.setSubmittingImage(buttonElementID);

		$.getJSONExt(CommentManager.METHOD_NAME_ADD_COMMENT,
		{
			commentMode: strCommentMode,
			commentTargetID: commentTargetID,
			commentType: CM.commentType,
			commentText: commentText,
			isprivate: isprivate
		},
				function(json) {
					commentDomProxy.clearCommentText();
					commentDomProxy.incrementTotalItemCount();
					commentDomProxy.reloadCommentList(json.result.noteID);
				},

				function(json) {
					CommentManager.private_handleError(commentDomProxy, json);
				},

				function(json) {
					commentDomProxy.clearSubmittingImage(buttonElementID);
				});

	},

	enableCommentEdit: function(commentID)
	{
		var commentDomProxy = CM.privateGetCommentDomProxy();
		commentDomProxy.enableCommentEdit(commentID);
	},

	editComment: function(commentID, commentText, buttonElementID)
	{
		var commentDomProxy = CM.privateGetCommentDomProxy();
		commentDomProxy.clearMessages();

		commentDomProxy.setSubmittingImage(buttonElementID);
		$.getJSONExt(CommentManager.METHOD_NAME_EDIT_COMMENT,
		{
			commentID: commentID,
			commentText: commentText
		},
				function(json) {
					commentDomProxy.reloadCommentList(commentID);
				},

				function(json) {
					CommentManager.private_handleError(commentDomProxy, json);
				},

				function(json) {
					commentDomProxy.clearSubmittingImage(buttonElementID);
				});

	},

	deleteComment: function(commentID)
	{
		var commentDomProxy = CM.privateGetCommentDomProxy();
		commentDomProxy.clearMessages();

		$.getJSONExt(CommentManager.METHOD_NAME_DELETE_COMMENT,
		{
			commentID: commentID
		},
				function(json) {
					//remove the rows from the commented user table
					commentDomProxy.handleDeleteComment(json);
				},

				function(json) {
					CommentManager.private_handleError(commentDomProxy, json);
				});

	},

	banListenerFromCommenting: function(strListenerIDToBan) {
		var commentDomProxy = CM.privateGetCommentDomProxy();
		commentDomProxy.clearMessages();

		$.getJSONExt(CommentManager.METHOD_NAME_BAN_COMMENT,
		{
			listenerIDToBan: strListenerIDToBan
		},
				function(json) {
					//remove the rows from the commented user table
					commentDomProxy.handleBanListener(json);
				},

				function(json) {
					CommentManager.private_handleError(commentDomProxy, json);
				});
	},

	private_handleError: function(commentDomProxy, json) {
		var message = CM.getExceptionMessage(json);
		commentDomProxy.showMessage(message, true);
	},

	getExceptionMessage: function(json) {
		var errorCode = json.code;
		//TODO:  determine which error codes will be returned and provide good messages

		var msg;
		// error codes are defined in BaseAjaxHandler.java
		if (errorCode == "1010") {
			msg = "Sorry! Your message did not pass our spam filter";
		} else if (errorCode == "1011") {
			msg = "Sorry! This Pandora user is no longer accepting your comments";
		} else if (errorCode == "1012") {
			msg = "You have reached your daily note posting limit";
		} else if (errorCode == "1013") {
			msg = "Pandora is conducting system maintenance";
		} else if (errorCode == "1017") {
			msg = "Please enter a comment";
		} else if (errorCode == "1018") {
			msg = "Your comment contained only prohibited content.";
		} else {
			msg = "Sorry! We have experienced an unexpected problem. Please try again.";
		}
		return msg;
	}
}
var CM = CommentManager;


////////////////////////////////////////////////
// BaseDomProxy
////////////////////////////////////////////////
var BaseDomProxy =
{
	initDomProxy: function()
	{
		var me = this;

		//init the pandora pager
		$("#" + me.idCommentsTable).pandoraPager({
			id: me.idCommentsTable,
			navContainers: $("#" + me.idNavContainer)
		});

		me.onInit();
	},

	showMessage: function(message, blnIsError)
	{
		var jqDivMessage = $("#" + this.idMessageContainer);

		if (blnIsError == true) {
			jqDivMessage.addClass("error");
		} else {
			jqDivMessage.removeClass("error");
		}

		jqDivMessage.html(message);
		jqDivMessage.show();

 		window.setTimeout(function(){
 			jqDivMessage.fadeOut(3000, function(){
 				jqDivMessage.hide();
 			}, 1000);
 		});
	},

	clearCommentText: function()
	{
		var jqTextBox = $("#" + this.idCommentTextBox);
		
		jqTextBox.val("");
		jqTextBox.keyup();
	},

	clearMessages: function()
	{
		$("#" + this.idMessageContainer).html("");
		$("#" + this.idMessageContainer).hide();
	},

	handleDeleteComment: function(json) {
		reloadWindow();

		//TODO:  I'm just reloading window upon delete at this point
		//       If I don't reload the window, the table paging gets messed up
		//       We don't really want to reload - we should look into populating the table via
		//       ajax and resetting the paging controls.
	},

	incrementTotalItemCount: function(){
		var jqTable = $("#" + this.idCommentsTable);

		//update the total item count
		var totalItemCount = parseInt(jqTable.attr("totalitemcount"));
		if (isNaN(totalItemCount)) {
			totalItemCount = 0;
		}

		jqTable.attr("totalitemcount", totalItemCount + 1);
	},

	handleBanListener: function(json) {
		reloadWindow();
	},

	getIsPrivate: function() {
		return $("#" + this.idChkIsPrivate + "[@type='checkbox'][@checked]").length > 0;
	},

	getCommentText: function() {
		return $("#" + this.idCommentTextBox).val();
	},

	getCommentTr: function (commentsTableID, noteID){
		return $("#tr_" + noteID, $("#" + commentsTableID));
	},

	setSubmittingImage: function(buttonElementID) {
		var jqButton = $("#" + buttonElementID);
		jqButton.css("display", "none");

		var jqImgAnim1 = $("<img src=\"/images/submitting_anim.gif\" />");
		jqButton.after(jqImgAnim1);
		jqImgAnim1.addClass("submitting_animation_image");
		jqImgAnim1.css("padding", "1px");

		var jqImgAnim2 = $("<img src=\"/images/submitting_anim_70.gif\" />");
		jqImgAnim2.css("display", "none");
		jqButton.after(jqImgAnim2);
		jqImgAnim2.addClass("submitting_animation_image");
		jqImgAnim2.css("padding", "1px");

		this.animationInterval = window.setInterval(function(){
			jqImgAnim1.toggle();
			jqImgAnim2.toggle();
		}, 250);
	},

	clearSubmittingImage: function(buttonElementID) {
		window.clearInterval(this.animationInterval);

		var jqButton = $("#" + buttonElementID);
		jqButton.css("display", "inline");

		$(".submitting_animation_image", jqButton.parent()).remove();
	}
 }


////////////////////////////////////////////////
// Dom Proxy Instances
////////////////////////////////////////////////
 var CommentDomProxyProfile =
 {
	 idCommentsTable: "tbl_comments_table",

	 idNavContainer: "comment_nav_container",

	 idCommentTextBox: "note",

	 idSubmitAnchor: "hrefAddComment",

	 idChkIsPrivate: "chkIsPrivate",

	 idMessageContainer: "comment_message_container",

	 onInit: function(){
	 },

	 onCommentDownload: function(){
		 $(".sample_link	").each(insertSample);
	 },

	 enableCommentEdit: function(commentID){
	 },

	 reloadCommentList: function(selectedItemID) {
		 var me = this;

		 var url_rowprovider = $("#" + me.idCommentsTable).attr("urlrowprovider");

		 $.ajax({
			 type: "get",
			 url: url_rowprovider + "&itemID=" + selectedItemID,
			 dataType: "html",
			 processData: false,
			 success: function(html) {
				 $("#" + me.idCommentsTable).children("tbody").children("tr").remove();

				 $("#" + me.idCommentsTable).children("tbody").append(html);

				 $("#" + me.idCommentsTable).pandoraPager(
				 {
					 currentPage: 1,
					 id: me.idCommentsTable,
					 navContainers: $("#" + me.idNavContainer)
				 });

				 me.getCommentTr(me.idCommentsTable, selectedItemID)
					 .find("div", "#sidebar_box")
					 .highlightFade({start:"#ff9900", end:"#ffffff", speed:5000, iterator:"exponential"});
			 }
		 });
	 }

 }
 $.extend(CommentDomProxyProfile, BaseDomProxy);

var CommentDomProxyMusic =
{
	idCommentsTable: "tbl_comments_table",

	idNavContainer: "comment_nav_container",

	idCommentTextBox: "note",

	idSubmitAnchor: "hrefAddComment",

	idChkIsPrivate: null,

	idMessageContainer: "comment_message_container",

	onInit: function() {
		// listenerWebname is the webname of the current listener.  if the user
		// is anonymous, listenerWebname is null.  listenerWebname is set in
		// the artist_cached, or similar...
		if (listenerWebname == null || listenerWebname.length == 0) {
			$("#comment_add_widget").css("display", "none");
		} else {
			$("#comment_add_widget").css("display", "inline");
		}
	},

	onCommentDownload: function(){
		$(".sample_link	").each(insertSample);

		// make the edit / delete boxes appear for notes belonging to this listener
		var jqOwnedCommentCells = $("td[@webname=" + listenerWebname + "]");

		jqOwnedCommentCells.parent("table").show();
		jqOwnedCommentCells.show();
	},

	enableCommentEdit: function(commentID){
		//if we're currently editing another comment, cancel out of it
		if (CommentDomProxyMusic.currentlyEditingCommentID != null){
			CommentDomProxyMusic.reloadCommentList(commentID);
		}
		CommentDomProxyMusic.currentlyEditingCommentID = commentID;

		var trNote = this.getCommentTr(this.idCommentsTable, commentID);
		var divNoteText = $("#comment_text", trNote);

		var divTextAndButtons = $("<div></div>");
		divTextAndButtons.css("text-align", "right");
		
		var txtBox = $("<textarea id=\"txtNewCommentText\" name=\"note\" rows=\"5\"></textarea>");
		txtBox.css("display", "block");

		var divNoteCount = $("<div id=\"txtNewCommentWordCount\" class=\"right\"></div>");

		//create a save button
		var saveButton = $("#hrefAddComment").clone();
		saveButton.attr("id", "hrefEditComment");
		saveButton[0].onclick = function (event){
			divNoteText.text(txtBox.val());
			CM.editComment(commentID, txtBox.val(), this.id);
			return false;
		};
		var saveButtonImg = $("img", saveButton);
		saveButtonImg.attr("src", "/images/save_button.gif");
		saveButtonImg[0].onmouseover = function (event){
			this.src='/images/save_button_hover.gif';
		};
		saveButtonImg[0].onmouseout = function (event){
			this.src='/images/save_button.gif';
		};

		//create a cance button
		var cancelButton = $("#hrefAddComment").clone();
		cancelButton.attr("id", "hrefCancelEditComment");
		cancelButton[0].onclick = function (event){
			CommentDomProxyMusic.reloadCommentList(commentID);
			return false;
		};
		var cancelButtonImg = $("img", cancelButton);
		cancelButtonImg.attr("src", "/images/cancel_button_80.gif");
		cancelButtonImg[0].onmouseover = function (event){
			this.src='/images/cancel_button_80_hover.gif';
		};
		cancelButtonImg[0].onmouseout = function (event){
			this.src='/images/cancel_button_80.gif';
		};

		//add the 'edit' mode elements
		divNoteText.after(divNoteCount);
		divNoteCount.after(divTextAndButtons);
		divTextAndButtons.append(txtBox);
		divTextAndButtons.append(saveButton);
		divTextAndButtons.append(cancelButton);

		txtBox.val(divNoteText.html().replace(/<br>/g, "\n"));
		txtBox.width(668);

		//hide the delete and edit buttons as well as the note text's div
		$(".trEnableEdit", trNote).hide();
		divNoteText.hide();

		//update the char count on keyup
		var function_onKeyUp = function(){
			var currLength = txtBox[0].value.length;
			divNoteCount.html(currLength + "/" + CM.MAX_NOTE_LENGTH);
			if (currLength > CM.MAX_NOTE_LENGTH){
				divNoteCount.addClass("classTooLong");
			}else{
				divNoteCount.removeClass("classTooLong");
			}
		}
		txtBox.keyup(function_onKeyUp);
		function_onKeyUp();
	},

	reloadCommentList: function(selectedItemID) {
		CommentDomProxyMusic.currentlyEditingCommentID = null;
		var me = this;

		var url_rowprovider = $("#" + me.idCommentsTable).attr("urlrowprovider");

		$.ajax({
			type: "get",
			url: url_rowprovider + "&itemID=" + selectedItemID + "&iecachefooler=" + new Date().getTime(),
			dataType: "html",
			processData: false,
			success: function(html) {
				while( $("#" + me.idCommentsTable).children("tbody").children().length > 0){
					$("#" + me.idCommentsTable).children("tbody").children().remove();
				}

				$("#" + me.idCommentsTable).children("tbody").append(html);


				if (selectedItemID == null){
					$("#" + me.idCommentsTable).pandoraPager({
						currentPage: 1,
						id: me.idCommentsTable,
						navContainers: $("#" + me.idNavContainer)
					});
				}else{
					var index = 1;
					var allTRs = $("#" + me.idCommentsTable).children("tbody").children("tr");
					for (var i = 0; i < allTRs.length; i++) {
						var tr = allTRs[i];
						if ($(tr).attr("item_id") == selectedItemID) {
							index = i;
							break;
						}
					}

					$("#" + me.idCommentsTable).pandoraPager({
						show_index: index,
						navContainers: $("#" + me.idNavContainer)
					});

					var commentTR =  me.getCommentTr(me.idCommentsTable, selectedItemID);
					var divSidebarBox = $("#sidebar_box", commentTR);
					divSidebarBox.highlightFade({start:"#ff9900", end:"#ffffff", speed:5000, iterator:"exponential"});
				}

			}
		});
	}

}
$.extend(CommentDomProxyMusic, BaseDomProxy);

$(document).ready(CommentManager.onPageLoad);
/**
 * Cookie plugin
 *
 * Copyright (c) 2006 Klaus Hartl (stilbuero.de)
 * Dual licensed under the MIT and GPL licenses:
 * http://www.opensource.org/licenses/mit-license.php
 * http://www.gnu.org/licenses/gpl.html
 *
 */

/**
 * Create a cookie with the given name and value and other optional parameters.
 *
 * @example $.cookie('the_cookie', 'the_value');
 * @desc Set the value of a cookie.
 * @example $.cookie('the_cookie', 'the_value', {expires: 7, path: '/', domain: 'jquery.com', secure: true});
 * @desc Create a cookie with all available options.
 * @example $.cookie('the_cookie', 'the_value');
 * @desc Create a session cookie.
 * @example $.cookie('the_cookie', '', {expires: -1});
 * @desc Delete a cookie by setting a date in the past.
 *
 * @param String name The name of the cookie.
 * @param String value The value of the cookie.
 * @param Object options An object literal containing key/value pairs to provide optional cookie attributes.
 * @option Number|Date expires Either an integer specifying the expiration date from now on in days or a Date object.
 *                             If a negative value is specified (e.g. a date in the past), the cookie will be deleted.
 *                             If set to null or omitted, the cookie will be a session cookie and will not be retained
 *                             when the the browser exits.
 * @option String path The value of the path atribute of the cookie (default: path of page that created the cookie).
 * @option String domain The value of the domain attribute of the cookie (default: domain of page that created the cookie).
 * @option Boolean secure If true, the secure attribute of the cookie will be set and the cookie transmission will
 *                        require a secure protocol (like HTTPS).
 * @type undefined
 *
 * @name $.cookie
 * @cat Plugins/Cookie
 * @author Klaus Hartl/klaus.hartl@stilbuero.de
 */

/**
 * Get the value of a cookie with the given name.
 *
 * @example $.cookie('the_cookie');
 * @desc Get the value of a cookie.
 *
 * @param String name The name of the cookie.
 * @return The value of the cookie.
 * @type String
 *
 * @name $.cookie
 * @cat Plugins/Cookie
 * @author Klaus Hartl/klaus.hartl@stilbuero.de
 */
jQuery.cookie = function(name, value, options) {
    if (typeof value != 'undefined') { // name and value given, set cookie
        options = options || {};
        var expires = '';
        if (options.expires && (typeof options.expires == 'number' || options.expires.toGMTString)) {
            var date;
            if (typeof options.expires == 'number') {
                date = new Date();
                date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000));
            } else {
                date = options.expires;
            }
            expires = '; expires=' + date.toGMTString(); // use expires attribute, max-age is not supported by IE
        }
        var path = options.path ? '; path=' + options.path : '';
        var domain = options.domain ? '; domain=' + options.domain : '';
        var secure = options.secure ? '; secure' : '';
        document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join('');
    } else { // only name given, get cookie
        var cookieValue = null;
        if (document.cookie && document.cookie != '') {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = jQuery.trim(cookies[i]);
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) == (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }
};
/**
 * modalContent jQuery Plugin
 *
 * @version   0.11
 * @since     2006-11-28
 * @copyright Copyright (c) 2006 Glyphix Studio, Inc. http://www.glyphix.com
 * @author    Gavin M. Roy <gmr@glyphix.com>
 * @license   MIT http://www.opensource.org/licenses/mit-license.php
 * @requires  >= jQuery 1.0.3 http://jquery.com/
 * @requires  dimensions.js http://jquery.com/dev/svn/trunk/plugins/dimensions/dimensions.js?format=raw
 *
 * History:
 *  0.11:
 *   2006-12-19 patch from Tim Saker <tjsaker@yahoo.com>
 *    1) Keyboard events are now only permitted on visible elements belonging to the modal layer (child elements). Attempting to place focus on any other page element will cause focus to be transferred back to the first (ordinal) visible child element of the modal layer.
 *    2) The modal overlay shading now covers the entire height of the document except for a small band at the bottom, which is the height of a scrollbar (a separate thread to be opened on this problem with dimension.js).
 *    3) I removed the code that disables and reenables the scrollbars.  This is just a suggestion really, realizing it could be one of those little things that causes fellow developers to become unnecessary foes ;=).  Personally, I found it an annoying behaviour to remove a visible scrollbar causing the page elements to shift right upon modal popup, then back after closure. If the intent was to prevent scrolling it doesn't anyway since you can still page down with the keyboard. Maybe it should be a boolean option passed in the function signature?
 *   2007-01-03 gmr
 *    1) Updated to set the top of the background div to 0
 *    2) Add 50px to the background div (ugly hack until dimensions.js returns the proper height)
 *    3) Removed the .focus from the $('#modalContent .focus') selector since that required something with a class of focus.
 *    4) Created a function for reaize and bound and unbound that so it doesnt clobber other resize functions on unbind
 *    5) Created a function for binding the .close class and bound/unbound click using it.  Close now will work on any clickable element including a map area.
 *    6) Renamed animation commands to match jQuery's.
 *
 * Call modalContent() on a DOM object and it will make a centered modal box over a div overlay preventing access to the page.
 * Create an element (anchor/img/etc) with the class "close" in your content to close the modal box on click.
 *
 * UPDATED: 2007-05-08 by Marc Novakowski to work with jQuery 1.1.x, and added "callback" parameter to modalContent() method
 */

/**
 * modalContent
 * @param content string to display in the content box
 * @param css obj of css attributes
 * @param animation (fadeIn, slideDown, show)
 * @param speed (valid animation speeds slow, medium, fast or # in ms)
 */
jQuery.modalContent = function(content, css, animation, speed, callback) {

  // if we already ahve a modalContent, remove it
  if ( $('#modalBackdrop') ) $('#modalBackdrop').remove();
  if ( $('#modalContent') ) $('#modalContent').remove();

  // position code lifted from http://www.quirksmode.org/viewport/compatibility.html
  if (self.pageYOffset) { // all except Explorer
  var wt = self.pageYOffset;
  } else if (document.documentElement && document.documentElement.scrollTop) { // Explorer 6 Strict
    var wt = document.documentElement.scrollTop;
  } else if (document.body) { // all other Explorers
    var wt = document.body.scrollTop;
  }

  // Get our dimensions

  // Get the docHeight and (ugly hack) add 50 pixels to make sure we dont have a *visible* border below our div
  var docHeight = $(document).outerHeight() + 50;
  var winHeight = $(window).height();
  var winWidth = $(window).innerWidth();
  if( docHeight < winHeight ) docHeight = winHeight;

  // Create our divs
  $('body').append('<div id="modalBackdrop" style="z-index: 1000; display: none;"></div><div id="modalContent" style="z-index: 1001; position: absolute;">' + $(content).html() + '</div>');

  // Keyboard and focus event handler ensures focus stays on modal elements only
  modalEventHandler = function( event ) {
    target = null;
    if ( event ) { //Mozilla
      target = event.target;
    } else { //IE
      event = window.event;
      target = event.srcElement;
    }
    if( $(target).filter('*:visible').parents('#modalContent').size() ) {
      // allow the event only if target is a visible child node of #modalContent
      return true;
    }
    if ( $('#modalContent') ) $('#modalContent').get(0).focus();
    return false;
  };
  $('body').bind( 'focus', modalEventHandler );
  $('body').bind( 'keypress', modalEventHandler );

  // Create our content div, get the dimensions, and hide it
  var modalContent = $('#modalContent').css('top', -1000);
  var mdcTop = wt + ( winHeight / 2 ) - (  modalContent.outerHeight() / 2);
  var mdcLeft = ( winWidth / 2 ) - ( modalContent.outerWidth() / 2);
  $('#modalBackdrop').css('top', 0).css(css).css('height', docHeight).css('width', winWidth).show();
  modalContent.css({top: mdcTop + 'px', left: mdcLeft + 'px'}).hide()[animation](speed, callback);

  // Bind a click for closing the modalContent
  modalContentClose = function(){close(); return false;};
  $('.close').bind('click', modalContentClose);

  // Close the open modal content and backdrop
  function close() {
    // Unbind the events
    $(window).unbind('resize',  modalContentResize);
    $('body').unbind( 'focus', modalEventHandler);
    $('body').unbind( 'keypress', modalEventHandler );
    $('.close').unbind('click', modalContentClose);

    // Set our animation parameters and use them
    if ( animation == 'fadeIn' ) animation = 'fadeOut';
    if ( animation == 'slideDown' ) animation = 'slideUp';
    if ( animation == 'show' ) animation = 'hide';

    // Close the content
    modalContent.hide()[animation](speed);

    // Remove the content
    $('#modalContent').remove();$('#modalBackdrop').remove();
  };

  // Move and resize the modalBackdrop and modalContent on resize of the window
   modalContentResize = function(){
    // Get our heights
    var docHeight = $(document).outerHeight();
    var winHeight = $(window).height();
    var winWidth = $(window).width();
    if( docHeight < winHeight ) docHeight = winHeight;

    // Get where we should move content to
    var modalContent = $('#modalContent');
    var mdcTop = ( winHeight / 2 ) - (  modalContent.outerHeight() / 2);
    var mdcLeft = ( winWidth / 2 ) - ( modalContent.outerWidth() / 2);

    // Apply the changes
    $('#modalBackdrop').css('height', docHeight).css('width', winWidth).show();
    modalContent.css('top', mdcTop).css('left', mdcLeft).show();
  };
  $(window).bind('resize', modalContentResize);

  $('#modalContent').focus();
};

/**
 * jQuery function init
 */
jQuery.fn.modalContent = function(css, animation, speed, callback)
{
  // If our animation isn't set, make it just show/pop
  if (!animation) { var animation = 'show'; } else {
    // If our animation isn't "fadeIn" or "slideDown" then it always is show
    if ( ( animation != 'fadeIn' ) && ( animation != 'slideDown') ) animation = 'show';
  }

  if ( !speed ) var speed = 'fast';

  // Build our base attributes and allow them to be overriden
  css = jQuery.extend({
    position: 'absolute',
    left: '0px',
    margin: '0px',
    background: '#000',
    opacity: '.55'
  }, css);

  // jQuery mojo
  this.each(function(){
    $(this).hide();
    new jQuery.modalContent($(this), css, animation, speed, callback);
  });

  // return this object
  return this;
};

/**
 * unmodalContent
 * @param animation (fadeOut, slideUp, show)
 * @param speed (valid animation speeds slow, medium, fast or # in ms)
 */
jQuery.fn.unmodalContent = function(animation, speed)
{
  // If our animation isn't set, make it just show/pop
  if (!animation) { var animation = 'show'; } else {
    // If our animation isn't "fade" then it always is show
    if ( ( animation != 'fadeOut' ) && ( animation != 'slideUp') ) animation = 'show';
  }
  // Set a speed if we dont have one
  if ( !speed ) var speed = 'fast';

  // Unbind the events we bound
  $(window).unbind('resize', modalContentResize);
  $('body').unbind( 'focus', modalEventHandler);
  $('body').unbind( 'keypress', modalEventHandler);
  $('.close').unbind('click', modalContentClose);

  // jQuery magic loop through the instances and run the animations or removal.
  this.each(function(){
    if ( animation == 'fade' ) {
      $('#modalContent').fadeOut(speed,function(){$('#modalBackdrop').fadeOut(speed, function(){$(this).remove();});$(this).remove();});
    } else {
      if ( animation == 'slide' ) {
        $('#modalContent').slideUp(speed,function(){$('#modalBackdrop').slideUp(speed, function(){$(this).remove();});$(this).remove();});
      } else {
        $('#modalContent').remove();$('#modalBackdrop').remove();
      }
    }
  });
};

/**
 *
 * The AuthenticationManager object handles user authentication via ajax
 *
 * depends on 3rd-party/jquery/jquery.jsonext.js
 *
 **/

var AuthenticationManager =
{
	COOKIE_NAME_ACTION_DATA: "actionData",

	promptForAuthentication: function(currentActionToken, data) {
		//the currentActionToken is string that clients of getJSONExt can use to figure out what the user was doing
		//prior to login.  Upon successful login, the browser will be sent back to the page that originally called getJSONExt.
		//this page can inspect the currentActionToken re-issue the pre-login ajax command.


		AuthenticationManager.setCurrentActionData(
		{
			token: currentActionToken,
			data: data
		});

		window.location.href = "/login.vm?target=" + encodeURIComponent(window.location.pathname);
	},

	setCurrentActionData: function(data) {
		$.cookie(AuthenticationManager.COOKIE_NAME_ACTION_DATA, $.toJSON(data));
	},

	getCurrentActionData: function()
	{
		var dataJSONString = $.cookie(AuthenticationManager.COOKIE_NAME_ACTION_DATA);
		return $.parseJSON(dataJSONString);
	},

	clearCurrentActionData: function()
	{
		//delete the cookie
		$.cookie(AuthenticationManager.COOKIE_NAME_ACTION_DATA, "", {expires: -1});
	}
}

/*
* depends on include/authenticationManager.js
*/
jQuery.extend(
{
	getJSONExt: function(method_name,
						 data,
						 callback_success,
						 callback_error,
						 callback_finally)
	{
		if (!callback_error)
		{   //if we don't have an error handler, throw when get an error
			callback_error = function(jsonErr)
			{
				throw jsonErr.message;
			}
		}

		if (!callback_finally)
		{   //if we don't have a finally, just create an empty one
			callback_finally = function() {
			};
		}

		var at = $.cookie('at');
		if (at != null) {
			data.at = at;
		}

		data.method = method_name;

		try {
			jQuery.getJSON("/services/ajax/", data, function(json)
			{
				try {
					if (json.stat == "ok")
					{
						callback_success(json);
					} else
					{
						if (json.code == 1000) {
							AuthenticationManager.promptForAuthentication(method_name, data);
						} else {
							callback_error(json);
						}
					}
				} finally {
					callback_finally();
				}

			});
		} catch(e) {
			callback_error(
			{
				stat: "fail",
				code: "0",
				message: e
			});
		}

	}
});
(function ($) {
    var m = {
            '\b': '\\b',
            '\t': '\\t',
            '\n': '\\n',
            '\f': '\\f',
            '\r': '\\r',
            '"' : '\\"',
            '\\': '\\\\'
        },
        s = {
            'array': function (x) {
                var a = ['['], b, f, i, l = x.length, v;
                for (i = 0; i < l; i += 1) {
                    v = x[i];
                    f = s[typeof v];
                    if (f) {
                        v = f(v);
                        if (typeof v == 'string') {
                            if (b) {
                                a[a.length] = ',';
                            }
                            a[a.length] = v;
                            b = true;
                        }
                    }
                }
                a[a.length] = ']';
                return a.join('');
            },
            'boolean': function (x) {
                return String(x);
            },
            'null': function (x) {
                return "null";
            },
            'number': function (x) {
                return isFinite(x) ? String(x) : 'null';
            },
            'object': function (x) {
                if (x) {
                    if (x instanceof Array) {
                        return s.array(x);
                    }
                    var a = ['{'], b, f, i, v;
                    for (i in x) {
                        v = x[i];
                        f = s[typeof v];
                        if (f) {
                            v = f(v);
                            if (typeof v == 'string') {
                                if (b) {
                                    a[a.length] = ',';
                                }
                                a.push(s.string(i), ':', v);
                                b = true;
                            }
                        }
                    }
                    a[a.length] = '}';
                    return a.join('');
                }
                return 'null';
            },
            'string': function (x) {
                if (/["\\\x00-\x1f]/.test(x)) {
                    x = x.replace(/([\x00-\x1f\\"])/g, function(a, b) {
                        var c = m[b];
                        if (c) {
                            return c;
                        }
                        c = b.charCodeAt();
                        return '\\u00' +
                            Math.floor(c / 16).toString(16) +
                            (c % 16).toString(16);
                    });
                }
                return '"' + x + '"';
            }
        };

	$.toJSON = function(v) {
		var f = isNaN(v) ? s[typeof v] : s['number'];
		if (f) return f(v);
	};

	$.parseJSON = function(v, safe) {
		if (safe === undefined) safe = $.parseJSON.safe;
		if (safe && !/^("(\\.|[^"\\\n\r])*?"|[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t])+?$/.test(v))
			return undefined;
		return eval('('+v+')');
	};

	$.parseJSON.safe = false;

})(jQuery);

/**
 *  jQuery Plugin highlightFade (jquery.offput.ca/highlightFade)
 *  (c) 2006 Blair Mitchelmore (offput.ca) blair@offput.ca
 */
/**
 * This is version 0.7 of my highlightFade plugin. It follows the yellow fade technique of Web 2.0 fame
 * but expands it to allow any starting colour and allows you to specify the end colour as well.
 *
 * For the moment, I'm done with this plug-in. Unless I come upon a really cool feature it should have
 * this plug-in will only receive updates to ensure future compatibility with jQuery.
 *
 * As of now (Aug. 16, 2006) the plugin has been written with the 1.0.1 release of jQuery (rev 249) which
 * is available from http://jquery.com/src/jquery-1.0.1.js
 *
 * A note regarding rgb() syntax: I noticed that most browsers implement rgb syntax as either an integer 
 * (0-255) or percentage (0-100%) value for each field, that is, rgb(i/p,i/p,i/p); however, the W3C 
 * standard clearly defines it as "either three integer values or three percentage values" [http://www.w3.org/TR/CSS21/syndata.html] 
 * which I choose to follow despite the error redundancy of the typical behaviour browsers employ.
 *
 * Changelog:
 *
 *    0.7:
 *        - Added the awesome custom attribute support written by George Adamson (slightly modified)
 *        - Removed bgColor plugin dependency seeing as attr is customizable now...
 *    0.6:
 *        - Abstracted getBGColor into its own plugin with optional test and data retrieval functions
 *        - Converted all $ references to jQuery references as John's code seems to be shifting away
 *          from that and I don't want to have to update this for a long time.
 *    0.5:
 *        - Added simple argument syntax for only specifying start colour of event
 *        - Removed old style argument syntax
 *        - Added 'interval', 'final, and 'end' properties
 *        - Renamed 'color' property to 'start'
 *        - Added second argument to $.highlightFade.getBGColor to bypass the e.highlighting check
 *    0.4:
 *        - Added rgb(%,%,%) color syntax
 *    0.3:
 *        - Fixed bug when event was called while parent was also running event corrupting the
 *          the background colour of the child
 *    0.2:
 *        - Fixed bug where an unspecified onComplete function made the page throw continuous errors
 *        - Fixed bug where multiple events on the same element would speed each subsequent event
 *    0.1:
 *        - Initial Release
 * 
 * @author          Blair Mitchelmore (blair@offput.ca)
 * @version         0.5
 */
jQuery.fn.highlightFade = function(settings) {
	var o = (settings && settings.constructor == String) ? {start: settings} : settings || {};
	var d = jQuery.highlightFade.defaults;
	var i = o['interval'] || d['interval'];
	var a = o['attr'] || d['attr'];
	var ts = {
		'linear': function(s,e,t,c) { return parseInt(s+(c/t)*(e-s)); },
		'sinusoidal': function(s,e,t,c) { return parseInt(s+Math.sin(((c/t)*90)*(Math.PI/180))*(e-s)); },
		'exponential': function(s,e,t,c) { return parseInt(s+(Math.pow(c/t,2))*(e-s)); }
	};
	var t = (o['iterator'] && o['iterator'].constructor == Function) ? o['iterator'] : ts[o['iterator']] || ts[d['iterator']] || ts['linear'];
	if (d['iterator'] && d['iterator'].constructor == Function) t = d['iterator'];
	return this.each(function() {
		if (!this.highlighting) this.highlighting = {};
		var e = (this.highlighting[a]) ? this.highlighting[a].end : jQuery.highlightFade.getBaseValue(this,a) || [255,255,255];
		var c = jQuery.highlightFade.getRGB(o['start'] || o['colour'] || o['color'] || d['start'] || [255,255,128]);
		var s = jQuery.speed(o['speed'] || d['speed']);
		var r = o['final'] || (this.highlighting[a] && this.highlighting[a].orig) ? this.highlighting[a].orig : jQuery.curCSS(this,a);
		if (o['end'] || d['end']) r = jQuery.highlightFade.asRGBString(e = jQuery.highlightFade.getRGB(o['end'] || d['end']));
		if (typeof o['final'] != 'undefined') r = o['final'];
		if (this.highlighting[a] && this.highlighting[a].timer) window.clearInterval(this.highlighting[a].timer);
		this.highlighting[a] = { steps: ((s.duration) / i), interval: i, currentStep: 0, start: c, end: e, orig: r, attr: a };
		jQuery.highlightFade(this,a,o['complete'],t);
	});
};

jQuery.highlightFade = function(e,a,o,t) {
	e.highlighting[a].timer = window.setInterval(function() { 
		var newR = t(e.highlighting[a].start[0],e.highlighting[a].end[0],e.highlighting[a].steps,e.highlighting[a].currentStep);
		var newG = t(e.highlighting[a].start[1],e.highlighting[a].end[1],e.highlighting[a].steps,e.highlighting[a].currentStep);
		var newB = t(e.highlighting[a].start[2],e.highlighting[a].end[2],e.highlighting[a].steps,e.highlighting[a].currentStep);
		jQuery(e).css(a,jQuery.highlightFade.asRGBString([newR,newG,newB]));
		if (e.highlighting[a].currentStep++ >= e.highlighting[a].steps) {
			jQuery(e).css(a,e.highlighting[a].orig || '');
			window.clearInterval(e.highlighting[a].timer);
			e.highlighting[a] = null;
			if (o && o.constructor == Function) o.call(e);
		}
	},e.highlighting[a].interval);
};

jQuery.highlightFade.defaults = {
	start: [255,255,128],
	interval: 50,
	speed: 400,
	attr: 'backgroundColor'
};

jQuery.highlightFade.getRGB = function(c,d) {
	var result;
	if (c && c.constructor == Array && c.length == 3) return c;
	if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(c))
		return [parseInt(result[1]),parseInt(result[2]),parseInt(result[3])];
	else if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(c))
		return [parseFloat(result[1])*2.55,parseFloat(result[2])*2.55,parseFloat(result[3])*2.55];
	else if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(c))
		return [parseInt("0x" + result[1]),parseInt("0x" + result[2]),parseInt("0x" + result[3])];
	else if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(c))
		return [parseInt("0x"+ result[1] + result[1]),parseInt("0x" + result[2] + result[2]),parseInt("0x" + result[3] + result[3])];
	else
		return jQuery.highlightFade.checkColorName(c) || d || null;
};

jQuery.highlightFade.asRGBString = function(a) {
	return "rgb(" + a.join(",") + ")";
};

jQuery.highlightFade.getBaseValue = function(e,a,b) {
	var s, t;
	b = b || false;
	t = a = a || jQuery.highlightFade.defaults['attr'];
	do {
		s = jQuery(e).css(t || 'backgroundColor');
		if ((s  != '' && s != 'transparent') || (e.tagName.toLowerCase() == "body") || (!b && e.highlighting && e.highlighting[a] && e.highlighting[a].end)) break; 
		t = false;
	} while (e = e.parentNode);
	if (!b && e.highlighting && e.highlighting[a] && e.highlighting[a].end) s = e.highlighting[a].end;
	if (s == undefined || s == '' || s == 'transparent') s = [255,255,255];
	return jQuery.highlightFade.getRGB(s);
};

jQuery.highlightFade.checkColorName = function(c) {
	if (!c) return null;
	switch(c.replace(/^\s*|\s*$/g,'').toLowerCase()) {
		case 'aqua': return [0,255,255];
		case 'black': return [0,0,0];
		case 'blue': return [0,0,255];
		case 'fuchsia': return [255,0,255];
		case 'gray': return [128,128,128];
		case 'green': return [0,128,0];
		case 'lime': return [0,255,0];
		case 'maroon': return [128,0,0];
		case 'navy': return [0,0,128];
		case 'olive': return [128,128,0];
		case 'purple': return [128,0,128];
		case 'red': return [255,0,0];
		case 'silver': return [192,192,192];
		case 'teal': return [0,128,128];
		case 'white': return [255,255,255];
		case 'yellow': return [255,255,0];
	}
};

