// from http://www.sitepoint.com/blogs/2006/01/17/javascript-inheritance/
function copyPrototype(descendant, parent) {
    var sConstructor = parent.toString();
    var aMatch = sConstructor.match( /\s*function (.*)\(/ );
    if ( aMatch != null ) { descendant.prototype[aMatch[1]] = parent; }
    for (var m in parent.prototype) {
        descendant.prototype[m] = parent.prototype[m];
    }
};

function PropertyChangeLoudMouth() {
	this.listeners = new Object();
	this.listenersMap = new Object();
	this.notificationDisabled = false;
}

PropertyChangeLoudMouth.prototype.notifyAll = function(property, value) {
    if(!this.notificationDisabled) {
		if(this.listeners[property])
		{
			for(var i=0; i<this.listeners[property].length; i++) 
			{
				this.listeners[property][i](property, value);
			}
		}
	}
}

PropertyChangeLoudMouth.prototype.enableNotification = function() {
    this.notificationDisabled = false;
}

PropertyChangeLoudMouth.prototype.disableNotification = function() {
    this.notificationDisabled = true;
}
	
PropertyChangeLoudMouth.prototype.registerListener = function(property, listener, uid) {
	if(!this.listeners[property])
	{
		this.listeners[property] = new Array();
	}
	
	if(uid) {
		if(!this.listenersMap[property]) {
			this.listenersMap[property] = new Object();
		}
		
		this.listenersMap[property][uid] = listener;
	}
	
	this.listeners[property].push(listener);
}

PropertyChangeLoudMouth.prototype.deregisterListener = function(property, uid) {
	if(this.listenersMap[property] && this.listeners[property]) {
		var listener = this.listenersMap[property][uid];
		for(var i=0; i<this.listeners[property].length; i++) {
		//for(var i in this.listeners[property]) {
			if(listener == this.listeners[property][i]) {
				this.listeners[property].splice(i, 1);
				break;
			}
		}
	}
}

function WP_Utils() {}

WP_Utils.validateNumeric = function(val) {

	return (/^[0-9]+$/.test(val));
}

WP_Utils.emptyStrIfNull = function(obj, str) {
	if(obj) return obj
	else if(str) return str
	else return "";
}

WP_Utils.arrayFilter = function(array, fn) {
	var results = new Array();
	// handle safari as well
	if( array.constructor == Array || array.constructor.toString().indexOf('Array') != -1 ) {
		for(var idx=0; idx<array.length; idx++) {
			if(fn(array[idx])) {
				results.push(array[idx]);
			}
		}
	} else {
		for(var idx in array) {
			if(fn(array[idx])) {
				results.push(array[idx]);
			}
		}
	}
	return results;
}

WP_Utils.formatNumber = function(amountStr, numberOfDecimalPlaces, fixedParam, wholeParam) {
	var fixed = false || fixedParam;
	var wholeOnly = false || wholeParam;
	var amount = Number(amountStr);
	var temp1 = Math.pow(10,numberOfDecimalPlaces);
	var wholeNum = Math.round(amount);
	var temp2 = Math.round(amount * temp1);
	
	if(typeof(amount) != "number") {
		return amountStr;
	}
	
	if(numberOfDecimalPlaces == 0 || (wholeOnly && wholeNum == amount)) {
		return "" + wholeNum;
	} else {
		var numberStr = "" + temp2;
		var frontPadding = numberOfDecimalPlaces - numberStr.length + 1;
		for(var i = 0; i < frontPadding; i++) {numberStr = "0" + numberStr;}
		var whole = String(numberStr).substr(0,numberStr.length-numberOfDecimalPlaces);
		var part = String(numberStr).substr(numberStr.length-numberOfDecimalPlaces, numberStr.length);
		

		if(!fixed) {
			var partStr = "" + part;
			var i = partStr.length;
			for(;i > 0 && partStr.charAt(i-1) == '0'; i--);
			if(i == 0) return whole;
			else if(i == partStr.length) return whole + "." + part;
			else return whole + "." + part.substr(0,i);
		} else {
			return whole + "." + part;			
		}
	}
}

function WP_Amount(id, amount, currency) {
	this.id = id;
	this.amount = amount;
	this.currency = currency;
	this.textValue = currency.format(amount);
}

function WP_DecimalCurrency(code, wholeSymbol, partSymbol, unicodeSymbol) {
	this.code = code;
	this.wholeSymbol = wholeSymbol;
	this.partSymbol = partSymbol;
	this.unicodeSymbol = unicodeSymbol;
}

WP_DecimalCurrency.prototype.format = function(amount) {
	if(amount >= 1 || amount == 0) {
		return this.wholeSymbol + WP_Utils.formatNumber(amount, 2, true, true);
	} else {
		return WP_Utils.formatNumber(amount * 100, 0, true, true) + this.partSymbol;
	}
}

WP_DecimalCurrency.prototype.formatUnicode = function(amount) {
	
	if(amount >= 1 || amount == 0) {
		var numberOfDecimalPlaces = 2;
			
		return this.unicodeSymbol + WP_Utils.formatNumber(amount, numberOfDecimalPlaces, true, true);
	} else {
		return WP_Utils.formatNumber(amount * 100, 0, true, true) + this.partSymbol;
	}
}

WP_DecimalCurrency.prototype.fixedFormat = function(amount) {
	var number = WP_Utils.formatNumber(amount, 2, true);
	
	if(number != "N.aN") {
		return this.wholeSymbol + number;
	} else {
		return amount;
	}
}

WP_DecimalCurrency.prototype.formatPennies = function(amount) {

}

WP_DecimalCurrency.GBP = new WP_DecimalCurrency("GBP", "&pound;", "p", "\u00a3");
WP_DecimalCurrency.EUR = new WP_DecimalCurrency("EUR", "&euro;", "c", "\u20ac");
WP_DecimalCurrency.USD = new WP_DecimalCurrency("USD", "&#x00024;", "c", "\u0024");

function Test() {
	this.tableEl = document.getElementById("results");
	this.currentTest;
	
	this.runTests = function(tests) {
		for(var i in tests) {
			this.currentTest = i;
			var result = tests[i](this);
		}
	}
	
	this.assert = function(name, expected, actual, comparisonFn) {
		var result = comparisonFn(expected, actual);
		this.outputResult({"name" : name, "result": result, "expected" : expected, "actual" : actual});
	}

	this.outputResult = function(result) {
		var newRow = this.tableEl.insertRow(this.tableEl.rows.length);
		var groupCell = newRow.insertCell(0);
		var testCell = newRow.insertCell(1);
		var resultCell = newRow.insertCell(2);
		var expectedCell =  newRow.insertCell(3);
		var actualCell = newRow.insertCell(4);
		
		groupCell.innerHTML = this.currentTest;
		testCell.innerHTML = result["name"];
		if(result["result"] == true) {
			resultCell.innerHTML = "PASS";
			resultCell.style.color = "green";
		} else {
			resultCell.innerHTML = "FAIL";
			resultCell.style.color = "red";
		}
		expectedCell.innerHTML = result["expected"];
		actualCell.innerHTML = result["actual"];
	}
}

Test.equal = function(e,a) {
	return e == a;
}

	


function XstanWagerProvider(xstanCardManager) {
	this.betTypeManager = new BetTypeManager();
	this.xcm = xstanCardManager;
	this.multiRacePools = new Object;
	this.forexManager;
	
	this.init = function() {
		this.getAvailableTracks();
		this.betTypeManager.init();
	}
	
	this.setForexManager = function(fMng) {
		this.forexManager = fMng;
	}

	this.getForexManager = function() {
		return this.forexManager;
	}
	
	this.getAvailableTracks = function() {
		var cards = this.xcm.getCards();
		var adaptedCards = new Array();
		
		for(var i in cards) {
			adaptedCards.push(new XstanCardAdapter(cards[i]));
		}
		
		return adaptedCards;
	};
	
	this.getAvailableRaces = function(trackCode) {
		var races = this.xcm.getCard(trackCode).races;
		var adaptedRaces = new Array();
		
		for(var i in races) {
			adaptedRaces.push(new XstanRaceAdapter(races[i]));
		}
		
		return adaptedRaces;
	};
	
	
	// out of a default selection of 7 return those that are applicable to the betType
	this.getAvailableAmounts = function(trackCode, raceCode, betType) {
		if(betType) {
			var supportedAvailableAmounts = new Array();
			var availableAmounts = XstanWagerProvider.AvailableAmounts[betType.currency];
			for(var i=0; i<availableAmounts.length; i++) {
				var amount = availableAmounts[i];
				if(amount.amount >= betType.minStake && amount.amount <= betType.maxStake) {
				    if(this.forexManager && betType.currency != this.forexManager.getSiteCurrency()) {
				    	availableAmounts[i].textValue = WP_DecimalCurrency[betType.currency].format(amount.amount) + '(' + WP_DecimalCurrency[this.forexManager.getSiteCurrency()].format(amount.amount / this.forexManager.getRate(betType.currency).rate) + ')';
					} 
					supportedAvailableAmounts.push(availableAmounts[i]);
				}
			}
			return supportedAvailableAmounts;
		} else {
			// get default currency from somewhere
			return XstanWagerProvider.AvailableAmounts.EUR;
		}
	};

	this.getAvailableBetTypes = function(trackCode, race) {
		var card = this.xcm.getCard(trackCode);
		
		var adaptedBetTypes = new Array();
		var uniqueBetTypes = new Object();
		
		// create datastructure with box types etc as variants - card.pools returns all pools i.e bet type / race combinations
		// only need one of each bet type
		this.singleRacePools = new Array();
		
		// get a unique map of pools for the card
		var cardPoolCodes = new Object;
		for(var i in card.pools) {
			cardPoolCodes[card.pools[i].pool_code] = true;
		}
		
		for(var i in card.pools) {
			var pool = card.pools[i];
			
			// may be more than one supported bet type for the pool code - e.g. EXA keybox
			var poolSupportedTypes = this.betTypeManager.getBetTypes(pool.pool_code);

			for(var j=0; j<poolSupportedTypes.length; j++) {
				var supportedType = poolSupportedTypes[j];
				var betType = this.buildXstanPoolAdapter(card.pools[i], card.pools, supportedType, cardPoolCodes);
				//var add = !uniqueBetTypes[supportedType.id];
				//if( betType.poolObj.provider ) add = betType.poolObj.provider == pool.type;
				//if(add) {
				if(!uniqueBetTypes[supportedType.id]){ 
					adaptedBetTypes.push(betType);
					
					// multi race pools can mean different things depending on the source so always re-insert
					//if(betType.type == "exotic" && !this.multiRacePools[pool.pool_code]) {
					// add to multi race pool set
					if(betType.type == "exotic") {
						this.multiRacePools[pool.pool_code] = card.pools[i];
					}
						
					uniqueBetTypes[supportedType.id] = supportedType.id;
				}
			}
		}
				
		return adaptedBetTypes;
	};
	
	// return object is a Race has that bet Type or null
	// cristian : adapt to verify if a race has a bet type
	this.verifyRaceAndBetType = function(trackCode, betType, raceId) {
		var pool;
		if( betType ) {
			// betType is the pool - but time to add runner info etc
			if(betType.type == "single" || betType.type == "multi") {
			    var cc = this.xcm.getCard(trackCode);
			    if(cc) {
			      var r = cc.getRace(raceId);
			      if(r) {
					pool = r.getPool(betType.code);
			      }
			    }
			} else if(betType.type == "exotic") {
				// multi race pools
				pool = this.multiRacePools[betType.poolCode];
			}
		}	
		return pool;
	};
	
	// returns test pools for all bet types and variants
	this.getPool = function(trackCode, betType, race) {
		// betType is the pool - but time to add runner info etc
		if(betType.type == "single" || betType.type == "multi") {
			var pool = this.xcm.getCard(trackCode).getRace(race.id).getPool(betType.code);
			
			
			var poolObj;
			if( pool ) {
			
				var legs = new Array();
				var xstanLegs = pool.getLegs();
			
				if( pool.use_site_currency == "true" )
				{
					poolObj = {numberOfLegs: pool.num_legs, "legs" : legs, "pool_oid" : pool.pool_oid, "currency" : this.forexManager.getSiteCurrency(), "has_favourite" : pool.has_favourite};
				}
				else
				{
					poolObj = {numberOfLegs: pool.num_legs, "legs" : legs, "pool_oid" : pool.pool_oid, "currency" : pool.currency, "has_favourite" : pool.has_favourite};
				}
				var count = 0;
				for(i in xstanLegs) {
					// check number of legs - don't add any more than needed - i.e. 1 for a box pool bet
					if(count++ < betType.numberOfLegs) {
						var leg = XstanWagerProvider.buildXstanLegAdapter(xstanLegs[i], race, betType);
						legs.push(leg);
					} //else {
//						break;
//					}
				}
			
				legs.sort(function(a,b) {
					return a.legNumber - b.legNumber;
				});
			} else {
				poolObj = {numberOfLegs: 0, "legs" : new Array(), "pool_oid" : -1, "currency" : "", "has_favourite" : false};
			}
				
			return poolObj;
		} else if(betType.type == "exotic") {
			// multi race pools
			var pool = this.multiRacePools[betType.poolCode];
			
			var legs = new Array();
			var xstanLegs = pool.getLegs();

			if( pool.use_site_currency == "true" )
			{
				poolObj = {numberOfLegs: pool.num_legs, "legs" : legs, "pool_oid" : pool.pool_oid, "currency" : this.forexManager.getSiteCurrency(), "minBet" : pool.min_stake, "maxBet" : pool.max_stake, "has_favourite" : pool.has_favourite };
			}
			else
			{
				poolObj = {numberOfLegs: pool.num_legs, "legs" : legs, "pool_oid" : pool.pool_oid, "currency" : pool.currency, "minBet" : pool.min_stake, "maxBet" : pool.max_stake, "has_favourite" : pool.has_favourite};
			}		
			
			for(var i in xstanLegs) {
				// get race
				var adaptedRace = new XstanRaceAdapter(xstanLegs[i].race);
				
				// build pool object with unpopulated legs
				var leg = XstanWagerProvider.buildXstanLegAdapter(xstanLegs[i], adaptedRace, betType);
				legs.push(leg);
			}
			
			return poolObj;
		} else {
			// unsupported pool type
			var common = new Common();
			common.createAlert("unsupported test pool type: " + betType.poolCode + ", variant: " + betType.variant);
		}
	};
		
	this.getLegEntries = function(uniqueTrackID, race, amount, pool) {
		var raceNumber = race.raceNumber;
		var poolCode = pool.poolCode;
		var legs = pool.poolObj.legs;
		
		var entriesForLegs = new Array();
		for(i=0; i<legs.length; i++) {
			var xstanLeg = legs[i].xstanLeg;
			
			var adaptedRunners = new Array();
			
			for(var j in xstanLeg.race.runners) {
				var legEntry = XstanWagerProvider.buildXstanLegEntryAdapter(xstanLeg.race.runners[j]);
				adaptedRunners.push(legEntry);
			}
			
			entriesForLegs.push(adaptedRunners);
		}
		
		return entriesForLegs;
		
	};
	
	this.buildXstanPoolAdapter = function(xstanPool, xstanPools, type, cardPoolCodes) {

	if( xstanPool.use_site_currency == "true" )
	{
		var pool = new Pool(type, type.name, type.poolCode, type.id, type.type, type.numLegs ? type.numLegs : xstanPool.num_legs, cardPoolCodes, 
							this.forexManager.getSiteCurrency(), xstanPool.type, xstanPool.has_favourite);
	}
	else
	{
		var pool = new Pool(type, type.name, type.poolCode, type.id, type.type, type.numLegs ? type.numLegs : xstanPool.num_legs, cardPoolCodes, 
							xstanPool.currency, xstanPool.type, xstanPool.has_favourite);
	}
	
	pool.minStake = xstanPool.min_stake;
	pool.maxStake = xstanPool.max_stake;
	return pool;
}
}

	XstanWagerProvider.AvailableAmounts = {
		"GBP" : new Array(
			new WP_Amount("50c", 0.5, WP_DecimalCurrency.GBP),
			new WP_Amount("1", 1, WP_DecimalCurrency.GBP),
			new WP_Amount("2", 2, WP_DecimalCurrency.GBP),
			new WP_Amount("5", 5, WP_DecimalCurrency.GBP),
			new WP_Amount("10", 10, WP_DecimalCurrency.GBP),
			new WP_Amount("25", 25, WP_DecimalCurrency.GBP),
			new WP_Amount("50", 50, WP_DecimalCurrency.GBP)),
		"EUR" : new Array(
			new WP_Amount("50c", 0.5, WP_DecimalCurrency.EUR),
			new WP_Amount("1", 1, WP_DecimalCurrency.EUR),
			new WP_Amount("2", 2, WP_DecimalCurrency.EUR),
			new WP_Amount("5", 5, WP_DecimalCurrency.EUR),
			new WP_Amount("10", 10, WP_DecimalCurrency.EUR),
			new WP_Amount("25", 25, WP_DecimalCurrency.EUR),
			new WP_Amount("50", 50, WP_DecimalCurrency.EUR)),
		  "USD" : new Array(
		   new WP_Amount("50c", 0.5, WP_DecimalCurrency.USD),
		   new WP_Amount("1", 1, WP_DecimalCurrency.USD),
		   new WP_Amount("2", 2, WP_DecimalCurrency.USD),
		   new WP_Amount("5", 5, WP_DecimalCurrency.USD),
		   new WP_Amount("10", 10, WP_DecimalCurrency.USD),
		   new WP_Amount("25", 25, WP_DecimalCurrency.USD),
		   new WP_Amount("50", 50, WP_DecimalCurrency.USD))
	};

XstanCardAdapter = function(xstanCard) {
	this.id = xstanCard.card_oid;
	this.textValue = xstanCard.card_name;
	this.code = xstanCard.card_oid;
	this.current_race = xstanCard.current_race;
	this.mtp = xstanCard.mtp;
}


function XstanRaceAdapter(xstanRace) {
	this.id = xstanRace.tote_race_number;
	this.textValue = xstanRace.race_name;
	this.name = xstanRace.race_name;
	this.raceNumber = xstanRace.display_race_number;
	this.mtp = xstanRace.card.mtp;
	this.time = xstanRace.post_time;
	this.date = xstanRace.card.card_date;
	this.raceDescription = xstanRace.race_description;
	this.raceTypeDescription = xstanRace.race_type_description;
	this.status = xstanRace.status;
	this.current_race = xstanRace.card.current_race;

	this.display_jpt_units = false;
	this.display_plp_units = false;
	for( var i in xstanRace.card.pools )
	{
		var pool = xstanRace.card.pools[i];
		//if( pool.pool_code == 'JPT' || pool.pool_code == 'PLP' )
		if( pool.pool_code == 'JPT' )
		{
			if( pool.type == 'hri' )
			{
				var last_race_no = 0;
				for( var l in pool.legs )
				{
					var leg = pool.legs[l];
					if( leg.race.tote_race_number > last_race_no ) last_race_no = leg.race.tote_race_number;
				}
				if( last_race_no == this.raceNumber )
				{
					if( pool.pool_code == 'JPT' ) this.display_jpt_units = true;
					//else this.display_plp_units = true;
				}
			}
		}
	}
}

XstanWagerProvider.buildXstanLegAdapter = function(xstanLeg, race, pool) {
	return {legNumber : xstanLeg.sequence_number, "race" : race, "xstanLeg" : xstanLeg, "pool" : pool};
}

XstanWagerProvider.buildXstanLegEntryAdapter = function(xstanRunner, runner) {
	
	return {
		"racingAsNameForUI" : xstanRunner.runner_name,
		"toteRunnerNumber" : xstanRunner.program_number, 
		"programNumber" : xstanRunner.program_number, 
		"id" : xstanRunner.program_number, 
		"scratched" : xstanRunner.isScratched(), 
		"bettingInterestNumber" : xstanRunner.betting_interest_number, 
		"odds" : xstanRunner.getWinOdds(),
		"owner" : xstanRunner.owner,
		"jockey" : xstanRunner.jockey,
		"trainer" : xstanRunner.trainer,
		"winDividends" : xstanRunner.getWinDividends(),
		"placeDividends" : xstanRunner.getPlaceDividends()
	};
}

var poolsMap;
var pools = new Array(this.EXA, this.PKn, this.QNL, this.SPR, this.TRI, this.TRO, this.WPS);


/**
 * create a map of all supported pool types including box, key box variants etc
 **/
function initPools() {
	poolsMap = new Object;
	for(var i in pools) {
		var pool = pools[i];
		var supportedPoolTypes = pool.getSupportedPoolTypes();
		for(var j in supportedPoolTypes) {
			poolsMap[supportedPoolTypes[j]] = pool;
		}
	}
}

function WP_Leg(raceObj) {
	this.raceObj = raceObj;
	
	// entries
	this.setEntries() = function(entries) {
		this.entries = entries;
	}
}

// determines which bet types and variants are supported

function BetType(id, poolCode, name, type, option, numLegs, reqNumLegSelections, provider, variant) {
	this.id = id;
	this.poolCode = poolCode;
	this.name = name;
	this.type = type;
	this.option = option;
	this.numLegs = numLegs;
	this.reqNumLegSelections = reqNumLegSelections;
	this.provider = provider;
	this.variant = variant;
	
	this.textValue = name;
	
	this.parent;
	this.variants = new Array();
	this.variantsPlusParent = new Array();		// convenience for UI stuff
};

BetType.prototype.isVariant = function() {
	return this.variant != null;
};

BetType.prototype.getVariants = function() {
	if(this.parent) {
		return this.parent.variants;
	} else {
		return this.variants;
	}
};

BetType.prototype.getVariantsIncludingParent = function() {
	if(this.parent) {
		return this.parent.variantsPlusParent;
	} else {
		return this.variantsPlusParent;
	}	
};

function BetTypeManager() {
	this.betTypes = new Array();
	this.betTypeMap = new Object();
};

BetTypeManager.DEFAULT_BET_TYPES = {
		"WIN" : {"poolCode" : "WIN", "id" : "WIN", "name" : "Win", "type" : "single"},
		"PLC" : {"poolCode" : "PLC", "id" : "PLC", "name" : "Place", "type" : "single"},
		"E/W" : {"poolCode" : "E/W", "id" : "E/W", "name" : "Each Way", "type" : "single"},
		"EXA" : {"poolCode" : "EXA", "id" : "EXA", "name" : "Exacta", "type" : "multi", "option" : "2", "variants" : {"box" : "EBX", "combs" : "EBC",/* "keybox" : "EXK",*/ "wheel" : "EXW", "banker" : "EXB", "boxbanker" : "EBR"}},
		"EBX" : {"poolCode" : "EBX", "id" : "EBX", "name" : "Reverse Exacta", "parent" : "EXA", "type" : "multi", "reqNumLegSelections" : "2", "option" : "4", "variant" : "box", "provider" : "hri"},
		"EBR" : {"poolCode" : "EBX", "id" : "EBR", "name" : "Reverse Banker", "parent" : "EXA", "type" : "multi", "reqNumLegSelections" : "1", "option" : "8", "variant" : "boxbanker", "provider" : "hri", "numLegs" : "2"},
		"EBC" : {"poolCode" : "EXA", "id" : "EBC", "name" : "Reverse Exacta", "parent" : "EXA", "type" : "multi", "reqNumLegSelections" : "2", "option" : "4", "variant" : "box", "provider" : "uk", "numLegs" : "1"},
		//"EXK" : {"poolCode" : "EXA", "id" : "EXK", "name" : "Exacta Keybox", "parent" : "EXA", "type" : "multi", "variant" : "keybox"},
		"EXW" : {"poolCode" : "EXA", "id" : "EXW", "name" : "Exacta Banker", "parent" : "EXA", "type" : "multi", "option" : "8", "variant" : "wheel", "provider" : "hri"},
		"EXB" : {"poolCode" : "EXA", "id" : "EXB", "name" : "Exacta Banker", "parent" : "EXA", "type" : "multi", "option" : "8", "variant" : "banker", "provider" : "uk"},
		"TRI" : {"poolCode" : "TRI", "id" : "TRI", "name" : "Trifecta", "type" : "multi", "option" : "2", "variants" : {"box" : "TBX", "combs" : "TBC", "banker" : "TRB"}},
		"TBX" : {"poolCode" : "TBX", "id" : "TBX", "name" : "Reverse Trifecta", "type" : "multi", "reqNumLegSelections" : "3", "option" : "4", "numLegs" : "1", "parent" : "TRI", "variant" : "box", "provider" : "hri"},
		"TBC" : {"poolCode" : "TRI", "id" : "TBC", "name" : "Reverse Trifecta", "type" : "multi", "reqNumLegSelections" : "3", "option" : "4", "numLegs" : "1", "parent" : "TRI", "variant" : "box", "provider" : "uk"},	
		"TRB" : {"poolCode" : "TRI", "id" : "TRB", "name" : "Trifecta Banker", "type" : "multi", "option" : "8", "parent" : "TRI", "variant" : "banker", "numLegs" : "2"},
		"TRO" : {"poolCode" : "TRO", "id" : "TRO", "name" : "Trio", "type" : "multi", "reqNumLegSelections" : "3", "numLegs" : "1", "variants" : {"banker" : "TOB"}},
		"TOB" : {"poolCode" : "TRO", "id" : "TOB", "name" : "Trio Banker", "type" : "multi", "parent" : "TRO", "numLegs" : "2", "option" : "8", "variant" : "banker"},
		//"TRX" : {"poolCode" : "TRX", "id" : "TRX", "name" : "Trio Box", "type" : "multi", "parent" : "TRO", "variant" : "box"},
		"JPT" : {"poolCode" : "JPT", "id" : "JPT", "name" : "Jackpot", "type" : "exotic"},
		"PLP" : {"poolCode" : "PLP", "id" : "PLP", "name" : "Placepot", "type" : "exotic"},
		"QPT" : {"poolCode" : "QPT", "id" : "QPT", "name" : "Quadpot", "type" : "exotic"},
		"SP6" : {"poolCode" : "SP6", "id" : "SP6", "name" : "Scoop 6", "type" : "exotic"},
		"SP7" : {"poolCode" : "SP7", "id" : "SP7", "name" : "Scoop 7", "type" : "exotic"}};

BetTypeManager.prototype.init = function(supportedBetTypesIn) {
	var supportedBetTypes;
	if(supportedBetTypesIn) {
		supportedBetTypes = supportedBetTypesIn;
	} else {
		supportedBetTypes = BetTypeManager.DEFAULT_BET_TYPES;
	}

	if(typeof(supportedBetTypes) != 'object') {
	  alert('supportedBetTypesIn is not an object');
	}
	
	// construct fully referenced data structure
	for(var i in supportedBetTypes) {
		var supportedBetType = supportedBetTypes[i];
		var betType = new BetType(supportedBetType.id, supportedBetType.poolCode, supportedBetType.name, supportedBetType.type, supportedBetType.option, supportedBetType.numLegs, supportedBetType.reqNumLegSelections, supportedBetType.provider, supportedBetType.variant);
		this.betTypes.push(betType);
		this.betTypeMap[betType.id] = betType;
	}
	
	// add parents and variants
	for(var i in supportedBetTypes) {
		var supportedBetType = supportedBetTypes[i];
		var betType = this.betTypeMap[supportedBetType.id];
		if(supportedBetType.parent) {
			var parentBetType = this.betTypeMap[supportedBetType.parent];
			betType.parent = parentBetType;
		}
		if(supportedBetType.variants) {
			for(var j in supportedBetType.variants) {
				var variantId = supportedBetType.variants[j];
				var variantBetType = this.betTypeMap[variantId];
				betType.variants.push(variantBetType);
			}
			betType.variantsPlusParent = betType.variants.concat([betType]);
		}
	}
};

BetTypeManager.prototype.getSupportedBetTypes = function() {
	return this.betTypes;
};

BetTypeManager.prototype.getBetTypes = function(poolCode) {
	return WP_Utils.arrayFilter(this.betTypes, function(e) {return e.poolCode == poolCode;});
}

BetTypeManager.prototype.getBetType = function(poolCode) {
	var filteredBetTypes =  WP_Utils.arrayFilter(this.betTypes, function(e) {return e.poolCode == poolCode && !e.isVariant();});
	return filteredBetTypes[0];
}

function Pool(poolObj, name, code, id, type, numberOfLegs, cardPoolCodes, currency, provider, has_favourite) {
	this.poolObj = poolObj;
	this.name = name;
	this.code = code;
	this.textValue = name;
	this.legEntries = new Object;
	this.legs = new Object;	// map of legs keyed on legNumber
	this.id = id;
	this.type = type;
	this.numberOfLegs = numberOfLegs;
	this.cardPoolCodes = cardPoolCodes;
	this.currency = currency;
	this.provider = provider;
	this.has_favourite = has_favourite;
	//this.numLegSelections = numLegSelections;
	
	this.setLegsInner = function(legs) {
		legs.sort(function(a,b) {return a.legNumber - b.legNumber;});
		this.poolObj.legs = legs;
		for(var i = 0; i < this.poolObj.legs.length; i++) {
			if( i >= this.numberOfLegs ) break;
			var leg = new Object;
			leg.leg = this.poolObj.legs[i];
			
			if (i > 0) {leg.previousLeg = this.poolObj.legs[i-1];}
			if (i <= this.poolObj.legs.length) {leg.nextLeg = this.poolObj.legs[i+1];}
			
			this.legs[this.poolObj.legs[i].legNumber] = leg;
		}
	}
	
	if(poolObj) {
		this.poolCode = poolObj.poolCode;
		this.pool_oid = poolObj.pool_oid;
		
		if(poolObj.legs) {
			this.setLegsInner(this.poolObj.legs);
		}
	}
	
	
	// legs
	this.setEntriesForLeg = function(legNumber, entries) {
		// if this pool doesn't have a favourite, remove the favourite runner
		if( !this.has_favourite )
		{
			for( var i in entries )
			{
				// TODO shouldn't hard code the favourite runner number
				if( entries[i].bettingInterestNumber == 41 )
				{
					entries.splice(i,1);
				}
			}
		}
		this.legEntries[legNumber] = entries;
	}
}

Pool.prototype.getLegRunner = function(legId, runnerId) {
	var entries = this.legEntries[legId];
	for(var i in entries) {
		if(entries[i].programNumber == runnerId) {
			return entries[i];
		}
	}
}

Pool.prototype.addLegRunner = function(legId, runnerId) {
	var entries = this.legEntries[legId];
	var common = new Common();
	for(var i in entries) {
		common.createAlert("programNumber: " + entries[i].programNumber + ", id: " + entries[i].id);
		if(entries[i].programNumber == runnerId) {
			entries[i].selected = true;
		}
	}
	this.legs[legId].leg.selectionSummary = this.getSummary(entries);
	common.createAlert("pool select leg runner: " + legId + ", " + runnerId + ", " + this.legs[legId].leg.selectionSummary);
}

Pool.prototype.getSummary = function(entries) {
	var str = "...";
	for(var i in entries) {
		if(entries[i].selected) {
			var common = new Common();
			common.createAlert("selected");
			str += entries[i].programNumber;
		}
	}
	
	return str;
}

Pool.prototype.removeLegRunner = function(legId, runnerId) {
	var common = new Common();
	common.createAlert("pool select leg runner: " + legId + ", " + runnerId);
	var entries = this.legEntries[legId];
	for(var i in entries) {
		if(entries[i].id == runnerId) {
			entries[i].selected = false;
		}
	}
	this.legs[legId].leg.selectionSummary = this.getSummary(entries);
	common.createAlert("pool select leg runner: " + legId + ", " + runnerId + ", " + this.legs[legId].leg.selectionSummary);
}

Pool.prototype.setLegs = function(legs) {
	this.setLegsInner(legs);
}

Pool.prototype.getLegs = function() {
	this.setLegsInner(legs);
}

Pool.prototype.getNumberOfLegs = function() {
	return this.poolObj.legs.length;
}

Pool.prototype.getFirstLeg = function() {
	return this.poolObj.legs[0];
}

Pool.prototype.getNextLeg = function(legNumber) {
	return this.legs[legNumber].nextLeg;
}

Pool.prototype.getPreviousLeg = function(legNumber) {
	return this.legs[legNumber].previousLeg;
}

Pool.getPool = function(poolCode) {
		if(!poolsMap) {
			initPools();
		}
		
		if(poolCode.search(/pk/i)>=0 || poolCode.search(/p1/i)>=0)
		{
			return PKn;
		}
		else
		{
			return poolsMap[poolCode];
		}
}

Pool.prototype.getVariants = function() {
	return null
}

Pool.prototype.getVariantBetTypes = function() {
	var cardPoolCodes = this.cardPoolCodes;
	var provider = this.provider;
	var variants = WP_Utils.arrayFilter(this.poolObj.getVariantsIncludingParent(), function(e) {return cardPoolCodes[e.poolCode] != null;});
	variants = WP_Utils.arrayFilter(variants, function(e) {return e.provider ? provider == e.provider : true; });
	return variants;
}

Pool.prototype.getVariantBetTypesExcludingParent = function(provider) {
	var cardPoolCodes = this.cardPoolCodes;
	var provider = this.provider;
	var variants = WP_Utils.arrayFilter(this.poolObj.getVariants(), function(e) {return cardPoolCodes[e.poolCode] != null;});
	variants = WP_Utils.arrayFilter(variants, function(e) {return e.provider ? provider == e.provider : true; });
	return variants;
}

/*

function EXA(poolObj, variant, name, code, id, legs, numLegSelections) {
	this.Pool(poolObj, name, code, id, "multi", legs?legs:2,numLegSelections);
	this.variant = variant;
}

copyPrototype(EXA, Pool);

EXA.getSupportedPoolTypes = function() {return ["EXA"];}
EXA.getBaseName = function() {return "Exacta";};
EXA.getInstances = function(poolObj) {
	return new Array(
		new EXA(poolObj, "", poolObj.name, poolObj.poolCode, poolObj.poolCode,2));
}
EXA.prototype.getVariants = function() {
	var poolObj = this.poolObj;
	return new Array(
		new EXA(poolObj, "box", poolObj.name + " (Box)", poolObj.boxCode, poolObj.boxCode + "1",1,2),
		new EXA(poolObj, "keybox", poolObj.name + " (Key Box)", poolObj.boxCode, poolObj.boxCode + "2",2));
}

function PKn(poolObj, name, code, id, numberOfLegs) {
	this.Pool(poolObj, name, code, id, "exotic");
}

copyPrototype(PKn, Pool);

PKn.getSupportedPoolTypes = function() {return ["DBL"];}
PKn.getBaseName = function() {return "Pick N or Double";};
PKn.getInstances = function(poolObj) {
	return [new PKn(poolObj, poolObj.name, poolObj.poolCode, poolObj.poolCode, poolObj.numberOfLegs )];
}

function QNL(poolObj, variant, name, code, id) {
	this.Pool(poolObj, name, code, id, "multi", 5);
	this.variant = variant;
}

copyPrototype(QNL, Pool);

QNL.getSupportedPoolTypes = function() {return ["QNL"];}
QNL.getBaseName = function() {return "Quinella";};
QNL.getInstances = function(poolObj) {
	return [
		new QNL(poolObj, "", poolObj.name, poolObj.poolCode, poolObj.poolCode)];
}
QNL.prototype.getVariants = function() {
	var poolObj = this.poolObj;
	return [
		new QNL(poolObj, "box", poolObj.name + " (Box)", poolObj.boxCode, poolObj.boxCode)];
}

function SPR(poolObj, variant, name, code, id) {
	this.Pool(poolObj, name, code, id, "multi", 4);
	this.variant = variant;
}

copyPrototype(SPR, Pool);

SPR.getSupportedPoolTypes = function() {return ["SPR","SFC"];}
SPR.getBaseName = function() {return "Superfecta";};
SPR.getInstances = function(poolObj) {
	return [
		new SPR(poolObj, "", poolObj.name, poolObj.poolCode, poolObj.poolCode)];
}
SPR.prototype.getVariants = function() {
	return [
		new SPR(this.poolObj, "box", this.poolObj.name + " (Box)", this.poolObj.boxCode, this.poolObj.boxCode)];
}


function TRI(poolObj, variant, name, code, id) {
	this.Pool(poolObj, name, code, id, "multi", 3);
	this.variant = variant;
}

copyPrototype(TRI, Pool);

TRI.getSupportedPoolTypes = function() {return ["TRI"];}
TRI.getBaseName = function() {return "Trifecta";};
TRI.getInstances = function(poolObj) {
	return [
		new TRI(poolObj, "", poolObj.name, poolObj.poolCode, poolObj.poolCode)];
}
TRI.prototype.getVariants = function() {
	return [
		new TRI(this.poolObj, "box", this.poolObj.name + " (Box)", this.poolObj.boxCode, this.poolObj.boxCode)];
}

function TRO(poolObj, variant, name, code, id, legs, numLegSelections) {
	this.Pool(poolObj, name, code, id, "multi", legs?legs:3, numLegSelections);
	this.variant = variant;
}

copyPrototype(TRO, Pool);

TRO.getSupportedPoolTypes = function() {return ["TRO"];}
TRO.getBaseName = function() {return "Trio";};
TRO.getInstances = function(poolObj) {
	return [
		new TRO(poolObj, "", poolObj.name, poolObj.poolCode, poolObj.poolCode,3)];
}
TRO.prototype.getVariants = function() {
	return [
		new TRO(this.poolObj, "box", this.poolObj.name + " (Box)", this.poolObj.boxCode, this.poolObj.boxCode,1,3)];
}

function WPS(poolObj, name, code, id) {
	this.Pool(poolObj, name, code, id, "single", 1);
}

copyPrototype(WPS, Pool);

WPS.getSupportedPoolTypes = function() {return ["WIN","SHW","WP","WPS","WS","PLC","PS","USP"];}
WPS.getBaseName = function() {return "Win/Place/Show";};
WPS.getInstances = function(poolObj) {
	return [new WPS(poolObj, poolObj.name, poolObj.poolCode, poolObj.poolCode)];
}
*/
var WP_Message1 = "The bet amount is outside the minimum and maximum for the bet type. Select a different bet amount.";
var WP_Message2 = "You have selected another track. Select OK to accept the new track and clear your selections or CANCEL to keep your existing track.";
var WP_Message3 = "You have selected another race. Select OK to accept the new race and clear your selections or CANCEL to keep your existing race.";
var WP_Message4 = "You have selected another Bet Type. Select OK to accept the Bet Type and clear your selections or CANCEL to keep your existing Bet Type.";

function WP_Leg(legNumber, race, entries) {
	this.PropertyChangeLoudMouth();
	this.legNumber = legNumber;
	this.race = race;
	this.selections = new Array();
	this.selectionSummary = "";
	this.entries = entries;
};

copyPrototype(WP_Leg, PropertyChangeLoudMouth);

WP_Leg.prototype.clearSelections = function(selectionNumberStr, entry) {
	this.selections.splice(0,this.selections.length);
	this.selectionSummary = this.getSummary();	
	this.notifyAll("selection", this);
}

WP_Leg.prototype.addSelection = function(selectionNumberStr, entry) {
	if(entry && entry.scratched) {
		return;
	}
	
	var selectionNumber = parseInt(selectionNumberStr);
	var found = false;
	var lastSelectionNumber = -1;
	for(var i=0; i<this.selections.length; i++) {
		var thisSelectionNumber = parseInt(this.selections[i].selectionNumber);
		if(selectionNumber == thisSelectionNumber) {
			// don't add
			found = true;
			break;
		} else if(selectionNumber > lastSelectionNumber && selectionNumber < thisSelectionNumber) {
			// add here
			this.selections.splice(i,0,{"selectionNumber" : selectionNumber, "selected" : true, "entry" : entry});
			found = true;
			break;
		}
		lastSelectionNumber = thisSelectionNumber;
	}
	
	if(!found) {
		this.selections.push({"selectionNumber" : selectionNumber, "selected" : true, "entry" : entry});
	}
	
	this.selectionSummary = this.getSummary();
	this.notifyAll("selection", this);
}

WP_Leg.prototype.removeSelection = function(selectionNumberStr) {
	var selectionNumber = parseInt(selectionNumberStr);
	for(var i=0; i<this.selections.length; i++) {
		var thisSelectionNumber = parseInt(this.selections[i].selectionNumber);
		if(selectionNumber == thisSelectionNumber) {
			// remove
			this.selections.splice(i,1);
		}
	}
	this.selectionSummary = this.getSummary();	
	this.notifyAll("selection", this);
}

WP_Leg.prototype.getSummaryElement = function(selectionNumberStr, i, selections) {
	var selectionNumber = parseInt(selectionNumberStr);
	var j;
	for(j = i+1; j < selections.length; j++) {
		var nextSelectionNumber = parseInt(selections[j-1].selectionNumber)+1;
		if( nextSelectionNumber != parseInt(selections[j].selectionNumber)) {
			break;
		}
	}
	if(j == i+1) {
		return [1,selectionNumber];
	} else if(j == i+2) {
		return [2,selectionNumber + ", " + parseInt(selections[i+1].selectionNumber)];
	} else if(j == selections.length) {
		return [j-i,selectionNumber + "-" + parseInt(selections[j-1].selectionNumber)];
	} else {
		return [j-i,selectionNumber + "-" + parseInt(selections[j-1].selectionNumber)];
	}
}

WP_Leg.prototype.getSummary = function() {
	var str = "";
	var strElement = "";
	var i;
	for(i = 0; i < this.selections.length;) {
		var thisSelectionNumber = this.selections[i].selectionNumber;
		var result = this.getSummaryElement(thisSelectionNumber, i, this.selections);
		i += result[0];
		strElement += result[1];
		str += strElement;
		strElement = ", ";
	}
	
	return str;
}

WP_Leg.prototype.getBetSummary = function() {
	var str = "";
	var strElement = "";
	var i;
	for(i = 0; i < this.selections.length; i++) {
		strElement += this.selections[i].selectionNumber;
		str += strElement;
		strElement = ",";
	}
	
	return str;
}

// reverse of getBetSummary
WP_Leg.prototype.setSelection = function(legSelection) {
	var selectionNumbers = legSelection.split(",");
	for(var i=0; i<selectionNumbers.length; i++) {
		var selectionNumber = selectionNumbers[i];
		
		for(var j in this.entries) {
			var entry = this.entries[j];
			if(entry.bettingInterestNumber == selectionNumber) {
				this.selections.push({"selectionNumber" : selectionNumber, "selected" : true, "entry" : entry});
			}
		}
	}
	this.selectionSummary = this.getSummary();	
	this.notifyAll("selection", this);
}

/* Wager control -  maintains current bet status etc - registers listeners etc */
function Wager() {
	this.PropertyChangeLoudMouth();
	this.track = "";
	this.race = "";
	this.betType = "";
	this.amount = "";
	this.betTotal = 0;
	this.validBet = false;
	this.selectionSummary = "";
	this.numberOfLegs = 0;
	this.legs = new Object;
	this.hasSelections = false;
	this.cleared = false;
	this.currency = "";
	this.use_site_currency = false;
}

copyPrototype(Wager, PropertyChangeLoudMouth);

Wager.prototype.update = function() {
	this.recalculate();
}
	
Wager.prototype.setForexManager = function(forexManager) {
		this.forexManager = forexManager;
	};

Wager.prototype.setTrack = function(track) {
		this.track = track;
	};

Wager.prototype.setRace = function(race) {
		this.race = race;
	};

Wager.prototype.setBetType = function(betType) {
		this.betType = betType;
	};

Wager.prototype.setAmount = function(amount) {
		this.amount = amount;
	};
	
Wager.prototype.getFormattedAmount = function() {
	if(WP_DecimalCurrency[this.currency]) {
	    if(this.forexManager && this.currency != this.forexManager.getSiteCurrency()) {
			return WP_DecimalCurrency[this.currency].format(this.amount.amount) + '(' + WP_DecimalCurrency[this.forexManager.getSiteCurrency()].format(this.amount.amount / this.forexManager.getRate(this.currency).rate) + ')';
		} else {
			return WP_DecimalCurrency[this.currency].format(this.amount.amount);
		}
	} else {
		return this.amount.amount;
	}
}
	
Wager.prototype.getFormattedBetTotal = function() {
	if(WP_DecimalCurrency[this.currency]) {
	    if(this.forexManager && this.currency != forexManager.getSiteCurrency()) {
		  return WP_DecimalCurrency[this.currency].format(this.betTotal) + '(' + WP_DecimalCurrency[forexManager.getSiteCurrency()].format(this.betTotal / forexManager.getRate(this.currency).rate) + ')';
		} else {
		  return WP_DecimalCurrency[this.currency].format(this.betTotal);
		}
	} else {
		return this.betTotal;
	}
}
	
Wager.prototype.setValidBet = function(valid) {
		this.validBet = valid;
		this.notifyAll("validBet", valid);
	};
	
Wager.prototype.setBetTotal = function(betTotal) {
		this.betTotal = betTotal;
	};

Wager.prototype.getBetTotal = function() {
		return  this.betTotal;
	};

Wager.prototype.setCurrency = function(currency) {
		this.currency = currency;
	};

Wager.prototype.getCurrency = function() {
		return this.currency;
	};
	
Wager.prototype.setUseSiteCurrency = function(use_site_currency) {
		this.use_site_currency = use_site_currency;
	};

Wager.prototype.getUseSiteCurrency = function() {
		return this.use_site_currency;
	};

Wager.prototype.getRate = function() {
	    if(this.forexManager && this.currency != forexManager.getSiteCurrency()) {
	      return forexManager.getRate(this.currency)
	    } else {
	      return null;
	    }
	};
	
Wager.prototype.getLegAsBitmap = function(leg) {
	if( leg.selections == null ) return 0;
	var bm = new Bitmap();
	for( var i = 0; i < leg.selections.length; i++ )
	{
		bm.set( leg.selections[i].selectionNumber );
	}
	return bm;
}	
	
Wager.prototype.recalculate = function() {
		var multiplier = 1;
		var legsWithSelectionsCount = 0;
		this.selectionsSummary = "";
		var sep = "";
		var requiredSelections = this.betType.poolObj.reqNumLegSelections ? this.betType.poolObj.reqNumLegSelections : 1;
		for(var i in this.legs) {
			var selections = this.legs[i].selections;
			if(selections.length >= requiredSelections)
				legsWithSelectionsCount += 1;
			this.selectionsSummary = this.selectionsSummary + sep + this.legs[i].selectionSummary;
			sep = " / ";
		}

		// edge case for Trios										
		if( this.betType.poolCode == "TRO" && this.numberOfLegs == 1) {
			var selections = this.legs[1].selections;
			for( j = 0; j < 3; j++ )
				multiplier *= selections.length -j;
			multiplier /= 6;
		// trio bankers	
		} else if( this.betType.poolCode == "TRO" && this.betType.poolObj.variant == "banker" ) {
			var bm1 = this.getLegAsBitmap( this.legs[1] );
			var bm2 = this.getLegAsBitmap( this.legs[2] );
			var count = 0;
			// double banker
			if( bm1.count() == 2 ) {
				count = bm2.count() - ( bm1.and( bm2 ).count() );
				multiplier = count;
			// single banker	
			} else {
				count = bm2.count() - ( bm1.and( bm2 ).count() );	
				multiplier = ( count * ( count - 1 ) ) / 2;
			}
		// trifecta banker		
		} else if( this.betType.poolCode == "TRI" && this.betType.poolObj.variant == "banker" ) {
			var bm1 = this.getLegAsBitmap( this.legs[1] );
			var bm2 = this.getLegAsBitmap( this.legs[2] );
			var count = bm2.count() - ( bm1.and( bm2 ).count() );
			multiplier = ( count * ( count - 1 ) );
		// edge case for E/W
		} else if( this.betType.poolCode == "E/W" ) {
		    if(this.legs[1])
				multiplier = this.legs[1].selections.length * 2;
		// box/reverse costing	
		} else if( this.betType.type == "multi" && this.numberOfLegs == 1 ) {
			var selections = this.legs[1].selections;
			for( j = 0; j < requiredSelections; j++ )					
				multiplier *= selections.length - j;
		// reverse exacta banker
		} else if( this.betType.poolObj.variant == "boxbanker" ) {
			multiplier = ( this.legs[1].selections.length * this.legs[2].selections.length ) - this.getLegAsBitmap( this.legs[1] ).and( this.getLegAsBitmap( this.legs[2] ) ).count();
            multiplier *= 2;
		// exacta and similar		
		} else if( this.betType.type == "multi" && this.numberOfLegs == 2 ) {
			multiplier = this.legs[1].selections.length * this.legs[2].selections.length;
			multiplier -= this.getLegAsBitmap( this.legs[1] ).and( this.getLegAsBitmap( this.legs[2] ) ).count();
		// trifecta and similar			
		} else if( this.betType.type == "multi" && this.numberOfLegs == 3 ) {
			var bm1 = this.getLegAsBitmap( this.legs[1] );
			var bm2 = this.getLegAsBitmap( this.legs[2] );
			var bm3 = this.getLegAsBitmap( this.legs[3] );
			multiplier = 
				( bm1.count() * bm2.count() * bm3.count() )
				- ( bm1.and(bm2).count() * bm3.count() )
				- ( bm1.and(bm3).count() * bm2.count() )
				- ( bm2.and(bm3).count() * bm1.count() )
				+ ( 2 * bm1.and(bm2).and(bm3).count() );	
		// everything else		
		} else {
			for(var i in this.legs) {
				var selections = this.legs[i].selections;
				multiplier *= selections.length;
			}	
		}	
		
		if(legsWithSelectionsCount == this.numberOfLegs && multiplier > 0) {
			this.setBetTotal(this.amount.amount * multiplier);
			this.setValidBet(true);
		} else {
			this.setBetTotal(0);
			this.setValidBet(false);
		}
		
		this.hasSelections = legsWithSelectionsCount > 0;
		if(this.hasSelections) {
		  this.cleared = false;
		}
		
		this.notifyAll("update", this);
	};
	
Wager.prototype.clear = function() {
	    this.cleared = true;
		this.setValidBet(false);
		this.setBetTotal(0);
	}
	
Wager.prototype.clearSelections = function() {
    this.cleared = true;
	for(var i in this.legs) {
		this.legs[i].clearSelections();
	}
}
	
Wager.prototype.setSelections = function(selections) {
	if(selections) {
	    this.cleared = false;
		// split into legs
		var legs = selections.split("/");
		var legIndex = 0;
		for(var i in this.legs) {
			var legSelection = legs[legIndex++];
			this.legs[i].setSelection(legSelection);
		}
	}
	
	this.notifyAll("updateSelection", this);
}
	
Wager.prototype.getBetSummary = function() {
		var betSummary = "";
		var sep = "";
		for(var i in this.legs) {
			betSummary = betSummary + sep + this.legs[i].getBetSummary();
			sep = "/";
		}
		
		return betSummary;
}

Wager.getWagerUIAdapter = function(wager) {
	// adapt a wager object as specified in wagerpad UI interface document
	// convert map to array
	var legArray = new Array();
	for(var i in wager.legs) {
		legArray.push(wager.legs[i]);
		
		for(var j=0; j<wager.legs[i].selections.length; j++) {
			var entry = wager.legs[i].selections[j].entry;
			entry.horseName = entry.racingAsNameForUI;
			entry.jockeyName = entry.jockey;
		}
		
		var race = wager.legs[i].race;
		race.raceDescription = race.name;
		race.raceTime = race.time;
		
	}
	
	var raceNumber = "";
	var raceDate = "";
	
	// only set for single race bets
	if(wager.betType.type != "exotic" && legArray[0]) {
		raceNumber = legArray[0].race.raceNumber;
	}

	if( legArray[0] ) {
		raceDate = legArray[0].race.date;
	}
	
	var wagerAdapter = {
      validWager : wager.validBet,
      "raceDate" : raceDate,
      "raceNumber" : raceNumber,
      track : {code : wager.track.code, trackDescription: wager.track.textValue},
      amount : wager.amount.amount,
      betType : wager.betType.textValue,
      option : wager.betType.poolObj.option,
      totalBetAmount : wager.betTotal,
      selectionsSummary : wager.selectionsSummary,
      getBetSummary : function() {return wager.getBetSummary();},
      numberOfLegs : wager.numberOfLegs,
      legs : legArray,
      pool_oid : wager.pool_oid,
      getFormattedAmount : function() {return wager.getFormattedAmount();},
      getFormattedBetTotal : function() {return wager.getFormattedBetTotal();},
      getBetTotal : function() {return wager.getBetTotal();},
      getCurrency : function() {return wager.getCurrency();},
      getRate : function() {return wager.getRate();},
      betTypeId : wager.betType.id
    }
    
    return wagerAdapter;
}


// Manages wagering dependencies, combinations etc - interactions with the wager object
// should be via the wager manager as it determines when to update available amounts, recalculate the total etc.
function WagerManager(wagerProvider) {
	this.PropertyChangeLoudMouth();
	this.wager = new Wager();
	if(wagerProvider.getForexManager()) {
		this.wager.setForexManager(wagerProvider.getForexManager());
	}
	this.availableBetTypes;
	
	if(wagerProvider) {
		this.wagerProvider = wagerProvider;
	} else {
		this.wagerProvider = new WagerProvider();
	}
	
	this.pool = null;
	
	this.init = function() {
		this.wagerProvider.init();
	}

	this.wagerChange = function() {
		// check the current state and determine whether current pool type is valid
		if(this.wager.track && this.wager.amount && this.wager.betType)
		{
			if(this.wager.race || this.wager.betType.type == "exotic") {
				// if there is a pool set then check it's the right one for this selection
				if(false && this.pool) {
					// do nothing for now
				} else {
					
					var poolData = this.wagerProvider.getPool(this.wager.track.code, this.wager.betType, this.wager.race);
					this.pool = this.wager.betType;
					this.pool.setLegs(poolData.legs);
					this.pool.minBet = poolData.minBet;
					this.pool.maxBet = poolData.maxBet;
					this.pool.currency = poolData.currency;
					this.wager.pool_oid = poolData.pool_oid;
					
					if( this.pool.use_site_currency == "true" )
					{
						this.wager.currency = this.forexManager.getSiteCurrency();
					}
					else
					{
						this.wager.currency = poolData.currency;
					}
					
					
					//alert("- venit" + this.pool.poolObj.legs.length);
					var legs = this.pool.poolObj.legs;
					
					var legEntries = this.wagerProvider.getLegEntries(
						this.wager.track.uniqueTrackID,
						this.wager.race,
						this.wager.amount.textValue,
						this.pool);
					
					var legCount = 0;
					
					for(i in this.wager.legs) {
						delete this.wager.legs[i];
					}
					
					for(var i=0;i < legs.length;i++)
					{	
						if( legCount >= this.pool.numberOfLegs ) break;
						legs[i].legEntries = legEntries[i];
						this.pool.setEntriesForLeg(legs[i].legNumber, legEntries[i]);
						
						this.wager.legs[legs[i].legNumber] = new WP_Leg(legs[i].legNumber, legs[i].race, legEntries[i]);
						legCount += 1;
					}
					
					this.wager.numberOfLegs = legCount;
					
					this.wager.recalculate();
					this.notifyAll("pool", this.pool);
					this.notifyAll("wagerUpdate", this.wager);
				}
			} else {
				for(i in this.wager.legs) {
					delete this.wager.legs[i];
				}
				this.wager.numberOfLegs = legCount;
				this.wager.recalculate();
				this.notifyAll("pool", null);
				this.notifyAll("wagerUpdate", this.wager);
			}
		}
	}	
	
	var thisObj = this;
	
// register listeners to the wager object
//	this.wager.registerListener("amount", function(property, value) {thisObj.wagerChangeListener(property, value);});
//	this.wager.registerListener("betType", function(property, value) {thisObj.wagerChangeListener(property, value);});
//	this.wager.registerListener("race", function(property, value) {thisObj.wagerChangeListener(property, value);});
//	this.wager.registerListener("track", function(property, value) {thisObj.wagerChangeListener(property, value);});
}

copyPrototype(WagerManager, PropertyChangeLoudMouth);
	

WagerManager.prototype.getAvailableTracks = function() {
		return this.wagerProvider.getAvailableTracks();
	};
	
WagerManager.prototype.getAvailableRaces = function() {
		return this.wagerProvider.getAvailableRaces(this.wager.track.code);
	};
	
WagerManager.prototype.getAvailableAmounts = function() {
		var betTypeCode = null;
		if(this.wager.betType) betTypeCode = this.wager.betType.poolCode;
		return this.wagerProvider.getAvailableAmounts(null, null, this.wager.betType);
	};
	
WagerManager.prototype.verifyRaceAndBetType = function (cardCode,matchedBetType,raceId)
	{
		return this.wagerProvider.verifyRaceAndBetType(cardCode,matchedBetType,raceId);
	}


WagerManager.prototype.getAvailableBetTypes = function() {
		// what is returned if no race selected?
		// It's up to the wager provider how the bet types are returned. Variants may
		// be treated as top level bet types or they may be nested within straight
		// bet types. Depends on the UI how they are displayed.
		this.betTypes = this.wagerProvider.getAvailableBetTypes(this.wager.track.code, this.wager.race);
	
		return this.betTypes;
	};
	
WagerManager.prototype.populateEntries = function(pool) {
		var legs = pool.poolObj.legs;
		
		var uniqueTrackID = this.wager.track.uniqueTrackID;
		var raceNumber = this.wager.race.raceNumber;
		var amount = this.wager.amount.textValue;
		var poolCode = pool.poolCode;
		
		var legEntries = this.wagerProvider.getLegEntries(uniqueTrackID, raceNumber, amount, poolCode);
		
		for(var i in legEntries)
		{
			pool.setEntriesForLeg(legs[i].legNumber, legEntries[i]);
		}
		return pool;
	};
	
WagerManager.prototype.getPool = function() {
		return this.wager.betType;
	}
	
WagerManager.prototype.clearSelections = function() {
	this.wager.clearSelections();
	this.wagerChange();
}
	
// formatted as cardCode, raceNumber, betCode, amount
WagerManager.prototype.preselect = function(preselection) {
	if(!preselection.cardCode) {
		return false;
	}

	var tracks = this.getAvailableTracks();
	for(i in tracks) {
		if(tracks[i].code == preselection.cardCode) {
			this.wager.setTrack(tracks[i]);
			break;
		}
	}

	// cristian
	// adapt, first the bet must to be set and then a race that 
	// is configured for this bet must be displayed
	var matchedBetType = null;
	var betTypes = this.getAvailableBetTypes();

	if(preselection.betCode) {
		for(i in betTypes) {
			if(betTypes[i].poolCode == preselection.betCode) {
				matchedBetType = betTypes[i];				
				break;
			}
		}
	}
	
	// default to WIN if available
	if(matchedBetType == null) {
		for(i in betTypes) {
			if(betTypes[i].poolCode == "WIN") {
				matchedBetType = betTypes[i];
				break;
			}
		}		
	}
	
	if(matchedBetType == null && betTypes[0]) {
		matchedBetType = betTypes[0];
	}

	if(matchedBetType != null) {
		this.wager.setBetType(matchedBetType);
	}
	
	var races = this.getAvailableRaces();
	var matchedRace = null;	
	
	// cristian
	// adapt,select a race that can accept selected bet
	
	var raceMatchBet = true;
	if (this.verifyRaceAndBetType(preselection.cardCode,matchedBetType,preselection.raceNumber) == null)
		raceMatchBet = false;
	
	if(preselection.raceNumber) {
		//var races = this.getAvailableRaces()
		for(i=0; i<races.length; i++) {
			if((races[i].raceNumber == preselection.raceNumber)&&(raceMatchBet == true)) {
				matchedRace = races[i];
				break;
			}
			
			else
				if (this.verifyRaceAndBetType(preselection.cardCode,matchedBetType,races[i].raceNumber) != null)
					{
						matchedRace = races[i];
						break;
					}
		}
	} 
	
	if( matchedRace == null ) {
		var matchedRace = null;
		for(i=0; i<races.length; i++) {
			if(races[i].status != Race.CLOSED && races[i].status != Race.OFFICIAL ) {
				matchedRace = races[i];
				break;
			}
		}
		if(matchedRace == null && races[0]) matchedRace = races[0];	
	}
	if(matchedRace != null) this.wager.setRace(matchedRace);				

	// protection if other bet amount is > or < than current bet amount limit
	// if amount is not in interval it is set to null
	if( matchedBetType != null ) {
		if (preselection.amount == null || matchedBetType.minStake > preselection.amount || preselection.amount > matchedBetType.maxStake)
		{
		 	preselection.amount = null;
		}
	}	

	var matchedAmount = null;
	var amounts = this.getAvailableAmounts(null, null, preselection.betCode)
	if(preselection.amount) {	
		for(i=0; i<amounts.length; i++) {
			if(amounts[i].amount == preselection.amount) {
				matchedAmount = amounts[i];
				break;
			}
		}
	}
	
	// the amount was found
	if (matchedAmount != null)
		{
		 this.wager.setAmount(matchedAmount);
		 return;
		}
	
	
	// the amount is null or is under or upper the limit	
	if( matchedAmount == null && amounts[0] && preselection.amount == null )
		{
		 matchedAmount = amounts[0];
		 this.wager.setAmount(matchedAmount);
		 return;
		}
	
	// if amount is != null then it is other amount type.
	if (matchedAmount == null && amounts[0] && preselection.amount >= matchedBetType.minStake && preselection.amount <= matchedBetType.maxStake)
		{
		  matchedAmount = new Object();
		  matchedAmount.currency = amounts[0].currency;
		  matchedAmount.id = preselection.amount;
		  matchedAmount.amount = preselection.amount;
		  
		  this.wager.setAmount(matchedAmount);
		  return;
		}
	
//	this.wagerChange();
}
		
WagerManager.prototype.setBetType = function(betType) {
	this.wager.setBetType(betType);
	if(this.wager.amount && !this.validateAmount(this.wager.amount))
	{
		var amounts = this.getAvailableAmounts()
		for(i=0; i<amounts.length; i++) {
			this.wager.setAmount(amounts[i]);
			break;
		}
	}	
	this.wagerChange();
};

// quick fudge until pool object model cleaned up
WagerManager.prototype.setBetType2 = function(betType) {
	var pools = WP_Utils.arrayFilter(this.betTypes, function(e) {return e.id == betType.id;});
	if(pools[0]) {
		this.setBetType(pools[0]);
	} else {
		return false;
	}
};

WagerManager.prototype.setTrack = function(track) {
	this.wager.setTrack(track);
	this.wagerChange();
};

WagerManager.prototype.setRace = function(race) {
	this.wager.setRace(race);
	this.wagerChange();
};

WagerManager.prototype.validateAmount = function(amount, betType) {
	if(!betType) {
		betType = this.wager.betType;
	}
	var validAmount = true;
	if(betType && betType.minStake) {
		if(amount.amount >= betType.minStake) {
			validAmount = true;
		} else {
			validAmount = false;
		}
	}
	
	if(validAmount && betType && betType.maxStake) {
		if(amount.amount <= betType.maxStake) {
			validAmount = true;
		} else {
			validAmount = false;
		}
	}
	
	return validAmount;
}	


WagerManager.prototype.validateAmountMin = function(amount, betType) {
	if(!betType) {
		betType = this.wager.betType;
	}
	var validAmount = true;
	if(betType && betType.minStake) {
		if(amount.amount >= betType.minStake) {
			validAmount = true;
		} else {
			validAmount = false;
		}
	}
	return validAmount;
}	


WagerManager.prototype.validateAmountMax = function(amount, betType) {
	if(!betType) {
		betType = this.wager.betType;
	}
	var validAmount = true;
	
	if(validAmount && betType && betType.maxStake) {
		if(amount.amount <= betType.maxStake) {
			validAmount = true;
		} else {
			validAmount = false;
		}
	}
	
	return validAmount;
}	


WagerManager.prototype.setAmount = function(amount) {
	var validAmount = this.validateAmount(amount);
	if(validAmount) {
		this.wager.setAmount(amount);
	
		this.wager.recalculate()
		this.notifyAll("wagerUpdate", this.wager);
	} else {
		var common = new Common();
		common.createAlert("invalid amount: " + amount.amount);
	}
};

WagerManager.prototype.setCurrency = function(currency) {
	this.wager.setCurrency(currency);
};

WagerManager.prototype.recalculateBet = function() {this.wager.recalculate();};
WagerManager.prototype.clearBet = function() {this.wager.clear();};
WagerManager.prototype.addLegRunner = function(legNumber, runner) {
	var entries = this.pool.getLegRunner(legNumber, runner);
	this.wager.legs[legNumber].addSelection(runner, entries);
	this.wager.recalculate();
	this.notifyAll("wagerUpdate", this.wager);
};
WagerManager.prototype.removeLegRunner = function(legNumber, runner) {
	var entries = this.pool.getLegRunner(legNumber, runner);
	this.wager.legs[legNumber].removeSelection(runner, entries);
	this.wager.recalculate();
	this.notifyAll("wagerUpdate", this.wager);
};
	
WagerManager.prototype.setSelections = function(selections) {
	this.wager.setSelections(selections);
	this.wager.recalculate();
	this.notifyAll("wagerUpdate", this.wager);
};
WagerManager.prototype.addLegRunners = function(legNumber, runners) {
    for(i in runners) {
		var entry = this.pool.getLegRunner(legNumber, runners[i].obj.bettingInterestNumber);
		this.wager.legs[legNumber].addSelection(runners[i].obj.bettingInterestNumber, entry);
	}
	this.wager.recalculate();
	this.notifyAll("wagerUpdate", this.wager);
};
WagerManager.prototype.removeLegRunners = function(legNumber, runners) {
    for(i in runners) {
		var entry = this.pool.getLegRunner(legNumber, runners[i].obj.bettingInterestNumber);
		this.wager.legs[legNumber].removeSelection(runners[i].obj.bettingInterestNumber, entry);
	}
	this.wager.recalculate();
	this.notifyAll("wagerUpdate", this.wager);
};
	

WagerManager.prototype.placeBet = function() {
	
	this.notifyAll("placeBet", this.wager);
};

WagerManager.prototype.suspend = function() {
	this.disableNotification();
};

WagerManager.prototype.resume = function() {
	this.enableNotification();
};
	
/** 
 * wagerPadUI is an implementation of the WagerPadUI interface. WagerPadUI implementations are responsible
 * for the actual UI rendering etc dependent on the deployment. For Bet Plus call centre it might be
 * BPCallCentreWagerPadUI. For Bet Plus online it might very well be BPOnlineWagerPadUI. WagerPad collaborators
 * do not need to access the WagerPadUI directly. All interaction is via the WagerPad interface.
 **/
function WagerPad(wagerPadUI, wagerProvider) {
	this.wagerPadUI = wagerPadUI;
	this.uiContext;
	this.pageController;
	
	// wager manager is independent of the UI
	this.wagerManager = new WagerManager(wagerProvider);
	
	/* uiContext is optional and will only be defined if needed */
	this.init = function(uiContext) {
		this.uiContext = uiContext
		
		this.wagerPadUI.init(this.wagerManager);
		this.pageController = wagerPadUI.pageController;
		var thisObj = this;
	};
		  
	/* useful for suspending/resuming resource hungry AJAX polling / listener notification etc */
	this.suspend = function() {this.wagerManager.suspend()}; 
	this.resume = function() {this.wagerManager.resume()};
	
	this.clearSelections = function() {
		this.wagerManager.clearSelections();
	}
	
	/* e.g. for betplus - beginWagering({track : {code : '-GG', name : 'Golden Gate'}) */
	this.beginWagering = function(preSelection) {

		this.suspend();
		var wagerManagerPreselection = new Object;
		
		var cardsLoaded = cardManager.getCards();
		
		if(preSelection.track && preSelection.track.code) {
			wagerManagerPreselection.cardCode = preSelection.track.code;
		} else {
			
			if (cardsLoaded.length != 0) 
				{ 
					var common = new Common();
					common.createAlert("Wagering must begin with a track selection");
				}
				
			return false;
		}
		
		wagerManagerPreselection.raceNumber = preSelection.raceNumber ? preSelection.raceNumber : this.wagerManager.wager.race.raceNumber;
		wagerManagerPreselection.betCode = preSelection.betCode ? preSelection.betCode : this.wagerManager.wager.betType.code;
		
		/*
		if (preSelection.amount == null || this.wagerManager.wager.betType == null || this.wagerManager.wager.betType.minStake > preSelection.amount || preSelection.amount > this.wagerManager.wager.betType.maxStake)
		{
		 	wagerManagerPreselection.amount = preSelection.amount? preSelection.amount : this.wagerManager.wager.amount.amount;
		}
		else
		{
		 wagerManagerPreselection.amount = preSelection.amount? this.wagerManager.wager.amount.amount : preSelection.amount;
		}
		*/
		
		if(preSelection.amount) {
			wagerManagerPreselection.amount = preSelection.amount;
		}
		
		this.wagerManager.preselect(wagerManagerPreselection);
		this.pageController.update(wagerManagerPreselection.cardCode, wagerManagerPreselection.raceNumber);
		this.resume();
		this.wagerManager.wagerChange();
		this.wagerManager.setSelections(preSelection.selections);
		this.pageController.refresh();
	};
		  
	/* returns the current wager - shouldn't need this if using listeners etc */
	this.getCurrentWager = function() {};
	  
	/* begin wagering - new wager object - e.g. function placeWagerListener(wager) */
	this.registerBeginWageringListener = function(listener) {
		var thisObj = this;
		this.wagerManager.registerListener("pool", function(property, value) {listener(Wager.getWagerUIAdapter(thisObj.wagerManager.wager));});
	};
		  
	/*  place bet button pressed - current Wager object is passed to the listener fn */
	this.registerPlaceBetListener = function(listener) {
		this.wagerManager.registerListener("placeBet", function(property, value) {listener(Wager.getWagerUIAdapter(value));});
	};
		  
	/* any change to the current wager - current Wager object is passed to the listener fn */
	this.registerCurrentWagerChangeListener = function(listener) {
		this.wagerManager.registerListener("wagerUpdate", function(property, value) {listener(Wager.getWagerUIAdapter(value));});
	};
		  
	/* e.g. registerCurrentWagerPropertyListeners({prop1:[listener1,listener2], prop2:[listener2], prop3:[listener3]}) 
	 * listeners are passed the value of the changed Wager property they are listening to. */
	this.registerCurrentWagerPropertyListeners = function(listeners) {};    
		
	/* e.g. registerCurrentWagerPropertyListener("betTotal", betTotalListener) */
	this.registerCurrentWagerPropertyListener = function(property, listener) {};  
		
	/* fully populated selected leg object is passed to the listener */
	this.registerSelectedLegListener = function(listener) {};
		  
	/* selectionNumbers is an array of selection numbers. 
	 * The selectionCallback is passed an updated leg object which should be used to update the race card selection which could
	 * include coupled runners etc */
	this.updateLegSelection = function(legNumber, selectionNumbers, selectionCallback) {};      
}

function SingleSelectionHandler(nextSelectionHandler) {
	this.currentSelection = null;
	this.nextSelectionHandler = nextSelectionHandler;
	this.select = function(index, option, optionMap) {
		if(this.currentSelection && this.currentSelection != index) {
			optionMap[this.currentSelection].selected = false;
			option.selected = true;
			this.currentSelection = index;
		} else if(!this.currentSelection) {
			option.selected = true;
			this.currentSelection = index;
		}
			
		this.nextSelectionHandler(option.option);
	}
}

// responsible for verifying user selections and updating the current bet model
function TabularPoolController(wager, variants) {
	this.wager = wager;
	this.PropertyChangeLoudMouth();
	this.currentLegSelection;
	this.pool;
	this.currentLegIndex;
	this.legEntriesSelectionModelMap = new Object;
	this.legSelectionModel;
	this.poolVariantMap;
	this.variants = variants;
	
	var thisObj = this;

	// update entry selection models if wager selections change - i.e. by other party
	this.wager.registerListener("updateSelection", function(property, value) {
		for(var i in thisObj.wager.legs) {
			var legEntriesSelectionModel = thisObj.legEntriesSelectionModelMap[thisObj.wager.legs[i].legNumber];
			for(var j=0; j<thisObj.wager.legs[i].selections.length; j++) {
				var selection = thisObj.wager.legs[i].selections[j];
				legEntriesSelectionModel.select(selection.entry.id, true);
			}
		}
		
		thisObj.refresh();
	});
}

copyPrototype(TabularPoolController, PropertyChangeLoudMouth);

TabularPoolController.prototype.refresh = function() {
		this.notifyAll("refresh", this.pool);
	};
	
TabularPoolController.prototype.legSelectionHandler = function(property, value) {
		if(this.currentLegSelection) {
			this.notifyAll("legDeselected", this.currentLegSelection);
		}
		this.currentLegSelection = value;
		this.notifyAll("legSelected", value);
}
	
TabularPoolController.prototype.variantSelectionHandler = function(property, value) {
		this.notifyAll("setVariantAction", value);
}
	
TabularPoolController.prototype.variantDeselectionHandler = function(property, value) {
		this.notifyAll("unsetVariantAction", value);
}
	
TabularPoolController.prototype.setSelectedLegEntries = function(entries) {
		this.notifyAll("newLegEntries", entries);
};

TabularPoolController.prototype.registerWagerLegListener = function(legNumber, listener) {
	this.wager.legs[legNumber].registerListener("selection", listener);	
};

function fnWrapper(legNumber, poolController) {
	this.legNumber = legNumber;
	this.poolController = poolController;
	var thisObj = this;
	
	
	this.entrySelectionFn = function(property, value) {
		thisObj.poolController.notifyAll("runnerSelected", {"legId" : thisObj.legNumber, "runnerId" : value.obj.bettingInterestNumber});
	};
	this.entryDeselectionFn = function(property, value) {
		thisObj.poolController.notifyAll("runnerDeselected", {"legId" : thisObj.legNumber, "runnerId" : value.obj.bettingInterestNumber});
	};
	
	this.allEntriesSelectedFn = function(property, value) {
		thisObj.poolController.notifyAll("allRunnersSelected", {"legId" : thisObj.legNumber, "runners" : value});
	};
	this.allEntriesDeselectedFn = function(property, value) {
		thisObj.poolController.notifyAll("allRunnersDeselected", {"legId" : thisObj.legNumber, "runners" : value});
	};
	
}

// new pool set first leg
TabularPoolController.prototype.setPool = function(pool) {
		if(pool) {
			var thisObj = this;
	
			// chance to clear up previous pool
			if(this.pool) {
				this.notifyAll("poolDeselected", this.pool);
			}
			
			this.pool = pool;
			
			// see if this pool has any variants
			var poolVariants = pool.getVariants();
			if(poolVariants) {
				// create pool navigation map if not already got one
				if(!this.poolVariantMap) {
					this.poolVariantMap = new Object;
					// include this as a variant
					var variant = new Object;
					variant.pool = pool;
					this.poolVariantMap[pool.id] = variant;
					
					for(var i=0; i<poolVariants.length; i++) {
					//for(var i in poolVariants) {
						variant.nextVariant = poolVariants[i];
						
						variant = new Object;
						variant.pool = poolVariants[i];
						this.poolVariantMap[poolVariants[i].id] = variant;
					}
					
					// wrap around
					variant.nextVariant = pool;
				}
			} else if(this.poolVariantMap && !this.poolVariantMap[pool.id]) {
				// if not a variant remove map
				this.poolVariantMap = null;
			}
			
			
			// initialise leg selection model map keyed on legNumber
			this.legEntriesSelectionModelMap = new Object;	// @todo delete first
			this.legEntriesSelectionModelArray = new Array();
			
			for(var i = 0; i<pool.poolObj.legs.length; i++) {
				var legNumber = pool.poolObj.legs[i].legNumber;
				
				var entriesSelectionModel = new SelectionModel(pool.legEntries[legNumber], null, function(option) {
					return option.bettingInterestNumber;
				});
	
				
				var fn = new fnWrapper(legNumber, this);
				
				entriesSelectionModel.registerListener("selected", fn.entrySelectionFn);
				entriesSelectionModel.registerListener("deselected", fn.entryDeselectionFn);
				entriesSelectionModel.registerListener("allSelected", fn.allEntriesSelectedFn);
				entriesSelectionModel.registerListener("allDeselected", fn.allEntriesDeselectedFn);
				
				
				
				this.legEntriesSelectionModelMap[legNumber] = entriesSelectionModel;
				this.legEntriesSelectionModelArray.push(entriesSelectionModel);
			}
			
			if(pool.poolObj.variant == "box") {
				new DuplicateCompositeSelectionModel(this.legEntriesSelectionModelArray);
			} else if(pool.poolObj.variant == "keybox") {
				new XORCompositeSelectionModel(this.legEntriesSelectionModelArray);
			// Exacta Banker for some reason is a "wheel" not a "banker" variant
		    } else if(pool.poolObj.variant == "wheel" && pool.poolObj.poolCode == "EXA") {
				new BankerCompositeSelectionModel(this.legEntriesSelectionModelArray);
			} else if(pool.poolObj.variant == "banker" && pool.poolObj.poolCode == "TRO") {
				new TROBankerCompositeSelectionModel(this.legEntriesSelectionModelArray);
			} else if(pool.poolObj.variant == "banker") {
				new BankerCompositeSelectionModel(this.legEntriesSelectionModelArray);
			// Reverse Exacta Banker comes under this
			} else if(pool.poolObj.variant == "boxbanker") {
				//new ReversibleBankerCompositeSelectionModel(this.legEntriesSelectionModelArray);
				new BankerCompositeSelectionModel(this.legEntriesSelectionModelArray);
			}
			
					
			this.legSelectionModel = new SingleSelectionModel(pool.poolObj.legs);
			this.legSelectionModel.registerListener("selected", function(property, value) {thisObj.legSelectionHandler(property, value);});
	
			// set up the variant selection model if present
			if(pool.getVariantBetTypes().length > 0) {
				this.variantSelectionModel = new SingleSelectionModel(pool.getVariantBetTypes());
				// select current pool before listener is registered
				this.variantSelectionModel.selectOption(pool.poolObj);
				this.variantSelectionModel.registerListener("selected", function(property, value) {thisObj.variantSelectionHandler(property, value);});
			}

			var variantBetTypesExcludingParent = null;
			// set up the variant selection model that doesn't include the perfecta if present
			if(this.variants) {
				variantBetTypesExcludingParent = WP_Utils.arrayFilter(pool.getVariantBetTypesExcludingParent(), function(obj) {return thisObj.variants[obj.variant] != null;});
			} else {
				variantBetTypesExcludingParent = pool.getVariantBetTypesExcludingParent();
			}	
			if(variantBetTypesExcludingParent.length > 0) {
				// selection model allows no selection
				this.variantSelectionModel2 = new SingleSelectionModel(variantBetTypesExcludingParent, true);
				// select current pool before listener is registered
				this.variantSelectionModel2.selectOption(pool.poolObj);
				this.variantSelectionModel2.registerListener("selected", function(property, value) {thisObj.variantSelectionHandler(property, value);});
				this.variantSelectionModel2.registerListener("deselected", function(property, value) {thisObj.variantDeselectionHandler(property, value);});
			}
			
			// notify of new pool before first leg is selected
			this.notifyAll("refresh", pool);
	
			// select first leg - cause notification via model and pool controller
			this.legSelectionModel.selectOption(pool.getFirstLeg());
		} else {
			if(this.pool) {
				this.notifyAll("poolDeselected", this.pool);
			}
			this.notifyAll("refresh", pool);
		}	
	};

TabularPoolController.prototype.getNextVariant = function() {
	if(this.poolVariantMap && this.poolVariantMap[this.pool.id]) {
		return this.poolVariantMap[this.pool.id].nextVariant;
	}
}

TabularPoolController.prototype.selectNextVariant = function() {
	var nextVariantPool = this.getNextVariant();
	if(nextVariantPool) {
		this.notifyAll("setNewPoolAction", nextVariantPool);
	}
}

TabularPoolController.prototype.getSelectedLegEntriesSelectionModel = function() {
	if (this.legEntriesSelectionModelMap[1] != null) {
		return this.legEntriesSelectionModelMap[this.currentLegSelection.obj.legNumber];
	}
		
	return false;
}
	
	
TabularPoolController.prototype.clearAllSelections = function() {
		for(var i=0; i<this.pool.legEntries.length; i++) {

			var entries = this.pool.legEntries[i];
			for(var j=0; j<entries.length; i++) {
				entries[j].selected = false;
			}
		}
		this.notifyAll("pool", this.pool);
	};
	
TabularPoolController.prototype.clearLegSelections = function(leg) {
		var entries = this.pool.legEntries[leg.legNumber];
		for(var i in entries) {
			entries[i].selected = false;
		}
		this.notifyAll("pool", this.pool);
	};
	
TabularPoolController.prototype.selectRunner = function(leg, entry){
		
		// update combinations - alter wager object
		entry.selected = true;
		this.notifyAll("pool", this.pool);
	};
	
TabularPoolController.prototype.toggleRunner = function(leg, entry){
		var common = new Common();
		common.createAlert("toggle runner");
		// update combinations - alter wager object
		if(entry.selected) {
			this.deselectRunner(leg, entry);
		} else {
			this.selectRunner(leg, entry);
		}
	};
	
TabularPoolController.prototype.deselectRunner = function(leg, entry){
		var common = new Common();
		common.createAlert("deselect runner");
		
		// update combinations - alter wager object
		entry.selected = false;
		this.notifyAll("pool", this.pool);
	};
	
TabularPoolController.prototype.selectAllRunners = function(leg){
		var entries = this.pool.legEntries[leg.legNumber];
		
		for(var i=0; i<entries.length; i++) {
			if(!entries[i].scratched) {
				entries[i].selected = true;
			}
		}
		this.notifyAll("pool", this.pool);
	};
	

/* utils */
function PropertyChangeListener(propertyChangedFn) {
	this.propertyChangedFn = propertyChangedFn;
	
	this.propertyChanged = function(property, value) {propertyChangedFn(property, value);};
}

function PropertyChangeMgr() {
	var listeners = new Array();

	this.notifyAll = function(property, value) {
		if(listeners[property])
		{
			for(i=0; i<listeners[property].length; i++)
			{
				listeners[property][i].propertyChanged(property, value);
			}
		}
	};
	
	this.registerListener = function(property, listener) {
		if(!listeners[property])
		{
			listeners[property] = new Array();
		}
		
		listeners[property].push(listener);
	};
}
function SelectionModel(options, validationMethod, linkingMethod) {
	this.PropertyChangeLoudMouth();
	this.selectionMap = new Object();
	this.selectionArray;
	this.duplicates = new Object();
	this.optionCount = 0;
	this.selectedCount = 0;
	this.allSelected = false;
	this.validationMethod = validationMethod;
	this.linkingMethod = linkingMethod;
	this.lastSelection;
	
	this.setOptionsInner = function(options) {
		this.optionCount = options.length;
		this.selectedCount = 0;
		this.selectionMap = new Object();
		this.selectionArray = new Array(options.length);
		this.allSelected = false;
		this.notifyAll("allSelected", this.allSelected);
		
		this.linkMap = new Object();
		
		var indexFn;
		if(options && options[0] && options[0].id) {
			indexFn = function(i) {return options[i].id;};
		} else {
			indexFn = function(i) {return i;}
		}
		
		for(var i=0; i<options.length; i++) {
			
			// always use generated unique key
			var index = indexFn(i);
//			options[i].id = index;
			var selection = {obj : options[i], selected : false, id : index, order : i, disabled : false};
			this.selectionMap[index] = selection;
			this.selectionArray[i] = selection;

			if(this.linkingMethod) {
				var linkId = this.linkingMethod(options[i]);
				if(this.linkMap[linkId]) {
					var linkedSelection = this.linkMap[linkId];
					if(linkedSelection.length) {
						// array add duplicate
						linkedSelection.push(selection);
					} else {
						var linkedSelectionsArray = new Array();
						linkedSelectionsArray.push(linkedSelection);
						linkedSelectionsArray.push(selection);
						this.linkMap[linkId] = linkedSelectionsArray;
					}
				} else {
					this.linkMap[linkId] = selection;
				}
			}
			
		}
	}

	/**
	 * initialise selection state objects
	 */
	if(options) {
		this.setOptionsInner(options);
	}
}

copyPrototype(SelectionModel, PropertyChangeLoudMouth);

SelectionModel.prototype.setOptions = function(options) {
	this.setOptionsInner(options);
	this.notifyAll("options", this.selectionMap);
}

SelectionModel.prototype.toggle = function(id) {
	var selection = this.selectionMap[id];
	
	if(selection) {
		// toggle selection state
		if(selection.selected) {
			this.deselect(id);
		} else {
			this.select(id);
		}
	}
}

SelectionModel.prototype.validate = function(id) {
	var validSelection = true;
	if(this.validationMethod) {
		var selection = this.selectionMap[id];
		if(selection) {
			validSelection = this.validationMethod(selection);
		}
	}
	
	return validSelection;
}

SelectionModel.prototype.getLinkedSelections = function(selection) {
	if(this.linkingMethod) {
		var linkId = this.linkingMethod(selection.obj);
		if(this.linkMap[linkId].length) {
			return this.linkMap[linkId];
		}	
	}
}

SelectionModel.prototype.select = function(id, validated) {
	var selection = this.selectionMap[id];
	
	this.selectSelection(selection, validated);
	this.lastSelection = selection;
	
	var linkedSelections = this.getLinkedSelections(selection);
	
	if(linkedSelections) {
		for(var i in linkedSelections) {
			this.selectSelection(linkedSelections[i]);
		}
	}
}

SelectionModel.prototype.selectSelection = function(selection, validated, doNotNotify) {
	var validSelection = validated || this.validate(selection.obj.id);
	
	if(selection && !selection.selected) {
		if(validSelection) {
			this.selectedCount += 1;
			selection.selected = true;
	
			if(this.selectedCount == this.optionCount) {
				this.allSelected = true;
				this.notifyAll("allSelected", this.allSelected);
			}
			
			this.notifyAll("selected", selection);
			if(!doNotNotify) {
				this.notifyAll("selected_internal", selection);
			}
		}
	}
	
	return validSelection;
}

SelectionModel.prototype.deselect = function(id, validated) {
	var selection = this.selectionMap[id];
	
	this.deselectSelection(selection, validated);
	
	var linkedSelections = this.getLinkedSelections(selection);
	
	if(linkedSelections) {
		for(var i in linkedSelections) {
			this.deselectSelection(linkedSelections[i]);
		}
	}
}

SelectionModel.prototype.deselectSelection = function(selection, validated, doNotNotify) {	
	
	if(selection && selection.selected) {
		var validSelection = validated || this.validate(selection.obj.id);
		if(validSelection) {
			selection.selected = false;
	
			this.selectedCount -= 1;
			if(this.allSelected) {
				this.allSelected = false;
				this.notifyAll("allSelected", this.allSelected);
			}
	
			this.notifyAll("deselected", selection);
			if(!doNotNotify) {
				this.notifyAll("deselected_internal", selection);
			}
		}
	}
}

SelectionModel.prototype.disable = function(id) {
	var selection = this.selectionMap[id];
	
	if(selection && selection.disabled == false) {
		selection.disabled = true;
		this.notifyAll("stateChanged", selection);
	}
}

SelectionModel.prototype.enable = function(id) {
	var selection = this.selectionMap[id];
	
	if(selection && selection.disabled == true) {
		selection.disabled = false;
		this.notifyAll("stateChanged", selection);
	}
}

/** if all selected then clear all, otherwise select all **/
SelectionModel.prototype.selectOrClearAll = function() {
	if(this.selectedCount == this.optionCount) {
		// clear all
		for(var i in this.selectionMap) {
			if(this.selectionMap[i].selected) {
				this.deselect(i);
			}
		}
		return "cleared";
	} else {
		// select all
		for(var i in this.selectionMap) {
			if(!this.selectionMap[i].selected) {
				this.select(i);
			}
		}
		return "all";
	}
}

/** StefanC: light version to allow faster selection
    Listerners should register themself for allSelected and allDeselected events
 **/
SelectionModel.prototype.selectOrClearAllLight = function() {
	if(this.selectedCount == this.optionCount) {
		
		// clear all
		for(var i in this.selectionMap) {
			if(this.selectionMap[i].selected) {
				this.selectionMap[i].selected = false;
			}
		}
		this.selectedCount = 0;
		this.allSelected = false;
		this.notifyAll("allDeselected", this.selectionMap);
		return "cleared";
		
	} else {
		
		// select all
		for(var i in this.selectionMap) {
			if(!this.selectionMap[i].selected) {
				this.selectionMap[i].selected = true;
			}
		}
		this.selectedCount = this.optionCount;
		this.allSelected = true;
		this.notifyAll("allSelected", this.selectionMap);
		return "all";
		
	}
}

// adapted to clear selected runners
SelectionModel.prototype.clearAll = function() {
		for(var i in this.selectionMap) {
			if(this.selectionMap[i].selected) {
				this.deselect(i);
			}
		} 
}


SelectionModel.prototype.selectOption = function(option) {
	return this.select(this.getIdForOption(option));
}

SelectionModel.prototype.getIdForOption = function(option) {
	for(var i in this.selectionMap) {
		if(this.selectionMap[i].obj == option) {
			return i;
		}
	}
}

/**
 * if no selection allowed is true then the model will default
 * to the first option selected and will not allow the current
 * selection to be deselected
 */
function SingleSelectionModel(options, noSelectionAllowed, defaultSelectionId, validationMethod) {
	this.SelectionModel(options, validationMethod);
	this.currentSelectionId;
	this.noSelectionAllowed = noSelectionAllowed;
	
	// default to first selected
	if(options) {
		if(defaultSelectionId) {
			this.select(defaultSelectionId);
		} else {
//			this.select(options[0].id);
		}
	}
}

copyPrototype(SingleSelectionModel, SelectionModel);

SingleSelectionModel.prototype.setOptions = function(options) {
	this.currentSelectionId = null;
	SelectionModel.prototype.setOptions.apply(this, [options]);
	if(!this.noSelectionAllowed) {
//		this.select(options[0].id);
	}	
}

SingleSelectionModel.prototype.select = function(id, validated) {
	var success = validated || this.validate(id);
	if(id != this.currentSelectionId) {
		if(success) {
			if(this.currentSelectionId >= 0 || this.currentSelectionId) {
				SelectionModel.prototype.deselect.apply(this, [this.currentSelectionId]);
			}

			SelectionModel.prototype.select.apply(this, [id, true]);
			this.currentSelectionId = id;
		}
	}
	
	return success;
}

SingleSelectionModel.prototype.selectNext = function() {
	var nextIndex = 0;
	if(this.lastSelection) {
		nextIndex = this.lastSelection.order;
		nextIndex++;
		if(nextIndex >= this.selectionArray.length) {
			nextIndex = 0;
		}
	}
	
	var nextSelection = this.selectionArray[nextIndex];
	if(nextSelection) {
		this.select(nextSelection.id);
	}
}

SingleSelectionModel.prototype.selectPrevious = function() {
	var previousIndex = 0;
	if(this.lastSelection) {
		previousIndex = this.lastSelection.order;
		previousIndex--;
		if(previousIndex < 0) {
			previousIndex = this.selectionArray.length - 1;
		}
	}
	
	var previousSelection = this.selectionArray[previousIndex];
	if(previousSelection) {
		this.select(previousSelection.id);
	}
}

	
// ignore toggle and deselect requests
SingleSelectionModel.prototype.toggle = function(id, selection) {
	var currentSelectionId = this.currentSelectionId;
	
	if(id >= 0 || id) {
		if(id != currentSelectionId) {
			var success = this.validate(id);
			if(success) {
				this.currentSelectionId = id;
				if(currentSelectionId >= 0 || currentSelectionId) {
					SelectionModel.prototype.deselect.apply(this, [currentSelectionId, true]);
				}
				
				SelectionModel.prototype.select.apply(this, [id, true]);
			}
		}
		else if(this.noSelectionAllowed) {
			SelectionModel.prototype.deselect.apply(this, [currentSelectionId]);
			this.currentSelectionId = null;
		}
		else
		{
			// add the posibility to react for an already selected item
			this.notifyAll("itemAlreadySelected");
		}
	}
}

SingleSelectionModel.prototype.deselect = function(id) {}

function sm_fnWrapper(index, selectionModel) {
	this.index = index;
	this.selectionModel = selectionModel;
	var thisObj = this;
	
	
	this.selectionFn = function(property, value) {
		thisObj.selectionModel.handleSelection(property, value, thisObj.index);
	};
	this.deselectionFn = function(property, value) {
		thisObj.selectionModel.handleDeselection(property, value, thisObj.index);
	};
}

// composite models have to have the same options in each
function XORCompositeSelectionModel(selectionModels) {
	this.selectionModels = selectionModels;
	
	this.handleSelection = function(property, value, selectionModelIndex) {
		for(var i=0; i<this.selectionModels.length; i++) {
			if(i == selectionModelIndex) {
			} else {
				this.selectionModels[i].disable(value.id);
			}
		}
	}
	
	this.handleDeselection = function(property, value, selectionModelIndex) {
		for(var i=0; i<this.selectionModels.length; i++) {
			if(i == selectionModelIndex) {
			} else {
				this.selectionModels[i].enable(value.id);
			}
		}
	}
	
	var thisObj = this;
	
	this.handleSelectionFn = function(index) {
		this.index = index;
	}
	
	for(var i=0; i<this.selectionModels.length; i++) {
		var index = i;
		var fn = new sm_fnWrapper(index, this);
			
		this.selectionModels[i].registerListener("selected_internal", fn.selectionFn);
		this.selectionModels[i].registerListener("deselected_internal", fn.deselectionFn);
	}
}

function DuplicateCompositeSelectionModel(selectionModels) {
	this.selectionModels = selectionModels;
	
	this.handleSelection = function(property, value, selectionModelIndex) {
		for(var i=0; i<this.selectionModels.length; i++) {
			if(i == selectionModelIndex) {
			} else {
				this.selectionModels[i].selectSelection(selectionModels[i].selectionMap[value.id], true, true);
			}
		}
	}
	
	this.handleDeselection = function(property, value, selectionModelIndex) {
		for(var i=0; i<this.selectionModels.length; i++) {
			if(i == selectionModelIndex) {
			} else {
				this.selectionModels[i].deselectSelection(selectionModels[i].selectionMap[value.id], true, true);
			}
		}
	}
	
	var thisObj = this;
	
	this.handleSelectionFn = function(index) {
		this.index = index;
	}
	
	for(var i=0; i<this.selectionModels.length; i++) {
		var index = i;
		var fn = new sm_fnWrapper(index, this);
			
		this.selectionModels[i].registerListener("selected_internal", fn.selectionFn);
		this.selectionModels[i].registerListener("deselected_internal", fn.deselectionFn);
	}
}

// composite models have to have the same options in each - this one selects all
// selections in other models other than the selected option.
function NOTCompositeSelectionModel(selectionModels) {
	this.selectionModels = selectionModels;
	
	this.handleSelection = function(property, value, selectionModelIndex) {
		for(var i=0; i<this.selectionModels.length; i++) {
			var otherSelectionModel = this.selectionModels[i];
			
			for(var j in otherSelectionModel.selectionMap) {
				var selection = otherSelectionModel.selectionMap[j];
				if((i != selectionModelIndex) || (i == selectionModelIndex && selection.id == value.id)) {
					// select without validating or notifying
					otherSelectionModel.selectSelection(selection, true, true);	
				} else {
					otherSelectionModel.deselectSelection(selection, true, true);	
				}
			}
		}
	}

	this.handleDeselection = function(property, value, selectionModelIndex) {
		var deselect = false;
		var otherSelectionModel = this.selectionModels[selectionModelIndex];
		if( otherSelectionModel ) {
			if( otherSelectionModel.selectedCount == 0 ) {	
				var selection = otherSelectionModel.selectionMap[value.id];
				if( selection ) deselect = true; 
			}
		}				
		if( deselect ) {
			for(var i=0; i<this.selectionModels.length; i++) {
				var otherSelectionModel = this.selectionModels[i];
				for(var j in otherSelectionModel.selectionMap) {
					var selection = otherSelectionModel.selectionMap[j];
					if( selection.selected )
						otherSelectionModel.deselectSelection( selection, true, true );
				}	
			}
		} else {
			this.handleSelection( property, value, selectionModelIndex );
		}	
	}
		
	var thisObj = this;

	
	for(var i=0; i<this.selectionModels.length; i++) {
		var index = i;
		var fn = new sm_fnWrapper(index, this);
			
		this.selectionModels[i].registerListener("selected_internal", fn.selectionFn);
		this.selectionModels[i].registerListener("deselected_internal", fn.deselectionFn);
	}
}

// composite models have to have the same options in each - this one is specific
// to reversible 2 leg bankers
function ReversibleBankerCompositeSelectionModel(selectionModels) {
	this.selectionModels = selectionModels;
	
	this.handleSelection = function(property, value, selectionModelIndex) {
		var currentLeg = this.selectionModels[selectionModelIndex];
		var otherLeg = this.selectionModels[selectionModelIndex == 0 ? 1 : 0];
		
		if( otherLeg.selectedCount > 1 && currentLeg.selectedCount > 1 ) {
			for(var j in currentLeg.selectionMap) {
				var selection = currentLeg.selectionMap[j];
				if( selection.selected == true && selection.id != value.id )
					currentLeg.deselectSelection( selection, true, true);	
			}
		} 	
		
		var otherSelectionModel = this.selectionModels[selectionModelIndex];
		if( otherSelectionModel ) {
			var selection = otherSelectionModel.selectionMap[value.id];
			if( selection ) otherSelectionModel.selectSelection(selection, true, true);
		}
	}

	this.handleDeselection = function(property, value, selectionModelIndex) {
		var otherSelectionModel = this.selectionModels[selectionModelIndex];
		if( otherSelectionModel ) {
			var selection = otherSelectionModel.selectionMap[value.id];
			if( selection ) otherSelectionModel.deselectSelection(selection, true, true);
		}	
	}
		
	var thisObj = this;

	for(var i=0; i<this.selectionModels.length; i++) {
		var index = i;
		var fn = new sm_fnWrapper(index, this);
			
		this.selectionModels[i].registerListener("selected_internal", fn.selectionFn, "internal");
		this.selectionModels[i].registerListener("deselected_internal", fn.deselectionFn, "internal");
	}
}

// composite models have to have the same options in each - this one selects all
// selections in other models other than the selected option.
function BankerCompositeSelectionModel(selectionModels) {
	this.selectionModels = selectionModels;
	
	this.handleSelection = function(property, value, selectionModelIndex) {
		if( selectionModelIndex == 0 ) {
			var otherSelectionModel = this.selectionModels[selectionModelIndex];
			if( otherSelectionModel && otherSelectionModel.selectedCount > 1 ) {
				for(var j in otherSelectionModel.selectionMap) {
					var selection = otherSelectionModel.selectionMap[j];
					if( selection.selected == true && selection.id != value.id ) {
						otherSelectionModel.deselectSelection( selection, true, true);	
					}
				}
			}		
		}
	
		var otherSelectionModel = this.selectionModels[selectionModelIndex];
		if( otherSelectionModel ) {
			var selection = otherSelectionModel.selectionMap[value.id];
			if( selection ) otherSelectionModel.selectSelection(selection, true, true);
		}
	}

	this.handleDeselection = function(property, value, selectionModelIndex) {
		var otherSelectionModel = this.selectionModels[selectionModelIndex];
		if( otherSelectionModel ) {
			var selection = otherSelectionModel.selectionMap[value.id];
			if( selection ) otherSelectionModel.deselectSelection(selection, true, true);
		}	
	}
		
	var thisObj = this;

	for(var i=0; i<this.selectionModels.length; i++) {
		var index = i;
		var fn = new sm_fnWrapper(index, this);
			
		this.selectionModels[i].registerListener("selected_internal", fn.selectionFn, "internal");
		this.selectionModels[i].registerListener("deselected_internal", fn.deselectionFn, "internal");
	}
}

// composite models have to have the same options in each - this one selects all
// selections in other models other than the selected option.
function TROBankerCompositeSelectionModel(selectionModels) {
	this.selectionModels = selectionModels;
	
	this.handleSelection = function(property, value, selectionModelIndex) {
		if( selectionModelIndex == 0 ) {
			var otherSelectionModel = this.selectionModels[selectionModelIndex];
			if( otherSelectionModel && otherSelectionModel.selectedCount > 2 ) {
				for(var j in otherSelectionModel.selectionMap) {
					var selection = otherSelectionModel.selectionMap[j];
					if( selection.selected == true && selection.id != value.id && selection.id != otherSelectionModel.lastSelection.id ) {
						otherSelectionModel.deselectSelection( selection, true, true);	
					}
				}
			}		
		}
	
		var otherSelectionModel = this.selectionModels[selectionModelIndex];
		if( otherSelectionModel ) {
			var selection = otherSelectionModel.selectionMap[value.id];
			if( selection ) otherSelectionModel.selectSelection(selection, true, true);
		}
	}

	this.handleDeselection = function(property, value, selectionModelIndex) {
		var otherSelectionModel = this.selectionModels[selectionModelIndex];
		if( otherSelectionModel ) {
			var selection = otherSelectionModel.selectionMap[value.id];
			if( selection ) otherSelectionModel.deselectSelection(selection, true, true);
		}	
	}
		
	var thisObj = this;

	for(var i=0; i<this.selectionModels.length; i++) {
		var index = i;
		var fn = new sm_fnWrapper(index, this);
			
		this.selectionModels[i].registerListener("selected_internal", fn.selectionFn, "internal");
		this.selectionModels[i].registerListener("deselected_internal", fn.deselectionFn, "internal");
	}
}
var groupControllerId = 0;

/* takes an array of discretely selectable elements
 * the selection handler is called when a selection/deselection is made
 * the selection model determines the displayed selections
 * Typical use might be the selectionHandler calls to the BM that a specific
 * selection has been made. The BM updates the selection model to show that
 * several selections should be shown as a result (e.g. coupled runners) */
function GroupController(group, selectionHandler, renderer, selectionModel, parentElement, groupSelectionMap, id) {
	this.selectionHandler = selectionHandler;	// may be null
	this.group;									// may be null
	this.staticElements;		
	this.renderer = renderer;
	this.selectionModel = selectionModel;
	this.selectionMap = new Object;				// mapping between group element id's and a selectable option
	this.groupBySelectionIdMap = new Object;	// mapping from selection id to group element
	this.id = id;
	this.matchingFn;
	this.mapping;
	this.uid = groupControllerId++;

	// apply handlers to the group elements
	this.applyHandlers = function() {
		if(selectionHandler) {
			var thisController = this;
			for(i = 0; i < this.group.length; i++) {
			//for(i in this.group) {
				this.group[i].onclick = function() {thisController.onclickHandler(this.id);};
			}
		}
	}
	
	if(group) {
		// static group elements are provided 
		this.group = group;
		this.staticElements = true;
	} else if(this.selectionModel) {
		// use the renderer to dynamically create group elements for all selections
		this.group = this.renderer.createGroup(this.selectionModel.optionCount);
		this.staticElements = false;
	}
	
		
	
	
	

	if(this.selectionModel) {
		var thisObj = this;
		selectionModel.registerListener("deselected", function(property, value) {thisObj.refresh(property, value)}, this.uid);
		selectionModel.registerListener("selected", function(property, value) {thisObj.refresh(property, value)}, this.uid);
		selectionModel.registerListener("allDeselected", function(property, value) {thisObj.refresh(property, value)}, this.uid);
		selectionModel.registerListener("allSelected", function(property, value) {thisObj.refresh(property, value)}, this.uid);
		selectionModel.registerListener("options", function(property, value) {thisObj.updateOptions(value);}, this.uid);
		selectionModel.registerListener("stateChanged", function(property, value) {thisObj.refresh(property, value);}, this.uid);
		
		// optional groupSelectionMap provides those mappings - element id maps to selectionModel.selectionMap entry
		if(groupSelectionMap && groupSelectionMap.groupMap) {
				// selections are premapped to group elements
				this.selectionMap = groupSelectionMap.selectionMap;
				this.groupBySelectionIdMap = groupSelectionMap.groupMap;
		} else if(groupSelectionMap) {
				this.mapping = groupSelectionMap;
				this.groupBySelectionIdMap = groupSelectionMap;
		} else { 
			this.updateSelectionMap();
		}
		
		

	}

	this.applyHandlers();
};

GroupController.prototype.destroy = function() {
	this.selectionModel.deregisterListener("deselected", this.uid);
	this.selectionModel.deregisterListener("selected", this.uid);
	this.selectionModel.deregisterListener("options", this.uid);
	this.selectionModel.deregisterListener("stateChanged", this.uid);	
}

// maps selections in the selection model to group elements
GroupController.prototype.updateSelectionMap = function() {
	if(this.mapping) {
		this.selectionMap = new Object;

		for(var i in this.groupBySelectionIdMap) {
			var groupElementId = this.groupBySelectionIdMap[i];
			var selection = this.selectionModel.selectionMap[i];
			this.selectionMap[groupElementId] = selection;
		}
	} else {
		// create the mapping of groupElementIds to a selection element
		this.selectionMap = new Object;
		var index = 0;

		for(var i in this.selectionModel.selectionMap) {
			// only set up the selection model if group element exists
			if(this.group[index]) {
				var groupElementId = this.group[index].id;
				var selection = this.selectionModel.selectionMap[i];
				this.selectionMap[groupElementId] = selection;
				this.groupBySelectionIdMap[selection.id] = groupElementId;
				index += 1;
			} else {
				break;
			}
		}
	}
}	
	
GroupController.prototype.updateOptions = function(options) {
	if(!this.staticElements) {
		// use the renderer to dynamically create group elements for all selections
		this.group = this.renderer.createGroup(this.selectionModel.optionCount);
		this.applyHandlers();
	}
	this.updateSelectionMap();
 	this.refresh(options);
}
	
/**
 * map the selection options to the UI group elements
 */
GroupController.prototype.setSelectionModel = function(selectionModel) {
	this.setSelectionModel(selectionModel);
};
	
/**
 * map back to the selection option object and call the handler with it
 */	
GroupController.prototype.onclickHandler = function(index1) {
	var selection = this.selectionMap[index1];
	
	//addapted by Cristian
	if ((wagerPad != null) && (selection != null)  && (selection.obj != null) && (selection.obj.currency == null))
	{
		if (wagerPad.pageController.otherButtonController.otherButtonEl.firstChild.tagName != null)
		    	wagerPad.pageController.otherButtonController.otherAmountEnteredHandler();
	}

	if(this.selectionHandler){
		this.selectionHandler.toggle(selection.id, selection);
	}			
};

GroupController.prototype.refresh = function(value) {

	var groupElementSelections = new Object;
	for(var selectionId in this.groupBySelectionIdMap) {
		var groupElementId = this.groupBySelectionIdMap[selectionId];
		var selection = this.selectionModel.selectionMap[selectionId];
		
		// add selected 
		if(selection && selection.selected) {
			groupElementSelections[groupElementId] = selection;
		}
	}

	for(i = 0; i < this.group.length; i++) {
	//for(i in this.group) {
		
		var mappedOption;
		
		if(groupElementSelections[this.group[i].id]) {
			mappedOption = groupElementSelections[this.group[i].id];
		} else {
			mappedOption = this.selectionMap[this.group[i].id];
		}
		
		if(mappedOption){
			this.renderer.render(this.group[i], mappedOption.obj, mappedOption.disabled, mappedOption.selected);
			// map option id to button
		} else {
			this.renderer.render(this.group[i], null, true, false);
		}
	}
	
	this.applyHandlers();
};

// creates a controller with shared group but with a new selection model
// a matching function matches the new selection model to this selection model
GroupController.prototype.createSharedController = function(id, selectionHandler, renderer, newSelectionModel, matchingFn) {
	// create a selectionMap
	var newSelectionMap = new Object;
	
	// created for special case where all selections map to the same group element
	var newGroupBySelectionIdMap = new Object;
	
	for(var i in this.selectionModel.selectionMap) {
		for(var j in newSelectionModel.selectionMap) {
			if(matchingFn(this.selectionModel.selectionMap[i].obj, newSelectionModel.selectionMap[j].obj)) {
				// match
				var groupElementId = this.groupBySelectionIdMap[i];
				newSelectionMap[groupElementId] = newSelectionModel.selectionMap[j];
				newGroupBySelectionIdMap[newSelectionModel.selectionMap[j].id] = groupElementId;
//				break;
			}
		}
	}
	
	var newController = new GroupController(this.group, selectionHandler, renderer, newSelectionModel, null, {"selectionMap" : newSelectionMap, "groupMap" : newGroupBySelectionIdMap}, id);
	newController.matchingFn = matchingFn;
	return newController;
}

// initialise with a single element
function CyclicGroupController(element, selectionHandler, renderer, singleSelectionModel, reverse, preventWrapAround) {
	this.reverse = reverse;
	this.element = element;
	this.preventWrapAround = preventWrapAround;
	
	if(element && singleSelectionModel) {
		// map all selections to the same button
		var groupBySelectionIdMap = new Object;
		for(var i in singleSelectionModel.selectionMap) {
			groupBySelectionIdMap[singleSelectionModel.selectionMap[i].id] = element.id;
		}
		this.GroupController([element], selectionHandler, renderer, singleSelectionModel, null, groupBySelectionIdMap);
	} else {
		this.GroupController(null, selectionHandler, renderer, singleSelectionModel);
	}
}

copyPrototype(CyclicGroupController, GroupController);

/**
 * map back to the selection option object and call the handler with it
 */	
CyclicGroupController.prototype.onclickHandler = function(index1) {
	if(this.selectionHandler){
	
		if (wagerPad != null)
			{
			 if (wagerPad.pageController.otherButtonController.otherButtonEl.firstChild.tagName != null)
		    	wagerPad.pageController.otherButtonController.otherAmountEnteredHandler();
			}
	
	
		if(this.reverse) {
			this.selectionHandler.selectPrevious();
		} else {
			this.selectionHandler.selectNext();
		}
	}			
};

CyclicGroupController.prototype.refresh = function(value) {
	var selection = this.selectionModel.lastSelection;
	if(selection) {
		var disabled = selection.disabled;
		if(this.preventWrapAround) {
			if(this.reverse && !this.selectionModel.hasPrevious()) disabled = true;
			else if(!this.reverse && !this.selectionModel.hasNext()) disabled = true;
		}
		this.renderer.render(this.element, selection.lastSelection, selection.disabled, selection.selected);
	}
	
	this.applyHandlers();
};

/* takes an array of button elements */
function ButtonGroupController(id, buttonGroup, selectionHandler, selectionModel) {
	this.id = id;
	this.GroupController(buttonGroup, selectionHandler, new ButtonRenderer(), selectionModel);
}

// inherit from GroupController
copyPrototype(ButtonGroupController, GroupController);

/* takes an array of button elements */
function CheckBoxGroupController(id, checkBoxGroup, selectionHandler, selectionModel) {
	this.id = id;
	this.GroupController(checkBoxGroup, selectionHandler, new CheckBoxRenderer(), selectionModel);
}

// inherit from GroupController
copyPrototype(CheckBoxGroupController, GroupController);

function FixedButtonRenderer() {}

FixedButtonRenderer.prototype.render = function(element, obj, disabled, selected) {
	element.disabled = disabled;
	
	if(selected) {
		element.className = "selected";
	} else if(disabled) {
		element.className = "disabled";
	} else {
		element.className = "";
	}
}


function ButtonRenderer(parentElement) {
	this.parentElement = parentElement;
}

ButtonRenderer.prototype.createGroup = function(size) {
	var group = new Array();
	
	for(var i = 0; i < size; i++)
	{
		var button = document.createElement("button");
		button.setAttribute("type","button");
		button.id = this.parentElement.id + "-row" + i + "-button";
		button.innerHTML = "&nbsp;";
		
		this.parentElement.appendChild(button);
		
		group.push(button);
	}
	
	return group;
}

ButtonRenderer.prototype.render = function(element, obj, disabled, selected) {
	if(obj) {
		element.innerHTML = obj.textValue;
	} else {
		element.innerHTML = "&nbsp;"
	}
	
	element.disabled = disabled;
	
	
	if(selected) {
		element.className = "selected";
	} else if(disabled) {
		element.className = "disabled";
	} else {
		element.className = "";
	}
}

function CheckBoxRenderer(parent) {
	this.parentElement = parent;
	this.createGroup = function(size) {
		var group = new Array();
		
		for(var i = 0; i < size; i++)
		{
			var button = document.createElement("input");
			button.type = "checkbox";
			button.id = this.parentElement.id + "-row" + i + "-button";
			button.name = this.parentElement.id + "-row" + i + "-button";
			
			this.parentElement.appendChild(button);
			
			group.push(button);
		}
		
		return group;
	}
	
	this.render = function(element, obj, disabled, selected) {
		element.disabled = disabled;
		element.checked = selected;
	}
}

function SelectController(selectElement, selectionHandler) {
	this.selectElement = selectElement;
	this.selectionHandler = selectionHandler;
	
	this.optionMap;
	
	this.onchangeHandler = function() {
		if(this.selectionHandler) {
			var option = this.optionMap[this.selectElement.value];
			this.selectionHandler(option);
		}
	}

	var thisController = this;
	var fn = function() {thisController.onchangeHandler(this.id);}
	this.selectElement.onchange = fn;
	
	this.options;
	
	this.populateOptions = function(options) {
		this.options = options;
	};
	
	this.refresh = function() {
		this.optionMap = new Object;
		
		for(i in this.options){
			var optionElement = document.createElement('option');
			optionElement.text=this.options[i].textValue;
			optionElement.value=i;
			
			this.optionMap[i] = this.options[i];
	
			try
			{
				selectElement.add(optionElement,null); // standards compliant
			}
			catch(ex)
			{
				selectElement.add(optionElement); // IE only
			}
		}
	};
}
function AbstractElementRenderer() {}

AbstractElementRenderer.prototype.create = function(parent, element, id, disabled, selected) {
	parent.id = id;
	this.render(parent, element, disabled, selected);
	
	return parent;
}

AbstractElementRenderer.prototype.render = function(parent, element, disabled, selected) {
	if(element.textValue) {
		parent.innerHTML = element.textValue;
	} else {
		parent.innerHTML = element;
	}
	
	if(selected) {
		parent.className = "selected";
	}
	
	parent.disabled = disabled;
};

AbstractElementRenderer.prototype.renderBlank = function(parent, element) {
	parent.innerHTML = "&nbsp;";
};

function ButtonElementRenderer() {
	this.AbstractElementRenderer();	
}

copyPrototype(ButtonElementRenderer, AbstractElementRenderer);

ButtonElementRenderer.prototype.create = function(parent, element, id) {
	var button = document.createElement("button");
	button.setAttribute("type", "button");
	
	if(parent) {
		parent.appendChild(button);
	}
	
	AbstractElementRenderer.prototype.create.apply(this, [button, element, id]);
	
	return button;
}

function AbstractGridRenderer(parentElement, defaultCellRenderer) {
	this.parentElement = parentElement;
	if(!defaultCellRenderer) {
		this.defaultCellRenderer = new DefaultElementRenderer();
	} else {
		this.defaultCellRenderer = defaultCellRenderer;
	}
}

AbstractGridRenderer.prototype.newRow = function(element) {};
AbstractGridRenderer.prototype.addCell = function(element, colspan) {};

function TableGridRenderer(parentElement, mergePaddingCells, defaultCellRenderer, offset) {
	this.AbstractGridRenderer(parentElement, defaultCellRenderer);
	this.offset = 0;
	if(offset) {this.offset = offset};
	this.currentRow;
	this.currentCell;
	this.mergePaddingCells = mergePaddingCells;
	this.gridRowCount = 0;
	this.rows = new Array();
}

copyPrototype(TableGridRenderer, AbstractGridRenderer);

TableGridRenderer.prototype.newRow = function() {
	this.currentRow = this.parentElement.insertRow(this.offset + this.gridRowCount);
	this.gridRowCount += 1;
	this.rows.push(this.currentRow);
	return this.currentRow;
}

TableGridRenderer.prototype.addCell = function(element, colspan) {
	this.currentCell = this.currentRow.insertCell(this.currentRow.cells.length);
	if(colspan) {
		this.currentCell.colspan = colspan;
	}
	
	var cellId = this.parentElement.id + "-r" + this.parentElement.rows.length + "-c" + this.currentRow.cells.length;
	
	var cell = this.defaultCellRenderer.create(this.currentCell, element, cellId);
	return cell;
}

TableGridRenderer.prototype.addPaddingCells = function(number) {
	if(number > 0 && this.mergePaddingCells) {
		this.currentCell = this.currentRow.insertCell(this.currentRow.cells.length);
		this.currentCell.colSpan = number;
		this.defaultCellRenderer.renderBlank(this.currentCell, "&nbsp;");
	} else {
		for(var i = 0; i < number; i++) {
			this.currentCell = this.currentRow.insertCell(this.currentRow.cells.length);
			this.defaultCellRenderer.renderBlank(this.currentCell, "&nbsp;");
		}
	}
}

function GridFlowLayout(numberOfColumns, gridRenderer) {
	this.numberOfColumns = numberOfColumns;
	this.gridRenderer = gridRenderer;
	this.rowCount = 0;
	this.currentRowColumnCount = 0;
}

GridFlowLayout.prototype.addElement = function(element, rightJustified, newRow) {
	var newCells = this.addElements(new Array(element), rightJustified, newRow);
	return newCells[0];
}

GridFlowLayout.prototype.padToEnd = function() {
	var spareCells = this.numberOfColumns - this.currentRowColumnCount;
	this.gridRenderer.addPaddingCells(spareCells);
	this.currentRowColumnCount = this.numberOfColumns;

}

GridFlowLayout.prototype.addNumberOfElements = function(size, rightJustified, newRow, keepTogether) {
	var elements = new Array(size);
	for(var i = 0; i < size; i++) {
		elements[i] = "&nbsp;";
	}	
	
	return this.addElements(elements, rightJustified, newRow, keepTogether);
}

GridFlowLayout.prototype.addElements = function(elements, rightJustified, newRow, keepTogether) {
	if(newRow || this.rowCount == 0) {
		this.gridRenderer.newRow();
		this.rowCount += 1;
	}
	
	if(keepTogether && elements.length <= this.numberOfColumns) {
		// attempt to keep together
	}
	
	if(rightJustified) {
		// pad to achieve right justification
		var surplusElementCount = elements.length % this.numberOfColumns;
		var freeColumnsInRow = this.numberOfColumns - this.currentRowColumnCount;
		if(surplusElementCount > (freeColumnsInRow)) {
			// need to start a new row - pad old first
			this.gridRenderer.addPaddingCells(freeColumnsInRow);
			this.gridRenderer.newRow();
			this.currentRowColumnCount = 0;
			freeColumnsInRow = this.numberOfColumns;
		}
		
		// pad
		this.gridRenderer.addPaddingCells(freeColumnsInRow - surplusElementCount);
		this.currentRowColumnCount += freeColumnsInRow - surplusElementCount;
	}
	
	var createdCells = new Array();
	
	for(var i in elements) {
		if(this.currentRowColumnCount == this.numberOfColumns) {
			this.gridRenderer.newRow();
			this.currentRowColumnCount = 0;
		}
		var createdCell = this.gridRenderer.addCell(elements[i]);
		createdCells.push(createdCell);
		this.currentRowColumnCount += 1;
	}
	
	return createdCells;
}

