function isIE()
{
    return navigator.appName.indexOf("Internet Explorer") >= 0;
}


MFilterContext.InitialStatus = "Select one or more.";

function MFilterContext(multiSelect, activeFilterIds, allKeywords, allFilterIds, bulletStyle, initialSearchString, multiColumn)
{

    this.filterTable = $('filterTable');
    this.filterSearchField = $('filterSearchField');

    this.rolloversDisabled = false;
    this.rolledOverElements = new Array();

    // mark selected filters
    this.multiSelect = multiSelect;
    this.bulletStyle = bulletStyle;
    this.initialSearchString = initialSearchString;
    this.multiColumn = multiColumn;
    this.selectedFilters = new Array();
    for (var i = 0; i < activeFilterIds.length; i++) {
        var filter = $(activeFilterIds[i]);
        // may be null if we have an active filter that is now not shown due to other choices
        if (filter != null) {
            if (filter.className.indexOf('filterChoiceHilite') >= 0) {
                this.select(filter, false);
            }
        }
    }

    this.keywords = allKeywords;
    this.filters = allFilterIds;

    this.filterStatusLabel = $('filterStatusMessage');
    this.updateStatus();
}

// slower initialization here for now, so we can get document.filterContext set quickly
MFilterContext.prototype.setup = function()
{
    if (this.filterSearchField) {
        this.filterSearchField.value = this.initialSearchString;

        this.origFilterContainers = new Array();
        this.findFilterContainers(this.filterTable);

        this.filterSearchField.onkeyup = this.searchChanged.bindAsEventListener(this);
        this.filterSearchField.onclick = this.searchFieldClicked.bindAsEventListener(this);
        
        this.filterSearchField.focus();     // safari needs this too
        this.filterSearchField.select();
    }
}

MFilterContext.prototype.findFilterContainers = function(node)
{
    var kids = node.childNodes;
    for (var i = 0; i < kids.length; i++) {
        var kid = kids[i];
        if (kid.tagName == "P") {
            this.origFilterContainers.push(node);
            return;
        } else if (kid.tagName) {
            this.findFilterContainers(kid);
        }
    }
}

MFilterContext.getFilterLabel = function(filter)
{
    var label = filter.label;       // try cached value
    if (label == null) {
        label = filter.firstChild.nodeValue;
        if (label == null) {
            // skip a bullet span we added when selecting nodes
            label = filter.firstChild.nextSibling.nodeValue;
        }
        filter.label = label;
    }
    return label;
}

MFilterContext.filterComparator = function(f1, f2)
{
    var label1 = MFilterContext.getFilterLabel(f1);
    var label2 = MFilterContext.getFilterLabel(f2);
    if (label1 < label2) {
        return -1;
    } else if (label1 > label2) {
        return 1;
    } else {
        return 0;
    }
}

MFilterContext.prototype.findMatchingNodes = function(searchString)
{
    if (searchString.length > 0) {
        var start = this.keywords.binarySearch(searchString);
        if (start < 0) {
            // exact match not found, so start what would be the insertion point
            start = -start-1;
        } else {
            // since we may have duplicate keywords, must back up to find earlier matches
            for (var i = start-1; i >= 0 && this.keywords[i].indexOf(searchString) == 0; i--) {
                start = i;
            }
        }

        // accumulate all the matches
        var results = new Array();
        for (var i = start; i < this.keywords.length; i++) {
            if (this.keywords[i].indexOf(searchString) == 0) {
                // we cache the result of resolving these ids in place of the ids
                var node = this.filters[i];
                if (typeof node == "string") {
                    node = document.getElementById(node);
                    this.filters[i] = node;
                }
                results.push(node);
            } else {
                break;  // we've gone past the range of prefix matches
            }
        }
        
        // If we found anything, add in the currently selected filters, but don't
        // add only those to a result set
        if (results.length > 0 && this.selectedFilters.length > 0) {
            results = results.concat(this.selectedFilters);
        }

        results.sort(MFilterContext.filterComparator);
        results.unique(MFilterContext.getFilterLabel);

        return results;
    } else {
        throw Error("Should not search for empty string");
    }
}

MFilterContext.ColumnLengthCutoffs = [0, 7, 20*2, 1000*1000*1000];

// Make new containers (one for each column), fill them with newMatches
MFilterContext.prototype.fillNewContainers = function(newMatches)
{
    if (this.newFilterContainers == null) {
        this.newFilterContainers = new Array(this.origFilterContainers.length);
        for (var i = 0; i < this.origFilterContainers.length; i++) {
            var origContainer = this.origFilterContainers[i];
            var newContainer = this.newFilterContainers[i] = origContainer.cloneNode(false);
            // the new and original ones point at each other
            newContainer.mate = origContainer;
            origContainer.mate = newContainer;
            // install new one in the DOM, undisplayed
            newContainer.style.display = "none";
            origContainer.parentNode.appendChild(newContainer);
        }
    } else {
        // existing columns should have been emptied
        for (var i = 0; i < this.newFilterContainers.length; i++) {
            if (this.newFilterContainers[i].hasChildNodes()) {
                throw Error("Unexpected nodes:" + this.newFilterContainers[i].firstChild);
            }
        }
    }

    // Column filling code is a copy of the algorithm in FilterDialog.java
    var numColumns = 1;
    if (this.multiColumn) {
        for (; newMatches.length > MFilterContext.ColumnLengthCutoffs[numColumns]; numColumns++)
            ;
    }
    var columnLength = Math.floor((newMatches.length-1) / numColumns) + 1;

    // put new nodes into columns
    var filterIndex = 0;
    for (var i = 0; i < numColumns; i++) {
        var container = this.newFilterContainers[i];
        for (var j = 0; j < columnLength && filterIndex < newMatches.length; j++) {
            var filter = newMatches[filterIndex++];
            if (filter.originalParent == null) {
               // record where it originally came from so we can put it back later, in order
               filter.originalParent = filter.parentNode;
               filter.originalNextSibling = filter.nextSibling;
            }
            container.appendChild(filter);
        }
    }
}

// Return nodes in these containers back to their original home
MFilterContext.prototype.returnOrphans = function()
{
    var containers = this.newFilterContainers;
    if (containers != null) {    
        // must go in reverse order to each node's original sibling is inserted before it
        for (var i = containers.length-1; i >= 0; i--) {
            var filters = containers[i].childNodes;
            for (var j = filters.length-1; j >= 0; j--) {
                var filter = filters[j];
                if (filter.originalParent != null) {
                    filter.originalParent.insertBefore(filter, filter.originalNextSibling);
                }
            }
        }
    }
}

// Install the given containers in the DOM
MFilterContext.prototype.installContainers = function(containers)
{
    for (var i = 0; i < containers.length; i++) {
        var container = containers[i];
        container.mate.style.display = "none";
        container.style.display = "block";
    }
}

MFilterContext.prototype.searchChanged = function(event)
{
    this.disableRollovers();

    // If you type fast, this might be called twice when the text field has the same value
    var searchString = this.filterSearchField.value.toLowerCase();
    if (searchString == this.lastSearchString || searchString == this.initialSearchString) {
        return;
    }

    var msg = $('noMatchMessage');

    // Put all nodes back first.  They must all go back at the same time to be able to keep them in order.
    this.returnOrphans();

    if (searchString.length == 0) {
        this.installContainers(this.origFilterContainers);
        msg.style.display = "none";
    } else {
        var newMatches = this.findMatchingNodes(searchString);

        this.fillNewContainers(newMatches);
            
        this.installContainers(this.newFilterContainers);
        if (newMatches.length == 0) {
            msg.style.display = "block";
        } else {
            msg.style.display = "none";
        }
    }

    this.updateStatus();
    
    this.lastSearchString = searchString;
}

MFilterContext.prototype.searchFieldClicked = function(event)
{
    if (this.filterSearchField.value == this.initialSearchString) {
        this.filterSearchField.select();
    }
}

MFilterContext.prototype.select = function(element, changeClassToo)
{
    this.selectedFilters.push(element);

    if (changeClassToo) {
        Element.addClassName(element, 'filterChoiceHilite');
    }
    
    // add a span with a bullet
    var newSpan = document.createElement("span");
    newSpan.className = this.bulletStyle;
    var textNode = document.createTextNode("\u2022");
    newSpan.appendChild(textNode);
    element.insertBefore(newSpan, element.firstChild);
}

MFilterContext.prototype.unselect = function(element, removeToo)
{
    // remove this element from the active list
    if (removeToo) {
        for (var i = 0; i < this.selectedFilters.length; i++) {
            if (this.selectedFilters[i] == element) {
                this.selectedFilters.splice(i, 1);
                break;
            }
        }
    }

    Element.removeClassName(element, 'filterChoiceHilite');
    
    // remove the bullet we added
    element.removeChild(element.firstChild);
}

MFilterContext.prototype.toggle = function(element)
{
    if (Element.hasClassName(element, 'filterChoiceHilite')) {
        this.unselect(element, true);
    } else {
        // Make sure only 1 discount filter is selected.
        if (this.multiSelect) {
            this.clearFilters()
        }
        
        this.select(element, true);
    }

    this.updateStatus();
    
    if (this.filterSearchField != null) {
        this.filterSearchField.focus();
        if (this.filterSearchField.value == this.initialSearchString) {
            this.filterSearchField.select();
        } else if (isIE()) {
            // IE dumbly forgets the selection, puts the caret at the start. The end is a better default behavior.
            range = document.selection.createRange();
            range.moveStart('character', this.filterSearchField.value.length);
            range.moveEnd('character', this.filterSearchField.value.length);
            range.select();
        }
    }
}

MFilterContext.prototype.updateStatus = function()
{
    if (this.selectedFilters.length == 0) {
        Element.addClassName($('clearButton'), 'buttonCellDisabled');
    } else {
        Element.removeClassName($('clearButton'), 'buttonCellDisabled');
    }

    if (this.filterStatusLabel != null) {
        if (this.selectedFilters.length == 0) {
            this.filterStatusLabel.innerHTML = MFilterContext.InitialStatus;
        } else if (this.selectedFilters.length == 1) {
            this.filterStatusLabel.innerHTML = "1 item selected.";
        } else {
            this.filterStatusLabel.innerHTML = this.selectedFilters.length + " items selected.";
        }
    }
}

// The rollover effect is distracting while typing

MFilterContext.prototype.disableRollovers = function()
{
    this.rolloversDisabled = true;
    for (var i = this.rolledOverElements.length-1; i >= 0; i--) {
        Element.removeClassName(this.rolledOverElements[i], 'filterChoiceRollover');
    }
    this.rolledOverElements.splice(0);
    this.mousemoveEventHandler = this.enableRollovers.bindAsEventListener(this);
    Event.observe(document, "mousemove", this.mousemoveEventHandler);
}

MFilterContext.prototype.enableRollovers = function()
{
    this.rolloversDisabled = false;
    Event.stopObserving(document, "mousemove", this.mousemoveEventHandler);
}

MFilterContext.prototype.mouseOver = function(element)
{
    if (!this.rolloversDisabled) {
        this.rolledOverElements.push(element);
        Element.addClassName(element, 'filterChoiceRollover');
    }
}

MFilterContext.prototype.mouseOut = function(element)
{
    for (var i = this.rolledOverElements.length-1; i >= 0; i--) {
        this.rolledOverElements.splice(i, 1);
    }
    Element.removeClassName(element, 'filterChoiceRollover');
}

MFilterContext.prototype.clearFilters = function()
{
    for (var i = 0; i < this.selectedFilters.length; i++) {
        this.unselect(this.selectedFilters[i], false);
    }
    this.selectedFilters = new Array();

    this.updateStatus();
 
    return false;
}

MFilterContext.prototype.applyFilters = function(url)
{

    for (var i = 0; i < this.selectedFilters.length; i++) {
        url += "&nfl[]=" + this.selectedFilters[i].id;
    }
    window.location.href = url;
    
    return false;
}

//
// Brands page support
//
MFilterContext.prototype.initBrandsPage = function(url, catId)
{
    this.setup();
    this.rawBrandsUrl = url;
    var anchor = window.location.hash;
    if (anchor && anchor.length > 3 && anchor.charAt(1) == 's' && (anchor.charAt(2) == 'b' || anchor.charAt(2) == 'r')) {
        var brandId = anchor.substring(2);
        var brandLink = $(brandId);
        if (brandLink != null) {
            this.selectBrand(brandLink, brandId, catId);
            $('filterTable').scrollTop = brandLink.offsetTop;
        }
    }
}

MFilterContext.prototype.brandClicked = function(element, brandId, catId)
{
    var selected = this.selectedFilters.pop();
    if (selected != null) {
        selected.className = 'brandListItem';
    }

    this.selectBrand(element, brandId, catId);
    tx_setLocationAnchor('s' + brandId);
}

MFilterContext.prototype.selectBrand = function(element, brandId, catId)
{
    document.filterContext.selectedFilters.push(element);
    element.className = this.bulletStyle;
    store_ajaxUpdateDiv(this.rawBrandsUrl + '?fl=' + brandId + '&cat=' + catId, 'browserContents');
}

// Ported from http://googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-nearly.html.
// NOTE this assumes distinct values.  If there are duplicates, is might not return the first one.
Array.prototype.binarySearch = function(key)
{
    var low = 0;
    var high = this.length - 1;

    while (low <= high) {
        var mid = (low + high) >>> 1;
        var midVal = this[mid];

        if (midVal < key)
            low = mid + 1;
        else if (midVal > key)
            high = mid - 1;
        else
            return mid; // key found
    }
    return -(low + 1);  // key not found.
}

// removes duplicates from a SORTED array
Array.prototype.unique = function(valueFunc)
{
    if (this.length < 2) {
        return;
    }

    if (valueFunc == null) {
        valueFunc = function(x) {return x;};
    }

    var prev = valueFunc(this[0]);
    for (var i = 1; i < this.length; ) {
        var curr = valueFunc(this[i]);
        if (prev == curr) {
            this.splice(i, 1);
        } else {
            prev = curr;
            i++;
        }
    }
}