import invariant from "invariant";
import { interval, of } from "rxjs";
import { scan, debounce, tap, takeWhile } from "rxjs/operators";
import { useEffect, useCallback, useState, useMemo, useRef } from "react";
import { log } from "@ledgerhq/logs";
import { getDerivationScheme, getDerivationModesForCurrency, runDerivationScheme, } from "@ledgerhq/coin-framework/derivation";
import { useReplaySubject } from "../../observable";
import { shouldUpgrade } from "../../apps";
import perFamilyAccount from "../../generated/account";
import { getImplementation } from "./implementations";
import { getDefaultAccountName } from "@ledgerhq/live-wallet/accountName";
const mapResult = ({ opened, device, appAndVersion, displayUpgradeWarning, }) => opened && device && !displayUpgradeWarning
    ? {
        device,
        appAndVersion,
    }
    : null;
const getInitialState = (device, request) => ({
    isLoading: !!device,
    requestQuitApp: false,
    requestOpenApp: null,
    unresponsive: false,
    isLocked: false,
    requiresAppInstallation: null,
    allowOpeningRequestedWording: null,
    allowOpeningGranted: false,
    allowManagerRequested: false,
    allowManagerGranted: false,
    device: null,
    deviceInfo: null,
    deviceId: null,
    latestFirmware: null,
    opened: false,
    appAndVersion: null,
    error: null,
    derivation: null,
    displayUpgradeWarning: false,
    installingApp: false,
    listingApps: false,
    request,
    currentAppOp: undefined,
    installQueue: [],
    listedApps: false,
    skippedAppOps: [],
    itemProgress: 0,
    progress: undefined,
});
const reducer = (state, e) => {
    switch (e.type) {
        case "unresponsiveDevice":
            return { ...state, unresponsive: true };
        case "lockedDevice":
            return { ...state, isLocked: true };
        // This event does not set isLocked and unresponsive properties, as
        // by itself it does not request anything from the device
        case "device-update-last-seen":
            return {
                ...state,
                deviceInfo: e.deviceInfo,
                latestFirmware: e.latestFirmware,
            };
        case "disconnected":
            // disconnected event can happen for example:
            // - when the wired device is unplugged
            // - before a ask-open-app event when an other app is already open
            return {
                ...getInitialState(null, state.request),
                isLoading: !!e.expected,
            };
        case "deviceChange":
            return { ...getInitialState(e.device, state.request), device: e.device };
        case "some-apps-skipped":
            return {
                ...state,
                skippedAppOps: e.skippedAppOps,
                installQueue: state.installQueue,
            };
        case "inline-install":
            return {
                ...getInitialState(state.device, state.request),
                isLoading: false,
                allowOpeningGranted: true,
                allowManagerRequested: false,
                allowManagerGranted: true,
                device: state.device,
                installingApp: true,
                progress: e.progress || 0,
                deviceInfo: undefined,
                latestFirmware: undefined,
                request: state.request,
                skippedAppOps: state.skippedAppOps,
                currentAppOp: e.currentAppOp,
                listedApps: state.listedApps,
                itemProgress: e.itemProgress || 0,
                installQueue: e.installQueue || [],
            };
        case "listing-apps":
            return {
                ...state,
                listedApps: false,
                listingApps: true,
                unresponsive: false,
                isLocked: false,
            };
        case "error":
            return {
                ...getInitialState(state.device, state.request),
                device: state.device || null,
                error: e.error,
                isLoading: false,
                listingApps: false,
                request: state.request,
                skippedAppOps: state.skippedAppOps,
            };
        case "ask-open-app":
            return {
                ...getInitialState(state.device, state.request),
                isLoading: false,
                device: state.device,
                requestOpenApp: e.appName,
                deviceInfo: undefined,
                latestFirmware: undefined,
                installingApp: undefined,
                listingApps: undefined,
                installQueue: undefined,
                listedApps: undefined,
                itemProgress: undefined,
                skippedAppOps: state.skippedAppOps,
            };
        case "ask-quit-app":
            return {
                ...getInitialState(state.device, state.request),
                isLoading: false,
                device: state.device,
                requestQuitApp: true,
                installingApp: undefined,
                listingApps: undefined,
                installQueue: undefined,
                listedApps: undefined,
                itemProgress: undefined,
                skippedAppOps: state.skippedAppOps,
            };
        case "device-permission-requested":
            return {
                ...getInitialState(state.device, state.request),
                isLoading: false,
                device: state.device,
                allowManagerRequested: true,
                deviceInfo: undefined,
                latestFirmware: undefined,
                installingApp: undefined,
                listingApps: undefined,
                listedApps: undefined,
                itemProgress: undefined,
                skippedAppOps: state.skippedAppOps,
                installQueue: state.installQueue,
            };
        case "device-permission-granted":
            return {
                ...getInitialState(state.device, state.request),
                isLoading: false,
                device: state.device,
                allowOpeningGranted: true,
                allowManagerGranted: true,
                deviceInfo: undefined,
                latestFirmware: undefined,
                installingApp: undefined,
                listingApps: undefined,
                itemProgress: undefined,
                skippedAppOps: state.skippedAppOps,
                installQueue: state.installQueue,
                listedApps: state.listedApps,
            };
        case "device-id":
            return {
                ...state,
                deviceId: e.deviceId,
            };
        case "app-not-installed":
            return {
                ...getInitialState(state.device, state.request),
                isLoading: false,
                device: state.device,
                requiresAppInstallation: {
                    appNames: e.appNames,
                    appName: e.appName,
                },
                deviceInfo: undefined,
                latestFirmware: undefined,
                installingApp: undefined,
                listingApps: undefined,
                installQueue: undefined,
                listedApps: undefined,
                itemProgress: undefined,
                skippedAppOps: state.skippedAppOps,
            };
        case "listed-apps":
            return {
                ...state,
                listedApps: true,
                installQueue: e.installQueue,
            };
        case "opened":
            return {
                ...getInitialState(state.device, state.request),
                isLoading: false,
                device: state.device,
                opened: true,
                appAndVersion: e.app,
                derivation: e.derivation,
                deviceInfo: undefined,
                latestFirmware: undefined,
                installingApp: undefined,
                listingApps: undefined,
                installQueue: undefined,
                itemProgress: undefined,
                request: state.request,
                skippedAppOps: state.skippedAppOps,
                listedApps: state.listedApps,
                displayUpgradeWarning: state.device && e.app ? shouldUpgrade(e.app.name, e.app.version) : false,
            };
    }
    return state;
};
/**
 * Map between an AppRequest and a ConnectAppRequest, allowing us to
 * specify an account or a currency without resolving manually the actual
 * applications we depend on in order to access the flow.
 */
function inferCommandParams(appRequest) {
    let derivationMode;
    let derivationPath;
    const { account, requireLatestFirmware, allowPartialDependencies = false, dependencies: appDependencies, } = appRequest;
    let { appName, currency } = appRequest;
    if (!currency && account) {
        currency = account.currency;
    }
    if (!appName && currency) {
        appName = currency.managerAppName;
    }
    invariant(appName, "appName or currency or account is missing");
    let dependencies = undefined;
    if (appDependencies) {
        dependencies = appDependencies.map(d => inferCommandParams(d).appName);
    }
    if (!currency) {
        return {
            appName,
            dependencies,
            requireLatestFirmware,
            allowPartialDependencies,
        };
    }
    let extra;
    if (account) {
        derivationMode = account.derivationMode;
        derivationPath = account.freshAddressPath;
        const m = perFamilyAccount[account.currency.family];
        if (m && m.injectGetAddressParams) {
            extra = m.injectGetAddressParams(account);
        }
    }
    else {
        const modes = getDerivationModesForCurrency(currency);
        derivationMode = modes[modes.length - 1];
        derivationPath = runDerivationScheme(getDerivationScheme({
            currency,
            derivationMode,
        }), currency);
    }
    return {
        appName,
        dependencies,
        requireLatestFirmware,
        requiresDerivation: {
            derivationMode,
            path: derivationPath,
            currencyId: currency.id,
            ...extra,
        },
        allowPartialDependencies,
    };
}
export let currentMode = "event";
export function setDeviceMode(mode) {
    currentMode = mode;
}
export const createAction = (connectAppExec) => {
    const useHook = (device, appRequest) => {
        const dependenciesResolvedRef = useRef(false);
        const firmwareResolvedRef = useRef(false);
        const outdatedAppRef = useRef();
        const request = useMemo(() => inferCommandParams(appRequest), // for now i don't have better
        // eslint-disable-next-line react-hooks/exhaustive-deps
        [
            appRequest.appName,
            appRequest.account?.id,
            appRequest.currency?.id,
            appRequest.dependencies,
        ]);
        const task = useCallback(({ deviceId, deviceName, request }) => {
            //To avoid redundant checks, we remove passed checks from the request.
            const { dependencies, requireLatestFirmware } = request;
            return connectAppExec({
                deviceId,
                deviceName,
                request: {
                    ...request,
                    dependencies: dependenciesResolvedRef.current ? undefined : dependencies,
                    requireLatestFirmware: firmwareResolvedRef.current ? undefined : requireLatestFirmware,
                    outdatedApp: outdatedAppRef.current,
                },
            }).pipe(tap(e => {
                // These events signal the resolution of pending checks.
                if (e.type === "dependencies-resolved") {
                    dependenciesResolvedRef.current = true;
                }
                else if (e.type === "latest-firmware-resolved") {
                    firmwareResolvedRef.current = true;
                }
                else if (e.type === "has-outdated-app") {
                    outdatedAppRef.current = e.outdatedApp;
                }
            }));
        }, []);
        // repair modal will interrupt everything and be rendered instead of the background content
        const [state, setState] = useState(() => getInitialState(device));
        const [resetIndex, setResetIndex] = useState(0);
        const deviceSubject = useReplaySubject(device);
        useEffect(() => {
            if (state.opened)
                return;
            const impl = getImplementation(currentMode)({
                deviceSubject,
                task,
                request,
            });
            const sub = impl
                .pipe(tap((e) => log("actions-app-event", e.type, e)), debounce((e) => ("replaceable" in e && e.replaceable ? interval(100) : of(null))), scan(reducer, getInitialState()), takeWhile((s) => !s.requiresAppInstallation && !s.error, true))
                .subscribe(setState);
            return () => {
                sub.unsubscribe();
            };
        }, [deviceSubject, state.opened, resetIndex, task, request]);
        const onRetry = useCallback(() => {
            // After an error we can't guarantee resolutions.
            dependenciesResolvedRef.current = false;
            firmwareResolvedRef.current = false;
            // The nonce change triggers a refresh.
            setResetIndex(i => i + 1);
            setState(getInitialState(device));
        }, [device]);
        const passWarning = useCallback(() => {
            setState(currState => ({
                ...currState,
                displayUpgradeWarning: false,
            }));
        }, []);
        return {
            ...state,
            inWrongDeviceForAccount: state.derivation && appRequest.account
                ? state.derivation.address !== appRequest.account.freshAddress &&
                    state.derivation.address !== appRequest.account.seedIdentifier // Use-case added for Hedera
                    ? {
                        accountName: getDefaultAccountName(appRequest.account),
                    }
                    : null
                : null,
            onRetry,
            passWarning,
        };
    };
    return {
        useHook,
        mapResult,
    };
};
export function dependenciesToAppRequests(dependencies) {
    if (!dependencies) {
        return [];
    }
    return dependencies.map(appName => ({ appName }));
}
//# sourceMappingURL=app.js.map