// This code depends on prototype.js and common.js.

// Class members prefixed with _ should not be used outside of their respective classes.

// Manages a group of Filters.
var FilterGroup = Class.create(hasCallback('outputChange'), {  
  // Creates a new FilterGroup containing the Filters in filters.
  initialize: function(filters) {
    filters = filters || [];
    
    // The results of applying filters to input items are stored in cachedMask entries for each of the filters. (Each cachedMask is simply an array of boolean values indicating whether an item passes the corresponding filter or not.) This keeps us from having to run each of the filters on each of the input items whenever a filter changes.
    //
    // Here, we initialize the caches to null. They will be refreshed as needed.
    this._filterCachePairs = filters.map(function(ftr) {
      return { filter: ftr, cachedMask: null };
    });
    
    this._initChangeHandlers();
    this.setInputItems([]);
  },
  
  // Changes the set of items to be filtered.
  setInputItems: function(l, okToFillCacheBlindly) {
    this._inputItems = l;
    this._asSingleOutputChange(function() {
      this._emptyCache();
      this._initLimits();
      
      // This call to _fillCacheBlindly is an optimization that assumes that calling _initLimits leaves all of the filters completely open. This is a valid assumption as of r497. If it is invalidated, this call must be removed.
      if(okToFillCacheBlindly) this._fillCacheBlindly();
    }.bind(this));
  },
  
  // Returns an Array containing the input items that pass all of the filters in this group.
  getFilteredItems: function() {
    return this.getPartitionedItems().passed;
  },
  
  // Partitions items into two sets: one set of items that passed through all the filters, and one set of items that did not.
  // Return value is an object with 'passed' and 'failed' arrays as properties.
  getPartitionedItems: function() {
    return partitionByBoolArray(this._inputItems, this._getCombinedMask());
  },
  
  // This method is just a shortcut. It sets filters' cached masks to let everything though. This kind of behavior is only valid when the filters are completely open to begin with. When they are, this approach avoids the computational cost of using filters' passesFilter methods to compose masks. 
  _fillCacheBlindly: function() {
    var mask = replicate(this._inputItems.size(), true);
    this._filterCachePairs.each(function(pair) {
      pair.cachedMask = mask;
    });
  },
  
  _getCombinedMask: function() {
    this._refreshCache();
    return arrayAnd(this._filterCachePairs.pluck('cachedMask'));
  },
  
  _emptyCache: function() {
    this._filterCachePairs.each(function(pair) {
      pair.cachedMask = null;
    });
  },
  
  _refreshCache: function() {
    groupConsoleOutput(function() {
      this._filterCachePairs.each(function(pair) {
        if(!pair.cachedMask) {
          var filter = pair.filter;
          var predicate = filter.passesFilter.bind(filter);
          logDuration(function() {
            pair.cachedMask = this._inputItems.map(predicate);
          }.bind(this), pair.filter.description);
        }
      }.bind(this));
    }.bind(this), "refreshing filters");
  },
  
  // Sets the limits for each contained filter based on the items in _inputItems.
  _initLimits: function() {
			for(var iLoop=0, iMax=this._filterCachePairs.length; iLoop < iMax; ++iLoop)
			{
				this._filterCachePairs[iLoop].filter.initLimits(this._inputItems);
			}
  },

  _asSingleOutputChange: function(fn) {
    this._suspendOutputChange = true;
    fn();
    this._suspendOutputChange = false;
    this._fireOutputChange();
  },
  
  // true iff the item passes successfully through each contained filter.
  _passesFilters: function(item) {
    return this._filterCachePairs.all(function(f) {
      return f.filter.passesFilter(item);
    });
  },
  
  _filterChange: function(pair) {
    pair.cachedMask = null;
    this._fireOutputChange();
  },
  
  _initChangeHandlers: function() {
    this._filterCachePairs.each(function(pair) {
      pair.filter.outputChange = this._filterChange.bind(this).curry(pair);
    }.bind(this));
  }
});

var UndoCapableFilterGroup = Class.create(FilterGroup, {
  _addNewUndoItems: false,
  _addNewRedoItems: false,
  _redoing: false,
  _undoStack: null,
  _redoStack: null,
  
  initialize: function($super, filters) {
    this._undoStack = new UndoStack();
    this._redoStack = new UndoStack();
    $super(filters);
  },
  setInputItems: function($super, l, okToFillCacheBlindly) {
    this._undoStack.clear();
    this._redoStack.clear();
    
    this._ignoringNewUndoItems(
      $super.bind(this).curry(l, okToFillCacheBlindly));
    this._addNewUndoItems = true; // redundant, but included for code clarity.
  },
  canUndo: function() {
    return this._undoStack.canUndo();
  },
  canRedo: function() {
    return this._redoStack.canUndo();
  },
  undo: function() {
    this._undoStack.undo();
  },
  redo: function() {
    this._redoStack.undo();
  },
  _ignoringNewUndoItems: function(fn) {
    this._addNewUndoItems = false;
    var result = fn();
    this._addNewUndoItems = true;
    return result;
  },
  _storingRedoItemsInsteadOfUndos: function(fn) {
    this._addNewRedoItems = true;
    var result = this._ignoringNewUndoItems(fn);
    this._addNewRedoItems = false;
    return result;
  },
  _execAsRedo: function(fn) {
    this._redoing = true;
    var result = fn();
    this._redoing = false;
    return result;
  },
  _filterChange: function($super, opaqueTag, reversionFn) 
  {
    // Note: YUI sliders take a while to load. When they're done they fire the slideEnd event, which may trigger _filterChange. That means that extra undo items may be created. However, this only happens when the page first loads, so these items should be wiped out when a set of search results is loaded.
    if(this._addNewRedoItems) 
    {
      var wrappedReversionFn =
        this._execAsRedo.bind(this).curry(reversionFn);
      this._redoStack.addUndoItem(wrappedReversionFn);
    }
    if(this._addNewUndoItems) 
    {
      var wrappedReversionFn =
        this._storingRedoItemsInsteadOfUndos.bind(this).curry(reversionFn);
      this._undoStack.addUndoItem(wrappedReversionFn);
      if(!this._redoing) 
      {
        this._redoStack.clear();
      }
    }
    
    // TODO: move GA code out of this class (into an event handler, for example)
    // Track the event in GA
    trackEvent("searchResults", "Filter", opaqueTag.filter.description);

    $super(opaqueTag);
  }
});

// Base class for mutable filtering objects.
//
// Subclasses must implement initLimits and passesFilter.  Additionally, they must call _filterChange when the behavior of passesFilter changes.
var Filter = Class.create(hasCallback('outputChange'), {
  description: "unknown filter",
  
  // Sets the appropriate limits for this filter assuming that it will be used on the specified collection of items.  Must be able to handle empty arrays.
  initLimits: pureVirtual,
  
  // A simple predicate that returns true iff the item passes successfully through this filter.  If this method's behavior changes, _filterChange must be invoked.
  passesFilter: pureVirtual,
  
  // Call this when the behavior of the filter changes.
  _filterChange: function() {
    this._fireOutputChange();
  }
});

var RevertableFilter = Class.create(Filter, {
  initLimits: function(items) {
    this._innerInitLimits(items);
    this._updateLastReversionFn();
  },
  _filterChange: function() {
    // This includes the reversion function as an argument when invoking the callback.
    this._fireOutputChange(this._lastReversionFn);
    this._updateLastReversionFn();
  },
  initialize: function() {
    this._updateLastReversionFn();
  },
  _updateLastReversionFn: function() {
    this._lastReversionFn = this._getReversionFn();
  },
  
  _innerInitLimits: pureVirtual,
  _getReversionFn: pureVirtual // Returns a function that reverts the filter to the state it had when _getReversionFn was called.
});
