// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

import * as fs from "fs";
import * as glob from "glob";
import * as os from "os";
import * as path from "path";
import * as vscode from "vscode";

import * as constants from "../common/constants";
import * as util from "../common/util";
import * as logger from "../logger/logger";

import { DeviceContext } from "../deviceContext";
import { IArduinoSettings } from "./arduinoSettings";
import { BoardManager } from "./boardManager";
import { ExampleManager } from "./exampleManager";
import { AnalysisManager,
         isCompilerParserEnabled,
         makeCompilerParserContext } from "./intellisense";
import { LibraryManager } from "./libraryManager";
import { VscodeSettings } from "./vscodeSettings";

import { arduinoChannel } from "../common/outputChannel";
import { ArduinoWorkspace } from "../common/workspace";
import { SerialMonitor } from "../serialmonitor/serialMonitor";
import { UsbDetector } from "../serialmonitor/usbDetector";
import { ProgrammerManager } from "./programmerManager";

/**
 * Supported build modes. For further explanation see the documentation
 * of ArduinoApp.build().
 * The strings are used for status reporting within the above function.
 */
export enum BuildMode {
    Verify = "Verifying",
    Analyze = "Analyzing",
    Upload = "Uploading",
    CliUpload = "Uploading using Arduino CLI",
    UploadProgrammer = "Uploading (programmer)",
    CliUploadProgrammer = "Uploading (programmer) using Arduino CLI",
}

/**
 * Represent an Arduino application based on the official Arduino IDE.
 */
export class ArduinoApp {

    private _boardManager: BoardManager;

    private _libraryManager: LibraryManager;

    private _exampleManager: ExampleManager;

    private _programmerManager: ProgrammerManager;

    /**
     * IntelliSense analysis manager.
     * Makes sure that analysis builds and regular builds go along
     * and that multiple subsequent analysis requests - as triggered
     * by board/board-configuration changes - are bundled to a single
     * analysis build run.
     */
    private _analysisManager: AnalysisManager;

    /**
     * Indicates if a build is currently in progress.
     * If so any call to this.build() will return false immediately.
     */
    private _building: boolean = false;

    /**
     * @param {IArduinoSettings} _settings ArduinoSetting object.
     */
    constructor(private _settings: IArduinoSettings) {
        const analysisDelayMs = 1000 * 3;
        this._analysisManager = new AnalysisManager(
            () => this._building,
            async () => { await this.build(BuildMode.Analyze); },
            analysisDelayMs);
    }

    /**
     * Need refresh Arduino IDE's setting when starting up.
     * @param {boolean} force - Whether force initialize the arduino
     */
    public async initialize(force: boolean = false) {
        if (!util.fileExistsSync(this._settings.preferencePath)) {
            try {
                // Use empty pref value to initialize preference.txt file
                await this.setPref("boardsmanager.additional.urls", "");
                this._settings.reloadPreferences(); // reload preferences.
            } catch (ex) {
            }
        }
        if (force || !util.fileExistsSync(path.join(this._settings.packagePath, "package_index.json"))) {
            try {
                // Use the dummy package to initialize the Arduino IDE
                await this.installBoard("dummy", "", "", true);
            } catch (ex) {
            }
        }

        if (this._settings.analyzeOnSettingChange) {
            // set up event handling for IntelliSense analysis
            const requestAnalysis = async () => {
                if (isCompilerParserEnabled()) {
                    await this._analysisManager.requestAnalysis();
                }
            };
            const dc = DeviceContext.getInstance();
            dc.onChangeBoard(requestAnalysis);
            dc.onChangeConfiguration(requestAnalysis);
            dc.onChangeSketch(requestAnalysis);
        }
    }

    /**
     * Initialize the arduino library.
     * @param {boolean} force - Whether force refresh library index file
     */
    public async initializeLibrary(force: boolean = false) {
        if (force || !util.fileExistsSync(path.join(this._settings.packagePath, "library_index.json"))) {
            try {
                // Use the dummy library to initialize the Arduino IDE
                await this.installLibrary("dummy", "", true);
            } catch (ex) {
            }
        }
    }

    /**
     * Set the Arduino preferences value.
     * @param {string} key - The preference key
     * @param {string} value - The preference value
     */
    public async setPref(key, value) {
        try {
            if (this.useArduinoCli()) {
                await util.spawn(this._settings.commandPath,
                    ["--build-property", `${key}=${value}`]);
            } else {
                await util.spawn(this._settings.commandPath,
                                 ["--pref", `${key}=${value}`, "--save-prefs"]);
            }
        } catch (ex) {
        }
    }

    /**
     * Returns true if a build is currently in progress.
     */
    public get building() {
        return this._building;
    }

    /**
     * Runs the arduino builder to build/compile and - if necessary - upload
     * the current sketch.
     * @param buildMode Build mode.
     *  * BuildMode.Upload: Compile and upload
     *  * BuildMode.UploadProgrammer: Compile and upload using the user
     *     selectable programmer
     *  * BuildMode.Analyze: Compile, analyze the output and generate
     *     IntelliSense configuration from it.
     *  * BuildMode.Verify: Just compile.
     * All build modes except for BuildMode.Analyze run interactively, i.e. if
     * something is missing, it tries to query the user for the missing piece
     * of information (sketch, board, etc.). Analyze runs non interactively and
     * just returns false.
     * @param buildDir Override the build directory set by the project settings
     * with the given directory.
     * @returns true on success, false if
     *  * another build is currently in progress
     *  * board- or programmer-manager aren't initialized yet
     *  * or something went wrong during the build
     */
    public async build(buildMode: BuildMode, buildDir?: string) {

        if (!this._boardManager || !this._programmerManager || this._building) {
            return false;
        }

        this._building = true;

        return await this._build(buildMode, buildDir)
        .then((ret) => {
            this._building = false;
            return ret;
        })
        .catch((reason) => {
            this._building = false;
            logger.notifyUserError("ArduinoApp.build",
                                   reason,
                                   `Unhandled exception when cleaning up build "${buildMode}": ${JSON.stringify(reason)}`);
            return false;
        });
    }

    // Include the *.h header files from selected library to the arduino sketch.
    public async includeLibrary(libraryPath: string) {
        if (!ArduinoWorkspace.rootPath) {
            return;
        }
        const dc = DeviceContext.getInstance();
        const appPath = path.join(ArduinoWorkspace.rootPath, dc.sketch);
        if (util.fileExistsSync(appPath)) {
            const hFiles = glob.sync(`${libraryPath}/*.h`, {
                nodir: true,
                matchBase: true,
            });
            const hIncludes = hFiles.map((hFile) => {
                return `#include <${path.basename(hFile)}>`;
            }).join(os.EOL);

            // Open the sketch and bring up it to current visible view.
            const textDocument = await vscode.workspace.openTextDocument(appPath);
            await vscode.window.showTextDocument(textDocument, vscode.ViewColumn.One, true);
            const activeEditor = vscode.window.visibleTextEditors.find((textEditor) => {
                return path.resolve(textEditor.document.fileName) === path.resolve(appPath);
            });
            if (activeEditor) {
                // Insert *.h at the beginning of the sketch code.
                await activeEditor.edit((editBuilder) => {
                    editBuilder.insert(new vscode.Position(0, 0), `${hIncludes}${os.EOL}${os.EOL}`);
                });
            }
        }
    }

     /**
      * Installs arduino board package.
      * (If using the aduino CLI this installs the corrosponding core.)
      * @param {string} packageName - board vendor
      * @param {string} arch - board architecture
      * @param {string} version - version of board package or core to download
      * @param {boolean} [showOutput=true] - show raw output from command
      */
    public async installBoard(packageName: string, arch: string = "", version: string = "", showOutput: boolean = true) {
        arduinoChannel.show();
        const updatingIndex = packageName === "dummy" && !arch && !version;
        if (updatingIndex) {
            arduinoChannel.start(`Update package index files...`);
        } else {
            try {
                const packagePath = path.join(this._settings.packagePath, "packages", packageName, arch);
                if (util.directoryExistsSync(packagePath)) {
                    util.rmdirRecursivelySync(packagePath);
                }
                arduinoChannel.start(`Install package - ${packageName}...`);
            } catch (error) {
                arduinoChannel.start(`Install package - ${packageName} failed under directory : ${error.path}${os.EOL}
                                      Please make sure the folder is not occupied by other procedures .`);
                arduinoChannel.error(`Error message - ${error.message}${os.EOL}`);
                arduinoChannel.error(`Exit with code=${error.code}${os.EOL}`);
                return;
            }
        }
        arduinoChannel.info(`${packageName}${arch && ":" + arch}${version && ":" + version}`);
        try {
            if (this.useArduinoCli()) {
                await util.spawn(this._settings.commandPath,
                    ["core", "install", `${packageName}${arch && ":" + arch}${version && "@" + version}`],
                    undefined,
                    { channel: showOutput ? arduinoChannel.channel : null });
            } else {
                await util.spawn(this._settings.commandPath,
                    ["--install-boards", `${packageName}${arch && ":" + arch}${version && ":" + version}`],
                    undefined,
                    { channel: showOutput ? arduinoChannel.channel : null });
            }
            if (updatingIndex) {
                arduinoChannel.end("Updated package index files.");
            } else {
                arduinoChannel.end(`Installed board package - ${packageName}${os.EOL}`);
            }
        } catch (error) {
            // If a platform with the same version is already installed, nothing is installed and program exits with exit code 1
            if (error.code === 1) {
                if (updatingIndex) {
                    arduinoChannel.end("Updated package index files.");
                } else {
                    arduinoChannel.end(`Installed board package - ${packageName}${os.EOL}`);
                }
            } else {
                arduinoChannel.error(`Exit with code=${error.code}${os.EOL}`);
            }
        }
    }

    public uninstallBoard(boardName: string, packagePath: string) {
        arduinoChannel.start(`Uninstall board package - ${boardName}...`);
        util.rmdirRecursivelySync(packagePath);
        arduinoChannel.end(`Uninstalled board package - ${boardName}${os.EOL}`);
    }

    /**
     * Downloads or updates a library
     * @param {string} libName - name of the library to download
     * @param {string} version - version of library to download
     * @param {boolean} [showOutput=true] - show raw output from command
     */

    public async installLibrary(libName: string, version: string = "", showOutput: boolean = true) {
        arduinoChannel.show();
        const updatingIndex = (libName === "dummy" && !version);
        if (updatingIndex) {
            arduinoChannel.start("Update library index files...");
        } else {
            arduinoChannel.start(`Install library - ${libName}`);
        }
        try {
            if (this.useArduinoCli()) {
                await  util.spawn(this._settings.commandPath,
                    ["lib", "install", `${libName}${version && "@" + version}`],
                    undefined,
                    { channel: showOutput ? arduinoChannel.channel : undefined });
            } else {
                await util.spawn(this._settings.commandPath,
                    ["--install-library", `${libName}${version && ":" + version}`],
                    undefined,
                    { channel: showOutput ? arduinoChannel.channel : undefined });
            }
            if (updatingIndex) {
                arduinoChannel.end("Updated library index files.");
            } else {
                arduinoChannel.end(`Installed library - ${libName}${os.EOL}`);
            }
        } catch (error) {
            // If a library with the same version is already installed, nothing is installed and program exits with exit code 1
            if (error.code === 1) {
                if (updatingIndex) {
                    arduinoChannel.end("Updated library index files.");
                } else {
                    arduinoChannel.end(`Installed library - ${libName}${os.EOL}`);
                }
            } else {
                arduinoChannel.error(`Exit with code=${error.code}${os.EOL}`);
            }
        }
    }

    public uninstallLibrary(libName: string, libPath: string) {
        arduinoChannel.start(`Remove library - ${libName}`);
        util.rmdirRecursivelySync(libPath);
        arduinoChannel.end(`Removed library - ${libName}${os.EOL}`);
    }

    public openExample(example) {
        function tmpName(name) {
            let counter = 0;
            let candidateName = name;
            // eslint-disable-next-line no-constant-condition
            while (true) {
                if (!util.fileExistsSync(candidateName) && !util.directoryExistsSync(candidateName)) {
                    return candidateName;
                }
                counter++;
                candidateName = `${name}_${counter}`;
            }
        }

        // Step 1: Copy the example project to a temporary directory.
        const sketchPath = path.join(this._settings.sketchbookPath, "generated_examples");
        if (!util.directoryExistsSync(sketchPath)) {
            util.mkdirRecursivelySync(sketchPath);
        }
        let destExample = "";
        if (util.directoryExistsSync(example)) {
            destExample = tmpName(path.join(sketchPath, path.basename(example)));
            util.cp(example, destExample);
        } else if (util.fileExistsSync(example)) {
            const exampleName = path.basename(example, path.extname(example));
            destExample = tmpName(path.join(sketchPath, exampleName));
            util.mkdirRecursivelySync(destExample);
            util.cp(example, path.join(destExample, path.basename(example)));
        }
        if (destExample) {
            // Step 2: Scaffold the example project to an arduino project.
            const items = fs.readdirSync(destExample);
            const sketchFile = items.find((item) => {
                return util.isArduinoFile(path.join(destExample, item));
            });
            if (sketchFile) {
                // Generate arduino.json
                const dc = DeviceContext.getInstance();
                const arduinoJson = {
                    sketch: sketchFile,
                    // TODO EW, 2020-02-18: COM1 is Windows specific - what about OSX and Linux users?
                    port: dc.port || "COM1",
                    board: dc.board,
                    configuration: dc.configuration,
                };
                const arduinoConfigFilePath = path.join(destExample, constants.ARDUINO_CONFIG_FILE);
                util.mkdirRecursivelySync(path.dirname(arduinoConfigFilePath));
                fs.writeFileSync(arduinoConfigFilePath, JSON.stringify(arduinoJson, null, 4));
            }

            // Step 3: Open the arduino project at a new vscode window.
            vscode.commands.executeCommand("vscode.openFolder", vscode.Uri.file(destExample), true);
        }
        return destExample;
    }

    public get settings() {
        return this._settings;
    }

    public get boardManager() {
        return this._boardManager;
    }

    public set boardManager(value: BoardManager) {
        this._boardManager = value;
    }

    public get libraryManager() {
        return this._libraryManager;
    }

    public set libraryManager(value: LibraryManager) {
        this._libraryManager = value;
    }

    public get exampleManager() {
        return this._exampleManager;
    }

    public set exampleManager(value: ExampleManager) {
        this._exampleManager = value;
    }

    public get programmerManager() {
        return this._programmerManager;
    }

    public set programmerManager(value: ProgrammerManager) {
        this._programmerManager = value;
    }

    /**
     * Runs the pre or post build command.
     * Usually before one of
     *  * verify
     *  * upload
     *  * upload using programmer
     * @param dc Device context prepared during one of the above actions
     * @param what "pre" if the pre-build command should be run, "post" if the
     * post-build command should be run.
     * @returns True if successful, false on error.
     */
    protected async runPrePostBuildCommand(dc: DeviceContext,
                                           environment: any,
                                           what: "pre" | "post"): Promise<boolean> {
        const cmdline = what === "pre"
            ? dc.prebuild
            : dc.postbuild;

        if (!cmdline) {
            return true; // Successfully done nothing.
        }

        arduinoChannel.info(`Running ${what}-build command: "${cmdline}"`);
        let cmd: string;
        let args: string[];
        // pre-/post-build commands feature full bash support on UNIX systems.
        // On Windows you have full cmd support.
        if (os.platform() === "win32") {
            args = [];
            cmd = cmdline;
        } else {
            args = ["-c", cmdline];
            cmd = "bash";
        }
        try {
            await util.spawn(cmd,
                                args,
                                {
                                    shell: os.platform() === "win32",
                                    cwd: ArduinoWorkspace.rootPath,
                                    env: {...environment},
                                },
                                { channel: arduinoChannel.channel });
        } catch (ex) {
            const msg = ex.error
                ? `${ex.error}`
                : ex.code
                    ? `Exit code = ${ex.code}`
                    : JSON.stringify(ex);
            arduinoChannel.error(`Running ${what}-build command failed: ${os.EOL}${msg}`);
            return false;
        }
        return true;
    }

    /**
     * Checks if the arduino cli is being used
     * @returns {bool} - true if arduino cli is being use
     */
    private useArduinoCli() {
        return this._settings.useArduinoCli;
        // return VscodeSettings.getInstance().useArduinoCli;
    }

    /**
     * Checks if the line contains memory usage information
     * @param line output line to check
     * @returns {bool} true if line contains memory usage information
     */
    private isMemoryUsageInformation(line: string) {
        return line.startsWith("Sketch uses ") || line.startsWith("Global variables use ");
    }

    /**
     * Private implementation. Not to be called directly. The wrapper build()
     * manages the build state.
     * @param buildMode See build()
     * @param buildDir See build()
     * @see https://github.com/arduino/Arduino/blob/master/build/shared/manpage.adoc
     */
    private async _build(buildMode: BuildMode, buildDir?: string): Promise<boolean> {
        const dc = DeviceContext.getInstance();
        const args: string[] = [];
        let restoreSerialMonitor: boolean = false;
        const verbose = VscodeSettings.getInstance().logLevel === constants.LogLevel.Verbose;

        if (!this.boardManager.currentBoard) {
            if (buildMode !== BuildMode.Analyze) {
                logger.notifyUserError("boardManager.currentBoard", new Error(constants.messages.NO_BOARD_SELECTED));
            }
            return false;
        }
        const boardDescriptor = this.boardManager.currentBoard.getBuildConfig();

        if (this.useArduinoCli()) {
            args.push("-b", boardDescriptor);
        } else {
            args.push("--board", boardDescriptor);
        }

        if (!ArduinoWorkspace.rootPath) {
            vscode.window.showWarningMessage("Workspace doesn't seem to have a folder added to it yet.");
            return false;
        }

        if (!dc.sketch || !util.fileExistsSync(path.join(ArduinoWorkspace.rootPath, dc.sketch))) {
            if (buildMode === BuildMode.Analyze) {
                // Analyze runs non interactively
                return false;
            }
            if (!await dc.resolveMainSketch()) {
                vscode.window.showErrorMessage("No sketch file was found. Please specify the sketch in the arduino.json file");
                return false;
            }
        }

        const selectSerial = async () => {
            const choice = await vscode.window.showInformationMessage(
                "Serial port is not specified. Do you want to select a serial port for uploading?",
                "Yes", "No");
            if (choice === "Yes") {
                vscode.commands.executeCommand("arduino.selectSerialPort");
            }
        }

        if (buildMode === BuildMode.Upload) {
            if ((!dc.configuration || !/upload_method=[^=,]*st[^,]*link/i.test(dc.configuration)) && !dc.port) {
                await selectSerial();
                return false;
            }

            if (this.useArduinoCli()) {
                args.push("compile", "--upload");
            } else {
                args.push("--upload");
            }

            if (dc.port) {
                args.push("--port", dc.port);
            }
        } else if (buildMode === BuildMode.CliUpload) {
            if ((!dc.configuration || !/upload_method=[^=,]*st[^,]*link/i.test(dc.configuration)) && !dc.port) {
                await selectSerial();
                return false;
            }

            if (!this.useArduinoCli()) {
                arduinoChannel.error("This command is only available when using the Arduino CLI");
                return false;
            }

            args.push("upload");

            if (dc.port) {
                args.push("--port", dc.port);
            }
        } else if (buildMode === BuildMode.UploadProgrammer) {
            const programmer = this.programmerManager.currentProgrammer;
            if (!programmer) {
                logger.notifyUserError("programmerManager.currentProgrammer", new Error(constants.messages.NO_PROGRAMMMER_SELECTED));
                return false;
            }
            if (!dc.port) {
                await selectSerial();
                return false;
            }

            if (this.useArduinoCli()) {
                args.push("compile",
                    "--upload",
                    "--programmer", programmer);
            } else {
                args.push("--upload",
                    "--useprogrammer",
                    "--pref", `programmer=${programmer}`);
            }

            args.push("--port", dc.port);
        } else if (buildMode === BuildMode.CliUploadProgrammer) {
            const programmer = this.programmerManager.currentProgrammer;
            if (!programmer) {
                logger.notifyUserError("programmerManager.currentProgrammer", new Error(constants.messages.NO_PROGRAMMMER_SELECTED));
                return false;
            }
            if (!dc.port) {
                await selectSerial();
                return false;
            }
            if (!this.useArduinoCli()) {
                arduinoChannel.error("This command is only available when using the Arduino CLI");
                return false;
            }

            args.push("upload",
                "--programmer", programmer,
                "--port", dc.port);
        } else {
            if (this.useArduinoCli()) {
                args.unshift("compile");
            } else {
                args.push("--verify");
            }
        }

        if (dc.buildPreferences) {
            for (const pref of dc.buildPreferences) {
                // Note: BuildPrefSetting makes sure that each preference
                // value consists of exactly two items (key and value).
                if (this.useArduinoCli()) {
                    args.push("--build-property", `${pref[0]}=${pref[1]}`);
                } else {
                    args.push("--pref", `${pref[0]}=${pref[1]}`);
                }
            }
        }

        // We always build verbosely but filter the output based on the settings

        this._settings.useArduinoCli ? args.push("--verbose") : args.push("--verbose-build");

        if (verbose && !this._settings.useArduinoCli) {
            args.push("--verbose-upload");
        }

        await vscode.workspace.saveAll(false);

        // we prepare the channel here since all following code will
        // or at leas can possibly output to it
        arduinoChannel.show();
        if (VscodeSettings.getInstance().clearOutputOnBuild) {
            arduinoChannel.clear();
        }
        arduinoChannel.start(`${buildMode} sketch '${dc.sketch}'`);

        if (buildDir || dc.output) {
            // 2020-02-29, EW: This whole code appears a bit wonky to me.
            //   What if the user specifies an output directory "../builds/my project"

            // the first choice of the path should be from the users explicit settings.
            if (dc.output) {
                buildDir = path.resolve(ArduinoWorkspace.rootPath, dc.output);
            } else {
                buildDir = path.resolve(ArduinoWorkspace.rootPath, buildDir);
            }

            const dirPath = path.dirname(buildDir);
            if (!util.directoryExistsSync(dirPath)) {
                util.mkdirRecursivelySync(dirPath);
            }

            if (this.useArduinoCli()) {
                args.push("--build-path", buildDir);

            } else {
                args.push("--pref", `build.path=${buildDir}`);
            }

            arduinoChannel.info(`Please see the build logs in output path: ${buildDir}`);
        } else {
            const msg = "Output path is not specified. Unable to reuse previously compiled files. Build will be slower. See README.";
            arduinoChannel.warning(msg);
        }

        // Environment variables passed to pre- and post-build commands
        const env = {
            VSCA_BUILD_MODE: buildMode,
            VSCA_SKETCH: dc.sketch,
            VSCA_BOARD: boardDescriptor,
            VSCA_WORKSPACE_DIR: ArduinoWorkspace.rootPath,
            VSCA_LOG_LEVEL: verbose ? constants.LogLevel.Verbose : constants.LogLevel.Info,
        };
        if (dc.port) {
            env["VSCA_SERIAL"] = dc.port;
        }
        if (buildDir) {
            env["VSCA_BUILD_DIR"] = buildDir;
        }

        // TODO EW: What should we do with pre-/post build commands when running
        //   analysis? Some could use it to generate/manipulate code which could
        //   be a prerequisite for a successful build
        if (!await this.runPrePostBuildCommand(dc, env, "pre")) {
            return false;
        }

        // stop serial monitor when everything is prepared and good
        // what makes restoring of its previous state easier
        if (buildMode === BuildMode.Upload ||
            buildMode === BuildMode.UploadProgrammer ||
            buildMode === BuildMode.CliUpload ||
            buildMode === BuildMode.CliUploadProgrammer) {
            restoreSerialMonitor = await SerialMonitor.getInstance().closeSerialMonitor(dc.port);
            UsbDetector.getInstance().pauseListening();
        }

        // Push sketch as last argument
        args.push(path.join(ArduinoWorkspace.rootPath, dc.sketch));

        const cocopa = makeCompilerParserContext(dc);

        const cleanup = async (result: "ok" | "error") => {
            let ret = true;
            if (result === "ok") {
                ret = await this.runPrePostBuildCommand(dc, env, "post");
            }
            await cocopa.conclude();
            if (buildMode === BuildMode.Upload || buildMode === BuildMode.UploadProgrammer) {
                UsbDetector.getInstance().resumeListening();
                if (restoreSerialMonitor) {
                    await SerialMonitor.getInstance().openSerialMonitor();
                }
            }
            return ret;
        }
        const stdoutcb = (line: string) => {
            if (cocopa.callback) {
                cocopa.callback(line);
            }
            if (verbose) {
                arduinoChannel.channel.append(line);
            } else {
                // Output sketch memory usage in non-verbose mode
                if (this.isMemoryUsageInformation(line)) {
                    arduinoChannel.channel.append(line);
                }
            }
        }
        const stderrcb = (line: string) => {
            if (os.platform() === "win32") {
                line = line.trim();
                if (line.length <= 0) {
                    return;
                }
                line = line.replace(/(?:\r|\r\n|\n)+/g, os.EOL);
                line = `${line}${os.EOL}`;
            }
            if (!verbose) {
                // Don't spill log with spurious info from the backend. This
                // list could be fetched from a config file to accommodate
                // messages of unknown board packages, newer backend revisions
                const filters = [
                    /^Picked\sup\sJAVA_TOOL_OPTIONS:\s+/,
                    /^\d+\d+-\d+-\d+T\d+:\d+:\d+.\d+Z\s(?:INFO|WARN)\s/,
                    /^(?:DEBUG|TRACE|INFO)\s+/,
                ];
                for (const f of filters) {
                    if (line.match(f)) {
                        return;
                    }
                }
            }
            arduinoChannel.channel.append(line);
        }

        return await util.spawn(
            this._settings.commandPath,
            args,
            { cwd: ArduinoWorkspace.rootPath },
            { /*channel: arduinoChannel.channel,*/ stdout: stdoutcb, stderr: stderrcb },
        ).then(async () => {
            const ret = await cleanup("ok");
            if (ret) {
                arduinoChannel.end(`${buildMode} sketch '${dc.sketch}'${os.EOL}`);
            }
            return ret;
        }, async (reason) => {
            await cleanup("error");
            const msg = reason.code
                ? `Exit with code=${reason.code}`
                : JSON.stringify(reason);
            arduinoChannel.error(`${buildMode} sketch '${dc.sketch}': ${msg}${os.EOL}`);
            return false;
        });
    }
}