// Constants
var CWP_RELEASE = 1;
var CWP_CATEGORY = 2;

var CWP_UNRENDERED = 0;
var CWP_RENDERED = 1;
var CWP_PLAYING = 2;
var CWP_PAUSED = 3;

var CWP_CLASS_HL = "cwpHighlighted";
var CWP_CLASS_SEL = "cwpSelected";

var CWP_PLACEHOLDER_ID = 1; // ID used when we create fake categories, won't try to match the categoryIDs of child releases
var CWP_NOSHOW = "NOSHOW";
var CWP_VERSION = "1.4.1";

var cwpFlashID = "playerwidget";

function cwpComScore(manager) {
	if ($("#cwpComScore").length===0){
		$(document.body).append("<img id='cwpComScore' height='1' width='1' />"); 
	}

	$("#cwpComScore").attr("src","http://beacon.securestudies.com/scripts/beacon.dll?C1=1&C2=3005660&C3=3005660&C4=" + window.location.hostname.toLowerCase() + window.location.pathname.toLowerCase() + "&C5=" + manager.currentPlay.Show + "&C6=&C7=" + escape(window.location.href) + "&C8=" + escape(document.title) + "&C9=" + escape(document.referrer) + "&rn=" + Math.floor(Math.random()*99999999));
}

// Helper methods
function cwpQS(qs) { // optionally pass a querystring to parse
	this.params = {};
	if (!qs){qs = location.search.substring(1, location.search.length);}
	if (qs.length === 0){return;}
	qs = qs.replace(/\+/g, ' ');
	var args = qs.split('&'); // parse out name/value pairs separated via &
	for (var i=0;i<args.length;i++) {
		var pair = args[i].split('=');
		var name = decodeURIComponent(pair[0]);
		var value = (pair.length==2)?decodeURIComponent(pair[1]):name;
		this.params[name] = value;
	}
}
cwpQS.prototype.get=function(key, default_){var value = this.params[key];return (typeof(value)=="string")?value:default_;};
cwpQS.prototype.contains=function(key){var value = this.params[key];return (typeof(value)=="string");};

function symbolsToEntities(sText) {
	if(!sText){sText="";}
	var sNewText = "";
	var iLen = sText.length;
	for (var i=0;i<iLen;i++) {
		var iCode = sText.charCodeAt(i);
		sNewText += (iCode > 256? "&#" + iCode + ";": sText.charAt(i));
	}
	return sNewText;
}
function cwpAjaxLinks(){
	$("a[href^='?releasePID=']").click(function(){
		if (typeof(focusOnTargetRelease)=="function"){
			focusOnTargetRelease("PID", $(this).attr("href").split("&")[0].split("=")[1]);
		}
		return false;
	});
}
function convertLength(data){
	var tl = new Date(data);
	var HH = tl.getHours(); var MM = tl.getMinutes(); var ss = tl.getSeconds();
	var str;

	if (ss<10){str=":0"+ss;}
	else{str=":"+ss;}
	str = MM + str;
	if (HH - 19 > 0){
		if (MM < 10){str="0"+str;}
		str = HH + ":" + str;
	}
	return str;
}

function cwpArrayContains(arr,field){
	for (var i=0;i<arr.length;i++){
		if (arr[i]==field){return true;}
	}
	return false;
}

function cwpDisableSelection(element) {
	element.bind("onselectstart",function(){return false;});
	element.attr("unselectable", "on");
	element.css("MozUserSelect", "none");
	element.css("cursor", "pointer");
}

function cwpRemoveSpaces(s){return s && s.replace(/[ ]/g,"");}
function cwpCleanStr(s){return s && s.toLowerCase().replace(/\s|[.,<>?\/;:"'{\[}\]|\\+=_-`!@#$%\^&*()]*|~*/g,"");}
function cwpCleanAdParams(s){return s && s.toLowerCase().replace(/[.,<>?\/;:"'{\[}\]|\\+=_-`!@#$%\^&*()]*|~*/g,"").replace(/[ ]/g,"_");}

function cwpTruncate(str,truncAt){
	// If it's not a string, return the empty string
	if(typeof(str)!=="string"){return "";}
	var truncatedString;
	if(str.length > truncAt){
		truncatedString = str.slice(0,truncAt);
		truncatedString = str.slice(0,truncatedString.lastIndexOf(" "));
		truncatedString += "&hellip;";
	}else{
		truncatedString = str;
	}
	return truncatedString;
}

function logError(errorMsg) {
	alert(errorMsg);
	return false;
}

function logDebug(debugMsg) {
	var dbgDiv = $("#debugOutput");
	var d = new Date();
	if (dbgDiv.length>0){dbgDiv.prepend("<" + d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds() + ">: " + debugMsg + "<br/>");}
}

function cwpGetFeedContents(feedJSON, callback){
	$.ajaxSetup({cache:"true"});
	$.getJSON(feedJSON, function(data){
		var str = "";
		for (var key in data.listInfo){
			if (data.listInfo.hasOwnProperty(key)){
				str += key + ": " + data.listInfo[key] + "|";	
			}
		}
		str += "<br/>";
		$.each(data.items,function(i,dataItem){
			str += i + " - ";
			for (var key in dataItem){
				if (dataItem.hasOwnProperty(key)){
					if (key.indexOf("ustomData") >0){
						str += key + ":: ";
						for (var cdKey in dataItem[key]){
							if (dataItem[key].hasOwnProperty(cdKey)){
								str += dataItem[key][cdKey].title + ": " + dataItem[key][cdKey].value + ", ";
							}
						}
						str += " | ";
					}else{
						str += key + ": " + dataItem[key] + " | ";
					}
				}
			}
			str += "<br/>";
		});
		callback(str);
	});
	$.ajaxSetup({cache:"false"});
}

function cwpIsCollapseCat(cat){
	return (cat.children.length==1 && cat.title==cat.children[0].title);
}

function cwpEventProto(obj,name,callback){
	if (this.mode == CWP_UNRENDERED){return;}
	if (typeof callback == "function"){
		obj[name + "Callback"] = callback;
	}else{
		if (obj[name + "Callback"]){
			obj[name + "Callback"](obj); // use local callback if available 
		}else{
			if (obj.type==CWP_CATEGORY) {
				obj.manager.catExists(obj.depth)[name](obj);
			}else if (obj.manager.rel[name]){
				obj.manager.rel[name](obj);
			}
		}
	}
}

function cwpVideoEventProto(obj,name,pdkEvent,callback){
	if (this.mode == CWP_UNRENDERED){return;}
	if (typeof callback == "function"){
		obj[name + "Callback"] = callback;
	}else{
		if (obj[name + "Callback"]){
			obj[name + "Callback"](obj,pdkEvent); // use local callback if available 
		}else{
			if (obj.type==CWP_CATEGORY) {
				logDebug("Looking for category event (" + obj.ID + ", " + obj.depth + ", '" + name + "').");
				obj.manager.catExists(obj.depth)[name](obj,pdkEvent);
			}else{
				logDebug("Looking for release event (" + obj.ID + ", '" + name + "').");
				if (obj.manager.rel[name]){
					obj.manager.rel[name](obj,pdkEvent);
				}
			}
		}
	}
}


// Objects
function cwpCategory(manager, data, parentCat, fieldNames, customFieldNames){
	// Initialization
	this.ID = data.ID;
	this.manager = manager;
	this.parentCat = parentCat || null;
	this.children = [];
	this.depth = data.depth;
	this.fullTitle = data.fullTitle;
	this.hasDirectReleases = data.hasReleases; // whether there are direct child releases
	this.hasReleases = this.hasDirectReleases; // whether they are child releases at any depth
	if (this.hasReleases && parentCat){
		parentCat.setHasReleases();
	}
	this.releasesLoaded = false;
	this.type = CWP_CATEGORY;
	fieldNames = fieldNames || manager.catFields;
	for (var i=0;i<fieldNames.length;i++){
		this[fieldNames[i]] = data[fieldNames[i]];
	}
	customFieldNames = customFieldNames || manager.catCustomFields;
	if (customFieldNames && data.customData){
		for (var i=0;i<data.customData.length;i++){
			// If we have a displayTitle, override the title property
			if (data.customData[i].title=="DisplayTitle"){
				if (data.customData[i].value.length>0){
					this.title = data.customData[i].value; 
				}
			}else{
				this[cwpRemoveSpaces(data.customData[i].title)] = data.customData[i].value;
			}
		}
	}

	this.playlistContainer = false;
	this.mode = CWP_UNRENDERED;
	this.jsonRequested = false;
	this.isSelected = false;

	// Methods
	this.getRenderObj = function(){
		var renderObj = this.manager.catRendering[this.depth];
		var renderDepth = this.depth;
		while (!renderObj){
			renderDepth--;
			if (renderDepth<0){return logError("Cannot render Category.  No rendering methods defined.");}
			renderObj = this.manager.catRendering[renderDepth];
		}
		return renderObj;
	};
	
	this.getClipTemplate = function(){
		var retVal = "inherit";
		if (this.clipTemplate === "inherit" || !this.clipTemplate){
			if (this.parentCat){
				retVal = this.parentCat.getClipTemplate();	
			}
		}else{
			retVal = this.clipTemplate;
		}
		return retVal;
	};

	this.setUnrendered = function(setRootUnrendered){
		if (setRootUnrendered){this.element=null;this.mode=CWP_UNRENDERED;this.isSelected=false;}
		$.each(this.children, function(i,obj){
			obj.setUnrendered(true);
		});
	};

	this.getParentAtDepth = function(targetDepth){
		if (this.depth > targetDepth && this.parentCat){
			return this.parentCat.getParentAtDepth(targetDepth); 
		}else{
			return this;
		}
	};

	this.click = function(callback){cwpEventProto(this.cwpTarget,"click",callback);};
	this.mouseover = function(callback){
		$(this).addClass(CWP_CLASS_HL);
		cwpEventProto(this.cwpTarget,"mouseover",callback);
	};
	this.mouseout = function(callback){
		$(this).removeClass(CWP_CLASS_HL);
		cwpEventProto(this.cwpTarget,"mouseout",callback);
	};
	this.select = function(callback){
		if (callback){
			cwpEventProto(this,"select",callback);
		}else if (!this.isSelected){
			this.isSelected = true;
			this.manager.setSelected(this,true);
			if (this.element){
				this.element.addClass(CWP_CLASS_SEL);
			}
			cwpEventProto(this,"select",callback);
		}else{
			logDebug("Cannot double select category: " + this.title);
		}
	};
	this.unselect = function(callback){
		if (callback){
			cwpEventProto(this,"select",callback);
		}else if (this.isSelected){
			this.isSelected = false;
			this.manager.setSelected(this,false);
			if (this.element){
				this.element.removeClass(CWP_CLASS_SEL);
			}
			cwpEventProto(this,"unselect",callback);
		}else{
			logDebug("Cannot double unselect category: " + this.title);
		}
	};
	this.onChildrenShow = function(callback){cwpEventProto(this,"onChildrenShow",callback);};

	// Used to propagate hasReleases upwards, so we know when a category has releases at any depth below
	this.setHasReleases = function(){
		if (!this.hasReleases){
			this.hasReleases = true;
			if (this.parentCat){
				this.parentCat.setHasReleases();
			}
		}
	};

	this.loadChildren = function(callback){
		if (!this.jsonRequested){
			if (!this.checkReleasesLoaded()){
				this.jsonRequested = true;
				this.manager.loadingPlaylistCat = this;
				var cat = this;
				this.fetchReleases(function(releaseList){
					cat.jsonRequested = false;
					cat.manager.releasesLoaded();
					if (typeof(callback)=="function"){callback();}
				});
			} else if (typeof(callback)=="function"){callback();}
		}
	};

	this.showChildren = function(targetContainer,startDepth,endDepth){
		logDebug("Entering showChildren method (" + this.title + ")");
		if (this.checkReleasesLoaded()){
			endDepth = (typeof(endDepth)=="number")?endDepth:-1;
			startDepth = (typeof(startDepth)=="number")?startDepth:-1;

			this.manager.loadingPlaylistCat = this;
			this.playlistContainer = targetContainer;
			// If a previous category was selected and it is at a higher depth than the previous selection,
			//  and the new selection is not a child of the previous selection, then we want to make it as unrendered
			if (this.manager.displayedPlaylistCat){
				var matchCat = this.getParentAtDepth(this.manager.displayedPlaylistCat.depth);
				if (this.manager.displayedPlaylistCat!==matchCat){
					this.manager.displayedPlaylistCat.getParentAtDepth(this.depth).setUnrendered(false); 
				}
			}
			targetContainer.html("");
			$.each(this.children,function(i,obj){
				obj.renderHTML(targetContainer,startDepth,endDepth,true);
			});
			this.manager.displayedPlaylistCat = this;
			this.onChildrenShow();
		}
		logDebug("Exiting showChildren method (" + this.title +")");
	};

	this.renderHTML = function(targetContainer,startDepth,endDepth,withReleases){
		if (this.manager.hideEmptyCats && !this.hasReleases){return;}
		endDepth = (typeof(endDepth)=="number")?endDepth:-1;
		startDepth = (typeof(startDepth)=="number")?startDepth:-1;
		withReleases = (typeof(withReleases)=="boolean")?withReleases:false;
		
		var renderObj = null;
		if (this.mode == CWP_UNRENDERED && this.depth>=startDepth){
			// We want to use the rendering methods for the depth of the element, but can default to rendering for higher depths
			//   this way is we want nested <ul> tags, we can just define one rendering that cascades down
			
			renderObj = this.getRenderObj();
			// The top level element is just the container for the list item and sublists.  The first child is the actual element we want
			this.element = renderObj.makeContent(targetContainer,this);
			cwpDisableSelection(this.element);
			this.element[0].cwpTarget = this;
			this.element.click(this.click);
			this.element.mouseout(this.mouseout);
			this.element.mouseover(this.mouseover);
			this.mode = CWP_RENDERED;
		}
		// If we have children that have not yet been rendered
		if (this.children.length > 0 && this.children[0].mode == CWP_UNRENDERED){
			// If the children respect the endDepth
			if (endDepth == -1 || this.children[0].depth <= endDepth){
				var nextTarget = targetContainer;
				// Create the child container if we are past the start depth
				if (this.depth>=startDepth){
					if (!renderObj){
						renderObj = this.getRenderObj();
					}
					nextTarget = renderObj.makeChildContainer(this.element,this);
				}
				for(var i=0;i<this.children.length;i++){
					this.children[i].renderHTML(nextTarget,startDepth,endDepth,withReleases);
				}
				this.onChildrenShow();
			}
		}
	};

	this.checkReleasesLoaded = function(){
		if (this.releasesLoaded){return true;}
		if (this.hasDirectReleases){return false;} // if releasesLoaded is false and we have direct child releases, we need to load them

		var newReleasesLoaded = true;
		if (this.children.length>0){
			$.each(this.children,function(i,cwpObj){
				// If we find a child category without releases loaded
				if (cwpObj.type==CWP_CATEGORY && cwpObj.checkReleasesLoaded()===false){
					newReleasesLoaded = false;
					return false;
				}
			});

		// Else if we are at a leaf node with no releases
		}else if (!this.hasDirectReleases){
			newReleasesLoaded = true;
		}else{
			newReleasesLoaded = false;
		}
		
		this.releasesLoaded = newReleasesLoaded;
		return this.releasesLoaded;
	};

	this.fetchReleases = function(callback,fieldNames,customReleaseCall,jsonURL){
		var customFieldNames = null;
		jsonURL = jsonURL || this.manager.jsonURL;
		customReleaseCall = customReleaseCall || this.manager.releaseCall;
		if (fieldNames === undefined || fieldNames === null){
			fieldNames = this.manager.relFields;
			customFieldNames = this.manager.relCustomFields;
		}else if (typeof fieldNames === 'string'){
			var tempFields = fieldNames.split(";");
			fieldNames = tempFields[0].split(",");
			if (tempFields.length>1){
				customFieldNames = tempFields[1].split(",");
			}
		}

		if (this.hasReleases){
			var constructedJSON = jsonURL + customReleaseCall;
			if (this.ID !== CWP_PLACEHOLDER_ID){
				constructedJSON += "&query=CategoryIDs|" + this.getCatIDList();
			}

			var sortCriteria = this.getInheritedSortCriteria(false);
			// Make sure we retrieve any fields we need for sorting
			var extraFields = [];
			if (sortCriteria.length>0){
				var splitChar = ";";
				if (sortCriteria.indexOf(splitChar)==-1){
					splitChar = ":"; // could be ; or :
				}
				sortCriteria = sortCriteria.split(splitChar);
				var sortField = sortCriteria[0];
				if (sortField.match(/^\d+$/)===null){
					extraFields.push(sortField);
					var sortDesc = (sortCriteria.length<=1)?false:(sortCriteria[1]=="DESC")?true:false;
					constructedJSON = constructedJSON.replace(/&sortField=[^&]*/,"&sortField="+sortField);
					constructedJSON = constructedJSON.replace(/&sortDescending=[^&]*/,"");
					if (sortDesc){
						constructedJSON += "&sortDescending=true";
					}
				}
			}
			this.addExtraSortFields(extraFields);
			for (var i=0;i<extraFields.length;i++){
				if (!cwpArrayContains(fieldNames,extraFields[i])){
					fieldNames.push(extraFields[i]);
				}
			}
			
			$.each(fieldNames, function(i,field){
				constructedJSON += "&field=" + field;
			});
			if (customFieldNames){
				$.each(customFieldNames, function(i,field){
					constructedJSON += "&contentCustomField=" + field;
				});
			}

			var currentObj = this;
			logDebug("Retrieving release list: " + constructedJSON);
			$.ajaxSetup({cache:"true"});
			$.getJSON(constructedJSON, function(data){
				logDebug("Retrieved JSON release data.");
				//if (data.listInfo.itemCount<data.listInfo.totalCount) alert("Unfortunately, the '" + currentObj.fullTitle + "' category has -" + data.listInfo.totalCount + "- releases under it and we currently support a maximum of -" + data.listInfo.itemCount + "-.  Anything after the release '" + data.items[data.listInfo.itemCount-1].title + "' alphabetically will not show up right now.  We are looking into a solution, but for now, please make a note of this category so we can address it later."); //commented out by Arron 2009.08.24 because there were more than 500 clips live and this alert was firing.
				currentObj.processFeed(data,fieldNames,customFieldNames);
				logDebug("Processed JSON release data.");
				if (typeof(callback)=="function"){callback();}
			});
			$.ajaxSetup({cache:"false"});
		}
	};

	this.getCatIDList = function(){ // Used to query for all releases in a category node and all its children
		if (this.children.length===0){
			return "" + this.ID;
		}else{
			var catIDList = this.ID;
			$.each(this.children,function(i,childObj){
				if (childObj.type == CWP_CATEGORY){
					catIDList += "," + childObj.getCatIDList();
				}
			});
			return catIDList;
		}
	};

	this.processFeed = function(data,fieldNames,customFieldNames){
		fieldNames = fieldNames || this.manager.relFields;
		if (typeof(fieldNames)=="string"){
			fieldNames = fieldNames.split(",");
		}
		customFieldNames = customFieldNames || this.manager.relCustomFields;
		if (typeof(customFieldNames)=="string"){
			customFieldNames = customFieldNames.split(",");
		}

		if (this.hasDirectReleases){
			var catObj = this;
			var sortCriteria = this.getInheritedSortCriteria(true);
			var straightPush = (sortCriteria.length===0);
			// Process releases belonging in this category
			$.each(data.items,function(i,relData){
				$.each(relData.categoryIDs,function(j,categoryID){
					// If the release belongs in this category, or if this category is one where we should not match ids
					if (catObj.ID==CWP_PLACEHOLDER_ID || categoryID==catObj.ID) {
						var newRelease = new cwpRelease(catObj.manager, relData, catObj, fieldNames, customFieldNames);
						if (straightPush){
							catObj.children.push(newRelease);
						}else{
							catObj.addReleaseInOrder(newRelease,sortCriteria);
						}
						return false;
					}
				});
			});
		}
		$.each(this.children,function(i,childObj){
			if (childObj.type == CWP_CATEGORY){
				this.processFeed(data,fieldNames,customFieldNames);
			}
		});
		this.releasesLoaded = true;
	};

	this.addReleaseInOrder = function(rel, sortCriteria){
		var splitChar = ";";
		if (sortCriteria.indexOf(splitChar)==-1){
			splitChar = ":"; // could be ; or :
		}
		sortCriteria = sortCriteria.split(splitChar);
		var sortField = sortCriteria[0];
		var sortDesc = (sortCriteria.length<=1)?false:(sortCriteria[1]=="DESC")?true:false;
		for (var i=0;i<this.children.length;i++){
			if (sortDesc){
			  if (rel[sortField]>this.children[i][sortField]){
					this.children.splice(i,0,rel);
					return;
				}
			}else if (rel[sortField]<this.children[i][sortField]){
				this.children.splice(i,0,rel);
				return;
			}
		}
		this.children.push(rel);
	};

	this.getCategory = function(key,value){
		if (this[key]==value){return this;}
		var returnCat = null;
		$.each(this.children,function(i,cat){
			returnCat = cat.getCategory(key,value);
			if (returnCat){return false;}
		});
		return returnCat;
	};

	this.getRelease = function(key,value){
		var returnCat = null;
		$.each(this.children,function(i,childObj){
			returnCat = childObj.getRelease(key,value);
			if (returnCat){return false;}
		});
		return returnCat;
	};

	this.getFirstRelease = function(doRecursive){
		doRecursive = (doRecursive!==false);
		var rel = null;
		for(var i=0;i<this.children.length;i++){
			if (this.children[i].type==CWP_RELEASE){
				rel=this.children[i];
			}else if (doRecursive){
				rel = this.children[i].getFirstRelease();
			}
			if (rel!==null){break;}
		}
		return rel;
	};
	
	this.findFirstParentCategory = function(rel){
		var returnCat = null;
		if (this.hasDirectReleases){
			var cat = this;
			$.each(rel.categoryIDs,function(i,relCatID){
				if (cat.ID == relCatID){
					returnCat = cat;
					return false;
				}
			});
		}
		if (!returnCat){
			$.each(this.children,function(i,childCat){
				if (childCat.type == CWP_CATEGORY){
					returnCat = childCat.findFirstParentCategory(rel);
					if (returnCat){return false;}
				}
			});
		}
		return returnCat;
	};

	this.loadChildrenAtDesiredDepth = function(targetContainer,depth,callback){
		if (this.depth==depth){
			var cat=this;
			this.loadChildren(function(){
				if (typeof(callback)=="function"){callback(cat);}
			});
			return true;
		}else if (this.parentCat){
			return this.parentCat.loadChildrenAtDesiredDepth(targetContainer,depth,callback);
		}else{
			return false;
		}
	};

	this.selectWithParents = function(toDepth){
		if (this.depth<toDepth){return;}
		if (parentCat){
			parentCat.selectWithParents(toDepth);
		}
		this.select();
	};

	this.getInheritedSortCriteria = function(stopAtLoadedCat){
		if (this.SortCriteria && this.SortCriteria.length>0 && this.SortCriteria != this.manager.defaultSort){
			return this.SortCriteria;
		// if we want to stop at loadedCat, we need to not continue when the parent of parent hasn't been loaded
		}else if (this.parentCat && (!stopAtLoadedCat || (this.parentCat.parentCat && this.parentCat.parentCat.releasesLoaded))){
			return this.parentCat.getInheritedSortCriteria(stopAtLoadedCat);
		}else{
			return "";
		}
	};

	this.addExtraSortFields = function(extraFields){
		if (this.SortCriteria && this.SortCriteria.length>0 && this.SortCriteria != this.manager.defaultSort){
			var splitChar = ";";
			if (this.SortCriteria.indexOf(splitChar)==-1){
				splitChar = ":"; // could be ; or :
			}
			var sortField = this.SortCriteria.split(splitChar)[0];
			if (sortField.match(/^\d+$/)===null && !cwpArrayContains(extraFields,sortField)){
				extraFields.push(sortField);
			}
		}
		for (var i=0;i<this.children.length;i++){
			if (this.children[i].type==CWP_CATEGORY){
				this.children[i].addExtraSortFields(extraFields);
			}
		}
	};

	this.isCollapseCat = function(){
		return (this.children.length==1 && this.title==this.children[0].title);
	}
}

function cwpRelease(manager, data, parentCat, fieldNames, customFieldNames){
	// Initialization
	this.ID = data.ID;
	this.contentID = data.contentID;
	this.PID = data.PID;
	this.manager = manager;
	this.parentCat = parentCat || null;
	this.depth = (this.parentCat)?this.parentCat.depth+1:0;
	this.type = CWP_RELEASE;
	this.URL = data.URL;
	this.length = data.length;
	this.mode = CWP_UNRENDERED;
	this.categoryIDs = data.categoryIDs;
	fieldNames = fieldNames || manager.relFields;
	for (var i=0;i<fieldNames.length;i++){
		this[fieldNames[i]] = data[fieldNames[i]];
	}
	customFieldNames = customFieldNames || manager.customRelFields;
	// There will always be custom data because of the ad content
	for (i=0;i<data.contentCustomData.length;i++){
		this[cwpRemoveSpaces(data.contentCustomData[i].title)] = data.contentCustomData[i].value;
	}
	if (!this.ClipType){
		this.ClipType = (!this.WebExclusive || this.WebExclusive=="No")?"episode":"other";
	}

	// Modify release URL with advertising information
	// set a cnt keyvalue for each item in the comma separated Subject field
	if (this.Subject){
		var cntVals = this.Subject.split(",");
		for (var i=0;i<cntVals.length;i++){
			this.URL += "&k"+(this.manager.extraParamCount+i)+"=cnt&v"+(this.manager.extraParamCount+i)+"="+cwpCleanAdParams(cntVals[i]);	
		}
	}

	// Calculate the correct zone
	var newZone = "Zone=";
	if (this.Zone){
		newZone += this.Zone;
	}else if (this.Network||this.Show){
		newZone += cwpCleanStr(this.Network+this.Show);
	}else{
		newZone += this.manager.defaultZone;
	}
	if (/Zone=[^&]*/.test(this.URL)){
		this.URL = this.URL.replace(/Zone=[^&]*/, newZone);
	}else{
		this.URL += "&" + newZone;
	}
	if (/Format=[^&]*/.test(this.URL)){
		this.URL += "&format=SMIL";
	}
	// If the clip has a duration greater than 2min, set clipLength to "long", otherwise it is "show"
	this.clipLength = "short";	
	if (this.length>120000){
		this.clipLength = "long";
	}
	
	// Add some metadata to the release call
	this.URL += "&show=" + cwpCleanAdParams(this.Show) + "&episode=" + cwpCleanAdParams(this.Episode) + "&network=" + cwpCleanAdParams(this.Network) + "&season=" + cwpCleanAdParams(this.Season) + "&clipLength=" + this.clipLength;
	this.isSelected = false;

	// Methods
	this.renderHTML = function(targetContainer,startDepth,endDepth,withReleases){
		if (typeof(this.manager.relRendering)!="function"){
			logError("Cannot render Release. No rendering method defined.");
		}else{
			this.element = this.manager.relRendering(targetContainer,this);
			cwpDisableSelection(this.element);
			this.element[0].cwpTarget = this;
			this.element.click(this.click);
			this.element.mouseover(this.mouseover);
			this.element.mouseout(this.mouseout);
			this.mode = CWP_RENDERED;
		}
	};

	this.setUnrendered = function(){
		this.element = null;
		this.mode = CWP_UNRENDERED;
		this.isSelected = false;
	};

	this.click = function(callback){cwpEventProto(this.cwpTarget,"click",callback);};
	this.mouseover = function(callback){
		if (this.cwpTarget.element){
			this.cwpTarget.element.addClass(CWP_CLASS_HL);
		}
		cwpEventProto(this.cwpTarget,"mouseover",callback);
	};
	this.mouseout = function(callback){
		if (this.cwpTarget.element){
			this.cwpTarget.element.removeClass(CWP_CLASS_HL);
		}
		cwpEventProto(this.cwpTarget,"mouseout",callback);
	};
	this.select = function(callback){
		logDebug("Selecting: "  + this.title);
		if (typeof(callback)==="function"){
			cwpEventProto(this,"select",callback);
		}else if (!this.isSelected){
			this.isSelected = true;
			this.manager.setSelected(this,true);
			if (this.element){
				this.element.addClass(CWP_CLASS_SEL);
			}
			cwpEventProto(this,"select",callback);
		}else{
			logDebug("Cannot double select release: " + this.title);
		}
	};
	this.unselect = function(callback){
		if (typeof(callback)==="function"){
			cwpEventProto(this,"select",callback);
		}else if (this.isSelected){
			this.isSelected = false;
			this.manager.setSelected(this,false);
			if (this.element){
				this.element.removeClass(CWP_CLASS_SEL);
			}
			cwpEventProto(this,"unselect",callback);
		}else{
			logDebug("Cannot double unselect release: " + this.title);
		}
	};
	this.onVideoStart = function(evt,callback){cwpVideoEventProto(this,"onVideoStart",evt,callback);};
	this.onVideoPause = function(evt,callback){cwpVideoEventProto(this,"onVideoPause",evt,callback);};
	this.onVideoUnpause = function(evt,callback){cwpVideoEventProto(this,"onVideoUnpause",evt,callback);};
	this.onVideoEnd = function(evt,callback){cwpVideoEventProto(this,"onVideoEnd",evt,callback);};
	this.onVideoComplete = function(evt,callback){
		if (this.manager.playAllVideosInCategory){
			this.playNextVideo();
		}
		cwpVideoEventProto(this,"onVideoComplete",evt,callback);
	};
	
	this.requestVideoPlay = function(){
		this.manager.setPlayingRelease(this);
		this.manager.queueReleaseForPlayback(this);
	};

	this.playVideo = function(){
		if (!tpController){
			logError("Could not find thePlatform playback component!");
		}else{
			logDebug("Attempting to start playback on release (" + this.ID + ") with URL: " + this.URL);
			tpController.setReleaseURL(this.URL,true);
			if (!this.manager.disableOmniture){
				// Send omniture information
				if ($("#" + cwpFlashID) && $("#" + cwpFlashID)[0].sendCategories){
					var bucketList = [];
					// If a playerTag has been defined, use that
					if (this.manager.playerTag){
						bucketList.push(this.manager.playerTag);
					}
					if (this.parentCat){
						bucketList.push(this.parentCat.fullTitle);
					}
					$("#" + cwpFlashID)[0].sendCategories(bucketList);
				}else{
					logDebug("Failed to find '" + cwpFlashID + "' to send bucket information for Omniture.");
				}
			}
		}
	};

	this.pauseVideo = function(doPause){
		if (!tpController){
			logError("Could not find thePlatform playback component!");
		}else{
			tpController.pause(doPause);
		}
	};

	this.playNextVideo = function(){
		if (!this.manager.relPlaylist){
			// If we don't have a parent there is no next video
			if (this.parentCat){
				// If the current video is the last in the list, we can't play next
				for (var i=0;i<this.parentCat.children.length-1;i++){
					if (this.parentCat.children[i]==this){
						this.parentCat.children[i+1].requestVideoPlay();
						this.parentCat.children[i+1].select();
						break;
					}
				}
			}
		}
	};

	this.playPrevVideo = function(){
		if (!this.manager.relPlaylist){
			// If we don't have a parent there is no next video
			if (this.parentCat){
				// If the current video is the first in the list, we couldn't play next
				for (var i=1;i<this.parentCat.children.length;i++){
					if (this.parentCat.children[i]==this){
						this.parentCat.children[i-1].requestVideoPlay();
						this.parentCat.children[i-1].select();
						break;
					}
				}
			}
		}
	};

	this.getRelease = function(key,value){
		if (this[key]==value){
			return this;
		}else{
			return null;
		}
	};

	this.getClipTemplate = function(){
		return this.parentCat ? this.parentCat.getClipTemplate() : "inherit";
	};

	this.selectWithParents = function(toDepth){
		if (this.depth<toDepth){return;}
		if (parentCat){
			parentCat.selectWithParents(toDepth);
		}
		this.select();
	};
}

function catRender(makeContent, makeChildContainer){
	this.makeContent = makeContent;
	this.makeChildContainer = makeChildContainer;
}

var cwpManager = null;

function cwpInitializeManager(data){
	cwpManager = new cwpManagerObj(data);
	return cwpManager;
}

function cwpManagerObj(data){
	var qs = new cwpQS();
	if (qs.contains("cwpDebug")){
		$("body").append("<div id='debugOutput'></div>");
	}
	if (!data.PID){
		return logError("You must provide a pid.");
	}
	if (!data.playerTag && !data.pidList){
		return logError("You must provide a playerTag or pidList to initialize the cwpManager.");
	}
	// Constants
	this.PID = data.PID;
	this.playerTag = data.playerTag;
	this.pidList = data.pidList;
	this.catPlayerTag = data.catPlayerTag || data.playerTag;
	this.preloadFirstRelease = data.preloadFirstRelease || false;
	this.categoryCall = "getCategoryList?callback=?&field=ID&field=depth&field=hasReleases&field=fullTitle&PID=" + this.PID;
	this.categoryCall += "&query=CustomText|PlayerTag|" + this.catPlayerTag;
	this.releaseCall = "getReleaseList?callback=?&field=ID&field=contentID&field=PID&field=URL&field=categoryIDs&field=length&startIndex=1&endIndex=500&sortField=title&PID=" + this.PID +
		"&contentCustomField=Show&contentCustomField=Episode&contentCustomField=Network&contentCustomField=Season&contentCustomField=Zone&contentCustomField=Subject"; // these are ad fields
	if (this.playerTag){
		this.releaseCall += "&query=Categories|" + this.playerTag;
	}
	this.site = data.site;
	if (data.site){
		this.releaseCall += "&param=Site|"+data.site;
	}
	this.defaultZone = "NOZONE";
	if (data.siteZone){
		this.defaultZone = data.siteZone;this.releaseCall += "&param=Zone|"+this.defaultZone;
	}
	this.version = CWP_VERSION;
	this.hideEmptyCats = !(data.hideEmptyCats===false);
	this.syncPageTitle = (data.syncPageTitle===true);

	// Incorporate ad parameters from in-page javascript object
	this.extraParamCount = 0;
	if (data.adParameters) {
		this.adParameters = data.adParameters;
		var extraParamCount = 0;
		var extraReleaseParams = "";
		for (var key in data.adParameters){
			if (data.adParameters.hasOwnProperty(key)){
				for (var i=0;i<data.adParameters[key].length;i++){
					extraReleaseParams += "&param=k"+(extraParamCount)+"|"+cwpCleanAdParams(key);
					extraReleaseParams += "&param=v"+(extraParamCount)+"|"+cwpCleanAdParams(data.adParameters[key][i]);	
					extraParamCount++;
				}
			}
		}
		this.releaseCall += extraReleaseParams;
		this.extraParamCount = extraParamCount;
	}

	// Properties
	this.relRendering = null;
	this.catRendering = [];
	data.categoryFields = data.categoryFields || "";
	data.categoryFields += ((data.categoryFields.length===0)?"":((data.categoryFields.indexOf(";")==-1)?";":",")) + "SortCriteria";
	this.defaultSort = "title"; // the default sort criteria
	// If a user wants to retrieve the title, retrieve displayTitle as well to make sure we give them the right string
	if (data.categoryFields.indexOf("title")!=-1 && data.categoryFields.indexOf("DisplayTitle")==-1){
		data.categoryFields += ((data.categoryFields.indexOf(";")==-1)?";":",") + "DisplayTitle";
	}
	if (!data.categoryFields){
		this.catFields = [];
		this.customCatFields = [];
	}else{
		var tempFields = data.categoryFields.split(";");
		this.catFields = tempFields[0].split(",");
		if (tempFields.length>1){ this.catCustomFields = tempFields[1].split(","); }
	}
	if (!data.releaseFields){
		this.relFields = [];
		this.customRelFields = [];
	}else{
		var tempFields = data.releaseFields.split(";");
		this.relFields = tempFields[0].split(",");
		if (tempFields.length>1){ this.relCustomFields = tempFields[1].split(","); }
	}
	this.rootCats = [];
	this.targetPlaylistContainer = null;
	if (!data.jsonURL){ // optional property
		this.jsonURL = "http://feeds.theplatform.com/ps/JSON/PortalService/2.2/";
	}else{
		this.jsonURL = data.jsonURL;
	}
	this.currentPlay = null;
	this.previousPlay = null;
	this.playlistRootCat = null;
	this.jsonRequested = false;
	this.selectedCat = [];
	this.selectedRel = null;
	// Unless otherwise defined, we will play all the videos in a category
	this.playAllVideosInCategory = (data.playAllInCat!==false);
	this.playBlocked = false;
	this.queuedRelease = null;
	this.preloadedRelease = null;
	this.disableOmniture = (data.disableOmniture===true);

	this.cat = [];
	
	var managerObj = this;
	this.OnPlayBlocked = null;
	this.catExists = function(depth){
		var curCat = this.cat[depth];
		if (!curCat) {
			curCat = {};
			curCat.depth = depth;
			if (depth>0){
				var prevCat = this.catExists(depth-1); // Make sure all previous levels exist
				curCat.click = function(obj){managerObj.cat[depth-1].click(obj);};
				curCat.mouseover = function(obj){managerObj.cat[depth-1].mouseover(obj);};
				curCat.mouseout = function(obj){managerObj.cat[depth-1].mouseout(obj);};
				curCat.onChildrenShow = function(obj){managerObj.cat[depth-1].onChildrenShow(obj);};
				curCat.select = function(obj){managerObj.cat[depth-1].select(obj);};
				curCat.unselect = function(obj){managerObj.cat[depth-1].unselect(obj);};
			}else{
				curCat.click = function(obj){obj.select();}; // click selects the category by default
				curCat.mouseover = function(obj){};
				curCat.mouseout = function(obj){};
				curCat.onChildrenShow = function(obj){};
				curCat.select = function(obj){};
				curCat.unselect = function(obj){};
			}
			this.cat[depth] = curCat;
		}
		return curCat;
	};
	this.catExists(0);
	
	this.evtProto = function(name,depth,callback){this.catExists(depth)[name] = function(obj){callback(obj);};};
	this.catClick = function(depth,callback){this.evtProto("click",depth,callback);};
	this.catMouseover = function(depth,callback){this.evtProto("mouseover",depth,callback);};
	this.catMouseout = function(depth,callback){this.evtProto("mouseout",depth,callback);};
	this.catSelect = function(depth,callback){this.evtProto("select",depth,callback);};
	this.catUnselect = function(depth,callback){this.evtProto("unselect",depth,callback);};
	this.catOnChildrenShow = function(depth,callback){this.evtProto("onChildrenShow",depth,callback);};

	this.rel = {};
	this.relClick = function(callback){this.rel.click = callback;};
	this.relMouseover = function(callback){this.rel.mouseover = callback;};
	this.relMouseout = function(callback){this.rel.mouseout = callback;};
	this.relSelect = function(callback){this.rel.select = callback;};
	this.relUnselect = function(callback){this.rel.unselect = callback;};
	this.relOnVideoStart = function(callback){this.rel.onVideoStart = callback;};
	this.relOnVideoEnd = function(callback){this.rel.onVideoEnd = callback;};
	this.relOnVideoComplete = function(callback){this.rel.onVideoComplete = callback;};
	this.relOnVideoPause = function(callback){this.rel.onVideoPause = callback;};
	this.relOnVideoUnpause = function(callback){this.rel.onVideoUnpause = callback;};
	this.rel.click = function(obj){obj.requestVideoPlay();obj.select();}; // click selects and plays a release by default

	this.misc = {};
	this.miscFormatPageTitle = function(callback){this.misc.formatPageTitle = callback;};
	this.misc.formatPageTitle = function(rel){return (rel)?rel.title:"";};

	this.toggleDevMode = function(devMode){
		if (this.playerTag){ // If there is no player tag, we won't do anything
			this.releaseCall = this.releaseCall.replace(/&query=Categories[|][^&]*/,"");
			if (!devMode){
				this.releaseCall += "&query=Categories|" + this.playerTag;
			}
		}
	};

	this.setPlayingRelease = function(rel){
		if (this.currentPlay != rel){
			this.previousPlay = this.currentPlay;
			this.currentPlay = rel;
		}
	};

	// Methods
	this.getCategories = function(callback){
		var constructedJSON = this.jsonURL + this.categoryCall;
		$.each(this.catFields, function(i,field){
			constructedJSON += "&field=" + field;
		});
		if (this.catCustomFields){
			$.each(this.catCustomFields, function(i,field){
				constructedJSON += "&customField=" + field;
			});
		}
		var currentObj = this;
		this.jsonRequested = true;
		logDebug("Requested category feed: " + constructedJSON);
		$.ajaxSetup({cache:"true"});
		$.getJSON(constructedJSON, function(data){
			currentObj.jsonRequested = false;
			logDebug("Retrieved JSON category data.");
			currentObj.handleCategoryFeed(data,callback);
		});
		$.ajaxSetup({cache:"false"});
	};

	this.handleCategoryFeed = function(data,callback){
		if (typeof(data)== 'string'){
			return logError("Category JSON request failed: " + data);
		}
		if (data.items.length===0){
			if (typeof(this.OnNoCategories)=="function"){
				this.OnNoCategories();
			}
			return;
		}
		var currentDepth = 0;
		var rootCatLevel = this.rootCats;
		var currentCatLevel = rootCatLevel;
		var parentCat = null;
		var managerObj = this;

		$.each(data.items,function(i,catData){
			if (currentDepth < catData.depth){
				if (currentCatLevel.length===0){
					logDebug("We skipped a level of depth (expected " + currentDepth + " and got " + catData.depth + ").");
				}else{
					parentCat = currentCatLevel[currentCatLevel.length-1]; // the last category we added
					currentCatLevel = parentCat.children;
				}
				currentDepth = catData.depth;
			}else if (currentDepth > catData.depth){
				do{
					parentCat = parentCat.parentCat;
					if (parentCat===null){
						currentCatLevel = rootCatLevel;
					}else{
						currentCatLevel = parentCat.children;
					}
				}while (currentCatLevel[0].depth > catData.depth);
				currentDepth = catData.depth;
			}
			currentCatLevel.push(new cwpCategory(managerObj, catData, parentCat, managerObj.catFields, managerObj.catCustomFields));
		});
		logDebug("Processed JSON category data.");
		callback();
	};

	this.renderCategories = function(targetContainer, startDepth, endDepth, withReleases){
		logDebug("Begin rendering top level categories.");
		endDepth = (typeof(endDepth)=="number")?endDepth:-1;
		startDepth = (typeof(startDepth)=="number")?startDepth:-1;
		withReleases = (typeof(withReleases)=="boolean")?withReleases:false;
		if (this.rootCats.length>0){
			$.each(this.rootCats, function(i,cat){
				cat.renderHTML(targetContainer, startDepth, endDepth, withReleases);
			});
		}
		logDebug("End rendering top level categories.");
	};

	this.getReleasesFromPidList = function(zone,pidList,fetchMetadata,callback){
		pidList = pidList || this.pidList; // If we don't override the pidList, use the default one from initialization
		// Don't bother fetching data unless we have an array of pids
		if (pidList && pidList.length>0){
			// We use "story" as the fullTitle, as that is the bucket used for Omniture tracking
			var catData = {"depth":0,"fullTitle":"story","hasReleases":true,"ID":CWP_PLACEHOLDER_ID,"title":"FAKECAT"};
			cwpManager.rootCats[0] = new cwpCategory(cwpManager, catData);
			
			// If we should try to fetch the releases with corresponding metadata
			if (fetchMetadata){
				this.jsonRequested = true;
				var cat = cwpManager.rootCats[0];
				this.loadingPlaylistCat = cat;
				var customReleaseCall = this.releaseCall + "&query=PIDs|";
				for (var i=0;i<pidList.length;i++){
					customReleaseCall += (i===0)?pidList[i]:","+pidList[i];
				}
				cat.fetchReleases(function(releaseList){
					managerObj.jsonRequested = false;
					cat.manager.releasesLoaded();
					if (typeof(callback)=="function"){callback();}
				},null,customReleaseCall);
			}else{
				for (var i=0;i<pidList.length;i++){
					this.rootCats[0].children.push(this.getReleaseFromPid(pidList[i],zone,this.rootCats[0]));
				}
				if (pidList.length>0){
					this.rootCats[0].releasesLoaded = true;
					this.releasesLoaded();
				}
			}
		}
	};

	this.getReleaseFromPid = function(pid,zone,parentCat){
		var relData = {"categoryIDs":[CWP_PLACEHOLDER_ID],"contentCustomData":[{"title":"Show","value":""},{"title":"Episode","value":""},{"title":"Network","value":""},{"title":"Season","value":""},{"title":"Zone","value":zone}],"contentID":CWP_PLACEHOLDER_ID,"ID":CWP_PLACEHOLDER_ID,"length":120000,"PID":pid,"URL":"http://release.theplatform.com/content.select?pid=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&UserName=Unknown&Embedded=True&Portal=Global%20News&Site=" + this.site + "&TrackBrowser=True&Tracking=True&TrackLocation=True"};
		relData.URL = relData.URL.replace(/pid=[^&]*/, "pid=" + pid);
		// We repurpose the code from initialization, but since we need the params in a different format, we don't use the same code
		if (typeof(this.adParameters)=="object") {
			var extraParamCount = 0;
			for (var key in data.adParameters.keyvalues){
				if (data.adParameters.keyvalues.hasOwnProperty(key)){
					var values = data.adParameters.keyvalues[key].split(";");
					for (var i=0;i<values.length;i++){
						relData.URL += "&k"+(extraParamCount)+"="+cwpCleanAdParams(key);
						relData.URL += "&v"+(extraParamCount)+"="+cwpCleanAdParams(values[i]);
						extraParamCount++;
					}
				}
			}
		}
		return new cwpRelease(this, relData, parentCat);
	};

	// Event listeners
	this.onMediaStart = function(evt){
		logDebug("Received onMediaStart event for: " + evt.data.baseClip.contentID);
		if (!cwpManager.currentPlay){logError("Received a start event without an active release.");}
		cwpManager.currentPlay.onVideoStart(evt);
		cwpManager.updatePlaybackQueueStatus(evt.data.baseClip.noSkip);
		cwpManager.firstPlayHappened = true;
		cwpManager.ffdRewClicked = false;
		// cwpComScore(cwpManager);
	};

	this.onMediaEnd = function(evt){
		logDebug("Received onMediaEnd event for: " + evt.data.baseClip.contentID);
		if (!cwpManager.currentPlay){logDebug("Received an end event without an active release.");}
		if (!evt.data.baseClip.isAd){
			if (cwpManager.currentPlay && cwpManager.currentPlay.contentID == evt.data.baseClip.contentID){
				cwpManager.currentPlay.onVideoEnd(evt);
			}else if (cwpManager.previousPlay && cwpManager.previousPlay.contentID == evt.data.baseClip.contentID){
				cwpManager.previousPlay.onVideoEnd(evt);
			}
		}
	};

	this.onReleaseEnd = function(evt){
		logDebug("Received onReleaseEnd event for: " + cwpManager.currentPlay.ID);
		cwpManager.updatePlaybackQueueStatus(false); // Turn off queue blocking because we know the ad is over
		if (cwpManager.currentPlay.ID==CWP_PLACEHOLDER_ID || cwpManager.currentPlay.ID==evt.data.playlistID){
			cwpManager.currentPlay.onVideoComplete(evt);
		}
	};

	this.onMediaPause = function(evt){
		logDebug("Received onMediaPause event for: " + evt.data.baseClip.contentID);
		if (!cwpManager.currentPlay){logError("Received a pause event without an active release.");}
		cwpManager.currentPlay.onVideoPause(evt);
	};

	this.onMediaUnpause = function(evt){
		logDebug("Received onMediaUnpause event for: " + evt.data.baseClip.contentID);
		if (!cwpManager.currentPlay){logError("Received an unpause event without an active release.");}
		cwpManager.currentPlay.onVideoUnpause(evt);
	};

	this.ffdRewClicked = false;
	this.onNextClip = function(){
		if (!cwpManager.ffdRewClicked){
			cwpManager.ffdRewClicked = true;
			logDebug("tpController onNextClip()");
			if (!cwpManager.currentPlay){logDebug("Received an nextClip event without an active release.");}
			cwpManager.currentPlay.playNextVideo();
		}
	};

	this.onPrevClip = function(){
		if (!cwpManager.ffdRewClicked){
			cwpManager.ffdRewClicked = true;
			logDebug("tpController onPreviousClip()");
			if (!cwpManager.currentPlay){logDebug("Received an previousClip event without an active release.");}
			cwpManager.currentPlay.playPrevVideo();
		}
	};

	this.onPlayerLoaded = function(){
		logDebug("*** Player has loaded ***");
		// If we've already queued a release, start playing it.
		if (cwpManager.queuedRelease){
			cwpManager.queueReleaseForPlayback(cwpManager.queuedRelease);
			cwpManager.queuedRelease = null;
		}else if (cwpManager.anyReleasesLoaded){
			// otherwise make the first release the pre-loaded one
			if (cwpManager.preloadFirstRelease && tpController.loadReleaseURL){
				var rel = cwpManager.rootCats[0].getFirstRelease();
				tpController.loadReleaseURL(rel.URL);
				cwpManager.preloadedRelease = rel;
			}
			if (tpController.disablePlayerControls){ 
				tpController.disablePlayerControls(false);
			}
		}
	};

	this.onReleaseSelected = function(options){
		// If a release was started by the user, we need to synchronize the playlist
		if (options && options.data.userInitiated && cwpManager.preloadedRelease){
			cwpManager.currentPlay = cwpManager.preloadedRelease;
			cwpManager.preloadedRelease.select();
			cwpManager.preloadedRelease = null;
		}
	};

	this.OnNoCategories = null;

	// Add event listeners
	if (tpController){
		tpController.addEventListener("OnMediaStart","cwpManager.onMediaStart");
		tpController.addEventListener("OnMediaEnd","cwpManager.onMediaEnd");
		tpController.addEventListener("OnReleaseEnd","cwpManager.onReleaseEnd");
		tpController.addEventListener("OnMediaPause","cwpManager.onMediaPause");
		tpController.addEventListener("OnMediaUnpause","cwpManager.onMediaUnpause");
		tpController.addEventListener("OnNextClip", "cwpManager.onNextClip");
		tpController.addEventListener("OnPreviousClip", "cwpManager.onPrevClip");
		tpController.addEventListener("OnPlayerLoaded", "cwpManager.onPlayerLoaded");
		tpController.addEventListener("OnReleaseSelected", "cwpManager.onReleaseSelected");
	}

	this.anyReleasesLoaded = false;
	this.releasesLoaded = function(){
		if (!this.anyReleasesLoaded){
			this.anyReleasesLoaded=true;
			if (this.preloadFirstRelease && tpController.loadReleaseURL){
				var rel = this.rootCats[0].getFirstRelease();
				tpController.loadReleaseURL(rel.URL);
				this.preloadedRelease = rel;
			}
			if (tpController.disablePlayerControls){
				tpController.disablePlayerControls(false);
			}
		}
	};

	this.getCategory = function(key,value){
		var returnCat = null;
		$.each(this.rootCats,function(i,cat){
			returnCat = cat.getCategory(key,value);
			if (returnCat){return false;}
		});
		return returnCat;
	};

	this.getRelease = function(key,value){
		var returnCat = null;
		$.each(this.rootCats,function(i,cat){
			returnCat = cat.getRelease(key,value);
			if (returnCat){return false;}
		});
		return returnCat;
	};

	this.fetchSingleRelease = function(key,value,callback){
		if (!this.jsonRequested){
			// construct category list
			var constructedJSON = this.jsonURL + this.releaseCall + "&query=" + key + "|" + value;
			$.each(this.relFields, function(i,field){
				constructedJSON += "&field=" + field;
			});
			if (this.relCustomFields){
				$.each(this.relCustomFields, function(i,field){
					constructedJSON += "&contentCustomField=" + field;
				});
			}
			var currentObj = this;
			this.jsonRequested = true;
			$.ajaxSetup({cache:"true"});
			$.getJSON(constructedJSON, function(data){
				currentObj.jsonRequested = false;
				var result = currentObj.processSingleReleaseFeed(data,callback);
				if (typeof(callback)=="function"){callback(result);}
			});
			$.ajaxSetup({cache:"false"});
		}	
	};
	
	this.processSingleReleaseFeed = function(data,callback){
		this.singleRelease = null;
		if (typeof(data)=='string'){return logDebug("Release JSON request failed: " + data);}
		if (data.items.length!=1){
			logDebug("Single release request returned " + data.items.length + " retults.  Expected exactly 1.");
			return false;
		}else{
			this.singleRelease = new cwpRelease(this,data.items[0],null,this.relFields,this.relCustomFields);
			return true;
		}
	};

	this.findFirstParentCategory = function(rel){
		var mObj = this;
		var parentCat = null;
		$.each(mObj.rootCats,function(i,obj){
			parentCat = obj.findFirstParentCategory(rel);
			if (parentCat){return false;}
		});
		return parentCat;
	};

	this.setSelected = function(obj,isSelect){
		// If we are selecting something, call unselect on the previously selected item at the same level
		if (obj.type==CWP_CATEGORY){
			if (isSelect){
				if (this.selectedCat[obj.depth] && this.selectedCat[obj.depth] != obj){
					this.selectedCat[obj.depth].unselect();
				}
				this.selectedCat[obj.depth] = obj;
			}else if (this.selectedCat[obj.depth] == obj){
				this.selectedCat[obj] = null;
			}
		}else if (obj.type==CWP_RELEASE){
			if (isSelect){
				if (this.selectedRel && this.selectedRel != obj){
					this.selectedRel.unselect();
				}
				this.selectedRel = obj;
				// We have selected a release, update the page title if necessary
				this.setPageTitle(obj);
			}else if (this.selectedRel == obj){
				this.selectedRel = null;
			}
		}
	};

	this.setPageTitle = function(rel){
		if (this.syncPageTitle) {
			document.title = this.misc.formatPageTitle(rel);
		}
	};
	
	this.queueReleaseForPlayback = function(rel){
		if (!this.playBlocked){
			// This method won't be available until the player finishes loading.
			var jqObj = $("#" + cwpFlashID);
			if (jqObj.length>0 && jqObj[0].sendCategories){
				this.playBlocked = true; // We should block until we confirm that it's not an ad playing next
				rel.playVideo();
			}else{
				this.queuedRelease = rel;
			}
		}else{
			if (this.OnPlayBlocked){
				this.OnPlayBlocked(true,rel);
			}
			this.queuedRelease = rel;
		}
	};

	this.updatePlaybackQueueStatus = function(blockQueue){
		this.playBlocked = blockQueue;
		// If the queue has become unblocked, play the most recent video request
		if (!blockQueue && this.queuedRelease){
			logDebug("Trying to play queued video: " + this.queuedRelease.title);
			this.queuedRelease.playVideo();
			if (this.OnPlayBlocked){
				this.OnPlayBlocked(false,this.queuedRelease);
			}
			this.queuedRelease = null;
		}
	};
}

var cwpFlashParams = null;
function cwpLoadFlashObjects(params){
	if (params.finishPlayerOutput && cwpFlashParams !== null){
		params = cwpFlashParams;
		params.skipCommOutput = true;
		params.skipPlayerOutput = false;
	}else{
		params.akamaiRoot = params.akamaiRoot || "http://a123.g.akamai.net/f/123/41524/60m/webdata.globaltv.com/";
		params.width = params.width || 608;
		params.height = params.height || params.width/16*9+20;
		params.url = params.url || window.location.href.replace(/(&|)(release|category)(ID|id|PID|pid)=[^&]*/,"").replace(/[?]$/,"");
		params.targetContainer = params.targetContainer || "playerFrame";
		params.swfFolder = params.swfFolder || "http://webdata.globaltv.com/global/canwestPlayer/swf/4.1/"; //"http://a123.g.akamai.net/f/123/41524/0h/webdata.globaltv.com/global/canwestPlayer/swf/4.1/";
		params.omnitureAccount = params.omnitureAccount || "canfood";
		
		params.flashID = params.flashID || cwpFlashID;
		cwpFlashID = params.flashID; // reset the flash ID we'll use in all the canwestPlayer code
		params.version = params.version || "9.0.0.0";
		params.flashBGColor = params.flashBGColor || "#000000";
		params.companionAdInfo = params.companionAdInfo || "|bannerSizes=120x240,468x60,250x250,160x90|bannerRegions=sky,banner,bbox,button";
	
		// Add parameters for other things we may want to override
		params.defaultParams = {
			quality:"high",
			scale:"noscale",
			salign:"tl",
			menu:"true",
			bgcolor:"#131313",
			allowFullScreen:"true",
			allowScriptAccess:"always",
			wmode:"opaque"
		};
		// Add/override any provided parameters
		if (params.params){
			for (var key in params.params){
				if (params.params.hasOwnProperty(key)){
					params.defaultParams[key] = params.params[key];
				}
			}
		}
	
		params.defaultVariables = {
			allowFullScreen:"true",
			allowLoad:"true",
			autoPlay:"false",
			showFullTime:"false",
			showTitle:"false",
			useDefaultLoadingIndicator:"false",
			useDefaultBufferingIndicator:"false",
			logLevel:"debug",
			emailServiceURL:"http://www.hgtv.ca/global/applications/resourceserver/handlers/email.ashx",
			ID:params.flashID,
			releaseURL:"",
			width:params.width,
			height:params.height,
			backgroundColor:"0x131313",
			controlBackgroundColor:"0x9AD3B7",
			controlColor:"0xFFFFFF",
			controlFrameColor:"0x545759",
			controlHoverColor:"0xbbffdd",
			controlSelectedColor:"0x05593d",
			frameColor:"0xFFFFFF",
			pageBackgroundColor:"0x000000",
			playProgressColor:"0x05593d",
			scrubberColor:"0xFFFFFF",
			scrubberFrameColor:"0x00CCFF",
			scrubTrackColor:"0xFFFFFF",
			loadProgressColor:"0xFFFFFF",
			textBackgroundColor:"0xFFFFFF",
			textColor:"0x000000",
			playerURL:params.playerURL || (params.url + ((params.url.indexOf("?")>0)?"&":"?") + "releasePID={releasePID}"),
			// Removed this line because the RSS Link doesn't work anyway.
			// RSSURL:params.rssURL || (params.akamaiRoot + "MRSS.ashx?U=" + params.url),
			skinURL:params.swfFolder + "skinFlat.swf"
		};
	
		// Add/override any provided variables
		if (params.variables){
			for (var key in params.variables){
				if (params.variables.hasOwnProperty(key)){
					params.defaultVariables[key] = params.variables[key].replace("FLASHPATH/", params.swfFolder);
				}
			}
		}
	
		params.defaultPlugins = {
			adcomponent:"URL=" + params.swfFolder + "inStream.swf|priority=1|host=ad.ca.doubleclick.net|overlaySize=400x50" + params.companionAdInfo,
			tracking:[
				"URL=" + params.swfFolder + "omnitureMedia.swf|account=" + params.omnitureAccount + "|visitorNamespace=" + params.omnitureAccount + "|dc=112|debug=true",
				"URL=" + params.swfFolder + "comScore.swf|playerName=" + params.omnitureAccount + "|c2=3005660|c4=" + params.comscoreSiteId
			],
			overlay:[
				"URL=" + params.swfFolder + "BufferAnimationPlugin.swf|subURL=" + params.swfFolder + "BufferAnimation.swf",
				"URL=" + params.swfFolder + "customEmailFormPlugin.swf|subURL=" + params.swfFolder + "customEmailFormSubplugin.swf",
				"URL=" + params.swfFolder + "customLinkFormPlugin.swf|subURL=" + params.swfFolder + "customLinkFormSubplugin.swf"
			]
		};
	
		// Add/override any provided plugins
		for (var key in params.plugins){
			if (params.plugins.hasOwnProperty(key)){
				// If it's a string, just add it
				if (typeof(params.plugins[key])=="string"){
					params.defaultPlugins[key] = params.plugins[key].replace("FLASHPATH/", params.swfFolder);
				// otherwise it's an array and we'll need to replace the FLASHPATH in all values
				}else{
					params.defaultPlugins[key] = params.plugins[key];
					for (var i=0;i<params.defaultPlugins[key].length;i++){
						params.defaultPlugins[key][i] = params.defaultPlugins[key][i].replace("FLASHPATH/", params.swfFolder);
					}
				}
			}
		}
	}

	if (params.skipPlayerOutput){
		cwpFlashParams = params;
	}

	if (!params.skipCommOutput){
		// Initialize the commManager and register the player we are going to create
		if (params.commBeforeThisDiv){
			tpSetCommManagerID("communicationwidget",true,params.swfFolder+"commManager.swf",params.commBeforeThisDiv);
		}else{
			tpSetCommManagerID("communicationwidget",true,params.swfFolder+"commManager.swf");
		}
		tpRegisterID(params.flashID);
	}
	
	if (!params.skipPlayerOutput){
		// Create the flash object
		var so = new SWFObject(params.swfFolder+"flvPlayer.swf", params.flashID, params.width, params.height, params.version, params.flashBGColor);
		// Add all params
		for (var key in params.defaultParams){
			if (params.defaultParams.hasOwnProperty(key)){
				so.addParam(key, params.defaultParams[key]);
			}
		}
		// Add all variables, except for plugins
		for (var key in params.defaultVariables){
			if (params.defaultVariables.hasOwnProperty(key)){
				so.addVariable(key, params.defaultVariables[key]);
			}
		}
		// Add all plugins
		var pluginIndex=0;
		for (var key in params.defaultPlugins){
			if (params.defaultPlugins.hasOwnProperty(key)){
				// If it's a string, just add it
				if (typeof(params.defaultPlugins[key])=="string"){
					so.addVariable("plugin"+pluginIndex, "type=" + key + "|" + params.defaultPlugins[key]);
					pluginIndex++;
					// otherwise it's an array and we have to replace all of them
				}else{
					for (var i=0;i<params.defaultPlugins[key].length;i++){
						so.addVariable("plugin"+pluginIndex, "type=" + key + "|" + params.defaultPlugins[key][i]);
						pluginIndex++;
					}
				}
			}
		}
		
		// Write the flash object to the page
		so.write(params.targetContainer);
		cwpParams = null;
	}
}

