// Publish the current client versions to the client.  When a client
// sees the subscription change and that there is a new version of the
// client available on the server, it can reload.
//
// By default there are two current client versions. The refreshable client
// version is identified by a hash of the client resources seen by the browser
// that are refreshable, such as CSS, while the non refreshable client version
// is identified by a hash of the rest of the client assets
// (the HTML, code, and static files in the `public` directory).
//
// If the environment variable `AUTOUPDATE_VERSION` is set it will be
// used as the client id instead.  You can use this to control when
// the client reloads.  For example, if you want to only force a
// reload on major changes, you can use a custom AUTOUPDATE_VERSION
// which you only change when something worth pushing to clients
// immediately happens.
//
// The server publishes a `meteor_autoupdate_clientVersions`
// collection. There are two documents in this collection, a document
// with _id 'version' which represents the non refreshable client assets,
// and a document with _id 'version-refreshable' which represents the
// refreshable client assets. Each document has a 'version' field
// which is equivalent to the hash of the relevant assets. The refreshable
// document also contains a list of the refreshable assets, so that the client
// can swap in the new assets without forcing a page refresh. Clients can
// observe changes on these documents to detect when there is a new
// version available.
//
// In this implementation only two documents are present in the collection
// the current refreshable client version and the current nonRefreshable client
// version.  Developers can easily experiment with different versioning and
// updating models by forking this package.

var Future = Npm.require("fibers/future");

Autoupdate = {};

// The collection of acceptable client versions.
ClientVersions = new Mongo.Collection("meteor_autoupdate_clientVersions",
  { connection: null });

// The client hash includes __meteor_runtime_config__, so wait until
// all packages have loaded and have had a chance to populate the
// runtime config before using the client hash as our default auto
// update version id.

// Note: Tests allow people to override Autoupdate.autoupdateVersion before
// startup.
Autoupdate.autoupdateVersion = null;
Autoupdate.autoupdateVersionRefreshable = null;
Autoupdate.autoupdateVersionCordova = null;
Autoupdate.appId = __meteor_runtime_config__.appId = process.env.APP_ID;

var syncQueue = new Meteor._SynchronousQueue();

// updateVersions can only be called after the server has fully loaded.
var updateVersions = function (shouldReloadClientProgram) {
  // Step 1: load the current client program on the server and update the
  // hash values in __meteor_runtime_config__.
  if (shouldReloadClientProgram) {
    WebAppInternals.reloadClientPrograms();
  }

  // If we just re-read the client program, or if we don't have an autoupdate
  // version, calculate it.
  if (shouldReloadClientProgram || Autoupdate.autoupdateVersion === null) {
    Autoupdate.autoupdateVersion =
      process.env.AUTOUPDATE_VERSION ||
      WebApp.calculateClientHashNonRefreshable();
  }
  // If we just recalculated it OR if it was set by (eg) test-in-browser,
  // ensure it ends up in __meteor_runtime_config__.
  __meteor_runtime_config__.autoupdateVersion =
    Autoupdate.autoupdateVersion;

  Autoupdate.autoupdateVersionRefreshable =
    __meteor_runtime_config__.autoupdateVersionRefreshable =
      process.env.AUTOUPDATE_VERSION ||
      WebApp.calculateClientHashRefreshable();

  Autoupdate.autoupdateVersionCordova =
    __meteor_runtime_config__.autoupdateVersionCordova =
      process.env.AUTOUPDATE_VERSION ||
      WebApp.calculateClientHashCordova();

  // Step 2: form the new client boilerplate which contains the updated
  // assets and __meteor_runtime_config__.
  if (shouldReloadClientProgram) {
    WebAppInternals.generateBoilerplate();
  }

  // XXX COMPAT WITH 0.8.3
  if (! ClientVersions.findOne({current: true})) {
    // To ensure apps with version of Meteor prior to 0.9.0 (in
    // which the structure of documents in `ClientVersions` was
    // different) also reload.
    ClientVersions.insert({current: true});
  }

  if (! ClientVersions.findOne({_id: "version"})) {
    ClientVersions.insert({
      _id: "version",
      version: Autoupdate.autoupdateVersion
    });
  } else {
    ClientVersions.update("version", { $set: {
      version: Autoupdate.autoupdateVersion
    }});
  }

  if (! ClientVersions.findOne({_id: "version-cordova"})) {
    ClientVersions.insert({
      _id: "version-cordova",
      version: Autoupdate.autoupdateVersionCordova,
      refreshable: false
    });
  } else {
    ClientVersions.update("version-cordova", { $set: {
      version: Autoupdate.autoupdateVersionCordova
    }});
  }

  // Use `onListening` here because we need to use
  // `WebAppInternals.refreshableAssets`, which is only set after
  // `WebApp.generateBoilerplate` is called by `main` in webapp.
  WebApp.onListening(function () {
    if (! ClientVersions.findOne({_id: "version-refreshable"})) {
      ClientVersions.insert({
        _id: "version-refreshable",
        version: Autoupdate.autoupdateVersionRefreshable,
        assets: WebAppInternals.refreshableAssets
      });
    } else {
      ClientVersions.update("version-refreshable", { $set: {
        version: Autoupdate.autoupdateVersionRefreshable,
        assets: WebAppInternals.refreshableAssets
      }});
    }
  });
};

Meteor.publish(
  "meteor_autoupdate_clientVersions",
  function (appId) {
    // `null` happens when a client doesn't have an appId and passes
    // `undefined` to `Meteor.subscribe`. `undefined` is translated to
    // `null` as JSON doesn't have `undefined.
    check(appId, Match.OneOf(String, undefined, null));

    // Don't notify clients using wrong appId such as mobile apps built with a
    // different server but pointing at the same local url
    if (Autoupdate.appId && appId && Autoupdate.appId !== appId)
      return [];

    return ClientVersions.find();
  },
  {is_auto: true}
);

Meteor.startup(function () {
  updateVersions(false);
});

var fut = new Future();

// We only want 'refresh' to trigger 'updateVersions' AFTER onListen,
// so we add a queued task that waits for onListen before 'refresh' can queue
// tasks. Note that the `onListening` callbacks do not fire until after
// Meteor.startup, so there is no concern that the 'updateVersions' calls from
// 'refresh' will overlap with the `updateVersions` call from Meteor.startup.

syncQueue.queueTask(function () {
  fut.wait();
});

WebApp.onListening(function () {
  fut.return();
});

var enqueueVersionsRefresh = function () {
  syncQueue.queueTask(function () {
    updateVersions(true);
  });
};

// Listen for the special {refresh: 'client'} message, which signals that a
// client asset has changed.
process.on('message', Meteor.bindEnvironment(function (m) {
  if (m && m.refresh === 'client') {
    enqueueVersionsRefresh();
  }
}, "handling client refresh message"));

// Another way to tell the process to refresh: send SIGHUP signal
process.on('SIGHUP', Meteor.bindEnvironment(function () {
  enqueueVersionsRefresh();
}, "handling SIGHUP signal for refresh"));

