/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module IModelConnection
 */
import { BentleyError, BentleyStatus, expectDefined, Guid, Logger } from "@itwin/core-bentley";
import { IModelError, IModelReadRpcInterface, IModelVersion, RpcManager, RpcOperation, RpcRequest, RpcRequestEvent, } from "@itwin/core-common";
import { FrontendLoggerCategory } from "./common/FrontendLoggerCategory";
import { IModelApp } from "./IModelApp";
import { IModelConnection } from "./IModelConnection";
import { IModelRoutingContext } from "./IModelRoutingContext";
import { IpcApp } from "./IpcApp";
const loggerCategory = FrontendLoggerCategory.IModelConnection;
/**
 * An IModelConnection to a Checkpoint of an iModel.
 * @see [CheckpointConnection]($docs/learning/frontend/IModelConnection)
 * @public
 */
export class CheckpointConnection extends IModelConnection {
    _fromIpc;
    /** The Guid that identifies the iTwin that owns this iModel. */
    get iTwinId() { return super.iTwinId ?? Guid.empty; }
    /** The Guid that identifies this iModel. */
    get iModelId() { return super.iModelId ?? Guid.empty; }
    /** Returns `true` if [[close]] has already been called. */
    get isClosed() { return this._isClosed ? true : false; }
    _isClosed;
    constructor(props, fromIpc) {
        super(props);
        this._fromIpc = fromIpc;
    }
    /** Type guard for instanceof [[CheckpointConnection]] */
    isCheckpointConnection() { return true; }
    /**
     * Open a readonly IModelConnection to a Checkpoint of an iModel.
     */
    static async openRemote(iTwinId, iModelId, version = IModelVersion.latest()) {
        if (undefined === IModelApp.hubAccess)
            throw new Error("Missing an implementation of IModelApp.hubAccess");
        const accessToken = await IModelApp.getAccessToken();
        const changeset = await IModelApp.hubAccess.getChangesetFromVersion({ accessToken, iModelId, version });
        let connection;
        const iModelProps = { iTwinId, iModelId, changeset };
        if (IpcApp.isValid) {
            connection = new this(await IpcApp.appFunctionIpc.openCheckpoint(iModelProps), true);
        }
        else {
            const routingContext = IModelRoutingContext.current || IModelRoutingContext.default;
            connection = new this(await this.callOpen(iModelProps, routingContext), false);
            RpcManager.setIModel(connection);
            connection.routingContext = routingContext;
            RpcRequest.notFoundHandlers.addListener(connection._reopenConnectionHandler);
        }
        IModelConnection.onOpen.raiseEvent(connection);
        return connection;
    }
    static async callOpen(iModelToken, routingContext) {
        // Try opening the iModel repeatedly accommodating any pending responses from the backend.
        // Waits for an increasing amount of time (but within a range) before checking on the pending request again.
        const connectionRetryIntervalRange = { min: 100, max: 5000 }; // in milliseconds
        let connectionRetryInterval = Math.min(connectionRetryIntervalRange.min, IModelConnection.connectionTimeout);
        const openForReadOperation = RpcOperation.lookup(IModelReadRpcInterface, "getConnectionProps");
        if (!openForReadOperation)
            throw new IModelError(BentleyStatus.ERROR, "IModelReadRpcInterface.getConnectionProps() is not available");
        openForReadOperation.policy.retryInterval = () => connectionRetryInterval;
        Logger.logTrace(loggerCategory, `IModelConnection.open`, iModelToken);
        const startTime = Date.now();
        const removeListener = RpcRequest.events.addListener((type, request) => {
            if (type !== RpcRequestEvent.PendingUpdateReceived) // eslint-disable-line @typescript-eslint/no-deprecated
                return;
            if (!(openForReadOperation && request.operation === openForReadOperation))
                return;
            Logger.logTrace(loggerCategory, "Received pending open notification in IModelConnection.open", iModelToken);
            const connectionTimeElapsed = Date.now() - startTime;
            if (connectionTimeElapsed > IModelConnection.connectionTimeout) {
                Logger.logError(loggerCategory, `Timed out opening connection in IModelConnection.open (took longer than ${IModelConnection.connectionTimeout} milliseconds)`, iModelToken);
                throw new IModelError(BentleyStatus.ERROR, "Opening a connection was timed out"); // NEEDS_WORK: More specific error status
            }
            connectionRetryInterval = Math.min(connectionRetryIntervalRange.max, connectionRetryInterval * 2, IModelConnection.connectionTimeout - connectionTimeElapsed);
            if (request.retryInterval !== connectionRetryInterval) {
                request.retryInterval = connectionRetryInterval;
                Logger.logTrace(loggerCategory, `Adjusted open connection retry interval to ${request.retryInterval} milliseconds in IModelConnection.open`, iModelToken);
            }
        });
        const openPromise = IModelReadRpcInterface.getClientForRouting(routingContext.token).getConnectionProps(iModelToken);
        let openResponse;
        try {
            openResponse = await openPromise;
        }
        finally {
            Logger.logTrace(loggerCategory, "Completed open request in IModelConnection.open", iModelToken);
            removeListener();
        }
        return openResponse;
    }
    _reopenConnectionHandler = async (request, response, resubmit, reject) => {
        if (!response.hasOwnProperty("isIModelNotFoundResponse"))
            reject();
        const iModelRpcProps = request.parameters[0];
        if (this._fileKey !== iModelRpcProps.key)
            reject(); // The handler is called for a different connection than this
        Logger.logTrace(loggerCategory, "Attempting to reopen connection", () => iModelRpcProps);
        try {
            const openResponse = await CheckpointConnection.callOpen(iModelRpcProps, this.routingContext);
            // The new/reopened connection may have a new rpcKey and/or changesetId, but the other IModelRpcTokenProps should be the same
            this._fileKey = openResponse.key;
            this.changeset = expectDefined(openResponse.changeset);
        }
        catch (error) {
            reject(BentleyError.getErrorMessage(error));
        }
        finally {
        }
        Logger.logTrace(loggerCategory, "Resubmitting original request after reopening connection", iModelRpcProps);
        request.parameters[0] = this.getRpcProps(); // Modify the token of the original request before resubmitting it.
        resubmit();
    };
    /** Close this CheckpointConnection */
    async close() {
        if (this.isClosed)
            return;
        this.beforeClose();
        if (this._fromIpc)
            await IpcApp.appFunctionIpc.closeIModel(this._fileKey);
        else
            RpcRequest.notFoundHandlers.removeListener(this._reopenConnectionHandler);
        this._isClosed = true;
    }
}
//# sourceMappingURL=CheckpointConnection.js.map