"use strict";

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.SevenZipDifferentialDownloader = exports.DifferentialDownloader = exports.DifferentialDownloaderOptions = undefined;

var _bluebirdLst;

function _load_bluebirdLst() {
    return _bluebirdLst = require("bluebird-lst");
}

var _bluebirdLst2;

function _load_bluebirdLst2() {
    return _bluebirdLst2 = _interopRequireDefault(require("bluebird-lst"));
}

let readBlockMap = (() => {
    var _ref = (0, (_bluebirdLst || _load_bluebirdLst()).coroutine)(function* (data) {
        return JSON.parse((yield inflateRaw(data)).toString());
    });

    return function readBlockMap(_x) {
        return _ref.apply(this, arguments);
    };
})();

var _builderUtilRuntime;

function _load_builderUtilRuntime() {
    return _builderUtilRuntime = require("builder-util-runtime");
}

var _blockMapApi;

function _load_blockMapApi() {
    return _blockMapApi = require("builder-util-runtime/out/blockMapApi");
}

var _fsExtraP;

function _load_fsExtraP() {
    return _fsExtraP = require("fs-extra-p");
}

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

const inflateRaw = (_bluebirdLst2 || _load_bluebirdLst2()).default.promisify(require("zlib").inflateRaw);
class DifferentialDownloaderOptions {}
exports.DifferentialDownloaderOptions = DifferentialDownloaderOptions;
function buildChecksumMap(file, fileOffset) {
    const checksumToOffset = new Map();
    const checksumToSize = new Map();
    let offset = fileOffset;
    for (let i = 0; i < file.checksums.length; i++) {
        const checksum = file.checksums[i];
        const size = file.sizes[i];
        checksumToOffset.set(checksum, offset);
        checksumToSize.set(checksum, size);
        offset += size;
    }
    return { checksumToOffset, checksumToOldSize: checksumToSize };
}
class DifferentialDownloader {
    constructor(blockAwareFileInfo, httpExecutor, options) {
        this.blockAwareFileInfo = blockAwareFileInfo;
        this.httpExecutor = httpExecutor;
        this.options = options;
        this.logger = options.logger;
        this.baseRequestOptions = (0, (_builderUtilRuntime || _load_builderUtilRuntime()).configureRequestOptionsFromUrl)(options.newUrl, {});
    }
    get signatureSize() {
        return 0;
    }
    createRequestOptions(method = "get") {
        return Object.assign({}, this.baseRequestOptions, { method, headers: Object.assign({}, this.options.requestHeaders, { Accept: "*/*" }) });
    }
    downloadNsisPackage(oldBlockMapFile) {
        var _this = this;

        return (0, (_bluebirdLst || _load_bluebirdLst()).coroutine)(function* () {
            const packageInfo = _this.blockAwareFileInfo;
            const offset = packageInfo.size - packageInfo.headerSize - packageInfo.blockMapSize;
            _this.fileMetadataBuffer = yield _this.readRemoteBytes(offset, packageInfo.size - 1);
            const newBlockMap = yield readBlockMap(_this.fileMetadataBuffer.slice(packageInfo.headerSize));
            const oldBlockMap = yield (0, (_fsExtraP || _load_fsExtraP()).readJson)(oldBlockMapFile);
            yield _this.download(oldBlockMap, newBlockMap);
        })();
    }
    downloadAppImage(oldBlockMap) {
        var _this2 = this;

        return (0, (_bluebirdLst || _load_bluebirdLst()).coroutine)(function* () {
            const packageInfo = _this2.blockAwareFileInfo;
            const fileSize = packageInfo.size;
            const offset = fileSize - (packageInfo.blockMapSize + 4);
            _this2.fileMetadataBuffer = yield _this2.readRemoteBytes(offset, fileSize - 1);
            const newBlockMap = yield readBlockMap(_this2.fileMetadataBuffer.slice(0, _this2.fileMetadataBuffer.length - 4));
            yield _this2.download(oldBlockMap, newBlockMap);
        })();
    }
    download(oldBlockMap, newBlockMap) {
        var _this3 = this;

        return (0, (_bluebirdLst || _load_bluebirdLst()).coroutine)(function* () {
            // we don't check other metadata like compressionMethod - generic check that it is make sense to differentially update is suitable for it
            if (oldBlockMap.version !== newBlockMap.version) {
                throw new Error(`version is different (${oldBlockMap.version} - ${newBlockMap.version}), full download is required`);
            }
            const operations = _this3.computeOperations(oldBlockMap, newBlockMap);
            if (_this3.logger.debug != null) {
                _this3.logger.debug(JSON.stringify(operations, null, 2));
            }
            let downloadSize = 0;
            let copySize = 0;
            for (const operation of operations) {
                const length = operation.end - operation.start;
                if (operation.kind === OperationKind.DOWNLOAD) {
                    downloadSize += length;
                } else {
                    copySize += length;
                }
            }
            const newPackageSize = _this3.blockAwareFileInfo.size;
            if (downloadSize + copySize + _this3.fileMetadataBuffer.length + _this3.signatureSize !== newPackageSize) {
                throw new Error(`Internal error, size mismatch: downloadSize: ${downloadSize}, copySize: ${copySize}, newPackageSize: ${newPackageSize}`);
            }
            _this3.logger.info(`Full: ${formatBytes(newPackageSize)}, To download: ${formatBytes(downloadSize)} (${Math.round(downloadSize / (newPackageSize / 100))}%)`);
            yield _this3.downloadFile(operations);
        })();
    }
    downloadFile(operations) {
        var _this4 = this;

        return (0, (_bluebirdLst || _load_bluebirdLst()).coroutine)(function* () {
            // todo we can avoid download remote and construct manually
            const signature = _this4.signatureSize === 0 ? null : yield _this4.readRemoteBytes(0, _this4.signatureSize - 1);
            const oldFileFd = yield (0, (_fsExtraP || _load_fsExtraP()).open)(_this4.options.oldPackageFile, "r");
            yield new (_bluebirdLst2 || _load_bluebirdLst2()).default(function (resolve, reject) {
                const streams = [];
                const digestTransform = new (_builderUtilRuntime || _load_builderUtilRuntime()).DigestTransform(_this4.blockAwareFileInfo.sha512);
                // to simply debug, do manual validation to allow file to be fully written
                digestTransform.isValidateOnEnd = false;
                streams.push(digestTransform);
                const fileOut = (0, (_fsExtraP || _load_fsExtraP()).createWriteStream)(_this4.options.newFile);
                fileOut.on("finish", function () {
                    fileOut.close(function () {
                        try {
                            digestTransform.validate();
                        } catch (e) {
                            reject(e);
                            return;
                        }
                        resolve();
                    });
                });
                streams.push(fileOut);
                let lastStream = null;
                for (const stream of streams) {
                    stream.on("error", reject);
                    if (lastStream == null) {
                        lastStream = stream;
                    } else {
                        lastStream = lastStream.pipe(stream);
                    }
                }
                const firstStream = streams[0];
                const w = function (index) {
                    if (index >= operations.length) {
                        firstStream.end(_this4.fileMetadataBuffer);
                        return;
                    }
                    const operation = operations[index++];
                    if (operation.kind === OperationKind.COPY) {
                        const readStream = (0, (_fsExtraP || _load_fsExtraP()).createReadStream)(_this4.options.oldPackageFile, {
                            fd: oldFileFd,
                            autoClose: false,
                            start: operation.start,
                            // end is inclusive
                            end: operation.end - 1
                        });
                        readStream.on("error", reject);
                        readStream.once("end", function () {
                            return w(index);
                        });
                        readStream.pipe(firstStream, {
                            end: false
                        });
                    } else {
                        // https://github.com/electron-userland/electron-builder/issues/1523#issuecomment-327084661
                        // todo to reduce http requests we need to consolidate non sequential download operations (Multipart ranges)
                        const requestOptions = _this4.createRequestOptions("get");
                        requestOptions.headers.Range = `bytes=${operation.start}-${operation.end - 1}`;
                        const request = _this4.httpExecutor.doRequest(requestOptions, function (response) {
                            // Electron net handles redirects automatically, our NodeJS test server doesn't use redirects - so, we don't check 3xx codes.
                            if (response.statusCode >= 400) {
                                reject(new (_builderUtilRuntime || _load_builderUtilRuntime()).HttpError(response));
                            }
                            response.pipe(firstStream, {
                                end: false
                            });
                            response.once("end", function () {
                                return w(index);
                            });
                        });
                        _this4.httpExecutor.addErrorAndTimeoutHandlers(request, reject);
                        request.end();
                    }
                };
                if (signature == null) {
                    w(0);
                } else {
                    firstStream.write(signature, function () {
                        return w(0);
                    });
                }
            }).finally(function () {
                return (0, (_fsExtraP || _load_fsExtraP()).close)(oldFileFd);
            });
        })();
    }
    computeOperations(oldBlockMap, newBlockMap) {
        const nameToOldBlocks = buildBlockFileMap(oldBlockMap.files);
        const nameToNewBlocks = buildBlockFileMap(newBlockMap.files);
        const oldEntryMap = buildEntryMap(oldBlockMap.files);
        let lastOperation = null;
        const operations = [];
        for (const blockMapFile of newBlockMap.files) {
            const name = blockMapFile.name;
            const oldEntry = oldEntryMap.get(name);
            if (oldEntry == null) {
                // new file
                operations.push({
                    kind: OperationKind.DOWNLOAD,
                    start: blockMapFile.offset,
                    end: blockMapFile.offset + blockMapFile.sizes.reduce((accumulator, currentValue) => accumulator + currentValue)
                });
                continue;
            }
            const newFile = nameToNewBlocks.get(name);
            let changedBlockCount = 0;
            const { checksumToOffset: checksumToOldOffset, checksumToOldSize } = buildChecksumMap(nameToOldBlocks.get(name), oldEntry.offset);
            let newOffset = blockMapFile.offset;
            for (let i = 0; i < newFile.checksums.length; newOffset += newFile.sizes[i], i++) {
                const blockSize = newFile.sizes[i];
                const checksum = newFile.checksums[i];
                let oldOffset = checksumToOldOffset.get(checksum);
                if (oldOffset != null && checksumToOldSize.get(checksum) !== blockSize) {
                    this.logger.warn(`Checksum ("${checksum}") matches, but size differs (old: ${checksumToOldSize.get(checksum)}, new: ${blockSize})`);
                    oldOffset = null;
                }
                if (oldOffset == null) {
                    changedBlockCount++;
                    if (lastOperation == null || lastOperation.kind !== OperationKind.DOWNLOAD || lastOperation.end !== newOffset) {
                        lastOperation = {
                            kind: OperationKind.DOWNLOAD,
                            start: newOffset,
                            end: newOffset + blockSize
                        };
                        operations.push(lastOperation);
                    } else {
                        lastOperation.end += blockSize;
                    }
                } else if (lastOperation == null || lastOperation.kind !== OperationKind.COPY || lastOperation.end !== oldOffset) {
                    lastOperation = {
                        kind: OperationKind.COPY,
                        start: oldOffset,
                        end: oldOffset + blockSize
                    };
                    operations.push(lastOperation);
                } else {
                    lastOperation.end += blockSize;
                }
            }
            if (changedBlockCount > 0) {
                this.logger.info(`File${blockMapFile.name === "file" ? "" : " " + blockMapFile.name} has ${changedBlockCount} changed blocks`);
            }
        }
        return operations;
    }
    readRemoteBytes(start, endInclusive) {
        var _this5 = this;

        return (0, (_bluebirdLst || _load_bluebirdLst()).coroutine)(function* () {
            const buffer = Buffer.allocUnsafe(endInclusive + 1 - start);
            const requestOptions = _this5.createRequestOptions();
            requestOptions.headers.Range = `bytes=${start}-${endInclusive}`;
            let position = 0;
            yield _this5.request(requestOptions, function (chunk) {
                chunk.copy(buffer, position);
                position += chunk.length;
            });
            return buffer;
        })();
    }
    request(requestOptions, dataHandler) {
        return new (_bluebirdLst2 || _load_bluebirdLst2()).default((resolve, reject) => {
            const request = this.httpExecutor.doRequest(requestOptions, response => {
                // Electron net handles redirects automatically, our NodeJS test server doesn't use redirects - so, we don't check 3xx codes.
                if (response.statusCode >= 400) {
                    reject(new (_builderUtilRuntime || _load_builderUtilRuntime()).HttpError(response));
                }
                if (response.statusCode !== 206) {
                    const acceptRanges = (0, (_builderUtilRuntime || _load_builderUtilRuntime()).safeGetHeader)(response, "accept-ranges");
                    if (acceptRanges == null || acceptRanges === "none") {
                        reject(new Error("Server doesn't support Accept-Ranges"));
                    }
                }
                response.on("data", dataHandler);
                response.on("end", () => {
                    resolve();
                });
            });
            this.httpExecutor.addErrorAndTimeoutHandlers(request, reject);
            request.end();
        });
    }
}
exports.DifferentialDownloader = DifferentialDownloader;
class SevenZipDifferentialDownloader extends DifferentialDownloader {
    constructor(packageInfo, httpExecutor, options) {
        super(packageInfo, httpExecutor, options);
    }
    get signatureSize() {
        return (_blockMapApi || _load_blockMapApi()).SIGNATURE_HEADER_SIZE;
    }
}
exports.SevenZipDifferentialDownloader = SevenZipDifferentialDownloader;
var OperationKind;
(function (OperationKind) {
    OperationKind[OperationKind["COPY"] = 0] = "COPY";
    OperationKind[OperationKind["DOWNLOAD"] = 1] = "DOWNLOAD";
})(OperationKind || (OperationKind = {}));
function buildEntryMap(list) {
    const result = new Map();
    for (const item of list) {
        result.set(item.name, item);
    }
    return result;
}
function buildBlockFileMap(list) {
    const result = new Map();
    for (const item of list) {
        result.set(item.name, item);
    }
    return result;
}

function formatBytes(value, symbol = " KB") {
    return new Intl.NumberFormat("en").format((value / 1024).toFixed(2)) + symbol;
}
//# sourceMappingURL=differentialPackage.js.map