"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Catalog = void 0;
const ArcGISContextManager_1 = require("../ArcGISContextManager");
const catalogContains_1 = require("../core/catalogContains");
const HubError_1 = require("../HubError");
const util_1 = require("../util");
const fetchEntityCatalog_1 = require("./fetchEntityCatalog");
const Collection_1 = require("./Collection");
const hubSearch_1 = require("./hubSearch");
const upgradeCatalogSchema_1 = require("./upgradeCatalogSchema");
const map_by_1 = require("../utils/map-by");
/**
 * Catalog Class
 *
 * Abstracts working with Catalogs and fetching collections with
 * the correct scope applied.
 *
 * For more information, check out the [Catalog & Collection Guide](/hub.js/guides/concepts/catalog-collection/)
 */
class Catalog {
    // internal - use static factory methods
    constructor(catalog, context) {
        this._containsCache = {};
        this._catalog = catalog;
        this._context = context;
    }
    /**
     * Fetch a catalog
     * At this point, it returns the `.catalog` property from the entity
     * @param identifier url, guid, cuid
     * @param context
     * @param options is possible, pass the hubEntityType to improve fetching performance
     * @returns
     */
    static async init(identifier, context, options) {
        // create default context if none passed
        if (!context) {
            const mgr = await ArcGISContextManager_1.ArcGISContextManager.create();
            context = mgr.context;
        }
        // fetch the catalog
        const fetched = await (0, fetchEntityCatalog_1.fetchEntityCatalog)(identifier, context, options);
        // return an instance
        if (fetched) {
            return new Catalog(fetched, context);
        }
        else {
            throw new HubError_1.default("Catalog.fetch", "No catalog found for the entity");
        }
    }
    /**
     * Create a Catalog instance from a Catalog Definition Json object
     * @param json
     * @param context
     * @returns
     */
    static fromJson(json, context) {
        // ensure it's in the latest structure
        const catalog = (0, upgradeCatalogSchema_1.upgradeCatalogSchema)(json);
        return new Catalog(catalog, context);
    }
    /**
     * Return the JSON object backing the instance
     * @returns
     */
    toJson() {
        return (0, util_1.cloneObject)(this._catalog);
    }
    /**
     * Return the schema version
     */
    get schemaVersion() {
        return this._catalog.schemaVersion;
    }
    /**
     * Title getter
     */
    get title() {
        return this._catalog.title;
    }
    /**
     * Title setter
     */
    set title(v) {
        this._catalog.title = v;
    }
    /**
     * Return the existing scopes hash
     */
    get scopes() {
        return this._catalog.scopes || {};
    }
    /**
     * Return an array of the entity types available in this Catalog
     */
    get availableScopes() {
        return Object.keys(this.scopes);
    }
    /**
     * Return the display configuration for the gallery
     */
    get displayConfig() {
        return this._catalog.displayConfig;
    }
    /**
     * Get the scope's query for a particular entity type
     * @param type
     * @returns
     */
    getScope(type) {
        var _a;
        return (_a = this._catalog.scopes) === null || _a === void 0 ? void 0 : _a[type];
    }
    /**
     * Set the scope for a specific entity type
     * @param type
     * @param query
     */
    setScope(type, query) {
        // TODO: This needs to be much smarter in terms of merging
        // existing filters with the new ones. Basically this
        // hides very little complexity from the developer
        this._catalog.scopes[type] = query;
    }
    /**
     * Get the collections array. Returns simple objects not Collection instances
     */
    get collections() {
        return this._catalog.collections || [];
    }
    /**
     * Get the names of the collections
     */
    get collectionNames() {
        return (0, map_by_1.mapBy)("key", this.collections);
    }
    /**
     * Get a Collection instance by name
     * @param name
     * @returns
     */
    getCollection(name) {
        const json = this.getCollectionJson(name);
        return Collection_1.Collection.fromJson(json, this._context);
    }
    /**
     * Add a collection
     * @param {IHubCollection} collection
     */
    addCollection(collection) {
        var _a;
        // if you are updating this file, remove this comment and address the lint error
        // eslint-disable-next-line @typescript-eslint/no-unused-expressions
        ((_a = this._catalog.collections) === null || _a === void 0 ? void 0 : _a.length)
            ? this._catalog.collections.push(collection)
            : (this._catalog.collections = [collection]);
    }
    /**
     * Get the Collection json by name
     * @param name
     * @returns
     */
    getCollectionJson(name) {
        const json = this.collections.find((entry) => entry.key === name);
        if (json) {
            // clone it then merge in the associated scope filter
            const clone = (0, util_1.cloneObject)(json);
            const catalogScope = this.getScope(clone.scope.targetEntity);
            if (catalogScope === null || catalogScope === void 0 ? void 0 : catalogScope.filters) {
                clone.scope.filters = [
                    ...(clone.scope.filters || []),
                    ...catalogScope.filters,
                ];
            }
            return clone;
        }
        else {
            throw new HubError_1.default("getCollectionJson", `Collection "${name}" is not present in the Catalog`);
        }
    }
    /**
     * Get a Collection instance, based on a specific entity type and filters
     * This extends from the base scope for the entity type
     * @param type
     * @param filters
     * @returns
     */
    getCustomCollection(type, filters) {
        // create the collection
        const collection = this.getCustomCollectionJson(type, filters);
        // create instance
        return Collection_1.Collection.fromJson(collection, this._context);
    }
    /**
     * Create a custom collection based on a specific entity type and filters
     * Returned collection extends from the base scope for the entity type
     * @param type
     * @param filters
     * @returns
     */
    getCustomCollectionJson(type, filters) {
        // get the scope
        const scopeQuery = this.getScope(type);
        // create the collection
        const collection = {
            key: `${type}-custom`,
            label: `${type} Custom`,
            targetEntity: type,
            scope: {
                targetEntity: type,
                filters: [...((scopeQuery === null || scopeQuery === void 0 ? void 0 : scopeQuery.filters) || []), ...filters],
            },
        };
        return collection;
    }
    /**
     * Search for Items
     * Will throw if the Catalog does not have a scope defined for items
     * @param query - string or IQuery
     * @param options
     * @returns
     */
    async searchItems(query, options) {
        if (!options) {
            options = this.getDefaultSearchOptions("item");
        }
        if (!this.getScope("item")) {
            const result = this.getEmptyResult();
            result.messages = [
                {
                    code: "missingScope",
                    message: "Catalog does not have a scope for items",
                    data: {
                        scope: "item",
                    },
                },
            ];
            return Promise.resolve(result);
        }
        else {
            // ensure it's an item search
            options.targetEntity = "item";
            return this.search(query, options);
        }
    }
    async contains(identifier, options) {
        const start = Date.now();
        // check if we have cached results for this identifier
        if (this._containsCache[identifier]) {
            const cachedResult = (0, util_1.cloneObject)(this._containsCache[identifier]);
            cachedResult.duration = Date.now() - start;
            return Promise.resolve(cachedResult);
        }
        else {
            // delegate to catalogContains
            const result = await (0, catalogContains_1.catalogContains)(identifier, this._catalog, this._context, options);
            // add to cache...
            this._containsCache[identifier] = result;
            result.duration = Date.now() - start;
            return result;
        }
    }
    /**
     * Search for Groups
     * Will throw if the Catalog does not have a scope defined for groups
     * @param query  - string or IQuery
     * @param options
     * @returns
     */
    async searchGroups(query, options) {
        if (!options) {
            options = this.getDefaultSearchOptions("group");
        }
        if (!this.getScope("group")) {
            const result = this.getEmptyResult();
            result.messages = [
                {
                    code: "missingScope",
                    message: "Catalog does not have a scope for groups",
                    data: {
                        scope: "group",
                    },
                },
            ];
            return Promise.resolve(result);
        }
        else {
            // ensure it's an group search
            options.targetEntity = "group";
            return this.search(query, options);
        }
    }
    /**
     * Search for Users
     * Will throw if the Catalog does not have a scope defined for users
     * @param query  - string or IQuery
     * @param options
     * @returns
     */
    async searchUsers(query, options = {}) {
        if (!this.getScope("user")) {
            const result = this.getEmptyResult();
            result.messages = [
                {
                    code: "missingScope",
                    message: "Catalog does not have a scope for user",
                    data: {
                        scope: "user",
                    },
                },
            ];
            return Promise.resolve(result);
        }
        else {
            // ensure it's an group search
            options.targetEntity = "user";
            return this.search(query, options);
        }
    }
    /**
     * Execute a term search against all the collections in the Catalog
     * or an IQuery against all collections that match the targetEntity.
     * Note: This will not search scopes which do not have corresponding collections.
     * If you want that behavior, use `searchCatalogs` function instead.
     * @param query  - string or IQuery
     * @param options
     * @returns
     */
    async searchCollections(query, options = {}) {
        // build a query
        let passedQuery = true;
        let qry;
        if (typeof query === "string") {
            passedQuery = false;
            qry = {
                targetEntity: "item", // this gets replaced below, but we need something here
                filters: [
                    {
                        predicates: [
                            {
                                term: query,
                            },
                        ],
                    },
                ],
            };
        }
        else {
            qry = query;
        }
        // iterate the colllections, issue searchs for each one
        const promiseKeys = [];
        const promises = this.collectionNames.reduce((acc, name) => {
            const col = this.getCollection(name);
            // if an IQuery was passed in,
            if (passedQuery) {
                // we only execute queries on collections that match the targetEntity
                // and skip the rest
                if (col.targetEntity === qry.targetEntity) {
                    promiseKeys.push(name);
                    acc.push(col.search(qry, options));
                }
            }
            else {
                // if a string was passed in, we execute the search on all collections
                // by replacing the targetEntity in the query to match the collection
                promiseKeys.push(name);
                qry.targetEntity = col.targetEntity;
                acc.push(col.search(qry, options));
            }
            return acc;
        }, []);
        const responses = await Promise.all(promises);
        // merge the responses into the hash
        const hash = {};
        for (let i = 0; i < promiseKeys.length; i++) {
            hash[promiseKeys[i]] = responses[i];
        }
        return hash;
    }
    /**
     * Execute a term search against all the scopes in the Catalog
     * @param query - term or IQuery
     * @param options
     * @returns
     */
    async searchScopes(query, options = {}, limitTo) {
        let qry;
        if (typeof query === "string") {
            qry = {
                targetEntity: "item", // this gets replaced below, but we need something here
                filters: [
                    {
                        predicates: [
                            {
                                term: query,
                            },
                        ],
                    },
                ],
            };
        }
        else {
            qry = query;
        }
        const promiseKeys = [];
        const promises = this.availableScopes.reduce((acc, entityType) => {
            if (!limitTo || limitTo.includes(entityType)) {
                promiseKeys.push(entityType);
                const qryClone = (0, util_1.cloneObject)(qry);
                const optsClone = (0, util_1.cloneObject)(options);
                qryClone.targetEntity = entityType;
                optsClone.targetEntity = entityType;
                acc.push(this.search(qryClone, optsClone));
            }
            return acc;
        }, []);
        // wait for all the searches to complete
        const responses = await Promise.all(promises);
        // merge the responses into the hash
        const hash = {};
        for (let i = 0; i < promiseKeys.length; i++) {
            hash[promiseKeys[i]] = responses[i];
        }
        return hash;
    }
    /**
     * Execute a term search or IQuery search against the Catalog.
     * @param query - term or IQuery
     * @param options
     * @returns
     */
    async search(query, options) {
        let targetEntity = options.targetEntity;
        let qry;
        if (typeof query === "string") {
            qry = {
                targetEntity,
                filters: [
                    {
                        predicates: [
                            {
                                term: query,
                            },
                        ],
                    },
                ],
            };
        }
        else {
            targetEntity = query.targetEntity;
            qry = (0, util_1.cloneObject)(query);
        }
        // Now merge in catalog scope level filters if they exist
        const scope = this.getScope(targetEntity);
        // Since the public methods (searchItem etc) all
        // check for the scope before calling this method
        // we can assume that the scope exists, but this is extra
        // defensive just to ensure we don't blow up
        /* istanbul ignore else -- @preserve */
        if (scope) {
            qry.filters = [...qry.filters, ...scope.filters];
        }
        const opts = (0, util_1.cloneObject)(options);
        // An Catalog instance always uses the context so we remove/replace any passed in auth
        delete opts.authentication;
        opts.requestOptions = this._context.hubRequestOptions;
        return (0, hubSearch_1.hubSearch)(qry, opts);
    }
    /**
     * Construct an empty result. Returned when a search is performed against an entity type that
     * does not have a scope defined.
     * @returns
     */
    getEmptyResult() {
        return {
            results: [],
            total: 0,
            hasNext: false,
            next: null,
        };
    }
    getDefaultSearchOptions(type) {
        return {
            targetEntity: type,
            num: 10,
            start: 1,
            requestOptions: this._context.hubRequestOptions,
        };
    }
}
exports.Catalog = Catalog;
//# sourceMappingURL=Catalog.js.map