/**
 * Simplifies the process of getting/setting values from/for the descendants
 * of any HTML element(s).  This is a bit like form serialization except
 * that it can both get and set values for form fields, div/span elements,
 * or any such mix of nodes.  To retrieve values, do:
 *
 *      var values = $('.parent').values();
 *
 * This will look amongst the descendants of the element(s) with class 'parent'
 * to find all elements with a 'name' attribute and then smartly
 * grabs the "value" for each of those elements.  By "smartly", i mean that
 * it use $el.val() for input elements, $el.find('option:selected').text()
 * for select elements, and $el.text(), in other words, it grabs the displayed
 * values by default.  It also trims whitespace when it uses text().
 * Once each value is retrieved it is added to a hash object (think JSON) using
 * the value of the 'name' attribute as its key, and in the end, that hash 
 * object is returned, giving you easy access to the values.
 *
 * If you pass in an object to this method, like:
 *
 *      $('.parent').values({ foo: 'bar', answer: 42 });
 *
 * it will reverse the process and set those values to the elements
 * with matching names.
 * 
 * If you pass in a different HTML element(s), like:
 *
 *      $('.parent').values($('form#foo'));
 *
 * then it will grab the values from <form id="foo">'s children
 * and copy them to the descendants of the element(s) with class 'parent'.
 *
 * If you wish to set or get a single named value from amongs an HTML element(s)
 * descendants, this plugin does accept string keys to identify the single value
 * you wish to get/set.  Just do something like:
 *
 *      var foo = $('.parent').values('foo')
 *
 * to get the value with the name 'foo' from the descendants of the element(s)
 * with the class 'parent'.  To set a value, just do:
 *
 *      $('.parent').values('bar', 42);
 *
 * and any descendants whose name attribute has the value 'bar' will have their
 * value set to 42.
 *
 * All of the example method calls above will accept an options object as a
 * last argument.  Or, you can just override the defaults at: $.values.defaults
 * The available options are 'keyAttr' to change the attribute containing
 * the value key (default is 'name', for easy use with input and select elements);
 * 'useSelectValue' to set whether the returned value for select elements is
 * the value of the selected option instead of its text (default is false);
 * and 'nodeFilter' to specify a selector used to filter out certain descendant
 * elements (default is undefined).
 * 
 * NOTE: if multiple elements with the same name/key are found during a "get"
 * call and those values are not equal, then they are pushed into an array
 * which is associated with that key.  This does not, however, work in reverse;
 * if multiple elements with the same name/key are found during a "set" call,
 * they are all given the specified value.
 *
 * Also, this plugin is quite modular, so it is easy to tweak the various
 * behaviors by overriding the methods in the $.values object.
 *
 * @version 1.0
 * @name values
 * @cat Plugins/Values
 * @author Nathan Bubna
 */
(function ($) {
    $.values = {
        defaults : {
            useSelectValue: false,
            keyAttr: 'name',
            nodeFilter: undefined
        },
        isOptions: function(obj) {
            return (obj && typeof obj == "object" &&
                    (obj.useSelectValue || obj.keyAttr || obj.nodeFilter));
        },
        setAll: function(values, options) {
            var $container = this;
            $.each(values, function(key, val) {
                // may be multiple fields w/same name
                var $fields = $container.find('*['+options.keyAttr+'='+key+']');
                if (options.nodeFilter) {
                    $fields = $fields.filter(options.nodeFilter);
                }
                $fields.each(function() {
                    $.values.setValue.apply(this, [val, options]);
                });
            });
        },
        setOne: function(key, value, options) {
            var values = {};
            values[key] = value;
            $.values.setAll.apply(this, [values, options]);
        },
        setValue: function(value, options) {
            var $field = $(this);
            if ($field.is('input')) {
                $field.val(value);
            } else if ($field.is('select')) {
                if (options.useSelectValue) {
                    $field.val(value);
                } else {
                    $field.children('option').each(function() {
                        if ($.trim($(this).text()) == value) {
                            this.selected = true;
                            return false;
                        }
                    });
                }
            } else {
                $field.text(value);
            }
        },
        getAll: function(options) {
            var $fields = this.find('*['+options.keyAttr+']');
            if (options.nodeFilter) {
                $fields = $fields.filter(options.nodeFilter);
            }
            var vals = {};
            $fields.each(function() {
                var key = $(this).attr(options.keyAttr);
                var val = $.values.getValue.apply(this, [options]);
                if (vals[key] === undefined) {
                    vals[key] = val;
                } else if (vals[key] != val) {
                    if (vals[key] instanceof Array) {
                        // add the new val to the array
                        vals[key].push(val);
                    } else {
                        // turn val into array of both vals
                        vals[key] = [vals[key], val];
                    }
                }
            });
            return vals;
        },
        getOne: function(key, options) {
            var $fields = this.find('*['+options.keyAttr+'='+key+']');
            var result;
            $fields.each(function() {
                var val = $.values.getValue.apply(this, [options]);
                if (result === undefined) {
                    result = val;
                } else if (result != val) {
                    if (result instanceof Array) {
                        result.push(val);
                    } else {
                        result = [result, val];
                    }
                }
            });
            return result;
        },
        getValue: function(options) {
            var $field = $(this);
            if ($field.is('input')) {
                return $field.val();
            }
            if ($field.is('select')) {
                if (options.useSelectValue) {
                    return $field.find('option:selected').val();
                }
                return $.trim($field.find('option:selected').text());
            }
            return $.trim($field.text());
        }
    };

    $.fn.values = function(first, second, third) {
        var options = $.extend({}, $.values.defaults);
        // grab any options in the args and then wipe that arg
        if (third) {
            options = $.extend(options, third);
        } else if ($.values.isOptions(second)) {
            options = $.extend(options, second);
            second = undefined;
        } else if ($.values.isOptions(first)) {
            options = $.extend(options, first);
            first = undefined;
        }
        // a get/set for a particular val?
        if (typeof first == "string") {
            if (second === undefined) {
                return $.values.getOne.apply(this, [first, options]);
            }
            $.values.setOne.apply(this, [first, second, options]);
            return this;
        // setting values?
        } else if (first) {
            // if an element or selection, replace with its values()
            if (first.jquery || first.nodeType) {
                first = $.values.getAll.apply($(first), [options]);
            }
            $.values.setAll.apply(this, [first, options]);
            return this;
        }
        // just get the values
        return $.values.getAll.apply(this, [options]);
    };

})(jQuery);
