var files = require('../fs/files');
var catalog = require('./catalog/catalog.js');

var release = exports;

var Release = function (options) {
  var self = this;

  // If an actual, proper, "released" release, the name of the
  // release, eg, "METEOR@1.0". If not a proper release, null.
  self.name = options.name;

  if (self.name === null) {
    // Running from checkout.
    self._manifest = null;
  } else {
    // Running a proper release
    self._manifest = options.manifest;
  }
};

Object.assign(Release.prototype, {
  // True if an actual, proper, "released" release. If so, this.name
  // will have the name of the release, eg, "1.0".
  isProperRelease: function () {
    return this.name !== null;
  },

  // True if this "release" is actually a checkout on disk. It is
  // defined by the packages in the checkout rather than by a
  // manifest. this.name will be null.
  isCheckout: function () {
    return this.name === null;
  },

  isRecommended: function () {
    return this._manifest.recommended;
  },

  getReleaseTrack: function () {
    var self = this;
    if (! self.isProperRelease()) {
      throw new Error("not a proper release?");
    }
    return self.name.split('@')[0];
  },

  getReleaseVersion: function () {
    var self = this;
    if (! self.isProperRelease()) {
      throw new Error("not a proper release?");
    }
    return self.name.split('@')[1];
  },

  // Return the package name for the command-line tools that this release
  // uses. Valid only for proper releases.
  getToolsPackage: function () {
    var self = this;

    if (! self.isProperRelease()) {
      throw new Error("not a proper release?");
    }
    // XXX validate
    return self._manifest.tool.split('@')[0];
  },

  // Return the version of the command-line tools that this release
  // uses. Valid only for proper releases.
  getToolsVersion: function () {
    var self = this;

    if (! self.isProperRelease()) {
      throw new Error("not a proper release?");
    }
    // XXX validate
    return self._manifest.tool.split('@')[1];
  },

  // Return the package name and version of the command-line tools that this
  // release uses. Valid only for proper releases.
  getToolsPackageAtVersion: function () {
    var self = this;

    if (! self.isProperRelease()) {
      throw new Error("not a proper release?");
    }
    return self._manifest.tool;
  },

  // Return the tool that we are using. If this is a proper release, return the
  // tool package listed in the manifest, otherwise return the version of the
  // meteor-tool package in checkout.
  getCurrentToolsVersion: function () {
    var self = this;

    if (release.current.name) {
      return self._manifest.tool;
    } else {
      return "meteor-tool@CHECKOUT";
    }
  },

  // Return a list of the upgraders (project migrations) for this
  // release, an (ordered!) array of strings. Valid only for proper
  // releases.
  getUpgraders: function () {
    var self = this;

    if (! self.isProperRelease()) {
      throw new Error("not a proper release?");
    }
    return self._manifest.upgraders || [];
  },

  getPackages: function () {
    var self = this;

    if (! self.isProperRelease()) {
      throw new Error("not a proper release?");
    }
    return self._manifest.packages;
  },

  getCatalogReleaseData: function () {
    var self = this;
    if (! self.isProperRelease()) {
      throw new Error("not a proper release?");
    }
    return self._manifest;
  },

  getDisplayName: function (options) {
    var self = this;
    const catalogUtils = require('./catalog/catalog-utils.js');
    return catalogUtils.displayRelease(
      self.getReleaseTrack(),
      self.getReleaseVersion(),
      options);
  }
});

// The current release. Once set, this does not change for the
// lifetime of the process.
//
// It is possible that we don't have a release. Currently this only
// comes up in one case: an app was created with a checkout version of
// Meteor, and then run with a release version of Meteor. In this case
// release.current will be null. (It will also be null during startup,
// until setRelease has been called.)
//
// (If you want to change the current release, you have to
// springboard, the same as if you want to change the current tools
// version. Besides being simpler to reason about, this helps to
// prepare us for a future where the 'meteor' tool itself is a Meteor
// app, running against a particular Meteor release.)
release.current = null;

// True if we are using release.current because we were forced to do that by the
// '--release' command line option or via throwing SpringboardToLatestRelease,
// else false. (It is true anytime --release was passed, even if it's the same
// release we would have used anyway. It is false anytime the current release is
// a checkin.) null if release.current is null.
release.forced = null;

// True if the release was explicitly specified by the user with the --release
// flag. Unlike release.forced, this is false when the release is overridden via
// SpringboardToLatestRelease.
release.explicit = null;

// True if release.current is the release we'd use if we wanted to run the app
// in the current project. (taking into account release.forced and whether we're
// currently running from a checkout).
release.usingRightReleaseForApp = function (projectContext) {
  if (release.current === null) {
    throw new Error("no release?");
  }

  if (! files.usesWarehouse() || release.forced) {
    return true;
  }

  return release.current.name === projectContext.releaseFile.fullReleaseName;
};

// Return the name of the latest release that is downloaded and ready
// for use. May not be called when running from a checkout.
// 'track' is optional (it defaults to the default track).
release.latestKnown = async function (track) {
  if (! files.usesWarehouse()) {
    throw new Error("called from checkout?");
  }
  // For self-test only.
  if (process.env.METEOR_TEST_LATEST_RELEASE) {
    return process.env.METEOR_TEST_LATEST_RELEASE;
  }


  var defaultRelease = await catalog.official.getDefaultReleaseVersion(track);

  if (!defaultRelease) {
    return null;
  }
  return defaultRelease.track + '@' + defaultRelease.version;
};

// Load a release and return it as a Release object without setting
// release.current to that release. Unlike release.setCurrent(), this
// may be called as many times as you like.
//
// This will fetch the release from the server if it isn't cached
// locally. If that happens it will print progress messages.
//
// Arguments:
// - name: release name to use. Or pass 'null' to use a checkout
// - options:
//   - quiet: if the release has to be downloaded, don't print
//     progress messages.
//
// Throws:
// - files.OfflineError if it was not possible to load the
//   release because it's not locally cached and we're not online.
// - warehouse.NoSuchReleaseError if no release called 'name' exists
//   in the world (confirmed with server).
release.load = async function (name, options) {
  options = options || {};

  if (! name) {
    return new Release({ name: null });
  }

  var parts = name.split('@');
  if (parts.length > 2) {
    // XXX #Pre090 better error handling
    throw Error("too many @s in release name");
  }
  var track, version;
  if (parts.length == 2) {
    track = parts[0];
    version = parts[1];
  } else {
    track = catalog.DEFAULT_TRACK;
    version = parts[0];
    name = track + '@' + version;
  }

  var releaseVersion = await catalog.official.getReleaseVersion(track, version);
  if (releaseVersion === null) {
    throw new release.NoSuchReleaseError;
  }

  return new Release({
    name: name,
    manifest: releaseVersion  // XXX rename from manifest?
  });
};

// Called by the startup code to set release.current. May only be
// called once.
//
// - releaseObject: a Release as returned from release.load()
// - forced: true if the chosen release was forced from the command
//   line (by the user or by the update springboard).
// - explicit: true if the release was specifically requested by the user.
release.setCurrent = function (releaseObject, forced, explicit) {
  if (release.current) {
    throw new Error("release set twice?");
  }

  release.current = releaseObject;
  release.forced = !! forced;
  release.explicit = !! explicit;
};

// An exception meaning that you asked for a release that doesn't exist in the
// new packaging world.  (It may still exist in the pre-0.9.0 packaging world.)
release.NoSuchReleaseError = function () {
};
