/** var $H = pulp.hash.getInstance; */
pulp.Modules.hash = '$H';

(function() {

  var $p = pulp.base;

  /**
   * Manage and iterate an object with key-value pairs
   * @name pulp.hash
   * @requires  base
   * @requires  cls
   * @example
   *   var $H = pulp.hash.getInstance;
   *   var colors = $H({banana: 'yellow', apple: 'green'});
   *   colors.set('apple', 'red');
   *   colors.each(function(value, key) {
   *     alert('my ' + key + ' is ' + value);
   *   });
   *   pulp.hash.without({banana: 'yellow', apple: 'green'}, 'green');
   *   // returns {banana: 'yellow'}
   */
  var staticApi = /** @scope pulp.hash */{
    /**
     * Execute a function on each item of an object
     * aliased as: {@link pulp.hash.each}
     * 
     * @function
     * @param {Object} hash  An object containing key-value paires
     * @param {Function} iterator  The function to call
     * @param {Object} [context=iterator]  The object scope in which to call the function
     * @return {pulp.hash}
     * @chainable
     */      
    forEach: $p.each,
    
    findKey: function(hash, value, context) {
      var prop;
      if (typeof value == 'function') {
        context = context || iterator;
        try {
          for (prop in hash) {
            if (value.call(context, prop, hash[prop], hash)) {
              return prop;
            }
          }
        } catch(e) { if (e != pulp.Break) throw e; }
      } else {
        for (prop in hash) {
          if (hash[prop] === value) {
            return prop;
          }
        }
      }
      return undefined;
    },
    /**
     * Return an object without the given value
     *
     * @param {Object} hash  The original object containing key-value pairs
     * @param {Mixed} value  The value to exclude
     * @return {Object}  A new object containing all other values
     */
    without: function(hash, value) {
      var results = {};
      for (var prop in hash) {
        if (hash[prop] != value) {
          hash[prop] = value;
        }
      }
      return results;
    },
    /**
     * Create an object containing all the same properties of the passed object
     *
     * @function
     * @param {Object} object  The object to clone
     * @return {Object}
     */    
    clone: $p.clone,
    /**
     * Return true if the iterator returns true for every item (aliased as: {@link pulp.hash.all})
     * 
     * @param {Object} hash  An object with key-value pairs
     * @param {Function} iterator  The iterator function (or object with "call" method)
     * @param {Object} [context=iterator]  The object context from which to call the method.
     * @return {Boolean}
     */
    every: function(hash, iterator, context) {
      iterator = iterator || pulp.K;
      context = context || iterator;
      try {
        for (var prop in hash) {
          if (!iterator.call(context, hash[prop], prop, hash)) {
            return false;
          }
        }
      } catch(e) {
        if (e != pulp.Break) throw e;
      }
      return true;        
    },
    /**
     * Build an array, object, string, or number using an iterator. Iterator receives
     * the memo, the item, the index, and the raw object. Optionally specify the context
     * in which to execute the iterator. The iterator should return the new value of
     * the memo.
     *
     * @method inject
     * @param {Object} hash  The object which to iterate
     * @param {Mixed} memo  The starting value
     * @param {Function} iterator  The iterator
     * @param {Object} [context=iterator]  The object relative to which to call the iterator.
     * @return {Mixed}  The final memo value
     */
    inject: function(hash, memo, iterator, context) {
      iterator = iterator || pulp.K;
      context = context || iterator;
      try {
        for (var prop in hash) {
          memo = iterator.call(context, memo, hash[prop], prop, hash);
        }
      } catch(e) {
        if (e != pulp.Break) throw e;
      }
      return memo;
    },
    /**
     * Call a method on every value in the object
     *
     * @param {Object} hash  The object which to iterate
     * @param {String} method  The name of the method to call
     * @param {Mixed} [argument1]  (Argument to pass to the method)
     * @param {Mixed} [argument2]  (Argument to pass to the method)
     * @param {Mixed} [argumentN]  (Argument to pass to the method)
     * @return {Object}  The original object
     */    
    invoke: function(hash, method) {
      var args = Array.prototype.slice.call(arguments, 2);      
      try {
        for (var prop in hash) {        
          Function.prototype.apply.call(hash[prop][method], hash[prop], args);
        }
      } catch(e) {
        if (e != pulp.Break) throw e;
      }
      return hash;    
    },
    /**
     * Return an object containing the value of the given property for each item.
     *
     * @param {Object} hash  The object which to iterate
     * @param {String} attr  The name of the attribute to pluck
     * @return {Object}  A new object containing the results
     */    
    pluck: function(hash, attr) {
      var results = {};
      try {
        for (var prop in hash) {
          results[prop] = hash[prop][attr];
        }
      } catch(e) {
        if (e != pulp.Break) throw e;
      }        
      return results;
    },
    /**
     * Set a property to a given value for each item. The oposite of pluck.
     *
     * @param {Object} hash  The object which to iterate
     * @param {String} attr  The name of the attribute to set
     * @param {Function|Mixed} value  The value to set, or a function to process the values
     * @param {Object} [context=item]  If value is a function, the scope in which to call the function
     * @return {Object}
     * @example
     *   var contacts = {
     *     '101': {"first-name": 'Dan', age: 24, team: 'Development'},
     *     '102': {"first-name": 'Bryan', age: 38, team: 'System Administration'},
     *     '103': {"first-name": 'Mark', age: 26, team: 'Quality Assurance'},
     *     '104': {"first-name": 'Aaron', age: 37, team: 'Operations'}
     *   };
     *   pulp.hash.setAll(contacts, 'age', 'N/A');
     *   pulp.hash.setAll(contacts, 'first-name', String.prototype.toUpperCase);
     *   pulp.hash.setAll(contacts, 'team', teamObject.nameToId, teamObject);
     */    
    setAll: function(hash, attr, value, context) {
      try {
        if (typeof value == 'function') {
          for (var prop in hash) {
            hash[prop][attr] = value.call(context || hash[prop], hash[prop][attr], attr, hash);
          }
        } else {
          for (var prop in hash) {
            hash[prop][attr] = value;
          }
        }
      } catch(e) {
        if (e != pulp.Break) throw e;
      }
      return hash;
    },
    /**
     * Return a new Object with the items for which the iterator returns true
     * (aliases: {@link pulp.hash#select}, {@link pulp.hash#findAll})
     * 
     * @param {Object} hash  The object which to iterate
     * @param {Function} iterator  The iterator function (or object with "call" method)
     * @param {Object} context  The object context from which to call the method.
     * @return {Object} 
     */    
    filter: function(hash, iterator, context) {
      iterator = iterator || pulp.K;
      context = context || iterator;
      var results = {};
      try {
        for (var prop in hash) {
          if (iterator.call(context, hash[prop], prop, hash)) {
            results[prop] = hash[prop];
          }
        }
      } catch(e) {
        if (e != pulp.Break) throw e;
      }
      return results;
    },
    /**
     * Return an object the result of calling the iterator on each item
     * (alias: {@link pulp.hash#collect})
     * 
     * @param {Object} hash  The object which to iterate
     * @param {Function} iterator  The iterator function (or object with "call" method)
     * @param {Object} context  The object context from which to call the method.
     * @return {Object} 
     */
    map: function(hash, iterator, context) {
      iterator = iterator || pulp.K;
      context = context || iterator;
      var results = {};
      try {
        for (var prop in hash) {
          results[prop] = iterator.call(context, hash[prop], prop, hash);
        }
      } catch(e) {
        if (e != pulp.Break) throw e;
      }
      return results;
    },
    /**
     * Return true if the iterator returns true for at least one item (alias: {@link pulp.hash#any})
     * 
     * @param {Object} hash  The object which to iterate
     * @param {Function} iterator  The iterator function (or object with "call" method)
     * @param {Object} context  The object context from which to call the method.
     * @return {Boolean}
     */
    some: function(hash, iterator, context) {
      iterator = iterator || pulp.K;
      context = context || iterator;
      try {
        for (var prop in hash) {
          if (iterator.call(context, hash[prop], prop, hash)) {
            return true;
          }
        }
      } catch(e) {
        if (e != pulp.Break) throw e;
      }
      return false;          
    },
    
    extend: $p.extend,
    
    min: function(hash, iterator, context) {
      iterator = iterator || pulp.K;
      context = context || iterator;
      var result, value;
      try {
        for (var prop in hash) {
          value = iterator.call(context, hash[prop], prop, hash);
          if (result == null || value < result) {
            result = value;
          }
        }
      } catch(e) {
        if (e != pulp.Break) throw e;
      }
      return result;        
    },
    max: function(hash, iterator, context) {
      iterator = iterator || pulp.K;
      context = context || iterator;
      var result, value;
      try {
        for (var prop in hash) {
          value = iterator.call(context, hash[prop], prop, hash);
          if (result == null || value >= result) {
            result = value;
          }
        }
      } catch(e) {
        if (e != pulp.Break) throw e;
      }
      return result;    
    },
    size: function(hash) {
      var i = 0;
      for (var key in hash) {
        i++;
      }
      return i;
    },
    keys: function(hash) {
      var keys = [], i = 0;
      for (var key in hash) {
        keys[i++] = key;
      }
      return keys;
    },
    values: function(hash) {
      var values = [], i = 0;
      for (var key in hash) {
        values[i++] = hash[key];
      }
      return values;
    },
    toQueryString: function(hash, glue, delimiter) {
      var pairs = [], glue = glue || '=', key, value;
      for (var prop in hash) {
        key = encodeURIComponent(prop);
        value = hash[prop];
        if ($p.isArray()) {
          for (var i in value) {
            pairs.push(key + '[]' + glue + encodeURIComponent(value[i]));
          }
        } else {
          pairs.push(key + glue + encodeURIComponent(value));
        }
      }
      return pairs.join(delimiter || '&');
    }
  };

  /**
   * Instance methods for a hash
   * @name pulp.hash
   */
  var self = pulp.hash = pulp.cls.create(/** @lends pulp.hash# */{
    /**
     * @constructs
     */
    initialize: function(data) {
      this.raw = data || {};
    },
    get: function(prop) {
      return this.raw[prop];
    },
    set: function(prop, value) {
      this.notify('set', function() {
        this.raw[prop] = value;  
      }, {property: prop, oldValue: this.raw[prop], newValue: value});
      return this;
    },
    extend: function() {
      $p.each(arguments, function(data) {
        for (var prop in data) {
          this.set(prop, data[prop]);
        }
      }, this);
    },
    unset: function(prop) {
      this.notify('unset', function() {
        delete this.raw[prop];
      }, {property: prop, oldValue: this.raw[prop]});
      return this;
    },
    clear: function() {
      this.notify('clear', function() {
        this.each(function(value, key) {
          this.unset(prop);
        }, this);        
      }, {values: this.raw});
      return this;
    },
    concat: function() {
      $p.each(arguments, function(hash) {
        for (var prop in hash) {
          this.set(prop, hash[prop]);
        }
      }, this);
      return this;
    },
    clone: function(hash) {
      return new self($p.extend({}, this.raw));
    },
    toObject: function() {
      return $p.extend({}, this.raw);
    }
  })

  .extend(staticApi)

  .addMethods(staticApi, 'raw')

  .aliasMethods({
    each: 'forEach',
    collect: 'map',
    all: 'every',
    any: 'some'
  });
  
  /**
   * @name pulp.hash.getInstance
   * @param {Object} [data={}]
   * @return {pulp.hash}
   * @factory pulp.hash
   */
  self.getInstance = function(data) {
    return new self(data);
  };  

})();
