import * as fs from 'fs';
import * as path from 'path';
import * as util from 'util';
import * as glob from 'glob';

// Promisified versions of fs functions for easier async/await usage.
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
const mkdir = util.promisify(fs.mkdir);
const access = util.promisify(fs.access);

/**
 * Interface for piconfig.json
 */
interface PiConfig {
    [sourceDir: string]: string;
}

/**
 * Reads and parses the piconfig.json file.
 * @returns The parsed configuration object.
 */
async function readConfig(): Promise<PiConfig> {
    const configPath = path.resolve('piconfig.json');
    try {
        const data = await readFile(configPath, 'utf8');
        const config: PiConfig = JSON.parse(data);
        console.log(`Loaded configuration from ${configPath}:`, config);
        return config;
    } catch (err) {
        console.error(`Error reading or parsing piconfig.json:`, err);
        process.exit(1);
    }
}

/**
 * Translates .pi content to Python syntax by replacing curly brackets with colons
 * and managing indentation.
 * @param content The content of the .pi file.
 * @returns The translated Python code.
 */
function translatePiToPy(content: string): string {
    const lines = content.split(/\r?\n/);
    const outputLines: string[] = [];
    const indentUnit = '    '; // 4 spaces
    let currentIndentLevel = 0;

    for (let line of lines) {
        let trimmedLine = line.trim();

        // Skip empty lines
        if (trimmedLine.length === 0) {
            outputLines.push('');
            continue;
        }

        // Handle closing brace
        if (trimmedLine.startsWith('}')) {
            currentIndentLevel = Math.max(currentIndentLevel - 1, 0);
            trimmedLine = trimmedLine.substring(1).trim();
            // If there's code after }, process it
            if (trimmedLine.length > 0) {
                // Fall through to process the remaining code
            } else {
                // No more code on this line
                outputLines.push('');
                continue;
            }
        }

        // Handle opening brace
        let hasOpeningBrace = false;
        if (trimmedLine.endsWith('{')) {
            hasOpeningBrace = true;
            trimmedLine = trimmedLine.substring(0, trimmedLine.length - 1).trim() + ':';
        }

        // Apply indentation
        const indent = indentUnit.repeat(currentIndentLevel);
        outputLines.push(indent + trimmedLine);

        // If there was an opening brace, increase the indent
        if (hasOpeningBrace) {
            currentIndentLevel += 1;
        }
    }

    return outputLines.join('\n');
}

/**
 * Ensures that the directory exists. If not, creates it recursively.
 * @param dirPath The directory path to ensure.
 */
async function ensureDirectory(dirPath: string) {
    try {
        await access(dirPath, fs.constants.F_OK);
        // Directory exists
    } catch {
        // Directory does not exist, create it
        await mkdir(dirPath, { recursive: true });
        console.log(`Created directory: ${dirPath}`);
    }
}

/**
 * Processes a single .pi file and writes the translated .py file.
 * @param inputPath The path to the .pi file.
 * @param outputPath The path to the output .py file.
 */
async function processFile(inputPath: string, outputPath: string) {
    try {
        console.log(`\nProcessing file: ${inputPath}`);
        const data = await readFile(inputPath, 'utf8');
        const translated = translatePiToPy(data);
        const outputDir = path.dirname(outputPath);
        await ensureDirectory(outputDir);
        await writeFile(outputPath, translated, 'utf8');
        console.log(`Successfully compiled ${inputPath} → ${outputPath}`);
    } catch (err) {
        console.error(`Error processing file ${inputPath}:`, err);
    }
}

/**
 * Processes files based on the provided configuration.
 * @param config The configuration object from piconfig.json.
 */
async function processConfig(config: PiConfig) {
    // Iterate over each key in the config
    for (const [sourceDir, targetDir] of Object.entries(config)) {
        console.log(`\nProcessing mapping: ${sourceDir} → ${targetDir}`);

        // Ensure the target directory exists
        await ensureDirectory(targetDir);

        // Define the glob pattern to find all .pi files recursively in sourceDir
        const pattern = `${sourceDir}/**/*.pi`;
        console.log(`Using glob pattern: ${pattern}`);

        const files = glob.sync(pattern, { nodir: true });
        console.log(`Found ${files.length} .pi file(s) in ${sourceDir}`);

        if (files.length === 0) {
            console.warn(`No .pi files found in ${sourceDir} using pattern ${pattern}`);
        }

        for (const file of files) {
            // Compute the relative path from the source directory
            const relativePath = path.relative(sourceDir, file);
            // Define the output file path in the target directory with .py extension
            const outputFilePath = path.join(targetDir, relativePath).replace(/\.pi$/i, '.py');
            await processFile(file, outputFilePath);
        }
    }
}

/**
 * Entry point of the script.
 */
async function main() {
    console.log("Starting PI to PY compilation...");

    // Read configuration
    const config = await readConfig();

    // Check if configuration is empty
    if (Object.keys(config).length === 0) {
        console.warn('piconfig.json is empty. No mappings to process.');
        return;
    }

    // Process files based on the configuration
    await processConfig(config);

    console.log("\nPI to PY compilation completed.");
}

main();

---

There's a case we need to handle for the dictionaries:

pi:
dic = {
    "name": "John"
}

print(dic)

---

being compiled to:
dic =:
    "name": "John"


print(dic)

---

Dictionaries, in general, shouldn't be touched, whether they're:

dic = { "name": "John" }

or:

dic = {
  "name": "John"
}

or:

my_function({
  "name": "John"
})

or:

my_function({ "Name": "John" })