import { createHash } from 'crypto';
import { dirname, isAbsolute, join } from 'path';
import { executeCodegen } from './codegen.js';
import { ensureContext } from './config.js';
import { lifecycleHooks } from './hooks.js';
import { debugLog } from './utils/debugging.js';
import { mkdirp, readFile, unlinkFile, writeFile } from './utils/file-system.js';
import { createWatcher } from './utils/watcher.js';
const hash = (content) => createHash('sha1').update(content).digest('base64');
export async function generate(input, saveToFile = true) {
    const context = ensureContext(input);
    const config = context.getConfig();
    await context.profiler.run(() => lifecycleHooks(config.hooks).afterStart(), 'Lifecycle: afterStart');
    let previouslyGeneratedFilenames = [];
    function removeStaleFiles(config, generationResult) {
        const filenames = generationResult.map(o => o.filename);
        // find stale files from previous build which are not present in current build
        const staleFilenames = previouslyGeneratedFilenames.filter(f => !filenames.includes(f));
        for (const filename of staleFilenames) {
            if (shouldOverwrite(config, filename)) {
                unlinkFile(filename, err => {
                    const prettyFilename = filename.replace(`${input.cwd || process.cwd()}/`, '');
                    if (err) {
                        debugLog(`Cannot remove stale file: ${prettyFilename}\n${err}`);
                    }
                    else {
                        debugLog(`Removed stale file: ${prettyFilename}`);
                    }
                });
            }
        }
        previouslyGeneratedFilenames = filenames;
    }
    const recentOutputHash = new Map();
    async function writeOutput(generationResult) {
        if (!saveToFile) {
            return generationResult;
        }
        if (config.watch) {
            removeStaleFiles(config, generationResult);
        }
        await context.profiler.run(async () => {
            await lifecycleHooks(config.hooks).beforeAllFileWrite(generationResult.map(r => r.filename));
        }, 'Lifecycle: beforeAllFileWrite');
        await context.profiler.run(() => Promise.all(generationResult.map(async (result) => {
            const previousHash = recentOutputHash.get(result.filename) || (await hashFile(result.filename));
            const exists = previousHash !== null;
            // Store previous hash to avoid reading from disk again
            if (previousHash) {
                recentOutputHash.set(result.filename, previousHash);
            }
            if (!shouldOverwrite(config, result.filename) && exists) {
                return;
            }
            let content = result.content || '';
            const currentHash = hash(content);
            if (previousHash && currentHash === previousHash) {
                debugLog(`Skipping file (${result.filename}) writing due to indentical hash...`);
                return;
            }
            // skip updating file in dry mode
            if (context.checkMode) {
                context.checkModeStaleFiles.push(result.filename);
                return;
            }
            if (content.length === 0) {
                return;
            }
            const absolutePath = isAbsolute(result.filename)
                ? result.filename
                : join(input.cwd || process.cwd(), result.filename);
            const basedir = dirname(absolutePath);
            await mkdirp(basedir);
            content = await lifecycleHooks(result.hooks).beforeOneFileWrite(absolutePath, content);
            content = await lifecycleHooks(config.hooks).beforeOneFileWrite(absolutePath, content);
            if (content !== result.content) {
                result.content = content;
                // compare the prettified content with the previous hash
                // to compare the content with an existing prettified file
                if (hash(content) === previousHash) {
                    debugLog(`Skipping file (${result.filename}) writing due to indentical hash after prettier...`);
                    // the modified content is NOT stored in recentOutputHash
                    // so a diff can already be detected before executing the hook
                    return;
                }
            }
            await writeFile(absolutePath, result.content);
            recentOutputHash.set(result.filename, currentHash);
            await lifecycleHooks(result.hooks).afterOneFileWrite(result.filename);
            await lifecycleHooks(config.hooks).afterOneFileWrite(result.filename);
        })), 'Write files');
        await context.profiler.run(() => lifecycleHooks(config.hooks).afterAllFileWrite(generationResult.map(r => r.filename)), 'Lifecycle: afterAllFileWrite');
        return generationResult;
    }
    // watch mode
    if (config.watch) {
        return createWatcher(context, writeOutput).runningWatcher;
    }
    const outputFiles = await context.profiler.run(() => executeCodegen(context), 'executeCodegen');
    await context.profiler.run(() => writeOutput(outputFiles), 'writeOutput');
    await context.profiler.run(() => lifecycleHooks(config.hooks).beforeDone(), 'Lifecycle: beforeDone');
    if (context.profilerOutput) {
        await writeFile(join(context.cwd, context.profilerOutput), JSON.stringify(context.profiler.collect()));
    }
    return outputFiles;
}
function shouldOverwrite(config, outputPath) {
    const globalValue = config.overwrite === undefined ? true : !!config.overwrite;
    const outputConfig = config.generates[outputPath];
    if (!outputConfig) {
        debugLog(`Couldn't find a config of ${outputPath}`);
        return globalValue;
    }
    if (isConfiguredOutput(outputConfig) && typeof outputConfig.overwrite === 'boolean') {
        return outputConfig.overwrite;
    }
    return globalValue;
}
function isConfiguredOutput(output) {
    return typeof output.plugins !== 'undefined';
}
async function hashFile(filePath) {
    try {
        return hash(await readFile(filePath));
    }
    catch (err) {
        if (err && err.code === 'ENOENT') {
            // return null if file does not exist
            return null;
        }
        // rethrow unexpected errors
        throw err;
    }
}
