"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
const path = require("path");
const os = require("os");
const temp = require("temp");
const decorators_1 = require("../decorators");
const constants_1 = require("../constants");
temp.track();
class TypeScriptService {
    constructor($childProcess, $fs, $logger, $npmService, $options, $projectConstants, $processService, $errors) {
        this.$childProcess = $childProcess;
        this.$fs = $fs;
        this.$logger = $logger;
        this.$npmService = $npmService;
        this.$options = $options;
        this.$projectConstants = $projectConstants;
        this.$processService = $processService;
        this.$errors = $errors;
    }
    transpile(projectDir, typeScriptFiles, definitionFiles, options) {
        return (() => {
            options = options || {};
            let compilerOptions = this.getCompilerOptions(projectDir, options).wait();
            let typeScriptCompilerSettings = this.getTypeScriptCompilerSettings({ useLocalTypeScriptCompiler: options.useLocalTypeScriptCompiler }).wait();
            this.noEmitOnError = compilerOptions.noEmitOnError;
            this.typeScriptFiles = typeScriptFiles || [];
            this.definitionFiles = definitionFiles || [];
            let runTranspilationOptions = { compilerOptions };
            if (this.typeScriptFiles.length > 0) {
                let typeScriptDefinitionsFiles = [];
                if (!this.hasTsConfigFile(projectDir).wait()) {
                    typeScriptDefinitionsFiles = this.getDefaultTypeScriptDefinitionsFiles(options.pathToDefaultDefinitionFiles).wait();
                }
                typeScriptDefinitionsFiles = typeScriptDefinitionsFiles.concat(this.getTypeScriptFilesData(projectDir).wait().definitionFiles);
                let filesToTranspile = this.typeScriptFiles.concat(typeScriptDefinitionsFiles);
                this.$logger.out("Compiling...".yellow);
                _.each(this.typeScriptFiles, file => {
                    this.$logger.out(`### Compile ${file}`.cyan);
                });
                runTranspilationOptions.filesToTranspile = filesToTranspile;
            }
            this.$logger.out(`Using tsc version ${typeScriptCompilerSettings.version}`.cyan);
            return this.runTranspilation(projectDir, typeScriptCompilerSettings.pathToCompiler, runTranspilationOptions).wait();
        }).future()();
    }
    getTypeScriptFilesData(projectDir) {
        return (() => {
            let rootNodeModules = path.join(projectDir, constants_1.NODE_MODULES_DIR_NAME);
            let projectFiles = this.$fs.enumerateFilesInDirectorySync(projectDir, (fileName, fstat) => fileName !== rootNodeModules);
            let typeScriptFiles = _.filter(projectFiles, this.isTypeScriptFile);
            let definitionFiles = _.filter(typeScriptFiles, file => _.endsWith(file, constants_1.FileExtensions.TYPESCRIPT_DEFINITION_FILE));
            return { definitionFiles: definitionFiles, typeScriptFiles: _.difference(typeScriptFiles, definitionFiles) };
        }).future()();
    }
    isTypeScriptProject(projectDir) {
        return (() => {
            let typeScriptFilesData = this.getTypeScriptFilesData(projectDir).wait();
            return !!typeScriptFilesData.typeScriptFiles.length;
        }).future()();
    }
    isTypeScriptFile(file) {
        return path.extname(file) === constants_1.FileExtensions.TYPESCRIPT_FILE;
    }
    hasTsConfigFile(projectDir) {
        return this.$fs.exists(this.getPathToTsConfigFile(projectDir));
    }
    getPathToTsConfigFile(projectDir) {
        return path.join(projectDir, this.$projectConstants.TSCONFIG_JSON_NAME);
    }
    getCompilerOptions(projectDir, options) {
        return (() => {
            let tsConfigFile;
            let pathToConfigJsonFile = this.getPathToTsConfigFile(projectDir);
            if (this.hasTsConfigFile(projectDir).wait()) {
                tsConfigFile = this.$fs.readJson(pathToConfigJsonFile).wait();
            }
            tsConfigFile = tsConfigFile || { compilerOptions: {} };
            let compilerOptions = options.compilerOptions || {};
            let defaultOptions = options.defaultCompilerOptions || {};
            let compilerOptionsKeys = _.union(_.keys(compilerOptions), _.keys(tsConfigFile.compilerOptions), _.keys(defaultOptions));
            let result = {};
            _.each(compilerOptionsKeys, (key) => {
                result[key] = this.getCompilerOptionByKey(key, compilerOptions, tsConfigFile.compilerOptions, defaultOptions);
            });
            result.noEmitOnError = result.noEmitOnError || false;
            return result;
        }).future()();
    }
    getCompilerOptionByKey(key, compilerOptions, tsConfigFileOptions, defaultOptions) {
        if (_.has(compilerOptions, key)) {
            return compilerOptions[key];
        }
        if (_.has(tsConfigFileOptions, key)) {
            return tsConfigFileOptions[key];
        }
        return defaultOptions[key];
    }
    getTypeScriptCompilerSettings(options) {
        return (() => {
            let typeScriptInNodeModulesDir = path.join(constants_1.NODE_MODULES_DIR_NAME, TypeScriptService.TYPESCRIPT_MODULE_NAME);
            if (!this.typeScriptModuleFilePath) {
                if (options.useLocalTypeScriptCompiler) {
                    let typeScriptJsFilePath = require.resolve(TypeScriptService.TYPESCRIPT_MODULE_NAME);
                    this.typeScriptModuleFilePath = typeScriptJsFilePath.substring(0, typeScriptJsFilePath.indexOf(typeScriptInNodeModulesDir) + typeScriptInNodeModulesDir.length);
                }
                else {
                    let typeScriptModuleInstallationDir = this.createTempDirectoryForTsc().wait();
                    let pluginToInstall = {
                        name: TypeScriptService.TYPESCRIPT_MODULE_NAME,
                        version: TypeScriptService.DEFAULT_TSC_VERSION,
                        installTypes: false
                    };
                    this.$npmService.install(typeScriptModuleInstallationDir, pluginToInstall).wait();
                    this.typeScriptModuleFilePath = path.join(typeScriptModuleInstallationDir, typeScriptInNodeModulesDir);
                }
            }
            let typeScriptCompilerPath = path.join(this.typeScriptModuleFilePath, "lib", "tsc");
            let typeScriptCompilerVersion = this.$fs.readJson(path.join(this.typeScriptModuleFilePath, this.$projectConstants.PACKAGE_JSON_NAME)).wait().version;
            return { pathToCompiler: typeScriptCompilerPath, version: typeScriptCompilerVersion };
        }).future()();
    }
    runTranspilation(projectDir, typeScriptCompilerPath, options) {
        return (() => {
            options = options || {};
            let startTime = new Date().getTime();
            let params = _([])
                .concat(typeScriptCompilerPath)
                .concat(options.filesToTranspile || [])
                .concat(this.getTypeScriptCompilerOptionsAsArguments(options.compilerOptions) || [])
                .value();
            let output = this.$childProcess.spawnFromEvent(process.argv[0], params, "close", { cwd: projectDir }, { throwError: false }).wait();
            let compilerOutput = output.stderr || output.stdout;
            let compilerMessages = this.getCompilerMessages(compilerOutput);
            this.logCompilerMessages(compilerMessages, compilerOutput);
            let endTime = new Date().getTime();
            let time = (endTime - startTime) / 1000;
            this.$logger.out(`${os.EOL}Success: ${time.toFixed(2)}s${os.EOL}.`.green);
            this.startWatchProcess(params, projectDir);
            return compilerOutput;
        }).future()();
    }
    startWatchProcess(params, projectDir) {
        if (!this._watchProcess && this.$options.watch) {
            params.push("--watch");
            this._watchProcess = this.$childProcess.spawn(process.argv[0], params, { cwd: projectDir });
            this.$processService.attachToProcessExitSignals(null, this._watchProcess.kill);
        }
    }
    getCompilerMessages(compilerOutput) {
        let level1ErrorCount = 0, level5ErrorCount = 0, nonEmitPreventingWarningCount = 0;
        let hasPreventEmitErrors = _.reduce(compilerOutput.split("\n"), (memo, errorMsg) => {
            let isPreventEmitError = !!this.noEmitOnError;
            if (errorMsg.search(/error TS1\d+:/) >= 0) {
                level1ErrorCount += 1;
            }
            else if (errorMsg.search(/error TS5\d+:/) >= 0) {
                level5ErrorCount += 1;
            }
            else if (errorMsg.search(/error TS\d+:/) >= 0) {
                nonEmitPreventingWarningCount += 1;
            }
            return memo || isPreventEmitError;
        }, false) || false;
        return {
            level1ErrorCount,
            level5ErrorCount,
            nonEmitPreventingWarningCount,
            hasPreventEmitErrors
        };
    }
    logCompilerMessages(compilerMessages, errorMessage) {
        let level1ErrorCount = compilerMessages.level1ErrorCount, level5ErrorCount = compilerMessages.level5ErrorCount, nonEmitPreventingWarningCount = compilerMessages.nonEmitPreventingWarningCount, hasPreventEmitErrors = compilerMessages.hasPreventEmitErrors;
        if (level1ErrorCount + level5ErrorCount + nonEmitPreventingWarningCount > 0) {
            let colorizedMessage = (level1ErrorCount + level5ErrorCount > 0) ? ">>>".red : ">>>".green;
            this.$logger.out(colorizedMessage);
            let errorTitle = "";
            if (level5ErrorCount > 0) {
                errorTitle += this.composeErrorTitle(level5ErrorCount, "compiler flag error");
            }
            if (level1ErrorCount > 0) {
                errorTitle += this.composeErrorTitle(level1ErrorCount, "syntax error");
            }
            if (nonEmitPreventingWarningCount > 0) {
                if (!level1ErrorCount && !level5ErrorCount && this.noEmitOnError) {
                    errorTitle += this.composeErrorTitle(nonEmitPreventingWarningCount, "non-emit-preventing type errors, but output is not generated as noEmitOnError option is true.");
                }
                else {
                    errorTitle += this.composeErrorTitle(nonEmitPreventingWarningCount, "non-emit-preventing type warning");
                }
            }
            if (hasPreventEmitErrors) {
                this.$errors.failWithoutHelp(`${os.EOL}${errorTitle}${os.EOL}${errorMessage.red}${os.EOL}${">>> ".red}`);
            }
            else {
                this.$logger.out(errorTitle);
                this.$logger.warn(errorMessage);
                this.$logger.out(">>>".green);
            }
        }
    }
    composeErrorTitle(count, title) {
        return `${count} ${title}${(count === 1) ? '' : 's'} ${os.EOL}`;
    }
    getTypeScriptCompilerOptionsAsArguments(options) {
        this.noEmitOnError = options.noEmitOnError;
        return _(options)
            .keys()
            .map((option) => {
            let value = options[option];
            if (typeof (value) === "string") {
                return [`--${option}`, value];
            }
            else if (value) {
                return [`--${option}`];
            }
            else {
                return null;
            }
        })
            .filter(argument => !!argument)
            .flatten()
            .value();
    }
    getDefaultTypeScriptDefinitionsFiles(defaultTypeScriptDefinitionsFilesPath) {
        return (() => {
            if (!this.$fs.exists(defaultTypeScriptDefinitionsFilesPath).wait()) {
                return [];
            }
            let defaultDefinitionsFiles = this.$fs.readDirectory(defaultTypeScriptDefinitionsFilesPath).wait();
            let remainingDefaultDefinitionFiles = _.filter(defaultDefinitionsFiles, defFile => !_.some(this.definitionFiles, f => path.basename(f) === defFile));
            return _.map(remainingDefaultDefinitionFiles, (definitionFilePath) => {
                return path.join(defaultTypeScriptDefinitionsFilesPath, definitionFilePath);
            }).concat(this.definitionFiles);
        }).future()();
    }
    createTempDirectoryForTsc() {
        return (() => {
            let tempDir = temp.mkdirSync(`typescript-compiler-${TypeScriptService.DEFAULT_TSC_VERSION}`);
            this.$fs.writeJson(path.join(tempDir, this.$projectConstants.PACKAGE_JSON_NAME), { name: "tsc-container", version: "1.0.0" }).wait();
            return tempDir;
        }).future()();
    }
}
TypeScriptService.DEFAULT_TSC_VERSION = "1.8.10";
TypeScriptService.TYPESCRIPT_MODULE_NAME = "typescript";
__decorate([
    decorators_1.exportedPromise("typeScriptService")
], TypeScriptService.prototype, "transpile", null);
exports.TypeScriptService = TypeScriptService;
$injector.register("typeScriptService", TypeScriptService);
