var ses = ses ? ses : {};

/**
 * Events are:
 *  load - when initial results are loaded
 *  loadsort - when full results are loaded
 *  sort - when results are sorted
 *  resize - when the model "window" size has changed
 */
 
// CONSTRUCTOR

ses.SearchResultsModel = function()
{
  this._baseurl = null;   // base url for results page
  this._query = null;     // query to execute
  this._encQuery = null; // URL-encoded query
  this._nodeid = null;    // nodeid for query
  this._fedid = null;     // fid for query
  this._isSW = false;     // is this query 'Search within'
  this._dg = new Array(); // data groups to filter
  this._obj = null;       // query response object
  this._timeStart = 0;    // start time when search request is sent
  this._timeTaken = 0;    // time taken for last search request
  this._groupAttribute = ses.SearchResultsModel.NO_GROUP_ATTRIBUTE;
  this._sortAttribute = ses.SearchResultsModel.DEFAULT_SORT_ATTRIBUTE;
  this._sortOrder = ses.SearchResultsModel.SORT_ASCENDING;
  this._event = new YAHOO.util.CustomEvent( "SearchResultsModelChanged", this );
  this._docFilter = new ses.AllPassFilter( this );
  this._initialStart = 1;
  this._initialEnd = 0;
  this._timezoneOffset = 0;
  this._qid = 0;
  this._defaultTopN = ses.SearchResultsModel.MIN_SORT_HITS_REQUESTED;
  this._maxTopN = ses.SearchResultsModel.MAX_TOPN_HITS_REQUESTED;

  /*
   * The various "views" on the result set:
   *
   *  - the client cache (all hits stored in browser client)
   *  - the active result set (the cache after filtering for result clustering)
   *  - the view window (when paging through results)
   */
  this._cacheSize = 0;

  this._windowStart = 1;
  this._windowEnd = 0;
  
  this._window = 0;

  this._pageSize = 10;

  // register this object for key-based lookup
  this._key = ses.SearchResultsModel._globalKey++;
  ses.SearchResultsModel._list[ this._key ] = this;
};

// INSTANCE METHODS

/**
 * Get the key uniquely identifying this SearchResultsModel object.
 *
 * @return key representing this object
 */
ses.SearchResultsModel.prototype.getKey = function()
{
  return this._key;
};

/**
 * Sets the base URL from which this model will be loaded.
 *
 * @param baseurl URL defining model service endpoint
 */
ses.SearchResultsModel.prototype.setBaseUrl = function( baseurl )
{
  this._baseurl = baseurl;
};

/**
 * Returns the query behind this model.
 *
 * @return the user search query string
 */
ses.SearchResultsModel.prototype.getQuery = function()
{
  return this._query;
};

/**
 * Sets the query behind this model.
 *
 * @param query query for this model
 */
ses.SearchResultsModel.prototype.setQuery = function( query )
{
  this._query = query;
};

/**
 * Set the URL-encoded query behind this model.
 *
 * @param encQuery url-encoded query for this model
 */
ses.SearchResultsModel.prototype.setEncQuery = function ( encQuery )
{
  this._encQuery = encQuery;
}

/**
 * Returns the timezone offset.
 *
 * @return the timezone offset.
 */
ses.SearchResultsModel.prototype.getTimezoneOffset = function()
{
  return this._timezoneOffset;
};

/**
 * Sets the timezone offset.
 *
 * @param timezoneOffset timezone offset.
 */
ses.SearchResultsModel.prototype.setTimezoneOffset = function( timezoneOffset )
{
  this._timezoneOffset = timezoneOffset;
};

/**
 * Returns the qid.
 *
 * @return the qid.
 */
ses.SearchResultsModel.prototype.getQueryId = function()
{
  return this._qid;
};

/**
 * Sets the qid.
 *
 * @param queryId the query ID
 */
ses.SearchResultsModel.prototype.setQueryId = function( queryId )
{
  this._qid = queryId;
};

/**
 * Returns the default number of hits to request for top-N results.
 *
 * @return the default top-N hits
 */
ses.SearchResultsModel.prototype.getDefaultTopN = function()
{
  return this._defaultTopN;
};

/**
 * Sets the default number of hits to request for top-N results.
 *
 * @param numHits the number of hits to request by default
 */
ses.SearchResultsModel.prototype.setDefaultTopN = function( numHits )
{
  this._defaultTopN = numHits;
};

/**
 * Returns the maximum number of hits to request for top-N results.
 *
 * @return the maximum top-N hits
 */
ses.SearchResultsModel.prototype.getMaxTopN = function()
{
  return this._maxTopN;
};

/**
 * Sets the maximum number of hits to request for top-N results.
 *
 * @param numHits the maximum number of hits to request
 */
ses.SearchResultsModel.prototype.setMaxTopN = function( numHits )
{
  this._maxTopN = numHits;
};


/**
 * Set the nodeid behind this model.
 *
 * @param nodeid node ID for this model
 */
ses.SearchResultsModel.prototype.setNodeId = function( nodeid )
{
  this._nodeid = nodeid;
};

/**
 * Set the fid behind this model.
 *
 * @param fedid fed ID for this model
 */
ses.SearchResultsModel.prototype.setFedId = function( fedid )
{
  this._fedid = fedid;
};

/**
 * Set whether the query behind this model is 'Search within'.
 *
 * @param isSW is the query 'Search within'
 */
ses.SearchResultsModel.prototype.setSearchWithin = function( isSW )
{
  this._isSW = isSW;
};

/**
 * Clear all data groups
 */
ses.SearchResultsModel.prototype.clearGroups = function()
{
  this._dg.splice( 0, this._dg.length );
};

/**
 * Add a data group
 */
ses.SearchResultsModel.prototype.addGroup = function( dg )
{
  //if( dg > Number.MIN_VALUE )
  this._dg.push( dg );
};

/**
 * Get alternate keywords for the query
 *
 * @return alternate keywords
 */
ses.SearchResultsModel.prototype.getAltKeywords = function()
{
  return this._obj.altKeywords;
};


/**
 * Get cache size (total number of results stored in browser client memory).
 *
 * @return results client cache size
 */
ses.SearchResultsModel.prototype.getClientCacheSize = function()
{
  return this._cacheSize;
};

/**
 * Get size of active result set: number of results after filtering for
 * result clustering, i.e. min( estimated-hit-count, cluster-node-size )
 *
 * @return active window size
 */
ses.SearchResultsModel.prototype.getActiveSize = function()
{
  return this._docFilter.getFilteredSize();
};

/**
 * Get view window start index - first hit result in current view window
 *
 * @return view window start index
 */
ses.SearchResultsModel.prototype.getWindowStart = function()
{
  return this._windowStart;
};

/**
 * Get view window end idnex - last hit result in current view window
 *
 * @return view window end index
 */
ses.SearchResultsModel.prototype.getWindowEnd = function()
{
  return this._windowEnd;
};

/**
 * Get window size (number of results in view window).
 *
 * @return results window size
 */
ses.SearchResultsModel.prototype.getWindowSize = function()
{
  return this._window;
};

/**
 * Get page size (number of results in a full page).
 *
 * @return results page size
 */
ses.SearchResultsModel.prototype.getPageSize = function()
{
  return this._pageSize;
};

// TODO: Come up with better names for this window stuff
/**
 * Get available window size (number of additional results not in view window).
 *
 * @return number of results not in view window
 */
ses.SearchResultsModel.prototype.getAvailableWindowSize = function()
{
  return( this._obj.results.length - this._window );
};

/**
 * Set the document filter for this model
 */
ses.SearchResultsModel.prototype.setDocFilter = function(filter) {
  this._docFilter = filter;

  this.setWindow( 1, this.getPageSize() );
};

/**
 * Get the document filter for this model
 */
ses.SearchResultsModel.prototype.getDocFilter = function() {
  return this._docFilter;
};

/**
 * Set the document filter for this model as a list of doc IDs
 *
 * @param clusterNode a node object with a list of docs and the size (number
 *                    of unique IDs in the list)
 */
ses.SearchResultsModel.prototype.setIdFilter = function(clusterNode) {
  this.setDocFilter(new ses.IdListFilter(clusterNode));

  // notify all listeners about the model change
  this._event.fire( this );
};

/**
 * Set the window size (number of results in view window).
 *
 * @param window number of results in view window
 */
ses.SearchResultsModel.prototype.setWindowSize = function( window )
{
  if( window < 0 )
    window = 0;

  this._window = parseInt( window );
  this._recalcWindow();

  // notify all listeners about the model change
  this._event.fire( this );
};

/**
 * Recalc the view window based on the current active result set.
 *
 */
ses.SearchResultsModel.prototype._recalcWindow = function()
{
  var desiredEnd = this._windowStart + this._window - 1
  this._windowEnd = Math.min( desiredEnd, this.getActiveSize() );

  if( this._windowEnd < this._windowStart )
    this._windowEnd = this._windowStart - 1;

  this._window = this._windowEnd - this.windowStart + 1;
};

/**
 * Set the window start index and size.
 *
 * @param start start index
 * @param size number of results in view window
 */
ses.SearchResultsModel.prototype.setWindow = function( start, size )
{
  this._windowStart = start;
  this.setWindowSize( size );
};

/**
 * Set the window start and end indexes.
 *
 * @param start start index
 * @param end end index
 */
ses.SearchResultsModel.prototype.setWindowBounds = function( start, end )
{
  this._windowStart = parseInt( start );
  this._windowEnd = parseInt( end );
  this.setWindowSize( end-start+1 );
};

/**
 * Load model with specified callback.
 * The load is performed using the AJAX framework, with a callback function
 * invoked after the asynchronous HTTP operation completes.
 *
 * @param hitsRequested number of hits to load into model
 * @param callback callback function to invoke upon completion of AJAX request
 */
ses.SearchResultsModel.prototype._load = function( hitsRequested, callback )
{
  if( !hitsRequested )
    return;

  var encQuery = (this._encQuery) ? this._encQuery : "";

  if( !this._isSW && encQuery == "" )
    return;

  var resultsUrl = this._baseurl + "/results.jsp?q=" + encQuery + 
                  "&num=" + hitsRequested + 
                  "&search.timezone=" + this._timezoneOffset +
                  "&qid=" + this._qid ;

  if( this._isSW )
  {
    var nodeid = (this._nodeid) ? this._nodeid : "";
    var fedid  = (this._fedid) ? this._fedid : "";
    resultsUrl += "&sw=t&nodeid=" + nodeid +
                  "&fid=" + fedid;
  }

  for( var i=0; i<this._dg.length; i++ )
  {
    if( this._dg[i] != -1 )
      resultsUrl += "&search_p_groups=" + this._dg[i];
  }
    
  this._timeStart = ( new Date() ).getTime();

  var cb = 
  {
    success:callback,
    failure:ses.SearchResultsModel.failureHandler,
    scope: this
  };

  var request = YAHOO.util.Connect.asyncRequest( 'GET', resultsUrl, cb, null );
};

/**
 * Load model with default callback.  This is the external function for
 * asynchronously loading data into the model.
 *
 * @param hitsRequested number of hits to load into model
 */
ses.SearchResultsModel.prototype.load = function(startnum, endnum)
{
  // if the number of hits required to perform top-N operations
  // exceeds the max limit, do not perform the top-N request.
  if( endnum > this.getMaxTopN() )
    return;

  this._window = Math.max( this.getDefaultTopN(),
                           endnum );
  this._initialStart = startnum;
  this._initialEnd = endnum;
  this._load( this._window, ses.SearchResultsModel.sortCallback );
};

/**
 * Get attribute currently used for grouping results.
 *
 * @return current group attribute
 */
ses.SearchResultsModel.prototype.getGroupAttribute = function()
{
  return this._groupAttribute;
};

/**
 * Get attribute currently used for sorting results.
 *
 * @return current sort attribute
 */
ses.SearchResultsModel.prototype.getSortAttribute = function()
{
  return this._sortAttribute;
};

/**
 * Returns true if current sort order is ascending.
 *
 * @return true if sort is ascending
 */
ses.SearchResultsModel.prototype.isSortAscending = function()
{
  return (this._sortOrder == ses.SearchResultsModel.SORT_ASCENDING);
};

/**
 * Group elements using specified attribute.
 * After the grouping is finished, notify all event listeners.  This method
 * assumes the model has already been previously loaded.  It will fetch
 * more results only if necessary because of the grouping.
 *
 * @param attribute attribute to group on
 */
ses.SearchResultsModel.prototype.group = function( attribute )
{
  if( !attribute )
    return;
  this._groupAttribute = attribute;
  this._doSort();

  if( attribute == ses.SearchResultsModel.NO_GROUP_ATTRIBUTE )
    this.setWindow( 1, this.getPageSize() );        // reset back to page 1
  else
    this.setWindow( 1, this._obj.results.length );  // show 'Results 1 - total ...'
};

/**
 * Sort elements using specified attribute.
 * After the sort is finished, notify all event listeners.  This method
 * assumes the model has already been previously loaded.  It will fetch
 * more results only if necessary because of the sort.
 *
 * @param attribute attribute to sort on
 */
ses.SearchResultsModel.prototype.sort = function( attribute )
{
  if( !attribute )
    return;
  this._sortAttribute = attribute;
  this._doSort();
};

/**
 * Sort elements in ascending order.
 * After the sort is finished, notify all event listeners.  This method
 * assumes the model has already been previously loaded.  It will fetch
 * more results only if necessary because of the sort.
 */
ses.SearchResultsModel.prototype.sortAscending = function()
{
  this._sortOrder = ses.SearchResultsModel.SORT_ASCENDING;
  this._doSort();
};

/**
 * Sort elements in descending order.
 * After the sort is finished, notify all event listeners.  This method
 * assumes the model has already been previously loaded.  It will fetch
 * more results only if necessary because of the sort.
 */
ses.SearchResultsModel.prototype.sortDescending = function()
{
  this._sortOrder = ses.SearchResultsModel.SORT_DESCENDING;
  this._doSort();
};

/**
 * Sort elements.
 * After the sort is finished, notify all event listeners.  This method
 * assumes the model has already been previously loaded.  It will fetch
 * more results only if necessary because of the sort.
 */
ses.SearchResultsModel.prototype._doSort = function()
{
  // local variables required for use by function closure
  var groupAttribute = this._groupAttribute;
  var sortAttribute = this._sortAttribute;
  var isGrouping = (groupAttribute != ses.SearchResultsModel.NO_GROUP_ATTRIBUTE);
  var isDescending = (this._sortOrder == ses.SearchResultsModel.SORT_DESCENDING);

  this._obj.results.sort
  (
    function( a, b ) 
    {
      var s = 0;

      // check leading edge and sort on trailing edge only if necessary
      if( isGrouping )
      {
        var a1 = a[groupAttribute];
        var b1 = b[groupAttribute];
        s = ses.SearchResultsModel.__sort( a1, b1 );
      }

      if( s == 0 )
      {
        var a2 = a[sortAttribute];
        var b2 = b[sortAttribute];
        s = ses.SearchResultsModel.__sort( a2, b2 );
      }

      // stable sort using original position in result list
      if( s == 0 )
        s = ses.SearchResultsModel.__sort( a['pos'], b['pos'] ) * -1;

      // flip for descending sort order
      if( isDescending && s != 0 )
        s *= -1;

      return s;
    }
  );

  // notify all listeners about the model change
  this._event.fire( this );
};

ses.SearchResultsModel.__sort = function( a, b )
{
  // nulls at the bottom
  if( ( a == null ) && ( b == null ) )
    return 0;
  if( b == null )
    return -1;
  if( a == null )
    return 1;
  
  if( ( typeof a ) == "string" )
  {
    // locale-specific comparison for strings
    return a.localeCompare( b );
  }
  else
  {
    // only other datatype is number
    // NOTE: assumes dates are represented as numbers
    return( b - a );
  }
};

/**
 * Get list of groupable attributes, as an associative
 * array consisting of attribute names and display values.
 *
 * @return associative array of groupable attributes and display values
 */
ses.SearchResultsModel.prototype.getGroupableAttributes = function()
{
  return this._obj.groupable;
};

/**
 * Get list of sortable attributes, as an associative
 * array consisting of attribute names and display values.
 *
 * @return associative array of sortable attributes and display values
 */
ses.SearchResultsModel.prototype.getSortableAttributes = function()
{
  return this._obj.sortable;
};

/**
 * Get suggested links as an array of associative arrays.  Each element in the
 * array represents a suggested link, as an associative array.  There are two
 * pairs in the associative array - one for the title and one for the url.
 *
 * @return array of suggested links
 */
ses.SearchResultsModel.prototype.getSuggestedLinks = function()
{
  return this._obj.suggestedLinks;
};

/**
 * Get cluster data.
 */
ses.SearchResultsModel.prototype.getClusterData = function()
{
  return this._obj.clusterData;
};

/**
 * Get results as an array of associative arrays.  Each element in the array
 * represents a result, as an associative array.  Each pair in the associative
 * array represents an attribute and value for the result.  The special
 * attribute "innerHTML" can be used for rendering the result.
 *
 * @return array of results
 */
ses.SearchResultsModel.prototype.getResults = function()
{
  return this._obj.results;
};

/**
 * Get estimated hit count.
 *
 * @return estimated hit count
 */
ses.SearchResultsModel.prototype.getEstimatedHitCount = function()
{
  if( this._obj != null )
    return this._obj.estimatedHitCount;

  return 0;
};

/**
 * Check if count is lower boundary for security reasons (QTA).
 *
 * @return true if count is lower bound, false otherwise
 */
ses.SearchResultsModel.prototype.isCountLowerBound = function()
{
  if( this._obj != null )
    return this._obj.countLowerBound;

  return false;
};

/**
 * Get documents returned.
 *
 * @return documents returned
 */
ses.SearchResultsModel.prototype.getDocsReturned = function()
{
  return this._obj.docsReturned;
};

/**
 * Get time taken to send query and get response.
 *
 * @return time taken to send query and get response
 */
ses.SearchResultsModel.prototype.getTimeTaken = function()
{
  return this._timeTaken;
};

/**
 * Get buckets for a number attribute.
 *
 * @return bucket as an array of arrays
 */
ses.SearchResultsModel.prototype.getBuckets = function( attr )
{
  return this._obj.buckets[attr];
};

/**
 * Returns the bucket that a number attribute value falls into.
 *
 * @return min and max value of the bucket, as an array of two numbers
 */
ses.SearchResultsModel.prototype.getNumberBucket = function( attr, num )
{
  var buckets = this.getBuckets( attr );

  for( i in buckets )
  {
    if( num >= buckets[i][0] && num <= buckets[i][1] )
      return buckets[i];
  }

  // if cannot find bucket, or no buckets exist..
  return [num, num];   // [min, max]
};

ses.SearchResultsModel.prototype.registerCallback = function( callback, context )
{
  this._event.subscribe( callback, context );
};


// CLASS METHODS

/** Default number of hits to fetch when a query is executed */
ses.SearchResultsModel.DEFAULT_HITS_REQUESTED = 10;

/** Minimum number of hits to fetch when a sort is executed */
ses.SearchResultsModel.MIN_SORT_HITS_REQUESTED = 100;

/** Maximum number of hits to fetch for top-N operations */
ses.SearchResultsModel.MAX_TOPN_HITS_REQUESTED = 300;

/** Default group attribute */
ses.SearchResultsModel.NO_GROUP_ATTRIBUTE = "ses_none";

/** Default sort attribute */
ses.SearchResultsModel.DEFAULT_SORT_ATTRIBUTE = "ses_score";

/** Sort in ascending order */
ses.SearchResultsModel.SORT_ASCENDING = "asc";

/** Sort in descending order */
ses.SearchResultsModel.SORT_DESCENDING = "desc";


/**
 * Load model for sorting when the asynchronous HTTP request for data returns.
 *
 * @param o response object
 */
ses.SearchResultsModel.sortCallback = function( o )
{
  var content = o.responseText;

  // evaluate JSON string from server
  this._obj = eval( "(" + content + ")" );
  var now = ( new Date() ).getTime();
  this._timeTaken = now - this._timeStart;
  this._cacheSize = this._obj.results.length;

  // if 0 hits were returned, show warning icon
  if( this._cacheSize == 0 )
  {
    ses.SearchResultsView.setStatusIcon( ses.SearchResultsView.STATUS_WARN,
                                         ses.Locale.getMessage( 'ERR_FETCH_RESULTS' ),
                                         true );

    ses.SearchResultsView.setSidebarStatus( ses.Locale.getMessage( 'ERR_FETCH_RESULTS' ) );
    return;
  }

  // respect startnum and endnum URL parameters if they exist
  if( this._initialStart != null && this._initialEnd != null )
    this.setWindowBounds( this._initialStart, this._initialEnd );
  else
    this.setWindowSize( this.getPageSize() );

  // notify all listeners about the model change
  this._event.fire( this, "newResults" );
};

/**
 * Handles failures with the asynchronous HTTP request for results.
 */
ses.SearchResultsModel.failureHandler = function( o )
{
  // display warning icon with tip message:
  //   "There was an error fetching additional results."
  ses.SearchResultsView.setStatusIcon( ses.SearchResultsView.STATUS_WARN,
                                       ses.Locale.getMessage( 'ERR_FETCH_RESULTS' ),
                                       true );

  ses.SearchResultsView.setSidebarStatus( ses.Locale.getMessage( 'ERR_FETCH_RESULTS' ) );

  /*
  var content = o.responseText;
  var obj = eval( "(" + content + ")" );
  if( obj.fault )
  {
    var errMsg = obj.fault.faultstring;
    // display message

    var stack = obj.fault.detail.stackTrace;
  }
  */
};

/** Global sequence for uniquely identifying each instance of this class */
ses.SearchResultsModel._globalKey = 0;

/** Array of instances of this class */
ses.SearchResultsModel._list = new Array();

/**
 * Factory method for obtaining a SearchResultsModel object.
 * If a key is specified, return the object corresponding to that key
 * from the global list (and null if no object matches the key).  If the
 * key is not specified, construct a new object and return it.
 */
ses.SearchResultsModel.getModel = function( key )
{
  var model = null;
  if( key != null )
    model = ses.SearchResultsModel._list[ key ];
  else
    model = new ses.SearchResultsModel();
  return model;
};
