// TripDisplayList encapsulates the display logic for listing flight search results. Trips are initially added to the display using setTrips. The show and hide methods can then be used to control the set of results that is visible to the user.
//
// This class is getting kind of big. Might be a good idea to split it up.
var TripDisplayList = Class.create({
  // Specifies the trips to be included in the display. The trips will initially be hidden. Use show to make them visible.
  setTrips: function(newTrips) {
    this._tagTripsWithHashKeys(newTrips);
    
    this.clear();
		this._detachParent();
    this._trips = this._sortTrips(newTrips.clone());
    this._trips.each(this._createTripDisplay.bind(this));
		this._attachParent();
  },
  // Gets rid of all of the trips in the display list.
  clear: function() {
    this._replaceHighlightedDisplay(null);
    this._trips = new Array();
    this._displays = new Hash();
    this._clearTable();
  },
  // Makes a trip visible to the user. The trip must have been added previously using setTrips.
  show: function(trip) {
    var display = this._tryDisplayLookup(trip);
    display.show();
  },
  // Makes a trip invisible to the user. The trip must have been added previously using setTrips.
  hide: function(trip) {
    var display = this._tryDisplayLookup(trip);
    display.hide();
  },
  // Changes the comparator function used to sort the list. Takes the same kind of comparator used by the standard Javascript sort function. If the argument is null, no sorting is performed. If reverse is true, the sort order will be reversed.
  setSortComparator: function(cmp, reverse) {
    this._sortComparator = cmp;
    this._reverseSort = !!reverse;
    this._sortAndRearrangeIfNeeded();
  },
  // Highlights the top visible trip display (if there is one).
  highlightFirst: function() {
    this._trips.any(function(trip) {      
      var display = this._tryDisplayLookup(trip);
      if(display.visible()) {
        this._setDisplayHighlight(display, true);
        return true;
      } else {
        return false;
      }
    }.bind(this));
  },
  // Creates a TripDisplayList whose trips are shown as children of the specified element.
  initialize: function(grandparent) {
    this._grandparent = grandparent;
    this._parent = new Element('div');
    grandparent.insert(this._parent);
    
    this._displays = new Hash();
    this._trips = new Array();
  },
  
  _sortComparator: null,
  _reverseSort: false,
  
  _parent: null,
  _grandparent: null,
  
  _displays: null, // Map of trips to TripDisplays
  _trips: null, // List of trips whose order mirrors that of the displayed items
  
  _highlightedDisplay: null,
  
  _setDisplayHighlight: function(display, turnOn) {
    if( display != this._highlightedDisplay) {
      if(turnOn)
        this._replaceHighlightedDisplay(display);
      display.setHighlight(turnOn);
    }
  },
  _replaceHighlightedDisplay: function(displayOrNull) {
    if(this._highlightedDisplay) this._highlightedDisplay.setHighlight(false);

    this._highlightedDisplay = displayOrNull;
  },
  _clearTable: function() {
    this._detachParent();
    this._parent = new Element('div');
    this._attachParent();
  },
  _tagTripsWithHashKeys: function(trips) {
    // Give the trips unique IDs for hash keys. Ideally, we wouldn't have to change trip objects at all, but this seems like an acceptable option for now.
    var id = 0;
    trips.each(function(trip) { trip.__displayID = id++; });
  },
  _tripsInOrder: function(trips) {
    var cmp = this._sortComparator;
    if(cmp) {
      return inOrder(cmp, this._reverseSort, trips);
    } else {
      return true;
    }
  },
  _sortTrips: function(trips) {
    var cmp = this._sortComparator;
    if(cmp) {
      var sorted = trips.sort(cmp);
      return this._reverseSort ? sorted.reverse() : sorted;
    } else {
      return trips;
    }
  },
  _createTripDisplay: function(trip) {
    var display = new TripDisplay(this._parent, trip);
    display.mouseoverChange = this._setDisplayHighlight.bind(this, display);
    this._displays.set(trip.__displayID, display);
  },
  _tryDisplayLookup: function(trip) {
    var display = this._displays.get(trip.__displayID);
    if(display) {
      return display;
    } else {
      throw new Error("Trip display lookup failed. The trip may not have been entered properly using setTrips.");
    }
  },
  _sortAndRearrangeIfNeeded: function() {
    if(this._tripsInOrder(this._trips)) return;
    
    this._trips = logDuration(
      this._sortTrips.bind(this, this._trips),
      'sorting');
    this._reinsertTripDisplays();
  },
  _reinsertTripDisplays: function() {
    this._clearTable();
		this._detachParent();
    logDuration(function() {
      this._trips.each(function(trip) {
        var display = this._tryDisplayLookup(trip);
        display.appendTo(this._parent);
      }.bind(this));
    }.bind(this), 'reinserting trip displays');
		this._attachParent();
  },
  _detachParent: function() {
    if(this._parent.parentNode == this._grandparent) {
      this._parent.remove();
    }
  },
  _attachParent: function() {
    if(this._parent.parentNode != this._grandparent) {
      this._grandparent.appendChild(this._parent);
    }
  }
});

// Like TripDisplayList, but only shows N results at a time by default. If other results are present, a button will be shown at the bottom of the list to display more of them.
var PartialTripDisplayList = Class.create({
  // Same as in TripDisplayList.
  setTrips: function(newTrips) {
    this._clearSecondaryTripLists();
    this._tdl.setTrips(newTrips);
  },
  // Same as in TripDisplayList.
  setSortComparator: function(cmp, reverse) {
    this._tdl.setSortComparator(cmp, reverse);
    this.showAndHide(this._potentiallyVisibleTrips);
  },
  // Same as in TripDisplayList.
  highlightFirst: function() {
    this._tdl.highlightFirst();
  },
  // Same as in TripDisplayList.
  initialize: function(resultsParent, moreParent) {
    this._tdl = new TripDisplayList(resultsParent);
    this._moreResultsPanel = new MoreResultsPanel(moreParent, this._showMoreIncrement);
    this._clearSecondaryTripLists();
  },
  // Shows up to n trips from potentialTripsToShow and hides the rest.
  showAndHide: function(potentialTripsToShow) {
    this._showAndHideN(potentialTripsToShow, this._defaultMaxEntriesShown);
  },
  
  _tdl: null, // TripDisplayList
  _defaultMaxEntriesShown: (navigator.userAgent.include('MSIE')) ? 15 : 50,
  _showMoreIncrement: (navigator.userAgent.include('MSIE')) ? 15 : 50,
  _moreResultsPanel: null,
  
  // _visibleTrips are the trips that are actually visible in the display. _potentiallyVisibleTrips are the trips that are either visible or would be visible if the user clicked 'show more results' enough.
  _visibleTrips: null,
  _potentiallyVisibleTrips: null,
  
  _clearSecondaryTripLists: function() {
    this._visibleTrips = [];
    this._potentiallyVisibleTrips = [];
  },
  // Show the top n trips in potentialTripsToShow (n = maxShown).
  _showAndHideN: function(potentialTripsToShow, maxShown) {
    logDuration(function() {
      // WARNING: Breaking encapsulation (and code clarity) for performance.
    
      // Make a hash keyed with IDs for all of the trips we might want to show.
      var trips = this._tdl._trips;
      var idsToShow = {};
      potentialTripsToShow.each(function(trip) {
        idsToShow[trip.__displayID] = true;
      });
    
      var show = this._tdl.show.bind(this._tdl);
      var hide = this._tdl.hide.bind(this._tdl);
      
      // Keeps track of the trips that we show
      var newVisibleTrips = [];
      
      // Subset of idsToShow containing only the first n IDs.
      var limitedIdsToShow = {};

      // Show up to maxShown results whose IDs are in idsToShow, filling newVisibleTrips and limitedIdsToShow in the process.
      for(var i = 0;
          i < trips.length && newVisibleTrips.length < maxShown;
          ++i)
      {
        var trip = trips[i];
        var id = trip.__displayID;
        if(idsToShow[id]) {
          show(trip);
          newVisibleTrips.push(trip);
          limitedIdsToShow[id] = true;
        }
      }
      
      // Hide anything else that was previously visible and shouldn't be now.
      this._visibleTrips.each(function(trip) {
        if(!limitedIdsToShow[trip.__displayID]) {
          hide(trip);
        }
      });
      
      this._visibleTrips = newVisibleTrips;
      this._potentiallyVisibleTrips = potentialTripsToShow;
    
      // Show the 'more results' panel if needed
      if(newVisibleTrips.length == maxShown) {
        var extraResultCount =
          potentialTripsToShow.size() - newVisibleTrips.length;
        var showMoreFn = this._showAndHideN.bind(
          this, potentialTripsToShow, maxShown + this._showMoreIncrement);
        this._moreResultsPanel.show(extraResultCount, showMoreFn);
      } else {
        this._moreResultsPanel.hide();
      }			
    }.bind(this), "showing & hiding results");
  }
});

// Responsible for showing this:
//   "...and 28734978 additional results. (Show me more)"
var MoreResultsPanel = Class.create({
  // Assembles a MoreResultsPanel. It will be hidden until show() is called.
  initialize: function(parent, showMoreIncrement) {
    this._parent = parent;
    this._parent.hide();
    this._showMoreIncrement = showMoreIncrement;
    var innerDiv = new Element('div', { className: "more_results_message" });
    this._parent.appendChild(innerDiv);
    
    innerDiv.insert("...and ");
    this._countElt = new Element('span', {
      id: 'additional_result_count',
      'class': 'more_results_link' }).update('???');
    innerDiv.insert(this._countElt);
    innerDiv.insert("&nbsp;additional results. "); // nbsp ensures IE keeps the space
    this._moreElt = new Element('a', {
      id: "show_more_results"
    }).update(this.getMoreLinkText( this._showMoreIncrement));
    innerDiv.insert(this._moreElt);
    
    this._parent.hide();
  },
  // Shows the panel with the specified result count. When the panel is clicked, showFn will be executed.
  show: function(additionalResultCount, showFn) {
    this._countElt.update(additionalResultCount.toString());
    
    if( additionalResultCount < this._showMoreIncrement )
      this._moreElt.update(this.getMoreLinkText(additionalResultCount));
    else
      this._moreElt.update(this.getMoreLinkText(this._showMoreIncrement));
    
    this._moreElt.onclick = showFn;
    this._parent.show();
  },
  getMoreLinkText: function(numToShow)
  {
    return '(Show me ' + numToShow + ' more)';
  },
  // Removes the panel from view.
  hide: function() {
    this._parent.hide();
  },
  
  _parent: null,
  _countElt: null,
  _moreElt: null
});
