//
// Globals
//
var vvxHandle = null;			// our class library
var roomRecord = null;			// the record type for the rooms store
var roomsGrid = null;			// the rooms grid
var roomsStore = null;			// the rooms store (add/delete here and it shows up in the grid)
var participantStore = null;	// the participant data store
var participantGrid = null;		// the participant grid
var participantAttrs = null;	// attributes stored for a live grid (account, channel, etc)

var loginPanel = null;			// the panel used for login
var createChannelWin = null;	// the create channel panel
var joinProtectedChannelWin = null;	// the join protected channel password prompting window.


var debugOn = false;
if ((typeof PUGGABLE_DEBUG) === 'undefined') {
	var PUGGABLE_DEBUG = false;
} else {
	debugOn = true;
}

var log = null;

function VIVOX_startup () {

    try {
        //// Voice Setup
        window.voice = new Vivox.API(PUGGABLE.VVX_BACKEND, {
            autoDownload : false,
            logger: voiceLog,
//            debug : { debug: true, info: true, error: true, api: true},
            callbacks : {
                onVersionCheck : onVoiceVersionCheck,
                onInstall : onVoiceInstall,
                onConnected : onVoiceConnected,
                onLoggedIn : onVoiceLoggedIn,
                onLoggedOut : onVoiceLoggedOut,
                onSessionStart : onVoiceSessionStart,
                onSessionEnd : onVoiceSessionEnd,
                onParticipantAdded : onVoiceParticipantAdded,
                onParticipantUpdated : onVoiceParticipantUpdated,
                onParticipantRemoved : onVoiceParticipantRemoved,
                onChannelError : onVoiceChannelError,
                onParticipantError : onVoiceParticipantError,
                onUnhandledError : onVoiceUnhandledError,
                onLocalMicMute : onVoiceLocalMicMute
            }
        });
        page_start_install_check();
    }
        catch (e){
        trackEvent("Error", "Startup", e.errorCode + " - " + e.errorString );       
    }
}


function voiceLog(msg, type){
    switch(type){
    case "info":
    case "warn":
        info(msg);
        break;
    case "error":
        error_log(msg);
        break;
    case "debug":
        debug(msg);
    }
}
function onVoiceVersionCheck (e) {
    // this happens at voice startup, and periodically during the installation process
    // the rendering for no-voice in the grid is done on a timer, so just sets some flags here
    
    switch( e.StatusCode ){
        case Vivox.OK:
            // record a successful install
            if( getCookie("installing") ){
                trackEvent("Download", "Success");
                setCookie("installing", 0, -1);
            }

            trackEvent("Voice", "Installed", e.InstalledVersion);
            $("#allowInstall").hide();
            $('#install-wait-dialog').dialog('close');
            $('#install-restart-dialog').dialog('close');
            window.install_required = false;
            window.upgrade_required = false;                
            return;

        case Vivox.INSTALL_REQUIRED:
            if( ! window.installing ){
                trackEvent("Voice", "NeedInstall");
            }
            window.install_required = true;
            return;
        case Vivox.UPGRADE_REQUIRED:
            if( ! window.installing ){
                trackEvent("Voice", "NeedUpgrade", e.InstalledVersion);
            }
            window.upgrade_required = true;
            return;

        case Vivox.OS_NOT_SUPPORTED:
            trackEvent("Voice", "UnsupportedOS");
            return;
        case Vivox.BROWSER_NOT_SUPPORTED:
            trackEvent("Voice", "UnsupportedBrowser");
            return;
        default:
    }
}


function onVoiceInstall (e) {
    if( e.RestartRequired ) {
        $('#install-restart-dialog').dialog('open');
        return;
    }
    $('#install-wait-dialog').dialog('open');
}

function onVoiceConnected (e) {
    trackEvent("Voice", "Connect");
    VIVOX_login();
}

function onVoiceLoggedIn (e) {
    member_set_voicestatus(PUGGABLE.userkey, "LOGGED_IN")
    member_set_voiceicon(PUGGABLE.userkey, "LOGGED_IN");
    trackEvent("Voice", "Login");
    voice.startSession(PUGGABLE.vchannel, {
        name: PUGGABLE.urlkey
    });
    show_mutestatus();
}

function onVoiceLoggedOut (e) {
    member_set_voicestatus(PUGGABLE.userkey, "LOGGED_OUT");
    member_set_voiceicon(PUGGABLE.userkey, "LOGGED_OUT");
    trackEvent("Voice", "Logout");
    hide_mutestatus();
    VIVOX_login();
}

function onVoiceSessionStart (e) {
    $("#audio_state").html("Current Channel: "+e.Session.Uri);
    trackEvent("Voice", "SessionStart");
    show_mutestatus();
}
function onVoiceSessionEnd (e) {
    trackEvent("Voice", "SessionEnd");
    hide_mutestatus();

}

function onVoiceLocalMicMute(e){
    var value = e.MicMute;

	// figure out if user is really logged in
	var is_logged_in = voice.isLoggedIn();
	if (is_logged_in ) {
		show_mutestatus();
	} else {
		hide_mutestatus();
	}

	// figure out what voice icon to show
	set_mutestatus(value);
	var voicestatus = compute_voicestatus(is_logged_in, false, value, false);

	info("LocalMicMute("+value+") -> voicestatus="+voicestatus);

	// remember this member's voicestatus
	member_set_voicestatus(PUGGABLE.userkey, voicestatus);

	// set own voice icon
	member_set_voiceicon(PUGGABLE.userkey, voicestatus);

}


/*
 * Whenever a Participant is added this callback gets called.
 *
 * For this web application example we create two rows in the table for each participant.
 * The first row contains the participants display name or URI and an icon (with menu choices).
 * The second row has the speaking indication.
 *
 */
function onVoiceParticipantAdded ( e ){
	var uri = e.Participant.Uri;

	// find userkey
	var member_row = $("[toon_name='"+e.Participant.DisplayName.toLowerCase()+"']");
	var voiceicon_field = member_row.children(".voice");
	var userkey = member_row.attr("userkey");

	//// save this member info
	member_set_sipuri(userkey, uri);

	// figure out what voice icon to show
	var voicestatus = compute_voicestatus(true,
		false,
		isAudioMuted(e.Participant),
		e.Participant.IsSpeaking);

	// remember this member's voicestatus
	member_set_voicestatus(userkey, voicestatus);
	member_set_voiceicon_html(voiceicon_field, voicestatus);

	// remember this member's volume
	info('ParticipantAdded('+e.AccountHandle+','+ e.SessionUri +','+uri+') -> userkey='+userkey+', volume='+e.Participant.Volume);
	member_set_volume(userkey, e.Participant.Volume);
}

/*
 * Whenever a Participant is updated this callback gets called.
 *
 * For this web application example we update the speaking indication and the
 * participant icon to indicate the mute state of the participant.
 *
 */
function onVoiceParticipantUpdated ( e ) {

	// find userkey
	var member_row = $("[toon_name='"+e.Participant.DisplayName.toLowerCase()+"']");
	var voiceicon_field = member_row.children(".voice");
	var userkey = member_row.attr("userkey");

	if (voiceicon_field) {
        info("ParticipantUpdated() '"+e.Participant.DisplayName+"'");
		// figure out what voice icon to show
		var voicestatus = compute_voicestatus(true,
			false,
			isAudioMuted(e.Participant),
			e.Participant.IsSpeaking == "true");

		// remember this member's voicestatus
		member_set_voicestatus(userkey, voicestatus);
		member_set_voiceicon_html(voiceicon_field, voicestatus);

		// remember this member's volume
		member_set_volume(userkey, e.Participant.Volume);
	} else {
		info("ParticipantUpdated() -> unknown participant '"+e.Participant.DisplayName+"'");
	}
}


/*
 * Whenever a Participant is removed this callback gets called.
 *
 */
var reasonMap = {
    RemovedReasonLeft : "You left ",
    RemovedReasonTimedout : "You were timed out of channel ",
    RemovedReasonKicked : "You were kicked out of channel ",
    RemovedReasonBanned : "You were banned from entering channel "
};

function onVoiceParticipantRemoved(e){
	info('ParticipantRemoved() ' +  e.RemovedReason + " , " + e.Participant.DisplayName);

	$("#audio_state").html("ParticipantRemoved: "+e.Participant.DisplayName);

	// find userkey
	var member_row = $("[toon_name='"+e.Participant.DisplayName.toLowerCase()+"']");
	var userkey = member_row.attr("userkey");

	// figure out what voice icon to show
	var voicestatus = compute_voicestatus(false, false, false, false);

	// remember this member's voicestatus
	member_set_voicestatus(userkey, voicestatus);

    // do nothing for voice disconnects of other users
    if( ! e.Participant.IsMe ) {
        return;
    }

    // changing guest names requires bouncing the room, reconnect.
    if ( e.RemovedReason == Vivox.REMOVED_REASON_LEFT) {
		return;
	}

	var channelName = focusUri;
	alert( reasonMap[e.RemovedReason] + channelName );
}


// Kick a participant.
function kickParticipant(participant) {
	info('kickParticipant()');
	var row = participantGrid.getSelectionModel().getSelected();
	if (row == undefined) {
		info('kickParticipant() no row selected');
		alert('please select a participant');
	} else {
		info('kickParticipant() -> selected rows');
		var account = participantAttrs.myAttrs.AccountHandle;
		var chanuri = participantAttrs.myAttrs.FocusUri;
		var sipuri = row.data.URI;
		voice.kickChannelUser(chanuri, sipuri, completedKick);
	}
}
function completedKick(results, options){
 	if ( ! results.Success ){
        info('Kick() failed ' +results.StatusCode + ' ' + results.StatusString);
        alert('Kick operation failed: ' + results.StatusCode + ' ' + results.StatusString);
 	}
}
/*
 * Ban a participant.
 *
 */
function banParticipant(){
	info('banParticipant()');
	var row = participantGrid.getSelectionModel().getSelected();
	if (row == undefined) {
		info('banParticipant() no row selected');
		alert('please select a participant');
	}
	else {
		info('banParticipant() -> selected rows');
		var account = participantAttrs.myAttrs.AccountHandle;
		var chanuri = participantAttrs.myAttrs.FocusUri;
		var sipuri = row.data.URI;
        voice.banChannelUser(chanuri, sipuri, completedBan);
	}
}
function completedBan(results, options){
    if ( ! results.Success ){
        info('Ban() failed ' + results.StatusCode + ' ' + results.StatusString);
        alert('Ban operation failed: ' + results.StatusString + ' (' + results.StatusCode+')');
 	}
}

/*
 * mute a participant
 *
 */
function muteParticipant(){
	info('muteParticipant()');
	var row = participantGrid.getSelectionModel().getSelected();
	if (row == undefined) {
		info('muteParticipant() no row selected');
		alert('please select a participant');
	}
	else {
		info('muteParticipant() -> selected rows');
		var account = participantAttrs.myAttrs.AccountHandle;
		var chanuri = participantAttrs.myAttrs.FocusUri;
		var sipuri = row.data.URI;
        var participant = voice.getParticipant(chanuri, sipuri);
        var muteFlag = participant.IsModeratorAudioMuted;
		if ( muteFlag ){
			info('Mute '+chanuri+', sipuri '+sipuri);
            voice.muteChannelUser(chanuri, sipuri, completedMute, { name: 'Mute'});
		} else {
			info('Unmute '+chanuri+', sipuri '+sipuri);
			voice.unmuteChannelUser(chanuri, sipuri, completedMute, { name: 'UnMute'});
		}
	}
}
function completedMute(response, data){
 	if ( ! results.Success){
        info(data.name + '() failed ' + results.StatusCode + ' ' + results.StatusString);
        alert(data.name + ' operation failed: ' + results.StatusString + ' (' + results.StatusCode+')');
 	}
}

/*
 * mute audio for me.
 *
 *
 */
function muteForMe(){
	info('muteForMe()');
	var row = participantGrid.getSelectionModel().getSelected();
	if (row == undefined) {
		info('muteParticipant() no row selected');
		alert('please select a participant');
	} else {
		info('muteParticipant() -> selected rows');
		var account = participantAttrs.myAttrs.AccountHandle;
		var chanuri = participantAttrs.myAttrs.FocusUri;
		var sipuri = row.data.URI;
		var participant = voice.getParticipant(account, chanuri, sipuri);
		var muteFlag = ! participant.IsLocalAudioMuted;
		info('chanuri ' + chanuri + ', sipuri ' + sipuri + ' muteFlag ' + muteFlag);
		voice.setUserLocalMute(chanuri, sipuri, muteFlag);
	}
}

/////////////////////////////////////// Puggable Functions


///////////////// Page Actions

function select_toon() {
    trackEvent("Select", "Prompt");
	$('#toon-dialog').dialog('open');
}
function change_toon(event) {
    trackEvent("Select", "Switch");
    $('#toon-dialog').dialog('open');
}

function share_pug() {
	$('#share-dialog').dialog('open');
}

function members_setup() {
	// initialize
	PUGGABLE.members = {};

	// load initial members
	members_load();

	// setup a timer to refresh member list frequently
	$.timer(5000, function (timer) {
			pug_refresh();
			// ZZZ timer.reset(5000); // remove before submit!
		});
}

// check if voice functionality is available for this member
function member_has_voice(userkey) {
	if (userkey && PUGGABLE.members[userkey]) {
		if (PUGGABLE.members[userkey].voicestatus) {
			return true;
		}
	}
	return false;
}

// remember this member's sip URI
function member_set_sipuri(userkey, sipuri) {
	if (!PUGGABLE.members[userkey]) {
		PUGGABLE.members[userkey] = {};
	}
	PUGGABLE.members[userkey].sipuri = sipuri;
}

// remember this member's voicestatus
function member_set_voicestatus(userkey, voicestatus) {
	if (!PUGGABLE.members[userkey]) {
		PUGGABLE.members[userkey] = {};
	}
	PUGGABLE.members[userkey].voicestatus = voicestatus;
}

// remember this member's volume
function member_set_volume(userkey, volume) {
	//info("member_set_volume("+userkey+", "+volume+")");
	if (!userkey) {
		return;
	}
	if (!PUGGABLE.members[userkey]) {
		PUGGABLE.members[userkey] = {};
	}
	PUGGABLE.members[userkey].volume = volume;
}
function member_get_volume(userkey) {
	if (PUGGABLE.members[userkey]) {
		return PUGGABLE.members[userkey].volume;
	}
	return 50;
}
// remember this member's volume
function member_change_volume(userkey, volume) {
	if (!userkey) {
		return;
	}
	if (!PUGGABLE.members[userkey]) {
		PUGGABLE.members[userkey] = {};
	}
	PUGGABLE.members[userkey].volume = volume;

	// send the new local volume setting to Vivox
	voice.setUserLocalVolume(PUGGABLE.vchannel, PUGGABLE.members[userkey].sipuri, volume);
}

function member_is_muted(userkey) {
	if (userkey && PUGGABLE.members[userkey] && PUGGABLE.members[userkey].is_muted) {
		return true;
	}
	return false;
}
function member_mute(userkey, muted) {
	info("member_mute("+userkey+", "+muted+")");

	if (muted > 0) {
		// set this member to mute
		if (!PUGGABLE.members[userkey]) {
			PUGGABLE.members[userkey] = {};
		}
		PUGGABLE.members[userkey].is_muted = true;

		// change button to read "UnMute"
		$("#mute-"+userkey).text("UnMute").attr("muted") = 0;
	} else {
		// set this member to un-mute
		if (!PUGGABLE.members[userkey]) {
			PUGGABLE.members[userkey] = {};
		}
		PUGGABLE.members[userkey].is_muted = false;

		// change button to read "Mute"
		$("#mute-"+userkey).text("Mute").attr("muted") = 1;
	}

	// send the mute setting to Vivox
	voice.setUserLocalMute(PUGGABLE.vchannel, PUGGABLE.members[userkey].sipuri, muted);
}

function members_load() {
	pug_refresh();
}

function pug_refresh() {
	$.getJSON("/ajax/pug_status.php?pug="+PUGGABLE.urlkey + "&nocache=" + new Date().getTime(),
						function(data) {
							//// status
							PUGGABLE.status == data.status;
							if (data.status == 'kicked') {
								// show "you've been kicked"
								$('#kicked-dialog').dialog('open');

								// log out of the vchannel
						        VIVOX_logout();
								return;
							}

							//// instance/boss

							//// members
							// clear existing member info
							$("#members tbody").html("");

							// for each pug member
						  	$.each(data.members, function(i, member) {
						    // remember this member
								if (!PUGGABLE.members[member.userkey]) {
									PUGGABLE.members[member.userkey] = {};
									PUGGABLE.members[member.userkey].voicestatus = '';
									PUGGABLE.members[member.userkey].sipuri = '';
								}

								if (member.name) {
									member.namelower = member.name.toLowerCase();
								} else {
									member.namelower = "";
								}


								// add a row to the members table
								var tblRow =
									'<tr id="member-'+member.userkey+'" userkey="'+member.userkey+'" toon_name="'+member.namelower+'" class="'
									+((member.is_leader>=1)?'leader':'member')
									//+((member.userkey==PUGGABLE.userkey)?'selfmember':'member')
							+((member.last_seen>=10)?' disconnected':'')+'">'
							        +'<td>'+'<select id="role-'+member.userkey+'" class="ui-widget-content">'
												+'<option '+(((member.pug_role_id==1) || (member.pug_role_id==2) || (member.pug_role_id==3))?'':'selected')+'></option>'
												+'<option '+((member.pug_role_id==1)?'selected':'')+'>DPS</option>'
												+'<option '+((member.pug_role_id==2)?'selected':'')+'>Heals</option>'
												+'<option '+((member.pug_role_id==3)?'selected':'')+'>Tank</option>'
												+'</select>'+'</td>'
								      +'<td>'+(member.armory_url?('<a href="http://www.wowarmory.com/character-sheet.xml?'+member.armory_url+'" target="_blank">'):'')+(member.wow_toon_name || '-')+(member.armory_url?'</a>':'')+((member.userkey==PUGGABLE.userkey)?' <a class="small" onClick="change_toon();">[change]</a>':'')+'</td>'
							        +'<td>'+(member.level || '-')+'</td>'
//							        +'<td>'+(member.armory_url?('<a href="http://www.wowarmory.com/character-achievements.xml?'+member.armory_url+'" target="_blank">'):'')+(member.armory_score || '-')+(member.armory_url?'</a>':'')+'</td>'
							        +'<td>'+(member.wowhead_gearscore > 0
                                            ?('<a href="' + member.wowhead_url + '" target="_blank">')
                                            :'')
                                            +(member.wowhead_gearscore > 0 ? member.wowhead_gearscore : '-')
                                            +(member.wowhead_gearscore > 0
                                              ?'</a>'
                                              :'')
                                            +'</td>'
							        +'<td class="medium">'+((member.talent_name || '-')+' '+(member.class_name || '')+'<br/>'+(member.talent_spec || ''))+'</td>'
							        +'<td class="medium">'+(member.talent2_name?((member.talent2_name || '-')+' '+(member.class_name || '')+'<br/>'+(member.talent2_spec || '')):'')+'</td>'
							        +'<td class="medium">'+(member.emblems || '-')+'</td>'
							        +'<td class="voice">'+' '+'</td>'
							        +'<td id="voice-'+member.userkey+'"><div id="volume-'+member.userkey+'" class="volume-slider"></div></td>'
											+'<td><div id="buttons-'+member.userkey+'"</div></td>'
							        +'<td class="tiny">'+member.userkey+'</td>'
							        +'</tr>';
							    $(tblRow).appendTo("#members tbody");

								// setup of role selector
								$("#role-"+member.userkey).change(function(event){
                                    var selected = $("#role-"+member.userkey + " option:selected");
                                    var role = selected.text();
                                    $.post('ajax/select_role.php', {
                                        role: role,
                                        userkey: member.userkey,
                                        pug: PUGGABLE.urlkey
									});
								});

								if (member_has_voice(member.userkey)) {
									// setup voiceicon
									member_set_voiceicon(member.userkey,
										 PUGGABLE.members[member.userkey].voicestatus);

									if (member.userkey!=PUGGABLE.userkey) {
										// setup volume slider
										$("#volume-"+member.userkey).slider({
													value: member_get_volume(member.userkey),
													orientation: "horizontal",
													slide: function(event, ui) {
														member_change_volume(member.userkey, ui.value);
													}
												});
									}

									/*
									// setup mute button
									if (member_is_muted(member.userkey)) {
										$("#buttons-"+member.userkey).append('<button id="mute-'+member.userkey+'" muted=0 class="ui-state-default ui-corner-all mute" type="button">UnMute</button>');
									} else {
										$("#buttons-"+member.userkey).append('<button id="mute-'+member.userkey+'" muted=1 class="ui-state-default ui-corner-all mute" type="button">Mute</button>');
									}
									$("#mute-"+member.userkey).bind("click", function(e){
										member_mute(member.userkey, $("#mute-"+member.userkey).attr("muted"));
									});
									*/
                                }
                                  if( (window.install_required || window.upgrade_required)
                                          && (member.userkey==PUGGABLE.userkey) ){

                                      var msg = (window.upgrade_required) ? 'Upgrade Voice Plugin' : 'Join Voice Chat';
                                      $("#voice-"+member.userkey).html('<button id="join_voice" class="ui-state-default ui-corner-all kick" type="button">' + msg + '</button>');
                                      $('#join_voice').click( function () {
                                          $('#install-dialog').dialog('open');
                                      });
                                  }

								if (PUGGABLE.is_leader && (member.userkey!=PUGGABLE.userkey)) {
									// setup kick button
									$("#buttons-"+member.userkey).append('<button id="kick-'+member.userkey+'" class="ui-state-default ui-corner-all kick" type="button">Kick</button>');
									$("#kick-"+member.userkey).click(function () {kick_confirm(member.userkey)});
								}

						  });
						});

}

// helper function for dealing with user/moderator, muted, speaking, etc.
function compute_voicestatus(is_loggedin, is_moderator, is_muted, is_speaking) {
	info("compute_voicestatus("+is_loggedin+", "+is_moderator+", "+is_muted+", "+is_speaking+")");

    // ...
    if( is_loggedin.constructor == String ){
        is_loggedin = (is_loggedin == "true");
    }
    if( is_moderator.constructor == String ){
        is_moderator = (is_moderator == "true");
    }
    if( is_muted.constructor == String ){
        is_muted = (is_muted == "true");
    }
    if( is_speaking.constructor == String ){
        is_speaking = (is_speaking == "true");
    }

	if (is_loggedin) {
		var voicestatus = "LOGGED_IN";
		if (is_moderator) {
			voicestatus = voicestatus+"_MODERATOR";
		}
		if (is_muted) {
			voicestatus = voicestatus+"_MUTED";
		} else if (is_speaking) {
			voicestatus = voicestatus+"_SPEAKING";
		}
		info("compute_voicestatus("+is_loggedin+", "+is_moderator+", "+is_muted+", "+is_speaking+") -> "+voicestatus);
		return voicestatus;
	} else if (0) {
		return "LOGGED_OUT_CONNECTED";
	} else {
		return "LOGGED_OUT";
	}
	return "UNKNOWN";
}

function member_voiceicon_html(voicestatus) {
	//info('member_set_voiceicon() -> '+userkey+' '+status);
	// find the right member row, and put an image in the right field
	var voiceicon;
	if (voicestatus == "LOGGED_IN") {
		voiceicon = '<img src="images/voice_loggedin.png" title="voice: logged in" width="20" height="20">';
	} else if (voicestatus == "LOGGED_IN_SPEAKING") {
		voiceicon = '<img src="images/voice_speaking.png" title="voice: speaking" width="20" height="20">';
	} else if (voicestatus == "LOGGED_IN_MUTED") {
		voiceicon = '<img src="images/voice_muted.png" title="voice: muted" width="20" height="20">';
	} else if (voicestatus == "LOGGED_IN_MODERATOR") {
		voiceicon = '<img src="images/voice_loggedin_moderator.png" title="voice moderator: logged in" width="20" height="20">';
	} else if (voicestatus == "LOGGED_IN_MODERATOR_SPEAKING") {
		voiceicon = '<img src="images/voice_speaking_moderator.png" title="voice moderator: speaking" width="20" height="20">';
	} else if (voicestatus == "LOGGED_IN_MODERATOR_MUTED") {
		voiceicon = '<img src="images/voice_muted_moderator.png" title="voice moderator: muted" width="20" height="20">';
	} else if (voicestatus == "LOGGED_OUT") {
		voiceicon = '<img src="images/voice_loggedout.png" title="voice: logged out" width="20" height="20">';
	} else {
		voiceicon = '';
	}
	return (voiceicon);
}

function member_set_voiceicon(userkey, voicestatus) {
	//info('member_set_voiceicon() -> '+userkey+' '+status);
	// find the right member row, and put an image in the right field
	$('#member-'+userkey+' > .voice').html(member_voiceicon_html(voicestatus));
}

function member_set_voiceicon_html(element, voicestatus) {
	// set this eleemnt to show the right voicicon image
	element.html(member_voiceicon_html(voicestatus));
}

function kick_confirm(kickee) {
	PUGGABLE.kickee = kickee;
	$('#kickconfirm-dialog').dialog('open');
}

function show_mutestatus() {
	info('show_mutestatus() ');
	$("#mutestatus").show();
}

function hide_mutestatus() {
	$("#mutestatus").hide();
}

function set_mutestatus(is_muted) {
	info('set_mutestatus('+is_muted+') ');
	if (is_muted) {
		$("#mutestatus").addClass('is_muted').html('<a onClick="toggle_mutestatus();" title="Click to Unmute"><img src="images/mic-mute.png"> Mic is <span <span class="mute-text">MUTED</span></a>');
	} else {
		$("#mutestatus").removeClass('is_muted').html('<a onClick="toggle_mutestatus();" title="Click to Mute"><img src="images/mic.png"> Mic is <span class="mute-text">Live</span></a>');
	}
}

function toggle_mutestatus() {
	var is_muted = false;
	if ($("#mutestatus").hasClass('is_muted')) {
		is_muted = true;
	}
	info('toggle_mutestatus('+is_muted+') ');
	voice.setLocalMicMute(!is_muted);
	set_mutestatus(!is_muted);
}

function wowdata_setup() {
	// parse WOW_INSTANCES to populate instance selector
    var classattr;
	for (var i=0; i<WOW_INSTANCES.length; i++) {
		if (WOW_INSTANCES[i] && (WOW_INSTANCES[i][0] != "")) {
            classattr = ( WOW_INSTANCES[i].length == 1 ) ? "class='option-header'" : "";
			$("#wow-instance").append("<option " + classattr + " value='"+i+"'>"+WOW_INSTANCES[i][0]+"</option>");
		}
	}

	// setup default boss info links
	var orig_links = {};
	orig_links['#link-tactics-wowwiki'] = $('#link-tactics-wowwiki').attr('href');
	orig_links['#link-tactics-thottbot'] = $('#link-tactics-thottbot').attr('href');
	orig_links['#link-tactics-wowhead'] = $('#link-tactics-wowhead').attr('href');
	orig_links['#link-loot-wowhead'] = $('#link-loot-wowhead').attr('href');
	orig_links['#link-loot-thottbot'] = $('#link-loot-thottbot').attr('href');
	orig_links['#link-movies-tankspot'] = $('#link-movies-tankspot').attr('href');
	orig_links['#link-movies-youtube'] = $('#link-movies-youtube').attr('href');
	orig_links['#link-movies-warcraftmovies'] = $('#link-movies-warcraftmovies').attr('href');

    function default_boss_links() {
        var default_text = "";
		format_boss_link('#link-tactics-wowwiki', default_text,  orig_links['#link-tactics-wowwiki']);
		format_boss_link('#link-tactics-thottbot', default_text,  orig_links['#link-tactics-thottbot']);
		format_boss_link('#link-tactics-wowhead', default_text,  orig_links['#link-tactics-wowhead']);
		format_boss_link('#link-loot-wowhead', default_text,  orig_links['#link-loot-wowhead']);
		format_boss_link('#link-loot-thottbot', default_text,  orig_links['#link-loot-thottbot']);
		format_boss_link('#link-movies-tankspot', default_text,  orig_links['#link-movies-tankspot']);
		format_boss_link('#link-movies-youtube', default_text,  orig_links['#link-movies-youtube']);
		format_boss_link('#link-movies-warcraftmovies', default_text,  orig_links['#link-movies-warcraftmovies']);
	}

	//// setup change event for instance selector
	// keep a copy of original boss selector
	var wow_boss_clone = $('#wow-boss').clone();
	$('#wow-instance').change(function() {
		var instance = parseInt($(this).val());
		// reset the boss select on each change
		$('#wow-boss').html(wow_boss_clone.html());

		if (instance >= 0) {
			// populate the boss select with values from WOW_INSTANCES
			for (var j=instance; (j<WOW_INSTANCES.length) && WOW_INSTANCES[j][2] && ((j==instance) || (WOW_INSTANCES[j][0]=="")); j++) {
				$("#wow-boss").append("<option value='"+j+"'>"+WOW_INSTANCES[j][2]+"</option>");
			}
		}

		// set instance info link(s)
		if (instance >= 0) {
			$('#link-instance').html('WoW Head: <b>'+WOW_INSTANCES[instance][0]+'</b>').attr('href', WOW_INSTANCES[instance][1]);
		} else {
			$('#link-instance').html('').attr('href', '');
		}

		// unset boss info links
		default_boss_links();

        var options = $('#wow-boss > option');
        if( options.length > 1 ){
            options[1].selected = true;
            select_boss( options[1].value );
        }
	});

	//// setup change event for boss selector
	$('#wow-boss').change(function() {
		var boss = parseInt($(this).val());
        select_boss(boss);
    });

    function select_boss ( boss ) {
		// set boss info link(s)
		if (boss < 0 ) {
			default_boss_links();
            return;
		}

        var boss_name = WOW_INSTANCES[boss][2];
        format_boss_link('#link-tactics-wowhead', boss_name, WOW_INSTANCES[boss][3]);
        format_boss_link('#link-tactics-thottbot', boss_name, WOW_INSTANCES[boss][4]);
        format_boss_link('#link-tactics-wowwiki', boss_name, WOW_INSTANCES[boss][5]);
        format_boss_link('#link-loot-wowhead', boss_name, WOW_INSTANCES[boss][6]);
        format_boss_link('#link-loot-thottbot', boss_name, WOW_INSTANCES[boss][7]);
        format_boss_link('#link-movies-tankspot', boss_name, WOW_INSTANCES[boss][8]);
        format_boss_link('#link-movies-youtube', boss_name, WOW_INSTANCES[boss][9]);
        format_boss_link('#link-movies-warcraftmovies', boss_name, WOW_INSTANCES[boss][10]);

	}

}

function format_boss_link (link_id, name, link) {
    var el = $(link_id);
    var text = String(el.html()).replace(/\s-\s.*$/, '');
    el.html( name ? text + " - " + name : text);
    el.attr('href', link)
}

function wowdata_select_instance(instance) {
	if (PUGGABLE.wow_instance) {

	}

	for (var i=0; i<WOW_INSTANCES.length; i++) {
		if (WOW_INSTANCES[i][0] != "") {
			$("#wow-instance").append("<option value='"+i+"'></option>");
		}
	}

}

function wowdata_select_boss(instance, boss) {
	if (PUGGABLE.wow_instance) {

	}

}


///////////////// Vivox Voice Actions
function VIVOX_logout () {
    if( window.voice ){
        voice.logout();
    }
}

/*
 * Called in response to link click to start the vivox plugin download
 */
function VIVOX_install(){
    var status = voice.detectPluginSupport();
    switch(status){
        case Vivox.BROWSER_NOT_SUPPORTED:
            alert("Vivox Web Voice does not yet support your browser.");
            trackEvent("Download", "failed-browser");
            return;
        case Vivox.OS_NOT_SUPPORTED:
            alert("Vivox Web Voice does not yet support your operating system.");
            trackEvent("Download", "failed-os");
            return;
        case Vivox.OK:
    }

    // display a UI aid to help them find the browser allow download bar
    if( jQuery.browser.msie || jQuery.browser.mozilla ){
        $("#allowInstall").show();
    }
    
    info("setting install cookie");
    setCookie("installing", (new Date()).getTime(), 1);

    // timeout prevents cancelling of pageTracker
    setTimeout(function () {
        window.installing = true;
        trackEvent("Download", "Start");
        voice.startPluginInstall();
    }, 500);
}

/*
 * Called to perform a login
 */
function VIVOX_login() {

	info('login() ');

    if( ! PUGGABLE.toon.name ) {
        info('VIVOX_login() -> Toon not selected, deferring login.');
        return;
    }

	var username = PUGGABLE.toon.region+'.'+PUGGABLE.toon.server+'.'+PUGGABLE.toon.name;

    if( !(window.voice  && voice.getConnector()) ){
        info('VIVOX_login() -> Not Connected.');
        return;
    }

    if ( voice.isLoggedIn() ) {

        if( voice.getAccount().DisplayName == username ) {
            info('VIVOX_login() -> User is already logged in as that user.');
            return;
        }

        info('VIVOX_login() -> User is already logged in as another user: logging out.');
        voice.logout();
        return;
    }

	info('login() -> '+username);
    voice.anonymousLogin(username);
}


/*
 * utility function to determine if the IParticipant structure has the user
 * as being muted or not.
 *
 */
function isAudioMuted(participant){
    return ( participant.IsModeratorAudioMuted
            || participant.IsLocalAudioMuted
            || participant.IsLocalAudioMutedTarget
            || participant.IsModeratorAudioMuted );
}



/*
 * handle channel error by displaying an Error dialog
 */
var errorCodeMap = ["No error",
					"Operation failed on channel",
					"Invalid Channel URI ",
					"Operation not valid for Peer to Peer call ",
					"Invalid syntax ",
					"Session is already active ",
					"Text not enabled on channel ",
					"Access denied ",
					"Channel not found "
];

function onVoiceChannelError(e){
    trackEvent("Error", "Channel", e.ErrorCode + ", " + e.SessionUri);
	var msg='';
	var channelName = e.SessionUri;
	if (e.ErrorCode > 0 && e.ErrorCode < 9) {
			msg = errorCodeMap[e.ErrorCode]+'('+e.ErrorCode+'): '+channelName;
	} else {
			msg = 'Error number '+ e.ErrorCode+' on channel '+channelName;
	}
	alert('ChannelError: '+msg);
}

/*
 * handle participant error by displaying an Error dialog
 */
function onVoiceParticipantError(e){
    trackEvent("Error", "Participant", e.ErrorCode + ", " + e.SessionUri+ ', ' + e.ParticipantUri);
	var msg='';
    var channelName = e.SessionUri;

	if (e.ErrorCode > 0 && e.ErrorCode < 9) {
		msg = errorCodeMap[e.ErrorCode] +channelName+' for '+ e.ParticipantUri;
	} else {
		msg = 'Channel error number ' + e.ErrorCode + ' '+channelName+' for ' + e.ParticipantUri;

	}
	alert('ParticipantError: '+msg);
}

/*
 *
 * handle unhandled error by displaying an Error dialog
 *
 *
 */
function onVoiceUnhandledError(e){
    trackEvent("Error", "Unhandled", e.ErrorCode + ", " + e.DiagnosticCode + ', ' + e.DiagnosticString);
	var msg='';
	if (e.ErrorCode > 0 && e.ErrorCode < 9) {
		msg = 'Error ' + errorCodeMap[e.ErrorCode] + ': (' + e.DiagnosticCode + ') ' + e.DiagnosticString;
	} else {
		msg = 'Error '+e.ErrorCode+': ('+e.DiagnosticCode+') '+e.DiagnosticString;
	}
	info('UnhandledError: '+msg);
}

/////////////////// Utility Functions

function clientMessageHandler(xml){
    info('clientMessageHandler() -> xml ' + xml);
}




function info(msg){
    if (debugOn == false) {return;}
	if ( window.log ){
        window.log.info('Puggable: '+msg);
    }
    if (window.console) {
        if (window.console.log) {
            window.console.log('Puggable: ' + msg);
        }
    }
}
function debug(msg){
    if (debugOn == false) {return;}
	if ( log ){log.info('Puggable (debug): '+msg); return;}
    if (window.console) {
        if (window.console.log) {
            window.console.log('Puggable (debug): ' + msg);
        }
    }
}
function error_log(msg){
    if (debugOn == false) {return;}
	if ( log ){log.debug('Puggable (error_log): '+msg);}
    if (window.console) {
        if (window.console.log) {
            window.console.log('Puggable (error_log): '+msg);
        }
    }
}


/*
*
*	jQuery Timer plugin v0.1
*		Matt Schmidt [http://www.mattptr.net]
*
*	Licensed under the BSD License:
*		http://mattptr.net/license/license.txt
*
*/
jQuery.timer = function (interval, callback)
{
 /**
  *
  * timer() provides a cleaner way to handle intervals
  *
  *	@usage
  * $.timer(interval, callback);
  *
  *
  * @example
  * $.timer(1000, function (timer) {
  * 	alert("hello");
  * 	timer.stop();
  * });
  * @desc Show an alert box after 1 second and stop
  *
  * @example
  * var second = false;
  *	$.timer(1000, function (timer) {
  *		if (!second) {
  *			alert('First time!');
  *			second = true;
  *			timer.reset(3000);
  *		}
  *		else {
  *			alert('Second time');
  *			timer.stop();
  *		}
  *	});
  * @desc Show an alert box after 1 second and show another after 3 seconds
  *
  *
  */
	var interval = interval || 100;

	if (!callback)
		return false;

	_timer = function (interval, callback) {
		this.stop = function () {
			clearInterval(self.id);
		};

		this.internalCallback = function () {
			callback(self);
		};

		this.reset = function (val) {
			if (self.id)
				clearInterval(self.id);

			var val = val || 100;
			this.id = setInterval(this.internalCallback, val);
		};

		this.interval = interval;
		this.id = setInterval(this.internalCallback, this.interval);

		var self = this;
	};

	return new _timer(interval, callback);
};


/*
	parseUri 1.2.1
	(c) 2007 Steven Levithan <stevenlevithan.com>
	MIT License
*/
function parseUri (str) {
	var	o   = parseUri.options,
		m   = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
		uri = {},
		i   = 14;

	while (i--) uri[o.key[i]] = m[i] || "";

	uri[o.q.name] = {};
	uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
		if ($1) uri[o.q.name][$1] = $2;
	});

	return uri;
};
parseUri.options = {
	strictMode: false,
	key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
	q:   {
		name:   "queryKey",
		parser: /(?:^|&)([^&=]*)=?([^&]*)/g
	},
	parser: {
		strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
		loose:  /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
	}
};

// {{{ serialize
function serialize( mixed_value ) {
    // Generates a storable representation of a value
    //
    // +    discuss at: http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_serialize/
    // +       version: 812.3015
    // +   original by: Arpad Ray (mailto:arpad@php.net)
    // +   improved by: Dino
    // +   bugfixed by: Andrej Pavlovic
    // +   bugfixed by: Garagoth
    // %          note: We feel the main purpose of this function should be to ease the transport of data between php & js
    // %          note: Aiming for PHP-compatibility, we have to translate objects to arrays
    // *     example 1: serialize(['Kevin', 'van', 'Zonneveld']);
    // *     returns 1: 'a:3:{i:0;s:5:"Kevin";i:1;s:3:"van";i:2;s:9:"Zonneveld";}'
    // *     example 2: serialize({firstName: 'Kevin', midName: 'van', surName: 'Zonneveld'});
    // *     returns 2: 'a:3:{s:9:"firstName";s:5:"Kevin";s:7:"midName";s:3:"van";s:7:"surName";s:9:"Zonneveld";}'

    var _getType = function( inp ) {
        var type = typeof inp, match;
        var key;
        if (type == 'object' && !inp) {
            return 'null';
        }
        if (type == "object") {
            if (!inp.constructor) {
                return 'object';
            }
            var cons = inp.constructor.toString();
            if (match = cons.match(/(\w+)\(/)) {
                cons = match[1].toLowerCase();
            }
            var types = ["boolean", "number", "string", "array"];
            for (key in types) {
                if (cons == types[key]) {
                    type = types[key];
                    break;
                }
            }
        }
        return type;
    };
    var type = _getType(mixed_value);
    var val, ktype = '';

    switch (type) {
        case "function":
            val = "";
            break;
        case "undefined":
            val = "N";
            break;
        case "boolean":
            val = "b:" + (mixed_value ? "1" : "0");
            break;
        case "number":
            val = (Math.round(mixed_value) == mixed_value ? "i" : "d") + ":" + mixed_value;
            break;
        case "string":
            val = "s:" + mixed_value.length + ":\"" + mixed_value + "\"";
            break;
        case "array":
        case "object":
            val = "a";
            /*
            if (type == "object") {
                var objname = mixed_value.constructor.toString().match(/(\w+)\(\)/);
                if (objname == undefined) {
                    return;
                }
                objname[1] = serialize(objname[1]);
                val = "O" + objname[1].substring(1, objname[1].length - 1);
            }
            */
            var count = 0;
            var vals = "";
            var okey;
            var key;
            for (key in mixed_value) {
                ktype = _getType(mixed_value[key]);
                if (ktype == "function") {
                    continue;
                }

                okey = (key.match(/^[0-9]+$/) ? parseInt(key) : key);
                vals += serialize(okey) +
                        serialize(mixed_value[key]);
                count++;
            }
            val += ":" + count + ":{" + vals + "}";
            break;
    }
    if (type != "object" && type != "array") val += ";";
    return val;
}// }}}


// courtesy of http://www.w3schools.com/JS/js_cookies.asp
function setCookie(c_name,value,expiredays)
{
    var exdate=new Date();
    exdate.setDate(exdate.getDate()+expiredays);
    document.cookie=c_name+ "=" +escape(value)+
                    ((expiredays==null) ? "" : ";expires="+exdate.toGMTString());
}
function getCookie(c_name)
{
    if (document.cookie.length>0)
    {
        var c_start = document.cookie.indexOf(c_name + "=");
        if (c_start!=-1)
        {
            c_start=c_start + c_name.length+1;
            c_end=document.cookie.indexOf(";",c_start);
            if (c_end==-1) c_end=document.cookie.length;
            return unescape(document.cookie.substring(c_start,c_end));
        }
    }
    return "";
}

function page_start_install_check () {
    // a cookie indicates that we recently started install flow, here we try to catch
    // IE's refresh on allow-download to maintain dialog state and begin the download on refresh

    var cookie = getCookie("installing");
    if( ! cookie ){
        // not resuming an installation
        return;
    }

    // First time visiting after a install, excellent! we'll catch and log them in onVersionCheck
    if(  voice.isPluginInstalled() ){
        return;
    }

    if( jQuery.browser.msie ){
        trackEvent("Download", "IERefreshCatch");       
        VIVOX_install();
        return;
    }

    // this metric won't mean much, as who knows if they'll come back. Something, though.
    trackEvent("Download", "MissingOnRefresh");
    setCookie("installing", 0, -1);
}

function configure_ptt () {
    if( window.voice && voice.isPluginInstalled() ) {
        trackEvent("Voice", "PTTConfig");        
        voice.owiPushToTalk();
    }
    else {
        $("#install-dialog").dialog('open');
    }
}