"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
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.ImpactDetectionService = void 0;
const fs_1 = __importDefault(require("fs"));
const path = __importStar(require("path"));
const lsp = __importStar(require("vscode-languageserver-protocol/node"));
const winston_1 = __importDefault(require("winston"));
const parse_diff_1 = __importDefault(require("parse-diff"));
function rangeToString(range) {
    return `${range.start} - ${range.end}`;
}
function parseDiff(text) {
    return (0, parse_diff_1.default)(text);
}
class ImpactDetectionService {
    constructor(workspace) {
        this.workspace = workspace;
        this.lspServices = [];
        this.logger = winston_1.default.createLogger({
            transports: [
                new winston_1.default.transports.Console(),
            ],
            level: 'info',
            format: winston_1.default.format.simple(),
        });
    }
    addLspService(service) {
        // service.workspace = this.workspace;
        this.lspServices.push(service);
    }
    stopAllLspServices() {
        return __awaiter(this, void 0, void 0, function* () {
            for (const lspService of this.lspServices) {
                if (lspService.isStarted) {
                    yield lspService.stop();
                }
            }
        });
    }
    getDocumentSymbols(filePath) {
        return __awaiter(this, void 0, void 0, function* () {
            const lspService = this.lspServices.find(e => e.isMatch(filePath));
            if (!lspService) {
                return null;
            }
            if (!lspService.isStarted) {
                yield lspService.start();
            }
            this.logger.debug(`start to parse ${filePath} with ${lspService.constructor.name}`);
            return yield lspService.getDocumentSymbols(filePath);
        });
    }
    static sortAndFilterSymbols(symbols) {
        // TODO: The black/white list should not be hardcoded
        const whiteListKinds = new Set([lsp.SymbolKind.Constant, lsp.SymbolKind.Constructor, lsp.SymbolKind.Function, lsp.SymbolKind.Method, lsp.SymbolKind.Property]);
        const blackListKinds = new Set([]);
        symbols.sort((a, b) => {
            const d = a.location.range.start.line - b.location.range.start.line;
            return d ? d : (b.location.range.end.line - a.location.range.end.line);
        });
        // start to filter symbols
        const filteredSymbols = [];
        let previousEndLine = -1;
        let shouldSkip = false;
        for (const symbol of symbols) {
            if (symbol.location.range.end.line > previousEndLine) { // new segment
                shouldSkip = blackListKinds.has(symbol.kind); // reset shouldSkip
            }
            if (!shouldSkip && whiteListKinds.has(symbol.kind)) {
                filteredSymbols.push(symbol);
                previousEndLine = symbol.location.range.end.line;
                shouldSkip = true; // the overlapped ones should be skipped
            }
        }
        return filteredSymbols;
    }
    findImpactLinesByDiff(diff) {
        return __awaiter(this, void 0, void 0, function* () {
            // 1. parse the diff content
            const diffFiles = parseDiff(diff);
            // 2. for each modified files, use lsp to parse it (skip deleted file and new file)
            // 3. for each new lines and deleted lines, find their max belonging execution block (method/function/const)
            const impactedLinesList = [];
            for (const diffFile of diffFiles) {
                this.logger.info(`Finding impacted lines: ${diffFile.to}`);
                // skip new/deleted file
                if (diffFile.new || diffFile.deleted)
                    continue;
                const documentSymbols = yield this.getDocumentSymbols(diffFile.to);
                if (!documentSymbols)
                    continue;
                const impactedLines = { filePath: diffFile.to, chunks: [] };
                const sortedSymbols = ImpactDetectionService.sortAndFilterSymbols(documentSymbols);
                const chunks = [...diffFile.chunks];
                // here symbols and chunks are all sorted!
                let i = 0;
                let j = 0;
                while (i < sortedSymbols.length && j < chunks.length) {
                    const symbol = sortedSymbols[i];
                    // the line of symbol start from 0
                    const symbolRange = { start: symbol.location.range.start.line, end: symbol.location.range.end.line };
                    const chunk = chunks[j];
                    // the line of chunk start from 1, so we need to -1 to align with symbol's
                    // and also the chunk is half-open inverval [start, end), so the end should -1 to get the actual line
                    const chunkRange = { start: chunk.newStart - 1, end: chunk.newStart - 1 + chunk.newLines - 1 };
                    this.logger.debug(`symbol: ${rangeToString(symbolRange)}, chunk: ${rangeToString(chunkRange)}`);
                    if (symbolRange.end < chunkRange.start) {
                        this.logger.debug(`symbol is in front of chunk`);
                        i++; // move to next symbol
                        continue;
                    }
                    else if (chunkRange.end < symbolRange.start) {
                        this.logger.debug(`symbol is in the back of chunk`);
                        j++; // move to next chunk
                        continue;
                    }
                    else {
                        this.logger.debug(`symbol and chunk are overlapped`);
                        impactedLines.chunks.push(symbolRange);
                        i++; // move to next symbol, keep current chunk
                    }
                }
                impactedLinesList.push(impactedLines);
            }
            return impactedLinesList;
        });
    }
    markImpactedLines(impactedLinesList, prefix = '[impacted] ') {
        const impactedFiles = [];
        for (const impactLines of impactedLinesList) {
            const filePath = path.resolve(this.workspace, impactLines.filePath);
            const source = fs_1.default.readFileSync(filePath, { encoding: 'utf-8' });
            // FIXME: split('\n') doesn't take os difference into account
            // let's just ignore it until something wrong happens.
            const lines = source.split('\n');
            let i = 0;
            let j = 0;
            while (i < lines.length && j < impactLines.chunks.length) {
                const chunk = impactLines.chunks[j];
                if (chunk.end < i) {
                    j++;
                    continue;
                }
                if (chunk.start <= i) { // which also imply i <= chunk.end
                    lines[i] = prefix + lines[i];
                }
                i++;
            }
            impactedFiles.push({
                filePath,
                content: lines.join('\n'),
            });
            // fs.writeFileSync(impactLines.filePath, lines.join('\n'));
        }
        return impactedFiles;
    }
}
exports.ImpactDetectionService = ImpactDetectionService;
