/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module RpcInterface
 */
import { RpcConfiguration } from "./rpc/core/RpcConfiguration";
import { CURRENT_REQUEST } from "./rpc/core/RpcRegistry";
import { aggregateLoad, RpcRequest } from "./rpc/core/RpcRequest";
import { RpcRoutingToken } from "./rpc/core/RpcRoutingToken";
import { IpcSession } from "./ipc/IpcSession";
import { IModelError, NoContentError } from "./IModelError";
import { RpcRequestEvent, RpcRequestStatus } from "./rpc/core/RpcConstants";
import { BeDuration, BentleyStatus } from "@itwin/core-bentley";
/** An RPC interface is a set of operations exposed by a service that a client can call, using configurable protocols,
 * in a platform-independent way. TheRpcInterface class is the base class for RPC interface definitions and implementations.
 * @public
 */
export class RpcInterface {
    static findDiff(backend, frontend) {
        return backend.major !== frontend.major ? "major" :
            backend.minor !== frontend.minor ? "minor" :
                backend.patch !== frontend.patch ? "patch" :
                    backend.prerelease !== frontend.prerelease ? "prerelease" :
                        "same";
    }
    static parseVer(version) {
        // separate the version from the prerelease tag
        const parts = version.split(/[:-]/);
        // Split the major.minor.path into separate components
        const prefix = parts[0].split(".");
        const ver = { major: Number(prefix[0]), minor: Number(prefix[1]), patch: Number(prefix[2]) };
        if (parts.length > 1)
            ver.prerelease = parts[1];
        return ver;
    }
    /** Determines whether the backend version of an RPC interface is compatible (according to semantic versioning) with the frontend version of the interface. */
    static isVersionCompatible(backend, frontend) {
        if (backend === frontend)
            return true; // most common case, versions are identical
        const backendSemver = this.parseVer(backend);
        const frontendSemver = this.parseVer(frontend);
        // if either has a prerelease tag, they are not compatible unless version strings are identical
        if (backendSemver.prerelease || frontendSemver.prerelease)
            return false;
        const difference = this.findDiff(backendSemver, frontendSemver);
        // If the major versions are different, the versions are not compatible
        if (difference === "major")
            return false;
        // special case for major version 0. If patch difference, backend patch must be greater than frontend patch
        if (backendSemver.major === 0)
            return (difference === "patch" && frontendSemver.patch < backendSemver.patch);
        // patch difference is fine. If minor versions differ, compatible as long as backend minor version is greater
        return difference === "patch" || (difference === "minor" && frontendSemver.minor < backendSemver.minor);
    }
    /** @beta */
    constructor(routing = RpcRoutingToken.default) {
        this.routing = routing;
        this.configuration = RpcConfiguration.supply(this);
    }
    /** Obtains the implementation result for an RPC operation. */
    async forward(parameters) {
        const parametersCompat = (arguments.length === 1 && typeof (parameters) === "object") ? parameters : arguments;
        const parametersArray = Array.isArray(parametersCompat) ? parametersCompat : Array.prototype.slice.call(parametersCompat);
        const operationName = parametersArray.pop();
        const session = IpcSession.active;
        if (session) {
            return intercept(session, this, operationName, parametersArray);
        }
        else {
            const request = new this.configuration.protocol.requestType(this, operationName, parametersArray);
            request.submit(); // eslint-disable-line @typescript-eslint/no-floating-promises
            this[CURRENT_REQUEST] = request;
            return request.response;
        }
    }
}
RpcInterface.prototype.configurationSupplier = undefined;
class InterceptedRequest extends RpcRequest {
    async load() { throw new Error(); }
    async send() { throw new Error(); }
    setHeader(_name, _value) { throw new Error(); }
}
async function intercept(session, client, operation, parameters) {
    const request = new InterceptedRequest(client, operation, []);
    client[CURRENT_REQUEST] = request;
    const context = await client.configuration.protocol.serialize(request);
    request.parameters = parameters;
    const info = {
        definition: {
            interfaceName: context.operation.interfaceDefinition,
            interfaceVersion: context.operation.interfaceVersion,
        },
        operation,
        parameters,
        context: {
            applicationId: context.applicationId,
            applicationVersion: context.applicationVersion,
            id: context.id,
            sessionId: context.sessionId,
            protocolVersion: (context.protocolVersion || 0).toString(),
        },
    };
    const dispatch = async () => {
        aggregateLoad.lastRequest = new Date().getTime();
        const response = await session.handleRpc(info);
        aggregateLoad.lastResponse = new Date().getTime();
        if (typeof (response) === "object" && response.hasOwnProperty("iTwinRpcCoreResponse") && response.hasOwnProperty("managedStatus")) {
            const status = response;
            if (status.managedStatus === "pending") {
                return handlePending(request, status, dispatch);
            }
            else if (status.managedStatus === "notFound") {
                return handleNotFound(request, status, dispatch);
            }
            else if (status.managedStatus === "noContent") {
                return handleNoContent();
            }
        }
        else {
            return response;
        }
    };
    return dispatch();
}
async function handlePending(request, status, dispatch) {
    request._status = RpcRequestStatus.Pending;
    request._extendedStatus = status.responseValue.message;
    RpcRequest.events.raiseEvent(RpcRequestEvent.PendingUpdateReceived, request);
    const delay = request.operation.policy.retryInterval(request.client.configuration);
    await BeDuration.wait(delay);
    return dispatch();
}
async function handleNotFound(request, status, dispatch) {
    return new Promise((resolve, reject) => {
        let resubmitted = false;
        RpcRequest.notFoundHandlers.raiseEvent(request, status.responseValue, async () => {
            if (resubmitted) {
                throw new IModelError(BentleyStatus.ERROR, `Already resubmitted using this handler.`);
            }
            resubmitted = true;
            try {
                const response = await dispatch();
                resolve(response);
            }
            catch (err) {
                reject(err); // eslint-disable-line @typescript-eslint/prefer-promise-reject-errors
            }
        }, reject);
    });
}
async function handleNoContent() {
    throw new NoContentError();
}
//# sourceMappingURL=RpcInterface.js.map