// These variables will be used within this file
var masterList = null;      // array:   all product objects
var filteredList = [ ];     // array:   product objects matching the active filter
var processingLock = false; // boolean: is some operation in progress?
var cd_prefix;              // string:  rendered CD's will have a name prefix
var sliderMinValues = { };  // object:  used to track minimum values while filtering
var skipMinRatingChange;    // boolean: used during filter resets

// These variables will be initialized in the dashboard template using JSON's
var informa_last_updated; // string: date for last Informa feed update (if applicable)
var bitmasks = { };       // object:  bitmask constants, organized by checkbox group
var defaultFilter = { };  // object:  default filter parameters (for resetting)
var filter = { };         // object:  filter parameters
var producttypes = { };   // object:  producttype id constants
var producttype_id;       // integer: current producttype id
var render_onload;        // boolean: submit dashboard forms upon load?

////////////////////////////////////////////////////////////////////////////////

/**
 * Escape a string for use within a RegExp.  The < and > characters were removed
 * from the replacement regex, as they do not appear to be special characters
 * according to JSLint and the Mozilla references.
 *
 * @source http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_preg_quote/
 * @see http://www.php.net/preg_quote
 * @param string str
 * @return string
 */
function preg_quote(str) {
  return (str+'').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\|\:])/g, "\\$1");
}

/**
 * Format a number with grouped thousands.
 *
 * @source http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/
 * @see http://www.php.net/number_format
 * @param numeric number        Number to format
 * @param integer decimals      Decimal places (optional, default: 0)
 * @param string  dec_point     Decimal point character (optional, default: '.')
 * @param string  thousands_sep Thousands separator character (optional, default: ',')
 * return string
 */
function number_format(number, decimals, dec_point, thousands_sep) {
  var n = number, c = isNaN(decimals = Math.abs(decimals)) ? 0 : decimals;
  var d = dec_point == undefined ? "." : dec_point;
  var t = thousands_sep == undefined ? "," : thousands_sep, s = n < 0 ? "-" : "";
  var i = parseInt(n = Math.abs(+n || 0).toFixed(c)) + "", j = (j = i.length) > 3 ? j % 3 : 0;
  
  return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + Math.abs(n - i).toFixed(c).slice(2) : "");
}


/**
 * Generate a string representation of a VTY number, as either a rounded
 * absolute value, or abbreviated with a 'k' suffix for one thousand.
 *
 * @param float vty
 * @return string
 */
function makeVtyDisplay(vty) {
  vty = Math.abs(Math.round(vty));
  
  // VTY display should be either an abbreviated form or rounded integer
  if (vty >= 10000) {
    return Number(vty/1000).toFixed(1).replace('.0', '') + 'k';
  } else {
    return vty.toString();
  }
}

/**
 * Return the value parameter if it falls between min and max.  Otherwise,
 * return whichever boundary (min or max) that the value exceeds.  If the
 * value parameter is not a number, min will be returned.
 *
 * @param integer value
 * @param integer min
 * @param integer max
 * @return integer
 */
function constrainValue(value, min, max) {
  return (isNaN(value) || value < min ? min : (value > max ? max : value));
}

/**
 * Change event callback for sliders and their associated input fields.
 * Constrains the input field to the slider's min/max limits and stepping.
 *
 * @param object  fieldObj     Input field as jQuery object
 * @param object  sliderObj    Slider element as jQuery object
 */
function syncSlider(fieldObj, sliderObj) {
    if (!fieldObj || !sliderObj) {
      return;
    }
    
    var min = sliderObj.data('min.slider');
    var max = sliderObj.data('max.slider');
    
    if (isNaN(min) || isNaN(max)) {
      return;
    }
    
    // Constrain field value to the slider's range
    fieldObj.val(constrainValue(parseInt(fieldObj.val(), 10), min, max));
    
    
    /* Do not force the text input to match the slider's stepping.  In the case
     * of sliders with very large ranges, this will still allow the user to
     * input a precise value via the text input.
     */
    
    // Sync slider to the field value
    sliderObj.slider('value', parseInt(fieldObj.val(), 10));
}

/**
 * Sets the lower boundaries for certain sliders of the current product type.
 * The following properties of the object parameter may be provided:
 *
 *   Credit cards: rate, intro_rate, transfer_rate
 *   CD's: apy
 *
 * Note that for sliders that restrict by maximum value (e.g. Max APR), the
 * minimum provided will be rounded up for the slider's lower bound.  For those
 * sliders that restrict minimum value (e.g. Min APY), minimums provided will be
 * rounded down.  This ensures that even at the lowest point of the slider, it
 * will match at least one product.
 *
 * @param object min
 */
function setSliderLowerBounds(min) {
  
  // TODO: Determine why this is causing a Javascript null access error
  // see: http://groups.google.com/group/jquery-en/browse_thread/thread/e05bfb7aafe870a1/f1465114941a4391
  return;
  
  if (typeof(min) != 'object') {
    return;
  }
  
  if (producttype_id == producttypes.CD) {
    if (typeof(min.apy) == 'number') {
      var min_apy = Math.floor(min.apy);
      $('#cd_apy_min_slider').slider('disable');
      $('#cd_apy_min_slider').data('min.slider', min_apy);
      $('#cd_apy_min_slider .labels .min').text(min_apy+'%');
      $('#cd_apy_min_slider').slider('enable');
      syncSlider($('#cd_apy_min'), $('#cd_apy_min_slider'));
    }
  } else if (producttype_id == producttypes.CREDIT_CARD) {
    if (typeof(min.rate) == 'number') {
      var min_rate = Math.ceil(min.rate);
      $('#cc_rate_max_slider').slider('disable');
      $('#cc_rate_max_slider').data('min.slider', min_rate);
      $('#cc_rate_max_slider .labels .min').text(min_rate+'%');
      $('#cc_rate_max_slider').slider('enable');
      syncSlider($('#cc_rate_max'), $('#cc_rate_max_slider'));
    }
    
    if (typeof(min.intro_rate) == 'number') {
      var min_intro_rate = Math.ceil(min.intro_rate);
      $('#cc_intro_rate_max_slider').slider('disable');
      $('#cc_intro_rate_max_slider').data('min.slider', min_intro_rate);
      $('#cc_intro_rate_max_slider .labels .min').text(min_intro_rate+'%');
      $('#cc_intro_rate_max_slider').slider('enable');
      syncSlider($('#cc_intro_rate_max'), $('#cc_intro_rate_max_slider'));
    }
    
    if (typeof(min.transfer_rate) == 'number') {
      var min_transfer_rate = Math.ceil(min.transfer_rate);
      $('#cc_transfer_rate_max_slider').slider('disable');
      $('#cc_transfer_rate_max_slider').data('min.slider', min_transfer_rate);
      $('#cc_transfer_rate_max_slider .labels .min').text(min_transfer_rate+'%');
      $('#cc_transfer_rate_max_slider').slider('enable');
      syncSlider($('#cc_transfer_rate_max'), $('#cc_transfer_rate_max_slider'));
    }
  }
}

/**
 * Synchronize a checkbox group.  When a group has no specific options checked,
 * the "all" checkbox should remain set.  Otherwise, "all" should remain unset
 * while any other checkboxes are set.  This function expects the DOM id for the
 * "all" checkbox to be the groupClass suffixed with "_all".  The changedObj
 * parameter is optional.
 *
 * The actsAsRadio parameter is also optional.  If set, the checkbox group will
 * emulate a set of radio buttons: only one checkbox can be active at a time,
 * and clicking the active checkbox cannot uncheck it.
 *
 * @param string  groupClass CSS class for the checkbox group
 * @param object  changedObj Input field as jQuery object (if called from a change event)
 * @param boolean actAsRadio If true, checkbox group acts as a radio button group
 */
function syncCheckboxGroup(groupClass, changedObj, actAsRadio) {
  if (actAsRadio !== undefined && actAsRadio) {
    if (changedObj !== undefined && !changedObj.is(':checked') && !$('div.checklist.'+groupClass+' input[type=checkbox]').is(':checked')) {
      // Unchecking this box left the group unchecked, recheck it
      changedObj.attr('checked', true);
    } else if (changedObj !== undefined && changedObj.is(':checked')) {
      // Checking this box should uncheck everyone else in the group
      $('div.checklist.'+groupClass+' input[type=checkbox][id!="'+changedObj.attr('id')+'"]').attr('checked', false);
    }
  } else {
    var allFieldId = groupClass + '_all';
    
    // If the "any" checkbox was just checked, uncheck all other checkboxes
    if (changedObj !== undefined && changedObj.attr('id') == allFieldId && changedObj.is(':checked')) {
      $('div.checklist.'+groupClass+' input[type=checkbox]').attr('checked', false);
    }
    
    $('#'+allFieldId).attr('checked', !$('div.checklist.'+groupClass+' input[type=checkbox][id!="'+allFieldId+'"]').is(':checked'));
  }
}

/**
 * Saves field values to the backend session.
 *
 * @param object fieldObjs Selection of input fields (in a jQuery object)
 */
function saveToSession(fieldObjs) {
  if (!fieldObjs.length) {
    return;
  }
  
  var data = { producttype_id: producttype_id };
  
  fieldObjs.filter(':input').each(function(){
    $this = $(this);
    if ($this.is(':checkbox')) {
      data[$this.attr('name')] = $this.is(':checked') ? 1 : 0;
    } else {
      data[$this.attr('name')] = $this.val();
    }
  });
  
  // Apply focus to the body (correction for Internet Explorer)
  $('body')[0].focus();
    
  jQuery.get('/productpicker/saveFilter', data);
}

/**
 * Checks if the master list is ready (a non-null object).<b> 
 *
 * @return boolean
 */
function isMasterListReady() {
  return (typeof(masterList) == 'object' && masterList !== null);
}

/**
 * Examines all product records in an array and marks the product with the
 * greatest vty property.  The bestFieldName parameter will be evaluated as the
 * name of the boolean property to set for each product record (true for the
 * product with the greatest vty and false for all others).
 *
 * @param array  products  Array of products
 * @param string fieldName Name of boolean field to set on product records
 */
function setBestValue(list, bestFieldName) {
  // Ensure we have a valid list of products with vty fields
  if (typeof(list) != 'object' || list === null || list.length < 1 ||
      typeof(list[0].vty) == 'undefined') {
    return;
  }
  
  // Track index for the product with the greatest VTY
  var bestIndex = 0;
  
  for (var i = 0; i < list.length; i++) {
    list[i][bestFieldName] = false;
    
    // Remember index if this is the best VTY we've seen
    if (list[i].vty > list[bestIndex].vty) {
      bestIndex = i;
    }
  }
  
  // Set the flag for the product with the largest VTY
  list[bestIndex][bestFieldName] = true;
}

/**
 * Save appropriate form field values to the filter and create bitmasks for
 * attribute groups (corresponding to checkboxes).
 */
function updateFilter() {
  var fieldObj;
  
  for (var parameter in filter) {
    if (filter.hasOwnProperty(parameter)) {
      // Fetch the form field object using the parameter name as a DOM id
      fieldObj = $('#'+parameter);
      
      // Only process filter parameters that correspond to actual form fields
      if (fieldObj.length) {
        // Fetch checkboxes as booleans, and all other fields normally
        filter[parameter] = fieldObj.attr('type') == 'checkbox' ? fieldObj.is(':checked') : fieldObj.val();
      }
    }
  }
  
  // Handle bitmask and numeric parsing according to product type
  switch (producttype_id) {
    case producttypes.CD:
      filter.cd_duration_months = parseInt(filter.cd_duration_months, 10);
      filter.cd_deposit = parseInt(filter.cd_deposit, 10);
      filter.cd_apy_min = parseInt(filter.cd_apy_min, 10);
      
      // Create filter bitmasks based on selected checkboxes
      filter.m_type = (filter.cd_type_fixed_standard    ? bitmasks.m_type.is_type_fixed_standard    : 0) |
                      (filter.cd_type_fixed_ira         ? bitmasks.m_type.is_type_fixed_ira         : 0) |
                      (filter.cd_type_variable_standard ? bitmasks.m_type.is_type_variable_standard : 0) |
                      (filter.cd_type_variable_ira      ? bitmasks.m_type.is_type_variable_ira      : 0);
      
      // Determine the CD name prefix based on the active type filter
      if      (filter.cd_type_fixed_standard)    { cd_prefix = 'Fixed Rate '; }
      else if (filter.cd_type_variable_standard) { cd_prefix = 'Variable Rate '; }
      else if (filter.cd_type_fixed_ira)         { cd_prefix = 'Fixed Rate IRA '; }
      else if (filter.cd_type_variable_ira)      { cd_prefix = 'Variable Rate IRA '; }
      else                                       { cd_prefix = ''; }
      
      break;
    case producttypes.CREDIT_CARD:
      filter.cc_rate_max = parseInt(filter.cc_rate_max, 10);
      filter.cc_intro_rate_max = parseInt(filter.cc_intro_rate_max, 10);
      filter.cc_transfer_rate_max = parseInt(filter.cc_transfer_rate_max, 10);
      
      // Create filter bitmasks based on selected checkboxes
      filter.m_brand = (filter.cc_brand_amex       ? bitmasks.m_brand.is_brand_amex       : 0) |
                       (filter.cc_brand_visa       ? bitmasks.m_brand.is_brand_visa       : 0) |
                       (filter.cc_brand_mastercard ? bitmasks.m_brand.is_brand_mastercard : 0) |
                       (filter.cc_brand_discover   ? bitmasks.m_brand.is_brand_discover   : 0);
      
      filter.m_bank = (filter.cc_bank_bofa       ? bitmasks.m_bank.is_bank_bofa       : 0) |
                      (filter.cc_bank_capitalone ? bitmasks.m_bank.is_bank_capitalone : 0) |
                      (filter.cc_bank_chase      ? bitmasks.m_bank.is_bank_chase      : 0) |
                      (filter.cc_bank_citi       ? bitmasks.m_bank.is_bank_citi       : 0) |
                      (filter.cc_bank_hsbc       ? bitmasks.m_bank.is_bank_hsbc       : 0);
      
      filter.m_type = (filter.cc_type_business      ? bitmasks.m_type.is_type_business      : 0) |
                      (filter.cc_type_student       ? bitmasks.m_type.is_type_student       : 0) |
                      (filter.cc_type_creditbuilder ? bitmasks.m_type.is_type_creditbuilder : 0) |
                      (filter.cc_type_fixedrate     ? bitmasks.m_type.is_type_fixedrate     : 0) |
                      (filter.cc_type_reward        ? bitmasks.m_type.is_type_reward        : 0) |
                      (filter.cc_type_noannualfee   ? bitmasks.m_type.is_type_noannualfee   : 0);
      
      filter.m_reward = (filter.cc_reward_cashback            ? bitmasks.m_reward.is_reward_cashback            : 0) |
                        (filter.cc_reward_gasoline            ? bitmasks.m_reward.is_reward_gasoline            : 0) |
                        (filter.cc_reward_travelentertainment ? bitmasks.m_reward.is_reward_travelentertainment : 0) |
                        (filter.cc_reward_sports              ? bitmasks.m_reward.is_reward_sports              : 0) |
                        (filter.cc_reward_frequentflyer       ? bitmasks.m_reward.is_reward_frequentflyer       : 0);
      break;
  }
  
  // Parse string values for numeric fields (for all product types)
  filter.min_rating = parseInt(filter.min_rating, 10);
  filter.min_rating_count = parseInt(filter.min_rating_count, 10);
  
  /* The product picker's search field should be converted into a RegExp.  We'll
   * process this field manually since it's limited to the front-end and not
   * already defined in the filter JSON.
   */
  var searchTerms = jQuery.trim(filter.product_title_search);
  
  if (searchTerms.length) {
    filter.search = new RegExp(preg_quote(searchTerms), 'i');
  } else {
    filter.search = false;
  }
}

/* The filter methods below examine records for particular product types and
 * return true/false depending on whether the product satisfies the current
 * filter..
 *
 * @param Array   product Product record
 * @param integer index   Record's index within masterList 
 * @return boolean
 */
function filterGeneric(product, index) {
  return (!filter.search || filter.search.test(product.name) ||
          filter.search.test(product.company_name)) &&
         product.rating       >= filter.min_rating &&
         product.rating_count >= filter.min_rating_count;
}

/**
 * This filter references the global sliderMinValues object and also invokes
 * the filterGeneric function.
 *
 * @see filterGeneric
 */
function filterCD(product, index) {
  // Filter first by VTY input ranges, checkboxes and rating sliders
  if (!((product.m_type & filter.m_type) &&
        product.month_min      <= filter.cd_duration_months &&
        product.month_max      >= filter.cd_duration_months &&
        product.deposit_min    <= filter.cd_deposit &&
        // Deposit limit may be 0, which means there is no limit
        (!product.deposit_max || product.deposit_max >= filter.cd_deposit) &&
        filterGeneric(product, index))) {
    return false;
  }
  
  // Consider this CD's APY as a possible minimum
  if (product.apy < sliderMinValues.apy) {
    sliderMinValues.apy = product.apy;
  }
  
  // Lastly, filter by APY
  return (product.apy >= filter.cd_apy_min);
}

/**
 * This filter references the global sliderMinValues object and also invokes
 * the filterGeneric function.
 *
 * @see filterGeneric
 */
function filterCreditCard(product, index) {
  // Hide business/student types unless they are explicitly checked (ticket #1735)
  if ((!(filter.m_type & bitmasks.m_type.is_type_business) && product.m_type & bitmasks.m_type.is_type_business) ||
      (!(filter.m_type & bitmasks.m_type.is_type_student)  && product.m_type & bitmasks.m_type.is_type_student)) {
    return false;
  }
  
  // Filter first by checkboxes and rating sliders
  if (!(((!filter.m_brand && !filter.m_bank) ||
         product.m_brand & filter.m_brand ||
         product.m_bank  & filter.m_bank) &&
        (!filter.m_type   || product.m_type & filter.m_type) &&
        (!filter.m_reward || product.m_reward & filter.m_reward) &&
        filterGeneric(product, index))) {
    return false;
  }
  
  // Consider this credit card's rates as possible minimums
  if (product.rate < sliderMinValues.rate) {
    sliderMinValues.rate = product.rate;
  }
  if (product.intro_rate < sliderMinValues.intro_rate) {
    sliderMinValues.intro_rate = product.intro_rate;
  }
  if (product.transfer_rate < sliderMinValues.transfer_rate) {
    sliderMinValues.transfer_rate = product.transfer_rate;
  }
  
  // Lastly, filter by rates
  return (product.rate          <= filter.cc_rate_max &&
          product.intro_rate    <= filter.cc_intro_rate_max &&
          product.transfer_rate <= filter.cc_transfer_rate_max);
}

////////////////////////////////////////////////////////////////////////////////

/**
 * Clears a particular error message.
 *
 * @param string type
 */
function clearError(type) {
  if (type == 'zip') {
    $('#f_productpicker_vty').clearFormErrors();
  }
}

/**
 * Displays a particular error message.
 *
 * @param string type
 */
function triggerError(type) {
  
  $('#f_productpicker_vty').clearFormErrors();
  
  if (type.indexOf('zip_') === 0) {
    if (type == 'zip_invalid') {
      $('#f_productpicker_vty').formErrors({ cd_zip: 'Please enter a valid zip code so we can find CD rates near you.' });
    } else if (type == 'zip_nonexistent') {
      $('#f_productpicker_vty').formErrors({ cd_zip: 'This zip code does not exist. Please enter your zip code.' });
    }
  }
}

/**
 * Returns true if the string parameter matches a zip code format (5 digits), or
 * false otherwise.
 *
 * @param string val
 * @return boolean
 */
function isValidZip(val) {
  return (val.search(/^\d{5}$/) != -1);
}

/**
 * Performs any necessary validation before submitting the VTY form.  Currently,
 * this just validates a zip code field if one is present.  If validation fails,
 * any progress dialog is hidden and the field error is displayed.
 */
function validateThenSubmitVTY() {
  
  var zipField = $('#f_productpicker_vty :input[id$=_zip]');
  
  if (zipField.length) {
    // Check formatting before passing the zip code value to the back-end
    if (isValidZip(zipField.val())) {
      data = { zipcode: zipField.val() };
      
      jQuery.getJSON('/widgets/zipExist', data, function(json){
        if (parseInt(json.count, 10) > 0) {
          clearError('zip');
          $('#f_productpicker_vty').submit();
        } else {
          hideProgressDialog();
          triggerError('zip_nonexistent');
        }
      });
    } else {
      hideProgressDialog();
      triggerError('zip_invalid');
    }
  } else {
    // No zip code field exists, so just chain to the  VTY form
    $('#f_productpicker_vty').submit();
  }
}

////////////////////////////////////////////////////////////////////////////////

$(document).ready(function(){
  // VTY form submit handler
  $('#f_productpicker_vty').submit(function(){
    // Abort if an operation is in progress
    if (processingLock) {
      return false;
    }
    
    processingLock = true;
    showProgressDialog();
    updateFilter();
    
    var data = { producttype_id: producttype_id };
    
    if (producttype_id == producttypes.CD) {
      /* The CD master list is localized and determined by the zip and deposit
       * terms, so any VTY form submission should request a new master list.
       * Once the master list is fetched, we chain to the filter form's submit.
       */
      data.cd_zip = filter.cd_zip;
      data.cd_deposit = filter.cd_deposit;
      data.cd_duration_months = filter.cd_duration_months;
      
      jQuery.getJSON('/productpicker/jsonListProduct', data, function(json){
        masterList = json;
        
        for (var i = 0; i < masterList.length; i++) {
          masterList[i].vtyDisplay = makeVtyDisplay(masterList[i].vty);
        }
        
        setBestValue(masterList, 'masterBest');
        processingLock = false;
        $('#f_productpicker_filter').submit();
      });
    }
    
    else if (producttype_id == producttypes.CREDIT_CARD) {
      data.cc_avg_monthly_spending = filter.cc_avg_monthly_spending;
      data.cc_avg_monthly_balance = filter.cc_avg_monthly_balance;
      data.cc_duration_months = filter.cc_duration_months;
      
      /* If a master list is already available, we need only fetch new VTY's
       * and import them into the existing list.  Otherwise, a new master list
       * must be fetched.
       */
      if (isMasterListReady()) {
        jQuery.getJSON('/productpicker/jsonListVty', data, function(json){
          // Update VTY's in the master list and mark the best VTY
          for (var vtyIndex, i = 0; i < masterList.length; i++) {
            // The VTY list is indexed by the product id in string form
            vtyIndex = masterList[i].id.toString();
            
            if (typeof(json[vtyIndex]) != 'undefined') {
              masterList[i].vty = json[vtyIndex];
              masterList[i].vtyDisplay = makeVtyDisplay(masterList[i].vty);
            }
          }
          
          setBestValue(masterList, 'masterBest');
          processingLock = false;
          $('#f_productpicker_filter').submit();
        });
      } else /* !isMasterListReady() */ {
        jQuery.getJSON('/productpicker/jsonListProduct', data, function(json){
          masterList = json;
          
          for (var i = 0; i < masterList.length; i++) {
            masterList[i].vtyDisplay = makeVtyDisplay(masterList[i].vty);
          }
          
          setBestValue(masterList, 'masterBest');
          processingLock = false;
          $('#f_productpicker_filter').submit();
        });
      }
    }
    
    return false;
  });
  
  // Filter form submit handler
  $('#f_productpicker_filter').submit(function(){
    // Abort if an operation is in progress or validation fails
    if (processingLock) {
      return false;
    }
    
    processingLock = true;
    updateFilter();
    
    /* If a master list is available, we can filter and render it.  Otherwise,
     * we may need to chain to the VTY form handler or initialize the master
     * list here (for generic product types).
     */
    if (isMasterListReady()) {
      var filterCallback = filterGeneric;
      
      /* Select a filter callback for this product type.  Also initialize the
       * sliderMinValues object with outlandishly high values.
       */
      if (producttype_id == producttypes.CD) {
        filterCallback = filterCD;
        sliderMinValues = { apy: 10 };
      } else if (producttype_id == producttypes.CREDIT_CARD) {
        filterCallback = filterCreditCard;
        sliderMinValues = { rate: 35, intro_rate: 35, transfer_rate: 35 };
      }
      
      // Apply the callback method over the master list to yield a filtered list
      filteredList = jQuery.grep(masterList, filterCallback);
      
      // Adjust lower bounds for any sliders (if the filtered list isn't empty)
      if (filteredList.length) {
        setSliderLowerBounds(sliderMinValues);
      }
      
      // Mark the product in the filtered list with the best VTY
      if (producttype_id == producttypes.CD ||
          producttype_id == producttypes.CREDIT_CARD) {
        setBestValue(filteredList, 'filterBest');
      }
      
      // Update displayed product counts
      $('#currentproducts').text(number_format(filteredList.length));
      //$('#totalproducts').text(number_format(masterList.length));
      
      // Render the filtered product list
      render(filteredList);
      
      hideProgressDialog();
      processingLock = false;
    } else /* !isMasterListReady() */ {
      // Only show progress dialog if filtering requires fetching a master list
      showProgressDialog();
      
      // If a VTY form exists, we can chain to it to generate a master list
      if ($('#f_productpicker_vty').length) {
        processingLock = false;
        validateThenSubmitVTY();
      } else {
        var data = { producttype_id: producttype_id };
        
        jQuery.getJSON('/productpicker/jsonListProduct', data, function(json){
          masterList = json;
          
          processingLock = false;
          $('#f_productpicker_filter').submit();
        });
      }
    }
    
    return false;
  });
  
  /* If any checkbox within a group is changed, all checkboxes in that group
   * should be synchronized and then the entire group saved to the session.
   */
  if (producttype_id == producttypes.CD) {
    $('div.checklist.cd_type input[type=checkbox]').change(function(){
      syncCheckboxGroup('cd_type', $(this), true);
      saveToSession($('div.checklist.cd_type input[type=checkbox]'));
    });
    
    syncCheckboxGroup('cd_type');
  } else if (producttype_id == producttypes.CREDIT_CARD) {
    $('div.checklist.cc_type input[type=checkbox]').change(function(){
      syncCheckboxGroup('cc_type', $(this));
      saveToSession($('div.checklist.cc_type input[type=checkbox][id!="cc_type_all"]'));
    });
    $('div.checklist.cc_reward input[type=checkbox]').change(function(){
      syncCheckboxGroup('cc_reward', $(this));
      saveToSession($('div.checklist.cc_reward input[type=checkbox][id!="cc_reward_all"]'));
    });
    $('div.checklist.cc_brand_bank input[type=checkbox]').change(function(){
      syncCheckboxGroup('cc_brand_bank', $(this));
      saveToSession($('div.checklist.cc_brand_bank input[type=checkbox][id!="cc_brand_bank_all"]'));
    });
    
    syncCheckboxGroup('cc_type');
    syncCheckboxGroup('cc_reward');
    syncCheckboxGroup('cc_brand_bank');
  }
  
  /* Initialize all slider elements and set their slider movement handlers to
   * sync with the corresponding input field.  A change event should save the
   * current field value to the session and submit the appropriate form.  We
   * cannot directly invoke the input field's onChange event as that would cause
   * recursion problems.
   */
  if (producttype_id == producttypes.CD) {
    $('#cd_deposit_slider').slider({
      min: 0, max: 250000, range: 'min', orientation: 'horizontal', step: 1000,
      stop: function(e, ui){
        $('#cd_deposit').val(ui.value);
        validateThenSubmitVTY();
      },
      slide: function(e, ui){
        $('#cd_deposit').val(ui.value);
      }
    });
    
    $('#cd_duration_months_slider').slider({
      min: 3, max: 60, range: 'min', orientation: 'horizontal', step: 3,
      stop: function(e, ui){
        $('#cd_duration_months').val(ui.value);
        validateThenSubmitVTY();
      },
      slide: function(e, ui){
        $('#cd_duration_months').val(ui.value);
      }
    });
    
    $('#cd_apy_min_slider').slider({
      min: 0, max: 10, range: 'min', orientation: 'horizontal', step: 1,
      stop: function(e, ui){
        $('#cd_apy_min').val(ui.value);
        saveToSession($('#cd_apy_min'));
        $('#f_productpicker_filter').submit();
      },
      slide: function(e, ui){
        $('#cd_apy_min').val(ui.value);
      }
    });
  } else if (producttype_id == producttypes.CREDIT_CARD) {
    $('#cc_avg_monthly_balance_slider').slider({
      min: 0, max: 25000, range: 'min', orientation: 'horizontal', step: 10,
      stop: function(e, ui){
        $('#cc_avg_monthly_balance').val(ui.value);
        validateThenSubmitVTY();
      },
      slide: function(e, ui){
        $('#cc_avg_monthly_balance').val(ui.value);
      }
    });
    
    $('#cc_avg_monthly_spending_slider').slider({
      min: 0, max: 25000, range: 'min', orientation: 'horizontal', step: 10,
      stop: function(e, ui){
        $('#cc_avg_monthly_spending').val(ui.value);
        validateThenSubmitVTY();
      },
      slide: function(e, ui){
        $('#cc_avg_monthly_spending').val(ui.value);
      }
    });
    
    $('#cc_duration_months_slider').slider({
      min: 6, max: 36, range: 'min', orientation: 'horizontal', step: 3,
      stop: function(e, ui){
        $('#cc_duration_months').val(ui.value);
        validateThenSubmitVTY();
      },
      slide: function(e, ui){
        $('#cc_duration_months').val(ui.value);
      }
    });
    
    $('#cc_rate_max_slider').slider({
      min: 0, max: 35, range: 'min', orientation: 'horizontal', step: 1,
      stop: function(e, ui){
        $('#cc_rate_max').val(ui.value);
        saveToSession($('#cc_rate_max'));
        $('#f_productpicker_filter').submit();
      },
      slide: function(e, ui){
        $('#cc_rate_max').val(ui.value);
      }
    });
    
    $('#cc_intro_rate_max_slider').slider({
      min: 0, max: 35, range: 'min', orientation: 'horizontal', step: 1,
      stop: function(e, ui){
        $('#cc_intro_rate_max').val(ui.value);
        saveToSession($('#cc_intro_rate_max'));
        $('#f_productpicker_filter').submit();
      },
      slide: function(e, ui){
        $('#cc_intro_rate_max').val(ui.value);
      }
    });
    
    $('#cc_transfer_rate_max_slider').slider({
      min: 0, max: 35, range: 'min', orientation: 'horizontal', step: 1,
      stop: function(e, ui){
        $('#cc_transfer_rate_max').val(ui.value);
        saveToSession($('#cc_transfer_rate_max'));
        $('#f_productpicker_filter').submit();
      },
      slide: function(e, ui){
        $('#cc_transfer_rate_max').val(ui.value);
      }
    });
  }
  
  $('#min_rating_count_slider').slider({
    min: 0, max: 5, range: 'min', orientation: 'horizontal', step: 1,
    stop: function(e, ui){
      $('#min_rating_count').val(ui.value);
      saveToSession($('#min_rating_count'));
      $('#f_productpicker_filter').submit();
    },
    slide: function(e, ui){
      $('#min_rating_count').val(ui.value);
    }
  });
  
  /* Ensure that any changes to input fields also change the corresponding
   * slider.  The syncSlider() function will also ensure that field values are
   * saved to the backend session.
   */
  if (producttype_id == producttypes.CD) {
    $('#cd_deposit').change(function(){ syncSlider($('#cd_deposit'), $('#cd_deposit_slider')); });
    $('#cd_duration_months').change(function(){ syncSlider($('#cd_duration_months'), $('#cd_duration_months_slider')); });
    $('#cd_apy_min').change(function(){ syncSlider($('#cd_apy_min'), $('#cd_apy_min_slider')); });
    
    syncSlider($('#cd_deposit'), $('#cd_deposit_slider'));
    syncSlider($('#cd_duration_months'), $('#cd_duration_months_slider'));
    syncSlider($('#cd_apy_min'), $('#cd_apy_min_slider'));
  } else if (producttype_id == producttypes.CREDIT_CARD) {
    $('#cc_avg_monthly_balance').change(function(){ syncSlider($('#cc_avg_monthly_balance'), $('#cc_avg_monthly_balance_slider')); });
    $('#cc_avg_monthly_spending').change(function(){ syncSlider($('#cc_avg_monthly_spending'), $('#cc_avg_monthly_spending_slider')); });
    $('#cc_duration_months').change(function(){ syncSlider($('#cc_duration_months'), $('#cc_duration_months_slider')); });
    $('#cc_rate_max').change(function(){ syncSlider($('#cc_rate_max'), $('#cc_rate_max_slider')); });
    $('#cc_intro_rate_max').change(function(){ syncSlider($('#cc_intro_rate_max'), $('#cc_intro_rate_max_slider')); });
    $('#cc_transfer_rate_max').change(function(){ syncSlider($('#cc_transfer_rate_max'), $('#cc_transfer_rate_max_slider')); });
    
    syncSlider($('#cc_avg_monthly_balance'), $('#cc_avg_monthly_balance_slider'));
    syncSlider($('#cc_avg_monthly_spending'), $('#cc_avg_monthly_spending_slider'));
    syncSlider($('#cc_duration_months'), $('#cc_duration_months_slider'));
    syncSlider($('#cc_rate_max'), $('#cc_rate_max_slider'));
    syncSlider($('#cc_intro_rate_max'), $('#cc_intro_rate_max_slider'));
    syncSlider($('#cc_transfer_rate_max'), $('#cc_transfer_rate_max_slider'));
  }
  
  $('#min_rating_count').change(function(){ syncSlider($('#min_rating_count'), $('#min_rating_count_slider')); });
  syncSlider($('#min_rating_count'), $('#min_rating_count_slider'));
  
  //Initialize the rating plugin
  $('#f_productpicker_filter input.hover-star').rating({
    cancel: 'Not Rated',
    cancelValue: '0',
    required: false,
    focus: function(value, link){
      if (link.title) {
        $('#hover-star-caption').text(link.title);
      }
    },
    blur: function(value, link){ 
      $('#hover-star-caption').empty();
    },
    callback: function(value, link){
      /* This is necessary as during a filter reset, we don't want to trigger
       * the change event and save to the session just yet, since we can wait
       * until all fields are reset and able to be saved at once.
       */
      if (!skipMinRatingChange) {
        $('#min_rating').change();
      }
    }
  });
  
  /* The rating plugin replaces the radio group with a hidden form input of the
   * the same name.  Later, we need to reference this element by an id when
   * constructing the filter, so create an id attribute from the name now.
   */
  $('#f_productpicker_filter input[type=hidden][name=min_rating]').attr('id', 'min_rating');
  
  // Initialize its value to zero in case it's not a number
  if (!$('#min_rating').val()) {
    $('#min_rating').val(0);
  }
  
  // Enable the reset button for the filter form
  $('#f_productpicker_filter_reset').click(function(){
    /* Each filter input will be reset according to its corresponding value in
     * the default filter.  Special treatment is necessary for checkboxes and
     * elements with related sliders.
     */
    $('#f_productpicker_filter :input').each(function(){
      var $this = $(this);
      var dv = defaultFilter[$this.attr('id')];
      
      if (dv !== undefined) {
        if ($this.is(':checkbox')) {
          $this.attr('checked', (dv ? true : false));
        } else {
          $this.val(dv);
          var $slider = $('#'+$this.attr('id')+'_slider');
          
          if ($slider.length) {
            syncSlider($this, $slider);
          }
        }
      }
    });
    
    // Sync checkbox groups as necessary
    if (producttype_id == producttypes.CD) {
      syncCheckboxGroup('cd_type');
    } else if (producttype_id == producttypes.CREDIT_CARD) {
      syncCheckboxGroup('cc_type');
      syncCheckboxGroup('cc_reward');
      syncCheckboxGroup('cc_brand_bank');
    }
    
    // Reset stars, and prevent triggering of #min_rating's change event
    skipMinRatingChange = true;
    $('div.min_rating_contain div.cancel a').click();
    skipMinRatingChange = false;
    
    // Save all filter fields to the session and re-apply filters
    saveToSession($('#f_productpicker_filter :input'));
    $('#f_productpicker_filter').submit();
    return false;
  });
  
  // Any form field changes should trigger their parent form to submit
  $('#f_productpicker_vty :input').change(function(){ validateThenSubmitVTY(); });
  $('#f_productpicker_filter :input').change(function(){
    // Fields other than search should be saved to the session
    if (this.id != 'product_title_search') {
      saveToSession($(this));
    }
    
    $('#f_productpicker_filter').submit();
  });
  

  /*
   * Internet Explorer will only fire a change() event after the element loses
   * focus, so it's necessary to blur the checkbox after each click.  We'll
   * re-focus the checkbox after blurring to avoid breaking keyboard navigation.
   * 
   * @see http://msdn.microsoft.com/en-us/library/ms536912(VS.85).aspx
   */
  $('#f_productpicker_filter input:checkbox').click(function(){ this.blur(); this.focus(); });
  
  // Check if the dashboard needs us to render the initial view
  if (render_onload) {
    $('#f_productpicker_filter').submit();
  }
});