
var base64String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var base64Pad = '=';

var base64Replacements = [
  ['a', '('],
  ['b', ')'],
  ['c', '{'],
  ['+', 'a'],
  ['/', 'b'],
  ['=', 'c'],
  ['(', '*'],
  [')', '-'],
  ['{', '~']
];

for (var i = 0; i < base64Replacements.length; i++) {
  base64String = base64String.replace(base64Replacements[i][0], base64Replacements[i][1]);
  base64Pad = base64Pad.replace(base64Replacements[i][0], base64Replacements[i][1]);
}


// {{{ numberArrayToBase64(aNumbers)
function numberArrayToBase64(aNumbers) {
  var sOutput = "";
  var p = "";
  var c = aNumbers.length % 3;
  var n24 = "";
  var n6 = new Array();
  
  if (!aNumbers.length) {
    return "";
  }
  
  //Add a right zero pad to make this string a multiple of 3 characters
  if (c > 0) {
    while (c < 3) {
      p = p + base64Pad;
      aNumbers[aNumbers.length] = 0;
      c = c + 1;
    }
  }
  
  //Increment over the length of the string, three characters at a time
  for (var c = 0; c < aNumbers.length; c = c+3) {
    
    //these 8-bit ascii characters become one 24-bit number
    n24 = Math.pow(2,(8*2)) * aNumbers[c] + Math.pow(2,(8*1)) * aNumbers[c+1] + Math.pow(2,(8*0)) * aNumbers[c+2];
    
    //this 24-bit number gets separated into four 6-bit numbers
    n6 = new Array();
    n6[n6.length] = (n24 / Math.pow(2,(6*3))) & 63;
    n6[n6.length] = (n24 / Math.pow(2,(6*2))) & 63;
    n6[n6.length] = (n24 / Math.pow(2,(6*1))) & 63;
    n6[n6.length] = (n24 / Math.pow(2,(6*0))) & 63;
    
    //those four 6-bit numbers are used as indecies into the base64String
    sOutput = sOutput + base64String.charAt(n6[0]) + base64String.charAt(n6[1]) + base64String.charAt(n6[2]) + base64String.charAt(n6[3]);
    
  }
  
  return sOutput.substring(0, sOutput.length-p.length) + p;
}
// }}} numberArrayToBase64(aNumbers)

// {{{ base64ToNumberArray(s)
function base64ToNumberArray(s) {
  //replace padding with zero-pad
  var p = (s.charAt(s.length-1) == base64Pad ? (s.charAt(s.length-2) == base64Pad ? 'AA' : 'A') : "");
  
  var r = new Array();
  
  var base64chars = base64String.split("");
  var base64inv = {};
  for (var i = 0; i < base64chars.length; i++) {
    base64inv[base64chars[i]] = i;
  }
  
  s = s.substr(0, s.length - p.length) + p;
  
  //remove non-base64 characters
  //s = s.replace(new RegExp('[^' + base64String + ']', 'g'), "");
  
  //loop over length of string, four characters at a time
  for (var c = 0; c < s.length; c += 4) {
    //each of the four characters represents a 6-bit index in the base64 character list
    var n = (base64inv[s.charAt(c)] << 18) + base64inv[s.charAt(c+3)] +
      (base64inv[s.charAt(c+1)] << 12) + (base64inv[s.charAt(c+2)] << 6);
    
    //split the 24-bit number into three 8-bit characters
    r[r.length] = (n >>> 16) & 255;
    r[r.length] = (n >>> 8) & 255;
    r[r.length] = n & 255;
  }
  
  //remove zero pad
  r.length = r.length - p.length;
  return r;
}
// }}} base64ToNumberArray(s)

// {{{ check for prototype
function convertVersionString(versionString) {
  var r = versionString.split('.');
  return parseInt(r[0])*100000 + parseInt(r[1])*1000 + parseInt(r[2]);
}

if ((typeof Prototype == 'undefined') ||
    (typeof Element == 'undefined') ||
    (typeof Element.Methods == 'undefined') ||
    (convertVersionString(Prototype.Version) <
     convertVersionString('1.6.0'))) {
  throw("FilterUtility.js requires the Prototype JavaScript framework >= " + '1.6.0');
}
// }}} check for prototype

// {{{ require other .js files
$A(document.getElementsByTagName("script")).findAll(
  function(s) {
    return (s.src && s.src.match(/FilterUtility\.js(\?.*)?$/))
  }
).each(
  function(s) {
    //alert(s);
    var path = s.src.replace(/FilterUtility\.js(\?.*)?$/,'');
    var includes = s.src.match(/\?.*load=([a-z,]*)/);
    (includes ? includes[1] : 'Abstract,Link,Checkbox,Range').split(',').each(
      function(include) {
        document.write('<sc'+'ript type="text/javascript" src="' + path+include+'Filter.js"><\/script>');
      }
    );
  }
);
// }}} require other .js files

var FilterUtility = Class.create();
FilterUtility.prototype = {
  
  // {{{ initialize() (constructor)
  initialize:function() {
    //alert("initializing...");
    
    this.aFilterDisplays = new Array();
    this.stListeners = new Hash();
    
    this.aFilterDisplays.push(new LinkFilter(this));
    this.aFilterDisplays.push(new CheckboxFilter(this));
    this.aFilterDisplays.push(new RangeFilter(this));
  },
  // }}} initialize() (constructor)
  
  // {{{ addListener(eventName, listener)
  addListener:function(eventName,listener) {
    if (!this.stListeners.get(eventName)) {
      this.stListeners.set(eventName, new Array());
    }
    
    this.stListeners.get(eventName).push(listener);
  },
  // }}} addListener(eventName, listener)
  
  // {{{ itemOnClick(item)
  itemOnClick:function(item) {
    var aReturn = new Array();
    aReturn[0] = false;
    aReturn[1] = false;
    
    for (var i = 0; i < this.aFilterDisplays.length; i++) {
      aReturn = this.aFilterDisplays[i].itemOnClick(item);
      if (aReturn[0]) {
        //matched filter item to a type
        this._fireEvent("itemOnClick", item);
        this._fireEvent("updateResults", this.encodeAppliedFilters());
        return aReturn[1];
      }
    }
    
    return aReturn[1];
  },
  // }}} itemOnClick(item)
  
  // {{{ clearFilters(filterTypeID)
  clearFilters:function(filterTypeID) {
    var bReturn = false;
    
    for (var i = 0; i < this.aFilterDisplays.length; i++) {
      bReturn = this.aFilterDisplays[i].clearFilters(filterTypeID);
      
      if (bReturn) {
        //atched filter item to a type
        this._fireEvent("updateResults", this.encodeAppliedFilters());
        return bReturn;
      }
    }
    
    return bReturn;
  },
  // }}} clearFilters(filterTypeID)
  
  // {{{ getAppliedFiltersStruct()
  getAppliedFiltersStruct:function() {
    var oAppliedFilterBits = new Hash();
    var filterTypeID = "";
    
    for (var i = 0; i < this.aFilterDisplays.length; i++) {
      filterAppliedBits = this.aFilterDisplays[i].getAppliedFilters();
      var filterTypeIDs = filterAppliedBits.keys();
      
      for (var j = 0; j < filterTypeIDs.length; j++) {
        filterTypeID = filterTypeIDs[j];
        for (var k = 0; k < filterAppliedBits.get(filterTypeID).length; k++) {
          bitNumber = filterAppliedBits.get(filterTypeID)[k];
          
          if (!oAppliedFilterBits.get(filterTypeID)) {
            oAppliedFilterBits.set(filterTypeID, new Array());
          }
          
          oAppliedFilterBits.get(filterTypeID).push(bitNumber);
        }
      }
    }
    
    return oAppliedFilterBits;
  },
  // }}} getAppliedFiltersStruct()
  
  // {{{ allSelected(filterTypeID)
  allSelected:function(filterTypeID) {
    var aReturn = new Array();
    aReturn[0] = false; //indicates that the filterTypeID was found
    aReturn[1] = false; //indicates whether or not all are selected
    
    for (var i = 0; i < this.aFilterDisplays.length; i++) {
      aReturn = this.aFilterDisplays[i].allSelected(filterTypeID);
      
      if (aReturn[0]) {
        //matched filter type
        return aReturn[1];
      }
    }
    
    return aReturn[1];
  },
  // }}} allSelected(filterTypeID)
  
  // {{{ encodeAppliedFilters()
  encodeAppliedFilters:function() {
    return this._encodeAppliedFilters(this.getAppliedFiltersStruct());
  },
  // }}} encodeAppliedFilters()
  
  // {{{ _encodeAppliedFilters(oAppliedFilterBits)
  _encodeAppliedFilters:function(oAppliedFilterBits) {
    var filterTypeIDs = oAppliedFilterBits.keys();
    var filterTypeID = "";
    var numFilterItemBytes = 0;
    var aFilterItemNumbers = new Array();
    var byteNum = "";
    var bitNum = "";
    var aNumbers = new Array();
    var EncodingFormat = "";
    
    for (var i = 0; i < filterTypeIDs.length; i++) {
      filterTypeID = filterTypeIDs[i];
      aFilterItemNumbers = new Array();
      
      //determine the maximum bit number
      maxBit = 0;
      for (var j = 0; j < oAppliedFilterBits.get(filterTypeID).length; j++) {
        if (Math.round(oAppliedFilterBits.get(filterTypeID)[j]) > maxBit) {
          maxBit = Math.round(oAppliedFilterBits.get(filterTypeID)[j]);
        }
      }
      
      //knowing the maximum bit number, we can determine the number of bytes
      //needed to encode that high of bit number
      numFilterItemBytes = Math.ceil(maxBit / 8);
      
      //fill aFilterItemNumbers array with 0's
      for (var j = 0; j < numFilterItemBytes; j++) {
        aFilterItemNumbers[j] = 0;
      }
      
      for (var j = 0; j < oAppliedFilterBits.get(filterTypeID).length; j++) {
        byteNum = Math.ceil(oAppliedFilterBits.get(filterTypeID)[j] / 8);
        bitNum = (oAppliedFilterBits.get(filterTypeID)[j]-1) % 8;
        aFilterItemNumbers[byteNum-1] = (aFilterItemNumbers[byteNum-1] | Math.pow(2,bitNum));
      }
      
      //add this filter type's numbers to the aNumbers array
      EncodingFormat = "00";
      if (filterTypeID >= 1 && filterTypeID <= 7) {
        if (numFilterItemBytes >= 1 && numFilterItemBytes <= 7) {
          EncodingFormat = "00";
        } else if (numFilterItemBytes >= 1 && numFilterItemBytes <= 255) {
          EncodingFormat = "10";
        } else if (numFilterItemBytes >= 1 && numFilterItemBytes <= ( Math.pow(2, 8*3) - 1)) {
          EncodingFormat = "11";
        } else {
          EncodingFormat = "";
          //TODO: EncodingFormat theoretically possible, but not yet supported
        }
      } else if (filterTypeID >= 1 && filterTypeID <= 63) {
        if (numFilterItemBytes >= 1 && numFilterItemBytes <= 255) {
          EncodingFormat = "10";
        } else {
          EncodingFormat = "";
          //TODO: EncodingFormat theoretically possible, but not yet supported
        }
      } else if (filterTypeID >= 1 && filterTypeID <= 255) {
        if (numFilterItemBytes >= 1 && numFilterItemBytes <= 63) {
          EncodingFormat = "01";
        } else if (numFilterItemBytes >= 1 && numFilterItemBytes <= 255) {
          EncodingFormat = "11";
        } else {
          EncodingFormat = "";
          //TODO: EncodingFormat theoretically possible, but not yet supported
        }
      } else {
        EncodingFormat = "";
        //TODO: EncodingFormat theoretically possible, but not yet supported
      }
      
      if (!EncodingFormat.length) {
        throw "Filter Limits Exceeded: The number of filteritem bytes, " + numFilterItemBytes + ", must be 255 or less; The FilterTypeID, " + filterTypeID + ", must be 255 or less. Theoretically, these limits could be much higher in a backward-compatible way, but the Filter components have not implemented higher limits at this time.";
      }
      
      switch (EncodingFormat) {
        case "00":
          aNumbers.push(0 + Math.round((numFilterItemBytes * Math.pow(2, 3))) + Math.round(filterTypeID));
          break;
        case "01":
          aNumbers.push(64 + Math.round(numFilterItemBytes));
          aNumbers.push(filterTypeID);
          break;
        case "10":
          aNumbers.push(128 + Math.round(filterTypeID));
          aNumbers.push(numFilterItemBytes);
          break;
        case "11":
          aNumbers.push(192 + 8 + 1);
          aNumbers.push(numFilterItemBytes);
          aNumbers.push(filterTypeID);
          break;
      }
      
      
      for (var j = 0; j < aFilterItemNumbers.length; j++) {
        aNumbers.push(aFilterItemNumbers[j]);
      }
      
    }
    
    return numberArrayToBase64(aNumbers);
  },
  // }}} _encodeAppliedFilters(oAppliedFilterBits)
  
  // {{{ _fireEvent(eventName)
  _fireEvent:function(eventName, arg1, arg2, arg3) {
    var listeners = this.stListeners.get(eventName);
    
    if (listeners) {
      for (var i = 0; i < listeners.length; i++) {
        if (listeners[i] instanceof Array) {
          //listeners[i][0] is an object
          //listeners[i][1] is a function name in the object
          listeners[i][0][ listeners[i][1] ](arg1, arg2, arg3);
        } else {
          listeners[i](arg1, arg2, arg3);
        }
      }
    }
  },
  // }}} _fireEvent(eventName)
  
  
  
  // {{{ addAppliedFilters(appliedFilters)
  addAppliedFilters: function(appliedFilters) {
    for (var i = 0; i < this.aFilterDisplays.length; i++) {
      this.aFilterDisplays[i].addAppliedFilters(appliedFilters);
    }
  },
  // }}} addAppliedFilters(appliedFilters)
  
  // {{{ _numberArrayToBitNumberArray(aNumberArray)
  _numberArrayToBitNumberArray: function(aNumberArray) {
    var j = 0;
    var k = 0;
    
    var aReturn = new Array();
    
    for (j = aNumberArray.length - 1; j >= 0; j--) {
      for (k = 0; k < 8; k++) {
        if (aNumberArray[aNumberArray.length - j - 1] & Math.pow(2, k)) {
          aReturn.push( (k+1) + (8* (aNumberArray.length - j - 1)));
        }
      }
    }
    
    return aReturn;
  },
  // }}} _numberArrayToBitNumberArray(aNumberArray)
  
  // {{{ decodeAppliedFilters(sEncodedFilters)
  decodeAppliedFilters: function(sEncodedFilters) {
    var unsignedByteArray = base64ToNumberArray(sEncodedFilters);
    var oAppliedFilterBits = new Hash();
    var i = 0;
    var j = 0;
    var k = 0;
    var aRawAppliedFilters = new Array();
    
    if (!unsignedByteArray.length) {
      return oAppliedFilterBits;
    }
    
    // {{{ decode binary into aRawAppliedFilters
    while (i < unsignedByteArray.length) {
      var oRawAppliedFilter = new Hash();
      
      // get header byte
      var header = unsignedByteArray[i];
      
      var encodingFormat = "";
      if (! (64 & header || 128 & header)) {
        encodingFormat = "00";
      } else if (64 & header && !(128 & header)) {
        encodingFormat = "01";
      } else if (128 & header && !(64 & header)) {
        encodingFormat = "10";
      } else {
        encodingFormat = "11";
      }
      
      //assuming that unsignedByteArray is long enough, maybe some day this will
      //have a nice check first
      
      switch (encodingFormat) {
        case '00':
          n = Math.floor(header / Math.pow(2, 3));
          oRawAppliedFilter.set('filterTypeID', header - n*Math.pow(2, 3));
          break;
        case '01':
          n = header - 64;
          
          //move to next byte
          i++;
          
          oRawAppliedFilter.set('filterTypeID', unsignedByteArray[i]);
          break;
        case '10':
          oRawAppliedFilter.set('filterTypeID', header - 128);
          
          //move to next byte
          i++;
          
          n = unsignedByteArray[i];
          break;
        case '11':
          if (header != 201) {
            alert("Error: Filter Limites Exceeded");
            return;
          }
          
          // move to next byte
          i++;
          n = unsignedByteArray[i];
          
          // move to next byte
          i++;
          oRawAppliedFilter.set('filterTypeID', unsignedByteArray[i]);
          break;
      }
      
      //check to make sure the binary string is long enough
      if (unsignedByteArray.length - i < n) {
        //TODO: real error handling, throw something interesting
        alert("ERROR: invalid sEncodedFilters");
        return;
      }
      
      //move to next byte
      i++;
      
      //now the filter item bytes
      oRawAppliedFilter.set('aFilterItems', new Array());
      var endI = i+n;
      for (; i < endI; i++) {
        oRawAppliedFilter.get('aFilterItems').push(unsignedByteArray[i]);
      }
      
      aRawAppliedFilters.push(oRawAppliedFilter);
    }
    // }}} decode binary into aRawAppliedFilters
    
    // {{{ decode aRawAppliedFilters into oAppliedFilterBits
    for (i = 0; i < aRawAppliedFilters.length; i++) {
      var filterTypeID = aRawAppliedFilters[i].get('filterTypeID');
      if (!oAppliedFilterBits.get(filterTypeID)) {
        oAppliedFilterBits.set(filterTypeID, this._numberArrayToBitNumberArray( aRawAppliedFilters[i].get('aFilterItems') ));
      }
    }
    // }}} decode aRawAppliedFilters into oAppliedFilterBits
    
    return oAppliedFilterBits;
  },
  // }}} decodeAppliedFilters(sEncodedFilters)
  
  // {{{ addEncodedAppliedFilters(sEncodedFilters)
  addEncodedAppliedFilters: function(sEncodedFilters) {
    this.addAppliedFilters( this.decodeAppliedFilters(sEncodedFilters) );
  },
  // }}} addEncodedAppliedFilters(sEncodedFilters)
  
  // {{{ clearAppliedFilters()
  clearAppliedFilters: function() {
    for (var i = 0; i < this.aFilterDisplays.length; i++) {
      this.aFilterDisplays[i].clearAppliedFilters();
    }
  }
  // }}} clearAppliedFilters()
  
  
  
};
