"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.StorageManager = void 0;
const _1 = require(".");
const internal_1 = require("../internal");
const Logger_1 = require("../Utilities/Logging/Logger");
const StorageUtils_1 = require("../Utilities/StorageUtils");
const ADLSAdapter_1 = require("./ADLSAdapter");
const CdmStandardsAdapter_1 = require("./CdmStandardsAdapter");
const RemoteAdapter_1 = require("./RemoteAdapter");
const ResourceAdapter_1 = require("./ResourceAdapter");
const using_statement_1 = require("using-statement");
class StorageManager {
    constructor(corpus) {
        this.TAG = StorageManager.name;
        this.corpus = corpus;
        this.namespaceAdapters = new Map();
        this.namespaceFolders = new Map();
        this.systemDefinedNamespaces = new Set();
        this.registeredAdapterTypes = new Map([
            ['cdm-standards', CdmStandardsAdapter_1.CdmStandardsAdapter.prototype],
            ['local', _1.LocalAdapter.prototype],
            ['adls', ADLSAdapter_1.ADLSAdapter.prototype],
            ['remote', RemoteAdapter_1.RemoteAdapter.prototype],
            ['github', _1.GithubAdapter.prototype]
        ]);
        // set up default adapters
        this.mount('local', new _1.LocalAdapter(process.cwd()));
        this.systemDefinedNamespaces.add('local');
        try {
            this.mount('cdm', new CdmStandardsAdapter_1.CdmStandardsAdapter());
            this.systemDefinedNamespaces.add('cdm');
        }
        catch (e) {
            Logger_1.Logger.error(this.ctx, this.TAG, 'constructor', undefined, internal_1.cdmLogCode.ErrStorageCdmStandardsMissing, "cdm.objectModel.cdmstandards");
        }
    }
    /**
     * Maximum number of documents read concurrently when loading imports.
     */
    get maxConcurrentReads() {
        return this.corpus.documentLibrary.concurrentReadLock.permits;
    }
    /**
     * Maximum number of documents read concurrently when loading imports.
     */
    set maxConcurrentReads(value) {
        this.corpus.documentLibrary.concurrentReadLock.permits = value;
    }
    /**
     * @internal
     */
    get ctx() {
        return this.corpus.ctx;
    }
    /**
     * Mounts a namespace to the specified adapter
     */
    mount(namespace, adapter) {
        return (0, using_statement_1.using)((0, Logger_1.enterScope)(StorageManager.name, this.ctx, this.mount.name), _ => {
            if (!namespace) {
                Logger_1.Logger.error(this.ctx, this.TAG, this.mount.name, undefined, internal_1.cdmLogCode.ErrStorageNullNamespace);
                return;
            }
            if (adapter) {
                adapter.ctx = this.ctx;
                this.namespaceAdapters.set(namespace, adapter);
                const fd = new internal_1.CdmFolderDefinition(this.ctx, '');
                fd.corpus = this.corpus;
                fd.namespace = namespace;
                fd.folderPath = '/';
                this.namespaceFolders.set(namespace, fd);
                this.systemDefinedNamespaces.delete(namespace);
            }
            else {
                Logger_1.Logger.error(this.ctx, this.TAG, this.mount.name, undefined, internal_1.cdmLogCode.ErrStorageNullAdapter);
            }
        });
    }
    /**
     * Mounts the config JSON to the storage manager/corpus.
     * @param adapterConfig The adapters config in JSON.
     * @param adapterOrDoesReturnErrorList A boolean value that denotes whether we want to return a list of adapters that were not found.
     */
    mountFromConfig(adapterConfig, doesReturnErrorList = false) {
        if (!adapterConfig) {
            Logger_1.Logger.error(this.ctx, this.TAG, this.mountFromConfig.name, undefined, internal_1.cdmLogCode.ErrStorageNullAdapterConfig);
            return undefined;
        }
        let adapterConfigJson = JSON.parse(adapterConfig);
        if (adapterConfigJson.appId) {
            this.corpus.appId = adapterConfigJson.appId;
        }
        if (adapterConfigJson.defaultNamespace) {
            this.defaultNamespace = adapterConfigJson.defaultNamespace;
        }
        const unrecognizedAdapters = [];
        for (const item of adapterConfigJson.adapters) {
            let nameSpace;
            // Check whether the namespace exists.
            if (item.namespace) {
                nameSpace = item.namespace;
            }
            else {
                Logger_1.Logger.error(this.ctx, this.TAG, this.mountFromConfig.name, undefined, internal_1.cdmLogCode.ErrStorageMissingNamespace);
                continue;
            }
            let configs;
            // Check whether the config exists.
            if (item.config) {
                configs = item.config;
            }
            else {
                Logger_1.Logger.error(this.ctx, this.TAG, this.mountFromConfig.name, undefined, internal_1.cdmLogCode.ErrStorageMissingJsonConfig, nameSpace);
                continue;
            }
            if (!item.type) {
                Logger_1.Logger.error(this.ctx, this.TAG, this.mountFromConfig.name, undefined, internal_1.cdmLogCode.ErrStorageMissingTypeJsonConfig, nameSpace);
                continue;
            }
            const adapterType = this.registeredAdapterTypes.get(item.type);
            if (!adapterType) {
                unrecognizedAdapters.push(item);
            }
            else {
                const adapter = new adapterType.constructor();
                adapter.updateConfig(JSON.stringify(configs));
                this.mount(nameSpace, adapter);
            }
        }
        return doesReturnErrorList ? unrecognizedAdapters : undefined;
    }
    /**
     * Unmounts a namespace
     */
    unMount(nameSpace) {
        return (0, using_statement_1.using)((0, Logger_1.enterScope)(StorageManager.name, this.ctx, this.unMount.name), _ => {
            if (!nameSpace) {
                Logger_1.Logger.error(this.ctx, this.TAG, this.unMount.name, undefined, internal_1.cdmLogCode.ErrStorageNullNamespace);
                return false;
            }
            if (this.namespaceAdapters.has(nameSpace)) {
                this.namespaceAdapters.delete(nameSpace);
                this.namespaceFolders.delete(nameSpace);
                this.systemDefinedNamespaces.delete(nameSpace);
                // The special case, use Resource adapter.
                if (nameSpace === 'cdm') {
                    this.mount(nameSpace, new ResourceAdapter_1.ResourceAdapter());
                    Logger_1.Logger.warning(this.ctx, this.TAG, this.unMount.name, undefined, internal_1.cdmLogCode.WarnUnMountCdmNamespace);
                }
                return true;
            }
            else {
                Logger_1.Logger.warning(this.ctx, this.TAG, this.unMount.name, undefined, internal_1.cdmLogCode.WarnStorageRemoveAdapterFailed, nameSpace);
            }
        });
    }
    /**
     * @internal
     * Allow replacing a storage adapter with another one for testing, leaving folders intact.
     */
    setAdapter(nameSpace, adapter) {
        if (!nameSpace) {
            Logger_1.Logger.error(this.ctx, this.TAG, this.setAdapter.name, undefined, internal_1.cdmLogCode.ErrStorageNullNamespace);
            return;
        }
        if (adapter) {
            this.namespaceAdapters.set(nameSpace, adapter);
        }
        else {
            Logger_1.Logger.error(this.ctx, this.TAG, this.setAdapter.name, undefined, internal_1.cdmLogCode.ErrStorageNullAdapter);
        }
    }
    /**
     * Retrieves the adapter for the specified namespace.
     */
    fetchAdapter(namespace) {
        if (!namespace) {
            Logger_1.Logger.error(this.ctx, this.TAG, this.fetchAdapter.name, undefined, internal_1.cdmLogCode.ErrStorageNullNamespace);
            return undefined;
        }
        if (this.namespaceFolders.has(namespace)) {
            return this.namespaceAdapters.get(namespace);
        }
        Logger_1.Logger.error(this.ctx, this.TAG, this.fetchAdapter.name, undefined, internal_1.cdmLogCode.ErrStorageAdapterNotFound, namespace);
        return undefined;
    }
    /**
     * Given the namespace of a registered storage adapter, returns the root folder containing the sub-folders and documents.
     */
    fetchRootFolder(namespace) {
        return (0, using_statement_1.using)((0, Logger_1.enterScope)(StorageManager.name, this.ctx, this.fetchRootFolder.name), _ => {
            if (!namespace) {
                Logger_1.Logger.error(this.ctx, this.TAG, this.fetchRootFolder.name, undefined, internal_1.cdmLogCode.ErrStorageNullNamespace);
                return undefined;
            }
            let folder;
            if (this.namespaceFolders.has(namespace)) {
                folder = this.namespaceFolders.get(namespace);
            }
            else if (namespace === 'default') {
                folder = this.namespaceFolders.get(this.defaultNamespace);
            }
            if (!folder) {
                Logger_1.Logger.error(this.ctx, this.TAG, this.fetchRootFolder.name, undefined, internal_1.cdmLogCode.ErrStorageAdapterNotFound, namespace);
            }
            return folder;
        });
    }
    /**
     * Takes a storage adapter domain path, figures out the right adapter to use and then returns a corpus path.
     */
    adapterPathToCorpusPath(adapterPath) {
        return (0, using_statement_1.using)((0, Logger_1.enterScope)(StorageManager.name, this.ctx, this.adapterPathToCorpusPath.name), _ => {
            let result;
            // keep trying adapters until one of them likes what it sees
            if (this.namespaceAdapters) {
                for (const pair of this.namespaceAdapters) {
                    if (result === undefined) {
                        result = pair[1].createCorpusPath(adapterPath);
                        if (result !== undefined) {
                            // got one, add the prefix
                            result = `${pair[0]}:${result}`;
                        }
                    }
                }
            }
            if (result === undefined) {
                Logger_1.Logger.error(this.ctx, this.TAG, this.adapterPathToCorpusPath.name, undefined, internal_1.cdmLogCode.ErrStorageInvalidAdapterPath, adapterPath);
            }
            return result;
        });
    }
    /**
     * Takes a corpus path, figures out the right adapter to use and then returns an adapter domain path.
     */
    corpusPathToAdapterPath(corpusPath) {
        return (0, using_statement_1.using)((0, Logger_1.enterScope)(StorageManager.name, this.ctx, this.corpusPathToAdapterPath.name), _ => {
            if (!corpusPath) {
                Logger_1.Logger.error(this.ctx, this.TAG, this.corpusPathToAdapterPath.name, undefined, internal_1.cdmLogCode.ErrStorageNullCorpusPath);
                return undefined;
            }
            let result;
            // break the corpus path into namespace and ... path
            const pathTuple = StorageUtils_1.StorageUtils.splitNamespacePath(corpusPath);
            if (!pathTuple) {
                Logger_1.Logger.error(this.ctx, this.TAG, this.corpusPathToAdapterPath.name, undefined, internal_1.cdmLogCode.ErrStorageNullCorpusPath);
                return undefined;
            }
            const namespace = pathTuple[0] || this.defaultNamespace;
            // get the adapter registered for this namespace
            const namespaceAdapter = this.fetchAdapter(namespace);
            if (namespaceAdapter === undefined) {
                Logger_1.Logger.error(this.ctx, this.TAG, this.corpusPathToAdapterPath.name, undefined, internal_1.cdmLogCode.ErrStorageNamespaceNotRegistered, namespace);
            }
            else {
                // ask the storage adapter to 'adapt' this path
                result = namespaceAdapter.createAdapterPath(pathTuple[1]);
            }
            return result;
        });
    }
    createAbsoluteCorpusPath(objectPath, obj) {
        return (0, using_statement_1.using)((0, Logger_1.enterScope)(StorageManager.name, this.ctx, this.createAbsoluteCorpusPath.name), _ => {
            if (!objectPath) {
                Logger_1.Logger.error(this.ctx, this.TAG, this.createAbsoluteCorpusPath.name, undefined, internal_1.cdmLogCode.ErrPathNullObjectPath);
                return undefined;
            }
            if (this.containsUnsupportedPathFormat(objectPath)) {
                // already called statusRpt when checking for unsupported path format.
                return;
            }
            const pathTuple = StorageUtils_1.StorageUtils.splitNamespacePath(objectPath);
            if (!pathTuple) {
                Logger_1.Logger.error(this.ctx, this.TAG, this.createAbsoluteCorpusPath.name, undefined, internal_1.cdmLogCode.ErrPathNullObjectPath);
                return undefined;
            }
            const nameSpace = pathTuple[0];
            let newObjectPath = pathTuple[1];
            let finalNamespace;
            let prefix;
            let namespaceFromObj;
            if (obj && obj.namespace && obj.folderPath) {
                prefix = obj.folderPath;
                namespaceFromObj = obj.namespace;
            }
            else if (obj && obj.inDocument) {
                prefix = obj.inDocument.folderPath;
                namespaceFromObj = obj.inDocument.namespace;
            }
            if (prefix && this.containsUnsupportedPathFormat(prefix)) {
                // already called statusRpt when checking for unsupported path format.
                return;
            }
            if (prefix && prefix.length > 0 && prefix[prefix.length - 1] !== '/') {
                Logger_1.Logger.warning(this.ctx, this.TAG, this.createAbsoluteCorpusPath.name, undefined, internal_1.cdmLogCode.WarnStorageExpectedPathPrefix, prefix);
                prefix += '/';
            }
            // check if this is a relative path
            if (newObjectPath && newObjectPath.charAt(0) !== '/') {
                if (!obj) {
                    // relative path and no other info given, assume default and root
                    prefix = '/';
                }
                if (nameSpace && nameSpace !== namespaceFromObj) {
                    Logger_1.Logger.error(this.ctx, this.TAG, this.createAbsoluteCorpusPath.name, undefined, internal_1.cdmLogCode.ErrStorageNamespaceMismatch, nameSpace);
                    return;
                }
                newObjectPath = `${prefix}${newObjectPath}`;
                finalNamespace = namespaceFromObj || nameSpace || this.defaultNamespace;
            }
            else {
                finalNamespace = nameSpace || namespaceFromObj || this.defaultNamespace;
            }
            return `${finalNamespace ? `${finalNamespace}:` : ''}${newObjectPath}`;
        });
    }
    /**
     * Takes a corpus path (relative or absolute) and creates a valid relative corpus path with namespace.
     * @param objectPath The path that should be made relative, if possible
     * @param relativeTo The object that the path should be made relative with respect to.
     */
    createRelativeCorpusPath(objectPath, relativeTo) {
        return (0, using_statement_1.using)((0, Logger_1.enterScope)(StorageManager.name, this.ctx, this.createRelativeCorpusPath.name), _ => {
            let newPath = this.createAbsoluteCorpusPath(objectPath, relativeTo);
            const namespaceString = relativeTo ? `${relativeTo.namespace}:` : '';
            if (namespaceString && newPath && newPath.startsWith(namespaceString)) {
                newPath = newPath.slice(namespaceString.length);
                if (newPath.startsWith(relativeTo.folderPath)) {
                    newPath = newPath.slice(relativeTo.folderPath.length);
                }
            }
            return newPath;
        });
    }
    /**
     * @inheritdoc
     */
    fetchConfig() {
        const adaptersArray = [];
        // Construct the JObject for each adapter.
        for (const namespaceAdapterTuple of this.namespaceAdapters) {
            // Skip system-defined adapters and resource adapters.
            if (this.systemDefinedNamespaces.has(namespaceAdapterTuple[0]) || namespaceAdapterTuple[1] instanceof ResourceAdapter_1.ResourceAdapter) {
                continue;
            }
            const config = namespaceAdapterTuple[1].fetchConfig();
            if (!config) {
                Logger_1.Logger.error(this.ctx, this.TAG, this.fetchConfig.name, undefined, internal_1.cdmLogCode.ErrStorageNullAdapter);
                continue;
            }
            const jsonConfig = JSON.parse(config);
            jsonConfig.namespace = namespaceAdapterTuple[0];
            adaptersArray.push(jsonConfig);
        }
        const resultConfig = {};
        /// App ID might not be set.
        if (this.corpus.appId) {
            resultConfig.appId = this.corpus.appId;
        }
        resultConfig.defaultNamespace = this.defaultNamespace;
        resultConfig.adapters = adaptersArray;
        return JSON.stringify(resultConfig);
    }
    /**
     * Saves adapters config into a file.
     */
    saveAdaptersConfigAsync(name, adapter) {
        return __awaiter(this, void 0, void 0, function* () {
            yield adapter.writeAsync(name, this.fetchConfig());
        });
    }
    /**
     * Checks whether the paths has an unsupported format, such as starting with ./ or containing ../  or  /./
     * In case unsupported path format is found, function calls statusRpt and returns true.
     * Returns false if path seems OK.
     * @param path The path to be checked.
     * @returns True if an unsupported path format was found.
     */
    containsUnsupportedPathFormat(path) {
        let statusMessage;
        if (path.startsWith('./') || path.startsWith('.\\')) {
            statusMessage = 'The path starts with ./';
        }
        else if (path.indexOf('../') !== -1 || path.indexOf('..\\') !== -1) {
            statusMessage = 'The path contains ../';
        }
        else if (path.indexOf('/./') !== -1 || path.indexOf('\\.\\') !== -1) {
            statusMessage = 'The path contains /./';
        }
        else {
            return false;
        }
        Logger_1.Logger.error(this.ctx, this.TAG, this.containsUnsupportedPathFormat.name, undefined, internal_1.cdmLogCode.ErrStorageInvalidPathFormat, statusMessage);
        return true;
    }
}
exports.StorageManager = StorageManager;
//# sourceMappingURL=StorageManager.js.map