/**
 * @name discovermynet
 * @author G.Manfredi
 * Copyright (c) 2009 Firehose, LLC
 */

var TemplateCode = {}; // holds template code for creation of albums, albumPages, etc.
var Controller; // an instance of the Controller Class
var View; // an instance of the View Class
var Model; // an instance of the Model Class 
var LocalFlag; // local flag to see if we're running locally
var HideLoadingTimerID; // timerID for pause after "showmenu" on flash player and removal of loading indicator
var AlbumToBePlayed; // holds album to be played
var TrackContinue = false;
var HoveredAlbum;
var a2z_1_so;
var _WI; // used by windowit
var InitCommand = {}; // holds info on search and other commands given in URL arguments
var AlbumHashID = 0;

// Initialization
// $(function(){
$("console.log").ready(function(){
	$.debug(true);
	
    // force "Array.indexOf" for IE
    if (!Array.indexOf) {
        Array.prototype.indexOf = function(obj){
            for (var i = 0; i < this.length; i++) {
                if (this[i] == obj) {
                    return i;
                }
            }
            return -1;
        };
    }
    
    // add sorter jQuery plugin
    jQuery.fn.sort = function() {
	  return this.pushStack( [].sort.apply( this, arguments ), []);
	}; 
	
	// get URL search argument, if any, and replace _ with spaces if so
	var str = URLQueryString("search");
	if (str) {
		InitCommand.search = str.replace(/_/g, " ");
	} else {
		// if no search argument, see if we have an explicit album ID given to show
		str = URLHashString();
		if(str.slice(0,2) == "id"){
			AlbumHashID = parseInt(str.slice(2));
		}
	}
	
	// just show intro for now
	$("#home").show();
		$("#music").hide();
		
	// are we running locally?
	LocalFlag = ((document.URL.substr(0, 5) == "file:") || (document.URL.substr(17, 4) == "8001") ||
		(document.URL.substr(21,4) == "8001") || (document.URL.substr(17, 4) == "8000") ||
		(document.URL.substr(21,4) == "8000"));
	
	// hijax the history
	$.ajaxHistory.initialize();
	
	// pull template code from the DOM
	TemplateCode.album = $("#templateCode #theAlbum li.album");
	TemplateCode.albumPage = $("#templateCode .albumPage");
	TemplateCode.albumMultiSearch = $("#templateCode #albumMultiSearch li.album");
	TemplateCode.albumGettingStarted = $("#templateCode #albumGettingStarted li.album");
	TemplateCode.albumListView = $("#templateCode #listAlbum li.album");
	TemplateCode.welcomeBack = $("#templateCode #welcomeBack");
	TemplateCode.customCollection = $("#templateCode #customCollection");
	TemplateCode.yesnoquery = $("#templateCode #yesnoquery");
	TemplateCode.inputquery = $("#templateCode #inputquery");
	TemplateCode.inform = $("#templateCode #inform");
	TemplateCode.player = $("#templateCode #player").html();
	TemplateCode.email_tr = $("#templateCode #email_tr tr");
	$("#templateCode").remove();
	
	// setup Model, View & Controller
	Model = new ModelClass();
	Controller = new ControllerClass();
	View = new ViewClass();
	View.initialize();
	Controller.initialize();
	
	// tell Model what the GenreBrowser's model class is
	Model.setGenreBrowserModel(View.discoverNav.genreBrowser.model);
	
	// check cookie if we're logged in, if so, then login
	if($.cookie("authtoken")){
		Model.authenticate({
			type: "return",
			loginName: $.cookie("authname")
		});
		Controller.loginSuccess();
	}
	
	// if given album ID in hash, show album id in main screen WITHOUT logging in
	if(AlbumHashID){
		View.discover.model.getAlbumDetail(AlbumHashID, function(data){
			View.showAlbumPage(data);
		});
	}
	/*
	// temporary work arounds
	var str = URLQueryString("cmd");
	if (str) {
		if(str == "pass"){
			$("#normalLogin").show();
			$("#requestInvite").hide();
		} else if(str == "thankyou"){
			View.inform({
				title: "Got it...",
				content: "You will receive an email invite soon to DiscoverMyMuse.com.  Thanks!",
				w: 320,
				h: 200
			})
		}
	}
	*/
});

/*
 * OOP Extend
 */
var OopExtend = function(subClass, baseClass){
	function inheritance(){
	}
	inheritance.prototype = baseClass.prototype;
	subClass.prototype = new inheritance();
	subClass.prototype.constructor = subClass;
	subClass.baseConstructor = baseClass;
	subClass.superClass = baseClass.prototype;
};


/*
 * Main Model Class
 */
function ModelClass(){
	this.allData = {};
	this.callback = null; // callback used for public getting of items
	this.genreBrowserModel = null; // holds reference to the model of the genre browser
	this.loginName = ""; // hold loginname of authenticated user
	this.numInvites = 0; // number of invites available to this user
	this.authenticated = false;
	this.keep = {}; // holds item ids or other for keep (temporary until we install functionality on app layer)
	this.admin = 0; // admin user? 0 or userId of admin
	this.showAdmin = false; // show admin info
	this.a_repSet = null; // for admin - hold our repset
	this.a_viewUser = null; // for admin - view a particular user's votes
	this.genre = null; // holds latest genre set by one of the genrebrowsers
};

ModelClass.prototype.setGenreBrowserModel = function(obj){
	this.genreBrowserModel = obj;
};

ModelClass.prototype.getCurrentGenre = function(){
	return this.genre;
};

// called from other genre navigation controls to override what's on there
ModelClass.prototype.setCurrentGenre = function(aGenre){
	this.genre = aGenre;
};

// do login -- called from a View control
ModelClass.prototype.login = function(args, success, error, remember){
	// if local, simulate login
	if (LocalFlag) {
		this.authenticate({
			type: "login",
			loginName: args[0].value,
			remember: remember
		});
		success();
	}
	else { // not local, do login action
		var me = this;
		$.ajax({
			url: "/fhws/login",
			data: args,
			cache: false,
			error: function(XMLHttpRequest){
				error(XMLHttpRequest);
			},
			success: function(data, status){
				me.authenticate({
					type: "login",
					loginName: args[0].value,
					remember: remember
				});
				success();
			}
		});
	}
};

ModelClass.prototype.logout = function(callback){
	// if local, simulate logout
	if (LocalFlag) {
		this.authenticate({
			type: "logout"
		});
		callback();
	}
	else { // not local, do logout
		var me = this;
		$.get("/fhws/login?action=logout", function(result1, result2){
			me.authenticate({
				type: "logout"
			});
			callback();
		});
	}
};


// interest - register email
ModelClass.prototype.interest = function(email, success, error){
	// if local, simulate success
	if (LocalFlag) {
		success();
	}
	else {
		var me = this;
		$.ajax({
			url: "/fhws/interest",
			data: { email: email },
			error: function(XMLHttpRequest){
				error(XMLHttpRequest);
			},
			success: function(data, status){
				success();
			}
		});
	}
};

// do login -- called from a View control
ModelClass.prototype.retrievePassword = function(args, success, error){
	var me = this;
	// if local, simulate success
	if (LocalFlag) {
		$.get("localdata/sampleReclaim.xml", args, function(xml){
			me._retrieveResult(xml, success, error);
		});
	}
	else {
		$.get("/fhws/reclaim", args, function(xml){
			me._retrieveResult(xml, success, error);
		});
	}
};

ModelClass.prototype._retrieveResult = function(xml, success, error){
	// in case of local testing where xml is string
	if (typeof(xml) == "string") xml = this.convertXMLString(xml);
	if($("response", xml).eq(0).attr("found") == "1")
		success();
	else
		error();
};

// do register -- called from Controller
ModelClass.prototype.register = function(args, success, error){
	// if local, simulate login
	if (LocalFlag) {
	    _WI.close();
		this.authenticate({
			type: "login",
			loginName: args[0].value
		});
	    Controller.loginSuccess();
	}
	else { // not local, do register action
		var me = this;
		$.ajax({
			url: "/fhws/register",
			data: args,
			error: function(XMLHttpRequest){
				error(XMLHttpRequest);
				View.showServerError(XMLHttpRequest);
			},
			cache: false,
			success: function(xml){
				// Controller.registerSuccess takes care of authenticate call
				success(xml, args);
			}
		});
	}
};

// set appropriate data elements according to authentication
ModelClass.prototype.authenticate = function(args){
	switch (args.type){
		case "login":
			this.loginName = args.loginName
			this.authenticated = true;
			
			// keep logged in for 2 weeks if checkbox selected
			if (args.remember) {
				$.cookie("authname", this.loginName, {
					expires: 14
				});
				$.cookie("authtoken", $.cookie("authtoken"), {
					expires: 14
				});
			} else {
				// remember me for session
				$.cookie("authname", this.loginName);
				$.cookie("authtoken", $.cookie("authtoken"));
			}
			
			break;
			
		case "return":
			this.loginName = args.loginName
			this.authenticated = true;
			break;
			
		case "logout":
			this.authenticated = false;
			$.cookie("authtoken", null);
			$.cookie("authname", null);
			this.admin = 0;
			break;
	}
		
	// admin hatch
	switch (this.loginName){
		case "kim":
			this.admin = 1;
			this.showAdmin = true;
			break;
		case "jakob":
			this.admin = 2;
			this.showAdmin = true;
			break;
		case "gary":
			this.admin = 3;
			this.showAdmin = true;
			break;
	}
	
	// if not admin then remove "Send Invites" for now
	// if(!this.admin) $("#authenticated .sendInvites").parent().remove();

};


// send invites -- called from Controller
ModelClass.prototype.sendInvites = function(args, success, error){
	// if local, simulate send
	if (LocalFlag) {
	    success();
	}
	else { // not local, do send
		var me = this;
		$.ajax({
			url: "/fhws/useradm/invite",
			data: args,
			error: function(XMLHttpRequest){
				error(XMLHttpRequest);
				//View.showServerError(XMLHttpRequest);
			},
			success: function(xml){
				success();
			}
		});
	}
};

ModelClass.prototype.loadUserDetails = function(callback){
	callback = (callback || function(){} );
	
	// if local, simulate load
	if (LocalFlag) {
		this.numInvites = 10;
		callback();
	}
	else { // not local, get details
		var me = this;
		$.get("/fhws/useradm/getdetails", function(xml){
			var details = $("userdetails", xml).eq(0);
			me.loginName = $(details).attr("loginname");
			me.numInvites = parseInt($(details).attr("invitationallowance"));
			callback();
		});
	}
};


// track a page with optional page URL ("/albumDetail") in Google Analytics
ModelClass.prototype.trackPage = function(pageURL){
	if(LocalFlag || Model.admin) return;
	var pageTracker = _gat._getTracker("UA-3717483-1");
	pageTracker._initData();
	pageTracker._trackPageview(pageURL);
};


// segment user in analytics
ModelClass.prototype.segmentUser = function(segmentUser){
	if(LocalFlag || Model.admin) return;
	var pageTracker = _gat._getTracker("UA-3717483-1");
	pageTracker._setVar(segmentUser);
};


// Go to Amazon Product
ModelClass.prototype.amazonProductLink = function(asin){
	return {
		href: 'http://www.amazon.com/gp/product/' + asin +
			'?ie=UTF8&tag=disc09-20&linkCode=as2&camp=1789&creative=9325&creativeASIN=' + asin,
		img: '<img src="http://www.assoc-amazon.com/e/ir?t=disc09-20&l=as2&o=1&a=' + asin + 
			'" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />'
	}
};

/*
<a href="http://www.amazon.com/gp/product/B001EJDF6M?ie=UTF8&tag=disc09-20&linkCode=as2&camp=1789&
	creative=9325&creativeASIN=B001EJDF6M">October Language</a><img src="http://www.assoc-amazon.com
	/e/ir?t=disc09-20&l=as2&o=1&a=B001EJDF6M" width="1" height="1" border="0" alt="" 
	style="border:none !important; margin:0px !important;" />
*/

ModelClass.prototype.a_getRepSet = function(id, callback){
	this.extCallback = callback;
	
	// if local, then just load sample file, else load from server
	if (LocalFlag) {
		var me = this;
		$.ajax({
			url: "localdata/sampleRSet.xml",
			error: function(XMLHttpRequest){
				View.showServerError(XMLHttpRequest);
			},
			cache: false,
			success: function(xml){
				Model.a_parseRepSet(xml);
			}
		});
	}
	else {
		$.ajax({
			url: "/fhws/admin/reputations",
			data: { userid: id },
			error: function(XMLHttpRequest){
				View.showServerError(XMLHttpRequest);
			},
			cache: false,
			success: function(xml, status){
				Model.a_parseRepSet(xml);
			}
		});
	}
};

ModelClass.prototype.a_parseRepSet = function(xml){
	
	// in case of local testing where xml is string
	if (typeof(xml) == "string") 
		xml = this.convertXMLString(xml);
		
	// parse only top n number in repSet
	var reps = [];
	var r = $("reputation", xml);
	r.slice(0,299).each(function(i){
		reps.push({
			id: $(this).attr("userid"),
			value: Math.round(parseFloat($(this).attr("value")) * 100) / 100 // round to 2 decimals
		});
	});
	
	// show admin notice
	$("#adminNotice").remove();
	$('<span id="adminNotice"><a href="javascript:void(0)">Toggle Admin</a> | repSet: '+ r.length + '</span>')
		.find("a")
			.click(function(){
				Model.showAdmin = !Model.showAdmin;
				View.display.refresh();
			}).end().appendTo("body");
	this.extCallback(reps);
};


// Model - in case of local file system testing, parse a string of xml data into proper xml object
ModelClass.prototype.convertXMLString = function(s){
	var doc;
	if (window.ActiveXObject) {
		doc = new ActiveXObject('Microsoft.XMLDOM');
		doc.async = 'false';
		doc.loadXML(s);
	}
	else {
		doc = (new DOMParser()).parseFromString(s, 'text/xml');
	}
	return doc;
	// return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null;
};

/*
 * Keep function -- used for generic persistent storage of string items for this user
 */
ModelClass.prototype.getKeep = function(label, callback){
    var me = this;
    this._loadKeepCheck(label, function(){
        callback(me.keep[label].data);
    });
};

ModelClass.prototype.setKeep = function(label, list){
    var me = this;
    this._loadKeepCheck(label, function(){
        me.keep[label].set(list);
    });
};

// simply unload the keeps (for logout, etc.)
ModelClass.prototype.unloadKeeps = function(){
    this.keep = {};
};

ModelClass.prototype.clearKeep = function(label){
    var me = this;
    this._loadKeepCheck(label, function(){
        me.keep[label].clear();
    });
};

ModelClass.prototype.addToKeep = function(label, str){
    var me = this;
    this._loadKeepCheck(label, function(){
        me.keep[label].addItem(str);
    });
};

// requires keep to be preloaded
ModelClass.prototype.existsInKeep = function(label, str){
    var me = this;
    return me.keep[label].itemExists(str);
};

ModelClass.prototype.removeFromKeep = function(label, str){
    var me = this;
    this._loadKeepCheck(label, function(){
        me.keep[label].removeItem(str);
    });
};

// remove keep completely
ModelClass.prototype.removeKeep = function(label){
	if(this.keep[label]){
		this.keep[label].remove();
		delete this.keep[label];
	}
};

ModelClass.prototype._loadKeepCheck = function(label, callback){
	if (!this.keep[label]) {
		this.keep[label] = new ItemsKeep(label);
		this.keep[label].load(callback);
	} else {
		callback();
	}
};

/*
 * Data Store for storing & retrieving JSON under a particular label
 */
ModelClass.prototype.saveJSON = function(label, data){
	var json = JSON.stringify(data);
	
	if (LocalFlag) {
		label += "_" + this.loginName;
		$.cookie(label, json, {
			expires: 365
		});
	}
	else {

		$.ajax({
			url: "/fhws/setuserdata",
			data: {
				key: label,
				value: json
			},
			dataType: "text",
			error: function(XMLHttpRequest){
				View.showServerError(XMLHttpRequest);
			},
			success: function(){
			}
		});
		
	}
};


ModelClass.prototype.loadJSON = function(label, callback){
	if (LocalFlag) {
		label += "_" + this.loginName;
		this._finishLoadJSON($.cookie(label), callback);
	} else {
		
		var me = this;
		$.ajax({
			url: "/fhws/getuserdata",
			data: {
				key: label
			},
			cache: false,
			dataType: "json",
			error: function(XMLHttpRequest) {
				// skip error display to skirt login breakdown on error
				me._finishLoadJSON(null, callback);
				// View.showServerError(XMLHttpRequest);
			},
			success: function(json){
				// spaces were escaped when sending, so unescape
				me._finishLoadJSON(json, callback);
			}
		});
	}
};

ModelClass.prototype._finishLoadJSON = function(json, callback){
	if(json){
		if(typeof(json) == "string") json = JSON.parse(json); // .replace(/\+/g, " ")
		callback(json);
	} else {
		callback(); // callback with NULL
	}
};

ModelClass.prototype.clearSavedData = function(label){
	if (LocalFlag) {
		label += "_" + this.loginName;
		$.cookie(label, null);
	}
	else {
		$.ajax({
			url: "/fhws/setuserdata",
			data: {
				key: label
			},
			error: function(XMLHttpRequest){
				View.showServerError(XMLHttpRequest);
			},
			success: function(){
			}
		});
	}
};



/*
 * Main View Class
 *
 */
function ViewClass(){
	this.audioLauncher = null; // will hold a window used for launching audio player (temp until direct mp3 streaming)
	this.tooltip;
	this.context = "music";
	this.notWin = (navigator.appVersion.indexOf("Win")==-1); // 1 for not windows, 0 for windows - used for music playback
	this.customTabs; // holds _TabManager instance for managing custom tabs
	this.display = null; // holds reference to the currently displayed album grid view
	this.discover = null;
	this.myVotes = null;
	this.search = null;
	this.collection = null;
	this.starTooltips;
	this.lalaPlayerWin = null;
	this.discoverNav = null;
};


ViewClass.prototype.initialize = function(){
	
	// lightbox for start page
	$("#previewArea ul a").lightBox({ overlayOpacity: 0.5 });
	
	// set up main tabs
	$(".subnav > ul").tabs({
		select: function(blank, ui){ Controller.tabSwitch(ui); },
		show: function(blank, ui){ Controller.tabShown(ui); }
		// fxFade: true
	});
	this.hideSearchTab();
	this.hideOtherVotesTab();
	
	// set up side panel tabs
	$("#panelTabs ul").tabs({
		select: function(blank, ui){
			// make sure any highlighted tabs are back to white after clicking on them
			// $(ui.tab).parent().css({ backgroundColor: "#ffffff" });
			
			// if switching to genre browser, set the genre to what's shown
			if($(ui.panel).attr("id") == "genreBrowser"){
				Model.setCurrentGenre(Model.genreBrowserModel.currentGenre);
			}
			
			// remember this selection between sessions
			Controller.setDefault("genreTab", $(ui.panel).attr("id"));
		}
	});
	
	// setup getting started tabs
	// $("#gstarted-tabs").tabs();
	
	// send invites link
	$("#authenticated .sendInvites").click(function(){
		View.showInviter();
	});
	
	// feedback button
	$(".account .feedback").click(function(){
		document.location.href="Mail" + "To:" + "service" + "@discovermy.net";
	});
	
	$(".account .aboutUs").click(function(){
		View.showAbout();
	});
	
	$("#srinstr a").click(function(){
		View.showAbout();
	})
	
	// login controls
	$("#login .action").removeAttr("disabled").click(function(){
		// disable login button while attempting
		$("#login .action").attr("disabled", "disabled");
		$("#login .error").hide();
		
		// send login args to controller for login attempt
		var args = $("#login input").serializeArray();
		var rememberFlag = $("#rememberMe input").attr("checked");
		args[0].value = $.trim(args[0].value); // trim loginname
		Controller.login(args, rememberFlag);
		return false;
	});
	
	$("#login input").keypress(function(event){ // catch ENTER key as a submit
		if ((event.which || event.keyCode) == 13) $("#login .action").click();
	}).filter("#loginname").focus();
	$("#forgotPassword").click(function(){
		View.inputQuery({
			title: "Retrieve Password",
			content: "Please enter the email that you signed up with",
			defaultValue: $("#login input").val(),
			actionName: "Send",
			label: "Email:",
			closeOnSubmit: false,
			callback: function(email){
				email = email.toLowerCase();
				Model.retrievePassword({ loginname: email },
					function(){ // success function
						_WI.close();
						View.inform({
							content: "Your password has been emailed.",
							w: 320,
							h: 200
						});
					}, function(){ // not found function
						$("#inputquery .error").empty();
						setTimeout(function(){
							$("#inputquery .error").html("That email is not registered.  Please try again");
						}, 50);
					}
				);
			}
		});
	});
	
	
	// interest control
	var iValidate = $("#interest form").validate({
        rules: { interestName: { required: true, email: true }},
        submitHandler: function(){
			$("#interest .message").hide();
			
            // pass validator for a custom show error call if interest fails
			Controller.doInterest($("#interestName").val(), iValidate, $("#interest"));
        },
        // do custom placement of errors for terms of service link
        errorPlacement: function(error, element){
			$("#interest .action").after(error);
		},
		wrapper: "p"
	});
	
	$(".account .logout").click(function(){
		Controller.logout();
		return false;
	});
	$("#passwd").focus(function(){ 
		$(this).select(); // select full password on focus
	});
		
	// register/join control
	$(".register").click(function(){
		View.showRegistration();
	});
	
	// create generic tooltip object for rating
	this.tooltip = $("<div></div>").appendTo($("body")).hide().css({
		position: 'absolute',
		backgroundColor: '#ffd',
		border: 'solid 1px #999',
		color: '#444',
		fontSize: '90%',
		padding: '0 2px',
		zIndex: 3000
	});
		
	// Search Input field
	$("#searchInput").val("artist or album") // default field copy
		.filterKeys("-' !:") // filter all special characters out except for these
		.focus(function(){ // set focus behavior
			var field = $("#searchInput").select();
			if (field.val() == "artist or album") 
				field.val("");
		}).keypress(function(event){ // catch ENTER key as a submit
			if ((event.which || event.keyCode) == 13) {
				Controller.search();
			}
		});
	
	// Search button
	$("#itemSearch input[type=submit]").click(function(){
		Controller.search();
	});
	
	// Multi-Search Link
	$("#multiSearch").click(function(){
		Controller.showMultiSearch();
	});
	
	// set up custom tab manager and setup "add tab" button
	this.customTabs = new TabManager();
	var me = this;
	$(".subnav > ul").append($('<li id="addTab"><a href="javascript:void(0)">+</a></li>').click(function(){
		me.customTabs.queryNewTab(); // <img class="spacer" src="images/spacer.gif" alt="">
	}));
	// if($.browser.mozilla) $("#addTab").css("top", "1px");
	
	// setup tooltip tags used for star rating control
	this.starTooltipContent = ["Horrible", "Bad", "Quite Bad", "Below Average", "Almost OK", "Just OK",
			"Decent", "Good", "Very Good", "Great"];
	
	// set getting started action
	$("#gs_enterFavs .action").click(function(){
		Controller.gs_doMultiSearch({
			searchStr: $("#gettingStarted textArea").val(),
			exactMatch: false
		});
	});
	$("#gs_enterFavs").show();
	
	// setup instruction tip close control
	$(".instructions img").click(function(e){
		var tip = $(e.target).parent().hide(500);
		Controller.rememberDismissal(tip.attr("id"));
	});
	
	// if IE, recommend upgrading browser
	if($.browser.msie){
		$("#chrome").show();
	}
};

/*
 * Setup Discover View
 */
ViewClass.prototype.setupDiscoverView = function(){
	
	// set model & contentArea to be used by views
	var model = new DiscoverModel();
	var contentArea = $("#discover .contentArea")
	
	// setup side nav
	this.discoverNav = new DiscoverNav(model, contentArea);
	
	// setup two album views for discover
	this.discoverGridView = new DiscoverGridView(model, contentArea, this.discoverNav.pager);
	this.discoverListView = new DiscoverListView(model, contentArea, this.discoverNav.pager);
	
	// set default view: first remove model observeration for non active view
	View.discoverGridView.model.removeObserver(View.discoverGridView);
	View.discover = this.discoverListView;
}

/*
 * Setup My Votes view
 */
ViewClass.prototype.setupMyVotesView = function(){
	
	// set model & contentArea to be used by views
	var model = new MyVotesModel();
	var contentArea = $("#myVotes .contentArea");
	
	// setup side nav
	this.myVotesNav = new MyVotesNav(model, contentArea);
	
	// setup two album views for MyVotes
	View.myVotesGridView = new MyVotesGridView(model, contentArea, this.myVotesNav.pager);
	View.myVotesListView = new MyVotesListView(model, contentArea, this.myVotesNav.pager);
	
	// set default view: first remove model observeration for non active view
	View.myVotesGridView.model.removeObserver(View.myVotesGridView);
	View.myVotes = this.myVotesListView;
}


/*
 * Setup Other Votes view
 */
ViewClass.prototype.setupOtherVotesView = function(){
	var contentArea = $("#otherVotes .contentArea")
	
	// set model to be used by views
	var model = new UserVotesModel();
	
	// setup side nav
	this.otherVotesNav = new UserVotesNav(model, contentArea);
	
	// setup singular view for Other Votes
	View.otherVotes = new UserVotesView(model, contentArea, this.otherVotesNav.pager);
}

// hide home & show signed in content
// this is done separately to allow tab refreshes to be done behind the scenes
ViewClass.prototype.showMain = function(){
	$("#music").show();
	$("#home").hide();
};


// Create & display the album detail page
ViewClass.prototype.showAlbumPage = function(data, div){
	// get albumPage template
	var albumPage = TemplateCode.albumPage.clone();
	
	// latch on lala preview link
	var me = this;
	if (data.lalaId) {
		me.setLaLaPlayerLink(data, albumPage);
	}
	else {
		me.display.model.getLaLaData(data, function(){
			me.setLaLaPlayerLink(data, albumPage);
		});
	}
	
	// create a new album page from album detail
	albumPage.windowit({
		w: 940,
		h: 720,
		title: data.artist + ": " + data.title,
		callback: function(){
			SetURLHashString();
		}
	});
	
	// set URL hash to item id to allow bookmarking and email to:
	SetURLHashString("id" + data.id);
	
	// give windowit a chance to refresh before completing album display
	setTimeout(function(){
		me._finishAlbumPage(data, albumPage);
	}, 10);
};


ViewClass.prototype._finishAlbumPage = function(data, albumPage){
	
	// consider this the Hovered Album for music player control
	HoveredAlbum = albumPage;
	
	// set the album id
	albumPage.attr("id", data.id);
	
	// create audio player
	View.display.createPlayer(albumPage, $(".flashContent", albumPage).get(0), data.asin);
	
	// manually remove player for IE to stop audio by extending generic window close
	if ($.browser.msie) {
		var fnc = _WI.close;
		_WI.close = function(){
			$(".flashContent", albumPage).children().remove();
			fnc();
		}
	}
	
	// set email friend link
	$(".emailFriend a:eq(0)", albumPage).attr("href", "mail" + "to:?subject=I%20recommend%20%22" + data.title + 
		"%22%20by%20" + data.artist + "&body=Hi,%20I%20thought%20you'd%20like%20%22" + data.title + 
		"%22%20by%20" + data.artist + ".%0AYou%20can%20listen%20at%3A%20http://www.discovermymuse.com/%23id" + 
		data.id + "%0AFound%20with%20DiscoverMyMuse.com.")
		.attr("title", "You can also copy and paste this page's URL");
		
	// set "link to" url & display action
	$(".emailFriend div input", albumPage)
		.attr("value", "http://www.discovermymuse.com/#id" + data.id);
	$(".emailFriend a:eq(1)", albumPage).click(function(){
		$(".emailFriend div", albumPage).toggle(300, function(){
			$(".emailFriend div input", albumPage).select();
		});
	});
	
	// add editorial expand code
	$(".editorialReview h3 a", albumPage).click(function(){
		$(".editorialReview ul li")
			.toggleClass("collapsed")
			.toggleClass("expanded");
		$(".editorialReview p").toggle();
		$(".editorialReview h5").toggle();
	});
	
	// set artist and do search on artist name on click
	$(".artist a", albumPage).html(data.artist).click(function(){
		_WI.close(300); // close detail window
		Controller.search(data.artist);
	});
	
	// set title
	$(".title", albumPage).html(data.title);
	
	// set art & click action
	$(".albumArt", albumPage)
		.attr("href", data.largeArt.URL)
		.lightBox({ overlayOpacity: 0.5 });
	$(".albumArt img", albumPage)
		.attr("src", data.largeArt.URL)
		.width(250).height(250);
	
	// Set Amazon Download link
	var link;
	if (data.binding.indexOf("Download") + 1){
		link = Model.amazonProductLink(data.asin);
		$(".amazon a.mp3", albumPage).attr({
			href: link.href,
			target: "_BLANK"
		}).after($(link.img));
	} else $(".amazon a.mp3", albumPage).hide();
	
	// Set Amazon album link
	if(data.cdasin){
		link = Model.amazonProductLink(data.cdasin);	
		$(".amazon a.cd", albumPage).attr({
			href: link.href,
			target: "_BLANK"
		}).after($(link.img));
	} else $(".amazon .cd", albumPage).hide();
	
	// create graphical star rating
	var id = data.id;
	$(".starRating", albumPage).srating({
		numSteps: 10,
		numStars: 5,
		rating: data.vote,
		tooltip: View.tooltip,
		tips: View.starTooltipContent,
		callback: function(el, vote){
			Controller.userVote(id, vote);
		}
	});
	
	// set folder icon action
	$(".foldericon", albumPage).click(function(e){
		View.customTabs.folderMenu(albumPage, data, {x: e.pageX, y: e.pageY});
		return false;
	});
	
	// set extra info
	$(".format", albumPage).html(data.format.join());
	if(data.origReleased) $(".origReleased", albumPage).html(View.dateString(data.origReleased));
	else if (data.released) $(".origReleased", albumPage).html(View.dateString(data.released));
	else $(".origReleased", albumPage).parent().remove();
	$(".binding", albumPage).html(data.binding);
	$(".label", albumPage).html(data.label);
	// remove any empty entries
	if(data.format.length == 0) $(".format", albumPage).parent().remove();
	
	// set editorials
	if (!data.editorials.length) {
		$(".editorialReview", albumPage).hide();
	}
	else {
		$.each(data.editorials, function(i){
			$(".editorialReview", albumPage).append($('<h5>'+ this.source +'</h5>'));
			$(".editorialReview", albumPage).append($('<p class="content">'+ this.content +'</p>'));
		});
		$(".editorialReview", albumPage).show();
	}
	
	// set user reviews
	if (data.reviews) {
		var aReview;
		for (var i = 0; i < data.reviews.length; i++) {
			aReview = this.createReview(data.id, data.reviews[i]);
			$("ol.customerReviews", albumPage).append(aReview);
		}
	}
	// var aDate = new Date(data.releaseDate[0], data.releaseDate[1], data.releaseDate[2]);
	
	// set tracks
	var aTrack;
	for (var i = 0; i < data.discs.length; i++) {
		var iData = data.discs[i];
		for (var j = 0; j < iData.length; j++) {
			$(".tracks ol", albumPage).append('<li>'+ iData[j] +'</li>');
		}
	}
	if ($(".tracks li", albumPage).length == 0) $(".tracks", albumPage).remove();
	
	// collect full genre info
	var li, name;
	var genres = [];
	$.each(data.genreIds, function(i, gid) {
		var genre = Model.genreBrowserModel.getGenre(gid);
		if (genre.name) genres.push(genre);
	});
	
	// sort list by names
	genres.sort(function(a, b){
		return (a.name > b.name);
	});
	
	// apply sorted genres to album display
	$.each(genres, function(i, genre){
		if((genre.name != "All Genres") && (genre.name.slice(-7) != "General")){
			li = $('<li><a href="javascript:void(0)">'+ genre.name +'</a></li>')
				.data("id", genre.id)
				.click(function() {
					_WI.close();
					Controller.discoverAGenre(genre);
				});
			$(".genreInfo ul", albumPage).append(li);
		}
	});
	
	// ((i < data.genreIds.length - 1) ? "," : ""));
	
	//album.animate({left:a.offset().left, top:a.offset().top}, 1000);
	return albumPage;
};


ViewClass.prototype.createReview = function(id, reviewData){
	var aReview = $('<li class="review"><h2 class="reviewerName"></h2><div class="reviewerStars"></div>' +
		'<h2 class="summary"><span class="reviewDate"></span></h2><div class="content"></div></li>').attr("id", id);
	
	// make stars
	for (var j = 0; j < 5; j++) {
		var img = (j < reviewData.rating) ? 'reviewerStar.gif' : 'reviewerNoStar.gif'
		$(".reviewerStars", aReview).append('<img src="images/' + img + '" alt="*" />');
	}
	
	
	
	// set Reviewer name
	$(".reviewerName", aReview).html(reviewData.reviewerName);
	/*
	$(".reviewerName", aReview).html('<a href="javascript:void(0)">'+ reviewData.reviewerName +'</a>')
		.bind("click", {
			id: reviewData.userId,
			name: reviewData.reviewerName
		}, function(e){
			_WI.close(300); // close detail window
			View.triggerTab("otherVotes");
			Controller.otherVotes({ name: e.data.name, id: e.data.id });
		});
	*/
	
	$(".summary", aReview).html(reviewData.summary);
	$(".reviewDate", aReview).html(reviewData.date);
	$(".content", aReview).html(reviewData.content);
	
	return aReview;
};


// convert date string to human readable date without time or day name
ViewClass.prototype.dateString = function(str){
	str = (str || "").replace(/-/g,"/").replace(/[TZ]/g," ");
	if(str.split("/").length == 2) str+= "/01"; // add day if none given
	var date = new Date(str);
	var arr = date.toLocaleDateString().split(",");
	arr.shift();
	return arr.join();
}


// add the lala player link to an album page
ViewClass.prototype.setLaLaPlayerLink = function(albumData, albumPage){
	// add lala launcher code, if lalaId available
	if(albumData.lalaId && (albumData.lalaId != -1)){
		if(!albumData.lalaFullStream) $(".lala button", albumPage).addClass("preview").html("Preview tracks");
		$(".lala button", albumPage).click(function(){
			// first stop any playing albums either on album list or in detail page
			View.display.stopMusicPlayer();
			View.display.stopPlayerInAlbumPage();
			
			// if safari/chrome, close any current lala window to force focus on it when recreated
			if ($.browser.safari){
				View.stopLalaPlayer();
			}
			
			// now launch new window and set focus
			var url = 'http://www.lala.com/external/flash/PlaylistWidget.swf?hideNewWindow=true&' + 
				'host=www.lala.com&autoPlay=true&albumId=' + albumData.lalaId;
			View.lalaPlayerWin = window.open(url, 'lalaPlayer', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=1,width=300,height=280');
			if (window.focus) View.lalaPlayerWin.focus();
			
			return false;
		});
		$(".lala .loading", albumPage).remove();
		$(".lala .player", albumPage).show();
		
	} else {
		$(".lala .loading", albumPage).fadeOut(500);
		$(".lala .player", albumPage).hide();
	}
	
};

// Get yes/cancel input from user (optional buttonName will set the action button text
ViewClass.prototype.yesNoQuery = function(options){
	// default options
	options = jQuery.extend({
		title: "Window",
		content: "Are you sure?",
		buttonName: "Yes",
		callback: function(str){}
	}, options);
	
	var query = TemplateCode.yesnoquery.clone();
	query.find(".content").html(options.content).end()
		.find(".cancel").click(function(){
			_WI.close(300);
		}).end()
		.find(".action").html(options.buttonName).click(function(){
			_WI.close(300);
			options.callback();
		}).end()
		.windowit({
			title: options.title,
			w: 280,
			h: 200,
			modal: true
		});
};

// get text input from user. "content" & "callback" are required.
// fValidation is a validation function that takes a str and returns true if valid or false if invalid
// validationRules is a str (of html) that states the validation rules to the user
// inputFilter is allowed characters for the filterKeys plugin.  An empty string means JUST alphanumeric characters
// defaultValue is any default value that is within the input entry
ViewClass.prototype.inputQuery = function(options) {
	// default options
	options = jQuery.extend({
		title: "Window",
		content: "Please enter below",
		callback: function(str){},
		inputFilter: null,
		fValidation: null,
		label: "",
		validationRules: null,
		actionName: "Enter",
		defaultValue: "",
		w: 360,
		h: 240,
		closeOnSubmit: true
	}, options);
	
	var query = TemplateCode.inputquery.clone();
	if (options.inputFilter || options.inputFilter == "") $("input", query).filterKeys(options.inputFilter);
	$("input", query).val(options.defaultValue)
	$("label", query).html(options.label);
	
	// define submit action
	var fAction = function(){
		var str = $("input", query).val();
		if(!options.fValidation || options.fValidation(str)){
			if(options.closeOnSubmit) _WI.close(300);
			options.callback(str);
		} else {
			$(".error", query).html(options.validationRules || "Incorrect entry.  Try again.");
			$(".input", query).select();
		}
	}
	
	// fill in content & behavior
	$(".content", query).html(options.content);
	$(".cancel", query).click(function(){
			_WI.close(300);
		});
	$(".action", query).html(options.actionName).click(fAction);
	query.windowit({
			title: options.title,
			w: options.w,
			h: options.h,
			modal:true
		});
	$("input", query).keypress(function(event){ // catch ENTER key as a submit
		if ((event.which || event.keyCode) == 13) fAction();
	}).focus();
};

// Simply inform user with a simple message
ViewClass.prototype.inform = function(options){
	// default options
	options = jQuery.extend({
		title: "Window",
		content: "<No content set>",
		buttonName: "Close",
		callback: function(str){},
		w: 420,
		h: 300
	}, options);
	
	var query = TemplateCode.inform.clone();
	query.find(".content").html(options.content).end()
		.find(".action").html(options.buttonName).click(function(){
			_WI.close(300);
		}).end()
		.windowit({
			title: options.title,
			w: options.w,
			h: options.h,
			modal: true,
			callback: options.callback
		});
};

// just show loading indicator
ViewClass.prototype.showLoading = function(html){
	$('<div id="loading"><p></p><img src="images/ajax-loader.gif"/>' +
	  '<img class="close" src="images/tabclose.gif" alt="Cancel"></div>')
		.appendTo("body")
		.find("p").html(html || "Retrieving results").end()
		.find(".close").click(function(){
			Controller.abortLoad();
		}).end()
		.css({
			left: $(window).width() / 2 - 120,
			top: $(window).height() / 2 - 25
		});
};


// block UI and show loading indicator
ViewClass.prototype.blockAndShowLoading = function(html){
	this.blockUI();
	this.showLoading(html);
};


// hide loading indicator
ViewClass.prototype.hideLoading = function(){
	$("#loading").remove();
	$("#WI_overlay").remove(); // if it was created
};


// shown an arbitrary content file in an DHTML window
ViewClass.prototype.showContent = function(options){
	
	options = jQuery.extend({
		filename: "aboutUs.html",
		selector: "",
		w: 520,
		h: 380,
		callback: function(str){}
	}, options);
	
	Model.trackPage("showContent/" + options.filename);
	
	var div = $("<div></div>")
		.windowit(options)
		.load("/content/" + options.filename + " " + options.selector, function(){
			options.callback(div);
		});
};


ViewClass.prototype.showRegistration = function(){
    var div = $("<div></div>").windowit({
        w: 540,
        h: 490,
		modal: true,
		forceSize: true,
		title: "Register"
    }).load("/content/register.html .formContent", function(){
		// set validation rules
        var validator = $("form", div).validate({
            rules: {
                loginname: {
                    required: true,
                    email: true
                },
                passwd: {
                    required: true,
                    minLength: 6
                },
                cpasswd: {
                    required: true,
                    minlength: 6,
                    equalTo: "#passwd"
                },
				recaptcha_response_field: "required",
                eulaaccepted: "required"
			},
			submitHandler: function() {
				// pass validator for a custom show error call if register fails
				Controller.register(validator);
			},
            // do custom placement of errors for terms of service link
            errorPlacement: function(error, element){
                if ($(element).attr("id") == "eulaaccepted") 
                    $(element).next().after(error);
                else if ($(element).attr("id") == "recaptcha_response_field") 
                    $("#recaptcha_div").after(error);
				else
                    element.after(error);
            }
        });
		
		// Show Recaptcha
		View.showRecaptcha();
		
		// select full password on focus
		$("#passwd", div).focus(function(){
			$(this).select();
		});
		
		// set cancel  action
        $(".cancel", div).click(function(){
            _WI.close();
        });
		
		// set focus to first field
        $("#username", div).focus();
    });
};

ViewClass.prototype.showRecaptcha = function(callback){
	if(!callback) callback = function(){};
	
	// if recaptcha existing, destroy it first
	if (typeof Recaptcha == 'object') {
		Recaptcha.destroy();
	}
	
	// if we haven't loaded the library load it first
	if (typeof RecaptchaTemplates == 'undefined') {
		$.getScript('http://api.recaptcha.net/js/recaptcha_ajax.js', function(){
			View.createRecaptcha(callback);
		});
	} else {
		View.createRecaptcha(callback);
	}
};

ViewClass.prototype.createRecaptcha = function(callback){
	Recaptcha.create("6LcFYAcAAAAAADH9k3_Y_zo6YOAd2gBO0D_YdvJw", "recaptcha_div", {
		theme: "white",
		callback: callback,
		tabindex: 9
	});
}

ViewClass.prototype.showInviter = function(){
    var div = $("<div></div>").windowit({
        w: 550,
        h: 440,
		modal: true
    }).load("/content/invite.html .copyContent", function(){
		// set validation rules
		var validator = $("#inviteForm").validate({
            rules: {
                invitees: {
                    required: true
                }
            },
			submitHandler: function() {
				// pass validator for a custom show error call if invite error
				Controller.sendInvites(validator);
			}
        });
		$("#emailsInput").filterKeys(".@_-, ");
		
		// set num invites left display
		$("#numInvites", div).html(Model.numInvites);
		
		// set cancel  action
        $(".cancel", div).click(function(){
            _WI.close();
        });
		
		// limit to 512 characters and show remaining characters
		$("#messageInput", div).keyup(function(){
			var max = 512;
			var len = Math.max(max - $(this).val().length, 0);
			$(".numLeft", div).html(len + ((len == 1) ? " character left" : " characters left"));
			if(len < 1){
				$("#messageInput", div).val($(this).val().substr(0,max));
			}
		});
		
		// set focus to first field
        $("#emailsInput", div).focus();
    }); 
};


ViewClass.prototype.showServerError = function(XMLHttpRequest){
    this.hideLoading();
	
	View.inform({
		content: $(XMLHttpRequest.responseText)
	});
	
	/*
    if (XMLHttpRequest.responseXML) {
		alert("Server Error:" + $(XMLHttpRequest.responseXML).text());
	}
	else {
		View.inform({
			content: $(XMLHttpRequest.responseText)
		});
	}
	*/
};


ViewClass.prototype.resetForm = function(){
	// $(".message", formDiv).hide();
	$(".error", formDiv).hide();
	$(".action", formDiv).removeAttr("disabled");
};

ViewClass.prototype.showFormMessage = function(formDiv, html){
	// $(".error", formDiv).hide();
	$(".action", formDiv).removeAttr("disabled");
	$(".message", formDiv).html(html).show();
}

ViewClass.prototype.showFormError = function(formDiv, html){
	// $(".message", formDiv).hide();
	$(".action", formDiv).removeAttr("disabled");
	$(".error", formDiv).html(html).show();
}

ViewClass.prototype.blockUI = function(){
	if($("#WI_overlay").length) return;
	
	// apply overlay
	$("body", "html").css({
		height: "100%",
		width: "100%"
	});// IE 6 fix
	
	// apply overlay
	$(document.body).append($('<div id="WI_overlay"></div>'));
	
	// if IE6 then set position of overlay to absolute instead of fixed
	if ($.browser.msie) {
		var n = (navigator.appVersion).indexOf("MSIE") + 5;
		var version = parseInt((navigator.appVersion).substr(n, n + 3));
		if (version == 6) 
			$("#WI_overlay").css("position", "absolute");
	}
};

// should be in controller
ViewClass.prototype.playAudio = function(asin, disc, track){
	var url = "http://www.amazon.com/exec/obidos/clipserve/" +
		asin +
		this.prefixZeros(disc, 3) +
		this.prefixZeros(track, 3) +
		"/" + this.notWin; // not windows (1) triggers Real playback, windows (0) triggers Windows Media Player
	this.audioLauncher = window.open(url, "newWin", "toolbar=0,menubar=0,resizable=1,width=10,height=10,location=0");
	// this.audioLauncher.moveTo(0, 0);
	setTimeout('View.closeAudioLauncher()', 3000);
};


// close the audio launcher window -- temp fix
ViewClass.prototype.closeAudioLauncher = function(){
	this.audioLauncher.close();
};


ViewClass.prototype.stopLalaPlayer = function(){
	// if lala player playing, close player window
	if (this.lalaPlayerWin != null) {
		this.lalaPlayerWin.close();
		this.lalaPlayerWin = null;
	}
};


ViewClass.prototype.embedLalaPlayer = function(data){
	$("#lalaPlayer").remove();
	this.lala =
		$('<div id="lalaPlayer" <object type="application/x-shockwave-flash" data="http://www.lala.com/external/flash/PlaylistWidget.swf" ' + 
		'id="lalaAlbumEmbed" width="180" height="254"><param name="movie" value="http://www.lala.com/external/flash/' +
		'PlaylistWidget.swf"/><param name="wmode" value="transparent"/><param name="allowNetworking" value="all"/> ' +
		'<param name="allowScriptAccess" value="always"/><param name="flashvars" value="albumId=' + data.lalaId +
		'&host=www.lala.com&partnerId=memberAffiliate.null"/><embed id="lalaAlbumEmbed" name="lalaAlbumEmbed" ' +
		'src="http://www.lala.com/external/flash/PlaylistWidget.swf" width="180" height="254" type="application/' +
		'x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" wmode="transparent" ' +
		'allowNetworking="all" allowScriptAccess="always" flashvars="albumId=' + data.lalaId +
		'&host=www.lala.com&partnerId=memberAffiliate.null"></embed></object><div style="font-size: 9px; ' +
		'margin-top: 2px;"><a href="http://www.lala.com/album/' + data.lalaId +'" title="' + data.title + ' - ' +
		data.artist + '">' + data.title + ' - ' + data.artist + '</a></div></div>');
	$("body").append(this.lala);
};


// scroll the screen to y location in ms milliseconds
ViewClass.prototype.scrollTo = function(y, ms){
	$('html,body').animate({ scrollTop: y }, ms);
};

ViewClass.prototype.prefixZeros = function(num, numDigits){
	var str = num.toString();
	for (var i = str.length; i < numDigits; i++) {
		str = "0" + str;
	}
	return str;
};


// manually trigger the search results tab to show and come to focus *** todo avoid hardwired numbers of tabs
ViewClass.prototype.triggerTab = function(tabName){
	if (tabName != View.display.name) {
		$(".subnav > ul").tabs("select", "#" + tabName);
	}
};

// manually set the genre tab
ViewClass.prototype.setGenreTab = function(panelIDorIndex){
	$("#panelTabs ul").tabs("select", panelIDorIndex);
};


// show the search results tab
ViewClass.prototype.showSearchTab = function(){
	$("#searchResultsTab").show();
	// $("#itemSearch").css("left", "450px");
};

// hide the search results tab
ViewClass.prototype.hideSearchTab = function(){
	$("#searchResultsTab").hide();
};


// show the other votes tab
ViewClass.prototype.showOtherVotesTab = function(userId){
	$("#otherVotesTab").show().find("a").html(userId);
};

// hide other votes tab
ViewClass.prototype.hideOtherVotesTab = function(){
	$("#otherVotesTab").hide();
};


ViewClass.prototype.flashMyVotes = function(){
	this.flashTab($("li#myVotesTab"));
};

// flash a tab
ViewClass.prototype.flashTab = function(tabElement){
	/*
	var loc = tabElement.offset();
	var hilite = tabElement.clone();
	tabElement.parent().append(hilite);
	hilite.css({
		position: "absolute",
		left: loc.left,
		top: loc.top,
		backgroundColor: "#FF9800"
	}); //.fadeOut(1200, function(){ hilite.remove(); });
	*/
	
	/*
	var clr = tabElement.css("background-color");
	tabElement.css("background-color", "#ccc");
	setTimeout(function(){ tabElement.css("background-color", clr); }, 300);
	*/
	
	/*
	var clr = $("a", tabElement).css("color");
	$("a", tabElement)
		.css({ color: "#FF9800" })
		.animate({ color: clr }, 1200);
	*/
};

// show about
ViewClass.prototype.showAbout = function(){
	this.showContent({
		filename: "aboutUs.html",
		selector: ".copyContent",
		title: "About DiscoverMy Technology",
		w: 660,
		h: 500
	});
};


// create admin link for showing code
ViewClass.prototype.createLauncherLink = function(){
	// setup launcher page
	if (!Model.admin) return;
	$("popPageLink").remove();
	var popPageLink = $('<small id="popPageLink" style="position:absolute; right:10px"><a href="#">[SHOW LIST CODE]</a></small>');
	$(".pager", View.display.contentArea).append(popPageLink.click(function(){
		// grab all albums in current album list
		// var newList = $("<ol></ol>");
		var table = $("<table></table>");
		$(".album", View.display.contentArea).each(function(i){
			
			// get album data
			var album = $(this);
			var id = album.data("id");
			var data = View.display.model.albumData(id);
			
			// debug
			if (LocalFlag) data.lalaId = "1657606138220511464";
			
			// Attach it have a valid lala ID?
			var tr;
			if (data.lalaId && (data.lalaId != -1)) {
				tr = $(TemplateCode.email_tr).clone();
				$(".albumArt", tr).attr("src", $(".albumArt", album).attr("src"));
				$(".artist a", tr).html($(".artist a", album).html());
				$(".title a", tr).html($(".title", album).html());
				// $(".title", tr).height($(".title", album).height);
				if($(".title a", tr).text().length > 19) $(".title", tr).height("2.2em");
				$(".reviewerStars", tr).html($(".reviewerStars", album).html());
				$(".reviewerStars img", tr).attr("src", "images/reviewerStar.gif");
				$(".review .summary", tr).html($(".review .summary", album).html());
				$(".review .reviewerName", tr).html($(".review .reviewerName", album).html());
				$(".review .reviewDate", tr).html($(".review .reviewDate", album).html());
				$(".review .content a", tr).html($(".review .content", album).html());
				
				// attach lala link
				var url = 'http://www.lala.com/external/flash/PlaylistWidget.swf?hideNewWindow=true&' +
					'host=www.lala.com&autoPlay=true&albumId=' + data.lalaId;
				$(".controls a", tr).attr("href", url);
				
				// attach to table
				table.append(tr);
			}
		});
		
		// leave only 7 albums 
		$(".listAlbum:gt(6)", table).remove();
		
		// pop up html source in a text area
		$('<textarea rows="30" cols="100">' + table.html() + '</textarea>').windowit({
			w: 900,
			h: 600
		});
	}));
}


/*
ViewClass.prototype.stopPulseButton = function(button){
	if(button.data("timeoutId")) clearTimeout(button.data("timeoutId"));
	button.stop();
	button.css("color", "black");
	button.removeData("pulseOn");
};
*/

/*
 *  Controller Class
 *  This class takes user input events and makes decisions on what to do
 */
function ControllerClass(){
	this.windowWidth = 0; // hold previous window width for catching multiple refresh calls
	this.voteDirty = true; // when true, reload votes when switching to MyVotes
	this.tempObserver = null;
	this.observers = new ObserverFactory();
	this.defaults = {};
	this.multiSearchLoop;
	this.currentTab; // holds last tab to see if a myVotes click is from within myVotes or not
};

ControllerClass.prototype.initialize = function(){
	View.setupDiscoverView();
	View.setupMyVotesView();
	View.setupOtherVotesView();
	View.search = new SearchView(new SearchModel(), $("#search .contentArea"));
	View.multiSearch = new MultiSearchView(new SearchModel(), $("#search .contentArea"));
	View.gettingStarted = new GettingStartedView(new SearchModel(), $("#gettingStarted .contentArea"));
	View.gettingStarted.gridSettings.leftNavWidth = 80; // account for special margins in calculating grid
	View.gettingStarted.voteAction = function(){}; // override normal vote feedback for gettingStarted
	View.exactSearch = new SearchView(new ExactSearchModel(), $("#gettingStarted .contentArea"));
	View.exactSearch.name = "gettingStarted"; // override "search" name to be identified as getting started
	View.exactSearch.notFoundMessage = ""; // override not found "search" message
	
	// debug tool for viewing genre profiles of arbitrary users
	this.genreProfileGenerator = new ProfileGenerator();
	
	// start with discover view
	View.display = View.gettingStarted;
	View.display.calcGrid();
	
	// Window resize behavior
	window.onresize = function(){
		View.display.windowResized();
	}
	
	// init the genreProfileView with the model of MyView which has the genreProfile
	View.discoverNav.genreProfileView.initialize();
	
	// set model of MyVotes genreProfile to same as Discover (same data but different action)
	View.myVotesNav.genreProfileView.setModel(View.discoverNav.genreProfileView.model);
};

// get defaults
ControllerClass.prototype.loadDefaults = function(json){
	Model.loadJSON("defaults", function(json){
		
		// if no saved data, then first time users so set default custom collection and setup getting started
		if (!json) {
			// View.customTabs.addTab("Listen Later");
			// View.customTabs.addTab("Wish List");
			View.myVotesNav.setMinVote(1);
			View.otherVotesNav.setMinVote(1);
			View.myVotesNav.setSortBy("rank");
			View.showMain();
			Controller.prepareTab("gettingStarted");
			return;
		}
		
		// store for next save
		Controller.defaults = json;
		
		// set default views for discover & myVotes
		if(json.discoverView){
			if (json.discoverView == "grid") {
				Controller.switchDiscoverView("grid", false);
				$("#discoverViewSwitch a:eq(1)").addClass("selected");
				$("#discoverViewSwitch a:eq(0)").removeClass("selected");
			}
		}
		if(json.myVotesView){
			if(json.myVotesView == "grid") {
				Controller.switchMyVotesView("grid", false);
				$("#myVotesViewSwitch a:eq(1)").addClass("selected");
				$("#myVotesViewSwitch a:eq(0)").removeClass("selected");
			}
		}
		
		// set saved popularity
		if(json.popularity) View.discoverNav.popularity = json.popularity;
		
		// set last genre if saved
		if (json.lastTab.genre){
			Model.setCurrentGenre(json.lastTab.genre);
		}
		
		// if given search string, then override saved tab
		if(InitCommand.search){
			json.lastTab.tabName = "gettingStarted";
		}
		
		// show last tab
		if (json.lastTab.tabName) {
			View.triggerTab(json.lastTab.tabName);
		}
		
		// set the selected genre browser tab
		if(json.genreTab){
			View.setGenreTab(json.genreTab);
		}
		
		// do post default tab setup actions after screen refresh
		setTimeout(function(){
			// show main content AFTER all the default tab switching
			View.showMain();
			
			//  trigger discover & search actions if those were last tabs
			if(json.lastTab.tabName == "discover"){
				View.discoverNav.destroySlider(); // destroy slider hack to affirm slider is visible when initiating
				View.discoverNav.setupSlider(); // confidence slider must be visible to instantiate
				Controller.discover()
				
			} else if (json.lastTab.tabName == "myVotes"){
				View.myVotesNav.destroySlider(); // destroy slider hack to affirm slider is visible when initiating
				View.myVotesNav.setupSlider(); // minVotes slider must be visible to instantiate
			
			} else if (json.lastTab.tabName == "userVotes"){
				View.otherVotesNav.destroySlider(); // destroy slider hack to affirm slider is visible when initiating
				View.otherVotesNav.setupSlider(); // minVotes slider must be visible to instantiate
				
			} else if (json.lastTab.tabName == "search"){
				Controller.search(json.lastTab.searchStr);
				
			// if last tab is gettingStarted or no tab saved, manually default to getting started
			} else if (json.lastTab.tabName == "gettingStarted" || json.lastTab.tabName == null) {
				Controller.prepareTab("gettingStarted")
			}
		}, 100);
		
		// set min vote
		var minVote = (json.minVote || 1);
		View.myVotesNav.setMinVote(minVote);
		View.otherVotesNav.setMinVote(minVote);
		
		// set default myVotes sort
		if(json.sortBy){
			View.myVotesNav.setSortBy(json.sortBy);
		}
		
		// hide any dismissed elements (usually instructions)
		if(json.dismissed){
			$.each(json.dismissed, function(){
				$("#" + this).hide();
			});
		}
	});
};

ControllerClass.prototype.setDefault = function(property, value){
	if(!Model.authenticated) return;
	this.defaults[property] = value;
	this.saveDefaults();
};

ControllerClass.prototype.saveDefaults = function(){
	Model.saveJSON("defaults", this.defaults);
};

// user clicks on tab
ControllerClass.prototype.tabSwitch = function(ui){
	// first stop any currently playing albums
	View.display.stopMusicPlayer();
	
	// get tab name and have view prepare itself
	var tabName = $(ui.panel).attr("id");
	this.prepareTab(tabName);
	
	// hide other votes tab
	// if(tabName != "otherVotes") View.hideOtherVotesTab();
};

ControllerClass.prototype.prepareTab = function(tabName){
	// reset scroll
	View.scrollTo(0, 0);
	
	// unbind myVotes tab used re-click reloading
	$("#myVotesTab a").unbind('click.reload');
	
	// do any extra actions
	var me = this;
	switch (tabName) {
		case "discover":
			View.display = View.discover;
			View.display.calcGrid();
			Model.trackPage("discover");
			Controller.saveTabDefault("discover");
			break;
		case "myVotes":
			View.display = View.myVotes;
			View.display.calcGrid();
			// bind event to myVotes tab for reloading myvotes (check currentTab b/c IE doesn't unbind)
			// also do it on a timeout because IE will immediately pass current click event to reload if not
			setTimeout(function(){
				$("#myVotesTab a").bind('click.reload', function(){
					if (me.currentTab == "myVotes") Controller.myVotes();
				});
			}, 500);
			if (Controller.voteDirty) {
				Controller.voteDirty = false;
				Controller.myVotes();
			}
			Model.trackPage("myVotes");
			Controller.saveTabDefault("myVotes");
			break;
		case "otherVotes":
			View.display = View.otherVotes;
			View.display.calcGrid();
			Model.trackPage("otherVotes");
			break;
		case "search":
			View.display = View.search;
			View.display.calcGrid();
			// no direct call to Controller.userSearch() because it is always triggered by search button
			break;
		case "gettingStarted":
			if (InitCommand.search) View.display = View.exactSearch;
			else View.display = View.gettingStarted;
			View.display.calcGrid();
			Model.trackPage("gettingStarted");
			Controller.saveTabDefault("gettingStarted");
			break;
		default:
			// if there are no albums in the custom tab then attempt to load
			View.display = View.customTabs.getView(tabName);
			if (!View.display.loaded) {
				Controller.collection(tabName);
			} else {
				Model.trackPage(tabName);
			}
			break;
	}
	
	// add vertical spacing to contentArea if there's are topControls
	View.display.sizeForTopControls();
};


// do any extra actions after show
ControllerClass.prototype.tabShown = function(ui){
	if(!View.discover) return;
	var tabName = $(ui.panel).attr("id");
	switch (tabName) {
		case "discover":
			View.discoverNav.setupSlider(); // confidence slider must be visible to instantiate
			break;
		case "myVotes":
			View.myVotesNav.setupSlider(); // confidence slider must be visible to instantiate
			break;
		case "otherVotes":
			View.otherVotesNav.setupSlider(); // confidence slider must be visible to instantiate
			break;
		case "collection":
			break;
		case "search":
			break;
	}
	this.currentTab = tabName;
};


ControllerClass.prototype.saveTabDefault = function(tabName){
	// save last tab defaults
	var lastTab = { tabName: tabName };
	if (tabName == "discover") lastTab.genre = Model.getCurrentGenre();
	else if (tabName == "search"){
		lastTab.searchStr = View.search.model.searchStr;
	}
	if ($.inArray(tabName, ["discover", "myVotes", "gettingStarted", "search"]) != -1)
		Controller.setDefault("lastTab", lastTab);
};


ControllerClass.prototype.rememberDismissal = function(id){
	var d = this.defaults.dismissed;
	if(typeof(d) != "array") d = [];
	d.push(id);
	this.setDefault("dismissed", d);
};


// do login -- called from register popup
ControllerClass.prototype.register = function(validator){
	// grab form
	var div = $("#registerForm");
	
	// disable register button
	$(".submit", div).attr("disabled", "disabled");
	
	// get args from form
	var args = $(div).serializeArray();
	args[0].value = $.trim(args[0].value.toLowerCase()); // trim user name
	args[3].value = (args[3].value == "on") ? 1 : 0; // use 1 or 0 for eulaaccepted
	args.splice(2, 1); // remove second password
	
	// switch names of captcha for our api
	for(var i = 3; i <= 4; i++){
		if(args[i].name == "recaptcha_challenge_field") args[i].name = "challenge";
		else if(args[i].name == "recaptcha_response_field") args[i].name = "response";
	}
	
	// have model do register with success & error functions
	var me = this;
	Model.register(args, function(xml, args){
		me.registerSuccess(xml, args, validator);
	}, function(XMLHttpRequest){
		me.registerError(XMLHttpRequest);
	});
	
	// don't pass to normal submit button
	return false;
};

ControllerClass.prototype.registerSuccess = function(xml, args, validator){
    // see if error response is there
    if ($("response"), xml) {
        if ($("response", xml).attr("error") == "EXISTS") {
			// Username already exists
            validator.showErrors({
                "loginname": "That email is registered already"
            });
            $("#registerForm #passwd").val("");
            $("#registerForm #cpasswd").val("");
            $("#registerForm #loginname").select();
            $("#registerForm .submit").removeAttr("disabled");
            View.showRecaptcha();
        }
        else if ($("response", xml).attr("error") == "CAPTCHAFAILED") {
			// CAPTCHA failed
            validator.showErrors({
                "recaptcha_response_field": "Incorrect words - please try again"
            });
            $("#recaptcha_response_field").focus().val(args[4].value);
            $("#registerForm .submit").removeAttr("disabled");
            View.showRecaptcha(Recaptcha.focus_response_field);
			
        } else {
			
            // Successful registration
            Model.trackPage("register");
            _WI.close();
            Model.authenticate({
                type: "login",
                loginName: args[0].value
            });
            // do normal login actions
            this.loginSuccess();
        }
    }
    
};


ControllerClass.prototype.registerError = function(XMLHttpRequest){
	$("#registerForm .action").removeAttr("disabled");
	View.showServerError(XMLHttpRequest);
	View.showRecaptcha();
};

// do login -- called from login popup
ControllerClass.prototype.login = function(args, rememberFlag){
	var me = this;
	args[0].value = $.trim(args[0].value.toLowerCase()); // trim loginname
	Model.login(args, function(){
		me.loginSuccess();
	}, function(){
		me.loginError();
	}, rememberFlag);
	
	// don't pass login event to the normal submit button
	return false;
};


// login success
ControllerClass.prototype.loginSuccess = function(){
	
	// get user data
	Model.loadUserDetails();
	
	// track login and segment user
	Model.segmentUser(Model.loginName);
	
	// load custom tabs for this user
	View.customTabs.loadTabs();
	
	// drop version num display
	$("#versionNum").remove();
	
	// get the genre profile from cookie and update genre profile viewer
	View.discoverNav.genreProfileView.model.loadGenreProfile();
	
	// load stored defaults (showMain is triggered by loadDefaults AFTER it shows correct tabs)
	this.loadDefaults();
	
	// initiate search if given in arguments
	if(InitCommand.search){
		this.gs_doMultiSearch({
			searchStr: InitCommand.search,
			exactMatch: true
		});
	}
	
	// get repset if admin
	if(Model.admin){
		Model.a_getRepSet(Model.admin, function(repSet){
			Model.a_repSet = repSet;
		})
	}
	// admin display is off by default
	Model.showAdmin = false;
	
	// drop preloaded content
	$("#preloadObjects").remove();
};


// callback given to Model for login
ControllerClass.prototype.loginError = function(r1, r2){
	// insert error test in the pop up
	$("#login .error").html("Incorrect username/password combination.  Try again.").show();
	
	// re-enable login button for next attempt
	$("#login .action").removeAttr("disabled");
};


// do interest -- called from front page popup
ControllerClass.prototype.doInterest = function(email, validator, formDiv){
	var me = this;
	Model.interest(email, function(){
		formDiv.html("Thank you for your interest.  We'll send an invite when we open our beta more.")
	}, function(){
		validator.showErrors({
			"interestName": "That email is already submitted"
		});
	});
};

ControllerClass.prototype.logout = function(){
	// turn on login button
	$("#login .action").removeAttr("disabled");
	
	// do logout
	Model.logout(function(){
		// simply reload page to clear everything
		window.location = "index.html"
	});
};


ControllerClass.prototype.sendInvites = function(validator){
	// grab form
	var div = $("#inviteForm");
	
	// process emails
	var emailStr = $("#emailsInput", div).val().replace(/ /g, "");
	var emails = [];
	var badEmails = [];
	var me = this;
	$.each(emailStr.split(/\n/), function(i, aLine){
		$.each(aLine.split(","), function(j, anItem){
			var email = $.trim(anItem);
			if (email) {
				if(me.validateEmail(email)){
					emails.push(email);
				} else {
					badEmails.push(email);
				}
			}
		});
	});
	
	// show errors & exit if malformed emails
	if(badEmails.length){
		var plural = (badEmails.length > 1) ? 'are not valid emails.' : 'is not a valid email.'
		validator.showErrors({
			"invitees": '<br />"' + badEmails.join('","') + '" ' + plural
		});
		return false;
	}
	
	if(emails.length == 0){
		validator.showErrors({
			"invitees": "Please enter an email"
		});
		return false;
	}
	
	// get args from form (only first 2 needed)
	var message = $("#messageInput", div).val();
	if ($.trim(message) == "") message = "No message";
	
	// close invite window
	_WI.close();
	
	// now have Model send invites
	Model.sendInvites({invitees: emails.join(), message:message}, function(xml, args){
		Model.trackPage("invitationSuccess");
		View.inform({content:"Your invitations have been sent.  Thanks for sharing!"});
		// update num left display
		Model.loadUserDetails();
	}, function(XMLHttpRequest){
		Model.trackPage("invitationError");
		View.inform({content:"There was an unexpected error.  Please try again later."});
	});

	// don't pass to normal submit button
	return false;
};


// validate a single email string
ControllerClass.prototype.validateEmail = function(email){
	var filter = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
	if (filter.test(email)) return true;
	else return false;
};


// called from other genre navigators 
ControllerClass.prototype.discoverAGenre = function(aGenre){
	// set genre
	Model.setCurrentGenre(aGenre);
	
	// reset page to 1
	View.discoverNav.pager.setPage(1);
	
	// discover
	this.discover();
};


// Change Discover View
ControllerClass.prototype.switchDiscoverView = function(str, refreshFlag){
	// stop any stepping
	View.display.stopStepping();
	
	// first remove all observers of the model (they share the same model)
	View.discoverGridView.model.removeObserver(View.discoverGridView);
	View.discoverListView.model.removeObserver(View.discoverListView);
	
	// get view
	var newView;
	if(str == "grid") newView = View.discoverGridView;
	else newView = View.discoverListView;
	
	// set the new view
	View.discover = newView;
	View.discover.model.addObserver(newView);
	
	// save default
	Controller.setDefault("discoverView", str)
	
	// if refreshFlag, then reset view
	if(refreshFlag){
		this.discover();
	}
	
}

// switch my votes view ("grid" or "list")
ControllerClass.prototype.switchMyVotesView = function(str, refreshFlag){
	// stop any stepping
	View.display.stopStepping();
	
	// first remove all observers of the model (they share the same model)
	View.myVotesGridView.model.removeObserver(View.myVotesGridView);
	View.myVotesListView.model.removeObserver(View.myVotesListView);
	
	// get view
	var newView;
	if (str == "grid") newView = View.myVotesGridView;
	else newView = View.myVotesListView;
	
	// set the new view
	View.myVotes = newView;
	View.myVotes.model.addObserver(newView);
	
	// save default
	Controller.setDefault("myVotesView", str)
	
	// if refreshFlag, then reset view
	if(refreshFlag){
		View.display = View.myVotes;
		View.myVotes.refresh();
	}
	
}

// get recommendations
ControllerClass.prototype.discover = function(){
	
	// only show recs for authenticated users for now - TODO remove when not-private-beta
	if (!Model.authenticated) 
		return;
		
	// have the View prepare the Recommendations tab
	View.triggerTab("discover");
	View.display = View.discover;
	View.display.model.clear();
	View.hideOtherVotesTab(); // in case its showing
	View.display.hideBottomPager();
	
	// set title of content area
	var popStr = View.discoverNav.getSliderStep();
	if(popStr == "Medium") popStr = "Medium Popular";
	var dateStr;
	if((View.discoverNav.yearMin == null) && (View.discoverNav.yearMax == null)) dateStr = " All Time";
	else dateStr = (View.discoverNav.yearMin || "Before") + "-" + (View.discoverNav.yearMax || "Present");
	$(".contentTitle", View.discover.contentArea)
		.html(popStr + " " + Model.getCurrentGenre().name + " from " + dateStr.replace(/Before-/, "Before "));
	
	// highlight genre in genreProfile if it is there
	View.discoverNav.genreProfileView.setSelectedGenre(Model.getCurrentGenre().id);
	
	// track page in Google Analytics
	Model.trackPage("discover/" + this._prepSearchTerms(Model.getCurrentGenre().name));
	
	// scroll up to top
	View.scrollTo(0, 0);
	
	// get results from Model
	View.display.hideMessage(); // remove any messages
	var p = View.discover.getAlbumRange();
	View.display.showRecStats(p);
	this.discoverFrom(p.start, p.count);
	// this.discoverFrom(0, View.discover.numAlbums); // albumGrid.length);
};


// get recommendation range and add to content area
ControllerClass.prototype.discoverFrom = function(start, num){
	View.display.stopMusicPlayer();
	
	// show loading indicator in current content area
	View.blockAndShowLoading("Finding " + View.discoverNav.getSliderStep() +
		" <strong>" + Model.getCurrentGenre().name + "</strong>...");
			
	// get confidence & discover
	var popularity = Math.max(1, View.discoverNav.popularity) / 100.0;
	View.discover.model.loadAlbums(start, num , popularity, View.discoverNav.yearMin, View.discoverNav.yearMax);
	
	// save this tab as default next login
	this.saveTabDefault("discover");
};


// get my votes
ControllerClass.prototype.myVotes = function(){
	// only show recs for authenticated users for now - TODO remove when not-private-beta
	if (!Model.authenticated) 
		return;
	
	// have the View prepare the My Votes tab
	View.display = View.myVotes;
	View.display.model.clear();
	View.display.hideBottomPager();
	
	// scroll up to top
	View.scrollTo(0, 0);
	
	// give screen a chance to update display before searching.
	var me = this;
	setTimeout(function(){
		me.myVotesFrom(0, View.myVotes.model.maxServerItems); 
	}, 100);
};


// get specific votes and add to content area.
ControllerClass.prototype.myVotesFrom = function(start, num){
	View.display.stopMusicPlayer();
	
	// show loading display (if ADMIN show user id)
	var str = "Retrieving your Votes...";
	if (Model.showAdmin && Model.a_viewUser) 
		str = "Retrieving <strong>" + Model.a_viewUser + "'s</strong> Votes...";
	View.blockAndShowLoading(str);
	
	// do retrieval
	View.myVotes.model.loadAlbums(start, num, {
		errorFunction: function(){
			// if error make sure it reloads on next try
			Controller.voteDirty = true;
		}
	});
};


// get my votes, but do it in the backgroun
ControllerClass.prototype.myVotesBackground = function(){
	// only show recs for authenticated users for now - TODO remove when not-private-beta
	if (!Model.authenticated) 
		return;
	
	// do retrieval
	View.myVotes.model.loadAlbums(0, View.myVotes.model.maxServerItems);
};


// get another's votes
ControllerClass.prototype.otherVotes = function(userData){
	// only show recs for authenticated users for now - TODO remove when not-private-beta
	if (!Model.authenticated) 
		return;
	
	// have the View prepare the My Votes tab
	View.display = View.otherVotes;
	View.display.model.clear();
	View.showOtherVotesTab(userData.name);
	
	// scroll up to top
	View.scrollTo(0, 0);
	
	// give screen a chance to update display before searching.
	var me = this;
	setTimeout(function(){
		me.otherVotesFrom(0, View.myVotes.model.maxServerItems, userData);
	}, 100);
};


// get specific votes and add to content area.
ControllerClass.prototype.otherVotesFrom = function(start, num, userData){
	View.display.stopMusicPlayer();
	
	// show loading display
	View.blockAndShowLoading("Retrieving <strong>" + userData.name + "'s</strong> Votes...");
	$(".contentTitle", View.display.contentArea).html(userData.name + "'s Votes").show();
	
	// do retrieval
	View.otherVotes.model.loadAlbums(start, num, userData.id);
};


// get my collection
ControllerClass.prototype.collection = function(tabName){
	// only show recs for authenticated users for now - TODO remove when not-private-beta
	if (!Model.authenticated) 
		return;
	
	// have the View prepare the collections tab
	View.display = View.customTabs.getView(tabName);
	View.display.model.clear();
	
	// track page in Google analytics
	Model.trackPage(tabName);
	
	// scroll up to top
	View.scrollTo(0, 0);
	
	// give screen a chance to update display before searching
	var me = this;
	setTimeout(function(){
		me.collectionFrom(0, View.display.model.maxServerItems);
	}, 100);
};


// get collection and add to content area.
ControllerClass.prototype.collectionFrom = function(start, num){
	View.display.stopMusicPlayer();
	
	// show loading display
	View.blockAndShowLoading("Retrieving your collection " + View.customTabs.tabs[View.display.model.tabName].displayName);
	
	// do retrieval
	View.display.model.loadAlbums(start, num);
};



// execute user search
ControllerClass.prototype.search = function(searchStr){
	
	// only show recs for authenticated users for now - TODO remove when not-private-beta
	if (!Model.authenticated) 
		return;
	
	//  validate text input here
	var searchStr = searchStr || $.trim($("#searchInput").val());
	
	// Have View prepare the search tab
	View.search.model.clear();
	View.showSearchTab();
	View.triggerTab("search");
	
	// set search model's searchStr so saveTabDefault can get access to it
	View.display.model.searchStr = searchStr;
	
	// set the view.display & calc grid here (in addition to switchTab) because Multi-Search & this share same tab
	View.display = View.search;
	View.display.calcGrid();

	// set title of content area to search string
	$(".contentTitle", View.search.contentArea).html('Search for "' + searchStr + '"');
	
	// track page in Google Analytics
	Model.trackPage("search/" + this._prepSearchTerms(searchStr));
	
	// scroll up to top
	View.scrollTo(0, 0);
	
	// give screen a chance to show search tab and then call userSearch
	var me = this;
	setTimeout(function(){
		me.searchFrom(0, View.display.numAlbums * 2, searchStr); // albumGrid.length, searchStr);
	}, 100);
};


// search specific albums and add to content area
ControllerClass.prototype.searchFrom = function(start, num, searchStr){
	View.display.stopMusicPlayer();
	
	// show loading indicator in current content area
	View.blockAndShowLoading('Searching for <strong>"' + searchStr + '"</strong>');
	
	// do search
	View.search.model.loadAlbums(start, num, searchStr);
	
	// save this tab as default next login
	this.saveTabDefault("search");
};




// search multiple items
ControllerClass.prototype.doMultiSearch = function(txt){
	View.display.stopMusicPlayer();
	// parse items based on lines & commas
	var items = [];
	var str;
	$.each(txt.split(/\n/), function(i, aLine){
		$.each(aLine.split(","), function(j, anItem){
			str = $.trim(anItem);
			if(str) items.push(str);
		});
	});
	
	// if local use test strings
	if (LocalFlag) items = ["ambient"];
	
	// prep search tab
	View.multiSearch.model.clear();
	View.showSearchTab();
	View.triggerTab("search"); // trigger tab will set View.display
	View.display = View.multiSearch; // override normal search view with multi-search View

	// set title of content area to search string
	$(".contentTitle", View.search.contentArea).html("Search Results");
	
	// track page in Google Analytics
	Model.trackPage("multiSearch/");
	
	// show loading indicator
	View.blockAndShowLoading("Searching your albums...");
	
	// set width of content area manually
	var contentArea = View.display.contentArea;
	contentArea.width(Math.floor($(window).width()
		- View.display.gridSettings.leftNavWidth)
		- parseInt(contentArea.css("padding-left"))
		- parseInt(contentArea.css("padding-right")));
	
	// setup sections
	var me = this;
	$.each(items, function(i, searchStr){
		var section = $('<div class="loading" id="section_'+ searchStr.replace(/ /g, "_") +'">' +
			'<h2 class="multiSearch">'+ searchStr +'</h2><ol></ol></div>');
		$(".albumList", View.display.contentArea).append(section);
	});
	
	// call multiple search requests
	$.each(items, function(i, searchStr){
		View.multiSearch.model.loadAlbums(0, 20, searchStr);
	});
};


// search multiple items in Getting Started
ControllerClass.prototype.gs_doMultiSearch = function(args){
	
	// stop music & set correct display
	View.display.stopMusicPlayer();
	$("#gs_enterFavs").hide();
	$("#home").hide();
	$("#music").show();
	
	// stop any previous multi-search loops
	if(this.multiSearchLoop){
		clearTimeout(this.multiSearchLoop.timeoutId);
		this.multiSearchLoop = null;
	}
	
	// parse items based on lines & commas
	var items = [];
	$.each(args.searchStr.split(/\n/), function(i, aLine){
		$.each(aLine.split(","), function(j, anItem){
			str = $.trim(anItem);
			if(str) items.push(str);
		});
	});
	
	// if local use test string
	if (LocalFlag) items = ["ambient"];	
	
	// set show & hide areas behavior
	var prevStep = $("#gs_enterFavs").hide();
	var thisStep = $("#gs_rateAlbums").show();
	var me = this;
	$(".back", thisStep).click(function(){
		View.gettingStarted.model.clear();
		thisStep.hide();
		prevStep.show();
	});
	
	// set action for next step
	$(".action", thisStep).click(function(e){
		me.gs_showGenres();
	});
	
	// clear display of either possible view sharing "gettingstarted" area
	View.gettingStarted.model.clear();
	View.exactSearch.model.clear();
	
	// prep general view
	View.gettingStarted.contentArea.css({ paddingTop: "135px" });
	View.scrollTo(0, 0); // thisStep.offset().top - 30, 1000);
	
	// track page in Google Analytics
	Model.trackPage("gettingStarted/search");
	
	// setup sections if doing sectional search (not exact match)
	var me = this;
	if(!args.exactMatch){
		var contentArea = View.display.contentArea;
		contentArea.width(Math.floor($(window).width()
			- View.display.gridSettings.leftNavWidth)
			- parseInt(contentArea.css("padding-left"))
			- parseInt(contentArea.css("padding-right")));
			
		$.each(items, function(i, searchStr){
			var section = $('<div class="loading" id="section_'+ searchStr.replace(/ /g, "_") +'">' +
				'<h2 class="multiSearch">'+ searchStr +'</h2><ol></ol></div>');
			$(".albumList", View.display.contentArea).append(section);
		});
	}
	
	// create an encapsulated named "observer" for observing updates from genre model (which responds to votes)
	this.observers.add("getStarted", View.discoverNav.genreProfileView.model, function(){
		me.gs_updateGenreList();
	});
	
	// set our current view for sending search requests
	var view = (args.exactMatch) ? View.exactSearch : View.gettingStarted;
	view.calcGrid();
	
	// call multiple search requests with short intervals between
	this.multiSearchLoop = function(n){
		view.model.loadAlbums(0, view.numAlbums, items[n]);
		if(++n < items.length) this.timeoutId = setTimeout(function(){ me.multiSearchLoop(n); }, 250);
	}
	this.multiSearchLoop.timeoutId = 0; // make timeoutId a member of this function for outside cancelling
	this.multiSearchLoop(0);
};

ControllerClass.prototype.gs_showGenres = function(){
	
	// hide previous step
	$("#gs_rateAlbums").hide();
	$(".albumList", View.display.contentArea).hide();
	
	// show next step 
	var thisStep = $("#gs_showGenres").find("ol").empty().end().show();
	View.scrollTo(thisStep.offset().top - 30, 1000);
	
	// set back action
	$(".back", thisStep).click(function(){
		thisStep.hide();
		$(".albumList", View.display.contentArea).show();
		$("#gs_rateAlbums").show();
	});
	
	// show genres
	this.gs_updateGenreList();
};

ControllerClass.prototype.gs_updateGenreList = function(){
	// clear display
	var thisStep = $("#gs_showGenres").find("ol").empty().end();
		
	// get list of top genres
	var topGenres = View.discoverNav.genreProfileView.model.genreProfile
	var topGenres = topGenres.slice(0, Math.min(15, topGenres.length));
	
	// add to next step display
	var list = thisStep.find("ol");
    $.each(topGenres, function(i, data){
        list.append(
			$('<li><a href="javascript:void(0)">'+ data.name +'</a></li>')
				.click(function(){
					// select myGenres genre profile tab
					View.setGenreTab("genreProfile");
					
					// now give recommendations for this genre
					Controller.discoverAGenre({
						id: data.id,
						name: data.name
					});
		        })
		);
    });
	$("<li>...</li>").css({
		"float": "none",
		"clear": "left",
		"opacity": 0.01,
		"line-height": "1px"
	}).appendTo(list);
};

/*
 * Do introspection on array of album data
 */
ControllerClass.prototype.doIntrospect = function(albumsData, callback){
	if(!Model.admin) return;
	var ids = [];
	$.each(albumsData, function(){
		ids.push(this.id);
	});
	View.display.model.introspect(Model.admin, ids, function(){
		callback();
	});
}

// cancel a load -- called from View's loader display
ControllerClass.prototype.abortLoad = function(){
	View.display.model.abort();
	View.hideLoading();
};


// Prep for showing the album detail page for the given jQuery album object
ControllerClass.prototype.albumDetailRequest = function(albumData){
	// track page in Google Analytics
	Model.trackPage("detail");
	
	// display album data
	View.showAlbumPage(albumData);
};


// user votes on an item - called from rating plugin
ControllerClass.prototype.userVote = function(id, vote){
	// server vote with callback for display to react to voting
	View.display.model.addVote(id, vote, function(){
		// do visual vote action
		View.display.voteAction(id, vote); 
		
		// manually add "vote" & fake "ranking" for now until we get <vote...> data in getitems
		var data = View.display.model.albumData(id);
		data.vote = vote;
		data.rank = (new Date()).getTime();
		
		// now add album's data to myVotes (which will broadcast to view to update)
		View.myVotes.model.addAlbumData(data);
	});
	
	// weigh vote into genre profile
	var albumData = View.display.model.albumData(id);
	var me = this;
	$.each(albumData.genreIds, function(i, gid){
		View.discoverNav.genreProfileView.model.weighGenre(gid, vote);
	});
	View.discoverNav.genreProfileView.model.calcGenreProfile();
		
	 // flash MyVotes tab
	View.flashMyVotes();
		
	// make sure to reload MyVotes when switching to myVotes tab
	// this.voteDirty = true;
};


// Prep search terms (convert spaces to +'s)
ControllerClass.prototype._prepSearchTerms = function(str){
	return $.trim(str).replace(/ /g, "+");
};


// Controller
ControllerClass.prototype.windowResized = function(){
	// catch the false window size commands (only width needed for test)
	var w = $(window).width();
	if (w == this.windowWidth) 
		return;
	this.windowWidth = w;
	
	// valid window resize so relay event to view
	View.display.windowResized();
};


ControllerClass.prototype.showMultiSearch = function(){
	Model.trackPage("multiSearch");
	
	var div = $("<div></div>")
		.windowit({ w: 620, h: 320 })
		.load("/content/multiSearch.html .copyContent", function(){
			div.find(".action").click(function(){
				var str = div.find("textarea").val();
				_WI.close(300);
				Controller.doMultiSearch(str);
			});	
		});
	
};


/*
 * Custom Tab Manager - Manage custom tabs - is both a controller & model
 */ 
function TabManager(){
	this.tabs = {};
	this.base = $(".subnav > ul");
	this.addToNewTab = null; // holds album to add to a new tab during "add to new tab" action
};

// load tabs from storage and create them
TabManager.prototype.loadTabs = function(){
	// reset any curent tabs
	this.reset(); 
	
	// get keep
	var me = this;
	Model.getKeep("customTabs", function(data){
		me.finishLoadTabs(data);
	});
};
	
// finish load tabs - create them
TabManager.prototype.finishLoadTabs = function(data){
	// load list of tab names from storage and create their views
	var me = this;
	$.each(data, function(i, tabName){
		me.tabs[tabName] = new CustomTab(me.base, tabName);
		
		// make sure tab content is loaded for future existsInKeep calls
		Model.getKeep("tab-" + tabName, function(){});
	});
	
	
	// reposition addTab button to end of tab list
	this.base.append($("#addTab").remove().click(function(){
		View.customTabs.queryNewTab();
	}));
	// if($.browser.mozilla) $("#addTab").css({ "top": "0px" });
	
};


// query user for new tab name
TabManager.prototype.queryNewTab = function(){
	var me = this;
	View.inputQuery({
		title: "New Collection",
		content: "Please name the new collection",
		label: "Tab name:",
		inputFilter: " -", // don't allow anything other than alphanumeric characters, spaces & dashes
		callback:function(str){
			me.addTab(str);
		}
	});
};


// add new tab based on user input -- change spaces to underscores
TabManager.prototype.addTab = function(nameStr){
	var tabName = nameStr.replace(/ /g, "_");
	this.tabs[tabName] = new CustomTab(this.base, tabName);
	this._saveTabs();
	
	// reposition addTab button to end of tab list
	$(".subnav > ul").append($("#addTab").remove().click(function(){
		View.customTabs.queryNewTab();
	}));
	
	// if this was an "add album to new tab" action, then add album
	if (this.addToNewTab) {
		this.tabs[tabName].view.addAlbum(this.addToNewTab);
		Model.trackPage("addTab/menu");
	} else {
		Model.trackPage("addTab");
	}
};


// save list of tab names
TabManager.prototype._saveTabs = function(){
	Model.setKeep("customTabs", this.tabNames());
};

// get list of tab names
TabManager.prototype.tabNames = function(){
	var tabNames = [];
	$.each(this.tabs, function(tabName, data){
		tabNames.push(tabName);
	});
	return tabNames;
};


// return the collection view for given tabname
TabManager.prototype.getView = function(tabName){
	return this.tabs[tabName].view;
};


TabManager.prototype.renameTab = function(tabName, newDisplayName){
	var newName = newDisplayName.replace(/ /g, "_");
	
	// first get tab obj & rename its properties
	var tab = this.tabs[tabName];
	tab.tabName = newName;
	tab.displayName = newDisplayName;
	
	// rename actual tab DOM element
	tab.element.find("a").html(newDisplayName).attr("href", "javascript:void(0)" + newName);
	tab.element.attr("id", "tab-" + newName);
	
	// rename DIV element
	$("#" + tabName).attr("id", newName);
	
	// update view's model with new name
	this.tabs[tabName].view.model.rename(newName);
	
	// change property name on list by replacing old property name with new one
	var newList = {};
	$.each(this.tabs, function(prop, data){
		if(prop == tabName){
			newList[newName] = data;
		} else {
			newList[prop] = data;
		}
	});
	this.tabs = newList;
	
	// track action
	Model.trackPage("renameTab");
	
	// save new tab list
	this._saveTabs();
};


// user requests to remove a tab so remove DOM element & remove from data & storage
TabManager.prototype.removeTab = function(tabName){
	// first go to discover
	View.triggerTab("discover");
	
	// remove tab & tab div from DOM
	this.base.tabs("remove", this._tabNum(tabName));
	$("#" + tabName).remove();
	
	// have view's model remove the storage
	this.tabs[tabName].view.model.remove();
	
	// remove the view from Tab Manager
	this.tabs[tabName] = null;
	
	// delete from data
	delete this.tabs[tabName];
	
	// track action
	Model.trackPage("removeTab");
	
	// update storage
	this._saveTabs();
	
};


// remove all custom tabs
TabManager.prototype.reset = function(){
	var me = this;
	$.each(this.tabs, function(tabName, data){
		me.base.tabs("remove", me._tabNum(tabName));
	});
	this.tabs = {};
};


// figure out the tab position (zero based) of the given tabName
TabManager.prototype._tabNum = function(tabName){
	var tabNum;
	$("li", this.base).each(function(i){
		if($(this).attr("id") == "tab-" + tabName){
			tabNum = i;
			return false;
		}
	});
	return tabNum;
};

// create a dynamic menu of for adding & removing an album from a custom folder
TabManager.prototype.folderMenu = function(album, data, loc){
	// keep controls open while menu is active
	album.data("keepOpen", true);
	
	// create base folder menu
	var fmenu = $('<ul id="fmenu"><li class="section">Add To:</li></ul>');
	var me = this;
	
	// add to collection action
	var removeFrom = [];
	$.each(this.tabs, function(i, tab){
		if (Model.existsInKeep("tab-" + tab.tabName, data.id)) {
			removeFrom.push(tab.tabName)
		}
		else {
			$('<li><a href="javascript:void(0)">' + tab.displayName + '</a></li>').click(function(){
				tab.view.addAlbum(data);
				me.closeFolderMenu(album);
				me.flashTab(tab.tabName);
			}).appendTo(fmenu);
		}
	});
	
	// add to NEW collection action
	$('<li><a href="javascript:void(0)">[New Collection...]</a></li>').click(function(){
		me.addToNewTab = data; // tell addTab method to add album when done
		me.queryNewTab();
		me.closeFolderMenu(album);
	}).appendTo(fmenu);
	
	
	// remove from collection action
	if(removeFrom.length) fmenu.append('<li class="section">Remove From:</li>');
	$.each(removeFrom, function(i, tabName){
		$('<li><a href="javascript:void(0)">' + tabName.replace(/_/g, " ") + '</a></li>').click(function(){
			me.getView(tabName).removeAlbum(data);
			me.closeFolderMenu(album);
			me.flashTab(tabName);
		}).appendTo(fmenu);
	});
	
	// append to menu body, set location and set close menu action on mouseDOWN on document
	$(document.body).append(fmenu.css({ left: loc.x, top: loc.y })).mousedown(function(e){
		if ($(e.target).parents("#fmenu").length == 0) me.closeFolderMenu(album);
	});
	
	// set width of li elements to cover the width of the menu so rollovers cover the whole line
	$("li", fmenu).width(fmenu.width());
};

TabManager.prototype.closeFolderMenu = function(album){
	album.data("keepOpen", false); // allow controls to close again
	album.trigger("mouseout"); // mouse is off of album so close controls
	$(document.body).unbind("mousedown");
	$("#fmenu").remove();
};

TabManager.prototype.flashTab = function(tabName){
	View.flashTab($(".subnav .ui-tabs-nav #tab-" + tabName));
} 


/*
 * A Custom Tab - holds its name, view, controller, etc.
 */
function CustomTab(base, tabName){
	this.tabName = tabName;
	this.view;
	this.displayName;
	this.element;
	
	// create content area for this tab
	var el = TemplateCode.customCollection.clone().attr("id", this.tabName);
	$("#music").append(el);
	
		
	// create DOM tab & set id & custom behavior
	this.displayName = tabName.replace(/_/g, " ");
	base.tabs("add", "#" + this.tabName, this.displayName); // sets tab href & display
	this.element = $("li:last", base);
	
	// move container back out to #music level because .tabs() relocates it for some reason
	$("#music").append(el);
	
	// customize display to fit current tab methodology and set remove function
	var me = this;
	this.element.attr("id", "tab-" + this.tabName)
		.addClass("customTab")
		.attr("title", "Double-click to rename")
		.append('<img class="spacer" src="images/spacer.gif" alt="">')
		.append($('<img class="close" src="images/tabclose.gif" alt="">').click(function(){
				View.yesNoQuery({
					title: "Remove Collection",
					content: 'Are you sure you want to remove collection "'+ me.displayName +'"?',
					buttonName: "Remove",
					callback: function(str){
						me.element.fadeOut(300, function(){
							View.customTabs.removeTab(me.tabName);
						})
					}
				});
			}));
			
	// set rename function
	this.element.dblclick(function(){
		View.inputQuery({
			title: "Rename Collection",
			content: "Rename your custom collection",
			inputFilter: " -", // don't allow anything other than alphanumeric characters, spaces & dashes
			callback:function(str){
				View.customTabs.renameTab(me.tabName, str);
			}
		});
	});
	
	// create new Collection View & model and store view for it
	this.view = new CollectionView(new CollectionModel(this.tabName), $(".contentArea", el));
};


/*
 * SuperClass Model for all Album Grid Views
*/
function AlbumGridModel(){
	// call Observable base class constructor
	AlbumGridModel.baseConstructor.call(this);
	
	this.maxServerItems = 60; // maximum that server will take on lc: request.
	this.allData = [];
	this.updateData = []; // used for holding  updates to data that views will request on broadcast
	this.request = {};
	this.callback = null; // set by subclass' get method
	this.XMLHttpRequest = null;
	this.raters = {};
	this.totalItems = null; // some subclass models will store this from a server return
};

// make Observable
OopExtend(AlbumGridModel, _Observable);

// Model
AlbumGridModel.prototype.addVote = function(itemId, vote, callback){
	// if local just exit
	if (LocalFlag) {
		callback();
		return;
	}
	
	var me = this;
	$.ajax({
		url: "/fhws/vote",
		data: {
			itemid: itemId,
			rating: vote
		},
		cache: false,
		error: function(XMLHttpRequest){
			View.showServerError(XMLHttpRequest);
		},
		success: function(data, status){
			// update album object's data
			me.albumData(itemId).vote = vote;
			callback();
		}
	});
};

// Override in subclasses - they usually call getFromServer, and always end up calling _process
AlbumGridModel.prototype.loadAlbums = function(){
};



// basic search function, used by Search & Recommend
AlbumGridModel.prototype._getFromServer = function(method, args){
	 
	// if local, then just load sample file, else load from server
	var me = this;
	if (LocalFlag) {
		var me = this;
		this.XMLHttpRequest = $.ajax({
			url: (method == "browse") ? "localdata/sampleBrowse.xml" : "localdata/sampleSearchResults.xml",
			error: function(XMLHttpRequest){
				View.showServerError(XMLHttpRequest);
			},
			cache: false,
			success: function(xml){
				me._process(xml);
			}
		});
	}
	else {
		this.XMLHttpRequest = $.ajax({
			url: "/fhws/" + method,
			data: args,
			error: function(XMLHttpRequest){
				View.showServerError(XMLHttpRequest);
			},
			cache: false,
			success: function(xml, status){
				me._process(xml);
			}
		});
	}
};


// process browse/search
AlbumGridModel.prototype._process = function(xml){
	
	// in case of local testing where xml is string
	if (typeof(xml) == "string") 
		xml = Model.convertXMLString(xml);
		
	// parse albums
	var me = this;
	this.totalItems = ($("totalitems", xml).attr("value") || null);
	var data, id;
	this.updateData = [];
	$("Item", xml).each(function(i){
		id = $(this).attr("itemid");
		data = me._parseAlbum($(this), id);
		me.allData.push(data);
		me.updateData.push(data);
	});
	
	// parse request info (todo potentially separate this meta data parsing as a function that contains this function as Kim suggests)
	var req = $("request", xml);
	this.request = {
		query: $("query", req).attr("value"),
		ll: $("ll", req).attr("value"),
		lc: $("lc", req).attr("value"),
		genre: $("genre", req).attr("value"),
		conflevelmin: $("conflevelmin", req).attr("value")
	};
	
	// inform observers that my state has changed
	this.broadcast("addition");
};



// get album data for this id (assumes AT LEAST basic album data loaded) TODO revisit naming of album detail functions
AlbumGridModel.prototype.getAlbumDetail = function(id, callback){
	
	// get current album data
	var albumData = this.albumData(id)
	if(albumData == null){
		// if none, create minimal album data to be filled by getItems (probably anonymous album detail request)
		albumData = { id: id };
		this.allData.push(albumData);
	}
	
	// if detail data is already loaded (the music "label" loaded), confirm we have reviews too and callback
	if(albumData.label){
		// callback(albumData);
		this.getReviews(id, callback);
		
	} else {
		// get album detail from server
		var me = this;
		if (LocalFlag) {
			this.XMLHttpRequest = $.get("localdata/sampleItemLookup.xml", function(xml){
				me._processDetail(xml, albumData, callback);
			});
		}
		else {
			this.XMLHttpRequest = $.ajax({
				url: "/fhws/getitems",
				data: {
					itemlist: [id]
				},
				cache: false,
				error: function(XMLHttpRequest) {
					View.showServerError(XMLHttpRequest);
				},
				success: function(xml, status){
					me._processDetail(xml, albumData, callback);
				}
			});
		}
	}
};


// process album detail request, latching on Reviews
AlbumGridModel.prototype._processDetail = function(xml, albumData, callback){
	// in case of local testing where xml is string
	if (typeof(xml) == "string") 
		xml = Model.convertXMLString(xml);
	
	// parse and merge detail data (just give first item in case).  Parse whole album if no prev data (no artist)
	if(albumData.artist) $.extend(albumData, this._parseAlbumDetail($("Item", xml).eq(0)));
	else $.extend(albumData, this._parseAlbum($("Item", xml).eq(0)));
	
	// now get first page of reviews, and then get lala ID.
	var me = this;
	this.getReviews(albumData.id, callback);
};


AlbumGridModel.prototype.getLaLaData = function(albumData, callback){
	var me = this;
	if (LocalFlag) {
		$.ajax({
			url: "localdata/sampleLSearch.txt",
			cache: false,
			dataType: "json",
			error: function(XMLHttpRequest){
				me._processGetLaLaData(null, albumData, callback);
				// View.showServerError(XMLHttpRequest);
			},
			success: function(json){
				me._processGetLaLaData(json, albumData, callback);
			}
		});
	} else {
		// create search str and remove any words with colons as that voids searches in lala
		var searchStr = albumData.artist + " " + albumData.title;
		var noColons = [];
		$.each(searchStr.split(" "), function(){
			if(this.indexOf(":") == -1) noColons.push(this);
		});
		searchStr = noColons.join(" ");
		
		// now remove offending or extra string data to simplify search
		searchStr = $.trim(searchStr.replace(/\[.*\]/g, "")); // remove bracketed content (e.g. [explicit])
		searchStr = $.trim(searchStr.replace(/\(.*\)/g, "")) // remove parenthesis content (e.g. (special))
			.replace(/ú/g, "u").replace(/Ü/g, "U"); // convert special characters
		
		// do search call
		$.ajax({
			url: "/lsearch",
			data: {
				albumsQ: searchStr,
				albumsCount: 10,
				songsQ: searchStr,
				songsCount: 10,
				artistsQ: searchStr,
				artistsCount: 10,
				sortKey: "Relevance",
				sortDir: "Desc",
				webSrc: "lala"
			},
			cache: true, // this doesn't change so cache it
			dataType: "json",
			error: function(XMLHttpRequest) {
				me._processGetLaLaData(null, albumData, callback);
				// View.showServerError(XMLHttpRequest);
			},
			success: function(json){
				me._processGetLaLaData(json, albumData, callback);
			}
		});
	}
};

AlbumGridModel.prototype._processGetLaLaData = function(json, albumData, callback){
	var me = this;
	// set to not found value in case find fails
	albumData.lalaId = -1;
	
	// now search through json for id
	if (json) {
		if (typeof(json) == "string"){
			json = JSON.parse(json);
		}
		if (json.data != undefined) {
			// search through discs for exact matches, and if so, add to albumData object
			if (typeof(json.data.albums == "object") && (json.data.albums.total > 0)) {
				$.each(json.data.albums.list, function(){
					if (me._simplify(this.artist) == me._simplify(albumData.artist) &&
						me._simplify(this.title) == me._simplify(albumData.title)) {
						albumData.lalaId = this.id;
						albumData.lalaFullStream = this.isLicensedForStreaming;
						return false;
					}
				});
			}
		}
	}
	// in any case, do callback
	callback();
};


// simplify string for comparison with lala search results
AlbumGridModel.prototype._simplify = function(str){
	if(!str) return "";
	str = $.trim(str.replace(/\[.*\]/g, "")); // remove bracketed content (e.g. [explicit])
	str = $.trim(str.replace(/\(.*\)/g, "")) // remove parenthesis content (e.g. (special))
		.replace(/\&/g,"and") // convert ampersand to "and"
		.toLowerCase();
	return(str);
}


// get album data for this id (assumes AT LEAST basic album data loaded)
AlbumGridModel.prototype.getReviews = function(id, callback, ll, lc){
	// default ll & lc if not given
	if(ll == null) ll = 0;
	lc = lc || 20;
	
	// if reviews already loaded (or ANY reviews when requesting a single review), then justreturn data
	var albumData = this.albumData(id);
	
	if(albumData.reviewsLoaded || (ll == 0 && lc == 1 && albumData.firstReviewLoaded)){
		callback(albumData);
		
	} else {
		// get album reviews from server
		var me = this;
		if (LocalFlag) {
			this.XMLHttpRequest = $.get("localdata/sampleReviews.xml", function(xml){
				// set reviews loaded flags
				if(ll == 0 && lc == 1) albumData.firstReviewLoaded = true;
				else albumData.firstReviewLoaded = albumData.reviewsLoaded = true;
				
				// now process reviews
				me._processReviews(xml, albumData, callback);
			});
		}
		else {
			this.XMLHttpRequest = $.ajax({
				url: "/fhws/reviews",
				data: {
					itemid: id,
					ll: ll,
					lc: lc
				},
				cache: false,
				error: function(XMLHttpRequest) {
					View.showServerError(XMLHttpRequest);
				},
				success: function(xml, status){
					// set reviews loaded flags
					if(ll == 0 && lc == 1) albumData.firstReviewLoaded = true;
					else albumData.firstReviewLoaded = albumData.reviewsLoaded = true;
					
					// now process reviews
					me._processReviews(xml, albumData, callback);
				}
			});
		}
	}
};


AlbumGridModel.prototype._processReviews = function(xml, albumData, callback){
	// in case of local testing where xml is string
	if (typeof(xml) == "string") 
		xml = Model.convertXMLString(xml);
		
	// set user reviews
	var reviews = [];
	var review;
	$("Review", xml).each(function(i){
		review = {};
		review.rating = parseInt($("Rating", this).text());
		review.userId = $(this).attr("userid");
		review.reviewerName = $("Reviewer > Name", this).text();
		review.summary = $("Summary", this).text();
		review.date = $("Date", this).text();
		review.content = $("Content", this).text();
		reviews.push(review);
	});
	albumData.reviews = reviews;
	
	// do callback with new data
	callback(albumData);
};



// get album data for this id (assumes AT LEAST basic album data loaded)
AlbumGridModel.prototype.introspect = function(userId, itemArray, callback){
	if (!itemArray || itemArray.length == 0) {
		callback();
		return;
	}
	var me = this;
	if (LocalFlag) {
		this.XMLHttpRequest = $.get("localdata/sampleIntrospect.xml", function(xml){
			me._processIntrospect(xml, callback);
		});
	}
	else {
		this.XMLHttpRequest = $.ajax({
			url: "/fhws/admin/introspect",
			data: {
				userid: userId,
				itemlist: itemArray.join()
			},
			cache: false,
			error: function(XMLHttpRequest) {
				callback();
				// View.showServerError(XMLHttpRequest);
			},
			success: function(xml, status){
				me._processIntrospect(xml, callback);
			}
		});
	}
};


AlbumGridModel.prototype._processIntrospect = function(xml, callback){
	// in case of local testing where xml is string
	if (typeof(xml) == "string") 
		xml = Model.convertXMLString(xml);
	
	// collect introspected albums
	var introspectAlbums = [];
	
	// set user reviews
	var me = this;
	var $pred, itemId, data, $rating, raterId, amt, rep, raterInfo;
	$("prediction", xml).each(function(){
		$pred = $(this);
		itemId = $pred.attr("itemid");
		data = me.albumData(itemId);
		if(data){
			introspectAlbums.push(data);
			data.prediction = parseFloat($pred.attr("pred")) + 3;
			data.confidence = parseFloat($pred.attr("confidence"));
			data.confwpred = parseFloat($pred.attr("confwpred"));
			data.ratings = [];
			$("rating", $pred).each(function(){
				$rating = $(this);
				
				// ad rating to this album
				raterId = $rating.attr("userid");
				amt = $rating.attr("rating");
				rep = $rating.attr("reputation");
				data.ratings.push({
					userId: raterId,
					rating: amt,
					reputation: rep
				})
				
				// collect all ratings centrally by rater
				raterInfo = me.raters[raterId];
				if(!raterInfo){
					raterInfo = { reputation: rep, ratings: []};
					me.raters[raterId] = raterInfo;
				}
				raterInfo.ratings.push({ itemId: itemId, rating: amt });
			});
			
			// sort descending by reputation
			data.ratings.sort(function(a,b){
				return (a.reputation > b.reputation);
			});
		}
	});
	
	// do callback with new data
	callback(introspectAlbums);
};


AlbumGridModel.prototype.clear = function(){
	this.allData = [];
	this.updateData = [];
	this.request = {};
	this.broadcast("clear");
};


AlbumGridModel.prototype.abort = function(){
	this.XMLHttpRequest.abort();
};

// Get data starting from range.start with range.count items
AlbumGridModel.prototype.getData = function(range){
	if(range.start >= this.allData.length) return [];
	return this.allData.slice(range.start, Math.min(this.allData.length, (range.start + range.count)));
};


// Get data update from loadAlbums
AlbumGridModel.prototype.getAllUpdate = function(){
	return this.updateData;
};


// Get update data starting from range.start with range.count items
AlbumGridModel.prototype.getUpdate = function(range){
	if(range.start >= this.updateData.length) return [];
	return this.updateData.slice(range.start, Math.min(this.updateData.length, (range.start + range.count)));
};


// Get all data of this model
AlbumGridModel.prototype.getAllData = function(){
	return this.allData;
};

// Just get data for a single album id
AlbumGridModel.prototype.albumData = function(id){
	var result = null
	$.each(this.allData, function(i, val){
		if (val.id == id) {
			result = val;
			return false;
		}
	});
	return result;
};

// add a single album data - updates any duplicate if found
AlbumGridModel.prototype.addAlbumData = function(data){
	// add to allData if does not exist
	var album = this.albumData(data.id);
	if (album) $.extend(album, data);
	else this.allData.push(data);
	
	// update view (refresh all to confirm all albums in order)
	this.broadcast("refresh");
};


// remove a single album data
AlbumGridModel.prototype.removeData = function(item){
	var me = this;
	$.each(this.allData, function(i, obj){
		if(obj.id == item.id){
			me.allData.splice(i, 1);
			return false;
		}
	});
	
	this.updateData = [item]; // used for View requesting update
	this.broadcast("deletion");
};


// Model - parse a single search result and return a JSON object
AlbumGridModel.prototype._parseAlbum = function(xml, id){
	var data = {};
	
	// get prediction attributes: internal firehose id, rating, confidence and rank
	var prediction = $("prediction", xml).add($("vote", xml)); // simply latch on "vote" for now - may separate later
	data.id = prediction.attr("itemid");
	data.rating = prediction.attr("rating");
	data.confidence = prediction.attr("confidence");
	data.rank = prediction.attr("rank");
	
	// if given id is not undefined, then use this id as it was taken from the Item attribute instead of vote/prediction
	if(id) data.id = id;
	
	// get admin info if any
	if (Model.admin) {
		data.numVotes = prediction.attr("numconsiderations");
		data.maxRep = $("mostsignificantconsiderations > user", xml).attr("id");
	}
	
	// if it was vote, then put rating into a vote variable
	var vote = $("vote", xml).attr("rating");
	data.vote = vote || 0;
	
	// get ASIN
	data.asin = $("ASIN", xml).eq(0).text();
	
	// get the artist/author info
	var str = $("Artist", xml).text();
	if (str == "") 
		str = $("Creator", xml).text();
	if (str == "") {
		var authors = $("Author", xml)
		str = (authors.length > 2) ? "Various Artists" : authors.text();
	}
	data.artist = str;
	
	// get album title
	data.title = $("Title", xml).eq(0).text();
	
	// get album art
	data.mediumArt = {
		URL: $("MediumImage > URL", xml).eq(0).text(),
		width: parseInt($("MediumImage > Width", xml).eq(0).text()),
		height: parseInt($("MediumImage > Height", xml).eq(0).text())
	}
	if (!data.mediumArt.URL) 
		data.mediumArt.URL = "http://ec1.images-amazon.com/images/G/01/nav2/dp/no-image-avail-img-map._V46862177_AA192_.gif";
	
	// get genre info
	data.genreIds = [];
	str = $("genres", xml).eq(0).text();
	if(typeof(str) == "string") $.each(str.split(","), function(i, genreID){
		if(data.genreIds.indexOf(genreID) == -1) data.genreIds.push(genreID);
	});
	
	// set flag that states whether reviews have been fully loaded
	data.reviewsLoaded = false;
	data.firstReviewLoaded = false;
	
	// for new queries, merge extra data - todo unify parsing
	$.extend(data, this._parseAlbumDetail(xml));
	
	// add to AllData for centralized storage
	Model.allData[data.id] = data;
	
	return data;
};

// Model - parse an Item Lookup and return JSON object
AlbumGridModel.prototype._parseAlbumDetail = function(xml){
	// create new data object to hold parsed album detail
	var data = {};
	
	// get album detail
	data.detailPageURL = $("DetailPageURL", xml).eq(0).text();
	
	// get art
	data.largeArt = {
		URL: $("LargeImage > URL", xml).eq(0).text(),
		width: parseInt($("LargeImage > Width", xml).eq(0).text()),
		height: parseInt($("LargeImage > Height", xml).eq(0).text())
	}
	if (!data.largeArt.width || !data.largeArt.height){
		data.largeArt.width = 500;
		data.largeArt.height = 500;
	}
	if (!data.largeArt.URL) 
		data.largeArt.URL = $("MediumImage > URL", xml).eq(0).text();
	if (!data.largeArt.URL) 
		data.largeArt.URL = "http://ec1.images-amazon.com/images/G/01/nav2/dp/no-image-avail-img-map._V46862177_AA192_.gif";
	//if (!data.mediumArt.URL)
	//	data.mediumArt.URL = data.largeArt.URL;
		
	// get extra info
	data.label = $("Label", xml).eq(0).text();
	data.binding = $("Binding", xml).eq(0).text();
	data.released = $("ReleaseDate", xml).eq(0).text();
	data.origReleased = $("OriginalReleaseDate", xml).eq(0).text();
	
	// get cd asin
	data.cdasin = $("CDASIN", xml).eq(0).text();
	
	// format is multiple
	data.format = [];
	$("Format", xml).each(function(i){
		data.format.push($(this).text());
	});
	
	// get price info, incl date stamp
	data.listPrice = $("ListPrice > FormattedPrice", xml).text();
	data.lowestPrice = $("LowestNewPrice > FormattedPrice", xml).text();
	data.priceDateStamp = "***"; // TODO do price date stamp
	var orig = parseInt($("ListPrice > Amount", xml).text());
	var lowest = parseInt($("LowestNewPrice > Amount", xml).text());
	data.priceSavings = "$" + (orig - lowest) / 100;
	
	// get editorial review - todo iterate over multiple editorial reviews
	data.editorials = [];
	$("EditorialReview", xml).each(function(i){
		data.editorials.push({
			source: $("Source", this).text(),
			content: $("Content", this).text()
		})
	});
	
	// todo release date?
	//var aDate = new Date(data.releaseDate[0], data.releaseDate[1], data.releaseDate[2]);
	
	// set tracks
	var discs = [];
	var tracks;
	$("Disc", xml).each(function(i){
		tracks = [];
		$("Track", this).each(function(j){
			tracks.push($(this).text());
		})
		discs.push(tracks);
	});
	data.discs = discs;
	
	return data;
};



/*
 * SuperClass View for ALL Album Grid Views (Discover, MyVotes, Search, etc.) 
 */
function AlbumGridView(model, contentArea){
	this.model = model;
	this.contentArea = contentArea; // pointer to my content area
	this.topControls = $(".topControls", contentArea.parent());
	this.windowWidth = null;
	this.name = ""; // subclasses will set as "discover", "myVotes", or "search"
	this.albumPlaying = null; // holds the currently playing album
	this.postponedVoteCleanup = null; // holds posponed cleanup function for votes applied while song playing
	this.notFoundMessage = ""; // override in subclasses to display specific message
	
	// observe my model
	this.model.addObserver(this);
	
	// set grid settings
	this.gridSettings = {
		leftNavWidth: 0,
		rowHeight: 230,
		w: 160,
		albumWidth: 160,
		albumsTop: 140
	}
	
};

// broadcast from observed model
AlbumGridView.prototype.broadcast = function(eventStr){
	var me = this;
	switch (eventStr){
		case "addition":
			this.addition();
			break;
		case "deletion":
			this.deletion();
			break;
		case "refresh":
			this.refresh();
			break;
		case "clear":
			this.clear();
			break;
	}
};


// addition to model - add new albums to view
AlbumGridView.prototype.addition = function(){
	var updateData = this.model.getAllUpdate();
	
	// if admin mode then get introspection data first before creating albums
	var me = this;
	if (Model.showAdmin) {
		var request = me.model.request;
		Controller.doIntrospect(updateData, function(){
			me.set({
				request: request,
				albums: updateData
			});
		});
	} else {
		this.set({
			request: this.model.request,
			albums: updateData
		});
	}
};

// remove albums from view
AlbumGridView.prototype.deletion = function(){
	this.remove(this.model.getAllUpdate());
};


// refresh full display
AlbumGridView.prototype.refresh = function(){
	// refresh all albums
	this.clear();
	this.set({
		request: {},
		albums: this.model.getAllData()
	});
};

// add given albums to view.  data is in form { request: requestObj, albums: arrayOfAlbums }
AlbumGridView.prototype.set = function(data){
	// hide loading indicator & bottom pager
	View.hideLoading();
	this.hideBottomPager();
	
	// show any message if needed
	this.hideMessage();
	if (data.albums.length == 0){
		this.showMessage(this.notFoundMessage);
		return;
	}
	
	this.cycle = {
		data: data.albums,
		index: 0,
		timeoutId: 0
	}
	
	this.stepDisplay();
};

AlbumGridView.prototype.stepDisplay = function(){
	// if at end, cleanup & exit 
	if (this.cycle.index >= this.cycle.data.length) {
		this.stopStepping();
		return;
	}
	
	// create album & position it
	var data = this.cycle.data[this.cycle.index];
    var album = this._createAlbum(data);
	
	// size album
	this._sizeAlbum(album);
	
	// add introspection data
	if (Model.showAdmin) this.applyIntrospect(album, data);
	
    // next step
	this.cycle.index++;
	var me = this;
	this.cycle.timeoutId = setTimeout(function(){
			me.stepDisplay();
	}, 1);
};

// override in subclasses to change method of sizing
AlbumGridView.prototype._sizeAlbum = function(album){
    album.css({
        width: this.gridSettings.w,
		height: this.gridSettings.rowHeight
    });
	$(".albumArt", album).width(this.gridSettings.albumWidth).height(this.gridSettings.albumWidth);
	$(".shadow", album).width(this.gridSettings.albumWidth-4).height(this.gridSettings.albumWidth-4);
	$(".shadow2", album).width(this.gridSettings.albumWidth).height(this.gridSettings.albumWidth);
    $(".albumList", this.contentArea).append(album);
}

// cleanup album grid after step display
AlbumGridView.prototype.stopStepping = function(){
	if (this.cycle) {
		clearTimeout(this.cycle.timeoutId);
		this.cycle = {
			data: [],
			index: 0,
			timeoutId: 0
		}
	}
	
	$(".bottomPager", this.contentArea.parent()).show();
};

// attach introspection data to album
AlbumGridView.prototype.applyIntrospect = function(album, data){
	$(".introspect", album).remove();
	if(typeof(data.ratings) != "object" || data.ratings.length == 0) return;
	var ul = $('<ul class="introspect"></ul>').prependTo(album);
	ul.append('<li title="prediction">p[' + roundNumber(data.prediction, 2) + ']</li>');
	ul.append('<li title="confidence">c[' + roundNumber(data.confidence, 2) + ']</li>');
	ul.append('<li title="confwpred">cp[' + roundNumber(data.confwpred, 2) + ']</li>');
	ul.append($('<li title="highest rater">high[<a href="javascript:void(0)">' + data.ratings[0].userId + '</a>]</li>')
		.find("a").click(function(){
			View.triggerTab("otherVotes");
			var id = data.ratings[0].userId;
			Controller.otherVotes({ name: id, id: id });
		}).end()
	);
};


// remove these items
AlbumGridView.prototype.remove = function(data){
	var me = this;
	$.each(data, function(i, obj){
		$(".albumList ." + obj.id, me.contentArea).remove();
	});
};


AlbumGridView.prototype.clear = function(){
	// stop any stepping
	this.stopStepping();
	
	// remove all albums
	$(".albumList .album", this.contentArea).remove();
	
	// reset num rows
	this.calcGrid();
};

// Update layout
AlbumGridView.prototype.update = function(){
};

// call back from Model.addVote
AlbumGridView.prototype.voteAction = function(id, vote){
	// get album
	var album = $("." + id, this.contentArea);
	
	// if album playing, then postpone removing the album until after music plays
	var me = this;
	if(album.hasClass("playing")){
		this.postponedVoteCleanup = [id, function(){
			me.voteAction(id, vote);
			me.postponedVoteCleanup = null;
		}];
		return;
	}
	
	// set post animation cleanup function
	var cleanup = function(){
		album.remove();
		me.update();
	}
	
	// do feedback animation
	switch (Math.floor(vote)) {
		case 0:
		case 1:
		case 2:
			album.animate({
				top: "+=100",
				opacity: 0
			}, {
				duration: 500,
				complete: cleanup
			});
			break;
		case 3:
			album.fadeOut(500, cleanup);
			break;
		case 4:
		case 5:
			album.animate({
				top: "-=100",
				opacity: 0
			}, {
				duration: 500,
				complete: cleanup
			});
			break;
	}
};


// implement in subclasses for specific behavior
AlbumGridView.prototype.windowResized = function(){
	// update grid
	this.calcGrid();
	
	// update layout
	this.update();
};


// If there are top controls, show them
AlbumGridView.prototype.sizeForTopControls = function(ms){
	var tc = $(".topControls", this.contentArea.parent());
	if(tc.children().length){
		tc.show();
		this.contentArea.css({ "top": "30px" });
	} else {
		tc.hide();
		this.contentArea.css({ "top": "0" });
	}
};


// Show generic message on screen
AlbumGridView.prototype.showMessage = function(html){
	this.hideMessage();
	var message = $("<p class='message'></p>").html(html);
	this.contentArea.append(message);
};


AlbumGridView.prototype.hideMessage = function(){
	$(".message", this.contentArea).remove();
};


// Create a return a jQuery album from XML data
AlbumGridView.prototype._createAlbum = function(data, alternateAlbum){
	
	// create an album from template (use alternate if alternate album structure is given - should be cloned already)
	var album = (alternateAlbum) ? alternateAlbum : TemplateCode.album.clone(true);
	
	// store data in dom object for easy retrieval
	album.addClass(data.id);
	album.data("id", data.id);
	album.data("rank", data.rank);
	
	// artist info
	$(".artist a", album).html(data.artist).click(function(){
		Controller.search(data.artist);
	});
	
	// set height larger for bigger named artist
	if (data.artist.length > 20) $(".artist", album).css("height", "2.2em");
	
	// click function for album detail (Model may do a server lookup, so pass a callback function)
	var me = this;
	var clickFunction = function(){
		var id = album.data("id");
		me.model.getAlbumDetail(id, function(albumData){
			Controller.albumDetailRequest(albumData)
		});
		return false;
	}
	
	// title & album art
	$(".title", album).html(data.title).history(clickFunction);
	$(".albumArt", album)
		.attr("src", data.mediumArt.URL).width(1).height(1).click(clickFunction);
	
	// Uncomment to show local art
	// $(".albumArt", album).attr("src", "images/testAlbumPic.jpg");
	
	// graphical star rating
	var id = data.id;
	$(".starRating", album).srating({
		numSteps: 10,
		numStars: 5,
		rating: data.vote,
		tooltip: View.tooltip,
		tips: View.starTooltipContent,
		callback: function(el, vote){
			Controller.userVote(id, vote);
		}
	});
	
	// add custom folder menu action
	$(".foldericon", album).click(function(e){
		View.customTabs.folderMenu(album, data, {x: e.pageX, y: e.pageY});
		return false;
	});
			
	// album hover - show controls
	album.hover(function(){ me.albumHover(album, data); },
		function(){ me.albumHoverOut(album); });
	
	// make zoomed art react
	$(".albumArt", album).mousedown(function(){
		// unzoom art
		$(".albumArt", album).css({
			left: 0,
			top: 0
		})
	});
	
	$(".albumArt", album).mouseup(function(){
		// unzoom art
		$(".albumArt", album).css({
			left: -2,
			top: -2
		})
	});
	
	// ADMIN: admin info display
	if (Model.showAdmin && data.numVotes) {
		var conf = Math.round(data.confidence * 10000) / 10000;
		$('<p class="a_info">maxRep:<a class="maxRep" href="javascript:void(0)">' + data.maxRep + '</a> | ' +
			'<a class="genreInfo" href="javascript:void(0)">GenreProfile</a><br />votes:' + data.numVotes +
			'conf:'+ conf +'</p>')
			.insertAfter($(".title", album))
			.find("a.maxRep").click(function(){
				Model.a_viewUser = data.maxRep;
				Controller.voteDirty = true;
				View.triggerTab("myVotes");
			}).end()
			.find("a.genreInfo").click(function(){
				Controller.genreProfileGenerator.start(data.maxRep);
			});
	}
	
	// hide the controls by default
	$(".controls", album).hide();
	
	return album;
};

// hover effects
AlbumGridView.prototype.albumHover = function(album, data){
	// set global HoveredAlbum for catching swf commands (see function a2z_1_AMPlayerProd_DoFSCommand)
	HoveredAlbum = album;
	
	// create player if not there
	var me = this;
	var fdiv = $(".flashContent", album);
	fdiv.parent().addClass("loading"); // adding loading indicator
	if(fdiv.children().length == 0){
		me.createPlayer(album, fdiv.get(0), data.asin);
	}
	
	// exit if its to be kept open
	if(album.data("keepOpen")) return; // this is set externally by anybody who wants to keep the controls open
	
	var controlsWidth = (album.hasClass("albumMultiSearch") || album.hasClass("listAlbum")) ? 120 : album.width()
	$(".controls", album).width(controlsWidth).height("auto").css("opacity", 1).show(); // .hide().fadeIn(300);
	
	// zoom art
	$(".albumArt", album).css({
		left: -2,
		top: -2
	})
}

// hover out
AlbumGridView.prototype.albumHoverOut = function(album){
	if (album.data("keepOpen")) 
		return; // this is set externally by anybody who wants to keep the controls open
	$(".flashContent", album).parent().removeClass("loading");
	$(".controls", album).height("auto").css("opacity", 1).stop().hide(); // .show().fadeOut(300); // width(150)
	// unzoom art
	$(".albumArt", album).css({
		left: 0,
		top: 0
	});
}

AlbumGridView.prototype.hideBottomPager = function(){
	$(".bottomPager", this.contentArea.parent()).hide();
}

AlbumGridView.prototype.createPlayer = function(album, playerDivID, asin){
    try {
        // a2z_1_so = new SWFObject("http://images.amazon.com/images/G/01/digital/music/swfs/AlbumSampler_2_Prod._V8494131_.swf?playlist_url=http%3A%2F%2Fwww.amazon.com%2Fgp%2Fdmusic%2Fmedia%2Fsample_xspf.xspf%2Fref%3Ddm_dp_smpl_all%3Fie%3DUTF8%26catalogItemType%3Dalbum%26ASIN%3DB000QZR386%26qid%3D1217606307%26sr%3D8-1&single_track_mode=0&allow_domain=www.amazon.com&notifications=1", "a2z_1_AMPlayerProd", "450", "100%", "8.0.0.0", "#FFFFFF");
        a2z_1_so = new SWFObject("http://images.amazon.com/images/G/01/digital/music/swfs/AlbumSampler_2_Prod._V8494131_.swf?playlist_url=http%3A%2F%2Fwww.amazon.com%2Fgp%2Fdmusic%2Fmedia%2Fsample_xspf.xspf%2Fref%3Ddm_dp_smpl_all%3Fie%3DUTF8%26catalogItemType%3Dalbum%26ASIN%3D"+ asin + "%26qid%3D1217606307%26sr%3D8-1&single_track_mode=0&allow_domain=www.amazon.com&notifications=1", "a2z_1_AMPlayerProd", "450", "100%", "8.0.0.0", "#FFFFFF");
        
		a2z_1_so.addVariable("amazonPort", encodeURIComponent("80"));
        a2z_1_so.addVariable("sessionId", encodeURIComponent("002-2367989-1478400"));
        a2z_1_so.addVariable("amazonServer", encodeURIComponent("www.amazon.com"));
        // a2z_1_so.addVariable("swfLocation", encodeURIComponent("http://images.amazon.com/images/G/01/digital/music/swfs/AlbumSampler_2_Prod._V8494131_.swf?playlist_url=http%3A%2F%2Fwww.amazon.com%2Fgp%2Fdmusic%2Fmedia%2Fsample_xspf.xspf%2Fref%3Ddm_dp_smpl_all%3Fie%3DUTF8%26catalogItemType%3Dalbum%26ASIN%3DB000QZR386%26qid%3D1217606307%26sr%3D8-1&single_track_mode=0&allow_domain=www.amazon.com&notifications=1"));
        a2z_1_so.addVariable("swfLocation", encodeURIComponent("http://images.amazon.com/images/G/01/digital/music/swfs/AlbumSampler_2_Prod._V8494131_.swf?playlist_url=http%3A%2F%2Fwww.amazon.com%2Fgp%2Fdmusic%2Fmedia%2Fsample_xspf.xspf%2Fref%3Ddm_dp_smpl_all%3Fie%3DUTF8%26catalogItemType%3Dalbum%26ASIN%3D"+ asin +"%26qid%3D1217606307%26sr%3D8-1&single_track_mode=0&allow_domain=www.amazon.com&notifications=1"));
        
		a2z_1_so.addVariable("amazonAmbLink", encodeURIComponent(""));
        a2z_1_so.addVariable("javascriptOn", "1");
        a2z_1_so.addParam("bgcolor", "#FFFFFF");
        a2z_1_so.addParam("salign", "LT");
        a2z_1_so.addParam("allowScriptAccess", "always");
        a2z_1_so.addParam("quality", "low");
        a2z_1_so.addParam("wmode", "transparent");
        a2z_1_so.setAttribute("width", "450");
        a2z_1_so.setAttribute("height", "27");
		
        var agt = navigator.userAgent;
        var reFirefox = new RegExp("firefox/", "i");
        var regx = agt.split(reFirefox);
        var ffVersion = 8;
        if (regx[1]) {
            var pts = regx[1].split(/[.]/);
            ffVersion = parseFloat(pts[0] + "." + pts[1]);
        }
        var minorVersion = parseFloat(navigator.ap);
        if ((navigator.appVersion.indexOf("Mac") != -1) || (ffVersion < 1.5)) {
            a2z_1_so.setAttribute("height", 27);
            a2z_1_so.setAttribute("width", 450);
            a2z_1_so.addVariable("oldFirefox", "1");
        }
        if (a2z_1_so.write(playerDivID)) {
            this.fp_resizePlayerSpace(album, 450, 27);
        }
        else {
            this.fp_resizePlayerSpace(album, 0, 0);
        }
    } 
    catch (err) {
        this.fp_resizePlayerSpace(album, 0, 0);
    }
    return false;
};

AlbumGridView.prototype.fp_resizePlayerSpace = function(album, w, h){
    if (h > 0) {
		$(".clipDiv", album).get(0).style.height = h;
		$(".playerControl", album).get(0).style.height = h;
    } else {
		$(".clipDiv", album).get(0).style.height = "";
		$(".playerControl", album).get(0).style.height = "";
    }
};

// stop flash music player on screen
AlbumGridView.prototype.stopMusicPlayer = function(exceptAlbum){
	// stop any playing albums
	var me = this;
	$(".playing", this.contentArea).each(function(i){
		var album = $(this);
		if(!exceptAlbum || (exceptAlbum != undefined && (album.data("id") != exceptAlbum.data("id")))){
			if ($.browser.msie) $(".flashContent", album).children().remove();
			me.stopMode(album);
			album.trigger("mouseleave");
		}
	});
	
	this.albumPlaying = null;
};


AlbumGridView.prototype.stopPlayerInAlbumPage = function(){
	var albumPage = $(".albumPage").eq(0);
	if (albumPage.length == 0) return;
	
	// for IE, we need to remove & recreate player to stop it
	if ($.browser.msie) {
		// remove player
		$(".flashContent", albumPage).children().remove();
		
		// get album data
		var data = View.display.model.albumData(albumPage.attr("id"));
		
		// recreate player
		View.display.createPlayer(albumPage, $(".flashContent", albumPage).get(0), data.asin);
		
	} else {
		// for ff & others, suffice to just show & hide player to stop it
		$(".albumPage .flashContent").hide(10, function(){
			$(".albumPage .flashContent").show();
		});
	}
};


// put album in play visual mode (keep album open)
AlbumGridView.prototype.playMode = function(album){
	if(album.hasClass("playing") || !album.hasClass("album")) return
	album.data("keepOpen", true);
	album.addClass("playing");
	// show playing background color - safari doesn't animate well
	if ($.browser.safari) album.css({ backgroundColor: "#fbb" });
	else album.animate({ backgroundColor: "#fbb" }, 1000);
};


AlbumGridView.prototype.stopMode = function(album){
	album.data("keepOpen", false);
	album.removeClass("playing");
	
	// turn off playing highlight
	if ($.browser.safari) {
		album.css({ backgroundColor: "transparent" });
	}
	else {
		album.animate({ backgroundColor: "white" }, 250,
			function(){ album.css({ backgroundColor: "transparent" });
		});
	}
	
	// unzoom art
	$(".albumArt", album).css({
		left: 0,
		top: 0
	});
	
	// do any postponed vote cleanup
	if(this.postponedVoteCleanup){
		if(album.data("id") == this.postponedVoteCleanup[0])
			this.postponedVoteCleanup[1]();
	}
};


// calculate num albums in grid - give nRows to override default setting of nRows
AlbumGridView.prototype.calcGrid = function(nRows){
	
	// set num rows
	var h = $(window).height() - this.gridSettings.albumsTop;
	var numRows = Math.floor((h + 60) / this.gridSettings.rowHeight);
	numRows = Math.max(3, numRows); // min is 3 rows
	
	// set num albums
	var rowWidth = Math.floor($(window).width() - this.gridSettings.leftNavWidth);
	var w = this.gridSettings.w + 35; // albums have margin-left: 35px
	var numPerRow = Math.floor(rowWidth / w);
	this.numAlbums = numPerRow * numRows * 3; // make it 3 times what fits on the screen
	
};



/*
 * Model for DiscoverView
 */
function DiscoverModel(){
	// call my base class constructor
	DiscoverModel.baseConstructor.call(this);
};

// subclass AlbumGridModel
OopExtend(DiscoverModel, AlbumGridModel);


// Model - get recommendations from server
DiscoverModel.prototype.loadAlbums = function(start, num, popularity, year_min, year_max){
	
	// prepare arguments for getting recommendations
	var args = { // ?q=beatles&ll=0&lc=10
		genre: Model.getCurrentGenre().id,	
		ll: start, // paging start page
		lc: Math.min(num, this.maxServerItems), // number per page
		maxpop: popularity
	}
	
	// set from and to date filters
	if(year_min) args["year_min"] = year_min;
	if(year_max) args["year_max"] = year_max;
	
	// execute search
	this._getFromServer("browse", args);
};


/*
 * Class: DiscoverNav - nav for Discover tab
 */
function DiscoverNav(model, contentArea){
	this.model = model;
	this.popularity = 100; // default is "Popular"
	this.sliderSteps = ["Obscure", "Medium", "Popular"];
	this.yearMin = null;
	this.yearMax = null;

	// setup "All Genres" browser
	var me = this;
	this.genreBrowser = new _PaneBrowser($("#genreBrowser"));
	this.genreBrowser.initialize(function(id){
		View.display.pager.setPage(1); // reset page to 1
		Controller.discover();
	});
	Model.setCurrentGenre(this.genreBrowser.model.currentGenre);
	
	// setup "My Genres" browswer (model is given by Controller to genreProfileView during initialize)
	this.genreProfileView = new _GenreProfileView($("#genreProfile"),
		function(aGenre){
			Controller.discoverAGenre(aGenre);
		}
	);
	
	// observe genreBrowser for genre changes to update Model's genre
	this.genreBrowser.model.addObserver(this);
	
	// setup release date filter
	$("#released select").change(function(){
		var arr = $("option:selected", this).val().split("-");
		me.yearMin = (arr[0] || null);
		me.yearMax = (arr[1] || null);
		Controller.discover();
	});
	
	// default release date filter
	$("#released option:contains('2000s')").attr("selected", "selected");
	this.yearMin = 2000;
	
	// setup pager
	this.pager = new PagerClass($(".pager", contentArea), {
		callback: function(n){
			Controller.discover();
		}
	});
	// add bottom pager
	this.pager.addDupeLoc($(".bottomPager", contentArea.parent()));
	this.pager.hide();
	
	// set View switcher
	$("#discoverViewSwitch a").eq(0).click(function(){
		Controller.switchDiscoverView("list", true);
		$(this).addClass("selected").siblings().removeClass("selected");
	});
	$("#discoverViewSwitch a").eq(1).click(function(){
		Controller.switchDiscoverView("grid", true);
		$(this).addClass("selected").siblings().removeClass("selected");
	});
};

// receive broadcasts from observed genreBrowser
DiscoverNav.prototype.broadcast = function(eventStr){
	//  update current genre if genre changed
	if(eventStr == "genreChange"){
		Model.setCurrentGenre(Model.genreBrowserModel.currentGenre);
	}
};


// Setup slider (call ONLY when slider is visible)
DiscoverNav.prototype.setupSlider = function(){
	// setup confidence slider
	var me = this;
	if (!this.sliderExists()){
		$("#popSlider").slider({
			min: 0,
			max: 100,
			stepping: 50,
			startValue: me.popularity,
			slide: function(e, ui){
				me.popularity = Math.round(ui.value);
				me.setSliderDisplay();
			},
			change: function(e, ui){
				me.popularity = Math.round(ui.value);
				me.setSliderDisplay();
				Controller.setDefault("popularity", me.popularity);
				me.pager.setPage(1); // reset to page 1
				Controller.discover();
			}
		});
		
	}
	// set popularity display
	this.setSliderDisplay();
};

DiscoverNav.prototype.destroySlider = function(){
	if (this.sliderExists()){
		$("#popSlider").slider("destroy");
	}
};

DiscoverNav.prototype.sliderExists = function(){
	return ($("#popSlider").slider("value", 0) != undefined);
};


DiscoverNav.prototype.setSliderDisplay = function(){
	$("#popularity span").html("(" + this.getSliderStep() + ")");
};


DiscoverNav.prototype.getSliderStep = function(){
	var n = Math.min(this.sliderSteps.length - 1, Math.floor(this.popularity * this.sliderSteps.length / 100));
	return this.sliderSteps[n];
};


	
/*
 * Class: DiscoverGridView - view for Discover tab
 *   Inherits from AlbumView
 */
function DiscoverGridView(model, contentArea, pager){
	DiscoverGridView.baseConstructor.call(this, model, contentArea);
	this.name = "discover";
	this.gridSettings.leftNavWidth = 183; // Discover view has a left genre navigation
	this.pager = pager;
	
	this.notFoundMessage = '<div class="copyContent">No albums matched that genre<br />' +
		'<small><ol><li>If you have not yet RATED any albums, you must go to <strong>Getting Started</strong> first</li>' +
		'<li>Or if you just finished <strong>Getting Started</strong>, please wait another minute and click a genre again</li>' +
		'<li>If none of the above, set Popularity higher or Released Date larger</li></ol></small></div>';
	
	// show default message
	this.showMessage.call(this, "Click a genre on the left side to start the recommendations");
};

// subclass AlbumGridView
OopExtend(DiscoverGridView, AlbumGridView);


// Override set to show the pager
DiscoverGridView.prototype.set = function(data){
	// show pager by default
	this.pager.show();
	
	// set extent of recommendation stats
	if(this.model.totalItems) this.updateRecStatsExtent(this.model.totalItems);
	
	// call superclass broadcast function
	DiscoverGridView.superClass.set.call(this, data);
};


// If there are top controls, make the panel lower to fit
DiscoverGridView.prototype.sizeForTopControls = function(ms){
	// call superclass sizeForTopControls function
	DiscoverGridView.superClass.sizeForTopControls.call(this, ms);
	
	var tc = $(".topControls", this.contentArea.parent());
	var sp = $(".sidePanel", this.contentArea.parent());
	if(tc.children().length){
		sp.css({ "top": "120px" });
	} else {
		sp.css({ "top": "90px" });
	}
};

// extend superclass' showMessage method to hide the pager
DiscoverGridView.prototype.showMessage = function(html){
	// call superclass showMessage function
	DiscoverGridView.superClass.showMessage.call(this, html);
	
	// hide pager
	this.pager.hide();
	
	// clear the rec stat
	this.clearRecStats();
};

DiscoverGridView.prototype.showRecStats = function(range){
	$(".recStats", this.contentArea.parent()).html('Your top '+ (range.start + 1) +'-'+ 
		(range.start + range.count) +' recommendations<span></span>...');
}

DiscoverGridView.prototype.updateRecStatsExtent = function(n){
	$(".recStats span", this.contentArea.parent()).html(" of " + n + " albums");
}

DiscoverGridView.prototype.clearRecStats = function(){
	$(".recStats", this.contentArea.parent()).empty();
}

DiscoverGridView.prototype.clear = function(){
	// call superclass
	DiscoverGridView.superClass.clear.call(this);
	
	// clear the rec stat
	this.clearRecStats();
};

// get start and album count for current page
DiscoverGridView.prototype.getAlbumRange = function(){
	var start = (this.pager.pageNum - 1) * this.numAlbums;
	return {
		start: start,
		count: this.numAlbums
	}
}


/*
 * Class: DiscoverListView - special alternate view that shows some reviews too
 * Inherits from AlbumView, pairs up with same model of DiscoverGridView
 */
function DiscoverListView(model, contentArea, pager){
	DiscoverListView.baseConstructor.call(this, model, contentArea, pager);
	
	// set variables specific to this view
	this.name = "discover";
	this.notFoundMessage = '<div class="copyContent">No albums matched that genre<br />' +
		'<small><ol><li>If you have not yet RATED any albums, you must go to <strong>Getting Started</strong> first</li>' +
		'<li>Or if you just finished <strong>Getting Started</strong>, please wait another minute and click a genre again</li>' +
		'<li>If none of the above, set Popularity higher or Released Date larger</li></ol></small></div>';
	
	// set grid settings
	this.gridSettings = {
		leftNavWidth: 0,
		rowHeight: 140,
		w: "100%",
		albumWidth: 120,
		albumsTop: 140,
		section2w: 400 // width of div containing the reviews
	}
};

// subclass AlbumGridView
OopExtend(DiscoverListView, DiscoverGridView);


// Override set to update the width of section2
DiscoverListView.prototype.set = function(data){
	// update width of section2 according to available space
	this.gridSettings.section2w = $(window).width() - 520; // 520 is hardwired according to objects
	
	// call superclass broadcast function
	DiscoverListView.superClass.set.call(this, data);
	
	// If admin show code snippet link
	if(Model.admin) View.createLauncherLink();
};

// extend _createAlbum to add float left & other controls
DiscoverListView.prototype._createAlbum = function(data){
	// call superclass _createAlbum function with custom album
	var album = DiscoverListView.superClass._createAlbum.call(this, data, TemplateCode.albumListView.clone(true));
	
	// update the width of section2 div containing the review
	$(".section2", album).width(this.gridSettings.section2w);
	
	// set height larger for bigger named artist
	if (data.title.length > 20) $(".title", album).css("height", "2.2em");
	else $(".title", album).css("height", "1.2em");
	
	// show first review
	var aReview;
	var me = this;
	this.model.getReviews(data.id, function(data){
		if(data.reviews.length){
			// create review
			aReview = View.createReview(data.id, data.reviews[0]);
			
			// make review itself clickable to open detail
			$(".content", aReview).click(function(){
				me.model.getAlbumDetail(data.id, function(albumData){
					Controller.albumDetailRequest(albumData);
				});
				return false;
			});
			
			// attach review to album
			$("ol.customerReviews", album).append(aReview);
		}
	}, 0, 1);
	
	return album;
};

// hardwire only a certain # at a time
DiscoverListView.prototype.calcGrid = function(nRows){
	this.numAlbums = 20;
};


// on update, update the width of section2 (holding reviews)
DiscoverListView.prototype.update = function(){
	this.gridSettings.section2w = $(window).width() - 540; // 540 is hardwired according to objects
	$(".album .section2", this.contentArea).width(this.gridSettings.section2w);
}


/*
 * Model for a User's Votes view
 */
function UserVotesModel(){
	// call my base class constructor
	UserVotesModel.baseConstructor.call(this);
	this.maxServerItems = 180;
	this.currentId; // holds most recent userId used
	this.minVote = 1;
};

// subclass AlbumGridModel
OopExtend(UserVotesModel, AlbumGridModel);


// Model - get votes (give null for userId if doing MyVotes)
UserVotesModel.prototype.loadAlbums = function(start, num, userId, options){
	// remember this id for now
	this.currentId = userId;
	
	// default options
	options = jQuery.extend({
		sort: "rating",
		errorFunction: null
	}, options);
	
	var me = this;
	if (LocalFlag) {
		$.get("localdata/sampleMyVotes.xml", function(xml){
			me._process(xml);
		});
	}
	else {
		var url = "/fhws/myvotes";
		var args = {
			ll: start, // paging start page
			lc: Math.min(num, this.maxServerItems), // number per page
			o: options.sort
		}
		
		// if using another user id, change some options
		if (userId) {
			url = "/fhws/admin/votes";
			args.userid = userId;
		}
		
		this.XMLHttpRequest = $.ajax({
			url: url,
			data: args,
			cache: false,
			error: function(XMLHttpRequest){
				if (options.errorFunction) options.errorFunction();
				View.showServerError(XMLHttpRequest);
			},
			success: function(xml, status){
				me._process(xml);
			}
		});
	}
};


// sort albums by "rating", "rank", "artist" or "title"
UserVotesModel.prototype.sort = function(sortBy){
	var me = this;
	this.allData.sort(function(a,b){
		var flag = null;
		switch(sortBy){
			case "rating":
				flag = b.vote > a.vote ? 1 : -1;
				break;
			case "rank":
				flag = b.rank > a.rank ? 1 : -1;
				break;
			case "artist":
				flag = a.artist > b.artist ? 1 : -1;
				break;
			case "title":
				flag = a.title > b.title ? 1 : -1;
		}
		return flag;
	});
	this.updateData = this.allData;
};

// filter albums on criteria ("minVote", "genre", etc.)
UserVotesModel.prototype.filter = function(options){
	var me = this;
	this.updateData = this.allData;
	
	$.each(options, function(option, value){
		switch(option){
			case "minVote":
				me.updateData = $.grep(me.updateData, function(album){
					return (album.vote >= value);
				});
				break;
			case "genre":
				me.updateData = $.grep(me.updateData, function(album){
					return ($.inArray(value, album.genreIds) != -1);
				});
				break
		}
	});
};


/* 
 * Class UserVotesNav
 * Nav for UserVotes & MyVotes
 */

function UserVotesNav(model, contentArea){
	this.model = model;
	this.contentArea = contentArea;
	
    // setup sort control
    var me = this;
	this.model.sortBy = $(".sortBy", this.contentArea.parent()).val();
    $(".sortBy", contentArea.parent()).change(function(){
        me.model.sortBy = $(".sortBy", contentArea.parent()).val();
        Controller.setDefault("sortBy", me.model.sortBy);
		me.pager.setPage(1);
		View.display.refresh();
    });
	
	// setup pager
	this.pager = new PagerClass($(".pager", contentArea), {
		callback: function(n){
			View.display.refresh();
		}
	});
	// add bottom pager
	this.pager.addDupeLoc($(".bottomPager", contentArea.parent()));
};


// Setup slider (call ONLY when slider is visible)
UserVotesNav.prototype.setupSlider = function(){
	// setup stars slider
	var me = this;
	if (!this.sliderExists()){
		$(".starsSlider", this.contentArea.parent()).slider({
			min: 1,
			max: 10,
			stepping: 1,
			startValue: Math.round(me.model.minVote * 2),
			slide: function(e, ui){
				me.model.minVote = ui.value / 2;
				me.setSliderDisplay();
			},
			change: function(e, ui){
				me.model.minVote = ui.value / 2;
				me.setSliderDisplay();
				Controller.setDefault("minVote", me.model.minVote);
				me.pager.setPage(1);
				View.display.refresh();
			}
		});
		
	}
	// set popularity display
	this.setSliderDisplay();
};

UserVotesNav.prototype.setSliderDisplay = function(){
	var n = Math.floor(this.model.minVote);
	$(".minStars span", this.contentArea.parent()).html((n? n : "") + ((this.model.minVote % 1) ? "&frac12;" : ""));
};


UserVotesNav.prototype.destroySlider = function(){
	if (this.sliderExists()){
		$(".starsSlider", this.contentArea.parent()).slider("destroy");
	}
};

UserVotesNav.prototype.sliderExists = function(){
	return ($(".starsSlider", this.contentArea.parent()).slider("value", 0) != undefined);
};

// set sortBy and the update droplist.  Sortby can be "rank", "rating", "artist", or "title" - does not trigger refresh
UserVotesNav.prototype.setSortBy = function(str){
	this.model.sortBy = str;
	$(".sortBy option[value=" + str + "]", this.contentArea.parent()).attr("selected", "selected");
};

// set minimum votes externally - does not trigger refresh
UserVotesNav.prototype.setMinVote = function(n){
	n = parseFloat(n);
	this.model.minVote = n;
	$(".minVote option:selected", this.contentArea.parent()).removeAttr("selected");
	$(".minVote option", this.contentArea.parent()).eq(parseInt(n * 2 - 1)).attr("selected", true);
};


/*
 * Class: UserVotesView - a user's votes View
 * Inherits from AlbumView
 */
function UserVotesView(model, contentArea, pager){
    UserVotesView.baseConstructor.call(this, model, contentArea, pager);
	
    // set variables specific to this view
	this.pager = pager;
    this.name = "userVotes";
    this.gridSettings.leftNavWidth = 183; // MyVotes view also has a left genre navigation
    this.notFoundMessage = "No votes yet on any albums";
	
};

// subclass AlbumGridView
OopExtend(UserVotesView, AlbumGridView);


// override superclass to simply refresh (added albums change sort & filter so refresh all)
UserVotesView.prototype.addition = function(){
	this.refresh();
};


// overrides superclass to filter & sort the model and then request a single page
UserVotesView.prototype.refresh = function(){
	// clear albums
	this.clear();
			
	// sort & filter albums & get update data
	if (this.model.sortBy == "rating") this.model.sort("artist"); // first sort alphabetical if by vote
	this.model.sort(this.model.sortBy);
	this.model.filter(this.getFilterOptions());
	
	// get update data
	var updateData = this.model.getUpdate(this.getAlbumRange());
	
	// bound the pager
	this.pager.setMaxPage(Math.ceil(this.model.getAllUpdate().length / this.numAlbums));
	
	// if admin mode then get introspection data first before creating albums
	var me = this;
	if (Model.showAdmin) {
		Controller.doIntrospect(updateData, function(){
			me.set({
				request: me.model.request,
				albums: updateData
			});
		});
	} else {
		this.set({
			request: this.model.request,
			albums: updateData
		});
	}
};

// get start and album count for current page
UserVotesView.prototype.getAlbumRange = function(){
	var start = (this.pager.pageNum - 1) * this.numAlbums;
	return {
		start: start,
		count: this.numAlbums
	}
}

// override in subclasses to change filter options given to model on this.model.filter(options)
UserVotesView.prototype.getFilterOptions = function(){
	return { minVote: this.model.minVote };
};


/* VISUALLY filter low vote albums or ones that don't match current genre filter
  - used for quick filtering instead of refreshing whole screen */
UserVotesView.prototype.filterAlbums = function(){
	var me = this;
	$(".album", this.contentArea).hide().each(function(){
		me._filterAlbum($(this));
	});
};

// VISUALLY filter a particular albums - used for quick filtering instead of refreshing whole screen
UserVotesView.prototype._filterAlbum = function(album){
	album = $(album);
	var data = this.model.albumData(album.data("id"));
	if ($(".starRating", album).data("rating") < this.model.minVote) return;
	else album.show();
};


// extend _createAlbum to keep stars visible
UserVotesView.prototype._createAlbum = function(data){
	// modify album template so the stars are always visible
	var album = TemplateCode.album.clone(true);
	$(".voteCol", album).insertAfter($(".controls", album));
	
	// call superclass _createAlbum function
	album = UserVotesView.superClass._createAlbum.call(this, data, album);
	
	return album;
};

UserVotesView.prototype.showAdminControls = function(){
	// if admin, setup repset droplist
	var uIds = $("#a_repList", this.topControls);
	if (Model.showAdmin && !uIds.length) {
		// create div & select control + action
		var uIds = $('<li id="a_repList">Switch to: <select name="userList"></select></li>').appendTo($(".topControls", this.contentArea.parent()));
		var sl = $("select", uIds).append($('<option value="self">Self</option>')).change(function(){
			var str = $("select option:selected", uIds).eq(0).attr("value");
			Model.a_viewUser = (str == "self") ? null : str;
			Controller.myVotes();
		});
		
		// add all repSet option entries
		$.each(Model.a_repSet, function(i, rep){
			sl.append($('<option value="' + rep.id + '">ID' + rep.id + ' [' + rep.value + ']</option>'));
		});
		
		// pre-select user and set correct page title
		if (Model.a_viewUser) {
			sl.val(Model.a_viewUser); // select user
		}
		
		// add launcher for repSetviewer
		var url = (LocalFlag) ? "/DiscoverMyMusic/repViewer/repViewer.html" : "/repViewer/repViewer.html"
		$('<a href="'+ url +'?user='+ Model.admin +'" target="_blank"> Launch Viewer</a>').appendTo(uIds).css("cursor", "pointer");
	}
};


// override album vote to simply refilter that particular album after vote
UserVotesView.prototype.voteAction = function(album, vote){
	this._filterAlbum(album);
};


// If there are top controls, make the panel lower to fit
UserVotesView.prototype.sizeForTopControls = function(ms){
	// call superclass sizeForTopControls function
	UserVotesView.superClass.sizeForTopControls.call(this, ms);
	
	var tc = $(".topControls", this.contentArea.parent());
	var sp = $(".sidePanel", this.contentArea.parent());
	if(tc.children().length){
		sp.css({ "top": "120px" });
	} else {
		sp.css({ "top": "90px" });
	}
};


/*
 * Model for MyVotes view - extends userVotes to deal with my genre profile
 */
function MyVotesModel(){
	MyVotesModel.baseConstructor.call(this);
	this.maxServerItems = 1000;
	this.sortBy = null;
};

// subclass AlbumGridModel
OopExtend(MyVotesModel, UserVotesModel);


// Model - get votes
MyVotesModel.prototype.loadAlbums = function(start, num, options){
	// if start equals 0, then we're reloading the whole myVotes, so reset genreProfile structure
	if (start == 0)
		View.discoverNav.genreProfileView.model._gProfileWork = {};
	
    // now call superclass
    MyVotesModel.superClass.loadAlbums.call(this, start, num, null, options);
}


MyVotesModel.prototype._parseAlbum = function(xml){
    // call superclass _createAlbum function
    var data = MyVotesModel.superClass._parseAlbum.call(this, xml);
    
    // weigh genre while parsing to create genre profile
	var me = this;
	var genres = [];
	var str = $("genres", xml).eq(0).text();
	if(typeof(str) == "string") $.each(str.split(","), function(i, genreID){
		if(genres.indexOf(genreID) == -1) {
			genres.push(genreID);
			View.discoverNav.genreProfileView.model.weighGenre(genreID, data.vote);
		}
	});
    
    return data;
};




/* 
 * Class MyVotesNav
 * Nav for MyVotes
 */
function MyVotesNav(model, contentArea){
	MyVotesNav.baseConstructor.call(this, model, contentArea);
	
    // setup genre profile view (model is given by Controller to genreProfileView during initialize)
	var me = this;
    this.genreProfileView = new _MyVotesGenreProfileView($("#genreProfileMyVotes"), function(aGenre){
        // set title of content area to genre name
        if (aGenre.id == 0) 
            $(".contentTitle", me.contentArea).html("My Votes");
        else
            $(".contentTitle", me.contentArea).html(aGenre.name + ' Votes').show();
        
        // refresh full view
		me.pager.setPage(1);
        View.display.refresh();
        
        // reset scroll
        View.scrollTo(0, 500);
    });
	
	// set View switchers
	$("#myVotesViewSwitch a").eq(0).click(function(){
		Controller.switchMyVotesView("list", true);
		$(this).addClass("selected").siblings().removeClass("selected");
	});
	$("#myVotesViewSwitch a").eq(1).click(function(){
		Controller.switchMyVotesView("grid", true);
		$(this).addClass("selected").siblings().removeClass("selected");
	});
};

// subclass UserVotesNav
OopExtend(MyVotesNav, UserVotesNav);


/*
 * View for MyVotes View
 */
function MyVotesGridView(model, contentArea, pager){
	MyVotesGridView.baseConstructor.call(this, model, contentArea, pager);
	
	// set variables specific to this view
	this.name = "myVotes";
	this.notFoundMessage = "No votes yet on any albums <small>(If you do have votes, click MyVotes again)</small>";

}

// subclass AlbumGridModel
OopExtend(MyVotesGridView, UserVotesView);


// extend superclass to add genre filter
MyVotesGridView.prototype.getFilterOptions = function(){
	// get options from superclass
	var options = MyVotesGridView.superClass.getFilterOptions.call(this);
	
	// extend with genre filter if genre set
	if(View.myVotesNav.genreProfileView.currentGenre) options.genre = View.myVotesNav.genreProfileView.currentGenre;
	return options;
}

// calc genre profile after stopping
MyVotesGridView.prototype.stopStepping = function(){
	// call superclass
	MyVotesGridView.superClass.stopStepping.call(this);
	
	// cleanup genre profile
	View.myVotesNav.genreProfileView.model.calcGenreProfile();
};


// Overrides superclass to add genre filtering
MyVotesGridView.prototype._filterAlbum = function(album){
	album = $(album);
	var data = this.model.albumData(album.data("id"));
	if ($(".starRating", album).data("rating") < this.model.minVote) return;
	else if (View.myVotesNav.genreProfileView.currentGenre &&
			(data.genreIds.indexOf(View.myVotesNav.genreProfileView.currentGenre) == -1)) return;
		else album.show();
};


/*
 * List view for MyVotes
 */
function MyVotesListView(model, contentArea, pager){
	MyVotesListView.baseConstructor.call(this, model, contentArea, pager);
	
	// set variables specific to this view
	this.name = "myVotes";
	this.notFoundMessage = "No votes yet on any albums <small>(If you do have votes, click MyVotes again)</small>";
	
	// set grid settings
	this.gridSettings = {
		leftNavWidth: 0,
		rowHeight: 140,
		w: "100%",
		albumWidth: 120,
		albumsTop: 140,
		section2w: 400 // width of div containing the reviews
	}
};

// subclass AlbumGridView
OopExtend(MyVotesListView, MyVotesGridView);


// Override set to update the width of section2
MyVotesListView.prototype.set = function(data){
	// update width of section2 according to available space
	this.gridSettings.section2w = $(window).width() - 540; // 540 is hardwired according to objects
	
	// call superclass broadcast function
	MyVotesListView.superClass.set.call(this, data);
	
	// If admin show code snippet link
	if(Model.showAdmin) View.createLauncherLink();
};

// extend _createAlbum to add float left & other controls
MyVotesListView.prototype._createAlbum = function(data){
	// Use discoverListView's create album as it's identical
	return DiscoverListView.prototype._createAlbum.call(this, data);
};

// hardwire only a certain # at a time
MyVotesListView.prototype.calcGrid = function(nRows){
	this.numAlbums = 20;
};

// on update, update the width of section2 (holding reviews)
MyVotesListView.prototype.update = function(){
	this.gridSettings.section2w = $(window).width() - 540; // 540 is hardwired according to objects
	$(".album .section2", this.contentArea).width(this.gridSettings.section2w);
}



/*
 * Model for Search View
 */
function SearchModel(){
	// call my base class constructor
	SearchModel.baseConstructor.call(this);
	this.searchStr = null;
};

// subclass AlbumGridModel
OopExtend(SearchModel, AlbumGridModel);


// Model
SearchModel.prototype.loadAlbums = function(start, num, searchStr){
	// remember for others to query
	this.searchStr = searchStr;
	
	// prepare arguments for search
	var args = { // ?q=beatles&ll=0&lc=10
		genre: View.discoverNav.genreBrowser.rootId,
		q: searchStr, // this.getCurrentGenre().name,
		ll: start, // paging start page
		lc: Math.min(num, this.maxServerItems) // number per page
	};
	
	// execute search
	this._getFromServer("search", args);
};


/*
 * Class: SearchView - Search Tab View
 * Inherits from AlbumView
 */
function SearchView(model, contentArea){
	SearchView.baseConstructor.call(this, model, contentArea);
	
	// set variables specific to this view
	this.name = "search";
	this.leftNavWidth = 0;
	this.notFoundMessage = "No albums match that query.  Please try again.";
};

// subclass AlbumGridView
OopExtend(SearchView, AlbumGridView);


SearchView.prototype.clear = function(){
	// call superclass
	SearchView.superClass.clear.call(this);
	
	// clear the section titles & other misc.
	$(".albumList", this.contentArea).empty();
};

// override album vote to do no special visual update to voting action for search section
SearchView.prototype.voteAction = function(album, vote){
};


/*
 * Model for Collection View
 */
function CollectionModel(tabName){
	// call my base class constructor
	CollectionModel.baseConstructor.call(this);
	this.tabName = tabName;
};

// subclass AlbumGridModel
OopExtend(CollectionModel, AlbumGridModel);


// Model for getting for album data for list of ids
CollectionModel.prototype.loadAlbums = function(){

	// get album ids for this album from storage
	var me = this;
	Model.getKeep("tab-" + this.tabName, function(ids){
		me._finishLoadAlbums(ids);
	});
};

// for collections the return result is simply a list of ids.  So get detail on those IDs now
CollectionModel.prototype._finishLoadAlbums = function(ids){
	// if no ids stored then just process an empty xml file
	if(ids.length == 0){
		this._process($('<?xml version="1.0" encoding="UTF-8"?><searchresult></searchresult>'));
		return;
	}
	
	// get album detail based on id
	var me = this;
	if (LocalFlag) {
		this.XMLHttpRequest = $.get("getitems.xml", function(xml){
			me._process(xml);
		});
	}
	else {
		this.XMLHttpRequest = $.ajax({
			url: "/fhws/getitems",
			data: {
				itemlist: ids.join()
			},
			cache: false,
			error: function(XMLHttpRequest) {
				View.showServerError(XMLHttpRequest);
			},
			success: function(xml, status){
				me._process(xml);
			}
		});
	}
};


// order changed, so save new order of albums (takes jQuery object representing ".album" instances on screen)
CollectionModel.prototype.updateOrder = function(albums){
	var ids = [];
	albums.each(function(i, album){
		ids.push($(this).data("id"));
	});
	Model.setKeep("tab-" + this.tabName, ids);
};


// save current albums
CollectionModel.prototype.saveAlbums = function(){
	var ids = [];
	$.each(this.allData, function(i, data){
		ids.push(data.id);
	});
	Model.setKeep("tab-" + this.tabName, ids);
};


// add new rename method for renaming the custom tab
CollectionModel.prototype.rename = function(newName){
	// keep old name for now
	var oldName = this.tabName;
	
	// save album list into new name storage
	this.tabName = newName;
	this.saveAlbums();
	
	// remove old storage
	Model.removeFromKeep("tab-" + oldName);
};


// remove data from storage -- in prep for total remove
CollectionModel.prototype.remove = function(){
	Model.removeKeep("tab-" + this.tabName);
};



/*
 * Class: CollectionView - a Custom Tab view
 * Inherits from AlbumView
 */
function CollectionView(model, contentArea){
	CollectionView.baseConstructor.call(this, model, contentArea);
	
	// set variables specific to this view
	this.name = "collection";
	this.loaded = false; // true only if the albums were loaded into view
};

// subclass AlbumGridView
OopExtend(CollectionView, AlbumGridView);


// extend _createAlbum to add float left & other controls
CollectionView.prototype._createAlbum = function(data){
	
	// modify album template so the stars are always visible
	var album = TemplateCode.album.clone(true);
	$(".voteCol", album).insertAfter($(".controls", album));
	$(".artDiv", album).prepend('<img class="sortIcon" alt="" title="Drag to sort albums" src="images/sortIcon.gif" />');
	
	// call superclass _createAlbum function
	album = CollectionView.superClass._createAlbum.call(this, data, album);
	
	return album;
};


CollectionView.prototype.albumHover = function(album, data){
	// call superclass
	CollectionView.superClass.albumHover.call(this, album, data);
	
	// show sort Icon
	$(".sortIcon", album).show();	
}


CollectionView.prototype.albumHoverOut = function(album){
	// call superclass
	CollectionView.superClass.albumHoverOut.call(this, album);
	
	// hide sort Icon
	$(".sortIcon", album).hide();	
}

// completely override set to just add albums to albumlist 
CollectionView.prototype.set = function(data){
	// hide loading indicator in case its showing
	View.hideLoading();
	
	// indicate this collection has been loaded
	this.loaded = true;

	// show any message if needed
	this.hideMessage();
	if(data.albums.length == 0) this.showMessage("No albums yet in this collection. " +
		"You can add albums with the folder icon on albums");

	var album;
	for (var i = 0; i < data.albums.length; i++) {
		// create an album object and append
		album = this._createAlbum(data.albums[i]);
		album.width(this.gridSettings.w);
		$(".albumArt", album).width(this.gridSettings.albumWidth).height(this.gridSettings.albumWidth);
		$(".shadow", album).width(this.gridSettings.albumWidth-2).height(this.gridSettings.albumWidth-2);
		$(".albumList", this.contentArea).append(album);
	}
	
	// This is sortable's options
	var me = this;
	var options = {
		update: function(){
			// save new order of albums (model takes jQuery albums)
			me.model.updateOrder($(".albumList .album:not(.ui-sortable-helper)", me.contentArea));
		},
		handle: ".sortIcon"
	};
	
	// work around for bug in sortable that doesn't allow empty lists to be initialized	
	if ($(".albumList li", this.contentArea).length == 0) {
		$(".albumList", this.contentArea).append("<li></li>").sortable(options).empty();
	}
	else {
		$(".albumList", this.contentArea).sortable(options);
	}
	
};


// extend clear with sortableDestroy
CollectionView.prototype.clear = function(data){
	// first remove sortable function
	if($(".album", this.contentArea).length) $(".albumList", this.contentArea).sortable("destroy");
	
	// call superclass clear function
	CollectionView.superClass.clear.call(this)
};


// add an album to the collection
CollectionView.prototype.addAlbum = function(data){

	// if collection loaded, prepend DOM object and update View's model
	if (this.loaded) {
		this.model.addAlbumData(data);
		$(".albumList", this.contentArea).sortable("refresh");
	}
	
	// add id to data store of item ids
	Model.addToKeep("tab-" + this.model.tabName, data.id);
};


// add an album to the collection
CollectionView.prototype.removeAlbum = function(data){
	
	// if collection loaded, remove data from Model to update View
	if (this.loaded) {
		this.model.removeData(data);
		$(".albumList", this.contentArea).sortable("refresh");
	}
	
	// remove id from data store of item ids
	Model.removeFromKeep("tab-" + this.model.tabName, data.id);
};

// override album vote to do no special visual update to voting action for collection section
CollectionView.prototype.voteAction = function(album, vote){
};


/*
 * Class: MultiSearchView - special view for multi-search
 * Inherits from AlbumView, pairs up with normal SearchModel
 */
function MultiSearchView(model, contentArea){
	MultiSearchView.baseConstructor.call(this, model, contentArea);
	
	// set variables specific to this view
	this.name = "multiSearch";
	this.notFoundMessage = "No albums matched that query.";
	
	// set grid settings
	this.gridSettings = {
		leftNavWidth: 0,
		rowHeight: 75,
		w: 275,
		albumWidth: 75,
		albumsTop: 140
		// maxSizeLevels: 0,
		// xMargin: 10,
		// yMargin: 10,
		// fontSize: 12
	}
};

// subclass AlbumGridView
OopExtend(MultiSearchView, AlbumGridView);


// extend _createAlbum to add float left & other controls
MultiSearchView.prototype._createAlbum = function(data, alternateAlbum){
	// call superclass _createAlbum function with custom album
	var album = MultiSearchView.superClass._createAlbum.call(this, data, (alternateAlbum || TemplateCode.albumMultiSearch.clone(true)));
	
	// make sure the artist never takes two lines
	$(".artist", album).css("height", "1.2em");
	
	// remove admin data for testing
	$("p.a_info", album).hide();
	
	// remove link from artist name
	$(".artist", album).unbind('click').html($(".artist a", album).html());
	
	return album;
};

// completely override set
MultiSearchView.prototype.set = function(data){
	
	// hide loading indicator in case its showing
	View.hideLoading();
	this.hideMessage();
	
	// find section created during gs_doMultiSearch (and remove class)
	var section = $('#section_'+ data.request.query.replace(/ /g, "_"), this.contentArea);
	
	// empty display
	if(data.albums.length == 0) section.append("<p>No albums match this query...</p>");

	// create albums and append to pre-made section
	var album;
	for (var i = 0; i < data.albums.length; i++) {
		// create an album object and append
		album = this._createAlbum(data.albums[i]);
		$(".albumArt", album).width(75).height(75);
		$(".shadow", album).width(73).height(73);
		$("ol", section).append(album);
	}
	
	// remove loading indicator
	section.removeClass("loading");
};

// disable normal update and just resize the sections as we're just floating left for multi-search view
MultiSearchView.prototype.update = function(data){
	// reset width of content area manually
	this.contentArea.width(Math.floor($(window).width()
		- this.gridSettings.leftNavWidth)
		- parseInt(this.contentArea.css("padding-left"))
		- parseInt(this.contentArea.css("padding-right")));
};

MultiSearchView.prototype.clear = function(){
	// call superclass
	var album = MultiSearchView.superClass.clear.call(this);
	
	// clear the section titles & other misc.
	$(".albumList", this.contentArea).empty();
};


/*
 * Class: GettingStartedView - special view for Getting Started - variant of multi-search
 * Inherits from MultiSearchView, pairs up with normal SearchModel
 */
function GettingStartedView(model, contentArea){
	GettingStartedView.baseConstructor.call(this, model, contentArea);
	
	// set variables specific to this view
	this.name = "gettingStarted";
};

// subclass AlbumGridView
OopExtend(GettingStartedView, MultiSearchView);

// extend _createAlbum to use special template code album and add "don't know it" link functionality
GettingStartedView.prototype._createAlbum = function(data){
	// call superclass _createAlbum function with custom album
	var album = GettingStartedView.superClass._createAlbum.call(this, data, TemplateCode.albumGettingStarted.clone(true));
	var me = this;
	$(".dontKnow", album).click(function(){
		album.fadeOut(500, function(){
			album.remove();
			me.update();
		});
	});
	return album;
};



/*
 * Model for Exact Search that takes artist/album pairs and does exact searches (used in iTunes reader search)
 * The search str given must separate artist & title with "$" and separate albums with ","
 * Futher subclasses SearchModel
 */
function ExactSearchModel(){
	// call my base class constructor
	SearchModel.baseConstructor.call(this);
	
	// keep original album/artist string content for comparison
	this.searchData = {};
	
	this.counter = 0;
};

// subclass SearchModel
OopExtend(ExactSearchModel, SearchModel);


// Model
ExactSearchModel.prototype.loadAlbums = function(start, num, searchStr){ // , searches){
	// this.searches = searches;
	
	// simplify search string
	searchStr = this._simplify(searchStr);
	
	// extract artist & title
	var data = searchStr.split("$");
	
	// replace "$" with " " and save data
	searchStr = searchStr.replace(/\$/g, " ");
	this.searchData[searchStr] = data;
	
	// now call normal superclass loadAlbums
	ExactSearchModel.superClass.loadAlbums.call(this, start, num, searchStr);
};


// process search - override search model to keep only exact matches
ExactSearchModel.prototype._process = function(xml){
	// in case of local testing where xml is string
	if (typeof(xml) == "string") 
		xml = Model.convertXMLString(xml);
	
	// parse request info to get search string
	var req = $("request", xml);
	this.request = {
		query: $("query", req).attr("value"),
		ll: $("ll", req).attr("value"),
		lc: $("lc", req).attr("value"),
		genre: $("genre", req).attr("value"),
		conflevelmin: $("conflevelmin", req).attr("value")
	};
	
	// get saved search data
	var searchData = this.searchData[this.request.query];
	
	// now cycle through albums and add only exact matches
	var me = this;
	var data, id;
	this.updateData = [];
	$("Item", xml).each(function(i){
		id = $(this).attr("itemid");
		data = me._parseAlbum($(this), id);
		if(me._similar(searchData[0], data.artist) && me._similar(searchData[1], data.title)){
			me.allData.push(data);
			me.updateData.push(data);
			return false; // exit search for album
		}
	});
	
	// inform observers that my state has changed
	this.broadcast("addition");

};


// if enough words from str1 in str2, then return true
ExactSearchModel.prototype._similar = function(str1, str2){
	var words1 = this._simplify(str1).split(" ");
	var str2 = this._simplify(str2);
	var count = 0;
	$.each(words1, function(i, word){
		if(str2.indexOf(word) != -1) count++;
	});
	return (count >= words1.length * 0.5) // if x % of words correct, then return true
};


/*
 * Pager
 */
function PagerClass(div, options){
	this.base = $(div).addClass("pager");
	this.options = jQuery.extend({
		bufferNext: 4,
		bufferPrev: 4,
		startPage: 1,
		callback: function(str){}
	}, options);
	
	this.dupeLocs = [];
	this.pageNum = this.options.startPage;
	this.maxPage = null; // null is infinite
	this.draw();
}

// add a place to put cloned pagers for mirroring the control
PagerClass.prototype.addDupeLoc = function(div){
	this.dupeLocs.push(div);
	this.draw();
}

PagerClass.prototype.draw = function(){
	this.base.empty().show();
	var me = this;
	
	// add << jump to first page
	if (this.pageNum > this.options.bufferPrev){
		this.base.append($('<a href="javascript:void(0)"><< </a>').click(function(){
			me.pageNum = 1;
			me.options.callback(me.pageNum);
			me.draw();
		}));
	}
	
	// previous
	this.base.append((this.pageNum == 1) ? '<span class="disabled">previous</span>' : 
		$('<a class="prev" href="javascript:void(0)">previous</a>').click(function(){
			me.pageNum = Math.max(me.pageNum - 1, 1);
			me.options.callback(me.pageNum);
			me.draw();
		})
	);
	
	// get start & end pages
	var bounds = this._getDisplayBounds();
	
	// add pages to dom
	var num;
	for (var i = bounds.b; i <= bounds.e; i++) {
		var num = $('<a class="page" href="javascript:void(0)">' + i + '</a>').click(function(e){
			me.pageNum = parseInt($(e.target).text());
			me.options.callback(me.pageNum);
			me.draw();
		});
		if(i == this.pageNum) num.addClass("selected");
		if(i < 10) num.prepend(" ");
		this.base.append(num);
	}
	
	// add next
	this.base.append((this.pageNum == this.maxPage) ? '<span class="disabled"> next</span>' : 
		$('<a class="next" href="javascript:void(0)">next</a>').click(function(){
			me.options.callback(++me.pageNum);
			me.draw();
		})
	);
	
	// mirror control dupes
	$.each(this.dupeLocs, function(){
		this.empty().append(me.base.clone(true));
	});
};

PagerClass.prototype.disable = function(){
	this.base.empty();
	// previous?
	this.base.append('Previous');
	
	// get start & end pages
	var bounds = this._getDisplayBounds();
	
	// add disabled page numbers
	for (var i = bounds.b; i <= bounds.e; i++) {
		this.base.append(i);
	}
	// add next
	this.base.append('Next');
};

PagerClass.prototype.setPage = function(n){
	this.pageNum = n;
	this._bound();
	this.draw();
};

PagerClass.prototype.setMaxPage = function(n){
	this.maxPage = n;
	this._bound();
	this.draw();
};

PagerClass.prototype._getDisplayBounds = function(){
	var center;
	if(!this.maxPage) center = this.pageNum;
	else center = Math.min(this.pageNum, this.maxPage - this.options.bufferNext + 1);
	var b = (this.options.bufferPrev == -1) ? 1 : Math.max(center - this.options.bufferPrev + 1, 1);
	var e = center + this.options.bufferNext - (center - b - this.options.bufferPrev + 1);
	if (this.maxPage != null) e = Math.min(e, this.maxPage);
	return { b: b, e: e};
};

// bound pageNum against maxPage, if set
PagerClass.prototype._bound = function(){
	if(!this.maxPage) return;
	if(this.pageNum > this.maxPage) this.pageNum = this.maxPage;
}

PagerClass.prototype.hide = function(){
	this.base.hide();
	// hide dupes
	$.each(this.dupeLocs, function(){
		this.hide();
	});
}

PagerClass.prototype.show = function(){
	this.base.show();
	$.each(this.dupeLocs, function(){
		this.show();
	});
}



/*
 * Profile Generator
 * Used in Admin mode to generate a genre profile on demand for any user id
 */
function ProfileGenerator(){
	this.maxServerItems = 180;
	this.genreProfiler = new GenreProfileModel();
	this.userId = null;
};


// Model - get votes
ProfileGenerator.prototype.start = function(userId){
	this.userId = userId;
	var me = this;
	this.genreProfiler._gProfileWork = {};
	if (LocalFlag) {
		$.get("localdata/sampleMyVotes.xml", function(xml){
			me._process(xml);
		});
	}
	else {
		$.ajax({
			url: "/fhws/admin/votes",
			data: {
				userid: userId,
				ll: 0, 
				lc: Math.min(this.maxServerItems), // number per page
				o: "rating"
			},
			error: function(XMLHttpRequest){
				if(errorFunction) errorFunction();
				View.showServerError(XMLHttpRequest);
			},
			success: function(xml, status){
				me._process(xml);
			}
		});
	}
};

// process search
ProfileGenerator.prototype._process = function(xml){
	// in case of local testing where xml is string
	if (typeof(xml) == "string") xml = Model.convertXMLString(xml);
	var me = this;
	$("Item", xml).each(function(i){
		var id = $(this).attr("itemid");
		var vote = $("vote", $(this)).attr("rating") || 0;
		var str = $("genres", $(this)).eq(0).text();
		var genres = [];
		if(typeof(str) == "string") $.each(str.split(","), function(i, genreID){
			if(genres.indexOf(genreID) == -1) {
				genres.push(genreID);
				me.genreProfiler.weighGenre(genreID, vote);
			}
		});
	});
	this.genreProfiler.calcGenreProfile(48);
	var table = $('<table id="gpTable"></table>');
	$.each(this.genreProfiler.genreProfile, function(i, val){
		var tr = $('<tr><td>'+ val.name +'</td><td>'+ val.weight +'</td></tr>');
		$("td:eq(0)", tr).css({ "fontSize" : (85 + (val.weight * 40) + "%") });
		table.append(tr);
		// html += val.name + ":" + val.weight + "<br />";
	});
	View.inform({ title: this.userId + " Genre Profile", content: table, w:320, h:500 });
};



/*
 * Generic Items store for storing & retrieving collections of unique strings
 */
function ItemsKeep(label){
	this.data = [];
	this.name = (LocalFlag) ? label + "-" + Model.loginName : label;
};

ItemsKeep.prototype.load = function(callback){
	if (LocalFlag) {
		this._loadDone($.cookie(this.name), callback);
	} else {
		var me = this;
		$.ajax({
			url: "/fhws/getuserdata",
			data: {
				key: this.name
			},
			cache: false,
			error: function(XMLHttpRequest) {
				View.showServerError(XMLHttpRequest);
			},
			success: function(str){
				// spaces were escaped when sending, so unescape
				me._loadDone(str.replace(/\+/g, " "), callback);
			}
		});
	}
};

ItemsKeep.prototype._loadDone = function(str, callback){
	if (str) {
		this.data = str.split(",");
		callback(this.data);
	} else {
		callback(); // call back with null
	}
};

ItemsKeep.prototype.save = function(){
	if (LocalFlag) {
		$.cookie(this.name, this.data.join(), {
			expires: 365
		});
	}
	else {
		$.ajax({
			url: "/fhws/setuserdata",
			data: {
				key: this.name,
				value: this.data.join()
			},
			error: function(XMLHttpRequest){
				View.showServerError(XMLHttpRequest);
			},
			success: function(){
			}
		});
	}
};

ItemsKeep.prototype.get = function(id){
	return this.data;
};

// replace with new set
ItemsKeep.prototype.set = function(list){
	this.data = list;
	this.save();
};

// empty the data
ItemsKeep.prototype.clear = function(id){
	this.data = [];
	this.remove();
};

// remove data store completely
ItemsKeep.prototype.remove = function(){
	if (LocalFlag) {
		$.cookie(this.name, null);
	}
	else {
		$.ajax({
			url: "/fhws/setuserdata",
			data: {
				key: this.name
			},
			error: function(XMLHttpRequest){
				View.showServerError(XMLHttpRequest);
			},
			success: function(){
			}
		});
	}
};

ItemsKeep.prototype.itemExists = function(id){
	return ($.inArray(id, this.data) != -1);
};

ItemsKeep.prototype.getItem = function(id){
	var n = $.inArray(id, this.data);
	if(n+1) return this.data[i]
	else return null;
};

ItemsKeep.prototype.addItem = function(id){
	if($.inArray(id, this.data)+1) return;
	this.data.push(id);
	this.save();
};

ItemsKeep.prototype.removeItem = function(id){
	var n = $.inArray(id, this.data);
	if(n+1){
		this.data.splice(n, 1);
		this.save();
	}
};


/*
 * Observer factory for creating temporary, named observers
 */
function ObserverFactory(){
	this.observers = {};
};

ObserverFactory.prototype.add = function(key, observed, callback){
	this.remove(key);
	var observer = { broadcast: function(eventStr){
		callback(eventStr);
	}};
	this.observers[key] = { observer: observer, observed: observed };
	observed.addObserver(observer);
};

ObserverFactory.prototype.remove = function(key){
	var obj = this.observers[key];
	if(obj){
		obj.observed.removeObserver(obj.observer);
		delete this.observers[key];
	}
	
};


function PadDigits(n, totalDigits) { 
    n = n.toString(); 
    var pd = ''; 
    if (totalDigits > n.length) 
    { 
        for (var i=0; i < (totalDigits-n.length); i++) 
        { 
            pd += '0'; 
        } 
    } 
    return pd + n.toString(); 
}


// performance debugging tool
// usage:
/*
var timer = new Timer();
// ...
timer.set("event");
// ...
timer.stop();
*/
function Timer(){
	this.timerStops = {};
	this.timerStops["begin"] = 0;
	this.lastStop = new Date();
	this.firstStop = this.lastStop;
	this.counter = 0;
};
	
Timer.prototype.set = function(str){
	var curTimer = new Date();
	this.timerStops[(this.counter++) + ". " + str] = curTimer.getTime() - this.lastStop.getTime();
	this.lastStop = curTimer;
};
	
Timer.prototype.stop = function(){
	this.set("end");
	this.timerStops["TOTAL"] = (new Date()).getTime() - this.firstStop;
	jQuery.dump(this.timerStops);
};

function a2z_1_AMPlayerProd_DoFSCommand(command, args){
	// if flash player just loading (showmenu), timeout the loading indicator (set by album.hover())
	if (command == "showmenu") {
		var playerParent = $(".flashContent", HoveredAlbum).parent();
		// if no timeout set yet for this album and album does indeed have loading indicator
		if (!HideLoadingTimerID && playerParent.hasClass("loading")) {
			HideLoadingTimerID = setTimeout(function(){
				playerParent.removeClass("loading");
				HideLoadingTimerID = null;
			}, 1000);
		}
	}
	
	// if player has track state changed, then it is functional so do actions
	if ((command == "TrackStateChanged")) {
		
		switch (args.charAt(args.length - 1)) {	
			
			// BUFFERING (user initiates play via play/next/prev)
			case "1": 
				if(!TrackContinue) {
					// keep open the album while it is loading to play, and during play
					AlbumToBePlayed = HoveredAlbum;
					View.display.playMode(AlbumToBePlayed);
				}
				break;
				
			// CONTINUE (to next track via auto play)
			case "2":
				// track it so we don't confuse eventual play of this as a user command
				TrackContinue = true;
				break;
			
			// STOP
			case "3":
				// If user has pressed stop (user is hovering on this album), don't keep open anymore
				if (View.display.albumPlaying == HoveredAlbum) {
					View.display.stopMode(View.display.albumPlaying);
					View.display.albumPlaying = null;
				}
				break;
			
			// PLAY (track starts playing after buffering period)
			case "4":
				// if auto tracking jumping, then don't do anything, just let it
				if (TrackContinue) {
					TrackContinue = false;
				} else {
					
					View.display.stopMusicPlayer(AlbumToBePlayed);
					View.stopLalaPlayer();
					// set current album and set to stay open
					View.display.albumPlaying = AlbumToBePlayed;
					View.display.playMode(AlbumToBePlayed);
				}
				break;
		}
	}
};


/*
 * Get Query String from URL
 */
function URLQueryString(val){
    var q = unescape(location.search.substr(1)).split('&');
    for (var i = 0; i < q.length; i++) {
        var t = q[i].split('=');
        if (t[0].toLowerCase() == val.toLowerCase()) 
            return t[1];
    }
    return '';
};

/*
 * Get hash from URL
 */
function URLHashString(){
	return (document.location.hash).slice(1);
}

function SetURLHashString(str){
	document.location.hash = (str || "main");
	document.title = "discovermymuse.com";
}

/*
 Observable - streamlined observable pattern
 Makes implementors observable by multiple, anonymous observers.  Observers subscribe to receive events with "addObserver(this)"
 Observables will broadcast events (e.g. "broadcast()") with optional arguments.  Observers
 then receive the event via their implemented "broadcast" method which they must implement and can optionally process arguments
 */
function _Observable(){
	this.observers = [];
};

// public method to add an observer
_Observable.prototype.addObserver = function(obj){
	if ($.inArray(obj, this.observers) == -1) 
		this.observers.push(obj);
};

// public method to remove a particular observer
_Observable.prototype.removeObserver = function(obj){
	var n = $.inArray(obj, this.observers);
	if (n > -1) this.observers.splice(n, 1);
};

// broadcast an event to all observers
_Observable.prototype.broadcast = function(eventStr){
	$.each(this.observers, function(){
		this.broadcast(eventStr);
	});
};

// override in subclasses to return appropriate state from observers
_Observable.prototype.getState = function(){
};

function roundNumber(num, dec) {
	var result = Math.round( Math.round( num * Math.pow( 10, dec + 1 ) ) / Math.pow( 10, 1 ) ) / Math.pow(10,dec);
	return result;
}

// clone an object and all its properties
function clone(obj){
    if(obj == null || typeof(obj) != 'object')
        return obj;

    var temp = new obj.constructor(); // changed (twice)
    for(var key in obj)
        temp[key] = clone(obj[key]);

    return temp;
}
