"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments)).next());
    });
};
const path = require("path");
const npa = require("npm-package-arg");
const pnpm_logger_1 = require("pnpm-logger");
const fetch_1 = require("./fetch");
const linkBins_1 = require("./linkBins");
const memoize_1 = require("../memoize");
const hardlinkDir_1 = require("../fs/hardlinkDir");
const mkdirp_1 = require("../fs/mkdirp");
const installChecks = require("pnpm-install-checks");
const pnpmPkgJson_1 = require("../pnpmPkgJson");
const link_dir_1 = require("link-dir");
const exists = require("exists-file");
const logInstallStatus_1 = require("../logging/logInstallStatus");
const installCheckLogger = pnpm_logger_1.default('install-check');
function installAll(ctx, dependencies, optionalDependencies, modules, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const nonOptionalDependencies = Object.keys(dependencies)
            .filter(depName => !optionalDependencies[depName])
            .reduce((nonOptionalDependencies, depName) => {
            nonOptionalDependencies[depName] = dependencies[depName];
            return nonOptionalDependencies;
        }, {});
        const installedPkgs = Array.prototype.concat.apply([], yield Promise.all([
            installMultiple(ctx, nonOptionalDependencies, modules, Object.assign({}, options, { optional: false })),
            installMultiple(ctx, optionalDependencies, modules, Object.assign({}, options, { optional: true })),
        ]));
        if (options.fetchingFiles) {
            yield options.fetchingFiles;
        }
        yield mkdirp_1.default(modules);
        yield Promise.all(installedPkgs
            .filter(subdep => !subdep.fromCache)
            .map(function (subdep) {
            return __awaiter(this, void 0, void 0, function* () {
                if (ctx.installationSequence.indexOf(subdep.id) === -1) {
                    ctx.installationSequence.push(subdep.id);
                }
                const dest = path.join(modules, subdep.pkg.name);
                yield link_dir_1.default(subdep.hardlinkedLocation, dest);
            });
        }));
        yield linkBins_1.default(modules, options.preserveSymlinks);
        return installedPkgs;
    });
}
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = installAll;
function installMultiple(ctx, pkgsMap, modules, options) {
    return __awaiter(this, void 0, void 0, function* () {
        pkgsMap = pkgsMap || {};
        const pkgs = Object.keys(pkgsMap).map(pkgName => getRawSpec(pkgName, pkgsMap[pkgName]));
        ctx.graph = ctx.graph || {};
        const installedPkgs = (yield Promise.all(pkgs.map(function (pkgRawSpec) {
            return __awaiter(this, void 0, void 0, function* () {
                let pkg;
                try {
                    pkg = yield install(pkgRawSpec, modules, ctx, options);
                    if (options.keypath && options.keypath.indexOf(pkg.id) !== -1) {
                        return null;
                    }
                    return pkg;
                }
                catch (err) {
                    if (options.optional) {
                        pnpm_logger_1.default.warn({
                            message: `Skipping failed optional dependency ${pkg && pkg.id || pkgRawSpec}`,
                            err,
                        });
                        return null; // is it OK to return null?
                    }
                    throw err;
                }
            });
        })))
            .filter(pkg => pkg);
        return installedPkgs;
    });
}
function install(pkgRawSpec, modules, ctx, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const keypath = options.keypath || [];
        const update = keypath.length <= options.depth;
        // Preliminary spec data
        // => { raw, name, scope, type, spec, rawSpec }
        const spec = npa(pkgRawSpec);
        const fetchedPkg = yield fetch_1.default(ctx, spec, modules, Object.assign({}, options, { update }));
        logFetchStatus(spec.rawSpec, fetchedPkg);
        const pkg = yield fetchedPkg.fetchingPkg;
        if (!options.force) {
            yield isInstallable(pkg, fetchedPkg, options);
        }
        const dependency = Object.assign({}, fetchedPkg, {
            keypath,
            dependencies: [],
            optional: options.optional === true,
            pkg,
            // TODO: what about bundled/cached deps?
            // The package realpath should be under node_modules/<pkgname>. This way, babel-runtime@5 can		
            // require('babel-runtime') within itself.
            hardlinkedLocation: path.join(options.nodeModulesStore, fetchedPkg.id, 'node_modules', pkg.name),
        });
        if (dependency.fromCache || keypath.indexOf(dependency.id) !== -1) {
            return dependency;
        }
        addInstalledPkg(ctx.installs, dependency);
        // NOTE: the current install implementation
        // does not return enough info for packages that were already installed
        addToGraph(ctx.graph, options.dependent, dependency);
        const modulesInStore = path.join(dependency.hardlinkedLocation, 'node_modules');
        if (!ctx.installed.has(dependency.id)) {
            ctx.installed.add(dependency.id);
            dependency.dependencies = yield installDependencies(pkg, dependency, ctx, modulesInStore, options);
        }
        yield dependency.fetchingFiles;
        yield memoize_1.default(ctx.resolutionLinked, dependency.hardlinkedLocation, function () {
            return __awaiter(this, void 0, void 0, function* () {
                if (!(yield exists(path.join(dependency.hardlinkedLocation, 'package.json')))) {
                    yield hardlinkDir_1.default(dependency.path, dependency.hardlinkedLocation);
                }
            });
        });
        return dependency;
    });
}
function logFetchStatus(pkgRawSpec, fetchedPkg) {
    return __awaiter(this, void 0, void 0, function* () {
        const pkg = yield fetchedPkg.fetchingPkg;
        yield fetchedPkg.fetchingFiles;
        logInstallStatus_1.default({ status: 'done', pkg: { rawSpec: pkgRawSpec, name: pkg.name, version: pkg.version } });
    });
}
function addToGraph(graph, dependent, dependency) {
    graph[dependent] = graph[dependent] || {};
    graph[dependent].dependencies = graph[dependent].dependencies || {};
    updateDependencyResolution(graph, dependent, dependency.pkg.name, dependency.id);
    graph[dependency.id] = graph[dependency.id] || {};
    graph[dependency.id].dependents = graph[dependency.id].dependents || [];
    if (graph[dependency.id].dependents.indexOf(dependent) === -1) {
        graph[dependency.id].dependents.push(dependent);
    }
}
function updateDependencyResolution(graph, dependent, depName, newDepId) {
    if (graph[dependent].dependencies[depName] &&
        graph[dependent].dependencies[depName] !== newDepId) {
        removeIfNoDependents(graph, graph[dependent].dependencies[depName], dependent);
    }
    graph[dependent].dependencies[depName] = newDepId;
}
function removeIfNoDependents(graph, id, removedDependent) {
    if (graph[id] && graph[id].dependents && graph[id].dependents.length === 1 &&
        graph[id].dependents[0] === removedDependent) {
        Object.keys(graph[id].dependencies || {}).forEach(depName => removeIfNoDependents(graph, graph[id].dependencies[depName], id));
        delete graph[id];
    }
}
function isInstallable(pkg, fetchedPkg, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const warn = (yield installChecks.checkPlatform(pkg)) || (yield installChecks.checkEngine(pkg, {
            pnpmVersion: pnpmPkgJson_1.default.version,
            nodeVersion: options.nodeVersion
        }));
        if (!warn)
            return;
        installCheckLogger.warn(warn);
        if (options.engineStrict || options.optional) {
            yield fetchedPkg.abort();
            throw warn;
        }
    });
}
function installDependencies(pkg, dependency, ctx, modules, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        const depsInstallOpts = Object.assign({}, opts, {
            keypath: (opts.keypath || []).concat([dependency.id]),
            dependent: dependency.id,
            root: dependency.srcPath,
            fetchingFiles: dependency.fetchingFiles,
        });
        const bundledDeps = pkg.bundleDependencies || pkg.bundleDependencies || [];
        const filterDeps = getNotBundledDeps.bind(null, bundledDeps);
        const deps = filterDeps(pkg.dependencies || {});
        const optionalDeps = filterDeps(pkg.optionalDependencies || {});
        const installedDeps = yield installAll(ctx, deps, optionalDeps, modules, depsInstallOpts);
        return installedDeps;
    });
}
function getNotBundledDeps(bundledDeps, deps) {
    return Object.keys(deps)
        .filter(depName => bundledDeps.indexOf(depName) === -1)
        .reduce((notBundledDeps, depName) => {
        notBundledDeps[depName] = deps[depName];
        return notBundledDeps;
    }, {});
}
function addInstalledPkg(installs, newPkg) {
    if (!installs[newPkg.id]) {
        installs[newPkg.id] = newPkg;
        return;
    }
    installs[newPkg.id].optional = installs[newPkg.id].optional && newPkg.optional;
}
function getRawSpec(name, version) {
    return version === '*' ? name : `${name}@${version}`;
}
//# sourceMappingURL=installMultiple.js.map