// Copyright (c) 2009 Daniel Wachsstock
// MIT license:
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:

// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
(function($){
// create the master widget
$.widget("ui.widget",{
	// Aspect Oriented Programming tools from Justin Palmer's article
	yield: null,
	returnValues: { },
	before: function(method, f) {
		var original = this[method];
		this[method] = function() {
			f.apply(this, arguments);
			return original.apply(this, arguments);
		};
	},
	after: function(method, f) {
		var original = this[method];
		this[method] = function() {
			this.returnValues[method] = original.apply(this, arguments);
			return f.apply(this, arguments);
		};
	},
	around: function(method, f) {
		var original = this[method];
		this[method] = function() {
			var tmp = this.yield;
			this.yield = original;
			var ret = f.apply(this, arguments);
			this.yield = tmp;
			return ret;
		};
	}
});

// from http://groups.google.com/group/comp.lang.javascript/msg/e04726a66face2a2 and
// http://webreflection.blogspot.com/2008/10/big-douglas-begetobject-revisited.html
var object = (function(F){
	return (function(o){
			F.prototype = o;
			return new F();
	});
})(function (){});

// create a widget subclass
var OVERRIDE = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; 
$.ui.widget.subclass = function subclass(name){
	names = name.split('.');
	if( typeof $[names[0]][names[1]] !== "undefined" && typeof console !== "undefined" ) {
        console.error("$.ui.widget.subclass(): duplicate classname: ", name);    
    }
	$.widget(name); // Slightly inefficient to create a widget only to discard its prototype, but it's not too bad
	var widget = $[names[0]][names[1]], superclass = this, superproto = superclass.prototype;
	
	var proto = arguments[0] = widget.prototype = object(superproto); // inherit from the superclass
	$.extend.apply(null, arguments); // and add other add-in methods to the prototype
	widget.subclass = subclass;

	// Subtle point: we want to call superclass init and destroy if they exist
	// (otherwise the user of this function would have to keep track of all that)
	for (key in proto) if (proto.hasOwnProperty(key)) switch (key){
		case '_create':
			var create = proto._create;
			proto._create = function(){
				superproto._create.apply(this);
				create.apply(this);
			};
		break;
		case '_init':
			var init = proto._init;
			proto._init = function(){
				superproto._init.apply(this);
				init.apply(this);
			};
		break;
		case 'destroy':
			var destroy = proto.destroy;
			proto.destroy = function(){
				destroy.apply(this);
				superproto.destroy.apply(this);
			};
		break;
		case 'options':
			var options = proto.options;
			proto.options = $.extend ({}, superproto.options, options);
		break;
		case 'required':
			var required = proto.required;
			proto.required = $.extend ({}, superproto.required, required);
		break;
		default:
			if ($.isFunction(proto[key]) && $.isFunction(superproto[key]) && OVERRIDE.test(proto[key])){
				proto[key] = (function(name, fn){
					return function() {
						var tmp = this._super;
						this._super = superproto[name];
						try { var ret = fn.apply(this, arguments); }   
						finally { this._super = tmp; }					
						return ret;
					};
				})(key, proto[key]);
			}
		break;
	}
};
})(jQuery);
//just a factory for elements
$.el = function(type){
    return $(document.createElement(type));
};
/**
 * Dormant : visible messaging
 */
$.toast = function(msg){
	
	var expiry = function($el) {
		setTimeout(function(){
			$el.slideUp('slow',function(){
				$el.remove();
			});
		},5000);
	};
	
	var container = $.el('div').addClass('toast').text(msg);
	$('body').append(container);
	expiry(container);
	return true;
};

$.getCqEditMode = function() {
    $.cqEditMode = $(document.body).hasClass("cq-wcm-edit") 
                || $(document.body).hasClass("cq-wcm-design");	
    return $.cqEditMode;
};

/**
 *  @return www.premierleague.com
 */
$.getDomain = function() {
    if(! $.getDomain.domain ) {
        $.getDomain.domain = (String(document.location).match(/\w+:\/\/([^\/:]+)/) || ["","www.premierleague.com"])[1];
    }
    return $.getDomain.domain;
};
$.getDomain.domain = "";

/**
 *  Returns the top level domain for the website, assumes domian is 4+ letters long
 *  Assumes country code is at max 2 groups 3 letters, ie .com, .co.uk
 *  Assumes country code can compise upto two 3 letter TLDs, ie .com or .goc
 *  @return premierleague.com
 */
$.getTopLevelDomain = function() {
    if(! $.getTopLevelDomain.domain ) {
        var subdomain = $.getDomain();

        if( subdomain.match(/\.\w{2,3}\.\w{2,3}$/) ) { // check for .co.uk
            var domain = subdomain.split('.').slice(-3).join('.') || "premierleague.com"; 
        } else {
            var domain = subdomain.split('.').slice(-2).join('.') || "premierleague.com";
        }
        $.getTopLevelDomain.domain = domain;
    }
    return $.getTopLevelDomain.domain;
};
$.getTopLevelDomain.domain = "";

//***** Ajax Plugins *****//

//-- Not in use at the moment --//
///**
// *  This code sets $._ajaxOverideOptions inside a try block, then resets it again afterwards
// *  This code works a bit like a lisp let block, for setting the force options via the ajaxPrefilter
// *  Doing it this way, as its too dangerous to trust inline code to reset _ajaxOverideOptions
// *
// *  @param {Hash}     forceOptions  options to force override the options passed to any inline $.ajax() calls
// *  @param {Function} callback      code to call with these override options
// *  @param {Widget}   context       context to call the callback in
// */ 
//$.ajaxOptionsOverride = function( forceOptions, callback, context ) {
//    try {
//        $._ajaxOverideOptions = $.extend( $._ajaxOverideOptions, forceOptions );
//        callback.apply(context);
//    } finally {
//        $._ajaxOverideOptions = {};
//    }
//};
//$._ajaxOverideOptions = {};
//$.ajaxPrefilter(function( options, originalOptions, xhr ) {
//    if( typeof $._ajaxOverideOptions == "object" ) {
//        if( typeof options.original === "undefined" ) {
//            options.original = {};
//        }
//        for( var key in $._ajaxOverideOptions ) {
//            options.original[key] = originalOptions[key];
//            options[key] = $._ajaxOverideOptions[key];
//        }
//    }
//});



//*** Plugin Functions ***//


/**
 *  Gets a dot notation key out of a json hash
 *  @param {String|Array} key           key to find
 *  @param {Hash}         json          json to extract out of
 *  @param {String}       nomatch=null  what to return if nothing found
 */
$.getKey = function( key, json, nomatch ) {
    if( typeof nomatch === "undefined" ) { nomatch = null; } // typeof nomatch === "undefined" is actually faster that nomatch === undefined
    if( key && json ) {
        var keys  = key instanceof Array ? key : String(key).split(".");
        var first = keys.shift();
        var rest  = keys;

        if( first in json ) {
            if( rest.length === 0 ) {
                if( json[first] === null ) {
                    return nomatch;
                } else {
                    return json[first];
                }
            } else {
                return $.getKey( rest, json[first], nomatch );
            } 
        } else {
            return nomatch;
        }
    } else {
        return nomatch;
    }
};

/**
 *  Returns the subtree within a json structure that has the given findKey
 *  Performs a breath-first search of the json tree
 *  @param findKey {String}  child key to search for
 *  @param json    {Object}  data structure to search
 *  @Returns       {Object}  value of key, undefined if not found
 */
$.breadthFirstKeySearch = function( findKey, json ) {
    var queue = [json];
    var data;
    while( data = queue.pop() ) {
        if( data && ( typeof data === "object" || typeof data === "function" ) && !data.jquery ) {
            if( findKey in data ) {
                return data[findKey];
            } else {
                for( var key in data ) {
                    if( !json[key] ) { continue; }
                    if( typeof data[key] === "object" && !data[key].jquery ) {
                        queue.push( data[key] );
                    }
                }
            }
        }
    }
    return undefined;
};

/**
 *  Returns the subtree within a json structure that has the given findKey
 *  Performs a depth-first search of the json tree
 *  @param findKey {String}  child key to search for
 *  @param json    {Object}  data structure to search
 *  @Returns       {Object}  value of key, undefined if not found
 */
$.depthFirstKeySearch = function( findKey, json ) {
    if( json && ( typeof json === "object" || typeof json === "function" ) && !json.jquery ) {
        if( findKey in json ) {
            return json[findKey];
        } else {
            for( var key in json ) {
                if( !json[key] ) { continue; }
                var data = $.depthFirstKeySearch( findKey, json[key] );
                if( data !== undefined ) {
                    return data;
                }
            }
        }
    }
    return undefined;
};

/**
 *  Find all the keys that match findKey
 *  @param findKey {String}  child key to search for
 *  @param json    {Object}  data structure to search
 *  @Returns       {Object}  { path: object }
 */
$.exaustiveKeySearch = function( parentPath, findKey, maxDepth, useParent ) {
    findKey    = findKey    || "";
    maxDepth   = maxDepth   || 0;
    parentPath = parentPath || "";
    useParent  = useParent  || false;
    
    var data = $.getKey(parentPath, window);
   
    var queue = [{parentPath: parentPath||"", data: data, depth: 0 }];
    var queueItem;
    var found = {};
    while( queueItem = queue.pop() ) {
        if( queueItem.data && (typeof queueItem.data === "object" || typeof queueItem.data === "function" ) && !queueItem.data.jquery ) {
            if( typeof findKey === "string" && findKey in queueItem.data
             || findKey instanceof Function && findKey(queueItem.data) 
            ) {
                if( useParent ) {
                    found[queueItem.parentPath] = queueItem.data;
                } else {
                    var path = (queueItem.parentPath === "") ? findKey : queueItem.parentPath + "." + findKey;
                    found[path] = queueItem.data[findKey];
                }
            } 
            if( maxDepth && queueItem.depth >= maxDepth ) { 
                continue; 
            }
            for( var key in queueItem.data ) {
                if(! queueItem.data[key]   ) { continue; } // skip empty objects 
                if( key === findKey        ) { continue; } // skip nested keys
                if( key === "superclass"   ) { continue; } // skip superclass
                if( key === "parentWidget" ) { continue; } // skip superclass
                var path = (queueItem.parentPath === "") ? key : queueItem.parentPath + "." + key;

                queue.push({ parentPath: path, data: queueItem.data[key], depth: queueItem.depth+1 });
            }
        }
    }
    return found;
};

// http://stackoverflow.com/questions/1489624/modifying-document-location-hash-without-page-scrolling
$.setDocumentHash = function(hash) {
    hash = hash.replace( /^#/, '' );
    var fx, node = $( '#' + hash );
    if( node.length ) {
        fx = $('<div></div>');
        fx.css({
            position:   'absolute',
            visibility: 'hidden',
            top:        $(window).scrollTop() + 'px'
        });
        fx.attr( 'id', hash );
        fx.appendTo( document.body );
        node.attr( 'id', '' );
    }

    document.location.hash = hash;

    if( node.length ) {
        fx.remove();
        node.attr( 'id', hash );
    }
};

/**
 *  Creates a hash of expando properties for a node and converts numeric answers to real numbers
 *  <div widget="svgWrapper" svgheight="220" color="#fff"> = { widget: "svgWrapper" svgheight: 220, color: "#fff" }
 *  @param  {Hash} defaultHash  [optional] default params to add
 *  @return {Hash}
 */
$.fn.getAttributeHash = function( defaultHash ) {
    return $.getAttributeHash( this.get(0), defaultHash );
};

/**
 *  Opposite of $.fn.getAttributeHash
 *  { widget: "svgWrapper" svgheight: 220, data: {hello: "world"} } = "widget='svgWrapper' svgheight='220' data='{"hello":"world"}'"
 *  @param  {Hash}        options         options to tag encode
 *  @param  {Hash|Array}  options.ignore  keys to ignore
 *  @return {String}
 */
$.getOptionsHTML = function( options ) {
    var optionsHTML = "";
    if( typeof options.ignore == "undefined" ) { options.ignore = {}; }
    if( options.ignore instanceof Array      ) { options.ignore = $.arrayToHash(options.ignore,true); }
    options.ignore.ignore = true;

    if( options ) {
        for( var key in options ) {
            if( key in options.ignore ) { continue; }
            var value = "";
            switch( typeof options ) {
                case "undefined": break;
                case "number":
                case "string": value = options[key];           break;
                case "object":
                default:       value = $.toJSON(options[key]); break;
            }
            if( key === "className" ) { key = "class"; }
            value = value.replace(/^"|"$/g,'');
            value = value.replace(/'/g,"\\'");
            if( value ) {
                optionsHTML += key+"='"+value+"' ";
            }
        }
    }
    return optionsHTML;
};


/**
 *  Finds child nodes matching selector, but also adds self if self also matches selector
 */
$.fn.findAndSelf = function( selector ) {
    return this.find( selector ).add( this.filter(selector) );
};

$.fn.uuid = function() {
    for( var i=0, n=this.length; i<n; i++ ) {
        if( !this[i].getAttribute("uuid") ) {
            this[i].setAttribute("uuid", "uuid"+(++$.fn.uuid.count) );
        }
    }
    return this;
};
$.fn.getUuid = function() {
    return this.uuid().attr("uuid");
};
$.fn.uuid.count = 0;


//*** Utility Functions ***//

$.fn.loadSync = function( url, callback ) {
    var async = $.ajaxSettings.async;
    $.ajaxSetup({ async: false });
    this.load( url, callback );
    $.ajaxSetup({ async: async });
    return this;
};


/**
 *  Loads an image and returns its size via callback
 *  @param {String}   url
 *  @param {Function} function(width,height) - only called if data is valid
 */                 
$.loadImageSize = function( url, callback ) {
    if( url && callback instanceof Function ) {
        $("<img/>")
        .attr( "src",  url )
        .one(  "load", function() {
            var img = this;
            if( false && img.width && img.height ) {
                callback( img.width, img.height );
            } else {
                setTimeout(function() {
                    if( img.width && img.height ) {
                        callback( img.width, img.height );
                    }
                }, 0);
            }
        });
    }
};


/**
 *  Creates a hash of expando properties for a node and converts numeric answers to real numbers
 *  Now preserves case of expando properties defined in defaultHash
 *  <div widget="svgWrapper" svgheight="220" color="#fff"> = { widget: "svgWrapper" svgheight: 220, color: "#fff" }
 *
 *
 *  @see http://dev.w3.org/html5/spec-LC/elements.html#embedding-custom-non-visible-data-with-the-data-attributes
 *
 *  @param  {Element|jQuery} node         node to parse
 *  @param  {Hash}           defaultHash  [optional] default params to add
 *  @return {Hash}
 */
$.getAttributeHash = function( node, defaultHash ) {
    if( node && node.jquery ) { node = node.get(0); }

    var hash = {};
    var toCamel = {};
    if( typeof defaultHash !== "undefined" ) {
        for( var key in defaultHash ) {
            toCamel[ key.toLowerCase() ] = key;
        }

        // Copy rather than pass by reference
        for( var key in defaultHash ) {
            hash[key] = defaultHash[key];
        }
    }
    for( var i=0, n=node.attributes.length; i<n; i++ ) {
        var attribute = node.attributes[i];
        var nodeName = attribute.nodeName.replace(/^data-/); // HTML5 Data Attributes
        nodeName = toCamel[nodeName] || nodeName; // Preserve case of entries in defaultHash
        hash[ nodeName ] = attribute.nodeValue;
    }
    // Convert all attributes to real Numbers, lets avoid string arithmetic "1"+"2" == "12"
    for( var key in hash ) {
        if( typeof hash[key] === "string" ) {
            if(      hash[key] === "true"  ) { hash[key] = true;  }
            else if( hash[key] === "false" ) { hash[key] = false; }
            else if( hash[key].match(/^[+-]?\d*\.?\d+$/) ) {
                hash[key] = Number(hash[key]);
            }
            else if( hash[key].match(/^\[.*\]$/) ) { // Array
                hash[key] = hash[key].replace(/^\[(.*)\]$/g, '$1').split(',');
                for( var i=0, n=hash[key].length; i<n; i++ ) {
                    if( hash[key][i].match(/^[+-]?\d*\.?\d+$/) ) {
                        hash[key][i] = Number(hash[key][i]);
                    }
                    else if( hash[key][i].match(/^(['"])(.*)\1$/) ) {
                        hash[key][i].replace(/^['"](.*)\1$/, "$2"); // Strip quotes from strings
                    }
                }
            }
            else if( hash[key].match(/^\{.*\}$/) ) { // JSON
                hash[key] = $.parseJSON( hash[key] );
            }
        }
    }
    return hash;
};


/**
 *  Initializes HTML widgets marked as [widget] or [data-widget]
 *  @param  {jQuery} rootNode   rootNode to search from, includes self
 *  @return {jQuery}            list of nodes marked as widget
 */
$.initWidgets = function( rootNode ) {
    // This is init code, any uncaught exceptions here will kill all the javascript on the page
    try {
        $.initMiniWidgets(rootNode);
    } catch( e ) {
        console.error("$.initMiniWidgets(",rootNode,"): exception" );
        console.dir(e);
    }
    
    var selector = "[widget]";
    var notSelector = ".template [widget]";
    var nodes = $(selector, rootNode).add( $(rootNode).filter(selector) ).not( notSelector );
    var emptyjQuery = $([]);

    for( var i=0, n=nodes.length; i<n; i++ ) {
        try {
            var widgetClasses = nodes[i].getAttribute("widget").split(/\s+/);
            for( var j=0, m=widgetClasses.length; j<m; j++ ) {
            	var widgetClass = widgetClasses[j]; 
	            if( widgetClass && (widgetClass in $.ui || widgetClass in emptyjQuery) ) {
	                $(nodes[i])[widgetClass]({});
	            } else {
	                console.error("$.initWidgets(): class not found: <node widget='",widgetClass,"'> = ", nodes[i] );
	            }
            }
        } catch( e ) {
            console.error("$.initWidgets(): exception during widget init: <node widget='",widgetClass,"'> = ", nodes[i] );
            console.dir(e);
        }
    }
    return nodes;
};

/**
 *  Strips out any references to jQuery.prevObject that could potentually cause memory leaks
 *  @param {jQuery|Array|Hash}  data   only scans one level deep for hashes and arrays
 */
$.fn.jQueryGC = function() {
    $.jQueryGC(this);
    return this;
};
$.jQueryGC = function( data )  {
    if( typeof data === undefined ) {
        return;
    }
    else if( data instanceof jQuery ) {
        data.prevObject = $.jQueryGC.emptyjQuery;
    }
    else if( data instanceof Array ) {
        for( var i=0, n=data.length; i<n; i++ ) {
            if( data[i] instanceof jQuery ) {
                data[i].prevObject = $.jQueryGC.emptyjQuery;
            }
        }
    }
    else if( typeof data === "object" ) {
        for( var key in data ) {
            if( data[key] instanceof jQuery ) {
                data[key].prevObject = $.jQueryGC.emptyjQuery;
            }
        }
    }
};
$.jQueryGC.emptyjQuery = $([]);


$.fn.emptyGC = function() {
	this.find("[widget]").each(function(){
		var widget = $(this).data("widget");
		if( widget && widget.destroy instanceof Function ) {
			widget.destroy(); 
		}
		$(this).data("widget",null);
	});
	this.jQueryGC();
	this.empty();
	return this;
};


/**
 *  Takes an array and creates a hash based on a key/property of each array item
 *  $.indexArrayByKey( [{id: 1}, {id: 2}, {id: 3}], "id" ) -> { 1: {id: 1}, 2: {id: 2}, 3: {id: 3} }
 *  @param  {Array} list
 *  @return {Hash}
 */
$.indexArrayByKey = function( list, key ) {
    var hash = {};
    try {
        var value;
        for( var i=0, n=list.length; i<n; i++ ) {
            value            = list[i];
            hash[value[key]] = value;
        }
    } catch(e) {
        console.log('EXCEPTION: $.indexArrayByKey = function(', list ,') ', e);
    }
    return hash;
};

/**
 *  @param  {Hash}  hash
 *  @return {Array} array of hash keys
 */
$.hashToKeyArray = function( hash ) {
    var list = [];
    for( var key in hash ) {
        list.push( key );
    }
    return list;
};

/**
 *  @param  {Hash}  hash
 *  @return {Array} array of hash values
 */
$.hashToValueArray = function( hash ) {
    var list = [];
    for( var key in hash ) {
        list.push( hash[key] );
    }
    return list;
};

/**
 *  Converts an array into a hash map, with array values as keys, and optional user-defined value
 *  @param  {Array}   list   array to process
 *  @param  {Boolean} value  [optional] hash value for keys defined, ie true, default is array value
 *  @return
 */
$.arrayToHash = function( list, value ) {
    var hash = {};
    for( var i=0, n=list.length; i<n; i++ ) {
        value = ( typeof value === "undefined" ) ? list[i] : value;
        hash[ list[i] ] = value;
    }
    return hash;
};

/**
 *  Converts an array into a hash map, by splitting each array entry into a key/value pair
 *  @param  {Array<String>}   list   array to process
 *  @param  {Regexp}          split  regexp to split each string via
 *  @return
 */
$.arrayToSplitHash = function( list, split ) {
    var hash = {};
    for( var i=0, n=list.length; i<n; i++ ) {
        var pair = (list[i] || "").split( split );
        hash[ pair[0] ] = pair[1];
    }
    return hash;
};

/**
 *  Recursively sorts a hash, returning a sorted copy
 *  @param {Object|Array} hash     object to sort
 *  @param {Function}     sortFunc [optional] function(a,b) { return -1, 0, 1 } 
 */
$.sortHash = function( hash, sortFunc ) {
    if(!( sortFunc instanceof Function )) {
        sortFunc = undefined; // ensure its valid or undefined, else Array.sort() will throw
    }

    if( hash instanceof Array ) {
        return hash.sort(sortFunc);

    } else if( typeof hash === "object" ) {
        var keys = [];
        for( var key in hash ) {
            keys.push(key);
        }
        keys = keys.sort(sortFunc);

        var newHash = {};
        for( var i=0, n=keys.length; i<n; i++ ) {
            var key = keys[i];
            var value = (typeof hash[key] === "object") ? $.sortHash(hash[key], sortFunc) : hash[key]; // typeof [] === "object"
            newHash[key] = value;
            return newHash;
        }

    } else {
        return hash;
    }
};

$.sortQueryString = function( queryString, sortFunc ) {
    if(!( sortFunc instanceof Function )) {
        sortFunc = undefined; // ensure its valid or undefined, else Array.sort() will throw
    }

    if( typeof queryString === "string" ) {
        var match, newQueryString = "";
        if( queryString.match(/^\?/) ) {
            newQueryString += "?";
            queryString = queryString.slice(1);
        }
        else if( match = queryString.match(/^(\w+:|\/).*?\?/) ) {
            newQueryString += match[0];
            queryString = queryString.replace(/^.*?\?/, "");
        }

        newQueryString += queryString.split("&").sort(sortFunc).join("&").replace(/^&+|&+$/g, "");
        return newQueryString;
    } else {
        return queryString;
    }
};

/**
 *  Returns a hash of headers related to the XHR object
 *  @param  {xhr}  xhr
 *  @return {Hash}
 */
$.getHeaders = function( xhr ) {
    if( xhr && xhr.getAllResponseHeaders instanceof Function ) {
        var lines = xhr.getAllResponseHeaders().split("\n");
        var headers = $.arrayToSplitHash( lines, /:\s+/ );
        return headers;
    } else {
        console.warn("$.getHeaders(",xhr,"): invalid xhr, from: ", arguments.callee.caller );
        return {};
    }
};

/**
 *  Small amount of indirection to handle YWA tracking
 * 
 */
$.ywaTrack = function() {
	// review args, split and fire
	// we need to first convert [arguments] into a proper array
	if (typeof window.YWATracker !== 'undefined') {
		
		var wYWAT = window.YWATracker;
		var argsArray = Array.prototype.slice.call(arguments);
		var e = argsArray.shift();
		var EvPg = argsArray.shift();
		
		//console.log("EvPg",EvPg,"argsArray",argsArray);
		
		if (EvPg === "action") {
			wYWAT.setAction.apply(wYWAT,argsArray);
		} else if (EvPg === "cf") {
			wYWAT.setCF.apply(wYWAT,argsArray);
		} else if (EvPg === "isk") {
			wYWAT.setISK.apply(wYWAT,argsArray);
		} else if (EvPg === "isr") {
			wYWAT.setISR.apply(wYWAT,argsArray);
		} else if (EvPg === "submit") {
			wYWAT.submit_action();
		}
	} else {
		console.warn("tracking event fired with no framework loaded");
		return false;
	}
};
/**
 *  Quick guide to extending jQuery templates
 *
 *  In HTML: {{tag( options, mappings ) json }}
 *  _tmpltags_tag = function( json, options, mappings ) { return html; }
 *  $.extend(jQuery.tmpl.tag, { tag: { _default: { $1: "{}", $2: "{}" }, open: '__=__.concat(_tmpltags_tag($1,$2));' }}) // even if more than 2 args
 *
 *  Rules:
 *  - jQuery.tmpl reads your tag as a string, then parses it into a function call
 *  - We have jQuery.tmpl unit tests in /cq/jcr_root/etc/designs/premierleague/test/exlibs/jquery-tmpl/tests/core.js
 *  - _tmpltags_tag($1)    must be called as {{tag arg1 }}
 *  - _tmpltags_tag($1,$2) must be called as {{tag(arg2,arg3) arg1 }}
 *
 *  - arguments cannot contain inline function defintions
 *  - arguments cannot contain array defintions
 *  - arguments cannot contain nested hashes
 *  - the last arg1 must have spaces both sides if it is a hash {{tag("arg2") {arg1:"value"} }} - (a triple }}} will break the regexp)
 *  - nested hashes must have spaces after each closing tag, a double }} will be interpereted as a close for the tmpl tag
 *  - according to unit tests, the following will be validly parsed:
 *    {{joinHash( {a:{b:{c:[3,{d:4}]} } } ) {x:{y:{z:[9,{w:0}]} } } }}
 *
 *  jQuery templates should be defined within <script type="text/x-jquery-tmpl" class=""></script> tags.
 *  Sometimes you can get away with defining them as inline HTML, but they may be potentually buggy with json dot notation
 */
(function($) {

    var _formatKey = function( key, value ) {
        key = key.replace(/percentage/, '');
        key = key.replace(/plus/, '+');
        key = key.replace(/([A-Z]+)/g, " $1");
        key = key.replace(/([0-9+-]+)/g, " $1 ");
        key = key.replace(/\s\s+/g,    " ");
        key = key.replace(/^.*extra *time.*$/i, ""); // Hide Extra Time if data is zero, always the case for premier league matches
        key = key.replace(/(\d+) To (\d+)/i, "$1 to $2");
        key = key.trim();
        key = key.substring(0,1).toUpperCase() + key.substring(1).toLowerCase();
        return key;
    };

    var _formatValue = function( value, key ) {
        if( String(key).match(/^extraTime$/i) && value == 0 ) {
            value = ""; // Hide Extra Time if data is zero, always the case for premier league matches
        }
        if( String(key).match(/percentage/) ) {
            value = String(value) + " %";
        }
        return value;
    };

    var _parseMappings = function( json, mappings ) {
        mappings = $.extend({
            allFields: false,  // {Boolean} render all fields in json, not just those specified
            ignore:    {},     // {Array} fields to ignore
            fields:    {}      // {Hash} <key>: <Title|""> // Autogenerate <title> if ""
        }, mappings );

        mappings.ignore = (mappings.ignore instanceof Array) ? $.arrayToHash(mappings.ignore,true) : mappings.ignore;
        mappings.fields = (mappings.fields instanceof Array) ? $.arrayToHash(mappings.fields,"")   : mappings.fields;

        var noFieldsDefined = true;
        for( var key in mappings.fields ) { noFieldsDefined = false; break; }
        if( noFieldsDefined ) {
            mappings.allFields = true;
        }

        mappings.fieldCount = 0;
        for( var key in json ) {
            if( (mappings.allFields || key in mappings.fields) && typeof $.getKey(key,json) !== "object" ) {
                mappings.fieldCount++;
            }
        }

        return mappings;
    };

    /**
     *  @param json
     *  @param mappings
     *  @param callback  function( key, value, label, json, index ) 
     */
    var _rowHTML = function( json, mappings, callback ) {
        var html = "";
        var index = 0;
        for( var key in mappings.fields ) {
            if( key in mappings.ignore                 ) { continue; } // ignore
            if( typeof $.getKey(key,json) === "object" ) { continue; } // invalid
            
            var value = _formatValue(json[key], key);
            var label = mappings.fields[key] || _formatKey(key,value);
            html += callback(key, value, label, json, index++);
        }
        for( var key in json ) {
            if( !mappings.allFields                    ) { break; }    // skip
            if( key in mappings.fields                 ) { continue; } // already rendered
            if( key in mappings.ignore                 ) { continue; } // ignore
            if( typeof $.getKey(key,json) === "object" ) { continue; } // invalid
            
            var value = _formatValue(json[key], key);
            var label = _formatKey(key,value);

            if( value !== "" && label !== "" ) {
                html += callback(key, value, label, json, index++);
            }
        }
        return html;
    };


    /**
     *  {{widget("svgGoalsByPitchPosition", { data: goalsForDetails.goalsByPitchPosition})}}
     *  @param {String} templateSelector [unused]
     *  @param {String} widgetName
     *  @param {Hash}   options
     */
    _tmpltags_widget = function( templateSelector, widgetName, options ) {
        var html = "<div widget='"+widgetName+"' "+$.getOptionsHTML(options)+"></div>";
        return html;
    };

    /**
     *  {{matchinfo({}, { fields: { shotsPerMatch  }  }) goalsForDetails.goalsPercentagesByMatchTime }}
     *  @param {Hash}     json                 subsection of the json to render
     *  @param {Hash}     options              HTML options to be passed into the widget
     *  @param {Hash}     mappings             mappings for how the tmpl is rendered
     */
    _tmpltags_matchinfo = function( json, options, mappings ) {
        json = json || [];

        options = $.extend({
            className: ""
        }, options);

        mappings = $.extend({
            maxPerRow: 6       // {Number}  number of entries before starting a new line
        }, mappings);

        mappings = _parseMappings( json, mappings );
        
        var rows    = Math.ceil(mappings.fieldCount/mappings.maxPerRow);
        var rowSize = Math.ceil(mappings.fieldCount/rows);
        options.className += " size"+rowSize;

        var html = "";
        html += '<div '+$.getOptionsHTML(options)+'>';
        html += '<ul>';

        html += _rowHTML( json, mappings, function(key,value,label,json,index) {
            var html = "";
            label = label.replace(/ per /i, ' / ');
            label = label.replace(/^(saves|blocks) /i, "$1 made ");
            label = label.replace(/^(goals) scored /i, "$1 ");

            if( index !== 0 && index % (mappings.maxPerRow) === 0 ) {
                html += '</ul>';
                html += '</div>';
                html += '<div '+$.getOptionsHTML(options)+'>';
                html += '<ul>';
            }
            html += '<li>';
            html += '<p class="label">'+label+'</p>';
            html += '<span class="data">'+value+'</span>';
            html += '</li>';
            
            return html;
        });

        html += '</ul>';
        html += '</div>';

        return html;
    };

    /**
     *  {{keyvaluetable({ widget: "svgGoalTimes", className: "data-tables", svgwidth: 340, svgheight: 194 }, { keyTitle: "Goals Scored", valueTitle: "Time Scored", ignore: ['total'] }) goalsForDetails.goalsPercentagesByMatchTime }}
     *  @param {Hash}   json      subsection of the json to render
     *  @param {Hash}   options   HTML options to be passed into the widget
     *  @param {Hash}   mappings  mappings for how the tmpl is rendered
     */
    _tmpltags_keyvaluetable = function( json, options, mappings ) {
        json = json || [];

        options = $.extend({
            className: "",
            ignore: []         // {Array} of keys
        }, options);

        mappings = $.extend({
            keyTitle:   "",
            valueTitle: ""
        }, mappings );

        mappings = _parseMappings( json, mappings );

        var html = "";
        html += "<table "+$.getOptionsHTML(options)+">\n";

        if( mappings.keyTitle || mappings.valueTitle ) {
            html += "<thead>\n";
            html += "  <tr>\n";
            html += "    <th>" + mappings.keyTitle   + "</th>\n";
            html += "    <th>" + mappings.valueTitle + "</th>\n";
            html += "  </tr>\n";
            html += "</thead>\n";
        }

        html += "<tbody>\n";
        html += _rowHTML( json, mappings, function(key,value,label,json,index) {
            var html = "";
            html += "  <tr>\n";
            html += "    <th>" + label + "</th>\n";
            html += "    <td>" + value + "</td>\n";
            html += "  </tr>\n";
            return html;
        });
        html += "</tbody>\n";
        html += "</table>\n";
        return html;
    };
    /**
     *  {{keyvaluelist({ }, { title: "Goals Scored", ['total'] }) goalsForDetails.goalsPercentagesByMatchTime }}
     *  @param {Hash}   json      subsection of the json to render
     *  @param {Hash}   options   HTML options to be passed into the widget
     *  @param {Hash}   mappings  mappings for how the tmpl is rendered
     */
    _tmpltags_keyvaluelist = function( json, options, mappings ) {
        json = json || [];

        options = $.extend({
        }, options);

        mappings = $.extend({
            title:  ""
        }, mappings );

        mappings = _parseMappings( json, mappings );

        var html = "";
        html += "<ul "+$.getOptionsHTML(options)+">\n";

        if( mappings.title ) {
            html += "<li class='title'>" + mappings.title + "</li>\n";
        }

        html += _rowHTML( json, mappings, function(key,value,label,json,index) {
            var oddeven = (index % 2) ? "even" : "odd"; 

            var html = "";
            html += "<li class='"+oddeven+"'>";
            html += "    <span class='key'>"   + label + "</span>\n";
            html += "    <span class='value'>" + value + "</span>\n";
            html += "</li>";
            return html;
        });

        html += "</ul>\n";
        return html;
    };
    /**
     *  @example
     *  {{table({fields: { "opponent.name": "Opponent", "total": "Scored", "avgPerMatch": "Average / Match"  }, columns: 2, className: "data-tables" }) goalsForDetails.opponentsScored}}
     *
     *  @param {Array}  options.json      - json recieved from the server
     *  @param {Hash}   options.fields    - { <json_key>: { title:, fields:, columns:, className: }
     *  @param {Number} options.columns   - Number of cols to display
     *  @param {Number} options.className - CSS class name for the table element
     */
    _tmpltags_table = function( json, options, mappings ) {
        mappings = $.extend({
            fields:   {},
            columns:  1,
            className: ""
        }, mappings);

        for( var key in mappings.fields ) {
            mappings.fields[key] = $.extend({
                title:     "",
                prefix:    "",
                postfix:   "",
                className: ""
            }, mappings.fields[key]);
        }

        var cols = [];
        var rowsPerCol = Math.ceil( json.length / mappings.columns );
        for( var c = 0; c < mappings.columns; c++ ) {
            cols[c] = [];
            for( var i=c*rowsPerCol, n=i+rowsPerCol; i<n; i++ ) {
                cols[c].push( json[i] );
            }
        }

        // TODO: Make One Table
        var html = "";
        html += "<table "+$.getOptionsHTML(options)+">\n";
        html += "<tr>\n";
        for( var c=0, cn=cols.length; c<cn; c++ ) {
            for( var key in mappings.fields ) {
                var field = mappings.fields[key];
                html += "<th class='"+field.className+"'>"+field.title+"</th>\n";
            }
        }
        html += "</tr>\n";
        for( var i=0, n=rowsPerCol; i<n; i++ ) {
            html += "<tr>\n";
            for( var c=0, cn=cols.length; c<cn; c++ ) {
                var row = cols[c][i];
                for( var key in mappings.fields ) {
                    var field = mappings.fields[key];
                    var value = $.getKey( key, row, "" );
                    if( value ) { value = (field.prefix||"") + value + (field.postfix||""); }
                    html += "<td class='"+field.className+"'>"+value+"</td>\n";
                }
            }
            html += "</tr>\n";
        }
        html += "</table>\n";
        return html;
    };
    // http://blog.sterkwebwerk.nl/2010/12/15/custom-jquery-template-tags-1/
    _tmpltags_textarea = function(value, name) {
        var html_name;
        var html_id;
        if(name) {
            html_name = 'name="'+ name +'"';
            html_id = 'id="id_'+ name +'"';
        }

        var html = '<textarea '+ html_name +' '+ html_id +'>'+ value +'</textarea>';
        return html;
    };


    $.extend(jQuery.tmpl.tag, {
        table: {
            _default: { $1: "null", $2: "null" },
            open: '__=__.concat(_tmpltags_table($1,$2));'
        },
        widget: {
            _default: { $1: "''", $2: "null" },
            open: '__=__.concat(_tmpltags_widget($1,$2));'
        },
        keyvaluetable: {
            _default: { $1: "null", $2: "null" },
            open: '__=__.concat(_tmpltags_keyvaluetable($1,$2));'
        },
        keyvaluelist: {
            _default: { $1: "null", $2: "null" },
            open: '__=__.concat(_tmpltags_keyvaluelist($1,$2));'
        },
        matchinfo: {
            _default: { $1: "null", $2: "null" },
            open: '__=__.concat(_tmpltags_matchinfo($1,$2));'
        },
        matchinfo: {
            _default: { $1: "null", $2: "null" },
            open: '__=__.concat(_tmpltags_matchinfo($1,$2));'
        },
        textarea: {
            _default: { $1: "''", $2: "null" },
            open: '__=__.concat(_tmpltags_textarea($1, $2));'
        }
    });
})(jQuery);
/*
	Base.js, version 1.1a
	Copyright 2006-2010, Dean Edwards
	License: http://www.opensource.org/licenses/mit-license.php
*/

var Base = function() {
	// dummy
};

Base.extend = function(_instance, _static) { // subclass
	var extend = Base.prototype.extend;
	
	// build the prototype
	Base._prototyping = true;
	var proto = new this;
	extend.call(proto, _instance);
  proto.base = function() {
    // call this method from any other method to invoke that method's ancestor
  };
	delete Base._prototyping;
	
	// create the wrapper for the constructor function
	//var constructor = proto.constructor.valueOf(); //-dean
	var constructor = proto.constructor;
	var klass = proto.constructor = function() {
		if (!Base._prototyping) {
			if (this._constructing || this.constructor == klass) { // instantiation
				this._constructing = true;
				constructor.apply(this, arguments);
				delete this._constructing;
			} else if (arguments[0] != null) { // casting
				return (arguments[0].extend || extend).call(arguments[0], proto);
			}
		}
	};
	
	// build the class interface
	klass.ancestor = this;
	klass.extend = this.extend;
	klass.forEach = this.forEach;
	klass.implement = this.implement;
	klass.prototype = proto;
	klass.toString = this.toString;
	klass.valueOf = function(type) {
		//return (type == "object") ? klass : constructor; //-dean
		return (type == "object") ? klass : constructor.valueOf();
	};
	extend.call(klass, _static);
	// class initialisation
	if (typeof klass.init == "function") klass.init();
	return klass;
};

Base.prototype = {	
	extend: function(source, value) {
		if (arguments.length > 1) { // extending with a name/value pair
			var ancestor = this[source];
			if (ancestor && (typeof value == "function") && // overriding a method?
				// the valueOf() comparison is to avoid circular references
				(!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) &&
				/\bbase\b/.test(value)) {
				// get the underlying method
				var method = value.valueOf();
				// override
				value = function() {
					var previous = this.base || Base.prototype.base;
					this.base = ancestor;
					var returnValue = method.apply(this, arguments);
					this.base = previous;
					return returnValue;
				};
				// point to the underlying method
				value.valueOf = function(type) {
					return (type == "object") ? value : method;
				};
				value.toString = Base.toString;
			}
			this[source] = value;
		} else if (source) { // extending with an object literal
			var extend = Base.prototype.extend;
			// if this object has a customised extend method then use it
			if (!Base._prototyping && typeof this != "function") {
				extend = this.extend || extend;
			}
			var proto = {toSource: null};
			// do the "toString" and other methods manually
			var hidden = ["constructor", "toString", "valueOf"];
			// if we are prototyping then include the constructor
			var i = Base._prototyping ? 0 : 1;
			while (key = hidden[i++]) {
				if (source[key] != proto[key]) {
					extend.call(this, key, source[key]);

				}
			}
			// copy each of the source object's properties to this object
			for (var key in source) {
				if (!proto[key]) extend.call(this, key, source[key]);
			}
		}
		return this;
	}
};

// initialise
Base = Base.extend({
	constructor: function() {
		this.extend(arguments[0]);
	}
}, {
	ancestor: Object,
	version: "1.1",
	
	forEach: function(object, block, context) {
		for (var key in object) {
			if (this.prototype[key] === undefined) {
				block.call(context, object[key], key, object);
			}
		}
	},
		
	implement: function() {
		for (var i = 0; i < arguments.length; i++) {
			if (typeof arguments[i] == "function") {
				// if it's a function, call it
				arguments[i](this.prototype);
			} else {
				// add the interface using the extend method
				this.prototype.extend(arguments[i]);
			}
		}
		return this;
	},
	
	toString: function() {
		return String(this.valueOf());
	}
});
/**
 *  EventManager manages event listeners and event triggers.
 *  This allows for decoupled event-based communication between widgets.
 *
 *  @examples
 *    eventManager.register( this, "setActiveComponent", function() {} );
 *    eventManager.trigger( "createDialogeBox", "Hello World" );
 *    eventManager.unregister( this );
 *    eventManager.logging = true;
 *
 *
 *  @events
 *  Below is a list of events registered/triggered within the application (please update as new ones are added):
 *
 *  //----- Command Events - trigger these events to tell other widgets to do things -----//
 *
 *
 *
 *  //----- Status Events - triggered when the state of the application has changed -----//
 *
 *
 *
 *  //----- Getter Events - trigger in order to request data from the rest of the application -  -----//
 *
 *
 *  Static Class
 *  @author James McGuigan
 */
EventManager = Base.extend({},{
    klass: "EventManager",
    
    events:      {},    // {Hash}
    stack:       [],    // {Array}         for debugging, stack of current events being triggered
    lastEventId: 0,

    // Constructor options
    logging:     false, // {Boolean}       if true, add logging to all functions
    logKlass:    {},    // {Hash<Boolean>} if this.logKlass[context.klass] === true, then add logging
    logEvent:    {},    // {Hash<Boolean>} if this.logEvent[eventName] === true, then add logging

//*** Static Class ***//
//    constructor: function( options ) {
//        this.options  = options || {};
//
//        // Init objects here, otherwise they become class rather than instance variables
//        this.events   = {};
//        this.stack    = [];
//
//        this.logging  = this.options.logging  || false;
//        this.logKlass = this.options.logKlass || {};
//        this.logEvent = this.options.logEvent || {};
//    },

    /**
     *  Returns a count of the number of each type of widget in memory.
     *  This is primary if use in debugging and locating memory leaks.
     *  If a klass is provided, the returned data is filtered to only include that klass
     *
     *  @param  {String} klass  [optional] the klass name
     *  @return {Hash<Number>}  list of widgets in memory indexed by klass
     */
    getWidgetCountInMemory: function( klassName ) {
        klassName = klassName || '';
        var count   = {};
        var widgets = this.trigger( "returnAll"+klassName );
        for( var i=0, n=widgets.length; i<n; i++ ) {
            var klass = widgets[i].klass;
            if( !count[klass] ) { count[klass] = 0; }
            count[klass]++;
        }
        return count;
    },



    /**
     *  Registers an handler function, for a given object for a perticular eventName
     *  @param {Object}   context         the instance to listen to the event
     *  @param {String}   eventName       the name of the event to listen for
     *  @param {Function} handler         the function to call when the event is triggered, may take multiple args passed in via trigger
     *  @param {Boolean}  options.delayed call handler after all non-delayed functions
     */
    register: function( context, eventName, handler, options ) {
        console.assert( typeof eventName === "string", this.klass+"::register: eventName must be of type String ", arguments ); // instanceof String fails in FF2
        console.assert( handler instanceof Function,   this.klass+"::register: handler must be of type Function ", arguments );

        if( !this.events[eventName] ) { this.events[eventName] = {}; }

        var eventHash = {
            eventId:   ++this.lastEventId,
            context:   context,
            eventName: eventName,
            handler:   handler,
            delayed:   !!(options && options.delayed)
        };

        this.events[eventName][eventHash.eventId] = eventHash;

        if( this.logging || this.logEvent[eventName] || this.logKlass[context.klass] ) {
            console.debug( context.klass, '::register(',eventName,') on context:', context.klass,'(',context,'), handler: ', handler, ' = ', this.events[eventName] );
        }
    },
    /**
     *  Unregisters any event handlers bound to an eventName
     *  @param {Object}   context    the instance to listen to the event
     *  @param {String}   eventName  [optional] eventName that was being listened for, if empty unbind all eventNames
     *  @param {Function} handler    [optional] reference to handler function, if empty unbind all functions
     */
    unregister: function( context, eventName, handler ) {
        var name, key;
        var eventNameHash = {};
        if( !this.events[eventName] ) { this.events[eventName] = {}; }

        if( eventName ) {
            eventNameHash[eventName] = eventName; // loop over only eventName
        } else {
            eventNameHash = this.events;          // loop over all eventNames
        }

        for( name in eventNameHash ) {
            if( !this.events[name] ) {
                continue;
            }
            for( key in this.events[name] ) {
                if( this.events[name][key]
                 && (!context || context === this.events[name][key].context)
                 && (!handler || handler === this.events[name][key].handler) ) {
                    delete this.events[name][key];
                }
            }
        }

        if( this.logging || this.logEvent[eventName] || this.logKlass[context.klass] ) {
            console.debug( context.klass, '::unregister(',eventName,') on context:', context.klass,'(',context,'), handler: ', handler, ' = ', this.events[eventName] );
        }
    },

    /**
     *  Fires an event, calls all listeners
     *  @param  {String} eventName  eventName to fire
     *  @param  {Object} arg        [optional] arg to pass to the event handlers
     *  @param  {Object} argN       [optional] may pass in multiple arguments
     *  @return {Array}             return values of all handler functions called
     */
    trigger: function( eventName ) {
        console.assert( typeof eventName === "string", this.klass+"::trigger: eventName must be of type String", arguments ); // instanceof String fails in FF2

        var i, pdi, key, eventHash, argKlass, args = [], result, results = [], processingDelayed;

        this.stack.push(eventName); // For debugging purposes
        if( this.logging || this.logEvent[eventName] ) {
            var eventNameKlass = {}, klass;
            for( key in this.events[eventName] ) {
                klass = this.events[eventName][key].context.klass;
                eventNameKlass[klass] = (eventNameKlass[klass]||0) + 1; // useful for debugging event memory leaks
            }
            console.debug( this.klass, '::START::trigger( ', arguments, ') count: ', eventNameKlass, " stack: ", this.stack, ", over: ", this.events[eventName] );
        }

        for( i=1, n=arguments.length; i<n; i++ ) { // skip first argument, its the eventName
            args.push( arguments[i] );
        }

        // TODO: profile if this.events[eventName] is better off as a hash or an array
        if( this.events[eventName] ) {
            for( pdi=0, processingDelayed=false; pdi<2; pdi++, processingDelayed=true ) {
                for( key in this.events[eventName] ) {
                    eventHash = this.events[eventName][key];
                    if( !eventHash ) {
                        continue; // nothing to see here... move on
                    }
                    if( eventHash.delayed != processingDelayed ) {
                        continue; // skip the delayed events first, then the normal ones on the second loop
                    }

                    if( eventHash.context._destroyed ) {
                        this.unregister( eventHash.context ); // Garbage collection
                        continue;
                    }


                    if( eventHash.handler instanceof Function ) {

                        // Fire at William
                        result = eventHash.handler.apply( eventHash.context, args );
                        if( typeof result !== "undefined" ) {
                            results.push( result );
                        }

                        // Logging
                        if( eventHash.context && this.logKlass[eventHash.context.klass] ) {
                            argKlass = args[0] && args[0].klass || '';
                            console.debug( eventHash.context.klass, "::triggered(", eventName, ") args: ", argKlass, "(", args, ") = ", result, " stack: ", this.stack );
                        }
                    }
                }
            }
        }
        if( this.logging || this.logEvent[eventName] ) {
            console.debug( this.klass, '::END::trigger( ', eventName, args, ') over: ', this.events[eventName] );
        }
        this.stack.pop(); // For debugging purposes
        return results;
    }
});
/**
 *  UidEventManager - Allows server-generated uid-indexed json to be split and routed to all intrested widgets and listeners
 *  It has a slightly different API to that of EventManager, using option hashes rather than multiple parameters
 *  Processing and dispatching incomming json events needs to be very efficent, and potentually scale to large number of listeners.
 *
 *  Guarantees:
 *  - Every listener with a matching UID will be fired
 *  - Each listener will be fired once regardless of the number of uids it is listening to, unless uid is also in componentMessages:
 *  - Registrations with the delayed:true will fire after all other listeners with the default delayed:false
 *  - uids in the componentMessages: subhash will also be processed in a single pass, preserving delayed:true functionality
 *
 *  @examples
 *    uidEventManager.register({
 *        context: this,
 *        uids:    [this.uid],
 *        keys:    ['refresh', 'reload', 'version'],
 *        handler: this.handlerNotificationDelayed,
 *        delayed: true
 *    });
 *    uidEventManager.unregister({ context: this });
 *    uidEventManager.trigger({ "uid": { "version": "33", "componentMessages": { "uid": "42" }} }})
 *    uidEventManager.logging = true
 *
 *  @author James McGuigan
 */
UidEventManager = Base.extend({},{
    klass: "UidEventManager",

    lastListenerId: 0,     // {Number}  for indexing purposes
    listeners:     {},     // {Hash}    this.listeners[uidType][uid][listenerHash.listenerId] = <listenerHash> {};
    contexts:      {},     // {Hash}    this.contexts[uidType][contextUid] = [ <listenerId>, <listenerId> ] - lookup listenerIds associated with an contextUid - used in unregister

    _prioritiesHash:   {}, // {Hash}  list of registered priorties
    _prioritiesSorted: [], // {Array} list of registered priorties

    // Constructor Options
    logging:       false,  // {Boolean} if true, add logging to all functions
    logContext:    null,   // {Object}  if set, add logging for events for this given context
    logUidType:    null,   // {String}  if set, add logging for events with this given uidType

//    /**
//     *  @param {Boolean} options.logging
//     *  @param {Object}  options.logContext
//     *  @param {String}  options.logUidType
//     */
//    constructor: function( options ) {
//        this.base( options );
//        this.options = options || {};
//        this.listeners = {};
//        this.contexts  = {};
//
//        this._prioritiesHash   = {};
//        this._prioritiesSorted = [];
//
//        this.logging    = this.options.logging    || false;
//        this.logContext = this.options.logContext || false;
//        this.logUidType = this.options.logUidType || false;
//    },

    /**
     *  Registers a listener for given uids
     *  @param {Object}      options.context  the context of the handler to be called, often "this" from the calling class
     *  @param {String|null} options.uidType  [optional] the uid parameter name sent via ajax
     *  @param {Array}       options.uids     an array of uid strings to listen for
     *  @param {Array}       options.keys     [optional] an array of json keys to search for, such as 'LockedBy', if not found, don't pass json to listener
     *  @param {Function}    options.handler  function to be called when uid is mentioned in ajax response
     *  @param {Boolean}     options.allUids  [default: false] if true, listen to all uid events, alternitively set uids === ["*"]
     *  @param {Boolean}     options.subset   [default: true]  if true, only pass a json subset with relivant uid data, rather than the whole server json hash
     *  @param {Number}      options.priority [default: 0]     controls the firing ordering of handler functions
     *  @param {Number}      options.delayed  [default: false] if true, trigger handler after other non-delayed handlers, set priority to 20
     */
    register: function( options ) {
        options.context = this.makeEventable( options.context );

        console.assert( options.context && options.context.contextUid,          this.klass+"::register(): options.context.contextUid must be defined", options);
        console.assert( options.uids    && options.uids    instanceof Array,    this.klass+"::register(): options.uids must be of type Array", options);
        console.assert( options.handler && options.handler instanceof Function, this.klass+"::register(): options.handler must be of type Function", options);
        console.assert(!options.uidType || typeof options.uidType === "string", this.klass+"::register(): options.uidType if supplied must be of type String", options);

        var listenerHash = $.extend({
            listenerId: "_" + (++this.lastListenerId), // BUGFIX: Chrome does array ordering, rather than insertion ordering for hashes with numeric indexes
            uidType:    undefined,
            handler:    undefined,
            uids:       [],
            keys:       [],
            allUids:    false,
            subset:     true,
            delayed:    false,
            priority:   0,
            context:    undefined,
            contextUid: undefined
        }, options );

        if( listenerHash.allUids || listenerHash.uids[0] === "*" ) {
            listenerHash.uids    = ["*"];
            listenerHash.allUids = true;
            listenerHash.subset  = false; // if we are listening to everything, then we need to send everything
        }
        listenerHash.contextUid = listenerHash.context.contextUid;


        var uids    = listenerHash.uids;
        var uidType = listenerHash.uidType;
        var context = listenerHash.context;
        var handler = listenerHash.handler;

        var i, n, uid;

        if( listenerHash.delayed ) {
            listenerHash.priority = 20;
        }
        if( !this._prioritiesHash[listenerHash.priority] ) {
            this._prioritiesHash[listenerHash.priority] = true;
            this._prioritiesSorted.push( listenerHash.priority );
            this._prioritiesSorted = this._prioritiesSorted.sort();
        }

        if( !this.listeners[uidType] ) { this.listeners[uidType] = {}; }
        if( !this.contexts[uidType]  ) { this.contexts[uidType]  = {}; }
        for( i=0, n=uids.length; i<n; i++ ) {
            uid = uids[i];
            if( uid === 'manual' ) { continue; } // Ignore the uid 'manual' as it does not apply to a specific story/component

            if( !this.listeners[uidType][uid] ) { this.listeners[uidType][uid] = {}; }
            this.listeners[uidType][uid][listenerHash.listenerId] = listenerHash;

            if( !this.contexts[uidType][context.contextUid] ) { this.contexts[uidType][context.contextUid] = []; }
            this.contexts[uidType][context.contextUid].push( listenerHash.listenerId );
        }
    },

    /**
     *  Unregisters a listener
     *  @param {Object}    options.context  the context of the handler to be called, often "this" from the calling class
     *  @param {String}    options.uidType  [optional] the uid parameter name sent via ajax
     *  @param {Function}  options.handler  [optional] function to be called when uid is mentioned in ajax response
     */
    unregister: function( options ) {
        // Don't call .makeEventable, as we shouldn't be unregistering events that we havn't registered
        console.assert(  options.context && options.context.contextUid,          this.klass+"::unregister(): options.context.contextUid must be defined", options);
        console.assert( !options.uidType || typeof options.uidType === "string", this.klass+"::unregister(): options.uidType must be of type String", options);
        console.assert( !options.handler || options.handler instanceof Function, this.klass+"::unregister(): options.handler must be of type Function", options);

        var i, n, listener, listenerId, listenerHash, eventName, remainingListenerIds = [], uid, uidTypeHash = {}, anyListenersForUidType;
        var context = options.context;
        var uidType = options.uidType;
        var handler = options.handler;
        var contextUid = context.contextUid;

        if( uidType ) {
            uidTypeHash[uidType] = this.listeners[uidType]; // use only a single key
        } else {
            uidTypeHash = this.listeners;                   // loop over all uidType keys
        }

        for( uidType in uidTypeHash ) { // uidTypeHash only used for keys
            if( !this.listeners[uidType]            ) { continue; } // double check we have a valid hash to look for
            if( !this.contexts[uidType][contextUid] ) { continue; } // if the contextUid has no associated listeners, no point looking for them


            // Context is required, listenerIds for a context can be looked up via this.contexts[uidType][contextUid]
            // Either loop over this.listeners[uidType][uid][listenerId] and scan for context.contextUid
            // Or loop over this.listeners[uidType][uid] and pluck out listenerIds from this.contexts[uidType][contextUid]
            // If a listenerHash is being removed, its listenerId can be removed from this.contexts[uidType][contextUid]

            remainingListenerIds = [];
            for( uid in this.listeners[uidType] ) {

                //// First method without this.contexts
                //for( listenerId in this.listeners[uidType][uid] ) {
                //    listenerHash = this.listeners[uidType][uid][listenerId];
                //    if( listenerHash.contextUid === contextUid && (!handler || handler === listenerHash.handler) ) {
                //        delete this.listeners[uidType][uid][listenerId];
                //    }
                //}

                //// Second method with this.contexts - should be faster
                for( i=0, n=this.contexts[uidType][context.contextUid].length; i<n; i++ ) {
                    listenerId   = this.contexts[uidType][contextUid][i];
                    listenerHash = this.listeners[uidType][uid][listenerId];
                    if( !listenerHash ) { // context lookup may refer to another uidType
                        continue;
                    } else if( listenerHash.contextUid === contextUid && (!handler || handler === listenerHash.handler) ) {
                        delete this.listeners[uidType][uid][listenerId];
                    } else {
                        remainingListenerIds.push( listenerId ); // this is clearing this.contexts[uidType] before the for( uidType in uidTypeHash ) iteration is complete
                    }
                }


                // Third, cleanup any uid keys in this.listeners[uidType] that no longer have any listeners
                anyListenersForUidType = false;
                for( listenerId in this.listeners[uidType][uid] ) {
                    anyListenersForUidType = true;
                    break;
                }
                if( anyListenersForUidType === false ) {
                    delete this.listeners[uidType][uid];
                }
            }
            this.contexts[uidType][context.contextUid] = remainingListenerIds;
        }

        if( this.logging || context === this.logContext || (uidType && uidType === this.logUidType) ) {
            console.debug( this.klass, "::unregister( ", options ," ) - this.listeners[uidType]: ", this.listeners[uidType] );
        }
    },

    /**
     *  Used by AjaxPoller
     *  @param  {String|Hash} uidTypeFilter  [optional] only test for keys in this hash
     *  @return {Boolean}     true if any uids are activly registered, false otherwise
     */
    hasUids: function( uidTypeFilter ) {
        // this.listeners[uidType][uid][listenerHash.listenerId] = <listenerHash> {};
        var uidType, uid, listenerId;
        for( uidType in this.listeners ) {
            if( uidTypeFilter ) {
                if( (typeof uidTypeFilter === "object" && !uidTypeFilter[uidType])
                 || (typeof uidTypeFilter === "string" && uidTypeFilter !== uidType) )
                {
                    continue;
                }
            }

            for( uid in this.listeners[uidType] ) {
                for( listenerId in this.listeners[uidType][uid] ) {
                    if( this.listeners[uidType][uid][listenerId] ) {
                        return true;
                    }
                }
            }
        }
        return false;
    },

    /**
     *  Used by AjaxPoller
     *  @param  {String|Hash}    uidTypeFilter  [optional] only test for keys in this hash
     *  @return {Hash<UidArray>} a list of uids currently registered, indexed by uidType.
     */
    getUidHash: function( uidTypeFilter ) {
        var uidHash = {};
        var uidType, uid, listener;
        for( uidType in this.listeners ) {
            if( uidTypeFilter ) {
                if( (typeof uidTypeFilter === "object" && !uidTypeFilter[uidType])
                 || (typeof uidTypeFilter === "string" && uidTypeFilter !== uidType) )
                {
                    continue;
                }
            }

            if( !uidHash[uidType] ) { uidHash[uidType] = []; }
            for( uid in this.listeners[uidType] ) {
                uidHash[uidType].push( uid );
            }
            if( uidHash[uidType].length === 0 ) {
                delete uidHash[uidType];
            }
        }
        return uidHash;
    },

    /**
     *  @param {Hash}             json              json block to parse
     *  @param {Number}           prority           [optional]
     */
    trigger: function( json, prority ) {
        var i, n, uid, subUid, listenersToFire;

        // Convert to object
        if( typeof json === 'string' ) {
            try {
                json = eval('('+json+')');
            } catch( error ) {
                console.warn(this.klass, '::trigger() invalid json string: ', json);
                return;
            }
        }

        // Log
        if( this.logging ) {
            console.debug( this.klass, '::trigger() json:', json );
        }

        // Set json[uid].componentMessages._isComponentMessagesEntry
        for( uid in json ) {
            if( typeof json[uid].componentMessages === "object" ) {
                for( subUid in json[uid].componentMessages ) {
                    json[uid].componentMessages[subUid]._isComponentMessagesEntry = true;
                }
            }
        }

        // Lookup and fire
        var prorityListenersToFire = this._getPriorityListenersToFire( json, prority );
        var prorities = (typeof prority === "undefined") ? this._prioritiesSorted : [prority];
        for( i=0, n=prorities.length; i<n; i++ ) {
            listenersToFire = prorityListenersToFire[prorities[i]];
            if( typeof listenersToFire === "object" ) {
                this._triggerListeners( listenersToFire, json, prorities[i] );
            }

            // Recurse trigger for componentMessages at this prority
            for( uid in json ) {
                if( typeof json[uid].componentMessages === "object" ) {
                    this.trigger( json[uid].componentMessages, prorities[i] );
                }
            }
        }
    },

    /**
     *  Triggers a json block based on a pre-determined set of listeners
     *  @param {Hash<listenerId>} listenersToFire   listenerHash's indexed by listenerId
     *  @param {Hash}             json              json block to parse
     *  @param {Number}           prority           [unused] for debugging purposes
     */
    _triggerListeners: function( listenersToFire, json, priority ) {
        var subset, listener, listenerId;
        for( listenerId in listenersToFire ) {
            listener = listenersToFire[listenerId];

            // Validate listener
            if( listener.context && listener.context._destroyed === true ) {
                this.unregister({ context: listener.context }); // Garbage collection
                continue; // next listenerId
            }
            if( listener.suspended ) { continue; } // next listenerId

            // Build subset
            subset = this._getSubset( listener, json );
            if( subset === null ) { continue; }

            // Log
            if( this.logging || listener.context === this.logContext ) {
                console.debug( this.klass, "::trigger(", subset, ") - listener: ", listener );
            }

            // Fire at William
            listener.handler.call( listener.context, subset );
        }
    },


    /**
     *  Returns a sorted hash of listenersToFire in prority order
     *  @param  {Hash} json
     *  @return {Hash} listenersToFire[prority][listenerId] = listener
     */
    _getPriorityListenersToFire: function( json, priority ) {
        var i, n, listener, listenerId;
        var listenersToFire = {};

        // Create a sorted hash of listenersToFire
        for( i=0, n=this._prioritiesSorted.length; i<n; i++ ) {
            listenersToFire[this._prioritiesSorted[i]] = {};
        }

        // Build list of matching listeners
        // Loop over uids in json - should be faster if json.keys().length is smaller than this.listeners[uidType].keys().length - TODO: profile
        for( uidType in this.listeners ) {
            for( uid in json ) {
                if( !this.listeners[uidType][uid] ) { continue; }
                for( listenerId in this.listeners[uidType][uid] ) {
                    listener = this.listeners[uidType][uid][listenerId];
                    listenersToFire[listener.priority][listenerId] = listener;
                }
            }
            if( this.listeners[uidType]["*"] ) {
                for( listenerId in this.listeners[uidType]["*"] ) {
                    listener = this.listeners[uidType]["*"][listenerId];
                    listenersToFire[listener.priority][listenerId] = listener;
                }
            }
        }

        //// Loop over uids in this.listeners[uidType] - should be faster if json.keys().length is larger than this.listeners[uidType].keys().length - TODO: profile
        //for( uidType in this.listeners ) {
        //    for( uid in this.listeners[uidType] ) {
        //        for( listenerId in this.listeners[uidType][uid] ) {
        //            if( uid === "*" || json[uid] ) {
        //                listener = this.listeners[uidType][uid][listenerId];
        //                listenersToFire[listener.priority][listenerId] = listener;
        //            }
        //        }
        //    }
        //}

        // Quicker to cut here than check in the above loop
        if( typeof priority !== "undefined" ) {
            var listenersSubset = {};
            listenersSubset[priority] = listenersToFire[priority] || {};
            listenersToFire = listenersSubset;
        }

        return listenersToFire;
    },

    /**
     *  Returns a subset of json based on uid keys defined for listener
     *  @param  {Hash}      listener
     *  @param  {Hash}      json
     *  @return {Hash|null} json subset or null if nothing matches
     */
    _getSubset: function( listener, json ) {
        var i, n, k, kk, uid, subset, subsetSize, containsKey;

        if( !listener.subset ) {
            subset = json;
        } else {
            subset = {};
            subsetSize = 0;
            for( i=0, n=listener.uids.length; i<n; i++ ) {
                uid = listener.uids[i];
                if( !json[uid] ) { continue; } // Skip uids that where not set

                // Skip uids that don't contain any of the required keys - we still send through the whole hash though
                if( listener.keys && listener.keys.length ) {
                    for( containsKey = false, k=0, kk=listener.keys.length; k<kk; k++ ) {
                        if( typeof json[uid][listener.keys[k]] !== 'undefined' ) { containsKey = true; break; }
                    }
                    if( containsKey === false ) { continue; }  // skip uids without required keys
                }

                // If we havn't continued by this point, then add to the subset
                subset[uid] = json[uid];
                subsetSize++;
            }
            if( subsetSize === 0 ) {
                return null; // skip listeners with no relevant data
            }
        }
        return subset;
    },

    /**
     *  This is a wrapper-extender for objects being passed into UidEventManager
     *  - It adds an contextUid to enable objects to be individually identified
     *  - It also extends any destroy method, to add a _destroyed flag, a backup for event garbarge collection
     *  @param  {Object} The context object to be extended
     *  @return {Object} The extended context with .contextUid and ._destroyed
     */
    makeEventable: function( context ) {
        if( typeof context == "undefined" ) {
            console.warn( "UidEventManager(", context,"): context undefined" );
            context = {};
        }
        if( typeof context.contextUid == "undefined" ) {
            context.contextUid = ++UidEventManager.maxContextUid;
        }
        if( typeof context.destroy == "function" ) {
            if( typeof context._destroyed == "undefined" ) {
                var originalDestroy = context.destroy;
                context.destroy = function() {
                    context._destroyed = true;
                    return originalDestroy.destroy.apply(context, arguments);
                };
                context._destroyed = false;
            }
        }
        return context;
    }
},{
    // Class Variables
    maxContextUid: 0    // {Number}  counter for creating new contextUids
});
/**
 *  AjaxPoller polls the server /statusCheck.do and disseminates data based on UID
 *
 *  Use AjaxPoller.register(), AjaxPoller.unregister() as class methods, 
 *      instances will be created for you automattically
 *      
 *  @example (ticker.js)
 *  AjaxPoller.register({
 *      url:          url,
 *      type:         "GET",
 *      interval:     this.options.ajaxInterval,
 *      dataType:     "json",
 *      handler:      [this, this.ajaxPollHandler]
 *  });
 *
 *  TODO: Merge AjaxPoller and DataCache into one class
 *  @author James McGuigan
 */
AjaxPoller = Base.extend({
    klass: "AjaxPoller",

    // Constructor options
    url:          "",     // {String} url to pass to jQuery.ajax()
    type:         "GET",  // {String} GET or POST to pass to jQuery.ajax()
    dataType:     "json", // {String} dataType to pass to jQuery.ajax()
    ifModified:   true,   // {Boolean} option passed to jQuery.ajax()
    interval:     60000,  // {Number} in ms - how often do we poll, default 1 minute
    initialPoll:  null,   // {Number} in ms - how soon for the inital poll, default this.interval
    handlers:     null,   // {Array}

    // Instance Variables
    lastJson:        null,  // {Hash} contents of last json data fetched
    count:           0,     // Iterator for number of calls to the server made
    xhr:             null,  // Currently active xhr requests
    etag:            null,  // {String} last recieved Etag
    ajaxPollRunning: false, // {Boolean} has the ajaxPoll started yet

    /**
     *  @param {String}          options.url                url to poll
     *  @param {Number}          options.interval           polling interval in seconds, default 60s
     *  @param {Any}             options.handler            $.proxy(function(){}), [context, function], ["eventName", EventManager], UidEventManager
     *  @param {Array<handler>}  options.handlers           Array version of above
     */
    constructor: function( options ) {
        this.options = options || {};

        this.interval    = Number(options.interval)    || this.interval;
        this.initialPoll = Number(options.initialPoll) || 0;             // Run first poll immediatly
        this.type        = (options.type)              || this.type;     // Default to GET
        this.count       = 0;
        this.handlers    = [];

        this.registerEventManager();
        this.register(options);
    },
    register: function( options ) {
        if( this.options.handler      ) { this.addHandler( options.handler ); }

        if( typeof options.interval === "number" ) {
            this.interval = Math.min( options.interval, this.interval );
        }

        if( !options.disabled ) {
            this.startAjaxPoll();
        }
    },
    unregister: function( options ) {
        if( options.handler ) { this.removeHandler( options.handler ); }
    },

    addHandler: function( handler ) {
        // TODO: Add validation
        if( handler ) {
            this.handlers.push( handler );
        }
    },
    removeHandler: function( handler ) {
        for( var i=0, n=this.handlers.length; i<n; i++ ) {
            if( handler === this.handlers[i]
             || handler instanceof Array && this.handler[i] instanceof Array
             && handler[0] === this.handler[i][0] && handler[1] === this.handler[i][1]
            ) {
                Array.remove( this.handlers, i );
                break;
            }
        }
        if( this.handlers.length === 0 ) {
            this.stopAjaxPoll();
        }
    },

    /**
     *  Defines an event manager for startAjaxPoll and stopAjaxPoll commands
     *  Setter needs to be called addEventManager as part of the implied EventManager interface
     *  @param {EventManager} eventManager to load
     */
    registerEventManager: function() {
        EventManager.register( this, "startAjaxPoll", this.startAjaxPoll );
        EventManager.register( this, "stopAjaxPoll",  this.stopAjaxPoll );
    },
    unregisterEventManager: function() {
        EventManager.unregister( this, "startAjaxPoll" );
        EventManager.unregister( this, "stopAjaxPoll"  );
    },


    /**
     *  Does an inital server poll, then sets up a repeating poll loop
     *  The inital poll is set using a small timeout, to allow this function to be
     *  repeatedly called at startup with only a single call to the server actually being made
     */
    startAjaxPoll: function() {
        if( this.ajaxPollRunning ) { return; }

        clearTimeout( this._startAjaxPollSemaphore );
        clearTimeout( this._refreshTimeoutId );

        var myself = this;
        this._startAjaxPollSemaphore = setTimeout( function() {
            try {
                myself.statusPollLoop();
                myself.pollServer();
                myself.ajaxPollRunning = true;
            } catch(e) {
                console.error("Exception: AjaxPoller.startAjaxPoll ", this, e);
                console.dir(e);
            }
        }, this.initialPoll );
    },

    /**
     *  Set a repeating timeout to dynamically update the lock and workflow status
     */
    statusPollLoop: function() {
        var myself = this;
        if( this._refreshTimeoutId ) {
            clearTimeout( this._refreshTimeoutId );
        }
        if( this.interval > 0 ) {
            this._refreshTimeoutId = setTimeout( function() {
                try {
                myself.statusPollLoop();
                myself.pollServer();
                myself.ajaxPollRunning = true;
                } catch(e) {
                    console.error("Exception: AjaxPoller.statusPollLoop ", this, e);
                    console.dir(e);
                }
            }, this.interval );
        } else {
            this.ajaxPollRunning = false;
        }
    },
    /**
     *  Stops the repeating timeout to dynamically update the lock and workflow status
     */
    stopAjaxPoll: function() {
        if( this._startAjaxPollSemaphore ) { clearTimeout( this._startAjaxPollSemaphore ); }
        if( this._refreshTimeoutId       ) { clearTimeout( this._refreshTimeoutId );         }
        this.ajaxPollRunning = false;
    },

    trigger: function( json, status, xhr ) {
        for( var i=0, n=this.handlers.length; i<n; i++ ) {
            // Function - context supplied via $.proxy()
            if( this.handlers[i] instanceof Function ) {
                this.handlers[i]( json, status, xhr );
            }
            else if( this.handlers[i] instanceof Array && this.handlers[i].length == 2 ) {
                // [ context, Function ]
                if( this.handlers[i][1] instanceof Function ) {
                    this.handlers[i][1].call( this.handlers[i][0], json, status, xhr );
                }
                // [ eventName, EventManager ]
                if( this.handlers[i][1] instanceof EventManager ) {
                    this.handlers[i][1].trigger( this.handlers[i][0], json, status, xhr );
                }
            }
            // UidEventManager
            else if( this.handlers[i] instanceof UidEventManager ) {
                this.handlers[i].trigger( json, status, xhr );
            }
            else {
                console.warn( this.klass+"::trigger(): Invalid event: ", this.handlers[i], this );
            }
        }
    },

    pollServer: function() {
        var myself = this;

        // Skip sending the next poll request if the previous one has not returned
        if( this.xhr && this.xhr.readyState !== 0 && this.xhr.readyState !== 4 ) {
            this.count--; // ensure every 10th request is a pollForWorkflow, even if we skip some
            return;
        }

        this.xhr = $.ajax({
            type:       this.type,
            url:       (this.options.url instanceof Function) ? this.options.url() : this.options.url,
            dataType:   this.options.dataType,
            ifModified: this.options.ifModified,
            success:  $.proxy(function( json, status, xhr ) {
                if( json && xhr.status != 304 ) { // 304 notmodified, also assumes that json="" is an invalid response
                    //var headers = $.getHeaders( xhr );
                    myself.trigger( json, status, xhr );
                }
            },this),
            error: $.proxy(function( xhr, status, error ) {
                if( xhr.status == 404 || xhr.status == 502 ) { // Not Found or Proxy Error
                    this.stopAjaxPoll();
                    console.warn( this.klass+"::pollServer(): 404 on ", this.url, " aborting further polling ", this );
                }
            },this)
        });
    },
    destroy: function() {
        this.stopAjaxPoll();
        this.unregisterEventManager();

        if( this.xhr && this.xhr.readyState !== 4 ) {
            try {
                this.xhr.abort();
            } catch(e) {}
        }
        this.xhr = null;
        this.base();
    }
},{
    // Class functions

    ajaxPollers: {},

    /**
     *  @param {String}          options.url                url to poll
     *  @param {Number}          options.interval           polling interval in seconds, default 60s
     *  @param {Any}             options.handler            $.proxy(function(){}), [context, function], ["eventName", EventManager], UidEventManager
     *  @param {Array<handler>}  options.handlers           Array version of above
     *  @param {EventManager}    options.eventManager       [optional] registers startAjaxPoll and stopAjaxPoll events
     */
    register: function( options ) {
        var url = options.url;
        if( this.ajaxPollers[url] ) {
            this.ajaxPollers[url].register(options);
        } else {
            this.ajaxPollers[url] = new AjaxPoller(options);
        }
    },
    unregister: function( options ) {
        var url = options.url;
        if( this.ajaxPollers[url] ) {
            this.ajaxPollers[url].unregister(options);
        }
    }
});
/**
 *  Manages cached lazy loading of urls
 *
 *  TODO: Merge AjaxPoller and DataCache into one class
 *  @example
 *    DataCache.register( "/ajaxLiveTvUrl.json", function(){} );
 *    DataCache.lazyLoad( "/ajaxLiveTvUrl.json", function(){} );
 *
 *  @author James McGuigan
 */
DataCache = Base.extend({},{
    klass: "DataCache",

    // Options
    logging:            false, // {Boolean}  enable/disable logging

    // Internal
    cache:              {}, // {Hash<url>:<JSON>}                data cache
    loading:            {}, // {Hash<url>:<Boolean>}             is the url currently loading
    loaded:             {}, // {Hash<url>:<Boolean>}             has the url successfully loaded
    interval:           {}, // {Hash<url>:<Number>}              ms between polling, if zero then don't poll
    callbacks:          {}, // {Hash<url>:<function(json, url)>} callbacks defined for each url
    _ajaxPollSemaphore: {}, // {Hash<url>:<Number>}              semaphore for poll setTimeout

//    constructor: function( options ) {
//        this.options  = options || {};
//
//        this.logging  = options.logging  || this.logging;
//        this.interval = options.interval || this.interval;
//
//        this.cache     = {}; // {Hash<url>:<JSON>}                  data cache
//        this.loading   = {}; // {Hash<url>:<Boolean>}               is the url currently loading
//        this.loaded    = {}; // {Hash<url>:<Boolean>}               has the url successfully loaded
//        this.interval  = {}; // {Hash<url>:<Number>}                ms to wait between polling, 0 for no-poll
//        this.callbacks = {}; // {Hash<url>:[<function(json, url)>]} callbacks defined for each url
//        this._ajaxPollSemaphore = {}; // {Hash<url>:<Number>}
//    },
    /**
     *  Registers a callback for a data url, and triggers callback with loaded data
     *  Callback will be retriggered when if the url is reloaded from the server 
     *  Callback not triggered if data is already cached @see this.lazyLoad(url,callback)
     *  NOTE: Rebind the callback with $.proxy() in _create(), 
     *        Calling $.proxy() inline returns a unique function on each method call, which may result in duplicate registerations
     *  @param {String}   url
     *  @param {Function} callback
     */
    register: function( url, callback, interval ) {
        if(!( this.callbacks[url] instanceof Array )) {
            this.callbacks[url] = [];
        }
        this.callbacks[url].push(callback);
        this.callbacks[url] = this.callbacks[url].uniq(); 
        this.interval[url]  = (typeof interval !== "undefined") ? interval : (this.interval[url] || null);

        if( this.logging ) { console.log(this.klass+":register(",url,", ",callback,", ", interval ,") - callbacks: ", this.callbacks, ", this: ", this); }
        this.lazyLoad(url);
    },
    /**
     *  Lazy loads the url, and triggers optional callback with loaded/cached data
     *  Doesn't register the callback for subsequent url reloads @see this.register()
     *  @param {String}   url
     *  @param {Function} [optional] callback - function(json,url)
     */
    lazyLoad: function( url, callback ) {
        if( this.logging ) { console.log(this.klass+":lazyLoad(",url,", ",callback,") - callbacks: ", this.callbacks, ", this: ", this); }
        if( this.loading[url] ) {
            $.noop(); // Do nothing
        }
        else if( this.loaded[url] ) {
            if( callback instanceof Function ) {
                callback(this.cache[url],url);
            }
        } else {
            this.load(url); 
        }
    },
    /**
     *  Loads/reloads a data url, triggering registered callbacks as required
     *  Checks if the url is currently loading, and refuses to load if currently loading
     *  @threadsafe 
     *  @param {String} url
     */
    load: function( url ) {
        if( this.logging ) { console.log(this.klass+":load(",url,") - callbacks: ", this.callbacks, ", this: ", this); }
        if( !this.loading[url] ) { 
            this.loading[url] = true;
            $.ajax({
                url: url,
                type: "GET",
                ifModified: !this.loaded[url], // check we have the data in cache before doing a conditional GET
                success: $.proxy( function( json, status, xhr ) {
                    if( json && xhr.status != 304 ) { // 304 notmodified, also assumes that json="" is an invalid response
                        this.loaded[url] = true;
                        this.cache[url]  = json;
                        this.trigger(url);
                    }
                }, this),
                complete: $.proxy( function() {
                    this.loading[url] = false;
                    this.startAjaxPoll(url);
                }, this)
            });    
        }
    },
    startAjaxPoll: function(url) {
        var self = this;
        if( this.interval[url] ) {
            this.stopAjaxPoll(url);
            this._ajaxPollSemaphore[url] = setTimeout( function() {
                self.load(url);                
            }, this.interval[url] );
        }
    },
    stopAjaxPoll: function(url) {
        if( this._ajaxPollSemaphore[url] ) {
            clearTimeout( this._ajaxPollSemaphore[url] );
        }
        this._ajaxPollSemaphore[url] = null;
    },

    /**
     *  Triggers registered callbacks for a given URL
     *  @param {String} url
     */
    trigger: function(url) {
        if( this.callbacks[url] instanceof Array ) {
            for( var i=0, n=this.callbacks[url].length; i<n; i++ ) {
                var callback = this.callbacks[url][i];
                if( callback instanceof Function ) {
                    callback(this.cache[url], url);
                }
            }
        }
    },
    /**
     *  Clears the cache for a given URL
     *  @param {String} url
     */
    clear: function(url) {
        this.loaded[url] = false;
        delete this.cache[url];
    }
});
/**
 *  Initializes miniwidgets marked by html lookups, but without the whole UI widgets overhead
 *  NOTE: This is init code, any uncaught exceptions here will kill all the javascript on the page
 *  @param  {jQuery} rootNode   rootNode to search from, includes self
 *  @return {jQuery}            list of nodes marked as widget
 */
$.initMiniWidgets = function( rootNode ) {
    try {
        $('input.auto-clear', rootNode).each(function() {
            var def = this.value;
            this.onfocus = function() {
                if( this.value == def ) {
                    this.value = "";
                    this.style.color = "#333";
                }
            };
            this.onblur = function() {
                if( this.value == "" ) {
                    this.value = def;
                    this.style.color = "#999";
                }
            };
        });
    } catch( e ) {
        console.error("$.initMiniWidgets(): exception: $(input.auto-clear,",rootNode,"): " );
        console.dir(e);
    }



    try {
        $('a.ext,a[rel=external],a[href^=http]', rootNode).each(function() {
            if(!( this.getAttribute("href").match( $.getTopLevelDomain() ) )) {
                // rel="external" doesn't actually trigger a new window, we need target="_blank" for that
                this.setAttribute("rel", String(this.getAttribute("rel")).replace(/external|null/, '') + " external" );
                this.setAttribute("target","_blank");
            }
        });
    } catch( e ) {
        console.error("$.initMiniWidgets(): exception: $('a.ext,a[rel=external],a[href^=http]', rootNode).each(function() {");
        console.dir(e);
    }

};
setTimeout(function() {
    try {
        var hideUntilLoad = $(".galleryView, .imageandvideocarousel").filter(function() { return ($(this).css("visibility") === "visible"); } );
        hideUntilLoad.css("visibility", "hidden");
        $(document).ready(function() { hideUntilLoad.css("visibility", "visible"); });
    } catch( e ) {
        console.error("hideUntilLoad(): exception: $('.galleryView, .imageandvideocarousel')" );
        console.dir(e);
    }
},0);
/**
 *  This is the base widget for common functionality between all widgets
 */
$.ui.widget.subclass('ui.basewidget', {
    klass: "$.ui.basewidget",
    options: {                   // {Hash} auto-extended - provides defaults for html expando properties
    },
    required: {                  // {Hash} Validation rules for options
    },
    requiredElements: {          // {Hash} Validation rules for this.el lookups, first line of _init()
    },
    el: null,                    // {Hash} namespace for all jQuery references
    data: null,                  // {Hash} storage for parseHtmlTable() - not called by default, except in svgWidget

    /**
     *  _create(), _init() and destroy() are automattically called before there subclass counterparts
     */
    _create: function() {
        this.el = {};
        this.data = {};
        this.options = $.getAttributeHash( this.element, this.options );
        this.validateOptions();

        this.element.data("widget", this);
    },
    _init: function() {
        this.validateRequiredElements();
    },

    _destroyed: false,
    destroy: function() {
        if( this._destroyed ) {
            return;
        } else {
            this._destroyed = true;
        }

        this.unregisterEventManager();

        for( var key in this.el ) {
            if( this.el[key] instanceof jQuery ) {
                this.el[key].prevObject = undefined;
                this.el[key] = $.ui.basewidget.emptyjQuery;
            } else if( this.el[key] instanceof Array ) {
                this.el[key] = [];
            } else {
                this.el[key] = {};
            }
        }
        this.element.data("widget", null);
        this.element.prevObject = undefined;
        this.element = $.ui.basewidget.emptyjQuery;
        this.options = {};
    },

    validateOptions: function() {
        // Validate widget
        for( var field in this.required ) {

            // Values
            if( typeof this.required[field] === "boolean" ) {
                if( this.required[field] === true ) {
                    if( !this.options[field] && this.options[field] !== 0 ) {
                        console.error( this.klass, ":_init(): this.options.", field, ": ", this.options[field], " must be defined - this.options: ", this.options, " - this: ", this );
                    }
                }
            }
            // Class literals
            else if( this.required[field] === Array || this.required[field] === RegExp ) {
                if( !(this.options[field] instanceof this.required[field]) ) {
                    console.error( this.klass, ":_init(): this.options.", field, ": ", this.options[field], " must be of type ", this.required[field], " - this.options: ", this.options, " - this: ", this );
                }
            }
            else if( this.required[field] === Number || this.required[field] === String || this.required[field] === Object || this.required[field] === Boolean ) {
                var type = "";
                switch( this.required[field] ) {
                    case Number:  type = "number";  break;
                    case String:  type = "string";  break;
                    case Object:  type = "object";  break;
                    case Boolean: type = "boolean"; break;
                    default:      type = "";        break;
                }
                if( typeof this.options[field] !== type ) {
                    console.error( this.klass, ":_init(): this.options.", field, ": ", this.options[field], " must be of type ", this.required[field], " - this.options: ", this.options, " - this: ", this );
                }
            }

            // Explicit Options
            else if( this.required[field] instanceof Array ) {
                var isValid = false;
                for( var i=0, n=this.required[field].length; i<n; i++ ) {
                    if( this.options[field] === this.required[field][i] ) {
                        isValid = true;
                        break;
                    }
                }
                if( !isValid ) {
                    console.error( this.klass, ":_init(): this.options.", field, ": ", this.options[field], " must be one of: ", this.required[field], " - this.options: ", this.options, " - this: ", this );
                }
            }

            // Functions
            else if( this.required[field] instanceof RegExp ) {
                if( String.match( String(this.options[field]), this.required[field]) === null ) { // This works on numbers too
                    console.error( this.klass, ":_init(): this.options.", field, ": ", this.options[field], " must match ", this.required[field].toString(), " - this.options: ", this.options, " - this: ", this );
                }
            }
            else if( this.required[field] instanceof Function ) {
                if( !this.required[field]( this.options[field] ) ) {
                    console.error( this.klass, ":_init(): this.options.", field, ": ", this.options[field], " must match ", this.required[field].toString(), " - this.options: ", this.options, " - this: ", this );
                }
            }

            else {
                console.error( this.klass, ":_init(): this.required.", field, ": ", this.required[field], " is invalid - this: ", this );
            }
        }
    },
    validateRequiredElements: function() {
        if( this.requiredElements ) {
            for( var field in this.requiredElements ) {
                var node = this.el[field];
                var condition = this.requiredElements[field];

                if( !node ) {
                    console.error(this.klass+":validateRequiredElements() - this.el."+field+" was not defined: ", this.el[field], " - this: ", this);               
                }
                else if( !node.jquery ) {
                    console.error(this.klass+":validateRequiredElements() - this.el."+field+" is not of type jQuery: ", this.el[field], " - this: ", this);               
                }
                else if( condition === true ) {
                    if( node.length === 0 ) { 
                        console.error(this.klass+":validateRequiredElements() - this.el."+field+" matches no elements: ", this.el[field], " - this: ", this);               
                    }
                } 
                else if( typeof condition === "number" ) {
                    if( node.length === condition ) { 
                        console.error(this.klass+":validateRequiredElements() - this.el."+field+" must match exactly ", condition ," this: ", this.el[field], this);               
                    }
                }
            }
        }
    },

    /**
     *  Parses a HTML table
     *  @param  {jQuery} table  table to parse
     *  @return {Hash} data
     *                 data.values   // {Hash<Row|Col><Col|Row>} = {Number}
     *                 data.rows     // {Hash<Col>} = {Hash<attribute>}
     *                 data.cols     // {Hash<Row>} = {Hash<attribute>}
     *                 data.totals   // {Hash<Row|Col>} = {Number}
     *                 data.colNames // {Array} = Col
     *                 data.rowNames // {Array} = Row
     *                 data.label    // {String}
     */
    parseHtmlTable: function( table ) {
        if( !this.table ) { this.getTableNode(); }


        var data = {};
        data.values  = {};  // {Hash<Row|Col><Col|Row>} = {Number}
        data.strings = {};  // {Hash<Row|Col><Col|Row>} = {String}
        data.rows = {};     // {Hash<Col>} = {Hash<attribute>}
        data.cols = {};     // {Hash<Row>} = {Hash<attribute>}
        data.cells = {};    // {Hash<Row><Col>} = {Hash<attribute>}
        data.totals = {};   // {Hash<Row|Col>} = {Number}
        data.colNames = []; // {Array} = Col
        data.rowNames = []; // {Array} = Row
        data.stats = {
            max:   Number.MIN_VALUE, // Datasets will not always include zero in their range
            min:   Number.MAX_VALUE,
            count: 0,
            avg:   0,
            total: 0
        };

        data.label = this.element.find(".label").text()
                  || this.table.find("thead .label").text()
                  || this.element.children().first().text();

        var cols = this.table.find("thead th").not(":first-child, .ignore");
        var rows = this.table.find("tbody tr").not(".ignore");

        for( var i=0, n=cols.length; i<n; i++ ) {
            var colName = this.getColName( cols[i] ); // Allow HTML override
            var colData = $.getAttributeHash( cols[i], { name: colName, index: i, node: cols[i] } );
            data.cols[colName] = colData;
            data.colNames.push( colName );
        }

        for( var i=0, n=rows.length; i<n; i++ ) {
            var rowName = this.getRowName( rows[i] );
            var rowData = $.getAttributeHash( rows[i], { name: rowName, index: i, node: rows[i] } );
            data.rows[rowName] = rowData;

            if( this.element.attr("nodeName") === "TR" && this.element[0] !== rows[i] ) {
                // Don't add to data.rowNames
                $.noop();
            } else {
                data.rowNames.push( rowName );
            }

            var cells = $(rows[i]).find("td");
            for( var j=0, m=cells.length; j<m; j++ ) {
                var cellString = this.getCellString(cells[j]);
                var cellValue  = this.getCellValue(cells[j], cellString);
                var colName    = data.colNames[j];

                // TODO: Is doing [rowName][colName] then [colName][rowName] going to lead to subtle bugs with duplicate row/col names
                data.values[rowName] = data.values[rowName] || {};
                data.values[colName] = data.values[colName] || {};
                data.values[rowName][colName] = cellValue;
                data.values[colName][rowName] = cellValue;

                data.strings[rowName] = data.strings[rowName] || {};
                data.strings[colName] = data.strings[colName] || {};
                data.strings[rowName][colName] = cellString;
                data.strings[colName][rowName] = cellString;

                data.totals[rowName] = Number(data.totals[rowName] || 0) + cellValue; // Row/Team Totals
                data.totals[colName] = Number(data.totals[colName] || 0) + cellValue; // Column Totals

                data.stats.count++;
                data.stats.total += cellValue;
                data.stats.max = Math.max( cellValue, data.stats.max );
                data.stats.min = Math.min( cellValue, data.stats.min );
            }
        }
        data.stats.avg = data.stats.count === 0 ? 0 : data.stats.total / data.stats.count;

        for( var rowName in data.rows ) {
            var cells = $(data.rows[rowName].node).find("td").not(".ignore");
            for( var i=0, n=cells.length; i<n; i++ ) {
                var colName = data.colNames[i];

                data.cells[rowName] = data.cells[rowName] || {};
                data.cells[colName] = data.cells[colName] || {};
                data.cells[rowName][colName] = $.getAttributeHash( cells[i], { colName: colName, rowName: rowName, node: cells[i] } );
                data.cells[colName][rowName] = $.getAttributeHash( cells[i], { colName: colName, rowName: rowName, node: cells[i] } );
            }
        }

        // Check for row/column namespace collisions
        for( var i=0, n=data.rowNames.length; i<n; i++ ) {
            var rowName = data.rowNames[i];
            for( var j=0, m=data.colNames.length; j<m; j++ ) {
                var colName = data.colNames[j];
                if( rowName === colName ) {
                    console.error(this.klass+":parseHtmlTable(): row/column namespace collision - rowName: ", rowName, " colName: ", colName, " this.element: ", this.element, ", this: ", this );
                }
            }
        }
        
        // Final Validation
        if( data.stats.max === Number.MIN_VALUE ) { 
            data.stats.max = 0; 
            //console.error(this.klass+":parseHtmlTable() - invalid data set, data.stats.max === Number.MIN_VALUE", this); 
        }
        if( data.stats.min === Number.MAX_VALUE ) { 
            data.stats.min = 0; 
            //console.error(this.klass+":parseHtmlTable() - invalid data set, data.stats.min === Number.MAX_VALUE", this); 
        }
        
        return data;
    },
    getColName: function( colNode ) {
        colNode = $(colNode);
        var colName = $.trim( colNode.attr("name") || colNode.text() || colNode.getUuid() );
        return colName;
    },
    getRowName: function( rowNode ) {
        rowNode = $(rowNode);
        var rowName = $.trim( rowNode.attr("name") || rowNode.find("th").text() || rowNode.getUuid() );
        return rowName;
    },
    getCellString: function( cellNode ) {
        var cellString = $.trim( $(cellNode).text() ) || "";
        return cellString;
    },
    getCellValue: function( cellNode, cellString ) {
        cellString = cellString || this.getCellString(cellNode) || ""; // Optimization, avoid second DOM query
        var cellValue = Number( cellString.replace(/[^\d\.+-]/g, '') );
        return cellValue;
    },


    /**
     *  Finds the relevant table node for the widget
     *  First searches this.element, then up the tree, then down the tree
     *  @return {jQuery}
     */
    getTableNode: function() {
        if( !this.table ) {
            this.table = this.element;
        }
        if( this.table.attr("nodeName") !== "table" ) {
            this.table = this.element.find("table").first();
        }
        if( this.table.attr("nodeName") !== "table" ) {
            this.table = this.element.closest("table").first();
        }
        return this.table;
    },
    /**
     * @return {Number}
     */
    getFontSize: function() {
        if(! this.options.fontSize ) {
            this.options.fontSize = this.options.fontSize || Number( $(this.element).css("font-size").replace(/[^\d\.]+/g,'') );
        }
        return this.options.fontSize;
    },



    //*** Event Manager Interface ***//

    registerEventManager: function() {
        // Override in subclasses
        $.noop();
    },
    unregisterEventManager: function() {
        EventManager.unregister(this);
    }

});
$.ui.basewidget.emptyjQuery = $([]);
/**
 *  tooltip widget wrapper class for
 *  js/libs/jquery.tooltip.min.js
 *  
 *  http://flowplayer.org/tools/demos/tooltip/
 */ 

$.ui.widget.subclass('ui.addTooltip', {
    klass: '$.ui.addTooltip',
    options: {
        hash:          		null,		// {Hash}         define objects and arrays within the constructor, else it will create a class variable
        predelay:         	   0,		// {Timestamp}    Hover delay in ms to tooltip is shown
        effect:		 	  'fade',		// {String}	  	  Fade duration
        position:	'top center',		// {String}		  px
        opacity:        	   0.9,		// {int}		  px
        type:		     	  '',		// {String}		  Apply class type
        classes:			  '',
        offsets:			  []
    },

    // Called from constructor before _init() – automatically calls this._super() before function
   _create: function() { 
        this.options.hash = {};
        this.options = $.getAttributeHash( this.element, this.options );
        this.options.classes += ' ' + this.element.attr('class');
    },

    // Called from constructor after _create() – automatically calls this._super() before function
    _init: function() {
        this.doSomething('tooltip init' );
        this.addTooltip();
    },

    // Not called from constructor – need to call this._super(arg) manually if required
    doSomething: function( arg ) {
        //console.log( arg, this.element.text(), this.options, this );
    },
    
    addTooltip: function() {
    	if(this.options.type == 'index') {
    		this.offsets = [12,0];
    	}
    	this.classes = 'tooltip ' + this.options.type + ' ' + this.options.classes;
    	
    	$(this.element).tooltip({
    		predelay: 	this.options.predelay,
    		effect:  	this.options.effect,
    		position:   this.options.position,
    		opacity: 	this.options.opacity,
    		offset:		this.offsets,
    		tipClass:   this.classes
    	});
    	
    }
    
});
/**
 *  We can either call this on a <form> element, and autosubmit for any field
 *  Or we call this on individual inputs, and only autosubmit when these change 
 *
 *  The submit button will only be hidden if it is inside the <widget="autosubmit"> tag
 */
$.ui.widget.subclass('ui.autosubmit', {
    klass: '$.ui.autosubmit',
    options: {
        hideSubmit: true
    },
    _create: function() {
        this.el = {};

        this.el.form = this.element.findAndSelf("form");
        if( this.el.form.length === 0 ) { 
            this.el.form = this.element.closest("form"); 
        }

        // Note: search from this.element, not this.el.form
        this.el.submit = this.element.find("input[type=submit]");
        this.el.inputs = this.element.findAndSelf("input,select,textarea,button").not(this.el.submit);

        this.onChange = $.proxy(this.onChange, this);
    },
    _init: function() {
        if( this.options.hideSubmit ) {
            this.el.submit.hide();
        }
        this.el.inputs.bind("change", this.onChange);
    },
    onChange: function() {
        this.el.form.submit();
    }
});
/**
 *  <input type="checkbox" widget="onEvent" event="onCheck" target="#weeks" addClass="selected" removeClass="unselected"/>
 * 
 *  <select widget="onEvent" event="onChange" target="#weeks">
 *  	<option updateSelect="[01,02,03,04]">2011-2012</option>
 *  </select>
 *  <select id="weeks"></select>
 *  
 *  
 *  Define event and target, all other options are commands to perform upon event
 */
$.ui.basewidget.subclass('ui.onEvent', {
    klass: '$.ui.onEvent',
    options: {
        event:        null,  // {String} one of: onCheck, onChange
        target:       null,  // {Selector}
        addClass:     null,  // {String}
        removeClass:  null,  // {String}
        updateSelect: null   // {Array|Hash} "[01,02,03,04]" or { "01": "Week 1", "02": "Week 2" }
        // TODO: add other commands as required
    },
    required: {
        event:  String,
        target: String
    },
    _create: function() {
        this.target = $(this.options.target);
        this.onEvent = $.proxy( this.onEvent, this );
        this.onCheck = $.proxy( this.onCheck, this );
        this.onChange = $.proxy( this.onChange, this );
        this.events = this.options.event.split(",");

        if( this.target.length === 0 ) {
            console.error(this.klass+":_init(): invalid target: ", this.target, this.options.target, " this: ", this);
        }
    },
    _init: function() {
        for( var i=0, n=this.events.length; i<n; i++ ) {
            switch( this.events[i] ) {
                case "onCheck":
                    this.element.bind("change", this.onCheck );
                    this.onCheck();
                    break;
                case "onChange":
                	this.element.bind("change", this.onChange );
                    this.onChange();
                    break;
                default:
                    console.error(this.klass+":_init(): invalid event: ", this.events[i], " this: ", this);
            }
        }

        this.element.bind( this.options.event, this.onEvent );
    },
    destroy: function() {
        this.element.unbind( this.options.event, this.onEvent );
    },

    onCheck: function() {
        if( this.element.attr("checked") === "checked" ) {
            this.onEvent();
        }
    },
    onChange: function() {
    	this.onEvent();
    },

    onEvent: function() {
    	// Take parameters from selected option if appropriate
    	var options = this.options;
    	if( this.element.is("select") ) {
    		options = $.getAttributeHash( this.element.find("option:selected"), this.options );
    	}
    	
    	// Handle Events
        if( options.addClass ) {
            this.target.addClass( options.addClass );
        }
        if( options.removeClass ) {
            this.target.removeClass( options.removeClass );
        }
        if( options.updateSelect ) {
        	this.updateSelect(options);
        }
    },
    updateSelect: function( options ) {
    	if( this.target.is("select") ) {
    		this.target.find("option").remove();
    		if( options.updateSelect instanceof Array ) {
    			for( var i=0; i<options.updateSelect.length; i++ ) {
    				var value = options.updateSelect[i];
    				this.target.append("<option>"+value+"</option>");
    			}
    		} else if( typeof options.updateSelect === "object" ) {
    			for( var key in options.updateSelect ) {
    				var value = options.updateSelect[key];
    				this.target.append("<option value='"+key+"'>"+value+"</option>");
    			};
    		} else {
    			console.warn(this.klass+":updateSelect() - this.updateSelect is not of type Array or Object: ", this.updateSelect, " - this: ", this  );
    		}
    	} else {
    		console.warn(this.klass+":updateSelect() - this.target: ", this.target, " is not a <select> - this: ", this  );
    	}
    }
});
$.ui.basewidget.subclass('ui.teamofweek', {
    klass: '$.ui.teamofweek',
    // TODO: Ajax Prefix not passing through
    options: {
        teamNodes:      ["gk","de1","de2","de3","de4","de5","mf1","mf2","mf3","mf4","mf5","st1","st2","st3"],
        //towTab:         "",
        ajaxPrefix:     "",
        ajaxPostfix:    "_jcr_content.infinity.json",
        targetSelector: ".target"
    },
    _create: function() {
        this.el = {};
        
        this.el.form       = this.element.findAndSelf("form");
        this.el.target     = this.element.find(this.options.targetSelector);
        this.el.season     = this.el.form.find("select[name=season]");
        this.el.week       = this.el.form.find("select[name=week]");
        this.el.ajaxPrefix = this.el.form.find("input[name=ajaxPrefix]");
    },
    _init: function() {
        //this.el.inputs.bind("change", $.proxy(this.onChange, this) );
        //if( this.options.hideSubmit ) {
        //    this.el.submit.hide();
        //}
        
        var self = this;
        this.el.form.bind("submit", function() {
            self.onSubmit();
            return false;
        });
        this.onSubmit();
    },
    getAjaxUrl: function() {
        var ajaxUrl = this.options.ajaxPrefix
            //+ "/" + this.options.towTab
            + "/" + this.el.season.val()
            + "/" + this.el.week.val()
            + "/" + this.options.ajaxPostfix;
        
        //ajaxUrl = ajaxUrl.replace("/"+this.options.towTab+"/"+this.options.towTab, "/"+this.options.towTab );
        //ajaxUrl = ajaxUrl.replace("\/\/","/");
        
        return ajaxUrl; 
    },
    onSubmit: function() {
        var self = this;
        this.renderSpinner();
        $.ajax({
            url:      this.getAjaxUrl(),
            type:     "GET",
            dataType: "text",
            success: function( data ){
                self.renderData( data );
            }
        });     
    },    
    renderSpinner: function() {
        if( this.el.target.is(":empty") ) {
            this.el.target.html("<div class='spinner'><span>Loading...</span></div>");
        }
    },
    renderData: function(data) {
        var JSON = (jQuery.parseJSON(data));
        //var append = $.el("div").addClass("team-wrapper");
        //var thisSpan = $.el("span").addClass("key");
        //thisSpan.append("<p>Latest points 63 &nbsp; Index rank (116)</p>");
        //append.append(thisSpan);
        
        // $(teamNodes).each(function(i,v){
        /*
        var html = "<div class='team-wrapper'>";
            + "<span class='key'>"
            + "<p>Latest points 63 &nbsp; Index rank (116)</p>"
            + "</span>";
        
        */
        //CHECK TO SEE WHICH TAB IS BEING RENDER - BECAUSE FPL DOES NOT HAVE A RANK
        whichTab = this.options.ajaxPrefix;
        whichTab = whichTab.substring(whichTab.length-3,whichTab.length);

        var html = '';
        rowMemberCount=0;   
        lastNode='';
        htmlHolder='';
        
        for(i=0; i<this.options.teamNodes.length; i++ ) {
            var teamNode = this.options.teamNodes[i];
            var currentNodeRow = teamNode.substring(0, 2);
            if (currentNodeRow==lastNode) {
            	// Nothing
            } else if(html != ""){
                //new row
                htmlHolder 	  += '<div class="rows_' + rowMemberCount + '">'+html+'</div>';
                html 		   = '';
                rowMemberCount =0;
            }

            var player = JSON[teamNode]; 
            
            if (player && player.player != "N/D" && player.player != '') {
                html += "<div class='"+teamNode+"'>";
                if(player.image.fileReference){
                    html += '<img src="'+player.image.fileReference+'" alt="" />';
                }
                else{
                    html += '<img src="'+player.kitref+'" alt="" />';
                }
                if (player.player) {
                    html += "<p>"+player.player+"</p>";
                }
                if (player.score){
                    html += "<p>"+player.score;
                }
                if (player.rank && whichTab != 'fpl'){
                    html +=" ("+player.rank+")</p>";
                }
                if (player.score || (player.rank && whichTab != 'fpl')) {
                	html +="</p>";
                }
                html +="</div>";
                rowMemberCount++;
            }
            //do the first record
            if (i==0) {
                //new row
                htmlHolder += '<div class="rows_' + rowMemberCount + '">'+html+'</div>';
                html = '';
                rowMemberCount=0;
            }
            //do the last row
            if (i==this.options.teamNodes.length-1) {
                htmlHolder += '<div class="rows_' + rowMemberCount + '">'+html+'</div>';
            }
            lastNode = currentNodeRow;
        };
        html += "</div>";
        var tempHtmlHolderForRender = "<div class='team-wrapper'></span>"+htmlHolder+"</div>";    
        this.el.target.empty().html(tempHtmlHolderForRender);
    }
});
// Requires libs/jquery-custom/jquery.extensions.js
(function($) {
	/**
	 * topnav event model and construction
	 */
	$.fn.topnav = function (options) {
		if (this.length === 0) {return this;} // Quit early on empty selector
		
		var opts = $.extend({}, $.fn.topnav.defaults, options);
				
		var buildNav = function($el,urlRoot) {
			var navData = $el.data();
			var nu = $el.closest('li').find('.sub');
			nu.empty();
			
			for (var key in navData) {
				//console.log('jquery?',key);
				if (navData.hasOwnProperty(key) && key.indexOf("jcr:") < 0 && key.indexOf("jQuery") < 0 ) {
					//console.log(key,navData[key]);
					var li = $.el('li');
					var la = $.el('a').attr( {'href':urlRoot+'/'+key+'.html'}).text(navData[key]['jcr:content']['jcr:title']);
					nu.append(li.html(la));
				}
			}

			$el.after(nu);
			$el.data({'loaded':true});
		};
		
		var showNav = function($t){
			
			var my$li = $t.closest('li');
			
			if (my$li.hasClass('active')) {
				// do nothing
			} else {
				$('.active','#navigation').removeClass('active');
				$('.sub','#navigation').fadeOut('fast');
				my$li.addClass('active').find('.sub').show();
			}
			
		};
		
		return this.each(function(){
			
			var $t = $(this); 
			var init = function() {
				
				/*var img = $.el('img').attr({
                    src:'images/throbber.gif',
                    width:'16',
                    height:'16'
                });*/
                var li = $.el('li').html("loading");
				var nu = $.el('ul').addClass('sub').html(li);
				
				$t.after(nu);
				//alert("in init")
			}();
			
			$t.click(function(){
				
				th = this.href;
				
				if ($t.data("loaded") !== true) {
					// get the nav data
					$.ajax({
						url:$.fn.topnav.sourceJSON(this.href),
						success: function(data){		
						  $t.data(data);
						  buildNav($t,th.split('.')[0]);
						},
						error:function(){}
					});
				}
				showNav($t);
				return false;
			});
		});
	};

	/**
	 * Define how to locate our source for the nav data
	 */
	$.fn.topnav.sourceJSON = function(href) {
	    var hrp = href.split('.');
	    var jsh = hrp[0]+".2.json";
		return jsh;
	};
	/**
	 * Define topnav defaults
	 */
	$.fn.topnav.defaults = {};
	
})(jQuery);
// Import subclass function
$.ui.tabs.subclass = $.ui.widget.subclass;

/**
 *  
 *  @attr  <root><ul><li><a ajax="">       explicit url to load via ajax, overrides options.ajaxRewrite
 *  @param {Function} options.ajaxRewrite  a function to convert the href url into an ajax url, default function adds .tab before .html
 */
$.ui.tabs.subclass('ui.pagetabs', {
    klass: "$.ui.tabs",
    options: {
		selectedHref: '',   // {String} [optional] load tab with selectedHref on load  
        ajaxOptions: {
            dataType: "html"
        },
        ajaxRewrite: function(url) { 
            return url && url.replace(/(\.tab)*\.html/, '.tab.html'); 
        }
    },
    _tabify: function(init) {
        this._super(init);
        
    	// _create() and _init() don't get called within ui.tabs for some reason 
        this.options = $.getAttributeHash( this.element, this.options );
        this.element.data("widget", this);
		
        // Auto-append the anchor tag to the browser location bar
        this.anchors.bind( "click.tabs", function(event) {
            var hash = this.getAttribute("href");
            $.setDocumentHash(hash); // Prevents page from scrolling
		});
        
        // Load up selectedHref if defined 
        if( this.options.selectedHref ) {
        	var selectedTab = this.anchors.filter('[href='+this.options.selectedHref+']');
        	if( selectedTab.length ) {
        		selectedTab.click();
        	} else {
        		console.warn(this.klass+":_tabify(): invalid options.selectedHref: ", this.options.selectedHref, ' - this: ', this);
        	}
        }
    },
    load: function( index ) {
        var i = this._getIndex( index );
		var a = this.anchors.eq(i)[0];

        if( !$.data(a, "load.original") ) {
            $.data(a, "load.original", $.data(a, "load.tabs")); // save original, also acts as onetime flag
            $.data(a, "load.tabs", this.options.ajaxRewrite( $.data(a, "load.tabs") ));
        }

        // Read <a ajax=""> attribute, overrides this.options.ajaxRewrite
        if( a.getAttribute("ajax") ) {
            $.data(a, "load.tabs", a.getAttribute("ajax"));
        }

        this._super(index);
    }
});
$.ui.basewidget.subclass('ui.ticker', {
    klass: "$.ui.ticker",
    options: {
        visible:         9,          // {Number}      maximum number of ticker items visable as per design
        position:        0,          // {Number}      start position, counting from the right hand side
        ajax:            "/ajax/site-header.json",                      // {String}      url to poll for updates to matchday items
        ajaxFixtures:    "/ajax/site-header/ajax-all-fixtures.json",
        ajaxResults:     "/ajax/site-header/ajax-all-results.json",
        ajaxAll:         "/ajax/site-header/ajax-all-matches.json",
        ajaxLiveTvUrl:   "/content/premierleague-ajax/livetvbymatch.ajax",
        ajaxInterval:    60000,      // {Number}      ms for ajax poll frequency
        scrollBuffer:    5,          // {Number}      how close to the edge can you scroll before loading the full data set
        filter:          "*"         // {String}      [optional] "*", ".PRE_MATCH", ".POST_MATCH"
    },
    required: {
        visible:  Number,
        position: Number,
        ajax:     String,
        filter:   String
    },
    items:               null,    // {Hash<jQuery>}    jQuery references to ticker tape items
    _ajaxResultsLoaded:  false,   // {Boolean}         Have the full set of results been loaded
    _ajaxFixturesLoaded: false,   // {Boolean}         Have the full set of results been loaded

    //***** Init *****//

    _create: function() {
        this.matchIndex = {};
        this.oldData    = {};

        this.el.ul             = this.element.find(".ticker-tape ul");
        this.el.nextArrow      = this.element.find(".ticker-arrow-next");
        this.el.prevArrow      = this.element.find(".ticker-arrow-prev");
        this.el.template       = this.element.find(".template");
        this.el.filters        = this.element.find(".ticker-filter li");

        this.onlyPostMatchFlag = this.el.ul.hasClass("ONLY_POST_MATCH");
        this.position = Number(this.options.position);

        this.registerAjaxPoller( this.options.ajax );
        this.registerEventManager();
        this.addEventListeners();

        this.renderLiveTV = $.proxy( this.renderLiveTV, this ); // $.proxy so we have a unique function to pass to DataCache
        DataCache.register( this.options.ajaxLiveTvUrl, this.renderLiveTV );
    },
    /**
     *  This function is called on init, and after the ticker is scrolled
     */
    _init: function() {
        this.lookupItems();
        this.setPosition(); // Uses this.position by default
        this.onlyPostMatchFix();

        DataCache.lazyLoad( this.options.ajaxLiveTvUrl, this.renderLiveTV );
    },
    destroy: function() {
        this.unregisterAjaxPoller();
        this._super();
    },


    /**
     *  This is a failsafe for any bugs causing jsp exceptions inside .template
     *  We lose the ability to dynamically update the ticker, but we ensure we don't blat it with a JSP exception
     */
    _isTemplateValidAnswer: null,
    isTemplateValid: function() {
        if( this._isTemplateValidAnswer === null ) {
            var isValid  = false;
            var html     = "";
            var template = null;
            try {
                html = this.el.template.html();
                template = $(html);
                if( template && template.length === 1 && template[0].nodeName === "LI" ) {
                    isValid = true;
                }
                template.remove();
            } catch( e ) {
            }
            if( isValid === false ) {
                console.error(this.klass+":isValidTemplate(): invalid template:" + this.el.template, html, " - this: ", this );
            }
            this._isTemplateValidAnswer = isValid;
        } 
        return this._isTemplateValidAnswer;
    },

    lookupItemsInit: function() {
        if(!( this.items && this.items.loaded )) {
            this.lookupItems();
        }
    },
    lookupItems: function() {
        $.jQueryGC( this.items );
        this.items = {};

        this.items.all        = this.element.find(".ticker-tape ul li").not(".template");
        this.items.filtered   = this.items.all.filter(this.options.filter);
        this.items.POST_MATCH = this.items.all.filter(".POST_MATCH").not(".SAME_MATCH_DAY");
        this.items.LIVE       = this.items.all.filter(".LIVE,.SAME_MATCH_DAY");
        this.items.PRE_MATCH  = this.items.all.filter(".PRE_MATCH");

        // Cached node lookup - we want polling to be quick - allows for duplicate IDs
        this.items.id = {};
        for( var i=0, n=this.items.all.length; i<n; i++ ) {
            var matchId = this.items.all[i].getAttribute("matchId");
            if( this.items.id[matchId] ) {
                //console.warn(this.klass+":lookupItems(): duplicate matchId: ", matchId, ' nodes: ', this.items.id[matchId], " - this: ", this);
                this.items.id[matchId].remove(); // Sometimes we get duplicate Ids when PA send test data, keep latest match
                this.items.id[matchId] = this.items.id[matchId].add( this.items.all[i] );
            } else {
                this.items.id[matchId] = $(this.items.all[i]);
            }
        }
        this.items.loaded = true;
    },
    removeDuplicateMatchIds: function() {
        for( var matchId in this.items.id ) {
            if( this.items.id[matchId].length >= 2 ) {
                var last = this.items.id[matchId].last();
                this.items.id[matchId].not(last).remove();
            }
        }
    },

    addEventListeners: function() {
        // Scroll the list right, to the future
        this.el.nextArrow.bind("click", { widget: this }, function(event, ui) {
            if( $(this).hasClass("disabled") ) { return; }
            event.data.widget.moveNext.call( event.data.widget );
        });

        // Scroll the list left, to the past
        this.el.prevArrow.bind("click", { widget: this }, function(event, ui) {
            if( $(this).hasClass("disabled") ) { return; }
            event.data.widget.movePrev.call( event.data.widget );
        });

        this.el.filters.bind("click", { widget: this }, function(event, ui) {
            if( $(this).hasClass("selected") ) { return; }

            event.data.widget.setFilter( this.getAttribute("filter") );
            event.data.widget.el.filters.removeClass("selected");
            $(this).addClass("selected");
        });
        this.el.filters.css("cursor", "pointer");


        // Nasty hardcodedness for links to ticker icons, nested <a href=""> are illegal
        this.element.find(".ticker-tape").bind("click", function(event) {
            var target = $(event.target);
            var li     = target.closest("li");
            var href   = target.closest("a").attr("href") || "";
            var icon   = target.closest(".ticker-broadcaster, .ticker-icons");

            if( href && icon.length ) {
                if(!( href.match(/\.html/) )) {
                    console.log('DEBUG: ', this&&this.klass||'' ,' ticker link doesn\'t contain .html');
                }

                // Broadcaster icon - has child image
                if( icon.hasClass("ticker-broadcaster") && icon.find("img").length ) {
                    event.preventDefault();
                    document.location = href.replace(/^(.*)\.html/, '$1.tv-radio.html');
                    return false;
                }

                // Tickets icon - has TX class on li
                // We have no way of determining if we clicked on the icon or the html block element
                if( icon.hasClass("ticker-icons") && li.hasClass("TX") ) {
                    event.preventDefault();
                    document.location = href.replace(/^(.*)\.html/, '$1.tickets.html');
                    return false;
                }
            }

            // else do nothing, allow the default <a href=""> click handler to work its magic
        });
    },
//    addLinkClickHandlers: function() {
//        this.el.links.find("a").bind("click", function(event) {
//            // @context {this.element}
//        });
//    },
    registerEventManager: function() {
        EventManager.register( this, "ticker.moveNext",     this.moveNext );
        EventManager.register( this, "ticker.movePrev",     this.movePrev );
        //EventManager.register( this, "ticker.removeItem",   this.removeItem );
        //EventManager.register( this, "ticker.addItem",      this.addItem );
        EventManager.register( this, "ticker.ajaxPollHandler", this.ajaxPollHandler );
    },
    renderLiveTV: function( json ) {
        this.lookupItemsInit();

        // TODO: If we implement polling, we may need to iterate over the entire set of ticker items,
        //       to remove items that are not in the ajax data set
        var fixtureList = json && json.fixtureList || [];
        for( var i=0, n=fixtureList.length; i<n; i++ ) {
            var fixture = fixtureList[i];
            if( fixture && fixture.matchId && this.hasItem(fixture.matchId) ) {
                if( fixture.channel || fixture.channelImage ) {
                    var altText = (fixture.channel || "").split(/[\W\s]+/).map(function(item){return item.capitalize();}).join(" ");
                    var channelImage = fixture.channelImage ? "<img src='"+fixture.channelImage+"' alt='"+altText+"'/>" : altText;
                } else {
                    var altText      = "";
                    var channelImage = "";
                }
                this.getItem(fixture.matchId).find(".ticker-broadcaster").html(channelImage);

                if( fixture.tickets ) {
                    this.getItem(fixture.matchId).addClass("TX");
                } else {
                    this.getItem(fixture.matchId).removeClass("TX");
                }
            }
        }
    },
    registerAjaxPoller: function( url ) {
        if( !this.isTemplateValid() ) { return; }

        AjaxPoller.register({
            url:          url,
            type:         "GET",
            interval:     this.options.ajaxInterval,
            dataType:     "json",
            handler:      [this, this.ajaxPollHandler]
        });
    },
    unregisterAjaxPoller: function() {
        AjaxPoller.unregister({
            handler:      [this, this.ajaxPollHandler]
        });
    },

    //***** Getters/Setters *****//

    firstItem: function() {
        return this.items.visible.first();
    },
    lastItem: function() {
        return this.items.visible.last();
    },
    nextItems: function() {
        if( this.items.visible.length ) {
            return this.items.visible.last().nextAll( this.options.filter );
        } else {
            return this.items.filtered.last();
        }
    },
    prevItems: function() {
        if( this.items.visible.length ) {
            return this.items.visible.first().prevAll( this.options.filter );
        } else {
            return this.items.filtered.first();
        }
    },

    /**
     * @param  {Number} matchId
     * @return {jQuery}
     */
    getItem: function( matchId ) {
        return this.items.id[ matchId ] || $([]);
    },
    setItem: function( matchId, item ) {
        this.items.id[ matchId ] = item;
    },
    hasItem: function( matchId ) {
        return !!( this.items.id[matchId] && this.items.id[matchId].length );
    },


    /**
     *  Position is defined as offset from first matchday item
     */
    getPositionZero: function() {
        var positionZero = 0;
        switch( this.options.filter ) {
            default:
            case "*":           positionZero = this.items.POST_MATCH.length; break;  // First Live Match on Left - Arrows both ways
            case ".PRE_MATCH":  positionZero = 0; break;                             // First Fixture on Left    - Arrow future
            case ".POST_MATCH": positionZero = this.items.filtered.length; break;    // Last Result on Right     - Arrow past
        }
        return positionZero;
    },
    getPosition: function() {
        return this.position;
    },
    setPosition: function( position ) {
        this.lookupItemsInit();

        if( typeof position === "undefined" ) { position = this.position; }
        var positionZero  = this.getPositionZero();
        var visible       = this.options.visible;

        this.position = Math.max( -positionZero, Math.min( position, this.items.filtered.length - positionZero - visible ));
        this.items.all.hide(); // Premature Optimization: this.items.visible.hide(); instead, possibly buggy
        this.items.visible = this.items.filtered.slice( this.position + positionZero, this.position + positionZero + visible ).show();
        this.updateArrows();
    },

    setFilter: function( filter ) {
        this.lookupItemsInit();

        this.options.filter = filter;
        this.items.filtered = this.items.all.filter( this.options.filter );
        this.setPosition(0);
    },
    updateArrows: function() {
        // Check the arrows have the right state
        if( this.nextItems().length === 0 ) {
            this.el.nextArrow.addClass("disabled");
        } else {
            this.el.nextArrow.removeClass("disabled");
        }

        if( this.prevItems().length === 0 ) {
            this.el.prevArrow.addClass("disabled");
        } else {
            this.el.prevArrow.removeClass("disabled");
        }
    },

    //***** Commands *****//

    moveNext: function() {
        this.setPosition( this.getPosition() + 1 );
        if( !this._ajaxFixturesLoaded && this.nextItems().length < this.options.scrollBuffer ) {
            this.ajaxLoadFixtures();
        }
    },
    movePrev: function() {
        this.setPosition( this.getPosition() - 1 );
        if( !this._ajaxResultsLoaded && this.prevItems().length < this.options.scrollBuffer ) {
            this.ajaxLoadResults();
        }
    },

    /**
     *  @note   Need to run lookupItems() after calling this function
     *  @return {Hash}   matchData  { matchId: 3285269, matchName: "AST v LIV", boxText: "FT", date: "29/02/12", time: "16:00", dateStatus: "fixture", timeStatus: "future" }
     *  @return {jQuery}            updated node
     */
    updateItem: function( itemData ) {
        if( !this.isTemplateValid() ) { return; }

        var matchId = itemData.matchId;
        var node  = this.getItem(matchId);
        var clone = this.el.template.tmpl(itemData).insertAfter(node.last()); // BUG: sometimes we get duplicate match IDs 
        node.remove();

        this.setItem( matchId, this.getItem(matchId).not(node).add(clone) ); // update cache
        return clone;
    },

    //***** Ajax Commands *****//

    ajaxLoadFixtures: function() {
        if( !this.isTemplateValid() ) { return; }

        this._ajaxFixturesLoaded = true;
        $.ajax({
            type: "GET",
            url: this.options.ajaxFixtures,
            success: $.proxy( function(json, xhr, status) {
                // Redraw all Fixtures
                var jsonList = this.reformatMatchData(json); // Index by ID and reformat
                this.items.PRE_MATCH.remove();
                this.el.template.tmpl(jsonList).appendTo( this.el.ul );
                this._init();
            }, this),
            error: $.proxy( function(xhr, status) {
                this._ajaxFixturesLoaded = false;
            }, this)
        });
    },
    ajaxLoadResults: function() {
        if( !this.isTemplateValid() ) { return; }

        this._ajaxResultsLoaded = true;
        $.ajax({
            type: "GET",
            url: this.options.ajaxResults,
            success: $.proxy( function(json, xhr, status) {
                // Redraw all Results
                var jsonList = this.reformatMatchData(json); // Index by ID and reformat
                this.items.POST_MATCH.remove();
                this.el.template.tmpl(jsonList).prependTo( this.el.ul );
                this._init();
            }, this),
            error: $.proxy( function(xhr, status) {
                this._ajaxResultsLoaded = false;
            }, this)
        });
    },
    ajaxLoadAll: function() {
        if( !this.isTemplateValid() ) { return; }

        this._ajaxResultsLoaded  = true;
        this._ajaxFixturesLoaded = true;
        $.ajax({
            type: "GET",
            url: this.options.ajaxAll,
            success: $.proxy( function(json, xhr, status) {
                // Redraw all Results
                var jsonList = this.reformatMatchData(json); // Index by ID and reformat
                this.items.all.remove();
                this.el.template.tmpl(jsonList).prependTo( this.el.ul.empty() );
                this._init();
            }, this),
            error: $.proxy( function(xhr, status) {
                this._ajaxResultsLoaded  = false;
                this._ajaxFixturesLoaded = false;
            }, this)
        });
    },

    /**
     *  Takes an id hash of itemData and adds any newly created nodes
     *  @param {Hash} matchData  { siteHeaderSection: { matches: [ { matchName: "AST v LIV", id: 3285269, type: "FT", date: 1306076400000 }, ... ]}}
     */
    ajaxPollHandler: function( json, status, xhr ) {
        if( !this.isTemplateValid() ) { return; }

        this.lookupItemsInit();

        var jsonList = this.reformatMatchData(json); // Index by ID and reformat
        var jsonHash = $.indexArrayByKey( jsonList, "matchId" ); // same format as this.matchId

        // Assumptions - no new items are created or destroyed
        //             - we only poll for updates on matchDay and surrounding items
        if( this.items.all.length === 0 ) {
            // Nothing got loaded first time, so lets just dump all our JSON onto the page
            this.el.template.tmpl(jsonList).prependTo( this.el.ul.empty() );
        } else {
            for( var matchId in jsonHash ) {
                var matchData = jsonHash[matchId];

                // TODO: should we hash these entries to reduce DOM updates
                if( this.hasItem(matchId) ) {
                    this.updateItem(matchData);
                } else {
                    // NOTE: This may just be a data issue
                    //console.warn(this.klass,':ajaxPollHandler(json,status,xhr) - matchId (', matchId,') not found for matchData: ', matchData, ' in jsonHash: ', jsonHash);
                    $.noop();
                }
            }
        }
        this._init();
    },

    onlyPostMatchFix: function() {
    	// Position is ill defined if the JSP sets the ONLY_POST_MATCH
        if( this.onlyPostMatchFlag && this.items.all.length > this.items.POST_MATCH.length ) {
    		this.el.ul.removeClass("ONLY_POST_MATCH");
    		this.onlyPostMatchFlag = false;
    		this.setPosition(0);
        }
    },


    //***** Data Services *****//

    /**
     * @param  {Hash} json { siteHeaderSection: { matches: [ { matchName: "AST v LIV", id: 3285269, type: "FT", date: 1330552800 }, ... ]}}
     * @return {Array}     [ {matchId: 3285269, matchName: "AST v LIV", boxText: "FT", date: "29/02/12", time: "16:00", dateStatus: "fixture", timeStatus: "future"}, ... ]
     */
    reformatMatchData: function( json ) {
        var matches = [];

        // Keep in sync with: apps/premierleague/components/content/ticker/ticker.jsp
        if( json && json.siteHeaderSection && json.siteHeaderSection.matches instanceof Array ) {
            for( var i=0, n=json.siteHeaderSection.matches.length; i<n; i++ ) {
                matches.push( this.reformatMatchDataEntry(json.siteHeaderSection.matches[i]) );
            }
        } else {
            console.warn( this.klass+":reformatJson(json): json.siteHeaderSection.matches not defined: ", json, this );
        }
        //matches = matches.sort( function(a,b) { return Number(b.timestamp) - Number(a.timestamp); });
        return matches;
    },

    /**
     *  @see http://venus:20284/league-table/date-timeline/100/2010-2011/02-02-2011/CURRENT_STANDINGS.json
     *  @param  {Hash}  CURRENT_STANDINGS.json:siteHeaderSection.matches[i]
     *  @return {Hash}  modified version of the above
     */
    reformatMatchDataEntry: function( matchData ) {
        try {
            matchData.cssClass = "";

            // Add/Reformat Fields
            var timestamp = new Date(matchData.timestamp);
            var today = (new Date()).toString("dd/MM/yy");
            matchData.date = timestamp.toString("dd/MM/yy");
            matchData.time = timestamp.toString("HH:mm");
            if( today === matchData.date ) {
                matchData.date = "TODAY";
                if( matchData.matchState !== "SAME_MATCH_DAY" ) {
                    matchData.cssClass = "SAME_MATCH_DAY";
                }
            }


            matchData.matchName = "";
            var homeTeam = matchData.homeTeamCode;
            var awayTeam = matchData.awayTeamCode;
            if( matchData.score && matchData.matchState !== "PRE_MATCH"  ) {
                var homeScore = matchData.score.home;
                var awayScore = matchData.score.away;
                matchData.matchName = homeTeam + " " + homeScore + "-" + awayScore + " " + awayTeam;
            } else {
                matchData.matchName = homeTeam + " v " + awayTeam;
            }

            // Set boxText, normally time, but possibly LIVE, HT or FT
            // Ignore: PT, FHS, SHS keys
            matchData.boxText = "";
            if( matchData.matchStateKey === "FT"
             || matchData.detailedStateKey === "FT" ) {
                matchData.boxText = "FT";
            }
            // Full Time matches have both POST_MATCH && SAME_MATCH_DAY
            else if( matchData.matchState === "POST_MATCH" && matchData.detailedState === "SAME_MATCH_DAY" ) {
                matchData.boxText = "FT";
            }
            else if( matchData.matchState === "LIVE" && matchData.detailedState === "HALF_TIME" ) {
                matchData.boxText = "HT";
            }
            else if( matchData.matchState === "LIVE" ) {
                matchData.boxText = "LIVE";
            }
            else {
                matchData.boxText = matchData.time;
            }

            //// Add hashCode
            //matchData.hashCode = "";
            //for( var key in matchData ) {
            //    matchData.hashCode += String(matchData[key]);
            //}
      } catch(e) {
          console.log('EXCEPTION: ', this&&this.klass||'' ,' reformatMatchData: function(',matchData,') ',  e);
          console.dir(e);
      }
      return matchData;
    }
});

$.ui.basewidget.subclass('ui.addCountdown', {
    klass: '$.ui.addCountdown',
    options: {
        eventManager:  	null,       		// {EventManager} 	reference to js/libs/EventManager.js, passed in via init.js
        hash:          	null,       		// {Hash}         	define objects and arrays within the constructor, else it will create a class variable
        deadline: 		null,  				// {Number}   	 	Countdown end point (milliseconds)
        timezone:		0,					// {Number}			Offset for timezone
        format:			'DHMS',				// {String}			Format of counter output
        labels:        	['yrs','mths','wks','days','hrs','mins','secs'],
        layout:			'<p class="timer">{dnn} {sep} {hnn} {sep} {mnn} {sep} {snn}</p><p><span>{dl}</span><span>{hl}</span><span>{ml}</span><span>{sl}</span>',
        									// {String}			layout for countdown to render
        expiryText:		''					// {String}			text to display on expiry
    },
    required: {
    	deadline:  		Number
    },
   _create: function() { 
        this.options.hash = {};
        this.options = $.getAttributeHash( this.element, this.options );
    },
   _init: function() {
        this.doCountdown();
    },
    doCountdown: function() {
    	try {
    		var dateUntil = new Date( this.options.deadline );
    		//http://keith-wood.name/countdown.html
        	this.element.countdown({
    	        until: 	  	dateUntil,
    	        layout:   	this.options.layout,
    	        labels:   	this.options.labels,
    	        labels1:  	this.options.labels,
    	        timezone:	this.options.timezone,
    	        expiryText: this.options.expiryText
    	    });
    	} catch (e) {
    		console.log('EXCEPTION: ', this&&this.klass||'' ,' addCountdown: function(',dateUntil,this.options.layout,this.options.labels,this.options.timezone,') target: ',this.element,  e);
            console.dir(e);
    	}
    }
});


/* http://keith-wood.name/countdown.html
   Countdown for jQuery v1.5.11.
   Written by Keith Wood (kbwood{at}iinet.com.au) January 2008.
   Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and 
   MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses. 
   Please attribute the author if you use it. */

/* Display a countdown timer.
   Attach it with options like:
   $('div selector').countdown(
       {until: new Date(2009, 1 - 1, 1, 0, 0, 0), onExpiry: happyNewYear}); */

(function($) { // Hide scope, no $ conflict

/* Countdown manager. */
function Countdown() {
	this.regional = []; // Available regional settings, indexed by language code
	this.regional[''] = { // Default regional settings
		// The display texts for the counters
		labels: ['Years', 'Months', 'Weeks', 'Days', 'Hours', 'Minutes', 'Seconds'],
		// The display texts for the counters if only one
		labels1: ['Year', 'Month', 'Week', 'Day', 'Hour', 'Minute', 'Second'],
		compactLabels: ['y', 'm', 'w', 'd'], // The compact texts for the counters
		whichLabels: null, // Function to determine which labels to use
		timeSeparator: ':', // Separator for time periods
		isRTL: false // True for right-to-left languages, false for left-to-right
	};
	this._defaults = {
		until: null, // new Date(year, mth - 1, day, hr, min, sec) - date/time to count down to
			// or numeric for seconds offset, or string for unit offset(s):
			// 'Y' years, 'O' months, 'W' weeks, 'D' days, 'H' hours, 'M' minutes, 'S' seconds
		since: null, // new Date(year, mth - 1, day, hr, min, sec) - date/time to count up from
			// or numeric for seconds offset, or string for unit offset(s):
			// 'Y' years, 'O' months, 'W' weeks, 'D' days, 'H' hours, 'M' minutes, 'S' seconds
		timezone: null, // The timezone (hours or minutes from GMT) for the target times,
			// or null for client local
		serverSync: null, // A function to retrieve the current server time for synchronisation
		format: 'DHMS', // Format for display - upper case for always, lower case only if non-zero,
			// 'Y' years, 'O' months, 'W' weeks, 'D' days, 'H' hours, 'M' minutes, 'S' seconds
		layout: '', // Build your own layout for the countdown
		compact: false, // True to display in a compact format, false for an expanded one
		significant: 0, // The number of periods with values to show, zero for all
		description: '', // The description displayed for the countdown
		expiryUrl: '', // A URL to load upon expiry, replacing the current page
		expiryText: '', // Text to display upon expiry, replacing the countdown
		alwaysExpire: true, // True to trigger onExpiry even if never counted down
		onExpiry: null, // Callback when the countdown expires -
			// receives no parameters and 'this' is the containing division
		onTick: null, // Callback when the countdown is updated -
			// receives int[7] being the breakdown by period (based on format)
			// and 'this' is the containing division
		tickInterval: 1 // Interval (seconds) between onTick callbacks
	};
	$.extend(this._defaults, this.regional['']);
	this._serverSyncs = [];
	// Shared timer for all countdowns
	function timerCallBack(timestamp) {
		var drawStart = (timestamp || new Date().getTime());
		if (drawStart - animationStartTime >= 1000) {
			$.countdown._updateTargets();
			animationStartTime = drawStart;
		}
		requestAnimationFrame(timerCallBack);
	}
	var requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame ||
		window.mozRequestAnimationFrame || window.oRequestAnimationFrame ||
		window.msRequestAnimationFrame || null; // this is when we expect a fall-back to setInterval as it's much more fluid
	var animationStartTime = 0;
	if (!requestAnimationFrame) {
		setInterval(function() { $.countdown._updateTargets(); }, 980); // Fall back to good old setInterval
	}
	else {
		animationStartTime = window.mozAnimationStartTime || new Date().getTime();
		requestAnimationFrame(timerCallBack);
	}
}

var PROP_NAME = 'countdown';

var Y = 0; // Years
var O = 1; // Months
var W = 2; // Weeks
var D = 3; // Days
var H = 4; // Hours
var M = 5; // Minutes
var S = 6; // Seconds

$.extend(Countdown.prototype, {
	/* Class name added to elements to indicate already configured with countdown. */
	markerClassName: 'hasCountdown',

	/* List of currently active countdown targets. */
	_timerTargets: [],
	
	/* Override the default settings for all instances of the countdown widget.
	   @param  options  (object) the new settings to use as defaults */
	setDefaults: function(options) {
		this._resetExtraLabels(this._defaults, options);
		extendRemove(this._defaults, options || {});
	},

	/* Convert a date/time to UTC.
	   @param  tz     (number) the hour or minute offset from GMT, e.g. +9, -360
	   @param  year   (Date) the date/time in that timezone or
	                  (number) the year in that timezone
	   @param  month  (number, optional) the month (0 - 11) (omit if year is a Date)
	   @param  day    (number, optional) the day (omit if year is a Date)
	   @param  hours  (number, optional) the hour (omit if year is a Date)
	   @param  mins   (number, optional) the minute (omit if year is a Date)
	   @param  secs   (number, optional) the second (omit if year is a Date)
	   @param  ms     (number, optional) the millisecond (omit if year is a Date)
	   @return  (Date) the equivalent UTC date/time */
	UTCDate: function(tz, year, month, day, hours, mins, secs, ms) {
		if (typeof year == 'object' && year.constructor == Date) {
			ms = year.getMilliseconds();
			secs = year.getSeconds();
			mins = year.getMinutes();
			hours = year.getHours();
			day = year.getDate();
			month = year.getMonth();
			year = year.getFullYear();
		}
		var d = new Date();
		d.setUTCFullYear(year);
		d.setUTCDate(1);
		d.setUTCMonth(month || 0);
		d.setUTCDate(day || 1);
		d.setUTCHours(hours || 0);
		d.setUTCMinutes((mins || 0) - (Math.abs(tz) < 30 ? tz * 60 : tz));
		d.setUTCSeconds(secs || 0);
		d.setUTCMilliseconds(ms || 0);
		return d;
	},

	/* Convert a set of periods into seconds.
	   Averaged for months and years.
	   @param  periods  (number[7]) the periods per year/month/week/day/hour/minute/second
	   @return  (number) the corresponding number of seconds */
	periodsToSeconds: function(periods) {
		return periods[0] * 31557600 + periods[1] * 2629800 + periods[2] * 604800 +
			periods[3] * 86400 + periods[4] * 3600 + periods[5] * 60 + periods[6];
	},

	/* Retrieve one or more settings values.
	   @param  name  (string, optional) the name of the setting to retrieve
	                 or 'all' for all instance settings or omit for all default settings
	   @return  (any) the requested setting(s) */
	_settingsCountdown: function(target, name) {
		if (!name) {
			return $.countdown._defaults;
		}
		var inst = $.data(target, PROP_NAME);
		return (name == 'all' ? inst.options : inst.options[name]);
	},

	/* Attach the countdown widget to a div.
	   @param  target   (element) the containing division
	   @param  options  (object) the initial settings for the countdown */
	_attachCountdown: function(target, options) {
		var $target = $(target);
		if ($target.hasClass(this.markerClassName)) {
			return;
		}
		$target.addClass(this.markerClassName);
		var inst = {options: $.extend({}, options),
			_periods: [0, 0, 0, 0, 0, 0, 0]};
		$.data(target, PROP_NAME, inst);
		this._changeCountdown(target);
	},

	/* Add a target to the list of active ones.
	   @param  target  (element) the countdown target */
	_addTarget: function(target) {
		if (!this._hasTarget(target)) {
			this._timerTargets.push(target);
		}
	},

	/* See if a target is in the list of active ones.
	   @param  target  (element) the countdown target
	   @return  (boolean) true if present, false if not */
	_hasTarget: function(target) {
		return ($.inArray(target, this._timerTargets) > -1);
	},

	/* Remove a target from the list of active ones.
	   @param  target  (element) the countdown target */
	_removeTarget: function(target) {
		this._timerTargets = $.map(this._timerTargets,
			function(value) { return (value == target ? null : value); }); // delete entry
	},

	/* Update each active timer target. */
	_updateTargets: function() {
		for (var i = this._timerTargets.length - 1; i >= 0; i--) {
			this._updateCountdown(this._timerTargets[i]);
		}
	},

	/* Redisplay the countdown with an updated display.
	   @param  target  (jQuery) the containing division
	   @param  inst    (object) the current settings for this instance */
	_updateCountdown: function(target, inst) {
		var $target = $(target);
		inst = inst || $.data(target, PROP_NAME);
		if (!inst) {
			return;
		}
		$target.html(this._generateHTML(inst));
		$target[(this._get(inst, 'isRTL') ? 'add' : 'remove') + 'Class']('countdown_rtl');
		var onTick = this._get(inst, 'onTick');
		if (onTick) {
			var periods = inst._hold != 'lap' ? inst._periods :
				this._calculatePeriods(inst, inst._show, this._get(inst, 'significant'), new Date());
			var tickInterval = this._get(inst, 'tickInterval');
			if (tickInterval == 1 || this.periodsToSeconds(periods) % tickInterval == 0) {
				onTick.apply(target, [periods]);
			}
		}
		var expired = inst._hold != 'pause' &&
			(inst._since ? inst._now.getTime() < inst._since.getTime() :
			inst._now.getTime() >= inst._until.getTime());
		if (expired && !inst._expiring) {
			inst._expiring = true;
			if (this._hasTarget(target) || this._get(inst, 'alwaysExpire')) {
				this._removeTarget(target);
				var onExpiry = this._get(inst, 'onExpiry');
				if (onExpiry) {
					onExpiry.apply(target, []);
				}
				var expiryText = this._get(inst, 'expiryText');
				if (expiryText) {
					var layout = this._get(inst, 'layout');
					inst.options.layout = expiryText;
					this._updateCountdown(target, inst);
					inst.options.layout = layout;
				}
				var expiryUrl = this._get(inst, 'expiryUrl');
				if (expiryUrl) {
					window.location = expiryUrl;
				}
			}
			inst._expiring = false;
		}
		else if (inst._hold == 'pause') {
			this._removeTarget(target);
		}
		$.data(target, PROP_NAME, inst);
	},

	/* Reconfigure the settings for a countdown div.
	   @param  target   (element) the containing division
	   @param  options  (object) the new settings for the countdown or
	                    (string) an individual property name
	   @param  value    (any) the individual property value
	                    (omit if options is an object) */
	_changeCountdown: function(target, options, value) {
		options = options || {};
		if (typeof options == 'string') {
			var name = options;
			options = {};
			options[name] = value;
		}
		var inst = $.data(target, PROP_NAME);
		if (inst) {
			this._resetExtraLabels(inst.options, options);
			extendRemove(inst.options, options);
			this._adjustSettings(target, inst);
			$.data(target, PROP_NAME, inst);
			var now = new Date();
			if ((inst._since && inst._since < now) ||
					(inst._until && inst._until > now)) {
				this._addTarget(target);
			}
			this._updateCountdown(target, inst);
		}
	},

	/* Reset any extra labelsn and compactLabelsn entries if changing labels.
	   @param  base     (object) the options to be updated
	   @param  options  (object) the new option values */
	_resetExtraLabels: function(base, options) {
		var changingLabels = false;
		for (var n in options) {
			if (n != 'whichLabels' && n.match(/[Ll]abels/)) {
				changingLabels = true;
				break;
			}
		}
		if (changingLabels) {
			for (var n in base) { // Remove custom numbered labels
				if (n.match(/[Ll]abels[0-9]/)) {
					base[n] = null;
				}
			}
		}
	},
	
	/* Calculate interal settings for an instance.
	   @param  target  (element) the containing division
	   @param  inst    (object) the current settings for this instance */
	_adjustSettings: function(target, inst) {
		var now;
		var serverSync = this._get(inst, 'serverSync');
		var serverOffset = 0;
		var serverEntry = null;
		for (var i = 0; i < this._serverSyncs.length; i++) {
			if (this._serverSyncs[i][0] == serverSync) {
				serverEntry = this._serverSyncs[i][1];
				break;
			}
		}
		if (serverEntry != null) {
			serverOffset = (serverSync ? serverEntry : 0);
			now = new Date();
		}
		else {
			var serverResult = (serverSync ? serverSync.apply(target, []) : null);
			now = new Date();
			serverOffset = (serverResult ? now.getTime() - serverResult.getTime() : 0);
			this._serverSyncs.push([serverSync, serverOffset]);
		}
		var timezone = this._get(inst, 'timezone');
		timezone = (timezone == null ? -now.getTimezoneOffset() : timezone);
		inst._since = this._get(inst, 'since');
		if (inst._since != null) {
			inst._since = this.UTCDate(timezone, this._determineTime(inst._since, null));
			if (inst._since && serverOffset) {
				inst._since.setMilliseconds(inst._since.getMilliseconds() + serverOffset);
			}
		}
		inst._until = this.UTCDate(timezone, this._determineTime(this._get(inst, 'until'), now));
		if (serverOffset) {
			inst._until.setMilliseconds(inst._until.getMilliseconds() + serverOffset);
		}
		inst._show = this._determineShow(inst);
	},

	/* Remove the countdown widget from a div.
	   @param  target  (element) the containing division */
	_destroyCountdown: function(target) {
		var $target = $(target);
		if (!$target.hasClass(this.markerClassName)) {
			return;
		}
		this._removeTarget(target);
		$target.removeClass(this.markerClassName).empty();
		$.removeData(target, PROP_NAME);
	},

	/* Pause a countdown widget at the current time.
	   Stop it running but remember and display the current time.
	   @param  target  (element) the containing division */
	_pauseCountdown: function(target) {
		this._hold(target, 'pause');
	},

	/* Pause a countdown widget at the current time.
	   Stop the display but keep the countdown running.
	   @param  target  (element) the containing division */
	_lapCountdown: function(target) {
		this._hold(target, 'lap');
	},

	/* Resume a paused countdown widget.
	   @param  target  (element) the containing division */
	_resumeCountdown: function(target) {
		this._hold(target, null);
	},

	/* Pause or resume a countdown widget.
	   @param  target  (element) the containing division
	   @param  hold    (string) the new hold setting */
	_hold: function(target, hold) {
		var inst = $.data(target, PROP_NAME);
		if (inst) {
			if (inst._hold == 'pause' && !hold) {
				inst._periods = inst._savePeriods;
				var sign = (inst._since ? '-' : '+');
				inst[inst._since ? '_since' : '_until'] =
					this._determineTime(sign + inst._periods[0] + 'y' +
						sign + inst._periods[1] + 'o' + sign + inst._periods[2] + 'w' +
						sign + inst._periods[3] + 'd' + sign + inst._periods[4] + 'h' + 
						sign + inst._periods[5] + 'm' + sign + inst._periods[6] + 's');
				this._addTarget(target);
			}
			inst._hold = hold;
			inst._savePeriods = (hold == 'pause' ? inst._periods : null);
			$.data(target, PROP_NAME, inst);
			this._updateCountdown(target, inst);
		}
	},

	/* Return the current time periods.
	   @param  target  (element) the containing division
	   @return  (number[7]) the current periods for the countdown */
	_getTimesCountdown: function(target) {
		var inst = $.data(target, PROP_NAME);
		return (!inst ? null : (!inst._hold ? inst._periods :
			this._calculatePeriods(inst, inst._show, this._get(inst, 'significant'), new Date())));
	},

	/* Get a setting value, defaulting if necessary.
	   @param  inst  (object) the current settings for this instance
	   @param  name  (string) the name of the required setting
	   @return  (any) the setting's value or a default if not overridden */
	_get: function(inst, name) {
		return (inst.options[name] != null ?
			inst.options[name] : $.countdown._defaults[name]);
	},

	/* A time may be specified as an exact value or a relative one.
	   @param  setting      (string or number or Date) - the date/time value
	                        as a relative or absolute value
	   @param  defaultTime  (Date) the date/time to use if no other is supplied
	   @return  (Date) the corresponding date/time */
	_determineTime: function(setting, defaultTime) {
		var offsetNumeric = function(offset) { // e.g. +300, -2
			var time = new Date();
			time.setTime(time.getTime() + offset * 1000);
			return time;
		};
		var offsetString = function(offset) { // e.g. '+2d', '-4w', '+3h +30m'
			offset = offset.toLowerCase();
			var time = new Date();
			var year = time.getFullYear();
			var month = time.getMonth();
			var day = time.getDate();
			var hour = time.getHours();
			var minute = time.getMinutes();
			var second = time.getSeconds();
			var pattern = /([+-]?[0-9]+)\s*(s|m|h|d|w|o|y)?/g;
			var matches = pattern.exec(offset);
			while (matches) {
				switch (matches[2] || 's') {
					case 's': second += parseInt(matches[1], 10); break;
					case 'm': minute += parseInt(matches[1], 10); break;
					case 'h': hour += parseInt(matches[1], 10); break;
					case 'd': day += parseInt(matches[1], 10); break;
					case 'w': day += parseInt(matches[1], 10) * 7; break;
					case 'o':
						month += parseInt(matches[1], 10); 
						day = Math.min(day, $.countdown._getDaysInMonth(year, month));
						break;
					case 'y':
						year += parseInt(matches[1], 10);
						day = Math.min(day, $.countdown._getDaysInMonth(year, month));
						break;
				}
				matches = pattern.exec(offset);
			}
			return new Date(year, month, day, hour, minute, second, 0);
		};
		var time = (setting == null ? defaultTime :
			(typeof setting == 'string' ? offsetString(setting) :
			(typeof setting == 'number' ? offsetNumeric(setting) : setting)));
		if (time) time.setMilliseconds(0);
		return time;
	},

	/* Determine the number of days in a month.
	   @param  year   (number) the year
	   @param  month  (number) the month
	   @return  (number) the days in that month */
	_getDaysInMonth: function(year, month) {
		return 32 - new Date(year, month, 32).getDate();
	},

	/* Determine which set of labels should be used for an amount.
	   @param  num  (number) the amount to be displayed
	   @return  (number) the set of labels to be used for this amount */
	_normalLabels: function(num) {
		return num;
	},

	/* Generate the HTML to display the countdown widget.
	   @param  inst  (object) the current settings for this instance
	   @return  (string) the new HTML for the countdown display */
	_generateHTML: function(inst) {
		// Determine what to show
		var significant = this._get(inst, 'significant');
		inst._periods = (inst._hold ? inst._periods :
			this._calculatePeriods(inst, inst._show, significant, new Date()));
		// Show all 'asNeeded' after first non-zero value
		var shownNonZero = false;
		var showCount = 0;
		var sigCount = significant;
		var show = $.extend({}, inst._show);
		for (var period = Y; period <= S; period++) {
			shownNonZero |= (inst._show[period] == '?' && inst._periods[period] > 0);
			show[period] = (inst._show[period] == '?' && !shownNonZero ? null : inst._show[period]);
			showCount += (show[period] ? 1 : 0);
			sigCount -= (inst._periods[period] > 0 ? 1 : 0);
		}
		var showSignificant = [false, false, false, false, false, false, false];
		for (var period = S; period >= Y; period--) { // Determine significant periods
			if (inst._show[period]) {
				if (inst._periods[period]) {
					showSignificant[period] = true;
				}
				else {
					showSignificant[period] = sigCount > 0;
					sigCount--;
				}
			}
		}
		var compact = this._get(inst, 'compact');
		var layout = this._get(inst, 'layout');
		var labels = (compact ? this._get(inst, 'compactLabels') : this._get(inst, 'labels'));
		var whichLabels = this._get(inst, 'whichLabels') || this._normalLabels;
		var timeSeparator = this._get(inst, 'timeSeparator');
		var description = this._get(inst, 'description') || '';
		var showCompact = function(period) {
			var labelsNum = $.countdown._get(inst,
				'compactLabels' + whichLabels(inst._periods[period]));
			return (show[period] ? inst._periods[period] +
				(labelsNum ? labelsNum[period] : labels[period]) + ' ' : '');
		};
		var showFull = function(period) {
			var labelsNum = $.countdown._get(inst, 'labels' + whichLabels(inst._periods[period]));
			return ((!significant && show[period]) || (significant && showSignificant[period]) ?
				'<span class="countdown_section"><span class="countdown_amount">' +
				inst._periods[period] + '</span><br/>' +
				(labelsNum ? labelsNum[period] : labels[period]) + '</span>' : '');
		};
		return (layout ? this._buildLayout(inst, show, layout, compact, significant, showSignificant) :
			((compact ? // Compact version
			'<span class="countdown_row countdown_amount' +
			(inst._hold ? ' countdown_holding' : '') + '">' + 
			showCompact(Y) + showCompact(O) + showCompact(W) + showCompact(D) + 
			(show[H] ? this._minDigits(inst._periods[H], 2) : '') +
			(show[M] ? (show[H] ? timeSeparator : '') +
			this._minDigits(inst._periods[M], 2) : '') +
			(show[S] ? (show[H] || show[M] ? timeSeparator : '') +
			this._minDigits(inst._periods[S], 2) : '') :
			// Full version
			'<span class="countdown_row countdown_show' + (significant || showCount) +
			(inst._hold ? ' countdown_holding' : '') + '">' +
			showFull(Y) + showFull(O) + showFull(W) + showFull(D) +
			showFull(H) + showFull(M) + showFull(S)) + '</span>' +
			(description ? '<span class="countdown_row countdown_descr">' + description + '</span>' : '')));
	},

	/* Construct a custom layout.
	   @param  inst             (object) the current settings for this instance
	   @param  show             (string[7]) flags indicating which periods are requested
	   @param  layout           (string) the customised layout
	   @param  compact          (boolean) true if using compact labels
	   @param  significant      (number) the number of periods with values to show, zero for all
	   @param  showSignificant  (boolean[7]) other periods to show for significance
	   @return  (string) the custom HTML */
	_buildLayout: function(inst, show, layout, compact, significant, showSignificant) {
		var labels = this._get(inst, (compact ? 'compactLabels' : 'labels'));
		var whichLabels = this._get(inst, 'whichLabels') || this._normalLabels;
		var labelFor = function(index) {
			return ($.countdown._get(inst,
				(compact ? 'compactLabels' : 'labels') + whichLabels(inst._periods[index])) ||
				labels)[index];
		};
		var digit = function(value, position) {
			return Math.floor(value / position) % 10;
		};
		var subs = {desc: this._get(inst, 'description'), sep: this._get(inst, 'timeSeparator'),
			yl: labelFor(Y), yn: inst._periods[Y], ynn: this._minDigits(inst._periods[Y], 2),
			ynnn: this._minDigits(inst._periods[Y], 3), y1: digit(inst._periods[Y], 1),
			y10: digit(inst._periods[Y], 10), y100: digit(inst._periods[Y], 100),
			y1000: digit(inst._periods[Y], 1000),
			ol: labelFor(O), on: inst._periods[O], onn: this._minDigits(inst._periods[O], 2),
			onnn: this._minDigits(inst._periods[O], 3), o1: digit(inst._periods[O], 1),
			o10: digit(inst._periods[O], 10), o100: digit(inst._periods[O], 100),
			o1000: digit(inst._periods[O], 1000),
			wl: labelFor(W), wn: inst._periods[W], wnn: this._minDigits(inst._periods[W], 2),
			wnnn: this._minDigits(inst._periods[W], 3), w1: digit(inst._periods[W], 1),
			w10: digit(inst._periods[W], 10), w100: digit(inst._periods[W], 100),
			w1000: digit(inst._periods[W], 1000),
			dl: labelFor(D), dn: inst._periods[D], dnn: this._minDigits(inst._periods[D], 2),
			dnnn: this._minDigits(inst._periods[D], 3), d1: digit(inst._periods[D], 1),
			d10: digit(inst._periods[D], 10), d100: digit(inst._periods[D], 100),
			d1000: digit(inst._periods[D], 1000),
			hl: labelFor(H), hn: inst._periods[H], hnn: this._minDigits(inst._periods[H], 2),
			hnnn: this._minDigits(inst._periods[H], 3), h1: digit(inst._periods[H], 1),
			h10: digit(inst._periods[H], 10), h100: digit(inst._periods[H], 100),
			h1000: digit(inst._periods[H], 1000),
			ml: labelFor(M), mn: inst._periods[M], mnn: this._minDigits(inst._periods[M], 2),
			mnnn: this._minDigits(inst._periods[M], 3), m1: digit(inst._periods[M], 1),
			m10: digit(inst._periods[M], 10), m100: digit(inst._periods[M], 100),
			m1000: digit(inst._periods[M], 1000),
			sl: labelFor(S), sn: inst._periods[S], snn: this._minDigits(inst._periods[S], 2),
			snnn: this._minDigits(inst._periods[S], 3), s1: digit(inst._periods[S], 1),
			s10: digit(inst._periods[S], 10), s100: digit(inst._periods[S], 100),
			s1000: digit(inst._periods[S], 1000)};
		var html = layout;
		// Replace period containers: {p<}...{p>}
		for (var i = Y; i <= S; i++) {
			var period = 'yowdhms'.charAt(i);
			var re = new RegExp('\\{' + period + '<\\}(.*)\\{' + period + '>\\}', 'g');
			html = html.replace(re, ((!significant && show[i]) ||
				(significant && showSignificant[i]) ? '$1' : ''));
		}
		// Replace period values: {pn}
		$.each(subs, function(n, v) {
			var re = new RegExp('\\{' + n + '\\}', 'g');
			html = html.replace(re, v);
		});
		return html;
	},

	/* Ensure a numeric value has at least n digits for display.
	   @param  value  (number) the value to display
	   @param  len    (number) the minimum length
	   @return  (string) the display text */
	_minDigits: function(value, len) {
		value = '' + value;
		if (value.length >= len) {
			return value;
		}
		value = '0000000000' + value;
		return value.substr(value.length - len);
	},

	/* Translate the format into flags for each period.
	   @param  inst  (object) the current settings for this instance
	   @return  (string[7]) flags indicating which periods are requested (?) or
	            required (!) by year, month, week, day, hour, minute, second */
	_determineShow: function(inst) {
		var format = this._get(inst, 'format');
		var show = [];
		show[Y] = (format.match('y') ? '?' : (format.match('Y') ? '!' : null));
		show[O] = (format.match('o') ? '?' : (format.match('O') ? '!' : null));
		show[W] = (format.match('w') ? '?' : (format.match('W') ? '!' : null));
		show[D] = (format.match('d') ? '?' : (format.match('D') ? '!' : null));
		show[H] = (format.match('h') ? '?' : (format.match('H') ? '!' : null));
		show[M] = (format.match('m') ? '?' : (format.match('M') ? '!' : null));
		show[S] = (format.match('s') ? '?' : (format.match('S') ? '!' : null));
		return show;
	},
	
	/* Calculate the requested periods between now and the target time.
	   @param  inst         (object) the current settings for this instance
	   @param  show         (string[7]) flags indicating which periods are requested/required
	   @param  significant  (number) the number of periods with values to show, zero for all
	   @param  now          (Date) the current date and time
	   @return  (number[7]) the current time periods (always positive)
	            by year, month, week, day, hour, minute, second */
	_calculatePeriods: function(inst, show, significant, now) {
		// Find endpoints
		inst._now = now;
		inst._now.setMilliseconds(0);
		var until = new Date(inst._now.getTime());
		if (inst._since) {
			if (now.getTime() < inst._since.getTime()) {
				inst._now = now = until;
			}
			else {
				now = inst._since;
			}
		}
		else {
			until.setTime(inst._until.getTime());
			if (now.getTime() > inst._until.getTime()) {
				inst._now = now = until;
			}
		}
		// Calculate differences by period
		var periods = [0, 0, 0, 0, 0, 0, 0];
		if (show[Y] || show[O]) {
			// Treat end of months as the same
			var lastNow = $.countdown._getDaysInMonth(now.getFullYear(), now.getMonth());
			var lastUntil = $.countdown._getDaysInMonth(until.getFullYear(), until.getMonth());
			var sameDay = (until.getDate() == now.getDate() ||
				(until.getDate() >= Math.min(lastNow, lastUntil) &&
				now.getDate() >= Math.min(lastNow, lastUntil)));
			var getSecs = function(date) {
				return (date.getHours() * 60 + date.getMinutes()) * 60 + date.getSeconds();
			};
			var months = Math.max(0,
				(until.getFullYear() - now.getFullYear()) * 12 + until.getMonth() - now.getMonth() +
				((until.getDate() < now.getDate() && !sameDay) ||
				(sameDay && getSecs(until) < getSecs(now)) ? -1 : 0));
			periods[Y] = (show[Y] ? Math.floor(months / 12) : 0);
			periods[O] = (show[O] ? months - periods[Y] * 12 : 0);
			// Adjust for months difference and end of month if necessary
			now = new Date(now.getTime());
			var wasLastDay = (now.getDate() == lastNow);
			var lastDay = $.countdown._getDaysInMonth(now.getFullYear() + periods[Y],
				now.getMonth() + periods[O]);
			if (now.getDate() > lastDay) {
				now.setDate(lastDay);
			}
			now.setFullYear(now.getFullYear() + periods[Y]);
			now.setMonth(now.getMonth() + periods[O]);
			if (wasLastDay) {
				now.setDate(lastDay);
			}
		}
		var diff = Math.floor((until.getTime() - now.getTime()) / 1000);
		var extractPeriod = function(period, numSecs) {
			periods[period] = (show[period] ? Math.floor(diff / numSecs) : 0);
			diff -= periods[period] * numSecs;
		};
		extractPeriod(W, 604800);
		extractPeriod(D, 86400);
		extractPeriod(H, 3600);
		extractPeriod(M, 60);
		extractPeriod(S, 1);
		if (diff > 0 && !inst._since) { // Round up if left overs
			var multiplier = [1, 12, 4.3482, 7, 24, 60, 60];
			var lastShown = S;
			var max = 1;
			for (var period = S; period >= Y; period--) {
				if (show[period]) {
					if (periods[lastShown] >= max) {
						periods[lastShown] = 0;
						diff = 1;
					}
					if (diff > 0) {
						periods[period]++;
						diff = 0;
						lastShown = period;
						max = 1;
					}
				}
				max *= multiplier[period];
			}
		}
		if (significant) { // Zero out insignificant periods
			for (var period = Y; period <= S; period++) {
				if (significant && periods[period]) {
					significant--;
				}
				else if (!significant) {
					periods[period] = 0;
				}
			}
		}
		return periods;
	}
});

/* jQuery extend now ignores nulls!
   @param  target  (object) the object to update
   @param  props   (object) the new settings
   @return  (object) the updated object */
function extendRemove(target, props) {
	$.extend(target, props);
	for (var name in props) {
		if (props[name] == null) {
			target[name] = null;
		}
	}
	return target;
}

/* Process the countdown functionality for a jQuery selection.
   @param  command  (string) the command to run (optional, default 'attach')
   @param  options  (object) the new settings to use for these countdown instances
   @return  (jQuery) for chaining further calls */
$.fn.countdown = function(options) {
	var otherArgs = Array.prototype.slice.call(arguments, 1);
	if (options == 'getTimes' || options == 'settings') {
		return $.countdown['_' + options + 'Countdown'].
			apply($.countdown, [this[0]].concat(otherArgs));
	}
	return this.each(function() {
		if (typeof options == 'string') {
			$.countdown['_' + options + 'Countdown'].apply($.countdown, [this].concat(otherArgs));
		}
		else {
			$.countdown._attachCountdown(this, options);
		}
	});
};

/* Initialise the countdown functionality. */
$.countdown = new Countdown(); // singleton instance

})(jQuery);
/**
 * $.ui.fanzone
 * 
 * 		Base fanzone widget functionality, ensuring all widgets made for 
 * 		fanzone provide an ajax URL to submit to and provide error-handling 
 * 		functionality in the case of failure.
 * 
 *  <h3 class="question noborder">
 *	    <%=question%>
 *		<span class="club">
 *		<!-- Inserted by Javascript -->
 *	        <img src="<%=userClubLogo%>" alt="<%=userClubName%>" />
 *	        <%=userClubName%>
 *      <!-- Inserted by Javascript -->
 *      </span>
 *	</h3>
 *   
 */

$.ui.basewidget.subclass('ui.fanzone', {
    klass: '$.ui.fanzone',
    options: {
        ajax:           null,    	 // {String}       ajax URL for the given match
        homeId:			null,	 	 // {Number}	   paId of home team
        awayId:			null,	 	 // {Number}	   paId of away team
        homecolor:	   '#d5302a',    // {Hex}		   Home team fans cell background colour
        awaycolor:	   '#202986',	 // {Hex}		   Away team fans cell background colour
        neutralcolor:  '#999999',	 // {Hex}		   Neutral fans cell background colour
        yourcolor:	   '#00b8f1',	 // {Hex}		   Your input cell background colour
        logoSize:	    23,		 	 // {Number}	   club logo size for fanzone components
        tracking:		null		 // {String}	   clubA-vs-clubB suffix for tracking
    },
    required: {
        ajax:           String
    },
   _create: function() {
        this.options.hash  = {};
        this.options       = $.getAttributeHash( this.element, this.options );
        
        this.options.tracking = this.options.tracking || "";

        this.data.loggedin = false;
        this.data.teamName = null;
        this.data.teamLogo = null;
        this.data.fanGroup = 'neutral';
    },
   _init: function() {
    	this.el.legend 	 = this.element.find('ul.legend');
    	this.el.faveClub = this.element.find('span.club');
    	
    	this.getLogin();
    },
    getLogin: function() {
    	try {
    		this.data.userId   = window.userObject.user.id;
    		this.data.teamId   = window.userObject.user.faveClubCode;
        	if (this.data.teamId === this.options.homeId) { this.data.fanGroup = 'home'; }
        	if (this.data.teamId === this.options.awayId) { this.data.fanGroup = 'away'; }
    		this.data.loggedin = true;
    	} catch (error) {
    		//console.error(this.klass+":getLogin(): Invalid user object:" + window.userObject, " - this: ", this );
    	}
    },
    addClubInfo: function() {
    	if (this.data.teamId) {
	    	// Form AJAX url specifically for user's team
	    	var ajax = '/ajax/club/' + this.data.teamId + '.json';
	    	// Quite hardcoded, with the expectation that club logo location is not going to change
	    	var logo = '/content/dam/premierleague/shared-images/clubs/{initial}/{club}/logo.png'
	    				+ '/_jcr_content/renditions/cq5dam.thumbnail.'
	    				+ this.options.logoSize 
	    				+ '.'
	    				+ this.options.logoSize 
	    				+'.png';
	
	    	var self = this;
	    	$.ajax({
				type:  	    "GET",
	            url:   	    ajax,
	            cache: 	    true,
	            dataType:   "json",
	            beforeSend: function( xhr ) {
	            },
	            success: function( json, xhr, status ) {
	            	try {
	            		if (json.atomicClub) {
			            	//Getting team name is simples
			            	self.data.teamName = json.atomicClub.clubShortName;
			            	//Getting logo needs a bit of manipulation and replacing
			            	var cmsAlias = json.atomicClub.cmsAlias[0];
			            	self.data.teamLogo = logo
			            		.replace('{initial}', cmsAlias.charAt(0))
			            		.replace('{club}',	  cmsAlias);
			            	// Check we have everything needed, target, name and logo!
			            	if ( self.el.faveClub && self.data.teamName && self.data.teamLogo ) {
			            		self.el.faveClub.html('<img src="' + self.data.teamLogo + '" alt="' 
			            				+ self.data.teamName + '" />' + self.data.teamName);
			            		
			            		// Show club info
			            		self.el.faveClub.fadeIn();
			            	}
	            		}
	            	} catch (error) {
	            		console.error(this.klass+":getFaveClub(): No clubShortName or cmsAlias in:" + json, " - this: ", this );
	            	}
	            },
	            error: function( xhr, status, error ) {
	            	console.error(this.klass+":getFaveClub(): ajax error:" + xhr.status, " - this: ", this );
	            }
			});
    	}
    },
    doTracking: function ( cfVal, actionVal ) {
    	
    	window.YWABeacon
    		.trigger("YWAEvent:track",["cf", cfVal, this.options.tracking])
    		.trigger("YWAEvent:track",["action", actionVal])
	        .trigger("YWAEvent:track",["submit"]);
    	
    }
});
/**
 *  <div id="homeSelect">
 *	   <span class="up"></span>
 *	   <span class="down"></span>
 *	</div>
 *	<img class="score home <%=homePrediction%>" alt="" src="<%=designPath%>/images/shim.gif" />
 *	<p class="scoredivider">&ndash;</p>
 *	<img class="score away <%=awayPrediction%>" alt="" src="<%=designPath%>/images/shim.gif" />
 *	<div id="awaySelect">
 *	   <span class="up"></span>
 *	   <span class="down"></span>
 *	</div>
 *
 *  AJAX:
 *  Fanzone Page: Banner Receipt + Information Display
 *  /ajax/fanzone/score/{matchId}/{userId}/{homeScore}/{awayScore}/{homeTeamId}/{awayTeamId}/{userClubId}.json
 *  /ajax/fanzone/score/3329064/{userId}/{homescore}/{awayscore}/42/4/{userClubId}.json
 *  
 *  Other Pages: Banner Receipt + Link to Fanzone
 *  /ajax/fanzone/quick-score/{matchId}/{userId}/{homeScore}/{awayScore}/{homeTeamId}/{awayTeamId}/{userClubId}.json
 *  /ajax/fanzone/quick-score/3329064/{userId}/{homescore}/{awayscore}/42/4/{userClubId}.json
 *  
 *  N.B. Upon ajax success the component fires a tracking code submission for Yahoo
 *  	 Analytics
 * 
 */

$.ui.fanzone.subclass('ui.fanzonebanner', {
    klass: '$.ui.fanzonebanner',
    options: {
		type:			null,	 // {String}	   'quick-score' || 'score'
		fanzoneurl:		null,	 // {String}	   page url for fan-zone tab
		loginurl:		null,	 // {String}	   page url for login
        state:			null,	 // {String} 	   class identifier of form 'state#'
        initial:		0,		 // {Number}	   default for score values
        min:			0,		 // {Number}	   Minimum value for selection
        max:			6		 // {Number}	   Maximum value for selection 6 => 5plus
    },
    required: {
    	fanzoneurl:		String,
    	loginurl:		String,
    	state:			String,
    	type:			String
    },

    create: function() {
        this.options.hash  = {};
        this.options 	   = $.getAttributeHash( this.element, this.options );
    },
   _init: function() {

    	this.el.target 	 	= this.element.find('#banner-receipt');
    	this.el.receipt		= this.element.find('.receipt');
    	this.el.linkButton  = this.element.find('#view');
    	this.el.errorTarget = this.element.find('.text-error');
    	
    	this.el.allstates	= this.element.find('[class^="state"]');
    	this.el.state1		= this.element.find('.state1');
    	this.el.state2		= this.element.find('.state2');
    	this.el.state3		= this.element.find('.state3');
    	this.el.state4		= this.element.find('.state4');
    	
        this.state 		 	= this.options.state;
    	this.type  		 	= this.options.type;
    	this.fanzoneURL  	= this.options.fanzoneurl;
    	
    	this.hideStates();
    	this.addEvents();
    	this.addSpinnerControl('home');
    	this.addSpinnerControl('away');
    },
    addSpinnerControl: function ( team ) {
    	
    	var selectorId   = team + 'Select';
    	var initial 	 = this.options.initial;
    	var interval	 = this.options.interval;
    	var min 		 = this.options.min;
    	var max 		 = this.options.max;
    	var inputControl = $('#'+selectorId+'Input');

        // validate if the object is a input of text type.
        if (!inputControl.is(':text')) { return inputControl; }
        if (inputControl.hasClass('spinnerControl')) { return inputControl; }
        else { inputControl.addClass('spinnerControl'); }

        // create the Spinner Control body.
        var strContainerDiv = '';
        strContainerDiv += '<div id="'+team+'Select">';
        strContainerDiv += '<span class="updown updown_up"></span>';
        strContainerDiv += '<span class="updown updown_down"></span>';
        strContainerDiv += '</div>';
        strContainerDiv += '<img class="valueDisplay score '+team+'" alt="" src="/etc/designs/premierleague/images/shim.gif" />';

        // add the above created control to page
        var objContainerDiv = $(strContainerDiv).insertAfter(inputControl);

        // hide the input control and place within the Spinner Control body
        inputControl.insertAfter($(".valueDisplay." + team)).css('display', 'none');

        // set default value;
        if (initial < min || initial > max) {
            initial = min;
        }
        inputControl.val(initial);
        this.element.find('.valueDisplay.' + team).addClass('predict'+initial);
        
        var selectedValue = initial;

        if ((max - min) > 1) {
            // attach events;
            $("span.updown_up", objContainerDiv).click(function() {
                if ((selectedValue + 1) <= max) {
                	var newClass;
                	var oldClass = 'predict'+selectedValue;
                	selectedValue += 1;
                	if (selectedValue == 6) {
                		selectedValue = '5+';
                		newClass = 'predict5plus';
                	} else {
                		newClass = 'predict'+selectedValue;
                	}
                    $(".valueDisplay." + team).switchClass( oldClass, newClass, 'fast' );
                    inputControl.val(selectedValue);
                }
            });

            $("span.updown_down", objContainerDiv).click(function() {
            	var oldClass;
            	if (selectedValue == '5+') {
            		selectedValue = max;
            		oldClass = 'predict5plus';
            	} else {
            		oldClass = 'predict'+selectedValue;
            	}
                if ((selectedValue - 1) >= min) {
                	selectedValue -= 1;
                    var newClass = 'predict'+selectedValue;
                    $(".valueDisplay." + team).switchClass( oldClass, newClass, 'fast' );
                    inputControl.val(selectedValue);
                }
            });
        };
    },
    hideStates: function () {
    	this.el.allstates.hide();
    	
    	if ( this.options.state === '.state1' ) {
    		this.el.target.hide();
    		// Show either 'login' or 'enter prediction' buttons 
    		if ( this.data.loggedin ) {
    			this.element.find( '#fanzonebanner-login' ).remove();
    			this.element.find( '#fanzonebanner-enter' ).fadeIn();
    		} else {
    			this.element.find( '#fanzonebanner-enter' ).remove();
    			this.element.find( '#fanzonebanner-login' ).fadeIn();
    		}
    	}
    	this.element.find( this.options.state ).show();
    },
    addEvents: function () {
    	var ajax = this.options.ajax;
    	var self = this; // Only use in the ajax call! :(
    	
    	// STATE 1 --> STATE 2
    	this.el.state1.find('#fanzonebanner-enter .submit').bind('click', $.proxy( function() {
    		this.el.state1.fadeOut();
    		this.el.state2.fadeIn();
    	}, this));

		// STATE 2 --> STATE 3
    	this.el.state2.find('.submit').bind('click', $.proxy( function() {
    		
    		this.el.state2.fadeOut();
			/**
			 * Submit Form, Retrieve receipt, alter DOM and display 
			 */
			var data = ajax;
				data = data.replace('{homescore}',  $('#homeSelectInput').val());
				data = data.replace('{awayscore}',  $('#awaySelectInput').val());
				data = data.replace('{userId}',     this.data.userId);
				data = data.replace('{userClubId}', this.data.teamId);

			$.ajax({
				type:  "GET",
                url:   data,
                cache: false,
                dataType: "json",
                beforeSend: function( xhr ) {
                },
                success: function( json, xhr, status ) {
                	self.render( json );
                	self.doTracking( "17", "03" );	// TRACKING
                },
                error: function( xhr, status, error ) {
			    	self.renderError( xhr.status, error );
	            }
			});
			//cancel the submit button default behaviours
			return false;
		}, this ) );
    },
    render: function ( json ) {
    	
    	var bannerPercentage = "";
    	var self = this;
    	
    	if (!json.exception) {
	    	
    		if ( this.type === "quick-score" && json.fanZoneScorePredictionShortReceipt) {
	        	// Short Receipt Data
	        	bannerPercentage = json.fanZoneScorePredictionShortReceipt.percentageSameFanGroupPredictedSameScore;
	        	this.el.receipt.html(bannerPercentage+"% of "+this.data.fanGroup+" fans predicted the same score");
	    		this.el.linkButton.attr("href", this.fanzoneURL);
	    		
	    		/****************TEMP DO NOT DISPLAY LINK******************/
	    		this.el.linkButton.remove();
	    	
    		} else if (this.type === "score" && json.fanZoneScorePredictionLongReceipt) {

    			json = json.fanZoneScorePredictionLongReceipt;
    			
	    		// Short Receipt Data for Banner
	    		bannerPercentage = json.percentageFansPredictedSameScore;
	    		this.el.receipt.html(bannerPercentage+"% of "+this.data.fanGroup+" fans predicted the same score");
	    		
	    		// Extended Receipt
	    		var display 		= {}; // Overall display view
	    		
	    		var popularHome 	= {}; // Home supporter scores
	    		var popularNeutral 	= {}; // Neutral supporter scores
	    		var popularAway 	= {}; // Away supporter scores
	    		
	    		var votedHome		= {}; // Home supporter W/D/L breakdown
	    		var votedNeutral	= {}; // Neutral supporter W/D/L breakdown
	    		var votedAway		= {}; // Away supporter W/D/L breakdown
	    		
	    		if ( o = json.modeScorePredictionOverall ) {
		    		display.home 		= (o.homeScore == '5+') ? 'predict5plus' : 'predict'+o.homeScore;
		    		display.away 		= (o.awayScore == '5+') ? 'predict5plus' : 'predict'+o.awayScore;
	    		}
	    		if ( h = json.modeScorePredictionHomeFans ) {
	    			popularHome.home	= (h.homeScore == '5+') ? 'predict5plus' : 'predict'+h.homeScore;
	    			popularHome.away	= (h.awayScore == '5+') ? 'predict5plus' : 'predict'+h.awayScore;
	    		}
	    		if ( n = json.modeScorePredictionNeutralFans ) {
	    			popularNeutral.home	= (n.homeScore == '5+') ? 'predict5plus' : 'predict'+n.homeScore;
	    			popularNeutral.away	= (n.awayScore == '5+') ? 'predict5plus' : 'predict'+n.awayScore;
	    		}
	    		if ( a = json.modeScorePredictionAwayFans ) {
	    			popularAway.home	= (a.homeScore == '5+') ? 'predict5plus' : 'predict'+a.homeScore;
	    			popularAway.away	= (a.awayScore == '5+') ? 'predict5plus' : 'predict'+a.awayScore;
	    		}
	    		if ( hb = json.homeFansScorePredictionsBreakdown ) {
		    		votedHome.Hwin		= hb.percentagePredictedHomeWin+'%';
		    		votedHome.draw		= hb.percentagePredictedDraw+'%';
		    		votedHome.Awin		= hb.percentagePredictedAwayWin+'%';
	    		}
	    		if ( nb = json.neutralFansScorePredictionsBreakdown ) {
		    		votedNeutral.Hwin	= nb.percentagePredictedHomeWin+'%';
		    		votedNeutral.draw	= nb.percentagePredictedDraw+'%';
		    		votedNeutral.Awin	= nb.percentagePredictedAwayWin+'%';
	    		}
	    		if ( ab = json.awayFansScorePredictionsBreakdown ) {
		    		votedAway.Hwin		= ab.percentagePredictedHomeWin+'%';
		    		votedAway.draw		= ab.percentagePredictedDraw+'%';
		    		votedAway.Awin		= ab.percentagePredictedAwayWin+'%';
	    		}
	    		
	    		// Set Overall display image classes
	    		this.el.target.find('.display .prediction.home .score.home').addClass(display.home);
	    		this.el.target.find('.display .prediction.away .score.away').addClass(display.away);
	    		
	    		// Set Popular Predictions display image classes
	    		this.el.target.find('.popularpredictions .fans.home .score.home').addClass(popularHome.home);
	    		this.el.target.find('.popularpredictions .fans.home .score.away').addClass(popularHome.away);
	    		this.el.target.find('.popularpredictions .fans.neutral .score.home').addClass(popularNeutral.home);
	    		this.el.target.find('.popularpredictions .fans.neutral .score.away').addClass(popularNeutral.away);
	    		this.el.target.find('.popularpredictions .fans.away .score.home').addClass(popularAway.home);
	    		this.el.target.find('.popularpredictions .fans.away .score.away').addClass(popularAway.away);
	    		
	    		// Set How Fans Voted table
	    		this.el.target.find('.howfansvoted .hWin' ).html(votedHome.Hwin);
	    		this.el.target.find('.howfansvoted .hDraw').html(votedHome.draw);
	    		this.el.target.find('.howfansvoted .hLoss').html(votedHome.Awin);
	    		this.el.target.find('.howfansvoted .nWin' ).html(votedNeutral.Hwin);
	    		this.el.target.find('.howfansvoted .nDraw').html(votedNeutral.draw);
	    		this.el.target.find('.howfansvoted .nLoss').html(votedNeutral.Awin);
	    		this.el.target.find('.howfansvoted .aWin' ).html(votedAway.Hwin);
	    		this.el.target.find('.howfansvoted .aDraw').html(votedAway.draw);
	    		this.el.target.find('.howfansvoted .aLoss').html(votedAway.Awin);
	    		
	    		// Remove pesky display button, init SVG and just show it!
	    		this.el.linkButton.remove();
	    		
	    		/**
	    		 * Clunky cleaning up for pie charts
	    		 * 
	    		 * find the wrapper, 
	    		 * take out the table, 
	    		 * remove the wrapper, 
	    		 * re-wrap the table,
	    		 * add the widgets to the rows (not added in the JSP)
	    		 * and then initialise the widgets again...
	    		 */ 
    			var svgWrapper = self.el.target.find('.howfansvoted .svg-wrapper');	//find
    			self.el.target.find('table').insertAfter(svgWrapper);				//take out
    			svgWrapper.remove(); 												//remove		
    			self.el.target.find('table').wrap('<div widget="svgWrapper" svgheight="220" svgwidth="710" />'); //re-wrap
    			self.el.target.find('tr').attr('widget','svgPieChart');				//add widgets				
        		$.initWidgets( self.el.target );									//init widgets
	    		this.el.target.slideDown('fast');
	    	}
    	} else {
	        // Receipt Error Text - Should not reach this point 
	        this.el.receipt.html("There was an error submitting your prediction");
        	this.el.linkButton.remove();
	    }
    	// Show receipt
    	this.el.state3.fadeIn();
    },
    renderError: function ( status, json ) {
    	
    	var results	= this.el.linkButton.remove();

    	switch ( status ) {
    	
    	case 404:
    		this.el.receipt.html('<p class="data-error">There was an error submitting your prediction:<br/>Not found</p><!--404-->');
    		break;
    	case 400:
    		this.el.receipt.html('<p class="data-error">There was an error submitting your prediction:<br/>Please try again later</p><!--400-->');
    		break;
    	case 403:
    		this.el.receipt.html('<p class="data-error">There was an error submitting your prediction:<br/>No submissions allowed</p><!--403-->');
    		break;
    	case 409:
    		this.el.receipt.html('<p class="data-error">There was an error submitting your prediction:<br/>You have already submitted</p><!--409-->');
    		break;
    	case 401:
    		this.el.receipt.html('<p class="data-error">There was an error submitting your prediction:<br/>Unauthorised. Please log in.</p><!--403-->');
    		break;
    	case 500:
    		this.el.receipt.html('<p class="data-error">There was a server error submitting your prediction</p><!--500-->');
    		break;
    	default:
    		this.el.receipt.html('<p class="data-error">There was a server error submitting your prediction</p>');
    	
    	}
    	// Re-render target
    	this.el.state3.fadeIn();
    }
});
/**
 *  AJAX:
 *  pollType: 'vote'
 *  /fanzone/vote/{matchId}/{userId}/{input}/{userClubId}.json
 *  /fanzone/vote/3329064/135/{input}/1006.json
 *  
 *  pollType: 'team-vote'
 *  /fanzone/team-vote/{matchId}/{userId}/{input}/{homeClubId}/{awayClubId}/{userClubId}.json
 *  /fanzone/team-vote/3329064/135/{input}/1006/4/1006.json
 * 
 */

$.ui.fanzone.subclass('ui.fanzonepoll', {
    klass: '$.ui.fanzonepoll',
    options: {
        type:           null    // {String}       'team-vote' || 'vote'
    },
    required: {
        type:           String
    },
    create: function() {
        this.options.hash  = {};
        this.options       = $.getAttributeHash( this.element, this.options );
    },
    _init: function() {

    	this.el.inputs = this.element.find('input');
        this.el.submit = this.el.inputs.filter('[type=submit]');
    	this.el.target = this.element.find('#fanzonepoll-result');
    	this.type      = this.options.type;

    	// Show either 'login' or 'submit' buttons 
		if ( this.data.loggedin ) {
			this.element.find( '#fanzonepoll-login'  ).remove();
			this.element.find( '#fanzonepoll-submit' ).fadeIn();
		} else {
			this.el.inputs.attr('disabled', 'disabled');
			this.element.find( '#fanzonepoll-submit' ).remove();
			this.element.find( '#fanzonepoll-login'  ).fadeIn();
		}
        if (this.options.ajax) {
        	this.el.legend.hide();
        	this.addClubInfo(); //_super()
        	this.el.target.hide();
        	this.addEvents();
        }
    },
    addEvents: function() {
        var self = this;
        var ajax = this.options.ajax;
        
        this.el.submit.bind('click', $.proxy( function () {
        	// Input check!
        	this.el.value = this.el.inputs.filter(':radio[name=answer]:checked').val();
        	if ( this.el.value ) {
	        	// Remove button
        		this.el.submit.fadeOut();
	            
	            // Workaround for nice removal of inputs
        		this.element.find('tr#inputs input').slideUp();
        		this.element.find('tr#inputs td'   ).css('padding','0');
	        	
	        	// Add selection to ajax string
	            var data = ajax;
	                data = data.replace('{input}',  	this.el.value);
	                data = data.replace('{userId}',     this.data.userId);
	                data = data.replace('{userClubId}', this.data.teamId);
	
	            // Submit Form, Retrieve receipt, call render()
	            $.ajax({
				    type: "GET",
				    url:  data,
				    cache: false,
				    dataType: "json",
				    beforeSend: function( xhr ) {
				    },
				    success: function( json, xhr, status ) {
				        self.render( json, self.type );
				    },
				    error: function( xhr, status, error ) {
				    	self.renderError( xhr.status, error );
		            }
				});
        	} //else do nothing
        	
            // Cancel the submit button default behaviours
            return false;
        }, this ) );// proxy
    },
    render: function( json, polltype ) {
    	// Target <div id="fanzonepoll-result"> element
    	if (!json.exception) {

        	// Remove unneeded result tables
    		this.el.target.find('table:not(.'+polltype+')').remove();
        		
            if ( polltype == "team-vote" && json.fanZoneVoteTeamReceipt) {

            	// Set floating span for yourAnswer == X.answer
                var spanBox = '<span class="yourAnswer" style="background:'+this.options.yourcolor+'"></span>';
            	
            	// Receipt Data
            	var home 			= {}; 
            	var away 			= {}; 
            	var neutral 		= {};
                var yourAnswer			= json.fanZoneVoteTeamReceipt.yourAnswer; 						// 2
                	home.answer			= json.fanZoneVoteTeamReceipt.homeFansModeAnswer; 				// 2
                	home.percentage		= json.fanZoneVoteTeamReceipt.percentageHomeFansModeAnswer; 	// 33
                	neutral.answer 		= json.fanZoneVoteTeamReceipt.neutralFansModeAnswer; 			// 3
                	neutral.percentage	= json.fanZoneVoteTeamReceipt.percentageNeutralFansModeAnswer; 	// 42
                	away.answer			= json.fanZoneVoteTeamReceipt.awayFansModeAnswer; 				// 5
                	away.percentage		= json.fanZoneVoteTeamReceipt.percentageAwayFansModeAnswer; 	// 65

            	// Target Table Cells
            	this.el.homeTarget		= this.el.target.find('.home .result' + home.answer);
            	this.el.awayTarget		= this.el.target.find('.away .result' + away.answer);
            	this.el.neutralTarget	= this.el.target.find('.neutral .result' + neutral.answer);
            	this.el.yourTarget		= this.el.target.find('.'+this.data.fanGroup+' .result' + yourAnswer);

            	this.el.homeTarget.css({'background' : this.options.homecolor})
                		  .html(home.percentage    + "%");
            	this.el.awayTarget.css({'background' : this.options.awaycolor})
                		  .html(away.percentage    + "%");
            	this.el.neutralTarget.css({'background' : this.options.neutralcolor})
                		  .html(neutral.percentage + "%");
                
                // Set yourAnswer
                switch ( this.data.teamId ) {
	                case this.options.homeId:
	                	if ( home.answer == yourAnswer ) {
	                		this.el.homeTarget.html(spanBox+home.percentage+"%");
	                	} else {
	                		this.el.yourTarget.css({'background' : this.options.yourcolor});
	                	}
	                	break;
	                case this.options.awayId:
	                	if ( away.answer == yourAnswer ) {
	                		this.el.awayTarget.html(spanBox+away.percentage+"%");
	                	} else {
	                		this.el.yourTarget.css({'background' : this.options.yourcolor});
	                	}
	                	break;
	                default: //neutral
	                	if ( neutral.answer == yourAnswer) {
	                		this.el.neutralTarget.html(spanBox+neutral.percentage+"%");
	                	} else {
	                		this.el.yourTarget.css({'background' : this.options.yourcolor});
	                	}
	                	break;
                }                
                
                // Move to after options and slideDown
                this.el.target.slideDown();
                this.el.legend.fadeIn();
                
            } else if ( polltype === "vote" && json.fanZoneVoteSimpleReceipt) {
            	this.el.target.show();
                var	answer 		= {}; 
                	answer.your	= json.fanZoneVoteSimpleReceipt.yourAnswer; 				// 2
                	answer.a1	= json.fanZoneVoteSimpleReceipt.percentageVotedForAnswer1; 	// 10
                	answer.a2	= json.fanZoneVoteSimpleReceipt.percentageVotedForAnswer2; 	// 40
                	answer.a3	= json.fanZoneVoteSimpleReceipt.percentageVotedForAnswer3; 	// 20
                	answer.a4	= json.fanZoneVoteSimpleReceipt.percentageVotedForAnswer4; 	// 20
                	answer.a5	= json.fanZoneVoteSimpleReceipt.percentageVotedForAnswer5; 	// 10
            	
                this.el.target.find('.result1').text(answer.a1+"%");
                this.el.target.find('.result2').text(answer.a2+"%");
                this.el.target.find('.result3').text(answer.a3+"%");
                this.el.target.find('.result4').text(answer.a4+"%");
                this.el.target.find('.result5').text(answer.a5+"%");
                
                this.el.target.find('.result'+answer.your).attr({
                	'color' : this.options.yourcolor
                });
                
                // Add widget data to table
                this.el.target.find('table').attr({
            		'widget': 		'svgBarChart',
            		'svgheight': 	'180',
            		'svgwidth':		'710',
            		'showcollabel':	'false',
            		'barSpacing':	'1',
            		'barHeight':	'150'
            	});
                
            	// Initial SVG widget
            	$.initWidgets('table', this.element);
            	
            	// Show target with slideDown
            	this.el.target.hide().slideDown();
            	this.el.legend.fadeIn();
            }
        } else {
            // Receipt Error Text
        	this.el.target.empty().html('<p class="data-error">There was an error submitting your opinion</p>');
            // Move to after options and slideDown
        	this.el.target.insertAfter(this.element).slideDown();
        }
    },
    renderError: function( status, json ) {
    	
    	// Hide target to render error
    	this.el.target.fadeOut().empty();
    	
    	switch ( status ) {
    	case 404:
    		this.el.target.html('<p class="data-error">There was an error submitting your prediction:<br/>Not found</p><!--404-->');
    		break;
    	case 400:
    		this.el.target.html('<p class="data-error">There was an error submitting your prediction:<br/>Please try again later</p><!--400-->');
    		break;
    	case 403:
    		this.el.target.html('<p class="data-error">There was an error submitting your prediction:<br/>No submissions allowed</p><!--403-->');
    		break;
    	case 409:
    		this.el.target.html('<p class="data-error">There was an error submitting your prediction:<br/>You have already submitted</p><!--409-->');
    		break;
    	case 401:
    		this.el.target.html('<p class="data-error">There was an error submitting your prediction:<br/>Unauthorised. Please log in.</p><!--403-->');
    		break;
    	case 500:
    		this.el.target.html('<p class="data-error">There was a server error submitting your prediction</p><!--500-->');
    		break;
    	default:
    		this.el.target.html('<p class="data-error">There was a server error submitting your prediction</p>');
    	}

    	// Re-render target
    	this.el.target.fadeIn();
    }
    
    
});
/**
 *  File: etc/designs/premierleague/clientlibs/js/widgets/jquery.leagueTable.js
 *
 *
 * <script type="text/x-jquery-tmpl" class="leagueTable-P" render=".overview-title" ajax="false"></script>
 * <script type="text/x-jquery-tmpl" class="leagueTable-P" render=".overview-data"  ajax="true"></script>
 * 
 * <table class="leagueTable" matchurl="matches/" widget="leagueTable">
 * <td class="dataRow" template=".leagueTable-P" ajax="/ajax/league-table/date-timeline/expanded/2010-2011/02-02-2011/12/P.json">24</td>
 * <tr class="club-overview-row">
 *   <div class="overview-title"></div>
 *   <div class="overview-data"></div>
 * </tr>
 * </table>
 */
$.ui.basewidget.subclass('ui.leagueTable', {
    klass: "$.ui.leagueTable",
    options: {
        matchesurl:			null,
        clubsurl:			null,
        playersurl:			null,
        icalurl:			null,
		links:           	'.club-row td[ajax]',
		rows:            	'.club-row',
        targets:         	'.club-overview-row',
        spinnerSelector: 	'.spinner',
        selectedRowClass:   'selected-row',
        selectedCellClass:	'selected-cell',
        accentClasses:      'accent1 accent2 accent3 accent4 accent5 accent6 accent7 accent8 accent9',
        autoExpandTabshow:  false,
        autoExpandInit:     false,
        updateLogoSizes:    true
    },
    required: {
    	matchesurl:			String,
        links:           	String,
        targets:         	String,
        selectedRowClass:   String,
        selectedCellClass:	String,
        spinnerSelector: 	String
    },
    requiredElements: {
        templates: true,
        rows:      true,
        links:     true,
        targets:   true
    },

    _create: function() {
        this.matchesurl   = this.options.matchesurl;
        this.clubsurl     = this.options.clubsurl;
        this.playersurl   = this.options.playersurl;
        this.icalurl 	  = this.options.icalurl;
    	this.el.templates = $("script[type='text/x-jquery-tmpl']");
        this.el.links     = this.element.find(this.options.links);
        this.el.rows      = this.element.find(this.options.rows);
        this.el.targets   = this.element.find(this.options.targets);
        this.renderedSelector      = this.el.templates.map(function(){ return this.getAttribute("render"); }).get().uniq().join(", "); // $.map returns only an array like object, need to call .get() before .join()
        this.templateClassNamesAll = this.getTemplateClassNames("*");

        this.data         = this.parseHtmlTable();
        this.loaded       = false;
    },
    _init: function() {
        this.el.rows.bind("click", $.proxy(this.onClickTD, this));
        this.el.links.addClass("clickable");
        this.el.targets.hide();
        this.bindUIEvents();
        this.updateLogoSizes();
    },
    validateRequiredElements: function() {
        this._super();

        var self = this;
        var hasTemplate = _.memoize(function(selector) { return !!self.el.templates.filter(String(selector)).length;} );
        var missingTemplates = this.el.links.map(function() { return this.getAttribute("template"); })
                                            .filter(function() { return !hasTemplate(this); } )
                                            .toArray().uniq();
        if( missingTemplates.length ) {
            var missingSelector = _.map(missingTemplates, function(t) { return "[template='"+t+"']"; }).join(",");
            var missingLinks = this.el.links.filter( missingSelector ); 
            console.error(this.klass, ":validateRequiredElements(): missingTemplates: ", missingTemplates, " in missingLinks: ", missingLinks, " for this.el: ", this.el, " - this: ", this  );
        }
    },

    updateLogoSizes: function() {
        if( !this.options.updateLogoSizes ) { return; }

        for( var rowName in this.data.rows ) {
            $.loadImageSize( this.data.rows[rowName].logourl, $.proxy(function(rowName, width, height) {
                this.data.rows[rowName].logoheight = height;
                this.data.rows[rowName].logowidth  = width;
            }, this, rowName));
        }
    },

    /**
     *  _.memorize from underscore.js - keeps a cache of computed results
     *  @param  {String} selector
     *  @return {String} space separated list of classnames
     */
    getTemplateClassNames: _.memoize(function(selector) {
        return this.el.templates.filter(selector).map(function(){ return this.className.split(" "); }).toArray().uniq().sort().join(" ");
    }),

    bindUIEvents: function() {
        if( this.options.autoExpandTabshow ) {
            this.element.parents("[widget=tabs],[widget=pagetabs]").bind("tabsshow", $.proxy(function(event,ui) {
                if( !this.loaded && this.element.closest(ui.panel).length > 0 ) {
                    this.el.links.first().trigger("click"); 
                }
            }, this));
        }
        if( !this.loaded && this.options.autoExpandInit && this.element.is(":visible") ) {
            this.el.links.first().trigger("click"); 
        }
    },

    onClickTD: function( event ) {
        var self      = this;
        var node      = $(event.target).closest("[template]");
        var link      = $(event.target).closest(this.el.links);
        var row       = $(event.target).closest(this.el.rows);
        var target    = row.next(this.el.targets);

        var ajax               = node.attr("ajax");
        var templateSelector   = node.attr("template");
        var templates          = this.el.templates.filter( templateSelector );
        var spinnerTemplates   = this.el.templates.filter(this.options.spinnerSelector);
        var staticTemplates    = templates.filter("[static]");
        var ajaxTemplates      = templates.not(staticTemplates);
        var templateClassNames = this.getTemplateClassNames( templateSelector );

        // TODO: Add caching and preloading
        if( templates.length && link.length && row.length && node.length ) {
            this.el.targets.hide();
            target.find(this.renderedSelector).empty();
            target.removeClass( this.templateClassNamesAll ).addClass( templateClassNames );

            staticTemplates.each( $.proxy(this.render, this, {}, target) ); // template passed as third param
            
            this.el.rows.removeClass( this.options.selectedRowClass );
            this.el.rows.removeClass( this.options.accentClasses );
            row.addClass( this.options.selectedRowClass );
            link.siblings().removeClass( this.options.selectedCellClass );
            link.addClass( this.options.selectedCellClass );
            this.loaded = true;

            if( ajax ) {
                spinnerTemplates.each( $.proxy(this.render, this, {}, target) ); // template passed as third param

                var counter = ++$.ui.leagueTable.counter; // Semaphore to prevent multiple loading
                $.ajax({
                    type: "GET",
                    url:  ajax,
                    dataType: "json",
                    beforeSend: function( xhr ) {
                    },
                    success: $.proxy(function( json, xhr, status ) {
                        if( counter != $.ui.leagueTable.counter ) { return; }
                        json.matchesurl = this.matchesurl; 	// Add MatchesURL base location for linking
                        json.clubsurl   = this.clubsurl; 	// Add ClubsURL base location for linking
                        json.playersurl = this.playersurl; 	// Add PlayersURL base location for linking
                        json.icalurl 	= this.icalurl; 	// Add iCalURL base location for linking
                        spinnerTemplates.each( $.proxy(this.hide, this, {}, target) );
                        ajaxTemplates.each( $.proxy(this.render, this, json, target) ); // template passed as third param
                    }, this),
                    error: $.proxy(function( xhr, status ) {
                        //console.log('DEBUG: ', this&&this.klass||'' ,' error: function(  ', '  xhr: ',  xhr, ' status: ', status);
                        ajaxTemplates.each( $.proxy(this.renderError, this, null, target) ); // template passed as third param
                    }, this)
                });
            }
        }
    },
    render: function( json, target, index, template ) {
        var renderIn = target.find( template.getAttribute("render") ).empty();
        var rendered = $(template).tmpl(json).appendTo( renderIn );

        target.show();

        $.initWidgets(rendered);
    },
    renderError: function( html, target, index, template ) {
        html = html || "<div class='ajax-error'>Unable to load content</div>";
        
        var renderIn = target.find( template.getAttribute("render") ).empty();
        var rendered = $(html).appendTo( renderIn );
        target.show();
    },
    hide: function( json, target, index, template ) {
        var renderIn = target.find( template.getAttribute("render") ).empty();
    }
});
$.ui.leagueTable.counter = 0;


$.ui.leagueTable.subclass('ui.statsTabs', {
    klass: '$.ui.statsTabs',
    options: {
        matchesurl:	"",
        rows:       "ul.statsFatTabs",
        links:      "ul.statsFatTabs li[ajax]",
        targets:    "div.target",
        autoExpandTabshow:  true,
        autoExpandInit:     true
    },
    _create: function() {
        // Make links with <span class="data">0</span> unclickable
        this.el.links = this.el.links.filter(function() {
            return ($(this).find(".data").text() !== "0");
        });
    }
});
$.ui.basewidget.subclass('ui.leagueTableReset', {
    klass: '$.ui.leagueTableReset',
    options: {
        selector: 			null,
        selectedRowClass:   'selected-row',
        selectedCellClass:	'selected-cell'
    },
    required: {
        selector: 			String,
        selectedRowClass:   String,
        selectedCellClass:	String
    },
    _init: function() {
        this.element.bind( "click", $.proxy(this.onClick, this) );
    },
    onClick: function() {
    	this.element.closest(this.options.selector).hide();
    	
    	//Select table, remove selected classes and add accented classes
    	var tr = this.element.closest(this.options.selector).prev();
    	var table = tr.closest('table');
    	
    	tr.removeClass( this.options.selectedRowClass );
    	tr.children('td').removeClass( this.options.selectedCellClass );
    	    	
    	table.find('tr.club-row').each( function(i) {
			if ( i == 0 ) {
    			$(this).addClass( 'accent1' );
        	} else if ( i >= 1 && i <= 3 ) {
        	   	$(this).addClass( 'accent2' );
        	} else if ( i == 4 ) {
        	   	$(this).addClass( 'accent3' );
        	} else if ( i >= 17 ) {
        	   	$(this).addClass( 'accent4' );
        	} else {
        	    //do nothing
        	}
    	});
    	
        return false;
    }
});
// Use this as a javascript template
// /js/init.js - will automatically create the widget for you upon $(document).ready()
// @see /jcr_root/etc/designs/premierleague/clientlibs/js/libs/jquery.ui.subclass.js
// @see /jcr_root/etc/designs/premierleague/exlibs/js/jquery.jcarousel.js
// @see /jcr_root/etc/designs/premierleague/exlibs/js/jquery.jcarousel.extensions.js

$.ui.basewidget.subclass('ui.listToCarousel', {
    klass: '$.ui.listToCarousel',
    options: {
        visible:      5, 						// {Number}     number of visible <li> elements (clipping) defined by CSS
        scroll: 	  1,                        // {Number}     How many elements to scroll by when arrows are clicked
        wrap: 		  null,                     // {String}     Wrapping type (jcarousel option feed-in)
        nextinput:	  null,						// {String}		class of 'next' input control element in parent
        previnput:	  null,						// {String}	    class of 'prev' input control element in parent
        linked:       null                      // {String|jQuery} selectors for other jcarousel DOM nodes to also animate on scroll 
    },
    jcarousel: null, // {jCarousel} reference to the wrapped jCarousel object

    // Called from constructor before _init() – automatically calls this._super() before function
   _create: function() { 
        this.options = $.getAttributeHash( this.element, this.options );
    },
    // Called from constructor after _create() – automatically calls this._super() before function
    _init: function() {
        this.initCarousel();
    },
    initCarousel: function (arg) {
    	// Set base options
    	var options = {};
		options.scroll 	= this.options.scroll;
		options.wrap	= null; // this.options.wrap;
		options.visible = this.options.visible;
        options.linked  = $(this.options.linked);
    	
    	if( this.options.nextinput != null && this.options.previnput != null) {
    		options.buttonNextHTML = null;
    		options.buttonPrevHTML = null;
    		options.initCallback   = $.proxy( this.addInputs, this );
    	}

    	if (typeof $.fn.jcarousel === "function") {
	    	this.jcarousel = $(this.element).jcarousel( options ).data("jcarousel");
    	} else {
    		// Throw an error, dependent plug-in not detected
    		console.error(this.klass, ":initCarousel(): $.fn.jcarousel is not a function - ", $.fn.jcarousel, " - options: ", options, "- - this", this);
    	}
    	
    },
    addInputs: function( jcarousel ) {
    	$(this.options.nextinput).bind('click', function() {
			jcarousel.next();
			return false;
	    });
   		$(this.options.previnput).bind('click', function(){
    		jcarousel.prev();
    		return false;
    	});
    }
});
$.ui.basewidget.subclass('ui.localeDate', {
    klass: "$.ui.localeDate",
    options: {
        timestamp: null,  // {Number}  13 digit millisecond timestamp
        format:    null,  // {String}  Date Format, ie HH:mm or dd/MM/yy
        force:     null   // {Boolean} If true, don't check that existing text is a valid date
    },
    required: {
        timestamp: Number,
        format:    String
    },
    _init: function() {
        if( this.options.timestamp && this.options.format ) {
            this.text = this.element.text().trim();
            var formatRegexp = this.options.format.replace(/\w/g, '\\d');
            if( this.options.force || this.text.match(formatRegexp) ) {
                this.localeDateString = (new Date( this.options.timestamp )).toString( this.options.format );
                this.element.text( this.localeDateString );
            } else {
                if( !this.text.match(/\b(TODAY|LIVE|FT|HT|FHS|SHS)\b/) ) {
                    console.error( this.klass+":_init(): text: "+this.text+" does not match format: "+this.options.format + " regexp: "+formatRegexp );
                }
            }
        }
    }
});
/**
 * @example
 *  <div class="galleryCarousel" widget="galleryCarousel">
 *      <ul widget="listToCarousel" visible="1" id="gallerydir_<%=uuid+1%>"> </ul>
 *      <ul widget="listToCarousel" visible="6" scroll="6" wrap="circular" id="#gallerydir_<%=uuid+2%>" class="thumbs"> </ul>
 *  </div>
 *
 *  @see /jcr_root/etc/designs/premierleague/exlibs/js/jquery.jcarousel.js
 *  @see /jcr_root/etc/designs/premierleague/exlibs/js/jquery.jcarousel.extensions.js
 */
$.ui.basewidget.subclass('ui.galleryCarousel', {
    klass: '$.ui.galleryCarousel',
    options: {
        childWidgetSelector: "*[widget=listToCarousel]"
    },
    required: {
        childWidgetSelector: String
    },
    _create: function() {
        this.el.carousels     = this.element.find(this.options.childWidgetSelector);
        this.el.imageCarousel = this.el.carousels.eq(0);
        this.el.thumbCarousel = this.el.carousels.eq(1);

        if( this.el.carousels.length != 2 ) {
            console.warn(this.klass+":_create() - options.childWidgetSelector = ", this.options.childWidgetSelector, " needs to match exactly 2 nodes. this.el.carousels = ", this.el.carousels );
        }
    },
    _init: function() {
        // Wait for child widgets to initalize before continuing
        setTimeout($.proxy(this.__init, this), 0);
    },
    __init: function() {
        try {
            // Remember that we need .data("widget").jcarousel to access the jcarousel object
            this.imageWidget = this.el.imageCarousel.data("widget");
            this.thumbWidget = this.el.thumbCarousel.data("widget");
            this.imageCarousel = this.imageWidget.jcarousel;
            this.thumbCarousel = this.thumbWidget.jcarousel;
            
            this.addHighlightHandler();
            this.addThumbClickHandler();
            this.addViewScrollHandler();
            this.addCaptionSlideOnHover();
        } catch(e) {
            console.error(this.klass,":__init(): exception: ", e );
        }
    },

    /**
     * onClick for thumb LI should scroll the imageWidget 
     */
    addThumbClickHandler: function() {
        var self = this;
        this.thumbCarousel.getLIs().each(function(index, node) {
            $(this).bind("click", function() {
                var index = $.jcarousel.getIndexOfNode(this);
                self.imageCarousel.scroll(    index, true );
                self.thumbCarousel.highlight( index, true );
            });
        });
    },
    addHighlightHandler: function() {
        var self = this;
        var _imageCarouselScroll = this.imageCarousel.scroll;
        this.imageCarousel.scroll = function(index, animate) {
            _imageCarouselScroll.apply(this, arguments);
            self.thumbCarousel.highlight(index);
        };
        this.thumbCarousel.highlight(this.thumbCarousel.options.start);
    },
    addViewScrollHandler: function() {
        var self = this;
        var _imageCarouselScroll = this.imageCarousel.scroll;
        this.imageCarousel.scroll = function(index, animate) {
            _imageCarouselScroll.apply(this,arguments);

            if( index < self.thumbCarousel.first ) {
                if( index < self.thumbCarousel.first - self.thumbCarousel.options.scroll ) {
                    self.thumbCarousel.scroll(index);
                } else {
                    self.thumbCarousel.scroll(self.thumbCarousel.first - self.thumbCarousel.options.scroll);
                }
            } else if( index > self.thumbCarousel.last ) {
                self.thumbCarousel.scroll(index);
            }
        };
    },

    addCaptionSlideOnHover: function() {
        // Logic:
        // Mouseover LI or .overlay - slideUp if hidden or queue slideUp if slidingDown
        // Mouseout  LI or .overlay - slideDown if fully up, check mouseout:li isn't really a mouseover:.overlay

        var self = this;
        self._overlayMouseoutId    = null;
        self._overlayMouseoverId   = null;
        self._overlayAnimatingUp   = false;
        self._overlayAnimatingDown = false;
        this.imageCarousel.getLIs().bind("mouseover", function(event) {
            clearTimeout( self._overlayMouseoutId  );  // kill bubbling mouseout event
            clearTimeout( self._overlayMouseoverId );  // kill duplicate mouseover event

            var overlay = $(this).find(".overlay");
            self._overlayMouseoverId = setTimeout(function() { // create a new thread, for visual smoothness
                if( overlay.is(":hidden") && !self._overlayAnimatingUp ) {
                    self._overlayAnimatingUp = true;
                    overlay.effect("slide", {direction: "down", mode: "show"}, "normal", function() {
                        self._overlayAnimatingUp = false;
                    });
                }
            }, 100);
        });
        this.imageCarousel.getLIs().bind("mouseout", function(event) {
            var overlay = $(this).find(".overlay");
            self._overlayMouseoutId = setTimeout(function() { // check for non-bubbling mouseout event
                if( overlay.is(":visible") && !self._overlayAnimatingDown ) {
                    self._overlayAnimatingDown = true;
                    overlay.effect("slide", {direction: "down", mode: "hide"}, "slow", function() {
                        self._overlayAnimatingDown = false;
                    });
                }
            }, 100);
        });
    }
    
});

// TODO: this class should subclass tabs, rather than be a wrapper - James
$.ui.basewidget.subclass('ui.standardtabs', {
    klass: '$.ui.standardtabs',
    options: {
        hash:          null        // {Hash}         define objects and arrays within the constructor, else it will create a class variable
    },

    // Called from constructor before _init() – automatically calls this._super() before function
   _create: function() { 
        this.options.hash = {};
        this.options = $.getAttributeHash( this.element, this.options );
    },

    // Called from constructor after _create() – automatically calls this._super() before function
    _init: function() {
        this.addTabs(); 
    },
    addTabs: function() {
        //no options required at this point. Extend if required.
        $(this.element).tabs();
    }
    
});
$.ui.basewidget.subclass('ui.hideParent', {
    klass: '$.ui.hideParent',
    options: {
        selector: null
    },
    required: {
        selector: String
    },
    _init: function() {
        this.element.bind( "click", $.proxy(this.onClick, this) );
    },
    onClick: function() {
        this.element.closest(this.options.selector).hide();
        return false;
    }
});

$.ui.widget.subclass('ui.expandlink', {
    klass: '$.ui.expandlink',
    options: {
        hash:         null     // {Hash}         define objects and arrays within the constructor, else it will create a class variable
    },

    create: function() { 
        this.options.hash = {};
        this.options = $.getAttributeHash( this.element, this.options );
    },

    _init: function() {
        this.addHover();
        this.addClick();
    },

    
    addClick: function () {
    	$(this.element).bind('click',function(){
    		var dest = $(this).find('a:first').attr('href');
    		window.location.href = dest;
    		return false;
    	});
    },
    
    addHover: function(){
    	$(this.element).bind("mouseenter mouseout",function(event){
    		if (event.type == "mouseenter") {
    			$(this).addClass("hover");
    		} else {
    			$(this).removeClass("hover");
    		}
    	});
    }
});
$.ui.basewidget.subclass('ui.fixtureSearch', {
    klass: '$.ui.fixtureSearch',
    options: {
        hash:         null,    // {Hash}         define objects and arrays within the constructor, else it will create a class variable
        dayNamesMin : ['S','M','T','W','T','F','S'] // {Array} 
    },
    // Called from constructor before _init() – automatically calls this._super() before function
   _create: function() { 
        this.options.hash = {};
        this.options = $.getAttributeHash( this.element, this.options );
    },
    // Called from constructor after _create() – automatically calls this._super() before function
    _init: function() {
        this.initDatepickers();
        this.initDateRangeSelection();
        this.getState();
        this.onMonthChange();
        this.monthHighlight();
        this.onDayChange();
        this.dayHighlight();
		this.changeDateSelection();
		
		this.element.find('input').change();
		this.element.find('select').change();
    },
    /**
     * @deprecated
     */
    initDatepickers: function () {
    	// quit early
    	return true;
    },
    initDateRangeSelection : function(){
    	var that = this;
    	$(':radio:checked','#range-types').live("change",function(){
        	console.log($(this).val());
    		var act = $(this).val();
        	that.swapView(act);
        });
    },
    getState: function() {
    	var act = $(':radio:checked','#range-types').val();
    	if (act == null) {
    		var act = $('#range-types option:selected').val();
    	}
    	this.swapView(act);
    	//insert other state-initialisation here
    },
    onMonthChange: function() {
    	var that = this;
    	$('.dateMonth input','#range-options').live('change',function(){
    		that.monthHighlight();
    	});
    },
    monthHighlight: function() {
    	var $sel =  $('.dateMonth :radio:checked','#range-options');
		$('.dateMonth label','#range-options').removeClass('on');
		$sel.closest('li').find('label').addClass('on');
		// hide + disable week's day drop-down
		$('.dateMonth select').hide().removeAttr('selected').attr('disabled','disabled');
		$sel.closest('li').find('select').removeAttr('disabled').show();
    },
    onDayChange: function() {
    	var that = this;
    	$('.dateDays input','#range-options').live('change',function(){
    		that.dayHighlight();
    	});
    },
    dayHighlight: function() {
    	var $sel =  $('.dateDays :radio:checked','#range-options');
    	$('.dateDays label','#range-options').removeClass('on');
    	$sel.closest('li').find('label').addClass('on');
    },
    changeDateSelection: function() {
    	$('.dateMonth select').live('change',function(){
    		$('.dateMonth select option').removeClass('selected');
    		$('.dateMonth select :selected').addClass('selected');
    		$(this).attr("name", "dateSelected");
    	});
    },
    swapView: function(act) {
    	if (act === '.dateSeason') {
    		$(act+' :checked')
    			.removeAttr('checked')
    			.closest('ul')
    			.find('label.on')
    			.removeClass('on');
    		$('#range-options').children().not(act)
    			.slideUp('fast', function() {
    			//Disable unwanted inputs and selects
    			$('#range-options input').attr('disabled','disabled');
    		});
    	} else {
    		var today = new Date();
    		$('label[for="month'+(today.getMonth() + 1)+'"]').click();
    		$('#range-options input')
    			.removeAttr('disabled');
    		$('#range-options').children()
    			.not(act)
    			.removeAttr('selected')
    			.attr('disabled','disabled');
    		$(act,'#range-options').slideDown('fast');
    	}
    }
});

$.ui.fixtureSearch.subclass('ui.broadcastSearch', {
    klass: '$.ui.broadcastSearch',
    
    initDateRangeSelection : function(){
		var that = this;
	    $("#range-types").live("change",function(){
	    	var act = $(this).val();
	    	that.swapView(act);
	    });
	},
    getState: function() {
		var that = this;
	    $("#range-types").live("change",function(){
	    	var act = $(this).val();
	    	that.swapView(act);
	    });
	},
	swapView: function(act) {
    	if (act === '.dateSeason') {
    		$(act+' :checked')
    			.removeAttr('checked')
    			.closest('ul')
    			.find('label.on')
    			.removeClass('on');
    	} else {
    		var today = new Date();
    		$('label[for="month'+(today.getMonth() + 1)+'"]').click();
    	}
    	// Use callback to init SlideDown once SlideUp complete
    	$('#range-options').children().not(act)
    		.slideUp('fast', function() {
    			//Disable unwanted inputs and selects
    			$('#range-options option').removeAttr('selected');
    			$('#range-options select').removeAttr('selected').attr('disabled','disabled');
    			//Slide down new options
    			$(act,'#range-options').slideDown('fast');
    	});
    }
});
/**
 *  Class extended by templateTabs.js
 *
 *  If cqEditMode is on, we render the meganav visible and inline, and disable all other links
 *  If in preview or publish, we render meganav hidden and as a position:absolute flyout
 *
 *  TODO: ipad/touch spec to be defined/tested
 */
$.ui.basewidget.subclass('ui.meganav', {
    klass: "$.ui.meganav",
    options: {
        target:          '#meganav',   // {String}  selector for target
        links:           'li',         // {String}  selector for links
        ajaxLinks:       'li[ajax]',   // {String}  selector for ajaxLinks
        prefix:          'meganav',    // {String}  css class prefix for flyouts, <flyout class="$prefix-inner $prefix-$name">
        selectedClass:   'active',     // {String}  css selected class for links
        closeButtonClass:'close-icon', // {String}  css class for close button
        expandEvent:     'mouseenter.megaexpand',                  // {String}  event(s) to bind on links for meganav flyout
        collapseEvent:   'mouseleave.megaexpand click.megaexpand', // {String}  event(s) to bind on links for meganav flyout
        collapseTimeout: 500,          // {Number}  ms after mouseout to close the flyout
        nocache:         false,        // {Boolean} always reload the flyouts via ajax, mostly for development
        preload:         true,         // {Boolean} ajax preload the content of all links on document.ready
        editMode:        null,         // {Boolean} are we in cqEditMode with an editable dropdown, null for auto
        renderInline:    false         // {Boolean} render inline, just like edit mode, for development purposes
    },
    required: {
        target: String,
        ajaxLinks:  String
    },
    counter: 0,            // {Number}  Semaphore to ensure that only the last click is rendered
    _closeTimeoutId: null, // {Number}  [private] setTimeout id for collapseEvent

    //*** Init ***//
    _create: function() {
        // TODO: touch untested
        //if( Modernizr && Modernizr.touch ) {
        //    this.options.expandEvent   = "touchstart";
        //    this.options.collapseEvent = "click";
        //}

        this.el = {};
        this.el.links         = this.element.find(this.options.links).not(this.options.target);
        this.el.ajaxLinks     = this.element.find(this.options.ajaxLinks).not(this.options.target);
        this.el.target        = $(this.options.target).eq(0);
        this.el.closeButton   = $([]);
        this.el.flyouts       = this.el.target.children().not(this.el.closeButton);
        this.el.editFlyout    = ($.cqEditMode) ? this.el.target.children(".meganav-initial") : $([]);
        this.options.editMode = (this.options.editMode === null) ? $.cqEditMode : this.options.editMode;

        this._onMouseEnter = $.proxy(this._onMouseEnter, this);
        this._onMouseLeave = $.proxy(this._onMouseLeave, this);
        this._onBodyClick  = $.proxy(this._onBodyClick,  this);
    },
    _init: function() {
        if( this.options.editMode ) {
            // You can't edit a component if clicking on it takes you to another page
            // CQ may add edit the content after page load
            this.el.target.find("a").bind("click", function(event){ event.preventDefault(); });
            $(".cq-wcm-edit .meganav .column a, " +
              ".cq-wcm-edit .meganav .row    a").live("click", function(event) { 
                event.preventDefault(); 
            });
        } else {
            if( this.options.preload ) {
                this.preloadAll();
            }

            if( this.options.renderInline ) {
                this.renderInline();
            } else {
                this.hide();
            }
            this.bindMouseEvents();
        }
        this.addLinkHoverStates();
    },
    destroy: function() {
        this.unbindMouseEvents();
        this.unbindBodyClickEvent();
        this.close();
    },

    renderInline: function() {
        this.options.renderInline = true;
        if( this.el.links.filter(".currentpage").length ) {
            this.load( this.el.links.filter(".currentpage") );
        }
    },

//    /**
//     *  @unused, intended for touch interface
//     */
//    addCloseButton: function() {
//        if( Modernizr && Modernizr.touch ) {
//            this.el.closeButton = $("<div class='"+this.options.closeButtonClass+"'>").prependTo(this.el.target);
//            this.el.closeButton.bind("click", $.proxy(this.hide, this));
//        }
//    },


    //*** Getters / Setters ***//

    getFlyoutWrapper: function( url, name ) {
        var flyout = this.el.target.children("[url='"+url+"']");
        if( this.options.nocache ) {
            this.el.flyouts = this.el.flyouts.not(flyout).jQueryGC();
            flyout.remove();
        }
        if( flyout.length === 0 ) {
            flyout = $("<div class='"+this.options.prefix+"-inner "+this.options.prefix+"-"+name+"' url='"+url+"'></div>").appendTo( this.el.target );
        }
        this.el.flyouts = this.el.flyouts.add(flyout);
        return flyout;
    },
    isFlyoutRendered: function( flyout ) {
        return !flyout.text().match(/^\s*$/);
    },


    //*** Actions ***//

    hide: function() {
        this.el.ajaxLinks.removeClass(this.options.selectedClass);
        this.el.flyouts.hide();
        this.el.closeButton.hide();
    },
    close: function( delay ) {
        delay = delay || 10; // 10ms is enough time for this.el.target:mouseenter to trigger
        if( this._closeTimeoutId ) { 
            clearTimeout( this._closeTimeoutId ); 
        }
        this._closeTimeoutId = setTimeout($.proxy(function() {
            this.hide();
            this.unbindBodyClickEvent();
        }, this), delay);
    },
    cancelClose: function() {
        if( this._closeTimeoutId ) { 
            clearTimeout( this._closeTimeoutId ); 
        }
    },
    show: function( flyout ) {
        this.cancelClose();
        var url = flyout.attr("url");

        this.hide();
        this.el.ajaxLinks.filter("[ajax='"+url+"']").addClass(this.options.selectedClass);
        this.el.closeButton.show();
        flyout.show();
    },
    
    

    //*** Render Actions ***//

    /**
     *  @param  {jQuery} flyout   flyout node to render in
     *  @param  {String} html     html to render
     *  @return {jQuery}          node of html rendered
     */
    render: function( flyout, html ) {
        var node = this.renderHidden(flyout, html);
        this.show( flyout );
        return node;
    },
    renderHidden: function( flyout, html ) {
        flyout.hide().empty();
        var node = $(html).appendTo( flyout );
        return node;
    },
    renderPreload: function( flyout, html ) {
        flyout.empty();
        var node = $(html).appendTo( flyout );
        return node;
    },


    //*** Bind / Unbind Event Handlers ***//

    addLinkHoverStates: function() {
        var self = this;
        this.el.links.hover(
            function(event) {
                // @context {this.element}
                var node = event.target;
                self.element.addClass("hovering");
                self.el.links.removeClass("hover");
                $(node).closest(self.el.links).addClass("hover");
            },
            function(event) {
                // @context {this.element}
                self.element.removeClass("hovering");
                self.el.links.removeClass("hover");
            }
        );
    },
    bindBodyClickEvent: function() {
        if( this.options.collapseEvent.match(/click/) && !this.options.renderInline ) {
            this.unbindBodyClickEvent();
            $(document.body).bind("click", this._onBodyClick);
        }
    },
    unbindBodyClickEvent: function() {
        if( this.options.collapseEvent.match(/click/) ) {
            $(document.body).unbind("click", this._onBodyClick);
        }
    },
    bindMouseEvents: function() {
        if( this.options.collapseEvent.match(/mouseleave/) ) {
            this.unbindMouseEvents();
            $([]).add(this.el.target).add(this.element).bind(  "mouseleave", this._onMouseLeave);
            $([]).add(this.el.target).add(this.el.links).bind( "mouseenter", this._onMouseEnter);
        }
    },
    unbindMouseEvents: function() {
        if( this.options.collapseEvent.match(/mouseleave/) ) {
            $([]).add(this.el.target).add(this.el.links).unbind( "mouseenter", this._onMouseEnter);
            $([]).add(this.el.target).add(this.element).unbind(  "mouseleave", this._onMouseEnter);
        }
    },


    //*** Events ***//
    /**
     *  this.el.links:mouseenter - open nav, close others
     *  this.el.links:mouseleave - close nav, unless this.target.mouseenter
     *  this.el.links:mouseenter - cancel close
     *  this.el.links:mouseleave - set timeout to close
     *  $(body):click - close
     */
    _onMouseEnter: function( event ) {
        var node        = $(event.currentTarget); // this.el.ajaxLink[] or this.el.target
        var eventTarget = $(event.target);        // $("a") or $("span") etc

        if( eventTarget.closest(this.el.ajaxLinks).length ) {
            this.load( node );
        }
        else if( eventTarget.closest(this.el.links).not(this.el.ajaxLinks).length ) {
            this.close();
        }
        else if( eventTarget.closest(this.el.target).length ) {
            this.cancelClose();
        }
    },
    _onMouseLeave: function( event ) {
        var node        = $(event.currentTarget); // this.element[] or this.el.target
        var eventTarget = $(event.target);        // $("a") or $("span") etc

        var node = this; // this.element or this.el.target
        if( !this.options.renderInline ) {
            this.close( this.options.collapseTimeout ); // add a delay
        }
    },
    _onBodyClick: function( event ) {
        var node        = $(event.currentTarget); // $(document.body)
        var eventTarget = $(event.target);        // $("a") or $("span") etc

        if( eventTarget.closest(this.el.ajaxLinks).length === 0
         && eventTarget.closest(this.el.target).length    === 0 ) {
            this.close();
        }
    },


    load: function( node ) {
        this.cancelClose();
        this.bindBodyClickEvent(); // $(body) events are rebound on each load/close, 
                                   // mouseenter/mouseleave events are bound once on init
       
        var url    = $(node).attr("ajax");
        var name   = $(node).attr("name");
        var flyout = this.getFlyoutWrapper( url, name );

        if( this.isFlyoutRendered(flyout) ) {
            this.show(flyout);
        } else {
            this.render( flyout, "<div class='spinner'><p>...</p></div>" );

            var counter = ++this.counter;
            $.ajax({
                type: "GET",
                url:  url,
                dataType: "text",
                success: $.proxy( function( html, xhr, status ) {
                    if( counter === this.counter ) {
                        this.render( flyout, html );
                    } else {
                        this.renderHidden( flyout, html );
                    }
                }, this),
                error: $.proxy( function( xhr, status ) {
                    if( counter !== this.counter ) { return; }

                    var html = "<div class='ajax-error'>Unable to load navigation content</div>";
                    this.render( flyout, html );

                    // Failsafe fallback - if content cannot be loaded, click the link - TODO: Confirm Spec
                    //var href = node.findAndSelf("a[href]").attr("href");
                    //if( href ) { document.location.assign(href); }
                }, this)
            });
        }
    },
    preloadAll: function() {
        for( var i=0, n=this.el.ajaxLinks.length; i<n; i++ ) {
            this.preload( this.el.ajaxLinks[i] );
        }
    },
    preload: function( node ) {
        var url    = $(node).attr("ajax");
        var name   = $(node).attr("name");
        var flyout = this.getFlyoutWrapper( url, name );

        if( !this.isFlyoutRendered(flyout) ) {
            $.ajax({
                type: "GET",
                url:  url,
                dataType: "text",
                success: $.proxy( function( html, xhr, status ) {
                    this.renderPreload( flyout, html );
                }, this)
            });
        }
    }
});
$.ui.basewidget.subclass('ui.slidetoggle', {
    klass: '$.ui.slidetoggle',
    options: {
		controller:  null,		// {String} Clickable slide controller class
		target:		 null,		// {String} Target object to slide up/down
		toggleclass: null		// {String} Toggle controller class
    },
    required: {
    	controller: String
    },
    _create: function() { 
        this.options.hash = {};
        this.target = this.options.target || this.element;
    },
    _init: function() {
        this.doToast();
    },
    doToast: function() {
    	var self = this;
    	var toggleClass = self.options.toggleclass;
    	
    	// find Controller element in object widget applied to
    	this.element.find( this.options.controller ).click(function() {
    		$t = $(this);
    		// find Target element and add slideToggle
    		if ( self.target == self.element ) {
    			self.target.slideToggle();
    		} else {
    			self.element.find( self.target ).slideToggle();
    		}
    		// toggle 
    		if (toggleClass) {
    			if ( $t.hasClass( toggleClass ) ) {
    				$t.removeClass( toggleClass );
    			} else {
    				$t.addClass( toggleClass );
    			}
    		}
	    });
    }
});
/**
 * 		tableswap widget.
 * 
 * 		Usage: apply to a <div> element which is parsed for tables to be swapped.
 * 			   A select box is created based on titles and labels provided.
 * 
 * 	    Required: a unique 'name' attribute for each table, and a 'title' class 
 * 			   applied to a table heading tag for display in the dropdown
 *
 * 		Example:			   
 *		<div widget="tableswap" label="View type:">
 *			<!-- dynamically inserted <p> here --->
 *			<table name="table-id-1">
 *				<tr><th class="title">Type 1</th></tr>
 *				<tr><td>Data1</td><td>Data2</td></tr>
 *			</table>
 *			<table name="table-id-2">
 *				<tr><th class="title">Type 2</th></tr>
 *				<tr><td>Data1</td><td>Data2</td></tr>
 *			</table>
 *		</div>
 */

$.ui.widget.subclass('ui.tableswap', {
    klass: '$.ui.tableswap',
    options: {
		label:		  '',
        maxPlayers:   9,       // {Number}       maximum number of players in list as per design
        hash:         null     // {Hash}         define objects and arrays within the constructor, else it will create a class variable
    },

    // Called from constructor before _init() – automatically calls this._super() before function
   _create: function() { 
        this.options.hash = {};
        this.options = $.getAttributeHash( this.element, this.options );
    },

    // Called from constructor after _create() – automatically calls this._super() before function
   _init: function() {
        //this.doSomething('players to watch init' );
        this.addSelect();
        this.addEvents();
        this.onChange();
    },
   
    addSelect: function (args) {
    	var tables = this.element.find('table[name]');

    	var data = {};
    	for( var i=0, n=tables.length; i<n; i++ ) {
    		var name  = $(tables[i]).attr('name');
    		var title = $('th.title', tables[i]).text();

    		if( title && name ) {
    			data[name] = title;
    		}
    	}
    	
    	var html = "<p class='select'>" + this.options.label + "<select>";
    	for( var name in data ) {
    		var title = data[name]; 
    		html += "<option value='"+name+"'>"+title+"</option>";
    	}
    	html += "</select></p>";
    	
    	this.element.prepend(html);
    	this.select = this.element.find("select");
    },
    
    addEvents: function (args) {
    	$('select',this.element).live('change', $.proxy(this.onChange,this));
    },
    onChange: function() {
		var showClass = this.select.val();
		$('table:not([name='+showClass+'])',this.element).hide();
		$('table[name='+showClass+']',this.element).fadeIn( 400 );    	
    }
});
/**
 *  <div widget="templateTabs">
 *      <ul>
 *          <li name="match" template=".match" ajax="./match.json"></li>
 *      </ul>
 *      <div class="templates">
 *          <div class="match">
 *          </div>
 *      </div>
 *      <div class="target">
 *      </div>
 *  </div>
 */
$.ui.meganav.subclass('ui.templateTabs', {
    klass: "$.ui.templateTabs",
    options: {
    	templates:     'script',
        target:        '.target',
        links:         'li[ajax]',
        prefix:        'templateTabs',
        selectedClass: 'selected',
        renderInline:  true,
        nocache:       false,
        autoselect:    0
    },
    _create: function() {
    	this.el.templates = this.options.templates.match(/#/) ? $(this.options.templates) : this.element.find(this.options.templates);
        this.el.links     = this.element.find(this.options.links ).not(this.options.target)
        this.el.target    = this.element.find(this.options.target).eq(0);
    },
    _init: function() {
        //if( typeof this.options.autoselect == "number" ) {
        //    this.el.links.eq(this.options.autoselect).trigger("click");
        //}
    },
    _onBodyClickEvent:    function() {},
    bindBodyClickEvent:   function() {},
    unbindBodyClickEvent: function() {},
    
    renderTemplate: function( flyout, template, json ) {
        this.el.target.children().not( this.el.links.parents() ).hide();
        this.el.templates.filter(template).tmpl(json).appendTo(flyout.empty());
        $.initWidgets(flyout);
        flyout.show();
    },
    _onClick: function( node ) {
        var url              = node.getAttribute("ajax");
        var name             = node.getAttribute("name");
        var templateSelector = node.getAttribute("template");
        var flyout           = this.getFlyoutWrapper( url, name );

        if( this.isFlyoutRendered(flyout) ) {
            this.show(flyout);
        } else {
            this.render( flyout, "<div class='spinner'><p>...</p></div>" );

            var counter = ++this.counter;
            $.ajax({
                type: "GET",
                url:  url,
                dataType: "json",
                success: $.proxy( function( json, xhr, status ) {
                    if( counter === this.counter ) {
                        this.renderTemplate( flyout, templateSelector, json );
                    }
                }, this),
                error: $.proxy( function( xhr, status ) {
                    if( counter !== this.counter ) { return; }

                    var html = "<div class='ajax-error'>Unable to load content</div>";
                    this.render( flyout, html );
                }, this)
            });
        }
    }
});
$.ui.basewidget.subclass('ui.slideshow', {
    klass: "$.ui.slideshow",
    options: {
        fadeClick:    0,   // {Number} milliseconds for fade when clicked
        fadeCycle:  500,   // {Number} milliseconds for fade when cycling
        cycleTime: 5000,   // {Number} milliseconds between fades
        clickCycleMultiplier: 1.5 // {Number} extra delay after clicking, before next cycle
    },
    required: {
        fadeClick: Number,
        fadeCycle: Number
    },
    position:      0,  // {Number} position of the currently selected image
    _eventCounter: 0,  // {Number} event counter
    
    _create: function() {
        this.el = {};
        this.el.imageWrapper = this.element.find(".image-wrapper");
        this.el.images       = this.el.imageWrapper.find(".image");
        this.el.buttons      = this.element.find(".buttons li");
    },
    _init: function() {
        var self = this;
        this.el.buttons.bind("mouseover", function() {
            // @context this refers to the li DOM node we clicked on
            var position = Number(this.getAttribute("position"));
            self.selectImage( position, self.options.fadeClick, self.options.cycleTime*self.options.clickCycleMultiplier );
            return false;
        });
        
        this.cycleLoop();
        //$.loggedInState();
    },
    cycleLoop: function( nextCycleTime ) {
        var self = this;
        if( this._cycleLoopTimeoutId ) {
            clearTimeout( this._cycleLoopTimeoutId );   
        }
        this._cycleLoopTimeoutId = setTimeout( function() {
            // self.cycleLoop(); // Called from within selectImage()
            self.selectNextImage( self.options.fadeCycle );
        }, nextCycleTime || this.options.cycleTime );       
    },
    stopCycleLoop: function() {
        if( this._cycleLoopTimeoutId ) {
            clearTimeout( this._cycleLoopTimeoutId );   
        }       
    },
    
    selectNextImage: function( fadeTime ) {
        var nextPosition = (this.position+1) % this.el.images.length;
        this.selectImage( nextPosition, fadeTime ); 
    },    
    selectImage: function( position, fadeTime, cycleTime ) {
        if( this.position != position ) {
            this.position = position;
            
            var oldImage = this.el.images.filter(":visible");
            var newImage = this.el.images.filter("[position='"+position+"']");          
            var button   = this.el.buttons.filter("[position='"+position+"']");
                
            // Highlight button on click
            this.el.buttons.removeClass("selected");
            button.addClass("selected");
            
            var _eventCounter = ++this._eventCounter; // fadeOut Semaphore 
            oldImage.fadeOut( fadeTime, $.proxy(function() {
                if( _eventCounter != this._eventCounter ) { return; } // Only fade in the last click
                
                newImage.fadeIn(fadeTime, $.proxy(function() {
                    this.cycleLoop( cycleTime );
                     
                    //// Highlight button after fadeOut
                    //this.el.buttons.removeClass("selected");
                    //button.addClass("selected");                  
                },this));
            }, this));
        }
    }    
});
/**
 *  Requires: <script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false">
 *
 *  Widget not quite complete, but we have a basic google maps widget
 *  @see http://code.google.com/apis/maps/documentation/javascript/reference.html
 *  @see http://code.google.com/apis/maps/documentation/javascript/services.html#DirectionsResults
 *
 *  Still needs cleaning up and working out how to properly render directions
 */
$.ui.basewidget.subclass('ui.googlemaps', {
    klass: '$.ui.googlemaps',
    options: {
      zoom:   15,
      mapTypeId: (typeof google !== 'undefined') ? google.maps.MapTypeId.HYBRID : "-1",
      travelMode: "walking"
    },
    _create: function() {
        this.stadium       = {};
        this.el.map        = this.element.find(".map");
        this.el.directions = this.element.find(".directions");
        this.el.travelMode = this.element.find(".travelMode");
        this.el.form       = this.element.find("form");
        this.el.input      = this.element.find("form input[type=text]");
        this.defaultValue = this.el.input.val();
            
     },
    _init: function() {
        if (typeof google !== 'undefined') {
        	this.draw();
	        this.bindEventHandlers();
	        
	        this.directionsService = new google.maps.DirectionsService();
	        this.directionsDisplay = new google.maps.DirectionsRenderer({ map: this.map });
	        this.stepDisplay       = new google.maps.InfoWindow();
	        this.markerArray = [];
        }
    },
    draw: function() {
        this.stadium.point = new google.maps.LatLng(this.options.lat, this.options.lon);
        
        this.map = new google.maps.Map(this.el.map[0], {
              zoom:      this.options.zoom,
              center:    this.stadium.point,        
              mapTypeId: this.options.mapTypeId
        });
        this.stadium.marker = new google.maps.Marker({
            position:  this.stadium.point, 
            map:       this.map,
            animation: google.maps.Animation.DROP
        });
        this.stadium.info = new google.maps.InfoWindow({
            minHeight:  100,
            content:   "<b>Emirates Stadium.</b><br/>England, United Kingdom<br/> Population: 0<br/>Elevation: 0 <br/>Time zone: Europe/London"
        });
        var $tM = $.el('input').attr({type:'hidden',id:'travelMode'}),
        	$tD = $.el('span').addClass('driving'),
        	$tW = $.el('span').addClass('walking'),
        	$tS = $.el('span').addClass('travelMode'); 
        this.el.form.append($tM).prepend($tS.append($tD).append($tW));
        
    },
    bindEventHandlers: function() {
        $(this.el.form).bind("submit", $.proxy(this._onSubmit,this));
        google.maps.event.addListener(this.stadium.marker, 'click', $.proxy(this._onClickMarker, this));
        
        $('input.location',this.el.form).live('focus',function(){
        	$t = $(this);
        	$t.addClass('active');
        	$t.val(($t.val() != this.defaultValue) ? $t.val() : "");
        });
        $('input.location',this.el.form).live('blur',function(){
        	$t = $(this);
        	
        	$t.val(($t.val() != "") ? $t.val() : this.defaultValue); 
        	if ($t.val() == this.defaultValue) {
        		$t.removeClass('active');
        	}
        });
        $('.travelMode span',this.el.form).live('click',function(){
        	var d = $(this).hasClass('driving');
        	var act = (d) ? 'driving' : 'walking';
        	var rem = (!d) ? 'driving' : 'walking';
        	$(this).parent().addClass(act).removeClass(rem);
        	$('#travelMode').val(act);
        });
        $('.travelMode span.'+this.options.travelMode).click();
        
    },
    // @see http://code.google.com/apis/maps/documentation/javascript/services.html#DirectionsResults
    _onSubmit: function() {
        try {
            var location = this.el.input.val();
            if( !location.match(/^\s*$/) ) {
                this.directionsService.route({
                    origin:      location,
                    destination: this.stadium.point,
                    travelMode:  travelM = ($('#travelMode',this.el.form).val() == 'walking') ? 
                    		google.maps.TravelMode.WALKING : 
                    			google.maps.TravelMode.DRIVING
                }, $.proxy(function( directionsResult, directionsStatus ) {
                    if( directionsStatus == google.maps.DirectionsStatus.OK ) {
                        this.directionsDisplay.setDirections( directionsResult );
                       // this.drawRouteMarkers( directionsResult );
                        this.drawRouteText( directionsResult );

                        //var route = directionsResult.routes[0].overview_path;
                        //this.el.directions[0].innerHTML += "<b>"+ (i+1) + ": " + route[i] + "</b><br />";

                        //// For each route, display summary information.
                        //var route = directionsResult.routes[0];
                        //this.el.directions[0].innerHTML = "";
                        //for (var i = 0; i < route.legs.length; i++) {
                        //    var routeSegment = i+1;
                        //    this.el.directions[0].innerHTML += "<b>Route Segment: " + routeSegment + "</b><br />";
                        //    this.el.directions[0].innerHTML += route.legs[i].start_address + " to ";
                        //    this.el.directions[0].innerHTML += route.legs[i].end_address + "<br />";
                        //    this.el.directions[0].innerHTML += route.legs[i].distance.text + "<br /><br />";
                        //}
                    }
                },this));
            }
        } catch( exception ) {
            console.log('Exception: ', this&&this.klass||'' , ':_onSubmit(): ', exception);
            console.dir(exception);
        }       
        return false;
    },
    drawRouteText: function( directionsResult ) {
        var route = directionsResult.routes[0].legs[0];
        var steps = route.steps;
                        
        this.el.directions[0].innerHTML = "";
        for( var i=0, n=steps.length; i<n; i++ ) {
            var text = steps[i].instructions;
            var time = steps[i].duration.text

            /*this.el.directions[0].innerHTML += "<div style='padding:0.5em; border-bottom: 1px solid #666'>" +
                    "<span style='float:right; clear:both;'>" + time + "</span>" +
                    "<span>" + text + "</span>" +
                "</div>";*/
            this.el.directions[0].innerHTML += "<div class=\"step\">" +
            "<span class=\"time\">" + time + "</span>" +
            "<span class=\"text\">" + text + "</span>" +
        "</div>";
        }
        if (steps.length > 0) { 
        	$(this.el.directions[0]).addClass("returned"); 
        } else {
        	$(this.el.directions[0]).removeClass("returned");
        }
        
        	
        //console.log('DEBUG: ', this&&this.klass||'' ,' route.steps ', route.steps);
    },
    drawRouteMarkers: function( directionsResult ) {
        var route = directionsResult.routes[0].legs[0];

        for( var i = 0; i < route.steps.length; i++ ) {
            var text   = route.steps[i].instructions;
            var marker = new google.maps.Marker({
                position: route.steps[i].start_point,
                map:      this.map
            });
            google.maps.event.addListener(marker, 'click', $.proxy(function(marker, text) {
                this.stepDisplay.setContent(text);
                this.stepDisplay.open(this.map, marker);
            }, this, marker, text));
            this.markerArray[i] = marker;
        }
    },
    _onClickMarker: function() {
        if( this.stadium.info_open === true ) {
            this.stadium.info.close();
            this.stadium.info_open = false;
        } else {
            this.stadium.info.open(this.map, this.stadium.marker);
            this.stadium.info_open = true;
        }
    }
});  
$.displayFaqItems = function(data) {
	var items = "";
	var category = $("#filter-faq-select option:selected").val();
	var sort = $("#sort-faq-select option:selected").val();
	var url = $("#faq-ajax-url").text();

	$.each(data, function(index, obj) {
		var tgs = "";
		if (obj.tagNames) {
			$.each(obj.tagNames, function(idx, tag) {
				tgs += ' ' + tag;
			});
		}
		var div = '<div class="faq-item ' + tgs + '">';
		div += '<a href="' + obj.path + '.html">';
		div += obj.question + '</a><br/>';
		div += '<p>' + obj.description + '</p>';
		div += '<span class="tag-association">Tags:';
		if (obj.tagTitles) {
			$.each(obj.tagTitles, function(idx, tag) {
				div += '&nbsp;<a href="#' + obj.tagNames[idx] + '" class="faqtag">' + tag + '</a> ,';
			});
			// Remove the last comma from the tag list
			div = div.replace(/,$/, "");
			//
		}
		div += '</span>';
		div += '</div>';
		items += div;

	});
	$("#faq-list").empty().append(items);
};

$("a.faqtag").live("click",function(e){
	$("#filter-faq-select").val(this.href.split('#').pop());
	$("#filter-faq-select").change();
	return false;
});

$.loadFaqItems = function() {
	var category = $("#filter-faq-select option:selected").val();
	var sort = $("#sort-faq-select option:selected").val();
	var url = $("#faq-ajax-url").text();
	url = url + "?category=" + category + "&sort=" + sort;

	$.getJSON(url, function(data) {
		$.displayFaqItems(data);
	});
};

$(document).ready(function() {
	var faq_options = $("#filter-faq-select option");
	if (faq_options.length > 0) { 
		faq_options.sort(function(a, b) {
			if (a.text.toLowerCase() === 'all')
				return -1;
			else if (b.text.toLowerCase() === 'all')
				return 1;
			else if (a.text > b.text)
				return 1;
			else if (a.text < b.text)
				return -1;
			else
				return 0;
		});
	
		$("#filter-faq-select").empty().append(faq_options);
		$("#filter-faq-select option[value='all']").attr("selected", "selected");
		$("#filter-faq-select").change(function(e) { return $.loadFaqItems();});
		$("#sort-faq-select").change(function(e) { return $.loadFaqItems();});
		$.loadFaqItems();
	}
});
var Chances = {
		currentPage:0,
		activeTiles:0,
		activePages:0,
		map:null,
		currentInfoBox:[]
};

Chances.updateList = function (){
		// @TODO: Data bind instead
		$('.chances-boxes div').removeClass('result');
		
		var selectedSeason = "season_" + $('#chances-seasons').val();  
		
		/* the OR logic */
		$(':checkbox:checked').each(function(){
			$('.chances-boxes div.chance-box.' + selectedSeason + '.' + $(this).val()).addClass('result');
		});
		
		/* the AND logic
		 $('#category-filter input:checkbox:checked').each(function() {
			var catVal = $(this).val();
			$('#topic-filter input:checkbox:checked').each(function() {
				var topicVal = $(this).val();
				$('.chances-boxes div.' + selectedSeason + '.' + catVal + '.' + topicVal).addClass('result');
			});
		});
		*/
		
		// Rebuild the pagination
		Chances.buildPagination();
};
	
Chances.buildPagination = function() {
		$('.chances-boxes div').hide();
		$('.chances-boxes div.result').show();
		
		Chances.activeTiles = $('.chances-boxes div.result').size();
		Chances.activePages = Math.ceil(Chances.activeTiles / 8);
		
		if(Chances.activeTiles > 8){
			$('.paging-links ul li').remove();
			$('.paging-links ul').append('<li><a href="#" class="begin">&laquo;</a></li>');
			$('.paging-links ul').append('<li><a href="#" class="prev">Prev</a></li>');
			
			/* Run through the list */
			for(var i = 0; i < Chances.activePages; i++){
				$('.paging-links ul').append('<li><a href="#" class="num">' + (i + 1) + '</a></li>');
				if(i === 0) {
					$('.paging-links ul li a:last').addClass('current');
				}
			}
			
			$('.paging-links ul').append('<li><a href="#" class="next">Next</a></li>');
			$('.paging-links ul').append('<li><a href="#" class="end">&raquo;</a></li>');
			
			
			$('.paging-links').show();
		} else {
			$('.paging-links').hide();
		}
		
		/* Bind pagination links */
		Chances.bindLinks();
};
	
Chances.bindLinks = function (){
		/* Reset page count and position and current link */
		Chances.currentPage = 1;
		$('.chances-boxes ul').stop(true, true).animate({top: 0});

		$('.paging-links a').click(function(){
			if($(this).hasClass('begin')){	// Scroll to the beginning
				Chances.currentPage = 1;
				$('.chances-boxes ul').stop(true, true).animate({top: 0});
			} else if ($(this).hasClass('end')){	// Scroll to the end
				Chances.currentPage = Chances.activePages;
				$('.chances-boxes ul').stop(true, true).animate({top: '-' + ((Chances.activePages - 1) * 380)});
			} else if ($(this).hasClass('prev')){	// Scroll prev page
				if(Chances.currentPage != 1){
					$('.chances-boxes ul').stop(true, true).animate({top: '+=380'});
					Chances.currentPage = Chances.currentPage-1;
				}
			} else if ($(this).hasClass('next')){	// Scroll next page
				if(Chances.currentPage != Chances.activePages){
					$('.chances-boxes ul').stop(true, true).animate({top: '-=380'});
					Chances.currentPage = Chances.currentPage + 1;
				}
			} else {
				var scrollAmount = ($(this).text() * 380) - 380;
				$('.chances-boxes ul').stop(true, true).animate({top: '-' + scrollAmount + 'px'});
				Chances.currentPage = parseInt($(this).text(), 10);
			}
			
			/* Highlight! */
			$('.paging-links a').each(function(){
				$(this).removeClass('current');
				if($(this).text() == Chances.currentPage) {
					$(this).addClass('current');
                }
			});
			
			Chances.showNumLinks();

			return false;
		});
		
		Chances.showNumLinks();
};
	
/* Show/Hide closest num links - If over 5 num links*/
Chances.showNumLinks = function(){
		if($('.paging-links a.num').size() > 5){
			$('.paging-links a.num').each(function(){
				if($(this).text() < (Chances.currentPage + 3) && $(this).text() > (Chances.currentPage - 3)){
					$(this).parent().show();
				} else {
					$(this).parent().hide();
				}
			});
		}
};

Chances.displayChanceItems = function(data) {
	var items = "";

	$.each(data, function(index, obj) {
		var tgs = "";
		if (obj.initiative) {
			$.each(obj.initiative, function(idx, tag) {
				if (tag.match(/^[0-9].+$/)) {
					tag = "season_" + tag;
				}	
				tgs += ' ' + tag;
			});
		}
		var li = '<li><div class="chance-box ' + tgs + '">';
		
		if (obj.openInNewWindow && obj.openInNewWindow == "true") {
			li += '<a target="_blank" href="' + obj.link  +'">';
		}
		else {
			li += '<a href="' + obj.link  +'">';
		}
		li += '<div class="overlay">';
		li += '<h4>' + obj.title + '</h4>';
		if (obj["abstract"]) { // abstract is a javscript reserved word
			li += obj["abstract"];
		}
		if (obj.logo) {
			li += '<img class="logo" src="' + obj.logo + '" />';
		}
		li += '</div>';
		
		if (!obj.landingImageAlt) {
			obj.landingImageAlt = obj.title;
		}
		
		if (!obj.image) {
			obj.image =  "/etc/designs/premierleague/images/shim.gif";
		}	
		
		li += '<img src="' + obj.image + '" width="224" height="190" alt="' + obj.landingImageAlt + '" />';
		li += '</a></div>';
		items += li;
	});

	$("#creatingChances .chances-boxes ul").empty().append(items);
	$("#creatingChances .chances-boxes .chance-box .overlay").hide();
};

Chances.mapInit = function(){
	$('#map_canvas').hide();
	$('#map_canvas').attr('style', 'left:0; position:relative; display:none;');
	
	/* View Map Page */
	$('.view a').toggle(function(){
		$(this).text('Grid View');
		$(this).parent().removeClass('map');
		$(this).parent().addClass('grid');

		$('.chances-boxes, .paging-links').stop(true, true).fadeOut('500', function(){
			$('#map_overlay, #map_canvas, .map_footer').fadeIn();
		});
		
		$('ul#chance-main-menu a.selected').each(function(){
			$(this).click();
		});
		return false;

	}, function(){
		$(this).text('Map View');
		$(this).parent().removeClass('grid');
		$(this).parent().addClass('map');

		$('#map_overlay').stop(true, true).fadeOut('500', function(){
			$('.chances-boxes, .paging-links').fadeIn();
		});
		$('ul#chance-main-menu a.selected').each(function(){
			$(this).click();
		});
		return false;
	});
};

Chances.displayMapMarkers = function(data) {
	$.each(data, function(index, val) {

	    var markerColor = val.markerColor;
	    if (!val.markerColor) {
	    	markerColor = "red";
	    }
	   
		var image = '/etc/designs/premierleague/images/chances/mapmarker_' + markerColor + '.png';
		var myLatLng = new google.maps.LatLng(val.lat, val.lon);
		var marker = new google.maps.Marker({
			position: myLatLng,
			map: Chances.map,
			icon: image,
			title: val.title
		});
		
		var contentString = '<div id="contentBubble">';
		if (val.logo) {
			contentString += '<img height="50" src="' + val.logo + '" alt="' + val.title + '" />';
		}
		contentString += '<div style="float:left;width:100px"><h4>' + val.title + '</h4>';
		if (val["abstract"]) { // abstract is a javscript reserved word
			contentString += val["abstract"];
		}	
		contentString += '<a href="' + val.link + '">&raquo; See more</a>';
		contentString += '</div></div>';
		
		var infowindow = new google.maps.InfoWindow({
			content: contentString
		});
		
		google.maps.event.addListener(marker, 'click', function() {
			infowindow.open(Chances.map,marker);
			$(Chances.currentInfoBox).each(function(){
				this.close();
				Chances.currentInfoBox.pop();
			});
			
			Chances.currentInfoBox.push(infowindow);
		});
	});
};

Chances.loadChanceData = function() {
	var url = $("#chances-ajax-url").text();
	$.getJSON(url, function(data) {
		Chances.displayChanceItems(data);
		Chances.displayMapMarkers(data);
		Chances.updateList();
	});
};


/* entry point */

$(document).ready(function() {
	if($('#creatingChances').size() > 0){
		
		Chances.loadChanceData();
		
		/* Create Body ID */
		$('body').addClass('creatingChances');
		
		/* Add green field */
		$('.bodypsys').addClass('green-field');
		
		/* Category Dropdown */
		$('#category-filter').hide();
		$('#chance-main-menu li.category a').click(
			function(){
				if($(this).hasClass('selected')){
					$('#category-filter').slideUp();
					$(this).removeClass('selected');
				} else {
					$('#chance-main-menu a').removeClass('selected');
					$(this).addClass('selected');
					$('#topic-filter').slideUp();
					$('#category-filter').slideDown();
				}
	
				return false;
		});
		
		$('#chances-seasons').change(function() { 
			Chances.updateList();
		});
		
		$('#category-filter .close-filter').click(function(){
			$('#chance-main-menu li.category a').click();
			return false;
		});
		$('#category-filter button').click(function(){
			$('#category-filter input').removeAttr('checked');
			Chances.updateList();
			return false;
		});
		
		/* Topic Dropdown */
		$('#topic-filter').hide();
		$('#chance-main-menu li.topic a').click(
			function(){
				if($(this).hasClass('selected')){
					$('#topic-filter').slideUp();
					$(this).removeClass('selected');
				} else {
					$('#chance-main-menu a').removeClass('selected');
					$(this).addClass('selected');
					$('#category-filter').slideUp();
					$('#topic-filter').slideDown();
				}
	
				return false;
		});
		$('#topic-filter .close-filter').click(function(){
			$('#chance-main-menu li.topic a').click();
			return false;
		});
		$('#topic-filter button').click(function(){
			$('#topic-filter input').removeAttr('checked');
			Chances.updateList();
			return false;
		});
		
		/* Overlay overlay */
		$('.chance-box').live({
			  mouseenter: function() { 
			   $(this).find('.overlay').fadeIn();
			  },
			  mouseleave: function () {
			   $(this).find('.overlay').fadeOut();
			  }
		 });
		
		/* Category/Topic Sort */
		$('input[name="topic"], input[name="category"]').click(function(){
			Chances.updateList();
		});
		
		/* Init the Google Map */
		var latlng = new google.maps.LatLng(15, 0);
		var myOptions = {
			zoom: 2,
			center: latlng,
			mapTypeId: google.maps.MapTypeId.ROADMAP
		};
		Chances.map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
		google.maps.event.addDomListener(window, 'load', Chances.mapInit);
	}
});
/* 
 *  MODAL JS
 *  VML KC 2011
 *  
 *  DESCRIPTION: 
 *      A jquery.DOMWindow extension to render specific media types.  It's pretty much a pre-processor for
 *      DOMWindow that formats the media being displayed to a user.
 *  DEPENDENCIES:
 *      jquery.DOMWindow (http://swip.codylindley.com/DOMWindowDemo.html) 
 *      swfObject ()
 */

$.ui.basewidget.subclass('ui.mediamodal', {

    klass: "$.ui.mediamodal",

    //MOST of these options are the DOMWindow jquery plugin defaults
    options: {
        anchoredClassName:'',
        anchoredSelector:'',
        borderColor:'#333',
        borderSize:'0',
        draggable:0,
        eventType:null, //click, blur, change, dblclick, error, focus, load, mousedown, mouseout, mouseup etc...
        fixedWindowY:100,
        functionCallOnOpen:null,
        functionCallOnClose:null,
        height:-1,
        loader:0,
        loaderHeight:0,
        loaderImagePath:'',
        loaderWidth:0,
        modal:0,
        overlay:1,
        overlayColor:'#000',
        overlayOpacity:'85',
        positionLeft:0,
        positionTop:0,
        positionType:'centered', // centered, anchored, absolute, fixed
        width:-1,
        windowclass:'',
        windowBGColor:'#000',
        windowBGImage:null, // http path
        windowHTTPType:'get',
        windowPadding:0,
        windowSource:'inline', //inline, ajax, iframe
        windowSourceID:'',
        windowSourceURL:'',
        windowSourceAttrURL:'href',
        eventManager: null,
        mediaType: '', 
        modalheight: -1,
        modalwidth: -1,
        autoHeight: false,
        flashVars: '',
        onAuthSuccess: '',
        _tmpl_yahoo_video : function( flash_vars ) {
            return ' ' +
            '<div class="video-container"> ' +
            '   <div class="epl_top_container"> ' +
            '       <div class="spon_left"> <script language="JavaScript" type="text/javascript" src="http://uk.adserver.yahoo.com/a?f=2144372856&p=&l=EVL2&c=r"></script></div> ' +
            '       <div class="spon_right"> <script language="JavaScript" type="text/javascript" src="http://uk.adserver.yahoo.com/a?f=2144372856&p=&l=EVL&c=r"></script> </div> ' +
            '       <div class="epl_clear"> </div> '+
            '   </div> ' +
            '   <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="640" height="500" id="yahoovideoplayer1"> ' +
            '       <param name="allowScriptAccess" value="always" /> '+
            '       <param name="allowfullscreen" value="true" /> '+
            '       <param name="movie" value="http://d.yimg.com/nl/premier-league/site/player.swf"/> '+
            '       <param name="flashVars" value="'+ flash_vars +'"/> '+
            '       <param name="browseCarouselUI" value="hide"> '+
            '       <param name="wmode" value="transparent"/> '+
            '       <embed allowfullscreen="true" id="yahoovideoplayer1" src="http://d.yimg.com/nl/premier-league/site/player.swf" type="application/x-shockwave-flash" allowscriptaccess="always" flashvars="'+ flash_vars +'" width="640" height="500"/> '+
            '   </object> '+
            '</div>';
        }
    },

    _create : function() {
        this.el = {};
        this.options.hash = {};
        this.el.targets = this.element.find( 'a' );
        this.options = $.getAttributeHash( this.element, this.options );
        /*- Convert any modalheight and modalwidth parameters to 
            height and width ( done to protect the styling of the dom ) -*/
        this.options.height = parseInt( this.options.modalheight, 10 );
        this.options.width = parseInt( this.options.modalwidth, 10 );
    },

    _init : function() {

        var self        = this;
        var $targets    = $( this.el.targets );
        
        $targets.click( function( e ) {

            e.preventDefault();
            var $target = $( this );

            // Media Types allowed for the modal
            switch( self.options.mediaType ){
                case 'video':
                    self.open_video( $target, self.options, self ); break;
                case 'game':
                    self.open_game( $target, self.options, self ); break;
                case 'photo':
                    self.open_photo( $target, self.options, self ); break;
                case 'audio': break;
                default:
                    self.open_html( $target, self.options, self ); break;
            }

        });
        
    },

    _commonOnOpen : function( defaults ) {
        var liquidHeight = defaults.autoHeight === "true" || defaults.autoHeight === true;
        
        if( liquidHeight )
            $( '#DOMWindow' ).height( 'auto' ); 

        if( $( '#defaultDOMWindowClose' ).length <= 0 ){
            var $closeWindowLink = $("<a/>", {
                "id"    : "defaultDOMWindowClose",
                "class" : "closeDOMWindow",
                "href"  : "#",
                text    : "Close"
            });
            $closeWindowLink.appendTo( $( '#DOMWindow' ) );
        }

        // Pass defaults to the dom so the login modal can read them
        $( '#DOMWindow' ).data( 'mediamodal', defaults );
    },

    _hasExtension : function( expected_ext, target ) {
        
        // get file extension
        var filepath = target.attr( 'href' );
        var ext = filepath.substr( filepath.lastIndexOf( '.' ) + 1 );
        
        //if var ext != 'expected_ext', change the mediamodal load to a "pseudo-html-mediamodal" type 
        //by setting the media defaults before calling this, and then follow the html-media-type actions
        if( ext !== expected_ext ) { return false; }
        return true;

    },

    _psudoHtmlMedia : function( trigger, defaults, self ) {

        defaults.windowSource = 'iframe';
        defaults.autoHeight = true;
        return self.open_html( trigger, defaults, self );

    },

    open_html : function( trigger, defaults, self ) { 
        
        var html_url = trigger.attr( 'href' );

        //htmlmedia default width and height
        if( defaults.height === -1 ) defaults.height = 560;
        if( defaults.width === -1 ) defaults.width = 640;

        //htmlmedia default class name - only inserted if there is none set
        if( defaults.windowclass === '' ){
            defaults.windowclass = 'modal_html';
        }

        //htmlmedia types for inline, iframe, or the default (ajax)
        if( defaults.windowSourceID !== '' ) {

            var debug_msg = 'this is an inline html call';

        } else if( defaults.windowSource === 'iframe' ) {

            defaults.windowSourceURL = html_url;
            var debug_msg = 'this is an iframe html call';

        } else {

            defaults.windowSource = 'ajax';
            defaults.windowSourceURL = html_url;
            var debug_msg = 'this is an AJAX html call';

        }

        defaults.functionCallOnOpen = function() {

            //common tasks
            self._commonOnOpen( defaults );

            //check if the site loaded the page or if it failed
            if( $( '#DOMWindow' ).children().length < 0 ){
                console.error( 'ERROR: The url ('+defaults.windowSourceURL+') did not load' );
                $.closeDOMWindow();
            }

        };

        return $.openDOMWindow( defaults );
    },

    open_photo : function( trigger, defaults, self ) {

        defaults.windowclass = 'modal_video';

        if( defaults.height === -1 ) defaults.height = 560;
        if( defaults.width === -1 ) defaults.width = 640;

        defaults.functionCallOnOpen = function() {
            //insert the image, preload, then fade in the image
            $( '<img/>', { src: $( trigger ).attr( 'href' ), alt:'' } )
                .fadeTo( 0, 0 )
                .load( function() {
                    $( '#DOMWindow' ).append( this );
                    self._commonOnOpen( defaults ); //common tasks
                    $( this ).fadeTo( 800, 1 );
                });
        };

        return $.openDOMWindow( defaults );
    },

    open_video : function( trigger, defaults, self ) {

        var flash_vars = trigger.attr( 'rel' );
        defaults.windowclass = 'modal_video';
        
        if( defaults.height === -1 ) defaults.height = 475;
        if( defaults.width === -1 ) defaults.width = 776;

        //insert the template with the proper playlist and selected video (via flash vars)
        defaults.functionCallOnOpen = function() {
            self._commonOnOpen( defaults ); //common tasks
            var tmpl = defaults._tmpl_yahoo_video( flash_vars );
            $( '#DOMWindow' ).append( tmpl );
        };

        return $.openDOMWindow( defaults );

    },

    open_game : function( trigger, defaults, self ) {
        var game_url = $( trigger ).attr( 'href' );
        defaults.windowclass  = 'modal_game';

        //default height/width for a game if none specified
        if( defaults.height === -1 ) defaults.height = 475;
        if( defaults.width === -1 ) defaults.width = 776;

        //if the game is not an swf, change the mediamodal load to a "pseudo-html-mediamodal" type 
        //by setting the media defaults to the game defaults, and then follow the html-media-type actions
        
        if( !self._hasExtension( 'swf', trigger ) ) {
            defaults.windowSource = 'iframe';
            self.open_html( trigger, defaults, self );
            return;
        }
        
        defaults.functionCallOnOpen = function() {

            var flashvars = {};
            var attributes = {};
                attributes.id = 'flashContent';
            var params = {};
                params.allowscriptaccess = 'always';
                params.wmode = 'transparent';

            self._commonOnOpen( defaults ); //common tasks

            //container for the swf that will be generated below
            $( '#DOMWindow' ).append( $( '<div>', { id: 'flashContent' } ) );

            //set the url base of the game ( so the swf loads external files properly )
            //assumes that the base will always be the same url directory as the swf.
            if( game_url.lastIndexOf( '/' ) !== -1 ) {
                params.base = game_url.substring( 0, game_url.lastIndexOf( '/' ) + 1 );
            }

            //extend and/or override flashvars
            if( defaults.flashVars !== '' ) {
                $.extend( flashvars, eval( defaults.flashVars ) );
            }

            //embed the swf in the domwindow content
            swfobject.embedSWF(
                game_url, 
                "flashContent", 
                defaults.width, defaults.height, 
                "10.0.0", 
                "/flash/expressInstall.swf", 
                flashvars, params, attributes );
        };

        return $.openDOMWindow( defaults );
    }
});

var PL_Video_swfobject = swfobject;
/***
 *  This is a wrapper around the selectToUISlider jquery plugin
 */

$.ui.basewidget.subclass('ui.selectToSlider', {
    klass: '$.ui.selectToSlider',
    options: {
        wrapperClass: 'selectToSliderWrapper',
        hideSelects:  true,
        hideLabels:   true,
        labels: 7,
        tooltip: true
    },

    _create: function() {
        this.el = {};
        this.element.wrap("<div class='"+this.options.wrapperClass+"'></div>");
        this.el.wrapper = this.element.parent();
        this.el.selects = this.element.findAndSelf("select");
        this.sliderOptions = {};
    },

    _init: function() {
        var labelCount  = this.element.find("option").length;
        var showTooltip = this.options.tooltip && !!(labelCount > this.options.labels);

        // If you want a double slider, this.el.selects needs contain two select elements
    	this.el.selects.selectToUISlider({
			labels:   this.options.labels,
			labelSrc: "text",
			tooltip:  showTooltip,
            sliderOptions: this.sliderOptions,
            hideLabels: this.options.hideLabels
		});
    	
        if( this.options.hideSelects ) {
            this.element.findAndSelf("select").hide();
        }
        if( this.options.hideLabels ) {
            this.element.find("label").hide();
        }
    }
});
/**
 * This widget has been customised for functionality required by the Premier League - James McGuigan
 *
 * --------------------------------------------------------------------
 * jQuery-Plugin - selectToUISlider - creates a UI slider component from a select element(s)
 * by Scott Jehl, scott@filamentgroup.com
 * http://www.filamentgroup.com
 * reference article: http://www.filamentgroup.com/lab/update_jquery_ui_16_slider_from_a_select_element/
 * demo page: http://www.filamentgroup.com/examples/slider_v2/index.html
 * 
 * Copyright (c) 2008 Filament Group, Inc
 * Dual licensed under the MIT (filamentgroup.com/examples/mit-license.txt) and GPL (filamentgroup.com/examples/gpl-license.txt) licenses.
 *
 * Usage Notes: please refer to our article above for documentation
 *  
 * --------------------------------------------------------------------
 */


jQuery.fn.selectToUISlider = function(settings){
	var selects = jQuery(this).filter("select").length ? jQuery(this).filter("select") : jQuery(this).find("select");
	
	//accessible slider options
	var options = jQuery.extend({
		labels: 7, //number of visible labels
		tooltip: true, //show tooltips, boolean
		tooltipSrc: 'text',//accepts 'value' as well
		labelSrc: 'value',//accepts 'value' as well	,
		sliderOptions: null,
        hideSlider: true,
        hideLabels: true
	}, settings);

        
    //var labels = jQuery(this).find("labels");
    //if( options.hideLabels ) { 
    //    labels.hide(); 
    //}

    //var wrapper = jQuery(this);
    //if( options.wrapperClass ) {
    //    wrapper = jQuery(this).wrap("<div class='"+this.options.wrapperClass+"'></div>");
    //}


	//handle ID attrs - selects each need IDs for handles to find them
	var handleIds = (function(){
		var tempArr = [];
		selects.each(function(){
			tempArr.push('handle_'+jQuery(this).attr('id'));
		});
		return tempArr;
	})();
	
	//array of all option elements in select element (ignores optgroups)
	var selectOptions = selects.eq(0).find("option[value!='']").map(function(){
        return {
            value: jQuery(this).attr('value'),
            text:  jQuery(this).text()
                               .replace(/^\d\d(\d\d)-\d\d(\d\d)/, '$1/$2') // Short Years: 2010-2011 -> 10-11
        };
	});
	
	//array of opt groups if present
	var groups = (function(){
		if(selects.eq(0).find('optgroup').size()>0){
			var groupedData = [];
			selects.eq(0).find('optgroup').each(function(i){
				groupedData[i] = {};
				groupedData[i].label = jQuery(this).attr('label');
				groupedData[i].options = [];
				jQuery(this).find("option[value!='']").each(function(){
					groupedData[i].options.push({
                        text:     jQuery(this).text()
                                              .replace(/^\d\d(\d\d)-\d\d(\d\d)/, '$1/$2'),
                        value:    jQuery(this).attr('value'),
                        disabled: jQuery(this).attr('disabled')
                    });
				});
			});
			return groupedData;
		}
		else {
            return null;
        }
	})();	
	
	//check if obj is array
	function isArray(obj) {
		return obj.constructor == Array;
	}
	//return tooltip text from option index
	function ttText(optIndex){
        if(!( selectOptions[optIndex] )) { return ""; }
		return (options.tooltipSrc == 'text') ? selectOptions[optIndex].text : selectOptions[optIndex].value;
	}

    function updateSliderBar() {
        //change background width
        var sliderBar    = sliderComponent.find('.ui-slider-range');
        var sliderHandle = sliderComponent.find('.ui-slider-handle');
        sliderBar.css("width", sliderHandle.offset().left + sliderHandle.width()/2 - sliderBar.offset().left );
    }

	
    var selectedIndex = selects.attr("selectedIndex");
	//plugin-generated slider options (can be overridden)
	var sliderOptions = {
		step: 1,
		min: 0,
		orientation: 'horizontal',
		max: selectOptions.length-1,
		range: "min",
		slide: function(e, ui) {//slide function
            var thisHandle = jQuery(ui.handle);
            //handle feedback 
            var textval = ttText(ui.value);
            thisHandle
                .attr('aria-valuetext', textval)
                .attr('aria-valuenow', ui.value)
                .find('.ui-slider-tooltip .ttContent')
                .text( textval );

            setTimeout( updateSliderBar, 0 );
		},
        change: function(e, ui) {
            // If we select a disabled option, then jump back to the original selectedIndex
            var thisHandle = jQuery(ui.handle);
            var currSelect = jQuery('#' + thisHandle.attr('id').split('handle_')[1]);
            var currOption = currSelect.find('option:selected');
            var dragOption = currSelect.find('option').eq(ui.value);
            if( dragOption.index() != currOption ) {
                if( dragOption.attr("disabled") ) {
                    sliderComponent.slider("values", [selectedIndex]);
                } else {
                	currOption.removeAttr('selected'); //remove from all
    				dragOption.attr('selected', 'selected'); //add to selected
                }
            }
            
            setTimeout( updateSliderBar, 0 );
        },
		values: (function(){
			var values = [];
			selects.each(function(){
				values.push( jQuery(this).get(0).selectedIndex );
			});
			return values;
		})()
	};
	
	//slider options from settings
	options.sliderOptions = (settings) ? jQuery.extend(sliderOptions, settings.sliderOptions) : sliderOptions;
		
	//select element change event	
	selects.bind('change keyup click', function(){
		var thisIndex = jQuery(this).get(0).selectedIndex;
		var thisHandle = jQuery('#handle_'+ jQuery(this).attr('id'));
		var handleIndex = thisHandle.data('handleNum');
        sliderComponent.slider("values", handleIndex, thisIndex );
		//thisHandle.parents('.ui-slider:eq(0)').slider("values", handleIndex, thisIndex);
	});
	

	//create slider component div
	var sliderComponent = $.el('div').addClass("selectToUISliderComponent");

	//CREATE HANDLES
	selects.each(function(i){
		var hidett = '';
		
		//associate label for ARIA
		var thisLabel = jQuery('label[for=' + jQuery(this).attr('id') +']');
		//labelled by aria doesn't seem to work on slider handle. Using title attr as backup
		var labelText = (thisLabel.size()>0) ? 'Slider control for '+ thisLabel.text()+'' : '';
		var thisLabelId = thisLabel.attr('id') || thisLabel.attr('id', 'label_'+handleIds[i]).attr('id');
		
		
		if( options.tooltip == false ) { hidett = ' style="display: none;"'; }
		jQuery('<a '+
                   'href="#" tabindex="0" '+
                   'id="'+handleIds[i]+'" '+
                   'class="ui-slider-handle" '+
                   'role="slider" '+
                   'aria-labelledby="'+thisLabelId+'" '+
                   'aria-valuemin="'+options.sliderOptions.min+'" '+
                   'aria-valuemax="'+options.sliderOptions.max+'" '+
                   'aria-valuenow="'+options.sliderOptions.values[i]+'" '+
                   'aria-valuetext="'+ttText(options.sliderOptions.values[i])+'" '+
               '>' + 
                    '<span class="screenReaderContext">'+labelText+'</span>'+
                    '<span class="ui-slider-tooltip ui-widget-content ui-corner-all"'+ hidett +'>' + 
                        '<span class="ttContent"></span>'+
                        '<span class="ui-tooltip-pointer-down ui-widget-content"><span class="ui-tooltip-pointer-down-inner"></span></span>'+
                    '</span>' + 
               '</a>')
			.data('handleNum',i)
			.appendTo(sliderComponent);
	});
	
	//CREATE SCALE AND TICS
	
	//write dl if there are optgroups
	if(groups) {
		var inc = 0;
		var scale = sliderComponent.append('<dl class="ui-slider-scale ui-helper-reset" role="presentation"></dl>').find('.ui-slider-scale:eq(0)');
		jQuery(groups).each(function(h){
			scale.append('<dt style="width: '+ (100/groups.length).toFixed(2) +'%' +'; left:'+ (h/(groups.length-1) * 100).toFixed(2)  +'%' +'"><span class="ui-slider-optgroup-label">'+this.label+'</span></dt>');//class name becomes camelCased label
			var groupOpts = this.options;
            var optGroupLabel = this.label;
			jQuery(this.options).each(function(i){
				var style = (inc == selectOptions.length-1 || inc == 0) ? 'style="display: none;"' : '' ;
				var labelText = (options.labelSrc == 'text') ? groupOpts[i].text : groupOpts[i].value;
				var disabled  = (options.disabled === true)  ? ' disabled="disabled"' : "";

                var node = $('<dd style="left:'+ leftVal(inc) +'" class="optgroup-'+optGroupLabel+'" '+disabled+'><span class="ui-slider-label">'+ labelText +'</span><span class="ui-slider-tic ui-widget-content"'+ style +'></span></dd>');
                scale.append(node);
				inc++;
			});
		});
	}
	//write ol
	else {
		var scale = sliderComponent.append('<ol class="ui-slider-scale ui-helper-reset" role="presentation"></ol>').find('.ui-slider-scale:eq(0)');
		jQuery(selectOptions).each(function(i){
			var style = (i == selectOptions.length-1 || i == 0) ? 'style="display: none;"' : '' ;
			var labelText = (options.labelSrc == 'text') ? this.text : this.value;
                
			scale.append('<li style="left:'+ leftVal(i) +'"><span class="ui-slider-label">'+ labelText +'</span><span class="ui-slider-tic ui-widget-content"'+ style +'></span></li>');
		});
	}
	
	function leftVal(i){
		return (i/(selectOptions.length-1) * 100).toFixed(2)  +'%';
		
	}
	

	
	
	//show and hide labels depending on labels pref
	//show the last one if there are more than 1 specified
	if(options.labels > 1) { sliderComponent.find('.ui-slider-scale li:last span.ui-slider-label, .ui-slider-scale dd:last span.ui-slider-label').addClass('ui-slider-label-show'); }


	//set increment
	var increm = Math.max(1, Math.round(selectOptions.length / options.labels));
	//show em based on inc
	for(var j=0; j<selectOptions.length; j+=increm){
		if((selectOptions.length - j) > increm){//don't show if it's too close to the end label
			sliderComponent.find('.ui-slider-scale li:eq('+ j +') span.ui-slider-label, .ui-slider-scale dd:eq('+ j +') span.ui-slider-label').addClass('ui-slider-label-show');
		}
	}

	//style the dt's
	sliderComponent.find('.ui-slider-scale dt').each(function(i){
		jQuery(this).css({
			'left': ((100 /( groups.length))*i).toFixed(2) + '%'
		});
	});
	
    // Center labels
	sliderComponent.find('span.ui-slider-label').each(function() {
        $(this).css("margin-left", ($(this).width()/2)+"px");       
    });

	//inject and return 
	sliderComponent
	.insertAfter(jQuery(this).eq(this.length-1))
	.slider(options.sliderOptions)
	.attr('role','application')
	.find('.ui-slider-label')
	.each(function(){
		jQuery(this).css('marginLeft', -jQuery(this).width()/2);
	});
	
	//update tooltip arrow inner color
	sliderComponent.find('.ui-tooltip-pointer-down-inner').each(function(){
				var bWidth = jQuery('.ui-tooltip-pointer-down-inner').css('borderTopWidth');
				var bColor = jQuery(this).parents('.ui-slider-tooltip').css('backgroundColor');
				jQuery(this).css('border-top', bWidth+' solid '+bColor);
	});
	
	var values = sliderComponent.slider('values');
	
	if(isArray(values)){
        var ttContent = sliderComponent.find('.ui-slider-tooltip .ttContent');
        for( var i=0, n=values.length; i<n; i++ ) {
            ttContent.eq(i).text( ttText(values[i]) );
        }
	}
	else {
		sliderComponent.find('.ui-slider-tooltip .ttContent').eq(0).text( ttText(values) );
	}
    // Center tooltips
    sliderComponent.find('.ui-slider-tooltip').each(function() {
        var sliderHandleWidth = 6;
        var marginLeft = ( $(this).width() - sliderHandleWidth ) / 2;
        $(this).css("margin-left", -marginLeft+"px");
    });
	
    updateSliderBar(); // No setTimeout(), apply immediatly on render
	return this;
};


$.ui.basewidget.subclass('ui.contactus', {
    klass: '$.ui.contactus',
    _create: function() {
        this.target = this.element;
    },
    _init: function() {
		this.element.find('.tabLinks a:first').addClass('active');
        this.tabSwitch();
		//this.mapView();		// @Todo: Map View JS init
    },
    tabSwitch: function() {
    	var self = this;
		
		this.element.find('.tabLinks a').click(function(){
			var linkself = $(this);
			if(!$(this).hasClass('active')){
				self.element.find('.tabLinks a').removeClass('active').removeClass('inactive');
				$(this).addClass('active');
				self.element.find('.tabLinks a:not(.active)').addClass('inactive');
				
				self.element.find('.tabContent > li').fadeOut('slow', function(){
					self.element.find('.tabContent li.' + linkself.attr('rel')).fadeIn('slow');
					
				});
			}
			return false;	
		});
    }
});
/**
 * $.ui.loginmodal
 * 
 * 		Login modal
 *   
 */

$.ui.basewidget.subclass('ui.loginmodal', {
    klass: '$.ui.loginmodal',
    options: {
		action: 	null			// {String} Ajax url for submission
    },
    required: {
    	action:	String
    },
   _create: function() {
        this.options.hash  = {};
        this.options       = $.getAttributeHash( this.element, this.options );
        
        // Inputs
        this.el.submit	   = this.element.find( 'input[type=submit]'   );
        this.el.email	   = this.element.find( 'input[name=email]'    );
        this.el.password   = this.element.find( 'input[name=password]' );
        
        // Error elements
        this.el.error	   = this.element.find( '.errormsg' );
        this.el.errorEmail = this.element.find( 'span'   );
        
        this.el.targets = $([]); // this.element.find('a');
    },
   _init: function() {
    	this.el.submit.bind("click", $.proxy(this.submit, this));
    	this.el.error.hide();
    },
    submit: function(e) {
    	e.preventDefault();
    	
    	var spinner = '<div class="spinner"/>';

		//cache jquery objects
		this.el.submit.attr( 'disabled', 'disabled' ).addClass( 'disabled' );
		this.element.append( spinner );
		
		//validate form for empty values
		if ( typeof this.el.email.val() === 'undefined' 
				|| this.el.email.val() === '' ) {
			this.el.email.parent().addClass( 'error' );
		} else {
			this.el.email.parent().removeClass( 'error' );
		}
		if ( typeof this.el.password.val() === 'undefined' 
				|| this.el.password.val() === '' ) {
			this.el.password.parent().addClass( 'error' );
		} else {
			this.el.password.parent().removeClass( 'error' );
		}
		
		//clear overlay and remove disabled state if any errors exsits
		if ( this.element.find('.error').length > 0 ) {
			this.el.submit.removeAttr( 'disabled' ).removeClass( 'disabled' );
		    this.element.find( '.spinner' ).remove();
		    return;
		}
		
		var ajax = this.options.action 
			+ '?email=' 	+ this.el.email.val() 
			+ '&password=' 	+ this.el.password.val();
		
		//ajax post to jsonp REST url
		$.ajax({
            url:  	  ajax,
            type:	  "GET",
            dataType: "jsonp",
            timeout:  10000,
            beforeSend: function( xhr ) {
            },
            success: $.proxy(function( json, xhr, status ) {
            	// A bit messy here. Needs to be refactored somewhat with mediamodal.js
            	var mediamodal = $('#DOMWindow').data('mediamodal');
    		    var successRedirect = ( mediamodal.onAuthSuccess != '' ) 
    		    	? mediamodal.onAuthSuccess : "http://fantasy.premierleague.com/my-team/";
    		    if ( $('#DOMWindow').length && $('#DOMWindow').is( ':visible' ) ) {
    		    	window.location = successRedirect;
    		    }
            }, this),
            error: $.proxy(function( xhr, status ) {
            	// Set error text to email entered
            	this.el.errorEmail.html( this.el.email.val() );
            	// Remove disabled attribute and spinner
            	this.el.submit.removeAttr( 'disabled' ).removeClass( 'disabled' );
            	this.element.find( '.spinner' ).remove();
            	// Show error
            	this.el.error.show();
            }, this)
        });
		
		return false;
	}
});
//----- js/widgets/jquery.svgWidget.js -----//

$.ui.basewidget.subclass('ui.svgWidget', {
    klass: "$.ui.svgWidget",
    options: {
        svgheight:     null,
        svgwidth:      null,
        showlabel:     true,
        showcollabel:  true,
        labelColor:    "#989898",
        labelSize:       10,
        labelWeight:    700
    },
                               // Note: Object literals declared here will shared between all instances
    data:    null,             // {Hash}
    xy:      null,             // {Hash}
    sprite:  null,             // {Hash<jQuery>} Namespace for all svg elements
    table:   null,             // {jQuery} Node representing the HTML table
    wrapper: null,             // {jQuery} Node representing the HTML wrapper div for the graphics
    canvas:  null,             // {Raphael}
    createdOwnWrapper: false,  // {Boolean}

    _create: function() {
        this.data   = {};
        this.xy     = {};
        this.sprite = {};

        this.getTableNode();
        this.data    = this.options.data || this.parseHtmlTable(); // parseHtmlTable() defined in basewidget.js
        this.options = $.getAttributeHash(this.element, this.options); // html overrides this.options
        this.parseOptions();
        this.calculateXY();
    },
    _init: function() {
        this.getCanvas();
    },
    draw: function() {
        $.noop();
    },

    /**
     *  Modifies this.options with various calculations
     */
    parseOptions: function() {
        var o = this.options;
        return o;
    },
    /**
     *  Creates this.xy for a coordinate mapping
     */
    calculateXY: function() {
        this.xy = this.xy || {};
        return this.xy;
    },



    //*** Init ***//

    /**
     *  The wrapper is the HTML div that defines positioning.
     *  It does not surround the HTML table but nesting can be achieved via an svgWrapper
     *  CSS: .svg-wrapper { positioning: relative  }
     *
     *  @return {jQuery}
     */
    getWrapper: function() {
        this.wrapper = this.wrapper || this.options.wrapper || this.getParentWrapper() || this.createWrapper();
        return this.wrapper;
    },
    getParentWrapper: function() {
        var closest = this.element.closest(".svg-wrapper");
        return closest.length ? closest : null;
    },
    createWrapper: function() {
        this.wrapper = $("<div class='svg-wrapper'></div>").insertBefore( this.getTableNode()[0] || this.element[0] ); // Create a new one
        this.createWrapperInit();
        return this.wrapper;
    },
    createWrapperInit: function() {
        //if( this.options.offset ) { // TODO: Do we need this line?
            this.wrapper.height( this.getInitHeight() ); // Canvas is position absolute so we need to explictly define wrapper size
            this.wrapper.width(  this.getInitWidth()  );
        //}
        this.wrapper.data("widget", this);
        this.createdOwnWrapper = true;
    },


    /**
     *  This is the Raphael canvas element, it sits inside the .svg-wrapper node
     *  It is offset from the parent svgWrapper through <node offset=""> via this.getWrapperOffsetX()
     *
     *  @return {Raphael}
     */
    getCanvas: function() {
        this.canvas = this.canvas || this.options.canvas || this.createCanvas() || null; // Don't use parent canvas
        return this.canvas;
    },
    getParentCanvas: function() {
        return this.getWrapper().data("canvas") || null;
    },
    createCanvas: function() {
        this.canvas = Raphael( this.getWrapper().get(0), this.getInitWidth(), this.getInitHeight() );
        this.canvas.realWidth  = this.getInitWidth();
        this.canvas.realHeight = this.getInitHeight();

        if( this.createdOwnWrapper ) {
            this.getWrapper().data("canvas", this.canvas);
        }
        if( this.options.offset ) {
            this.canvas.canvas.style.cssText = "position:absolute;"
                                             + "left:" + this.getWrapperOffsetX() + "px;"
                                             + "top:"  + this.getWrapperOffsetY() + "px;"
                                             + "z-index:" + $.ui.svgWidget.zIndex++;       // This fixes IE background rendering bug
        }

        this.drawBackground();
        this.drawBorder();
        return this.canvas;
    },



    //*** Getters ***//

    /**
     *  This gets the wrapper width/height which may be set by the parent svgWidget/svgWrapper
     *  This is used for calculating the offset when creating the canvas
     */
    getWrapperWidth: function() {
        return this.getWrapper().data("canvas").realWidth;
    },
    getWrapperHeight: function() {
        return this.getWrapper().data("canvas").realHeight;
    },

    /**
     *  This gets the width/height of the canvas drawing element
     *  @return {Number}
     */
    getCanvasWidth: function() {
        return this.getCanvas() && this.getCanvas().realWidth || null;
    },
    getCanvasHeight: function() {
        return this.getCanvas() && this.getCanvas().realHeight || null;
    },

    /**
     *  This calculates the width/height that a new canvas should be drawn at
     *  @return {Number}
     */
    getInitWidth: function() {
        if( !this.options.svgwidth ) {
            this.options.svgwidth = Number(this.options.svgwidth) || this.element.width();
        }
        return this.options.svgwidth;
    },
    getInitHeight: function() {
        if( !this.options.svgheight ) {
            this.options.svgheight = Number(this.options.svgheight) || this.element.height();
        }
        return this.options.svgheight;
    },

    /**
     *  This calcuates the offset from the parent canvas, based on from this.options.offset
     *  @return {Number}
     */
    getWrapperOffsetX: function( offsetString ) {
        offsetString = offsetString || this.options.offset || "";

        var offsetX = 0;
        var xmatch = offsetString.match(/(left|right|center)(:\s*(\d+))?/);
        if( xmatch ) {
            switch( xmatch[1] ) {
                default:       // follow through
                case "left":   offsetX += 0; break;
                case "center": offsetX += Math.floor(this.getWrapperWidth()/2 - this.getInitWidth()/2); break;
                case "right":  offsetX += Math.floor(this.getWrapperWidth()   - this.getInitWidth()  ); break;
            }
            if( typeof xmatch[3] !== "undefined" ) {
                switch( xmatch[1] ) {
                    default:       // follow through
                    case "left":   offsetX += Number(xmatch[3]); break;
                    case "center": offsetX += Number(xmatch[3]); break;
                    case "right":  offsetX -= Number(xmatch[3]); break;
                }
            }
        }
        return offsetX;
    },
    getWrapperOffsetY: function( offsetString ) {
        offsetString = offsetString || this.options.offset || "";

        var offsetY = 0;
        var ymatch = offsetString.match(/(top|bottom|middle)(:\s*([+-]?\d+))?/);
        if( ymatch ) {
            switch( ymatch[1] ) {
                default:       // follow through
                case "top":    offsetY += 0; break;
                case "middle": offsetY += Math.floor(this.getWrapperHeight()/2 - this.getInitHeight()/2); break;
                case "bottom": offsetY += Math.floor(this.getWrapperHeight()   - this.getInitHeight()  ); break;
            }
            if( typeof ymatch[3] !== "undefined" ) {
                switch( ymatch[1] ) {
                    default:       // follow through
                    case "top":    offsetY += Number(ymatch[3]); break;
                    case "middle": offsetY += Number(ymatch[3]); break;
                    case "bottom": offsetY -= Number(ymatch[3]); break;
                }
            }
        }
        return offsetY;
    },

    /**
     *  return {String}
     */
    getLabelText: function() {
        var text = this.data.label;
        if( this.options.showcollabel ) {
            text += "\n"
                 +  "( " + this.data.colNames.join(" / ") + " )";
        }
        text = text.toUpperCase();
        return text;
    },
    drawBackground: function() {
        if( this.options.svgbackground ) {
            this.canvas.rect( 0, 0, this.getCanvasWidth(), this.getCanvasHeight() )
                       .attr( "fill",   this.options.svgbackground )
                       .attr( "stroke", this.options.svgbackground )
                       .attr( "stroke-width", 0 )
                       .attr( "z-index", 0 );
        }
    },
    drawBorder: function() {
        if( this.options.svgborder ) {
            this.canvas.rect( 0, 0, this.getCanvasWidth(), this.getCanvasHeight() )
                       .attr( "stroke", this.options.svgborder )
                       .attr( "stroke-width", 1 )
                       .attr( "z-index", 0 );
        }
    },
    drawLabel: function() {
        if( this.options.showlabel ) {
            this.sprite.label = this.canvas.text( this.xy.label.x, this.xy.label.y, this.getLabelText() );
            this.sprite.label.attr({
                "font-size":   this.options.labelSize,
                "fill":        this.options.labelColor,
                "font-weight": this.options.labelWeight
            });
            return this.sprite.label;
        }
    }
});

$.ui.svgWidget.zIndex = 1;
$.ui.svgWidget.subclass('ui.svgWrapper', {
    klass: "$.ui.svgWrapper",
    options: {
    },
    _create: function() {
        $.noop();
    },
    _init: function() {
        $.noop();
    },
    /**
     *  Nothing to see here, move along
     *  @return {Object}
     */
    parseHtmlTable: function() {
        return {};
    },

    /**
     *  Define the current element as the wrapper, we need to do this to ensure HTML nesting
     *  @return {jQuery}
     */
    createWrapper: function() {
        this.wrapper = this.element;
        this.wrapper.addClass("svg-wrapper"); // Needs to be a parent to child nodes
        this.createWrapperInit();
        return this.wrapper;
    }
});
$.ui.svgWidget.subclass('ui.svgImage', {
    options: {
    },
    _create: function() {
    },
    _init: function() {
        this.draw();
    },
    draw: function() {
        this.canvas.image( this.element.attr("src"), 0, 0, this.getInitWidth(), this.getInitHeight() );
        this.element.hide();
    }
});
/**
 *  Works for both tables and trs
 */
$.ui.svgWidget.subclass('ui.svgBarChart', {
    klass: "$.ui.svgBarChart",
    options: {
        barHeight:      0,
        barSpacing:    30,
        barWidth:       0,
        barFontSize:   20,
        barFontWeight: "bold"
    },
    _create: function() {
        // Automatically calls super::_create()
    },
    _init: function() {
        this.draw();
    },
    parseOptions: function() {
        this.options.barCount     = this.options.barCount     || this.data.rowNames.length * this.data.colNames.length;
        this.options.barSpacing   = this.options.barSpacing   || Math.round( this.getInitWidth()/this.options.barCount * 0.2 );
        this.options.barWidth     = this.options.barWidth     || Math.round( this.getInitWidth()/this.options.barCount - this.options.barSpacing );
        this.options.barHeight    = this.options.barHeight    || Math.round( this.getInitHeight() - this.getFontSize()*2.5 - this.options.barFontSize*1.5 );
    },
    draw: function() {
        this.drawData();
        this.table.hide();
    },
    drawData: function() {
        this.sprite.bars    = {};
        this.sprite.barText = this.canvas.set();
        this.sprite.labels  = this.canvas.set();

        var barCount = 0;
        for( var i=0, n=this.data.rowNames.length; i<n; i++ ) {
            var rowName         = this.data.rowNames[i];
            this.sprite.bars    = this.canvas.set();
            this.sprite.barText = this.canvas.set();

            for( var j=0, m=this.data.colNames.length; j<m; j++, barCount++ ) {
                var colName   = this.data.colNames[j];
                var barHeight = Math.round( this.options.barHeight * this.data.values[rowName][colName] / this.data.stats.max ) || 0;
                var barLeft   = Math.round( (this.options.barWidth + this.options.barSpacing) * barCount );
                var barBase   = this.options.barHeight + this.options.barFontSize*1.5;
                var barTop    = barBase - barHeight;
                var barColor  = this.data.cells[colName][rowName].color || this.data.rows[rowName].color;

                this.sprite.bars.push(
                    this.canvas.rect( barLeft, barTop, this.options.barWidth, barHeight )
                               .attr( "fill",   barColor )
                               .attr( "stroke", barColor )
                               .attr( "stroke-width", 0 )
                );

                var textY = barTop - this.options.barFontSize;
                if (this.options.showlabel) {
	                this.sprite.barText.push(
	                    this.canvas.text( barLeft, textY, this.data.strings[rowName][colName] )
	                               .attr( "fill",        this.options.labelColor )
	                               .attr( "font-size",   this.options.barFontSize )
	                               .attr( "font-weight", this.options.barFontWeight )
	                               .attr( "text-anchor", "start" )
	                );
                }

                var labelY = barBase + this.options.labelSize*1.5;
                var label  = colName.toUpperCase().replace(/^(.*) (.*?)$/, "$1\n$2"); // Replace last space
                if (this.options.showcollabel) {
	                this.sprite.labels.push(
	                    this.canvas.text( barLeft, labelY, label )
	                               .attr( "fill",        this.options.labelColor )
	                               .attr( "font-weight", this.options.labelWeight ) // Bold
	                               .attr( "style",       "text-align:left" )
	                               .attr( "text-anchor", "start" )
	                );
                }
            }
        }
    }
});
$.ui.svgWidget.subclass('ui.svgHorizontalBar', {
    klass: "$.ui.svgHorizontalBar",
    options: {
        rowHeight:     20,
        cardHeight:    12,
        rowSpacing:     2,
        rowWidth:    null,
        centerWidth:  128,
        rowBackground:    "#f0f0f0",
        centerBackground: "#ffffff"
    },
    _create: function() {
        // Automatically calls super::_create()
    },
    _init: function() {
        this.options.rowWidth     = this.options.rowWidth     || Math.round( (this.getInitWidth()   - this.options.centerWidth)/2 );
        this.options.barOffset    = this.options.barOffset    || Math.round( this.options.rowHeight + this.options.rowSpacing );
        this.options.leftOffset   = this.options.leftOffset   || Math.round( 0 );
        this.options.centerOffset = this.options.centerOffset || Math.round( this.options.leftOffset + this.options.rowWidth + this.options.centerWidth/2 );
        this.options.rightOffset  = this.options.rightOffset  || Math.round( this.getInitWidth()     - this.options.rowWidth );
        this.options.browserTextOffset = $.browser.msie ? 2 : 0; // Annoying rendering bug in MSIE
        this.options.textHeightOffset = this.options.textHeightOffset || Math.round( this.options.rowHeight/2 + this.options.browserTextOffset );

        this.draw();
    },
    getInitHeight: function() {
        var canvasHeight = this.data.colNames.length * (this.options.rowHeight + this.options.rowSpacing ) - this.options.rowSpacing;
        return canvasHeight;
    },
    draw: function() {
        this.drawBackground();
        this.drawBackgroundBars();
        this.drawForegroundBars();
        this.drawLabels();
        this.table.hide();
    },
    drawBackgroundBars: function() {
        this.sprite.backgroundBars = this.canvas.set();
        for( var i=0, n=this.data.colNames.length; i<n; i++ ) {
            var colName   = this.data.colNames[i];
            
            switch( this.data.cols[colName].display ) {
                case "card": break;
                case "number": // follow through
                default:
                    this.sprite.backgroundBars.push(
                        this.canvas.rect( this.options.leftOffset,  this.options.barOffset*i, this.options.rowWidth, this.options.rowHeight ),
                        this.canvas.rect( this.options.rightOffset, this.options.barOffset*i, this.options.rowWidth, this.options.rowHeight )
                    );
            }
        }
        this.sprite.backgroundBars.attr({ 
            "fill":   this.options.rowBackground, 
            "stroke": this.options.rowBackground, // Chrome needs this set explicitly
            "stroke-width": 0 
        });
    },
    drawForegroundBars: function() {
        if( this.sprite.bars    ) { this.sprite.bars.remove(); }
        if( this.sprite.barText ) { this.sprite.barText.remove(); }

        this.sprite.bars    = this.canvas.set();
        this.sprite.barText = this.canvas.set();

        for( var teamName in this.data.rows ) {
            for( var i=0, n=this.data.colNames.length; i<n; i++ ) {
                var colName   = this.data.colNames[i];
                var barColor  = this.data.cols[colName].color || this.data.rows[teamName].color || this.options.color;

                switch( this.data.cols[colName].display ) {
                    case "card":
                        var barHeight  = this.options.cardHeight;
                        var barWidth   = Math.round( barHeight / 1.6 );     // Golden Ratio
                        var barHOffset = Math.round( this.options.rowHeight - barHeight )/2;
                        break;
                    case "number":
                        var barHeight  = this.options.rowHeight;
                        var barWidth   = 0;
                        var barHOffset = 0;
                        break;
                    default:
                        var barHeight  = this.options.rowHeight;
                        var barWidth   = Math.round( 0.95 * this.options.rowWidth * this.data.values[teamName][colName] / this.data.stats.max ) || 0;
                        var barHOffset = 0;
                        break;
                }

                var text = String(this.data.values[teamName][colName]);//.replace( /(\d+\.\d\d\d)\d+$/, '$1'); // round to 3dp if required
                if ( this.data.cols[colName].display == "label" ) {
                    text = this.data.strings[teamName][colName];
                }
                var textWidth = text.length * this.getFontSize()/5; // Offset for large numbers
                switch( this.data.rows[teamName].index % 2 ) {
                    case 0:
                        var sideOffset = Math.round( this.options.leftOffset + this.options.rowWidth - barWidth );
                        if( barWidth > this.options.rowHeight ) {
                            var textOffset = Math.round( this.options.rowWidth - textWidth - this.options.rowHeight/4);
                            var textColor  = "white";  // Inside the bar
                            if (barColor == "#FFFFFF" || barColor == "#ffffff" || barColor == "white") {
                                textColor = "black";
                            }
                        } else {
                            if (barColor == "#FFFFFF" || barColor == "#ffffff" || barColor == "white") {
                                var textOffset = Math.round( this.options.rowWidth - textWidth - this.options.rowHeight/4);
                            }
                            else {
                                var textOffset = Math.round( sideOffset - this.options.rowHeight/4 - textWidth );
                            }
                            var textColor  = "black";  // Outside the bar
                        }   
                        break;
                    case 1:
                        var sideOffset = Math.round( this.options.rightOffset );
                        if( barWidth > this.options.rowHeight ) {
                            var textOffset = Math.round( sideOffset + this.options.rowHeight/4 + textWidth);
                            var textColor  = "white"; // Inside the bar
                            if (barColor == "#FFFFFF" || barColor == "#ffffff" || barColor == "white") {
                                textColor = "black";
                            }
                        } else {
                            if (barColor == "#FFFFFF" || barColor == "#ffffff" || barColor == "white") {
                                var textOffset = Math.round( sideOffset + this.options.rowHeight/4 + textWidth);
                            }
                            else {
                                var textOffset = Math.round( sideOffset + barWidth + this.options.rowHeight/4 + textWidth);
                            }
                            var textColor  = "black"; // Outside the bar
                        }
                        break;
                    default:
                        console.error(this.klass ,':drawData(): invalid this.data.rows[',teamName,'].side ', this.data.rows[teamName].side);
                        break;
                }
                
                if (barColor == "#FFFFFF" || barColor == "#ffffff" || barColor == "white") {
                   /* this.sprite.bars.push(
                        this.canvas.rect( sideOffset, this.options.barOffset*i+barHOffset, barWidth, barHeight )
                                   .attr( "fill",   barColor )
                                   .attr( "stroke", "#666666" )
                                   .attr( "stroke-width", 0.2 )
                    );*/
                } else {
                    this.sprite.bars.push(
                        this.canvas.rect( sideOffset, this.options.barOffset*i+barHOffset, barWidth, barHeight )
                                   .attr( "fill",   barColor )
                                   .attr( "stroke", barColor )
                                   .attr( "stroke-width", 0 )
                    );
                }

                this.sprite.barText.push(
                    this.canvas.text( textOffset, (this.options.barOffset*i+this.options.textHeightOffset), text )
                               .attr( "fill",        textColor )
                               .attr( "font-weight", this.options.labelWeight ) // Bold
                );
            }
        }
    },
    drawLabels: function() {
        this.sprite.labels = this.canvas.set();
        for( var i=0, n=this.data.colNames.length; i<n; i++ ) {
            this.sprite.labels.push(
                this.canvas.text(
                    this.options.centerOffset,
                    this.options.barOffset*i + this.options.textHeightOffset,
                    this.data.colNames[i].toUpperCase()
                )
            );
        }
        this.sprite.labels.attr( "fill",        this.options.labelColor )
                          .attr( "font-weight", this.options.labelWeight ); // Bold
    }
});
$.ui.svgWidget.subclass('ui.svgLineChart', {
    klass: "ui.svgLineChart",
    options: {
        colorViewport: '#f3f8fb',
        colorGrid:     '#c7d0d7',
        colorSquare:   {},         // {Hash}   color for squares, { default:, WON:, DRAWN:, LOST: }, defaults to line color
        colorLine:     '#999',     // {String} color for line,    overridden by this.data.row[].color
        rangeXmin:  0,             // {Number} viewport min X: 0,  "auto-0.5"
        rangeXmax:  38,            // {Number} viewport max X: 38, "auto+0.5"
        rangeYmin:  0,             // {Number} viewport min Y
        rangeYmax:  20,            // {Number} viewport max Y
        gridUnitX:  1,             // {Number}
        gridUnitY:  0,             // {Number}
        labelUnitX: 5,             // {Number}
        labelUnitY: 0,             // {Number}
        lowestYValue:  null,       // {Number}  if set, autocalculate rangeYmin, rangeYmin, gridUnitY, labelUnitY
        highestYValue: null,       // {Number}  if set, autocalculate rangeYmin, rangeYmin, gridUnitY, labelUnitY
        labelX:     [],            // {Array}   Explicit entries to label
        labelY:     [],            // {Array}   Explicit entries to label
        invertX:    false,         // {Boolean} Invert the graph on X
        invertY:    true,          // {Boolean} Invert the graph on Y
        squareSize: 4,             // {Number}  pixels for each dot
        datapoint:  "midpoint"     // {String}  "line" or "midpoint"
    },
    required: {
        colorViewport: String,
        colorGrid:     String,
        rangeXmin:  /^(\d+|auto|(auto)?[+-]?\d*\.?\d+)$/,
        rangeXmax:  /^(\d+|auto|(auto)?[+-]?\d*\.?\d+)$/,
        rangeYmin:  /^(\d+|auto|(auto)?[+-]?\d*\.?\d+)$/,
        rangeYmax:  /^(\d+|auto|(auto)?[+-]?\d*\.?\d+)$/,
        gridUnitX:  Number,
        gridUnitY:  Number,
        labelUnitX: Number,
        labelUnitY: Number,
        labelX:     Array,
        labelY:     Array,
        invertX:    Boolean,
        invertY:    Boolean,
        squareSize: Number,
        datapoint: ["line", "midpoint"]
    },
    _create: function() {
    },
    _init: function() {
        this.draw();
    },
    parseHtmlTable: function() {
        var data = this._super();
        data.rowLabel = data.label.toUpperCase().split(/\s*\/\s*/)[0];
        data.colLabel = data.label.toUpperCase().split(/\s*\/\s*/)[1];
        return data;
    },
    parseOptions: function() {
        // Parse "auto" within rangeYmin, rangeYmax, rangeXmin, rangeXmax
        if( String(this.options.rangeYmin).match(/^auto[-+\d.]*$/) ) {
            this.options.rangeYmin = eval( this.options.rangeYmin.replace(/auto/, this.data.stats.min) );
        }
        if( String(this.options.rangeYmax).match(/^auto[-+\d.]*$/) ) {
            this.options.rangeYmax = eval( this.options.rangeYmax.replace(/auto/, this.data.stats.max) );
        }
        if( String(this.options.rangeXmin).match(/^auto[-+\d.]*$/) ) {
            this.options.rangeXmin = eval( this.options.rangeXmin.replace(/auto/, Math.min.apply(null, this.data.colNames) ) );
        }
        if( String(this.options.rangeXmax).match(/^auto[-+\d.]*$/) ) {
            this.options.rangeXmax = eval( this.options.rangeXmax.replace(/auto/, Math.max.apply(null, this.data.colNames)) );
        }

        // Ensure that specified data range actually covers given data
        if( this.options.rangeYmin > this.data.stats.min ) { console.error(this.klass,":parseOptions() - this.options.rangeYmin: ", this.options.rangeYmin, " > this.data.stats.min: ", this.data.stats.min ); }
        if( this.options.rangeYmax < this.data.stats.max ) { console.error(this.klass,":parseOptions() - this.options.rangeYmax: ", this.options.rangeYmax, " < this.data.stats.max: ", this.data.stats.max ); }
        this.options.rangeYmin = Math.min( this.options.rangeYmin, this.data.stats.min );
        this.options.rangeYmax = Math.max( this.options.rangeYmax, this.data.stats.max );

        switch( this.options.datapoint ) {
            case "line":
                break;
            case "midpoint":
                this.options.rangeXmin -= 0.5;
                this.options.rangeXmax += 0.5;
                this.options.rangeYmin -= 0.5;
                this.options.rangeYmax += 0.5;
                break;
            default:
                break;
        }

        if( this.options.invertX ) {
            this.options.rangeXmin = this.options.rangeXmin * -1;
            this.options.rangeXmax = this.options.rangeXmax * -1;
        }
        if( this.options.invertY ) {
            this.options.rangeYmin = this.options.rangeYmin * -1;
            this.options.rangeYmax = this.options.rangeYmax * -1;
        }


        // Ensure min is less than max
        if( this.options.rangeXmin > this.options.rangeXmax ) {
            var rangeXmin = this.options.rangeXmin;
            var rangeXmax = this.options.rangeXmax;
            this.options.rangeXmin = rangeXmax;
            this.options.rangeXmax = rangeXmin;
        }
        if( this.options.rangeYmin > this.options.rangeYmax ) {
            var rangeYmin = this.options.rangeYmin;
            var rangeYmax = this.options.rangeYmax;
            this.options.rangeYmin = rangeYmax;
            this.options.rangeYmax = rangeYmin;
        }


        // Define rangeXY as diff between min and max
        this.options.rangeX = this.options.rangeXmax - this.options.rangeXmin;
        this.options.rangeY = this.options.rangeYmax - this.options.rangeYmin;

        // Ensure we are not trying to graph a range of 0 
        while( this.options.rangeX < 4 ) {
            this.options.rangeX = ++this.options.rangeXmax - this.options.rangeXmin;
        }
        while( this.options.rangeY < 4 ) {
            this.options.rangeY = ++this.options.rangeYmax - this.options.rangeYmin;
        }

        // Auto calculate our grid and label unit spacing
        // calculateXY() will double the labelUnit if its less that getFontSize()*1.25
        var gridSpacings = [100,50,20,10,5,2,1];
        if( this.options.gridUnitX === 0 ) {
            for( var i=0, n=gridSpacings.length; i<n; i++ ) {
                this.options.gridUnitX  = gridSpacings[i];
                this.options.labelUnitX = gridSpacings[i]; // Updated in calculateXY()
                if( this.options.rangeX / gridSpacings[i] > 60 ) { break; }
            }
        }
        if( this.options.gridUnitY === 0 ) {
            for( var i=0, n=gridSpacings.length; i<n; i++ ) {
                this.options.gridUnitY  = gridSpacings[i];
                this.options.labelUnitY = gridSpacings[i]; // Updated in calculateXY()
                if( this.options.rangeY / gridSpacings[i] > 20 ) { break; }
            }
        }
        this.options.gridUnitX  = this.options.gridUnitX  || 1;
        this.options.labelUnitX = this.options.labelUnitX || 1;
        this.options.gridUnitY  = this.options.gridUnitY  || 1;
        this.options.labelUnitY = this.options.labelUnitY || 1;
    },
    calculateXY: function() {
        this._super();
        this.xy = this.xy || {};
        this.xy.viewport = {
            top:    this.getFontSize(),
            left:   Math.round( this.getFontSize()*3.5 ), // Needs a bigger margin due to viewport resizing below
            base:   Math.round( this.getInitHeight() - this.getFontSize()*2.5 ),
            right:  Math.round( this.getInitWidth()  - 2 )
        };
        this.xy.viewport.width  = Math.round( this.xy.viewport.right - this.xy.viewport.left );
        this.xy.viewport.height = Math.round( this.xy.viewport.base  - this.xy.viewport.top  );

        this.xy.valueSpacing = {
            x: this.xy.viewport.width  / this.options.rangeX,
            y: this.xy.viewport.height / this.options.rangeY
        };
        this.xy.gridSpacing = {
            x: this.xy.valueSpacing.x * this.options.gridUnitX,
            y: this.xy.valueSpacing.y * this.options.gridUnitY
        };

        // This really should be part of this.parseOptions(), but we need access to this.xy.valueSpacing
        while( this.xy.valueSpacing.x * this.options.labelUnitX < this.getFontSize()*1.25 ) {
            this.options.labelUnitX = this.options.labelUnitX * 2;
        }
        while( this.xy.valueSpacing.y * this.options.labelUnitY < this.getFontSize()*1.25 ) {
            this.options.labelUnitY = this.options.labelUnitY * 2;
        }

        //// Resize off the viewport to exactly fit the grid
        //this.xy.viewport.left   = this.xy.viewport.right - (this.xy.valueSpacing.x * this.options.rangeX);
        //this.xy.viewport.base   = this.xy.viewport.top   + (this.xy.valueSpacing.y * this.options.rangeY);
        //this.xy.viewport.width  = Math.round( this.xy.viewport.right - this.xy.viewport.left );
        //this.xy.viewport.height = Math.round( this.xy.viewport.base  - this.xy.viewport.top  );

        // Define Labels
        this.xy.label = {};
        this.xy.label.rowNumber = { x: this.xy.viewport.left - this.getFontSize()   };
        this.xy.label.rowTitle  = { x: this.xy.viewport.left - this.getFontSize()*2 };
        this.xy.label.colNumber = { y: this.xy.viewport.base + this.getFontSize()   };
        this.xy.label.colTitle  = { y: this.xy.viewport.base + this.getFontSize()*2 };

        // Define the origin
        this.xy.valueOrigin = {};
        switch( this.options.invertX ) {
                 case true:  this.xy.valueOrigin.x = Math.round( this.xy.viewport.left - this.xy.valueSpacing.x * this.options.rangeXmin ); break;
        default: case false: this.xy.valueOrigin.x = Math.round( this.xy.viewport.left - this.xy.valueSpacing.x * this.options.rangeXmin ); break;
        }
        switch( this.options.invertY ) {
                 case true:  this.xy.valueOrigin.y = Math.round( this.xy.viewport.base + this.xy.valueSpacing.y * this.options.rangeYmin ); break;
        default: case false: this.xy.valueOrigin.y = Math.round( this.xy.viewport.base + this.xy.valueSpacing.y * this.options.rangeYmin ); break;
        }
        if( this.options.invertX ) { this.options.labelX = $.map( this.options.labelX, function(x) { return -x; } ); }
        if( this.options.invertY ) { this.options.labelY = $.map( this.options.labelY, function(y) { return -y; } ); }

        switch( this.options.datapoint ) {
            case "line":
                this.xy.gridOrigin  = this.xy.valueOrigin;
                break;
            case "midpoint":
                this.xy.gridOrigin = {};
                this.xy.gridOrigin.x = Math.round( this.xy.valueOrigin.x - this.xy.gridSpacing.x/2 );
                this.xy.gridOrigin.y = Math.round( this.xy.valueOrigin.y - this.xy.gridSpacing.y/2 );
                break;
            default:
                console.error(this.klass+":parseXY() - invalid this.options.datapoint: ", this.options.datapoint);
        }


        this.xy.points = {};
        for( var i=0, n=this.data.rowNames.length; i<n; i++ ) {
            var rowName = this.data.rowNames[i];
            this.xy.points[rowName] = {};
            for( var j=0, m=this.data.colNames.length; j<m; j++ ) {
                var colName = this.data.colNames[j];

                var x = this.xy.valueOrigin.x + this.xy.valueSpacing.x * Number(colName)                    * (this.options.invertX ? -1 : 1);
                var y = this.xy.valueOrigin.y - this.xy.valueSpacing.y * this.data.values[rowName][colName] * (this.options.invertY ? -1 : 1);
                this.xy.points[rowName][colName] = { x: x, y: y };
            }
        }
        this.makePointsNonOverlapping();
        return this.xy;
    },

    /**
     *  Modifies this.xy.points inline, side shifts any overlapping points
     */
    makePointsNonOverlapping: function() {

        // Create mapping of overlapping points
        var pointHash = {};
        for( var i=0, n=this.data.rowNames.length; i<n; i++ ) {
            var rowName = this.data.rowNames[i];
            for( var j=0, m=this.data.colNames.length; j<m; j++ ) {
                var colName = this.data.colNames[j];

                var point = this.xy.points[rowName][colName];
                var hash  = point.x + ":" + point.y;
                if( !pointHash[hash] ) { pointHash[hash] = []; }
                pointHash[hash].push( point );
            }
        }
            
        // Adjust those that are overlapping
        var squareSize = this.options.squareSize;
        for( var hash in pointHash ) {
            switch( pointHash[hash].length ) {
                case 0:
                case 1:
                    // [0]
                    $.noop();
                    break; 
                case 2:
                    // [0][1]
                    pointHash[hash][0].x -= squareSize/2;
                    pointHash[hash][1].x += squareSize/2;
                    break;
                case 3: 
                    // [0][1][2]
                    pointHash[hash][0].x -= squareSize;
                    pointHash[hash][1].x += 0;
                    pointHash[hash][2].x += squareSize;
                    break;
                case 4:
                    // [2][3]
                    // [0][1]
                    pointHash[hash][0].x -= squareSize/2;
                    pointHash[hash][0].y -= squareSize/2;
                    pointHash[hash][1].x += squareSize/2;
                    pointHash[hash][1].y -= squareSize/2;
                    pointHash[hash][2].x -= squareSize/2;
                    pointHash[hash][2].y += squareSize/2;
                    pointHash[hash][3].x += squareSize/2;
                    pointHash[hash][3].y += squareSize/2;
                    break;
                case 5:
                    //    [4]
                    // [0][1][2]
                    //    [3]
                    pointHash[hash][0].x -= squareSize;
                    pointHash[hash][1].x += 0;
                    pointHash[hash][2].x += squareSize;
                    pointHash[hash][3].y -= squareSize;
                    pointHash[hash][4].y += squareSize;
                    break;
                default: 
                    console.warn(this.klass,":makePointsNonOverlapping(): too many points: pointHash[hash].length ", pointHash[hash].length );
                    // follow through
                case 6:
                    // [3][4][5]
                    // [0][1][2]
                    pointHash[hash][0].x -= squareSize;
                    pointHash[hash][0].y -= squareSize/2;
                    pointHash[hash][1].x += 0;
                    pointHash[hash][1].y -= squareSize/2;
                    pointHash[hash][2].x += squareSize;
                    pointHash[hash][2].y += squareSize/2;
                    
                    pointHash[hash][3].x -= squareSize;
                    pointHash[hash][3].y += squareSize/2;
                    pointHash[hash][4].x += 0;
                    pointHash[hash][4].y += squareSize/2;
                    pointHash[hash][5].x += squareSize;
                    pointHash[hash][5].y += squareSize/2;
                    break;
            }
        }
    },
    draw: function() {
        this.drawViewport();
        this.drawGrid();
        this.drawGridLabels();
        this.drawData();
        this.addEventHandlers();
        this.table.hide();
    },
    drawViewport: function() {
        this.canvas.rect( this.xy.viewport.left, this.xy.viewport.top, this.xy.viewport.width, this.xy.viewport.height )
            .attr({
                "fill":   this.options.colorViewport,
                "stroke": this.options.colorGrid,
                "stroke-width": 1
            });
    },
    drawGrid: function() {
        var gridPath = ["M", 0, 0];

        // We need to center on grid origin, and space by gridSpacing, but only rendering within the viewport
        // HACK: Do the loop twice, starting from the origin. In theory we should be able to do this a single loop
        for( var y = this.xy.gridOrigin.y; y > this.xy.viewport.top+2;  y = Number(y - this.xy.gridSpacing.y) ) {   // bottom to top
            if( y >= this.xy.viewport.base ) { continue; } // don't draw outside viewport
            gridPath.push([ "M", this.xy.viewport.left, Number(y), "L", this.xy.viewport.right, Number(y)  ]);
        }
        for( var y = this.xy.gridOrigin.y; y < this.xy.viewport.base-2; y = Number(y + this.xy.gridSpacing.y) ) {   // bottom to top
            if( y <= this.xy.viewport.top ) { continue; } // don't draw outside viewport
            gridPath.push([ "M", this.xy.viewport.left, Number(y), "L", this.xy.viewport.right, Number(y)  ]);
        }

        for( var x = this.xy.gridOrigin.x; x < this.xy.viewport.right-2; x = Number(x + this.xy.gridSpacing.x) ) { // left to right
            if( x <= this.xy.viewport.left ) { continue; } // don't draw outside viewport
            gridPath.push([ "M", Number(x), this.xy.viewport.top, "L", Number(x), this.xy.viewport.base  ]);
        }
        for( var x = this.xy.gridOrigin.x; x > this.xy.viewport.left-2;  x = Number(x - this.xy.gridSpacing.x) ) { // left to right
            if( x >= this.xy.viewport.right ) { continue; } // don't draw outside viewport
            gridPath.push([ "M", Number(x), this.xy.viewport.top, "L", Number(x), this.xy.viewport.base  ]);
        }
        this.canvas.path( gridPath ).attr({
            "stroke": this.options.colorGrid,
            "stroke-width": 0.5
        });
    },
    drawGridLabels: function() {
        var render = { x: [], y: [] };

        // Out range should center on 0
        var moduloXmin = this.options.rangeXmin - this.options.rangeXmin % this.options.labelUnitX;
        var moduloYmin = this.options.rangeYmin - this.options.rangeYmin % this.options.labelUnitY;

        // Calculate which labels we will display, this.options.labelXY + (rangeXYmin -> rangeXYmax)
        for( var i=0; i<this.options.labelX.length; i++                              ) { render.x.push( this.options.labelX[i] ); }
        for( var i=moduloXmin; i<=this.options.rangeXmax; i+=this.options.labelUnitX ) { render.x.push( i ); }
        for( var i=0; i<this.options.labelY.length; i++                              ) { render.y.push( this.options.labelY[i] ); }
        for( var i=moduloYmin; i<=this.options.rangeYmax; i+=this.options.labelUnitY ) { render.y.push( i ); }

        // Render Labels
        for( var i=0, n=render.x.length; i<n; i++ ) {
            var colValue  = Number( render.x[i] );
            var colText   = colValue * (this.options.invertX ? -1 : 1 );
            if( colValue < this.options.rangeXmin ) { continue; }
            var colOffset = this.xy.valueOrigin.x + this.xy.valueSpacing.x * colValue;
            this.canvas.text( colOffset, this.xy.label.colNumber.y, colText );
        }
        for( var i=0, n=render.y.length; i<n; i++ ) {
            var rowValue  = Number( render.y[i] );
            var rowText   = rowValue * (this.options.invertY ? -1 : 1 );
            if( rowValue < this.options.rangeYmin ) { continue; }
            var rowOffset = this.xy.valueOrigin.y - this.xy.valueSpacing.y * rowValue;
            this.canvas.text( this.xy.label.rowNumber.x, rowOffset, rowText );
        }

        // Render Titles
        this.canvas.text( this.xy.label.rowTitle.x, this.xy.viewport.top + this.xy.viewport.height/2, this.data.rowLabel ).attr("font-weight", 700).rotate(-90);
        this.canvas.text( this.xy.viewport.left + this.xy.viewport.width/2, this.xy.label.colTitle.y, this.data.colLabel ).attr("font-weight", 700);
    },
    drawData: function() {
        this.sprite.points = {};
        for( var i=0, n=this.data.rowNames.length; i<n; i++ ) {
            var rowName    = this.data.rowNames[i];  
            var lineColor  = this.data.rows[rowName].color || this.options.colorLine;
            var squareSize = this.options.squareSize;
            
            var squares = [];
            var linePath = [];
            this.sprite.points[rowName] = {};

            var lastX = 0;
            var lastY = 0;
            for( var j=0, m=this.data.colNames.length; j<m; j++ ) {
                var colName     = this.data.colNames[j];
                var result      = this.data.cells[rowName][colName].result;
                var squareColor = this.options.colorSquare[result] || this.options.colorSquare['default'] || lineColor;
                
                var x = this.xy.points[rowName][colName].x;
                var y = this.xy.points[rowName][colName].y;
                var squareX = x - this.options.squareSize/2;
                var squareY = y - this.options.squareSize/2;

                if( linePath.length ) {
                    linePath.push([ "L", lastX, lastY, x, y ]);
                } else {
                    linePath.push([ "M", x, y ]);
                }

                // Backing Square
                if( $.browser.msie ) {
	                squares.push({ 
	                	x:      squareX-squareSize, 
	                	y:      squareY-squareSize, 
	                	width:  squareSize*3, 
	                	height: squareSize*3, 
	                	fill:   "none", 
	                	stroke: "none", 
	                	"stroke-width": 0,
	                	rowName: rowName,
	                	colName: colName
	                });
                }
                // Foreground Squares - rendered after line 
                squares.push({ 
                	x:      squareX, 
                	y:      squareY, 
                	width:  this.options.squareSize, 
                	height: this.options.squareSize, 
                	fill:   squareColor, 
                	stroke: squareColor, 
                	"stroke-width": 1,
                	rowName: rowName,
                	colName: colName
                });
                
                lastX = x;
                lastY = y;
            }
            
            // Render
            this.canvas.path(linePath).attr({
                "stroke": lineColor,
                "stroke-width": 1
            });
            
            for( var k=0, o=squares.length; k<o; k++) {
            	var square = squares[k];
            	var rect = this.canvas.rect( square.x, square.y, square.width, square.height );
            	rect.attr({
        			"fill":         square.fill,
                    "stroke":       square.stroke,
                    "stroke-width": square["stroke-width"]
                });
            	this.sprite.points[square.rowName][square.colName] = rect;
            }

        }
    },
    addEventHandlers: function() {
        for( var rowName in this.sprite.points ) {
            for( var colName in this.sprite.points[rowName] ) {
                this.sprite.points[rowName][colName].hover(
                    $.proxy( this.onHover, this, this.data.cells[rowName][colName] ),
                    $.proxy( this.unHover, this, this.data.cells[rowName][colName] )
                );
            }
        }
    },
    onHover: function( cell, event ) {
        //console.log('DEBUG: ', this&&this.klass||'' ,' onHover: function( event ) { ',  event);
    },
    unHover: function( cell, event ) {
        //console.log('DEBUG: ', this&&this.klass||'' ,' unHover: function( event ) { ',  event);
    }
});






$.ui.svgLineChart.subclass('ui.svgLineChartTooltip', {
    klass: "ui.svgLineChartTooltip",
    options: {
        ajaxUrlExample: "",
        tooltip: {
            width:   165,
            height:  60,
            tipSize: 10,
            border:     '#ccc',
            background: '#fff'
        }
    },
    tooltip: null,                 // {Raphael.set} set of sprites comprising the tooltip
    spinner: null,                 // {Raphael.set} nodes representing the spinner
    parentWidget: null,            // {Widget}      parent widget - contains logo url info
    


    //***** Init *****//

    _init: function() {
        this.parentWidget = this.element.parents("[widget]").first().data("widget");
        this.tooltip = this.getCanvas().set();
        this.spinner = this.getCanvas().set();
    },
    bindBodyClickEvent: function( tooltip ) {
        $(document.body).unbind("click.bodyClickEvent");
        $(document.body).bind("click.bodyClickEvent", $.proxy(this._onBodyClickEvent, this, tooltip) );
    },
    unbindBodyClickEvent: function() {
        $(document.body).unbind("click.bodyClickEvent");
    },


    //***** Event Handlers *****//

    onHover: function( cell, event ) {
        var self = this;
        var row = this.data.rows[cell.rowName];
        var matchId = cell.matchid; // lowercase within html

        var ajaxUrlExample = cell.ajaxurlexample || row.ajaxurlexample || this.options.ajaxUrlExample || "";
        var ajaxUrl        = ajaxUrlExample.replace(/MATCH_ID/, matchId).replace(/(\.json)?$/, '.json');

        if( ajaxUrl ) {
            var counter = ++$.ui.svgLineChartTooltip.counter; // Semaphore, only display the last onHover event
            self.drawTooltip( cell );
            self.drawSpinner( cell );
            $.ajax({
                type: "GET",
                url: ajaxUrl,
                success: function( json, xhr, status ) {
                    if( counter == $.ui.svgLineChartTooltip.counter ) {
                        self.fillTooltip( cell, json );
                    }
                }
            });
        }
    },
    unHover: function( cell, event ) {
        //undrawTooltip();
    },
    _onBodyClickEvent: function( tooltip, event ) {
        var canvasTop  = $(this.getCanvas().canvas).offset().top;
        var canvasLeft = $(this.getCanvas().canvas).offset().left;
        
        if( event.pageX > canvasLeft + tooltip.left
         && event.pageX < canvasLeft + tooltip.right
         && event.pageY > canvasTop  + tooltip.top 
         && event.pageY < canvasTop  + tooltip.base
        ) {
            // Click was inside tooltip
            $.noop();
        } else {
            // Click was outside tooltip - remove
            this.undrawTooltip();
            this.unbindBodyClickEvent();
        }
    },



    //***** Data Functions *****//

    getTooltipTextXY: function( cell ) {
        var tooltip = this.options.tooltip;
        var xy_point = this.xy.points[cell.rowName][cell.colName];

        tooltip.trueLeft   = Math.round( xy_point.x - tooltip.width/2 );
        tooltip.left       = Math.max( tooltip.trueLeft,  2 );
        tooltip.trueRight  = tooltip.left + tooltip.width;
        tooltip.right      = Math.min( tooltip.trueRight, this.getWrapper().width()-4 );
        tooltip.left       = Math.max( tooltip.right - tooltip.width, 2 ); // double check against right
        tooltip.width      = Math.min( tooltip.right - tooltip.left, tooltip.width );
        tooltip.center     = tooltip.trueLeft + tooltip.width/2;
        
        if( xy_point.y + this.options.squareSize*2 + tooltip.height <= this.getWrapper().height() ) {
            tooltip.top     = Math.round( xy_point.y + this.options.squareSize*2 );
            tooltip.base    = tooltip.top + tooltip.height;
            tooltip.bodyTop = tooltip.top + tooltip.tipSize;
            tooltip.invert  = false;
        } else { // Invert
            tooltip.base    = Math.round( xy_point.y - this.options.squareSize*2 );
            tooltip.top     = tooltip.base - tooltip.height;
            tooltip.bodyTop = tooltip.top;
            tooltip.invert  = true;
        }
        tooltip.middle     = tooltip.top    + tooltip.height/2;
        tooltip.bodyHeight = tooltip.height - tooltip.tipSize;
        tooltip.textIndent = tooltip.bodyHeight * 1.1;
                
        tooltip.text = {
            x: tooltip.left    + tooltip.textIndent,
            y: tooltip.bodyTop + (tooltip.bodyHeight - this.getFontSize()*2)/2
        };
        if( $.browser.msie ) { tooltip.text.y += 2; }

        return tooltip;
    },

    getResultDigest: function( json ) {
        var resultDigest = $.breadthFirstKeySearch( "resultDigest", json );
        resultDigest          = resultDigest          || { at:"-", result: "" };
        resultDigest.vs       = resultDigest.vs       || { name: "", id: "" };
        resultDigest.score    = resultDigest.score    || { home: "", away: ""};
        if( resultDigest.cmsAlias ) {
            resultDigest.matchUrl = "matches/" 
                                    + resultDigest.cmsAlias[0] + "/"
                                    + resultDigest.cmsAlias[1] + ".html/"
                                    + resultDigest.cmsAlias[2];
        } else {
            resultDigest.matchUrl = "";
        }
        return resultDigest;
    },

    /**
     *  @param  {Object}        resultDigest
     *  @return {Array<Object>} { text: "", attr: {} }
     */
    getTooltipTextData: function( tooltip, resultDigest, cell ) {
        var textData = [];
        textData.push({ 
            text: "v " + resultDigest.vs.name + " (" + String(resultDigest.at||'-').substr(0,1) + ")",
            attr: { "font-weight": 700 }
        });
        textData.push({
            text: resultDigest.result.capitalize() + " " + resultDigest.score.home + "-" + resultDigest.score.away,
            attr: {}
        });
        if( resultDigest.matchUrl ) {
            textData.push({
                text: "Match Report",
                attr: { "font-weight": 700, "href": resultDigest.matchUrl }
            });
        }
        return textData;
    },
    getTooltipImageData: function( tooltip, resultDigest, cell ) {
        var parentWidgetRow = this.parentWidget.data.rows[ resultDigest.vs.id ] || {};
        var tooltip = this.getTooltipTextXY(cell);

        var logoData = [];
        logoData.push({
            url:    parentWidgetRow.logourl,
            width:  parentWidgetRow.logowidth,
            height: parentWidgetRow.logoheight,
            x:      tooltip.left    + (tooltip.textIndent - parentWidgetRow.logowidth)/2,
            y:      tooltip.bodyTop + (tooltip.bodyHeight - parentWidgetRow.logoheight)/2
        });
        return logoData;
    },



    //***** Render Functions *****//
    
    drawSpinner: function( cell ) {
        this.undrawSpinner();
        this.spinner.push( this.canvas.text(this.options.tooltip.center, this.options.tooltip.middle, "...") );
        this.tooltip.push( this.spinner ); // Ensure it gets removed on undraw tooltip
    },
    undrawSpinner: function( cell ) {
        this.spinner.remove();
    },


    drawTooltip: function( cell ) {
        this.undrawTooltip();

        var tooltip = this.getTooltipTextXY( cell );
        if( tooltip.invert === false ) {
            var path = this.canvas.path([
                "M", tooltip.left,                      tooltip.top + tooltip.tipSize,
                "L", tooltip.center - tooltip.tipSize,  tooltip.top + tooltip.tipSize,
                "L", tooltip.center,                    tooltip.top,
                "L", tooltip.center + tooltip.tipSize,  tooltip.top + tooltip.tipSize,
                "L", tooltip.right,                     tooltip.top + tooltip.tipSize,
                "L", tooltip.right,                     tooltip.base,
                "L", tooltip.left,                      tooltip.base,
                "Z"
            ]);
        } else { // tooltip.invert === true
            var path = this.canvas.path([
                "M", tooltip.left,                      tooltip.base - tooltip.tipSize,
                "L", tooltip.center - tooltip.tipSize,  tooltip.base - tooltip.tipSize,
                "L", tooltip.center,                    tooltip.base,
                "L", tooltip.center + tooltip.tipSize,  tooltip.base - tooltip.tipSize,
                "L", tooltip.right,                     tooltip.base - tooltip.tipSize,
                "L", tooltip.right,                     tooltip.top,
                "L", tooltip.left,                      tooltip.top,
                "Z"
            ]);
        } 
        
        path.attr({
            "stroke":       tooltip.border,
            "stroke-width": 1,
            "fill":         tooltip.background
        });
        this.tooltip.push( path );
        this.bindBodyClickEvent(tooltip);
    },
    undrawTooltip: function() {
        this.tooltip.remove();
    },


    fillTooltip: function( cell, json ) {
        this.undrawSpinner();

        var tooltip = this.options.tooltip;
        var resultDigest = this.getResultDigest( json );

        if( resultDigest && this.tooltip && this.options.tooltip ) {
            try {
                var tooltipTextData  = this.getTooltipTextData(  tooltip, resultDigest, cell );
                var tooltipImageData = this.getTooltipImageData( tooltip, resultDigest, cell );

                // TODO: How can we ensure that the text is never bigger than the tooltip?
                
                // Render Text Strings
                for( var i=0, n=tooltipTextData.length; i<n; i++ ) {
                    var text = tooltipTextData[i];
                    var node = this.canvas.text( tooltip.text.x, tooltip.text.y + this.getFontSize()*i, text.text );
                    for( var key in text.attr ) {
                        node.attr( key, text.attr[key] );
                    }
                    node.attr({ "text-anchor": "start" });
                    this.tooltip.push(node);
                }
                    
                for( var i=0, n=tooltipImageData.length; i<n; i++ ) {
                    var logo = tooltipImageData[i];
                    var node = this.canvas.image( logo.url, logo.x, logo.y, logo.width, logo.height );
                    for( var key in logo.attr ) {
                        node.attr( key, logo.attr[key] );
                    }
                    this.tooltip.push(node);
                }
            } catch(e) {
                console.error('Exception: ', this&&this.klass||'', 'fillTooltip(',cell, json,')', e);
            }
        }
    }
});
$.ui.svgLineChartTooltip.counter = 0;
$.ui.svgWidget.subclass('ui.svgPieChart', {
    klass: "$.ui.svgPieChart",
    options: {
        radius:             0,
        insideRadius:       0,
        anticlockwise:  false,
        colors:            "",
        strokes:           "",
        rowName:         null,
        svgwidth:         140,
        labelOffset:       30,
        sliceLabelSize:    16,
        sliceLabelWeight: 700,
        sliceLabelColor:  "white",
        naTxt:			  "N/A",
        naColor:		  "#CCCCCC"
    },
    _create: function() {
    },

    _init: function() {
        // We only want one row for this widget
        this.drawPieChart();
        this.checkPieChart();
        this.drawLabel();
        this.table.hide();
    },


    getInitHeight: function() {
        return Math.ceil( this.options.svgwidth + this.xy.origin.y*2 + this.options.labelOffset*1.5 );
    },
    getInitWidth: function() {
        return Math.ceil( this.options.svgwidth );
    },


    /**
     *  @return {Hash}  this.data
     */
    parseHtmlTable: function() {
        this.data = this._super();

        if( !this.options.rowName ) {
            this.options.rowName = this.element.attr("name") || this.element.find("th").text(); // Same as in _super()
        }
        return this.data;
    },
    parseOptions: function() {
        this._super();
        this.options.radius = this.options.radius || this.options.svgwidth/2 - 4;
        return this.options;
    },
    calculateXY: function() {
        var o = this.options;
        var xy = this.xy = this.xy || {};

        xy.origin = { x: 0, y: 0 }; // Avoid cropping
        xy.label = {
            x: this.getInitWidth()/2,
            y: this.getInitHeight() - this.getFontSize()*1.5
        };
        
        return xy;
    },
    drawPieChart: function( rowName ) {
        var sliceInfo = this.getSlices() 	|| {};
        var slices	  = sliceInfo.s 		|| {};
        var total	  = sliceInfo.total;
        
        if ( total > 0 ) {
	        for( var i=0, n=slices.length; i<n; i++ ) {
	            this.drawSlice( slices[i] );
	        }
	        // Ensure labels are always on top of slices
	        for( var i=0, n=slices.length; i<n; i++ ) {
	            this.drawSliceLabel( slices[i] );
	        }
        }
    },
    checkPieChart: function () {
    	// Just check if the total is 0 and draw circle as fallback
    	// Inefficient, but works
    	var runningTotal = 0;
        for( var i=0, n=this.data.colNames.length; i<n; i++ ) {
            var colName  = this.data.colNames[i];
            var rowValue = Number( this.data.values[this.options.rowName][colName] );
            runningTotal += rowValue;
        }
        if (runningTotal === 0) {
        	/**
        	 * drawCircle instead
        	 */
	    	var c = c || {};
	    	c.x			 = c.x			|| this.options.svgwidth/2 + this.xy.origin.x;
	    	c.y			 = c.y			|| this.options.svgwidth/2 + this.xy.origin.y;
	    	c.radius     = c.radius     || this.options.radius; // Avoid cropping
	        c.color		 = this.options.naColor;
	    	this.canvas.circle(
	    		c.x,
	    		c.y,
	    		c.radius
	    	).attr({
	    		"stroke-width": 1, // Avoids sharp edges
	            "stroke": c.color,
	            "fill":   c.color
	    	});
	    	
	        var label    = this.options.naTxt;
	        var sprite = this.canvas.text( c.x, c.y, label.toUpperCase() );
	        sprite.attr( "fill",        this.options.sliceLabelColor );
	        sprite.attr( "font-size",   this.options.sliceLabelSize );
	        sprite.attr( "font-weight", this.options.sliceLabelWeight );
	
	        this.sprite.sliceLabels = this.sprite.sliceLabels || [];
	        this.sprite.sliceLabels.push( sprite );
        }
    },
    getSlices: function() {
        var slices = [];
        var runningTotal = 0;
        for( var i=0, n=this.data.colNames.length; i<n; i++ ) {
            var colName  = this.data.colNames[i];
            var rowValue = Number( this.data.values[this.options.rowName][colName] );

            var label      = rowValue + "%";
            var startValue = runningTotal;
            var endValue   = runningTotal + rowValue;
            if( rowValue === 0 ) { startValue--; }
            if( rowValue === this.data.totals[ this.options.rowName ] ) { endValue--;   }

            var colors  = this.options.colors && this.options.colors.split(",");
            var color   = colors && colors[ i % colors.length ] || this.data.cols[colName].color;
            var strokes = this.options.strokes && this.options.strokes.split(",");
            var stroke  = strokes && strokes[ i % strokes.length ] || this.data.cols[colName].stroke;
            
            
            var sliceOptions = {
                startValue: startValue,
                endValue:   endValue, 
                label:      label,
                color:      color,
                stroke:		stroke,
                radius:     this.options.radius,
                inradius:   this.options.insideRadius,
                anticlock:  this.options.anticlockwise ? 1 : 0
            };
            slices.push( this.getSlice(sliceOptions) );
            runningTotal += rowValue;
        }
        return {s: slices, total: runningTotal};
    },

    /**
     *  @param {Hash}   s               Param hash, additional values will be returned
     *  @param {Number} s.startValue    [required] value slice should start from
     *  @param {Number} s.endValue      [required] value slice should end from
     *  @param {Number} s.radius        [override] radius of the slice
     *  @param {Number} s.inradius      [override] inside radius of the slice
     *  @param {Number} s.total         [override] what is considered 100%
     *  @param {Number} s.startAngle    [override] startAngle in radians
     *  @param {Number} s.endAngle      [override] endAngle in radians
     *  @param {Number} s.cx            [override] x center of the circle
     *  @param {Number} s.cy            [override] y center of the circle
     */
    getSlice: function( s ) {
        s = s || {};

        console.assert( typeof s.startValue === "number", this.klass+":getSlice(", s ,"): s.startValue is ", typeof s.startValue );
        console.assert( typeof s.endValue   === "number", this.klass+":getSlice(", s ,"): s.endValue is ",   typeof s.endValue );

        // Note all angles in radians. circle = 2 pi radians
        s.total      = s.total      || this.data.totals[ this.options.rowName ];
        s.radius     = s.radius     || this.options.svgwidth/2 - 4; // Avoid cropping
        s.inradius   = s.inradius   || 0;
        s.startAngle = s.startAngle || (s.startValue / s.total) * Math.PI*2;
	    s.endAngle   = s.endAngle   || (s.endValue   / s.total) * Math.PI*2;
        s.cx         = s.cx         || this.options.svgwidth/2 + this.xy.origin.x;
        s.cy         = s.cx         || this.options.svgwidth/2 + this.xy.origin.y;

        if( s.startAngle > Math.PI*2 ) {
            s.startAngle = s.startAngle % (Math.PI*2);
        }
        if( s.endAngle > Math.PI*2 ) {
            s.endAngle = s.endAngle % (Math.PI*2);
        }
        
        if( s.anticlock ) {
            s.startAngle = -s.startAngle;
            s.endAngle   = -s.endAngle;
        }

        s.angle       = s.endAngle - s.startAngle;
        s.midAngle    = s.startAngle + s.angle/2;
        s.isLargeArc  = Math.abs(s.angle) > Math.PI ? 1 : 0;
        s.sweep       = Number( !!s.anticlock );
        s.antisweep   = Number(  !s.anticlock );

        s.x1  = Math.round( s.cx - s.radius   * Math.sin(s.startAngle) );
        s.x2  = Math.round( s.cx - s.radius   * Math.sin(s.endAngle  ) );
        s.ix1 = Math.round( s.cx - s.inradius * Math.sin(s.startAngle) );
        s.ix2 = Math.round( s.cx - s.inradius * Math.sin(s.endAngle  ) );
        //s.xm  = Math.round( s.cx - s.radius/2 * Math.sin(s.midAngle  ) );
        s.xm = Math.round( s.cx - (s.inradius + (s.radius-s.inradius)/2) * Math.sin(s.midAngle) );

        s.y1  = Math.round( s.cy - s.radius   * Math.cos(s.startAngle) );
        s.y2  = Math.round( s.cy - s.radius   * Math.cos(s.endAngle  ) );
        s.iy1 = Math.round( s.cy - s.inradius * Math.cos(s.startAngle) );
        s.iy2 = Math.round( s.cy - s.inradius * Math.cos(s.endAngle  ) );
        //s.ym  = Math.round( s.cy - s.radius/2 * Math.cos(s.midAngle  ) );
        s.ym  = Math.round( s.cy - (s.inradius + (s.radius-s.inradius)/2) * Math.cos(s.midAngle) );


        // Presentation Hack - Vertical align labels if both sides between 44% and 56%
        if( this.data.colNames.length == 2 
         && Math.abs(s.angle) > Math.PI * 7/8
         && Math.abs(s.angle) < Math.PI * 9/8
        ) {
            s.ym = s.cy;
        }

        return s;
    },

    /**
     *  @see http://www.w3.org/TR/SVG/paths.html#PathData
     *  @see https://github.com/DmitryBaranovskiy/g.raphael/raw/master/g.pie.js
     *  @param  {Hash}    slice  this.getSlice( startValue, endValue )
     *  @return {Raphael}
     */
    drawSlice: function( slice ) {
        var s = slice;
        var path = [];
        if( s.inradius ) {
            path = [
                "M", s.x1,  s.y1,                                                         // Line to outside
                "A", s.radius,   s.radius,   0, s.isLargeArc, s.sweep,     s.x2, s.y2,    // Arc outside 
                "L", s.ix2, s.iy2,                                                        // Line to inside
                "A", s.inradius, s.inradius, 0, s.isLargeArc, s.antisweep, s.ix1, s.iy1,  // Arc inside
                "M", s.x1,  s.y1,                                                         // Line to outside
                "Z"                                                                       // Close
            ];
        } else {
            path = [
                "M", s.cx, s.cy,                                                          // Move
                "L", s.x1, s.y1,                                                          // Line
                "A", s.radius, s.radius,     0, s.isLargeArc, s.sweep,     s.x2, s.y2,    // Arc: (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
                "Z"                                                                       // Close
            ];
        }

        // Draw
        var sprite = this.canvas.path(path);
        sprite.attr({
            "stroke-width": 1, // Avoids sharp edges
            "stroke": s.stroke,
            "fill":   s.color
        });

        // Housekeep
        this.sprite.slices = this.sprite.slices || [];
        this.sprite.slices.push( sprite );

        return sprite;
    },
    drawSliceLabel: function( slice ) {
        var sprite = this.canvas.text( slice.xm, slice.ym, slice.label.toUpperCase() );
        var labelColor = (slice.color === "#FFFFFF" || slice.color === "#ffffff" || slice.color === "white") 
        	? "#000000" : this.options.sliceLabelColor;
        sprite.attr( "fill",        labelColor );
        sprite.attr( "font-size",   this.options.sliceLabelSize );
        sprite.attr( "font-weight", this.options.sliceLabelWeight );

        this.sprite.sliceLabels = this.sprite.sliceLabels || [];
        this.sprite.sliceLabels.push( sprite );
        return sprite;
    }
});
$.ui.svgPieChart.subclass('ui.svgPieDoughnut', {
    klass: "$.ui.svgPieDoughnut",
    options: {
        rowName:     null,
        svgwidth:     200,
        radius:         0,
        insideRadius:   0,
        labelOffset:    0,
        labelSize:      10,
        sliceLabelSize: 14,
        anticlockwise:  true
    },
    parseOptions: function() {
        this._super();
        this.options.insideRadius = this.options.insideRadius || this.options.radius/2;
        return this.options;
    },
    calculateXY: function() {
        var o = this.options;
        var xy = this._super();

        xy.origin = { x: 0, y: 0 }; // Avoid cropping
        xy.label = {
            x: this.getInitWidth()/2,
            y: this.getInitHeight()/2
        };

        return xy;
    },
    getLabelText: function() {
        var text = this.data.label.replace(/\s+/, "\n").toUpperCase();
        return text;
    }
});
$.ui.svgWidget.subclass('ui.svgCircleLabel', {
    klass: "$.ui.svgCircleLabel",
    options: {
		radius:			   40,
        color:             "",
        stroke:             "",
        rowName:         null,
        svgwidth:         140,
        labelOffset:       30,
        sliceLabelSize:    16,
        sliceLabelWeight: 700,
        sliceLabelColor:  "#ffffff"
    },
    _create: function() {
    },

    _init: function() {
        // We only want one row for this widget
        this.draw();
    },

    getInitHeight: function() {
        return Math.ceil( this.options.svgwidth + this.xy.origin.y*2 + this.options.labelOffset*1.5 );
    },
    getInitWidth: function() {
        return Math.ceil( this.options.svgwidth );
    },


    /**
     *  @return {Hash}  this.data
     */
    parseHtmlTable: function() {
        this.data = this._super();

        if( !this.options.rowName ) {
            this.options.rowName = this.element.attr("name") || this.element.find("th").text(); // Same as in _super()
        }
        return this.data;
    },
    parseOptions: function() {
        this._super();
        this.options.radius 		 = this.options.radius || this.options.svgwidth/2 - 4;
        this.options.sliceLabelColor = (this.options.color === this.options.sliceLabelColor) ? "#000000" : this.options.sliceLabelColor;
        return this.options;
    },
    calculateXY: function() {
        var o = this.options;
        var xy = this.xy = this.xy || {};

        xy.origin = { x: 0, y: 0 }; // Avoid cropping
        xy.circle = {
                x: this.getInitWidth()/2,
                y: this.getInitHeight()/2
            };
        xy.label = {
            x: this.getInitWidth()/2,
            y: this.getInitHeight() - this.getFontSize()*1.5
        };
        
        return xy;
    },

    draw: function() {
        this.drawCircle();
        this.drawLabel();
        this.table.hide();
    },
    drawCircle: function( ) {
    	var c = c || {};
    	c.radius     = c.radius     || this.options.radius; // Avoid cropping
        c.color		 = c.color		|| this.options.color;
        c.stroke	 = c.stroke     || this.options.stroke;
    	this.canvas.circle(
    		this.xy.circle.x,
    		this.xy.circle.y,
    		c.radius
    	).attr({
    		"stroke-width": 1, // Avoids sharp edges
            "stroke": c.stroke,
            "fill":   c.color
    	});
    	
    	var rowValue = Number( this.data.values[this.options.rowName][this.data.colNames[0]] );
        var label      = rowValue + "%";

        var sprite = this.canvas.text( this.xy.circle.x, this.xy.circle.y, label.toUpperCase() );
        sprite.attr( "fill",        this.options.sliceLabelColor );
        sprite.attr( "font-size",   this.options.sliceLabelSize );
        sprite.attr( "font-weight", this.options.sliceLabelWeight );

        this.sprite.sliceLabels = this.sprite.sliceLabels || [];
        this.sprite.sliceLabels.push( sprite );
        return sprite;
    }
});
$.ui.svgWidget.subclass('ui.svgBackgroundNumbers', {
    klass: "$.ui.svgBackgroundNumbers",
    options: {
        image:       "",
        labelColor:  "black",
        labelSize:    12,
        labelWeight: 700,
        svgheight:   100,
        svgwidth:    100,
        nulldefault: "0",
        renderLabelBackgrounds: false,  // {Boolean} if true render label backgrounds
        labelBackground:       '#fff',  // {String}  default color to render label backgrounds, override in this.options.xy
        xy:    {},
        data:  {},
        textOffset: {     // {Hash} hack, for some reason Match->Past Meetings->GoalsByPitchPosition has shifted text
            x: 0,     
            y: 0
        },
        errormsg: "Detailed statistics are currently not available"
    },
    _init: function() {
        this.draw();
    },
    getTableNode: function() {
        return this.element; // Required for putting .svg-wrapper in the right place
    },
    hasData: function() {
        for( var key in this.options.data ) {
            return true;
        }
        return false;
    },
    draw: function() {
        this.drawImage();
        if( this.hasData() ) {
            this.drawText();
        } else {
            this.drawNoData();
        }
        this.element.hide();
    },
    drawImage: function() {
        this.getCanvas().image( this.options.image, 0, 0, this.getInitWidth(), this.getInitHeight() );
    },
    drawText: function() {
        for( var key in this.options.xy ) {
            if( key in this.options.data ) {
                var label = this.getLabelText(key);
                if( this.options.renderLabelBackgrounds ) {
                    this.renderLabelBackground(key, label);
                }
                this.renderLabelText(key,label);
            }
        }
    },
    getFontSize: function(key) {
        return Math.round(Number( this.options.xy[key] && this.options.xy[key].labelSize || this.options.labelSize ));
    },
    getLabelTitle: function( key ) {
        var title = String(key).replace(/([A-Z]+|[0-9]+)/g, " $1").toLowerCase().capitalize();
        return title;
    },
    getLabelText: function( key ) {
        var self = this;
        var text = (this.options.data[key] === null) ? this.options.nulldefault : String(this.options.data[key]);
        var label = "";
        if( text ) {
            var prefix  = this.options.xy[key].prefix  || "";
            var postfix = this.options.xy[key].postfix || "";
            prefix  = prefix.replace( /key:([\w\.]+)/g, function(all, first) { return $.getKey(first, self.data, "0"); });
            postfix = postfix.replace(/key:([\w\.]+)/g, function(all, first) { return $.getKey(first, self.data, "0"); });

            label = String(prefix + String(text) + postfix).trim();
        }
        return label;
    },
    getLabelXY: function( key, label ) {
        var fontSize = this.getFontSize(key);

        this.options.xy[key]       = this.options.xy[key]       || {};
        this.options.xy[key].label = this.options.xy[key].label || {};

        var labelXY    = this.options.xy[key].label;
        labelXY.x      = this.options.xy[key].x;
        labelXY.y      = this.options.xy[key].y;
        labelXY.width  = fontSize * 0.5 * (label.length + 2.5);
        labelXY.height = fontSize * 1.5;
        labelXY.top    = labelXY.y - labelXY.height/2;
        labelXY.base   = labelXY.y + labelXY.height/2;
        labelXY.left   = labelXY.x - labelXY.width/2;
        labelXY.right  = labelXY.x + labelXY.width/2;
        labelXY.tip = {
            center: labelXY.x,
            top:    labelXY.base,
            base:   labelXY.base + fontSize/3,
            left:   labelXY.x    - fontSize/3,
            right:  labelXY.x    + fontSize/3
        };
        return labelXY;
    },
    renderLabelBackground: function( key, label ) {
        var labelXY = this.getLabelXY( key, label );
        this.getCanvas().path([
            "M", labelXY.left,       labelXY.base,
            "L", labelXY.left,       labelXY.top,
            "L", labelXY.right,      labelXY.top,
            "L", labelXY.right,      labelXY.base,
            "L", labelXY.tip.right,  labelXY.tip.top,
            "L", labelXY.tip.center, labelXY.tip.base,
            "L", labelXY.tip.left,   labelXY.tip.top,
            "Z"
        ]).attr({
            "fill":			this.options.xy[key].labelBackground || this.options.labelBackground,
            "stroke":       this.options.xy[key].labelBackground || this.options.labelBackground,
            "stroke-width":	"1px"
        });
    },
    renderLabelText: function( key, label ) {
        var fontSize = this.getFontSize(key);

        var node  = this.canvas.text( 
            this.options.xy[key].x + (this.options.textOffset.x||0),
            this.options.xy[key].y + (this.options.textOffset.y||0),
            label );
        node.attr({
            "title":       this.getLabelTitle(key),
            "font-size":   fontSize,
            "fill":        this.options.xy[key].labelColor  || this.options.labelColor,
            "font-weight": this.options.xy[key].labelWeight || this.options.labelWeight
        });
    },
    drawNoData: function() {
        this.getCanvas().rect( 0, 0, this.getInitWidth(), this.getInitHeight() ).attr({
            "fill":			"#ffffff",
            "stroke-width":	"1px",
            "stroke": 		"#ffffff",
            "opacity":  	0.7
        });

        var node = this.canvas.text( this.options.svgwidth / 2, this.options.svgheight / 2, this.options.errormsg );
        node.attr({
            "font-size":   Math.round( this.options.labelSize * 1.1 ),
            "fill":        this.options.labelColor,
            "font-weight": this.options.labelWeight
        });
    }
});
$.ui.svgBackgroundNumbers.subclass('ui.svgGoalsByPitchPosition', {
    klass: "$.ui.svgGoalsByPitchPosition",
    options: {
        image: "/etc/designs/premierleague/images/svg/goalsByPitchPositionBlank.png",
        svgheight: 194,
        svgwidth:  345,
        labelSize:  11,
        renderLabelBackgrounds: true,
        xy: {
            fromLeft6YardArea:       { x: 157, y:  28, postfix: "%", labelBackground: "#FF9900" },
            fromRight6YardArea:      { x: 187, y:  28, postfix: "%", labelBackground: "#FF9900" },
            fromLeftOfPenaltyArea:   { x: 125, y:  38, postfix: "%", labelBackground: "#FF9900" },
            fromRightOfPenaltyArea:  { x: 221, y:  38, postfix: "%", labelBackground: "#FF9900" },
            fromCentreOfPenaltyArea: { x: 172, y:  62, postfix: "% + key:fromPenaltySpot% pen", labelBackground: "#EE6E19" },
            fromLeftByline:          { x:  72, y:  45, postfix: "%", labelBackground: "#FFF" },
            fromRightByline:         { x: 275, y:  45, postfix: "%", labelBackground: "#FFF" },
            fromLeftWing:            { x:  58, y: 106, postfix: "%", labelBackground: "#FFF" },
            fromRightWing:           { x: 290, y: 106, postfix: "%", labelBackground: "#FFF" },
            fromLeftChannel:         { x: 132, y: 116, postfix: "%", labelBackground: "#FF9900" },
            fromRightChannel:        { x: 211, y: 116, postfix: "%", labelBackground: "#FF9900" },
            fromOwnHalf:             { x: 172, y: 165, postfix: "%", labelBackground: "#FFF" }
        }
    }
});
$.ui.svgBackgroundNumbers.subclass('ui.svgShotsScored', {
    klass: "$.ui.svgShotsScored",
    options: {
        image: "/etc/designs/premierleague/images/svg/shotsScored.png",
        svgheight: 194,
        svgwidth:  345,
        labelColor: '#666666',
        xy: {
            //total:                   { x: 169, y: 176, labelColor: '#609dca' },
            percentageHighLeftOfNet:   { x: 130, y:  55, postfix: "%" },
            percentageHighCentreOfNet: { x: 169, y:  55, postfix: "%" },
            percentageHighRightOfNet:  { x: 208, y:  55, postfix: "%" },
            percentageLowLeftOfNet:    { x: 130, y:  82, postfix: "%" },
            percentageLowCentreOfNet:  { x: 169, y:  82, postfix: "%" },
            percentageLowRightOfNet:   { x: 208, y:  82, postfix: "%" }
        }
    }
});
$.ui.svgBackgroundNumbers.subclass('ui.svgShotsMissed', {
    klass: "$.ui.svgShotsMissed",
    options: {
        image: "/etc/designs/premierleague/images/svg/shotsMissed.png",
        svgheight: 194,
        svgwidth:  345,
        labelColor: '#666666',
        xy: {
            //total:                   { x: 170, y: 176, labelColor: '#609dca' },
            percentageWideHighLeft:    { x:  82, y:  35, postfix: "%" },
            percentageWideHighRight:   { x: 260, y:  35, postfix: "%" },
            percentageWideLeft:        { x:  82, y:  82, postfix: "%" },
            percentageWideRight:       { x: 260, y:  82, postfix: "%" },
            percentageHitPostLeft:     { x: 109, y:  60, postfix: "%" },
            percentageHitPostRight:    { x: 232, y:  60, postfix: "%" },
            percentageHitCrossbar:     { x: 170, y:  38, postfix: "%" },
            percentageHitOverCrossbar: { x: 170, y:  12, postfix: "%" }
        }
    }
});
$.ui.svgWidget.subclass('ui.svgShotsOnGoal', {
    klass: "$.ui.svgShotsOnGoal",
    options: {
        colorGoal:             "#cccccc",
        colorLabel:            "#ffffff",
        colorPenaltyLine:      "#ffffff",
        colorStripeDark:       "#218221",
        colorStripeLight:      "#339933",
        colorUnderGround:      "#035b0f",
        stripeCount:           7,
        pitchAngle:            7*Math.PI/60,
        pitchWidthBase:      304,
        pitchHeight:          45,
        pitchDepth:           11,
        goalWidthRatio:        0.6,    // GoalWidth/PitchTopWidth
        goalHeightRatio:       0.333,  // GoalHeight/GoalWidth - 8 feet by 8 yards
        goalThickness:         5,      // px
        penaltySpotSize:       7,
        penaltyLineThickness:  2, 
        penaltyLineOpacity:    0.9,
        dataSquareSize:        23,
        labelOffset:           38
    },
    _create: function() {
    },
    _init: function() {
        this.draw();
    },
    parseOptions: function( o ) {
        o = o || this.options;
        o.pitchIndent     = o.pitchIndent     || Math.ceil( o.pitchHeight * Math.tan(o.pitchAngle) * 2 );
        o.pitchWidthTop   = o.pitchWidthTop   || Math.ceil( o.pitchWidthBase - (o.pitchIndent * 2) );
        
        o.goalWidth       = o.goalWidth       || Math.ceil( o.pitchWidthTop  * o.goalWidthRatio );
        o.goalHeight      = o.goalHeight      || Math.ceil( o.goalWidth      * o.goalHeightRatio );
        return o;
    },
    calculateXY: function( o ) {
        o = this.options;
        var xy = this.xy = this.xy || {};
        xy.origin = { x: 5, y: 5 }; 
       
        
        xy.goal = {
            top:   xy.origin.y,
            base:  xy.origin.y + o.goalHeight,
            left:  xy.origin.x + o.pitchWidthBase/2 - o.goalWidth/2,
            right: xy.origin.x + o.pitchWidthBase/2 + o.goalWidth/2
        };

        
        xy.pitch = {
            top:    xy.origin.y + o.goalHeight,
            base:   xy.origin.y + o.goalHeight + o.pitchHeight,
            center: xy.origin.x + o.pitchWidthBase/2
        };
        xy.pitchTop = {
            left:   xy.origin.x + o.pitchIndent,
            right:  xy.origin.x + o.pitchWidthBase - o.pitchIndent
        };
        xy.pitchBase = {
            left:   xy.origin.x,
            right:  xy.origin.x + o.pitchWidthBase
        };

        xy.stripePointsTop  = [];
        xy.stripePointsBase = [];
        for( var i=0; i<=o.stripeCount; i++ ) {
            xy.stripePointsTop[i] = { 
                x: xy.pitchTop.left + Math.round( i * o.pitchWidthTop/o.stripeCount ),
                y: xy.pitch.top
            };
            xy.stripePointsBase[i] = { 
                x: xy.pitchBase.left + Math.round( i * o.pitchWidthBase/o.stripeCount ),
                y: xy.pitch.base
            };
        }
        
        xy.label = {
            x: this.getInitWidth()/2,
            y: this.getInitHeight() - this.getFontSize()*1.5
        };

        xy.penalty = {
            top:  Math.round( xy.pitch.top ),
            base: Math.round( xy.pitch.top + o.pitchHeight/2 )
        };
        xy.penaltyTop = {
            left:  xy.goal.left,
            right: xy.goal.right
        };
        xy.penaltyBase = {
            left:  xy.goal.left  - o.pitchIndent/2 * o.goalWidthRatio,
            right: xy.goal.right + o.pitchIndent/2 * o.goalWidthRatio
        };
        xy.penaltySpot = {
            x: xy.pitch.center,
            y: xy.pitch.base - o.pitchHeight/4
        };


        xy.dataSquare = {
            top:        Math.round( xy.goal.top + o.goalHeight/2 - o.dataSquareSize/2 ),
            middle:     Math.round( xy.goal.top + o.goalHeight/2 ),
            base:       Math.round( xy.goal.top + o.goalHeight/2 + o.dataSquareSize/2 ),
            goalOffset: Math.round( o.goalHeight/2 + o.goalThickness/2 )  
        };
        for( var i=0, n=this.data.rowNames.length; i<n; i++ ) {
            var rowName = this.data.rowNames[i];
            var postX   = ( i % 2 === 0 ) ? xy.goal.left : xy.goal.right;
            var inside  = ( i % 2 === 0 ) ?  1 : -1;
            var outside = ( i % 2 === 0 ) ? -1 :  1;
            var direction;

            xy.dataSquare[rowName] = {};
            for( var j=0, m=this.data.colNames.length; j<m; j++ ) {
                var colName = this.data.colNames[j];
 
                if(      colName.match( /on|scored/i) ) { direction = inside;  }
                else if( colName.match(/off|missed/i) ) { direction = outside; }
                else { console.error(this&&this.klass||'' ,':calculateXY(): invalid colName ', colName, this); }

                xy.dataSquare[rowName][colName] = {
                    top:    xy.dataSquare.top,
                    middle: xy.dataSquare.middle, 
                    base:   xy.dataSquare.base,
                    left:   postX + xy.dataSquare.goalOffset * direction - o.dataSquareSize/2,
                    center: postX + xy.dataSquare.goalOffset * direction,
                    right:  postX + xy.dataSquare.goalOffset * direction + o.dataSquareSize/2
                };
            }
        }

        return xy;
    },
    getInitHeight: function() {
        return Math.ceil( this.options.goalHeight + this.options.pitchHeight + this.options.pitchDepth 
                        + this.options.labelOffset*1.5 + this.xy.origin.y );
    },
    getInitWidth: function() {
        return this.options.pitchWidthBase + this.xy.origin.x*2;
    },
    draw: function() {
        this.drawGoal();
        this.drawUnderground();
        this.drawPitch();
        this.drawPenaltyBox();
        this.drawPenaltySpot();
        this.drawDataSquares();
        this.drawDataSquareLabels();
        this.drawLabel();
        this.element.hide();
    },
    drawGoal: function() {
        // Goal has no base, so loops back on itself
        this.sprite.goal = this.canvas.path([
            "M", this.xy.goal.left,  this.xy.goal.base, 
            "L", this.xy.goal.left,  this.xy.goal.top,  
            "L", this.xy.goal.right, this.xy.goal.top,
            "L", this.xy.goal.right, this.xy.goal.base,

            "L", this.xy.goal.right, this.xy.goal.top,  
            "L", this.xy.goal.left,  this.xy.goal.top,  
            "Z"
        ]);
        this.sprite.goal.attr({
            "stroke":       this.options.colorGoal,
            "stroke-width": this.options.goalThickness
        });
    },
    drawPenaltyBox: function() {
        // Penalty has no base, so loops back on itself
        this.sprite.goal = this.canvas.path([
            "M", this.xy.penaltyTop.left,   this.xy.penalty.top,
            "L", this.xy.penaltyBase.left,  this.xy.penalty.base,
            "L", this.xy.penaltyBase.right, this.xy.penalty.base,
            "L", this.xy.penaltyTop.right,  this.xy.penalty.top,

            "L", this.xy.penaltyBase.right, this.xy.penalty.base,
            "L", this.xy.penaltyBase.left,  this.xy.penalty.base,
            "Z"
        ]);
        this.sprite.goal.attr({
            "stroke":         this.options.colorPenaltyLine,
            "stroke-width":   this.options.penaltyLineThickness,
            "stroke-opacity": this.options.penaltyLineOpacity
        });
    },
    drawPenaltySpot: function() {
        this.canvas.ellipse( 
            this.xy.penaltySpot.x,          this.xy.penaltySpot.y,
            this.options.penaltySpotSize/2, this.options.penaltySpotSize/2 * 0.6 // 0.6 = Isometric projection
        ).attr({
            "fill":           this.options.colorPenaltyLine,
            "stroke-width":   0,
            "stroke-opacity": this.options.penaltyLineOpacity
        });
    },
    drawPitch: function() {
        // Dark stripes
        var darkPath = [];
        var lightPath = [];
        for( var i=0; i<this.options.stripeCount; i=i+2 ) {
            darkPath.push(
                "M", this.xy.stripePointsTop[i].x,    this.xy.stripePointsTop[i].y,
                "L", this.xy.stripePointsBase[i].x,   this.xy.stripePointsBase[i].y,
                "L", this.xy.stripePointsBase[i+1].x, this.xy.stripePointsBase[i+1].y,
                "L", this.xy.stripePointsTop[i+1].x,  this.xy.stripePointsTop[i+1].y,
                "Z"
            );
        }
        for( var i=1; i<this.options.stripeCount; i=i+2 ) {
            lightPath.push(
                "M", this.xy.stripePointsTop[i].x,    this.xy.stripePointsTop[i].y,
                "L", this.xy.stripePointsBase[i].x,   this.xy.stripePointsBase[i].y,
                "L", this.xy.stripePointsBase[i+1].x, this.xy.stripePointsBase[i+1].y,
                "L", this.xy.stripePointsTop[i+1].x,  this.xy.stripePointsTop[i+1].y,
                "Z"
            );
        }

        this.canvas.path( darkPath  ).attr({
            "fill":         this.options.colorStripeDark,
            "stroke":       this.options.colorStripeDark,
            "stroke-width": 1
        });
        this.canvas.path( lightPath ).attr({
           "fill":         this.options.colorStripeLight,
           "stroke":       this.options.colorStripeLight,
           "stroke-width": 1
        });
    },
    drawUnderground: function() {
        this.canvas.rect( this.xy.pitchBase.left,      this.xy.pitch.base, 
                          this.options.pitchWidthBase, this.options.pitchDepth )
            .attr({ 
                "fill":         this.options.colorUnderGround,
                "stroke":       this.options.colorUnderGround,
                "stroke-width": 1
            });
    },
    drawDataSquares: function() {
        for( var i=0, n=this.data.rowNames.length; i<n; i++ ) {
            var rowName = this.data.rowNames[i];
            for( var j=0, m=this.data.colNames.length; j<m; j++ ) {
                
            	var colName = this.data.colNames[j];
                var color   = this.data.rows[rowName].color;
                var stroke	= this.data.rows[rowName].stroke || color;  
                
                this.canvas.rect(
                    this.xy.dataSquare[rowName][colName].left,
                    this.xy.dataSquare[rowName][colName].top,
                    this.options.dataSquareSize,
                    this.options.dataSquareSize
                ).attr({
                    "fill": 		color,
                    "stroke": 		stroke,
                    "stroke-width": 1.5
                });
            }
        }
    },
    drawDataSquareLabels: function() {
        for( var i=0, n=this.data.rowNames.length; i<n; i++ ) {
            var rowName = this.data.rowNames[i];
            for( var j=0, m=this.data.colNames.length; j<m; j++ ) {
                
            	var colName = this.data.colNames[j];
            	var color	= (this.options.colorLabel === this.data.rows[rowName].color) ? "#000000" : this.options.colorLabel;

                this.canvas.text(
                    this.xy.dataSquare[rowName][colName].center + ($.browser.msie ? -1 : 0),
                    this.xy.dataSquare[rowName][colName].middle + ($.browser.msie ?  2 : 0),
                    this.data.values[rowName][colName]
                ).attr({
                    "fill":        color,
                    "font-size":   12,
                    "font-weight": 700
                });
            }
        }
    }
});
$.ui.svgWidget.subclass('ui.svgTeamPositions', {
    klass: "$.ui.svgTeamPositions",
    options: {
		team:				   '',			// {String} 'home' or 'away' 
		formation:			   null,		// {[[Array]]} Array formatted Str
		colorGoal:             "#cccccc",   // {#Hex}
        colorLabel:            "#ffffff",	// {#Hex}
        colorPenaltyLine:      "#ffffff",	// {#Hex}
        colorStripeDark:       "#139124",	// {#Hex}
        colorStripeLight:      "#35a63f",	// {#Hex}
        colorUnderGround:      "#035c0f",	// {#Hex}
        stripeCount:           8,
        pitchAngle:           60,
        pitchWidthBase:      340,
        pitchHeight:         160,
        pitchDepth:           11,
        goalWidthRatio:        0.25,   // GoalWidth/PitchTopWidth
        goalHeightRatio:       0.333,  // GoalHeight/GoalWidth - 8 feet by 8 yards
        goalThickness:         3,      // px
        sidelineInset:		   4,
        sidelineThickness:     2,
        sidelineOpacity:       0.9,
        labelSize:			   15,
        svgbackground:		   "#f3f8fb"	// {#Hex}
    },
    required: {
    	formation: 	String
    },
    _create: function() {
    },
    _init: function() {
        this.draw();
    },
    parseOptions: function( o ) {
        o = o || this.options;
        o.pitchIndent     = o.pitchIndent     || Math.ceil( o.pitchHeight * Math.tan(o.pitchAngle) );
        o.pitchWidthTop   = o.pitchWidthTop   || Math.ceil( o.pitchWidthBase - (o.pitchIndent * 2) );
        
        o.goalWidth       = o.goalWidth       || Math.ceil( o.pitchWidthTop  * o.goalWidthRatio );
        o.goalHeight      = o.goalHeight      || Math.ceil( o.goalWidth      * o.goalHeightRatio );

        o.formation = this.formation 	  = eval("["+this.options.formation.toString()+"]");
     
        return o;
    },
    calculateXY: function( o ) {
        o = this.options;
        var xy = this.xy = this.xy || {};
        xy.origin = { x: 0, y: 15 };
       
        xy.goal = {
            top:   xy.origin.y + o.sidelineInset,
            base:  xy.origin.y + o.sidelineInset + o.goalHeight,
            left:  xy.origin.x + o.pitchWidthBase/2 - o.goalWidth/2,
            right: xy.origin.x + o.pitchWidthBase/2 + o.goalWidth/2
        };
        
        xy.pitch = {
            top:    xy.origin.y + o.goalHeight,
            base:   xy.origin.y + o.goalHeight + o.pitchHeight,
            center: xy.origin.x + o.pitchWidthBase/2
        };
        xy.pitchTop = {
            left:   xy.origin.x + o.pitchIndent,
            right:  xy.origin.x + o.pitchWidthBase - o.pitchIndent
        };
        xy.pitchBase = {
            left:   xy.origin.x,
            right:  xy.origin.x + o.pitchWidthBase
        };

        xy.stripePointsTop  = [];
        xy.stripePointsBase = [];
        for( var i=0; i<=o.stripeCount; i++ ) {
            xy.stripePointsTop[i] = { 
                x: xy.pitchTop.left + Math.round( i * o.pitchWidthTop/o.stripeCount ),
                y: xy.pitch.top
            };
            xy.stripePointsBase[i] = { 
                x: xy.pitchBase.left + Math.round( i * o.pitchWidthBase/o.stripeCount ),
                y: xy.pitch.base
            };
        }
        
        xy.sideline = {
        	top: xy.pitch.top + o.sidelineInset,
        	base: xy.pitch.base - o.sidelineInset * 3,
        	center: xy.pitch.center
        };
        xy.sidelineTop = {
            left:   xy.origin.x + o.pitchIndent + o.sidelineInset,
            right:  xy.origin.x + o.pitchWidthBase - o.pitchIndent - o.sidelineInset
        };
        xy.sidelineBase = {
            left:   xy.pitchBase.left + o.sidelineInset * 2, // Maths ain't right
            right:  xy.pitchBase.right - o.sidelineInset * 2 // 
        };
        xy.sidelineOHBase = {
        	left:   xy.pitchBase.left + o.sidelineInset,
        	right:  xy.pitchBase.right - o.sidelineInset
        }

        xy.sixYd = {
            top:  Math.round( xy.pitch.top + o.sidelineInset ),
            base: Math.round( xy.pitch.top + o.sidelineInset + o.pitchHeight/10 )
        };
        xy.sixYdTop = {
            left:  xy.goal.left,
            right: xy.goal.right
        };
        xy.sixYdBase = {
            left:  xy.goal.left - o.pitchIndent/o.stripeCount * o.goalWidthRatio,
            right: xy.goal.right + o.pitchIndent/o.stripeCount * o.goalWidthRatio
        };
        
        xy.eighteenYd = {
            top:  Math.round( xy.pitch.top + o.sidelineInset ),
            base: Math.round( xy.pitch.top + o.sidelineInset + o.pitchHeight/4 )
        };
        xy.eighteenYdTop = {
            left:  xy.goal.left - o.goalWidth / 2,
            right: xy.goal.right + o.goalWidth / 2 //Stay with the nice stripes
        };
        xy.eighteenYdBase = {
            left:  xy.goal.left - o.goalWidth / 2  - o.pitchIndent/o.stripeCount,
            right: xy.goal.right + o.goalWidth / 2 + o.pitchIndent/o.stripeCount
        };
        xy.eighteenYdArc = {
        	left:  Math.round( xy.eighteenYdBase.left  + 1.5*(xy.eighteenYdBase.left + xy.eighteenYdBase.right)/18 ),
        	right: Math.round( xy.eighteenYdBase.right - 1.5*(xy.eighteenYdBase.left + xy.eighteenYdBase.right)/18 ),
        	curveX: xy.pitch.center,
        	curveY: Math.round( xy.eighteenYd.base + (xy.eighteenYd.top + xy.eighteenYd.base) / 6 )
        };

        // Player Row locations y (stagger back by small sidelineInsets)
        xy.playerRow = [];
        
        for( var i=0; i<o.formation.length; i++ ) {
        	switch (i)
        	{
        	case 0: var tmpPos = Math.round( (xy.goal.base + xy.sixYd.base) / 2 );
        			var tmpIn = Math.ceil( (o.pitchHeight - tmpPos ) * Math.tan(o.pitchAngle) );
        			xy.playerRow[i] = {			    	// Goalkeeper: Halfway in 6yrd box
        	    		ypos: 	 tmpPos,
        	    		indent:  tmpIn,
        	    		spacing: Math.ceil( (o.pitchWidthBase - 2*tmpIn) / (o.formation[i].length + 1) )
    	    		};
        			break;
        	case 1: var tmpPos = xy.eighteenYd.base;
        			var tmpIn = Math.ceil( (o.pitchHeight - tmpPos) * Math.tan(o.pitchAngle) );
        			xy.playerRow[i] = {					// Defenders:  On edge of 18yrd box
			            ypos: 	 tmpPos,
			            indent:  tmpIn,
    	    			spacing: Math.ceil( (o.pitchWidthBase - 2*tmpIn) / (o.formation[i].length + 1) )
			        }
        			break;
        	default:var tmpPos = Math.round( xy.playerRow[i-1].ypos + 
        					(xy.sideline.base - xy.eighteenYd.base - 2 * o.sidelineInset)
        					/ (o.formation.length - 2) );
        			var tmpIn = Math.ceil( (o.pitchHeight - tmpPos) * Math.tan(o.pitchAngle) );
        			xy.playerRow[i] = {				// Mids + Att: Equally positioned to halfway
		            	ypos: tmpPos,
					    indent: tmpIn,
						spacing: Math.ceil( (o.pitchWidthBase - 2*tmpIn) / (o.formation[i].length + 1) )
	        		}
        			break;
        	}// switch
        	//console.log("playerRow",i,this.xy.playerRow,o.formation[i]);

        }// for each formation row
    },
    getInitHeight: function() {
        return 206;
    },
    getInitWidth: function() {
        return 345;
    },
    // Custom wrapper because two pitches are drawn
    createWrapper: function() {
        this.wrapper = $("<div class='svg-wrapper "+ this.options.team +"'></div>").insertAfter( this.table[0] || this.element[0] ); // Create a new one
        this.createWrapperInit();
        return this.wrapper;
    },
    
    draw: function() {
    	this.drawPitch();
    	this.drawGoal();
        this.drawSixYdBox();
        this.drawEighteenYdBox();
        this.drawSidelines();
        this.drawUnderground();
        this.drawPlayerLabels();
        this.addTooltips( this.options.team );
    },
    drawPitch: function() {    	
        // Dark stripes
        var darkPath = [];
        var lightPath = [];
        for( var i=0; i<this.options.stripeCount; i=i+2 ) {
            darkPath.push(
                "M", this.xy.stripePointsTop[i].x,    this.xy.stripePointsTop[i].y,
                "L", this.xy.stripePointsBase[i].x,   this.xy.stripePointsBase[i].y,
                "L", this.xy.stripePointsBase[i+1].x, this.xy.stripePointsBase[i+1].y,
                "L", this.xy.stripePointsTop[i+1].x,  this.xy.stripePointsTop[i+1].y,
                "Z"
            );
        }
        for( var i=1; i<this.options.stripeCount; i=i+2 ) {
            lightPath.push(
                "M", this.xy.stripePointsTop[i].x,    this.xy.stripePointsTop[i].y,
                "L", this.xy.stripePointsBase[i].x,   this.xy.stripePointsBase[i].y,
                "L", this.xy.stripePointsBase[i+1].x, this.xy.stripePointsBase[i+1].y,
                "L", this.xy.stripePointsTop[i+1].x,  this.xy.stripePointsTop[i+1].y,
                "Z"
            );
        }

        this.canvas.path( darkPath  ).attr({
            "fill":         this.options.colorStripeDark,
            "stroke":       this.options.colorStripeDark,
            "stroke-width": 1
        });
        this.canvas.path( lightPath ).attr({
           "fill":         this.options.colorStripeLight,
           "stroke":       this.options.colorStripeLight,
           "stroke-width": 1
        });
        
    },
    drawUnderground: function() {
        this.canvas.rect( this.xy.pitchBase.left,      this.xy.pitch.base, 
                          this.options.pitchWidthBase, this.options.pitchDepth )
            .attr({ 
                "fill":         this.options.colorUnderGround,
                "stroke":       this.options.colorUnderGround,
                "stroke-width": 1
            });
    },
    drawGoal: function() {
        // Goal has no base, so loops back on itself
        this.sprite.goal = this.canvas.path([
            "M", this.xy.goal.left,  this.xy.goal.base, 
            "L", this.xy.goal.left,  this.xy.goal.top,  
            "L", this.xy.goal.right, this.xy.goal.top,
            "L", this.xy.goal.right, this.xy.goal.base,

            "L", this.xy.goal.right, this.xy.goal.top,  
            "L", this.xy.goal.left,  this.xy.goal.top,  
            "Z"
        ]);
        this.sprite.goal.attr({
            "stroke":       this.options.colorGoal,
            "stroke-width": this.options.goalThickness
        });
    },
    drawSixYdBox: function() {
        // Penalty has no base, so loops back on itself
        this.sprite.box6 = this.canvas.path([
            "M", this.xy.sixYdTop.left,   this.xy.sixYd.top,
            "L", this.xy.sixYdBase.left,  this.xy.sixYd.base,
            "L", this.xy.sixYdBase.right, this.xy.sixYd.base,
            "L", this.xy.sixYdTop.right,  this.xy.sixYd.top,

            "L", this.xy.sixYdBase.right, this.xy.sixYd.base,
            "L", this.xy.sixYdBase.left,  this.xy.sixYd.base,
            "Z"
        ]);
        this.sprite.box6.attr({
            "stroke":         this.options.colorPenaltyLine,
            "stroke-width":   this.options.sidelineThickness,
            "stroke-opacity": this.options.sidelineOpacity
        });
    },
    drawEighteenYdBox: function() {
        // Penalty has no base, so loops back on itself
        this.sprite.box18 = this.canvas.path([
            "M", this.xy.eighteenYdTop.left,   this.xy.eighteenYd.top,
            "L", this.xy.eighteenYdBase.left,  this.xy.eighteenYd.base,
            "L", this.xy.eighteenYdBase.right, this.xy.eighteenYd.base,
            "L", this.xy.eighteenYdTop.right,  this.xy.eighteenYd.top,

            "L", this.xy.eighteenYdBase.right, this.xy.eighteenYd.base,
            "L", this.xy.eighteenYdBase.left,  this.xy.eighteenYd.base,
            "Z"
        ]);
        this.sprite.box18.attr({
            "stroke":         this.options.colorPenaltyLine,
            "stroke-width":   this.options.sidelineThickness,
            "stroke-opacity": this.options.sidelineOpacity
        });
        // Penalty box arc
        var arc = "M" + this.xy.eighteenYdArc.left + "," + this.xy.eighteenYd.base
        	+ "S" + this.xy.eighteenYdArc.curveX + "," + this.xy.eighteenYdArc.curveY
        	+ "," + this.xy.eighteenYdArc.right + "," + this.xy.eighteenYd.base
        	+ "Z";
        this.sprite.arc18 = this.canvas.path( arc );
        this.sprite.arc18.attr({
            "stroke":         this.options.colorPenaltyLine,
            "stroke-width":   this.options.sidelineThickness,
            "stroke-opacity": this.options.sidelineOpacity
        });
    },
    drawSidelines: function() {
        this.sprite.sidelines = this.canvas.path([
			"M", this.xy.sidelineTop.left,   this.xy.sideline.top,
			"L", this.xy.sidelineBase.left,  this.xy.sideline.base,
			"L", this.xy.sidelineBase.right, this.xy.sideline.base,
			"L", this.xy.sidelineTop.right,  this.xy.sideline.top,
			"Z"
        ]);
        this.sprite.sidelines.attr({
            "stroke":         this.options.colorPenaltyLine,
            "stroke-width":   this.options.sidelineThickness,
            "stroke-opacity": this.options.sidelineOpacity
        });
        // Part of the other half, loops back too
        this.sprite.otherHalf = this.canvas.path([
			"M", this.xy.sidelineOHBase.left,  this.xy.pitch.base,
			"L", this.xy.sidelineBase.left,    this.xy.sideline.base,
			"L", this.xy.sidelineBase.right,   this.xy.sideline.base,
			"L", this.xy.sidelineOHBase.right, this.xy.pitch.base,
			
			"L", this.xy.sidelineBase.right,   this.xy.sideline.base,
			"L", this.xy.sidelineBase.left,    this.xy.sideline.base,
			"Z"
		 ]);
		 this.sprite.otherHalf.attr({
		     "stroke":         this.options.colorPenaltyLine,
		     "stroke-width":   this.options.sidelineThickness,
		     "stroke-opacity": this.options.sidelineOpacity
		 });
		 // Center Circle
		 this.sprite.circle = this.canvas.ellipse(
		    this.xy.pitch.center, this.xy.sideline.base,
		    this.options.pitchWidthBase/6, this.options.pitchHeight/5
		 );
		 this.sprite.circle.attr({
		     "stroke":         this.options.colorPenaltyLine,
		     "stroke-width":   this.options.sidelineThickness,
		     "stroke-opacity": this.options.sidelineOpacity
		 });
    },
    drawPlayerLabels: function() {
    	var playerId;		// PlayerId for lookup in table data
    	var playerCount = 0;
   	
    	// Get spacing for each team formation row (eg 4, GK, DF, MF, AT)
    	for ( var i=0, n=this.formation.length; i<n; i++ ) {

    		//console.log('Formation ',i,this.formation[i]);
    		
    		// For each player in formation row
    		for ( var j=0; j<this.formation[i].length; j++ ) {
    			// Get data for playerId
    			playerId = this.formation[i][j];
    			
    			var player = this.player = this.player || {};
    			
    			player.img   = this.data.strings[playerId].img		|| '';
    	    	player.name  = this.data.strings[playerId].players	|| '-'; 
	    		player.eappi = this.data.values[playerId].eappi		|| '-';
	    		player.fpl   = this.data.values[playerId].fpl		|| '-';
	    		player.bfr   = this.data.values[playerId].bfr		|| '-';
    			
    			// Draw label relative to origin point
    	    	/********************************************
    	    	this.sprite.label = this.canvas.path([
    	    	    "M","[x]","playerRow[i].ypos",
    	    	    "h", labelSq,
    	    	    "v", labelSq,
    	    	    "l", -labelTri, labelTri,
    	    	    "l", -labelTri, -labelTri,
    	    	    "Z"
    	    	]).attr({
    	    		"fill": this.data.rows[this.data.rowNames[0]].color,
    	    		"stroke-width": 0.001
    	    	});
    	    	*********************************************/
    	    	// Label height = 2/3 square and 1/3 triangle tip
    	    	var labelSq  = this.options.labelSize * 2 / 3;
    	    	var labelTri = this.options.labelSize / 3;
    	    	var labelMargin   = {
    	    		x: this.xy.playerRow[i].indent + (j+1) * this.xy.playerRow[i].spacing - labelSq/2,
    	    		y: this.xy.playerRow[i].ypos-this.options.labelSize
    	    	}
	    	    
    	    	this.sprite.label = this.canvas.path([
	    	  	    "M", labelMargin.x, labelMargin.y,
	    	  	    "h", labelSq,
	    	  	    "v", labelSq,
	    	  	    "l", -labelTri,  labelTri,
	    	  	    "l", -labelTri, -labelTri,
	    	  	    "Z"
	    	  	]).attr({
	    	  		"fill": this.data.rows[this.data.rowNames[0]].color,
	    	  		"stroke-width": 0.001,
	    	  		"href":"#pLabel" + playerCount
	    	  	});
	    	    
    			// addTooltip()
	    	    /********************************************
    	    	layout:
    	    	<div class="tooltip player">
    	    		<img src="--" alt="XXX XXX" />
    	    		<span class="name">
    	    			#. <a href="<%= playerTabUrl...(${playerId}) %>">
	    	    			XXXXX XXXXXX
	    	    		</a>
	    	    		<span class="goal"></span>
	    	    		<span class="Y">Y</span>
	    	    		<span class="R">R</span>
	    	    		<span class="sub">(##)</span>
	    	    	</span>
	    	    	<table>
	    	    		<tr><td class="ea-ppi">EA PPI</td><td>${EAPPI}</td></tr>
	    	    		<tr><td class="fpl">FPL</td><td>${FPL}</td></tr>
	    	    		<tr><td class="bfr">BFR</td><td>${BFR}</td></tr>
	    	    	</table>
    	    	</div>
    	    	*********************************************/
	    	    var tooltipHTML = "<div class=\"tooltip player\" id=\"playerTip"
	    	    	+ playerCount + this.options.team + "\">"
	    	    	+ this.player.img
	    	    	+ "<span class='name'>" 		  + this.player.name + "</span>"
	    	    	+ "<table>"
	    	    	+ "<tr><td>EA PPI</td><td class='ea-ppi'>" + this.player.eappi + "</td></tr>"
	    	    	+ "<tr><td>FPL</td>   <td class='fpl   '>" + this.player.fpl   + "</td></tr>"
	    	    	+ "<tr><td>BFR</td>   <td class='bfr   '>" + this.player.bfr   + "</td></tr>"
    	    		+ "</table>"
    	    		+ "</div>";
	    	    
	    	    $('body').append(tooltipHTML);
	    	    playerCount++;
    		} // for each player
    	} // for each formation row
    },
    addTooltips: function( team ) {
    	
    	this.wrapper.hover(function(){
	    	// Select all linked paths in the team's wrapper
	    	$('a','.svg-wrapper.' + team).each(function(){
	    		var $t = $(this);
	    		var labelAttr = $t.attr("href");
	    		var myidx = labelAttr.replace("#pLabel","");
	
	    		var baseOffset = $t.offset();
	    		var lStr = (baseOffset.left - 90)+"px",
	    		    tStr = (baseOffset.top  - 110)+"px";
	
	    		// Add to CSS for each tooltip
	    		var $target = $('div#playerTip' + myidx + team);
	    		$target.css({
	    			"position":"absolute",
	    			"z-index":"900000",
	    			"top":tStr,
	    			"left":lStr,
	    		    "display":"none"
	    		});
	
	    		// Add toggle event for mouseover
        		$t.click(function() {
        		    $target.fadeIn();
        		});
        		$t.mouseout(function() {
        		    $target.delay(2000).fadeOut();
        		});
	    	});
    	},
    	{
    		//do nothing
    	});
    },
    /**
     *  Parses a HTML table
     *  @param  {jQuery} table  table to parse
     *  @return {Hash} data
     *                 data.values   // {Hash<Row|Col><Col|Row>} = {Number}
     *                 data.rows     // {Hash<Col>} = {Hash<attribute>}
     *                 data.cols     // {Hash<Row>} = {Hash<attribute>}
     *                 data.totals   // {Hash<Row|Col>} = {Number}
     *                 data.colNames // {Array} = Col
     *                 data.rowNames // {Array} = Row
     *                 data.label    // {String}
     */
    parseHtmlTable: function( table ) {
        if( !this.table ) { this.getTableNode(); }

        var data = {};
        data.values  = {};  // {Hash<Row|Col><Col|Row>} = {Number}
        data.strings = {};  // {Hash<Row|Col><Col|Row>} = {String}
        data.rows = {};     // {Hash<Col>} = {Hash<attribute>}
        data.cols = {};     // {Hash<Row>} = {Hash<attribute>}
        data.colNames = []; // {Array} = Col
        data.rowNames = []; // {Array} = Row

        data.label = this.element.find(".label").text()
                  || this.table.find("thead .label").text()
                  || this.element.children().first().text();

        var cols = this.table.find("thead th").not(":first-child, .ignore");
        var rows = this.table.find("tbody tr").not(".ignore");

        for( var i=0, n=cols.length; i<n; i++ ) {
            var text    = $.trim( $(cols[i]).text() );
            var colData = $.getAttributeHash( cols[i], { name: text, index: i } );
            var colName = colData["class"]; // Allow HTML override
            data.cols[colName] = colData;
            data.colNames.push( colName );
        }

        for( var i=0, n=rows.length; i<n; i++ ) {
            var rowHead = $(rows[i]).find("th");
            var text    = $.trim( rowHead.text() );
            var rowData = $.getAttributeHash( rows[i], { name: text, index: i } );
            var rowName = rowData["name"];
            data.rows[rowName] = rowData;

            if( this.element.attr("nodeName") === "TR" && this.element[0] !== rows[i] ) {
                // Don't add to data.rowNames
                $.noop();
            } else {
                data.rowNames.push( rowName );
            }

            var cells = $(rows[i]).find("td");
            for( var j=0, m=cells.length; j<m; j++ ) {
                var string = $.trim( $(cells[j]).html() ); // Get inner HTML
                var value  = Number( string.replace(/[^\d\.+-]/g, '') );
                var colName = data.colNames[j];

                data.values[rowName] = data.values[rowName] || {};
                data.values[colName] = data.values[colName] || {};
                data.values[rowName][colName] = value;
                data.values[colName][rowName] = value;

                data.strings[rowName] = data.strings[rowName] || {};
                data.strings[colName] = data.strings[colName] || {};
                data.strings[rowName][colName] = string;
                data.strings[colName][rowName] = string;
            }
        }
        return data;
    }

});
$.ui.svgWidget.subclass('ui.svgGoalPositions', {
    klass: "$.ui.svgGoalPositions",
    options: {
        colorGoal:             "#cccccc",   // {#Hex}
        colorLabel:            "#ffffff",	// {#Hex}
        colorPenaltyLine:      "#ffffff",	// {#Hex}
        colorStripeDark:       "#139124",	// {#Hex}
        colorStripeLight:      "#35a63f",	// {#Hex}
        colorUnderGround:      "#035c0f",	// {#Hex}
        stripeCount:           8,
        pitchAngle:           70,
        pitchWidthBase:     null,
        pitchHeight:         160,
        pitchDepth:           11,
        goalWidthRatio:        0.25,   // GoalWidth/PitchTopWidth
        goalHeightRatio:       0.333,  // GoalHeight/GoalWidth - 8 feet by 8 yards
        goalThickness:         3,      // px
        sidelineInset:		   4,
        sidelineThickness:     2,
        sidelineOpacity:       0.9,
        labelSize:			   18,
        labelTextSize:		   11
    },
    _create: function() {
    },
    _init: function() {
        this.draw();
    },
    parseOptions: function( o ) {
        o = o || this.options;
        o.pitchWidthBase  = o.pitchWidthBase  || this.getInitWidth();
        
        o.pitchIndent     = o.pitchIndent     || Math.ceil( o.pitchHeight / Math.tan(o.pitchAngle * Math.PI / 180) );
        o.pitchWidthTop   = o.pitchWidthTop   || Math.ceil( o.pitchWidthBase - (o.pitchIndent * 2) );
        
        o.goalWidth       = o.goalWidth       || Math.ceil( o.pitchWidthTop  * o.goalWidthRatio );
        o.goalHeight      = o.goalHeight      || Math.ceil( o.goalWidth      * o.goalHeightRatio );
        
        o.formation 	  = this.data  || {};
        return o;
    },
    calculateXY: function( o ) {
        o = this.options;
        var xy = this.xy = this.xy || {};
        xy.origin = { x: 0, y: 15 };
       
        xy.goal = {
            top:   xy.origin.y + o.sidelineInset,
            base:  xy.origin.y + o.sidelineInset + o.goalHeight,
            left:  xy.origin.x + o.pitchWidthBase/2 - o.goalWidth/2,
            right: xy.origin.x + o.pitchWidthBase/2 + o.goalWidth/2
        };
        
        xy.pitch = {
            top:    xy.origin.y + o.goalHeight,
            base:   xy.origin.y + o.goalHeight + o.pitchHeight,
            center: xy.origin.x + o.pitchWidthBase/2
        };
        xy.pitchTop = {
            left:   xy.origin.x + o.pitchIndent,
            right:  xy.origin.x + o.pitchWidthBase - o.pitchIndent
        };
        xy.pitchBase = {
            left:   xy.origin.x,
            right:  xy.origin.x + o.pitchWidthBase
        };

        xy.stripePointsTop  = [];
        xy.stripePointsBase = [];
        for( var i=0; i<=o.stripeCount; i++ ) {
            xy.stripePointsTop[i] = { 
                x: xy.pitchTop.left + Math.round( i * o.pitchWidthTop/o.stripeCount ),
                y: xy.pitch.top
            };
            xy.stripePointsBase[i] = { 
                x: xy.pitchBase.left + Math.round( i * o.pitchWidthBase/o.stripeCount ),
                y: xy.pitch.base
            };
        }
        
        xy.sideline = {
        	top: xy.pitch.top + o.sidelineInset,
        	base: xy.pitch.base - o.sidelineInset * 3,
        	center: xy.pitch.center
        };
        xy.sidelineTop = {
            left:   xy.origin.x + o.pitchIndent + o.sidelineInset,
            right:  xy.origin.x + o.pitchWidthBase - o.pitchIndent - o.sidelineInset
        };
        xy.sidelineBase = {
            left:   xy.pitchBase.left + o.sidelineInset * 2, // Maths ain't right
            right:  xy.pitchBase.right - o.sidelineInset * 2 // 
        };
        xy.sidelineOHBase = {
        	left:   xy.pitchBase.left + o.sidelineInset,
        	right:  xy.pitchBase.right - o.sidelineInset
        }

        xy.sixYd = {
            top:  Math.round( xy.pitch.top + o.sidelineInset ),
            base: Math.round( xy.pitch.top + o.sidelineInset + o.pitchHeight/10 )
        };
        xy.sixYdTop = {
            left:  xy.goal.left,
            right: xy.goal.right
        };
        xy.sixYdBase = {
            left:  xy.goal.left - o.pitchIndent/o.stripeCount * o.goalWidthRatio,
            right: xy.goal.right + o.pitchIndent/o.stripeCount * o.goalWidthRatio
        };
        
        xy.eighteenYd = {
            top:  Math.round( xy.pitch.top + o.sidelineInset ),
            base: Math.round( xy.pitch.top + o.sidelineInset + o.pitchHeight/4 )
        };
        xy.eighteenYdTop = {
            left:  xy.goal.left - o.goalWidth / 2,
            right: xy.goal.right + o.goalWidth / 2 //Stay with the nice stripes
        };
        xy.eighteenYdBase = {
            left:  xy.goal.left - o.goalWidth / 2  - o.pitchIndent/o.stripeCount,
            right: xy.goal.right + o.goalWidth / 2 + o.pitchIndent/o.stripeCount
        };
        xy.eighteenYdArc = {
        	left:  Math.round( xy.eighteenYdBase.left  + 1.5*(xy.eighteenYdBase.left + xy.eighteenYdBase.right)/18 ),
        	right: Math.round( xy.eighteenYdBase.right - 1.5*(xy.eighteenYdBase.left + xy.eighteenYdBase.right)/18 ),
        	curveX: xy.pitch.center,
        	curveY: Math.round( xy.eighteenYd.base + (xy.eighteenYd.top + xy.eighteenYd.base) / 6 )
        };

        // Box tip positions in {x,y} format
        xy.tipPositions = {};
        xy.tipPositions.ownhalf = {
        	x: xy.pitch.center,
        	y: xy.pitch.base - o.sidelineInset * 2 // Centre line is -*3 
        };
        xy.tipPositions.midfield = {
        	y: Math.round( xy.eighteenYd.base + (xy.sideline.base - xy.eighteenYd.base) / 2 ),
        	xleft:  xy.pitch.center - 3/16*( o.pitchWidthBase - 2*( xy.eighteenYd.base + (xy.sideline.base - xy.eighteenYd.base) / 2 ) / Math.tan(o.pitchAngle * Math.PI / 180) ),
        	xright: xy.pitch.center + 3/16*( o.pitchWidthBase - 2*( xy.eighteenYd.base + (xy.sideline.base - xy.eighteenYd.base) / 2 ) / Math.tan(o.pitchAngle * Math.PI / 180) )
        };
        xy.tipPositions.wing = {
        	y: Math.round( xy.eighteenYd.base + (xy.sideline.base - xy.eighteenYd.base) / 2.5 ),
        	xleft:  xy.pitch.center - 7/16*( o.pitchWidthBase - 2*( xy.eighteenYd.base + (xy.sideline.base - xy.eighteenYd.base) / 2.5 ) / Math.tan(o.pitchAngle * Math.PI / 180) ),
        	xright: xy.pitch.center + 7/16*( o.pitchWidthBase - 2*( xy.eighteenYd.base + (xy.sideline.base - xy.eighteenYd.base) / 2.5 ) / Math.tan(o.pitchAngle * Math.PI / 180) )
        };
        xy.tipPositions.corner = {
        	y: Math.round( xy.eighteenYd.base - (xy.eighteenYd.base - xy.eighteenYd.top) * 0.5 ),
        	xleft:  xy.pitch.center - 3/10*( o.pitchWidthBase - 2*( xy.eighteenYd.base - (xy.eighteenYd.base - xy.eighteenYd.top) / 1.5 ) / Math.tan(o.pitchAngle * Math.PI / 180) ),
        	xright: xy.pitch.center + 3/10*( o.pitchWidthBase - 2*( xy.eighteenYd.base - (xy.eighteenYd.base - xy.eighteenYd.top) / 1.5 ) / Math.tan(o.pitchAngle * Math.PI / 180) )
        };
        xy.tipPositions.eighteenYd = {
        	y: xy.sixYd.base - o.sidelineInset,
        	xleft:  xy.pitch.center - 3/20*( o.pitchWidthBase - 2*xy.sixYd.base / Math.tan(o.pitchAngle * Math.PI / 180) ),
        	xright: xy.pitch.center + 3/20*( o.pitchWidthBase - 2*xy.sixYd.base / Math.tan(o.pitchAngle * Math.PI / 180) )
        };
        xy.tipPositions.sixYd = {
        	y: xy.sixYd.top + ( xy.sixYd.base - xy.sixYd.top ) / 2 - o.sidelineInset,
        	xleft:  xy.pitch.center - 1/20*( o.pitchWidthBase - ( xy.sixYd.top - xy.sixYd.base ) / Math.tan(o.pitchAngle * Math.PI / 180) ),
        	xright: xy.pitch.center + 1/20*( o.pitchWidthBase - ( xy.sixYd.top - xy.sixYd.base ) / Math.tan(o.pitchAngle * Math.PI / 180) )
        };

        xy.tipPositions.penalty = {
        	x: xy.pitch.center,
        	y: xy.eighteenYd.base - 2*o.sidelineInset
        }
    },
    draw: function() {    	
    	this.drawPitch();
    	this.drawGoal();
        this.drawSixYdBox();
        this.drawEighteenYdBox();
        this.drawSidelines();
        this.drawUnderground();
        this.drawTipLabels();
        this.element.hide();
    },
    drawPitch: function() {    	
        // Dark stripes
        var darkPath = [];
        var lightPath = [];
        for( var i=0; i<this.options.stripeCount; i=i+2 ) {
            darkPath.push(
                "M", this.xy.stripePointsTop[i].x,    this.xy.stripePointsTop[i].y,
                "L", this.xy.stripePointsBase[i].x,   this.xy.stripePointsBase[i].y,
                "L", this.xy.stripePointsBase[i+1].x, this.xy.stripePointsBase[i+1].y,
                "L", this.xy.stripePointsTop[i+1].x,  this.xy.stripePointsTop[i+1].y,
                "Z"
            );
        }
        for( var i=1; i<this.options.stripeCount; i=i+2 ) {
            lightPath.push(
                "M", this.xy.stripePointsTop[i].x,    this.xy.stripePointsTop[i].y,
                "L", this.xy.stripePointsBase[i].x,   this.xy.stripePointsBase[i].y,
                "L", this.xy.stripePointsBase[i+1].x, this.xy.stripePointsBase[i+1].y,
                "L", this.xy.stripePointsTop[i+1].x,  this.xy.stripePointsTop[i+1].y,
                "Z"
            );
        }

        this.canvas.path( darkPath  ).attr({
            "fill":         this.options.colorStripeDark,
            "stroke":       this.options.colorStripeDark,
            "stroke-width": 1
        });
        this.canvas.path( lightPath ).attr({
           "fill":         this.options.colorStripeLight,
           "stroke":       this.options.colorStripeLight,
           "stroke-width": 1
        });
        
    },
    drawUnderground: function() {
        this.canvas.rect( this.xy.pitchBase.left,      this.xy.pitch.base, 
                          this.options.pitchWidthBase, this.options.pitchDepth )
            .attr({ 
                "fill":         this.options.colorUnderGround,
                "stroke":       this.options.colorUnderGround,
                "stroke-width": 1
            });
    },
    drawGoal: function() {
        // Goal has no base, so loops back on itself
        this.sprite.goal = this.canvas.path([
            "M", this.xy.goal.left,  this.xy.goal.base, 
            "L", this.xy.goal.left,  this.xy.goal.top,  
            "L", this.xy.goal.right, this.xy.goal.top,
            "L", this.xy.goal.right, this.xy.goal.base,

            "L", this.xy.goal.right, this.xy.goal.top,  
            "L", this.xy.goal.left,  this.xy.goal.top,  
            "Z"
        ]);
        this.sprite.goal.attr({
            "stroke":       this.options.colorGoal,
            "stroke-width": this.options.goalThickness
        });
    },
    drawSixYdBox: function() {
        // Penalty has no base, so loops back on itself
        this.sprite.box6 = this.canvas.path([
            "M", this.xy.sixYdTop.left,   this.xy.sixYd.top,
            "L", this.xy.sixYdBase.left,  this.xy.sixYd.base,
            "L", this.xy.sixYdBase.right, this.xy.sixYd.base,
            "L", this.xy.sixYdTop.right,  this.xy.sixYd.top,

            "L", this.xy.sixYdBase.right, this.xy.sixYd.base,
            "L", this.xy.sixYdBase.left,  this.xy.sixYd.base,
            "Z"
        ]);
        this.sprite.box6.attr({
            "stroke":         this.options.colorPenaltyLine,
            "stroke-width":   this.options.sidelineThickness,
            "stroke-opacity": this.options.sidelineOpacity
        });
    },
    drawEighteenYdBox: function() {
        // Penalty has no base, so loops back on itself
        this.sprite.box18 = this.canvas.path([
            "M", this.xy.eighteenYdTop.left,   this.xy.eighteenYd.top,
            "L", this.xy.eighteenYdBase.left,  this.xy.eighteenYd.base,
            "L", this.xy.eighteenYdBase.right, this.xy.eighteenYd.base,
            "L", this.xy.eighteenYdTop.right,  this.xy.eighteenYd.top,

            "L", this.xy.eighteenYdBase.right, this.xy.eighteenYd.base,
            "L", this.xy.eighteenYdBase.left,  this.xy.eighteenYd.base,
            "Z"
        ]);
        this.sprite.box18.attr({
            "stroke":         this.options.colorPenaltyLine,
            "stroke-width":   this.options.sidelineThickness,
            "stroke-opacity": this.options.sidelineOpacity
        });
        // Penalty box arc
        var arc = "M" + this.xy.eighteenYdArc.left + "," + this.xy.eighteenYd.base
        	+ "S" + this.xy.eighteenYdArc.curveX + "," + this.xy.eighteenYdArc.curveY
        	+ "," + this.xy.eighteenYdArc.right + "," + this.xy.eighteenYd.base
        	+ "Z";
        this.sprite.arc18 = this.canvas.path( arc );
        this.sprite.arc18.attr({
            "stroke":         this.options.colorPenaltyLine,
            "stroke-width":   this.options.sidelineThickness,
            "stroke-opacity": this.options.sidelineOpacity
        });
    },
    drawSidelines: function() {
        this.sprite.sidelines = this.canvas.path([
			"M", this.xy.sidelineTop.left,   this.xy.sideline.top,
			"L", this.xy.sidelineBase.left,  this.xy.sideline.base,
			"L", this.xy.sidelineBase.right, this.xy.sideline.base,
			"L", this.xy.sidelineTop.right,  this.xy.sideline.top,
			"Z"
        ]);
        this.sprite.sidelines.attr({
            "stroke":         this.options.colorPenaltyLine,
            "stroke-width":   this.options.sidelineThickness,
            "stroke-opacity": this.options.sidelineOpacity
        });
        // Part of the other half, loops back too
        this.sprite.otherHalf = this.canvas.path([
			"M", this.xy.sidelineOHBase.left,  this.xy.pitch.base,
			"L", this.xy.sidelineBase.left,    this.xy.sideline.base,
			"L", this.xy.sidelineBase.right,   this.xy.sideline.base,
			"L", this.xy.sidelineOHBase.right, this.xy.pitch.base,
			
			"L", this.xy.sidelineBase.right,   this.xy.sideline.base,
			"L", this.xy.sidelineBase.left,    this.xy.sideline.base,
			"Z"
		 ]);
		 this.sprite.otherHalf.attr({
		     "stroke":         this.options.colorPenaltyLine,
		     "stroke-width":   this.options.sidelineThickness,
		     "stroke-opacity": this.options.sidelineOpacity
		 });
		 // Center Circle
		 this.sprite.circle = this.canvas.ellipse(
		    this.xy.pitch.center, this.xy.sideline.base,
		    this.options.pitchWidthBase/6, this.options.pitchHeight/5
		 );
		 this.sprite.circle.attr({
		     "stroke":         this.options.colorPenaltyLine,
		     "stroke-width":   this.options.sidelineThickness,
		     "stroke-opacity": this.options.sidelineOpacity
		 });
    },
    drawTipLabels: function() {
    	var xy   = this.xy;
    	var data = this.data;
    	// Own Half
    	this.drawTipLabel(
    		xy.tipPositions.ownhalf.x, 
    		xy.tipPositions.ownhalf.y,
    		data.values.ownhalf.goals
    	);
    	// Wings
    	this.drawTipLabel(
    		xy.tipPositions.wing.xleft, 
    		xy.tipPositions.wing.y,
    		data.values.lWing.goals
    	);
    	this.drawTipLabel(
    		xy.tipPositions.wing.xright, 
    		xy.tipPositions.wing.y,
    		data.values.rWing.goals
    	);
    	// Corners
    	this.drawTipLabel(
    		xy.tipPositions.corner.xleft, 
    		xy.tipPositions.corner.y,
    		data.values.lCorner.goals
    	);
    	this.drawTipLabel(
    		xy.tipPositions.corner.xright, 
    		xy.tipPositions.corner.y,
    		data.values.rCorner.goals
    	);
    	// Midfield
    	this.drawTipLabel(
    		xy.tipPositions.midfield.xleft, 
    		xy.tipPositions.midfield.y,
    		data.values.lMid.goals
    	);
    	this.drawTipLabel(
    		xy.tipPositions.midfield.xright, 
    		xy.tipPositions.midfield.y,
    		data.values.rMid.goals
    	);
    	// 18 Yard Box
    	this.drawTipLabel(
    		xy.tipPositions.eighteenYd.xleft, 
    		xy.tipPositions.eighteenYd.y,
    		data.values.l18Yd.goals
    	);
    	this.drawTipLabel(
    		xy.tipPositions.eighteenYd.xright, 
    		xy.tipPositions.eighteenYd.y,
    		data.values.r18Yd.goals
    	);
    	// 6 Yard Box
    	this.drawTipLabel(
    		xy.tipPositions.sixYd.xleft, 
    		xy.tipPositions.sixYd.y,
    		data.values.l6Yd.goals
    	);
    	this.drawTipLabel(
    		xy.tipPositions.sixYd.xright, 
    		xy.tipPositions.sixYd.y,
    		data.values.r6Yd.goals
    	);
    	// Penalty Area
    	this.drawTipLabel(
    		xy.tipPositions.penalty.x, 
    		xy.tipPositions.penalty.y,
    		data.strings.c18Yd.goals		// NB STRING
    	);
    	
    },
    drawTipLabel: function( x, y, val ) {
		// Draw label relative to origin point
		// Label height = Square and 1/3 triangle tip
		var labelSq  = this.options.labelSize;
		var labelTri = 5;
		var labelMargin;
		
		var output = val.toString();
		// If normal numeric
		if (!output.match(/(\d+\s)?\+\s\d+\spen/)) {
			labelMargin = {
				x: x - this.options.labelSize / 2,
				y: y - this.options.labelSize
			}
			var color = {};
				color.bg  = "#fff";
				color.txt = "#666";
			/* Get darker if more goals scored */
			if (val>0 && val<=3) {
				color.bg  = "#fc0";
				color.txt = "#333";
			}
			else if (val>3 && val<25) {
				color.bg  = "#f90";
				color.txt = "#fff";
			}
			else if (val>=25) {
				color.bg  = "#ee6e19";
				color.txt = "#fff";
			}
			this.sprite.tip = this.canvas.path([
		  	    "M", labelMargin.x, labelMargin.y,
		  	    "h", labelSq,
		  	    "v", labelSq,
		  	    "h", -4,
		  	    "l", -labelTri,  labelTri,
		  	    "l", -labelTri, -labelTri,
		  	    "h", -4,
		  	    "Z"
		  	]).attr({
		  		"fill": color.bg,
		  		"stroke-width": 0.001
		  	});
			this.sprite.label = this.canvas.text(
				labelMargin.x + labelSq/2,
				labelMargin.y + labelSq/2,
				val );
	        this.sprite.label.attr({
	        	"fill": 	   color.txt,
	            "font-size":   this.options.labelTextSize,
	            "font-weight": this.options.labelWeight,
	            "text-anchor": "middle"
	        });
		} else {
			// Must be the penalty square!
			labelMargin = {
				x: x - this.options.labelSize * 2,
				y: y - this.options.labelSize
			}
			var color = {};
				color.bg  = "#ee6e19";
				color.txt = "#fff";
			this.sprite.tip = this.canvas.path([
		  	    "M", labelMargin.x, labelMargin.y,
		  	    "h", 4*labelSq,
		  	    "v", labelSq,
		  	    "h", -31,
		  	    "l", -labelTri,  labelTri,
		  	    "l", -labelTri, -labelTri,
		  	    "h", -31,
		  	    "Z"
		  	]).attr({
		  		"fill": color.bg,
		  		"stroke-width": 0.001
		  	});
			this.sprite.label = this.canvas.text(
				labelMargin.x + labelSq*2,
				labelMargin.y + labelSq/2,
				val );
	        this.sprite.label.attr({
	        	"fill": 	   color.txt,
	            "font-size":   this.options.labelTextSize,
	            "font-weight": this.options.labelWeight,
	            "text-anchor": "middle"
	        });
		}
    }
});
/**
 *  Works for both tables and trs
 */
$.ui.svgWidget.subclass('ui.svgGoalTimes', {
    klass: "$.ui.svgGoalTimes",
    options: {
		yaxislabel:		'GOALS SCORED',
		xaxislabel:		'TIME SCORED',
		xaxiscount:		1,
		tipName:		'goalcount',
        tipText:        'goal',
        stripeCount:    8,
		barColor:		'#639ec8',
        barUnit:        4,
        barSpacing:     1,
        barWidth:       0,
        barFontSize:   20,
        barFontWeight:  'bold',
        bgStripes:		8,
        bgColor:		'#eff5f9',
        asPercentages:  true
    },
    tooltips: $([]), // empty jQuery - shared between all instances of this class

    _create: function() {
    },
    _init: function() {
        this.draw();
    },
    parseOptions: function() {
        this.options.barCount     = this.options.barCount   || this.data.rowNames.length * this.data.colNames.length;
        this.options.barSpacing   = this.options.barSpacing || Math.round( this.getInitWidth()/this.options.barCount * 0.2 );
        this.options.barWidth     = this.options.barWidth   || Math.round( ( this.getInitWidth() - this.getFontSize()*3.5 )/this.options.barCount - this.options.barSpacing );
        this.options.barHeight    = this.options.barHeight  || Math.round( this.getInitHeight() - this.getFontSize()*3 - this.options.barFontSize*1.5 );

        if( this.data.stats.max === 0 ) {
            this.options.barUnit      = 1;
            this.options.stripeHeight = this.options.barHeight/this.options.stripeCount;
        } else {
            var units = [1,2,4,5,10,25,50,100,250,500,1000];
            for( var i=0, n=units.length; i<n; i++ ) {
                if( this.data.stats.max / units[i] < this.options.stripeCount ) {
                    this.options.barUnit      = units[i];
                    this.options.stripeHeight = units[i] * this.options.barHeight/this.data.stats.max;
                    break;
                }
            }
        }
    },
    draw: function() {
    	this.drawBackStripes();
        this.drawData();
        this.drawAxisLabels();
        this.table.hide();
    },
    calculateXY: function() {
        this.xy = {};
    },
    drawBackStripes: function() {
        var barBase = this.options.barHeight + this.options.barFontSize*1.5;

    	this.sprite.bgStripes = this.canvas.set();
    	for( var i=0, n=this.options.bgStripes; i<=n; i++) {
            var stripe = {};
            stripe.left   = 0;
            stripe.right  = this.getInitWidth();
            stripe.width  = stripe.right - stripe.left;
            stripe.top    = barBase - (i * this.options.stripeHeight);
            stripe.base   = stripe.top + this.options.stripeHeight;
            stripe.top    = Math.max( 0, stripe.top );
            stripe.height = stripe.base - stripe.top;

            if( i % 2 == 1 ) {// && stripe.top >= this.getFontSize()/2 ) {
                this.sprite.bgStripes.push(
                    this.canvas.rect( stripe.left, stripe.top, stripe.width, stripe.height )
                               .attr( "fill",   this.options.bgColor )
                               .attr( "stroke", this.options.bgColor )
                               .attr( "stroke-width", 0.001 )
                );
            }
        }

    	for( var i=0, n=this.options.bgStripes; i<=n; i++) {
            var labelX = this.getFontSize()*2.25;
            var labelY = barBase - (i * this.options.stripeHeight);
            var labelText = String(this.options.barUnit * i);
            if( this.options.asPercentages ) { labelText += "%"; }

            if( labelY >= this.getFontSize()/2 ) {
                this.canvas.text( labelX, labelY, labelText )
                           .attr( "fill",        this.options.labelColor )
                           .attr( "font-weight", this.options.labelWeight )
                           .attr( "style",       "text-align:center" )
                           .attr( "text-anchor", "middle" );
            }
    	}
    },
    drawData: function() {
        this.sprite.bars    = {};
        this.sprite.barText = this.canvas.set();
        this.sprite.labels  = this.canvas.set();

        this.data.rowNames = this.data.rowNames.sort(); // PL-1769 - Ensure that goal times display in order

        var barCount = 0;
        for( var i=0, n=this.data.rowNames.length; i<n; i++ ) {
            var rowName = this.data.rowNames[i];

            this.sprite.bars    = this.canvas.set();
            this.sprite.barText = this.canvas.set();

            for( var j=0, m=this.data.colNames.length; j<m; j++, barCount++ ) {
                var colName   = this.data.colNames[j];
                var barHeight = Math.round( this.options.barHeight * (this.data.values[rowName][colName]/this.data.stats.max) ) || 2;
                var barLeft   = Math.round( (this.options.barWidth + this.options.barSpacing) * barCount + this.getFontSize()*3.5 );
                var barBase   = this.options.barHeight + this.options.barFontSize*1.5;
                var barTop    = barBase - barHeight;

                var barNode = this.canvas.rect( barLeft, barTop, this.options.barWidth, barHeight )
                	.attr( "fill",   this.options.barColor )
                	.attr( "stroke", this.options.barColor )
                	.attr( "stroke-width", 0.001 );

                this.sprite.bars.push( barNode );

                var tooltipHTML =
                	  "<div class='tooltip svgGoalTimesTooltip " + this.options.tipName + "'>"
	    	    	+ "<span class='time'>" + this.formatTooltipTime( this.data.rowNames[barCount] ) + "</span>"
	    	    	+ "<span class='num'>"  + this.data.values[rowName][colName]
                    + ((this.options.asPercentages) ? "%" : "")
                    + " "
                    + ((this.data.values[rowName][colName]==1)
                         ? this.options.tipText
                         : (this.options.tipText.replace(/s$/,"se")+"s") // 1 cross, 2 crosses
                      )
                    + "</span></div>";

                var tooltip = $(tooltipHTML).appendTo(this.getWrapper());
                var offset = {
            		top:  -tooltip.height()*1.25 + 4,
            		left: this.options.barWidth/2 - tooltip.width()/2 - 2
                };

                tooltip.css({
        			"position":"absolute",
        			"z-index":"900000",
        			"top":  (barTop  + offset.top)  + "px",
        			"left": (barLeft + offset.left) + "px",
        		    "display":"none"
        		});
                this.tooltips.push( tooltip[0] );

                barNode.hover(
                    $.proxy( this.onHover,      this, tooltip),
                    $.proxy( this.onBarUnhover, this, tooltip)
                );

                if ( barCount % this.options.xaxiscount === 0 ) {// Draw label every 3rd column
	                var labelX = barLeft + this.options.barWidth/2;
                	var labelY = barBase + this.options.labelSize*1.2;
	                var label  = this.formatLabel(rowName);
	                this.sprite.labels.push(
	                    this.canvas.text( labelX, labelY, label )
	                               .attr( "fill",        this.options.labelColor )
	                               .attr( "font-weight", this.options.labelWeight ) // Bold
	                               .attr( "style",       "text-align:center" )
	                               .attr( "text-anchor", "middle" )
	                );
                }
            }
        }

        $(this.getCanvas().canvas).hover(
           function() {}, // hover
           $.proxy( this.onUnhover, this, null )
        );
        $(document.body).bind("click.svgGoalTimes", $.proxy(this.onBodyClick, this));
    },

    _onHoverLastTooltip: null,
    _onHoverLastTimestamp: null,
    onHover: function( tooltip, event ) {
        this._onHoverLastTooltip = tooltip;
        this._onHoverLastTimestamp = (new Date()).getTime();

        this.tooltips.not(tooltip).fadeOut();
        tooltip.fadeIn();
    },
    onUnhover: function( tooltip, event ) {
        var self = this;
        var timestampdiff = (new Date().getTime()) - this._onHoverLastTimestamp;

        // Bugfix: Tooltip flashes when hovering on border pixel or 0-data
        // fadeOut when the mouse is over the tooltip, will trigger a new hover event
        // this causes the flashing, but we need a catch to ensure the tooltip never becomes perminantly stuck

        // Canvas unhover doesn't pass in a tooltip, but ensure its only unhover from background
        if( tooltip === null && event.target.nodeName !== "rect" ) {
            this.tooltips.fadeOut("slow");
        }
        // average flash delay on Firefox is 30ms
        else if( tooltip === this._onHoverLastTooltip && timestampdiff > 100 ) {
            this.tooltips.fadeOut("slow");
        }
    },
    onBodyClick: function(event) {
        // fadeout on body click - this is out catch to prevent the tooltip from getting stuck
        if( $(event.target).closest(this.element).length === 0 ) {
            this.tooltips.fadeOut("slow");
        }
    },

    /**
     *  @param  {String} label   text as defined in the HTML
     *  @return {String}
     */
    formatLabel: function( label ) {
        label = String(label).trim();
        label = label.toUpperCase().replace(/^(.*) (.*?)$/, "$1\n$2"); // Replace last space
        label = label.replace(/Extra\s*Time/i, "ET");                  // Add minute sign if not included in backing data
        label = label.replace(/^(\d+)\s*to\s*(\d+)$/i, "$2");          // 0 to 10 -> 10
        label = label.replace(/(\d+)/, "$1'");                       // Add minute sign if not included in backing data
        return label;
    },
    formatTooltipTime: function( label ) {
        label = String(label).trim();
        label = label.replace(/(\d+)/g, "$1'");                       // Add minute sign if not included in backing data
        return label;
    },
    drawAxisLabels: function() {
        // TODO: Draw X Axis Labels
    	var yAxisLabel = {
    		x: this.getFontSize()*3/4,
    		y: ( this.getInitHeight() - this.getFontSize()*3 ) / 2
    	};
    	var xAxisLabel = {
    		x: this.getFontSize()*3.5 + ( this.getInitWidth() - this.getFontSize()*3.5 ) / 2,
    		y: this.getInitHeight() - this.getFontSize()
    	};

        this.canvas.text( yAxisLabel.x, yAxisLabel.y, this.options.yaxislabel )
			.attr( "fill",        this.options.labelColor )
			.attr( "font-weight", this.options.labelWeight ) // Bold
			.attr( "text-anchor", "middle" )
			.rotate(270,yAxisLabel.x,yAxisLabel.y);

    	this.canvas.text( xAxisLabel.x, xAxisLabel.y, this.options.xaxislabel )
		    .attr( "fill",        this.options.labelColor )
		    .attr( "font-weight", this.options.labelWeight ) // Bold
		    .attr( "text-anchor", "middle" );
    }
});
/**
 * Shots list item currently commented out.
 */

$.ui.svgWidget.subclass('ui.svgMatchPitch', {
    klass: "$.ui.svgMatchPitch",
    options: {
        colorGoal:             "#cccccc",
        colorLabel:            "#ffffff",
        colorPenaltyLine:      "#ffffff",
        colorStripeDark:       "#139124",
        colorStripeLight:      "#35a63f",
        colorUnderGround:      "#035c0f",
        stripeCount:          16,
        pitchAngle:           45,
        pitchWidthBase:      710,
        pitchHeight:         160,
        pitchDepth:           11,
        goalWidthRatio:        0.15,   // GoalWidth/PitchHeight
        goalHeightRatio:       0.333,  // GoalHeight/GoalWidth - 8 feet by 8 yards
        goalThickness:         3,      // px
        sidelineInset:           4,
        sidelineThickness:     2,
        sidelineOpacity:       0.9,
        labelSize:               15
    },



    //***** Init *****//

    _init: function() {
        this.draw();
    },
    draw: function() {
        this.drawPitch();
        this.drawSixYdBoxes();
        this.drawEighteenYdBoxes();
        this.drawSidelines();
        this.drawUnderground();
        this.element.hide();
    },



    //***** Data *****//

    parseOptions: function( o ) {
        o = o || this.options;
        o.pitchIndent     = o.pitchIndent     || Math.ceil( o.pitchHeight * Math.tan(o.pitchAngle * Math.PI / 180) );
        o.pitchWidthTop   = o.pitchWidthTop   || Math.ceil( o.pitchWidthBase - (o.pitchIndent * 2) );

        o.goalWidth       = o.goalWidth       || Math.ceil( o.pitchHeight    * o.goalWidthRatio );
        o.goalHeight      = o.goalHeight      || Math.ceil( o.goalWidth      * o.goalHeightRatio );
        return o;
    },
    calculateXY: function() {
        var xy = this.xy = this._super();
        var o  = this.options;

        xy.perspectiveWeight = 10;
        xy.origin = { x: 0, y: 0 };

        xy.pitch = {
            top:     xy.origin.y,
            base:    xy.origin.y + o.pitchHeight,
            centerx: xy.origin.x + o.pitchWidthBase/2,
            centery: xy.origin.y + o.pitchHeight/2
        };
        xy.pitchTop = {
            left:   xy.origin.x + o.pitchIndent,
            right:  xy.origin.x + o.pitchWidthBase - o.pitchIndent
        };
        xy.pitchBase = {
            left:   xy.origin.x,
            right:  xy.origin.x + o.pitchWidthBase
        };

        xy.stripePointsTop  = [];
        xy.stripePointsBase = [];
        for( var i=0; i<=o.stripeCount; i++ ) {
            xy.stripePointsTop[i] = {
                x: xy.pitchTop.left + Math.round( i * o.pitchWidthTop/o.stripeCount ),
                y: xy.pitch.top
            };
            xy.stripePointsBase[i] = {
                x: xy.pitchBase.left + Math.round( i * o.pitchWidthBase/o.stripeCount ),
                y: xy.pitch.base
            };
        }

        xy.sideline = {
            top: xy.pitch.top + o.sidelineInset,
            base: xy.pitch.base - o.sidelineInset,
            center: xy.pitch.centerx
        };
        xy.sidelineTop = {
            left:   xy.pitchTop.left  + ( o.sidelineInset * Math.tan(o.pitchAngle * Math.PI / 180) ),
            right:  xy.pitchTop.right - ( o.sidelineInset * Math.tan(o.pitchAngle * Math.PI / 180) )
        };
        xy.sidelineBase = {
            left:   xy.pitchBase.left  + o.sidelineInset + ( o.sidelineInset * Math.tan(o.pitchAngle * Math.PI / 180) ),
            right:  xy.pitchBase.right - o.sidelineInset - ( o.sidelineInset * Math.tan(o.pitchAngle * Math.PI / 180) )
        };

        xy.sixYd = { //common
            top:  Math.round( xy.pitch.centery + o.goalWidth - xy.perspectiveWeight ),
            base: Math.round( xy.pitch.centery - o.goalWidth - xy.perspectiveWeight )
        };
        xy.sixYdHBase = {
               left:  Math.round( (o.pitchHeight - xy.sixYd.base) * o.pitchIndent / o.pitchHeight + o.sidelineInset+3 ),
               right: Math.round( (o.pitchHeight - xy.sixYd.base) * o.pitchIndent / o.pitchHeight + o.sidelineInset+28 )
        };
        xy.sixYdHTop = {
            left:  Math.round( (o.pitchHeight - xy.sixYd.top) * o.pitchIndent / o.pitchHeight + o.sidelineInset+3 ),
               right: Math.round( (o.pitchHeight - xy.sixYd.top) * o.pitchIndent / o.pitchHeight + o.sidelineInset+32 )
        };
        xy.sixYdABase = {
            left:  Math.round( o.pitchWidthBase - ((o.pitchHeight - xy.sixYd.base) * o.pitchIndent / o.pitchHeight )- o.sidelineInset-3 ),
               right: Math.round( o.pitchWidthBase - ((o.pitchHeight - xy.sixYd.base) * o.pitchIndent / o.pitchHeight )- o.sidelineInset-28 )
        };
        xy.sixYdATop = {
            left:  Math.round( o.pitchWidthBase - ((o.pitchHeight - xy.sixYd.top) * o.pitchIndent / o.pitchHeight ) - o.sidelineInset-3 ),
               right: Math.round( o.pitchWidthBase - ((o.pitchHeight - xy.sixYd.top) * o.pitchIndent / o.pitchHeight ) - o.sidelineInset-32)
        };

        xy.eighteenYd = { //common
            top:  Math.round( xy.pitch.centery + o.goalWidth * 1.8 - xy.perspectiveWeight ),
            base: Math.round( xy.pitch.centery - o.goalWidth * 1.5 - xy.perspectiveWeight )
        };
        xy.eighteenYdHBase = {
               left:  Math.round( (o.pitchHeight - xy.eighteenYd.base) * o.pitchIndent / o.pitchHeight + o.sidelineInset+3 ),
               right: Math.round( (o.pitchHeight - xy.eighteenYd.base) * o.pitchIndent / o.pitchHeight + o.sidelineInset+84 )
        };
        xy.eighteenYdHTop = {
            left:  Math.round( (o.pitchHeight - xy.eighteenYd.top) * o.pitchIndent / o.pitchHeight + o.sidelineInset+3 ),
               right: Math.round( (o.pitchHeight - xy.eighteenYd.top) * o.pitchIndent / o.pitchHeight + o.sidelineInset+110 )
        };
        xy.eighteenYdABase = {
            left:  Math.round( o.pitchWidthBase - ((o.pitchHeight - xy.eighteenYd.base) * o.pitchIndent / o.pitchHeight )- o.sidelineInset-3 ),
               right: Math.round( o.pitchWidthBase - ((o.pitchHeight - xy.eighteenYd.base) * o.pitchIndent / o.pitchHeight )- o.sidelineInset-84 )
        };
        xy.eighteenYdATop = {
            left:  Math.round( o.pitchWidthBase - ((o.pitchHeight - xy.eighteenYd.top) * o.pitchIndent / o.pitchHeight ) - o.sidelineInset-3 ),
               right: Math.round( o.pitchWidthBase - ((o.pitchHeight - xy.eighteenYd.top) * o.pitchIndent / o.pitchHeight ) - o.sidelineInset-110)
        };
        xy.eighteenYdHArc = {
            /*left:  170,
            right: 200,
            curveX: xy.pitch.centery,
            curveY: Math.round( xy.eighteenYd.base + (xy.eighteenYd.top + xy.eighteenYd.base) / 6 )*/
        };
        xy.eighteenYdAArc = {
            /*left:  Math.round( xy.eighteenYdABase.left  + 1.5*(xy.eighteenYdABase.left + xy.eighteenYdABase.right)/18 ),
            right: Math.round( xy.eighteenYdABase.right - 1.5*(xy.eighteenYdABase.left + xy.eighteenYdABase.right)/18 ),
            curveX: xy.pitch.centerx,
            curveY: Math.round( xy.eighteenYd.base + (xy.eighteenYd.top + xy.eighteenYd.base) / 6 )*/
        };

        return this.xy;
    },



    //***** Render Functions *****//

    drawPitch: function() {
        // Dark stripes
        var darkPath  = [];
        var lightPath = [];
        for( var i=0; i<this.options.stripeCount; i=i+2 ) {
            darkPath.push(
                "M", this.xy.stripePointsTop[i].x,    this.xy.stripePointsTop[i].y,
                "L", this.xy.stripePointsBase[i].x,   this.xy.stripePointsBase[i].y,
                "L", this.xy.stripePointsBase[i+1].x, this.xy.stripePointsBase[i+1].y,
                "L", this.xy.stripePointsTop[i+1].x,  this.xy.stripePointsTop[i+1].y,
                "Z"
            );
        }
        for( var i=1; i<this.options.stripeCount; i=i+2 ) {
            lightPath.push(
                "M", this.xy.stripePointsTop[i].x,    this.xy.stripePointsTop[i].y,
                "L", this.xy.stripePointsBase[i].x,   this.xy.stripePointsBase[i].y,
                "L", this.xy.stripePointsBase[i+1].x, this.xy.stripePointsBase[i+1].y,
                "L", this.xy.stripePointsTop[i+1].x,  this.xy.stripePointsTop[i+1].y,
                "Z"
            );
        }

        this.canvas.path( darkPath  ).attr({
            "fill":         this.options.colorStripeDark,
            "stroke":       this.options.colorStripeDark,
            "stroke-width": 1
        });
        this.canvas.path( lightPath ).attr({
           "fill":         this.options.colorStripeLight,
           "stroke":       this.options.colorStripeLight,
           "stroke-width": 1
        });

    },
    drawUnderground: function() {
        this.canvas.rect( this.xy.pitchBase.left,      this.xy.pitch.base,
                          this.options.pitchWidthBase, this.options.pitchDepth )
            .attr({
                "fill":         this.options.colorUnderGround,
                "stroke":       this.options.colorUnderGround,
                "stroke-width": 1
            });
    },
    drawSixYdBoxes: function() {
        // Penalty has no base, so loops back on itself
        this.sprite.box6home = this.canvas.path([
            "M", this.xy.sixYdHTop.left,   this.xy.sixYd.top,
            "L", this.xy.sixYdHTop.right,  this.xy.sixYd.top,
            "L", this.xy.sixYdHBase.right, this.xy.sixYd.base,
            "L", this.xy.sixYdHBase.left,  this.xy.sixYd.base,

            "L", this.xy.sixYdHBase.right, this.xy.sixYd.base,
            "L", this.xy.sixYdHTop.right,  this.xy.sixYd.top,
            "Z"
        ]);
        this.sprite.box6home.attr({
            "stroke":         this.options.colorPenaltyLine,
            "stroke-width":   this.options.sidelineThickness,
            "stroke-opacity": this.options.sidelineOpacity
        });
        this.sprite.box6away = this.canvas.path([
            "M", this.xy.sixYdATop.left,   this.xy.sixYd.top,
            "L", this.xy.sixYdATop.right,  this.xy.sixYd.top,
            "L", this.xy.sixYdABase.right, this.xy.sixYd.base,
            "L", this.xy.sixYdABase.left,  this.xy.sixYd.base,

            "L", this.xy.sixYdABase.right, this.xy.sixYd.base,
            "L", this.xy.sixYdATop.right,  this.xy.sixYd.top,
            "Z"
        ]);
        this.sprite.box6away.attr({
            "stroke":         this.options.colorPenaltyLine,
            "stroke-width":   this.options.sidelineThickness,
            "stroke-opacity": this.options.sidelineOpacity
        });
    },
    drawEighteenYdBoxes: function() {
        // Penalty has no base, so loops back on itself
        this.sprite.box18home = this.canvas.path([
             "M", this.xy.eighteenYdHTop.left,   this.xy.eighteenYd.top,
             "L", this.xy.eighteenYdHTop.right,  this.xy.eighteenYd.top,
             "L", this.xy.eighteenYdHBase.right, this.xy.eighteenYd.base,
             "L", this.xy.eighteenYdHBase.left,  this.xy.eighteenYd.base,

             "L", this.xy.eighteenYdHBase.right, this.xy.eighteenYd.base,
             "L", this.xy.eighteenYdHTop.right,  this.xy.eighteenYd.top,
             "Z"
         ]);
         this.sprite.box18home.attr({
             "stroke":         this.options.colorPenaltyLine,
             "stroke-width":   this.options.sidelineThickness,
             "stroke-opacity": this.options.sidelineOpacity
         });
         this.sprite.box18away = this.canvas.path([
            "M", this.xy.eighteenYdATop.left,   this.xy.eighteenYd.top,
            "L", this.xy.eighteenYdATop.right,  this.xy.eighteenYd.top,
            "L", this.xy.eighteenYdABase.right, this.xy.eighteenYd.base,
            "L", this.xy.eighteenYdABase.left,  this.xy.eighteenYd.base,

            "L", this.xy.eighteenYdABase.right, this.xy.eighteenYd.base,
            "L", this.xy.eighteenYdATop.right,  this.xy.eighteenYd.top,
            "Z"
            ]);
         this.sprite.box18away.attr({
             "stroke":         this.options.colorPenaltyLine,
             "stroke-width":   this.options.sidelineThickness,
             "stroke-opacity": this.options.sidelineOpacity
         });
        // Penalty box arc
        var arc = "M207,46S220,80,170,99Z";
        this.sprite.arc18home = this.canvas.path( arc );
        this.sprite.arc18home.attr({
            "stroke":         this.options.colorPenaltyLine,
            "stroke-width":   this.options.sidelineThickness,
            "stroke-opacity": this.options.sidelineOpacity
        });
        arc = "M504,46S490,80,540,99Z";
        this.sprite.arc18away = this.canvas.path( arc );
        this.sprite.arc18away.attr({
            "stroke":         this.options.colorPenaltyLine,
            "stroke-width":   this.options.sidelineThickness,
            "stroke-opacity": this.options.sidelineOpacity
        });
    },
    drawSidelines: function() {
        this.sprite.sidelines = this.canvas.path([
            "M", this.xy.sidelineTop.left,   this.xy.sideline.top,
            "L", this.xy.sidelineBase.left,  this.xy.sideline.base,
            "L", this.xy.sidelineBase.right, this.xy.sideline.base,
            "L", this.xy.sidelineTop.right,  this.xy.sideline.top,
            "Z"
        ]);
        this.sprite.sidelines.attr({
            "stroke":         this.options.colorPenaltyLine,
            "stroke-width":   this.options.sidelineThickness,
            "stroke-opacity": this.options.sidelineOpacity
        });
        // Halfway Line
        this.sprite.halfway = this.canvas.path([
            "M", this.xy.pitch.centerx, this.xy.sideline.top,
            "L", this.xy.pitch.centerx, this.xy.sideline.base,
            "Z"
        ]);
        this.sprite.halfway.attr({
            "stroke":         this.options.colorPenaltyLine,
            "stroke-width":   this.options.sidelineThickness,
            "stroke-opacity": this.options.sidelineOpacity
        });
        // Center Circle
        this.sprite.circle = this.canvas.ellipse(
           this.xy.pitch.centerx, this.xy.pitch.centery - this.xy.perspectiveWeight,
           this.options.pitchWidthBase/12, this.options.pitchHeight/6
        );
        this.sprite.circle.attr({
            "stroke":         this.options.colorPenaltyLine,
            "stroke-width":   this.options.sidelineThickness,
            "stroke-opacity": this.options.sidelineOpacity
        });
        this.sprite.circle = this.canvas.ellipse(
           this.xy.pitch.centerx, this.xy.pitch.centery - this.xy.perspectiveWeight,
           this.options.pitchWidthBase/200, this.options.pitchHeight/100
        );
        this.sprite.circle.attr({
            "fill":              this.options.colorPenaltyLine,
            "stroke":         this.options.colorPenaltyLine,
            "stroke-width":   this.options.sidelineThickness,
            "stroke-opacity": this.options.sidelineOpacity

        });
    }
});






$.ui.svgMatchPitch.subclass('ui.svgMatchStats', {
    klass: "$.ui.svgMatchPitch",
    options: {

    },
    _lastTooltipZIndex: 90000,  // {Number} keeps track of last tooltip z-index
    homeFormation: null, // {Array}
    awayFormation: null, // {Array}

    _create: function() {
        this.el.tooltips = $([]);
        this.el.labels   = $([]);
    },

    draw: function() {
        this._super();

        this.addNav();
        this.addNavEvents();

        this.drawPlayerLabels("Line-ups");
        this.element.hide();
    },



    //***** Getters / Setters *****//

    getTooltipForLabel: function( labelNode ) {
        var myidx   = $(labelNode).attr("href").replace("#pLabel","");
        var tooltip = this.el.tooltips.filter('div#playerTip' + myidx);
        return tooltip;
    },

    getColName: function( colNode ) {
        colNode = $(colNode);
        var colName = colNode.attr("class") || this._super(colNode);
        return colName;
    },

    getCellString: function( cellNode ) {
        var cellString = $(cellNode).html();
        return cellString;
    },



    //***** CalculateXY Functions ****//

    calculateFormations: function() {
        this.homeFormation = [];
        this.awayFormation = [];

        var pid, crow, prow = null;

        //11 players per team. Blatant hack. (Very) poor coding all round here.
        for( var i=0; i<11; i++ ) {
            pid  = this.data.rowNames[i];
            crow = this.data.values[pid].row;
            // If a new row, add a new 2nd-D array
            if(crow != prow) {
                this.homeFormation[crow] = new Array();
                this.homeFormation[crow].push(pid);
            } else {
                this.homeFormation[prow].push(pid);
            }
            prow = crow;
        }
        //Also predicting the data structure is not very good coding practise!
        //11 players per team. Blatant hack. (Very) poor coding all round here.
        for( var i=12; i<this.data.rowNames.length; i++ ) {
            pid  = this.data.rowNames[i];
            crow = this.data.values[pid].row;
            // If a new row, add a new 2nd-D array
            if(crow != prow) {
                this.awayFormation[crow] = new Array();
                this.awayFormation[crow].push(pid);
            } else {
                this.awayFormation[prow].push(pid);
            }
            prow = crow;
        }
    },


    calculateXY: function() {
        var xy = this.xy = this._super();
        var o  = this.options;

        this.calculateFormations();

        this.xy.playerRow = {};
        this.xy.playerRow.home = this.calculatePlayerRow( this.homeFormation, "home" );
        this.xy.playerRow.away = this.calculatePlayerRow( this.awayFormation, "away" );

        return this.xy;
    },

    calculatePlayerRow: function( teamFormation, homeAway ) {
        var xy = this.xy;
        var o  = this.options;
        var leftRight = (homeAway === "away") ? "right" : "left";
        var plusMinus = (homeAway === "away") ? -1 : 1;


        var playerRow = [];
        for( var i=0; i<teamFormation.length; i++ ) {
            switch (i) {
                // Goalkeeper: Halfway in 6yrd box
                case 0:
                    playerRow[i] = {
                        xtop:    Math.ceil( xy.pitchTop[leftRight]  + (plusMinus * o.pitchWidthTop/o.stripeCount  )),
                        xbottom: Math.ceil( xy.pitchBase[leftRight] + (plusMinus * o.pitchWidthBase/o.stripeCount )),
                        yspace:  xy.pitch.centery
                    };
                break;

                // Defenders:  On edge of 18yrd box
                case 1:
                    playerRow[i] = {
                        xtop:      Math.ceil( xy.pitchTop[leftRight]  + (plusMinus * 3*i * o.pitchWidthTop/o.stripeCount  )),
                        xbottom: Math.ceil( xy.pitchBase[leftRight] + (plusMinus * 3*i * o.pitchWidthBase/o.stripeCount )),
                        yspace:  Math.ceil( o.pitchHeight / (teamFormation[i].length+1) )
                    };
                break;

                // Mids + Att: Equally positioned to halfway
                default:
                    playerRow[i] = {
                        xtop:    Math.ceil( playerRow[i-1].xtop    + (xy.pitch.centerx + ( -plusMinus * o.pitchWidthTop/o.stripeCount)  - playerRow[1].xtop)    / (teamFormation.length - 2) ),
                        xbottom: Math.ceil( playerRow[i-1].xbottom + (xy.pitch.centerx + ( -plusMinus * o.pitchWidthBase/o.stripeCount) - playerRow[1].xbottom) / (teamFormation.length - 2) ),
                        yspace:  Math.ceil( o.pitchHeight / (teamFormation[i].length+1) )
                    };
                break;
            }
        }
        return playerRow;
    },




    //***** Render Functions *****//

    drawPlayerLabels: function( type ) {
        var playerId;        // PlayerId for lookup in table data
        var playerCount = 0;
        var shotLabels, goalLabels;

        this.el.labels.empty();

        // For each team formation row (eg 4, GK, DF, MF, AT)
        for ( var i=0, n=this.homeFormation.length; i<n; i++ ) {
            // For each player in formation row
            for ( var j=0; j<this.homeFormation[i].length; j++ ) {

                /***********************************
                 * Get data for playerId
                 **********************************/
                playerId = this.homeFormation[i][j];
                var player = this.player = this.player || {};
                    player.img   = this.data.strings[playerId].img;
                    player.name  = this.data.strings[playerId].player;
                    player.goals = this.data.values[playerId].goals        || '-';
                    player.shots = this.data.values[playerId].shots        || '-';

                if ( (type == "Shots" && player.shots > 0) ||
                     (type == "Goals" && player.goals > 0) ||
                     (type == "Line-ups") ) {

                    var labelSq  = this.options.labelSize * 2 / 3;
                    var labelTri = this.options.labelSize / 3;
                    var yHeight  = this.xy.pitch.top + (j+1) * this.xy.playerRow.home[i].yspace - 2*this.options.labelSize + (j+1)*2*this.xy.perspectiveWeight/3;
                    var xWidth   = this.xy.playerRow.home[i].xtop - this.xy.playerRow.home[i].xbottom;
                    var labelMargin   = {
                        x: Math.round( this.xy.playerRow.home[i].xtop - yHeight * xWidth / this.options.pitchHeight - labelSq/2 ),
                        y: Math.round( yHeight )
                    };

                    this.el.labels = this.el.labels.add( this.canvas.path([
                        "M", labelMargin.x, labelMargin.y,
                        "h", labelSq,
                        "v", labelSq,
                        "l", -labelTri,  labelTri,
                        "l", -labelTri, -labelTri,
                        "Z"
                    ]).attr({
                        "fill": this.data.rows[this.data.rowNames[playerCount]].color,
                        "stroke-width": 0.001,
                        "href":"#pLabel" + playerCount // this makes this label an <a> tag
                    }) );

                    // Tooltip HTML
                    var tooltipHTML = "<div class=\"tooltip player\" id=\"playerTip"
                        + playerCount +"\" style=\"display:none\">"
                        + this.player.img
                        + "<span class=\"name\">"           + this.player.name  + "</span>"
                        + "<table>"
                        + "<tr><td>Goals</td><td class='goals'>" + this.player.goals + "</td></tr>"
                        + "<tr><td>Shots</td><td class='shots'>" + this.player.shots + "</td></tr>"
                        + "</table>"
                        + "</div>";

                    this.el.tooltips = this.el.tooltips.add( $(tooltipHTML).appendTo(this.getWrapper()) );
                }

                playerCount++;

            } // for each player
        } // for each formation row

        playerCount++; // Increase to start next set of players

        // For each team formation row (eg 4, GK, DF, MF, AT)
        for ( var i=0, n=this.awayFormation.length; i<n; i++ ) {
            // For each player in formation row
            for ( var j=0; j<this.awayFormation[i].length; j++ ) {

                /***********************************
                 * Get data for playerId
                 **********************************/
                playerId     = this.awayFormation[i][j];
                player.img   = this.data.strings[playerId].img;
                player.name  = this.data.strings[playerId].player;
                player.goals = this.data.values[playerId].goals        || '-';
                player.shots = this.data.values[playerId].shots        || '-';

                if ( (type == "Shots" && player.shots > 0) ||
                        (type == "Goals" && player.goals > 0) ||
                        (type == "Line-ups") ) {

                    var yHeight = this.xy.pitch.top + (j+1) * this.xy.playerRow.away[i].yspace - 2*this.options.labelSize + (j+1)*2*this.xy.perspectiveWeight/3;
                    var xWidth  = this.xy.playerRow.away[i].xbottom - this.xy.playerRow.away[i].xtop;
                    var labelMargin = {
                        x: Math.round( this.xy.playerRow.away[i].xtop + yHeight * xWidth / this.options.pitchHeight - labelSq/2 ),
                        y: Math.round( yHeight )
                    };

                    this.el.labels = this.el.labels.add( this.canvas.path([
                        "M", labelMargin.x, labelMargin.y,
                        "h", labelSq,
                        "v", labelSq,
                        "l", -labelTri,  labelTri,
                        "l", -labelTri, -labelTri,
                        "Z"
                    ]).attr({
                        "fill": this.data.rows[this.data.rowNames[playerCount]].color,
                        "stroke-width": 0.001,
                        "href":"#pLabel" + playerCount // this makes this label an <a> tag
                    }) );

                    // Tooltip HTML
                    var tooltipHTML = "<div class=\"tooltip player\" id=\"playerTip"
                        + playerCount +"\" style=\"display:none\">"
                        + this.player.img
                        + "<span class=\"name\">"           + this.player.name  + "</span>"
                        + "<table>"
                        + "<tr><td>Goals</td><td class='goals'>" + this.player.goals + "</td></tr>"
                        + "<tr><td>Shots</td><td class='shots'>" + this.player.shots + "</td></tr>"
                        + "</table>"
                        + "</div>";

                    this.el.tooltips = this.el.tooltips.add( $(tooltipHTML).appendTo(this.getWrapper()) );

                }

                playerCount++;

            } // for each player
        } // for each formation row

        this.addTooltipEvents();
    },

    removePlayerLabels: function() {
        this.removeTooltipEvents();

        this.el.tooltips.emptyGC();
        this.el.tooltips = $([]);
        this.getWrapper().find("a").remove();
    },



    //***** Events *****//

    removeTooltipEvents: function() {
        var pitchLabels = this.getWrapper().find("a");
        pitchLabels.unbind("click mouseout mouseover");
    },

    addTooltipEvents: function() {
        var self = this;
        var wrapperOffset = this.getWrapper().offset();
        var pitchLabels   = this.getWrapper().find("a");

        pitchLabels.unbind("click mouseout mouseover");

        pitchLabels.each(function() {
            self.getTooltipForLabel(this).css({
                "position": "absolute",
                "z-index":  this._lastTooltipZIndex++,
                "top":      ($(this).offset().top  - wrapperOffset.top  - 110) + "px",
                "left":     ($(this).offset().left - wrapperOffset.left -  90) + "px",
                "display":  "none"
            });
        });

        pitchLabels.bind("click", function(event) {
            event.preventDefault();
        });

        pitchLabels.bind("mouseover click", function() {
            var tooltip = self.getTooltipForLabel(this);
            self.el.tooltips.not(tooltip).not(":hidden,:animated").fadeOut();
            tooltip.css("z-index", this._lastTooltipZIndex++);  // Ensure last referenced tooltip is always above
            tooltip.fadeIn();
        });

        pitchLabels.bind("mouseout", function() {
            var tooltip = self.getTooltipForLabel(this);
            setTimeout( function() {
                tooltip.not(":hidden,:animated").fadeOut("slow");
            }, 1500);
        });
    },



    //***** Nav *****//

    addNav: function() {
        this.el.nav = $(
            '<ul class="statstabs">' +
                '<li class="selected">Line-ups</li>' +
                // '<li>Shots</li>' + // No Shots for now
                '<li>Goals</li>' +
            '</ul>')
            .insertBefore(this.getWrapper());
    },
    addNavEvents: function() {
        var self = this;
        this.el.nav.children('li').bind("click",function(){
            $(this).siblings().removeClass('selected');
            $(this).addClass('selected');

            self.removePlayerLabels();
            self.drawPlayerLabels( $(this).text() );
        });
    }
});
/**
 *  init.js - Application widget initialisation
 *  Loaded immediately after firebugx.js, jquery.js, globals.js
 */
if( typeof QUnit !== "undefined" && !QUnit.loadInitJs ) {
    // Do nothing
    $.noop();
} else {

    // TODO: implement $.ajaxPrefilter to add wrappers for success/complete 
    //       check for status="notmodified", and return cache as stored in HTML5 window.sessionStorage
    //       implement window.sessionStorage for HTML5 browsers 
    //          @see http://github.com/adamayres/jqueryplugins/tree/master/request-storage
    //          @see http://github.com/adamayres/jqueryplugins/tree/master/ajax-cache-response
    //      Question: How does eTag persistance compate with sessionStorage persistance?

    // Set Ajax defaults
    $.ajaxSetup({
        type:       "GET",
        dataType:   "json",
        ifModified: false    // returns data=undefined on 304 Not Modified
    });

    $.ajaxPrefilter( "json", function(options, originalOptions, xhr) {
        xhr.setRequestHeader("Accept", "application/json");
    });

    if( $.getCqEditMode() || $.getTopLevelDomain() === "localhost" ) {
        // define a global AJAX error handler...
        $(document).ajaxError(function(e, xhr, settings, exception) {
            console.warn('AJAX error in: ' + settings.url + ' \n'+'error:\n' + exception);
        });
    }

    // $.browser available flags: webkit, safari, opera, msie, mozilla
    // CSS: .browser-webkit, .browser-safari, .browser-opera, .browser-mozilla, 
    //      .browser-msie, .browser-msie6, .browser-msie7, .browser-msie8, .browser-msie9
    // NOTE: .ext-ie6 flags are only visible when CQ5 is loaded, don't use them for css to be published
    // Modernizr.js uses document.documentElement.className, so we should be safe to modify the DOM before $(document).ready()
    (function() {
        for( var name in $.browser ) {
            if( $.browser[name] === true && name !== "version" ) {
                var version    = parseInt($.browser.version,10);
                var compatMode = false;
                if( name === "msie" && version == 7 && navigator.appVersion.match(/Trident/) ) { version = 9; compatMode = true; }

                var className = " browser-"+name + " browser-"+name+version;              // browser-msie browser-msie6
                if( name === "msie" && version <= 6 ) { className += " browser-msie6";  } // treat all older version of IE as 6
                if( name === "msie" && version <= 7 ) { className += " browser-msie67"; } // common flag to keep the CSS short
                if( name === "msie" && compatMode   ) { className += " browser-msie9compatMode"; } 
                if( name === "msie" && compatMode   ) { $(document).ready(function(){     // Wait till DOM ready
                    if( $("body").hasClass("ext-ie7") ) {                                 // Check for CQ ext.js browser detect
                        $("body").removeClass("ext-ie7").addClass("ext-ie9 ext-ie9-compat");
                    }
                }); }

                document.documentElement.className += className; // $(html).addClass()
            }
        }
    })();
    
    // Add .beforeDocumentReady during load - removeClass needs to be run before $.initWidgets() - it breaks things due to $().width() === 0
    // Use class="hideBeforeDocumentReady" or CSS { visibility: hidden; } - DON'T use {display: none} as it messes up $().width()
    (function() {
        document.documentElement.className += " beforeDocumentReady"; // $(html).addClass()
        $(document).ready(function() { $(document.documentElement).removeClass("beforeDocumentReady"); });         
    })();


    $.updateLoggedInState = function() {
        try {
            var userCookie = $.cookie('pluser');
            if( userCookie ) {
                userCookie = userCookie.substring(1, userCookie.length-1);
                userCookie = Base64.decode(userCookie);
                window.userObject = JSON.parse(userCookie); // global! (for fanzone)
                $('div.login').hide();
                $('div.loggedin p.memberName').append('Welcome, '+userObject.user.firstName +' '+userObject.user.lastName);
                $('div.loggedin').show();
                $('.siteutils li.login','#masthead').hide();
                $('.siteutils li.logout,.siteutils li.myaccount','#masthead').show();
            }  
        } catch(e) {
            console.error("$.updateLoggedInState(): userCookie: ", userCookie, ", Exception:", e );
            console.dir( e );
        }
    };
    
    $(document).ready( function() {
        // Now we have access to the full set of libraries

        try {
            $.cqEditMode = $.getCqEditMode(); // @see libs/jquery.extensions.js
            
            // Refresh the page if we switch from CQ edit mode to CQ preview mode
            // This is needed to make the meganav refresh correctly
            // CQ will refresh the page for us on the transition the other way round
            if( $.cqEditMode ) {
                 setInterval(function () {
                    if( !$.getCqEditMode() ) {
                        location.reload();
                    }                    
                 }, 1000);
            }
        	

            /**
             *   Bind a custom event into which we fire values to trigger track events, e.g
             *   $('body').trigger("YWAEvent.track",["cf",22,"Facebook"])
             */
            window.YWABeacon = $.el('div');
            window.YWABeacon.bind("YWAEvent:track",$.ywaTrack);
            
            $.updateLoggedInState();

            // This is where all the action happens
            // @see libs/jquery.extensions.js - $.initWidgets(rootNode)
            // @see widgets/miniwidgets.js    - $.miniwidgets(rootNode)
            $.initWidgets( document.body );


        } catch(e) {
            console.error("init.js: Exception:", e );
            console.dir( e );
            debugger;
        }    
    });
    
}
$.ui.basewidget.subclass('ui.playerSearch_headToHead', {
    klass: '$.ui.playerindex',
    options: {
        baselogo:       '/etc/designs/premierleague/images/backgrounds/player-h2h-blank.png',        // {String}       location of image for unselected club logo
        eventManager:   null,       // {EventManager} reference to js/libs/EventManager.js, passed in via init.js
        hash:           null        // {Hash}         define objects and arrays within the constructor, else it will create a class variable
    },

    // Called from constructor before _init() – automatically calls this._super() before function
   _create: function() { 
        this.options.hash = {};
        this.options = $.getAttributeHash( this.element, this.options );
    },

    // Called from constructor after _create() – automatically calls this._super() before function
    _init: function() {
		this.addEvents();
        this.displayPlayerNamesInStats();
		this.displayCompareButton();
        //this.moveToHero();
    },
    displayPlayerNamesInStats: function(){
    	//updates the player names above the stats table
    	if($('#clubAField').val()!= '' && $('#clubBField').val() != ''){
    		$('#lhsPlayerName').text($('#clubAName').val());
    		$('#rhsPlayerName').text($('#clubBName').val());
    	}
    	
    },
    addEvents: function() {
        var self = this;
		
		//$('td.selector',this.element).each(function(){
        $('td.selector').each(function(){
            $t          			= $(this);
			var name    			= $t.attr('name');
            var logoRef 			= $t.attr('logo');
            var paId    			= $t.attr('paid');
			var teamId 				= $t.attr('teamid');
			var teamName 			= $t.attr('teamid');
			var playerNationality 	= $t.attr('nationality');
			var playerNationalityImg = $t.attr('nationality_flag');
			console.log('playerNationalityImg: ' + playerNationalityImg);
			var playerPosition 		= $t.attr('playerposition');
			var playerShirtNumber 	= $t.attr('playershirtnumber');
			var clubLogoSrc			= $t.attr('clubLogo');
            
            var submit  = $('input [type=submit]', self.element);
			
            var left          			= {};
                left.logo     			= $('#left-club img.logo');
                left.overlay  			= $('#left-club img.overlay');
                left.txt      			= $('#left-club p.playerName');
                left.input    			= $('#clubAField');
				left.teamName			= $('#left-club p.clubName'); //Needs to pull in img url
				left.nationality		= $('#left-club p.nationalityTxt span');
				left.nationalityImg		= $('#left-club .playerHeadToHeadHeroNationalFlag');
				left.playerPosition		= $('#left-club p.playerPosition');
				left.playerSquadNumber	= $('#left-club p.squadNumber');
			
				left.playerNameHidden	= $('#clubAName');
				left.changePlayer 		= $('#changePlayerLhs');
				left.clubLogo			= $('#lhsClubLogo');
				 
            var right         			= {};
                right.logo    			= $('#right-club img.logo');
                right.overlay 			= $('#right-club img.overlay');
                right.txt     			= $('#right-club p.playerName');
                right.input   			= $('#clubBField');
				right.teamName			= $('#right-club p.clubName');
				right.nationality		= $('#right-club p.nationalityTxt span');
				right.nationalityImg	= $('#right-club .playerHeadToHeadHeroNationalFlag');
				right.playerPosition	= $('#right-club p.playerPosition');
				right.playerSquadNumber	= $('#right-club p.squadNumber');

				right.playerNameHidden	= $('#clubBName');
				right.changePlayer 		= $('#changePlayerRhs');
				right.clubLogo			= $('#rhsClubLogo');

				$t.click(function(){
				
					if( (parseInt($('#clubAField').val()) !=0) && (parseInt($('#clubBField').val()) !=0)){
						bothFieldsPopulated=true;
						//show the compare button
						$('.compareButtonDiv').css('display','block');
					}
					else{
						bothFieldsPopulated=false;
					}

					if($(this).hasClass('plus-sign')){
						//add player data	
						if(left.txt.text().length>0 && bothFieldsPopulated == false){
							//the left data is there so set the right data
							$(this).removeClass( 'plus-sign' ).addClass( 'minus-sign' );
							//update the text 
							right.txt.text(name);
							$('#clubBName').val(name);
							right.input.val(paId);
							right.clubLogo.attr("src",clubLogoSrc);
							
							right.nationality.text(playerNationality);
							right.nationalityImg.attr("src",playerNationalityImg);
							
							right.playerPosition.text(playerPosition);
							right.playerSquadNumber.text(playerShirtNumber);
							
							//fade the new player images
							right.overlay.fadeOut();
							right.logo
								.fadeOut()
								.attr('src', logoRef)
								.attr('club-id', paId)
								.fadeIn(300);
							right.overlay.fadeIn(800);
															
							//prepare the url string and compare button
							var urlString = "/content/premierleague/en-gb/players/head-to-head.html?paramLhsComparePlayerId=" + $('#clubAField').val() + "&paramRhsComparePlayerId=" + $('#clubBField').val() + '&paramLhsComparePlayerName=' + $('#clubAName').val()+ '&paramRhsComparePlayerName=' + $('#clubBName').val();
							$('#compareLink').attr("href", urlString);
							document.getElementById('changePlayerRhs').style.display='block';
							$('.compareButtonDiv').css('display','block');
						}
						else if(bothFieldsPopulated==false){
							//no data in yet so set the left data
							$(this).removeClass( 'plus-sign' ).addClass( 'minus-sign' );
							left.txt.text(name);
							left.playerNameHidden.val(name);
							$('#clubAName').val(name);
							left.input.val(paId);
							left.clubLogo.attr('src',clubLogoSrc);
							
							//left.nationality.text(playerNationality);
							left.nationality.text(playerNationality);
							console.log('playerNationalityImg - left data : ' + playerNationalityImg);
							left.nationalityImg.attr("src",playerNationalityImg);
							
							left.playerPosition.text(playerPosition);
							left.playerSquadNumber.text(playerShirtNumber);
							
							left.overlay.fadeOut();
							left.logo
								.fadeOut()
								.attr('src', logoRef)
								.attr('club-id', paId)
								.fadeIn(300);
							left.overlay.fadeIn(800);
							//prepare the url string and compare button
							var urlString = "/content/premierleague/en-gb/players/head-to-head.html?paramLhsComparePlayerId=" + $('#clubAField').val() + "&paramRhsComparePlayerId=" + $('#clubBField').val() + '&paramLhsComparePlayerName=' + $('#clubAName').val()+ '&paramRhsComparePlayerName=' + $('#clubBName').val();
							$('#compareLink').attr("href", urlString);
							document.getElementById('changePlayerLhs').style.display='block';
						}
					}
					else{ 
						//remove player data on LHS
						if($('#clubAField').val() == paId){
							//remove data
							left.txt.text('');
							left.input.val('');
							left.teamName.text('');
							left.clubLogo.attr('src','');
							left.playerPosition.text('');
							left.nationality.text('');
							left.playerSquadNumber.text('');
							
							$('#clubAField').val(0);
							$('#clubAName').val('');
							
							left.overlay.fadeOut();
							left.logo
								.fadeOut()
								.attr('src', self.options.baselogo)
								.attr('club-id', '')
								.fadeIn();
							left.overlay.fadeIn(800);
							//hide the change player button
							left.changePlayer.css('display','none');
						}
						else{
							//remove data on RHS
							right.txt.text('');
							right.input.val('');
							right.teamName.text('');
							right.clubLogo.attr('src','');
							
							right.playerPosition.text('');
							right.nationality.text('');
							right.playerSquadNumber.text('');
							$('#clubBField').val(0);
							$('#clubBName').val();
							
							right.overlay.fadeOut();
							right.logo
								.fadeOut()
								.attr('src', self.options.baselogo)
								.attr('club-id', '')
								.fadeIn();
							right.overlay.fadeIn(800);
							right.txt.text('');
							
							//hide the change player button
							right.changePlayer.css('display','none');
						}
						//reset CSS styles
						$(this).removeClass( 'minus-sign' ).addClass( 'plus-sign' );
						
						//check to see if both players have been de-selected and hide the Compare Button
						if($('#clubAField').val()==0 && $('#clubBField').val()==0 ){
							$('.compareButtonDiv').css('display','none');
						}
					}
				//finally double check the LHS and RHS and display the compare button if both have been set
				if( (parseInt($('#clubAField').val()) !=0) && (parseInt($('#clubBField').val()) !=0)){
					//show the compare button
					$('.compareButtonDiv').css('display','block');
				}
            });
        });
    },
    moveToHero: function() {
        //$('.svg-wrapper').empty();
        //$.initWidgets($('#playerStatsTabs', this.element));
    	//$.initWidgets($('.tabs', this.element));
/*
    	var clubindex = '<div class="clubindex" />';
        $('.headimage',        this.element).appendTo('.hero').wrap( clubindex );
        $('.tabnav-container', this.element).appendTo('.hero').wrap( clubindex );
*/		
    },
	displayCompareButton: function() {
		if($('#clubAField').val()>0 && $('#clubBField').val()>0 ){
			$('.compareButtonDiv').css('display','block');
		}
	}
	
    
});

//called by the change player button on either LHS or RHS 
function changePlayer(whichSide){
			
			var baseLogo = "/etc/designs/premierleague/images/backgrounds/player-h2h-blank.png";
			
            var left          			= {};
                left.logo     			= $('#left-club img.logo');
                left.overlay  			= $('#left-club img.overlay');
                left.txt      			= $('#left-club p.playerName');
                left.input    			= $('#clubAField');
				//left.teamId				= $('#left-club p.teamId');
				left.teamName			= $('#left-club p.clubName'); //Needs to pull in img url
				left.nationality		= $('#left-club p.nationalityTxt');
				//left.nationalityImg		= $('#left-club p.nationalityTxt img');
				//national logo
				left.playerPosition		= $('#left-club p.playerPosition');
				left.playerSquadNumber	= $('#left-club p.squadNumber');
			
				left.playerNameHidden	= $('#clubAName');
				 
            var right         			= {};
                right.logo    			= $('#right-club img.logo');
                right.overlay 			= $('#right-club img.overlay');
                right.txt     			= $('#right-club p.playerName');
                right.input   			= $('#clubBField');
				//right.teamId			= $('#right-club p.teamId');
				right.teamName			= $('#right-club p.clubName');
				right.nationality		= $('#right-club p.nationalityTxt');
				//right.nationalityImg	= $('#right-club p.nationalityImg img');
				right.playerPosition	= $('#right-club p.playerPosition');
				right.playerSquadNumber	= $('#right-club p.squadNumber');

				right.playerNameHidden	= $('#clubBName');


	if(whichSide=='left'){
	/*
		left.txt.text('');
		left.input.val('');
		left.teamName.text('');
		left.playerPosition.text('');
		left.nationality.text('');
		left.playerSquadNumber.text('');
		
		left.nationality.text('');
		left.nationalityImg.attr('src','');
		
		$('#clubAField').val(0);
		$('#clubAName').val('');
/*		
		left.overlay.fadeOut();
		left.logo
			.fadeOut()
			.attr('src', baseLogo)
			.attr('club-id', '')
			.fadeIn();
		left.overlay.fadeIn(800);
		
		document.getElementById('changePlayerLhs').style.display='none';
		$('.compareButtonDiv').attr('display','none');
*/		
		
		//reload the page
		if($('#clubBField').val() != 0){
			var urlString = "/content/premierleague/en-gb/players/head-to-head.html?paramRhsComparePlayerId=" + $('#clubBField').val() + '&paramRhsComparePlayerName=' + $('#clubBName').val();		
		}
		else{
			var urlString = "/content/premierleague/en-gb/players/head-to-head.html";
		}
		window.location.href=urlString;
	}
	else{
	/*
		right.txt.text('');
		right.input.val('');
		right.teamName.text('');
		right.playerPosition.text('');
		right.nationality.text('');
		
		right.nationalityImg.attr('src','');
		
		right.playerSquadNumber.text('');
		$('#clubBField').val(0);
		$('#clubBName').val('');
/*		
		right.overlay.fadeOut();
		right.logo
			.fadeOut()
			.attr('src', baseLogo)
			.attr('club-id', '')
			.fadeIn();
		right.overlay.fadeIn(800);
		right.txt.text('');	

		document.getElementById('changePlayerRhs').style.display='none';
		$('.compareButtonDiv').attr('display','none');
*/
		//reload the page
		if($('#clubAField').val() != 0){
			var urlString = "/content/premierleague/en-gb/players/head-to-head.html?paramLhsComparePlayerId=" + $('#clubAField').val() + '&paramLhsComparePlayerName=' + $('#clubAName').val();		
		}	
		else{
			var urlString = "/content/premierleague/en-gb/players/head-to-head.html";
		}
		window.location.href=urlString;
	}
}

$.ui.basewidget.subclass('ui.playerSearch', {
    klass: "$.ui.playerSearch",
    options: {
		tabs: 			 '.table_tabs a',	  		// {String} Search type tab links
		search:		 	 '.search',			  		// {String} Class of search section parent div 
		filter: 		 '.filter',			  		// {String} Class of filter section parent div 
		resultsPerPage:  '.results_per_page', 		// {String} Class of result pagination section parent div
		paramResPerPage: 'paramItemsPerPage', 		// {String} Name  of input for how many items per page 
		searchTypeInput: '#paramSearchType',  		// {String} Id    of input for search type parameter
		searchdefault:   'Enter player name'  // {String} Default 'Quick Search' text
		//h2hiselect: 	 'td.h2h-add-sub a'   // {String} TBA: H2H Selection target cell
    },
   _create: function() {
		this.el.tabs 		   = this.element.find( this.options.tabs );
		this.el.form 		   = this.element.find( 'form' );
		this.el.searchType	   = this.element.find( this.options.searchTypeInput );
		this.el.searchInput    = this.element.find( 'input[type=text]',   this.options.search );
		this.el.searchButton   = this.element.find( 'input[type=submit]', this.options.search );
		this.el.filterAlpha    = this.element.find( '.alpha_sort',  	  this.options.filter );
		this.el.filterClub     = this.element.find( '.club_selectLi',     this.options.filter );
		this.el.filterSeason   = this.element.find( '#paramSeason',       this.options.filter );
		this.el.filterReset    = this.element.find( '#resetButton',       this.options.filter );
		this.el.pageLinks      = this.element.find( '.pagination', 		  this.options.resultsPerPage );
		this.el.pageNumItems   = this.element.find( '.items_per_page',    this.options.resultsPerPage );
		this.el.pageInput      = this.element.find( '[name=paramSelectedPageIndex]' );
		//this.el.h2hs 		   = this.element.find( this.options.h2hselect );
    },
   _init: function() {
    	this.el.tabs	    .live("click", 		$.proxy(this.tabClick, 	    this));
    	this.el.searchInput .live("focus blur", $.proxy(this.searchDefault, this));
    	this.el.searchButton.live("click",		$.proxy(this.searchClick,   this));
    	this.el.filterReset .live("click",		$.proxy(this.reset,			this));
    	this.el.filterAlpha .live("change",		$.proxy(this.submit,        this));
    	this.el.filterClub  .live("change",		$.proxy(this.submit,        this));
    	this.el.filterSeason.live("change",		$.proxy(this.submit,        this));
    	this.el.pageNumItems.live("change",     $.proxy(this.resultsClick,  this));
    	this.el.pageLinks   .live("change",		$.proxy(this.pageClick,     this));
    	//this.el.h2hs		.live("click",		$.proxy(this.h2hClick,		this));
    	
    	this.initTabs();
    },
    initTabs: function() {
    	if (this.el.tabs.filter('.current').attr('href') === '#az') {
    		this.el.filterClub.hide();
    	} else if (this.el.tabs.filter('.current').attr('href') === '#club') {
    		this.el.filterAlpha.hide();
    	}
    },
    tabClick: function(event) {
    	var type = "A_TO_Z";
    	var current = this.el.tabs.filter( event.currentTarget );
    	if (!current.hasClass('current')) {
    		this.el.tabs.not(current).removeClass('current');
    		current.addClass('current');
	    	if (current.attr('href') === "#az") {
	    		this.el.filterAlpha.slideDown();
	    		this.el.filterClub.hide();
	    	} else {
	    		this.el.filterAlpha.slideUp();
	    		this.el.filterClub.show();
	    		type = "BY_CLUB";
	    	}
	    	this.el.searchType.val(type);
    	}
    	return false;
    },
    searchDefault: function(event) {
    	var val = this.el.searchInput.val();
    	if (event.type === "focusin") {
    		// check value against default and set to blank if same
    		this.el.searchInput.val( 
    			(val.toLowerCase() === this.options.searchdefault.toLowerCase()) ? '' : (val)
    		);
    	} else if (event.type === "focusout") {
    		// check value is blank and set to default if it is.
    		this.el.searchInput.val( 
    			(val === '') ? this.options.searchdefault : val); 
    	}
    },
    searchClick: function() {
    	return (this.el.searchInput.val() != this.options.searchdefault 
    				&& this.el.searchInput.val() != '') ? this.submit() : false;
    },
    pageClick: function(event) {
    	var page = this.el.pageLinks.find( ':checked', event.currentTarget ).val();
    	this.el.pageInput.val(page);
    	this.submit();
    	return true;
    },
    resultsClick: function(event) {
    	var sel = this.el.pageNumItems.filter( event.currentTarget );
    	// Remove other select box
    	this.el.pageNumItems.not(sel).remove();
    	// Change input name if required
    	sel.attr('name', this.options.paramResPerPage);
    	this.submit();
    	return true;
    },
    submit: function() {
		if (this.el.searchInput.val() == this.options.searchdefault) {
			this.el.searchInput.val('');
    	}
		// Encode and strip string for search
		this.el.searchInput.val(this.el.searchInput.val().replace(/\s+/g,' ').trim());
		// Reset club input if A-Z tab 
		if ( this.el.tabs.filter('.current').attr('href') === '#az' ) {
			this.el.filterClub.remove();
		}
		// Remove A-Z input if club tab 
		if ( this.el.tabs.filter('.current').attr('href') === '#club' ) {
			this.el.filterAlpha.remove();
		}
		this.el.form.submit();
		return true;
    },
    reset: function() {
    	this.el.searchInput.val('');
    	this.el.filterAlpha.find('input').remove(); //Hacky way to reset but works
    	this.el.filterClub.val(0);
    	this.el.filterSeason.val(0);
    	this.el.pageNumItems.val(0);
    	
    	this.el.form.submit();
    	return true;
    }
});

$.ui.playerSearch.subclass('ui.playerSearchCustom', {
	klass: "$.ui.playerSearchCustom",
	options: {
		
	},
	_create: function() {
		this.el.filterPosition = this.element.find( '#paramPosition', this.options.filter );
	},
	_init: function() {
		this.el.filterPosition.live("change", $.proxy(this.submit, this));
	},
    reset: function() {
    	this.el.searchInput.val('');
    	this.el.filterAlpha.find('input').remove(); //Hacky way to reset but works
    	this.el.filterClub.val(0);
    	this.el.filterSeason.val(0);
    	this.el.filterPosition.val(0);
    	this.el.pageNumItems.val(0);
    	
    	this.el.form.submit();
    	return true;
    }
});

