/**
 * extensions to core JavaScript objects: Function, Array...
 */

/**
 * Class method for copying psuedo-arrays to *real* arrays, such
 * as function arguments and dom node collections (ie, document.getElementsByTagName("div"))
 * @param {Object} original array
 * @return {Array} new *real* array
 */
Array.Copy = function(original){
  var i,result = [];
  for(i=0;i<original.length;i++){
    result.push(original[i]);
  }
  return result;
}
/**
 * alters the context in which a method will run, can be handy when used in conjunction with events
 * @param {Object} context for method to run (sets up the "this" reference)
 * @param... all additional parameters are passed to the bound method as parameters
 * @return {Function} invoking this function in any context will run the method in the correct context
 */
/*
  example:
  var myObject = {
    name:"fred"
  };
  function doTest(param1,param2){
    alert(this.name +'\n'+ param1 +'\n'+ param2);
  }
  var boundMethod = doTest.bind(myObject,"12","jane");
  boundMethod();
*/
Function.prototype.bind = function() {
  var method = this;
  var args = Array.Copy(arguments);
  var obj = args.shift();
  return function() {
    return method.apply(obj, args);
  };
}

// JavaScript 1.6 methods
if(!Array.forEach){
  // perform action on each item of array - used in place of: for(i=0;i<myArray.length;i++) {...}
  /*
    example:
    var myArray = ['apple','banana','pear'];
    myArray.forEach(function(fruit){
      document.write(fruit);
    });
  */
	Array.prototype.forEach = function(fn, thisObj) {
    var scope = thisObj || window;
    for ( var i=0, j=this.length; i < j; ++i){
      fn.call(scope, this[i], i, this);
    }
  }
}
if(!Array.every){
  // check if every item in array matches some criteria
  /*
    example:
    var myArray = [
      {name:'apple',type:'fruit'},
      {name:'steak',type:'meat'},
      {name:'pear',type:'fruit'}
    ];
    // returns false
    var allItemsAreFruits = myArray.every(function(food){
      return (food.type == "fruit");
    });
  */
  Array.prototype.every =  function(fn, thisObj) {
    var scope = thisObj || window;
    for ( var i=0, j=this.length; i < j; ++i){
      if(!fn.call(scope, this[i], i, this)){
        return false;
      }
    }
    return true;
  }
}
if(!Array.some){
  // check if every item in array matches some criteria
  /*
    example:
    var myArray = [
      {name:'apple',type:'fruit'},
      {name:'steak',type:'meat'},
      {name:'pear',type:'fruit'}
    ];
    // returns true
    var someItemsAreFruits = myArray.some(function(food){
      return (food.type == "fruit");
    });
  */
  Array.prototype.some = function(fn, thisObj) {
      var scope = thisObj || window;
    for ( var i=0, j=this.length; i < j; ++i){
          if(fn.call(scope, this[i], i, this)){
              return true;
          }
      }
      return false;
  }
}
if(!Array.map){
  // create a new array based on contents of original array
  /*
    example:
    var myArray = [
      {name:'apple',type:'fruit'},
      {name:'steak',type:'meat'},
      {name:'pear',type:'fruit'}
    ];
    var arrayOfHtml = myArray.map(function(food){
      return "<div>"+ food.name +" ("+ food.type +")</div>";
    });
  */
  Array.prototype.map = function(fn, thisObj) {
      var scope = thisObj || window;
      var a = [];
      for ( var i=0, j=this.length; i < j; ++i){
          a.push(fn.call(scope, this[i], i, this));
      }
      return a;
  }
}
if(!Array.filter){
  // create a new array of filtered results
  /*
    example:
    var myArray = [
      {name:'apple',type:'fruit'},
      {name:'steak',type:'meat'},
      {name:'pear',type:'fruit'}
    ];
    // returns true
    var fruits = myArray.map(function(food){
      return (food.type == "fruit");
    });
  */
  Array.prototype.filter =  function(fn, thisObj) {
      var scope = thisObj || window;
      var a = [];
      for ( var i=0, j=this.length; i < j; ++i){
          if(!fn.call(scope, this[i], i, this)){
              continue;
          }
          a.push(this[i]);
      }
      return a;
  }
}
if(!Array.indexOf){
  // returns index of specified element (-1 if not found)
  Array.prototype.indexOf = function(el, start) {
      start = start || 0;
      for ( var i=start, j=this.length; i < j; ++i){
          if(this[i] === el){
              return i;
          }
      }
      return -1;
  }
}
if(!Array.lastIndexOf){
  // returns the last index of specified element (-1 if not found)
  Array.prototype.lastIndexOf = function(el, start) {
      start = start || this.length;
      if(start >= this.length){
          start = this.length;
      }
      if(start < 0){
           start = this.length + start;
      }
      for ( var i=start; i >= 0; --i){
          if(this[i] === el){
              return i;
          }
      }
      return -1;
  }
}
// more useful array methods
if(!Array.splice){
  Array.prototype.splice = function (iIndex , iLength ) {
    var i,aResult  = new Array();
    var aRemoved  = new Array();
    for (i=0; i < iIndex; i++){
      aResult.push(this[i]);
    }
    for (i=iIndex; i < iIndex+iLength; i++) {
     aRemoved.push(this[i]);
    }
    if (arguments.length > 2) {
      for (i=2; i < arguments.length; i++) {
        aResult.push(arguments[i]);
      }
    }
    for (i=iIndex+iLength; i < this.length; i++) {
      aResult.push(this[i]);
    }
    for (i=0; i < aResult.length; i++) {
      this[i] = aResult[i];
    }
    this.length = aResult.length;
    return aRemoved;
  }
}
if(!Array.remove){
  Array.prototype.remove = function (vItem )  {
    this.removeAt(this.indexOf(vItem));
    return vItem;
  }
}
if(!Array.removeAt){
  Array.prototype.removeAt = function (iIndex )  {
    var vItem = this[iIndex];
    if (vItem) {
      this.splice(iIndex, 1);
    }
    return vItem;
  }
}



// object/JSON sorting
(function() {
    // local object which creates comparator functions for sorting arrays of objects
    var Comparator = {
        cache:{},
        getComparator:function(array,criteria){
            // if array is empty, we can exit now
            if(array.length == 0){
                return function(a,b){return 0;};
            }
            // criteria are function arguments, need  to turn into 'real' array
            criteria = Array.Copy(criteria);
            var criteriaId = criteria.join();
            if(!Comparator.cache[criteriaId]){
                // generate function
                // "new Function" is generally not a good idea, but actually provides the best for performance in this case
                var functionBody = [];
                var thisCriteria,i;
                functionBody.push("  var val = 0;");
                for (i = 0; i < criteria.length; i++) {
                    thisCriteria = criteria[i];
                    switch(typeof array[0][thisCriteria]){
                        case "string":
                            functionBody.push("  val = (b."+ thisCriteria +" < a."+ thisCriteria +") - (a."+ thisCriteria +" < b."+ thisCriteria +");");
                            break;
                        case "number":
                            functionBody.push("  val = a."+ thisCriteria +" -  b."+ thisCriteria +";");
                            break;
                        case "boolean":
                            functionBody.push("  val = (a."+ thisCriteria +"*-1) -  (b."+ thisCriteria +"*-1);");
                            break;
                    }
                    functionBody.push("  if (val != 0) {");
                    // look ahead for -1;
                    if(criteria[i+1] === -1){
                        functionBody.push("    return val * -1;");
                    }else{
                        functionBody.push("    return val;");
                    }
                    functionBody.push("  }");
                }
                functionBody.push("  return val;");
                Comparator.cache[criteriaId] = new Function("a","b",functionBody.join("\n"));
            }
            return Comparator.cache[criteriaId];
        }
    };

    /**
        Object sorting (JSON sorting) method. To have a field use reverse order (DESC), make -1 the following parameter (see examples)
        Here is a sample objectSort call for DISTANCE sorting in SPG:
            searchResults.objectSort("isParticipating","favorite","distance","hasCash","cityName","propertyName");
        Simple example - for and array of people, sort by last name, then by first name
            people.sort("lsatName","firstName");
        Simple example - Sort people by age oldest to youngest, then by last name (a-z)
            people.sort("age",-1,"lastName");

    **/
    Array.prototype.objectSort = function() {
        // perform sort and return instance array
        this.sort(Comparator.getComparator(this,arguments))
        return this;
    };
})();

// object/JSON filtering
(function() {
    var FilterManager = {
        cache:{},
        getFilter:function(array,criteria){
            if(array.length === 0 || criteria.length === 0){
                return function(){return true;};
            }
            var criteriaId = criteria.join();
            if(!FilterManager.cache[criteriaId]){
                var functionBody = [];
                var conditions = [];
                var quoteChar = "";
                var thisField,thisValue,thisOperator,i,innerCondition,prefix,suffix;
                for (i = 0; i < criteria.length; i+=3) {
                    prefix = "";
                    suffix = "";
                    thisField = criteria[i];
                    thisValue = criteria[i+1];
                    // if value is not an array, turn it into one
                    if(!(typeof thisValue == "object" && thisValue.length > 0)){
                        thisValue = [thisValue];
                    }
                    thisOperator = criteria[i+2];
                    // should quoteChar be used? for now, always use quotes
                    //                    quoteChar = (typeof array[0][thisField] == "string") ? "\"":"";
                    quoteChar = "\"";
                    switch(thisOperator){
                        case "equals":
                            prefix = "item."+ thisField +" == "+ quoteChar;
                            suffix = quoteChar ;
                            break;
                        case "notEquals":
                            prefix = "item."+ thisField +" != "+ quoteChar;
                            suffix = quoteChar ;
                            break;
                        case "greaterThan":
                            prefix = "item."+ thisField +" > "+ quoteChar;
                            suffix = quoteChar ;
                            break;
                        case "lessThan":
                            prefix = "item."+ thisField +" < "+ quoteChar;
                            suffix = quoteChar ;
                            break;
                        case "greaterThanEquals":
                            prefix = "item."+ thisField +" >= "+ quoteChar;
                            suffix = quoteChar ;
                            break;
                        case "lessThanEquals":
                            prefix = "item."+ thisField +" <= "+ quoteChar;
                            suffix = quoteChar ;
                            break;
                        case "contains":
                            prefix = "item."+ thisField +".indexOf("+ quoteChar;
                            suffix = quoteChar +") > -1";
                            break;
                        default:
                            // if invalid operator passed, do not use this criteria
                            continue;
                    }
                    innerCondition = [];
                    thisValue.forEach(function(val){
                        innerCondition.push(prefix + val + suffix);
                    });
                    conditions.push("("+ innerCondition.join(" ||  ") +")");
                }
                functionBody.push("if( "+ conditions.join(" && ") +"){");
                functionBody.push("  return true;");
                functionBody.push("}");
                functionBody.push("return false;");
                FilterManager.cache[criteriaId] = new Function("item",functionBody.join("\n"));

            }
            return FilterManager.cache[criteriaId];
        }
    };

    Array.prototype.setFilter = function(field,value,operator) {
        // first time calling setFilter, setup objects and methods
        var criteria = [];
        var thisArray = this;
        thisArray.setFilter = function(field,value,operator){
            if(arguments.length === 0){
                criteria.length = 0;
                return thisArray;
            }
            operator = operator || "equals";
            criteria.push(field,value,operator);
            return thisArray;
        };
        thisArray.objectFilter = function(){
            var result = thisArray.filter(FilterManager.getFilter(thisArray,criteria));
            criteria.length = 0;
            return result;
        };

        // set this filter criteria and return array (note - this code only runs first time setFilter is called)
        thisArray.setFilter(field,value,operator);
        return thisArray;
    };
    // dummy placeholder method... here just in case objectFilter is called before setFilter
    Array.prototype.objectFilter = function(){
        // behave like normal, return copy of this array         
        return Array.Copy(this);
    }
})();