import path from "path";

import fs from "fs";



/** @typedef {{

    category: string;

    code: number;

    reportsUnnecessary?: {};

    reportsDeprecated?: {};

    isEarly?: boolean;

    elidedInCompatabilityPyramid?: boolean;

}} DiagnosticDetails */



/** @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\";",

        "",

        "function diag(code: number, category: DiagnosticCategory, key: string, message: string, reportsUnnecessary?: {}, elidedInCompatabilityPyramid?: boolean, reportsDeprecated?: {}): DiagnosticMessage {",

        "    return { code, category, key, message, reportsUnnecessary, elidedInCompatabilityPyramid, reportsDeprecated };",

        "}",

        "",

        "/** @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}),`);

    });



    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();