/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module NativeApp
 */
import { assert, BentleyError, IModelStatus, JsonUtils, Logger, LogLevel } from "@itwin/core-bentley";
import { getPullChangesIpcChannel, IModelError, IModelNotFoundResponse, ipcAppChannels, iTwinChannel, } from "@itwin/core-common";
import { ProgressStatus } from "./CheckpointManager";
import { BriefcaseDb, IModelDb, SnapshotDb, StandaloneDb } from "./IModelDb";
import { IModelHost } from "./IModelHost";
import { IModelNative } from "./internal/NativePlatform";
import { _nativeDb } from "./internal/Symbols";
import { cancelTileContentRequests } from "./rpc-impl/IModelTileRpcImpl";
/**
 * Used by applications that have a dedicated backend. IpcHosts may send messages to their corresponding IpcApp.
 * @note if either end terminates, the other must too.
 * @public
 */
export class IpcHost {
    static noStack = false;
    static _ipc;
    /** Get the implementation of the [IpcSocketBackend]($common) interface. */
    static get ipc() { return this._ipc; } // eslint-disable-line @typescript-eslint/no-non-null-assertion
    /** Determine whether Ipc is available for this backend. This will only be true if [[startup]] has been called on this class. */
    static get isValid() { return undefined !== this._ipc; }
    /**
     * Send a message to the frontend over an Ipc channel.
     * @param channel the name of the channel matching the name registered with [[IpcApp.addListener]].
     * @param data The content of the message.
     */
    static send(channel, ...data) {
        this.ipc.send(iTwinChannel(channel), ...data);
    }
    /**
     * Establish a handler for an Ipc channel to receive [[Frontend.invoke]] calls
     * @param channel The name of the channel for this handler.
     * @param handler A function that supplies the implementation for `channel`
     * @note returns A function to call to remove the handler.
     */
    static handle(channel, handler) {
        return this.ipc.handle(iTwinChannel(channel), handler);
    }
    /**
     * Establish a handler to receive messages sent via [[IpcApp.send]].
     * @param channel The name of the channel for the messages.
     * @param listener A function called when messages are sent over `channel`
     * @note returns A function to call to remove the listener.
     */
    static addListener(channel, listener) {
        return this.ipc.addListener(iTwinChannel(channel), listener);
    }
    /**
     * Remove a previously registered listener
     * @param channel The name of the channel for the listener previously registered with [[addListener]]
     * @param listener The function passed to [[addListener]]
     */
    static removeListener(channel, listener) {
        this.ipc.removeListener(iTwinChannel(channel), listener);
    }
    static notify(channel, briefcase, methodName, ...args) {
        if (this.isValid)
            return this.send(`${channel}/${briefcase.key}`, methodName, ...args);
    }
    /** @internal */
    static notifyIpcFrontend(methodName, ...args) {
        return IpcHost.send(ipcAppChannels.appNotify, methodName, ...args);
    }
    /** @internal */
    static notifyTxns(briefcase, methodName, ...args) {
        this.notify(ipcAppChannels.txns, briefcase, methodName, ...args);
    }
    /** @internal */
    static notifyEditingScope(briefcase, methodName, ...args) {
        this.notify(ipcAppChannels.editingScope, briefcase, methodName, ...args);
    }
    /**
     * Start the backend of an Ipc app.
     * @param opt
     * @note this method calls [[IModelHost.startup]] internally.
     */
    static async startup(opt) {
        this._ipc = opt?.ipcHost?.socket;
        if (opt?.ipcHost?.exceptions?.noStack)
            this.noStack = true;
        if (this.isValid) { // for tests, we use IpcHost but don't have a frontend
            IpcAppHandler.register();
        }
        await IModelHost.startup(opt?.iModelHost);
    }
    /** Shutdown IpcHost backend. Also calls [[IModelHost.shutdown]] */
    static async shutdown() {
        this._ipc = undefined;
        await IModelHost.shutdown();
    }
}
/**
 * Base class for all implementations of an Ipc interface.
 *
 * Create a subclass to implement your Ipc interface. Your class should be declared like this:
 * ```ts
 * class MyHandler extends IpcHandler implements MyInterface
 * ```
 * to ensure all methods and signatures are correct.
 *
 * Then, call `MyClass.register` at startup to connect your class to your channel.
 * @public
 */
export class IpcHandler {
    /**
     * Register this class as the handler for methods on its channel. This static method creates a new instance
     * that becomes the handler and is `this` when its methods are called.
     * @returns A function that can be called to remove the handler.
     * @note this method should only be called once per channel. If it is called multiple times, subsequent calls replace the previous ones.
     */
    static register() {
        const impl = new this(); // create an instance of subclass. "as any" is necessary because base class is abstract
        const prohibitedFunctions = Object.getOwnPropertyNames(Object.getPrototypeOf({}));
        return IpcHost.handle(impl.channelName, async (_evt, funcName, ...args) => {
            try {
                if (prohibitedFunctions.includes(funcName))
                    throw new Error(`Method "${funcName}" not available for channel: ${impl.channelName}`);
                const func = impl[funcName];
                if (typeof func !== "function")
                    throw new IModelError(IModelStatus.FunctionNotFound, `Method "${impl.constructor.name}.${funcName}" not found on IpcHandler registered for channel: ${impl.channelName}`);
                return { result: await func.call(impl, ...args) };
            }
            catch (err) {
                if (!JsonUtils.isObject(err)) // if the exception isn't an object, just forward it
                    return { error: err };
                const ret = { error: { ...err } };
                ret.error.message = err.message; // NB: .message, and .stack members of Error are not enumerable, so spread operator above does not copy them.
                if (!IpcHost.noStack)
                    ret.error.stack = err.stack;
                if (err instanceof BentleyError) {
                    ret.error.iTwinErrorId = err.iTwinErrorId;
                    if (err.hasMetaData)
                        ret.error.loggingMetadata = err.loggingMetadata;
                    delete ret.error._metaData;
                }
                return ret;
            }
        });
    }
}
/**
 * Implementation  of IpcAppFunctions
 */
class IpcAppHandler extends IpcHandler {
    get channelName() { return ipcAppChannels.functions; }
    _iModelKeyToPullStatus = new Map();
    async log(_timestamp, level, category, message, metaData) {
        switch (level) {
            case LogLevel.Error:
                Logger.logError(category, message, metaData);
                break;
            case LogLevel.Info:
                Logger.logInfo(category, message, metaData);
                break;
            case LogLevel.Trace:
                Logger.logTrace(category, message, metaData);
                break;
            case LogLevel.Warning:
                Logger.logWarning(category, message, metaData);
                break;
        }
    }
    async cancelTileContentRequests(tokenProps, contentIds) {
        return cancelTileContentRequests(tokenProps, contentIds);
    }
    async cancelElementGraphicsRequests(key, requestIds) {
        return IModelDb.findByKey(key)[_nativeDb].cancelElementGraphicsRequests(requestIds);
    }
    async openBriefcase(args) {
        const db = await BriefcaseDb.open(args);
        return db.toJSON();
    }
    async openCheckpoint(checkpoint) {
        return (await SnapshotDb.openCheckpoint(checkpoint)).getConnectionProps();
    }
    async openStandalone(filePath, openMode, opts) {
        return StandaloneDb.openFile(filePath, openMode, opts).getConnectionProps();
    }
    async openSnapshot(filePath, opts) {
        let resolvedFileName = filePath;
        if (IModelHost.snapshotFileNameResolver) { // eslint-disable-line @typescript-eslint/no-deprecated
            resolvedFileName = IModelHost.snapshotFileNameResolver.tryResolveFileName(filePath); // eslint-disable-line @typescript-eslint/no-deprecated
            if (!resolvedFileName)
                throw new IModelNotFoundResponse(); // eslint-disable-line @typescript-eslint/only-throw-error
        }
        return SnapshotDb.openFile(resolvedFileName, opts).getConnectionProps();
    }
    async closeIModel(key) {
        IModelDb.findByKey(key).close();
    }
    async saveChanges(key, description) {
        IModelDb.findByKey(key).saveChanges(description);
    }
    async abandonChanges(key) {
        IModelDb.findByKey(key).abandonChanges();
    }
    async hasPendingTxns(key) {
        return IModelDb.findByKey(key)[_nativeDb].hasPendingTxns();
    }
    async isUndoPossible(key) {
        return IModelDb.findByKey(key)[_nativeDb].isUndoPossible();
    }
    async isRedoPossible(key) {
        return IModelDb.findByKey(key)[_nativeDb].isRedoPossible();
    }
    async getUndoString(key) {
        return IModelDb.findByKey(key)[_nativeDb].getUndoString();
    }
    async getRedoString(key) {
        return IModelDb.findByKey(key)[_nativeDb].getRedoString();
    }
    async pullChanges(key, toIndex, options) {
        const iModelDb = BriefcaseDb.findByKey(key);
        this._iModelKeyToPullStatus.set(key, ProgressStatus.Continue);
        const checkAbort = () => this._iModelKeyToPullStatus.get(key) ?? ProgressStatus.Continue;
        let onProgress;
        if (options?.reportProgress) {
            const progressCallback = (loaded, total) => {
                IpcHost.send(getPullChangesIpcChannel(iModelDb.iModelId), { loaded, total });
                return checkAbort();
            };
            onProgress = throttleProgressCallback(progressCallback, checkAbort, options?.progressInterval);
        }
        else if (options?.enableCancellation) {
            onProgress = checkAbort;
        }
        try {
            await iModelDb.pullChanges({ toIndex, onProgress });
        }
        finally {
            this._iModelKeyToPullStatus.delete(key);
        }
        return iModelDb.changeset;
    }
    async cancelPullChangesRequest(key) {
        this._iModelKeyToPullStatus.set(key, ProgressStatus.Abort);
    }
    async pushChanges(key, description) {
        const iModelDb = BriefcaseDb.findByKey(key);
        await iModelDb.pushChanges({ description });
        return iModelDb.changeset;
    }
    async toggleGraphicalEditingScope(key, startSession) {
        const val = IModelDb.findByKey(key)[_nativeDb].setGeometricModelTrackingEnabled(startSession);
        if (val.error)
            throw new IModelError(val.error.status, "Failed to toggle graphical editing scope");
        assert(undefined !== val.result);
        return val.result;
    }
    async isGraphicalEditingSupported(key) {
        return IModelDb.findByKey(key)[_nativeDb].isGeometricModelTrackingSupported();
    }
    async reverseTxns(key, numOperations) {
        return IModelDb.findByKey(key)[_nativeDb].reverseTxns(numOperations);
    }
    async reverseAllTxn(key) {
        return IModelDb.findByKey(key)[_nativeDb].reverseAll();
    }
    async reinstateTxn(key) {
        return IModelDb.findByKey(key)[_nativeDb].reinstateTxn();
    }
    async restartTxnSession(key) {
        return IModelDb.findByKey(key)[_nativeDb].restartTxnSession();
    }
    async queryConcurrency(pool) {
        return IModelNative.platform.queryConcurrency(pool);
    }
}
/**
 * Prevents progress callback being called more frequently when provided interval.
 * @internal
 */
export function throttleProgressCallback(func, checkAbort, progressInterval) {
    const interval = progressInterval ?? 250; // by default, only send progress events every 250 milliseconds
    let nextTime = Date.now() + interval;
    const progressCallback = (loaded, total) => {
        const now = Date.now();
        if (loaded >= total || now >= nextTime) {
            nextTime = now + interval;
            return func(loaded, total);
        }
        return checkAbort();
    };
    return progressCallback;
}
//# sourceMappingURL=IpcHost.js.map