"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());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.prepareClientConnection = exports.TestStream = exports.diagnostics = exports.logLevel2Severity = exports.logLevels = void 0;
const chalk_1 = __importDefault(require("chalk"));
const chokidar_1 = __importDefault(require("chokidar"));
const fast_glob_1 = __importDefault(require("fast-glob"));
const fs_1 = __importDefault(require("fs"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const stream_1 = require("stream");
const vls_1 = require("vls");
const node_1 = require("vscode-languageserver/node");
const vscode_uri_1 = require("vscode-uri");
const logger_1 = require("../../logger");
const initParams_1 = require("./initParams");
const FileDiagnosticManager_1 = require("../../FileDiagnosticManager");
var DOC_VERSION;
(function (DOC_VERSION) {
    DOC_VERSION[DOC_VERSION["init"] = -1] = "init";
})(DOC_VERSION || (DOC_VERSION = {}));
exports.logLevels = ['ERROR', 'WARN', 'INFO', 'HINT'];
let disposeSuppressConsole;
let initialVueFilesCount = 0;
let initialVueFilesTick = 0;
const fileDiagnosticManager = new FileDiagnosticManager_1.FileDiagnosticManager();
exports.logLevel2Severity = {
    ERROR: node_1.DiagnosticSeverity.Error,
    WARN: node_1.DiagnosticSeverity.Warning,
    INFO: node_1.DiagnosticSeverity.Information,
    HINT: node_1.DiagnosticSeverity.Hint,
};
function diagnostics(workspace, logLevel, options = { watch: false, verbose: false, config: null }) {
    var _a;
    return __awaiter(this, void 0, void 0, function* () {
        if (options.verbose) {
            console.log('====================================');
            console.log('Getting Vetur diagnostics');
        }
        let workspaceUri;
        if (workspace) {
            const absPath = path_1.default.resolve(process.cwd(), workspace);
            console.log(`Loading Vetur in workspace path: ${chalk_1.default.green(absPath)}`);
            workspaceUri = vscode_uri_1.URI.file(absPath);
        }
        else {
            console.log(`Loading Vetur in current directory: ${chalk_1.default.green(process.cwd())}`);
            workspaceUri = vscode_uri_1.URI.file(process.cwd());
        }
        const result = yield getDiagnostics(workspaceUri, exports.logLevel2Severity[logLevel], options);
        if (options.verbose) {
            console.log('====================================');
        }
        // dispatch error summary in build mode
        if (!options.watch && typeof result === 'object' && result !== null) {
            const { initialErrorCount, initialWarningCount } = result;
            (_a = options === null || options === void 0 ? void 0 : options.onDispatchDiagnosticsSummary) === null || _a === void 0 ? void 0 : _a.call(options, initialErrorCount, initialWarningCount);
            process.exit(initialErrorCount > 0 ? 1 : 0);
        }
    });
}
exports.diagnostics = diagnostics;
class NullLogger {
    error(_message) { }
    warn(_message) { }
    info(_message) { }
    log(_message) { }
}
class TestStream extends stream_1.Duplex {
    _write(chunk, _encoding, done) {
        this.emit('data', chunk);
        done();
    }
    _read(_size) { }
}
exports.TestStream = TestStream;
function suppressConsole() {
    let disposed = false;
    const rawConsoleLog = console.log;
    console.log = () => { };
    return () => {
        if (disposed)
            return;
        disposed = true;
        console.log = rawConsoleLog;
    };
}
function prepareClientConnection(workspaceUri, severity, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const up = new TestStream();
        const down = new TestStream();
        const logger = new NullLogger();
        const clientConnection = (0, node_1.createProtocolConnection)(new node_1.StreamMessageReader(down), new node_1.StreamMessageWriter(up), logger);
        const serverConnection = (0, node_1.createConnection)(new node_1.StreamMessageReader(up), new node_1.StreamMessageWriter(down));
        // hijack sendDiagnostics
        serverConnection.sendDiagnostics = (publishDiagnostics) => __awaiter(this, void 0, void 0, function* () {
            var _a, _b;
            disposeSuppressConsole === null || disposeSuppressConsole === void 0 ? void 0 : disposeSuppressConsole();
            if (publishDiagnostics.version === DOC_VERSION.init) {
                return;
            }
            const absFilePath = vscode_uri_1.URI.parse(publishDiagnostics.uri).fsPath;
            publishDiagnostics.diagnostics = filterDiagnostics(publishDiagnostics.diagnostics, severity);
            const nextDiagnosticInFile = yield (0, logger_1.normalizePublishDiagnosticParams)(publishDiagnostics);
            fileDiagnosticManager.updateByFileId(absFilePath, nextDiagnosticInFile);
            const normalized = fileDiagnosticManager.getDiagnostics();
            const errorCount = normalized.filter((d) => d.level === node_1.DiagnosticSeverity.Error).length;
            const warningCount = normalized.filter((d) => d.level === node_1.DiagnosticSeverity.Warning).length;
            initialVueFilesTick++;
            // only starts to log when all .vue files are loaded
            // onDispatchDiagnostics will dispatch diagnostics of all watched files
            if (initialVueFilesTick >= initialVueFilesCount) {
                (_a = options.onDispatchDiagnostics) === null || _a === void 0 ? void 0 : _a.call(options, normalized);
                (_b = options.onDispatchDiagnosticsSummary) === null || _b === void 0 ? void 0 : _b.call(options, errorCount, warningCount);
            }
        });
        const vls = new vls_1.VLS(serverConnection);
        vls.validateTextDocument = (textDocument, cancellationToken) => __awaiter(this, void 0, void 0, function* () {
            const diagnostics = yield vls.doValidate(textDocument, cancellationToken);
            if (diagnostics) {
                // @ts-expect-error
                vls.lspConnection.sendDiagnostics({
                    uri: textDocument.uri,
                    version: textDocument.version,
                    diagnostics,
                });
            }
        });
        serverConnection.onInitialize((params) => __awaiter(this, void 0, void 0, function* () {
            yield vls.init(params);
            if (options.verbose) {
                console.log('Vetur initialized');
                console.log('====================================');
            }
            return {
                capabilities: vls.capabilities,
            };
        }));
        vls.listen();
        clientConnection.listen();
        const initParams = (0, initParams_1.getInitParams)(workspaceUri);
        if (options.config) {
            // Merge in used-provided VLS settings if present
            mergeDeep(initParams.initializationOptions.config, options.config);
        }
        yield clientConnection.sendRequest(node_1.InitializeRequest.type, initParams);
        return { clientConnection, serverConnection, vls, up, down, logger };
    });
}
exports.prepareClientConnection = prepareClientConnection;
function extToGlobs(exts) {
    return exts.map((e) => '**/*' + e);
}
const watchedDidChangeContent = ['.vue'];
const watchedDidChangeWatchedFiles = ['.js', '.ts', '.json'];
const watchedDidChangeContentGlob = extToGlobs(watchedDidChangeContent);
function getDiagnostics(workspaceUri, severity, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const { clientConnection } = yield prepareClientConnection(workspaceUri, severity, options);
        const files = fast_glob_1.default.sync([...watchedDidChangeContentGlob], {
            cwd: workspaceUri.fsPath,
            ignore: ['node_modules/**'],
        });
        if (files.length === 0) {
            console.log('[VLS checker] No input files');
            return { initialWarningCount: 0, initialErrorCount: 0 };
        }
        if (options.verbose) {
            console.log('');
            console.log('Getting diagnostics from: ', files, '\n');
        }
        const absFilePaths = files.map((f) => path_1.default.resolve(workspaceUri.fsPath, f));
        // VLS will stdout verbose log, suppress console before any serverConnection
        disposeSuppressConsole = suppressConsole();
        initialVueFilesCount = absFilePaths.length;
        let initialErrorCount = 0;
        let initialWarningCount = 0;
        yield Promise.all(absFilePaths.map((absFilePath) => __awaiter(this, void 0, void 0, function* () {
            // serve mode - step 1
            // build mode - step 1
            // report all existing files from client side to server with type `DidOpenTextDocumentNotification.type`
            const fileText = yield fs_1.default.promises.readFile(absFilePath, 'utf-8');
            clientConnection.sendNotification(node_1.DidOpenTextDocumentNotification.type, {
                textDocument: {
                    languageId: 'vue',
                    uri: vscode_uri_1.URI.file(absFilePath).toString(),
                    version: DOC_VERSION.init,
                    text: fileText,
                },
            });
            // build mode - step 2
            // use $/getDiagnostics to get diagnostics from server side directly
            if (!options.watch) {
                try {
                    let diagnostics = (yield clientConnection.sendRequest('$/getDiagnostics', {
                        uri: vscode_uri_1.URI.file(absFilePath).toString(),
                        version: DOC_VERSION.init,
                    }));
                    diagnostics = filterDiagnostics(diagnostics, severity);
                    let logChunk = '';
                    if (diagnostics.length > 0) {
                        logChunk +=
                            os_1.default.EOL +
                                diagnostics
                                    .map((d) => (0, logger_1.diagnosticToTerminalLog)((0, logger_1.normalizeLspDiagnostic)({
                                    diagnostic: d,
                                    absFilePath,
                                    fileText,
                                }), 'VLS'))
                                    .join(os_1.default.EOL);
                        diagnostics.forEach((d) => {
                            if (d.severity === node_1.DiagnosticSeverity.Error) {
                                initialErrorCount++;
                            }
                            if (d.severity === node_1.DiagnosticSeverity.Warning) {
                                initialWarningCount++;
                            }
                        });
                    }
                    console.log(logChunk);
                    return { initialErrorCount, initialWarningCount };
                }
                catch (err) {
                    console.error(err.stack);
                    return { initialErrorCount, initialWarningCount };
                }
            }
        })));
        if (!options.watch) {
            return { initialErrorCount, initialWarningCount };
        }
        // serve mode - step 2
        // watch files (.vue,.js,.ts,.json) change and send notification to server
        yield Promise.all(absFilePaths.map((absFilePath) => __awaiter(this, void 0, void 0, function* () {
            const fileText = yield fs_1.default.promises.readFile(absFilePath, 'utf-8');
            clientConnection.sendNotification(node_1.DidOpenTextDocumentNotification.type, {
                textDocument: {
                    languageId: 'vue',
                    uri: vscode_uri_1.URI.file(absFilePath).toString(),
                    version: DOC_VERSION.init,
                    text: fileText,
                },
            });
        })));
        const watcher = chokidar_1.default.watch([], {
            ignored: (path) => path.includes('node_modules'),
        });
        watcher.add(workspaceUri.fsPath);
        watcher.on('all', (event, filePath) => __awaiter(this, void 0, void 0, function* () {
            const extname = path_1.default.extname(filePath);
            // .vue file changed
            if (!filePath.endsWith('.vue'))
                return;
            const fileContent = yield fs_1.default.promises.readFile(filePath, 'utf-8');
            clientConnection.sendNotification(node_1.DidChangeTextDocumentNotification.type, {
                textDocument: {
                    uri: vscode_uri_1.URI.file(filePath).toString(),
                    version: Date.now(),
                },
                contentChanges: [{ text: fileContent }],
            });
            // .js,.ts,.json file changed
            if (watchedDidChangeWatchedFiles.includes(extname)) {
                clientConnection.sendNotification(node_1.DidChangeWatchedFilesNotification.type, {
                    changes: [
                        {
                            uri: vscode_uri_1.URI.file(filePath).toString(),
                            type: event === 'add' ? 1 : event === 'unlink' ? 3 : 2,
                        },
                    ],
                });
            }
        }));
        return null;
    });
}
function isObject(item) {
    return item && typeof item === 'object' && !Array.isArray(item);
}
function mergeDeep(target, source) {
    if (isObject(target) && isObject(source)) {
        for (const key in source) {
            if (isObject(source[key])) {
                if (!target[key])
                    Object.assign(target, { [key]: {} });
                mergeDeep(target[key], source[key]);
            }
            else {
                Object.assign(target, { [key]: source[key] });
            }
        }
    }
    return target;
}
function filterDiagnostics(diagnostics, severity) {
    /**
     * Ignore eslint errors for now
     */
    return diagnostics
        .filter((r) => r.source !== 'eslint-plugin-vue')
        .filter((r) => r.severity && r.severity <= severity);
}
