const lockfile = require('@yarnpkg/lockfile')
const semver = require('semver');

const parseYarnLock = (file) => lockfile.parse(file).object;

const extractPackages = (json) => {
    const packages={};
    const re = /^(.*)@([^@]*?)$/;

    Object.keys(json).forEach((name) => {
        console.log(name);
        const pkg = json[name];
        const match = name.match(re);

        let packageName, requestedVersion;
        if (match) {
            ([, packageName, requestedVersion]) = match;
        } else {
            packageName = name;
            requestedVersion = "*";
        }

        const [, packageName, requestedVersion] = name.match(re);
        packages[packageName] = packages[packageName] || [];
        packages[packageName].push({
            pkg,
            requestedVersion,
            satisfiedBy: new Set(),
        });
    });
    return packages;
}

module.exports.listDuplicates = (yarnLock, {
    includePackages=[],
    useMostCommon=false,
}={}) => {
    const json = parseYarnLock(yarnLock);
    const packages = extractPackages(json);
    const result = [];

    Object.keys(packages)
        // Filter out packages not specified in the inclusion list
        .filter((name) => {
            if (includePackages.length === 0) return true;
            return includePackages.includes(name);
        })
        .forEach((name) => {
            const packageInstances = packages[name];

            // Create a map with all versions installed for this package
            const versions = packageInstances.reduce((versions, packageInstance) => {
                // Do not process the same version again
                if ((packageInstance.pkg.version in versions)) return versions;
                versions[packageInstance.pkg.version] = {
                    pkg: packageInstance.pkg,
                    satisfies: new Set(),
                }
                return versions;
            }, {});

            // Link each version with all the package instances it satisfies
            Object.keys(versions).forEach(version => {
                const satisfies = versions[version].satisfies;
                packageInstances.forEach(packageInstance => {
                    if (semver.satisfies(version, packageInstance.requestedVersion)) {
                        satisfies.add(packageInstance);
                        packageInstance.satisfiedBy.add(version);
                    }
                })
            })

            // Map every package instance to the deduped version
            packageInstances.forEach(packageInstance => {
                const candidateVersions = Array.from(packageInstance.satisfiedBy);

                candidateVersions.sort((versionA, versionB) => {
                    if (useMostCommon) {
                        // Sort verions based on how many packages it satisfies. In case of a tie, put the
                        // highest version first.
                        if (versions[versionB].satisfies.size > versions[versionA].satisfies.size) return 1;
                        if (versions[versionB].satisfies.size < versions[versionA].satisfies.size) return -1;
                    }
                    return semver.rcompare(versionA, versionB);
                });

                // The best package is always the first one in the list thanks to the sorting above.
                const bestVersion = candidateVersions[0];
                if (bestVersion !== packageInstance.pkg.version) {
                    result.push(`Package "${name}" wants ${packageInstance.requestedVersion} and could get ${bestVersion}, but got ${packageInstance.pkg.version}`);
                }
            })
        })

    return result;
}

module.exports.fixDuplicates = (yarnLock, {
    includePackages=[],
    useMostCommon=false,
}={}) => {
    const json = parseYarnLock(yarnLock);
    const packages = extractPackages(json);

    Object.keys(packages)
        // Filter out packages not specified in the inclusion list
        .filter((name) => {
            if (includePackages.length === 0) return true;
            return includePackages.includes(name);
        })
        .forEach((name) => {
            const packageInstances = packages[name];

            // Create a map with all versions installed for this package
            const versions = packageInstances.reduce((versions, packageInstance) => {
                // Do not process the same version again
                if ((packageInstance.pkg.version in versions)) return versions;
                versions[packageInstance.pkg.version] = {
                    pkg: packageInstance.pkg,
                    satisfies: new Set(),
                }
                return versions;
            }, {});

            // Link each version with all the package instances it satisfies
            Object.keys(versions).forEach(version => {
                const satisfies = versions[version].satisfies;
                packageInstances.forEach(packageInstance => {
                    if (semver.satisfies(version, packageInstance.requestedVersion)) {
                        satisfies.add(packageInstance);
                        packageInstance.satisfiedBy.add(version);
                    }
                })
            })

            // Map every package instance to the deduped version
            packageInstances.forEach(packageInstance => {
                const candidateVersions = Array.from(packageInstance.satisfiedBy);

                candidateVersions.sort((versionA, versionB) => {
                    if (useMostCommon) {
                        // Sort verions based on how many packages it satisfies. In case of a tie, put the
                        // highest version first.
                        if (versions[versionB].satisfies.size > versions[versionA].satisfies.size) return 1;
                        if (versions[versionB].satisfies.size < versions[versionA].satisfies.size) return -1;
                    }
                    return semver.rcompare(versionA, versionB);
                });

                // The best package is always the first one in the list thanks to the sorting above.
                const bestVersion = candidateVersions[0]

                json[`${name}@${packageInstance.requestedVersion}`] = versions[bestVersion].pkg;
            })
        });

    return lockfile.stringify(json);
}

