// Define console to avoid errors when Firebug isn't available
if(!window.console){ window.console = {log:function(){}}; }

if(!Array.prototype.filter){
    Array.prototype.filter = function( fn ){
        var out = [];
        for(var i=0, l=this.length; i<l; i++){
            if(fn(this[i])){ out.push(this[i]); }
        }
        return out;
    };
}

if(!Array.prototype.indexOf){
    //  missing in IE7 at least
    Array.prototype.indexOf = function( obj ){
        for(var i=0; i<this.length; i++){
            if(this[i]==obj){
                return i;
            }
        }
        return -1;
    };
}

// array first/last rubyisms
if(!Array.prototype.last){
    Array.prototype.last = function (){ var len = this.length; return this[len-1]; }
}

if(!Array.prototype.first){
    Array.prototype.first = function (){ return this[0]; }
}

// string capitalize like ruby
String.prototype.capitalize = function(){
    if(this.length == 0) return this;
    return this[0].toUpperCase() + this.substr(1);
};

// string titleize like ruby
String.prototype.titleize = function(){
    return $.map(this.toLowerCase().split(" "), function(s){
        return s.capitalize();
    }).join(" ");
};

// string summarize with optional elipsis (boolean)
String.prototype.summarize = function(len, elipsis){
    if(this.length == 0 || this.length <= len) return this.toString();
    return (this.slice(0, len) + (elipsis ? "..." : "")).toString();
};

// toSentence like in active support
Array.prototype.toSentence = function() {
    switch (this.length) {
    case 0:  return "";
    case 1:  return this.first().toString();
    case 2:  return this[0] + " and " + this[1];
    default: return this.slice(0, -1).join(', ') + ', and ' + this.last();
    }
};

$(document).ready(function(){

/* jquery-ui button behaviors */

    $(".ui-state-default").live("mouseover", function(){
        $(this).addClass("ui-state-hover");
    }).live("mouseout", function(){
        $(this).removeClass("ui-state-hover");
    }).live("mousedown", function(){
        $(this).addClass("ui-state-active");
        return true;
    });
    $(".ui-state-active:not(.no-toggle)").live("mouseup", function(){
        $(this).removeClass("ui-state-active");
    });

/* provide browser detection - DON'T RELY ON THIS UNLESS YOU ABSOLUTELY HAVE TO */

    CC.browser = $.browser;

    var domain = document.domain;

    // we only care about forcing external pages in a new tab/window if we're on cloudcrowd.com
    if (domain && domain.indexOf("cloudcrowd.com") > -1){
        // add rel external to links linking to external sites
        $('a[href^="http://"], a[href^="https://"]').
            not('a[href^="http://fe."]').
            attr("rel","external");
    }

    // open rel=external links in a new window
    $('a[rel="external"]').attr("target", "_blank");

    // initialize tooltips, if present
    if (typeof($().tooltip) === "function"){
        $(".tooltip").tooltip();
    }
});

(function(){

    $A = $.makeArray;

    // add some convenience methods for the string type
    $.extend(String.prototype, {
        stripHTMLEntities : function(){
            return this.replace(/</g, '&lt;').replace(/>/g, '&gt;');
        },

        interpolate : function(hash){
            out = this;
            for(n in hash){
                out = out.replace(new RegExp('#{' + n + '}', 'g'), hash[n]);
            }
            return out;
        }
    });


    // convenience methods for function type
    $.extend(Function.prototype, {

        /*
        bind arguments to a function for callback purposes.
        First argument = scope, remaining arguments get passed to callback
        */
        bind: function() {
            var __method = this, args = $A(arguments), object = args.shift();
            return function() {
                return __method.apply(object, args.concat( $A(arguments) ));
            };
        },

        /*
        Calls a function once for each array of arguments passed to it.
        Scope is intially set to the function's scoped, but is reset to any
        non-Array elements passed in.
        */
        each : function() {
            var scope = this, args;

            for(var i = 0, l = arguments.length; i < l; i++){
                args = arguments[i];
                if(args.constructor == Array){
                    this.apply(scope, args);
                }else{
                    scope = args;
                }
            }
        },

        // Returns a function that is wrapped in another function
        wrap: function(wrapper) {
            var __method = this;
            return function() {
                return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
            };
        },

        // wraps a function with pre and post functions
        wrapWithProcessing: function(pre, post){
            var self = this;
            return this.wrap(function(func){
                var args = $A(arguments);
                args.shift();

                if(pre){
                    if(pre.apply(self, args) === false ) { return; }
                }

                var res = func.apply(self, args);

                if(post){
                    return post.apply(self, [res]);
                }

                return res;
            });
        },

        curry: function() {
            var __method = this, args = $A(arguments);
            return function() {
                return __method.apply(this, args.concat($A(arguments)));
            };
        },

        delay: function() {
            var __method = this, args = $A(arguments), timeout = args.shift() * 1000;
            return window.setTimeout(function() {
                return __method.apply(__method, args);
            }, timeout);
        },

        // returns a globally unique string that refers to the name of the current function
        globalReference: function(){
            if(!this.guid){
                this.guid = CC.guid();
                CC['globalRef' + this.guid] = this.bind(this);
            }

            return 'CC.globalRef' + this.guid;
        },

        argumentNames: function() {
            var names = this.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1].
                replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '').
                replace(/\s+/g, '').split(',');
            return names.length == 1 && !names[0] ? [] : names;
        }
    });

    $.extend(Function.prototype, {
        defer : Function.prototype.delay.curry(0.001)
    });


    // Make rel="external" links that aren't specifically popups open in a new window
    $('a[rel="external"]:not(.pop")').live('click', function() {
        window.open($(this).attr('href'));
        return false;
    });

    // jQuery Extensions
    $.extend($.fn, {
        // checks to see if all questions marked required are filled out
        // accepts a jquery selection of the task form element
        areRequiredFilled : function(debug){
            var required = this.hasValidator("required");
            if (required.size() === 0){ return true; }
            return required.hasValue(debug);
        },

        // tests to make either an element or its children have values
        hasValue : function(debug){
            var foo = this.not(".hint, .noserialize");
            if (foo.size() === 0){ return false; }
            // keep things that aren't checkboxes, radios, submit buttons, and .not_dirty classed items that are empty
            // also keep checkboxes that aren't checked
            // finally, since .not_dirty is supposed to communicate that an item hasn't been touched, it's
            // safe to assume that a required item that is .not_dirty would be empty (used in cases where
            // the field is populated with some data but that data is not data the user has acted upon such
            // as the ztail 'name builder' textarea "Brand + name + gender + type")
            var found = foo.not(':radio,:checked,select:has(:selected),:submit,.noserialize,:disabled').
                filter(function(){
                    return $.trim($(this).filter(':text,textarea,input[type="hidden"]').val()) === "";
                });

            if (debug) {
                if (window.firebug){
                    firebug.d.console.cmd.log(found);
                } else if (window.console){
                    console.log(found);
                }
            }

            return found.size() + this.uncheckedRadios() === 0;
        },

        findAll : function(expr){
            return this.filter(expr).add(this.find(expr));
        },

        // for a jQuery collection returns number of unchecked radio buttons grouped on their common names
        uncheckedRadios: function(){
            var radioNames = {};
            radios = this.filter(":radio");
            $.each(radios, function(){
                var name = $(this).attr("name");
                // we want one key per name
                radioNames[name] = name;
            });

            var radioGroupsNotChecked = 0;
            $.each(radioNames, function(i){
                if ($(':radio[name="'+this+'"]:checked').size() === 0){
                    radioGroupsNotChecked++;
                }
            });

            return radioGroupsNotChecked;
        },

        // zebrastripes a table or list
        // params: visible true will only take into account visible rows (useful if you're showing/hiding things)
        zebrastripe: function(params) {
            params = $.extend( {visible: true}, params);
            var visible = "";
            var $children = this.children();
            //  since thead, tbody, tfoot are technically optional, if they're present we have to go... deeper
            if ($children.filter("thead,tbody,tfoot").size() > 0) $children = $children.children();

            params.visible ? visible = ":visible" : visible = "";
            this.addClass("stylize");
            $children.filter('tr, li').removeClass('even').removeClass('odd');
            $children.filter('tr'+visible+':even, li'+visible+':even').addClass('even');
            $children.filter('tr'+visible+':odd, li'+visible+':odd').addClass('odd');
            return this;
        },

        //  quickly applies the 'name' class to the element(s), then removes it after 'delay'
        blinkClass : function(name, delay) {
            this.addClass(name);
            (function() { this.removeClass(name); }).bind(this).delay(delay || 0.5);
        },

        // returns all the character offsets in an array for a given string pattern found in a body of text
        // for speed purposes you can pass in the text body as a second parameter to avoid querying the dom
        indexAll : function(pat, txtbody){
            var txt = txtbody || this.text();
            var chunks = txt.split(pat);
            var offsets = [];
            var offsetStart = 0;
            var offsetEnd = 0;
            $.each(chunks, function(){
                var pos = this.toString().length;
                if (offsetEnd !== 0) { offsets.push([offsetStart,offsetEnd]); pos += pat.length; }
                offsetStart += pos;
                offsetEnd = offsetStart+pat.length;
            });

            return offsets;
        },

        // returns the current selection relative to the element.  probably should be applied to body most of the time.
        getSelection : function(){
            var input = this.jquery ? this[0] : this;
            var selection;

            if (typeof(document.getSelection) != "undefined") {
                // Mozilla and other sane browsers
                selection = window.getSelection().getRangeAt(0).toString();
            } else if (typeof(document.selection) != "undefined" && typeof(document.selection.createRange) != "undefined") {
                // Internet Explorer
                // IE uses windows style newlines, so here we replace win newlines with unix newlines
                selection = document.selection.createRange().text.replace(/\r\n|\n\r|\r/g,"\n");
            }
            return selection;
        },

        // retarded method of getting the equiv of outerHTML across browsers
        outerHTML : function(){
            return this.clone().wrap("<div></div>").parent().html();
        },

        // performs intelligent appending / replacement of innerHTML.
        //  - use the html class to specify that html should be preserved
        //  - correctly accounts for IE pre tag bugs
        // !!! NOTE: in the case of pre tags, the element is REPLACED and the
        //     replaced element is returned, so any event listeners
        //     will have to be REBOUND
        smartAppend : function(data){
            // get the tag type
            var tag = $(this).attr("tagName");

            // update the value of any input (not radios or checkboxes), selects, or textareas
            var $checkable = this.filter('input[type=radio],input[type=checkbox]');

            this.filter('input,select,textarea').not($checkable).val(data);

            // mark a radio or checkbox if the value matches the data
            $checkable.each(function(key, elem) {
                if($(elem).val() == data) {
                    $(elem).attr("checked", true);
                }
            });

            // PRE tags aren't handles properly in IE unless they're explicitly replaced.
            if (tag == "PRE"){
                // obviously PRE tags and html are kinda a silly combo, but it's useful now and then
                // when HTMLifying plain text
                if (!$(this).hasClass("html")){
                    var child = (data+'').stripHTMLEntities();
                } else {
                    var child = data;
                }

                var new_elem = $(this).outerHTML().replace(/<.([A-Za-z]*[A-Za-z0-9]w*)?(?=.*>)/gi, function(w) {return w.toLowerCase();});

                // replace the middle of the tag with child
                // avoiding literal '$1' substitutions by entity-tizing
                var value = "$1" + child.replace(/\$1/, '&#36;1') + "</pre>";
                new_elem = new_elem.replace(/\/(>$)|<\/pre>/, value);
                new_elem = $(new_elem).addClass("ie");

                this.replaceWith(new_elem);

                return (new_elem);
            // we've already handled inputs and selects
            } else if (this.filter('input,select').length === 0) {
                if (!$(this).hasClass("html")){
                    var child = $(document.createTextNode(data));
                    this.not('input,select').append(child);
                } else {
                    var child = data;
                    this.not('input,select').html(child);
                }
            }

            return this;
        },

        // same thing as smartAppend but clears the existing innerHTML
        smartReplace: function(data){
            this[0].innerHTML = "";
            return $(this).smartAppend(data);
        },

        // enables checkboxes and radios
        // flag:  (optional, assumed true) disables when false
        enable: function(flag) {
            if (arguments.length == 0)  { flag = true; }
            if (flag) {
                return $(this).attr('disabled', false).removeClass('disabled');
            }
            else {
                this.disable();
            }
        },

        // disables checkboxes and radios
        disable: function() {
            return $(this).attr('disabled', true).addClass('disabled');
        },

        // takes a form and performs serialization into JSON
        // - supports basic nested structures
        // - noserialize class on an element will cause it to be omitted
        // Dan: Added optional parameter 'childElem' which is essentially a jQuery selector
        //      filter that can be used to target specific child elements in the targeted form.
        serializeForm : function(childElem){

            // NOTE This will break if there is an element in the form with
            // name="_add" because it will overwrite the _add method
            var obj = {};

            function addToObj(name, value) {

                if (value === null) { return; }
                if ($.isArray(obj[name])) {
                    // Add element to existing array
                    obj[name].push(value);
                } else if (obj[name] !== null) {
                    // Convert existing string to array
                    // and add element to array
                    obj[name] = [obj[name]].concat(value);
                } else {
                    // No existing element
                    // Just add element as string
                    obj[name] = value;
                }
            }

            if (typeof(childElem) === "string"){
                target = this.find(childElem);
            } else { target = this; }

            // Get all elements in form
            var elems = target.map(function(){
                return this.elements ? $.makeArray(this.elements) : this;
            }).
                add(target).
                filter(function(){
                    return this.name &&
                        ((/select|textarea/i).test(this.nodeName) ||
                         (/radio|checkbox|text|hidden|password|search/i).test(this.type));
                }).not(".noserialize,:disabled");

            // Construct an object with a key for every element name in the form
            // While some inputs will have a unique name (like type="text"),
            // others will have a shared name (like type="radio")
            // This creates a placeholder for elements which have no selected value
            elems.each(function(index) {
                if (this.name === '_method') { return; }
                obj[this.name] = null;
            });

            // Add value of each element to the form
            elems.not(":hidden").each(function(index) {
                var val = $(this).val();
                if (/radio|checkbox/i.test(this.type)) {
                    // Only add radio/checkboxes when selected
                    if (this.checked) {
                        addToObj(this.name, val);
                    }
                } else {
                    addToObj(this.name, val);
                }
            });

             // Check for and assign hidden inputs if user-entered data does not exist
            elems.filter(":hidden").each(function(index) {
                var add = false;
                if (/radio|checkbox/i.test(this.type)) {
                    // Only add radio/checkboxes when selected
                    add = this.checked === true;
                } else {
                    add = true;
                }
                if (this.name == '_method') { return; }
                if(obj[this.name] === null && add){ addToObj(this.name, this.value); }
            });

            // filter out input sets that never got assigned (like radio button sets that weren't chosen)
            $.each(obj, function(key){
                if (!obj[key]) { delete obj[key]; }
            });

            // converts your objects stuff[named][like][1][such]='and such' to {stuff:{named:{like:[{such:'and such'}]}}}
            var explode = function(obj) {
                if(obj === null) return null;
                var exploded = {};
                $.each(obj, function(key, value){
                    if(typeof(value)==="object") value = explode(value);
                    if(typeof(key) !== "string"){ exploded = obj; return false; }
                    var key_segments = key.replace(/\]$/,'').split(/[\[\]]+/);
                    var base = exploded;
                    while(key_segments.length > 0){
                        var key_segment = key_segments.shift();
                        if(key_segment.match(/^\d+$/)) key_segment = parseInt(key_segment,10)-1;
                        if(key_segments.length === 0){
                            base[key_segment] = value;
                        }
                        else {
                            if(typeof base[key_segment]==="undefined"){
                                base[key_segment] = (key_segments[0].match(/^\d+$/)) ? [] : {};
                            }
                            base = base[key_segment];
                        }
                    }
                });
                return exploded;
            };

            return explode(obj);
        },

        // empties a form or element with form element children
        emptyForm : function(){
            $self = $(this);
            $self.find("input:checked").removeAttr("checked");
            $self.find("input:text,textarea").val("");
            $self.find("textarea").empty();
            $self.find("select option:selected").removeAttr("selected");

            return $self;
        },

        // copies data into a form intelligently
        // - handles all standard form types correctly
        deserializeForm : function( data ){
            $self = $(this);
            for(n in data){
                var dataType = typeof(data[n]);
                // build nested name structure if hierarchy is present
                if (dataType === "object" && data[n] !== window && data[n] !== null){
                    $.each(data[n], function(i,v){
                        var vType = typeof(v);
                        if (vType !== "object" || v === null){
                            deserializeError("null or non-Object, n = " + n + ", i = " + i, v);
                            return true;
                        }
                        $.each(v, function(key,value){
                            var keyType = typeof(key);
                            if(keyType !== "string" && keyType !== "number"){
                                deserializeError("invalid key", key);
                                return true;
                            }
                            $self.find('[name="'+n+'['+(parseInt(i,10)+1)+'][' + key + ']"]').setFormFieldValue(value);
                        });
                    });
                } else if (dataType === "number" || dataType === "string"){
                    $self.find('[name="' + n + '"]').setFormFieldValue( data[n] );
                } else {
                    deserializeError("unknown condition", n);
                    continue;
                }
            }

            return $self;

            function deserializeError(msg, data){
                console.log("deserializeForm:", msg, data);
            }
        },

        // sets a text field value
        setFormTextValue : function ( value ){
            if (this.hasClass("html")){
                this.html(value);
            } else {
                // TODO:needs more investigation
                //this.text(value);
                //this.val(value);
                try { this.text(value); } catch (e) { this.val(value); }
            }
        },

        // sets non-text form fields with a value
        setFormFieldValue : function( value ){
            // directly set the value of textareas and inputs
            this.filter('textarea,input:not(:checkbox):not(:radio):not(:submit)').val(value);
            // set 'checked' on radios & checkboxes, uncheck all others
            this.filter(':radio, :checkbox').
                filter(function(){return $(this).val()===value;}).
                attr('checked', true).
                end().
                filter(function(){return $(this).val()!==value;}).
                attr('checked', false);

            // set 'selected' on select box values, unset it on others
            this.filter('select').find('option').
                filter(function(){return $(this).val()===value;}).
                attr('selected', true).
                end().
                filter(function(){return $(this).val()!==value;}).
                attr('selected', false);
        }
    });

    var foo;
    $.extend($, {
        // JavaScript Method Overloading
        addMethod : function(object, name, fn){
            var    old = object[name];
            object[name] = function(){
                if(fn.length == arguments.length){
                    return fn.apply(this, arguments);
                }else if(typeof old == 'function'){
                    return old.apply(this, arguments);
                }
            };
        },

        /*
        Pass an array of functions to createDelegator.  You will get a function, that when called, will
        call the appropriate function based on either:

        - argument names matching a hash passed as the sole argument
        - matching number of arguments
        */

        /*
        TODO: this currently strips all optional arguments from the function list and delgates based on only the
              required arguments.  Although this works for all current use cases, it should ideally store a list
              of all functions with the same required arguments and then match the correct delegate based on all
              optional arguments in the call matching those in the optional list of the delegate function.
        */

        /*
         FIXME:

         Currently the createDelegator code is seriously questionable:
             /proj/cc/common/javascripts/global.js(565): lint warning: unreachable code
             /proj/cc/common/javascripts/global.js(593): lint warning: multiple statements separated by commas (use semicolons?)

         This is true because createDelegator: (function(){ ... }) immediately calls
         "return function(scope, fns){ ... }".  None of the subsequent calls get executed,
         right?  Or does it?


         A grep yields only a few places it's referenced.  Perhaps we can just nuke the thing altogether?

           ./common/javascripts/global.js:        Pass an array of functions to createDelegator.  You will get a function, that when called, will
           ./common/javascripts/global.js:        createDelegator: (function(){
           ./common/javascripts/klass.js:                prop[name] = $.createDelegator(prop[name]);
           ./common/javascripts/templates.js:            add : $.createDelegator([

         */
        createDelegator: (function(){

            return function( scope, fns ){
                if(!fns){
                    fns   = scope;
                    scope = window;
                }
                var obj = {};
                $.each(fns, function(o, o){
                    var names, hash, filtered;
                    obj[':' + (filtered = (names = o.argumentNames()).slice(0).sort().filter(remove$s)).join(' :')] = hash = {
                        fn   : o,
                        args : names
                    };
                    obj[filtered.length] = obj[filtered.length] || hash;
                });
                return delegateFunctionCall.bind(this, scope, obj);
            };

            // strip optional arguments from the hash list
            function remove$s( v ){
                return v.substr(0, 1) != '$';
            }

            function delegateFunctionCall( scope, fns, keys ){
                var args, fn, bySignature;

                if (bySignature = (arguments.length == 3 && keys.constructor == Object)){
                    args = [], fn;
                    $.each(keys, function(k, v){
                        args.push(k);
                    });

                    args.sort();
                } else {
                    args = $A(arguments).slice(2);
                }

                /*
                Call the corect function by
                - signature
                   - sort the arguments into the order that the function expects them to be in, and call.
                - # of arguments
                - return false
                */
                return (fn = (bySignature && fns[':' + args.filter(remove$s).join(' :')]) || fns[args.length]) ?
                    fn.fn.apply(scope, bySignature ? $.map(fn.args, function(n){ return keys[n]; }) : args) :
                    false;
            }
        })(),

        /*
        Safely get any object path that may or may not exist.  Recursively creates objects if needed.
        Optionally call functions, and apply recursively until we run out of arguments.

        Arguments: root, [path, arguments], ...
        */
        safe : function(A,J,H){
            for(var F=A,G=1,I=arguments,D=I.length; G<D; G+=2){
                J=arguments[G].split(".");
                for(var E=0,B=J.length,C; E<B && (C=F) && (F=F[J[E]]); E++){ continue; }
                if(F && I[G+1] && F.constructor===Function){
                    F=F.apply(C, [].concat(I[G+1]));
                }
            }
            return F;
        },

        // sort multiple arrays based on the sorting of the first
        multiSort: function(nArrays, sortFunction){
            var gather = [];

            function defSort(a, b){
                var aL = a.toLowerCase(), bL = b.toLowerCase();
                return aL<bL?-1:aL>bL?1:0;
            }

            //Sort leading array only.
            nArrays[0].sort(
                function(a,b){
                    var copyA = a + "";
                    var copyB = b + "";
                    return gather[++gather.length - 1] = (sortFunction || defSort)(copyA, copyB);
                }
            );

            //Reorder the other arrays:
            for(var i=1, l=nArrays.length; i<l; i++){
                var iterator = 0;
                nArrays[i].sort(
                    function(a,b){
                        return gather[ iterator++ ];
                    }
                );
            }
            return nArrays;
        },

        // taken straight from jQuery, with the exception of changing != to !==
        map: function( elems, callback ) {
            var ret = [];

            // Go through the array, translating each of the items to their
            // new value (or values).
            for ( var i = 0, length = elems.length; i < length; i++ ) {
                var value = callback( elems[ i ], i );

                if ( value !== null )
                    ret[ ret.length ] = value;
            }

            return ret.concat.apply( [], ret );
        },

        debugs: [],
        debug: function(s) {
            //  FIRST: put a <pre id="debug" /> somewhere
            $.debugs.push((new Date()).getTime() + ': ' + s);
            $('#debug').html($.debugs.join('<br /><hr />'));
        },

        returnTrue  : function(){ return true;  },
        returnFalse : function(){ return false; }
    });


    // jQuery selector extensions
    $.extend($.expr[':'], {
        // jQuery's :empty respects textNodes as being 'full' but that's almost never really what we want
        // so this :blank lets us know if it just has no text in it
        blank : function(elem){
            return elem.innerHTML === "";
        },

        /* Dear Mark: You are a fucking asshole. Go fuck yourself. */
        data : function(elem, i, match) {
            //var matches = /^\s*((?:[\w_-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*$/.exec(match[3]);
            var matches = match[3].match(/^\s*((?:[\w_-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*$/);
            var pieces  = $(matches).filter( function(i){ return i>0 && i!=3 && !!this;} );

            var type  = pieces[1];
            var check = pieces[2];
            var data  = $.data(elem, pieces[0]);
            var value = data + '';

            return data ===  null ?
                type  === "!=" :
                type  === "=" ?
                value === check :
                type  === "*=" ?
                value.indexOf(check) >= 0 :
                type  === "~=" ?
                (" " + value + " ").indexOf(check) >= 0 :
                !check ?
                value && data !== false :
                type  === "!=" ?
                value  != check :
                type  === "^=" ?
                value.indexOf(check) === 0 :
                type  === "$=" ?
                value.substr(value.length - check.length) === check :
                type  === "|=" ?
                value === check || value.substr(0, check.length + 1) === check + "-" :
                false;
        }
    });

    // add regular expression escaper function
    // returns a string safe for use in a regex
    regexEscape = function (text) {return text.replace(/(\/|\.|\*|\+|\?|\||\(|\)|\[|\]|\{|\}|\\)/g, "\\$1");};

    // CloudCrowd Support Functions
    window.CC = window.CC || {};
    $.extend(window.CC, {
        __guid    : 0,
        guid      : function(){ return ++CC.__guid; },

        isUndefined: function(v) {
            // stolen from underscore.js
            return v === void(0);
        },

        dataStore : {

            addToCache : function(obj){
                $.extend(CC.dataStore, obj);
            },

            getData : function(name, callback){
                var data;
                if(data = CC.dataStore[name]){
                    callback(data);
                    return;
                }

                // data can be automatically retreived via URL
                if(name.indexOf('/')===0){
                    $.get(name , {}, callback, 'json');
                }
            }
        },

        topmostWindow: function(filter) {
            // by default, we confirm that CC.logForComponent exists
            filter = filter || function(w) { !! (w['CC'] && w.CC.logForComponent); }

            var w = window, safe;
            while (true) {
                safe = null;

                try { safe = w.opener && w.opener.document && filter(w.opener); } catch(e) { }
                if (safe) {
                    // no cross-domain constraints on the window that .open'ed me
                    w = w.opener;
                    continue;
                }

                if (w == w.parent) {
                    // you've reached the top
                    break;
                }

                try { safe = w.parent.document && filter(w.opener); } catch(e) { }
                if (safe) {
                    // no cross-domain constraints on my parent window
                    w = w.parent;
                    continue;
                }

                // everyone's constrained ... i'm the best you can do
                break;
            }
            return w;
        },

        logForComponent: function(component, args) {
            args = Array.prototype.slice.call(args);
            args.unshift(component);
            try {
                // 1st line of this file should ensure that console exists, but be careful anyway
                var console = window['console'];
                if (console) {
                    console.log.apply(console, args);
                }
            } catch (e) { }
        },

        loadFile : function(file, callback){
            var ext = file.split('.').pop().toLowerCase();
            switch(ext){
                case 'js':
                    $.getScript(file, callback);
                    break;
                case 'css':
                    $("<link>").attr({
                        "rel"   : "stylesheet",
                        "type"  : "text/css",
                        "href"  : file,
                        "media" : "screen"
                    }).appendTo('head');
                    break;
                default: break;
            }
        },

        redirect: function(url, delay) {
            if (/^\//.test(url)) {
                var win = CC.realWindow();
                url = win.location.protocol + '//' +
                    win.location.host + url;
            }
            if (delay > 0) {
                setTimeout(function() {
                    window.location.href = url;
                }, delay);
            } else {
                window.location.href = url;
            }
        },

        /* in a bootstrap situation window.location will behave irregularly
           so we need to make sure to reference the owner window instead.
           if you're in a situation where bootstrap might happen, use this instead of
           vanilla 'window' */
        realWindow: function(){
            var location = window.location.toString();
            if (location.indexOf("javascript:") === 0 || location.indexOf("about:") === 0){
                return (window.opener || parent.window);
            } else { return window; }
        },

        realBaseURL: function(){
            var w = CC.realWindow();
            return w.location.protocol.replace(/:.*$/,'') + '://' + w.location.host;
        },

        toParamHash: function(query){
            var hash = {};
            if (query) {
                $.each(query.split('&'), function(q, q){
                    if (! q)  { return; }
                    var s = q.split('=');
                    hash[s[0]] = s[1];
                });
            }
            return hash;
        },

        generateUUID : function(){
            // http://www.ietf.org/rfc/rfc4122.txt
            var s = [];
            var hexDigits = "0123456789ABCDEF";
            for (var i = 0; i < 32; i++) {
                s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
            }
            s[12] = "4";  // bits 12-15 of the time_hi_and_version field to 0010
            s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1);  // bits 6-7 of the clock_seq_hi_and_reserved to 01

            var uuid = s.join("");
            return uuid;
        },

        //  derive a nonce / cache-buster from our current <script /> tags
        pageNonce: function() {
            //  there should be at least one, and any one will do
            var nonce = null, nonces = $.map($('script'), function(script) {
                var $script = $(script);
                var src = $script.attr('src'), nonce = null;
                if (src) {
                    var match = src.match(/^\/javascripts\/.+?[?](\d+)$/);
                    nonce = (match ? match[1] : null);
                }
                return nonce;
            });
            while ((! (nonce = nonces.shift())) && nonces.length);
            return nonce;
        },

        addMoney: function(a, b) {
            return Math.round((parseFloat(a) + parseFloat(b)) * 100)/100;
        },

        encodeHTML : function(val){
            return $('<div/>').text(val).html();
        },

        scriptNode: function(js, doc, elem) {
            doc = doc || document;
            var n = doc.createElement('script'), s;
            if (elem && (s = elem.getAttribute('charset'))) {
                n.setAttribute('charset', s);
            }
            if (js) {
                if ($.support.scriptEval) {
                    n.appendChild(doc.createTextNode(js));
                }
                else {
                    n.text = js;
                }
            }
            return n;
        },

        // Remove any leading symbol
        // Remove leading zeros when value < 1
        // Remove decimals beyond two
        // Remove trailing decimals when value is .00

        // Expected output:
        //  0       =>  $0.00
        //  1       =>  $1.00
        //  0.5     =>  $0.50
        //  0.01    =>  $0.01
        //  0.022   =>  $0.022
        //  0.33333 =>  $0.333
        // -0.5     => -$0.50
        formatMoney: function(str, symbol, precision) {
            if (CC.isUndefined(precision)) {
                precision = 2;
            }

            var sanitized = (str + '').replace(/[^0-9.\-]/g, '');
            var amount = parseFloat(sanitized || 0);
            if (isNaN(amount)) {
                return '';
            }
            var neg = amount < 0;

            amount = Math.abs(amount).toFixed(precision);
            if (amount.indexOf('.') === 0) {
                // someone needs a leading zero
                amount = '0' + amount;
            }

            // remove trailing zeros exceeding 2-decimal precision
            var trailZero = (precision - 2);
            while ((trailZero > 0) && amount.match(/0$/)) {
                amount = amount.substring(0, amount.length - 1);
                trailZero -= 1;
            }

            amount = (symbol || '').toString() + amount;
            if (neg) {
                amount = '-' + amount;
            }

            return amount;
        },

        // Check to see if third-party cookies are enabled
        // Third party cookies are required to track a user's session in Facebook.
        // Erase cookie if check successful
        checkCookies: function() {
            return;
            /* TODO: FIX THIS!  Doesn't work at all.  /cookie_off doesn't exist, throws safari4 into an infinite loop

            document.cookie = 'cc-testcookie=true;';
            if (document.cookie.search('cc-testcookie') == -1) {
                //Cookies not enabled, immediately disable and hide tabs on top
                $('li > *').click(function() { return false; });
                $('#cc-tabs').hide();
                window.location.href = '/cookie_off';
            }
            date = new Date();
            date.setTime(date.getTime() - 1000);
            document.cookie = 'cc-testcookie=true; expires=' + date.toGMTString();
            */
        },

        hashKeys: function(obj) {
            var keys = [], key;
            for (key in obj) {
                if (obj.hasOwnProperty(key))  { keys.push(key); }
            }
            return keys;
        },
        hashValues: function(obj, unique) {
            unique = unique || false;

            var vals = [], key;
            for (key in obj) {
                var val = obj[key];
                if (obj.hasOwnProperty(key) && ! (unique && (vals.indexOf(val) != -1))) {
                    vals.push(val);
                }
            }
            return vals;
        },
        hashLength: function(obj) {
            return CC.hashKeys(obj).length;
        },

        // removes a child from an array by name
        deleteArrayChild: function(arr, child){
            var i = $.inArray(child, arr);
            arr.splice(i,1);
            return arr;
        },

        // an empty namespace for the Project UI
        //   (i suspect because some of our older code assumes it's always there)
        //   the Project UI comes from:  project.core.js + project.js
        project: {},

        // this is actually a *Hash* of the current window's querystring.
        //   we populate it at end-of-file.
        // yes, you'd think this would be obsolete because of jQuery.query
        //   however, it's used in some of our 'site' / ModX Javascript code
        //   which does not include jQuery.query
        //   so we still need to provide this until we have broad support
        queryString: {}
    });
})();

// not using realWindow here because it could be confusing if we got our parent's params
CC.queryString = CC.toParamHash(window.location.search.substr(1));

