/** var $p = pulp.base */
pulp.Modules.base = '$p';

(function() {

  /**
   * General utility functions
   *  
   * @name pulp.base
   * @namespace
   * @requires pulp
   */
  var $p = pulp.base = /** @lends pulp.base */ {
    /**
     * Copy values from an iterable object to an array
     *
     * @param {Iterable} collection  Any iterable object
     * @return {Array}
     */
    makeArray: function(collection) {
      return collection ? Array.prototype.slice.call(collection, 0) : [];
    },
    /**
     * Return an object's type (e.g. Function, Array, Object)
     * from prototype 1.6.1_rc2 getClass()
     * @param {Object} o
     * @return {String}
     */
    objectType: function(o) {
      return Object.prototype.toString.call(o)
        .match(/^\[object\s(.*)\]$/)[1] || '';
    },
    /**
     * return true if object is an array (cross-frame safe)
     *
     * @param {Object} o  The object to test
     * @return {Boolean}
     */
    isArray: function(o) {
      return $p.objectType(o) == 'Array';
    },
    /**
     * Implementation of Array.prototype.indexOf for browsers that do not have it
     *
     * @param {Array} arr  The array to search
     * @param {Mixed} val  The needle for which to search
     * @param {Number} [startIdx]  The index at which to start searching
     * @return {Number}  The position found, or -1 if not found
     */
    arrayIndexOf: function(arr, val, startIdx) {
      if (typeof arr === 'string') {
        arr = arr.split(' ');
      }
      return arr.indexOf(val, startIdx);
    },
    /**
     * Return true if a member is found in the array
     *
     * @param {Array} arr  The array to search
     * @param {Mixed} val  The needle for which to search
     * @return {Boolean}  True if the value is in the array
     */
    inArray: function(arr, val) {
      return $p.arrayIndexOf(arr, val) > -1;
    },
    /**
     * Execute a function on each item of a collection
     * aliased as: {@link pulp.base.each}
     * 
     * @param {Iterable} array  Any iterable object
     * @param {Function} iterator  The function to call
     * @param {Object} [context]  The object scope in which to call the function
     * @return {pulp.base}
     * @chainable
     */
    forEach: function(array, iterator, context) {
      if (typeof array == 'string') {
        array = array.split(' ');
      }
      context = context || iterator;
      // Use a try-catch block to allow exiting the loop by
      // throwing pulp.Break within the iterator
      try {
        if ($p.isArray(array)) {
          // array
          array.forEach(iterator, context);
        } else {
          // object
          for (var prop in array) {
            iterator.call(context, array[prop], prop, array);
          }
        }
      } catch(e) {
        if (e != pulp.Break) throw e;
      }
      return this;
    },
    /**
     * Return the number of properties in an object (generally a vanilla object)
     *
     * @param {Object} o  The object which to count properties
     * @return {Number}  The number of properties
     */
    countProperties: function(o) {
      var i = 0; // TODO: use hasOwnProperty()?
      for (var p in o) {
        i++;
      }
      return i;
    },
    /**
     * Return a new function bound to the given object scope
     *
     * @param {Function} fn  The function to bind
     * @param {Object} context  The scope to bind
     * @return {Mixed}
     */
    bind: function(fn, context) {
      return function() {
        return fn.apply(context, $p.makeArray(arguments));
      };
    },
    /**
     * Return a function that executes the given function with the first argument set to the calling object or calling object property
     *
     * @param {Function} fn  The function to wrap
     * @param {String} [prop]  The property of the calling object to use instead of the calling object itself
     * @return {Function}
     */
    methodize: function(fn, prop) {
      prop = prop || '';
      if (!fn['_methodized-' + prop]) {
        fn['_methodized-' + prop] = function() {
          return fn.apply(null, [prop ? this[prop] : this].concat($p.makeArray(arguments)));
        };
      }
      return fn['_methodized-' + prop];
    },
    /**
     * Copy properties from one or more objects to another
     *
     * @param {Object} destination
     * @param {Object} source1
     * @param {Object} [source2]
     * @param {Object} [sourceN]
     * @return {Object}
     */
    extend: function(/*destination, source1[, source_2][, source_N...]*/) {
      var a = arguments;
      for (var i = 1, len = a.length, p; i < len; i++) {
        for (p in a[i]) {
          a[0][p] = a[i][p];
        }
      }
      return a[0];
    },
    /**
     * Create an object containing all the same properties of the passed object
     *
     * @param {Object} o  The object to clone
     * @return {Object}
     */
    clone: function(o) {
      return $p.extend({}, o);
    },
    /**
     * Remove whitespace from the beginning and end of the string
     *
     * @param {String} string
     * @return {String}
     */
    trim: function(string) {
      return String(string).trim();
    },
    /**
     * Remove html tags from a string
     *
     * @param {String} string
     * @return {String}
     */
    stripTags: function(string) {
      return string.replace(/<\/?[^>]+>/g, '');
    },
    /**
     * Convert & < and > to html entities
     *
     * @param {String} string
     * @return {String}
     */
    escapeHTML: function(string) {
      return $p.castAsString(string).replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;');
    },
    /**
     * Cast an object to a string (Same as Prototype's String.interpret)
     *
     * @param {Object} o
     * @return {String}
     */
    castAsString: function(o) {
      return (o === null || typeof o == 'undefined' ? '' : String(o));
    },
    /**
     * Replace all the instances of one string with another (Similar to Prototype's String#gsub)
     *   WARNING: do not pass a RegExp with the 'g' flag
     *
     * @param {String} string  The haystack
     * @param {String|RegExp} pattern  The needle
     * @param {String|Function} replacement  The replacement string or function that receives the match array and returns a new string
     * @param {Boolean} [multiline=false]  If true, allow dot to match newlines
     * @param {Number} [limit=0] If this number of matches is met, abort
     * @return {String}
     */
    replaceAll: function(string, pattern, replacement, multiline, limit) {
      // string must be copied to "source" to allow methodizing
      var result = '', source = string, match, count = 0;
      // ensure replacement is a function
      if (typeof replacement != 'function') {
        var strRepl = $p.castAsString(replacement);
        /** @ignore */
        replacement = function() { return strRepl; };
      }
      limit = parseFloat(limit);
      if (multiline) {
        // replace newlines with form feeds so we can do multi-line replacements
        // we use \f because form feeds will match \s
        source = source.replace(/\n/g, '\f');
      }

      while (source.length > 0 && (match = source.match(pattern))) {
        // we found a match, so reset source up to this point
        result += source.slice(0, match.index);
        // add the replacement instead of the match
        result += $p.castAsString(replacement(match, count++));
        source = source.slice(match.index + match[0].length);
        if (limit > 0 && limit <= count) {
          break;
        }
      }
      result += source;
      if (multiline) {
        // put back the newlines
        result = result.replace(/\f/g, '\n');
      }
      return result;
    },
    /**
     * Replace placeholder tags with values
     * 
     * @param {String} text  The template text
     * @param {Object} hash  Object containing name-value pairs of tags-values that need to be replaced
     * @param {RegExp} [tagMatcher=/(^|[^\\])\{\$([\w]+)}/]  RegExp designed to find tags (default is "{$tag}" format tags)
     * @return {String}
     * @example pulp.base.tpl('My favorite food is {$food}.', {food: 'pizza'}) // "My favorite food is pizza."
     */
    tpl: function(text, hash, tagMatcher) {
      return $p.replaceAll(text, (tagMatcher || /(^|[^\\])\{\$([\w]+)}/), function(match) {
        var v = hash[match[2]];
        return match[1] + (v === undefined || v === null ? '' : v);
      }); 
    },
    /**
     * Call code before and or after a given function
     *
     * @param {Function} method
     * @param {Function} wrapper
     * @return {Function}
     * @example pulp.base.wrapFunction(myFn, function(proceed, arg1, arg2) {
     *   // code before
     *   proceed(); // call wrapped function
     *   // code after
     * }); 
     */
    wrapFunction: function(method, wrapper) {
      return function() {
        return wrapper.apply(this, [$p.bind(method, this)].concat($p.makeArray(arguments)));
      };
    },
    /**
     * Change a pulp module shortcut
     *
     * @param {String} module  The name of the module
     * @param {String} shortcut  The name of the variable to use as a shortcut
     * @return {pulp.base}
     */
    setShortcut: function(module, shortcut) {
      if (pulp.Modules.name) {
        pulp.Modules[name].shortcut = shortcut;
      }
      return $p;
    },
    /**
     * Get a global unique id
     * 
     * @return {Number}
     */
    getGuid: function() {
      return ++guid;
    },
    /**
     * Determine whether a function is native to the browser
     * From NW.Matcher by Diego Perini
     *
     * @param {Function} [fn]  object in which to check
     * @return {Boolean}  true if method is native to the browser
     */
    isNative: function(fn) {
      return typeof fn == 'function' && (/\{\s*\[native code[^\]]*\]\s*\}/).test(fn);
    },
    /**
     * Get the id attribute of a node.  If id is not set, set it.
     * Account for situations where id is not a string such as a form with an input named ID
     * 
     * @param {HTMLElement} node  The node of which to get the ID
     * @return {String}
     */
    identifyNode: function(node) {      
      var id;
      if (node === pulp.window) {
        return '<window>';
      }
      if (node === pulp.document) {
        return '<document>';
      }       
      if (node.id && typeof node.id !== 'string') {
        // node has a property id such as a <form> tag with an input with name "id"
        // if the node also has an object named cloneNode, there will be a fatal error
        clone = node.cloneNode(false);
        id = clone.id;
        
      } else {
        id = node.id;
      }
      if (!id || typeof id !== 'string') {
        id = 'pulp-identifyNode-' + $p.getGuid();
        if (typeof node.setAttribute == 'function') {
          node.setAttribute('id', id);
          
        } else {
          node.id = id;
        }           
      }
      return id;       
    }    
  };
  
  if (!$p.isNative(Array.prototype.forEach)) {
    // add a slower version of Array#forEach for browsers without it
    $p.forEach = /** @ignore */ function(array, iterator, context) {
      if (typeof array == 'string') {
        array = array.split(' ');
      }
      context = context || iterator;
      // Use a try-catch block to allow exiting the loop by
      // throwing pulp.Break within the iterator
      try {
        if ($p.isArray(array)) {
          // array
          for (var i = 0, len = array.length; i < len; i++) {
            iterator.call(context, array[i], i, array);
          }
        } else {
          // object
          for (var prop in array) {
            iterator.call(context, array[prop], prop, array);
          }
        }
      } catch(e) {
        if (e != pulp.Break) throw e;
      }
      return array;
    };
  }
  if (!$p.isNative(Array.prototype.indexOf)) {
    // add a slower version of arrayIndexOf for browsers without it
    $p.arrayIndexOf = /** @ignore */ function(arr, val, startIdx) {
      if (typeof arr === 'string') {
        arr = arr.split(' ');
      }
      startIdx = startIdx || 0;
      startIdx = (startIdx < 0 ? arr.length + startIdx : startIdx);
      for (var i = startIdx, len = arr.length; i < len; i++) {
        if (arr[i] === val) {
          return i;
        }
      }
      return -1;
    }  
  }
  if (!$p.isNative(String.prototype.trim)) {
    // add trim for browsers that don't implement it
    $p.trim = /** @ignore */ function(string) {
      return String(string).replace(/^\s+/, '').replace(/\s+$/, '');
    };
  }
  // counter for global unique id
  var guid = 0;

  /** 
   * Alias of {@link pulp.base.forEach}
   * @name pulp.base.each
   * @function
   */
  $p.each = $p.forEach;

})();