/*jslint white:false plusplus:false browser:true nomen:false */
/*globals $, jQuery, console, Globals, window, google, User, localStorage, hex_sha1, YT */

var log = {};

var JbUI = function () {
    'use strict';

    var jbUI = this,
        instanceOf = function (obj, type) {
            return (obj && obj.hasOwnProperty && (obj instanceof type));
        };

    if (!(instanceOf(jbUI, JbUI))) {
        jbUI = new JbUI();
    }

    return jbUI;
};

// Sizzle Include
(function (J) {
    J.Sizzle = jQuery.find;
}(JbUI));

// OOP
(function (J) {
    'use strict';

    J.Class = function () {
    };
    J.Class.create = function (members) {
        return J.Class.extend(members);
    };
    J.Class.extend = function (members) {
        function Class() {
            if (this.init) {
                this.init.apply(this, arguments);
            }
        }

        var BaseClassProxy = function () {
        },
            base = BaseClassProxy.prototype = this.prototype,
            member,
            makeBaseFunc = function (member, func) {
                return function () {
                    var baseDefault = this.base,
                        BaseProxy = function (context) {
                            this[member] = (function (context, superFunc) {
                                return function () {
                                    return superFunc.apply(context, arguments);
                                };
                            }(context, BaseClassProxy.prototype[member]));
                        },
                        contextualFunc;
                    this.base = new BaseProxy(this);
                    contextualFunc = func.apply(this, arguments);
                    this.base = baseDefault;
                    return contextualFunc;
                };
            };

        Class.prototype = new BaseClassProxy();
        Class.prototype.constructor = Class;

        for (member in members) {
            if (members.hasOwnProperty(member)) { // ok jslint, you win
                if (typeof members[member] === 'function' && typeof BaseClassProxy.prototype[member] === 'function') {
                    Class.prototype[member] = makeBaseFunc(member, members[member]);
                } else {
                    Class.prototype[member] = members[member];
                }
            }
        }

        Class.extend = J.Class.extend;

        base = null;
        return Class;
    };
    J.Typed = J.Class.create({
        init:function (itemType) {
            this.itemType = itemType || Object;
        },
        getItemType:function () {
            return this.itemType;
        },
        serialize:function () {
            return JSON.stringify(this);
        }
    });
    J.TypedDictionary = J.Typed.extend({
        init:function (itemType) {
            this.base.init(itemType);
            this.collection = {};
        },
        push:function (key, value) {
            if (value instanceof this.getItemType()) {
                this.collection[key] = value;
            }
        }
    });
    J.TypedCollection = J.Typed.extend({
        init:function (itemType) {
            this.base.init(itemType);
            this.collection = [];
        },
        push:function (item) {
            if (item instanceof this.getItemType()) {
                item._originalIndex = this.collection.length;
                this.collection.push(item);
            }
        },
        unshift:function (item) {
            var i = 1;
            if (item instanceof this.getItemType()) {
                item._originalIndex = 0;
                this.collection.unshift(item);
                for (i; i < this.length(); i += 1) {
                    this.collection[i]._originalIndex += 1;
                }
            }
        },
        getEnumerable:function () {
            return this.collection;
        },
        length:function () {
            return this.collection.length;
        },
        each:function (callback, gcContext, ordered) {
            gcContext = gcContext || this;
            ordered = J.Utility.parseBoolean(ordered);
            var i = this.collection.length,
                item;
            while (i--) {
                if (ordered) {
                    item = this.collection[this.collection.length - 1 - i];
                    callback.apply(gcContext, [item, this.collection.length - 1 - i]);
                } else {
                    item = this.collection[i];
                    callback.apply(gcContext, [item, i]);
                }
            }
        },
        combine:function (collection) {
            if (collection instanceof JbUI.TypedCollection) {
                var i = 0,
                    collEnumerable = collection.getEnumerable();
                for (i; i < collEnumerable.length; i += 1) {
                    this.push(collEnumerable[i]);
                }
            }
        }
    });
    J.Enum = J.Class.create({
        init:function (values) {
            var i = values.length,
                value;
            if (i > 0) {
                while (i--) {
                    value = values[i];
                    if (value instanceof J.EnumItem) {
                        if (!this.hasOwnProperty(value.name)) {
                            this[value.name] = value;
                        }
                    }
                }
            }
        },
        findValue:function (value) {
            var property;
            for (property in this) {
                if (this.hasOwnProperty(property) && this[property] instanceof J.EnumItem) {
                    if (this[property].value === value) {
                        return this[property];
                    }
                }
            }

            return null;
        }
    });
    J.EnumItem = J.Class.create({
        init:function (name, value) {
            this.value = value;
            this.name = name;
        },
        getNameToLower:function () {
            return this.name.toLowerCase();
        }
    });
}(JbUI));

//Dom
(function (J) {
    'use strict';

    var date = new Date(),
        $windowStorageKeyPrefix = '$JWindowStore-Dom-' + date.getTime() + '-',
        _html5Supprted = null,
        _html5Elements = ['abbr', 'article', 'aside', 'audio', 'canvas', 'datalist', 'details', 'figcaption', 'figure', 'footer', 'header', 'hgroup', 'mark', 'meter', 'nav', 'output', 'progress', 'section', 'subline', 'summary', 'time', 'video'],
        _html5ProxyDiv = null,
        _getHtml5ProxyDiv = function () {
            if (_html5ProxyDiv === null) {
                var proxyFrag,
                    length;
                _html5ProxyDiv = document.createElement('div');
                _html5ProxyDiv.innerHTML = '<header></header>';

                proxyFrag = document.createDocumentFragment();
                length = _html5Elements.length;
                while (length--) {
                    proxyFrag.createElement(_html5Elements[length]);
                }
                proxyFrag.appendChild(_html5ProxyDiv);
            }

            return _html5ProxyDiv;
        };
    J.Dom = {};
    J.Dom.getDocumentBody = function () {
        var body = window[$windowStorageKeyPrefix + 'documentBody'];
        if (!body) {
            window[$windowStorageKeyPrefix + 'documentBody'] = body = ((typeof(document.compatMode) === "string" && document.compatMode.indexOf("CSS") >= 0 && document.documentElement) ? document.documentElement : document.body);
        }
        return body;
    };
    J.Dom.getHtml5Supported = function () {
        if (_html5Supprted === null) {
            var html5Test = document.createElement('div');
            html5Test.innerHTML = '<header></header>';
            _html5Supprted = html5Test.childNodes.length > 0;
        }

        return _html5Supprted;
    };
    J.Dom.innerHtml5 = function (innerHtml) {
        if (J.Dom.getHtml5Supported()) {
            return innerHtml;
        } else {
            var proxyEl = _getHtml5ProxyDiv(),
                proxyFrag,
                length;

            proxyEl.innerHTML = innerHtml;
            length = proxyEl.childNodes.length;
            proxyFrag = document.createDocumentFragment();
            while (length--) {
                proxyFrag.appendChild(proxyEl.firstChild);
            }

            return proxyFrag;
        }
    };

    if (typeof(window.pageXOffset) === "number") {
        J.Dom.getScrollLeft = function () {
            return window.pageXOffset;
        };
        J.Dom.getScrollTop = function () {
            return window.pageYOffset;
        };
    } else if (typeof(J.Dom.getDocumentBody().scrollLeft) === "number") {
        J.Dom.getScrollLeft = function () {
            return J.Dom.getDocumentBody().scrollLeft;
        };
        J.Dom.getScrollTop = function () {
            return J.Dom.getDocumentBody().scrollTop;
        };
    } else {
        J.Dom.getScrollLeft = J.Dom.getScrollTop = function () {
            return NaN;
        };
    }

    J.Dom.getScrollWidth = function () {
        return J.Dom.getDocumentBody().scrollWidth;
    };
    J.Dom.getScrollHeight = function () {
        return J.Dom.getDocumentBody().scrollHeight;
    };

    if (typeof(window.innerWidth) === "number") {
        J.Dom.getViewportHeight = function () {
            return window.innerHeight;
        };
        J.Dom.getViewportWidth = function () {
            return window.innerWidth;
        };
    } else if (typeof(J.Dom.getDocumentBody().clientWidth) === "number") {
        J.Dom.getViewportHeight = function () {
            return J.Dom.getDocumentBody().clientHeight;
        };
        J.Dom.getViewportWidth = function () {
            return J.Dom.getDocumentBody().clientWidth;
        };
    } else {
        J.Dom.getViewportHeight = J.Dom.getViewportWidth = function () {
            return NaN;
        };
    }

    //TODO - tom - add non-webkit support
    J.Dom.show = function (element, timeMs, callback, gcContext) {
        timeMs = J.Utility.isNumber(timeMs) ? timeMs : 250;

        if (typeof jQuery !== 'undefined') {
            element = element instanceof jQuery ? element[0] : element;
        }

        var transform = element.style.webkitTransform,
            transition = element.style.webkitTransition,
            duration = element.style.webkitTransitionDuration,
            transProp = element.style.webkitTransitionProperty,
            webkitEndHandler;

        if (J.Utility.isFunction(callback)) {
            webkitEndHandler = function () {
                window.setTimeout(function () {
                    element.style.webkitTransitionProperty = transProp;
                    element.style.webkitTransitionDuration = duration;
                    element.style.webkitTransition = transition;
                    element.style.webkitTransform = transform;
                    element.removeEventListener('webkitTransitionEnd', webkitEndHandler);
                    /*ignore-this-jslint-error*/
                    callback.apply(gcContext || this);
                }, 0);
            };
            element.addEventListener('webkitTransitionEnd', webkitEndHandler);
            /*ignore-this-jslint-error*/

            if (timeMs < 1 || (element.style.opacity === '1' || element.style.opacity === 1)) {
                webkitEndHandler();
            }
        }

        element.style.opacity = 1;
        element.style.webkitTransitionProperty = 'opacity';
        element.style.webkitTransitionDuration = timeMs + 'ms';
    };
    //TODO - tom - add non-webkit support
    J.Dom.hide = function (element, timeMs, callback, gcContext) {
        timeMs = J.Utility.isNumber(timeMs) ? timeMs : 250;

        if (typeof jQuery !== 'undefined') {
            element = element instanceof jQuery ? element[0] : element;
        }

        var transform = element.style.webkitTransform,
            transition = element.style.webkitTransition,
            duration = element.style.webkitTransitionDuration,
            transProp = element.style.webkitTransitionProperty,
            webkitEndHandler;

        if (J.Utility.isFunction(callback)) {
            webkitEndHandler = function () {
                window.setTimeout(function () {
                    element.style.webkitTransitionProperty = transProp;
                    element.style.webkitTransitionDuration = duration;
                    element.style.webkitTransition = transition;
                    element.style.webkitTransform = transform;
                    element.removeEventListener('webkitTransitionEnd', webkitEndHandler);
                    /*ignore-this-jslint-error*/
                    callback.apply(gcContext || this);
                }, 0);
            };
            element.addEventListener('webkitTransitionEnd', webkitEndHandler);
            /*ignore-this-jslint-error*/

            if (timeMs < 1 || (element.style.opacity === '' || element.style.opacity === '0' || element.style.opacity === 0)) {
                webkitEndHandler();
            }
        }

        element.style.opacity = 0;
        element.style.webkitTransitionProperty = 'opacity';
        element.style.webkitTransitionDuration = timeMs + 'ms';
    };
    J.Dom.querySelector = function (selector, parent) {
        parent = parent || document;
        if (!window.Touch && J.Utility.isFunction(document.querySelector)) { // iphone has better performance with sizzle
            return document.querySelector(selector); // todo - tom - implement parent search
        }

        var results = J.Sizzle(selector, parent);
        if (results.length === 0) {
            return null;
        } else {
            return results[0];
        }
    };
    J.Dom.querySelectorAll = function (selector, parent) {
        parent = parent || document;
        if (!window.Touch && J.Utility.isFunction(document.querySelector)) { // iphone has better performance with sizzle
            return document.querySelectorAll(selector); // todo - tom - implement parent search
        }

        var results = J.Sizzle(selector, parent);
        if (results.length === 0) {
            return null;
        } else {
            return results;
        }
    };
    J.Dom.getElement = function (idOrSelector, isQueryOrParent) {
        // warning: for mobile, just use document.getElementById
        // do not use #my-id if you are just getting the id, please just use 'my-id'
        if (!isQueryOrParent) {
            return document.getElementById(idOrSelector);
        } else if (true === isQueryOrParent) {
            return J.Dom.querySelectorAll(idOrSelector);
        } else {
            return J.Dom.querySelectorAll(idOrSelector, isQueryOrParent);
        }
    };
    J.Dom.elementExists = function (element) {
        var html = document.body.parentNode;
        while (element) {
            if (element === html) {
                return true;
            }
            element = element.parentNode;
        }
        return false;
    };
    J.Dom.stripIdHash = function (elementId) {
        if (elementId.charAt(0) === '#') {
            return elementId.slice(1);
        }
        return elementId;
    };
    J.Dom.contains = function (parent, child) {
        if (!parent) {
            return false;
        }

        while (parent) {
            if (parent === child) {
                return true;
            }

            try {
                child = child.parentNode;
                if (!child.tagName || child.tagName.toUpperCase() === "BODY") {
                    break;
                }
            } catch (ex) {
                break;
            }
        }

        return false;
    };
    J.Dom.hasCssClass = function (element, className) {
        if (!element || !element.className) {
            return;
        }
        var wordBoundary = /\b/;
        return element.className.search(new RegExp(wordBoundary.source + className + wordBoundary.source)) >= 0;
    };
    J.Dom.removeCssClass = function (element, className) {
        if (!element) {
            return;
        }
        if (!className) {
            return false;
        }

        // We would have liked to use s/^\s+|\s+$|\s+(?=\s)//g, but look-ahead assertions are not supported until IE 6.
        var wordBoundary = /\b/;
        element.className = element.className.replace(new RegExp(wordBoundary.source + className + wordBoundary.source, "g"), "").replace(/^\s+|\s+$|(\s)\s+/g, "$1");
        return true;
    };
    J.Dom.addCssClass = function (element, className, checkExisting) {
        if (!element) {
            return;
        }
        if (!className) {
            return false;
        }

        if (checkExisting !== false && J.Dom.hasCssClass(element, className)) {
            return false;
        }

        element.className += (element.className ? " " : "") + className;
        return true;
    };

    J.Dom.Point = J.Class.create({
        init:function (x, y) {
            this.x = x || 0;
            this.y = y || 0;
        },
        combine:function (point) {
            return new J.Dom.Point(
                this.x += point.x,
                this.y += point.y
            );
        }
    });
    J.Dom.Size = J.Class.create({
        init:function (width, height) {
            this.width = width || 0;
            this.height = height || 0;
        }
    });
    J.Dom.Point.fromJQuery = function (jQueryPosition) {
        return new J.Dom.Point(jQueryPosition.left, jQueryPosition.top);
    };
    J.Dom.Bounds = J.Class.create({
        init:function (left, top, width, height) {
            this.left = left || 0;
            this.top = top || 0;
            this.width = width || 0;
            this.height = height || 0;
        },
        getRight:function () {
            return this.left + this.width;
        },

        getBottom:function () {
            return this.top + this.height;
        },
        contains:function (point) {
            if ((this.top > point.y) || (this.getBottom() < point.y)) {
                return false;
            }

            if ((this.left > point.x) || (this.getRight() < point.x)) {
                return false;
            }

            return true;
        }
    });
    J.Dom.Bounds.fromElement = function (element) {
        if (!element || element.style.display === "none") {
            return new J.Dom.Bounds();
        }

        var box,
            bounds;
        if (element.getBoundingClientRect) {
            try {
                if (element.tagName === "HTML") {
                    return new J.Dom.Bounds();
                }

                box = element.getBoundingClientRect();
                return new J.Dom.Bounds(
                    box.left - J.Dom.getDocumentBody().clientLeft + J.Dom.getScrollLeft(),
                    box.top - J.Dom.getDocumentBody().clientTop + J.Dom.getScrollTop(),
                    element.offsetWidth,
                    element.offsetHeight);
            } catch (boundingEx) {
//                log.warn(boundingEx);
            }
        }

        // old firefox hack
        if (document.getBoxObjectFor) {
            try {
                box = document.getBoxObjectFor(element);
                return new J.Dom.Bounds(box.x, box.y, element.offsetWidth, element.offsetHeight);
            } catch (boxObjEx) {
//                log.warn(boxObjEx);
            }
        }

        bounds = new J.Dom.Bounds(element.offsetLeft, element.offsetTop, element.offsetWidth, element.offsetHeight);
        try {
            while ((element = element.offsetParent) !== null) {
                bounds.left += element.offsetLeft;
                bounds.top += element.offsetTop;

                if (element === document.body) {
                    break;
                }
            }
        } catch (boundsEx) {
//            log.warn(boundsEx);
        }

        return bounds;
    };
    J.Dom.createFragment = function (string) {
        var frag = document.createDocumentFragment(),
            proxy = document.createElement('div');
        proxy.innerHTML = string;

        while (proxy.firstChild) {
            frag.appendChild(proxy.firstChild);
        }

        return frag;
    };
    J.Dom.Size.fromElement = function (element) {
        if (!element) {
            return new J.Dom.Size();
        }

        return new J.Dom.Size(element.offsetWidth, element.offsetHeight);
    };
    J.Dom.MarkupWriter = J.Class.create({
        init:function () {
            this.writer = [];
        },
        toString:function () {
            return this.writer.join('');
        },
        toFragment:function () {
            return J.Dom.createFragment(this.toString());
        },
        push:function (string) {
            this.writer.push(string);
        },
        append:function (string) {
            this.push(string);
        },
        clear:function () {
            this.dispose();
            this.writer = [];
        },
        dispose:function () {
            var reset = this.reset;
            this.writer = reset;
            delete this.writer;
        },
        length:function () {
            return this.writer.length;
        },
        isEmpty:function () {
            return this.writer.length === 0;
        }
    });
    J.Dom.MarkupWriter.create = function () {
        return new J.Dom.MarkupWriter();
    };
    J.Dom.UIControl = J.Class.create({
        init:function (container, dataSource, dataSourceType) {
            if (J.Utility.isString(container)) {
                container = document.getElementById(container);
            }
            this.usesJQuery = false; // todo - tom - jquery element deprecate
            this.container = container;

            this.markupWriter = new J.Dom.MarkupWriter();
            this.baseClass = 'ui-control';
            this.cssClass = this.baseClass;

            this.dataSource = dataSource || {};
            this.dataSourceType = dataSourceType || Object;
            if (this.isValidDataSource(this.dataSource) === false) {
                throw 'please specify a data source of the correct type';
            }
        },
        isValidDataSource:function (dataSource) {
            return (dataSource instanceof this.dataSourceType);
        },
        getDataSourceType:function () {
            if (this.dataSourceType) {
                return this.dataSourceType;
            }

            throw 'no data source specified';
        },
        setContainer:function (value) {
            this.container = value;
        },
        parseRenderedDom:function () {
            if (this.container) {
                if (this.usesJQuery && this.container instanceof jQuery) {
                    this.contents = J.Dom.querySelector('#' + this.id, this.container[0]); // todo - tom - jquery element deprecate
                } else {
                    this.contents = J.Dom.querySelector('#' + this.id, this.container);
                }
            } else {
                this.contents = document.getElementById(this.id);
            }

            if (this.usesJQuery) {
                this.contents = $(this.contents);
            }
        },
        applyAttributes:function () {
            if (!this.id) {
                this.id = this.baseClass + '-' + J.Utility.Guid.create();
            }
        },
        renderContents:function (markupWriter) {
        },
        render:function () {
            this.markupWriter.clear();

            this.applyAttributes();

            this.renderContents(this.markupWriter);

            if (this.container) {
                if (this.usesJQuery && this.container instanceof jQuery) {
                    this.container[0].appendChild(this.markupWriter.toFragment());  // todo - tom - jquery element deprecate
                } else {
                    this.container.appendChild(this.markupWriter.toFragment());
                }
            }

            this.parseRenderedDom();
        },
        renderToString:function () {
            if (this.markupWriter.toString().length > 0) {
            } else {
                this.markupWriter.clear();
                this.applyAttributes();
                this.renderContents(this.markupWriter);
            }

            return this.markupWriter.toString();
        },
        dispose:function () {
            if (this.contents) {
                this.contents.remove();
                delete this.contents;
            }

            delete this.usesJQuery;
            delete this.container;

            delete this.markupWriter;
            delete this.baseClass;
            delete this.cssClass;

            delete this.dataSource;
            delete this.dataSourceType;
        }
    });
}(JbUI));

//Utility
(function (J) {
    'use strict';

    J.Utility = {};
    J.Utility.Guid = {}; // todo: tom - fully implement this
    J.Utility.Guid.create = function () {
        /*jslint bitwise:false */
        var S4 = function () {
            return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
        };
        /*jslint bitwise:true */
        return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
    };
    J.Utility.propertyMunge = function () {
        var target = arguments[0] || {},
            source = arguments[1],
            overwrite = arguments[2] || false,
            propName;

        if (!source) {
            return target;
        }

        for (propName in source) {
            if (source.hasOwnProperty(propName)) {
                if (overwrite || !target.hasOwnProperty(propName)) {
                    target[propName] = source[propName];
                }
            }
        }

        return target;
    };
    J.Utility.isEmptyObject = function (value) {
        if (J.Utility.isObject(value) === false) {
            log.warn('J.Utility.isEmptyObject: value is not an object'); // TODO - tom - implement error handling
        }

        for (var name in value) {
            if (value.hasOwnProperty(name)) {
                return false;
            }
        }
        return true;
    };
    J.Utility.trim = function (value) {
        return value.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
    };
    J.Utility.isEmptyString = function (value) {
        return (J.Utility.isString(value) === false) || !J.Utility.trim(value);
    };
    J.Utility.isArray = function (value) {
        return typeof(value) === 'array' || (value !== null && typeof(value) === 'object' && value instanceof Array);
    };
    J.Utility.isBoolean = function (value) {
        return typeof(value) === 'boolean' || (value !== null && typeof(value) === 'object' && value instanceof Boolean);
    };
    J.Utility.parseBoolean = function (value) {
        if (J.Utility.isBoolean(value)) {
            return value;
        } else if (J.Utility.isString(value)) {
            return value.toLowerCase() === 'true';
        } else {
            return false;
        }
    };
    J.Utility.isFunction = function (value) {
        return typeof(value) === 'function' || (value !== null && typeof(value) === 'object' && value instanceof Function);
    };
    J.Utility.isNumber = function (value) {
        return !isNaN(value) && (typeof(value) === 'number' || (value !== null && typeof(value) === 'object' && value instanceof Number));
    };
    J.Utility.isString = function (value) {
        return typeof(value) === 'string' || (value !== null && typeof(value) === 'object' && value instanceof String);
    };
    J.Utility.isObject = function (value) {
        if (!value) {
            return false;
        }
        return(typeof(value) === 'object' || J.Utility.isArray(value) || J.Utility.isFunction(value));
    };
    J.Utility.hasMethod = function (object, method) {
        return (J.Utility.isObject(object) && J.Utility.isFunction(object[method]));
    };
    J.Utility.cloneFunction = function (value) {
        if (J.Utility.isFunction(value) === false) {
            throw 'Utility.cloneFunction: you passed a value that is not a function';
        }
        var property,
            clonedFunction = function () {
                return value.apply(this, arguments);
            };

        clonedFunction.prototype = value.prototype;
        for (property in value) {
            if (value.hasOwnProperty(property) && property !== 'prototype') {
                clonedFunction[property] = value[property];
            }
        }

        return clonedFunction;
    };
    J.Utility.cookie = function (key, value, options) {
        var days, t, result, decode;
        // key and at least value given, set cookie...
        if (arguments.length > 1 && String(value) !== "[object Object]") {
            options = jQuery.extend({}, options);

            if (value === null || value === undefined) {
                options.expires = -1;
            }

            if (typeof options.expires === 'number') {
                days = options.expires;
                t = options.expires = new Date();
                t.setDate(t.getDate() + days);
            }

            value = String(value);

            return (document.cookie = [
                encodeURIComponent(key), '=',
                options.raw ? value : encodeURIComponent(value),
                options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
                options.path ? '; path=' + options.path : '',
                options.domain ? '; domain=' + options.domain : '',
                options.secure ? '; secure' : ''
            ].join(''));
        }

        // key and possibly options given, get cookie...
        options = value || {};
        decode = options.raw ? function (s) {
            return s;
        } : decodeURIComponent;
        return (result = new RegExp('(?:^|; )' + encodeURIComponent(key) + '=([^;]*)').exec(document.cookie)) ? decode(result[1]) : null;
    };
    J.Utility.serializeForm = function (el) {
        if (typeof el === "string") {
            el = $(el)[0];
        }
        if (el.nodeName.toLowerCase() !== 'form') {
            return false;
        }
        var i, j, viableNodes, rawChildren, formChildren, returnObject, nodeList, currentNode;
        viableNodes = ["input", "select", "textarea"];
        rawChildren = [];
        formChildren = [];
        returnObject = {};
        nodeList = [];
        for (i = 0; i < viableNodes.length; i++) {
            nodeList = el.getElementsByTagName(viableNodes[i]);
            for (j = 0; j < nodeList.length; j++) {
                rawChildren.push(nodeList[j]);
            }
        }
        // build list of viable form elements
        for (i = 0; i < rawChildren.length; i++) {
            currentNode = rawChildren[i];
            switch (rawChildren[i].nodeName.toLowerCase()) {
                case "input":
                    switch (currentNode.getAttribute("type")) {
                        case "text":
                            formChildren.push(currentNode);
                            break;
                        case "hidden":
                            formChildren.push(currentNode);
                            break;
                        case "password":
                            formChildren.push(currentNode);
                            break;
                        case "checkbox":
                            if (currentNode.checked) {
                                formChildren.push(currentNode);
                            }
                            break;
                        case "radio":
                            if (currentNode.checked) {
                                formChildren.push(currentNode);
                            }
                            break;
                    }
                    break;
                case "select":
                    formChildren.push(currentNode);
                    break;
                case "textarea":
                    formChildren.push(currentNode);
                    break;
            }
        }
        //build object of the name-value pairs
        for (i = 0; i < formChildren.length; i++) {
            currentNode = formChildren[i];
            if (!returnObject.hasOwnProperty(currentNode.name)) {
                returnObject[currentNode.name] = currentNode.value;
            } else if (currentNode.value) {
                returnObject[currentNode.name] += "," + currentNode.value;
            }
        }

        return returnObject;
    };
    J.Utility.getHashBangData = function () {
        if (!window.location.hash) {
            return false;
        }
        var data, len, i, hash = window.location.hash.split("!")[1].split("&");
        data = {};
        len = hash.length;
        for (i = 0; i < len; i++) {
            data[hash[i].split("=")[0]] = hash[i].split("=")[1];
        }
        return data;
    };
    J.Utility.isValidEmail = function (email) {
        var reg = /^([A-Za-z0-9_\-\.\+])+\@([A-Za-z0-9_\-\.\+])+\.([A-Za-z]{2,4})$/;
        return reg.test(email);
    };
    J.Utility.startsWith = function (string, value) {
        return string.slice(0, value.length) === value;
    };
    J.Utility.endsWith = function (string, value) {
        return string.slice(-value.length) === value;
    };
}(JbUI));

//Formatting
(function (J) {
    'use strict';

    J.Format = {};
    J.Format.number = function (number, locale) {
        // TODO - 11-15-2011- tom - make this not a hack

        if (J.Utility.isEmptyString(locale) === false && locale.slice(0, 'de'.length) === 'de') {
            number = '' + number;
            number = number.replace(',', '[,]');
            number = number.replace('.', ',');
            number = number.replace('[,]', '.');
            return number;
        }

        return '' + number;
    };
    J.Format.rank = function (rank, locale) {
        // TODO - 11-15-2011- tom - make this not a hack
        return rank + J.Format.rankSuffix(rank, locale);
    };
    J.Format.rankSuffix = function (rank, locale) {
        // TODO - 11-15-2011- tom - make this not a hack

        if (J.Utility.isEmptyString(locale) === false && locale.slice(0, 'de'.length) === 'de') {
            return '.';
        }

        var suffix = 'th';
        if ([11, 12, 13].indexOf(rank) < 0) {
            switch (rank % 10) {
                case 1:
                    suffix = 'st';
                    break;
                case 2:
                    suffix = 'nd';
                    break;
                case 3:
                    suffix = 'rd';
                    break;
            }
        }
        return suffix;
    };
}(JbUI));

//Events
(function (J) {
    // TODO: tom - test this in lt IE 8
    var JEvent = function (defaultContext, $arguments) {
        this.defaultContext = defaultContext || window;
        if ($arguments && $arguments.length === 1) {
            $arguments = $arguments[0];
        }
        this.data = $arguments;
        this.idemId = J.Utility.Guid.create();
    };

    J.Event = J.Class.create({
        init:function (defaultContext, eventName) {
            this.eventName = (J.Utility.isString(eventName) && J.Utility.isEmptyString(eventName) === false) ? eventName : ('$JEvent-' + J.Utility.Guid.create());


            if (document.createEvent) {
                this._eventProxy = document.createEvent('Event');
                this._eventProxy.initEvent(this.eventName, false, true);
                this._eventProxy.jEvent = new JEvent(this._defaultContext, null);
            } else if (document.createEventObject) {
                document.documentElement[this.eventName] = 0;
            } else {
                throw 'Fatal event error';
            }
            this._defaultContext = defaultContext || window;
            this._subscriptions = {};
        },
        getEventName:function () {
            return this.eventName;
        },
        remove:function (subscription_or_guid) {
            if (J.Utility.hasMethod(subscription_or_guid, 'getGuid')) {
                var guid = subscription_or_guid.getGuid();
                subscription_or_guid.remove(this.remove);
                delete this._subscriptions[guid];
            } else if (J.Utility.isString(subscription_or_guid)) {
                this._subscriptions[subscription_or_guid].remove(this.remove);
                delete this._subscriptions[subscription_or_guid];
            }
        },
        fire:function () {
            if (document.createEvent) {
                this._eventProxy.jEvent = new JEvent(this._defaultContext, arguments || null);
                document.dispatchEvent(this._eventProxy);
            } else if (document.createEventObject) {
                document.documentElement[this.eventName] += 1;
            }
        },
        subscribe:function (method, context, executeCount) {
            var subscription = new J.Event.Subscription(this, document, method, context || this._defaultContext, executeCount);
            this._subscriptions[subscription.getGuid()] = subscription;
            return this._subscriptions[subscription.getGuid()];
        }
    });

    J.Event.Subscription = function (event, element_or_eventContext, method, context, executeCount) {
        if (J.Utility.isString(method) && context) {
            method = context[method];
        }

        if (executeCount === true) {
            executeCount = 1;
        }

        if (J.Dom.elementExists(element_or_eventContext) || element_or_eventContext === window) {
            this.eventContext = element_or_eventContext;
        } else {
            this.eventContext = document;
        }


        this.guid = J.Utility.Guid.create();

        if (J.Utility.hasMethod(event, 'getEventName')) {
            this.eventName = event.getEventName();
            this._eventUnsubscribe = (function (event, guid) {
                return function () {
                    if (arguments[0] && arguments[0] === event.remove) {
                        return;
                    }
                    event.remove.apply(event, [guid]);
                };
            }(event, this.guid));
        } else {
            this.eventName = ((window.attachEvent) ? 'on' : '') + event;
            this._eventUnsubscribe = function () {
            };
        }

        this.jEventIdem = '';
        this.method = method;
        this.context = context;
        this.executeCount = executeCount || 0;
        this.isBound = true;

        this.listenerHandler = (function ($this) {
            return function () {
                var remove = --$this.executeCount === 0,
                    idemId,
                    $arguments = [],
                    $jEvent,
                    i = 0,
                    element;

                if ($this.isBound === false) {
                    return;
                }

                for (i = 0; i < arguments.length; i += 1) {
                    $arguments[$arguments.length] = arguments[i];
                }

                if (!$arguments[0]) {
                    $arguments[0] = window.event;
                }

                if ($arguments[0].target) {
                    element = $arguments[0].target;
                } else if ($arguments[0].srcElement) {
                    element = $arguments[0].srcElement;
                }

                if (element && element.nodeType === 3) {
                    element = element.parentNode;
                }

                if ($this.eventContext !== element) {
                    return false;
                }

                if (remove) {
                    $this.isBound = false;
                }

                try {
                    if ($arguments[0] && $arguments[0].hasOwnProperty('jEvent')) {
                        $jEvent = $arguments[0].jEvent;
                        idemId = $jEvent.idemId;
                        if ($this.jEventIdem === idemId) {
                            $jEvent.data = null;
                        } else {
                            $this.jEventIdem = idemId;
                        }

                        if (J.Utility.isArray($jEvent.data)) {
                            for (i = 0; i < $jEvent.data.length; i += 1) {
                                $arguments[$arguments.length] = $jEvent.data[i];
                            }
                        } else {
                            $arguments[$arguments.length] = $jEvent.data;
                        }
                    }
                    $this.method.apply($this.context, $arguments);
                } catch (ignored) {
                }

                if (remove) {
                    $this.remove();
                }
            };
        }(this));

        if (window.addEventListener) { /*ignore-this-jslint-error*/
            this.eventContext.addEventListener(this.eventName, this.listenerHandler, false);
            /*ignore-this-jslint-error*/
        } else if (window.attachEvent) {
            if (J.Utility.hasMethod(event, 'getEventName')) {
                var $eventName = this.eventName,
                    $listenerHandler = J.Utility.cloneFunction(this.listenerHandler);
                this.eventName = 'onpropertychange';
                this.listenerHandler = function (event) {
                    if (event.propertyName === $eventName) {
                        $listenerHandler();
                    }
                };
                this.eventContext.documentElement.attachEvent(this.eventName, this.listenerHandler);
            } else {
                this.eventContext.attachEvent(this.eventName, this.listenerHandler);
            }
        }
    };
    J.Event.Subscription.prototype = {
        _removeEventListener:function () {
            if (window.addEventListener) { /*ignore-this-jslint-error*/
                this.eventContext.removeEventListener(this.eventName, this.listenerHandler);
            } else if (window.attachEvent) {
                this.eventContext.detachEvent(this.eventName, this.listenerHandler);
            }
        },
        getGuid:function () {
            return this.guid;
        },
        remove:function () {
            this._removeEventListener.apply(this, arguments);
            this._eventUnsubscribe.apply(this, arguments);

            delete this.listenerHandler;
            delete this.eventName;
            delete this.method;
            delete this.context;
            delete this.args;
            delete this.executeCount;
            delete this.isBound;
        },
        attach:function () {
            if (this.isBound) {
                return false;
            }

            this.isBound = true;
            return true;
        },

        detach:function () {
            if (!this.isBound) {
                return false;
            }

            this.isBound = false;
            return true;
        }
    };
}(JbUI));

//Views
(function (J) {
    J.UIControls = {};
    J.UIControls.View = JbUI.Class.create({
        init:function (name, callback) {
            this.id = J.Utility.Guid.create();
            this.name = name;
            if (J.Utility.isFunction(callback) === false) {
                callback = function () {
                    return false;
                };
            }
            this.callback = callback;
            this.data = {};
            this.element = document.getElementById(this.name);
            this.isShowing = false;
        },
        dispose:function () {
            delete this.id;
            delete this.name;
            delete this.callback;
            delete this.data;
            delete this.element;
            delete this.isShowing;
        },
        getParsedHash:function () {
            var hash = window.location.hash,
                hashPath = '';
            if (J.Utility.isEmptyString(hash) === false) {
                hashPath = decodeURIComponent(hash.split('#')[1]);
                if (J.Utility.startsWith(hashPath, this.name + '/')) {
                    hashPath = hashPath.substring(this.name.length + 1, hashPath.length);
                }
            }

            return hashPath;
        },
        dispatchCallback:function () {
            var args = [this.getParsedHash()];
            args = args.concat(arguments);
            this.callback.apply(this, args);
        },
        show:function (isRewind) {
            this.isShowing = true;
        },
        hide:function (isRewind) {
            this.isShowing = false;
        },
        getIsShowing:function () {
            return this.isShowing;
        },
        getStorageKey:function () {
            throw 'not implemented';
        },
        getName:function () {
            return this.name;
        },
        getData:function () {
            return this.data;
        },
        getElement:function () {
            return this.element;
        }
    });

    J.UIControls.Page = JbUI.TypedDictionary.extend({
        init:function () {
            this.base.init(J.UIControls.View);
            this.guid = J.Utility.Guid.create();
            this.rootName = J.UIControls.Page.getHrefRoot();
            this.loadedState = J.UIControls.Page.getHrefStateName();
            this.history = [];
            this.windowHistoryLength = window.history.length;
            this._preventViewShowDispatch = false;

            this.onViewRequest = J.UIControls.Page.Event.onViewRequest.subscribe(this.handleViewRequest, this);
            this.onHashChange = new J.Event.Subscription('hashchange', window, this.handleHashChange, this);
        },
        handleViewRequest:function (e, data) {
            if (this._processingRequest) {
                return;
            }

            if (J.Utility.isNumber(data) && data === -1) {
                this._processingRequest = true;
                history.go(-1);

                return;
            }

            if (J.Utility.isEmptyString(data) === true) {
                return;
            }

            this._processingRequest = true;

            var prevLocation = window.location.href;
            window.location.assign(data);
            if (prevLocation === window.location.href) {
                this.handleHashChange();
            }
        },
        handleHashChange:function () {
            // has popped to the base view
            if (J.UIControls.Page.getHrefStateName() === this.rootName) {
                this.handleRewindToBaseView.apply(this, arguments);
                return;
            }

            // check to see if the back button was pressed
            var matchesHistory = this.history.length > 1 && this.history[this.history.length - 2].href === window.location.href;

            // double check to make sure that the history hasn't incremented;
            if (matchesHistory && this.windowHistoryLength < window.history.length) {
                matchesHistory = false;
            }

            if (matchesHistory) {
                this.handlePopView.apply(this, arguments);
            } else {
                this.handlePushView.apply(this, arguments);
            }

            this.windowHistoryLength = window.history.length;
        },
        handlePushView:function () {
            if (this.history.length > 1 && window.location.href === this.history[this.history.length - 1].href) {
                this._processingRequest = false;
                return;
            }
            var viewToHide = (this.history.length === 0) ? null : this.getCurrentView(), // don't hide the current view if pushing the first in the stack
                viewToShow;

            this.history[this.history.length] = new J.UIControls.Page.HistoryItem(window.location.href, J.UIControls.Page.getHrefStateName());
            viewToShow = this.getCurrentView();
            if (!viewToShow) {
                return; // this will keep trying until the view matches
            }

            this.transitionTo(viewToHide, viewToShow);
        },
        handlePopView:function () {
            if (this.history.length === 1 && this.history[0].view.name === this.rootName) {
                this._processingRequest = false;
                return;
            }

            var viewToHide = this.getCurrentView();
            this.history.splice(this.history.length - 1, 1);
            this.transitionTo(viewToHide, this.getCurrentView(), true);
        },
        handleRewindToBaseView:function () {
            var viewToHide = this.getCurrentView();
            this.history.splice(1, this.history.length - 1);
            if (viewToHide === this.getCurrentView()) {
                this._processingRequest = false;
                return;
            }
            this.transitionTo(viewToHide, this.getCurrentView(), true);
        },
        transitionTo:function (from, to, isHistoryPop) {
            var empty = {
                name:null,
                hide:function () {
                },
                show:function () {
                }
            },
                i,
                args = [];

            for (i = 2; i < arguments.length; i += 1) {
                args[args.length] = arguments[i];
            }

            to = arguments[1] || empty;
            to.show.apply(to, args);

            from = arguments[0] || empty;
            from.hide.apply(from, args);

            this.setPageEventData(new J.UIControls.PageEventData(to.name, from.name, ((isHistoryPop) ? -1 : 1)));
            this.dispatchViewShow();
        },
        setPageEventData:function (pageEventData) {
            this._pageEventData = pageEventData;
        },
        getPageEventData:function () {
            return this._pageEventData || null;
        },
        preventViewShowDispatch:function () {
            if (!this._preventViewShowDispatch) {
                this._preventViewShowDispatch = true;
            }

            return this._preventViewShowDispatch;
        },
        allowViewShowDispatch:function () {
            if (!this._preventViewShowDispatch) {
                return true;
            }

            this._preventViewShowDispatch = false;
            return false;
        },
        dispatchViewShow:function () {
            if (!this.allowViewShowDispatch()) {
                return;
            }

            this._processingRequest = false;

            J.UIControls.Page.Event.onViewShow.fire(this.getPageEventData());
        },
        getLoadedState:function () {
            return this.loadedState;
        },
        getRootName:function () {
            return this.rootName;
        },
        getCurrentView:function () {
            if (this.history.length === 0) {
                return this.collection[this.rootName];
            }
            return this.collection[this.history[this.history.length - 1].view];
        },
        push:function (value) {
            if (value.name) {
                if (this.collection.hasOwnProperty(value.name)) {
                    try {
                        this.collection[value.name].dispose();
                    } catch (ignored) {
                    }
                    delete this.collection[value.name];
                }
                this.base.push(value.name, value);
                if (this.history.length === 0 && value.name === this.loadedState) {
                    this.handlePushView();
                }
            } else {
                throw 'invalid view pushed';
            }
        }
    });

    J.UIControls.Page.HistoryItem = function (href, view) {
        this.href = href;
        this.view = view;
    };

    J.UIControls.PageEventData = J.Class.create({
        init:function (toName, fromName, direction) {
            this.toName = toName;
            this.fromName = fromName;
            this.direction = direction;
        }
    });
    J.UIControls.PageEvent = J.Event.extend({
        init:function (defaultContext) {
            this.base.init(defaultContext);
        },
        trigger:function (pageEventData) {
            if (!(pageEventData instanceof J.UIControls.PageEventData)) {
                throw 'Provide valid page event data';
            }

            this.base.fire(pageEventData);
        }
    });
    J.UIControls.Page.Event = {
        onViewShow:new J.UIControls.PageEvent(),
        onViewRequest:new J.Event(this)
    };

    J.UIControls.Page.pushState = function (url) {
        var frag = url.split('#'),
            state = J.UIControls.Page.getHrefStateName(url),
            base = '';

        if (J.Dom.elementExists(document.getElementById(state))) {
            base = window.location.pathname.split('/');
            base = base[base.length - 1];

            if (base !== state && frag.length > 0) {
                url = '#' + frag[1];
            }
        }

//        Device.log('requested url: ' + url + ' | window.location.href: ' + window.location.href);

        if (JbUI.Utility.startsWith(url, '#')) {
            url = window.location.href.split('#')[0] + url;
        }

//        Device.log('corrected url: ' + url + ' | window.location.href: ' + window.location.href);

        J.UIControls.Page.Event.onViewRequest.fire(url);
    };
    J.UIControls.Page.getHrefParams = function (href) {
        var params = [];
        href = href || window.location.href;

        href = href.split('#');

        if (href.length > 1) {
            params = decodeURIComponent(href[1]).split('/');
            params.shift();
        }

        return params;
    };

    J.UIControls.Page.getHrefStateName = function (href) {
        href = href || window.location.href;

        href = href.split('#');

        if (href.length <= 1) {
            href = href[0].split('/');
            href = href[href.length - 1];
        } else {
            href = decodeURIComponent(href[1]).split('/')[0];
        }

        return href;
    };
    J.UIControls.Page.getHrefRoot = function () {
        var root = window.location.pathname.split('/');
        root = root[root.length - 1];
        return root;
    };
}(JbUI));

//Framerate
(function (J) {
    'use strict';
    J.Framerate = J.Class.extend({
        init:function (targetFPS) {
            this.setTargetFPS(targetFPS);

            this.clearTicks();
        }
    });
    J.Framerate.prototype._averageRolling = function () {
        return J.Framerate.calculateFPS(this.frameSum, this.timeSpanSum);
    };
    J.Framerate.prototype._saveTicks = function (tickCount, timeStamp) {
        tickCount = tickCount || 1;
        timeStamp = timeStamp || Date.now();

        var timeSpan = timeStamp - this.getPrevTimeStamp();

        this.frameSum -= (this.rollingFrames[this.intervalIndex] || 0);
        this.frameSum += tickCount;
        this.rollingFrames[this.intervalIndex] = tickCount;

        this.timeSpanSum -= (this.rollingTimeSpan[this.intervalIndex] || 0);
        this.timeSpanSum += timeSpan;
        this.rollingTimeSpan[this.intervalIndex] = timeSpan;
        if ((this.intervalIndex += 1) >= this.tickBufferSize) {
            this.bufferFilled = true;
            this.intervalIndex = 0;
        }

        this.setPrevTimeStamp(timeStamp);
    };
    J.Framerate.prototype.setTargetFPS = function (value) {
        this.targetFPS = value || this.getTargetFPS();
        this.tickBufferSize = Math.floor(J.Framerate.MAX_FPS / this.targetFPS);

    };
    J.Framerate.prototype.getTargetFPS = function () {
        if (!this.targetFPS) {
            this.targetFPS = J.Framerate.DEFAULT_FPS;
            this.tickBufferSize = Math.floor(J.Framerate.MAX_FPS / this.targetFPS);

        }
        return this.targetFPS;
    };
    J.Framerate.prototype.setPrevTimeStamp = function (value) {
        this.prevTimeStamp = value;
    };
    J.Framerate.prototype.getPrevTimeStamp = function () {
        if (!this.prevTimeStamp) {
            this.prevTimeStamp = Date.now();
        }
        return this.prevTimeStamp;
    };
    J.Framerate.prototype.getFPS = function () {
        return J.Framerate.calculateFPS(this.rollingFrames[this.intervalIndex - 1], this.rollingTimeSpan[this.intervalIndex - 1]);
    };
    J.Framerate.prototype.getFPSRolling = function () {
        return this._averageRolling();
    };
    J.Framerate.prototype.isValid = function () {
        return this.getFPS() >= this.getTargetFPS();
    };
    J.Framerate.prototype.isValidRolling = function () {
        return this.bufferFilled && this._averageRolling() >= this.getTargetFPS();
    };
    J.Framerate.prototype.isEmpty = function () {
        return this.rollingFrames.length === 0 && this.rollingTimeSpan.length === 0;
    };
    J.Framerate.prototype.clearTicks = function () {
        this.intervalIndex = 0;

        this.rollingFrames = [];
        this.frameSum = 0;

        this.rollingTimeSpan = [];
        this.timeSpanSum = 0;

        this.bufferFilled = false;
    };
    J.Framerate.prototype.saveTicks = function (frames) {
        this._saveTicks(frames);
    };

    J.Framerate.MAX_FPS = 1000;
    J.Framerate.DEFAULT_FPS = 10;
    J.Framerate.calculateFPMS = function (frames, startTime_or_elapsedTime, endTime) {
        startTime_or_elapsedTime = (endTime) ? (endTime - startTime_or_elapsedTime) : startTime_or_elapsedTime;
        return frames / startTime_or_elapsedTime;
    };
    J.Framerate.calculateFPS = function () {
        return J.Framerate.calculateFPMS.apply(this, arguments) * 1000;
    };


    J.FramerateRecorder = J.Class.create();
    var $JFramerateRecorderKeyPrefix = '$JFramerateRecorder-' + J.Utility.Guid.create() + '-',
        JFramerateRecorder = J.FramerateRecorder.extend({
            init:function ($windowKey) {
                this.$windowKey = $windowKey;
            }
        });
    JFramerateRecorder.prototype._getRecordInterval = function () {
        if (!this.recordInterval) {
            this.recordInterval = 1;
        }

        return this.recordInterval;
    };
    JFramerateRecorder.prototype._getFramerateInstance = function () {
        if (!this.framerateInstance) {
            this.framerateInstance = new J.Framerate();
            this.framerateInstance.setTargetFPS(parseInt(this.$windowKey.substring($JFramerateRecorderKeyPrefix.length, this.$windowKey.length), 10));
        }

        return this.framerateInstance;
    };
    JFramerateRecorder.prototype._getOnFrameDropEvent = function () {
        if (!this.Event) {
            this.Event = {};
        }
        if (!this.Event.onFramesDrop) {
            this.Event.onFramesDrop = new J.Event(this);
        }

        return this.Event.onFramesDrop;
    };
    JFramerateRecorder.prototype._getOnFrameResumeEvent = function () {
        if (!this.Event) {
            this.Event = {};
        }
        if (!this.Event.onFramesResume) {
            this.Event.onFramesResume = new J.Event(this);
        }

        return this.Event.onFramesResume;
    };
    JFramerateRecorder.prototype._clearFrameCallback = function () {
        window.clearTimeout(this._timeout);
        delete this._timeout;
    };
    JFramerateRecorder.prototype._frameCallback = function () {
        this._clearFrameCallback();

        this._getFramerateInstance().saveTicks();
        if (!this._hasDroppedFrame && !this._getFramerateInstance().isValidRolling()) {
            this._hasDroppedFrame = true;
            this._getFramerateInstance().clearTicks();
            this._getOnFrameDropEvent().fire();
        } else if (this._hasDroppedFrame && this._getFramerateInstance().isValidRolling()) {
            this._hasDroppedFrame = false;
            this._getOnFrameResumeEvent().fire();
        }

        this._timeout = window.setTimeout((function ($this) {
            return function () {
                $this._frameCallback.apply($this);
            };
        }(this)), this._getRecordInterval());
    };
    JFramerateRecorder.prototype.record = function () {
        if (this._timeout) {
            return;
        }

        this._frameCallback();
    };
    JFramerateRecorder.prototype.getFPS = function () {
        return this._getFramerateInstance().getFPS();
    };
    JFramerateRecorder.prototype.getFPSRolling = function () {
        return this._getFramerateInstance().getFPSRolling();
    };
    JFramerateRecorder.prototype.onFrameDrop = function (subscription, gcContext) {
        if (J.Utility.isFunction(subscription)) {
            return this._getOnFrameDropEvent().subscribe(subscription, gcContext);
        }
    };
    JFramerateRecorder.prototype.onFrameResume = function (subscription, gcContext) {
        if (J.Utility.isFunction(subscription)) {
            return this._getOnFrameResumeEvent().subscribe(subscription, gcContext);
        }
    };
    JFramerateRecorder.prototype.stop = function () {
        this._clearFrameCallback();
    };
    J.FramerateRecorder.get = function () {
        var key = $JFramerateRecorderKeyPrefix + (J.Framerate.DEFAULT_FPS),
            recorder;

        if (!window.hasOwnProperty(key)) {
            window[key] = new JFramerateRecorder($JFramerateRecorderKeyPrefix);
        }

        recorder = window[key];
        recorder.record();
        return recorder;
    };
}(JbUI));

// To deprecate below:

/* BEGIN INHERITANCE */

function inherit(proto) {
    function F() {
    }

    F.prototype = proto;
    return new F();
}

function extend(Child, Parent) {
    Child.prototype = inherit(Parent.prototype);
    Child.prototype.constructor = Child;
    Child.parent = Parent.prototype;
}

/* Use as follows
 **

 function BaseClass(arg) {
 this._arg = arg;
 this.non_virtual = function () {
 return 'foo'
 };
 };
 BaseClass.prototype.virtual = function() {
 return 'baseclass';
 };

 function SubClass(arg) {
 SubClass.parent.constructor.apply(this, arguments);
 };
 extend(SubClass, BaseClass);
 SubClass.prototype.virtual = function() {
 var supers = SubClass.parent.virtual.apply(this, arguments);
 return supers + ' subclass'
 };

 **
 ** END INHERITANCE
 */

var App = App || {};

log.log = function (object) {
    if (typeof( console ) !== "undefined") {
        console.log(object);
    }
};
log.warn = function (object) {
    if (typeof( console ) !== "undefined") {
        console.warn(object);
    }
};

var Lang = Lang || {};
Lang.lang = function () {
    var lookup, l, args, i;
    lookup = arguments[0].split('.');
    l = Lang;
    while (lookup.length) {
        l = l[lookup[0]];
        if (l === undefined) {
            return lookup.join('.');
        }
        lookup.shift();
    }
    if ($.isFunction(l)) {
        args = [];
        for (i = 1; i < arguments.length; i++) {
            args.push(arguments[i]);
        }
        return l.apply(this, args);
    } else {
        return l;
    }
};
// make this commonly used function a little easier to call
var lang = Lang.lang;
Lang.processTags = function () {
    var expr, i, tag, tags = $(document).find("gel\\:lang");
    for (i = 0; i < tags.length; i++) {
        tag = $(tags[i]);
        expr = tag.attr('expr');
        tag.html(lang(expr));
    }
};

var URLMaker = {};
URLMaker.web_url = function (path, params) {
    var url, param_a, p;
    url = '';
    if (Globals.domain) {
        url = Globals.protocol + '//' + Globals.domain;
    }
    if (path) {
        url += path;
    }
    if (params) {
        param_a = [];
        for (p in params) {
            if (params.hasOwnProperty(p)) {
                param_a.push(p + '=' + encodeURIComponent(params[p]));
            }
        }
        url += '?' + param_a.join('&');
    }
    return url;
};
URLMaker.versioned_url = function (path, params) {
    var url, param_a, p;
    url = '';
    if (Globals.version_prefix) {
        url = Globals.version_prefix;
    }
    if (path) {
        url += path;
    }
    if (params) {
        param_a = [];
        for (p in params) {
            if (params.hasOwnProperty(p)) {
                param_a.push(p + '=' + encodeURIComponent(params[p]));
            }
        }
        url += '?' + param_a.join('&');
    }
    return url;
};

var Storage = {};

Storage._cacheThreshold = 1024 * 1024;

Storage.supported = function () {
    try {
        return ('localStorage' in window) && (window.localStorage !== null);
    } catch (e) {
        return false;
    }
};

Storage.get = function (key) {
    var value = null;
    if (Storage.supported()) {
        value = localStorage[key];
    }
    return value;
};

Storage.set = function (key, value) {
    if (Storage.supported()) {
        localStorage[key] = value;
        return true;
    }
    return false;
};

Storage.remove = function (key) {
    if (Storage.supported()) {
        localStorage.removeItem(key);
        return true;
    }
    return false;
};

Storage.removeAll = function () {
    if (Storage.supported()) {
        localStorage.clear();
    }
};

Storage.checkCache = function () {
    if (Storage.supported()) {
        window.setTimeout(function () {
            var key, parts, purge, oldest = Date.now(), total = 0;
            for (key in localStorage) {
                if (key.substring(0, 6) === 'cache_') {
                    parts = localStorage[key].split('_');
                    total += parseInt(parts[1], 10);
                    if (parts[0] < oldest) {
                        oldest = parts[0];
                        purge = key.substring(6);
                    }
                }
            }
            log.warn('total:' + total + 'oldest:' + purge);
            if (total > Storage._cacheThreshold) {
                Storage.cacheRemove(purge);
            }
        }, 1000);
    }
};

Storage.cacheInvalidatePrefix = function (prefix) {
    if (Storage.supported()) {
        var key;
        for (key in localStorage) {
            if (key.substring(0, prefix.length) === prefix) {
                Storage.cacheInvalidate(key);
            }
        }
    }
};

Storage.cacheInvalidate = function (key) {
    if (Storage.supported()) {
        Storage.cacheRemove(key);
    }
};

Storage.cacheRemove = function (key) {
    if (Storage.supported()) {
        log.warn('removing [' + key + ']');
        Storage.remove('cache_' + key);
        Storage.remove(key);
    }
};
Storage.cache = function (key, value) {
    if (Storage.supported()) {
        Storage.set('cache_' + key, Date.now() + '_' + value.length);
        Storage.set(key, value);
        Storage.checkCache();
    }
};


// TODO (tom): move this into .Dom
var display = {};

display.default_inputs = function () {
    var promptSpan, input, str, i = 0, inputs = $(".default_text");

    $(".default_text").each(function (i) {

        $(this).attr('title', lang($(this).attr('title')));
        $(this).attr('placeholder', lang($(this).attr('title')));

        /*
         $(this).addClass('input-prompt-' + i);
         var promptSpan = $('<span class="input-prompt"/>');
         $(promptSpan).attr('id', 'input-prompt-' + i);
         $(promptSpan).append($(this).attr('title'));
         $(promptSpan).click(function(){
         $(this).hide();
         $('.' + $(this).attr('id')).focus();
         });
         if($(this).val() !== ''){
         $(promptSpan).hide();
         }
         $(this).before(promptSpan);
         $(this).focus(function(){
         $('#input-prompt-' + i).hide();
         });
         $(this).blur(function(){
         if($(this).val() === ''){
         $('#input-prompt-' + i).show();
         }
         });
         */
    });
};

display._input_check = function (input) {
    var val = input.val().replace(/^\s+|\s+$/g, '');
    /*
     if (val === "") {
     input.addClass("default_text_active");
     input.val(input.attr('title'));
     input.attr('type', 'text');
     } else if (val === input.attr('title')) {
     input.addClass("default_text_active");
     input.attr('type', 'text');
     } else {
     input.removeClass("default_text_active");
     }
     */
};

display.input_check = function () {
    /*
     var i = 0, inputs = $(".default_text");
     for (;i<inputs.length;i++) {
     display._input_check($(inputs[i]));
     }
     */
};

display._input_clear = function (input) {
    /*
     if (input.val() === input.attr('title')) {
     input.removeClass("default_text_active");
     input.val("");
     }
     */
};

display.input_clear = function () {
    /*
     var i = 0, inputs = $(".default_text");
     for (;i<inputs.length;i++) {
     display._input_clear($(inputs[i]));
     }
     */
};


display.tag = function (tag, params, contents) {
    var a, result;
    result = '<' + tag;
    for (a in params) {
        if (params.hasOwnProperty(a)) {
            result += ' ' + a + '="' + params[a] + '"';
        }
    }
    // todo - this is is incomplete. it should be possible to pass no contents and still have a tag render correctly. for instance, certain browsers do not support self certain self-closing tags.
    if (contents !== undefined && contents !== null) {
        result += '>' + contents + '</' + tag + '>';
    }
    else {
        result += '/>';
    }
    return result;
};

var TAG = display.tag;

display.edit_span = function (id, callback, blank) {
    // add an edit box to any span
    var markup = '<a href="#" id="' + id + '-edit" class="editor">edit</a>';
    markup += '<input type="text" id="' + id + '-input" class="hidden"/>';
    markup += '<a href="#" id="' + id + '-done" class="hidden editor">done</a>';
    $('#' + id).parent().append(markup);
    $('#' + id + '-edit').bind('click', function () {
        if (blank !== true) {
            $('#' + id + '-input').val($('#' + id).html());
        }
        $('#' + id).hide();
        $('#' + id + '-edit').hide();
        $('#' + id + '-input').show();
        $('#' + id + '-input').focus();
        $('#' + id + '-done').show();
        return false;
    });
    $('#' + id + '-done').bind('click', function () {
        $('#' + id + '-input').hide();
        $('#' + id + '-done').hide();
        $('#' + id).show();
        $('#' + id + '-edit').show();
        return callback($('#' + id + '-input').val(), id);
    });
};

display.edit_name = function (id, id_first, id_last, callback, blank) {
    // add an edit box to any span
    var markup = '<a href="#" id="' + id + '-edit">edit</a>';
    markup += '<input type="text" id="' + id_first + '-input" class="hidden"/>';
    markup += '<input type="text" id="' + id_last + '-input" class="hidden"/>';
    markup += '<a href="#" id="' + id + '-done" class="hidden">done</a>';
    $('#' + id).parent().append(markup);
    $('#' + id + '-edit').bind('click', function () {
        if (blank !== true) {
            $('#' + id_first + '-input').val($('#' + id_first).html());
            $('#' + id_last + '-input').val($('#' + id_last).html());
        }
        $('#' + id).hide();
        $('#' + id + '-edit').hide();
        $('#' + id_first + '-input').show();
        $('#' + id_last + '-input').show();
        $('#' + id_first + '-input').focus();
        $('#' + id + '-done').show();
        return false;
    });
    $('#' + id + '-done').bind('click', function () {
        $('#' + id_first + '-input').hide();
        $('#' + id_last + '-input').hide();
        $('#' + id + '-done').hide();
        $('#' + id).show();
        $('#' + id + '-edit').show();
        return callback($('#' + id_first + '-input').val(), $('#' + id_last + '-input').val());
    });
};

display.search_span = function (id, callback, label) {
    // add an edit box to any span
    var markup = '';
    markup += '<input type="text" id="' + id + '-input" class="hidden"/>';
    markup += '<a href="#" id="' + id + '-done" class="search-button hidden">' + label + '</a>';
    $('#' + id).parent().append(markup);
    $('#' + id).bind('click', function () {
        $('#' + id).hide();
        $('#' + id + '-edit').hide();
        $('#' + id + '-input').show();
        $('#' + id + '-input').focus();
        $('#' + id + '-done').show();
        return false;
    });
    $('#' + id + '-done').bind('click', function () {
        $('#' + id + '-input').hide();
        $('#' + id + '-done').hide();
        $('#' + id).show();
        $('#' + id + '-edit').show();
        return callback($('#' + id + '-input').val());
    });
};

display.expandiv = function (id, len) {
    var text, markup;
    // add an edit box to any span
    text = $('#' + id).html();
    if (text.length < len) {
        $('#' + id).show();
        return false;
    }
    $('#' + id).hide();
    markup = TAG('div', {'class':'expandiv less-text', 'id':id + '-lesstext'},
        text.substr(0, len) + ' ' +
            TAG('a', {'href':'#', 'id':id + '-more'}, 'more')
    );
    markup += TAG('a', {'href':'#', 'id':id + '-less', 'class':'hidden'}, 'less');
    $('#' + id).parent().append(markup);
    $('#' + id + '-more').bind('click', function () {
        $('#' + id + '-lesstext').hide();
        $('#' + id + '-more').hide();
        $('#' + id).show();
        $('#' + id + '-less').show();
        return false;
    });
    $('#' + id + '-less').bind('click', function () {
        log.log('less');
        $('#' + id).hide();
        $('#' + id + '-less').hide();
        $('#' + id + '-lesstext').show();
        $('#' + id + '-more').show();
        return false;
    });
};

display.user_image = function (user, class_name, width, height) {
    return display.tag(
        'a',
        {
            'href':user.href,
            'class':class_name
        },
        TAG('img', {
            'alt':user.name,
            'height':height,
            'width':width,
            'border':0,
            'src':User.image(user.image, 'user_' + width + 'x' + height)
        })
    );
};

display.user_link = function (user, class_name) {
    return display.tag('a', {
        'href':user.href,
        'class':class_name
    }, user.name);
};

display.user_link_short = function (user, class_name) {
    return display.tag('a', {
        'href':user.href,
        'class':class_name
    }, user.first);
};

display.format = function (text) {
    return text.replace(/\n/g, '<br/>');
};

display.facebook_feed_link = function (item) {
    if (item.type === 'link') {
        return TAG('div', {'class':'feed_link'},
            TAG('a', {'href':item.link, 'class':'left'},
                !!item.picture ? TAG('img', { 'class':'link_image', 'src':item.picture}) : ''
            ) +
                TAG('div', { },
                    TAG('a', {'href':item.link},
                        item.name) +
                        TAG('div', {}, item.description)
                )
        );
    }
    else if (item.type === 'photo') {
        return TAG('div', {'class':'feed_link'},
            TAG('a', {'href':item.link},
                TAG('img', { 'class':'link_image', 'src':item.picture})
            )
        );
    }
};

display.facebook_feed_item = function (html, item) {
    html.push('<div class="feed_story"><div class="feed_story_image">');
    if (item.from.image) {
        html.push('<img src="' + item.from.image + '" class="small_image"/>');
    } else {
        html.push('<img src="https://graph.facebook.com/' + item.from.id + '/picture?type=square" class="small_image"/>');
    }
    html.push('</div>');
    html.push('<div class="feed_story_body">');
    html.push('<a href="#">' + item.from.name + '</a> ');
    html.push(display.format(item.message));
    html.push(display.facebook_feed_link(item));
    html.push('</div><div class="clear"></div></div>');
};

display.map = function (route) {
    var encoded = encodeURIComponent(route);
    $('#map-pop').html(TAG('iframe', {'src':'/nudge/map#' + encoded, 'height':'100%', 'width':'100%', 'frameborder':'0' }, ''));
    $('#map-close').bind('click', function () {
        $('.map-contents').hide();
    });
    $('.map-contents').show();
};

display.render_map = function () {
    var route, myLatlng, myOptions, map, map_layer;
    route = decodeURIComponent(window.location.hash);
    route = route.substring(1);
    myLatlng = new google.maps.LatLng(-34.397, 150.644);
    myOptions = {
        zoom:8,
        center:myLatlng,
        mapTypeId:google.maps.MapTypeId.ROADMAP
    };
    log.log(route);
    map = new google.maps.Map(document.getElementById('map-display'), myOptions);
    map_layer = new google.maps.KmlLayer(route);
    map_layer.setMap(map);
    google.maps.event.trigger(map, 'resize');
    $('.map-contents').show();
    return false;
};


display.message_entry = function (sent_message, activity, id, entry_class) {
    return TAG('div', {'id':id, 'class':entry_class},
        display.user_image(sent_message ? activity.to : activity.from, 'ra_image') +
            lang(sent_message ? 'jsact.sent_message' : 'jsact.received_message', display.user_link(sent_message ? activity.to : activity.from, 'ra_name')) +
            TAG('div', {'class':'ra_desc'}, activity.message)
    );
};

display._zero = function (i) {
    if (i < 10) {
        i = "0" + i;
    }
    return i;
};

display._time = function (date) {
    var h = date.getHours(), m = display._zero(date.getMinutes()), s = display._zero(date.getSeconds());
    return h + ":" + m + ':' + s;
};

display.date = function (timestamp) {
    var label = null, date = new Date(), now = new Date(), time, day, year, month;
    if (timestamp < 10000000000) {
        timestamp *= 1000; // convert to milliseconds (Epoch is usually expressed in seconds, but Javascript uses Milliseconds) 
    }
    date.setTime(timestamp);

    time = display._time(date);
    month = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][date.getMonth()];
    year = date.getFullYear();
    day = date.getDate();

    if (date.getFullYear() !== now.getFullYear()) {
        return month + ' ' + day + ' ' + year;
    } else if (date.getMonth() !== now.getMonth()) {
        return month + ' ' + day;
    } else if (date.getDate() !== now.getDate()) {
        return month + ' ' + day + ' ' + time;
    } else {
        return time;
    }
};


var nav = {};
nav.fragment = null;
nav.callbacks = [];
nav.contexts = [];
nav.set_callback = function (callback) {
    nav.callbacks.push(callback);
    $(document).ready(function () {
//        window.onhashchange = nav._check;
//        nav._check();
    });
};
nav._check = function () {
    var frag, decoded, i;
    frag = window.location.hash.split('#')[1];
    decoded = decodeURIComponent(frag);
    if (decoded !== nav.fragment) {
        for (i = 0; i < nav.callbacks.length; i++) {
            nav.callbacks[i](decoded);
        }
        nav.fragment = decoded;
    }

    //setTimeout(nav._check, 500);
};
nav.go = function (frag) {
    var encoded = encodeURIComponent(frag);
    window.location.hash = encoded;
    nav._check();
    return false;
};
nav.location_parse = function () {
    var args = {}, i, pair,
        query = window.location.search.substr(1),
        vars = query.split("&");
    for (i = 0; i < vars.length; i++) {
        pair = vars[i].split("=");
        args[pair[0]] = pair[1];
    }
    return args;
};
nav.nav = function (url) {
    window.top.location = url;
    // todo: tom - make sure window.location.assign(url); won't break anything and replace
};
nav.param = function (arg) {
    return nav.location_parse()[arg];
};
nav.part = function (index) {
    var parts, url;
    url = window.location.href;
    url = url.split('#')[0];
    url = url.split('?')[0];
    parts = url.split('/');
    parts = parts.slice(3);
    return parts.slice(index)[0];
};

nav.addContext = function (context) {
    nav.contexts.push(context);
};
nav._updateData = function (method, url, data) {
    var i, context;
    url = url || '';
    data = data || {};

    for (i = 0; i < nav.contexts.length; ++i) {
        context = nav.contexts[i];
        if (context) {
            data = context(method, url, data);
        }
    }
    return data;
};

// the nav store can be used for buffered request
// depends on sha1 and JSON
nav.storekey = function (url, data) {
    return hex_sha1(url + '?' + $.param(data));
};
nav.store = function (url, data, success_callback, failure_callback, force, key) {
    var cache;
    if (JbUI.Utility.isEmptyString(key) === true) {
        log.error('using deprecated cache key');
        key = nav.storekey(url, data);
    }

    if (!force) {
        cache = Storage.get(key);
    }
    if (cache) {
        // data is cached simply return it
        log.warn('cache hit [' + url + ']');
        success_callback(JSON.parse(cache));
    }
    else {
        // data is not cached let's go get it and cache the results
        log.warn('cache miss');
        return nav.get(
            url,
            data,
            function (response) {
                if (typeof response === 'undefined' || response === null || (JbUI.Utility.isString(response) && JbUI.Utility.isEmptyString(response))) {
                    log.warn('nav.store: bad success callback');
                    if (JbUI.Utility.isFunction(failure_callback)) {
                        failure_callback();
                    }
                    return;
                }

                Storage.cache(key, JSON.stringify(response));
                success_callback(response);
            },
            failure_callback);
    }
};
nav.get = function (url, data, success_callback, failure_callback) {
    nav.ajax({
        'type':'GET',
        'url':url,
        'data':data,
        'success':success_callback,
        'error':failure_callback
    });
};

nav.post = function (url, data, success_callback, failure_callback, options) {
    options = options || {};
    if (options.addContext) {
        data = nav._updateData('post', url, data);
    }
    nav.ajax({
        'type':'POST',
        'url':url,
        'data':data,
        'success':success_callback,
        'error':failure_callback
    });
};

nav.put = function (url, data, success_callback, failure_callback) {
    nav.ajax({
        'type':'PUT',
        'url':url,
        'data':data,
        'success':success_callback,
        'error':failure_callback
    });
};

nav.del = function (url, data, success_callback, failure_callback) {
    nav.ajax({
        'type':'DELETE',
        'url':url,
        'data':data,
        'success':success_callback,
        'error':failure_callback
    });
};

nav.formjax = function (elem, success, failure) {
    var input, action, method, i, data = {}, inputs = elem.find('input'), textareas = elem.find('textarea');
    display.input_clear();
    for (i = 0; i < inputs.length; i++) {
        input = inputs[i];
        if (input.type === 'radio') {
            if (input.checked) {
                data[input.name] = input.value;
            }
        }
        else if (input.type === "checkbox") {
            if (input.checked) {
                if (data[input.name]) {
                    data[input.name] += ',' + input.value;
                }
                else {
                    data[input.name] = input.value;
                }
            }
        }
        else {
            data[input.name] = input.value;
        }
    }
    for (i = 0; i < textareas.length; i++) {
        input = textareas[i];
        data[input.name] = input.value;
    }
    action = elem.attr('action');
    method = elem.attr('method');
    display.input_check();
    if (method === "POST") {
        nav.post(action, data, success, failure);
    } else {
        nav.get(action, data, success, failure);
    }
    return false;
};

App.login_response = function (response) {
    nav.nav(Globals.redirect);
};

App.signup_response = function (response) {
    nav.nav(Globals.redirect);
};


App.ajax_error = function (e) {
    // TODO: Lou 8/16/11 - This should be the place to add a redirect to login if the response code is 401 and the
    // error_type is 'authentication_error'. But i don't think we ever expire our tokens yet so nothing to test.
    log.warn(e);
};

App.ajax_prep = function (params) {
    /*
     if (JbUI.Utility.isObject(Globals) && JbUI.Utility.isEmptyString(Globals.ajax_protocol) === false && JbUI.Utility.isEmptyString(Globals.domain) === false && params.url[0] === '/') {
     params.url = Globals.ajax_protocol + '//' + Globals.domain + params.url;
     }
     */
};

nav.ajax = function (params) {
    App.ajax_prep(params);
    if (!params.error) {
        params.error = App.ajax_error;
    }
    $.ajax(params);
};

var STORE = nav.store;
var GET = nav.get;
var POST = nav.post;
var PUT = nav.put;
var DELETE = nav.del;
var AJAX = nav.ajax;

var MenuBar = {};

MenuBar.set_count = function (selector, count) {
    var offset, style;
    if (count === 0) {
        $(selector).html('');
    }
    else if (count < 10) {
        offset = (count - 1) * (70) + 10;
        style = "background-position: 0px -" + offset + "px";
        $(selector).html('<span class="button-update" style="' + style + '"></span>');
    }
    else {
        offset = (9 * 70);
        style = "background-position: 0px -" + offset + "px";
        $(selector).html('<span class="button-update" style="' + style + '"></span>');
    }
};

MenuBar.logout = function () {
    $.getJSON('/nudge/login/logout', function (data) {
        $('#menu-logged-in').hide();
        $('#menu-logged-out').show();
    });
    return false;
};

function AssertException(message) {
    this.message = message;
}
AssertException.prototype.toString = function () {
    return 'AssertException: ' + this.message;
};

function assert(exp, message) {
    if (!exp) {
        throw new AssertException(message);
    }
}

$(document).ready(function () {
    $.ajaxSetup({ cache:false });
    Lang.processTags();
    if ($('#feedback').length !== 0) {
        $.get('/user/signin/feedback', {'https':("https:" === document.location.protocol)}, function (data) {
            $('body').append(data);
        });
    }
    display.default_inputs();
});

// jquery extensions
$.extend({
    element:function (tag, att, text) {
        var i, newTag;
        newTag = document.createElement(tag);
        for (i in att) {
            if (att.hasOwnProperty(i)) {
                $(newTag).attr(i.split("Name")[0].replace(/\_/gi, "-"), att[i]);
            }
        }
        if (text) {
            $(newTag).html(text);
        }
        return newTag;
    },
    lightbox:function (options) {

        // storage
        this.dialog = {};
        this.dialogContent = {};
        this.dialogs = {};
        this.overlay = null;
        this.contentTarget = {};
        this.closeButton = {};
        this.closeCallback = null;

        // private
        this._init = function () {
            if (!options) {
                options = {};
            }
            options.height = options.height || 400;
            options.width = options.width || 400;
            this._checkDialogs();
            this._checkOverlay();
            this._setDialog();
            this._setEventListeners();
        };
        this._setEventListeners = function () {
            var $this = this;
            // window resize
            $(window).bind('resize.lightbox_resizer', function () {
                $this._resizeHandler();
            });
            $(window).bind('resize.lightbox_positioner', function () {
                $this._dialogPositioner();
            });

            // blur close
            $(this.dialogs).click(function (e) {
                $this._blurClose(e);
            });

            // esc key close
            $(document).keyup(function (e) {
                if (e.keyCode === 27) {
                    $this.close();
                }
            });

            // close callback additions
            if (options.closeCallback) {
                this.addCloseCallback(options.closeCallback);
            }
        };
        this._checkDialogs = function () {
            var testDialogs = $('#lightbox-dialogs');
            if (testDialogs.length === 0) {
                this.dialogs = new $.element('div', {
                    id:'lightbox-dialogs'
                });
                $(this.dialogs).hide();
                $('body').append(this.dialogs);
            } else {
                this.dialogs = testDialogs;
            }
        };
        this._checkOverlay = function () {
            var testOverlay, $this, timer;
            testOverlay = $('#lightbox-overlay');
            $this = this;
            if (testOverlay.length !== 0) {
                this.overlay = testOverlay[0];
            } else {
                this.overlay = new $.element('div', {
                    id:"lightbox-overlay"
                });
                $(this.overlay).hide();
                $('body').append(this.overlay);
            }
            timer = window.setTimeout(function () {
                $this._resizeHandler();
            }, 0);
        };
        this._setDialog = function () {
            var $this, header, test, contentHeight;
            $this = this;
            this.dialog = new $.element('div', {
                className:"lightbox"
            });
            if (options.width) {
                $(this.dialog).css('width', options.width);
            }
            if (options.height) {
                $(this.dialog).css('height', options.height + "px");
            }
            if (options.theme) {
                $(this.dialog).addClass(options.theme);
            }
            header = new $.element('h2', {},
                new $.element('span', {
                        className:"title"
                    }, options.title || null
                ));
            $(this.dialog).append(header);
            this.closeButton = new $.element('a', {
                href:"#",
                className:"lightbox-close"
            }, options.closeText || 'close');
            $(this.closeButton).click(function (e) {
                e.preventDefault();
                $this.close();
            });
            $(header).append(this.closeButton);
            this.dialogContent = new $.element('div', {
                id:"lightbox-content-" + Math.floor(Math.random() * 10000000),
                className:"lightbox-content loading"
            });
            contentHeight = parseInt($(this.dialog).css("height"), 10) - 20;
            $(this.dialogContent).css("height", parseInt($(this.dialog).css("height"), 10) - 60);

            if (options.content) {
                this.update(options.content);
            }
            if (options.className) {
                $(this.dialog).addClass(options.className);
            }
            $(this.dialog).append(this.dialogContent);
            $(this.dialog).hide();
            $(this.dialogs).append(this.dialog);
            this._resizeHandler();
            this._dialogPositioner();
            test = $this._dialogPositioner;
            $(this.dialog).draggable({
                handle:'h2',
                containment:'window',
                start:function () {
                    $(window).unbind('resize.lightbox_positioner');
                }
            });
            this.open();
        };
        this._resizeHandler = function () {
            if (options.resizeCallback) {
                options.resizeCallback();
            }
            $(this.overlay).css("width", $(window).width());
            $(this.overlay).css("height", $(document).height());
            $(this.dialogs).css("width", $(window).width());
            $(this.dialogs).css("height", $(document).height());
        };
        this._dialogPositioner = function () {
            $(this.dialog).css("left", ($(window).width() / 2) - ($(this.dialog).width() / 2) + "px");
            $(this.dialog).css("top", ($(window).height() / 2) - ($(this.dialog).height() / 2) + "px");
        };
        this._blurClose = function (e) {
            if (e.srcElement === this.dialogs || e.target === this.dialogs) {
                this.close();
            }
        };

        // public
        this.update = function (str, fn) {
            if (!str) {
                return false;
            }
            $(this.dialogContent).html(str);
            $(this.dialogContent).removeClass('loading');
            if (fn) {
                fn();
            }
        };
        this.updateTitle = function (str) {
            $(this.dialog).find('.title').html(str);
        };
        this.setLoading = function () {
            $(this.dialogContent).html('');
            $(this.dialogContent).addClass('loading');
        };
        this.close = function (fn) {
            if (fn) {
                this.closeCallback = fn;
            }
            $(this.dialog).fadeOut();
            $(this.dialogs).fadeOut();
            $(this.overlay).fadeOut();
            if (this.closeCallback) {
                this.closeCallback();
            }
        };
        this.open = function (fn) {
            var $this = this;
            $(this.dialogs).find('.lightbox').each(function () {
                if ($this.dialog !== this) {
                    $(this).hide();
                }
            });
            $(this.dialog).fadeIn();
            $(this.dialogs).fadeIn(function () {
                if (fn) {
                    fn();
                }
            });
            $(this.overlay).css('filter', 'alpha(opacity=60)');
            $(this.overlay).fadeIn();
        };
        this.destroy = function () {
            $(this.dialog).remove();
            $(this.overlay).hide();
            delete this.dialog;

        };
        this.height = function (num) {
            $(this.dialog).css("height", num);
            this._dialogPositioner();
        };
        this.width = function (num) {
            $(this.dialog).css("width", num);
            this._dialogPositioner();
        };
        this.addCloseCallback = function (fn, bool) {
            if (bool) {
                this.clearCloseCallbacks();
            }
            this.closeCallback = fn;
            // $(this.closeButton).click(function(){
            //	fn();
            // });
        };
        this.clearCloseCallbacks = function () {
            var $this = this;
            $(this.closeButton).unbind('click');
            $(this.closeButton).click(function (e) {
                e.preventDefault();
                $this.close();
            });
        };
        this.updateSize = function () {
            $(this.dialogContent).css("height", "auto");
            var newContentHeight = $(this.dialogContent).height();
            this.height(newContentHeight + 60);
        };

        // init
        this._init();
        return this;
    },
    videoLightbox:function (options) {
        var
            $this = this,
            util = JbUI.Utility;
        // storage
        this._autoplay = false;
        this._height = 600;
        this._width = 800;
        this._ytObject = null;
        this._ytId = 'ytvid-' + util.Guid.create();
        this.lightbox = null;

        // private
        this._init = function () {
            if (!options.url) {
                return false;
            }
            this._autoplay = options.autoplay || false;
            this._height = options.height || this._height;
            this._width = options.width || this._width;
            this._buildLightbox();
        };
        this._buildLightbox = function () {
            var iframe = new $.element('iframe', {
                id:$this._ytId,
                height:$this._height,
                width:$this._width,
                src:'http://www.youtube.com/embed/' + options.url.split("?v=")[1] + '?enablejsapi=1&amp;showinfo=0&amp;modestbranding=1&amp;autohide=1&amp;rel=0',
                frameborder:0
            });
            this.lightbox = new $.lightbox({
                height:$this._height + 50,
                width:$this._width + 60,
                theme:'video',
                closeCallback:function () {
                    if ($this._ytObject) {
                        $this._ytObject.pauseVideo();
                    }
                }
            });
            $(iframe).load(function () {
                $.getScript('http://www.youtube.com/player_api', function () {
                    var lag = window.setTimeout(function () {
                        $this._ytObject = new YT.Player($this._ytId, {
                            events:{
                                onReady:function () {
                                    if ($this._autoplay) {
                                        $this._ytObject.playVideo();
                                    }
                                }
                            }
                        });
                    }, 100);
                });
            });
            this.lightbox.update(iframe);
        };
        this.open = function () {
            this.lightbox.open(function () {
                $this._ytObject.playVideo();
            });
        };

        this._init();
        return this;
    },
    carousel:function (options) {

        // storage
        this._isAnimating = false;
        this._animationRange = 0;
        this._animationTarget = null;
        this._itemWidth = 0;
        this.mainElement = null;

        // private
        this._init = function () {
            if (!options) {
                options = {};
            }
            this._setCarousel();
        };
        this._setCarousel = function () {
            var $this, parentTarget, listItems, itemHeight, carouselMask, targetWidth, controlList, prevArrow, nextArrow;
            if (!options.el || !options.items) {
                return false;
            }
            $this = this;
            this._animationTarget = options.el;
            this._parentTarget = options.el[0].parentNode;

            listItems = options.el.find('li');
            this._itemWidth = listItems.outerWidth(true);
            itemHeight = listItems.height();
            this._animationRange = (this._itemWidth * options.items);

            this.mainElement = new $.element('div', {
                className:'carousel'
            });
            carouselMask = new $.element('div', {
                className:'carousel-mask'
            });
            $(carouselMask).width(this._itemWidth * options.items);
            $(carouselMask).height(itemHeight);
            $(this.mainElement).append(carouselMask);
            $(carouselMask).append(options.el);
            $(this._parentTarget).append(this.mainElement);

            listItems = options.el.find('li');
            targetWidth = (listItems.width() + 10) * listItems.length;
            options.el.width(targetWidth);
            options.el.css("position", "absolute");
            options.el.css("left", "0");
            options.el.css("right", "0");
            controlList = new $.element('ul', {className:'carousel-controls'});
            prevArrow = new $.element('li', {className:'prev-arrow'}, '&laquo;');
            nextArrow = new $.element('li', {className:'next-arrow'}, '&raquo;');
            $(controlList).append(prevArrow);
            $(controlList).append(nextArrow);
            $(this.mainElement).append(controlList);
            $(prevArrow).click(function (e) {
                e.preventDefault();
                $this.moveCarousel('prev');
            });
            $(nextArrow).click(function (e) {
                e.preventDefault();
                $this.moveCarousel('next');
            });
            this._animationTarget.hide();
            this._animationTarget.css("visibility", "visible");
            this._animationTarget.fadeIn();
        };
        this._moveElements = function (mode) {
            var i;
            switch (mode) {
                case "next":
                    for (i = 0; i < options.items; i++) {
                        this._animationTarget.append(this._animationTarget.find('li')[0]);
                        this._animationTarget.css('left', parseInt(this._animationTarget.css('left'), 0) + this._itemWidth);
                    }
                    break;
                case "prev":
                    for (i = 0; i < options.items; i++) {
                        this._animationTarget.prepend(this._animationTarget.find('li')[this._animationTarget.find('li').length - 1]);
                        this._animationTarget.css('left', parseInt(this._animationTarget.css('left'), 0) - this._itemWidth);
                    }
                    break;
            }
        };

        // public
        this.moveCarousel = function (mode) {
            var $this, leftPos, callback;
            if (!this._isAnimating) {
                this.isAnimating = true;
            }
            if (this._isAnimating) {
                return false;
            }
            $this = this;
            leftPos = 0;
            callback = function () {
            };
            switch (mode) {
                case "prev":
                    $this._moveElements(mode);

                    break;
                case "next":
                    leftPos = -(this._animationRange);
                    callback = function () {
                        $this._moveElements(mode);
                    };
                    break;
            }
            $(this._animationTarget).animate({
                left:leftPos
            }, {
                duration:1000,
                easing:'swing',
                complete:function () {
                    $this.isAnimating = false;
                    callback();
                }
            });
        };

        // init
        this._init();
        return this;
    },
    tabs:function (options) {
        if (!options || !options.el) {
            return null;
        }

        // storage
        this._tabSet = null;
        this._tabs = null;
        this._panes = null;

        // private
        this._init = function () {
            if (!options) {
                options = {};
            }
            this._setTabs();
            this._setListeners();
            this._checkHash();
        };
        this._setTabs = function () {
            var leftPos = 0;
            this._tabSet = $(options.el);
            this._tabs = this._tabSet.find('dt');
            this._panes = this._tabSet.find('dd');
            this._tabs.each(function (index) {
                if (index === 0) {
                    leftPos = $(this).outerWidth();
                } else {
                    $(this).css("left", leftPos);
                    leftPos = leftPos + $(this).outerWidth();
                }
            });
            $(this._tabs[0]).addClass("active");
            $(this._panes[0]).addClass("active");
        };
        this._setListeners = function () {
            var $this = this;
            this._tabs.click(function () {
                $this._tabClickHandler(this);
            });
        };
        this._tabClickHandler = function (el) {
            el = $(el);
            if (el.hasClass('active')) {
                return false;
            }
            this.changeTab($.trim(el[0].className));

        };
        this._checkHash = function () {
            var hash = window.location.hash.split("!")[1];
            if (hash && hash.length !== 0) {
                this.changeTab(hash);
            }
        };
        this.changeTab = function (str) {
            if (typeof str !== 'string') {
                return false;
            }
            var target = null;
            this._tabs.each(function () {
                if (str === this.className) {
                    target = $(this);
                }
            });
            if (target) {
                this._tabs.removeClass("active");
                this._panes.removeClass("active");
                target.addClass("active");
                target.next("dd").addClass("active");
                window.location.hash = "!" + str;
            }
        };

        // public


        this._init();
    },
    pulse:function (options) {
        this._count = 0;
        this._init = function () {
            var count = 0;
            if (!options || !options.el) {
                return false;
            }
            if (typeof options.el === "string") {
                options.el = $(options.el);
            }
            options.times = options.times || 3;
            this._pulse();
        };
        this._pulse = function () {
            var $this = this;
            $this._count++;
            options.el.fadeTo('fast', 0.5, function () {
                options.el.fadeTo('fast', 1, function () {
                    if ($this._count < options.times) {
                        $this._pulse();
                    }
                });
            });
        };
        this._init();
        return this;
    },
    pagedialog:function (options) {
        if (!options || !options.target || !options.message) {
            return false;
        }

        // private
        this._dialog = null;
        this._closeButton = null;
        this._contentTarget = null;
        this._init = function () {
            this._buildDialog();
            this._setListeners();
            options.showCloseButton = options.showCloseButton || true;
        };
        this._buildDialog = function () {
            this._dialog = new $.element('div', {className:'conf-box'});
            this._contentTarget = new $.element('div', {className:'dialog-content'}, options.message);
            if (options.showCloseButton) {
                this._closeButton = new $.element('a', {className:'close-button', href:'#'}, lang('www_js.close'));
                this._dialog.appendChild(this._closeButton);
            }
            if (options.error) {
                $(this._dialog).addClass('error');
            }
            this._dialog.appendChild(this._contentTarget);
            $(options.target).before(this._dialog);
            this.show();
        };
        this._setListeners = function () {
            var $this = this;
            $(this._closeButton).click(function (e) {
                e.preventDefault();
                $this.close();
            });
        };

        // public
        this.close = function () {
            $(this._dialog).fadeOut(function () {
                if (options.closeCallback) {
                    options.closeCallback();
                }
            });
        };
        this.show = function (animate) {
            if (typeof animate === 'undefined') {
                animate = true;
            }
            var $thisDialog = $(this._dialog);
            if ($thisDialog.css("display") !== "block") {
                $thisDialog.fadeIn();
            } else if (animate) {
                $.pulse({
                    el:$thisDialog,
                    times:3
                });
            }
        };
        this.update = function (str, animate) {
            var $thisContentTarget;
            if (!str) {
                return false;
            }
            $thisContentTarget = $(this._contentTarget);
            $thisContentTarget.html(str);
            this.show(animate);
        };
        this.setToError = function () {
            $(this._dialog).addClass('error');
        };
        this.toggleError = function () {
            if ($(this._dialog).hasClass('error')) {
                $(this._dialog).removeClass('error');
            } else {

            }
        };
        this.addCloseCallback = function (fn, bool) {
            if (bool) {
                this.clearCloseCallbacks();
            }
            $(this._closeButton).click(function () {
                fn();
            });
        };
        this.clearCloseCallbacks = function () {
            var $this = this;
            $(this.closeButton).unbind('click');
            $(this.closeButton).click(function (e) {
                e.preventDefault();
                $this.close();
            });
        };

        this._init();
        return this;
    },
    accordion:function (options) {
        if (!options || !options.target) {
            return false;
        }

        //storage
        this._triggers = null;
        this._panes = null;

        // private
        this._init = function () {
            this._setStorage();
            this._setListeners();
        };
        this._setStorage = function () {
            this._triggers = options.target.find('dt');
            this._panes = options.target.find('dd');
        };
        this._setListeners = function () {
            var $this = this;
            this._triggers.click(function () {
                $this.doAccordion(this);
            });
        };

        // public
        this.doAccordion = function (el) {
            el = $(el);
            var $this = this;
            if (el.hasClass('active')) {
                el.next("dd").slideUp(function () {
                    el.removeClass("active");
                    $(this).removeClass("active");
                });
                return false;
            }
            el.addClass("active");
            el.next("dd").slideDown(function () {
                $(this).addClass("active");
            });
            $this._triggers.each(function () {
                if (this !== el[0]) {
                    $(this).removeClass("active");
                }
            });
            $this._panes.each(function () {
                if (this !== el.next("dd")[0]) {
                    $(this).slideUp(function () {
                        $(this).removeClass("active");
                    });
                }
            });
        };
        this.showFirst = function () {
            this.doAccordion(this._triggers[0]);
        };

        this._init();
        return this;
    }
});


















