"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TileStorage = void 0;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
const zlib_1 = require("zlib");
const util_1 = require("util");
const core_common_1 = require("@itwin/core-common");
const core_bentley_1 = require("@itwin/core-bentley");
const BackendLoggerCategory_1 = require("./BackendLoggerCategory");
const IModelHost_1 = require("./IModelHost");
/**
 * Facilitates interaction with cloud tile cache.
 * @beta
 */
class TileStorage {
    /**
     * Allows using the underlying `ServerStorage` API directly.
     * @see https://github.com/iTwin/object-storage/
     */
    storage;
    constructor(storage) {
        this.storage = storage;
    }
    _initializedIModels = new Set();
    /**
     * Ensures any required cloud storage resources for a specific iModel are ready to use.
     */
    async initialize(iModelId) {
        if (this._initializedIModels.has(iModelId))
            return;
        if (!(await this.storage.baseDirectoryExists({ baseDirectory: iModelId }))) {
            try {
                await this.storage.createBaseDirectory({ baseDirectory: iModelId });
            }
            catch (e) {
                // Ignore 409 errors. This is what Azure blob storage returns when the container already exists.
                // Usually this means multiple backends tried to initialize tile storage at the same time.
                if (e.statusCode !== 409)
                    throw e;
            }
        }
        this._initializedIModels.add(iModelId);
    }
    /**
     * Returns config that can be used by frontends to download tiles
     * @param iModelId Id of the iModel
     * @param expiresInSeconds Optional number of seconds until the download URL expires. Defaults to expiring exactly at midnight of next Sunday to enable persistent client-side caching.
     *  It is recommended to set this to a shorter period when using S3-compatible storage - an exact expiry date cannot be ensured due to limitations in their API.
     * @see [TileStorage]($frontend)
     */
    async getDownloadConfig(iModelId, expiresInSeconds) {
        try {
            if (expiresInSeconds !== undefined)
                return await this.storage.getDownloadConfig({ baseDirectory: iModelId }, { expiresInSeconds });
            const expiresOn = new Date();
            expiresOn.setDate(expiresOn.getDate() + (7 - expiresOn.getDay())); // next Sunday
            expiresOn.setHours(0, 0, 0, 0); // exactly at midnight
            return await this.storage.getDownloadConfig({ baseDirectory: iModelId }, { expiresOn });
        }
        catch (err) {
            this.logException("Failed to get download config", err);
            throw err;
        }
    }
    /**
     * Uploads a tile to the cloud cache.
     */
    async uploadTile(iModelId, changesetId, treeId, contentId, content, guid, metadata) {
        try {
            await this.storage.upload((0, core_common_1.getTileObjectReference)(iModelId, changesetId, treeId, contentId, guid), Buffer.from(IModelHost_1.IModelHost.compressCachedTiles ? await (0, util_1.promisify)(zlib_1.gzip)(content.buffer) : content.buffer), metadata, IModelHost_1.IModelHost.compressCachedTiles ? { contentEncoding: "gzip" } : undefined);
        }
        catch (err) {
            this.logException("Failed to upload tile", err);
            throw err;
        }
    }
    /**
     * Downloads a tile from the cloud cache.
     */
    async downloadTile(iModelId, changesetId, treeId, contentId, guid) {
        try {
            const buffer = await this.storage.download((0, core_common_1.getTileObjectReference)(iModelId, changesetId, treeId, contentId, guid), "buffer");
            return IModelHost_1.IModelHost.compressCachedTiles ? await (0, util_1.promisify)(zlib_1.gunzip)(buffer) : buffer;
        }
        catch (err) {
            this.logException("Failed to download tile", err);
            throw err;
        }
    }
    /**
     * Returns an async iterator of all tiles that are found in the cloud cache.
     */
    async *getCachedTilesGenerator(iModelId) {
        const iterator = this.getCachedTilePages(iModelId);
        for await (const page of iterator) {
            for (const tile of page) {
                yield tile;
            }
        }
    }
    async *getCachedTilePages(iModelId) {
        const iterator = this.storage.getListObjectsPagedIterator({ baseDirectory: iModelId }, 500);
        let prevPage;
        do {
            // initiate loading the next page
            const page = iterator.next();
            // process results from the previous page
            if (prevPage)
                yield this.convertPage(prevPage.value);
            // finish loading the next page
            prevPage = await page;
        } while (!prevPage.done);
    }
    convertPage(page) {
        return page
            .map((objectReference) => ({
            parts: objectReference.relativeDirectory?.split("/") ?? [""],
            objectName: objectReference.objectName,
        }))
            .filter(({ parts, objectName }) => {
            if (parts[0] !== "tiles")
                return false;
            if (parts.length !== 3) {
                core_bentley_1.Logger.logWarning(BackendLoggerCategory_1.BackendLoggerCategory.IModelTileStorage, "Malformed tile id found in tile cache: {tileId}", { tileId: [...parts, objectName].join("/") });
                return false;
            }
            return true;
        }).map(({ parts, objectName }) => {
            // relativeDirectory = tiles/<treeId>/<guid>
            // objectName = <contentId>
            return {
                treeId: parts[1],
                contentId: objectName,
                guid: parts[2],
            };
        });
    }
    /**
     * Returns a list of all tiles that are found in the cloud cache.
     */
    async getCachedTiles(iModelId) {
        const results = [];
        for await (const page of this.getCachedTilePages(iModelId)) {
            results.push(...page);
        }
        return results;
    }
    /**
     * Returns a boolean indicating whether a tile exists in the cloud cache
     */
    async isTileCached(iModelId, changesetId, treeId, contentId, guid) {
        return this.storage.objectExists((0, core_common_1.getTileObjectReference)(iModelId, changesetId, treeId, contentId, guid));
    }
    logException(message, err) {
        core_bentley_1.Logger.logException(BackendLoggerCategory_1.BackendLoggerCategory.IModelTileStorage, err, (category, msg, errorMetadata) => core_bentley_1.Logger.logError(category, `${message}: {errorMessage}`, { ...errorMetadata, errorMessage: msg }));
    }
}
exports.TileStorage = TileStorage;
//# sourceMappingURL=TileStorage.js.map