"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    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) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CleanCommand = void 0;
const color_1 = require("../color");
const yok_1 = require("../common/yok");
const constants = require("../constants");
const os = require("os");
const path_1 = require("path");
const promises_1 = require("fs/promises");
const helpers_1 = require("../common/helpers");
const CLIPath = (0, path_1.resolve)(__dirname, "..", "..", "bin", "nativescript.js");
function bytesToHumanReadable(bytes) {
    const units = ["B", "KB", "MB", "GB", "TB"];
    let unit = 0;
    while (bytes >= 1024) {
        bytes /= 1024;
        unit++;
    }
    return `${bytes.toFixed(2)} ${units[unit]}`;
}
function promiseMap(values, mapper, concurrency = 10) {
    let index = 0;
    let pending = 0;
    let done = false;
    return new Promise((resolve, reject) => {
        const next = () => {
            done = index === values.length;
            if (done && pending === 0) {
                return resolve();
            }
            while (pending < concurrency && index < values.length) {
                const value = values[index++];
                pending++;
                mapper(value)
                    .then(() => {
                    pending--;
                    next();
                })
                    .catch();
            }
        };
        next();
    });
}
class CleanCommand {
    constructor($projectCleanupService, $projectConfigService, $terminalSpinnerService, $projectService, $prompter, $logger, $options, $childProcess) {
        this.$projectCleanupService = $projectCleanupService;
        this.$projectConfigService = $projectConfigService;
        this.$terminalSpinnerService = $terminalSpinnerService;
        this.$projectService = $projectService;
        this.$prompter = $prompter;
        this.$logger = $logger;
        this.$options = $options;
        this.$childProcess = $childProcess;
        this.allowedParameters = [];
    }
    execute(args) {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            const isDryRun = (_a = this.$options.dryRun) !== null && _a !== void 0 ? _a : false;
            const isJSON = (_b = this.$options.json) !== null && _b !== void 0 ? _b : false;
            const spinner = this.$terminalSpinnerService.createSpinner({
                isSilent: isJSON,
            });
            if (!this.$projectService.isValidNativeScriptProject()) {
                return this.cleanMultipleProjects(spinner);
            }
            spinner.start("Cleaning project...\n");
            let pathsToClean = [
                constants.HOOKS_DIR_NAME,
                constants.PLATFORMS_DIR_NAME,
                constants.NODE_MODULES_FOLDER_NAME,
                constants.PACKAGE_LOCK_JSON_FILE_NAME,
            ];
            try {
                const overridePathsToClean = this.$projectConfigService.getValue("cli.pathsToClean");
                const additionalPaths = this.$projectConfigService.getValue("cli.additionalPathsToClean");
                if (Array.isArray(overridePathsToClean)) {
                    pathsToClean = overridePathsToClean;
                }
                if (Array.isArray(additionalPaths)) {
                    pathsToClean.push(...additionalPaths);
                }
            }
            catch (err) {
            }
            const res = yield this.$projectCleanupService.clean(pathsToClean, {
                dryRun: isDryRun,
                silent: isJSON,
                stats: isJSON,
            });
            if (res.stats && isJSON) {
                console.log(JSON.stringify({
                    ok: res.ok,
                    dryRun: isDryRun,
                    stats: Object.fromEntries(res.stats.entries()),
                }, null, 2));
                return;
            }
            if (res.ok) {
                spinner.succeed("Project successfully cleaned.");
            }
            else {
                spinner.fail(color_1.color.red("Project unsuccessfully cleaned."));
            }
        });
    }
    cleanMultipleProjects(spinner) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!(0, helpers_1.isInteractive)() || this.$options.json) {
                this.$logger.warn("No project found in the current directory.");
                return;
            }
            const shouldScan = yield this.$prompter.confirm("No project found in the current directory. Would you like to scan for all projects in sub-directories instead?");
            if (!shouldScan) {
                return;
            }
            spinner.start("Scanning for projects... Please wait.");
            const paths = yield this.getNSProjectPathsInDirectory();
            spinner.succeed(`Found ${paths.length} projects.`);
            let computed = 0;
            const updateProgress = () => {
                const current = color_1.color.grey(`${computed}/${paths.length}`);
                spinner.start(`Gathering cleanable sizes. This may take a while... ${current}`);
            };
            updateProgress();
            const projects = new Map();
            yield promiseMap(paths, (p) => {
                return this.$childProcess
                    .exec(`node ${CLIPath} clean --dry-run --json --disable-analytics`, {
                    cwd: p,
                })
                    .then((res) => {
                    const paths = JSON.parse(res).stats;
                    return Object.values(paths).reduce((a, b) => a + b, 0);
                })
                    .catch((err) => {
                    this.$logger.trace("Failed to get project size for %s, Error is:", p, err);
                    return -1;
                })
                    .then((size) => {
                    if (size > 0 || size === -1) {
                        projects.set(p, size);
                    }
                    computed++;
                    updateProgress();
                });
            }, os.cpus().length);
            spinner.clear();
            spinner.stop();
            this.$logger.clearScreen();
            const totalSize = Array.from(projects.values())
                .filter((s) => s > 0)
                .reduce((a, b) => a + b, 0);
            const pathsToClean = yield this.$prompter.promptForChoice(`Found ${projects.size} cleanable project(s) with a total size of: ${color_1.color.green(bytesToHumanReadable(totalSize))}. Select projects to clean`, Array.from(projects.keys()).map((p) => {
                const size = projects.get(p);
                let description;
                if (size === -1) {
                    description = " - could not get size";
                }
                else {
                    description = ` - ${bytesToHumanReadable(size)}`;
                }
                return {
                    title: `${p}${color_1.color.grey(description)}`,
                    value: p,
                };
            }), true, {
                optionsPerPage: process.stdout.rows - 6,
            });
            this.$logger.clearScreen();
            spinner.warn(`This will run "${color_1.color.yellow(`ns clean`)}" in all the selected projects and ${color_1.color.red.bold("delete files from your system")}!`);
            spinner.warn(`This action cannot be undone!`);
            let confirmed = yield this.$prompter.confirm("Are you sure you want to clean the selected projects?");
            if (!confirmed) {
                return;
            }
            spinner.info("Cleaning... This might take a while...");
            let totalSizeCleaned = 0;
            for (let i = 0; i < pathsToClean.length; i++) {
                const currentPath = pathsToClean[i];
                spinner.start(`Cleaning ${color_1.color.cyan(currentPath)}... ${i + 1}/${pathsToClean.length}`);
                const ok = yield this.$childProcess
                    .exec(`node ${CLIPath} clean ${this.$options.dryRun ? "--dry-run" : ""} --json --disable-analytics`, {
                    cwd: currentPath,
                })
                    .then((res) => {
                    const cleanupRes = JSON.parse(res);
                    return cleanupRes.ok;
                })
                    .catch((err) => {
                    this.$logger.trace('Failed to clean project "%s"', currentPath, err);
                    return false;
                });
                if (ok) {
                    const cleanedSize = projects.get(currentPath);
                    const cleanedSizeStr = color_1.color.grey(`- ${bytesToHumanReadable(cleanedSize)}`);
                    spinner.succeed(`Cleaned ${color_1.color.cyan(currentPath)} ${cleanedSizeStr}`);
                    totalSizeCleaned += cleanedSize;
                }
                else {
                    spinner.fail(`Failed to clean ${color_1.color.cyan(currentPath)} - skipped`);
                }
            }
            spinner.clear();
            spinner.stop();
            spinner.succeed(`Done! We've just freed up ${color_1.color.green(bytesToHumanReadable(totalSizeCleaned))}! Woohoo! 🎉`);
            if (this.$options.dryRun) {
                spinner.info('Note: the "--dry-run" flag was used, so no files were actually deleted.');
            }
        });
    }
    getNSProjectPathsInDirectory(dir = process.cwd()) {
        return __awaiter(this, void 0, void 0, function* () {
            let nsDirs = [];
            const getFiles = (dir) => __awaiter(this, void 0, void 0, function* () {
                if (dir.includes("node_modules")) {
                    return;
                }
                const dirents = yield (0, promises_1.readdir)(dir, { withFileTypes: true }).catch((err) => {
                    this.$logger.trace('Failed to read directory "%s". Error is:', dir, err);
                    return [];
                });
                const hasNSConfig = dirents.some((ent) => ent.name.includes("nativescript.config.ts") ||
                    ent.name.includes("nativescript.config.js"));
                if (hasNSConfig) {
                    nsDirs.push(dir);
                    return;
                }
                yield Promise.all(dirents.map((dirent) => {
                    const res = (0, path_1.resolve)(dir, dirent.name);
                    if (dirent.isDirectory()) {
                        return getFiles(res);
                    }
                }));
            });
            yield getFiles(dir);
            return nsDirs;
        });
    }
}
exports.CleanCommand = CleanCommand;
yok_1.injector.registerCommand("clean", CleanCommand);
