/*---------------------------------------------------------------------------------------------
* 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
 */
import { assert, base64StringToUint8Array, IModelStatus } from "@itwin/core-bentley";
import { ImageSource } from "@itwin/core-common";
import { IModelApp } from "../IModelApp";
import { TileUser } from "./internal";
/** Represents a pending or active request to load the contents of a [[Tile]]. The request coordinates with the [[Tile.requestContent]] to obtain the raw content and
 * [[Tile.readContent]] to convert the result into a [[RenderGraphic]]. TileRequests are created internally as needed; it is never necessary or useful for external code to create them.
 * @public
 * @extensions
 */
export class TileRequest {
    /** Constructor */
    constructor(tile, user) {
        /** Determines the order in which pending requests are pulled off the queue to become active. A tile with a lower priority value takes precedence over one with a higher value. */
        this.priority = 0;
        this._state = TileRequest.State.Queued;
        this.tile = tile;
        this.channel = tile.channel;
        this.users = IModelApp.tileAdmin.getTileUserSetForRequest(user);
    }
    /** The set of [[Viewport]]s that are awaiting the result of this request. When this becomes empty, the request is canceled because no user cares about it. */
    get viewports() {
        return TileUser.viewportsFromUsers(this.users);
    }
    /** The request's current state. */
    get state() { return this._state; }
    /** True if the request has been enqueued but not yet dispatched. */
    get isQueued() { return TileRequest.State.Queued === this._state; }
    /** True if the request has been canceled. */
    get isCanceled() {
        // If iModel was closed, cancel immediately
        if (this.tile.iModel.tiles.isDisposed)
            return true;
        // After we've received the raw tile data, always finish processing it - otherwise tile may end up in limbo (and producing tile content should be faster than re-requesting raw data).
        if (TileRequest.State.Loading === this._state)
            return false;
        // If no user cares about this tile any more, we're canceled.
        return this.users.isEmpty;
    }
    /** The tile tree to which the requested [[Tile]] belongs. */
    get tree() { return this.tile.tree; }
    /** Indicate that the specified user is awaiting the result of this request.
     * @internal
     */
    addUser(user) {
        this.users = IModelApp.tileAdmin.getTileUserSetForRequest(user, this.users);
    }
    /** Transition the request from "queued" to "active", kicking off a series of asynchronous operations usually beginning with an http request, and -
     * if the request is not subsequently canceled - resulting in either a successfully-loaded Tile, or a failed ("not found") Tile.
     * @internal
     */
    async dispatch(onHttpResponse) {
        if (this.isCanceled)
            return;
        assert(this._state === TileRequest.State.Queued);
        this._state = TileRequest.State.Dispatched;
        let response;
        let gotResponse = false;
        try {
            response = await this.channel.requestContent(this.tile, () => this.isCanceled);
            gotResponse = true;
            // Set this now, so our `isCanceled` check can see it.
            this._state = TileRequest.State.Loading;
        }
        catch (err) {
            if (err.errorNumber && err.errorNumber === IModelStatus.ServerTimeout) {
                // Invalidate scene - if tile is re-selected, it will be re-requested.
                this.notifyAndClear();
                this._state = TileRequest.State.Failed;
                this.channel.recordTimeout();
            }
            else {
                // Unknown error - not retryable
                this.setFailed();
            }
        }
        // Notify caller that we have finished http activity.
        onHttpResponse();
        if (!gotResponse || this.isCanceled)
            return;
        if (undefined === response && this.channel.onNoContent(this)) {
            // Invalidate scene - if tile is re-selected, it will be re-requested - presumably via a different channel.
            this.notifyAndClear();
            this._state = TileRequest.State.Failed;
            return;
        }
        return this.handleResponse(response);
    }
    /** Cancels this request. This leaves the associated Tile's state untouched.
     * @internal
     */
    cancel() {
        this.notifyAndClear();
        if (TileRequest.State.Dispatched === this._state)
            this.channel.onActiveRequestCanceled(this);
        this._state = TileRequest.State.Failed;
    }
    /** Invalidates the scene of each [[TileUser]] interested in this request - typically because the request succeeded, failed, or was canceled. */
    notify() {
        this.users.forEach((user) => {
            if (user.onRequestStateChanged)
                user.onRequestStateChanged(this);
        });
    }
    /** Invalidates the scene of each [[TileUser]] interested in this request and clears the set of interested users. */
    notifyAndClear() {
        this.notify();
        this.users = IModelApp.tileAdmin.emptyTileUserSet;
        this.tile.request = undefined;
    }
    setFailed() {
        this.notifyAndClear();
        this._state = TileRequest.State.Failed;
        this.tile.setNotFound();
        this.channel.recordFailure();
    }
    /** Invoked when the raw tile content becomes available, to convert it into a tile graphic. */
    async handleResponse(response) {
        let content;
        let data;
        if (undefined !== response) {
            if (typeof response === "string")
                data = base64StringToUint8Array(response);
            else if (response instanceof Uint8Array || response instanceof ImageSource)
                data = response;
            else if (response instanceof ArrayBuffer)
                data = new Uint8Array(response);
            else if (typeof response === "object") {
                if ("content" in response)
                    content = response.content;
                else if ("data" in response)
                    data = response;
            }
        }
        if (!content && !data) {
            this.setFailed();
            return;
        }
        try {
            const start = Date.now();
            if (!content) {
                assert(undefined !== data);
                content = await this.tile.readContent(data, IModelApp.renderSystem, () => this.isCanceled);
                if (this.isCanceled)
                    return;
            }
            this._state = TileRequest.State.Completed;
            this.tile.setContent(content);
            this.notifyAndClear();
            this.channel.recordCompletion(this.tile, content, Date.now() - start);
        }
        catch {
            this.setFailed();
        }
    }
}
/** @public */
(function (TileRequest) {
    /** The states through which a [[TileRequest]] proceeds. During the first 3 states, the [[Tile]]'s `request` member is defined,
     * and its [[Tile.LoadStatus]] is computed based on the state of its request.
     *@ public
     */
    let State;
    (function (State) {
        /** Initial state. Request is pending but not yet dispatched. */
        State[State["Queued"] = 0] = "Queued";
        /** Follows `Queued` when request begins to be actively processed. */
        State[State["Dispatched"] = 1] = "Dispatched";
        /** Follows `Dispatched` when the response to the request is being converted into tile graphics. */
        State[State["Loading"] = 2] = "Loading";
        /** Follows `Loading` when tile graphic has successfully been produced. */
        State[State["Completed"] = 3] = "Completed";
        /** Follows any state in which an error prevents progression, or during which the request was canceled. */
        State[State["Failed"] = 4] = "Failed";
    })(State = TileRequest.State || (TileRequest.State = {}));
})(TileRequest || (TileRequest = {}));
//# sourceMappingURL=TileRequest.js.map