/* eslint-disable @typescript-eslint/no-unsafe-function-type */
import { Exception } from '@api-client/core/exceptions/exception.js';
import { Events } from '../../events/Events.js';
/**
 * A base class for all platform bindings.
 *
 * Platform bindings is the way how the application runs a platform specific logic.
 *
 * For example, it implements how the application stores the state or implements file picker / file saver.
 * Depending on the platform (Electron, web, Chrome, more?)  it uses different set of bindings
 * defined in the target application. This creates a framework for the bindings to exist.
 *
 * The IO thread can be any process that is separated from the browser tab context.
 * Can be a WebWorker, ServiceWorker, of an API call to a server.
 * When communication with the binding is not possible via the Promise API
 * this system should be used to communicate with the background page.
 *
 * ```typescript
 * class MyBinding extends ParentBaseBinding {
 *   channel: BroadcastChannel;
 *   constructor() {
 *     super();
 *     this.channel = new BroadcastChannel('my-channel');
 *   }
 *
 *   doStuff(arg: any): Promise<any> {
 *      return this.invoke('doStuff', 'own argument', arg);
 *   }
 * }
 * ```
 *
 * This assumes that the IO thread has the `doStuff` function implemented.
 * The IO thread extends the `IoThread` class which has the defined the communication logic.
 */
export class PlatformBindings {
    /**
     * Used with the queue as the identifier of the request.
     * USe the `getId()` to read a unique request id.
     */
    id = 0;
    queue = [];
    /**
     * @returns A unique for the current session id.
     */
    getId() {
        this.id += 1;
        return this.id;
    }
    /**
     * Invokes a function in the IO thread.
     *
     * @param fn The function name to invoke (or an identifier understood by the IO thread)
     * @param args The function arguments to pass.
     * @returns A promise that should be returned to the client's application logic.
     * The promise is resolved when the IO thread sends an event with the same identifier generated with this call.
     */
    invoke(fn, ...args) {
        return new Promise((resolve, reject) => {
            const id = this.getId();
            const item = {
                resolve,
                reject,
                id,
                fn,
                args,
            };
            this.queue.push(item);
            const cmd = {
                id,
                fn,
                args,
                kind: 'IO#Command',
            };
            this.sendIoCommand(cmd);
        });
    }
    /**
     * To be called by the child class when the IO thread send an event.
     * It recognizes the type of the message sent by the IO thread an performs the requested operation.
     *
     * @param message The message sent by the IO thread.
     */
    handleIoMessage(message) {
        if (!message.kind) {
            // eslint-disable-next-line no-console
            console.warn('Invalid message received on the app-config-channel.', message);
            return;
        }
        if (message.kind === 'IO#Command') {
            // message sent by self.
            return;
        }
        if (message.kind === 'IO#Notification') {
            this.notify(message);
        }
        else if (message.kind === 'IO#Event') {
            this.resolve(message);
        }
    }
    /**
     * Resolves a pending promise stored in the `queue`.
     *
     * @param event The IO thread event
     */
    resolve(event) {
        const { id, type, message, result } = event;
        if (typeof id !== 'number') {
            // eslint-disable-next-line no-console
            console.warn(`Unknown event from the IO thread. Id is not a number.`, event);
            return;
        }
        const index = this.queue.findIndex((i) => i.id === id);
        if (index < 0) {
            // this might be used by another tab.
            return;
        }
        const info = this.queue[index];
        this.queue.splice(index, 1);
        if (type === 'result') {
            info.resolve(result);
        }
        else if (type === 'error') {
            const apiErr = result;
            if (apiErr && apiErr.code) {
                info.reject(Exception.fromRawException(apiErr, 'Unknown exception'));
            }
            else {
                info.reject(new Error(message));
            }
        }
        else {
            // eslint-disable-next-line no-console
            console.warn(`Unknown event from the IO thread`, event);
        }
    }
    /**
     * Notifies this tab about something using the `Events` definition.
     *
     * This expects that the event has the path to the event function,
     * for example `Events.Config.Environment.State.created`.
     *
     * @param event The IO thread event
     */
    notify(event) {
        const { path, args } = event;
        const parts = path.split('.');
        let current = Events;
        if (parts[0] === 'Events') {
            parts.shift();
        }
        for (const part of parts) {
            // @ts-expect-error It's dynamic access to a deep property.
            if (!current[part]) {
                // eslint-disable-next-line no-console
                console.warn(`Invalid notification path`, path);
                return;
            }
            // @ts-expect-error It's dynamic access to a deep property.
            current = current[part];
        }
        ;
        current(...args);
    }
}
//# sourceMappingURL=PlatformBindings.js.map