// Backbone.Syphon, v0.4.1
// Copyright (c)2012 Derick Bailey, Muted Solutions, LLC.
// Distributed under MIT license
// http://github.com/derickbailey/backbone.syphon
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['underscore', "jquery", "backbone"], factory);
    }
}(this, function (_, jQuery, Backbone) {
    Backbone.Syphon = (function (Backbone, $, _) {
        var Syphon = {};

        // Ignore Element Types
        // --------------------

        // Tell Syphon to ignore all elements of these types. You can
        // push new types to ignore directly in to this array.
        Syphon.ignoredTypes = ["button", "submit", "reset", "fieldset"];

        // Syphon
        // ------

        // Get a JSON object that represents
        // all of the form inputs, in this view.
        // Alternately, pass a form element directly
        // in place of the view.
        Syphon.serialize = function (view, options) {
            var data = {};

            // Build the configuration
            var config = buildConfig(options);

            // Get all of the elements to process
            var elements = getInputElements(view, config);

            // Process all of the elements
            _.each(elements, function (el) {
                var $el = $(el);
                var type = getElementType($el);

                // Get the key for the input
                var keyExtractor = config.keyExtractors.get(type);
                var key = keyExtractor($el);

                // Get the value for the input
                var inputReader = config.inputReaders.get(type);
                var value = inputReader($el);

                // Get the key assignment validator and make sure
                // it's valid before assigning the value to the key
                var validKeyAssignment = config.keyAssignmentValidators.get(type);
                if (validKeyAssignment($el, key, value)) {
                    var keychain = config.keySplitter(key);
                    data = assignKeyValue(data, keychain, value);
                }
            });

            // Done; send back the results.
            return data;
        };

        // Use the given JSON object to populate
        // all of the form inputs, in this view.
        // Alternately, pass a form element directly
        // in place of the view.
        Syphon.deserialize = function (view, data, options) {
            // Build the configuration
            var config = buildConfig(options);

            // Get all of the elements to process
            var elements = getInputElements(view, config);

            // Flatten the data structure that we are deserializing
            var flattenedData = flattenData(config, data);

            // Process all of the elements
            _.each(elements, function (el) {
                var $el = $(el);
                var type = getElementType($el);

                // Get the key for the input
                var keyExtractor = config.keyExtractors.get(type);
                var key = keyExtractor($el);

                // Get the input writer and the value to write
                var inputWriter = config.inputWriters.get(type);
                var value = flattenedData[key];

                // Write the value to the input
                inputWriter($el, value);
            });
        };

        // Helpers
        // -------

        // Retrieve all of the form inputs
        // from the form
        var getInputElements = function (view, config) {
            var form = getForm(view);
            var elements = form.elements;

            elements = _.reject(elements, function (el) {
                var reject;
                var type = getElementType(el);
                var extractor = config.keyExtractors.get(type);
                var identifier = extractor($(el));

                var foundInIgnored = _.include(config.ignoredTypes, type);
                var foundInInclude = _.include(config.include, identifier);
                var foundInExclude = _.include(config.exclude, identifier);

                if (foundInInclude) {
                    reject = false;
                } else {
                    if (config.include) {
                        reject = true;
                    } else {
                        reject = (foundInExclude || foundInIgnored);
                    }
                }

                return reject;
            });

            return elements;
        };

        // Determine what type of element this is. It
        // will either return the `type` attribute of
        // an `<input>` element, or the `tagName` of
        // the element when the element is not an `<input>`.
        var getElementType = function (el) {
            var typeAttr;
            var $el = $(el);
            var tagName = $el[0].tagName;
            var type = tagName;

            if (tagName.toLowerCase() === "input") {
                typeAttr = $el.attr("type");
                if (typeAttr) {
                    type = typeAttr;
                } else {
                    type = "text";
                }
            }

            // Always return the type as lowercase
            // so it can be matched to lowercase
            // type registrations.
            return type.toLowerCase();
        };

        // If a form element is given, just return it.
        // Otherwise, get the form element from the view.
        var getForm = function (viewOrForm) {
            if (_.isUndefined(viewOrForm.$el) && viewOrForm.tagName.toLowerCase() === 'form') {
                return viewOrForm;
            } else {
                return viewOrForm.$el.is("form") ? viewOrForm.el : viewOrForm.$("form")[0];
            }
        };

        // Build a configuration object and initialize
        // default values.
        var buildConfig = function (options) {
            var config = _.clone(options) || {};

            config.ignoredTypes = _.clone(Syphon.ignoredTypes);
            config.inputReaders = config.inputReaders || Syphon.InputReaders;
            config.inputWriters = config.inputWriters || Syphon.InputWriters;
            config.keyExtractors = config.keyExtractors || Syphon.KeyExtractors;
            config.keySplitter = config.keySplitter || Syphon.KeySplitter;
            config.keyJoiner = config.keyJoiner || Syphon.KeyJoiner;
            config.keyAssignmentValidators = config.keyAssignmentValidators || Syphon.KeyAssignmentValidators;

            return config;
        };

        // Assigns `value` to a parsed JSON key.
        //
        // The first parameter is the object which will be
        // modified to store the key/value pair.
        //
        // The second parameter accepts an array of keys as a
        // string with an option array containing a
        // single string as the last option.
        //
        // The third parameter is the value to be assigned.
        //
        // Examples:
        //
        // `["foo", "bar", "baz"] => {foo: {bar: {baz: "value"}}}`
        //
        // `["foo", "bar", ["baz"]] => {foo: {bar: {baz: ["value"]}}}`
        //
        // When the final value is an array with a string, the key
        // becomes an array, and values are pushed in to the array,
        // allowing multiple fields with the same name to be
        // assigned to the array.
        var assignKeyValue = function (obj, keychain, value) {
            if (!keychain) {
                return obj;
            }

            var key = keychain.shift();

            // build the current object we need to store data
            if (!obj[key]) {
                obj[key] = _.isArray(key) ? [] : {};
            }

            // if it's the last key in the chain, assign the value directly
            if (keychain.length === 0) {
                if (_.isArray(obj[key])) {
                    obj[key].push(value);
                } else {
                    obj[key] = value;
                }
            }

            // recursive parsing of the array, depth-first
            if (keychain.length > 0) {
                assignKeyValue(obj[key], keychain, value);
            }

            return obj;
        };

        // Flatten the data structure in to nested strings, using the
        // provided `KeyJoiner` function.
        //
        // Example:
        //
        // This input:
        //
        // ```js
        // {
        //   widget: "wombat",
        //   foo: {
        //     bar: "baz",
        //     baz: {
        //       quux: "qux"
        //     },
        //     quux: ["foo", "bar"]
        //   }
        // }
        // ```
        //
        // With a KeyJoiner that uses [ ] square brackets,
        // should produce this output:
        //
        // ```js
        // {
        //  "widget": "wombat",
        //  "foo[bar]": "baz",
        //  "foo[baz][quux]": "qux",
        //  "foo[quux]": ["foo", "bar"]
        // }
        // ```
        var flattenData = function (config, data, parentKey) {
            var flatData = {};

            _.each(data, function (value, keyName) {
                var hash = {};

                // If there is a parent key, join it with
                // the current, child key.
                if (parentKey) {
                    keyName = config.keyJoiner(parentKey, keyName);
                }

                if (_.isArray(value)) {
                    keyName += "[]";
                    hash[keyName] = value;
                } else if (_.isObject(value)) {
                    hash = flattenData(config, value, keyName);
                } else {
                    hash[keyName] = value;
                }

                // Store the resulting key/value pairs in the
                // final flattened data object
                _.extend(flatData, hash);
            });

            return flatData;
        };

        return Syphon;
    })(Backbone, jQuery, _);

    // Type Registry
    // -------------

    // Type Registries allow you to register something to
    // an input type, and retrieve either the item registered
    // for a specific type or the default registration
    Backbone.Syphon.TypeRegistry = function () {
        this.registeredTypes = {};
    };

    // Borrow Backbone's `extend` keyword for our TypeRegistry
    Backbone.Syphon.TypeRegistry.extend = Backbone.Model.extend;

    _.extend(Backbone.Syphon.TypeRegistry.prototype, {

        // Get the registered item by type. If nothing is
        // found for the specified type, the default is
        // returned.
        get: function (type) {
            var item = this.registeredTypes[type];

            if (!item) {
                item = this.registeredTypes["default"];
            }

            return item;
        },

        // Register a new item for a specified type
        register: function (type, item) {
            this.registeredTypes[type] = item;
        },

        // Register a default item to be used when no
        // item for a specified type is found
        registerDefault: function (item) {
            this.registeredTypes["default"] = item;
        },

        // Remove an item from a given type registration
        unregister: function (type) {
            if (this.registeredTypes[type]) {
                delete this.registeredTypes[type];
            }
        }
    });


    // Key Extractors
    // --------------

    // Key extractors produce the "key" in `{key: "value"}`
    // pairs, when serializing.
    Backbone.Syphon.KeyExtractorSet = Backbone.Syphon.TypeRegistry.extend();

    // Built-in Key Extractors
    Backbone.Syphon.KeyExtractors = new Backbone.Syphon.KeyExtractorSet();

    // The default key extractor, which uses the
    // input element's "id" attribute
    Backbone.Syphon.KeyExtractors.registerDefault(function ($el) {
        return $el.prop("name");
    });


    // Input Readers
    // -------------

    // Input Readers are used to extract the value from
    // an input element, for the serialized object result
    Backbone.Syphon.InputReaderSet = Backbone.Syphon.TypeRegistry.extend();

    // Built-in Input Readers
    Backbone.Syphon.InputReaders = new Backbone.Syphon.InputReaderSet();

    // The default input reader, which uses an input
    // element's "value"
    Backbone.Syphon.InputReaders.registerDefault(function ($el) {
        return $el.val();
    });

    // Checkbox reader, returning a boolean value for
    // whether or not the checkbox is checked.
    Backbone.Syphon.InputReaders.register("checkbox", function ($el) {
        var checked = $el.prop("checked");
        return checked;
    });


    // Input Writers
    // -------------

    // Input Writers are used to insert a value from an
    // object into an input element.
    Backbone.Syphon.InputWriterSet = Backbone.Syphon.TypeRegistry.extend();

    // Built-in Input Writers
    Backbone.Syphon.InputWriters = new Backbone.Syphon.InputWriterSet();

    // The default input writer, which sets an input
    // element's "value"
    Backbone.Syphon.InputWriters.registerDefault(function ($el, value) {
        $el.val(value);
    });

    // Checkbox writer, set whether or not the checkbox is checked
    // depending on the boolean value.
    Backbone.Syphon.InputWriters.register("checkbox", function ($el, value) {
        $el.prop("checked", value);
    });

    // Radio button writer, set whether or not the radio button is
    // checked.  The button should only be checked if it's value
    // equals the given value.
    Backbone.Syphon.InputWriters.register("radio", function ($el, value) {
        $el.prop("checked", $el.val() === value);
    });

    // Key Assignment Validators
    // -------------------------

    // Key Assignment Validators are used to determine whether or not a
    // key should be assigned to a value, after the key and value have been
    // extracted from the element. This is the last opportunity to prevent
    // bad data from getting serialized to your object.

    Backbone.Syphon.KeyAssignmentValidatorSet = Backbone.Syphon.TypeRegistry.extend();

    // Build-in Key Assignment Validators
    Backbone.Syphon.KeyAssignmentValidators = new Backbone.Syphon.KeyAssignmentValidatorSet();

    // Everything is valid by default
    Backbone.Syphon.KeyAssignmentValidators.registerDefault(function () {
        return true;
    });

    // But only the "checked" radio button for a given
    // radio button group is valid
    Backbone.Syphon.KeyAssignmentValidators.register("radio", function ($el, key, value) {
        return $el.prop("checked");
    });


    // Backbone.Syphon.KeySplitter
    // ---------------------------

    // This function is used to split DOM element keys in to an array
    // of parts, which are then used to create a nested result structure.
    // returning `["foo", "bar"]` results in `{foo: { bar: "value" }}`.
    //
    // Override this method to use a custom key splitter, such as:
    // `<input name="foo.bar.baz">`, `return key.split(".")`
    Backbone.Syphon.KeySplitter = function (key) {
        var matches = key.match(/[^\[\]]+/g);

        if (key.indexOf("[]") === key.length - 2) {
            lastKey = matches.pop();
            matches.push([lastKey]);
        }

        return matches;
    }


    // Backbone.Syphon.KeyJoiner
    // -------------------------

    // Take two segments of a key and join them together, to create the
    // de-normalized key name, when deserializing a data structure back
    // in to a form.
    //
    // Example:
    //
    // With this data strucutre `{foo: { bar: {baz: "value", quux: "another"} } }`,
    // the key joiner will be called with these parameters, and assuming the
    // join happens with "[ ]" square brackets, the specified output:
    //
    // `KeyJoiner("foo", "bar")` //=> "foo[bar]"
    // `KeyJoiner("foo[bar]", "baz")` //=> "foo[bar][baz]"
    // `KeyJoiner("foo[bar]", "quux")` //=> "foo[bar][quux]"

    Backbone.Syphon.KeyJoiner = function (parentKey, childKey) {
        return parentKey + "[" + childKey + "]";
    }


    return Backbone.Syphon;
}));