var _ = require('underscore');
var packageVersionParser = require('./package-version-parser.js');
var utils = require('../utils/utils.js');
import { isIsobuildFeaturePackage } from '../isobuild/compiler.js';

// PackageMap: Represents the choices of package versions being used for a
// project. It knows all the packages that are used (direct and indirect
// dependencies), their versions, whether they are local or versioned packages,
// and the PackageSource object for any local packages.  Prefer using this
// function over arbitrary JSON representations when possible.  (A related class
// is projectContextModule.PackageMapFile which specifically represents the
// .meteor/packages file on disk.)
//
// It has a corresponding JSON format (used, eg, inside buildinfo files).
//
// If you specify the localCatalog option to the constructor, any package in
// that localCatalog will be considered to be local, and all others will be
// considered to be prebuilt versioned packages from troposphere.  If you do not
// specify the localCatalog option, all packages will be considered to prebuilt
// versioned packages.
exports.PackageMap = function (versions, options) {
  var self = this;
  options = options || {};
  self._map = {};
  self._localCatalog = options.localCatalog || null;

  _.each(versions, function (version, packageName) {
    // If the constraint solver told us that we needed an isobuild feature,
    // that's fine, but it's not a real package.
    if (isIsobuildFeaturePackage(packageName)) {
      return;
    }

    var packageSource = self._localCatalog &&
          self._localCatalog.getPackageSource(packageName);
    if (packageSource) {
      self._map[packageName] =
        { kind: 'local', version: version, packageSource: packageSource };
    } else {
      self._map[packageName] =
        { kind: 'versioned', version: version, packageSource: null };
    }
  });
};

Object.assign(exports.PackageMap.prototype, {
  eachPackage: async function (iterator) {
    var self = this;

    for (const [packageName, info] of Object.entries(self._map)) {
      // For reasons that are super unclear, if this `_.clone` is inlined into
      // the `iterator` call, the value produced can mysteriously turn into
      // undefined on the way into `iterator`. Presumably some sort of memory
      // corruption, maybe Fiber-related?  Trying to minimize has been an
      // exercise in nondeterminism. But this does seem to be a sure-fire way to
      // fix it, for now. Who knows why, and who knows when it will recur again.
      var infoClone = _.clone(info);
      await iterator(packageName, infoClone);
    }
  },
  getInfo: function (packageName) {
    var self = this;
    if (_.has(self._map, packageName)) {
      return self._map[packageName];
    }
    return null;
  },
  makeSubsetMap: function (packageNames) {
    var self = this;
    var subsetVersions = {};
    _.each(packageNames, function (packageName) {
      var info = self.getInfo(packageName);
      if (!info) {
        throw Error("not a subset: " + packageName);
      }
      subsetVersions[packageName] = info.version;
    });
    return new exports.PackageMap(subsetVersions, {
      localCatalog: self._localCatalog
    });
  },

  toJSON: function () {
    var self = this;
    var ret = {};
    _.each(self._map, function (info, packageName) {
      if (info.kind === 'local') {
        ret[packageName] = {
          kind: 'local',
          sourceRoot: info.packageSource.sourceRoot
        };
      } else {
        ret[packageName] = {
          kind: 'versioned',
          version: info.version
        };
      }
    });
    return ret;
  },
  // Returns a map from package name to version. In most cases, this is a far
  // worse representation than PackageMap... avoid using it!
  toVersionMap: function () {
    var self = this;
    var ret = {};
    _.each(self._map, function (info, packageName) {
      ret[packageName] = info.version;
    });
    return ret;
  },

  // Given some JSON as returned from toJSON, returns true if every package in
  // the JSON has the same mapping as in this map.
  isSupersetOfJSON: function (mapJSON) {
    var self = this;
    return _.all(mapJSON, function (jsonInfo, packageName) {
      var thisInfo = self.getInfo(packageName);
      if (! thisInfo) {
        return false;
      }
      if (jsonInfo.kind !== thisInfo.kind) {
        return false;
      }
      if (thisInfo.kind === 'local') {
        return thisInfo.packageSource.sourceRoot === jsonInfo.sourceRoot;
      } else {
        return thisInfo.version === jsonInfo.version;
      }
    });
  }
});

// Static method: returns a PackageMap that represents a (catalog)
// ReleaseVersion entry (including its tool).  Note that this function assumes
// that all packages will be prebuilt versioned, not local. This is mostly used
// to create PackageMaps to pass to tropohouse.downloadPackagesMissingFromMap;
// it should not be used as part of a ProjectContext because it does not allow
// you to override release packages with local packages.
exports.PackageMap.fromReleaseVersion = function (releaseVersion) {
  var toolPackageVersion = releaseVersion.tool &&
        utils.parsePackageAndVersion(releaseVersion.tool);
  if (!toolPackageVersion) {
    throw new Error("bad tool in release: " + releaseVersion.tool);
  }
  var toolPackage = toolPackageVersion.package;
  var toolVersion = toolPackageVersion.version;

  var versionMap = _.clone(releaseVersion.packages || {});
  versionMap[toolPackage] = toolVersion;

  // As described in this function's description, all packages in this map are
  // versioned, so we do not specify a localCatalog.
  return new exports.PackageMap(versionMap);
};



// PackageMapDelta: represents the change in a PackageMap between two constraint
// solver runs.
exports.PackageMapDelta = function (options) {
  var self = this;
  self._changedPackages = {};
  self.options = options;
};

Object.assign(exports.PackageMapDelta.prototype, {
  init: async function() {
    const self = this;

    await self.options.packageMap.eachPackage(function (packageName, info) {
      var oldVersion = _.has(self.options.cachedVersions, packageName)
          ? self.options.cachedVersions[packageName] : null;
      self._storeAddOrChange(
          packageName, info, oldVersion, self.options.anticipatedPrereleases,
          self.options.neededToUseUnanticipatedPrereleases);
    });

    _.each(self.options.cachedVersions, function (oldVersion, packageName) {
      if (! self.options.packageMap.getInfo(packageName)) {
        self._storeRemove(packageName, oldVersion);
      }
    });
  },
  _storeAddOrChange: function (packageName, newInfo, oldVersion,
                               anticipatedPrereleases,
                               neededToUseUnanticipatedPrereleases) {
    var self = this;

    // Store nothing if nothing has changed.
    if (newInfo.version === oldVersion) {
      return;
    }

    var backwardsIncompatible =
          oldVersion !== null &&
          (packageVersionParser.majorVersion(newInfo.version) !==
           packageVersionParser.majorVersion(oldVersion));

    var isPrerelease = /-/.test(newInfo.version);
    var isAnticipatedPrerelease = _.has(anticipatedPrereleases, packageName) &&
          _.has(anticipatedPrereleases[packageName], newInfo.version);
    self._changedPackages[packageName] = {
      oldVersion: oldVersion,
      newVersion: newInfo.version,
      isBackwardsIncompatible: backwardsIncompatible,
      isUnanticipatedPrerelease: (neededToUseUnanticipatedPrereleases &&
                                  isPrerelease && !isAnticipatedPrerelease)
    };
  },

  _storeRemove: function (packageName, oldVersion) {
    var self = this;
    self._changedPackages[packageName] = {
      oldVersion: oldVersion,
      newVersion: null
    };
  },

  eachChangedPackage: function (iterator) {
    var self = this;
    _.each(self._changedPackages, function (info, packageName) {
      iterator(packageName, _.clone(info));
    });
  },

  hasChanges: function () {
    var self = this;
    return ! _.isEmpty(self._changedPackages);
  },

  displayOnConsole: function (options) {
    var self = this;
    options = Object.assign({
      title: "Changes to your project's package version selections:"
    }, options);

    // Print nothing at all if nothing changed.
    if (! self.hasChanges()) {
      return;
    }

    var displayItems = [];
    var anyBackwardsIncompatible = false;
    var anyUnanticipatedPrerelease = false;
    self.eachChangedPackage(function (packageName, info) {
      if (info.newVersion === null) {
        displayItems.push({
          name: packageName,
          description: "removed from your project"
        });
        return;
      }

      var name = packageName;
      if (info.isBackwardsIncompatible) {
        name += '*';
        anyBackwardsIncompatible = true;
      }
      if (info.isUnanticipatedPrerelease) {
        name += '+';
        anyUnanticipatedPrerelease = true;
      }

      var description;
      if (info.oldVersion === null) {
        description = "added, version " + info.newVersion;
      } else if (packageVersionParser.lessThan(info.oldVersion,
                                               info.newVersion)) {
        description =
          "upgraded from " + info.oldVersion + " to " + info.newVersion;
      } else {
        description =
          "downgraded from " + info.oldVersion + " to " + info.newVersion;
      }

      if (info.deprecated) {
        name += ' - DEPRECATED';
        if (info.deprecatedMessage) {
          description += ' - ' + info.deprecatedMessage;
        }
      }

      displayItems.push({ name: name, description: description });
    });

    var Console = require('../console/console.js').Console;

    Console.info();
    Console.info(options.title);
    Console.info();
    utils.printPackageList(displayItems);
    if (anyBackwardsIncompatible) {
      Console.info("\n" +
"* These packages have been updated to new versions that are not backwards\n" +
"  compatible.");
    }
    if (anyUnanticipatedPrerelease) {
      Console.info("\n" +
"+ In order to resolve constraints, we had to use experimental versions of these\n" +
"  packages.");
    }
  }
});
