"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    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) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PersistenceLayer = void 0;
const _1 = require(".");
const internal_1 = require("../internal");
const DocumentPersistence_1 = require("./CdmFolder/DocumentPersistence");
const PersistenceTypes = {
    CdmFolder: _1.CdmFolder,
    ModelJson: _1.ModelJson
};
class PersistenceLayer {
    /**
     * Constructs a PersistenceLayer and registers persistence classes to load and save known file formats.
     * @param corpus The corpus that owns this persistence layer.
     */
    constructor(corpus) {
        this.TAG = PersistenceLayer.name;
        this.corpus = corpus;
        this.ctx = this.corpus.ctx;
        this.registeredPersistenceFormats = new Map();
        this.isRegisteredPersistenceAsync = new Map();
        // why do this? because in some mysterious situations the CdmFolder and ModelJson imported objects (turn into a required)
        // are undefined when the PersistenceTypes const is initialized.
        // later, like when this constructor is called and all the code is loaded and they are set.
        PersistenceTypes.CdmFolder = _1.CdmFolder;
        PersistenceTypes.ModelJson = _1.ModelJson;
    }
    /**
     * @param args arguments passed to the persistence class.
     * @param objectType any of cdmObjectType.
     * @param persistenceType a type supported by the persistence layer. Can by any of PersistenceTypes.
     */
    static fromData(...args) {
        const persistenceType = args.pop();
        const objectType = args.pop();
        const persistenceClass = PersistenceLayer.fetchPersistenceClass(objectType, persistenceType);
        return persistenceClass.fromData.apply(this, arguments);
    }
    /**
     * @param instance the instance that is going to be serialized.
     * @param resOpt information about how to resolve the instance.
     * @param persistenceType a type supported by the persistence layer. Can by any of PersistenceTypes.
     * @param options set of options to specify how the output format.
     */
    static toData(instance, resOpt, options, persistenceType) {
        const objectType = instance.objectType;
        const persistenceClass = PersistenceLayer.fetchPersistenceClass(objectType, persistenceType);
        return persistenceClass.toData(instance, resOpt, options);
    }
    static fetchPersistenceClass(objectType, persistenceType) {
        const persistenceLayer = PersistenceTypes[persistenceType];
        if (persistenceLayer) {
            let objectName = internal_1.cdmObjectType[objectType];
            // tslint:disable:newline-per-chained-call
            objectName = objectName.charAt(0).toUpperCase() + objectName.slice(1);
            const defString = 'Def';
            if (objectName.endsWith(defString)) {
                objectName = objectName.slice(0, (defString.length) * -1);
            }
            else if (objectName.endsWith('Ref')) {
                objectName += 'erence';
            }
            const persistenceClassName = `${objectName}Persistence`;
            const persistenceClass = persistenceLayer[persistenceClassName];
            if (!persistenceClass) {
                throw new Error(`Persistence class ${persistenceClassName} not implemented in type ${persistenceType}.`);
            }
            return persistenceClass;
        }
        else {
            throw new Error(`Persistence type ${persistenceType} not implemented.`);
        }
    }
    /**
     * @internal
     * Loads a document from the folder path.
     * @param folder The folder that contains the document we want to load.
     * @param docName The document name.
     * @param docContainer The loaded document, if it was previously loaded.
     * @param resOpt Optional parameter. The resolve options.
     */
    LoadDocumentFromPathAsync(folder, docName, docContainer, resOpt = undefined) {
        return __awaiter(this, void 0, void 0, function* () {
            let docContent;
            let jsonData;
            let fsModifiedTime;
            const docPath = folder.folderPath + docName;
            const adapter = this.corpus.storage.fetchAdapter(folder.namespace);
            try {
                if (adapter.canRead()) {
                    // log message used by navigator, do not change or remove
                    internal_1.Logger.debug(this.ctx, this.TAG, this.LoadDocumentFromPathAsync.name, docPath, `request file: ${docPath}`);
                    jsonData = yield adapter.readAsync(docPath);
                    if (internal_1.StringUtils.isBlankByCdmStandard(jsonData)) {
                        let errorMsg = 'Json data is null or empty.';
                        internal_1.Logger.error(this.ctx, this.TAG, this.LoadDocumentFromPathAsync.name, docPath, internal_1.cdmLogCode.ErrPersistFileReadFailure, docPath, folder.namespace, errorMsg);
                        return undefined;
                    }
                    // log message used by navigator, do not change or remove
                    internal_1.Logger.debug(this.ctx, this.TAG, this.LoadDocumentFromPathAsync.name, docPath, `received file: ${docPath}`);
                }
                else {
                    throw new Error('Storage Adapter is not enabled to read.');
                }
            }
            catch (e) {
                // log message used by navigator, do not change or remove
                internal_1.Logger.debug(this.ctx, this.TAG, this.LoadDocumentFromPathAsync.name, docPath, `fail file: ${docPath}`);
                // When shallow validation is enabled, log messages about being unable to find referenced documents as warnings instead of errors.
                if (resOpt && resOpt.shallowValidation) {
                    internal_1.Logger.warning(this.ctx, this.TAG, this.LoadDocumentFromPathAsync.name, docPath, internal_1.cdmLogCode.WarnPersistFileReadFailure, docPath, folder.namespace, e);
                }
                else {
                    internal_1.Logger.error(this.ctx, this.TAG, this.LoadDocumentFromPathAsync.name, docPath, internal_1.cdmLogCode.ErrPersistFileReadFailure, docPath, folder.namespace, e);
                }
                return undefined;
            }
            try {
                fsModifiedTime = yield adapter.computeLastModifiedTimeAsync(docPath);
            }
            catch (e) {
                internal_1.Logger.warning(this.ctx, this.TAG, this.LoadDocumentFromPathAsync.name, docPath, internal_1.cdmLogCode.WarnPersistFileModTimeFailure, e);
            }
            if (internal_1.StringUtils.isBlankByCdmStandard(docName)) {
                internal_1.Logger.error(this.ctx, this.TAG, this.LoadDocumentFromPathAsync.name, docPath, internal_1.cdmLogCode.ErrPersistNullDocName);
                return undefined;
            }
            // If loading an model.json file, check that it is named correctly.
            if (docName.toLowerCase().endsWith(internal_1.CdmConstants.modelJsonExtension) &&
                docName.toLowerCase() !== internal_1.CdmConstants.modelJsonExtension) {
                internal_1.Logger.error(this.ctx, this.TAG, this.LoadDocumentFromPathAsync.name, docPath, internal_1.cdmLogCode.ErrPersistDocNameLoadFailure, docName, internal_1.CdmConstants.modelJsonExtension);
                return undefined;
            }
            const docNameInLowCase = docName.toLowerCase();
            try {
                // Check file extensions, which performs a case-insensitive ordinal string comparison
                if (docNameInLowCase.endsWith(internal_1.CdmConstants.folioExtension) ||
                    docNameInLowCase.endsWith(internal_1.CdmConstants.manifestExtension)) {
                    docContent = _1.CdmFolder.ManifestPersistence.fromObject(this.ctx, docName, folder.namespace, folder.folderPath, JSON.parse(jsonData));
                }
                else if (docNameInLowCase.endsWith(internal_1.CdmConstants.modelJsonExtension)) {
                    docContent = (yield _1.ModelJson.ManifestPersistence.fromObject(this.ctx, JSON.parse(jsonData), folder));
                }
                else if (docNameInLowCase.endsWith(internal_1.CdmConstants.cdmExtension)) {
                    docContent = DocumentPersistence_1.DocumentPersistence.fromObject(this.ctx, docName, folder.namespace, folder.folderPath, JSON.parse(jsonData));
                }
                else {
                    // Could not find a registered persistence class to handle this document type.
                    internal_1.Logger.error(this.ctx, this.TAG, this.LoadDocumentFromPathAsync.name, docPath, internal_1.cdmLogCode.ErrPersistClassMissing, docName);
                    return undefined;
                }
            }
            catch (e) {
                // Could not find a registered persistence class to handle this document type.
                internal_1.Logger.error(this.ctx, this.TAG, this.LoadDocumentFromPathAsync.name, undefined, internal_1.cdmLogCode.ErrPersistDocConversionFailure, docName, e.message);
                return undefined;
            }
            //Add document to the folder, this sets all the folder/path things,
            // caches name to content association and may trigger indexing on content
            if (docContent) {
                if (docContainer) {
                    // there are situations where a previously loaded document must be re-loaded.
                    // the end of that chain of work is here where the old version of the document has been removed from
                    // the corpus and we have created a new document and loaded it from storage and after this call we will probably
                    // add it to the corpus and index it, etc.
                    // it would be really rude to just kill that old object and replace it with this replicant, especially because
                    // the caller has no idea this happened. so... sigh ... instead of returning the new object return the one that
                    // was just killed off but make it contain everything the new document loaded.
                    docContent = docContent.copy(new internal_1.resolveOptions(docContainer, this.ctx.corpus.defaultResolutionDirectives), docContainer);
                }
                if (folder.documents.allItems.find((x) => x.ID === docContent.ID) == null) {
                    folder.documents.push(docContent, docName);
                }
                docContent._fileSystemModifiedTime = fsModifiedTime;
                docContent.isDirty = false;
            }
            return docContent;
        });
    }
    /**
     * @internal
     * a manifest or document can be saved with a new or exisitng name. This function on the corpus does all the actual work
     * because the corpus knows about persistence types and about the storage adapters
     * if saved with the same name, then consider this document 'clean' from changes. if saved with a back compat model or
     * to a different name, then the source object is still 'dirty'
     * an option will cause us to also save any linked documents.
     */
    saveDocumentAsAsync(doc, options, newName, saveReferenced) {
        return __awaiter(this, void 0, void 0, function* () {
            // update the jsonSemanticVersion to the max(author setting, mimimum required by usage)
            doc.discoverMinimumRequiredJsonSemanticVersion();
            // find out if the storage adapter is able to write.
            let ns = doc.namespace;
            if (internal_1.StringUtils.isBlankByCdmStandard(ns)) {
                ns = this.corpus.storage.defaultNamespace;
            }
            const adapter = this.corpus.storage.fetchAdapter(ns);
            if (adapter === undefined) {
                internal_1.Logger.error(this.ctx, this.TAG, this.saveDocumentAsAsync.name, doc.atCorpusPath, internal_1.cdmLogCode.ErrPersistAdapterNotFoundForNamespace, ns);
                return false;
            }
            else if (adapter.canWrite() === false) {
                internal_1.Logger.error(this.ctx, this.TAG, this.saveDocumentAsAsync.name, doc.atCorpusPath, internal_1.cdmLogCode.ErrPersistAdapterWriteFailure, ns);
                return false;
            }
            else {
                if (internal_1.StringUtils.isBlankByCdmStandard(newName)) {
                    internal_1.Logger.error(this.ctx, this.TAG, this.saveDocumentAsAsync.name, doc.atCorpusPath, internal_1.cdmLogCode.ErrPersistNullDocName);
                    return false;
                }
                // What kind of document is requested?
                // Check file extensions using a case-insensitive ordinal string comparison.
                const newNameInLowCase = newName.toLowerCase();
                const persistenceType = newNameInLowCase.endsWith(internal_1.CdmConstants.modelJsonExtension) ? PersistenceLayer.modelJson : PersistenceLayer.cdmFolder;
                if (persistenceType === PersistenceLayer.modelJson && newNameInLowCase !== internal_1.CdmConstants.modelJsonExtension) {
                    internal_1.Logger.error(this.ctx, this.TAG, this.saveDocumentAsAsync.name, doc.atCorpusPath, internal_1.cdmLogCode.ErrPersistFailure, newName, internal_1.CdmConstants.modelJsonExtension);
                    return false;
                }
                // save the object into a json blob
                const resOpt = new internal_1.resolveOptions(doc, new internal_1.AttributeResolutionDirectiveSet());
                let persistedDoc;
                if (newNameInLowCase.endsWith(internal_1.CdmConstants.modelJsonExtension) ||
                    newNameInLowCase.endsWith(internal_1.CdmConstants.manifestExtension)
                    || newNameInLowCase.endsWith(internal_1.CdmConstants.folioExtension)) {
                    if (persistenceType === 'CdmFolder') {
                        persistedDoc = _1.CdmFolder.ManifestPersistence.toData(doc, resOpt, options);
                    }
                    else {
                        if (newNameInLowCase !== internal_1.CdmConstants.modelJsonExtension) {
                            internal_1.Logger.error(this.ctx, this.TAG, this.saveDocumentAsAsync.name, doc.atCorpusPath, internal_1.cdmLogCode.ErrPersistFailure, newName);
                            return false;
                        }
                        persistedDoc = yield _1.ModelJson.ManifestPersistence.toData(doc, resOpt, options);
                    }
                }
                else if (newNameInLowCase.endsWith(internal_1.CdmConstants.cdmExtension)) {
                    persistedDoc = _1.CdmFolder.DocumentPersistence.toData(doc, resOpt, options);
                }
                else {
                    // Could not find a registered persistence class to handle this document type.
                    internal_1.Logger.error(this.ctx, this.TAG, this.saveDocumentAsAsync.name, doc.atCorpusPath, internal_1.cdmLogCode.ErrPersistClassMissing, newName);
                    return false;
                }
                if (!persistedDoc) {
                    internal_1.Logger.error(this.ctx, this.TAG, this.saveDocumentAsAsync.name, doc.atCorpusPath, internal_1.cdmLogCode.ErrPersistFilePersistFailed, newName);
                    return false;
                }
                // turn the name into a path
                let newPath = `${doc.folderPath}${newName}`;
                newPath = this.corpus.storage.createAbsoluteCorpusPath(newPath, doc);
                if (newPath.startsWith(`${ns}:`)) {
                    newPath = newPath.slice(ns.length + 1);
                }
                // ask the adapter to make it happen
                try {
                    const content = JSON.stringify(persistedDoc, undefined, 4);
                    yield adapter.writeAsync(newPath, content);
                    doc._fileSystemModifiedTime = yield adapter.computeLastModifiedTimeAsync(newPath);
                    // write the adapter's config.
                    if (options.saveConfigFile !== false && options.isTopLevelDocument) {
                        yield this.corpus.storage.saveAdaptersConfigAsync('/config.json', adapter);
                        // the next documentwon't be top level, so reset the flag
                        options.isTopLevelDocument = false;
                    }
                }
                catch (e) {
                    internal_1.Logger.error(this.ctx, this.TAG, this.saveDocumentAsAsync.name, doc.atCorpusPath, internal_1.cdmLogCode.ErrPersistFilePersistError, newName, e);
                }
                // if we also want to save referenced docs, then it depends on what kind of thing just got saved
                // if a model.json there are none. If a manifest or definition doc then ask the docs to do the right things
                // definition will save imports, manifests will save imports, schemas, sub manifests
                if (saveReferenced && persistenceType === PersistenceLayer.cdmFolder) {
                    if (!(yield doc.saveLinkedDocuments(options))) {
                        internal_1.Logger.error(this.ctx, this.TAG, this.saveDocumentAsAsync.name, doc.atCorpusPath, internal_1.cdmLogCode.ErrPersistSaveLinkedDocs, newName);
                    }
                }
                return true;
            }
        });
    }
    fetchRegisteredPersistenceFormat(docName) {
        for (const registeredPersistenceFormat of this.registeredPersistenceFormats.entries()) {
            // Find the persistence class to use for this document.
            if (docName.toLowerCase().endsWith(registeredPersistenceFormat[0].toLowerCase())) {
                return registeredPersistenceFormat[1];
            }
        }
        return undefined;
    }
}
exports.PersistenceLayer = PersistenceLayer;
PersistenceLayer.cdmFolder = 'CdmFolder';
PersistenceLayer.modelJson = 'ModelJson';
//# sourceMappingURL=PersistenceLayer.js.map