// (Depends on common.js, filter.js, and sliders.js)
//
// Filtering UI components for flight search
//
// TODO: refactor to separate filters from their views

var SliderFilterBase = Class.create(
  RevertableFilter,
  hasCallback('ready'),
{
  _slider: null,
  _sliderLength: null, // set to null to use the default length
  _sliderStepSize: 1,
  _sliderStepOffset: 0,
  _extraBackgroundClass: null,
  _label: null,
//  _labelSeparator: " &#8211; ",     // en-dash
  _labelSeparator: " ",     // nothing
  
  _cachedUpperLimit: null,
  _cachedLowerLimit: null,
  
  initialize: function($super, sliderType, parent) {
    this._label = newElt('div', null, 'filter_label_slider', parent);
    this._slider = new sliderType(parent, this._sliderLength);
    this._slider.setStep(this._sliderStepSize, this._sliderStepOffset);
    
    if(this._extraBackgroundClass) {
      this._slider.addBackgroundClass(this._extraBackgroundClass);
    }
    
    this._slider.slideEnd = function() {
      this._cacheLimits();
      this._filterChange();
    }.bind(this);
    
    this._slider.change = function() {
      this._label.innerHTML = this._getLabelHTML();
    }.bind(this);
    
    this._slider.ready = this._fireReady.bind(this);
    
    $super();
  },
  passesFilter: function(trip) {
    var value = this._extractValue(trip);
    return value >= this._cachedLowerLimit &&
           value <= this._cachedUpperLimit;
  },
  _cacheLimits: function() {
    this._cachedLowerLimit = this._getLowerValue();
    this._cachedUpperLimit = this._getUpperValue();
  },
  _getLabelHTML: function() {
    return "<span class='filter_label_value_min'>" + 
      this._format(this._getLowerValue()) + "</span>" +
      this._labelSeparator +
      "<span class='filter_label_value_max'>" +
      this._format(this._getUpperValue()) + "</span>";
  },
  _findLimits: function(trips) {    
		var testVal = trips[0] ? this._extractValue(trips[0]) : 0;
		var myMin = testVal, myMax = testVal;
		for(var iLoop=0, iLen=trips.length; iLoop < iLen; ++iLoop )
		{
			testVal = this._extractValue(trips[iLoop]);
			if( testVal < myMin ) myMin = testVal;
			if( testVal > myMax ) myMax = testVal;
		}
    
    // Round down the min and round up the max
    var stepSize = this._sliderStepSize;
    myMin = Math.floor(myMin / stepSize) * stepSize;
    myMax = Math.ceil(myMax / stepSize) * stepSize;
    
    return { min: myMin,
	           max: myMax };
  },
  _innerInitLimits: function(trips) {
    var limits = this._findLimits(trips);
    this._setLimits(limits.min, limits.max);
    this._cacheLimits();
  },
  _getLowerValue: pureVirtual,
  _getUpperValue: pureVirtual,
  _format: pureVirtual,
  _setLimits: pureVirtual,
  _extractValue: pureVirtual
});

var SingleSliderFilterBase = Class.create(SliderFilterBase, {
  _getLowerValue: function() { return this._slider.getMin(); },
  _getUpperValue: function() { return this._slider.getValue(); },
  initialize: function($super, parent) {
    $super(SingleSlider, parent);
  },
  _setLimits: function(min, max) {
    this._slider.setLimits(min, max);
    this._slider.setValue(max);
  },
  _getReversionFn: function() {
    var val = this._slider.getValue();
    return function() {
      this._slider.setValue(val);
    }.bind(this);
  }
});

var DoubleSliderFilterBase = Class.create(SliderFilterBase, {  
  _getLowerValue: function() { return this._slider.getLowerValue(); },
  _getUpperValue: function() { return this._slider.getUpperValue(); },
  initialize: function($super, parent) {
    $super(DoubleSlider, parent);
  },
  _setLimits: function(min, max) {
    this._slider.setLimits(min, max);
    this._slider.setLowerValue(min);
    this._slider.setUpperValue(max);
  },
  _getReversionFn: function() {
    var lower = this._slider.getLowerValue();
    var upper = this._slider.getUpperValue();
    return function() {
      this._slider.setValues(lower, upper);
    }.bind(this);
  }
});

var TimeFilterBase = Class.create(DoubleSliderFilterBase, {
  _sliderStepSize: 3600000, // 1 hour in milliseconds
  _format: function(milliDate) {
    var date = new Date(milliDate);
    return formatTimeShort(minutesSinceMidnight(date)) + " " +
      formatAbbrevDOWFromDate(date);
  },
  // Should return time as milliseconds since 1/1/1970
  _extractValue: pureVirtual
});

var PriceFilter = Class.create(SingleSliderFilterBase, {
  description: "price filter",
  _format: formatPrice,
  _sliderLength: 300,
  _extraBackgroundClass: "slider-bg-wide",
  initialize: function($super, parent){
    $super(parent);
    this._label.className="filter_label_wide"
  },
  _extractValue: function(trip) {
    return trip.price.ceil();
  }
});

var DurationFilter = Class.create(SingleSliderFilterBase, {
  description: "maximum flight duration filter",
  _format: formatDuration,
  _extractValue: function(trip) {
    return Math.max(trip.outbound.duration, trip.inbound.duration);
  }
});

var SliderMaxStopFilter = Class.create(SingleSliderFilterBase, {
  description: "sliding maximum stop count filter",
  _format: function(n) {
    return n.toFixed(0);
  },
  _extractValue: function(trip) {
    return Math.max(trip.outbound.totalStops, trip.inbound.totalStops);
  }
});

var DepartureTakeoffTimeFilter = Class.create(TimeFilterBase, {
  description: "departure takeoff time filter",
  _extractValue: function(trip) {
    return trip.outbound.departure.time.getTime();
  }
});

var ReturnTakeoffTimeFilter = Class.create(TimeFilterBase, {
  description: "return takeoff time filter",
  _extractValue: function(trip) {
    return trip.inbound.departure.time.getTime();
  }
});

var DepartureLandingTimeFilter = Class.create(TimeFilterBase, {
  description: "departure landing time filter",
  _extractValue: function(trip) {
    return trip.outbound.arrival.time.getTime();
  }
});

var ReturnLandingTimeFilter = Class.create(TimeFilterBase, {
  description: "return landing time filter",
  _extractValue: function(trip) {
    return trip.inbound.arrival.time.getTime();
  }
});

var CheckedPricedList = Class.create({
  _div: null,
  
  initialize: function(parent, className) {
    this._div = newElt('div', null, 'checked_priced_list', parent);
    if(className) this._div.addClassName(className);
  },
  addEntry: function(entry) {
    this._div.insert(entry.getDOMElement());
  },
  clear: function() {
    this._div.update('');
  }
});

var CheckedPricedListEntry = Class.create(hasCallback('change'), {
  _box: null,
  _label: null,
  _price: null,
  _row: null,
  
  initialize: function() {
    this._row = newElt('div', null, 'entry');
    this._box = new Element('input', { type: 'checkbox' });
    this._box.observe('click', this._fireChange.bind(this));
    this._row.insert(this._box);
    this._label = newElt('div', null, 'filter_checkbox_text label', this._row);
    this._price = newElt('div', null, 'filter_label price', this._row);
  },
  setLabelContent: function(content, callbackFn, tooltip) {
    if( Object.isUndefined( callbackFn ))
      this._label.update(content);
    else
    {
      this._label.observe('click', callbackFn);
      this._label.appendChild(new Element('a', {title: tooltip}).update(content));
    }
  },
  setPriceContent: function(content) {
    this._price.update("<NOBR>" + content + "</NOBR>");
  },
  getDOMElement: function() {
    return this._row;
  },
  checked: function() {
    return this._box.checked;
  },
  setChecked: function(bool) {
    this._box.checked = bool;
    this._box.writeAttribute('defaultChecked', bool)
  },
  setEnabled: function(bool) {
    this._box.disabled = !bool;
  },
  enabled: function() {
    return !this._box.disabled;
  },
  addClassName: function(name) {
    this._row.addClassName(name);
  }
});

var StopOption = Class.create(hasCallback('change'), {
  _entry: null,
  _maxStops: null,
  _minStops: null,
  
  initialize: function(text, minStops, maxStops) {
    this._entry = new CheckedPricedListEntry();
    this._entry.setLabelContent(text);
    this._minStops = minStops;
    this._maxStops = maxStops || minStops;
    this._entry.change = this._fireChange.bind(this);
  },
  getDOMElement: function() {
    return this._entry.getDOMElement();
  },
  initContent: function(trips) {
    var atMostNStopTrips =
      trips.filter(this._tripHasAFlightInStopRangeAndNoneAboveIt.bind(this));
    var anyTripsExist = atMostNStopTrips.length > 0;
    var bestPrice = atMostNStopTrips.pluck('price').min();
    var priceOrNone = anyTripsExist ? formatFromPrice(bestPrice) : 'none';
    this._entry.setPriceContent(priceOrNone);
    this._entry.setEnabled(anyTripsExist);
    this._entry.setChecked(anyTripsExist);
  },
  tripOK: function(trip) {
    return this._entry.checked() &&
      this._tripHasAFlightInStopRangeAndNoneAboveIt(trip);
  },
  _stopsInRange: function(stops) {
    return stops >= this._minStops && stops <= this._maxStops;
  },
  _tripHasAFlightInStopRangeAndNoneAboveIt: function(trip) {
    var outStops = trip.outbound.totalStops;
    var inStops = trip.inbound.totalStops;
    
    var maxStops = this._maxStops;
    var minStops = this._minStops;
    
    return outStops <= maxStops &&
           inStops  <= maxStops &&
           (outStops >= minStops || inStops >= minStops);
  },
  checked: function() {
    return this._entry.checked();
  },
  setChecked: function(bool) {
    this._entry.setChecked(bool);
    this._entry.writeAttribute('defaultChecked', bool)
  },
  addClassName: function(name) {
    this._entry.addClassName(name);
  }
});

var CheckboxStopFilter = Class.create(RevertableFilter, {
  description: "checkbox stop count filter",
  _stopOps: null,
  
  initialize: function($super, parent) {
    this._stopOps = [
      new StopOption('0 stops' , 0),
      new StopOption('1 stop'  , 1),
      new StopOption('2+ stops', 2, Number.POSITIVE_INFINITY) ];
    
    var callback = this._filterChange.bind(this);
    this._stopOps.each(function(o) {
      o.change = callback;
    });
    
    var list = new CheckedPricedList(parent);
    this._stopOps.each(list.addEntry.bind(list));
    
    $super();
  },
  _innerInitLimits: function(trips) {
    this._stopOps.invoke('initContent', trips);
  },
  passesFilter: function(trip) {
    // Implemented in this style for speed.
    so = this._stopOps;
    return so[0].tripOK(trip) || so[1].tripOK(trip) || so[2].tripOK(trip);
  },
  _getReversionFn: function() {
    return checkboxFilterReversionFn(this, this._stopOps);
  }
});

function newLinkButton(text, callback) {
  var elt = new Element('a');
  elt.insert(text);
  elt.addClassName('minor_link alliance_link');
  elt.observe('click', callback);
  return elt;
}

// TODO: superclass for checkbox filters?
function checkboxFilterReversionFn(filter, checkboxes) {
  var restoreChecks = restoreChecksFn(checkboxes);
  return function() {
    restoreChecks();
    filter._filterChange();
  };
}

function restoreChecksFn(checkObjs) {
  var checklist = checkObjs.invoke('checked');
  return function() {
    checklist.zip(checkObjs, function(pair) {
      var checkObj = pair[1];
      var isChecked = pair[0];
      checkObj.setChecked(isChecked);
    });
  };
}

var CarrierFilter = Class.create(RevertableFilter, {
  description: "carrier filter",
  _entries: null,
  _allianceBar: null,
  _cplLeft: null,
  _cplRight: null,
  
  initialize: function($super, parent) {
    this._allianceBar = newElt('div', null, 'alliance_bar filter_container', parent);
    var cxrParent = newElt('div', null, null, parent);
    this._entries = new Hash();
    this._cplLeft = new CheckedPricedList(cxrParent, 'filter_container left_filter');
    this._cplRight = new CheckedPricedList(cxrParent, 'filter_container right_filter');
    
    $super();
  },
  _innerInitLimits: function(trips) {
    var groupedTrips = groupBy(function(trip) {
      return trip.carrier;
    }, trips);
    
    // (un)select all callback
    var setAll = function(bool, exceptElm) {
      this._entries.values().invoke('setChecked', bool);
      if( exceptElm )
        exceptElm.setChecked(!bool);
      this._filterChange();
    }.bind(this);
    
    // Hash of CheckedPricedListEntry objects, one per cxr
    this._entries = new Hash();
    groupedTrips.each(function(group) {
      var cxrTrips = group.value;
      var cxr = group.key;
      var entry = new CheckedPricedListEntry();
      var minPrice = cxrTrips.pluck('price').min();
      entry.setLabelContent((cxr == 'MULT') ? 'Multiple' : friendlyName(cxr), 
                            setAll.curry(false, entry), 
                            "Click to show only " + friendlyCarrierLongName(cxr) + " flights.");
      entry.setPriceContent(formatFromPrice(minPrice));
      entry.change = this._filterChange.bind(this);
      entry.setChecked(true);
      this._entries.set(cxr, entry);
    }.bind(this));
    
    var sortedTLAs =
      groupedTrips.keys().sortBy(function(tla) {
        return friendlyName(tla).toUpperCase();
      });
    
    // Special case: we want "Multiple" in front
    if(sortedTLAs.include('MULT')) {
      sortedTLAs = sortedTLAs.without('MULT');
      sortedTLAs.unshift('MULT');
    };
    
    var sortedEntries =
      sortedTLAs.map(function(tla) {
        return this._entries.get(tla);
      }.bind(this));
    
    
    // Put carriers into the CheckedPricedList
    this._cplRight.clear();
    this._cplLeft.clear();
    var half = (sortedEntries.size() / 2).ceil();
    sortedEntries.slice(0, half).
      each(this._cplLeft.addEntry.bind(this._cplLeft));
    sortedEntries.slice(half).
      each(this._cplRight.addEntry.bind(this._cplRight));
    
    // Set up alliance bar
    this._allianceBar.innerHTML = '';
    this._allianceBar.insert(newLinkButton('all', setAll.curry(true, null)));
    this._allianceBar.insert(newLinkButton('none', setAll.curry(false, null)));
    ALLIANCES.each(function(pair) {
      var allianceName = pair.key;
      var members = pair.value;
      this._allianceBar.insert(newLinkButton(allianceName, function() {
        this._entries.each(function(pair) {
          var cxr = pair.key;
          var entry = pair.value;
          entry.setChecked(members.include(cxr));
        });
        this._filterChange();
      }.bind(this)));
    }.bind(this));
  },
  passesFilter: function(trip) {
    // Start with whether the trip-level carrier is checked or not
    var passed = this._entries.get(trip.carrier).checked();

    // CURRENTLY REMOVED... this might get added back though
    // Also make sure that in the case of MULTiple carriers, the offending carrier is not on a leg
    /*
    if (trip.carrier.toUpperCase() == "MULT" && passed == true ) {
      [trip.outbound, trip.inbound].each(function(flight) {
        flight.legs.each(function(leg) {
          if (this._entries.get(leg.marketingCarrier) != null && this._entries.get(leg.marketingCarrier).checked() == false) {
            passed = false;
          }        
        }.bind(this));      
      }.bind(this));
    }
    */
    return passed;
  },
  _getReversionFn: function() {
    return checkboxFilterReversionFn(this, this._entries.values());
  }
});
