"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const errors_1 = require("@ledgerhq/errors");
const devices_1 = require("@ledgerhq/devices");
const pako_1 = require("pako");
const deviceAccess_1 = require("./deviceAccess");
const getDeviceInfo_1 = __importDefault(require("./getDeviceInfo"));
const customLockScreenFetchHash_1 = __importDefault(require("./customLockScreenFetchHash"));
const getAppAndVersion_1 = __importDefault(require("./getAppAndVersion"));
const isDashboardName_1 = require("./isDashboardName");
const attemptToQuitApp_1 = __importDefault(require("./attemptToQuitApp"));
const errors_2 = require("../errors");
const MAX_APDU_SIZE = 240;
function fetchImage({ deviceId, deviceName, request, }) {
    const { backupHash, allowedEmpty = false, deviceModelId } = request;
    const sub = (0, deviceAccess_1.withDevice)(deviceId, deviceName ? { matchDeviceByName: deviceName } : undefined)(transport => new rxjs_1.Observable(subscriber => {
        const timeoutSub = (0, rxjs_1.of)({
            type: "unresponsiveDevice",
        })
            .pipe((0, operators_1.delay)(1000))
            .subscribe(e => subscriber.next(e));
        const sub = (0, rxjs_1.from)((0, getDeviceInfo_1.default)(transport))
            .pipe((0, operators_1.mergeMap)(async () => {
            timeoutSub.unsubscribe();
            // Fetch the image hash from the device
            const imgHash = await (0, customLockScreenFetchHash_1.default)(transport);
            subscriber.next({ type: "currentImageHash", imgHash });
            // We don't have an image to backup
            if (imgHash === "") {
                if (allowedEmpty) {
                    subscriber.complete();
                    return;
                }
                else {
                    return subscriber.error(new errors_2.ImageDoesNotExistOnDevice(undefined, {
                        productName: (0, devices_1.getDeviceModel)(deviceModelId).productName,
                    }));
                }
            }
            else if (backupHash === imgHash) {
                subscriber.next({ type: "imageAlreadyBackedUp" });
                subscriber.complete();
                return;
            }
            // If we are here, either we didn't provide a backupHash or we are
            // not up to date with the device, in either case, continue to fetch
            const imageLengthResponse = await transport.send(0xe0, 0x64, 0x00, 0x00);
            const imageLengthStatus = imageLengthResponse.readUInt16BE(imageLengthResponse.length - 2);
            if (imageLengthStatus !== errors_1.StatusCodes.OK) {
                // this answer success even when no image is set, but the length of the image is 0
                return subscriber.error(new errors_1.TransportError("Unexpected device response", imageLengthStatus.toString(16)));
            }
            const imageLength = imageLengthResponse.readUInt32BE(0);
            if (imageLength === 0) {
                // It should never happen since we fetched the hash earlier but hey.
                return subscriber.error(new errors_2.ImageDoesNotExistOnDevice(undefined, {
                    productName: (0, devices_1.getDeviceModel)(deviceModelId).productName,
                }));
            }
            let imageBuffer = Buffer.from([]);
            let currentOffset = 0;
            while (currentOffset < imageLength) {
                subscriber.next({
                    type: "progress",
                    progress: (currentOffset + 1) / imageLength,
                });
                // 253 bytes for data, 2 for status
                const chunkSize = Math.min(MAX_APDU_SIZE - 2, imageLength - currentOffset);
                const chunkRequest = Buffer.alloc(5);
                chunkRequest.writeUInt32BE(currentOffset);
                chunkRequest.writeUInt8(chunkSize, 4);
                const imageChunk = await transport.send(0xe0, 0x65, 0x00, 0x00, chunkRequest);
                const chunkStatus = imageChunk.readUInt16BE(imageChunk.length - 2);
                if (chunkStatus !== errors_1.StatusCodes.OK) {
                    // TODO: map all proper errors
                    return subscriber.error(new errors_1.TransportError("Unexpected device response", chunkStatus.toString(16)));
                }
                imageBuffer = Buffer.concat([
                    imageBuffer,
                    imageChunk.slice(0, imageChunk.length - 2),
                ]);
                currentOffset += chunkSize;
            }
            const hexImage = await parseCustomLockScreenImageFormat(imageBuffer);
            subscriber.next({ type: "imageFetched", hexImage });
            subscriber.complete();
        }), (0, operators_1.catchError)((e) => {
            if (e instanceof errors_1.DeviceOnDashboardExpected ||
                (e &&
                    e instanceof errors_1.TransportStatusError &&
                    [0x6e00, 0x6d00, 0x6e01, 0x6d01, 0x6d02].includes(e.statusCode))) {
                return (0, rxjs_1.from)((0, getAppAndVersion_1.default)(transport)).pipe((0, operators_1.concatMap)(appAndVersion => {
                    return !(0, isDashboardName_1.isDashboardName)(appAndVersion.name)
                        ? (0, attemptToQuitApp_1.default)(transport, appAndVersion)
                        : (0, rxjs_1.of)({
                            type: "appDetected",
                        });
                }));
            }
            return (0, rxjs_1.throwError)(() => e);
        }))
            .subscribe(subscriber);
        return () => {
            timeoutSub.unsubscribe();
            sub.unsubscribe();
        };
    }));
    return sub;
}
exports.default = fetchImage;
// transforms from a Custom Lock Screen binary image format to an LLM hex string format
const parseCustomLockScreenImageFormat = async (staxImageBuffer) => {
    // const width = staxImageBuffer.readUint16LE(0); // always 400
    // const height = staxImageBuffer.readUint16LE(2); // always 672
    const bppCompressionByte = staxImageBuffer.readUInt8(4);
    // const bpp = bppCompressionByte >> 4; // always 2
    const compression = bppCompressionByte & 0x0f;
    const dataLengthBuffer = Buffer.from([
        staxImageBuffer.readUInt8(5),
        staxImageBuffer.readUInt8(6),
        staxImageBuffer.readUInt8(7),
        0x00,
    ]);
    const dataLength = dataLengthBuffer.readUInt32LE();
    const imageData = staxImageBuffer.slice(8);
    if (compression === 0) {
        return imageData.toString("hex");
    }
    let uncompressedImageData = Buffer.from([]);
    let offset = 0;
    while (offset < dataLength) {
        const currentChunkSize = imageData.readUInt16LE(offset);
        offset += 2;
        const chunk = imageData.slice(offset, offset + currentChunkSize);
        const uncompressedChunk = await (0, pako_1.ungzip)(chunk);
        uncompressedImageData = Buffer.concat([uncompressedImageData, uncompressedChunk]);
        offset += currentChunkSize;
    }
    return uncompressedImageData.toString("hex");
};
//# sourceMappingURL=customLockScreenFetch.js.map