// Backbone.Syphon, v0.4.1
// Copyright (c)2012 Derick Bailey, Muted Solutions, LLC.
// Distributed under MIT license
// http://github.com/derickbailey/backbone.syphon
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 + "]";
}
