"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateCustomLockScreenImageFormat = void 0;
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const errors_1 = require("@ledgerhq/errors");
const devices_1 = require("@ledgerhq/devices");
const getDeviceInfo_1 = __importDefault(require("./getDeviceInfo"));
const errors_2 = require("../errors");
const getAppAndVersion_1 = __importDefault(require("./getAppAndVersion"));
const isDashboardName_1 = require("./isDashboardName");
const attemptToQuitApp_1 = __importDefault(require("./attemptToQuitApp"));
const customLockScreenFetchSize_1 = __importDefault(require("./customLockScreenFetchSize"));
const customLockScreenFetchHash_1 = __importDefault(require("./customLockScreenFetchHash"));
const pako_1 = require("pako");
const screenSpecs_1 = require("../device/use-cases/screenSpecs");
const device_management_kit_1 = require("@ledgerhq/device-management-kit");
const core_1 = require("../deviceSDK/transports/core");
const MAX_APDU_SIZE = 255;
const COMPRESS_CHUNK_SIZE = 2048;
/**
 * Type guard to check if the given error is a DeviceDisconnectedWhileSendingError.
 * Ensures that the error is an object, is not null, and matches the expected structure.
 * This is used to identify specific disconnection errors from the DMK device.
 */
const isDmkDeviceDisconnectedError = (err) => typeof err === "object" &&
    err !== null &&
    (err instanceof device_management_kit_1.DeviceDisconnectedWhileSendingError ||
        ("_tag" in err && err._tag === "DeviceDisconnectedWhileSendingError"));
function loadImage({ deviceId, deviceName, request, }) {
    const { hexImage, padImage = true, deviceModelId } = request;
    const screenSpecs = (0, screenSpecs_1.getScreenSpecs)(deviceModelId);
    const sub = (0, core_1.withTransport)(deviceId, deviceName ? { matchDeviceByName: deviceName } : undefined)(({ transportRef }) => 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)(transportRef.current))
            .pipe((0, operators_1.mergeMap)(async () => {
            timeoutSub.unsubscribe();
            const imageData = await generateCustomLockScreenImageFormat(hexImage, true, !!padImage, screenSpecs);
            const imageLength = imageData.length;
            const imageSize = Buffer.alloc(4);
            imageSize.writeUIntBE(imageLength, 0, 4);
            subscriber.next({ type: "loadImagePermissionRequested" });
            const createImageResponse = await transportRef.current.send(0xe0, 0x60, 0x00, 0x00, imageSize, [errors_1.StatusCodes.NOT_ENOUGH_SPACE, errors_1.StatusCodes.USER_REFUSED_ON_DEVICE, errors_1.StatusCodes.OK]);
            const createImageStatus = createImageResponse.readUInt16BE(createImageResponse.length - 2);
            const createImageStatusStr = createImageStatus.toString(16);
            // reads last 2 bytes which correspond to the status
            if (createImageStatus === errors_1.StatusCodes.USER_REFUSED_ON_DEVICE) {
                return subscriber.error(new errors_2.ImageLoadRefusedOnDevice(createImageStatusStr, {
                    productName: (0, devices_1.getDeviceModel)(deviceModelId).productName,
                }));
            }
            else if (createImageStatus === errors_1.StatusCodes.NOT_ENOUGH_SPACE) {
                return subscriber.error(new errors_1.ManagerNotEnoughSpaceError());
            }
            else if (createImageStatus !== errors_1.StatusCodes.OK) {
                return subscriber.error(new errors_1.TransportError("Unexpected device response", createImageStatusStr));
            }
            let currentOffset = 0;
            // offset in number of charaters
            while (currentOffset < imageLength) {
                subscriber.next({
                    type: "progress",
                    progress: (currentOffset + 1) / imageLength,
                });
                // chunkSize in number of bytes
                const chunkSize = Math.min(MAX_APDU_SIZE - 4, imageLength - currentOffset);
                // we subtract 4 because the first 4 bytes of the data part of the apdu are used for
                // passing the offset of the chunk
                const chunkDataBuffer = imageData.slice(currentOffset, currentOffset + chunkSize);
                const chunkOffsetBuffer = Buffer.alloc(4);
                chunkOffsetBuffer.writeUIntBE(currentOffset, 0, 4);
                const apduData = Buffer.concat([chunkOffsetBuffer, chunkDataBuffer]);
                await transportRef.current.send(0xe0, 0x61, 0x00, 0x00, apduData);
                currentOffset += chunkSize;
            }
            subscriber.next({ type: "commitImagePermissionRequested" });
            const commitResponse = await transportRef.current.send(0xe0, 0x62, 0x00, 0x00, Buffer.from([]), [0x9000, 0x5501]);
            const commitStatus = commitResponse.readUInt16BE(commitResponse.length - 2);
            const commitStatusStr = commitStatus.toString(16);
            // reads last 2 bytes which correspond to the status
            if (commitStatus === 0x5501) {
                return subscriber.error(new errors_2.ImageCommitRefusedOnDevice(commitStatusStr, {
                    productName: (0, devices_1.getDeviceModel)(deviceModelId).productName,
                }));
            }
            else if (commitStatus !== 0x9000) {
                return subscriber.error(new errors_1.TransportError("Unexpected device response", commitStatusStr));
            }
            // Fetch image size
            const imageBytes = await (0, customLockScreenFetchSize_1.default)(transportRef.current);
            // Fetch image hash
            const imageHash = await (0, customLockScreenFetchHash_1.default)(transportRef.current);
            subscriber.next({
                type: "imageLoaded",
                imageSize: imageBytes,
                imageHash,
            });
            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)(transportRef.current)).pipe((0, operators_1.concatMap)(appAndVersion => {
                    return !(0, isDashboardName_1.isDashboardName)(appAndVersion.name)
                        ? (0, attemptToQuitApp_1.default)(transportRef.current, appAndVersion)
                        : (0, rxjs_1.of)({
                            type: "appDetected",
                        });
                }));
            }
            return (0, rxjs_1.throwError)(() => e);
        }))
            .subscribe(subscriber);
        return () => {
            timeoutSub.unsubscribe();
            sub.unsubscribe();
        };
    })).pipe((0, operators_1.catchError)(err => {
        if (err.name === "TimeoutError" || isDmkDeviceDisconnectedError(err)) {
            return (0, rxjs_1.throwError)(() => new errors_1.DisconnectedDevice());
        }
        return (0, rxjs_1.throwError)(() => err);
    }));
    return sub;
}
exports.default = loadImage;
function padHexImage(hexImage, screenSpecs) {
    // hexImage is a string that is a hex representation of the image data
    // each character is a pixel (between 0 and 15) and it starts from the top right
    // corner, goes down the column and then to the next column, until the bottom left
    // We need to pad the image on the edges to match the screen specs.
    const sourceWidth = screenSpecs.width - screenSpecs.paddingLeft - screenSpecs.paddingRight;
    const sourceHeight = screenSpecs.height - screenSpecs.paddingTop - screenSpecs.paddingBottom;
    const destHeight = screenSpecs.height;
    let result = "";
    // add right padding
    result += "0".repeat(screenSpecs.paddingRight * destHeight);
    // add the image data
    for (let columnIndex = 0; columnIndex < sourceWidth; columnIndex++) {
        const column = hexImage.slice(columnIndex * sourceHeight, (columnIndex + 1) * sourceHeight);
        const topPadding = "0".repeat(screenSpecs.paddingTop);
        const paddingBottom = "0".repeat(screenSpecs.paddingBottom);
        result += topPadding + column + paddingBottom;
    }
    // add left padding
    result += "0".repeat(screenSpecs.paddingLeft * destHeight);
    return result;
}
const bitsPerPixelToBppIndicator = {
    1: 0,
    4: 2,
};
async function generateCustomLockScreenImageFormat(hexImage, compressImage, padImage, screenSpecs) {
    const width = screenSpecs.width;
    const height = screenSpecs.height;
    const bpp = bitsPerPixelToBppIndicator[screenSpecs.bitsPerPixel];
    const compression = compressImage ? 1 : 0;
    const header = Buffer.alloc(8);
    header.writeUInt16LE(width, 0); // width
    header.writeUInt16LE(height, 2); // height
    header.writeUInt8((bpp << 4) | compression, 4);
    const paddedHexImage = padImage ? padHexImage(hexImage, screenSpecs) : hexImage;
    const imgData = Buffer.from(paddedHexImage, "hex");
    if (!compressImage) {
        const dataLength = imgData.length;
        header.writeUInt8(dataLength & 0xff, 5); // lowest byte
        header.writeUInt8((dataLength >> 8) & 0xff, 6); // middle byte
        header.writeUInt8((dataLength >> 16) & 0xff, 7); // biggest byte
        return Buffer.concat([header, imgData]);
    }
    const chunkedImgData = [];
    for (let i = 0; i < imgData.length; i += COMPRESS_CHUNK_SIZE) {
        chunkedImgData.push(imgData.slice(i, i + COMPRESS_CHUNK_SIZE));
    }
    const compressedChunkedImgData = await Promise.all(chunkedImgData.map(async (chunk) => {
        const compressedChunk = await (0, pako_1.gzip)(chunk);
        const compressedChunkSize = Buffer.alloc(2);
        compressedChunkSize.writeUInt16LE(compressedChunk.length);
        return Buffer.concat([compressedChunkSize, compressedChunk]);
    }));
    const compressedData = Buffer.concat(compressedChunkedImgData);
    const dataLength = compressedData.length;
    header.writeUInt8(dataLength & 0xff, 5); // lowest byte
    header.writeUInt8((dataLength >> 8) & 0xff, 6); // middle byte
    header.writeUInt8((dataLength >> 16) & 0xff, 7); // biggest byte
    return Buffer.concat([header, Buffer.concat(compressedChunkedImgData)]);
}
exports.generateCustomLockScreenImageFormat = generateCustomLockScreenImageFormat;
//# sourceMappingURL=customLockScreenLoad.js.map