"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const lockfile_file_1 = require("@pnpm/lockfile-file");
const lockfile_utils_1 = require("@pnpm/lockfile-utils");
const modules_yaml_1 = require("@pnpm/modules-yaml");
const read_modules_dir_1 = require("@pnpm/read-modules-dir");
const types_1 = require("@pnpm/types");
const utils_1 = require("@pnpm/utils");
const assert = require("assert");
const dependency_path_1 = require("dependency-path");
const normalizePath = require("normalize-path");
const path = require("path");
const resolveLinkTarget = require("resolve-link-target");
const semver = require("semver");
function forPackages(packages, projectPath, opts) {
    assert(packages, 'packages should be defined');
    if (!packages.length)
        return {};
    return dependenciesHierarchy(projectPath, packages, opts);
}
exports.forPackages = forPackages;
function default_1(projectPath, opts) {
    return dependenciesHierarchy(projectPath, [], opts);
}
exports.default = default_1;
async function dependenciesHierarchy(projectPath, searched, maybeOpts) {
    const modules = await modules_yaml_1.read(projectPath);
    const registries = utils_1.normalizeRegistries(Object.assign({}, maybeOpts && maybeOpts.registries, modules && modules.registries));
    const lockfileDirectory = maybeOpts && maybeOpts.lockfileDirectory || projectPath;
    const lockfile = await lockfile_file_1.readCurrentLockfile(lockfileDirectory, { ignoreIncompatible: false });
    if (!lockfile)
        return {};
    const opts = Object.assign({ depth: 0 }, maybeOpts);
    const include = maybeOpts && maybeOpts.include || {
        dependencies: true,
        devDependencies: true,
        optionalDependencies: true,
    };
    const importerId = lockfile_file_1.getLockfileImporterId(lockfileDirectory, projectPath);
    if (!lockfile.importers[importerId])
        return {};
    const modulesDir = path.join(projectPath, 'node_modules');
    const savedDeps = getAllDirectDependencies(lockfile.importers[importerId]);
    const allDirectDeps = await read_modules_dir_1.default(modulesDir) || [];
    const unsavedDeps = allDirectDeps.filter((directDep) => !savedDeps[directDep]);
    const getChildrenTree = getTree.bind(null, {
        currentDepth: 1,
        includeOptionalDependencies: include.optionalDependencies === true,
        maxDepth: opts.depth,
        modulesDir,
        registries,
        searched,
    }, lockfile.packages);
    const result = {};
    for (const dependenciesField of types_1.DEPENDENCIES_FIELDS.sort().filter(dependenciedField => include[dependenciedField])) {
        const topDeps = lockfile.importers[importerId][dependenciesField] || {};
        result[dependenciesField] = [];
        Object.keys(topDeps).forEach((alias) => {
            const { packageInfo, packageAbsolutePath } = getPkgInfo({
                alias,
                modulesDir,
                packages: lockfile.packages || {},
                ref: topDeps[alias],
                registries,
            });
            let newEntry = null;
            const matchedSearched = searched.length && matches(searched, packageInfo);
            if (packageAbsolutePath === null) {
                if (searched.length && !matchedSearched)
                    return;
                newEntry = packageInfo;
            }
            else {
                const relativeId = dependency_path_1.refToRelative(topDeps[alias], alias);
                const dependencies = getChildrenTree([relativeId], relativeId);
                if (dependencies.length) {
                    newEntry = Object.assign({}, packageInfo, { dependencies });
                }
                else if (!searched.length || matches(searched, packageInfo)) {
                    newEntry = packageInfo;
                }
            }
            if (newEntry) {
                if (matchedSearched) {
                    newEntry.searched = true;
                }
                result[dependenciesField].push(newEntry);
            }
        });
    }
    await Promise.all(unsavedDeps.map(async (unsavedDep) => {
        let pkgPath = path.join(modulesDir, unsavedDep);
        let version;
        try {
            pkgPath = await resolveLinkTarget(pkgPath);
            version = `link:${normalizePath(path.relative(projectPath, pkgPath))}`;
        }
        catch (err) {
            // if error happened. The package is not a link
            const pkg = await utils_1.safeReadPackageFromDir(pkgPath);
            version = pkg && pkg.version || 'undefined';
        }
        const pkg = {
            alias: unsavedDep,
            isPeer: false,
            name: unsavedDep,
            path: pkgPath,
            version,
        };
        const matchedSearched = searched.length && matches(searched, pkg);
        if (searched.length && !matchedSearched)
            return;
        const newEntry = pkg;
        if (matchedSearched) {
            newEntry.searched = true;
        }
        result.unsavedDependencies = result.unsavedDependencies || [];
        result.unsavedDependencies.push(newEntry);
    }));
    return result;
}
function getAllDirectDependencies(lockfileImporter) {
    return Object.assign({}, lockfileImporter.dependencies, lockfileImporter.devDependencies, lockfileImporter.optionalDependencies);
}
function getTree(opts, packages, keypath, parentId) {
    if (opts.currentDepth > opts.maxDepth || !packages || !packages[parentId])
        return [];
    const deps = opts.includeOptionalDependencies === false
        ? packages[parentId].dependencies
        : Object.assign({}, packages[parentId].dependencies, packages[parentId].optionalDependencies);
    if (!deps)
        return [];
    const getChildrenTree = getTree.bind(null, Object.assign({}, opts, { currentDepth: opts.currentDepth + 1 }), packages);
    const peers = new Set(Object.keys(packages[parentId].peerDependencies || {}));
    const result = [];
    Object.keys(deps).forEach((alias) => {
        const { packageInfo, packageAbsolutePath } = getPkgInfo({
            alias,
            modulesDir: opts.modulesDir,
            packages,
            peers,
            ref: deps[alias],
            registries: opts.registries,
        });
        let circular;
        const matchedSearched = opts.searched.length && matches(opts.searched, packageInfo);
        let newEntry = null;
        if (packageAbsolutePath === null) {
            circular = false;
            newEntry = packageInfo;
        }
        else {
            const relativeId = dependency_path_1.refToRelative(deps[alias], alias); // we know for sure that relative is not null if pkgPath is not null
            circular = keypath.includes(relativeId);
            const dependencies = circular ? [] : getChildrenTree(keypath.concat([relativeId]), relativeId);
            if (dependencies.length) {
                newEntry = Object.assign({}, packageInfo, { dependencies });
            }
            else if (!opts.searched.length || matchedSearched) {
                newEntry = packageInfo;
            }
        }
        if (newEntry) {
            if (circular) {
                newEntry.circular = true;
            }
            if (matchedSearched) {
                newEntry.searched = true;
            }
            result.push(newEntry);
        }
    });
    return result;
}
function getPkgInfo(opts) {
    let name;
    let version;
    let resolved = undefined;
    let dev = undefined;
    let optional = undefined;
    const relDepPath = dependency_path_1.refToRelative(opts.ref, opts.alias);
    if (relDepPath) {
        const parsed = lockfile_utils_1.nameVerFromPkgSnapshot(relDepPath, opts.packages[relDepPath]);
        name = parsed.name;
        version = parsed.version;
        resolved = lockfile_utils_1.pkgSnapshotToResolution(relDepPath, opts.packages[relDepPath], opts.registries)['tarball'];
        dev = opts.packages[relDepPath].dev;
        optional = opts.packages[relDepPath].optional;
    }
    else {
        name = opts.alias;
        version = opts.ref;
    }
    const packageAbsolutePath = dependency_path_1.refToAbsolute(opts.ref, opts.alias, opts.registries);
    const packageInfo = {
        alias: opts.alias,
        isPeer: Boolean(opts.peers && opts.peers.has(opts.alias)),
        name,
        path: packageAbsolutePath && path.join(opts.modulesDir, `.${packageAbsolutePath}`) || path.join(opts.modulesDir, '..', opts.ref.substr(5)),
        version,
    };
    if (resolved) {
        packageInfo['resolved'] = resolved;
    }
    if (optional === true) {
        packageInfo['optional'] = true;
    }
    if (typeof dev === 'boolean') {
        packageInfo['dev'] = dev;
    }
    return {
        packageAbsolutePath,
        packageInfo,
    };
}
function matches(searched, pkg) {
    return searched.some((searchedPkg) => {
        if (typeof searchedPkg === 'string') {
            return pkg.name === searchedPkg;
        }
        return searchedPkg.name === pkg.name &&
            !pkg.version.startsWith('link:') &&
            semver.satisfies(pkg.version, searchedPkg.range);
    });
}
