/**
 * JSON Object representation of Nikon lenses.
 * @author amcintyre
 */
jQuery.noConflict();

LensFilter = function(){
    var all = {};              // Set in Ajax call; an Object of Lens Objects.
    var categoryOrder = {};    // Set in Ajax call; ordering of lens categories and subcategories.
    var teleconverters = {};   // Holds the teleconverter lenses, which are treated separately.
    var $lenses = null;        // After first filtering op, a jQuery Object holding all tr.lens nodes.
    var lensesByType = {       // Object containing cached queries of all lenses by type.
        swm   : null,
        dx    : null,
        fx    : null,
        vr    : null,
        micro : null,
        pc    : null,
        zoom  : null,
        prime : null,
        ed    : null,
        n     : null      
    }
    var visibleLenses = {}    // A subset of all tr.lens items: only those that are :visible
    
    var LENS_URL = "/lenses.js";    // URL of our lens list.

    var LENS_LIST = '#lens_list'            // ID selector of lens list OL.

    var LENS_LOAD = "lens_load";                     // Lens load event name.    
    var STATUS_UPDATE = "status_update";             // Status update event name.
    var AJAX_ACTIVITY = "ajax_activity";             // Ajax request activity event name.
    var BEFORE_FILTER = "before_filter";             // Before filter starts.  
    var FILTER_COMPLETE = "filter_complete";         // Filter complete event.
    var BLACKLIST_UPDATE = "blacklist_update";       // We've modified the aperture blacklist!

    var filters = [];                       // Array of currently applied filters.     
    var filterRegex = /\w+/g;               // Regex for building filter classNames.
    var classRegex = /[\s#;&,\.\+\*~':"\!\^\$\[\]\(\)=>|\/]/g;    // Regex used for building legal classNames.
    
    var filterList = {                      // Object containing all filters we can use.
        swm    : 1,                         // Mainly used in HTML templating.
        dx     : 1,
        fx     : 1,
        vr     : 1,
        micro  : 1,
        pc     : 1,
        ed     : 1,
        n      : 1
    };
    
    var focalRange = [];    // Array of available focal ranges
    var minFocalRange = maxFocalRange = selectedMinFR = selectedMaxFR = 0;
    var MAX_FOCAL_MINIMUM = 24;
    var MIN_FOCAL_MAXIMUM = 200;
    var filterMinValue = filterMaxValue = false;
    
    var apertures = [];                 // Array of available apertures
    var selectedAperture = minAperture = maxAperture = 0;
    var resultsApertureBlacklist = {};
    
    var newImage;                                                      // Eventually set to the path of the "new" icon image    
    
    var th = null;                                                     // Cache top TH
    var thPos = null;                                                  // offset() of table header for floating TH 
    
    var shouldSave = false;                                            // Should we save these results?
    var LOAD_STATE_START = "load_state_start";                         // Start of our "load state" event
    var LOAD_STATE_END = "load_state_end";                             // End of our "load state" event

    // IE6/IE7 workaround flags
    var isIE = jQuery.browser.msie;
    var isIE6 = isIE && jQuery.browser.version < 7;
    var isIE7 = isIE && jQuery.browser.version < 8 && jQuery.browser.version >= 7;
    var isIE8 = isIE && jQuery.browser.version < 9 && jQuery.browser.version >= 8; 
    // IE8 will incorrectly report itself as IE7 if it's in LAN compatibility mode...
    if(isIE7 && navigator.userAgent.indexOf('Trident/4.0') > 0){
        isIE8 = true;
    }
    
    /***
     * John Hann's debounce function - http://unscriptable.com/index.php/2009/03/20/debouncing-javascript-methods/
     */
    var debounceScroll = function (func, threshold, execAsap) {
     
        var timeout;
     
        return function debounced () {
            var obj = this, args = arguments;
            function delayed () {
                if (!execAsap)
                    func.apply(obj, args);
                timeout = null; 
            };
     
            if (timeout)
                clearTimeout(timeout);
            else if (execAsap)
                func.apply(obj, args);
            timeout = setTimeout(delayed, threshold || 100); 
        };
     
    }
    
    var debounceResize = function (func, threshold, execAsap) {
     
        var timeout;
     
        return function debounced () {
            var obj = this, args = arguments;
            function delayed () {
                if (!execAsap)
                    func.apply(obj, args);
                timeout = null; 
            };
     
            if (timeout)
                clearTimeout(timeout);
            else if (execAsap)
                func.apply(obj, args);
            timeout = setTimeout(delayed, threshold || 100); 
        };
     
    }    
    
    var _bindLensEvents = function(){       // Set-up for binding the events we need on individual LIs
        // We'll bind these "live" events to all lenses. That way, we can handle
        // all filter in/out animations right here.
        jQuery(LENS_LIST + ' tr.subcategory')
            .addClass(FILTER_COMPLETE)
            .bind('lens.' + FILTER_COMPLETE, function(e){
                var subcat = this.getAttribute('data-subcategory');

                if(visibleLenses.filter(' .' + subcat).length == 0){
                    jQuery(this).hide();
                }
                else{
                    jQuery(this).show();
                }
            });  
        
        var moveHeader = function(){
            var st = jQuery().scrollTop();  

            if(! th){    // Set these up on our first run...
                th = jQuery('#infoTable thead');
                th.remove('tr:eq(2)');    // Remove the second tr, which holds our loading gif
                thPos = th.offset();
            } 
            
            if(! document.getElementById('float__')){
                jQuery('<div id="float__"></div>')
                    .appendTo('body')
                    .append('<table></table>')
                    .find('table')
                    .append(th.clone());                
            }    

            // We've scrolled our header off the screen!
            // ...but, have we scrolled too far?!
            if(st > thPos.top && st < jQuery('#lens_list').height() + thPos.top){
                var scroller = jQuery('#float__');
                if(isIE){
                    scroller.show();
                }
                else{
                    scroller.fadeIn();                    
                }
          
                scroller.css({
                        left : thPos.left
                    })
                    .animate({
                        top : st
                    }, 250, 'easeInOutCirc');
            }
            else{
                if(isIE){
                    jQuery('#float__').css('top',thPos.top).hide();                    
                }
                else{
                    jQuery('#float__').css('top',thPos.top).fadeOut();                    
                }
            }                
        };
        
        var resizeHeader = function () {                     
            if(th){
                thPos = th.offset();
                jQuery('#float__').css('left',thPos.left);                       
            }
        };        
        
        jQuery(window).scroll(debounceScroll(moveHeader,400));
        jQuery(window).bind('resize',debounceResize(resizeHeader, 250));           
    };
    
    var categories = {};                    // Object that will eventually hold lenses divided up by category.   
    
    var HELP_CONTAINER_ID = 'lensHelpContainer';    // Help Tabs HTML Element ID
    var FILTER_ID = 'lens_filter';                  // Filter Tools HTML Element ID  
    
    var STATE_COOKIE = 'lens-filter-state';        // Lens Filter state cookie name
    
    return{
        /***
         * Little function that lets us log private members
         * @param {String} prop Member to log
         */        
        __test__ : function(prop){
            return eval(prop);
        },
        /***
         * Basic init function used to set up events model and set initial state of the application.
         */
        init : function(){
            var o = this;
            jQuery('#stateLoadingMessage')
                .addClass(LOAD_STATE_START + ' ' + LOAD_STATE_END)  
                .bind('lens.' + LOAD_STATE_START, function(e){
                    jQuery('#stateLoadingMessage span').show();
                })
                .bind('lens.' + LOAD_STATE_END, function(e){
                    jQuery('#stateLoadingMessage').stop().fadeOut();
                });

            (function(){
                var os = jQuery('#top_container').offset();
                var lm = jQuery('#stateLoadingMessage');                

                lm.stop()
                    .css({    
                        top : os.top,
                        left : os.left,
                        opacity : 0,
                        display : 'block'
                    })
                    .fadeTo(350,.65);                
            }());
            
            jQuery('#list_loader')
                .addClass(LENS_LOAD)
                .bind('lens.' + LENS_LOAD, function(e){
                    jQuery(this).parent().parent().fadeOut();                     
                });                
            
            jQuery('#activity_indicator')
                .addClass(FILTER_COMPLETE + ' ' + AJAX_ACTIVITY + ' ' + BEFORE_FILTER)
                .bind('lens.' + FILTER_COMPLETE, function(e){
                    jQuery(this).css({
                        opacity : 0,
                        display : 'none'
                    });
                })
                .bind('lens.' + BEFORE_FILTER, function(e){
                    jQuery(this).css({
                        opacity : 1,
                        display : 'block'
                    });
                })
                .bind('lens.' + AJAX_ACTIVITY, function(e){
                    var el = jQuery(this).css('display','block');

                    if(el.hasClass('on')){
                        el.removeClass('on').fadeTo(0.25,0);                         
                    }    
                    else{
                        el.addClass('on').fadeTo(0.25,1);                       
                    }
                });
            
            jQuery('#displayed_lenses')
                .addClass(LENS_LOAD + ' ' + FILTER_COMPLETE + ' ' + AJAX_ACTIVITY + ' ' + BEFORE_FILTER)
                .bind('lens.' + LENS_LOAD, function(e){
                    LensFilter.updateCounter(jQuery(this), e.info);                     
                })
                .bind('lens.' + FILTER_COMPLETE, function(e){
                    var el = jQuery(this);
                    LensFilter.updateCounter(el, e.info);
                    
                    if(isIE){
                        el.css('visibility','visible');                        
                    }
                    else{
                        el.fadeTo(450,1);                        
                    }
                })
                .bind('lens.' + BEFORE_FILTER, function(e){
                    if(isIE){
                        jQuery(this).css('visibility','hidden');                        
                    }
                    else{
                        jQuery(this).stop().css('opacity',0);
                    }                      
                })
                .bind('lens.' + AJAX_ACTIVITY, function(e){
                    var el = jQuery(this);

                    if(el.hasClass('off')){
                        if(isIE){
                            el.css('visibility','visible');                        
                        }
                        else{
                            el.removeClass('off').fadeTo(0.25,1);      
                        }                        
                    }    
                    else{
                        if(isIE){
                            el.addClass('off').css('visibility','hidden');                        
                        }
                        else{
                            el.addClass('off').fadeTo(0.25,0);                       
                        }   
                    }
                });

            // Set up and configure focal range slider
            jQuery('#focal_range').slider({
                    animate : 'fast',
                    min     : 0,
                    max     : focalRange.length - 1,    // Map a 1-to-1 step-to-range relationship
                    range   : true,
                    rageGap : 1,
                    values  : [0, focalRange.length - 1]
                })
                .bind('slide', function(e,ui){
                    var min = ui.values[0];
                    var max = ui.values[1];
                    var minRestriction = jQuery.inArray(MIN_FOCAL_MAXIMUM,focalRange);
                    var maxRestriction = jQuery.inArray(MAX_FOCAL_MINIMUM,focalRange);
                                    
                    if(visibleLenses.inFocalRange(focalRange[min],focalRange[max]).length == 0){              
                        return false;    
                    }
                    else if(minRestriction > -1 && min > minRestriction){
                        return false;
                    } 
                    else if(maxRestriction > -1 && max < maxRestriction){
                        return false;
                    } 
                    else if(min == max || min > maxFocalRange || max < minFocalRange){                        
                        return false;
                    }  
                    else{
                        return true;                            
                    }           
                })
                .bind('slidestop', function(e,ui){
                    var min = ui.values[0];
                    var max = ui.values[1];     
                    var minRestriction = jQuery.inArray(MIN_FOCAL_MAXIMUM,focalRange);
                    var maxRestriction = jQuery.inArray(MAX_FOCAL_MINIMUM,focalRange);
                                                              
                    var elIndex = jQuery(ui.handle).parent().find('a').index(ui.handle);

                    if(elIndex == 1){
                        if(visibleLenses.inFocalRange(focalRange[min],focalRange[max]).length == 0){
                            jQuery(this).slider('values', elIndex, maxFocalRange);                                                            
                        }
                        else if(max <= minFocalRange && ! (maxRestriction > -1 && max < maxRestriction)){
                            jQuery(this).slider('values', elIndex, minFocalRange);                                                        
                        }
                        else if(maxRestriction > -1 && max < maxRestriction){
                            jQuery(this).slider('values', elIndex, maxRestriction);                                                        
                        }
                        else if(max <= min){
                            jQuery(this).slider('values', elIndex, min + 1);                            
                        }
                    }
                    else if(elIndex == 0) {
                        if(visibleLenses.inFocalRange(focalRange[min],focalRange[max]).length == 0){
                            jQuery(this).slider('values', elIndex, minFocalRange);                                                            
                        }
                        else if(min >= maxFocalRange && ! (minRestriction > -1 && min > minRestriction)){
                            jQuery(this).slider('values', elIndex, minFocalRange);                                                        
                        }                        
                        else if(minRestriction > -1 && min > minRestriction){
                            jQuery(this).slider('values', elIndex, minRestriction);                                                        
                        }                                               
                        else if(min >= max){
                            jQuery(this).slider('values', elIndex, max - 1);                            
                        }
                    }                      
                })
                .bind('slidechange', function(e,ui){  
                    LensFilter.handleRangeChange(this,ui.values[0],ui.values[1]);                                                      
                })
                .addClass(FILTER_COMPLETE)
                .bind('lens.' + FILTER_COMPLETE, function(e){
                    minFocalRange = e.minFocal;
                    maxFocalRange = e.maxFocal;  
                })    
                // Occasionally, the hover class isn't set on the second slider thumb. Doing it manually:  
                .find('a.ui-slider-handle:eq(1)')
                .addClass('ui-slider-handle-right')
                .bind('mouseover',function(){
                    jQuery(this).addClass('ui-state-hover ui-state-hover-right');
                })
                .bind('mouseout',function(){
                    jQuery(this).removeClass('ui-state-hover ui-state-hover-right');
                })  
                .end()              
                .addLabels(focalRange)                 
                .parent()
                .find('label')
                    .addClass(FILTER_COMPLETE)
                    .bind('lens.' + FILTER_COMPLETE, function(e){                    
                        var l = jQuery(this);
                        var val = parseInt(l.text());                        

                        if(val < focalRange[e.minFocal] || val > focalRange[e.maxFocal]){
                            l.addClass('disabled');
                        }
                        else{
                            l.removeClass('disabled');
                        }
                    })
                .end()
                .find('a.range_toggle')
                    .each(function(){
                        var min = LensFilter._findClosestMinRange(parseFloat(this.getAttribute('data-minrange')));
                        var max = LensFilter._findClosestMinRange(parseFloat(this.getAttribute('data-maxrange')));
                        var widthRange = max - min;  

                        var el = jQuery(this);
                        
                        var leftBorder = parseInt(el.css('borderLeftWidth'),10);
                        if(isNaN(leftBorder)){
                            leftBorder = 0;
                        }
                        
                        var rightBorder = parseInt(el.css('borderRightWidth'),10);
                        if(isNaN(rightBorder)){
                            rightBorder = 0;
                        }
                        
                        var borderWidth = leftBorder + rightBorder;
                        var totalWidth = (el.parent().parent().find('div.axis label:eq(0)').width() * widthRange) - borderWidth;

                        // Super Telephoto button needs 57px of width, otherwise it wraps and breaks the text
                        if(el.hasClass('super-telephoto') && totalWidth < 57){
                            totalWidth = 57;
                        }
                        el.width(totalWidth + 'px');    
                        
                        if(isIE6){
                            el.bind('mouseenter',function(){
                                el.addClass('range_toggle_hover');
                            })
                            .bind('mouseleave',function(){
                                el.removeClass('range_toggle_hover');
                            });
                        }                              
                    })
                    .bind('click',function(e){
                        e.preventDefault();
                        var min = LensFilter._findClosestMinRange(parseFloat(this.getAttribute('data-minrange')));
                        var max = LensFilter._findClosestMinRange(parseFloat(this.getAttribute('data-maxrange')));
                        var currentValues = jQuery('#focal_range').slider('values');

                        // If this button click could yield no results, we'll toss the click.
                        
                        var tmpLenses = $lenses.not(o._buildFilterClasses())
                                .matchesAperture(selectedAperture)
                                .inFocalRange(this.getAttribute('data-minrange'),this.getAttribute('data-maxrange'));
                        
                        if(tmpLenses.length == 0){
                            return false;
                        }                                                                    

                        // Make sure we're not setting the values such that min is greater than current max, 
                        // or max is less than current min, causing a negative width on the range.
                        // The IEs will throw an exception.
                        if(min >= currentValues[1]){
                            jQuery('#focal_range')                            
                                .slider('values',1,max);                                                        

                            jQuery('#focal_range')
                                .slider('values',0,min)                            
                        }
                        else{    // Max would be less than current min and our default case
                            jQuery('#focal_range')                            
                                .slider('values',0,min);                                                        

                            jQuery('#focal_range')
                                .slider('values',1,max)                                                           
                        }   
                    });                               
            
            // ...and do the same for the aperture slider
            jQuery('#aperture').slider({
                    animate : 'fast',
                    min     : 0,
                    max     : apertures.length - 1,
                    range : 'max',
                    step    : 1               
                })
                .bind('slide', function(e,ui){
                    var val = apertures[ui.value];
                    var index = jQuery.inArray(val,apertures);

                    if(val < minAperture){
                        return false;
                    }
                    else{
                        return true;
                    }
                })
                .bind('slidestop', function(e,ui){
                    var currentValue = parseFloat(jQuery(this).attr('currentValue'),10) || apertures[0];
                    
                    var start = jQuery.inArray(currentValue,apertures);
                    if(start < 0) start = 0;
                
                    var app = apertures[ui.value];
                    var end = jQuery.inArray(app,apertures);

                    // It's possible that we're working backwards, so we can switch start and end in this case...
                    if(start > end){
                        var tmp = start;
                        start = end;
                        end = tmp;
                    }

                    var removeItems = [];                                       
                    for(var i = start; i <= end; i++){
                        var curApp = apertures[i];
                        if(curApp in resultsApertureBlacklist){    
                            removeItems.push(curApp);
                        }                     
                    }                   
                    LensFilter.removeBlacklistAperture(removeItems,'results');
                })
                .bind('slidechange', function(e,ui){ 
                    jQuery(this).attr('currentValue',apertures[ui.value]);                                     
                    LensFilter.handleApertureChange(this,apertures[ui.value]);                        
                })
                .addClass(FILTER_COMPLETE)
                .bind('lens.' + FILTER_COMPLETE, function(e){
                    var type = 'filter';

                    // Translate our aperture values into legal values from our apertures array
                    var validApertures = e.apertures;
                    for(var i = 0; i < validApertures.length; i++){
                        validApertures[i] = LensFilter._findClosestAperture(validApertures[i]);
                    }

                    var missingApertures = [];
                    var availableApertures = [];                                              
                    
                    // Apertures min and max are reversed, thus the counter-intuitive code below
                    minAperture = Math.min.apply(Math,validApertures);
                    maxAperture = Math.max.apply(Math,validApertures);

                    for(var i = 0; i < apertures.length; i++){
                        var app = apertures[i];  
                        
                        // If this aperture not part of our valid set?
                        if(jQuery.inArray(app,validApertures) == -1){
                            missingApertures.push(app);
                        }
                        // Otherwise, we can wipe it.
                        else if(app in resultsApertureBlacklist){    
                            availableApertures.push(app);                                           
                        }                         
                    }
                    
                    LensFilter.blacklistAperture(missingApertures,'results');
                    LensFilter.removeBlacklistAperture(availableApertures,'results');                    
            
                    // Trigger our blacklist update event...
                    jQuery('#aperture_container label').trigger('lens.' + BLACKLIST_UPDATE);
                })
                .addLabels(apertures,'f/').slider('option','max',apertures.length - 1)
                .parent()
                .find('label')         
                    .bind('lens.' + BLACKLIST_UPDATE, function(e,ui){
                        var l = jQuery(this);
                        var t = l.text().substr('f/'.length);
                        
                        if(t in resultsApertureBlacklist){
                            l.addClass('disabled');
                        }
                        else{
                            l.removeClass('disabled');
                        }
                    });

            // Bind focal range filter actions
            jQuery('#toggle_pc')
                .bind('slidechange', function(e,ui){
                    if(e.target.getAttribute('id') == 'focal_range'){
                        var min = ui.values[0];
                        var max = ui.values[1];
                        if(min >= 85 && min <= 200){
                            jQuery(this).attr('disabled','disabled');                        
                        }    
                        else{
                            jQuery(this).removeAttr('disabled');
                        }                        
                    }
                });               
                
            jQuery('div#boolean_filters input')
                .filter(':not([data-filtertype="all"])')   // "All" radio buttons don't need to pay attention to this. Saves us 2 observers.
                .addClass(FILTER_COMPLETE)
                .bind('lens.' + FILTER_COMPLETE, function(e,ui){
                    var el = jQuery(this)
                    var type = el.attr('data-filtertype'); 
                    var totalLenses = lensesByType[type].filter(isIE8 ? function(){ return this.clientHeight > 0 && this.clientWidth > 0 } : (isIE6 || isIE7 ? ':not(:hidden)' : ':visible')).length;

                    // No need to disabled radio buttons
                    if(el.is(':radio') && el.parents('.booleanContainer').find('input.on:not([data-filtertype="all"])').length > 0){
                        // How many lenses would be available if this filter was enabled?
                        var filterString = o._buildFilterClasses();
                        var tmpLenses = $lenses.matchesAperture(selectedAperture)
                                            .inFocalRange(focalRange[selectedMinFR],focalRange[selectedMaxFR]);

                        // Switch on type, replacing each type with its opposite to see if there
                        // are or are not results here.
                        switch(type){
                            case 'zoom':
                                if(tmpLenses.not(filterString.replace('prime','zoom')).length > 0){                            
                                    el.removeAttr('disabled').parent().removeClass('disabled'); 
                                }    
                                else{
                                    el.attr('disabled','disabled').parent().addClass('disabled');
                                }                                                   
                                break;
                            case 'prime':
                                if(tmpLenses.not(filterString.replace('zoom','prime')).length > 0){                            
                                    el.removeAttr('disabled').parent().removeClass('disabled');                                                        
                                }                            
                                else{
                                    el.attr('disabled','disabled').parent().addClass('disabled');
                                }                                                                                   
                                break;
                            case 'fx':
                                if(tmpLenses.not(filterString.replace('dx','fx')).length > 0){                            
                                    el.removeAttr('disabled').parent().removeClass('disabled');                                                        
                                }                            
                                else{
                                    el.attr('disabled','disabled').parent().addClass('disabled');
                                }                                                                                   
                                break;
                            case 'dx':
                                if(tmpLenses.not(filterString.replace('fx','dx')).length > 0){                            
                                    el.removeAttr('disabled').parent().removeClass('disabled');                                                        
                                }                            
                                else{
                                    el.attr('disabled','disabled').parent().addClass('disabled');
                                }                                                                                   
                                break;
                        }
                    }
                    else if(totalLenses == 0){
                        el.attr('disabled','disabled').parent().addClass('disabled');    
                    }                               
                    else{
                        el.removeAttr('disabled').parent().removeClass('disabled');
                    }
                })
                .end()
                .filter(':checkbox')
                .bind('click', function(e){
                    var el = jQuery(this);
                    var type = el.attr('id').substr('toggle_'.length);
                    
                    if(el.hasClass('off')){
                        LensFilter.addFilter(type);                            
                        el.removeClass('off').addClass('on');
                    }    
                    else{
                        LensFilter.removeFilter(type);
                        el.removeClass('on').addClass('off');  
                    } 
                    LensFilter.filterLenses(el);     
                })
                .end()
                .filter(':radio')
                .bind('click', function(e){
                    var el = jQuery(this);
                    var type = el.attr('data-filtertype');
                    
                    // Find the current item and remove its filter
                    var curEl = el.parents('.booleanContainer').find('input.on').removeClass('on').addClass('off');

                    if(curEl.length > 0){
                        var removeType = curEl.attr('data-filtertype');                    
                        if(removeType.indexOf('all') == -1){
                            LensFilter.removeFilter(removeType);                                                    
                        }
                    }                    
                    
                    // If we aren't dealing with an "all" check,
                    // we'll add our new filter
                    if(type.indexOf('all') == -1){
                        LensFilter.addFilter(type);                          
                    } 
                    
                    // We add our "on" class
                    el.removeClass('off').addClass('on');

                    // ... and we can give the whole thing a whirl now!
                    LensFilter.filterLenses(el);                      
                });
                
                jQuery('#reset')
                    .bind('click',function(e){
                        LensFilter.reset().clearState();                            
                    });
                    
                window.onbeforeunload = function(){
                    // Only save if we've interacted with the filter tools.
                    if(shouldSave){
                        LensFilter.saveState();                         
                    }
                };                
            return this;
        },
        /***
         * Sets the value of the "New" image path we'll be using. 
         * @param {Object} img
         */
        setNewImage : function(img){
            newImage = img;    
        },
        /***
         * Resets the form back to its initial state.
         */
        reset : function(){
            filters = [];
             
            jQuery('#boolean_filters input')
                .removeAttr('disabled')
                .removeClass('on')
                .addClass('off')
                .filter('[data-filtertype="all"]')
                .attr('checked',true)
                .end()
                .filter(':checkbox')
                .attr('checked',false);

            minAperture = apertures[apertures.length - 1];
            maxAperture = apertures[0];                        
            apertureBlacklist = {};
            selectedAperture = apertures[0];
            jQuery('#aperture').slider('option','value',0);        
            jQuery('#aperture_container label').trigger('lens.' + BLACKLIST_UPDATE);

            minFocalRange = 0;
            maxFocalRange = focalRange.length - 1;
            jQuery('#focal_range').slider('values',0,0);
            jQuery('#focal_range').slider('values',1,focalRange.length - 1);            
                
            this.filterLenses();
            return this;
        },
        /***
         * Sets the user-defined focal range values for the application.
         * @param {String} aps A comma-separated list of focal range values.
         */        
        setFocalRangeValues : function(fr){
            focalRange = eval('[' + fr + ']').sort(function(a,b){ return a - b});
            minFocalRange = 0;
            maxFocalRange = selectedMaxFR = focalRange.length - 1;

            return this;            
        },
        /***
         * Returns the index of the value from focalRange array that is closest to min without going over min's value. 
         * @param {Number} min Number whose closest match we'd like to find.
         */
        _findClosestMinRange : function(min){
            for(var i = 0; i < focalRange.length; i++){
                f = focalRange[i]
                if(f < min){
                    continue;
                }
                else if(f == min){
                    return i;
                }
                else{
                    return i > 0 ? i - 1 : 0;                    
                }
            }   
        },
        /***
         * Returns the index of the value from focalRange array that is closest to min without going below min's value. 
         * @param {Number} max Number whose closest match we'd like to find.
         */
        _findClosestMaxRange : function(max){            
            for(var i = focalRange.length - 1; i >= 0; i--){
                f = focalRange[i];
                if(f > max){
                    continue;
                }
                else if(f == max){
                    return i;
                }                
                else{
                    return i + 1 <= focalRange.length ? i + 1 : focalRange.length;                    
                }
            }   
        }, 
        /***
         * Returns the index of the value from apertures array that is closest to min without going below min's value. 
         * @param {Number} min Number whose closest match we'd like to find.
         */
        _findClosestAperture : function(min){
            for(var i = 0; i < apertures.length; i++){
                var a = apertures[i];
                if(a > min){
                    continue;
                }
                else if(a == min){
                    return a;
                }
                else{
                    return i > 0 ? apertures[i - 1] : apertures[0];                    
                }
            }     
        },                  
        /***
         * Sets the user-defined aperture values for the application.
         * @param {String} aps A comma-separated list of aperture values.
         */
        setApertureValues : function(aps){
            apertures = eval('[' + aps + ']').sort(function(a,b){ return b - a; });
            selectedAperture = apertures[0];
            minAperture = apertures[apertures.length - 1];
            maxAperture = apertures[0];            
            
            return this;
        },
        /***
         * Get the initial JSON Object and our resulting pool of lenses. 
         * Execute callback if defined.
         * @param {Funciton} (Optional) callback Callback method to execute once request completes.
         */
        getLenses : function(callback){
            var o = this;
            jQuery('.' + AJAX_ACTIVITY).trigger({
                type : 'lens.' + AJAX_ACTIVITY,
                info : 'Sending request for lens JSON'    
            });            

            jQuery.ajax({
                type : "GET",
                url : LENS_URL,
                dataType : "jsonp",
                success : function(data){
                    all = data.lenses;
                    categoryOrder = data.categoryOrder;  
                    teleconverters = data.teleconverters.Teleconverters;                                     
                    o._generateHtml();  
                    _bindLensEvents();   
                                          
                    jQuery('.' + LENS_LOAD).trigger({
                        type : 'lens.' + LENS_LOAD,
                        info : jQuery(LENS_LIST + ' tbody tr.lens').length + '' 
                    });
                    
                    jQuery('.' + AJAX_ACTIVITY).trigger({
                        type : 'lens.' + AJAX_ACTIVITY
                    });                    
                    
                    if(callback){
                        callback();
                    }
                },
                error : function(){
                    alert(nikonLabel && nikonLabel.error ? nikonLabel.error : "We encountered a problem. Please try again.");
                }
            });  
        },
        /***
         * Convenience method for pulling a lens by ID.
         * @param {String} id ID of lens we'd like to retrieve.
         */
        getLens : function(id){
            return all[id];
        },
        /***
         * Find and return all lenses that *do not* have a certain property.
         * In this case, the property will be a CSS class assigned to lenses in the format "no_[propertyName]". This way,
         * we can more easily take advantage of native getElementsByClassName implementations.
         * @param {String} property Property that we want lenses to match
         * @return {jQuery} jQuery Object with all lenses to be filtered out.
         */
        getFilteredLenses : function(property){
            return jQuery('li.no_' + property);
        },
        /***
         * Find and return all lenses that have a certain property.
         * @param {String} property Property that we want lenses to match
         * @return {jQuery} jQuery Object with all lenses that match filter.
         */
        getMatchingLenses : function(property){
            return jQuery('li.' + property);
        },
        /***
         * Renders and shows the Quick View layer associated with the lens of lensId
         * @param {String} lensId ID of lens whose Quick View we'd like to show
         * @param {jQuery} linkEl Link Element that triggered this show Quick View event
         */
        showQuickView : function(lensId,linkEl){
            // Hide any other visible Quick Views
            jQuery('div.qv_flyout').hide();
            
            if(document.getElementById('quickview_' + lensId)){
                $qv = jQuery('#quickview_' + lensId);
                
                var newTop = parseInt($qv.css('top'),10);
                if(newTop + $qv.height() >= (document.getElementsByTagName('html')[0].clientHeight + jQuery().scrollTop())){
                    newTop = newTop - $qv.height();
                }                
                $qv.css('top',newTop).show();    
            }
            else{
                var l = this.getLens(lensId);
                var lqv = l.quickView;
                var pos = linkEl.offset();
                var weight = (lqv.weight != null ? '<ul><li>' + lqv.weight + '</li></ul>' : '');

                var qv = '<div id="quickview_' + lensId + '" class="qv_flyout">' 
                    + '<div class="hd"><h3>' + l.name + '</h3><a href="#" class="close">[x]</a></div>'
                    + '<div class="bd clearfix">'
                    + '<div class="left"><img src="' + lqv.image.replace('/Views/','/Views/160_') + '" alt="' + l.name + '" width="160" height="136"/>';

                    qv += '<ul class="attributes clearfix">';
                    for(var i in lqv.attributes){
                        var at = lqv.attributes[i];
                        
                        qv += '<li><img src="/static/images/lens-filter/icons/' + (at ? i.toUpperCase() + '_on' : i.toUpperCase() + '_off') + '.gif" alt="' + i + '" width="28" height="18"/></li>';
                    }                    
                    qv += '</ul>';
                    
                qv += '</div>'
                    + '<div class="right">' + lqv.bullets 
                    + weight
                    + '</div></div>'
                    + '<div class="ft clearfix">'
                    + '<a href="' + l.url + '">' + (nikonLabel && nikonLabel.lensFilter ? nikonLabel.lensFilter.findOutMore : 'Find out more') + '</a>'            
                qv += '</div></div>';

                var $qv = jQuery(qv)
                            .css({
                                visibility : 'hidden'
                            })
                            .appendTo('body');
                            
                var newTop = pos.top;
                if(newTop + $qv.height() >= (document.getElementsByTagName('html')[0].clientHeight + jQuery().scrollTop())){
                    newTop = newTop - $qv.height();
                }
                
                $qv.css({
                    top : newTop,                    
                    left : pos.left - $qv.width() - linkEl.width(),    // Align edge to QV icon row
                    visibility : 'visible'
                })
                .find('a.close')
                    .bind('click',function(e){
                        e.preventDefault();
                        jQuery(this).parents('div.qv_flyout').hide();
                    });
                    
                if(isIE6 && typeof DD_belatedPNG !== 'undefined'){
                    setTimeout(function(){
                        DD_belatedPNG.fix('.qv_flyout div.hd, .qv_flyout div.ft');
                    },450);
                }
            }
        },
        /***
         * Hides all open flyouts. Suitable for making sure things are cleaned up before panels switch.
         */
        hideFlyouts : function(){
            jQuery('div.qv_flyout, div.reccoTip').hide();            
        },
        /***
         * Handles changes in the focal range slider.
         * Really just a handler for the "sliderchange" event.
         * @param {HTMLElement} slider The slider that triggered this event.
         * @param {Number} min Minimum focal range setting.
         * @param {Number} max Maximum focal range setting.
         */
        handleRangeChange : function(slider,min,max){            
            selectedMinFR = min;
            selectedMaxFR = max;
            
            jQuery('#aperture_container label').trigger('lens.' + BLACKLIST_UPDATE);
            this.filterLenses(slider);
        },
        /***
         * Adds val to our list of blacklisted/disabled aperture values and updates the interface.
         * @param {Arrary} val Items we're blacklisting.
         */
        blacklistAperture : function(val){
            for(var i = 0; i < val.length; i++){
                resultsApertureBlacklist[val[i] + ''] = true;                    
            }             
        },
        /***
         * Removes val from our list of blacklisted/disabled aperture values and updates the interface.
         * @param {Array} val Items we're removing.
         */
        removeBlacklistAperture : function(val,type){
            for(var i = 0; i < val.length; i++){                        
                delete resultsApertureBlacklist[val[i]];                
            }                 
        },  
        /***
         * Handles changes in the aperture slider.
         * @param {HTMLElement} slider The slider that triggered this call.
         * @param {Number} app New aperture value we're working with.
         */     
        handleApertureChange : function(slider,app){
            selectedAperture = app;
            this.filterLenses(slider);        
        },
        /***
         * Adds a filter to the cumulative filter list.
         * Using array will allow multiple filters to be applied.
         * @param {String|Array} f Filter(s) to add.
         */
        addFilter : function(f){
            // Quick test: If a filter's in our array, make sure it's not applied again
            if(filters.join(' ').indexOf(f) > -1){
                return false;
            }
            
            if(typeof f === 'string'){
                filters.push(f);    
            }   
            else{
                for(var i = 0; i < f.length; i++){
                    filters.push(f[i]);
                }
            } 
                     
            return this;
        },
        /***
         * Removes a filter from the cumulative filter list.
         * Using array will allow multiple filters to be removed.
         * @param {String|Array} f Filter(s) to remove.
         */
        removeFilter : function(f){
            if(typeof f === 'string'){
                var pos = jQuery.inArray(f,filters);
                if(pos > -1){
                    filters = filters.slice(0,pos).concat(filters.slice(pos + 1));                        
                } 
            }   
            else{
                for(var i = 0; i < f.length; i++){
                    var pos = jQuery.inArray(f,filters);
                    filters = filters.slice(0,pos).concat(filters.slice(pos + 1));                 
                }
            } 
             
            return this;
        },
        /***
         * Run the applicable filters. 
         * @param {HTMLElement} callingObj (Optional) Element that triggered this call.
         */
        filterLenses : function(callingObj){
            var o = this;
            var filterString = this._buildFilterClasses();

            jQuery('.' + BEFORE_FILTER).trigger({
                type : 'lens.' + BEFORE_FILTER
            });        

            if(! $lenses){
                $lenses = jQuery(LENS_LIST + ' .lens');
            }

            var tmpFocal = [];
            var tmpApertures = [];       // A list of all available apertures

            visibleLenses = $lenses
                .each(function(){
                    var $el = jQuery(this);                    

                    if($el.not(filterString).length > 0
                        && $el.matchesAperture(selectedAperture).length > 0
                        && $el.inFocalRange(focalRange[selectedMinFR],focalRange[selectedMaxFR]).length > 0){                            
                        tmpFocal.push(parseInt(this.getAttribute('data-minfocal'),10));
                        tmpFocal.push(parseInt(this.getAttribute('data-maxfocal'),10));
                        tmpApertures.push(parseFloat(this.getAttribute('data-aperture'),10));                
                        $el.show();       
                    }    
                    else{
                        $el.hide();
                    }
                })
                .filter(isIE8 ? function(){ return this.clientHeight > 0 && this.clientWidth > 0 } : (isIE6 || isIE7 ? ':not(:hidden)' : ':visible'));                        

            var tmpMinFocal = Math.min.apply(Math,tmpFocal);
            var tmpMaxFocal = Math.max.apply(Math,tmpFocal);

            tmpApertures = jQuery.unique(tmpApertures)
            var tmpMinAperture = Math.min.apply(Math,tmpApertures);
            var tmpMaxAperture = Math.max.apply(Math,tmpApertures);

            shouldSave = true;

            jQuery('.' + FILTER_COMPLETE).trigger({
                type : 'lens.' + FILTER_COMPLETE,
                info : jQuery(LENS_LIST + ' tbody tr.lens').filter(isIE8 ? function(){ return this.clientHeight > 0 && this.clientWidth > 0 } : (isIE6 || isIE7 ? ':not(:hidden)' : ':visible')).length + '',
                minFocal : LensFilter._findClosestMinRange(tmpMinFocal),
                maxFocal : LensFilter._findClosestMaxRange(tmpMaxFocal),
                apertures : tmpApertures,
                callingObj : callingObj
            });   
                
        },
        /***
         * Updates the currently displayed lens counter.
         */ 
        updateCounter : function(el,msg){
            el.html(msg);
        },
        /***
         * Builds the CSS selector we'll be filtering out via jQuery.
         * Note that we only select visible elements as the rest will be filtered already.
         * @return {Object} Selectors we'll use (i.e. ".no_dx:visible, .no_fx:visible")
         */
        _buildFilterClasses : function(){            
            var f = '';

            if(filters.length > 0){
                f = LENS_LIST + ' .' +
                    filters.join(', .').replace(filterRegex, function($0){
                        return 'no_' + $0;
                    });        
            }
            else{
                f = '';
            }

            return f;
        },       
        /***
         * Quick client-side template for rendering out lenses from our JSON backing object.
         */
        _generateHtml : function(){
            var o = this;
            var h = ''; 
            var body = jQuery(LENS_LIST + ' tbody');
            var criteria = {
                swm   : '',
                dx    : '',
                fx    : '',
                vr    : '',
                micro : '',
                pc    : '',
                ed    : '',
                n     : ''
            }
            
            var hasIf = '';            
                       
            var template = '<tr id="lens_{id}" class="lens {classes}" data-minfocal="{min}" data-maxfocal="{max}" data-aperture="{app}">'
                + '<td>{range}</td>'
                + '<td>f/{app}</td>'
                + '<td class="name leftAlign"><div><a href="{url}" class="lens_name">{name}</a><a href="#quickview_{id}" class="qv">' + (nikonLabel && nikonLabel.quickView ? nikonLabel.quickView : 'Quick View') + '</a>{new}</div></td>'                 
                + '<td class="product_number">{id}</td>' 
                + '<td {dx}>&nbsp;</td>'
                + '<td {vr}>&nbsp;</td>'
                + '<td {swm}>&nbsp;</td>'
                + '<td {ed}>&nbsp;</td>'
                + '<td {n}>&nbsp;</td>'
                + '<td {if}>&nbsp;</td>'   
                + '<td class="right">{price}</td>'                                                                             
                + '</tr>';
            
            var className = ''; // Classes = swm dx fx vr micro pc ed n
                        
            for(var i in all){
                var l = all[i];
                                
                for(var f in filterList){
                    if(l.filter[f] && l.filter[f].value == true){
                        className += f + ' ';
                        criteria[f] = 'class="has"'                        
                    }
                    else{
                        className += 'no_' + f + ' ';
                        criteria[f] = '';
                    }
                }
                
                if(l["if"] == true){
                    hasIf = 'class="has"';
                }
                
                // Handle zoom/prime as well...
                if(l.filter.zoom && l.filter.zoom.value){
                    className += 'zoom no_prime ';
                }
                else{
                    className += 'no_zoom prime ';
                }
                
                var rangeMin = l.filter.rangeMin.value || focalRange[0];
                var rangeMax = l.filter.rangeMax.value || focalRange[focalRange.length - 1];
                // If the min and max range are the same, we'll use the min, else we use min-max
                var rangeStr = rangeMin == rangeMax ? rangeMin : rangeMin + '-' + rangeMax;
                var ap = l.filter.aperture.value || apertures[0];

                className += l.subcategory.name.replace(classRegex,'');
                if(typeof categories[l.subcategory.name] === 'undefined'){
                    categories[l.subcategory.name] = true;
                }
                
                var newStr = '';
                if(l.isNew){
                    newStr = newImage;            
                }
                
                var myPrice = '&nbsp;';
                if(l.price){
                    myPrice = l.price;
                }

                h +=  template.replace('{name}',l.name).replace(/{id}/g,l.number)                
                    .replace('{classes}',className)
                    .replace('{url}',l.url)
                    .replace('{new}',newStr)
                    .replace('{min}',rangeMin)
                    .replace('{max}',rangeMax)
                    .replace('{range}',rangeStr)                    
                    .replace(/{app}/g,ap)
                    .replace('{swm}',criteria.swm)
                    .replace('{dx}',criteria.dx)
                    .replace('{fx}',criteria.fx)
                    .replace('{vr}',criteria.vr)
                    .replace('{micro}',criteria.micro)
                    .replace('{ed}',criteria.ed)
                    .replace('{n}',criteria.n)
                    .replace('{if}',hasIf)
                    .replace('{price}',myPrice);
                    
                className = swm = dx = fx = vr = micro = pc = hasIf = '';
            }            

            var newHtml = jQuery('#tmp_html').append(h).find('tr');
            var tBody = body.css('visibility','hidden');

            for(var i = 0; i < categoryOrder.length; i++){
                var cat = categoryOrder[i];
                for(var j in cat){                    
                    var subcats = cat[j];
                    for(var s = 0; s < subcats.length; s++){
                        var subcat = subcats[s];
                        var categoryItems = newHtml.filter('.' + subcat.replace(classRegex,''));
                        if(categoryItems.length > 0){
                            body
                                .append('<tr class="subcategory" data-subcategory="' + subcat.replace(classRegex,'') + '"><td colspan="11" class="lensType">' + subcat + '</td></tr>')
                                .append(categoryItems);                    
                        }                    
                    }                    
                }
            }

            criteria.swm = criteria.dx = criteria.fx = criteria.vr = criteria.micro = criteria.pc = '';                     
            
            /***
             * Append the Teleconverters' markup to our result set
             */
            if(teleconverters.length > 0){
                body
                    .append('<tr id="teleconverters" class="categoryHead teleconverter"><td colspan="11" class="teleHead leftAlign">' + (nikonLabel && nikonLabel.lensFilter ? nikonLabel.lensFilter.teleconverters : "Teleconverters")+ '</td></tr>');
            }
            for(var i = 0; i < teleconverters.length; i++){
                var subcat = teleconverters[i];
                var $subcatLenses = newHtml.filter('.' + subcat.replace(classRegex,''));
                
                if($subcatLenses.length > 0){
                    body.append('<tr class="teleconverter"><td colspan="11" class="lensType">' + subcat + '</td></tr>');
                    $subcatLenses.each(function(idx){
                        var sEl = jQuery(this);
                        var name = sEl.find('td.name').remove('a.qv').html();
                        var prodNumber = sEl.find('td.product_number').html();
                        var price = sEl.find('td:last').html();
                        
                        var templateStr = '<tr class="teleconverter"><td class="empty" colspan="2">&nbsp;</td>'
                            + '<td class="leftAlign name">' + name + '</td>'
                            + '<td class="leftAlign product_number" colspan="7">' + prodNumber + '</td>'
                            + '<td>' + price + '</td></tr>';
                        
                        jQuery(templateStr)
                            .addClass(idx % 2 > 0 ? 'alt' : '')
                            .find('a.qv')
                            .remove()
                            .end()
                            .appendTo(body);
                    })
                    .remove();
                }
            }
            
            jQuery('#tmp_html').remove();   

            for(var i in lensesByType){
                lensesByType[i] = jQuery(LENS_LIST + ' tr.' + i);
            }

            visibleLenses = jQuery(LENS_LIST + ' tr.lens').filter(':odd').addClass('alt').end().filter(':visible');                      
            
            if(isIE6){
                visibleLenses.bind('mouseenter',function(){
                    jQuery(this).addClass('lens_hover');
                })        
                .bind('mouseleave',function(){
                    jQuery(this).removeClass('lens_hover');
                })
            }
            
            $lenses = jQuery(visibleLenses);
            $lenses
                .find('a.lens_name')
                .ellipsify()
                .end()
                .find('a.qv').bind('click',function(e){
                    e.preventDefault();
                    var lensId = this.href.substr(this.href.indexOf('#quickview_') + '#quickview_'.length);
                    o.showQuickView(lensId,jQuery(this));  
                });
            tBody.css('visibility','visible');   
        },
        /***
         * Shows all lenses. Used when flipping back to the initial screen.
         */
        showAll : function(){
            if(isIE8){
                jQuery('#lens_list tbody tr').show();                                
            }
            else{
                jQuery('#lens_list tbody tr:hidden').show();                                
            }
        },
        /***
         * Hides the teleconverters. Called when filter tools display as teleconverters only display on initial screen.
         */
        hideTeleconverters : function(){
            jQuery('#lens_list tr.teleconverter').hide();
        },
        /***
         * Sugar method for determining whether or not filters have been run.
         */
        isFiltered : function(){
            return shouldSave;
        },
        /***
         * Sugar for toggling the state in external classes.
         * @param {Boolean} bool Value we're setting shouldSave to.
         */
        setSave : function(bool){
            shouldSave = bool;
            return this;
        },
        /***
         * Saves the state of the application, persisting it to a cookie.
         */
        saveState : function(){
            var cookieData = '';    // Will wind up a as a ;-delimited list of name/value pairs.
            
            // Current "view" state. Will wind up as ID of "view" i.e. 'lens_filter'.
            // We also correct help tabs to display the filter tools.
            var currentState = FinderPanels.getCurrentPanel().attr('id');
            if(currentState == HELP_CONTAINER_ID){
                currentState = FILTER_ID;
            }
            cookieData += 'currentState=' + currentState + ';';
            
            // Focal Range slider - save max and min values
            var focalValues = jQuery('#focal_range').slider('values');
            cookieData += 'minFocalValue=' + focalValues[0] + ';maxFocalValue=' + focalValues[1] + ';';   
            
            // Aperture slider - save value
            cookieData += 'apertureValue=' + jQuery('#aperture').slider('value') + ';';
            
            // Boolean filter radio buttons
            var $bf = jQuery('#boolean_filters')
                        .find('input:checked');

            $bf.each(function(i){
                // We'll be building a construct that ultimate looks like this: booleans=[...] for eval-ing
                if(i == 0){
                    cookieData += 'booleans=[';
                }                
                
                // Grab the currently selected item's ID and add to our data
                cookieData += '"' + this.getAttribute('id') + '"';
                
                // Last item? Close our construct.
                if(i == $bf.length - 1){
                    cookieData += '];';             
                }
                else{
                    cookieData += ',';
                }
            });
            jQuery.cookie(STATE_COOKIE,cookieData);    
        },        
        loadState : function(){                
            var cookieData = jQuery.cookie('lens-filter-state');    
            if(cookieData && cookieData.length > 0){    
                // Hide results list/show loading animation?
                jQuery('.' + LOAD_STATE_START).trigger({
                    type : 'lens.' + LOAD_STATE_START
                });
                setTimeout(function(){
                    // Set appropriate slider values
                    var minFocalRange = cookieData.match(/minFocalValue=(\d+);/)[1];
                    var maxFocalRange = cookieData.match(/maxFocalValue=(\d+);/)[1];
                    
                    jQuery('#focal_range').slider('values',0,minFocalRange);
                    setTimeout(function(){
                        jQuery('#focal_range').slider('values',1,maxFocalRange);                        
                    },10);
                    
                    var maxAperture = cookieData.match(/apertureValue=(\d+);/)[1];
                    jQuery('#aperture').slider('value',maxAperture);
                    
                    // Set Boolean filter values
                    var booleans = eval(cookieData.match(/booleans=(\[.*\])/)[1]);    // To an array...
                    for(var i = 0; i < booleans.length; i++){
                        jQuery('#' + booleans[i]).click();
                    }
                          
                    // Set current "view" state.
                    var currentState = cookieData.match(/currentState=([^;]*)/)[1];
                    FinderPanels.panelSwitch('#' + currentState);
                    
                    // Remove any loading animation goodness.                
                    jQuery('.' + LOAD_STATE_END).trigger({
                        type : 'lens.' + LOAD_STATE_END
                    });  
                },550)  
            }
            else{
                // Remove any loading animation goodness.                
                jQuery('.' + LOAD_STATE_END).trigger({
                    type : 'lens.' + LOAD_STATE_END
                });                  
            }          
        },
        clearState : function(){
            jQuery.cookie(STATE_COOKIE,null);
            return this;
        }  
    }
}();


/***
 * A few miscellaneous jQuery plugins for doing lens range comparisons.
 */
jQuery.fn.inFocalRange = function(min,max){ 
    return this.filter(function(){
        return parseInt(this.getAttribute('data-minfocal')) >= min && parseInt(this.getAttribute('data-maxfocal')) <= max;
    });    
}

jQuery.fn.matchesAperture = function(val){ 
    return this.filter(function(){
        return parseFloat(this.getAttribute('data-aperture')) <= parseFloat(val);
    });    
}

/***
 * ...and one for dynamically rendering labels for sliders.
 * @param {Array} labels Array of labels to add to a slider control.
 * @param {String} prefix (Optional) Prefix to add to each label.
 */
jQuery.fn.addLabels = function(labels,prefix){
    return this.each(function(){
        var v = jQuery.browser.version;
        var m = jQuery.browser.msie;
        var isIE6 = m && v < 7;
        var isIE7 = m && v < 8 && v > 6;
        
        var el = jQuery(this);
        // labels.length - 1 -> We aren't really counting the first item
        var width = parseInt(100 / (labels.length - 1),10); 
           
        var offset = width / (labels.length - 1);
        // Round the IE's values, as they can't grok fractional percentages.
        if(isIE6 || isIE7){
            offset = Math.ceil(offset);    
        }
        
        var str = '<div class="axis clearfix">';
        for(var i = 0; i < labels.length; i++){
            str += '<label ';
            if(i == 0){
                str += 'class="first" style="width:' + width + '%;margin-left:-' + offset + '%;">';
            }
            else if(i == labels.length - 1){
                str += 'class="last" style="width:' + width + '%">';
            }
            else{
                str += 'style="width:' + width + '%;">';                
            }
            
            if(prefix){
                str += prefix;    
            } 
            str += labels[i] + '</label>';
        }
        str += '</div>';
        jQuery(str).insertBefore(el);    
    });
}

/***
 * Truncates lens names so that they do not "run into" new and quick link icons.
 */
jQuery.fn.ellipsify = function(){
    var ELLIPSIS = '...';
    
    var truncate = function(el,l,w,rightBoundary){
        var h = el.html().split(' ');

        var width = w;
        el.wrap('<span title="' + h + '"></span>')
            .html('<span>' + h.join('</span> <span>') + '</span>');          

        while(l + width >= rightBoundary){
            width = el.find('span:eq(0)').remove().end().width();                    
        }
        el.html(el.html() + ELLIPSIS);
    }
    
    return this.each(function(){
        var el = jQuery(this);
        var rightBoundary =  el.next().offset().left;
        var l = el.offset().left;
        var w = el.width();
        if(l + w  >= rightBoundary){
            truncate(el,l,w,rightBoundary);    
        }   
        return true;
    });  
};

/***
 * Reduce an array of Strings or Numbers to only unique values.
 * @param {Array} arr Array we're operating on.
 * @return {Array} Array of unique values from arr.
 */
jQuery.unique = function(arr){
    var o = {};
    var a = [];
    for(var i = 0; i < arr.length; i++){
        var ai = arr[i];
        if(o[ai]){
            continue;
        }
        a.push(ai);
        o[ai + ''] = true;
    }
    return a;
}

jQuery().ready(function(){
    LensFilter.init().getLenses(function(){
        LensFilter.loadState();
    }); 
});

/**
 * Cookie plugin
 *
 * Copyright (c) 2006 Klaus Hartl (stilbuero.de)
 * Dual licensed under the MIT and GPL licenses:
 * http://www.opensource.org/licenses/mit-license.php
 * http://www.gnu.org/licenses/gpl.html
 *
 */

/**
 * Create a cookie with the given name and value and other optional parameters.
 *
 * @example $.cookie('the_cookie', 'the_value');
 * @desc Set the value of a cookie.
 * @example $.cookie('the_cookie', 'the_value', { expires: 7, path: '/', domain: 'jquery.com', secure: true });
 * @desc Create a cookie with all available options.
 * @example $.cookie('the_cookie', 'the_value');
 * @desc Create a session cookie.
 * @example $.cookie('the_cookie', null);
 * @desc Delete a cookie by passing null as value. Keep in mind that you have to use the same path and domain
 *       used when the cookie was set.
 *
 * @param String name The name of the cookie.
 * @param String value The value of the cookie.
 * @param Object options An object literal containing key/value pairs to provide optional cookie attributes.
 * @option Number|Date expires Either an integer specifying the expiration date from now on in days or a Date object.
 *                             If a negative value is specified (e.g. a date in the past), the cookie will be deleted.
 *                             If set to null or omitted, the cookie will be a session cookie and will not be retained
 *                             when the the browser exits.
 * @option String path The value of the path atribute of the cookie (default: path of page that created the cookie).
 * @option String domain The value of the domain attribute of the cookie (default: domain of page that created the cookie).
 * @option Boolean secure If true, the secure attribute of the cookie will be set and the cookie transmission will
 *                        require a secure protocol (like HTTPS).
 * @type undefined
 *
 * @name $.cookie
 * @cat Plugins/Cookie
 * @author Klaus Hartl/klaus.hartl@stilbuero.de
 */

/**
 * Get the value of a cookie with the given name.
 *
 * @example $.cookie('the_cookie');
 * @desc Get the value of a cookie.
 *
 * @param String name The name of the cookie.
 * @return The value of the cookie.
 * @type String
 *
 * @name $.cookie
 * @cat Plugins/Cookie
 * @author Klaus Hartl/klaus.hartl@stilbuero.de
 */
jQuery.cookie = function(name, value, options) {
    if (typeof value != 'undefined') { // name and value given, set cookie
        options = options || {};
        if (value === null) {
            value = '';
            options.expires = -1;
        }
        var expires = '';
        if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) {
            var date;
            if (typeof options.expires == 'number') {
                date = new Date();
                date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000));
            } else {
                date = options.expires;
            }
            expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE
        }
        // CAUTION: Needed to parenthesize options.path and options.domain
        // in the following expressions, otherwise they evaluate to undefined
        // in the packed version for some reason...
        var path = options.path ? '; path=' + (options.path) : '';
        var domain = options.domain ? '; domain=' + (options.domain) : '';
        var secure = options.secure ? '; secure' : '';
        document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join('');
    } else { // only name given, get cookie
        var cookieValue = null;
        if (document.cookie && document.cookie != '') {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = jQuery.trim(cookies[i]);
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) == (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }
};