// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2008
// http://flock.com
//
// This file may be used under the terms of the
// GNU General Public License Version 2 or later (the "GPL"),
// http://www.gnu.org/licenses/gpl.html
//
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
//
// END FLOCK GPL

// TODO JVL: queue is now a stack but the syntax continues to use 'queue'.  Will fix later.

// XPConnect Helpers
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cr = Components.results;
const CU = Components.utils;

CU.import("resource://gre/modules/JSON.jsm");

// Constants 
const MYWORLD_SHORTNAME = "myworld";
const MYWORLD_FULLNAME = "Myworld";
const MYWORLD_TITLE = "Myworld Service";
const MYWORLD_FAVICON = "http://www.flock.com/favicon.ico";
const MYWORLD_CID = Components.ID("{bb35349f-fb23-3089-4d3d-28462e1c98eb}");
const MYWORLD_CONTRACTID = "@flock.com/myworld-service;1";
const CATEGORY_COMPONENT_NAME = "Myworld JS Component";

const PREFS = Cc["@mozilla.org/preferences-service;1"]
              .getService(Ci.nsIPrefBranch);
const PREF_MYWORLD_WIDGETS_LAYOUT = "flock.myworld.widgets.layout";

const CACHESERVICE_CONTRACTID = "@mozilla.org/network/cache-service;1";
const TIMER_CONTRACTID = "@mozilla.org/timer;1";
const XMLHTTPREQUEST_CONTRACTID = "@mozilla.org/xmlextras/xmlhttprequest;1";

const MAX_FLICKR_FARMS = 10;
const PROTOCOL = "http://";
const FLICKR_FARM = "farm";
const MAX_RETRIEVAL_ATTEMPTS = 3;
const DEFAULT_INTERVAL = 500;  // in milliseconds
const DEFAULT_TIMER_TYPE = Ci.nsITimer.TYPE_REPEATING_PRECISE;



// Component Utils (just used for the timer/scheduler)
var gCompTK;
function getCompTK() {
  if (!gCompTK) {
    var gCompTK = Cc["@flock.com/singleton;1"]
      .getService(Ci.flockISingleton)
      .getSingleton("chrome://browser/content/flock/services/common/load-compTK.js")
      .wrappedJSObject;
  }
  return gCompTK;
}



// ===========================================================
// ========== BEGIN flockMyworldService Component ============
// ===========================================================


// BEGIN Constructor
function myworldService()
{
  this._logger = Cc["@flock.com/logger;1"].createInstance(Ci.flockILogger);
  this._logger.init("MyworldService");

  this._logger.debug("BEGIN Ctor");
  this._queue = [];

  this._ctk = {
    interfaces: [
      "nsISupports",
      "nsIClassInfo",
      "nsISupportsCString",
      "nsIObserver",
      "flockIMyworldService"
    ],
    shortName: MYWORLD_SHORTNAME,
    fullName: MYWORLD_FULLNAME,
    description: MYWORLD_TITLE,
    favicon: MYWORLD_FAVICON,
    CID: MYWORLD_CID,
    contractID: MYWORLD_CONTRACTID
  };

  this._coop = Cc["@flock.com/singleton;1"]
               .getService(Ci.flockISingleton)
               .getSingleton("chrome://flock/content/common/load-faves-coop.js")
               .wrappedJSObject;

  var inst = this;  // this is necessary as you cannot use 'this' within the defined notify function
  var callback = { notify: function(aTimer) { inst.processQueue(); } };
  //this._timer = Cc[TIMER_CONTRACTID].createInstance(Ci.nsITimer);
  //this._timer.initWithCallback(callback, DEFAULT_INTERVAL, DEFAULT_TIMER_TYPE);

  this._logger.info("Myworld service created");
  this._logger.debug("END Ctor");
}
// END Constructor

// BEGIN flockIMyworldService
myworldService.prototype.flags = Ci.nsIClassInfo.SINGLETON;

// queue a unique resource for processing
myworldService.prototype.queueResource =
function myworldService_queueResource(aURN)
{
  this._logger.debug("BEGIN queueResource: " + aURN);
  if (!this.queueResourceExists(aURN))
  {
    this.createQueueResource(aURN);
  }
  this._logger.debug("END queueResource: " + aURN);
}


// clear the queue
myworldService.prototype.clearQueue =
function myworldService_clearQueue()
{
  while(this._queue.length > 0)
  {
    this._queue.shift();
  }
  this._logger.info("queue cleared");
}


// check the browser cache for the given key
// returns a valid cached key or null if not found
myworldService.prototype.checkCache = 
function myworldService_checkCache(aKey)
{
  this._logger.debug("BEGIN checkCache: " + aKey);
  if (aKey)
  {
    var cacheSession = this.getCacheSession();
    if (cacheSession)
    {
      // check in the browser cache for the given key
      if (this.isValidCacheEntry(cacheSession, aKey))
      {
        this._logger.debug("END checkCache: " + aKey);
        return aKey;
      }

      // we couldn't find the given resource in the browser cache, it may have been forwarded to a farm...
      // trim the url(key) for our search needs and search for this modified url(key)
      var keyBase = aKey;
      if (aKey.indexOf(PROTOCOL) == 0)
      {
        keyBase = aKey.substr(PROTOCOL.length);
      }

      for (var idx = 1; idx < MAX_FLICKR_FARMS + 1; idx++)
      {
        var key = PROTOCOL + FLICKR_FARM + idx + "." + keyBase;
        if (this.isValidCacheEntry(cacheSession, key))
        {
          this._logger.debug("END checkCache: " + key);
          return key;
        }
      }
    }
  }

  this._logger.debug("END checkCache: null");
  return null;
}

myworldService.prototype.getColumnLayout =
function myworldService_getColumnLayout() {

  if (PREFS.getPrefType(PREF_MYWORLD_WIDGETS_LAYOUT)) {
    return PREFS.getComplexValue(PREF_MYWORLD_WIDGETS_LAYOUT, Ci.nsIPrefLocalizedString).data;
  } else {
    var columns = [];

    columns.push({
      index: "0",
      label: "topfeeds",
      url: "chrome://flock/content/start/topfeeds.xul",
      show: "true"
    });
    columns.push({
      index: "1",
      label: "topfriends",
      url: "chrome://flock/content/start/topfriends.xul",
      show: "true"
    });
    columns.push({
      index: "2",
      label: "topmedia",
      url: "chrome://flock/content/start/topmedia.xul",
      show: "true"
    });
    columns.push({
      index: "3",
      label: "topsites",
      url: "chrome://flock/content/start/topsites.xul",
      show: "false"
    });
 
    var layoutString = JSON.toString(columns);
    this.setColumnLayout(layoutString);
    return layoutString;
  }
}

myworldService.prototype.setColumnLayout =
function myworldService_setColumnLayout(aLayout) {
  PREFS.setCharPref(PREF_MYWORLD_WIDGETS_LAYOUT, aLayout);
}

// END flockIMyworldService


// BEGIN flockMyworldService
// returns the cache session or null if an error occured
myworldService.prototype.getCacheSession =
function myworldService_getCacheSession()
{
  var cacheService = Cc[CACHESERVICE_CONTRACTID].getService(Ci.nsICacheService);
  if (!cacheService)
  {
    this._logger.debug("getCacheSession: No cache service");
    return null;
  }

  // check both memory and disk cache
  // NOTE: 'HTTP' is the name for the browser cache
  var cacheSession = cacheService.createSession("HTTP", Ci.nsICache.STORE_ANYWHERE, true);
  if (!cacheSession)
  {
    this._logger.debug("getCacheSession: No cache session");
    return null;
  }

  cacheSession.doomEntriesIfExpired = false;
  return cacheSession;
}


// check that the given key is in the browser cache
// returns true if found, false otherwise
myworldService.prototype.isValidCacheEntry =
function isValidCacheEntry(cacheSession, aKey)
{
  this._logger.debug("BEGIN isValidCacheEntry: " + aKey);
  if (!cacheSession)
  {
    this._logger.debug("END isValidCacheEntry: No cache session");
    return false;
  }

  if (!aKey || aKey.length < 1)
  {
    this._logger.debug("END isValidCacheEntry: No key supplied");
    return false;
  }

  try
  {
    // checks browser cache for given key(url)
    // found if a cacheEntryDescriptor is returned and the resource's size is > 0,
    // otherwise it will throw an exception
    var cacheEntryDescriptor = cacheSession.openCacheEntry(aKey, Ci.nsICache.ACCESS_READ, false);
    var found = this.isNonEmptyResource(cacheEntryDescriptor);
    var msg = "END isValidCacheEntry: openCacheEntry: " + aKey + (found ? " (found in cache)" : " (not found in cache)");
    this._logger.debug(msg);
    return found;
  }
  catch (ex)
  {
    this._logger.debug("isValidCacheEntry: " + aKey + " (not in cache)");
  }

  this._logger.debug("END isValidCacheEntry: false");
  return false;
}


// helper function to determine if the cacheEntryDescriptor is:
// 1) not null
// 2) size > 0
// also reports additional details if showDetails = true
// returns true if it passes the above criteria, false otherwise
myworldService.prototype.isNonEmptyResource =
function isNonEmptyResource(cacheEntryDescriptor, showDetails)
{
  if (showDetails)
    this._logger.debug("isNonEmptyResource:");

  // NOTE: when checking cacheEntryDescriptor for nullness, we need to check it explictly against null.  '!cacheEntryDescriptor' will not work!
  var found = (cacheEntryDescriptor != null);

  if (showDetails)
    this._logger.debug("    descriptor: " + (found ? "not null" : "null"));

  var size = cacheEntryDescriptor.dataSize;

  if (showDetails)
    this._logger.debug("    size: " + size);

  return found && (size > 0);
}


// process the elements stored in the queue
myworldService.prototype.processQueue =
function processQueue()
{
  var elem = null;
  var coopObj = null;
  var thumbnail = null;

  while (1)
  {
    //this._logger.debug("BEGIN processQueue: current queue size: " + this._queue.length);
    if (this._queue.length < 1)
    {
      //this._logger.debug("END processQueue: nothing in the queue to process");
      return;
    }

    // pop the first entry off the queue for processing
    elem = this._queue.shift();
    if (!elem)
    {
      this._logger.debug("END processQueue: element popped off queue is null, nothing to process");
      return;
    }

    this._logger.debug("processQueue:");
    this._logger.debug("    processing: " + elem.urn);
    this._logger.debug("    items left in queue: " + this._queue.length);
    
    // retrieve the appropriate resource from the rdf in memory
    coopObj = this._coop.get(elem.urn);
    if (!coopObj)
    {
      this._logger.debug("END processQueue: " + elem.urn + " (not in rdf)");
      return;
    }

    thumbnail = coopObj.thumbnail;
    this._logger.debug("processQueue: thumbnail: " + thumbnail);

    // if the retrieved resource doesn't have topmedia as a parent,
    // discard from the queue and just set the regular thumbnail in hopes it can show something
    if (this.belongsToTopMedia(coopObj))
    {
      this._logger.debug("processQueue: " + elem.urn + " (found in topmedia)");
      break;
    }
    else
    {
      this._logger.debug("processQueue: " + elem.urn + " (not in topmedia, process the next item in the queue)");
      coopObj.cachedThumbnail = thumbnail;
    }
  }

  // we only need to update the rdf with the cachedThumbnail if it doesn't already exist
  if (!this.isValidCachedThumbnail(coopObj.cachedThumbnail, thumbnail))
  {
    // take a look in the browser cache for the cached thumbnail url
    thumbnail = this.checkCache(thumbnail);
    if (thumbnail)
    {
      this._logger.debug("processQueue: " + thumbnail + " (in cache)");
      
      // found the cached thumbnail url, now store it in the rdf; we're done!
      coopObj.cachedThumbnail = thumbnail;
    }
    else
    {
      this._logger.debug("processQueue: " + coopObj.thumbnail + " (not in cache)");
      coopObj.cachedThumbnail = "";
      
      // we couldn't find the cached thumbnail url in the browser cache so retrieve the resource
      // from the social service to see if we can cache it ourselves and retrieve it later
      this.reRequestResource(coopObj.thumbnail);

      this._logger.debug("processQueue: re-add to queue:");
      this._logger.debug("    URN: " + elem.urn);
      this._logger.debug("    current attempt: " + elem.attempt);

      // since it wasn't in the browser cache this time, re-add the queued entry to the end of the
      // queue; we will try retrieving it later (for a maximum of MAX_RETRIEVAL_ATTEMPTS) otherwise,
      // just fail as there might be something wrong with the social service itself
      this.requeueResource(elem, coopObj)
    }
  }

  this._logger.debug("END processQueue");
}


// checks to see if the coop object has a topmedia as a parent
// returns true if topmedia is a parent, false otherwise
myworldService.prototype.belongsToTopMedia =
function myworldService_belongsToTopMedia(coopObj)
{
  if (!coopObj)
  {
    return false;
  }

  var parents = coopObj.getParents();
  if (!parents)
  {
    return false;
  }

  for (var idx in parents)
  {
    var parent = parents[idx];
    if (parent)
    {
      var grandParents = parent.getParents();
      if (grandParents)
      {
        for (var idxGP in grandParents)
        {
          var grandParent = grandParents[idxGP];
          if (grandParent.id() == "urn:flock:topmedia")
          {
            this._logger.debug("belongsToTopMedia: belongs to topmedia!");
            return true;
          }
        }
      }
    }
  }

  return false;
}


// check if the cachedThumbnail is:
// 1) not null or empty and
// 2) not the loading picture
// returns true if the cachedThumbnail passes the above criteria, false otherwise
myworldService.prototype.isValidCachedThumbnail =
function myworldService_isValidCachedThumbnail(cachedThumbnail, thumbnail)
{
  return cachedThumbnail && cachedThumbnail.length > 0;
}


// re-retrieve the resource from the social service to see if we can cache it ourselves and retrieve it later
myworldService.prototype.reRequestResource =
function myworldService_reRequestResource(thumbnail)
{
  this._logger.debug("BEGIN reRequestResource");
  if (thumbnail)
  {
    var request = Cc[XMLHTTPREQUEST_CONTRACTID].createInstance(Ci.nsIXMLHttpRequest);
    if (!request)
    {
      this._logger.debug("END reRequestResource: unable to instantiate HTTPRequest");
      return;
    }

    try
    {
      this._logger.debug("reRequestResource: HTTPRequest sent for " + thumbnail);
      request.open("GET", coopObj.thumbnail, true);
      request.send(null);
    }
    catch (ex)
    {
      this._logger.debug("reRequestResource: HTTPRequest failed for " + thumbnail);
    }
  }
  this._logger.debug("END reRequestResource");
}


// requeue the resource unless the maximum attempts to retrieve the resource has been exceeded
myworldService.prototype.requeueResource =
function myworldService_requeueResource(elem, coopObj)
{
  this._logger.debug("BEGIN requeueResource");
  elem.attempt++;
  if (elem.attempt < MAX_RETRIEVAL_ATTEMPTS)
  {
    this._queue.unshift(elem);
    this._logger.debug("requeueResource: " + elem.urn + " (re-added to queue successfully)");
  }
  else
  {
    // number of attempts exceeded, just set the displayed thumbnail to the original resource
    coopObj.cachedThumbnail = coopObj.thumbnail;
    this._logger.debug("requeueResource: " + elem.urn + " (not re-added to queue as we've already tried " + MAX_RETRIEVAL_ATTEMPTS + " times)");
  }

  this._logger.debug("    number of items in queue: " + this._queue.length);
  this._logger.debug("END requeueResource");
}


// check if the resource already exists in the queue to be processed
// returns false if it is unique to the queue, true otherwise
myworldService.prototype.queueResourceExists =
function myworldService_queueResourceExists(aURN)
{
  this._logger.debug("BEGIN queueResourceExists: " + aURN);
  if (!aURN || aURN.length < 1)
  {
    this._logger.debug("END queueResourceExists: true (empty string)");
    return true;
  }

  // ensure we don't add duplicates to the queue
  // NOTE: This may become an issue if there are an excessive number of items in the queue
  for (var idx = 0; idx < this._queue.length; idx++)
  {
    if (this._queue[idx].urn == aURN)
    {
      this._logger.debug("END queueResourceExists: true (already exists)");
      return true;
    }
  }

  this._logger.debug("END queueResourceExists: false");
  return false;
}


// create a new resource and add it to the service queue to be processed
myworldService.prototype.createQueueResource =
function myworldService_createQueueResource(aURN)
{
  if (aURN && aURN.length > 0)
  {
    // add a new url/retrieval attempts (key/value) pair to the queue
    var obj = {};
    obj.urn = aURN;
    obj.attempt = 0;
    this._logger.debug("createQueueResource:");
    this._logger.debug("    URN: " + obj.urn);
    this._logger.debug("    attempt: " + obj.attempt);
    this._queue.unshift(obj);
  }
}
// END flockMyworldService


// ========== END flockMyworldService Component ============



// ================================================
// ========== BEGIN XPCOM Module support ==========
// ================================================

const ENABLE_DEBUG = false; // switch to turn off slow debug code for production
function DEBUG(x) { if (ENABLE_DEBUG) debug("flockMyworldService: "+x+"\n"); }

function createModule(aParams) {
  DEBUG("createModule called\n    CID: " + aParams.CID + "\n    componentName: " + aParams.componentName + "\n    contractID: " + aParams.contractID + "\n    componentClass: " + aParams.componentClass);
  return {
    registerSelf: function (aCompMgr, aFileSpec, aLocation, aType) {
      aCompMgr.QueryInterface(Ci.nsIComponentRegistrar);
      aCompMgr.registerFactoryLocation( aParams.CID, aParams.componentName,
                                        aParams.contractID, aFileSpec,
                                        aLocation, aType );
      var catMgr = Cc["@mozilla.org/categorymanager;1"]
        .getService(Ci.nsICategoryManager);
      catMgr.addCategoryEntry( "flock-startup", aParams.componentName,
                               "service,"+aParams.contractID, true, true ); 
      if (!aParams.categories) { aParams.categories = []; }
      for (var i = 0; i < aParams.categories.length; i++) {
        var cat = aParams.categories[i];
        catMgr.addCategoryEntry( cat.category, cat.entry,
                                 cat.value, true, true );
      }
    },
    getClassObject: function (aCompMgr, aCID, aIID) {
      if (!aCID.equals(aParams.CID)) { throw Cr.NS_ERROR_NO_INTERFACE; }
      if (!aIID.equals(Ci.nsIFactory)) { throw Cr.NS_ERROR_NOT_IMPLEMENTED; }
      return { // Factory
        createInstance: function (aOuter, aIID) {
          if (aOuter != null) { throw Cr.NS_ERROR_NO_AGGREGATION; }
          var comp = new aParams.componentClass();
          if (aParams.implementationFunc) { aParams.implementationFunc(comp); }
          rv = comp.QueryInterface(aIID);
          return rv;
        }
      };
    },
    canUnload: function (aCompMgr) { return true; }
  };
}

// NS Module entrypoint
function NSGetModule(aCompMgr, aFileSpec) {
  DEBUG("NSGetModule called");
  return createModule({
    componentClass: myworldService,
    CID: MYWORLD_CID,
    contractID: MYWORLD_CONTRACTID,
    componentName: CATEGORY_COMPONENT_NAME,
    implementationFunc: function (aComp) { getCompTK().addAllInterfaces(aComp); },
    categories: [
    ]
  });
}


// ========== END XPCOM Module support ==========
