// 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

const PS_CONTRACTID = '@flock.com/poller-service;1';
const PS_CLASSID    = Components.ID('{a2b4cd0e-804b-4578-8c3c-4b61f8c1047a}');
const PS_CLASSNAME  = 'Flock Poller';


const PREF_POLLER_BRANCH            = 'flock.poller'
const PREF_REFRESH_DELAY            = '.refreshDelay';
const PREF_REFRESH_TIMEOUT          = '.refreshTimeout';
const PREF_DEFAULT_REFRESH_INTERVAL = '.defaultRefreshInterval';
const PREF_MAX_CONCURRENT_REFRESHES = '.maxConcurrentRefreshes';

// According to Mozilla docs, timers can't be set for more than approximately
// 6 hours. We'll limit to 4 hours just to be conservative.
const MAX_TIMER_INTERVAL = 4 * 60 * 60 * 1000;

// Delay actual polling for 30 seconds after component initialization, so
// there is reduced contention among other browser startup operations
const STARTUP_DELAY = 30 * 1000;


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


function getObserverService() {
  return Cc['@mozilla.org/observer-service;1']
    .getService(Ci.nsIObserverService);
} 


function RefreshListener(poller, urn, serviceId) {
  this._poller = poller;
  this._urn = urn;
  this._serviceId = serviceId;
}

RefreshListener.prototype = {
  onResult: function PSL_onResult() { 
    this._poller._logger.info('Successful refresh for ' + this._urn);
    this._poller._finishedRefresh(this._urn);
  },
  onError: function PSL_onError(error) {
    this._poller._logger.error('The service ' + this._serviceId + ' returned an ' +
                               'error while refreshing ' + this._urn);
    this._poller._finishedRefresh(this._urn);
  },
  notify: function PSL_notify() {
    this._poller._logger.error('The service ' + this._serviceId + ' timed out ' +
                               'while refreshing ' + this._urn);
    this._poller._finishedRefresh(this._urn);
  },
  QueryInterface: function PSL_QueryInterface(iid) {
    if (iid.equals(Ci.flockIPollerListener) ||
        iid.equals(Ci.nsITimerCallback) ||
        iid.equals(Ci.nsISupports))
      return this;
    throw Cr.NS_ERROR_NO_INTERFACE;
  }
}


function PollerService() {
  getObserverService().addObserver(this, "flock-data-ready", false);
}

PollerService.prototype = {
  _start: function PS__start() {
    this._logger = Cc['@flock.com/logger;1'].createInstance(Ci.flockILogger);
    this._logger.init('poller');
    this._logger.info('starting up...');

    this._profiler = Cc["@flock.com/profiler;1"].getService(Ci.flockIProfiler);
    var evtID = this._profiler.profileEventStart('poller-start');

    this._timer = Cc['@mozilla.org/timer;1'].createInstance(Ci.nsITimer);

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

    var prefService = Cc['@mozilla.org/preferences-service;1']
      .getService(Ci.nsIPrefBranch2);
    prefService.addObserver(PREF_POLLER_BRANCH, this, false);

    this.observe(null, 'nsPref:changed', PREF_REFRESH_DELAY);
    this.observe(null, 'nsPref:changed', PREF_REFRESH_TIMEOUT);
    this.observe(null, 'nsPref:changed', PREF_DEFAULT_REFRESH_INTERVAL);
    this.observe(null, 'nsPref:changed', PREF_MAX_CONCURRENT_REFRESHES);

    this._faves = Cc["@mozilla.org/rdf/datasource;1?name=flock-favorites"]
                  .getService(Ci.flockIRDFObservable);
    this._initializeQueue();
    this._watchRdfEvents();

    this._profiler.profileEventEnd(evtID, "");
  },
  _shutdown: function PS__shutdown() {
    var prefService = Cc['@mozilla.org/preferences-service;1']
      .getService(Ci.nsIPrefBranch2);
    prefService.removeObserver(PREF_POLLER_BRANCH, this);

    this._unwatchRdfEvents();

    this._timer.cancel();
    this._timer = null;
  },

  _prefChanged: function PS__prefChanged(pref) {
    var prefService = Cc['@mozilla.org/preferences-service;1']
      .getService(Ci.nsIPrefService);
    var prefBranch = prefService.getBranch(PREF_POLLER_BRANCH);

    switch (pref) {
      case PREF_REFRESH_DELAY:
        this._refreshDelay = prefBranch.getIntPref(pref);
        break;

      case PREF_REFRESH_TIMEOUT:
        this._refreshTimeout = prefBranch.getIntPref(pref);
        break;

      case PREF_DEFAULT_REFRESH_INTERVAL:
        this._defaultRefreshInterval = prefBranch.getIntPref(pref);
        break;

      case PREF_MAX_CONCURRENT_REFRESHES:
        this._maxConcurrentRefreshes = prefBranch.getIntPref(pref);
        break;
    }
  },

  _watchRdfEvents: function PS__watchRdfEvents() {
    var RDFS = Cc['@mozilla.org/rdf/rdf-service;1']
      .getService(Ci.nsIRDFService);
    var nextRefresh = RDFS.GetResource('http://flock.com/rdf#nextRefresh');
    var isPollable = RDFS.GetResource('http://flock.com/rdf#isPollable');

    this._faves.addArcObserver(Ci.flockIRDFObserver.TYPE_ALL, null,
                               nextRefresh, null, this);
    this._faves.addArcObserver(Ci.flockIRDFObserver.TYPE_ALL, null,
                               isPollable, null, this);
  },
  _unwatchRdfEvents: function PS__unwatchRdfEvents() {
    var RDFS = Cc["@mozilla.org/rdf/rdf-service;1"]
               .getService(Ci.nsIRDFService);
    var nextRefresh = RDFS.GetResource("http://flock.com/rdf#nextRefresh");
    var isPollable = RDFS.GetResource("http://flock.com/rdf#isPollable");

    this._faves.removeArcObserver(Ci.flockIRDFObserver.TYPE_ALL, null,
                                  nextRefresh, null, this);
    this._faves.removeArcObserver(Ci.flockIRDFObserver.TYPE_ALL, null,
                                  isPollable, null, this);
  },
  rdfChanged: function PS_rdfChanged(ds, type, rsrc, pred, obj, oldObj) {
    var uri = rsrc.ValueUTF8;
    var pollable = this._coop.get_from_resource(rsrc, uri, true);

    if (pollable && pollable.get("isPollable")) {
      var nextRefresh = pollable.get("nextRefresh");
      if (nextRefresh)
        this._addToQueue(uri, nextRefresh);
      else
        pollable.set("nextRefresh", new Date());
    } else {
      rsrc.QueryInterface(Ci.nsIRDFResource);
      this._removeFromQueue(uri);
    }
  },

  _initializeQueue: function PS__initializeQueue() {
    var queue = [];
    var dates = {};

    var RDFS = Cc['@mozilla.org/rdf/rdf-service;1']
      .getService(Ci.nsIRDFService);
    var isPollable = RDFS.GetResource('http://flock.com/rdf#isPollable');
    var trueLit = RDFS.GetLiteral("true");

    this._faves.QueryInterface(Ci.nsIRDFDataSource);
    var pollables = this._faves.GetSources(isPollable, trueLit, true);
    while (pollables.hasMoreElements()) {
      var pollable = this._coop.get_from_resource(pollables.getNext()
                                           .QueryInterface(Ci.nsIRDFResource));

      var urn = pollable.id();
      queue.push(urn);

      if (!pollable.nextRefresh)
        pollable.nextRefresh = new Date();

      dates[urn] = pollable.nextRefresh;
    }

    function sorter(a, b) {
      return dates[a] - dates[b];
    }
    queue.sort(sorter);

    this._queue = queue;
    this._dates = dates;

    this._refreshing = {};
    this._refreshOrder = [];

    this._lastRefresh = new Date(0);

    this._logger.debug('queue initialized: ' + this._queue.toSource());

    this._initTimer(STARTUP_DELAY);
  },

  _addToQueue: function PS__addToQueue(aUrn, aNextRefresh) {
    var date = this._dates[aUrn];
    if (date) {
      if (date - aNextRefresh == 0)
        return;
      else
        this._queue.splice(this._queue.indexOf(aUrn), 1);
    }

    this._dates[aUrn] = aNextRefresh;

    for (var i = 0; i < this._queue.length; i++) {
      var date = this._dates[this._queue[i]];
      if (aNextRefresh < date) {
        this._queue.splice(i, 0, aUrn);

        if (i == 0) {
          this._logger.debug("queue addition at start: " + aUrn);
          this._recalculateTimer();
        } else {
          this._logger.debug("queue addition at position " + i + ": " + aUrn);
        }

        return;
      }
    }

    this._queue.push(aUrn);

    if (this._queue.length == 1) {
      this._logger.debug("queue addition at start: " + aUrn);
      this._recalculateTimer();
    } else {
      this._logger.debug("queue addition at end: " + aUrn);
    }
  },

  _removeFromQueue: function PS__removeFromQueue(urn) {
    var date = this._dates[urn];
    if (!date)
      return;

    delete this._dates[urn];

    var index = this._queue.indexOf(urn);
    if (index == 0) {
      this._queue.shift();
      this._logger.debug("queue removal at start: " + urn);
      this._recalculateTimer();
    } else {
      this._queue.splice(index, 1);
      this._logger.debug("queue removal at position " + index + ": " + urn);
    }
  },

  _initTimer: function PS__initTimer(interval) {
    this._logger.debug('next refresh: ' + interval / 1000);
    this._timer.initWithCallback(this, interval, Ci.nsITimer.TYPE_ONE_SHOT);
  },
  _recalculateTimer: function PS__recalculateTimer() {
    this._timer.cancel();

    if (this._queue.length) {
      var date = this._dates[this._queue[0]];
      var now = new Date();

      var interval = date > now ? date - now : 0;
      interval = Math.min(interval, MAX_TIMER_INTERVAL);

      if (this._refreshOrder.length) {
        var urn = this._refreshOrder[this._refreshOrder.length - 1];
        var start = this._refreshing[urn].start;
        var delay = (this._refreshDelay * 1000) - (now - start);
        interval = Math.max(interval, delay);

        if (this._refreshOrder.length > this._maxConcurrentRefreshes - 1) {
          urn = this._refreshOrder[0];
          start = this._refreshing[urn].start;
          delay = (this._refreshTimeout * 1000) - (now - start);
          interval = Math.max(interval, delay);
        }
      } else {
        var delay = (this._refreshDelay * 1000) - (now - this._lastRefresh);
        interval = Math.max(interval, delay);
      }

      this._initTimer(interval);
    }
  },

  _finishedRefresh: function PS__finishedRefresh(urn) {
    var index = this._refreshOrder.indexOf(urn);
    if (index == -1)
      return;

    var refreshInfo = this._refreshing[urn]

    if (refreshInfo.timer) {
      refreshInfo.timer.cancel();
    }

    delete this._refreshing[urn];
    this._refreshOrder.splice(index, 1);

    var obj = this._coop.get(urn);
    if (!obj)
      return;

    obj.isRefreshing = false;

    var now = new Date();
    if (obj.nextRefresh < now) {
      var refreshInterval = obj.refreshInterval ? obj.refreshInterval
                                                : this._defaultRefreshInterval;

      obj.nextRefresh = new Date(now.getTime() + refreshInterval * 1000);
    }

    this._recalculateTimer();
  },
  _refresh: function PS__refresh(obj) {
    var urn = obj.id();
    if (this._refreshing[urn])
      return;

    var serviceId = obj.serviceId;
    this._logger.debug('serviceId = ' + serviceId);

    var service = null;
    if (serviceId && Cc[serviceId]) {
      try {
        service = Cc[serviceId].getService(Ci.flockIPollingService);
      }
      catch (e) {
        this._logger.error('Problem getting "' + serviceId + '" as a ' +
                           'flockIPollingService, while trying to refresh ' + 
                           urn);
      }
    }

    if (service) {
      this._logger.info('Refreshing ' + urn);
      try {
        var now = new Date();

        this._refreshing[urn] = { start: now };
        this._refreshOrder.push(urn);

        obj.isRefreshing = true;

        var listener = new RefreshListener(this, urn, serviceId);
        service.refresh(urn, listener);

        var refreshInfo = this._refreshing[urn];
        if (refreshInfo) {
          var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
          refreshInfo.timer = timer;
          timer.initWithCallback(listener, this._refreshTimeout * 1000,
                                 Ci.nsITimer.TYPE_ONE_SHOT);
        }

        this._lastRefresh = now;
      }
      catch (e) {
        this._logger.error("Exception while "
                           + serviceId
                           + " was refreshing a stream: "
                           + e);
        this._finishedRefresh(urn);
      }
    } else {
      this._logger.error('Problem getting the service, while trying to ' +
                         'refresh ' + urn);
    }
  },

  notify: function PS_notify(timer) {
    var urn = this._queue[0];
    var date = this._dates[urn];

    var now = new Date();
    if (now < date) {
      this._recalculateTimer();
      return;
    }

    this._queue.shift();  
    delete this._dates[urn];

    var obj = this._coop.get(urn);
    if (obj)
      this._refresh(this._coop.get(urn));
    else
      this._logger.warn('trying to refresh nonexistent object: ' + urn);

    this._recalculateTimer();
  },

  observe: function PS_observe(subject, topic, state) {

    switch (topic) {
      case 'flock-data-ready':
        var obs = getObserverService();
        obs.removeObserver(this, "flock-data-ready");
        obs.addObserver(this, "xpcom-shutdown", false);
        this._start();
        break;

      case 'xpcom-shutdown':
        getObserverService().removeObserver(this, "xpcom-shutdown");
        this._shutdown();
        break;

      case 'nsPref:changed':
        this._prefChanged(state);
        break;
    }
  },

  forceRefresh: function PS_forceRefresh(urn) {
    if (this._coop) {
      var obj = this._coop.get(urn);
      if (!obj) {
        throw "URN " + urn + " does not exist";
      }
      obj.nextRefresh = new Date(0);
    }
  },

  getInterfaces: function PS_getInterfaces(countRef) {
    var interfaces = [Ci.flockIPollerService, Ci.flockIRDFObserver,
                      Ci.nsITimerCallback, Ci.nsIObserver, Ci.nsIClassInfo,
                      Ci.nsISupports];
    countRef.value = interfaces.length;
    return interfaces;
  },
  getHelperForLanguage: function PS_getHelperForLanguage(language) {
    return null;
  },
  contractID: PS_CONTRACTID,
  classDescription: PS_CLASSNAME,
  classID: PS_CLASSID,
  implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
  flags: Ci.nsIClassInfo.SINGLETON,

  QueryInterface: function PS_QueryInterface(iid) {
    if (iid.equals(Ci.flockIPollerService) ||
        iid.equals(Ci.flockIRDFObserver) ||
        iid.equals(Ci.nsITimerCallback) ||
        iid.equals(Ci.nsIObserver) ||
        iid.equals(Ci.nsIClassInfo) ||
        iid.equals(Ci.nsISupports))
      return this;
    throw Cr.NS_ERROR_NO_INTERFACE;
  }
}


function GenericComponentFactory(ctor) {
  this._ctor = ctor;
}

GenericComponentFactory.prototype = {

  _ctor: null,

  // nsIFactory
  createInstance: function(outer, iid) {
    if (outer != null)
      throw Cr.NS_ERROR_NO_AGGREGATION;
    return (new this._ctor()).QueryInterface(iid);
  },

  // nsISupports
  QueryInterface: function(iid) {
    if (iid.equals(Ci.nsIFactory) ||
        iid.equals(Ci.nsISupports))
      return this;
    throw Cr.NS_ERROR_NO_INTERFACE;
  },
};

var Module = {
  QueryInterface: function(iid) {
    if (iid.equals(Ci.nsIModule) ||
        iid.equals(Ci.nsISupports))
      return this;

    throw Cr.NS_ERROR_NO_INTERFACE;
  },

  getClassObject: function(cm, cid, iid) {
    if (!iid.equals(Ci.nsIFactory))
      throw Cr.NS_ERROR_NOT_IMPLEMENTED;

    if (cid.equals(PS_CLASSID))
      return new GenericComponentFactory(PollerService);

    throw Cr.NS_ERROR_NO_INTERFACE;
  },

  registerSelf: function(cm, file, location, type) {
    var cr = cm.QueryInterface(Ci.nsIComponentRegistrar);
    cr.registerFactoryLocation(PS_CLASSID, PS_CLASSNAME, PS_CONTRACTID,
                               file, location, type);

    var catman = Cc['@mozilla.org/categorymanager;1']
      .getService(Ci.nsICategoryManager);
    catman.addCategoryEntry('flock-startup', PS_CLASSNAME,
                            'service,' + PS_CONTRACTID,
                            true, true);
  },

  unregisterSelf: function(cm, location, type) {
    var cr = cm.QueryInterface(Ci.nsIComponentRegistrar);
    cr.unregisterFactoryLocation(PS_CLASSID, location);
  },

  canUnload: function(cm) {
    return true;
  },
};

function NSGetModule(compMgr, fileSpec)
{
  return Module;
}
