"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.JsonFile = exports.JsonSyntax = void 0;
const os = __importStar(require("os"));
const jju = __importStar(require("jju"));
const Text_1 = require("./Text");
const FileSystem_1 = require("./FileSystem");
/**
 * Specifies the variant of JSON syntax to be used.
 *
 * @public
 */
var JsonSyntax;
(function (JsonSyntax) {
    /**
     * Specifies the exact RFC 8259 format as implemented by the `JSON.parse()` system API.
     * This format was designed for machine generated inputs such as an HTTP payload.
     * It is not a recommend choice for human-authored files, because it does not support
     * code comments.
     *
     * @remarks
     *
     * A well-known quote from Douglas Crockford, the inventor of JSON:
     *
     * "I removed comments from JSON because I saw people were using them to hold parsing directives,
     * a practice which would have destroyed interoperability.  I know that the lack of comments makes
     * some people sad, but it shouldn't.  Suppose you are using JSON to keep configuration files,
     * which you would like to annotate.  Go ahead and insert all the comments you like.
     * Then pipe it through JSMin before handing it to your JSON parser."
     *
     * @see {@link https://datatracker.ietf.org/doc/html/rfc8259 | RFC 8259}
     */
    JsonSyntax["Strict"] = "strict";
    /**
     * `JsonSyntax.JsonWithComments` is the recommended format for human-authored config files.
     * It is a minimal extension to `JsonSyntax.Strict` adding support for code comments
     * using `//` and `/*`.
     *
     * @remarks
     *
     * VS Code calls this format `jsonc`, but it should not be confused with unrelated file formats
     * and libraries that also use the name "JSONC".
     *
     * To fix VS Code syntax highlighting, add this setting:
     * `"files.associations": { "*.json": "jsonc" }`
     *
     * To fix GitHub syntax highlighting, add this to your `.gitattributes`:
     * `*.json linguist-language=JSON-with-Comments`
     */
    JsonSyntax["JsonWithComments"] = "jsonWithComments";
    /**
     * JSON5 is a project that proposes a JSON-like format supplemented with ECMAScript 5.1
     * notations for objects, numbers, comments, and more.
     *
     * @remarks
     * Files using this format should use the `.json5` file extension instead of `.json`.
     *
     * JSON5 has substantial differences from JSON: object keys may be unquoted, trailing commas
     * are allowed, and strings may span multiple lines.  Whereas `JsonSyntax.JsonWithComments` can
     * be cheaply converted to standard JSON by stripping comments, parsing JSON5 requires a
     * nontrivial algorithm that may not be easily available in some contexts or programming languages.
     *
     * @see {@link https://json5.org/ | JSON5 project website}
     */
    JsonSyntax["Json5"] = "json5";
})(JsonSyntax = exports.JsonSyntax || (exports.JsonSyntax = {}));
const DEFAULT_ENCODING = 'utf8';
/**
 * Utilities for reading/writing JSON files.
 * @public
 */
class JsonFile {
    /**
     * Loads a JSON file.
     */
    static load(jsonFilename, options) {
        try {
            const contents = FileSystem_1.FileSystem.readFile(jsonFilename);
            const parseOptions = JsonFile._buildJjuParseOptions(options);
            return jju.parse(contents, parseOptions);
        }
        catch (error) {
            if (FileSystem_1.FileSystem.isNotExistError(error)) {
                throw error;
            }
            else {
                throw new Error(`Error reading "${JsonFile._formatPathForError(jsonFilename)}":` +
                    os.EOL +
                    `  ${error.message}`);
            }
        }
    }
    /**
     * An async version of {@link JsonFile.load}.
     */
    static async loadAsync(jsonFilename, options) {
        try {
            const contents = await FileSystem_1.FileSystem.readFileAsync(jsonFilename);
            const parseOptions = JsonFile._buildJjuParseOptions(options);
            return jju.parse(contents, parseOptions);
        }
        catch (error) {
            if (FileSystem_1.FileSystem.isNotExistError(error)) {
                throw error;
            }
            else {
                throw new Error(`Error reading "${JsonFile._formatPathForError(jsonFilename)}":` +
                    os.EOL +
                    `  ${error.message}`);
            }
        }
    }
    /**
     * Parses a JSON file's contents.
     */
    static parseString(jsonContents, options) {
        const parseOptions = JsonFile._buildJjuParseOptions(options);
        return jju.parse(jsonContents, parseOptions);
    }
    /**
     * Loads a JSON file and validate its schema.
     */
    static loadAndValidate(jsonFilename, jsonSchema, options) {
        const jsonObject = JsonFile.load(jsonFilename, options);
        jsonSchema.validateObject(jsonObject, jsonFilename, options);
        return jsonObject;
    }
    /**
     * An async version of {@link JsonFile.loadAndValidate}.
     */
    static async loadAndValidateAsync(jsonFilename, jsonSchema, options) {
        const jsonObject = await JsonFile.loadAsync(jsonFilename, options);
        jsonSchema.validateObject(jsonObject, jsonFilename, options);
        return jsonObject;
    }
    /**
     * Loads a JSON file and validate its schema, reporting errors using a callback
     * @remarks
     * See JsonSchema.validateObjectWithCallback() for more info.
     */
    static loadAndValidateWithCallback(jsonFilename, jsonSchema, errorCallback, options) {
        const jsonObject = JsonFile.load(jsonFilename, options);
        jsonSchema.validateObjectWithCallback(jsonObject, errorCallback);
        return jsonObject;
    }
    /**
     * An async version of {@link JsonFile.loadAndValidateWithCallback}.
     */
    static async loadAndValidateWithCallbackAsync(jsonFilename, jsonSchema, errorCallback, options) {
        const jsonObject = await JsonFile.loadAsync(jsonFilename, options);
        jsonSchema.validateObjectWithCallback(jsonObject, errorCallback);
        return jsonObject;
    }
    /**
     * Serializes the specified JSON object to a string buffer.
     * @param jsonObject - the object to be serialized
     * @param options - other settings that control serialization
     * @returns a JSON string, with newlines, and indented with two spaces
     */
    static stringify(jsonObject, options) {
        return JsonFile.updateString('', jsonObject, options);
    }
    /**
     * Serializes the specified JSON object to a string buffer.
     * @param previousJson - the previous JSON string, which will be updated
     * @param newJsonObject - the object to be serialized
     * @param options - other settings that control serialization
     * @returns a JSON string, with newlines, and indented with two spaces
     */
    static updateString(previousJson, newJsonObject, options) {
        if (!options) {
            options = {};
        }
        if (!options.ignoreUndefinedValues) {
            // Standard handling of `undefined` in JSON stringification is to discard the key.
            JsonFile.validateNoUndefinedMembers(newJsonObject);
        }
        let stringified;
        if (previousJson !== '') {
            // NOTE: We don't use mode=json here because comments aren't allowed by strict JSON
            stringified = jju.update(previousJson, newJsonObject, {
                mode: 'cjson',
                indent: 2
            });
        }
        else if (options.prettyFormatting) {
            stringified = jju.stringify(newJsonObject, {
                mode: 'json',
                indent: 2
            });
            if (options.headerComment !== undefined) {
                stringified = JsonFile._formatJsonHeaderComment(options.headerComment) + stringified;
            }
        }
        else {
            stringified = JSON.stringify(newJsonObject, undefined, 2);
            if (options.headerComment !== undefined) {
                stringified = JsonFile._formatJsonHeaderComment(options.headerComment) + stringified;
            }
        }
        // Add the trailing newline
        stringified = Text_1.Text.ensureTrailingNewline(stringified);
        if (options && options.newlineConversion) {
            stringified = Text_1.Text.convertTo(stringified, options.newlineConversion);
        }
        return stringified;
    }
    /**
     * Saves the file to disk.  Returns false if nothing was written due to options.onlyIfChanged.
     * @param jsonObject - the object to be saved
     * @param jsonFilename - the file path to write
     * @param options - other settings that control how the file is saved
     * @returns false if ISaveJsonFileOptions.onlyIfChanged didn't save anything; true otherwise
     */
    static save(jsonObject, jsonFilename, options) {
        if (!options) {
            options = {};
        }
        // Do we need to read the previous file contents?
        let oldBuffer = undefined;
        if (options.updateExistingFile || options.onlyIfChanged) {
            try {
                oldBuffer = FileSystem_1.FileSystem.readFileToBuffer(jsonFilename);
            }
            catch (error) {
                if (!FileSystem_1.FileSystem.isNotExistError(error)) {
                    throw error;
                }
            }
        }
        let jsonToUpdate = '';
        if (options.updateExistingFile && oldBuffer) {
            jsonToUpdate = oldBuffer.toString(DEFAULT_ENCODING);
        }
        const newJson = JsonFile.updateString(jsonToUpdate, jsonObject, options);
        const newBuffer = Buffer.from(newJson, DEFAULT_ENCODING);
        if (options.onlyIfChanged) {
            // Has the file changed?
            if (oldBuffer && Buffer.compare(newBuffer, oldBuffer) === 0) {
                // Nothing has changed, so don't touch the file
                return false;
            }
        }
        FileSystem_1.FileSystem.writeFile(jsonFilename, newBuffer, {
            ensureFolderExists: options.ensureFolderExists
        });
        // TEST CODE: Used to verify that onlyIfChanged isn't broken by a hidden transformation during saving.
        /*
        const oldBuffer2: Buffer = FileSystem.readFileToBuffer(jsonFilename);
        if (Buffer.compare(buffer, oldBuffer2) !== 0) {
          console.log('new:' + buffer.toString('hex'));
          console.log('old:' + oldBuffer2.toString('hex'));
    
          throw new Error('onlyIfChanged logic is broken');
        }
        */
        return true;
    }
    /**
     * An async version of {@link JsonFile.save}.
     */
    static async saveAsync(jsonObject, jsonFilename, options) {
        if (!options) {
            options = {};
        }
        // Do we need to read the previous file contents?
        let oldBuffer = undefined;
        if (options.updateExistingFile || options.onlyIfChanged) {
            try {
                oldBuffer = await FileSystem_1.FileSystem.readFileToBufferAsync(jsonFilename);
            }
            catch (error) {
                if (!FileSystem_1.FileSystem.isNotExistError(error)) {
                    throw error;
                }
            }
        }
        let jsonToUpdate = '';
        if (options.updateExistingFile && oldBuffer) {
            jsonToUpdate = oldBuffer.toString(DEFAULT_ENCODING);
        }
        const newJson = JsonFile.updateString(jsonToUpdate, jsonObject, options);
        const newBuffer = Buffer.from(newJson, DEFAULT_ENCODING);
        if (options.onlyIfChanged) {
            // Has the file changed?
            if (oldBuffer && Buffer.compare(newBuffer, oldBuffer) === 0) {
                // Nothing has changed, so don't touch the file
                return false;
            }
        }
        await FileSystem_1.FileSystem.writeFileAsync(jsonFilename, newBuffer, {
            ensureFolderExists: options.ensureFolderExists
        });
        // TEST CODE: Used to verify that onlyIfChanged isn't broken by a hidden transformation during saving.
        /*
        const oldBuffer2: Buffer = await FileSystem.readFileToBufferAsync(jsonFilename);
        if (Buffer.compare(buffer, oldBuffer2) !== 0) {
          console.log('new:' + buffer.toString('hex'));
          console.log('old:' + oldBuffer2.toString('hex'));
    
          throw new Error('onlyIfChanged logic is broken');
        }
        */
        return true;
    }
    /**
     * Used to validate a data structure before writing.  Reports an error if there
     * are any undefined members.
     */
    static validateNoUndefinedMembers(jsonObject) {
        return JsonFile._validateNoUndefinedMembers(jsonObject, []);
    }
    // Private implementation of validateNoUndefinedMembers()
    static _validateNoUndefinedMembers(jsonObject, keyPath) {
        if (!jsonObject) {
            return;
        }
        if (typeof jsonObject === 'object') {
            for (const key of Object.keys(jsonObject)) {
                keyPath.push(key);
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                const value = jsonObject[key];
                if (value === undefined) {
                    const fullPath = JsonFile._formatKeyPath(keyPath);
                    throw new Error(`The value for ${fullPath} is "undefined" and cannot be serialized as JSON`);
                }
                JsonFile._validateNoUndefinedMembers(value, keyPath);
                keyPath.pop();
            }
        }
    }
    // Given this input:    ['items', '4', 'syntax', 'parameters', 'string "with" symbols", 'type']
    // Return this string:  items[4].syntax.parameters["string \"with\" symbols"].type
    static _formatKeyPath(keyPath) {
        let result = '';
        for (const key of keyPath) {
            if (/^[0-9]+$/.test(key)) {
                // It's an integer, so display like this:  parent[123]
                result += `[${key}]`;
            }
            else if (/^[a-z_][a-z_0-9]*$/i.test(key)) {
                // It's an alphanumeric identifier, so display like this:  parent.name
                if (result) {
                    result += '.';
                }
                result += `${key}`;
            }
            else {
                // It's a freeform string, so display like this:  parent["A path: \"C:\\file\""]
                // Convert this:     A path: "C:\file"
                // To this:          A path: \"C:\\file\"
                const escapedKey = key
                    .replace(/[\\]/g, '\\\\') // escape backslashes
                    .replace(/["]/g, '\\'); // escape quotes
                result += `["${escapedKey}"]`;
            }
        }
        return result;
    }
    static _formatJsonHeaderComment(headerComment) {
        if (headerComment === '') {
            return '';
        }
        const lines = headerComment.split('\n');
        const result = [];
        for (const line of lines) {
            if (!/^\s*$/.test(line) && !/^\s*\/\//.test(line)) {
                throw new Error('The headerComment lines must be blank or start with the "//" prefix.\n' +
                    'Invalid line' +
                    JSON.stringify(line));
            }
            result.push(Text_1.Text.replaceAll(line, '\r', ''));
        }
        return lines.join('\n') + '\n';
    }
    static _buildJjuParseOptions(options) {
        if (!options) {
            options = {};
        }
        const parseOptions = {};
        switch (options.jsonSyntax) {
            case JsonSyntax.Strict:
                parseOptions.mode = 'json';
                break;
            case JsonSyntax.JsonWithComments:
                parseOptions.mode = 'cjson';
                break;
            case JsonSyntax.Json5:
            default:
                parseOptions.mode = 'json5';
                break;
        }
        return parseOptions;
    }
}
/**
 * @internal
 */
JsonFile._formatPathForError = (path) => path;
exports.JsonFile = JsonFile;
//# sourceMappingURL=JsonFile.js.map