/**
 * Copyright (c) Siemens 2016 - 2026
 * SPDX-License-Identifier: MIT
 */
import { normalize } from '@angular-devkit/core';
import { SchematicsException } from '@angular-devkit/schematics';
import { isAbsolute } from 'path/posix';
import ts from 'typescript';
import { SchematicsFileSystem } from './schematics-file-system.js';
/**
 * Reads and parses a tsconfig file from the given path.
 */
export const parseTsconfigFile = (tsconfigPath, basePath, tree) => {
    // Parse the tsconfig file content
    const fs = new SchematicsFileSystem(tree);
    const { config, error } = ts.readConfigFile(tsconfigPath, fs.readText);
    if (error) {
        throw new SchematicsException(`Error parsing ${tsconfigPath}: ${error.messageText}`);
    }
    // Create a ts.ParseConfigHost that uses the schematics Tree
    const parseConfigHost = {
        fileExists: fs.exists,
        readFile: fs.readText,
        readDirectory: (path, _extensions, _excludes, _includes, _depth) => visitDirectory(fs, path),
        useCaseSensitiveFileNames: true
    };
    // Throw if incorrect arguments are passed to this function. Passing relative base paths
    // results in root directories not being resolved and in later type checking runtime errors.
    // More details can be found here: https://github.com/microsoft/TypeScript/issues/37731.
    if (!isAbsolute(basePath)) {
        throw Error('Unexpected relative base path has been specified.');
    }
    return ts.parseJsonConfigFileContent(config, parseConfigHost, basePath, {});
};
/**
 * Finds all nodes which match the given kind.
 */
export const findNodes = (node, kindOrGuard, max = Infinity, recursive = false) => {
    if (!node || max == 0) {
        return [];
    }
    const test = typeof kindOrGuard === 'function'
        ? kindOrGuard
        : // eslint-disable-next-line @typescript-eslint/no-shadow
            (node) => node.kind === kindOrGuard;
    const arr = [];
    if (test(node)) {
        arr.push(node);
        max--;
    }
    if (max > 0 && (recursive || !test(node))) {
        for (const child of node.getChildren()) {
            // eslint-disable-next-line @typescript-eslint/no-loop-func, @typescript-eslint/no-shadow
            findNodes(child, test, max, recursive).forEach(node => {
                if (max > 0) {
                    arr.push(node);
                }
                max--;
            });
            if (max <= 0) {
                break;
            }
        }
    }
    return arr;
};
/**
 * Gets the import nodes from a TypeScript file which start the given import path.
 */
export const getImportNodes = (filePath, content, specifier) => {
    const sourceFile = ts.createSourceFile(filePath, content.toString(), ts.ScriptTarget.Latest, true);
    // Find all import declarations in the source file
    const allImports = findNodes(sourceFile, ts.SyntaxKind.ImportDeclaration);
    return allImports.filter(node => node.moduleSpecifier &&
        ts.isStringLiteral(node.moduleSpecifier) &&
        node.moduleSpecifier.text.startsWith(specifier));
};
/**
 * Gets the imported symbols from an import declaration.
 */
export const getSymbols = (node) => {
    // Extract all imported component names or module names from @simpl/ imports
    return node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)
        ? node.importClause.namedBindings.elements
        : [];
};
const visitDirectory = (fs, dirPath) => {
    const entries = fs.getDir(dirPath);
    const files = [];
    entries.subfiles.forEach(filename => {
        if (!filename.startsWith('.')) {
            const fullPath = normalize(`${dirPath}/${filename}`);
            files.push(fullPath);
        }
    });
    entries.subdirs.forEach(subdirname => {
        if (!subdirname.startsWith('.') && subdirname !== 'node_modules') {
            const newFiles = visitDirectory(fs, `${dirPath}/${subdirname}`);
            files.push(...newFiles);
        }
    });
    return files;
};
/** Finds an import specifier with a particular name. */
export const findImportSpecifier = (nodes, specifierName) => {
    return nodes.find(element => {
        const { name, propertyName } = element;
        return propertyName ? propertyName.text === specifierName : name.text === specifierName;
    });
};
/**
 * Note: returns only matching imports specifiers,
 * Unmatched imports will be ignored (you won't get undefined), but a shorter array.
 */
export const getImportSpecifiers = (sourceFile, moduleName, specifierOrSpecifiers) => {
    const matches = [];
    for (const node of sourceFile.statements) {
        if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) {
            continue;
        }
        const namedBindings = node.importClause?.namedBindings;
        const isMatch = typeof moduleName === 'string'
            ? node.moduleSpecifier.text === moduleName
            : moduleName.test(node.moduleSpecifier.text);
        if (!isMatch || !namedBindings || !ts.isNamedImports(namedBindings)) {
            continue;
        }
        if (typeof specifierOrSpecifiers === 'string') {
            const match = findImportSpecifier(namedBindings.elements, specifierOrSpecifiers);
            if (match) {
                matches.push(match);
            }
        }
        else {
            for (const specifierName of specifierOrSpecifiers) {
                const match = findImportSpecifier(namedBindings.elements, specifierName);
                if (match) {
                    matches.push(match);
                }
            }
        }
    }
    return matches;
};
/**
 * Calculates distance between two strings.
 * Lower number = closer match.
 */
const getLevenshteinDistance = (a, b) => {
    const matrix = [];
    for (let i = 0; i <= b.length; i++)
        matrix[i] = [i];
    for (let j = 0; j <= a.length; j++)
        matrix[0][j] = j;
    for (let i = 1; i <= b.length; i++) {
        for (let j = 1; j <= a.length; j++) {
            if (b.charAt(i - 1) === a.charAt(j - 1)) {
                matrix[i][j] = matrix[i - 1][j - 1];
            }
            else {
                matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution
                matrix[i][j - 1] + 1, // insertion
                matrix[i - 1][j] + 1 // deletion
                );
            }
        }
    }
    return matrix[b.length][a.length];
};
/**
 * Find closest import statement.
 *
 * Note: the function assumes that there is at least one import in the file.
 */
const findClosestImport = (search, imports) => {
    return imports.reduce((closest, current) => {
        const currentModule = current.moduleSpecifier.text;
        const closestModule = closest.moduleSpecifier.text;
        const currentDist = getLevenshteinDistance(search, currentModule);
        const closestDist = getLevenshteinDistance(search, closestModule);
        return currentDist < closestDist ? current : closest;
    });
};
/** Ensures that a specific identifier is imported from the given module */
export const applyImport = (sourceFile, identifierName, moduleSpecifier) => {
    const imports = sourceFile.statements
        .filter(ts.isImportDeclaration)
        // Filter only external module imports
        .filter(i => i.moduleSpecifier &&
        ts.isStringLiteral(i.moduleSpecifier) &&
        !i.moduleSpecifier.text.startsWith('.'));
    const closestImport = findClosestImport(moduleSpecifier, imports);
    // Check if the module is already imported
    if (closestImport.moduleSpecifier.text === moduleSpecifier) {
        const namedBindings = closestImport.importClause?.namedBindings;
        if (namedBindings && ts.isNamedImports(namedBindings)) {
            // Check if the identifier already exists
            const hasIdentifier = namedBindings.elements.some(el => el.name.text === identifierName);
            if (hasIdentifier) {
                // Already imported, no transformation needed
                return null;
            }
            // Add at the end
            const elements = Array.from(namedBindings.elements);
            const lastElement = elements.at(-1);
            if (lastElement) {
                return {
                    start: lastElement.getEnd(),
                    end: lastElement.getEnd(),
                    replacement: `, ${identifierName}`
                };
            }
        }
    }
    const newImport = `import { ${identifierName} } from '${moduleSpecifier}';`;
    const fullText = sourceFile.getFullText();
    const end = closestImport.getEnd();
    // Find the position after the trailing newline(s) of the closest import
    let insertPosition = end;
    if (fullText[insertPosition] === '\r')
        insertPosition++;
    if (fullText[insertPosition] === '\n')
        insertPosition++;
    return {
        start: insertPosition,
        end: insertPosition,
        replacement: `${newImport}\n`
    };
};
