/*---------------------------------------------------------------------------------------------
* 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 { BentleyStatus } from "@itwin/core-bentley";
import { IModelError } from "../../IModelError";
import { RpcConfiguration } from "./RpcConfiguration";
import { RpcPendingQueue } from "./RpcPendingQueue";
import { initializeRpcRequest } from "./RpcRequest";
import { RpcRoutingToken } from "./RpcRoutingToken";
import { RpcInterface } from "../../RpcInterface";
import { RpcControlChannel } from "./RpcControl";
import { RpcOperation, RpcOperationPolicy } from "./RpcOperation";
/** @internal */
export const REGISTRY = Symbol.for("@itwin/core-common/RpcRegistry");
/** @internal */
export const OPERATION = Symbol.for("@itwin/core-common/RpcOperation");
/** @internal */
export const POLICY = Symbol.for("@itwin/core-common/RpcOperationPolicy");
/** @internal */
export const INSTANCE = Symbol.for("@itwin/core-common/RpcInterface/__instance__");
/** @internal */
export const CURRENT_REQUEST = Symbol.for("@itwin/core-common/RpcRequest/__current__");
/** @internal */
export const CURRENT_INVOCATION = Symbol.for("@itwin/core-common/RpcInvocation/__current__");
/** @internal */
export class RpcRegistry {
    constructor() {
        this.definitionClasses = new Map();
        this.proxies = new Map();
        this.implementations = new Map();
        this.suppliedImplementations = new Map();
        this.implementationClasses = new Map();
        this.id = (() => {
            let i = 0;
            return () => ++i;
        })();
    }
    static get instance() {
        if (!RpcRegistry._instance) {
            const globalObj = typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};
            if (!globalObj[REGISTRY])
                globalObj[REGISTRY] = new RpcRegistry();
            RpcRegistry._instance = globalObj[REGISTRY];
        }
        return RpcRegistry._instance;
    }
    lookupInterfaceDefinition(name) {
        if (!this.definitionClasses.has(name))
            throw new IModelError(BentleyStatus.ERROR, `RPC interface "${name}" is not initialized.`);
        return this.definitionClasses.get(name);
    }
    async describeAvailableEndpoints() {
        const requests = [];
        for (const channel of RpcControlChannel.channels) {
            requests.push(channel.describeEndpoints());
        }
        const responses = await Promise.all(requests);
        const endpoints = responses.reduce((a, b) => a.concat(b), []);
        for (const endpoint of endpoints) {
            const definition = this.definitionClasses.get(endpoint.interfaceName);
            endpoint.compatible = (definition && RpcInterface.isVersionCompatible(endpoint.interfaceVersion, definition.interfaceVersion)) ? true : false;
        }
        return endpoints;
    }
    getClientForInterface(definition, routing = RpcRoutingToken.default) {
        let instance;
        const proxies = this.proxies.get(definition.interfaceName);
        if (proxies) {
            instance = proxies.get(routing.id);
        }
        if (!instance)
            instance = this.instantiateClient(definition, routing);
        return instance;
    }
    getImplForInterface(definition) {
        let instance = this.implementations.get(definition.interfaceName);
        if (!instance)
            instance = this.instantiateImpl(definition);
        return instance;
    }
    lookupImpl(interfaceName) {
        const definition = this.lookupInterfaceDefinition(interfaceName);
        return this.getImplForInterface(definition);
    }
    registerImpl(definition, implementation) {
        this.unregisterImpl(definition);
        this.implementationClasses.set(definition.interfaceName, implementation);
    }
    unregisterImpl(definition) {
        this.implementationClasses.delete(definition.interfaceName);
        const impl = this.implementations.get(definition.interfaceName);
        if (impl) {
            impl.configuration.onRpcImplTerminated(definition, impl);
            this.implementations.delete(definition.interfaceName);
        }
    }
    supplyImplInstance(definition, instance) {
        this.suppliedImplementations.set(definition.interfaceName, instance);
    }
    isRpcInterfaceInitialized(definition) {
        return this.definitionClasses.has(definition.interfaceName);
    }
    initializeRpcInterface(definition) {
        if (this.definitionClasses.has(definition.interfaceName)) {
            const existing = this.definitionClasses.get(definition.interfaceName);
            if (existing && definition.interfaceVersion === "CONTROL" && existing !== definition) {
                this.configureOperations(definition); // configs that differ only by routing still need the control ops initialized
            }
            return;
        }
        this.notifyInitialize();
        this.definitionClasses.set(definition.interfaceName, definition);
        this.configureOperations(definition);
    }
    terminateRpcInterface(definition) {
        this.unregisterImpl(definition);
        this.purgeClient(definition);
        this.definitionClasses.delete(definition.interfaceName);
    }
    instantiateImpl(definition) {
        this.checkInitialized(definition);
        const registeredImplementation = this.implementationClasses.get(definition.interfaceName);
        if (!registeredImplementation)
            throw new IModelError(BentleyStatus.ERROR, `An RPC interface implementation class for "${definition.interfaceName}" is not registered.`);
        if (definition.prototype.configurationSupplier)
            registeredImplementation.prototype.configurationSupplier = definition.prototype.configurationSupplier;
        const supplied = this.suppliedImplementations.get(definition.interfaceName);
        const implementation = supplied || new registeredImplementation();
        if (!(implementation instanceof registeredImplementation))
            throw new IModelError(BentleyStatus.ERROR, `Invalid RPC interface implementation.`);
        if (supplied) {
            supplied.configuration = RpcConfiguration.supply(supplied);
        }
        this.implementations.set(definition.interfaceName, implementation);
        implementation.configuration.onRpcImplInitialized(definition, implementation);
        return implementation;
    }
    instantiateClient(definition, routing = RpcRoutingToken.default) {
        this.checkInitialized(definition);
        const proxy = new definition(routing);
        if (!this.proxies.has(definition.interfaceName)) {
            this.proxies.set(definition.interfaceName, new Map());
        }
        this.proxies.get(definition.interfaceName)?.set(routing.id, proxy);
        Object.getOwnPropertyNames(definition.prototype).forEach((operationName) => {
            if (operationName === "constructor" || operationName === "configurationSupplier")
                return;
            this.interceptOperation(proxy, operationName);
        });
        proxy.configuration.onRpcClientInitialized(definition, proxy);
        return proxy;
    }
    interceptOperation(proxy, operation) {
        const clientFunction = proxy[operation];
        proxy[operation] = function () {
            const args = Array.from(arguments);
            args.push(operation);
            return clientFunction.apply(proxy, args);
        };
    }
    checkInitialized(definition) {
        if (!this.definitionClasses.has(definition.interfaceName))
            throw new IModelError(BentleyStatus.ERROR, `RPC interface "${definition.interfaceName}" is not initialized.`);
    }
    configureOperations(definition) {
        const proto = definition.prototype;
        Object.getOwnPropertyNames(proto).forEach((operationName) => {
            if (operationName === "constructor" || operationName === "configurationSupplier")
                return;
            const propertyName = RpcOperation.computeOperationName(operationName);
            if (!proto[propertyName][OPERATION]) {
                const policy = definition[POLICY] || new RpcOperationPolicy();
                proto[propertyName][OPERATION] = new RpcOperation(definition, propertyName, policy);
            }
        });
    }
    purgeClient(definition) {
        const proxies = this.proxies.get(definition.interfaceName);
        if (proxies) {
            proxies.forEach((proxy) => proxy.configuration.onRpcClientTerminated(definition, proxy));
            this.proxies.delete(definition.interfaceName);
        }
    }
    notifyInitialize() {
        initializeRpcRequest();
        RpcPendingQueue.initialize();
    }
}
//# sourceMappingURL=RpcRegistry.js.map