"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Tiles
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.ArcGISTileMap = void 0;
const core_bentley_1 = require("@itwin/core-bentley");
const internal_1 = require("../internal");
const loggerCategory = "ArcGISTileMap";
const nonVisibleChildren = [false, false, false, false];
/** @internal */
class ArcGISTileMap {
    // We want to query a tile map that covers an area all around the top-lef missing tile, we offset the top-left corner position of the tilemap.
    // We used to create a 32x32 tiles area around the missing tiles, but this was causing the tilemap top-left position
    // to fall outside the dataset bundle of the remote server, thus giving invalid response.
    get tileMapOffset() { return (this.tileMapRequestSize * 0.5); }
    constructor(restBaseUrl, settings, fetchFunc) {
        // For similar reasons as the corner offset, we need to keep the tile map size not too big to avoid covering multiple bundles.
        this.tileMapRequestSize = 8;
        this.fallbackTileMapRequestSize = 2;
        this._tilesCache = new core_bentley_1.Dictionary((lhs, rhs) => (0, core_bentley_1.compareStrings)(lhs, rhs));
        this._restBaseUrl = restBaseUrl;
        this._fetchFunc = fetchFunc;
        this._settings = settings;
        this._callQueues = new Array(ArcGISTileMap.maxLod).fill(Promise.resolve(nonVisibleChildren));
    }
    async fetchTileMapFromServer(level, row, column, width, height) {
        const tmpUrl = `${this._restBaseUrl}/tilemap/${level}/${row}/${column}/${width}/${height}?f=json`;
        const response = await this._fetchFunc(new URL(tmpUrl));
        return response.json();
    }
    getAvailableTilesFromCache(tiles) {
        let allTilesFound = true;
        // Check children visibility from cache
        const available = tiles.map((tileId) => {
            const avail = this._tilesCache.get(tileId.contentId);
            if (undefined === avail) {
                allTilesFound = false;
            }
            return avail ?? false;
        });
        return { allTilesFound, available };
    }
    async getChildrenAvailability(childIds) {
        if (!childIds.length)
            return [];
        // Before entering the queue for a backend request,
        // let check if cache doesn't already contain what we are looking for.
        const cacheInfo = this.getAvailableTilesFromCache(childIds);
        if (cacheInfo.allTilesFound) {
            if (cacheInfo.available.includes(false))
                return cacheInfo.available;
            return cacheInfo.available;
        }
        // If we never encountered this tile level before, then a tilemap request must be made to get tiles visibility.
        // However, we dont want several overlapping large tilemap request being made simultaneously for tiles on the same level.
        // To avoid this from happening, we 'serialize' async calls so that we wait until the first tilemap request has completed
        // before making another one.
        const childLevel = childIds[0].level + 1;
        if (this._callQueues && childLevel < this._callQueues.length) {
            const res = this._callQueues[childLevel].then(async () => {
                return this.getChildrenAvailabilityFromServer(childIds);
            });
            this._callQueues[childLevel] = res.catch(() => nonVisibleChildren);
            return res;
        }
        else {
            // We should not be in this case, probably because server info is missing LODs in the capabilities?!
            core_bentley_1.Logger.logWarning(loggerCategory, `Skipped request queue for child level ${childLevel}`);
            return this.getChildrenAvailabilityFromServer(childIds);
        }
    }
    isCacheMissingTile(level, startRow, startColumn, endRow, endColumn) {
        let missingTileFound = false;
        if (endRow <= startRow || endColumn <= startColumn)
            return missingTileFound;
        for (let j = startColumn; j <= endColumn && !missingTileFound; j++) {
            for (let i = startRow; i <= endRow && !missingTileFound; i++) {
                if (j >= 0 && i >= 0) {
                    const contentId = internal_1.QuadId.getTileContentId(level, j, i);
                    if (this._tilesCache.get(contentId) === undefined) {
                        missingTileFound = true;
                    }
                }
            }
        }
        return missingTileFound;
    }
    collectTilesMissingFromCache(missingQueryTiles) {
        const missingTiles = [];
        for (const quad of missingQueryTiles) {
            const contentId = internal_1.QuadId.getTileContentId(quad.level, quad.column, quad.row);
            const avail = this._tilesCache.get(contentId);
            if (avail === undefined)
                missingTiles.push(quad);
        }
        return missingTiles;
    }
    // Query tiles are tiles that we need to check availability
    // The array is assumed to be in in row major orientation, i.e.: [TileRow0Col0, TileRow0Col1, TileRow1Col0, TileRow1Col1,]
    async fetchAndReadTilemap(queryTiles, reqWidth, reqHeight) {
        let available = queryTiles.map(() => false);
        if (queryTiles.length === 0) {
            return available;
        }
        // console.log(`queryTiles: ${queryTiles.map((quad) => quad.contentId)}`);
        // Find the top-left most corner of the extent covering the query tiles.
        const getTopLeftCorner = (tiles) => {
            let row;
            let column;
            for (const quad of tiles) {
                if (row === undefined || quad.row <= row)
                    row = quad.row;
                if (column === undefined || quad.column <= column) {
                    column = quad.column;
                }
            }
            return { row, column };
        };
        const level = queryTiles[0].level; // We assume all tiles to be on the same level
        let missingQueryTiles = this.collectTilesMissingFromCache(queryTiles);
        let gotAdjusted = false;
        let nbAttempt = 0; // Safety: We should never be making more requests than the number of queries tiles (otherwise something is wrong)
        while (missingQueryTiles.length > 0
            && (nbAttempt++ < queryTiles.length)) {
            const tileMapTopLeft = getTopLeftCorner(missingQueryTiles);
            if (tileMapTopLeft.row === undefined || tileMapTopLeft.column === undefined)
                return available; // Should not occurs since missingQueryTiles is non empty
            let tileMapRow = tileMapTopLeft.row;
            let tileMapColumn = tileMapTopLeft.column;
            const logLocationOffset = (newRow, newCol) => `[Row:${newRow !== tileMapTopLeft.row ? `${tileMapTopLeft.row}->${newRow}` : `${newRow}`} Column:${newCol !== tileMapTopLeft.column ? `${tileMapTopLeft.column}->${newCol}` : `${newCol}`}]`;
            // Position the top-left missing tile in the middle of the tilemap; minimizing requests if sibling tiles are requested right after
            // If previous response got adjusted, don't try to optimize tile map location
            if (queryTiles.length < this.tileMapRequestSize && !gotAdjusted) {
                const tileMapOffset = this.tileMapOffset - Math.floor(Math.sqrt(queryTiles.length) * 0.5);
                const missingTileBufferSize = Math.ceil(tileMapOffset * 0.5);
                if (this.isCacheMissingTile(level, tileMapRow - missingTileBufferSize, tileMapColumn - missingTileBufferSize, tileMapRow - 1, tileMapColumn - 1)) {
                    tileMapRow = Math.max(tileMapRow - tileMapOffset, 0);
                    tileMapColumn = Math.max(tileMapColumn - tileMapOffset, 0);
                    core_bentley_1.Logger.logTrace(loggerCategory, `Offset applied to location in top-left direction: ${logLocationOffset(tileMapRow, tileMapColumn)}`);
                }
                else {
                    const leftMissingTiles = this.isCacheMissingTile(level, tileMapRow, tileMapColumn - missingTileBufferSize, tileMapRow + missingTileBufferSize, tileMapColumn - 1);
                    const topMissingTiles = this.isCacheMissingTile(level, tileMapRow - missingTileBufferSize, tileMapColumn, tileMapRow - 1, tileMapColumn + missingTileBufferSize);
                    if (leftMissingTiles && topMissingTiles) {
                        tileMapRow = Math.max(tileMapRow - tileMapOffset, 0);
                        tileMapColumn = Math.max(tileMapColumn - tileMapOffset, 0);
                        core_bentley_1.Logger.logTrace(loggerCategory, `Offset applied to location in top-left direction. ${logLocationOffset(tileMapRow, tileMapColumn)}`);
                    }
                    else if (leftMissingTiles) {
                        tileMapColumn = Math.max(tileMapColumn - tileMapOffset, 0);
                        core_bentley_1.Logger.logTrace(loggerCategory, `Offset applied to location in left direction. ${logLocationOffset(tileMapRow, tileMapColumn)}`);
                    }
                    else if (topMissingTiles) {
                        tileMapRow = Math.max(tileMapRow - tileMapOffset, 0);
                        core_bentley_1.Logger.logTrace(loggerCategory, `Offset applied to location in top direction: ${logLocationOffset(tileMapRow, tileMapColumn)}`);
                    }
                    else
                        core_bentley_1.Logger.logTrace(loggerCategory, `No offset applied to location: ${logLocationOffset(tileMapRow, tileMapColumn)}`);
                }
            }
            const json = await this.fetchTileMapFromServer(level, tileMapRow, tileMapColumn, reqWidth, reqHeight);
            let tileMapWidth = reqWidth;
            let tileMapHeight = reqHeight;
            if (Array.isArray(json.data)) {
                // The response width and height might be different than the requested dimensions.
                // Ref: https://developers.arcgis.com/rest/services-reference/enterprise/tile-map.htm
                if (json.adjusted) {
                    gotAdjusted = true;
                    // If tilemap size got adjusted, I'm expecting to get adjusted size...
                    // otherwise there is something really odd with this server.
                    (0, core_bentley_1.assert)(json.location?.width !== undefined && json.location?.height !== undefined);
                    if (json.location?.width !== undefined && json.location?.height !== undefined) {
                        tileMapWidth = json.location?.width;
                        tileMapHeight = json.location?.height;
                    }
                }
                // Build cache from tile map response
                for (let j = 0; j < tileMapHeight; j++) {
                    for (let i = 0; i < tileMapWidth; i++) {
                        const avail = json.data[(j * tileMapWidth) + i] !== 0;
                        const curColumn = tileMapColumn + i;
                        const curRow = tileMapRow + j;
                        this._tilesCache.set(internal_1.QuadId.getTileContentId(level, curColumn, curRow), avail);
                    }
                }
                // Collect tile missing from the cache
                // There are 2 reasons why the tile map response would not cover all the missing tiles:
                // 1. The requested tile map size is not large enough to cover all tiles
                // 2. The tile map size has been adjusted by the server (i.e. data bundle limits)
                missingQueryTiles = this.collectTilesMissingFromCache(missingQueryTiles);
                if (missingQueryTiles.length > 0)
                    core_bentley_1.Logger.logTrace(loggerCategory, `There are ${missingQueryTiles.length} missing tiles from previous request`);
            }
            else {
                missingQueryTiles = [];
                // Mark all tilemap tiles to non-available in the cache too.
                for (let j = 0; j < tileMapWidth; j++) {
                    for (let i = 0; i < tileMapHeight; i++) {
                        this._tilesCache.set(internal_1.QuadId.getTileContentId(level, tileMapColumn + i, tileMapRow + j), false);
                    }
                }
            }
        } // end loop missing tiles
        if (nbAttempt > queryTiles.length) {
            core_bentley_1.Logger.logError(loggerCategory, `Request loop was terminated; unable to get missing tiles; `);
        }
        // Create final output array from cache
        available = queryTiles.map((quad) => this._tilesCache.get(quad.contentId) ?? false);
        if (available.includes(false))
            return available;
        return available;
    }
    async getChildrenAvailabilityFromServer(childIds) {
        let available;
        try {
            available = await this.fetchAndReadTilemap(childIds, this.tileMapRequestSize, this.tileMapRequestSize);
        }
        catch (err) {
            // if any error occurs, we assume tiles not to be visible
            core_bentley_1.Logger.logError(loggerCategory, `Error while fetching tile map data : ${err}`);
            available = childIds.map(() => false);
        }
        return available;
    }
}
exports.ArcGISTileMap = ArcGISTileMap;
ArcGISTileMap.maxLod = 30;
//# sourceMappingURL=ArcGISTileMap.js.map