
if (typeof(Object.Event) == "undefined") {
    throw "filter.js requires Object.Event to be loaded.";
}

/*
    API:
    itemContainers are the immediate parents of the individual block items that can be searched for.  If using a table,
       don't pass TDs, but use a DIV inside the TD, because this library will swap the containers in and out of the DOM
       which doesn't work with TDs.
    searchField is the INPUT with type=text where the user types
    allKeywords is a sorted array of keywords that can be searched for (including nested words with item names, if desired)
    allFilterIds is a parallel array to allKeywords, with the corresponding DOM id of the element with that keyword
    labelFunc is an optional func that returns the text label of an item.  There is a default implementation that works
       for a text node immediately inside the element (the FilterDialog case).

    (We should get rid of all the other params and let clients update that stuff based on events that are posted.
    The selectedNodes could be added by clients in response to a notification, and then all selection handling could
    be outside this class.  Same story for all the rollover on/off business.)

    Events posted via livepipe's Object.Event mechanism:
      didSearch - sent after a text search is performed
    
*/
function MFilterContext(itemContainers, msgDiv, searchField, multiSelect, activeFilterIds, allKeywords, allFilterIds, bulletStyle,
                        labelFunc, hideLargeLists,
                        initialSearchString, zeroSelectedMsg, oneSelectedMsg, manySelectedMsg, noMatchesMsg, pleaseTypeMsg)
{
    this.origFilterContainers = itemContainers;
    this.hideLargeLists = hideLargeLists;
    this.makeNewContainers();

    this.msgDiv = msgDiv;

    this.filterSearchField = searchField;
    this.labelFunc = labelFunc ? labelFunc : MFilterContext.defaultLabelFunc;

    this.rolloversDisabled = false;
    this.rolledOverElements = new Array();
    
    this.zeroSelectedMsg = zeroSelectedMsg;
    this.oneSelectedMsg = oneSelectedMsg;
    this.manySelectedMsg = manySelectedMsg;
    this.noMatchesMsg = noMatchesMsg;
    this.pleaseTypeMsg = pleaseTypeMsg;

    // mark selected filters
    this.multiSelect = multiSelect;
    this.multiSelectGroups = null;
    this.bulletStyle = bulletStyle;
    this.initialSearchString = initialSearchString;
    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();

    if (this.hideLargeLists) {
        // containers will be empty, start with the "please type" message
        this.showMessage(this.pleaseTypeMsg);
    }

    // Switch controlling a gross hack - In IE6, checkboxes lose their on/off state when they are removed/added
    // to the DOM, so we have extra hackery to save and restore this state when doing such manipulation.
    // Do this only if we are on IE6, and if our first item container even has any checkboxes.
    this.doCheckboxFixup = false;
    if (isIE6() && itemContainers.length > 0) {
        var firstItem = findFirstNontextChild(itemContainers[0]);
        var inputs = firstItem.getElementsByTagName('INPUT');
        if (inputs && inputs.length > 0) {
            this.doCheckboxFixup = true;
         }
     }
}

// allows us to use livepipe observe/notify mechanism
Object.Event.extend(MFilterContext);

// 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.filterSearchField.onkeydown = this.blockCR.bindAsEventListener(this);
        this.filterSearchField.onkeyup = this.searchChanged.bindAsEventListener(this);
        this.filterSearchField.onclick = this.searchFieldClicked.bindAsEventListener(this);
        
        this.filterSearchField.focus();     // safari needs this too
        this.filterSearchField.select();
    }
}

// Sets a bunch of regExps, which divide the filters into groups of mutually exclusive
// filters for detemining which items can be selected at the same time.
MFilterContext.prototype.setMultiSelectGroups = function(regExps)
{
    this.multiSelectGroups = regExps;
}

MFilterContext.prototype.getSearchField = function()
{
    return this.filterSearchField;
}

// The default impl of labelFunc, which works for the simple case of an element that immediately
// contains a text node.
MFilterContext.defaultLabelFunc = function(filter)
{
    label = filter.firstChild.nodeValue;
    // be tolerant of a bullet span we added when selecting nodes
    return label ? label : filter.firstChild.nextSibling.nodeValue;
}

// return the text label for a given item
MFilterContext.prototype.getFilterLabel = function(filter)
{
    var label = filter.label;       // try cached value
    if (label == null) {
        filter.label = this.labelFunc(filter);
    }

    if (filter.label == null) {
        throw Error("Could not find label for node:" + filter);
    }
    return filter.label;
}

MFilterContext.prototype.filterComparator = function(f1, f2)
{
    var label1 = this.getFilterLabel(f1).toLowerCase();
    var label2 = this.getFilterLabel(f2).toLowerCase();
    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(this.filterComparator.bind(this));
        results.unique(this.getFilterLabel.bind(this));

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

MFilterContext.prototype.makeNewContainers = function()
{
    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;
        if (this.hideLargeLists) {
            // install new one in the DOM, displayed.  Yank old one out of this part of the DOM, leave
            // it at the end of the body but undisplayed.
            origContainer.parentNode.appendChild(newContainer);
            newContainer.style.display = "block";
            origContainer.style.display = "none";
        } else {
            // install new one in the DOM, undisplayed
            newContainer.style.display = "none";
            origContainer.parentNode.appendChild(newContainer);
        }
    }
}

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

// Make new containers (one for each column), fill them with newMatches.  We don't create new DOM elements
// for the new containers, we just move the elements that were created on page load into new containers,
// and mark them with breadcrumbs so we can later put them back.  This is less crunching of the DOM than
// removing all the non-matches.
MFilterContext.prototype.fillNewContainers = function(newMatches)
{
    // 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.origFilterContainers.length > 1) {
        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;
            }

            var checkboxToRestore = this.doCheckboxFixup ? this.getTurnedOnInput(filter) : null;
            container.appendChild(filter);
            if (checkboxToRestore) {
                checkboxToRestore.checked = true;
            }
        }
    }
}

// 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) {
                    if (filter.originalNextSibling && filter.originalParent != filter.originalNextSibling.parentNode) {
                        throw 'Return of node will fail:' + filter.originalParent + filter.originalNextSibling + filter.originalNextSibling.parentNode;
                    }

                    var checkboxToRestore = this.doCheckboxFixup ? this.getTurnedOnInput(filter) : null;
                    filter.originalParent.insertBefore(filter, filter.originalNextSibling);
                    if (checkboxToRestore) {
                        checkboxToRestore.checked = true;
                    }
                }
            }
        }
    }
}

// Returns the first INPUT node within item, if that input is turned on
MFilterContext.prototype.getTurnedOnInput = function(item)
{
    var item = item.getElementsByTagName('INPUT')[0];
    return item.checked ? item : null;
}

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

// Show a given text message in place of results, or just hide the msg div.  We assume
// that when we're showing a message the divs with the results are empty, so we don't
// bother with the work of hiding them.
MFilterContext.prototype.showMessage = function(msg)
{
    if (msg) {
        this.msgDiv.innerHTML = msg;
        this.msgDiv.style.display = "block";
    } else {
        this.msgDiv.style.display = "none";
    }
}

MFilterContext.prototype.blockCR = function(event)
{
    if (event.keyCode == 13 ) {
        Event.stop(event);
    }
}

MFilterContext.prototype.searchChanged = function(event)
{
    try {
        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().trim();
        if (searchString != this.lastSearchString && searchString != this.initialSearchString) {
            // Put all nodes back in the original divs first.  They must all go back at the same time
            // so we can keep them in order when we pluck out the next bunch of matches.
            this.returnOrphans();

            if (searchString.length == 0) {
                if (this.hideLargeLists) {
                    // containers will be empty, just show the "please type" message
                    this.showMessage(this.pleaseTypeMsg);
                } else {
                    // show the original containers, with all the filters visible, hide the message
                    this.swapContainers(this.origFilterContainers);
                    this.showMessage(null);
                }

            } else {
                var newMatches = this.findMatchingNodes(searchString);

                // We show the results if
                //    1) We're in the mode that always shows results
                //    2) The user has already typed two chars
                //    3) The results are small enough to be quick to show
                if (!this.hideLargeLists || searchString.length >= 2 || newMatches.length < (isIE() ? 100 : 800)) {
                    this.fillNewContainers(newMatches);
    
                    if (!this.hideLargeLists) {
                        // If we might show large lists, we need to swap the original containers with the ones
                        // we create.  If we never show large list, the original ones are never visible.
                        this.swapContainers(this.newFilterContainers);
                    }
                    if (newMatches.length == 0) {
                        // show NoMatches
                        this.showMessage(this.noMatchesMsg);
                    } else {
                        // show no message
                        this.showMessage(null);
                    }
                } else {
                    // containers will be empty, just show the "please type" message
                    this.showMessage(this.pleaseTypeMsg);
                }
            }

            this.updateStatus();
            this.lastSearchString = searchString;
            this.notify('didSearch');
        }
    } catch (e) {
//        alert(e.name + '::' + e.message);
        throw e;
    }
}

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 {
        if (!this.multiSelect) {
            if (!this.multiSelectGroups) {
                // Make sure only 1 filter is selected.
                this.clearFilters(null);
            } else {
                // Make sure only 1 filter from each group is selected.
                for (var i = 0; i < this.multiSelectGroups.length; i++) {
                    var multiSelectGroup = this.multiSelectGroups[i];
                    if (multiSelectGroup.test(element.id)) {
                        this.clearFilters(multiSelectGroup);
                    }
                }
            }
        }
        
        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 ($('clearButton') != null) {
        var clearButtonDiv = Element.select($('clearButton'), 'div')[0];
        if (this.selectedFilters.length == 0) {
            clearButtonDiv.className = 'buttonCellDisabled';
        } else {
            clearButtonDiv.className = 'buttonCell';
        }
    }

    if (this.filterStatusLabel != null) {
        if (this.selectedFilters.length == 0) {
            this.filterStatusLabel.innerHTML = this.zeroSelectedMsg;
        } else if (this.selectedFilters.length == 1) {
            this.filterStatusLabel.innerHTML = this.oneSelectedMsg;
        } else {
            this.filterStatusLabel.innerHTML = this.selectedFilters.length + " " + this.manySelectedMsg;
        }
    }
}

// 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(groupRegExp)
{
    var newSelectedFilters = new Array();
    for (var i = 0; i < this.selectedFilters.length; i++) {
        var selectedFilter = this.selectedFilters[i];
        if (!groupRegExp || groupRegExp.test(selectedFilter.id)) {
            this.unselect(selectedFilter, false);
        } else {
            newSelectedFilters.push(selectedFilter);
        }
    }
    this.selectedFilters = newSelectedFilters;

    this.updateStatus();
 
    return false;
}

MFilterContext.prototype.sendFilters = function(element, 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';
    }

    store_emitOmnitureMicroPacket(23, brandId);
    this.notify('brandClicked', brandId);

    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', true);
}
