import fs from "fs";
import path from "path";

/** @typedef {{
    category: string;
    code: number;
    reportsUnnecessary?: {};
    reportsDeprecated?: {};
    isEarly?: boolean;
    elidedInCompatabilityPyramid?: boolean;
}} DiagnosticDetails */
void 0;

/** @typedef {Map<string, DiagnosticDetails>} InputDiagnosticMessageTable */

async function main() {
    if (process.argv.length < 3) {
        console.log("Usage:");
        console.log("\tnode processDiagnosticMessages.mjs <diagnostic-json-input-file>");
        return;
    }

    /**
     * @param {string} fileName
     * @param {string} contents
     */
    async function writeFile(fileName, contents) {
        const filePath = path.join(path.dirname(inputFilePath), fileName);
        try {
            const existingContents = await fs.promises.readFile(filePath, "utf-8");
            if (existingContents === contents) {
                return;
            }
        }
        catch {
            // Just write the file.
        }

        await fs.promises.writeFile(filePath, contents, { encoding: "utf-8" });
    }

    const inputFilePath = process.argv[2].replace(/\\/g, "/");
    console.log(`Reading diagnostics from ${inputFilePath}`);
    const inputStr = await fs.promises.readFile(inputFilePath, { encoding: "utf-8" });

    /** @type {{ [key: string]: DiagnosticDetails }} */
    const diagnosticMessagesJson = JSON.parse(inputStr);

    /** @type {InputDiagnosticMessageTable} */
    const diagnosticMessages = new Map();
    for (const key in diagnosticMessagesJson) {
        if (Object.hasOwnProperty.call(diagnosticMessagesJson, key)) {
            diagnosticMessages.set(key, diagnosticMessagesJson[key]);
        }
    }

    const infoFileOutput = buildInfoFileOutput(diagnosticMessages, inputFilePath);
    checkForUniqueCodes(diagnosticMessages);
    await writeFile("diagnosticInformationMap.generated.ts", infoFileOutput);

    const messageOutput = buildDiagnosticMessageOutput(diagnosticMessages);
    await writeFile("diagnosticMessages.generated.json", messageOutput);
}

/**
 * @param {InputDiagnosticMessageTable} diagnosticTable
 */
function checkForUniqueCodes(diagnosticTable) {
    /** @type {Record<number, true | undefined>} */
    const allCodes = [];
    diagnosticTable.forEach(({ code }) => {
        if (allCodes[code]) {
            throw new Error(`Diagnostic code ${code} appears more than once.`);
        }
        allCodes[code] = true;
    });
}

/**
 * @param {InputDiagnosticMessageTable} messageTable
 * @param {string} inputFilePathRel
 * @returns {string}
 */
function buildInfoFileOutput(messageTable, inputFilePathRel) {
    const result = [
        "// <auto-generated />",
        `// generated from '${inputFilePathRel}'`,
        "",
        'import { DiagnosticCategory, DiagnosticMessage } from "./types.js";',
        "",
        "function diag(code: number, category: DiagnosticCategory, key: string, message: string, reportsUnnecessary?: {}, elidedInCompatabilityPyramid?: boolean, reportsDeprecated?: {}): DiagnosticMessage {",
        "    return { code, category, key, message, reportsUnnecessary, elidedInCompatabilityPyramid, reportsDeprecated };",
        "}",
        "",
        "/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion*/", // type assertions are needed for isolatedDeclarations
        "/** @internal */",
        "export const Diagnostics = {",
    ];
    messageTable.forEach(({ code, category, reportsUnnecessary, elidedInCompatabilityPyramid, reportsDeprecated }, name) => {
        const propName = convertPropertyName(name);
        const argReportsUnnecessary = reportsUnnecessary ? `, /*reportsUnnecessary*/ ${reportsUnnecessary}` : "";
        const argElidedInCompatabilityPyramid = elidedInCompatabilityPyramid ? `${!reportsUnnecessary ? ", /*reportsUnnecessary*/ undefined" : ""}, /*elidedInCompatabilityPyramid*/ ${elidedInCompatabilityPyramid}` : "";
        const argReportsDeprecated = reportsDeprecated ? `${!argElidedInCompatabilityPyramid ? ", /*reportsUnnecessary*/ undefined, /*elidedInCompatabilityPyramid*/ undefined" : ""}, /*reportsDeprecated*/ ${reportsDeprecated}` : "";

        result.push(`    ${propName}: diag(${code}, DiagnosticCategory.${category}, "${createKey(propName, code)}", ${JSON.stringify(name)}${argReportsUnnecessary}${argElidedInCompatabilityPyramid}${argReportsDeprecated}) as DiagnosticMessage,`);
    });

    result.push("};");

    return result.join("\r\n");
}

/**
 * @param {InputDiagnosticMessageTable} messageTable
 * @returns {string}
 */
function buildDiagnosticMessageOutput(messageTable) {
    /** @type {Record<string, string>} */
    const result = {};

    messageTable.forEach(({ code }, name) => {
        const propName = convertPropertyName(name);
        result[createKey(propName, code)] = name;
    });

    return JSON.stringify(result, undefined, 2).replace(/\r?\n/g, "\r\n");
}

/**
 * @param {string} name
 * @param {number} code
 * @returns {string}
 */
function createKey(name, code) {
    return name.slice(0, 100) + "_" + code;
}

/**
 * @param {string} origName
 * @returns {string}
 */
function convertPropertyName(origName) {
    let result = origName.split("").map(char => {
        if (char === "*") return "_Asterisk";
        if (char === "/") return "_Slash";
        if (char === ":") return "_Colon";
        return /\w/.test(char) ? char : "_";
    }).join("");

    // get rid of all multi-underscores
    result = result.replace(/_+/g, "_");

    // remove any leading underscore, unless it is followed by a number.
    result = result.replace(/^_(\D)/, "$1");

    // get rid of all trailing underscores.
    result = result.replace(/_$/, "");

    return result;
}

main();
