/** var $A = pulp.array.getInstance */
pulp.Modules.array = '$A';

(function() {
  
  var $p = pulp.base;
  
  var slice = Array.prototype.slice;

  /**
   * Array wrapper to simulate standard behavior of Arrays, add utility functions, and allow subclassing
   * @name pulp.array
   *
   * @requires base
   * @requires cls
   */
  var self = pulp.array = pulp.cls.create(/** @lends pulp.array# */{
    /**
     * @constructs
     * @param {Array} [iterable=[]]  The array to wrap
     */
    initialize: function(iterable) {
      this.raw = $p.makeArray(iterable);
    }    
  });

  var core = /** @lends pulp.array# */{
    /**
     * Return the item at the given index
     *
     * @param {Number} index
     * @return {Mixed}
     */
    item: function(index) {
      return this.raw[index];
    },
    /**
     * Remove all data from the object
     *
     * @function clear
     * @return {pulp.array}  This object
     * @chainable
     */
    clear: function() {
      this.raw.length = 0;
      return this;
    },
    /**
     * Standard Array forEach method where iterator receives the item,
     *   the index, and the array reference. Optionally pass a context
     *   (e.g. object instance) in which to execute the iterator.
     * Varies from the browser-native function by allowing throwing pulp.Break and
     *
     * @param {Function} iterator  The iterator function (or object with "call" method)
     * @param {Object} context  The object context from which to call the method.
     *     Defaults to the iterator itself.
     * @return {pulp.array}  This object
     * @chainable
     */
    forEach: function(iterator, context) {
      context = context || iterator;
      // Use a try-catch block to allow exiting the loop by
      // throwing pulp.Break within the iterator
      try {
        for (var i = 0, len = this.raw.length; i < len; i++) {
          iterator.call(context, this.raw[i], i, this.raw);
        }
      } catch(e) { if (e != pulp.Break) { throw e; } }
      return this;
    },
    /**
     * Return the first element for which the iterator returns true
     *
     * @param {Function} iterator  The iterator function (or object with "call" method)
     * @param {Object} context  The object context from which to call the method.
     * @return {Mixed}
     */    
    find: function(iterator, context) {
      var result;
      try {
        for (var i = 0, len = this.raw.length; i < len; i++) {
          if (iterator.call(context, this.raw[i], i, this.raw)) {
            result = this.raw[i];
            break;
          }
        }
      } catch(e) { if (e != pulp.Break) { throw e; } }
      return result;
    },
    /**
     * Return true if the needle is found in the array (identical or === comparison)
     *
     * @param {Function|Mixed} needle  The item to find or iterator to use to test (or object with "call" method)
     * @param {Object} context  The object context from which to call the method.
     * @return {Number}
     */      
    include: function(needle, context) {
      return this.find(needle, context) > -1;
    },
    /**
     * Return the position of the value within the array. If not found,
     * returns -1. A comparison function may be passed instead of value. The comparison
     * function should return true if the item matches the criteria.
     *
     * @param {Function} callback  The method to call.
     * @param {Object} [context=callback]  The object context from which to call the method
     * @return {Number}  Return -1 if the item is not found within the collection
     */
    findKey: function(callback, context) {
      context = context || iterator;
      try {
        for (var i = 0, len = this.raw.length; i < len; i++) {
          if (needle.call(context, this.raw[i], i, this.raw)) {
            return i;
          }
        }
      } catch(e) { if (e != pulp.Break) { throw e; } }
      return -1;
    },
    /**
     * Return the first item in the array
     *
     * @function first
     * @return {Mixed}
     * @public
     */
    first: function() {
      return this.raw[0];
    },
    /**
     * Return the last item in the array
     *
     * @function last
     * @return {Mixed}
     * @public
     */
    last: function() {
      return this.raw[this.raw.length - 1];
    },
    /**
     * Return an array withot the given item
     *
     * @param {Mixed} value  The value to exclude
     * @return {pulp.array}  A new object containing all other values
     * @public
     * @example
     */
    without: function(value) {
      var results = [];
      for (var i = 0, len = this.raw.length; i < len; i++) {
        if (this.raw[i] != value) {
          results.push(this.raw[i]);
        }
      }
      return new self(results);
    },
    /**
     * Return a copy of this object
     *
     * @return {pulp.array}
     */
    clone: function() {
      return new self(this.raw);
    },
    /**
     * Build an array, object, string, or number using an iterator. Iterator receives
     * the memo, the item, the index, and the raw array. Optionally specify the context
     * in which to execute the iterator. The iterator should return the new value of
     * the memo.
     *
     * @param {Mixed} memo  The starting value
     * @param {Function} iterator  The iterator
     * @param {Object} context  The object relative to which to call the iterator.
     *    Defaults to the iterator itself.
     * @return {Mixed}  The final memo value
     */
    inject: function(memo, iterator, context) {
      iterator = iterator || pulp.K;
      context = context || iterator;
      try {
        for (var i = 0, len = this.raw.length; i < len; i++) {
          memo = iterator.call(context, memo, this.raw[i], i, this.raw);
        }
      } catch(e) { if (e != pulp.Break) { throw e; } }
      return memo;
    },
    /**
     * Turn nested arrays into a single list
     * @function pulp.array#flatten
     * @return {pulp.array}
     * @example
     *   $A([1, 2, [10, 20, [100, 200], 30], 3]).flatten();
     *   // [1, 2, 10, 20, 100, 200, 30, 3]
     */    
    flatten: (function() {
      function splat(arr) {
        var flat = [];
        for (var i = 0, len = arr.length; i < len; i++) {
          if ($p.isArray(arr[i])) {
            flat = flat.concat(splat(arr[i]));
          } else {
            flat.push(arr[i]);
          }
        }
        return flat;			
      }
      return function() {
        return new self(splat(this.raw));
      };
    })(),
    /**
     * Return an array without 0, null, undefined, false or '' (anthing that evaluates to null)
     *
     * @return {pulp.array}
     */      
    compact: function() {
      return this.select(function(value) {
        return value != null;
      });
    },
    /**
     * Call a method on every member of the array
     *
     * @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 {pulp.array}  This object
     * @chainable
     */
    invoke: function(method) {
      var args = slice.call(arguments, 1);      
      try {
        for (var i = 0, len = this.raw.length; i < len; i++) {        
          Function.prototype.apply.call(this.raw[i][method], this.raw[i], args);
        }
      } catch(e) { if (e != pulp.Break) { throw e; } }
      return this;
    },
    /**
     * Return an array containing the value of the given property for each item.
     *
     * @param {String} attr  The name of the attribute to pluck
     * @return {pulp.array}  A new object containing the results
     */
    pluck: function(attr) {
      var length = this.raw.length || 0,
      results = new Array(length);
      while (length--) {
        results[length] = this.raw[length][attr];
      }
      return new self(results);
    },
    /**
     * Set a property to a given value for each item. The oposite of pluck.
     *
     * @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]  If value is a function, the scope in which to call the function
     * @return {pulp.array}  This object
     * @chainable
     * @example
     *   var contacts = $A([
     *     {"first-name": 'Dan', age: 24, team: 'Development'},
     *     {"first-name": 'Bryan', age: 38, team: 'System Administration'},
     *     {"first-name": 'Mark', age: 26, team: 'Quality Assurance'},
     *     {"first-name": 'Aaron', age: 37, team: 'Operations'}
     *   ]);  
     *   contacts.setAll('age', 'N/A');
     *   contacts.setAll('first-name', String.prototype.toUpperCase);
     *   contacts.setAll('team', teamObject.nameToId, teamObject);
     */
    setAll: function(attr, value, context) {
      var length = this.raw.length || 0;
      if (typeof value == 'function') {
        while (length--) {
          this.raw[length][attr] = value.call(context || this.raw[length][attr], this.raw[length][attr], length, this.raw);
        }        
      } else {
        while (length--) {
          this.raw[length][attr] = value;
        }
      }
      return this;
    },
    /**
     * Return a new pulp.array with items removed that cause true to return
     * 
     * @param {Function} iterator  The iterator function (or object with "call" method)
     * @param {Object} context  The object context from which to call the method.
     * @return {Mixed}
     */
    reject: function(iterator, context) {
      iterator = iterator || pulp.K;    
      context = context || iterator;
      var results = [];
      try {
        for (var i = 0,
        len = this.raw.length; i < len; i++) {
          if (!iterator.call(context, this.raw[i], i)) {
            results.push(this.raw[i]);
          }
        }
      } catch(e) { if (e != pulp.Break) { throw e; } }
      return new self(results);
    },
    /**
     * Return the number of members of the array
     * 
     * @return {Number}
     */
    size: function() {
      return this.raw.length;
    },
    /**
     * Return the wrapped iterable to an Array (or make a copy if the iterable is an Array)
     * 
     * @return {Array}
     */
    toArray: function() {
      return slice.call(this.raw, 0);
    },
    /**
     * Return a new pulp.array without duplicate values
     * 
     * @param {Boolean} [isSorted=false]  If true, do a quicker comparison
     * @return {pulp.array}
     */
    unique: function(isSorted) {
      var results = [], r = 0;
      for (var i = 0, len = this.raw.length; i < len; i++) {
        if (!$p.inArray(results, this.raw[i])) {
          results[r++] = this.raw[i];
        }
      }
      return new self(results);
    },
    //
    // emulation native Array methods
    //    
    /**
     * Return true if the iterator returns true for every item (aliased as: {@link pulp.array#all})
     * 
     * @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}
     */
    every: function(iterator, context) {
      iterator = iterator || pulp.K;
      context = context || iterator;
      try {
        for (var i = 0, len = this.raw.length; i < len; i++) {
          if (!iterator.call(context, this.raw[i], i, this.raw)) {
            return false;
          }
        }
      } catch(e) { if (e != pulp.Break) { throw e; } }
      return true;        
    },
    /**
     * Return a new pulp.array with the items for which the iterator returns true
     * (aliases: {@link pulp.array#select}, {@link pulp.array#findAll})
     * 
     * @param {Function} iterator  The iterator function (or object with "call" method)
     * @param {Object} context  The object context from which to call the method.
     * @return {pulp.array} 
     */
    filter: function(iterator, context) {
      iterator = iterator || pulp.K;
      context = context || iterator;
      var results = [];
      try {
        for (var i = 0, len = this.raw.length; i < len; i++) {
          if (iterator.call(context, this.raw[i], i, this.raw)) {
            results.push(this.raw[i]);
          }
        }
      } catch(e) { if (e != pulp.Break) { throw e; } }
      return new self(results);
    },
    /**
     * Return a new pulp.array containing the result of calling the iterator on each item
     * (alias: {@link pulp.array#collect})
     * 
     * @param {Function} iterator  The iterator function (or object with "call" method)
     * @param {Object} context  The object context from which to call the method.
     * @return {pulp.array} 
     */
    map: function(iterator, context) {
      iterator = iterator || pulp.K;
      context = context || iterator;
      var results = [];
      try {
        for (var i = 0, len = this.raw.length; i < len; i++) {
          results.push(iterator.call(context, this.raw[i], i, this.raw));
        }
      } catch(e) { if (e != pulp.Break) { throw e; } }
      return new self(results);
    },
    /**
     * Return true if the iterator returns true for at least one item (alias: {@link pulp.array#any})
     * 
     * @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(iterator, context) {
      iterator = iterator || pulp.K;
      context = context || iterator;
      try {
        for (var i = 0, len = this.raw.length; i < len; i++) {
          if (iterator.call(context, this.raw[i], i, this.raw)) {
            return true;
          }
        }
      } catch(e) { if (e != pulp.Break) { throw e; } }
      return false;      
    },
    /**
     * Return a new pulp.array with the items of the given collections appended to this collection
     * 
     * @param {Iterable} argument1  an Array, pulp.array, HTMLNodeList etc. to append
     * @param {Iterable} argument2  an Array, pulp.array, HTMLNodeList etc. to append
     * @param {Iterable} argumentN  an Array, pulp.array, HTMLNodeList etc. to append
     * @return {pulp.array}
     */
    concat: function() {
      var result = slice.call(this.raw, 0), a = arguments, arr;
      for (var i = 0, len = a.length; i < len; i++) {
        arr = a[i] instanceof self ? a[i].raw : $p.makeArray(a[i]);
        result.concat(arr);
      }
      return new self(result);
    },
    /**
     * Return a string with each item of the collection separated by a comma
     * the same as this.join(',');
     * 
     * @return {String}
     */
    toString: function() {
      return this.join(',');
    }
  };
  
  /**
   * Create a string composed of each item joined by a glue string
   * (same as Array#join)
   * 
   * @function pulp.array#join
   * @param {String} [glue=","]  The delimiter
   * @return {String}
   */
  /**
   * Remove the last item from the collection and return it
   * (same as Array#pop)
   * 
   * @function pulp.array#pop
   * @return {Mixed}  The last item
   */
  /**
   * Add one or more items to the end of the collection
   * (same as Array#push)
   * 
   * @function pulp.array#push
   * @param {Mixed} argument1  An item to append
   * @param {Mixed} [argument2]  An item to append
   * @param {Mixed} [argumentN]  An item to append
   * @return {Mixed}  The last argument passed
   */
  /**
   * Remove the first item from the collection and return it
   * (same as Array#shift)
   * 
   * @function pulp.array#shift
   * @return {Mixed}  The first item
   */
  /**
   * Add one or more items to the beginning of the collection
   * (same as Array#unshift)
   * 
   * @function pulp.array#unshift
   * @param {Mixed} argument1  An item to prepend
   * @param {Mixed} [argument2]  An item to prepend
   * @param {Mixed} [argumentN]  An item to prepend
   * @return {Mixed}  The first argument passed
   */
  var valueReturners = 'join pop push shift unshift';
  
  /**
   * return a new pulp.array that is a sorted version of the collection from lowest to highest (objects appear first)
   * (same as Array#sort)
   * 
   * @function pulp.array#sort
   * @return {pulp.array}
   */
  /**
   * return a new pulp.array that is reversed
   * (same as Array#reverse)
   * 
   * @function pulp.array#reverse
   * @return {pulp.array}
   */
  /**
   * return a new pulp.array that contains items from and to the given positions
   * (same as Array#slice)
   * 
   * @function pulp.array#slice
   * @param {Number} start  The starting index (inclusive)
   * @param {Number} [end]  The ending index (not inclusive); If not given or larger than the length of the array, end at the last position
   * @return {pulp.array}
   * @example
   *   $A([0,1,2,3,4,5,6]).slice(2, 5);  // $A([2,3,4])
   *   $A([0,1,2,3,4,5,6]).slice(2);     // $A([2,3,4,5,6])
   *   $A([0,1,2,3,4,5,6]).slice(2, 10); // $A([2,3,4,5,6])
   */
  /**
   * Alter the collection by removing and or adding items
   * (same as Array#splice)
   * 
   * @function pulp.array#splice
   * @param {Number} index  Where to remove and or add new elements
   * @param {Number} howMany  How many elements should be removed. Must be a number. If 0 or less, remove no items.
   * @param {Number} [item1]  New element to add
   * @param {Number} [item2]  New element to add
   * @param {Number} [item3]  New element to add
   * @return {pulp.array}  Contains the elements removed
   * @example
   *   a = $A(['a','b','c','d','e']); a.splice(2, 1);  // a == $A(['a','b','d','e']), returns ['c']
   *   a = $A(['a','b','c','d','e']); a.splice(2, 2);  // a == $A(['a','b','e']), returns ['c','d']
   *   a = $A(['a','b','c','d','e']); a.splice(2, 1, 'q');  // a == $A(['a','b','q','d','e']), returns ['c']
   *   a = $A(['a','b','c','d','e']); a.splice(2, 1, 'q', 'r');  // a == $A(['a','b','q','r','d','e']), returns ['c']
   *   a = $A(['a','b','c','d','e']); a.splice(2, 0, 'q', 'r');  // a == $A(['a','b','q','r','c','d','e']), returns []
   */  
  var arrayReturners = 'sort reverse slice splice';
  
  /**
   * Call Math.min
   * 
   * @function pulp.array#min
   * @return {Number}
   * @example $A([1, 42, -3, 6]).min(); // -3
   */
  /**
   * Call Math.max
   * 
   * @function pulp.array#max
   * @return {Number}
   * @example $A([1, 42, -3, 6]).max(); // 42
   */  
  var mathMethods = 'min max';
  
  if ($p.isNative(Array.prototype.indexOf)) {
    valueReturners += ' indexOf';
    
  } else {
    /**
     * Same as Array#indexOf with the collection as the haystack
     * @name pulp.array#indexOf
     * @function
     * @param {Mixed} value  The needle
     * @param {Number} [startIndex=0]  The position in the collection at which to start
     * @return {Number}  Returns -1 when the item is not found within the collection
     */
    core.indexOf = function(value, startIndex) {
      return $p.arrayIndexOf(this.raw, value, startIndex);
    };
  }
  if ($p.isNative(Array.prototype.lastIndexOf)) {
    valueReturners += ' lastIndexOf';
    
  } else {
    /**
     * Same as Array#indexOf with the collection as the haystack
     * @name pulp.array#lastIndexOf
     * @function
     * @param {Mixed} value  The needle
     * @param {Number} [startIndex=%lastPosition%]  The position in the collection at which to start
     * @return {Number}  Returns -1 when the item is not found within the collection
     */
    core.lastIndexOf = function(value, startIndex) {
      startIndex = startIndex === undefined ? this.raw.length : startIndex;
      i = (startIndex < 0 ? this.raw.length + startIndex : startIndex) + 1;    
      while (i--) {
        if (this.raw[i] === value) {
          return i;
        }
      }
      return -1;
    };
  }
  
  // functions that return a value should return that raw value
  $p.each(valueReturners, function(fn) {
    /** @ignore */
    core[fn] = function() {
      return Array.prototype[fn].apply(this.raw, slice.call(arguments, 0));
    };
  });
  
  // functions that return an array will return pulp.array
  $p.each(arrayReturners, function(fn) {
    /** @ignore */
    core[fn] = function() {
      return new self(Array.prototype[fn].apply(this.raw, slice.call(arguments, 0)));
      //return new self(Array.prototype[fn].apply.call(this.raw, $p.makeArray(arguments)));
    };
  });
  
  if ($p.isNative(Array.prototype.forEach)) {
    /** @ignore */
    core.forEach = function(iterator, context) {
      context = context || iterator;
      // Use a try-catch block to allow exiting the loop by
      // throwing pulp.Break within the iterator
      try {
        this.raw.forEach(iterator, context);
      } catch(e) { if (e != pulp.Break) { throw e; } }
      return this;
    };
  }
  
  $p.each(mathMethods, function(fn) {
    /** @ignore */
    core[fn] = function() {
      return Math[fn].apply(null, this.raw);
    };
  });
  
  self.extendPrototype(core);

  var aliases = /** @lends pulp.array# */{
    /** @function  alias of {@link pulp.array#size} */
    getLength: 'size',
    
    /** @function  alias of {@link pulp.array#forEach} */    
    each: 'forEach',
    
    /** @function  alias of {@link pulp.array#collect} */    
    collect: 'map',
    
    /** @function  alias of {@link pulp.array#every} */    
    all: 'every',
    
    /** @function  alias of {@link pulp.array#some} */    
    any: 'some',
    
    /** @function  alias of {@link pulp.array#filter} */    
    select: 'filter',
    
    /** @function  alias of {@link pulp.array#filter} */    
    findAll: 'filter',
    
    /** @function  alias of {@link pulp.array#include} */    
    member: 'include',
    
    /** @function  alias of {@link pulp.array#toArray} */    
    entries: 'toArray',
    
    /** @function  alias of {@link pulp.array#find} */    
    detect: 'find',
    
    /** @function  alias of {@link pulp.array#unique} */    
    uniq: 'unique'
  };  
  
  self.aliasMethods(aliases);
  
//console.log(self.prototype);  
  
  /**
   * Factory to return pulp.array instance
   *
   * @factory
   * @param {Array|NodeList} [collection=[]]  The array or collection of any type to wrap
   * @return {pulp.array}
   * @name pulp.array.getInstance
   */
  self.getInstance = function(collection) {
    return new self(collection);
//    return new self($p.makeArray(collection));
  };
  
  /**
   * Methodize all pulp.array.prototype Functions to Array.prototype
   * 
   * @function 
   * @name pulp.array.exportToPrototype
   * @return undefined
   */
  self.exportToPrototype = function() {
    for (var method in self.prototype) {
      if (method == 'initialize' || $p.isNative(Array.prototype[method]) || pulp.cls.Base.prototype[method]) {
        continue;
      }
      if (0 && pulp.evalAvailable) {
        // not quite working yet      
        Array.prototype[method] = eval(self.prototype[method].toString().replace((/this\.raw/g), 'this'));
      }
      else {
        (function(method) {
          Array.prototype[method] = function() {
            var a = new self(this);
            var ret = a[method].apply(a, slice.call(arguments, 0));
            if (ret instanceof self) {
              if (0 || ret === a) { // mutator
                var i = ret.raw.length;
                this.length = 0;
                while (i--) {
                  this[i] = ret.raw[i];
                }
                a = ret = null;
                return this;
              }
              // return new array
              ret = ret.raw; // destroy object
              a = null;
              return ret;
            }
            return ret;
          };
        })(method);
      }
    }
    Array.prototype.clone = function(){
      return slice.call(this, 0);
    }
    //self.getInstance = $p.makeArray
  };    

})();