/**
//
// BEGIN SONGBIRD GPL
// 
// This file is part of the Songbird web player.
//
// Copyright(c) 2005-2008 POTI, Inc.
// http://songbirdnest.com
// 
// This file may be licensed under the terms of of the
// GNU General Public License Version 2 (the "GPL").
// 
// Software distributed under the License is distributed 
// on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either 
// express or implied. See the GPL for the specific language 
// governing rights and limitations.
//
// You should have received a copy of the GPL along with this 
// program. If not, go to http://www.gnu.org/licenses/gpl.html
// or write to the Free Software Foundation, Inc., 
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
// 
// END SONGBIRD GPL
//
 */
 
/**
 * \file sbSearchSuggester.js
 * Provides autocomplete suggestions based on player state/context
 * Originally based on the Mozilla nsSearchSuggestions.js implementation
 */ 



const BROWSER_SUGGEST_PREF = "browser.search.suggest.enabled";
const XPCOM_SHUTDOWN_TOPIC              = "xpcom-shutdown";
const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";

const SONGBIRD_DATAREMOTE_CONTRACTID = "@songbirdnest.com/Songbird/DataRemote;1";
const sbIDataRemote            = Components.interfaces.sbIDataRemote;


const SEARCH_SUGGEST_CONTRACTID =
  "@mozilla.org/autocomplete/search;1?name=songbird-autocomplete";
const SEARCH_SUGGEST_CLASSNAME = "Songbird Search Suggestions";
const SEARCH_SUGGEST_CLASSID =
  Components.ID("{0be64502-ee00-11db-8314-0800200c9a66}");

const SEARCH_BUNDLE = "chrome://songbird/locale/songbird.properties";

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;


/**
 * AutoCompleteResult contains the results returned by the Suggest
 * service - it implements nsIAutoCompleteResult and is used by the auto-
 * complete controller to populate the front end.
 * @constructor
 */
function AutoCompleteResult(searchString,
                                   defaultIndex,
                                   errorDescription,
                                   results,
                                   comments) {
  this._searchString = searchString;
  this._defaultIndex = defaultIndex;
  this._errorDescription = errorDescription;
  this._results = results;
  this._comments = comments;
}
AutoCompleteResult.prototype = {
  /**
   * The user's query string
   * @private
   */
  _searchString: "",

  /**
   * The default item that should be entered if none is selected
   * @private
   */
  _defaultIndex: 0,

  /**
   * The reason the search failed
   * @private
   */
  _errorDescription: "",

  /**
   * The list of words returned by the Suggest Service
   * @private
   */
  _results: [],

  /**
   * The list of Comments (number of results - or page titles) returned by the
   * Suggest Service.
   * @private
   */
  _comments: [],


  /**
   * @return the user's query string
   */
  get searchString() {
    return this._searchString;
  },

  /**
   * @return the result code of this result object, either:
   *         RESULT_IGNORED   (invalid searchString)
   *         RESULT_FAILURE   (failure)
   *         RESULT_NOMATCH   (no matches found)
   *         RESULT_SUCCESS   (matches found)
   */
  get searchResult() {
    if (this._results.length > 0) {
      return Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
    } else {
      Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
    }
  },

  /**
   * @return the default item that should be entered if none is selected
   */
  get defaultIndex() {
    return this._defaultIndex;
  },

  /**
   * @return the reason the search failed
   */
  get errorDescription() {
    return this._errorDescription;
  },

  /**
   * @return the number of results
   */
  get matchCount() {
    return this._results.length;
  },

  /**
   * Retrieves a result
   * @param  index    the index of the result requested
   * @return          the result at the specified index
   */
  getValueAt: function(index) {
    return this._results[index];
  },

  /**
   * Retrieves a comment (metadata instance)
   * @param  index    the index of the comment requested
   * @return          the comment at the specified index
   */
  getCommentAt: function(index) {
    return this._comments[index];
  },

  /**
   * Retrieves a style hint specific to a particular index.
   * @param  index    the index of the style hint requested
   * @return          the style hint at the specified index
   */
  getStyleAt: function(index) {
    if (!this._comments[index])
      return null;  // not a category label, so no special styling

    if (index == 0)
      return "suggestfirst";  // category label on first line of results

    return "suggesthint";   // category label on any other line of results
  },

  /** 
   * Retrieves an image url. 
   * @param  index    the index of the image url requested 
   * @return          the image url at the specified index 
   */ 
  getImageAt: function(index) { 
    return ""; 
  },
     
  /**
   * Removes a result from the resultset
   * @param  index    the index of the result to remove
   */
  removeValueAt: function(index, removeFromDatabase) {
    this._results.splice(index, 1);
    this._comments.splice(index, 1);
  },

  /**
   * Part of nsISupports implementation.
   * @param   iid     requested interface identifier
   * @return  this object (XPConnect handles the magic of telling the caller that
   *                       we're the type it requested)
   */
  QueryInterface: function(iid) {
    if (!iid.equals(Ci.nsIAutoCompleteResult) &&
        !iid.equals(Ci.nsISupports))
      throw Cr.NS_ERROR_NO_INTERFACE;
    return this;
  }
};






/**
 * Implements nsIAutoCompleteSearch to provide suggestions based 
 * on Songbird's state.
 *
 * To access this suggester set autocompletesearch="songbird-autocomplete"
 * on an autocomplete textbox.  See the search.xml binding for details.
 *
 * @constructor
 */
function SearchSuggester() {
  this._addObservers();
  this._loadSuggestPref();
  this._loadDataRemotes();
}
SearchSuggester.prototype = {

  /**
   * this._strings is the string bundle for message internationalization.
   */
  get _strings() {
    if (!this.__strings) {
      var sbs = Cc["@mozilla.org/intl/stringbundle;1"].
                getService(Ci.nsIStringBundleService);

      this.__strings = sbs.createBundle(SEARCH_BUNDLE);
    }
    return this.__strings;
  },
  __strings: null,

  /**
   * Search suggestions will be shown if this._suggestEnabled is true.
   */
  _loadSuggestPref: function SAC_loadSuggestPref() {
    var prefService = Cc["@mozilla.org/preferences-service;1"].
                      getService(Ci.nsIPrefBranch);
    this._suggestEnabled = prefService.getBoolPref(BROWSER_SUGGEST_PREF);
  },
  _suggestEnabled: null,

  // Metadata for the current playing track in Songbird
  _metadataTitle:  null,
  _metadataArtist: null,
  _metadataAlbum:  null,
  
  /**
   * Load metadata dataremotes
   */
  _loadDataRemotes: function SAC_loadDataRemotes() {
  
    // TODO: Is this an issue?  Should I be waiting for profile load before doing this?
    var createDataRemote = new Components.Constructor( 
                SONGBIRD_DATAREMOTE_CONTRACTID, sbIDataRemote, "init");
    this._metadataTitle   = createDataRemote("metadata.title", null);
    this._metadataArtist  = createDataRemote("metadata.artist", null);
    this._metadataAlbum   = createDataRemote("metadata.album", null);
  },  
  
  /**
   * Let go of metadata dataremotes just in case
   */
  _releaseDataRemotes: function() {
    this._metadataTitle.unbind();
    this._metadataArtist.unbind();
    this._metadataAlbum.unbind();
  },  


  /**
   * The object implementing nsIAutoCompleteObserver that we notify when
   * we have found results
   * @private
   */
  _listener: null,


  /**
   * Notifies the front end of new results.
   * @param searchString  the user's query string
   * @param results       an array of results to the search
   * @param comments      an array of metadata corresponding to the results
   * @private
   */
  onSearchResult: function(searchString, results, comments) {
    if (this._listener) {
      var result = new AutoCompleteResult(
          searchString,
          0,
          "",
          results,
          comments);

      this._listener.onSearchResult(this, result);

      // Null out listener to make sure we don't notify it twice, in case our
      // timer callback still hasn't run.
      this._listener = null;
    }
  },


  /**
   * Initiates the search result gathering process. Part of
   * nsIAutoCompleteSearch implementation.
   *
   * @param searchString    the user's query string
   * @param searchParam     unused, "an extra parameter"; even though
   *                        this parameter and the next are unused, pass
   *                        them through in case the form history
   *                        service wants them
   * @param previousResult  unused, a client-cached store of the previous
   *                        generated resultset for faster searching.
   * @param listener        object implementing nsIAutoCompleteObserver which
   *                        we notify when results are ready.
   */
  startSearch: function(searchString, searchParam, previousResult, listener) {
    this.stopSearch();
    
    var searchService = Cc["@mozilla.org/browser/search-service;1"].
                        getService(Ci.nsIBrowserSearchService);

    // If there's an existing request, stop it
    this.stopSearch();

    this._listener = listener;

    var results = [];

    var engine = searchService.currentEngine;

    // Normally we would do something asynchronous, but since 
    // for now all we're returning is dataremote values, we
    // might as well just do it immediately.
    
    // If there is no search query then get some default suggestions
    if (searchString == "") {
      results = this._getPlayerContextSuggestions();    
    }
    
    // TODO Add a localized comment
    var comments = [];
    for (var i = 0; i < results.length; i++) {
      comments.push("");
    }

    this.onSearchResult(searchString, results, comments);    
  },

  /**
   * Ends the search result gathering process. Part of nsIAutoCompleteSearch
   * implementation.
   */
  stopSearch: function() {
    // Nothing to do since we return our searches immediately.
  },


  /**
   * Get a list of suggestions to display regardless of the search query
   */
  _getPlayerContextSuggestions: function() {
    var results = [];
    
    // TODO: Do not return metadata unless playing or paused!
    // Currently returns metadata from the previous session.
    
    if (this._metadataTitle.stringValue != "") {
      results.push(this._metadataTitle.stringValue);
    }
    if (this._metadataAlbum.stringValue != "") {
      results.push(this._metadataAlbum.stringValue);
    }
    if (this._metadataArtist.stringValue != "") {
      results.push(this._metadataArtist.stringValue);
    }
    return results;
  },


  /**
   * nsIObserver
   */
  observe: function SAC_observe(aSubject, aTopic, aData) {
    switch (aTopic) {
      case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID:
        this._loadSuggestPref();
        break;
      case XPCOM_SHUTDOWN_TOPIC:
        this.stopSearch();
        this._removeObservers();
        this._releaseDataRemotes();
        break;
    }
  },

  _addObservers: function SAC_addObservers() {
    var prefService2 = Cc["@mozilla.org/preferences-service;1"].
                       getService(Ci.nsIPrefBranch2);
    prefService2.addObserver(BROWSER_SUGGEST_PREF, this, false);

    var os = Cc["@mozilla.org/observer-service;1"].
             getService(Ci.nsIObserverService);
    os.addObserver(this, XPCOM_SHUTDOWN_TOPIC, false);
  },

  _removeObservers: function SAC_removeObservers() {
    var prefService2 = Cc["@mozilla.org/preferences-service;1"].
                       getService(Ci.nsIPrefBranch2);
    prefService2.removeObserver(BROWSER_SUGGEST_PREF, this);

    var os = Cc["@mozilla.org/observer-service;1"].
             getService(Ci.nsIObserverService);
    os.removeObserver(this, XPCOM_SHUTDOWN_TOPIC);
  },

  /**
   * Part of nsISupports implementation.
   * @param   iid     requested interface identifier
   * @return  this object (XPConnect handles the magic of telling the caller that
   *                       we're the type it requested)
   */
  QueryInterface: function(iid) {
    if (!iid.equals(Ci.nsIAutoCompleteSearch) &&
        !iid.equals(Ci.nsIObserver) &&
        !iid.equals(Ci.nsISupports))
      throw Cr.NS_ERROR_NO_INTERFACE;
    return this;
  }
};



var gModule = {
  /**
   * Registers all the components supplied by this module. Part of nsIModule
   * implementation.
   * @param componentManager  the XPCOM component manager
   * @param location          the location of the module on disk
   * @param loaderString      opaque loader specific string
   * @param type              loader type being used to load this module
   */
  registerSelf: function(componentManager, location, loaderString, type) {
    if (this._firstTime) {
      this._firstTime = false;
      throw Cr.NS_ERROR_FACTORY_REGISTER_AGAIN;
    }
    componentManager =
      componentManager.QueryInterface(Ci.nsIComponentRegistrar);

    for (var key in this.objects) {
      var obj = this.objects[key];
      componentManager.registerFactoryLocation(obj.CID, obj.className, obj.contractID,
                                               location, loaderString, type);
    }
  },

  /**
   * Retrieves a Factory for the given ClassID. Part of nsIModule
   * implementation.
   * @param componentManager  the XPCOM component manager
   * @param cid               the ClassID of the object for which a factory
   *                          has been requested
   * @param iid               the IID of the interface requested
   */
  getClassObject: function(componentManager, cid, iid) {
    if (!iid.equals(Ci.nsIFactory))
      throw Cr.NS_ERROR_NOT_IMPLEMENTED;

    for (var key in this.objects) {
      if (cid.equals(this.objects[key].CID))
        return this.objects[key].factory;
    }

    throw Cr.NS_ERROR_NO_INTERFACE;
  },

  /**
   * Create a Factory object that can construct an instance of an object.
   * @param constructor   the constructor used to create the object
   * @private
   */
  _makeFactory: function(constructor) {
    function createInstance(outer, iid) {
      if (outer != null)
        throw Cr.NS_ERROR_NO_AGGREGATION;
      return (new constructor()).QueryInterface(iid);
    }
    return { createInstance: createInstance };
  },

  /**
   * Determines whether or not this module can be unloaded.
   * @return returning true indicates that this module can be unloaded.
   */
  canUnload: function(componentManager) {
    return true;
  }
};

/**
 * Entry point for registering the components supplied by this JavaScript
 * module.
 * @param componentManager  the XPCOM component manager
 * @param location          the location of this module on disk
 */
function NSGetModule(componentManager, location) {
  // Metadata about the objects this module can construct
  gModule.objects = {
    search: {
      CID: SEARCH_SUGGEST_CLASSID,
      contractID: SEARCH_SUGGEST_CONTRACTID,
      className: SEARCH_SUGGEST_CLASSNAME,
      factory: gModule._makeFactory(SearchSuggester)
    },
  };
  return gModule;
}
