"use strict";
var __assign = (this && this.__assign) || function () {
    __assign = Object.assign || function(t) {
        for (var s, i = 1, n = arguments.length; i < n; i++) {
            s = arguments[i];
            for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
                t[p] = s[p];
        }
        return t;
    };
    return __assign.apply(this, arguments);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.requirePackage = exports.solicitEdition = exports.determineEdition = exports.isCompatibleEdition = exports.isCompatibleEngines = exports.isCompatibleVersion = exports.isValidEdition = exports.loadEdition = void 0;
// Imports
var path_1 = require("path");
var version_range_1 = __importDefault(require("version-range"));
var util_js_1 = require("./util.js");
var process_1 = require("process");
var fs_1 = require("fs");
/**
 * Load the {@link Edition} with the loader.
 * @returns The result of the loaded edition.
 * @throws If failed to load, an error is thrown with the reason.
 */
function loadEdition(edition, opts) {
    var entry = (0, path_1.resolve)(opts.cwd || '', edition.directory, opts.entry || edition.entry || '');
    if (opts.loader == null) {
        throw (0, util_js_1.errtion)({
            message: "Could not load the edition [".concat(edition.description, "] as no loader was specified. This is probably due to a testing misconfiguration."),
            code: 'editions-autoloader-loader-missing',
            level: 'fatal',
        });
    }
    try {
        return opts.loader.call(edition, entry);
    }
    catch (loadError) {
        // Note the error with more details
        throw (0, util_js_1.errtion)({
            message: "Failed to load the entry [".concat(entry, "] of edition [").concat(edition.description, "]."),
            code: 'editions-autoloader-loader-failed',
            level: 'fatal',
        }, loadError);
    }
}
exports.loadEdition = loadEdition;
/**
 * Verify the {@link Edition} has all the required properties.
 * @returns if valid
 * @throws if invalid
 */
function isValidEdition(edition) {
    if (!edition.description ||
        !edition.directory ||
        !edition.entry ||
        edition.engines == null) {
        throw (0, util_js_1.errtion)({
            message: "An edition must have its [description, directory, entry, engines] fields defined, yet all this edition defined were [".concat(Object.keys(edition).join(', '), "]"),
            code: 'editions-autoloader-invalid-edition',
            level: 'fatal',
        });
    }
    // valid
    return true;
}
exports.isValidEdition = isValidEdition;
/**
 * Is this {@link Edition} suitable for these versions?
 * @returns if compatible
 * @throws if incompatible
 */
function isCompatibleVersion(range, version, opts) {
    // prepare
    var broadenRange = opts.broadenRange;
    if (!version)
        throw (0, util_js_1.errtion)({
            message: "No version was specified to compare the range [".concat(range, "] against"),
            code: 'editions-autoloader-engine-version-missing',
            level: 'fatal',
        });
    if (range == null || range === '')
        throw (0, util_js_1.errtion)({
            message: "The edition range was not specified, so unable to compare against the version [".concat(version, "]"),
            code: 'editions-autoloader-engine-range-missing',
        });
    if (range === false)
        throw (0, util_js_1.errtion)({
            message: "The edition range does not support this engine",
            code: 'editions-autoloader-engine-unsupported',
        });
    if (range === true)
        return true;
    // original range
    try {
        if ((0, version_range_1.default)(version, range))
            return true;
    }
    catch (error) {
        throw (0, util_js_1.errtion)({
            message: "The range [".concat(range, "] was invalid, something is wrong with the Editions definition."),
            code: 'editions-autoloader-invalid-range',
            level: 'fatal',
        }, error);
    }
    // broadened range
    // https://github.com/bevry/editions/blob/master/HISTORY.md#v210-2018-november-15
    // If none of the editions for a package match the current node version, editions will try to find a compatible package by converting strict version ranges likes 4 || 6 || 8 || 10 to looser ones like >=4, and if that fails, then it will attempt to load the last edition for the environment.
    // This brings editions handling of engines closer in line with how node handles it, which is as a warning/recommendation, rather than a requirement/enforcement.
    // This has the benefit that edition authors can specify ranges as the specific versions that they have tested the edition against that pass, rather than having to omit that information for runtime compatibility.
    // As such editions will now automatically select the edition with guaranteed support for the environment, and if there are none with guaranteed support, then editions will select the one is most likely supported, and if there are none that are likely supported, then it will try the last edition, which should be the most compatible edition.
    // This is timely, as node v11 is now the version most developers use, yet if edition authors specified only LTS releases, then the editions autoloader would reject loading on v11, despite compatibility being likely with the most upper edition.
    // NOTE: That there is only one broadening chance per package, once a broadened edition has been returned, a load will be attempted, and if it fails, then the package failed. This is intentional.
    if (broadenRange === true) {
        // check if range can be broadened, validate it and extract
        var broadenedRangeRegex = /^\s*([0-9.]+)\s*(\|\|\s*[0-9.]+\s*)*$/;
        var broadenedRangeMatch = range.match(broadenedRangeRegex);
        var lowestVersion = (broadenedRangeMatch && broadenedRangeMatch[1]) || '';
        // ^ can't do number conversion, as 1.1.1 is not a number
        // this also converts 0 to '' which is what we want for the next check
        // confirm the validation
        if (lowestVersion === '')
            throw (0, util_js_1.errtion)({
                message: "The range [".concat(range, "] is not able to be broadened, only ranges in format of [lowest] or [lowest || ... || ... ] can be broadened. Update the Editions definition and try again."),
                code: 'editions-autoloader-unsupported-broadened-range',
                level: 'fatal',
            });
        // create the broadened range, and attempt that
        var broadenedRange = ">= ".concat(lowestVersion);
        try {
            if ((0, version_range_1.default)(version, broadenedRange))
                return true;
        }
        catch (error) {
            throw (0, util_js_1.errtion)({
                message: "The broadened range [".concat(broadenedRange, "] was invalid, something is wrong within Editions."),
                code: 'editions-autoloader-invalid-broadened-range',
                level: 'fatal',
            }, error);
        }
        // broadened range was incompatible
        throw (0, util_js_1.errtion)({
            message: "The edition range [".concat(range, "] does not support this engine version [").concat(version, "], even when broadened to [").concat(broadenedRange, "]"),
            code: 'editions-autoloader-engine-incompatible-broadened-range',
        });
    }
    // give up
    throw (0, util_js_1.errtion)({
        message: "The edition range [".concat(range, "] does not support this engine version [").concat(version, "]"),
        code: 'editions-autoloader-engine-incompatible-original',
    });
}
exports.isCompatibleVersion = isCompatibleVersion;
/**
 * Checks that the provided engines are compatible against the provided versions.
 * @returns if compatible
 * @throws if incompatible
 */
function isCompatibleEngines(engines, opts) {
    // PRepare
    var versions = opts.versions;
    // Check engines exist
    if (!engines) {
        throw (0, util_js_1.errtion)({
            message: "The edition had no engines to compare against the environment",
            code: 'editions-autoloader-invalid-engines',
        });
    }
    // Check versions exist
    if (!versions) {
        throw (0, util_js_1.errtion)({
            message: "No versions were supplied to compare the engines against",
            code: 'editions-autoloader-invalid-versions',
            level: 'fatal',
        });
    }
    // Check each version
    var compatible = false;
    for (var key in engines) {
        if (engines.hasOwnProperty(key)) {
            // deno's std/node/process provides both `deno` and `node` keys
            // so we don't won't to compare node when it is actually deno
            if (key === 'node' && versions.deno)
                continue;
            // prepare
            var engine = engines[key];
            var version = versions[key];
            // skip for engines this edition does not care about
            if (version == null)
                continue;
            // check compatibility against all the provided engines it does care about
            try {
                isCompatibleVersion(engine, version, opts);
                compatible = true;
                // if any incompatibility, it is thrown, so no need to set to false
            }
            catch (rangeError) {
                throw (0, util_js_1.errtion)({
                    message: "The engine [".concat(key, "] range of [").concat(engine, "] was not compatible against version [").concat(version, "]."),
                    code: 'editions-autoloader-engine-error',
                }, rangeError);
            }
        }
    }
    // if there were no matching engines, then throw
    if (!compatible) {
        throw (0, util_js_1.errtion)({
            message: "There were no supported engines in which this environment provides.",
            code: 'editions-autoloader-engine-mismatch',
        });
    }
    // valid
    return true;
}
exports.isCompatibleEngines = isCompatibleEngines;
/**
 * Checks that the {@link Edition} is compatible against the provided versions.
 * @returns if compatible
 * @throws if incompatible
 */
function isCompatibleEdition(edition, opts) {
    try {
        return isCompatibleEngines(edition.engines, opts);
    }
    catch (compatibleError) {
        throw (0, util_js_1.errtion)({
            message: "The edition [".concat(edition.description, "] is not compatible with this environment."),
            code: 'editions-autoloader-edition-incompatible',
        }, compatibleError);
    }
}
exports.isCompatibleEdition = isCompatibleEdition;
/**
 * Determine which edition should be loaded.
 * If {@link VersionOptions.broadenRange} is unspecified (the default behavior), then we attempt to determine a suitable edition without broadening the range, and if that fails, then we try again with the range broadened.
 * @returns any suitable editions
 * @throws if no suitable editions
 */
function determineEdition(editions, opts) {
    // Prepare
    var broadenRange = opts.broadenRange;
    // Check
    if (!editions || editions.length === 0) {
        throw (0, util_js_1.errtion)({
            message: 'No editions were specified.',
            code: 'editions-autoloader-editions-missing',
        });
    }
    // Cycle through the editions determining the above
    var failure = null;
    for (var i = 0; i < editions.length; ++i) {
        var edition = editions[i];
        try {
            isValidEdition(edition);
            isCompatibleEdition(edition, opts);
            // Success! Return the edition
            return edition;
        }
        catch (error) {
            if (error.level === 'fatal') {
                throw (0, util_js_1.errtion)({
                    message: "Unable to determine a suitable edition due to failure.",
                    code: 'editions-autoloader-fatal',
                    level: 'fatal',
                }, error);
            }
            else if (failure) {
                failure = (0, util_js_1.errtion)(error, failure);
            }
            else {
                failure = error;
            }
        }
    }
    // Report the failure from above
    if (failure) {
        // try broadened
        if (broadenRange == null)
            try {
                // return if broadening successfully returned an edition
                var broadenedEdition = determineEdition(editions, __assign(__assign({}, opts), { broadenRange: true }));
                return __assign(__assign({}, broadenedEdition), { 
                    // bubble the circumstances up in case the loading of the broadened edition fails and needs to be reported
                    debugging: (0, util_js_1.errtion)({
                        message: "The edition ".concat(broadenedEdition.description, " was selected to be force loaded as its range was broadened."),
                        code: 'editions-autoloader-attempt-broadened',
                    }) });
            }
            catch (error) {
                throw (0, util_js_1.errtion)({
                    message: "Unable to determine a suitable edition, even after broadening.",
                    code: 'editions-autoloader-none-broadened',
                }, error);
            }
        // fail
        throw (0, util_js_1.errtion)({
            message: "Unable to determine a suitable edition, as none were suitable.",
            code: 'editions-autoloader-none-suitable',
        }, failure);
    }
    // this should never reach here
    throw (0, util_js_1.errtion)({
        message: "Unable to determine a suitable edition, as an unexpected pathway occurred.",
        code: 'editions-autoloader-never',
    });
}
exports.determineEdition = determineEdition;
/**
 * Determine which edition should be loaded, and attempt to load it.
 * @returns the loaded result of the suitable edition
 * @throws if no suitable editions, or the edition failed to load
 */
function solicitEdition(editions, opts) {
    var edition = determineEdition(editions, opts);
    try {
        return loadEdition(edition, opts);
    }
    catch (error) {
        throw (0, util_js_1.errtion)(error, edition.debugging);
    }
}
exports.solicitEdition = solicitEdition;
/**
 * Cycle through the editions for a package, determine the compatible edition, and load it.
 * @returns the loaded result of the suitable edition
 * @throws if no suitable editions, or if the edition failed to load
 */
function requirePackage(cwd, loader, entry) {
    var packagePath = (0, path_1.resolve)(cwd || '', 'package.json');
    try {
        // load editions
        var editions = JSON.parse((0, fs_1.readFileSync)(packagePath, 'utf8')).editions;
        // load edition
        return solicitEdition(editions, {
            versions: process_1.versions,
            cwd: cwd,
            loader: loader,
            entry: entry,
        });
    }
    catch (error) {
        throw (0, util_js_1.errtion)({
            message: "Unable to determine a suitable edition for the package [".concat(packagePath, "] and entry [").concat(entry, "]"),
            code: 'editions-autoloader-package',
        }, error);
    }
}
exports.requirePackage = requirePackage;
