/**
 * Copyright (c) 2009, Nathan Bubna
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 *
 * 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 no form tag is needed, and it can both get and set values for all kinds
 * of elements, not just form fields.
 *
 * Another way to look at it is as an HTML template
 * engine that is bi-directional and does not actually require a separate
 * template or syntax. Rather than replacing special placeholders, it uses
 * element attributes ("name=foo") to identify where values should be set
 * or retrieved.
 *
 * To retrieve values, simply 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 so on. For unrecognized elements, $el.text() 
 * and $el.html() are the fallbacks; 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: V.defaults
 * The available options are:
 *    keyAttr: to change the attribute containing the value key (default is 'name')
 *    nodeFilter: to specify a selector used to filter out certain descendant elements (default is undefined)
 *    copyToData: to also set values in the data() for the container element (default is true)
 *    copyToAttr: to also set values as attributes of the container element (default is false)
 *    suppressEvent: to suppress firing a custom 'valueChange' event when a value is set (default is false)
 *    useSelectValue: to set whether the returned value for select elements is
 *                    the value of the selected option instead of its text (default is false)
 *    uncheckedValue: to set the value to return when a checkbox or radio button is
 *                    unchecked.  if undefined, the element is skipped entirely when unchecked.
 *                    if true, the value is always returned, checked or unchecked. any
 *                    other setting for this value is returned itself (default is undefined).
 * 
 * The copyToData and copyToAttr settings serve as a way to ensure that any
 * set data is not entirely "lost" should there be no element with a matching 'name'
 * attribute for one or more of the keys.  I generally use them as more of a
 * debugging utility than anything else.
 *
 * 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 also works in reverse;
 * if multiple values for the same name/key are found during a "set" call,
 * and there are multiple matching elements, then the values are applied
 * to those elements in order.  If there are fewer values than elements,
 * the values are looped.  If fewer elements than values, the extra values
 * are ignored.
 *
 * Also, this plugin is extremely configurable and extensible.
 * Just tweak override the various methods and settings in the $.values
 * object to change or extend the behaviors.
 *
 * @version 1.2
 * @name values
 * @cat Plugins/Values
 * @author Nathan Bubna
 */
(function ($) {
    // expose functions and defaults for extension/configuration
    $.values = {
        defaults : {
            keyAttr: 'name',
            copyToData: false,
            copyToAttr: false,
            nodeFilter: undefined,
            suppressEvent: false,
            useSelectValue: false,
            uncheckedValue: undefined
        },
        isOptions: function(obj) {
            return (typeof obj == "object" && obj !== null &&
                    (obj.useSelectValue || obj.keyAttr || obj.copyToData ||
                     obj.nodeFilter || obj.copyToAttr || obj.suppressEvent ||
                     obj.uncheckedValue !== undefined));
        },
        setAll: function(values, options) {
            var $container = this;
            $.each(values, function(key, val) {
                V.setOne.apply($container, [key, val, options]);
            });
            if (options.copyToData) {
                this.data('values', values);
            }
        },
        setOne: function(key, value, options) {
            // may be multiple fields w/same name
            var selector = '['+options.keyAttr+'='+key+']';
            var $fields = this.find(selector);
            if (this.is(selector)) {
                $fields = $fields.add(this);
            }
            if (options.nodeFilter) {
                $fields = $fields.filter(options.nodeFilter);
            }
            if (options.copyToData) {
                this.data(key, value);
            }
            if (options.copyToAttr) {
                this.attr(key, value);
            }

            var info = V.buildSetInfo($fields, value);
            $fields.each(function() {
                V.setValue.apply(this, [info, options]);
                if (info.fieldCount > 1) {
                    info.index++;
                    if (info.values && !info.split) {
                        info.value = info.values[info.index];
                    }
                }
            });
        },
        buildSetInfo: function($fields, value) {
            var info = {
                fieldCount: $fields.size(),
                index: 0
            };
            if ($.isArray(value) && value.length > 0) {
                for (var i=0; i<value.length; i++) {
                    if (value[i] !== null) {
                        if (info.value !== undefined) {
                            if (info.fieldCount == 1) {
                                info.value = value.toString();
                            }
                            break;
                        }
                        info.value = value[i];
                    }
                }
                info.values = value;
            } else {
                info.value = value;
                if (typeof value == "string" && value.indexOf(',') >=0) {
                    info.split = true;
                    info.values = value.split(',');
                }
            }
            return info;
        },
        setValue: function(info, options) {
            var setter = V.set[this.nodeName.toLowerCase()];
            if (setter) {
                setter.apply(this, [info, options]);
            } else {
                V.set.standard.apply(this, [info, options]);
            }
            if (!options.suppressEvent) {
                $(this).trigger('valueChange', [info.key, info.value]);
            }
        },
        set: {
            input: function(info, options) {
                var type = this.type.toLowerCase();
                if (type == 'checkbox' || type == 'radio') {
                    this.checked = (this.value == info.value);
                    if (!this.checked && info.values) {
                        this.checked = false;
                        var has = this.value, vals = info.values;
                        for (var i=0,m=vals.length; i<m; i++) {
                            if (has == vals[i]) {
                                this.checked = true;
                                break;
                            }
                        }
                    }
                } else {
                    this.value = info.value;
                }
            },
            select: function(info, options) {
                var mult = (this.type != "select-one" && info.values !== undefined),
                    options = this.options,
                    useTxt = !options.useSelectValue;
                for (var i=0,m=options.length; i<m; i++) {
                    var option = options[i],
                        has = (useTxt ? $.trim(option.text) : $(option).val());
                    if (mult) {
                        option.selected = false;
                        for (var j=0,n=info.values.length; j<n; j++) {
                            if (has == info.values[j]) {
                                option.selected = true;
                                break;
                            }
                        }
                    } else {
                        option.selected = has == info.value;
                    }
                }
            },
            textarea: function(info, options) {
                this.value = info.value;
            },
            form: function(info, options) {
                $(this).attr('action', info.value);
            },
            iframe: function(info, options) {
                this.url = info.value;
            },
            img: function(info, options) {
                $(this).attr('src', info.value);
            },
            embed: function(info, options) {
                $(this).attr('src', info.value);
            },
            standard: function(info, options) {
                if (info.value == null) {
                    info.value = '';
                }
                $(this).text(info.value);
            }
        },
        getAll: function(options) {
            var selector = '['+options.keyAttr+']';
            var $fields = this.find(selector);
            if (this.is(selector)) {
                $fields = $fields.add(this);
            }
            if (options.nodeFilter) {
                $fields = $fields.filter(options.nodeFilter);
            }
            var vals = {};
            var counts = {};
            // gather the keys and drop dupes
            $fields.each(function() {
                var key = $(this).attr(options.keyAttr);
                if (key != '' && !vals[key]) {
                    vals[key] = key;
                    counts[key] = 0;
                }
            });
            // get values for each key
            for (var key in vals) {
                var got = V.getOne.apply(this, [key, options, $fields]);
                vals[key] = got.val;
                counts[key] = got.count;
            }
            vals.valuesCounts = counts;
            return vals;
        },
        getOne: function(key, options, $fields) {
            var selector = '['+options.keyAttr+'='+key+']';
            if ($fields === undefined) {
                $fields = this.find(selector);
                if (this.is(selector)) {
                    $fields = $fields.add(this);
                }
                if (options.nodeFilter) {
                    $fields = $fields.filter(options.nodeFilter);
                }
            } else {
                // assume nodeFilter was applied in getAll
                $fields = $fields.filter(selector);
            }
            var results = [];// for all
            var result;// for one
            var same = true;// decider
            $fields.each(function() {
                var val = V.getValue.apply(this, [options]);
                if (val !== undefined) {
                    if (result !== undefined && result != val) {
                        same = false;
                    }
                    result = val;
                    results.push(result);
                }
            });
            if (same) {
                return { count: results.length, val: result };
            }
            return { count: results.length, val: results };
        },
        getValue: function(options) {
            var getter = V.get[this.nodeName.toLowerCase()];
            if (getter) {
                return getter.apply(this, [options]);
            }
            return V.get.standard.apply(this, [options]);
        },
        get: {
            standard: function(options) {
                var $field = $(this), val = $field.val();
                if (val === undefined || val === null || val == '') {
                    val = $.trim($field.text());
                    if (val == '') {
                        val = $field.html();
                    }
                }
                return val;
            },
            input: function(options) {
                if (options.uncheckedValue !== true &&
                    (this.type == 'checkbox' || this.type == 'radio') && !this.checked) {
                    return options.uncheckedValue;
                }
                return this.value;
            },
            select: function(options) {
                var $field = $(this);
                var $selected = $field.find('option:selected');
                if ($selected.size() == 0) {
                    return null;
                }
                var one = $selected.size() == 1;
                var val = one ? null : [];
                $selected.each(function() {
                    var v = options.useSelectValue ? this.value : $.trim($(this).text());
                    if (one) {
                        val = v;
                    } else {
                        val.push(v);
                    }
                });
                return val;
            },
            form: function(options) {
                return $(this).attr('action');
            },
            iframe: function(options) {
                return this.url;
            },
            img: function(options) {
                return $(this).attr('src');
            },
            embed: function(options) {
                return $(this).attr('src');
            }
        }
    };
    var V = $.values;

    $.fn.values = function(first, second, third) {
        var options = $.extend({}, V.defaults);
        // grab any options in the args and then wipe that arg
        if (third) {
            options = $.extend(options, third);
        } else if (V.isOptions(second)) {
            options = $.extend(options, second);
            second = undefined;
        } else if (V.isOptions(first)) {
            options = $.extend(options, first);
            first = undefined;
        }
        // a get/set for a particular val?
        if (typeof first == "string") {
            if (second === undefined) {
                var got = V.getOne.apply(this, [first, options]);
                return got.val;
            }
            V.setOne.apply(this, [first, second, options]);
            return this;
        // setting values?
        } else if (first) {
            // if an element or selection, replace with its values()
            if (first.nodeType) {
                first = $(first);
            }
            if (first.jquery) {
                first = V.getAll.apply($(first), [options]);
            }
            V.setAll.apply(this, [first, options]);
            return this;
        }
        // just get the values
        return V.getAll.apply(this, [options]);
    };

})(jQuery);

