import { EMPTY, Observable, TimeoutError, concat, interval, of, timer, } from "rxjs";
import { DisconnectedDevice, DisconnectedDeviceDuringOperation, LockedDeviceError, } from "@ledgerhq/errors";
import { catchError, debounce, delayWhen, filter, switchMap, tap, timeout } from "rxjs/operators";
import { DeviceModelId, getDeviceModel } from "@ledgerhq/devices";
import { ConnectManagerTimeout } from "../../errors";
import { LocalTracer } from "@ledgerhq/logs";
import { LOG_TYPE } from "..";
import { getEnv } from "@ledgerhq/live-env";
export var ImplementationType;
(function (ImplementationType) {
    ImplementationType["event"] = "enum";
    ImplementationType["polling"] = "polling";
})(ImplementationType || (ImplementationType = {}));
const defaultRetryableWithDelayDisconnectedErrors = [
    DisconnectedDevice,
    DisconnectedDeviceDuringOperation,
];
export const defaultImplementationConfig = {
    pollingFrequency: 2000,
    initialWaitTime: 5000,
    reconnectWaitTime: 5000,
    connectionTimeout: getEnv("DETOX") ? 60000 : 20000,
};
const remapError = (error, deviceModelID) => {
    if (!(error instanceof TimeoutError)) {
        return error;
    }
    if (deviceModelID === DeviceModelId.nanoS) {
        return new LockedDeviceError();
    }
    return new ConnectManagerTimeout(undefined, {
        // TODO make this configurable
        productName: getDeviceModel(deviceModelID).productName,
    });
};
/**
 * Returns a polling implementation function that repeatedly performs a given task
 * with a given request, with a specified polling frequency and wait times until a
 * KO or OK end state is reached. This emulates the event based connection paradigm
 * from the USB stack in a BLE setting.
 * @template EmittedEvent - Type of the events emitted by the task's observable.
 * @template GenericRequestType - Type of the request to be passed to the task.
 **/
const pollingImplementation = (params) => new Observable((subscriber) => {
    const { deviceSubject, task, request, config = defaultImplementationConfig } = params;
    const { pollingFrequency, initialWaitTime, reconnectWaitTime, connectionTimeout } = config;
    const tracer = new LocalTracer(LOG_TYPE, { function: "pollingImplementation" });
    tracer.trace("Setting up device action polling implementation", { config });
    let shouldStopPolling = false;
    let connectSub;
    let loopTimeout;
    let firstRound = true;
    let currentDevice;
    const deviceSubjectSub = deviceSubject.subscribe(device => {
        currentDevice = device || currentDevice;
    });
    // Cover case where our device job never gets a transport instance
    // it will signal back to the consumer that we don't have a device.
    let initialTimeout = setTimeout(() => {
        return subscriber.next({
            type: "deviceChange",
            device: undefined,
        });
    }, initialWaitTime);
    // Runs every time we get a new device.
    function actionLoop() {
        if (currentDevice) {
            connectSub = concat(of({
                type: "deviceChange",
                device: currentDevice,
                replaceable: !firstRound,
            }), task({
                deviceId: currentDevice.deviceId,
                deviceName: currentDevice.deviceName ?? null,
                request,
            }))
                .pipe(
            // Any event should clear the initialTimeout.
            tap((_) => {
                if (initialTimeout) {
                    clearTimeout(initialTimeout);
                    initialTimeout = null;
                }
            }), 
            // More than `connectionTimeout` time of silence, is considered
            // a failed connection. Depending on the flow this may mean we'd have
            // to emit an `alive` kind of event.
            timeout(connectionTimeout), filter((event) => {
                if ("type" in event) {
                    return event.type !== "unresponsiveDevice";
                }
                return false;
            }), catchError((error) => {
                const maybeRemappedError = remapError(error, currentDevice.modelId);
                tracer.trace(`Error when running task in polling implementation: ${error}`, {
                    error,
                    maybeRemappedError,
                });
                return of({
                    type: "error",
                    error: maybeRemappedError,
                });
            }), debounce((event) => {
                if (event.type === "error" && "error" in event) {
                    const allowedRetryableErrors = params.retryableWithDelayDisconnectedErrors ||
                        defaultRetryableWithDelayDisconnectedErrors;
                    const error = event.error;
                    if (allowedRetryableErrors.find(e => error instanceof e)) {
                        // Delay emission of allowed disconnects to allow reconnection.
                        return timer(reconnectWaitTime);
                    }
                    // Other errors should cancel the loop.
                    shouldStopPolling = true;
                }
                // All other events pass through.
                return of(null);
            }))
                .subscribe({
                next: (event) => {
                    subscriber.next(event);
                },
                error: (e) => {
                    subscriber.error(e);
                },
                complete: () => {
                    if (shouldStopPolling)
                        return;
                    firstRound = false; // For proper debouncing.
                    loopTimeout = setTimeout(actionLoop, pollingFrequency);
                },
            });
        }
        else {
            // We currently don't have a device, try again.
            loopTimeout = setTimeout(actionLoop, pollingFrequency);
        }
    }
    // Delay the first loop run in order to be async.
    loopTimeout = setTimeout(actionLoop, 0);
    // Get rid of pending timeouts and subscriptions.
    return function cleanup() {
        if (deviceSubjectSub)
            deviceSubjectSub.unsubscribe();
        if (connectSub)
            connectSub.unsubscribe();
        if (initialTimeout)
            clearTimeout(initialTimeout);
        if (loopTimeout)
            clearTimeout(loopTimeout);
    };
});
const eventImplementation = (params) => {
    const { deviceSubject, task, request, config = defaultImplementationConfig } = params;
    const { reconnectWaitTime } = config;
    return new Observable((subscriber) => {
        const connectSub = deviceSubject
            .pipe(debounce(device => timer(!device ? reconnectWaitTime : 0)), switchMap(device => {
            const initialEvent = of({
                type: "deviceChange",
                device,
            });
            return concat(initialEvent, device
                ? task({
                    deviceId: device.deviceId,
                    deviceName: device.deviceName ?? null,
                    request,
                })
                : EMPTY);
        }), catchError((error) => of({
            type: "error",
            error,
        })), 
        // NB An error is a dead-end as far as the task is concerned, and by delaying
        // the emission of the event we prevent instant failures from showing flashing
        // UI that looks like a glitch. For instance, if the device is locked and we retry
        // this would allow a better UX, 800ms before a failure is totally acceptable.
        delayWhen((e) => e.type === "error" || e.type === "lockedDevice" ? interval(800) : interval(0)))
            .subscribe({
            next: (event) => {
                subscriber.next(event);
            },
            error: (e) => {
                subscriber.error(e);
            },
            complete: () => { },
        });
        // Get rid of subscriptions.
        return function cleanup() {
            if (connectSub)
                connectSub.unsubscribe();
        };
    });
};
const getImplementation = (currentMode) => currentMode === "polling" ? pollingImplementation : eventImplementation;
export { pollingImplementation, eventImplementation, getImplementation };
//# sourceMappingURL=implementations.js.map