"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const util_1 = require("util");
const get_1 = __importDefault(require("lodash/get"));
const has_1 = __importDefault(require("lodash/has"));
const set_1 = __importDefault(require("lodash/set"));
const json5_1 = __importDefault(require("json5"));
const write_file_atomic_1 = __importDefault(require("write-file-atomic"));
const code_frame_1 = require("@babel/code-frame");
const JsonFileError_1 = __importDefault(require("./JsonFileError"));
const readFileAsync = util_1.promisify(fs_1.default.readFile);
const writeFileAtomicAsync = util_1.promisify(write_file_atomic_1.default);
const DEFAULT_OPTIONS = {
    badJsonDefault: undefined,
    jsonParseErrorDefault: undefined,
    cantReadFileDefault: undefined,
    default: undefined,
    json5: false,
    space: 2,
    addNewLineAtEOF: true,
};
/**
 * The JsonFile class represents the contents of json file.
 *
 * It's polymorphic on "JSONObject", which is a simple type representing
 * and object with string keys and either objects or primitive types as values.
 * @type {[type]}
 */
class JsonFile {
    constructor(file, options = {}) {
        this.file = file;
        this.options = options;
    }
    readAsync(options) {
        return __awaiter(this, void 0, void 0, function* () {
            return readAsync(this.file, this._getOptions(options));
        });
    }
    writeAsync(object, options) {
        return __awaiter(this, void 0, void 0, function* () {
            return writeAsync(this.file, object, this._getOptions(options));
        });
    }
    getAsync(key, defaultValue, options) {
        return __awaiter(this, void 0, void 0, function* () {
            return getAsync(this.file, key, defaultValue, this._getOptions(options));
        });
    }
    setAsync(key, value, options) {
        return __awaiter(this, void 0, void 0, function* () {
            return setAsync(this.file, key, value, this._getOptions(options));
        });
    }
    mergeAsync(sources, options) {
        return __awaiter(this, void 0, void 0, function* () {
            return mergeAsync(this.file, sources, this._getOptions(options));
        });
    }
    deleteKeyAsync(key, options) {
        return __awaiter(this, void 0, void 0, function* () {
            return deleteKeyAsync(this.file, key, this._getOptions(options));
        });
    }
    deleteKeysAsync(keys, options) {
        return __awaiter(this, void 0, void 0, function* () {
            return deleteKeysAsync(this.file, keys, this._getOptions(options));
        });
    }
    rewriteAsync(options) {
        return __awaiter(this, void 0, void 0, function* () {
            return rewriteAsync(this.file, this._getOptions(options));
        });
    }
    _getOptions(options) {
        return Object.assign({}, this.options, options);
    }
}
JsonFile.readAsync = readAsync;
JsonFile.writeAsync = writeAsync;
JsonFile.getAsync = getAsync;
JsonFile.setAsync = setAsync;
JsonFile.mergeAsync = mergeAsync;
JsonFile.deleteKeyAsync = deleteKeyAsync;
JsonFile.deleteKeysAsync = deleteKeysAsync;
JsonFile.rewriteAsync = rewriteAsync;
exports.default = JsonFile;
function readAsync(file, options) {
    return __awaiter(this, void 0, void 0, function* () {
        let json;
        try {
            json = yield readFileAsync(file, 'utf8');
        }
        catch (error) {
            let defaultValue = cantReadFileDefault(options);
            if (defaultValue === undefined) {
                throw new JsonFileError_1.default(`Can't read JSON file: ${file}`, error, error.code);
            }
            else {
                return defaultValue;
            }
        }
        try {
            if (_getOption(options, 'json5')) {
                return json5_1.default.parse(json);
            }
            else {
                return JSON.parse(json);
            }
        }
        catch (e) {
            let defaultValue = jsonParseErrorDefault(options);
            if (defaultValue === undefined) {
                let location = locationFromSyntaxError(e, json);
                if (location) {
                    let codeFrame = code_frame_1.codeFrameColumns(json, { start: location });
                    e.codeFrame = codeFrame;
                    e.message += `\n${codeFrame}`;
                }
                throw new JsonFileError_1.default(`Error parsing JSON file: ${file}`, e, 'EJSONPARSE');
            }
            else {
                return defaultValue;
            }
        }
    });
}
function getAsync(file, key, defaultValue, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const object = yield readAsync(file, options);
        if (defaultValue === undefined && !has_1.default(object, key)) {
            throw new JsonFileError_1.default(`No value at key path "${key}" in JSON object from: ${file}`);
        }
        return get_1.default(object, key, defaultValue);
    });
}
function writeAsync(file, object, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const space = _getOption(options, 'space');
        const json5 = _getOption(options, 'json5');
        const addNewLineAtEOF = _getOption(options, 'addNewLineAtEOF');
        let json;
        try {
            if (json5) {
                json = json5_1.default.stringify(object, null, space);
            }
            else {
                json = JSON.stringify(object, null, space);
            }
        }
        catch (e) {
            throw new JsonFileError_1.default(`Couldn't JSON.stringify object for file: ${file}`, e);
        }
        const data = addNewLineAtEOF ? `${json}\n` : json;
        yield writeFileAtomicAsync(file, data, {});
        return object;
    });
}
function setAsync(file, key, value, options) {
    return __awaiter(this, void 0, void 0, function* () {
        // TODO: Consider implementing some kind of locking mechanism, but
        // it's not critical for our use case, so we'll leave it out for now
        let object = yield readAsync(file, options);
        object = set_1.default(object, key, value);
        return writeAsync(file, object, options);
    });
}
function mergeAsync(file, sources, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const object = yield readAsync(file, options);
        if (Array.isArray(sources)) {
            Object.assign(object, ...sources);
        }
        else {
            Object.assign(object, sources);
        }
        return writeAsync(file, object, options);
    });
}
function deleteKeyAsync(file, key, options) {
    return __awaiter(this, void 0, void 0, function* () {
        return deleteKeysAsync(file, [key], options);
    });
}
function deleteKeysAsync(file, keys, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const object = yield readAsync(file, options);
        let didDelete = false;
        for (let i = 0; i < keys.length; i++) {
            let key = keys[i];
            if (object.hasOwnProperty(key)) {
                delete object[key];
                didDelete = true;
            }
        }
        if (didDelete) {
            return writeAsync(file, object, options);
        }
        return object;
    });
}
function rewriteAsync(file, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const object = yield readAsync(file, options);
        return writeAsync(file, object, options);
    });
}
function jsonParseErrorDefault(options = {}) {
    if (options.jsonParseErrorDefault === undefined) {
        return options.default;
    }
    else {
        return options.jsonParseErrorDefault;
    }
}
function cantReadFileDefault(options = {}) {
    if (options.cantReadFileDefault === undefined) {
        return options.default;
    }
    else {
        return options.cantReadFileDefault;
    }
}
function _getOption(options, field) {
    if (options) {
        if (options[field] !== undefined) {
            return options[field];
        }
    }
    return DEFAULT_OPTIONS[field];
}
function locationFromSyntaxError(error, sourceString) {
    // JSON5 SyntaxError has lineNumber and columnNumber.
    if ('lineNumber' in error && 'columnNumber' in error) {
        return { line: error.lineNumber, column: error.columnNumber };
    }
    // JSON SyntaxError only includes the index in the message.
    let match = /at position (\d+)/.exec(error.message);
    if (match) {
        let index = parseInt(match[1], 10);
        let lines = sourceString.slice(0, index + 1).split('\n');
        return { line: lines.length, column: lines[lines.length - 1].length };
    }
    return null;
}
//# sourceMappingURL=JsonFile.js.map