"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BindingType = exports.UpdateChanges = exports.InputPropertyType = void 0;
const fs = require("fs");
const path = require("path");
const ts = require("typescript");
const tss = require("typescript/lib/tsserverlibrary");
const schema_1 = require("./schema");
const tsUtils_1 = require("./tsUtils");
const util_1 = require("./util");
const ServerHost_1 = require("./ServerHost");
const TSCONFIG_PATH = 'tsconfig.json';
var InputPropertyType;
(function (InputPropertyType) {
    InputPropertyType["EVAL"] = "eval";
    InputPropertyType["STRING"] = "string";
})(InputPropertyType = exports.InputPropertyType || (exports.InputPropertyType = {}));
/* eslint-disable arrow-parens */
class UpdateChanges {
    /**
     * Create a new base schematic to apply changes
     *
     * @param rootPath Root folder for the schematic to read configs, pass __dirname
     */
    constructor(rootPath, host, context) {
        this.rootPath = rootPath;
        this.host = host;
        this.context = context;
        this.tsconfigPath = TSCONFIG_PATH;
        this.conditionFunctions = new Map();
        this.valueTransforms = new Map();
        this._templateFiles = [];
        this._initialTsConfig = '';
        this._tsFiles = [];
        this._sassFiles = [];
        this.workspace = (0, util_1.getWorkspace)(host);
        this.sourcePaths = (0, util_1.getProjectPaths)(this.workspace);
        this.selectorChanges = this.loadConfig('selectors.json');
        this.classChanges = this.loadConfig('classes.json');
        this.outputChanges = this.loadConfig('outputs.json');
        this.inputChanges = this.loadConfig('inputs.json');
        this.themeChanges = this.loadConfig('theme-changes.json');
        this.importsChanges = this.loadConfig('imports.json');
        this.membersChanges = this.loadConfig('members.json');
        this.serverHost = new ServerHost_1.ServerHost(this.host);
    }
    get projectService() {
        var _a, _b, _c, _d;
        if (!this._projectService) {
            this._projectService = (0, tsUtils_1.createProjectService)(this.serverHost);
            // Force Angular service to compile project on initial load w/ configure project
            // otherwise if the first compilation occurs on an HTML file the project won't have proper refs
            // and no actual angular metadata will be resolved for the rest of the migration
            const wsProject = this.workspace.projects[this.workspace.defaultProject] || this.workspace.projects[0];
            const mainRelPath = ((_b = (_a = wsProject.architect) === null || _a === void 0 ? void 0 : _a.build) === null || _b === void 0 ? void 0 : _b.options['main']) ?
                path.join(wsProject.root, (_d = (_c = wsProject.architect) === null || _c === void 0 ? void 0 : _c.build) === null || _d === void 0 ? void 0 : _d.options['main']) :
                `src/main.ts`;
            // patch TSConfig so it includes angularOptions.strictTemplates
            // ivy ls requires this in order to function properly on templates
            this.patchTsConfig();
            const mainAbsPath = path.resolve(this._projectService.currentDirectory, mainRelPath);
            const scriptInfo = this._projectService.getOrCreateScriptInfoForNormalizedPath(tss.server.toNormalizedPath(mainAbsPath), false);
            this._projectService.openClientFile(scriptInfo.fileName);
            const project = this._projectService.findProject(scriptInfo.containingProjects[0].projectName);
            project.getLanguageService().getSemanticDiagnostics(mainAbsPath);
        }
        return this._projectService;
    }
    get templateFiles() {
        if (!this._templateFiles.length) {
            // https://github.com/angular/devkit/blob/master/packages/angular_devkit/schematics/src/tree/filesystem.ts
            this.sourceDirsVisitor((fulPath, entry) => {
                if (fulPath.endsWith('component.html')) {
                    this._templateFiles.push(entry.path);
                }
            });
        }
        return this._templateFiles;
    }
    get tsFiles() {
        if (!this._tsFiles.length) {
            this.sourceDirsVisitor((fulPath, entry) => {
                if (fulPath.endsWith('.ts')) {
                    this._tsFiles.push(entry.path);
                }
            });
        }
        return this._tsFiles;
    }
    /** Sass (both .scss and .sass) files in the project being updated. */
    get sassFiles() {
        if (!this._sassFiles.length) {
            // files can be outside the app prefix, so start from sourceRoot
            // also ignore schematics `styleext` as Sass can be used regardless
            const sourceDirs = (0, util_1.getProjects)(this.workspace).map(x => x.sourceRoot).filter(x => x);
            this.sourceDirsVisitor((fulPath, entry) => {
                if (fulPath.endsWith('.scss') || fulPath.endsWith('.sass')) {
                    this._sassFiles.push(entry.path);
                }
            }, sourceDirs);
        }
        return this._sassFiles;
    }
    get service() {
        if (!this._service) {
            this._service = (0, tsUtils_1.getLanguageService)(this.tsFiles, this.host);
        }
        return this._service;
    }
    get packageManager() {
        if (!this._packageManager) {
            this._packageManager = (0, util_1.getPackageManager)(this.host);
        }
        return this._packageManager;
    }
    /** Apply configured changes to the Host Tree */
    applyChanges() {
        const shouldInstallPkg = this.membersChanges && this.membersChanges.changes.length
            && !(0, util_1.canResolvePackage)(tsUtils_1.NG_LANG_SERVICE_PACKAGE_NAME);
        if (shouldInstallPkg) {
            this.context.logger.info(`Installing temporary migration dependencies via ${this.packageManager}.`);
            // try and get an appropriate version of the package to install
            let targetVersion = (0, util_1.getPackageVersion)(tsUtils_1.NG_CORE_PACKAGE_NAME) || 'latest';
            if (targetVersion.startsWith('11')) {
                // TODO: Temporary restrict 11 LS version, till update for new module loading
                targetVersion = '11.0.0';
            }
            (0, util_1.tryInstallPackage)(this.context, this.packageManager, `${tsUtils_1.NG_LANG_SERVICE_PACKAGE_NAME}@${targetVersion}`);
        }
        this.updateTemplateFiles();
        this.updateTsFiles();
        this.updateMembers();
        /** Sass files */
        if (this.themeChanges && this.themeChanges.changes.length) {
            for (const entryPath of this.sassFiles) {
                this.updateThemeProps(entryPath);
                this.updateSassVariables(entryPath);
                this.updateSassFunctionsAndMixins(entryPath);
            }
        }
        if (shouldInstallPkg) {
            this.context.logger.info(`Cleaning up temporary migration dependencies.`);
            (0, util_1.tryUninstallPackage)(this.context, this.packageManager, tsUtils_1.NG_LANG_SERVICE_PACKAGE_NAME);
        }
        // if tsconfig.json was patched, restore it
        if (this._initialTsConfig !== '') {
            this.host.overwrite(this.tsconfigPath, this._initialTsConfig);
        }
    }
    /** Add condition function. */
    addCondition(conditionName, callback) {
        this.conditionFunctions.set(conditionName, callback);
    }
    addValueTransform(functionName, callback) {
        this.valueTransforms.set(functionName, callback);
    }
    /** Path must be absolute. If calling externally, use this.getAbsolutePath */
    getDefaultLanguageService(entryPath) {
        const project = this.getDefaultProjectForFile(entryPath);
        return project.getLanguageService();
    }
    updateSelectors(entryPath) {
        let fileContent = this.host.read(entryPath).toString();
        let overwrite = false;
        for (const change of this.selectorChanges.changes) {
            let searchPttrn = change.type === 'component' ? '<' : '';
            searchPttrn += change.selector;
            if (fileContent.indexOf(searchPttrn) !== -1) {
                fileContent = this.applySelectorChange(fileContent, change);
                overwrite = true;
            }
        }
        if (overwrite) {
            this.host.overwrite(entryPath, fileContent);
        }
    }
    applySelectorChange(fileContent, change) {
        let regSource;
        let replace;
        switch (change.type) {
            case 'component':
                if (change.remove) {
                    regSource = String.raw `\<${change.selector}[\s\S]*?\<\/${change.selector}\>`;
                    replace = '';
                }
                else {
                    regSource = String.raw `\<(\/?)${change.selector}(?=[\s\>])`;
                    replace = `<$1${change.replaceWith}`;
                }
                break;
            case 'directive':
                if (change.remove) {
                    // Group match (\2) as variable as it looks like octal escape (error in strict)
                    regSource = String.raw `\s*?\[?${change.selector}\]?(=(["']).*?${'\\2'}(?=\s|\>))?`;
                    replace = '';
                }
                else {
                    regSource = change.selector;
                    replace = change.replaceWith;
                }
                break;
            default:
                break;
        }
        fileContent = fileContent.replace(new RegExp(regSource, 'g'), replace);
        return fileContent;
    }
    updateClasses(entryPath) {
        let fileContent = this.host.read(entryPath).toString();
        const alreadyReplaced = new Set();
        for (const change of this.classChanges.changes) {
            if (fileContent.indexOf(change.name) !== -1) {
                const positions = (0, tsUtils_1.getRenamePositions)(entryPath, change.name, this.service);
                // loop backwards to preserve positions
                for (let i = positions.length; i--;) {
                    const pos = positions[i];
                    // V.S. 18th May 2021: If several classes are renamed w/ the same import, erase them
                    // TODO: Refactor to make use of TSLS API instead of string replace
                    if (i === 0 && alreadyReplaced.has(change.replaceWith)) {
                        // only match the first trailing white space, right after the replace position
                        const trailingCommaWhiteSpace = new RegExp(/,([\s]*)(?=(\s}))/);
                        let afterReplace = fileContent.slice(pos.end);
                        const beforeReplace = fileContent.slice(0, pos.start);
                        const leadingComma = afterReplace[0] === ',' ? 1 : 0;
                        // recalculate if needed
                        afterReplace = !leadingComma ? afterReplace : fileContent.slice(pos.end + leadingComma);
                        const doubleSpaceReplace = beforeReplace[beforeReplace.length - 1].match(/\s/) !== null && afterReplace[0].match(/\s/) !== null ?
                            1 :
                            0;
                        fileContent = (fileContent.slice(0, pos.start - doubleSpaceReplace) +
                            '' +
                            afterReplace).replace(trailingCommaWhiteSpace, '');
                    }
                    else {
                        fileContent = fileContent.slice(0, pos.start) + change.replaceWith + fileContent.slice(pos.end);
                    }
                }
                if (positions.length) {
                    // using a set should be a lot quicker that getting position for renames of replace
                    alreadyReplaced.add(change.replaceWith);
                    this.host.overwrite(entryPath, fileContent);
                }
            }
        }
    }
    updateBindings(entryPath, bindChanges, type = BindingType.Output) {
        let fileContent = this.host.read(entryPath).toString();
        let overwrite = false;
        for (const change of bindChanges.changes) {
            if (fileContent.indexOf(change.owner.selector) === -1 || fileContent.indexOf(change.name) === -1) {
                continue;
            }
            let base;
            let replace;
            let searchPattern;
            if (type === BindingType.Output) {
                base = String.raw `\(${change.name}\)=(["'])(.*?)\1`;
                replace = `(${change.replaceWith})=$1$2$1`;
            }
            else {
                // Match both bound - [name] - and regular - name
                base = String.raw `(\s\[?)${change.name}(\s*\]?=)(["'])(.*?)\3`;
                replace = String.raw `$1${change.replaceWith}$2$3$4$3`;
            }
            let reg = new RegExp(base, 'g');
            if (change.remove || change.moveBetweenElementTags) {
                // Group match (\1) as variable as it looks like octal escape (error in strict)
                reg = new RegExp(String.raw `\s*${base}(?=\s|\>)`, 'g');
                replace = '';
            }
            switch (change.owner.type) {
                case 'component':
                    searchPattern = String.raw `\<${change.owner.selector}(?=[\s\>])[^\>]*\>`;
                    break;
                case 'directive':
                    searchPattern = String.raw `\<[^\>]*[\s\[]${change.owner.selector}[^\>]*\>`;
                    break;
            }
            const matches = fileContent.match(new RegExp(searchPattern, 'g'));
            if (!matches) {
                continue;
            }
            for (const match of matches) {
                let replaceStatement = replace;
                if (!this.areConditionsFulfilled(match, change.conditions, entryPath)) {
                    continue;
                }
                if (change.moveBetweenElementTags) {
                    const moveMatch = match.match(reg);
                    fileContent = this.copyPropertyValueBetweenElementTags(fileContent, match, moveMatch);
                }
                if (change.valueTransform) {
                    const regExpMatch = match.match(new RegExp(base));
                    const bindingType = regExpMatch && regExpMatch[1].endsWith('[') ? InputPropertyType.EVAL : InputPropertyType.STRING;
                    if (regExpMatch) {
                        const value = regExpMatch[4];
                        const transform = this.valueTransforms.get(change.valueTransform);
                        const args = { value, bindingType };
                        transform(args);
                        if (args.bindingType !== bindingType) {
                            replaceStatement = args.bindingType === InputPropertyType.EVAL ?
                                replaceStatement.replace(`$1`, `$1[`).replace(`$2`, `]$2`) :
                                replaceStatement.replace(`$1`, regExpMatch[1].replace('[', '')).replace('$2', regExpMatch[2].replace(']', ''));
                        }
                        replaceStatement = replaceStatement.replace('$4', args.value);
                    }
                }
                fileContent = fileContent.replace(match, match.replace(reg, replaceStatement));
            }
            overwrite = true;
        }
        if (overwrite) {
            this.host.overwrite(entryPath, fileContent);
        }
    }
    updateThemeProps(entryPath) {
        var _a;
        let fileContent = this.host.read(entryPath).toString();
        let overwrite = false;
        for (const change of this.themeChanges.changes) {
            if (change.type !== schema_1.ThemeType.Property) {
                continue;
            }
            if (fileContent.indexOf(change.owner) !== -1) {
                /** owner-func:( * ); */
                const searchPattern = String.raw `${change.owner}\([\s\S]+?\);`;
                const matches = fileContent.match(new RegExp(searchPattern, 'g'));
                if (!matches) {
                    continue;
                }
                for (const match of matches) {
                    if (match.indexOf(change.name) !== -1) {
                        const name = change.name.replace('$', '\\$');
                        const replaceWith = (_a = change.replaceWith) === null || _a === void 0 ? void 0 : _a.replace('$', '\\$');
                        const reg = new RegExp(String.raw `^\s*${name}:`);
                        const existing = new RegExp(String.raw `${replaceWith}:`);
                        const opening = `${change.owner}(`;
                        const closing = /\s*\);$/.exec(match).pop();
                        const body = match.substr(opening.length, match.length - opening.length - closing.length);
                        let params = this.splitFunctionProps(body);
                        params = params.reduce((arr, param) => {
                            if (reg.test(param)) {
                                const duplicate = !!replaceWith && arr.some(p => existing.test(p));
                                if (!change.remove && !duplicate) {
                                    arr.push(param.replace(change.name, change.replaceWith));
                                }
                            }
                            else {
                                arr.push(param);
                            }
                            return arr;
                        }, []);
                        fileContent = fileContent.replace(match, opening + params.join(',') + closing);
                        overwrite = true;
                    }
                }
            }
        }
        if (overwrite) {
            this.host.overwrite(entryPath, fileContent);
        }
    }
    isNamedArgument(fileContent, i, occurrences, change) {
        const openingBrackets = [];
        const closingBrackets = [];
        if (fileContent[(occurrences[i] + change.name.length)] !== ':'
            || (fileContent[(occurrences[i] + change.name.length)] === ' '
                && fileContent[(occurrences[i] + change.name.length) + 1] === ':')) {
            return false;
        }
        for (let j = occurrences[i]; j >= 0; j--) {
            if (fileContent[j] === ')') {
                closingBrackets.push(fileContent[j]);
            }
            else if (fileContent[j] === '(') {
                openingBrackets.push(fileContent[j]);
            }
        }
        return openingBrackets.length !== closingBrackets.length;
    }
    updateSassVariables(entryPath) {
        let fileContent = this.host.read(entryPath).toString();
        let overwrite = false;
        const allowedStartCharacters = new RegExp('(\:|\,)\s?', 'g');
        const allowedEndCharacters = new RegExp('[;),: \r\n]', 'g');
        for (const change of this.themeChanges.changes) {
            if (change.type !== schema_1.ThemeType.Variable) {
                continue;
            }
            if (!('owner' in change)) {
                const occurrences = (0, tsUtils_1.findMatches)(fileContent, change.name);
                for (let i = occurrences.length - 1; i >= 0; i--) {
                    const allowedStartEnd = fileContent[occurrences[i] - 1].match(allowedStartCharacters)
                        || fileContent[(occurrences[i] + change.name.length)].match(allowedEndCharacters);
                    if (allowedStartEnd && !this.isNamedArgument(fileContent, i, occurrences, change)) {
                        fileContent = (0, tsUtils_1.replaceMatch)(fileContent, change.name, change.replaceWith, occurrences[i]);
                        overwrite = true;
                    }
                }
            }
        }
        if (overwrite) {
            this.host.overwrite(entryPath, fileContent);
        }
    }
    updateSassFunctionsAndMixins(entryPath) {
        const aliases = this.getAliases(entryPath);
        let fileContent = this.host.read(entryPath).toString();
        let overwrite = false;
        for (const change of this.themeChanges.changes) {
            if (change.type !== schema_1.ThemeType.Function && change.type !== schema_1.ThemeType.Mixin) {
                continue;
            }
            let occurrences = [];
            if (aliases.length > 0 && !aliases.includes('*')) {
                aliases.forEach(a => occurrences = occurrences.concat((0, tsUtils_1.findMatches)(fileContent, a + '.' + change.name)));
                if (occurrences.length > 0) {
                    ({ overwrite, fileContent } = this.tryReplaceScssFunctionWithAlias(occurrences, aliases, fileContent, change, overwrite));
                    continue;
                }
            }
            occurrences = (0, tsUtils_1.findMatches)(fileContent, change.name);
            if (occurrences.length > 0) {
                ({ overwrite, fileContent } = this.tryReplaceScssFunction(occurrences, fileContent, change, overwrite));
            }
        }
        if (overwrite) {
            this.host.overwrite(entryPath, fileContent);
        }
    }
    getAliases(entryPath) {
        const fileContent = this.host.read(entryPath).toString();
        // B.P. 18/05/22 #11577 - Use RegEx to distinguish themed imports.
        const matchers = [
            /@use(\s+)('|")igniteui-angular\/theming\2\1as\1(\w+)/g,
            /@use(\s+)('|")igniteui-angular\/theme\2\1as\1(\w+)/g,
            /@use(\s+)('|")igniteui-angular\/lib\/core\/styles\/themes\/index\2\1as\1(\w+)/g
        ];
        var aliases = [];
        matchers.forEach(m => {
            const match = m.exec(fileContent);
            if (match) {
                aliases.push(match[3]); // access the captured alias
            }
        });
        return aliases;
    }
    updateImports(entryPath) {
        let fileContent = this.host.read(entryPath).toString();
        let overwrite = false;
        for (const change of this.importsChanges.changes) {
            if (fileContent.indexOf(change.name) === -1) {
                continue;
            }
            const replace = (0, util_1.escapeRegExp)(change.replaceWith);
            const base = (0, util_1.escapeRegExp)(change.name);
            const reg = new RegExp(base, 'g');
            fileContent = fileContent.replace(reg, replace);
            overwrite = true;
        }
        if (overwrite) {
            this.host.overwrite(entryPath, fileContent);
        }
    }
    updateClassMembers(entryPath, memberChanges) {
        let content = this.host.read(entryPath).toString();
        const absPath = tss.server.toNormalizedPath(path.join(process.cwd(), entryPath));
        // use the absolute path for ALL LS operations
        // do not overwrite the entryPath, as Tree operations require relative paths
        const changes = new Set();
        let langServ;
        for (const change of memberChanges.changes) {
            if (!content.includes(change.member)) {
                continue;
            }
            langServ = langServ || this.getDefaultLanguageService(absPath);
            let matches;
            if (entryPath.endsWith('.ts')) {
                const source = langServ.getProgram().getSourceFile(absPath);
                matches = (0, tsUtils_1.getIdentifierPositions)(source, change.member).map(x => x.start);
            }
            else {
                matches = (0, tsUtils_1.findMatches)(content, `.${change.member}`).map(pos => pos + 1);
            }
            for (const matchPosition of matches) {
                if ((0, tsUtils_1.isMemberIgniteUI)(change, langServ, absPath, matchPosition)) {
                    changes.add({ change, position: matchPosition });
                }
            }
        }
        const changesArr = Array.from(changes).sort((c, c1) => c.position - c1.position).reverse();
        for (const fileChange of changesArr) {
            content = (0, tsUtils_1.replaceMatch)(content, fileChange.change.member, fileChange.change.replaceWith, fileChange.position);
        }
        if (changes.size) {
            this.host.overwrite(entryPath, content);
        }
    }
    // TODO: combine both functions
    tryReplaceScssFunctionWithAlias(occurrences, aliases, fileContent, change, overwrite) {
        for (const alias of aliases) {
            const aliasLength = alias.length + 1; // + 1 because of the dot - alias.member
            for (let i = occurrences.length - 1; i >= 0; i--) {
                const isOpenParenthesis = fileContent[occurrences[i] + aliasLength + change.name.length] === '(';
                if (isOpenParenthesis) {
                    fileContent = (0, tsUtils_1.replaceMatch)(fileContent, change.name, change.replaceWith, occurrences[i] + aliasLength);
                    overwrite = true;
                }
            }
        }
        return { overwrite, fileContent };
    }
    tryReplaceScssFunction(occurrences, fileContent, change, overwrite) {
        for (let i = occurrences.length - 1; i >= 0; i--) {
            const isOpenParenthesis = fileContent[occurrences[i] + change.name.length] === '(';
            if (isOpenParenthesis) {
                fileContent = (0, tsUtils_1.replaceMatch)(fileContent, change.name, change.replaceWith, occurrences[i]);
                overwrite = true;
            }
        }
        return { overwrite, fileContent };
    }
    patchTsConfig() {
        var _a, _b, _c, _d;
        this.ensureTsConfigPath();
        if (this.serverHost.fileExists(this.tsconfigPath)) {
            let originalContent = '';
            try {
                originalContent = this.serverHost.readFile(this.tsconfigPath);
            }
            catch (_e) {
                (_a = this.context) === null || _a === void 0 ? void 0 : _a.logger.warn(`Could not read ${this.tsconfigPath}. Some Angular Ivy features might be unavailable during migrations.`);
                return;
            }
            let content;
            // use ts parser as it handles jsonc-style files w/ comments
            const result = ts.parseConfigFileTextToJson(this.tsconfigPath, originalContent);
            if (!result.error) {
                content = result.config;
            }
            else {
                (_b = this.context) === null || _b === void 0 ? void 0 : _b.logger.warn(`Could not parse ${this.tsconfigPath}. Angular Ivy language service might be unavailable during migrations.`);
                (_c = this.context) === null || _c === void 0 ? void 0 : _c.logger.warn(`Error:\n${result.error}`);
                return;
            }
            if (!content.angularCompilerOptions) {
                content.angularCompilerOptions = {};
            }
            if (!content.angularCompilerOptions.strictTemplates) {
                (_d = this.context) === null || _d === void 0 ? void 0 : _d.logger.info(`Adding 'angularCompilerOptions.strictTemplates' to ${this.tsconfigPath} for migration run.`);
                content.angularCompilerOptions.strictTemplates = true;
                this.host.overwrite(this.tsconfigPath, JSON.stringify(content));
                // store initial state and restore it once migrations are finished
                this._initialTsConfig = originalContent;
            }
        }
    }
    ;
    ensureTsConfigPath() {
        var _a, _b;
        if (this.host.exists(this.tsconfigPath)) {
            return;
        }
        // attempt to find a main tsconfig from workspace:
        const wsProject = this.workspace.projects[this.workspace.defaultProject] || this.workspace.projects[0];
        // technically could be per-project, but assuming there's at least one main tsconfig for IDE support
        const projectConfig = (_b = (_a = wsProject.architect) === null || _a === void 0 ? void 0 : _a.build) === null || _b === void 0 ? void 0 : _b.options['tsConfig'];
        if (!projectConfig || !this.host.exists(projectConfig)) {
            return;
        }
        if (path.posix.basename(projectConfig) === TSCONFIG_PATH) {
            // not project specific extended tsconfig, use directly
            this.tsconfigPath = projectConfig;
            return;
        }
        // look for base config through extends property
        const result = ts.parseConfigFileTextToJson(projectConfig, this.serverHost.readFile(projectConfig));
        if (!result.error && result.config.extends) {
            this.tsconfigPath = path.posix.join(path.posix.dirname(projectConfig), result.config.extends);
        }
    }
    ;
    loadConfig(configJson) {
        const filePath = path.join(this.rootPath, 'changes', configJson);
        if (fs.existsSync(filePath)) {
            return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
        }
    }
    areConditionsFulfilled(match, conditions, entryPath) {
        if (conditions) {
            for (const condition of conditions) {
                if (this.conditionFunctions && this.conditionFunctions.has(condition)) {
                    const callback = this.conditionFunctions.get(condition);
                    if (callback && !callback(match, entryPath)) {
                        return false;
                    }
                }
            }
        }
        return true;
    }
    copyPropertyValueBetweenElementTags(fileContent, ownerMatch, propertyMatchArray) {
        if (ownerMatch && propertyMatchArray && propertyMatchArray.length > 0) {
            const propMatch = propertyMatchArray[0].trim();
            const propValueMatch = propMatch.match(new RegExp(`=(["'])(.+?)${'\\1'}`));
            if (propValueMatch && propValueMatch.length > 0) {
                const propValue = propValueMatch[propValueMatch.length - 1];
                if (propMatch.startsWith('[')) {
                    return fileContent.replace(ownerMatch, ownerMatch + `{{${propValue}}}`);
                }
                else {
                    return fileContent.replace(ownerMatch, ownerMatch + propValue);
                }
            }
        }
        return fileContent;
    }
    sourceDirsVisitor(visitor, dirs = this.sourcePaths) {
        for (const sourcePath of dirs) {
            const srcDir = this.host.getDir(sourcePath);
            srcDir.visit(visitor);
        }
    }
    /**
     * Safe split by `','`, considering possible inner function calls. E.g.:
     * ```
     * prop: inner-func(),
     * prop2: inner2(inner-param: 3, inner-param: inner-func(..))
     * ```
     */
    splitFunctionProps(body) {
        const parts = [];
        let lastIndex = 0;
        let level = 0;
        for (let i = 0; i < body.length; i++) {
            const char = body[i];
            switch (char) {
                case '(':
                    level++;
                    break;
                case ')':
                    level--;
                    break;
                case ',':
                    if (!level) {
                        parts.push(body.substring(lastIndex, i));
                        lastIndex = i + 1;
                    }
                    break;
                default:
                    break;
            }
        }
        parts.push(body.substring(lastIndex));
        return parts;
    }
    updateTemplateFiles() {
        if (this.selectorChanges && this.selectorChanges.changes.length) {
            for (const entryPath of this.templateFiles) {
                this.updateSelectors(entryPath);
            }
        }
        if (this.outputChanges && this.outputChanges.changes.length) {
            // name change of output
            for (const entryPath of this.templateFiles) {
                this.updateBindings(entryPath, this.outputChanges);
            }
        }
        if (this.inputChanges && this.inputChanges.changes.length) {
            // name change of input
            for (const entryPath of this.templateFiles) {
                this.updateBindings(entryPath, this.inputChanges, BindingType.Input);
            }
        }
    }
    updateTsFiles() {
        if (this.classChanges && this.classChanges.changes.length) {
            // change class name
            for (const entryPath of this.tsFiles) {
                this.updateClasses(entryPath);
            }
        }
        if (this.importsChanges && this.importsChanges.changes.length) {
            // TODO: move logic to 7.0.2 migration
            for (const entryPath of this.tsFiles) {
                this.updateImports(entryPath);
            }
        }
    }
    updateMembers() {
        if (this.membersChanges && this.membersChanges.changes.length) {
            const dirs = [...this.templateFiles, ...this.tsFiles];
            for (const entryPath of dirs) {
                this.updateClassMembers(entryPath, this.membersChanges);
            }
        }
    }
    getDefaultProjectForFile(entryPath) {
        const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(tss.server.asNormalizedPath(entryPath), false);
        this.projectService.openClientFile(scriptInfo.fileName);
        const project = this.projectService.findProject(scriptInfo.containingProjects[0].projectName);
        project.addMissingFileRoot(scriptInfo.fileName);
        return project;
    }
}
exports.UpdateChanges = UpdateChanges;
var BindingType;
(function (BindingType) {
    BindingType[BindingType["Output"] = 0] = "Output";
    BindingType[BindingType["Input"] = 1] = "Input";
})(BindingType = exports.BindingType || (exports.BindingType = {}));
