import { readFileSync } from 'node:fs';
import { promisify } from 'node:util';
import { walk as _walk } from '@nodelib/fs.walk';
import fg, {} from 'fast-glob';
import picomatch from 'picomatch';
import { GLOBAL_IGNORE_PATTERNS, ROOT_WORKSPACE_NAME } from '../constants.js';
import { timerify } from './Performance.js';
import { compact } from './array.js';
import { debugLogObject } from './debug.js';
import { isFile } from './fs.js';
import { dirname, join, relative, toPosix } from './path.js';
const walk = promisify(_walk);
const _picomatch = timerify(picomatch);
const cachedGitIgnores = new Map();
const cachedGlobIgnores = new Map();
export const convertGitignoreToPicomatchIgnorePatterns = (pattern) => {
    const negated = pattern[0] === '!';
    if (negated)
        pattern = pattern.slice(1);
    let extPattern;
    if (pattern.endsWith('/'))
        pattern = pattern.slice(0, -1);
    if (pattern.startsWith('*/**/'))
        pattern = pattern.slice(5);
    if (pattern.startsWith('/'))
        pattern = pattern.slice(1);
    else if (!pattern.startsWith('**/'))
        pattern = `**/${pattern}`;
    if (pattern.endsWith('/*'))
        extPattern = pattern;
    else
        extPattern = `${pattern}/**`;
    return { negated, patterns: [pattern, extPattern] };
};
export const parseAndConvertGitignorePatterns = (patterns, ancestor) => {
    const matchFrom = ancestor ? new RegExp(`^(!?/?)(${ancestor})`) : undefined;
    return patterns
        .split(/\r?\n/)
        .filter(line => line.trim() && !line.startsWith('#'))
        .flatMap(line => {
        const pattern = line.replace(/^\\(?=#)/, '').trim();
        if (ancestor && matchFrom) {
            if (pattern.match(matchFrom))
                return [pattern.replace(matchFrom, '$1')];
            if (pattern.startsWith('/**/'))
                return [pattern.slice(1)];
            if (pattern.startsWith('!/**/'))
                return [`!${pattern.slice(2)}`];
            if (pattern.startsWith('/') || pattern.startsWith('!/'))
                return [];
        }
        return [pattern];
    })
        .map(pattern => convertGitignoreToPicomatchIgnorePatterns(pattern));
};
const findAncestorGitignoreFiles = (cwd) => {
    const gitignorePaths = [];
    let dir = dirname(cwd);
    let prev;
    while (dir) {
        const filePath = join(dir, '.gitignore');
        if (isFile(filePath))
            gitignorePaths.push(filePath);
        dir = dirname((prev = dir));
        if (prev === dir || dir === '.')
            break;
    }
    return gitignorePaths;
};
export const findAndParseGitignores = async (cwd) => {
    const init = ['.git', ...GLOBAL_IGNORE_PATTERNS];
    const ignores = new Set(init);
    const unignores = [];
    const gitignoreFiles = [];
    const pmOptions = { ignore: unignores };
    const matchers = new Set(init.map(pattern => _picomatch(pattern, pmOptions)));
    const matcher = (str) => {
        for (const isMatch of matchers) {
            const state = isMatch(str);
            if (state)
                return state;
        }
        return false;
    };
    const addFile = (filePath) => {
        gitignoreFiles.push(relative(cwd, filePath));
        const dir = dirname(toPosix(filePath));
        const base = relative(cwd, dir);
        const ancestor = base.startsWith('..') ? `${relative(dir, cwd)}/` : undefined;
        const dirIgnores = new Set(base === '' ? init : []);
        const dirUnignores = new Set();
        const patterns = readFileSync(filePath, 'utf8');
        for (const rule of parseAndConvertGitignorePatterns(patterns, ancestor)) {
            const [pattern1, pattern2] = rule.patterns;
            if (rule.negated) {
                if (base === '' || base.startsWith('..')) {
                    if (!unignores.includes(pattern2)) {
                        unignores.push(...rule.patterns);
                        dirUnignores.add(pattern1);
                        dirUnignores.add(pattern2);
                    }
                }
                else {
                    if (!unignores.includes(pattern2.startsWith('**/') ? pattern2 : `**/${pattern2}`)) {
                        const unignore = join(base, pattern1);
                        const extraUnignore = join(base, pattern2);
                        unignores.push(unignore, extraUnignore);
                        dirUnignores.add(unignore);
                        dirUnignores.add(extraUnignore);
                    }
                }
            }
            else {
                if (base === '' || base.startsWith('..')) {
                    ignores.add(pattern1);
                    ignores.add(pattern2);
                    dirIgnores.add(pattern1);
                    dirIgnores.add(pattern2);
                }
                else {
                    const ignore = join(base, pattern1);
                    const extraIgnore = join(base, pattern2);
                    ignores.add(ignore);
                    ignores.add(extraIgnore);
                    dirIgnores.add(ignore);
                    dirIgnores.add(extraIgnore);
                }
            }
        }
        const cacheDir = ancestor ? cwd : dir;
        const cacheForDir = cachedGitIgnores.get(cwd);
        if (ancestor && cacheForDir) {
            for (const pattern of dirIgnores)
                cacheForDir?.ignores.add(pattern);
            cacheForDir.unignores = Array.from(new Set([...cacheForDir.unignores, ...dirUnignores]));
        }
        else {
            cachedGitIgnores.set(cacheDir, { ignores: dirIgnores, unignores: Array.from(dirUnignores) });
        }
        for (const pattern of dirIgnores)
            matchers.add(_picomatch(pattern, pmOptions));
    };
    findAncestorGitignoreFiles(cwd).forEach(addFile);
    if (isFile('.git/info/exclude'))
        addFile('.git/info/exclude');
    const entryFilter = (entry) => {
        if (entry.dirent.isFile() && entry.name === '.gitignore') {
            addFile(entry.path);
            return true;
        }
        return false;
    };
    const deepFilter = (entry) => !matcher(relative(cwd, entry.path));
    await walk(cwd, {
        entryFilter: timerify(entryFilter),
        deepFilter: timerify(deepFilter),
    });
    debugLogObject('*', 'Parsed gitignore files', { gitignoreFiles });
    return { gitignoreFiles, ignores, unignores };
};
const _parseFindGitignores = timerify(findAndParseGitignores);
export async function glob(patterns, options) {
    if (Array.isArray(patterns) && patterns.length === 0)
        return [];
    const canCache = options.label && options.gitignore;
    const willCache = canCache && !cachedGlobIgnores.has(options.dir);
    const cachedIgnores = canCache && cachedGlobIgnores.get(options.dir);
    const _ignore = options.gitignore && Array.isArray(options.ignore) ? [...options.ignore] : [];
    if (options.gitignore) {
        if (willCache) {
            let dir = options.dir;
            let prev;
            while (dir) {
                const cacheForDir = cachedGitIgnores.get(dir);
                if (cacheForDir) {
                    _ignore.push(...cacheForDir.ignores);
                }
                dir = dirname((prev = dir));
                if (prev === dir || dir === '.')
                    break;
            }
        }
    }
    else {
        _ignore.push(...GLOBAL_IGNORE_PATTERNS);
    }
    const ignore = cachedIgnores || compact(_ignore);
    const { dir, label, ...fgOptions } = { ...options, ignore };
    const paths = await fg.glob(patterns, fgOptions);
    debugLogObject(relative(options.cwd, options.dir) || ROOT_WORKSPACE_NAME, label ? `Finding ${label} paths` : 'Finding paths', () => ({ patterns, ...fgOptions, ignore: cachedIgnores ? `identical to ${dir} ↑` : ignore, paths }));
    if (willCache)
        cachedGlobIgnores.set(options.dir, ignore);
    return paths;
}
export async function getGitIgnoredHandler(options) {
    cachedGitIgnores.clear();
    if (options.gitignore === false)
        return () => false;
    const gitignore = await _parseFindGitignores(options.cwd);
    const matcher = _picomatch(Array.from(gitignore.ignores), { ignore: gitignore.unignores });
    const isGitIgnored = (filePath) => matcher(relative(options.cwd, filePath));
    return timerify(isGitIgnored);
}
