// @ts-check
import { CancelToken } from "@esfx/canceltoken";
import chalk from "chalk";
import chokidar from "chokidar";
import del from "del";
import esbuild from "esbuild";
import { EventEmitter } from "events";
import fs from "fs";
import _glob from "glob";
import { task } from "hereby";
import path from "path";
import util from "util";

import { localizationDirectories } from "./scripts/build/localization.mjs";
import cmdLineOptions from "./scripts/build/options.mjs";
import { buildProject, cleanProject, watchProject } from "./scripts/build/projects.mjs";
import { localBaseline, localRwcBaseline, refBaseline, refRwcBaseline, runConsoleTests } from "./scripts/build/tests.mjs";
import { Debouncer, Deferred, exec, getDiffTool, memoize, needsUpdate, readJson } from "./scripts/build/utils.mjs";

const glob = util.promisify(_glob);

/** @typedef {ReturnType<typeof task>} Task */
void 0;

const copyrightFilename = "./scripts/CopyrightNotice.txt";
const copyright = memoize(async () => {
    const contents = await fs.promises.readFile(copyrightFilename, "utf-8");
    return contents.replace(/\r\n/g, "\n");
});


export const buildScripts = task({
    name: "scripts",
    description: "Builds files in the 'scripts' folder.",
    run: () => buildProject("scripts")
});

const libs = memoize(() => {
    /** @type {{ libs: string[]; paths: Record<string, string | undefined>; }} */
    const libraries = readJson("./src/lib/libs.json");
    const libs = libraries.libs.map(lib => {
        const relativeSources = ["header.d.ts", lib + ".d.ts"];
        const relativeTarget = libraries.paths && libraries.paths[lib] || ("lib." + lib + ".d.ts");
        const sources = relativeSources.map(s => path.posix.join("src/lib", s));
        const target = `built/local/${relativeTarget}`;
        return { target, sources };
    });
    return libs;
});


export const generateLibs = task({
    name: "lib",
    description: "Builds the library targets",
    run: async () => {
        await fs.promises.mkdir("./built/local", { recursive: true });
        for (const lib of libs()) {
            let output = await copyright();

            for (const source of lib.sources) {
                const contents = await fs.promises.readFile(source, "utf-8");
                output += "\n" + contents.replace(/\r\n/g, "\n");
            }

            await fs.promises.writeFile(lib.target, output);
        }
    },
});


const diagnosticInformationMapTs = "src/compiler/diagnosticInformationMap.generated.ts";
const diagnosticMessagesJson = "src/compiler/diagnosticMessages.json";
const diagnosticMessagesGeneratedJson = "src/compiler/diagnosticMessages.generated.json";

export const generateDiagnostics = task({
    name: "generate-diagnostics",
    description: "Generates a diagnostic file in TypeScript based on an input JSON file",
    run: async () => {
        await exec(process.execPath, ["scripts/processDiagnosticMessages.mjs", diagnosticMessagesJson]);
    }
});

const cleanDiagnostics = task({
    name: "clean-diagnostics",
    description: "Generates a diagnostic file in TypeScript based on an input JSON file",
    hiddenFromTaskList: true,
    run: () => del([diagnosticInformationMapTs, diagnosticMessagesGeneratedJson]),
});


// Localize diagnostics
/**
 * .lcg file is what localization team uses to know what messages to localize.
 * The file is always generated in 'enu/diagnosticMessages.generated.json.lcg'
 */
const generatedLCGFile = "built/local/enu/diagnosticMessages.generated.json.lcg";

/**
 * The localization target produces the two following transformations:
 *    1. 'src\loc\lcl\<locale>\diagnosticMessages.generated.json.lcl' => 'built\local\<locale>\diagnosticMessages.generated.json'
 *       convert localized resources into a .json file the compiler can understand
 *    2. 'src\compiler\diagnosticMessages.generated.json' => 'built\local\ENU\diagnosticMessages.generated.json.lcg'
 *       generate the lcg file (source of messages to localize) from the diagnosticMessages.generated.json
 */
const localizationTargets = localizationDirectories
    .map(f => `built/local/${f}/diagnosticMessages.generated.json`)
    .concat(generatedLCGFile);

const localize = task({
    name: "localize",
    dependencies: [generateDiagnostics],
    run: async () => {
        if (needsUpdate(diagnosticMessagesGeneratedJson, generatedLCGFile)) {
            await exec(process.execPath, ["scripts/generateLocalizedDiagnosticMessages.mjs", "src/loc/lcl", "built/local", diagnosticMessagesGeneratedJson], { ignoreExitCode: true });
        }
    }
});

export const buildSrc = task({
    name: "build-src",
    description: "Builds the src project (all code)",
    dependencies: [generateDiagnostics],
    run: () => buildProject("src"),
});

export const watchSrc = task({
    name: "watch-src",
    description: "Watches the src project (all code)",
    hiddenFromTaskList: true,
    dependencies: [generateDiagnostics],
    run: () => watchProject("src"),
});

export const cleanSrc = task({
    name: "clean-src",
    hiddenFromTaskList: true,
    run: () => cleanProject("src"),
});

/**
 * @param {string} entrypoint
 * @param {string} output
 */
async function runDtsBundler(entrypoint, output) {
    await exec(process.execPath, [
        "./scripts/dtsBundler.mjs",
        "--entrypoint",
        entrypoint,
        "--output",
        output,
    ]);
}


/**
 * @param {string} entrypoint
 * @param {string} outfile
 * @param {BundlerTaskOptions} [taskOptions]
 *
 * @typedef BundlerTaskOptions
 * @property {boolean} [exportIsTsObject]
 * @property {boolean} [treeShaking]
 * @property {() => void} [onWatchRebuild]
 */
function createBundler(entrypoint, outfile, taskOptions = {}) {
    const getOptions = memoize(async () => {
        /** @type {esbuild.BuildOptions} */
        const options = {
            entryPoints: [entrypoint],
            banner: { js: await copyright() },
            bundle: true,
            outfile,
            platform: "node",
            target: ["es2020", "node14.17"],
            format: "cjs",
            sourcemap: "linked",
            sourcesContent: false,
            treeShaking: taskOptions.treeShaking,
            packages: "external",
            logLevel: "warning",
            // legalComments: "none", // If we add copyright headers to the source files, uncomment.
        };

        if (taskOptions.exportIsTsObject) {
            // We use an IIFE so we can inject the footer, and so that "ts" is global if not loaded as a module.
            options.format = "iife";
            // Name the variable ts, matching our old big bundle and so we can use the code below.
            options.globalName = "ts";
            // If we are in a CJS context, export the ts namespace.
            options.footer = { js: `\nif (typeof module !== "undefined" && module.exports) { module.exports = ts; }` };

            // esbuild converts calls to "require" to "__require"; this function
            // calls the real require if it exists, or throws if it does not (rather than
            // throwing an error like "require not defined"). But, since we want typescript
            // to be consumable by other bundlers, we need to convert these calls back to
            // require so our imports are visible again.
            //
            // The leading spaces are to keep the offsets the same within the files to keep
            // source maps working (though this only really matters for the line the require is on).
            //
            // See: https://github.com/evanw/esbuild/issues/1905
            options.define = { require: "$$require" };
            options.plugins = [
                {
                    name: "fix-require",
                    setup: (build) => {
                        build.onEnd(async () => {
                            let contents = await fs.promises.readFile(outfile, "utf-8");
                            contents = contents.replace(/\$\$require/g, "  require");
                            await fs.promises.writeFile(outfile, contents);
                        });
                    },
                }
            ];
        }

        return options;
    });

    return {
        build: async () => esbuild.build(await getOptions()),
        watch: async () => {
            /** @type {esbuild.BuildOptions} */
            const options = { ...await getOptions(), logLevel: "info" };
            if (taskOptions.onWatchRebuild) {
                const onRebuild = taskOptions.onWatchRebuild;
                options.plugins = (options.plugins?.slice(0) ?? []).concat([{
                    name: "watch",
                    setup: (build) => {
                        let firstBuild = true;
                        build.onEnd(() => {
                            if (firstBuild) {
                                firstBuild = false;
                            }
                            else {
                                onRebuild();
                            }
                        });
                    }
                }]);
            }

            const ctx = await esbuild.context(options);
            ctx.watch();
        },
    };
}

let printedWatchWarning = false;

/**
 * @param {object} options
 * @param {string} options.name
 * @param {string} [options.description]
 * @param {Task[]} [options.buildDeps]
 * @param {string} options.project
 * @param {string} options.srcEntrypoint
 * @param {string} options.builtEntrypoint
 * @param {string} options.output
 * @param {Task[]} [options.mainDeps]
 * @param {BundlerTaskOptions} [options.bundlerOptions]
 */
function entrypointBuildTask(options) {
    const build = task({
        name: `build-${options.name}`,
        dependencies: options.buildDeps,
        run: () => buildProject(options.project),
    });

    const bundler = createBundler(options.srcEntrypoint, options.output, options.bundlerOptions);

    // If we ever need to bundle our own output, change this to depend on build
    // and run esbuild on builtEntrypoint.
    const bundle = task({
        name: `bundle-${options.name}`,
        dependencies: options.buildDeps,
        run: () => bundler.build(),
    });

    /**
     * Writes a CJS module that reexports another CJS file. E.g. given
     * `options.builtEntrypoint = "./built/local/tsc/tsc.js"` and
     * `options.output = "./built/local/tsc.js"`, this will create a file
     * named "./built/local/tsc.js" containing:
     *
     * ```
     * module.exports = require("./tsc/tsc.js")
     * ```
     */
    const shim = task({
        name: `shim-${options.name}`,
        run: async () => {
            const outDir = path.dirname(options.output);
            await fs.promises.mkdir(outDir, { recursive: true });
            const moduleSpecifier = path.relative(outDir, options.builtEntrypoint);
            await fs.promises.writeFile(options.output, `module.exports = require("./${moduleSpecifier.replace(/[\\/]/g, "/")}")`);
        },
    });

    const mainDeps = options.mainDeps?.slice(0) ?? [];
    if (cmdLineOptions.bundle) {
        mainDeps.push(bundle);
        if (cmdLineOptions.typecheck) {
            mainDeps.push(build);
        }
    }
    else {
        mainDeps.push(build, shim);
    }

    const main = task({
        name: options.name,
        description: options.description,
        dependencies: mainDeps,
    });

    const watch = task({
        name: `watch-${options.name}`,
        hiddenFromTaskList: true, // This is best effort.
        dependencies: (options.buildDeps ?? []).concat(options.mainDeps ?? []).concat(cmdLineOptions.bundle ? [] : [shim]),
        run: () => {
            // These watch functions return promises that resolve once watch mode has started,
            // allowing them to operate as regular tasks, while creating unresolved promises
            // in the background that keep the process running after all tasks have exited.
            if (!printedWatchWarning) {
                console.error(chalk.yellowBright("Warning: watch mode is incomplete and may not work as expected. Use at your own risk."));
                printedWatchWarning = true;
            }

            if (!cmdLineOptions.bundle) {
                return watchProject(options.project);
            }
            return bundler.watch();
        }
    });

    return { build, bundle, shim, main, watch };
}


const { main: tsc, watch: watchTsc } = entrypointBuildTask({
    name: "tsc",
    description: "Builds the command-line compiler",
    buildDeps: [generateDiagnostics],
    project: "src/tsc",
    srcEntrypoint: "./src/tsc/tsc.ts",
    builtEntrypoint: "./built/local/tsc/tsc.js",
    output: "./built/local/tsc.js",
    mainDeps: [generateLibs],
});
export { tsc, watchTsc };


const { main: services, build: buildServices, watch: watchServices } = entrypointBuildTask({
    name: "services",
    description: "Builds the typescript.js library",
    buildDeps: [generateDiagnostics],
    project: "src/typescript",
    srcEntrypoint: "./src/typescript/typescript.ts",
    builtEntrypoint: "./built/local/typescript/typescript.js",
    output: "./built/local/typescript.js",
    mainDeps: [generateLibs],
    bundlerOptions: { exportIsTsObject: true },
});
export { services, watchServices };

export const dtsServices = task({
    name: "dts-services",
    description: "Bundles typescript.d.ts",
    dependencies: [buildServices],
    run: async () => {
        if (needsUpdate("./built/local/typescript/tsconfig.tsbuildinfo", ["./built/local/typescript.d.ts", "./built/local/typescript.internal.d.ts"])) {
            await runDtsBundler("./built/local/typescript/typescript.d.ts", "./built/local/typescript.d.ts");
        }
    },
});


const { main: tsserver, watch: watchTsserver } = entrypointBuildTask({
    name: "tsserver",
    description: "Builds the language server",
    buildDeps: [generateDiagnostics],
    project: "src/tsserver",
    srcEntrypoint: "./src/tsserver/server.ts",
    builtEntrypoint: "./built/local/tsserver/server.js",
    output: "./built/local/tsserver.js",
    mainDeps: [generateLibs],
});
export { tsserver, watchTsserver };


export const min = task({
    name: "min",
    description: "Builds only tsc and tsserver",
    dependencies: [tsc, tsserver],
});

export const watchMin = task({
    name: "watch-min",
    description: "Watches only tsc and tsserver",
    hiddenFromTaskList: true,
    dependencies: [watchTsc, watchTsserver],
});



const { main: lssl, build: buildLssl, watch: watchLssl } = entrypointBuildTask({
    name: "lssl",
    description: "Builds language service server library",
    buildDeps: [generateDiagnostics],
    project: "src/tsserverlibrary",
    srcEntrypoint: "./src/tsserverlibrary/tsserverlibrary.ts",
    builtEntrypoint: "./built/local/tsserverlibrary/tsserverlibrary.js",
    output: "./built/local/tsserverlibrary.js",
    mainDeps: [generateLibs],
    bundlerOptions: { exportIsTsObject: true },
});
export { lssl, watchLssl };

export const dtsLssl = task({
    name: "dts-lssl",
    description: "Bundles tsserverlibrary.d.ts",
    dependencies: [buildLssl],
    run: async () => {
        if (needsUpdate("./built/local/tsserverlibrary/tsconfig.tsbuildinfo", ["./built/local/tsserverlibrary.d.ts", "./built/local/tsserverlibrary.internal.d.ts"])) {
            await runDtsBundler("./built/local/tsserverlibrary/tsserverlibrary.d.ts", "./built/local/tsserverlibrary.d.ts");
        }
    }
});

export const dts = task({
    name: "dts",
    dependencies: [dtsServices, dtsLssl],
});


const testRunner = "./built/local/run.js";
const watchTestsEmitter = new EventEmitter();
const { main: tests, watch: watchTests } = entrypointBuildTask({
    name: "tests",
    description: "Builds the test infrastructure",
    buildDeps: [generateDiagnostics],
    project: "src/testRunner",
    srcEntrypoint: "./src/testRunner/_namespaces/Harness.ts",
    builtEntrypoint: "./built/local/testRunner/runner.js",
    output: testRunner,
    mainDeps: [generateLibs],
    bundlerOptions: {
        // Ensure we never drop any dead code, which might be helpful while debugging.
        treeShaking: false,
        onWatchRebuild() {
            watchTestsEmitter.emit("rebuild");
        }
    },
});
export { tests, watchTests };


export const runEslintRulesTests = task({
    name: "run-eslint-rules-tests",
    description: "Runs the eslint rule tests",
    run: () => runConsoleTests("scripts/eslint/tests", "mocha-fivemat-progress-reporter", /*runInParallel*/ false),
});

export const lint = task({
    name: "lint",
    description: "Runs eslint on the compiler and scripts sources.",
    run: async () => {
        const folder = ".";
        const formatter = cmdLineOptions.ci ? "stylish" : "autolinkable-stylish";
        const args = [
            "node_modules/eslint/bin/eslint",
            "--cache",
            "--cache-location", `${folder}/.eslintcache`,
            "--format", formatter,
        ];

        if (cmdLineOptions.fix) {
            args.push("--fix");
        }

        args.push(folder);

        console.log(`Linting: ${args.join(" ")}`);
        return exec(process.execPath, args);
    }
});

const { main: cancellationToken, watch: watchCancellationToken } = entrypointBuildTask({
    name: "cancellation-token",
    project: "src/cancellationToken",
    srcEntrypoint: "./src/cancellationToken/cancellationToken.ts",
    builtEntrypoint: "./built/local/cancellationToken/cancellationToken.js",
    output: "./built/local/cancellationToken.js",
});

const { main: typingsInstaller, watch: watchTypingsInstaller } = entrypointBuildTask({
    name: "typings-installer",
    buildDeps: [generateDiagnostics],
    project: "src/typingsInstaller",
    srcEntrypoint: "./src/typingsInstaller/nodeTypingsInstaller.ts",
    builtEntrypoint: "./built/local/typingsInstaller/nodeTypingsInstaller.js",
    output: "./built/local/typingsInstaller.js",
});

const { main: watchGuard, watch: watchWatchGuard } = entrypointBuildTask({
    name: "watch-guard",
    project: "src/watchGuard",
    srcEntrypoint: "./src/watchGuard/watchGuard.ts",
    builtEntrypoint: "./built/local/watchGuard/watchGuard.js",
    output: "./built/local/watchGuard.js",
});

export const generateTypesMap = task({
    name: "generate-types-map",
    run: async () => {
        await fs.promises.mkdir("./built/local", { recursive: true });
        const source = "src/server/typesMap.json";
        const target = "built/local/typesMap.json";
        const contents = await fs.promises.readFile(source, "utf-8");
        JSON.parse(contents); // Validates that the JSON parses.
        await fs.promises.writeFile(target, contents);
    }
});


// Drop a copy of diagnosticMessages.generated.json into the built/local folder. This allows
// it to be synced to the Azure DevOps repo, so that it can get picked up by the build
// pipeline that generates the localization artifacts that are then fed into the translation process.
const builtLocalDiagnosticMessagesGeneratedJson = "built/local/diagnosticMessages.generated.json";
const copyBuiltLocalDiagnosticMessages = task({
    name: "copy-built-local-diagnostic-messages",
    dependencies: [generateDiagnostics],
    run: async () => {
        const contents = await fs.promises.readFile(diagnosticMessagesGeneratedJson, "utf-8");
        JSON.parse(contents); // Validates that the JSON parses.
        await fs.promises.writeFile(builtLocalDiagnosticMessagesGeneratedJson, contents);
    }
});


export const otherOutputs = task({
    name: "other-outputs",
    description: "Builds miscelaneous scripts and documents distributed with the LKG",
    dependencies: [cancellationToken, typingsInstaller, watchGuard, generateTypesMap, copyBuiltLocalDiagnosticMessages],
});

export const watchOtherOutputs = task({
    name: "watch-other-outputs",
    description: "Builds miscelaneous scripts and documents distributed with the LKG",
    hiddenFromTaskList: true,
    dependencies: [watchCancellationToken, watchTypingsInstaller, watchWatchGuard, generateTypesMap, copyBuiltLocalDiagnosticMessages],
});

export const local = task({
    name: "local",
    description: "Builds the full compiler and services",
    dependencies: [localize, tsc, tsserver, services, lssl, otherOutputs, dts],
});
export default local;

export const watchLocal = task({
    name: "watch-local",
    description: "Watches the full compiler and services",
    hiddenFromTaskList: true,
    dependencies: [localize, watchTsc, watchTsserver, watchServices, watchLssl, watchOtherOutputs, dts, watchSrc],
});

const runtestsDeps = [tests, generateLibs].concat(cmdLineOptions.typecheck ? [dts] : []);

export const runTests = task({
    name: "runtests",
    description: "Runs the tests using the built run.js file.",
    dependencies: runtestsDeps,
    run: () => runConsoleTests(testRunner, "mocha-fivemat-progress-reporter", /*runInParallel*/ false),
});
// task("runtests").flags = {
//     "-t --tests=<regex>": "Pattern for tests to run.",
//     "   --failed": "Runs tests listed in '.failed-tests'.",
//     "   --coverage": "Generate test coverage using c8",
//     "-r --reporter=<reporter>": "The mocha reporter to use.",
//     "-i --break": "Runs tests in inspector mode (NodeJS 8 and later)",
//     "   --keepFailed": "Keep tests in .failed-tests even if they pass",
//     "   --light": "Run tests in light mode (fewer verifications, but tests run faster)",
//     "   --dirty": "Run tests without first cleaning test output directories",
//     "   --stackTraceLimit=<limit>": "Sets the maximum number of stack frames to display. Use 'full' to show all frames.",
//     "   --no-color": "Disables color",
//     "   --timeout=<ms>": "Overrides the default test timeout.",
//     "   --built": "Compile using the built version of the compiler.",
//     "   --shards": "Total number of shards running tests (default: 1)",
//     "   --shardId": "1-based ID of this shard (default: 1)",
// };

export const runTestsAndWatch = task({
    name: "runtests-watch",
    dependencies: [watchTests],
    run: async () => {
        if (!cmdLineOptions.tests && !cmdLineOptions.failed) {
            console.log(chalk.redBright(`You must specifiy either --tests/-t or --failed to use 'runtests-watch'.`));
            return;
        }

        let watching = true;
        let running = true;
        let lastTestChangeTimeMs = Date.now();
        let testsChangedDeferred = /** @type {Deferred<void>} */(new Deferred());
        let testsChangedCancelSource = CancelToken.source();

        const testsChangedDebouncer = new Debouncer(1_000, endRunTests);
        const testCaseWatcher = chokidar.watch([
            "tests/cases/**/*.*",
            "tests/lib/**/*.*",
            "tests/projects/**/*.*",
        ], {
            ignorePermissionErrors: true,
            alwaysStat: true
        });

        process.on("SIGINT", endWatchMode);
        process.on("beforeExit", endWatchMode);
        watchTestsEmitter.on("rebuild", onRebuild);
        testCaseWatcher.on("all", onChange);

        while (watching) {
            const promise = testsChangedDeferred.promise;
            const token = testsChangedCancelSource.token;
            if (!token.signaled) {
                running = true;
                try {
                    await runConsoleTests(testRunner, "mocha-fivemat-progress-reporter", /*runInParallel*/ false, { token, watching: true });
                }
                catch {
                    // ignore
                }
                running = false;
            }
            if (watching) {
                console.log(chalk.yellowBright(`[watch] test run complete, waiting for changes...`));
                await promise;
            }
        }

        function onRebuild() {
            beginRunTests(testRunner);
        }

        /**
         * @param {'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir'} eventName
         * @param {string} path
         * @param {fs.Stats | undefined} stats
         */
        function onChange(eventName, path, stats) {
            switch (eventName) {
                case "change":
                case "unlink":
                case "unlinkDir":
                    break;
                case "add":
                case "addDir":
                    // skip files that are detected as 'add' but haven't actually changed since the last time tests were
                    // run.
                    if (stats && stats.mtimeMs <= lastTestChangeTimeMs) {
                        return;
                    }
                    break;
            }
            beginRunTests(path);
        }

        /**
         * @param {string} path
         */
        function beginRunTests(path) {
            if (testsChangedDebouncer.empty) {
                console.log(chalk.yellowBright(`[watch] tests changed due to '${path}', restarting...`));
                if (running) {
                    console.log(chalk.yellowBright("[watch] aborting in-progress test run..."));
                }
                testsChangedCancelSource.cancel();
                testsChangedCancelSource = CancelToken.source();
            }

            testsChangedDebouncer.enqueue();
        }

        function endRunTests() {
            lastTestChangeTimeMs = Date.now();
            testsChangedDeferred.resolve();
            testsChangedDeferred = /** @type {Deferred<void>} */(new Deferred());
        }

        function endWatchMode() {
            if (watching) {
                watching = false;
                console.log(chalk.yellowBright("[watch] exiting watch mode..."));
                testsChangedCancelSource.cancel();
                testCaseWatcher.close();
                watchTestsEmitter.off("rebuild", onRebuild);
            }
        }
    },
});

const doRunTestsParallel = task({
    name: "do-runtests-parallel",
    description: "Runs all the tests in parallel using the built run.js file.",
    dependencies: runtestsDeps,
    run: () => runConsoleTests(testRunner, "min", /*runInParallel*/ cmdLineOptions.workers > 1),
});

export const runTestsParallel = task({
    name: "runtests-parallel",
    description: "Runs all the tests in parallel using the built run.js file, linting in parallel if --lint=true.",
    dependencies: [doRunTestsParallel].concat(cmdLineOptions.lint ? [lint] : []),
});

// task("runtests-parallel").flags = {
//     "   --coverage": "Generate test coverage using c8",
//     "   --light": "Run tests in light mode (fewer verifications, but tests run faster).",
//     "   --keepFailed": "Keep tests in .failed-tests even if they pass.",
//     "   --dirty": "Run tests without first cleaning test output directories.",
//     "   --stackTraceLimit=<limit>": "Sets the maximum number of stack frames to display. Use 'full' to show all frames.",
//     "   --workers=<number>": "The number of parallel workers to use.",
//     "   --timeout=<ms>": "Overrides the default test timeout.",
//     "   --built": "Compile using the built version of the compiler.",
//     "   --shards": "Total number of shards running tests (default: 1)",
//     "   --shardId": "1-based ID of this shard (default: 1)",
// };

export const testBrowserIntegration = task({
    name: "test-browser-integration",
    description: "Runs scripts/browserIntegrationTest.mjs which tests that typescript.js loads in a browser",
    dependencies: [services],
    run: () => exec(process.execPath, ["scripts/browserIntegrationTest.mjs"]),
});

export const diff = task({
    name: "diff",
    description: "Diffs the compiler baselines using the diff tool specified by the 'DIFF' environment variable",
    run: () => exec(getDiffTool(), [refBaseline, localBaseline], { ignoreExitCode: true, waitForExit: false }),
});

export const diffRwc = task({
    name: "diff-rwc",
    description: "Diffs the RWC baselines using the diff tool specified by the 'DIFF' environment variable",
    run: () => exec(getDiffTool(), [refRwcBaseline, localRwcBaseline], { ignoreExitCode: true, waitForExit: false }),
});

/**
 * @param {string} localBaseline Path to the local copy of the baselines
 * @param {string} refBaseline Path to the reference copy of the baselines
 */
function baselineAcceptTask(localBaseline, refBaseline) {
    /**
     * @param {string} p
     */
    function localPathToRefPath(p) {
        const relative = path.relative(localBaseline, p);
        return path.join(refBaseline, relative);
    }

    return async () => {
        const toCopy = await glob(`${localBaseline}/**`, { nodir: true, ignore: `${localBaseline}/**/*.delete` });
        for (const p of toCopy) {
            const out = localPathToRefPath(p);
            await fs.promises.mkdir(path.dirname(out), { recursive: true });
            await fs.promises.copyFile(p, out);
        }
        const toDelete = await glob(`${localBaseline}/**/*.delete`, { nodir: true });
        for (const p of toDelete) {
            const out = localPathToRefPath(p).replace(/\.delete$/, "");
            await fs.promises.rm(out);
        }
    };
}

export const baselineAccept = task({
    name: "baseline-accept",
    description: "Makes the most recent test results the new baseline, overwriting the old baseline",
    run: baselineAcceptTask(localBaseline, refBaseline),
});

export const baselineAcceptRwc = task({
    name: "baseline-accept-rwc",
    description: "Makes the most recent rwc test results the new baseline, overwriting the old baseline",
    run: baselineAcceptTask(localRwcBaseline, refRwcBaseline),
});

// TODO(rbuckton): Determine if we still need this task. Depending on a relative
//                 path here seems like a bad idea.
export const updateSublime = task({
    name: "update-sublime",
    description: "Updates the sublime plugin's tsserver",
    dependencies: [tsserver],
    run: async () => {
        for (const file of ["built/local/tsserver.js", "built/local/tsserver.js.map"]) {
            await fs.promises.copyFile(file, path.resolve("../TypeScript-Sublime-Plugin/tsserver/", path.basename(file)));
        }
    }
});

// TODO(rbuckton): Should the path to DefinitelyTyped be configurable via an environment variable?
export const importDefinitelyTypedTests = task({
    name: "importDefinitelyTypedTests",
    description: "Runs the importDefinitelyTypedTests script to copy DT's tests to the TS-internal RWC tests",
    run: () => exec(process.execPath, ["scripts/importDefinitelyTypedTests.mjs", "./", "../DefinitelyTyped"]),
});


export const produceLKG = task({
    name: "LKG",
    description: "Makes a new LKG out of the built js files",
    dependencies: [local],
    run: async () => {
        if (!cmdLineOptions.bundle) {
            throw new Error("LKG cannot be created when --bundle=false");
        }

        const expectedFiles = [
            "built/local/cancellationToken.js",
            "built/local/tsc.js",
            "built/local/tsserver.js",
            "built/local/tsserverlibrary.js",
            "built/local/tsserverlibrary.d.ts",
            "built/local/typescript.js",
            "built/local/typescript.d.ts",
            "built/local/typingsInstaller.js",
            "built/local/watchGuard.js",
        ].concat(libs().map(lib => lib.target));
        const missingFiles = expectedFiles
            .concat(localizationTargets)
            .filter(f => !fs.existsSync(f));
        if (missingFiles.length > 0) {
            throw new Error("Cannot replace the LKG unless all built targets are present in directory 'built/local/'. The following files are missing:\n" + missingFiles.join("\n"));
        }

        await exec(process.execPath, ["scripts/produceLKG.mjs"]);
    }
});

export const lkg = task({
    name: "lkg",
    hiddenFromTaskList: true,
    dependencies: [produceLKG],
});

export const cleanBuilt = task({
    name: "clean-built",
    hiddenFromTaskList: true,
    run: () => del("built"),
});

export const clean = task({
    name: "clean",
    description: "Cleans build outputs",
    dependencies: [cleanBuilt, cleanDiagnostics],
});

export const configureNightly = task({
    name: "configure-nightly",
    description: "Runs scripts/configurePrerelease.mjs to prepare a build for nightly publishing",
    run: () => exec(process.execPath, ["scripts/configurePrerelease.mjs", "dev", "package.json", "src/compiler/corePublic.ts"]),
});

export const configureInsiders = task({
    name: "configure-insiders",
    description: "Runs scripts/configurePrerelease.mjs to prepare a build for insiders publishing",
    run: () => exec(process.execPath, ["scripts/configurePrerelease.mjs", "insiders", "package.json", "src/compiler/corePublic.ts"]),
});

export const configureExperimental = task({
    name: "configure-experimental",
    description: "Runs scripts/configurePrerelease.mjs to prepare a build for experimental publishing",
    run: () => exec(process.execPath, ["scripts/configurePrerelease.mjs", "experimental", "package.json", "src/compiler/corePublic.ts"]),
});

export const help = task({
    name: "help",
    description: "Prints the top-level tasks.",
    hiddenFromTaskList: true,
    run: () => exec("hereby", ["--tasks"], { hidePrompt: true }),
});
