"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __asyncValues = (this && this.__asyncValues) || function (o) {
    if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
    var m = o[Symbol.asyncIterator], i;
    return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
    function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
    function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GenericDownloadIter = exports.DirectDownloadIter = void 0;
exports.iterDownload = iterDownload;
exports.downloadFileV2 = downloadFileV2;
exports.downloadMedia = downloadMedia;
exports._downloadDocument = _downloadDocument;
exports._downloadContact = _downloadContact;
exports._downloadWebDocument = _downloadWebDocument;
exports._downloadCachedPhotoSize = _downloadCachedPhotoSize;
exports._downloadPhoto = _downloadPhoto;
exports.downloadProfilePhoto = downloadProfilePhoto;
const tl_1 = require("../tl");
const Utils_1 = require("../Utils");
const Helpers_1 = require("../Helpers");
const __1 = require("../");
const requestIter_1 = require("../requestIter");
const errors_1 = require("../errors");
const fs_1 = require("./fs");
const extensions_1 = require("../extensions");
const fs = __importStar(require("./fs"));
const path_1 = __importDefault(require("./path"));
const big_integer_1 = __importDefault(require("big-integer"));
// All types
const sizeTypes = ["w", "y", "d", "x", "c", "m", "b", "a", "s"];
// Chunk sizes for `upload.getFile` must be multiple of the smallest size
const MIN_CHUNK_SIZE = 4096;
const DEFAULT_CHUNK_SIZE = 64; // kb
const ONE_MB = 1024 * 1024;
const REQUEST_TIMEOUT = 15000;
const DISCONNECT_SLEEP = 1000;
const TIMED_OUT_SLEEP = 1000;
const MAX_CHUNK_SIZE = 512 * 1024;
class DirectDownloadIter extends requestIter_1.RequestIter {
    constructor() {
        super(...arguments);
        this._timedOut = false;
    }
    async _init({ fileLocation, dcId, offset, stride, chunkSize, requestSize, fileSize, msgData, }) {
        this.request = new tl_1.Api.upload.GetFile({
            location: fileLocation,
            offset,
            limit: requestSize,
        });
        this.total = fileSize;
        this._stride = stride;
        this._chunkSize = chunkSize;
        this._lastPart = undefined;
        //this._msgData = msgData;
        this._timedOut = false;
        this._sender = await this.client.getSender(dcId);
    }
    async _loadNextChunk() {
        const current = await this._request();
        this.buffer.push(current);
        if (current.length < this.request.limit) {
            // we finished downloading
            this.left = this.buffer.length;
            await this.close();
            return true;
        }
        else {
            this.request.offset = this.request.offset.add(this._stride);
        }
    }
    async _request() {
        try {
            this._sender = await this.client.getSender(this._sender.dcId);
            const result = await this.client.invokeWithSender(this.request, this._sender);
            this._timedOut = false;
            if (result instanceof tl_1.Api.upload.FileCdnRedirect) {
                throw new Error("CDN Not supported. Please Add an issue in github");
            }
            return result.bytes;
        }
        catch (e) {
            if (e.errorMessage == "TIMEOUT") {
                if (this._timedOut) {
                    this.client._log.warn("Got two timeouts in a row while downloading file");
                    throw e;
                }
                this._timedOut = true;
                this.client._log.info("Got timeout while downloading file, retrying once");
                await (0, Helpers_1.sleep)(TIMED_OUT_SLEEP);
                return await this._request();
            }
            else if (e instanceof errors_1.FileMigrateError) {
                this.client._log.info("File lives in another DC");
                this._sender = await this.client.getSender(e.newDc);
                return await this._request();
            }
            else if (e.errorMessage == "FILEREF_UPGRADE_NEEDED") {
                // TODO later
                throw e;
            }
            else {
                throw e;
            }
        }
    }
    async close() {
        this.client._log.debug("Finished downloading file ...");
    }
    [Symbol.asyncIterator]() {
        return super[Symbol.asyncIterator]();
    }
}
exports.DirectDownloadIter = DirectDownloadIter;
class GenericDownloadIter extends DirectDownloadIter {
    async _loadNextChunk() {
        // 1. Fetch enough for one chunk
        let data = Buffer.alloc(0);
        //  1.1. ``bad`` is how much into the data we have we need to offset
        const bad = this.request.offset.mod(this.request.limit).toJSNumber();
        const before = this.request.offset;
        // 1.2. We have to fetch from a valid offset, so remove that bad part
        this.request.offset = this.request.offset.subtract(bad);
        let done = false;
        while (!done && data.length - bad < this._chunkSize) {
            const current = await this._request();
            this.request.offset = this.request.offset.add(this.request.limit);
            data = Buffer.concat([data, current]);
            done = current.length < this.request.limit;
        }
        // 1.3 Restore our last desired offset
        this.request.offset = before;
        // 2. Fill the buffer with the data we have
        // 2.1. The current chunk starts at ``bad`` offset into the data,
        //  and each new chunk is ``stride`` bytes apart of the other
        for (let i = bad; i < data.length; i += this._stride) {
            this.buffer.push(data.slice(i, i + this._chunkSize));
            // 2.2. We will yield this offset, so move to the next one
            this.request.offset = this.request.offset.add(this._stride);
        }
        // 2.3. If we are in the last chunk, we will return the last partial data
        if (done) {
            this.left = this.buffer.length;
            await this.close();
            return;
        }
        // 2.4 If we are not done, we can't return incomplete chunks.
        if (this.buffer[this.buffer.length - 1].length != this._chunkSize) {
            this._lastPart = this.buffer.pop();
            //   3. Be careful with the offsets. Re-fetching a bit of data
            //   is fine, since it greatly simplifies things.
            // TODO Try to not re-fetch data
            this.request.offset = this.request.offset.subtract(this._stride);
        }
    }
}
exports.GenericDownloadIter = GenericDownloadIter;
/** @hidden */
function iterDownload(client, { file, offset = big_integer_1.default.zero, stride, limit, chunkSize, requestSize = MAX_CHUNK_SIZE, fileSize, dcId, msgData, }) {
    // we're ignoring here to make it more flexible (which is probably a bad idea)
    // @ts-ignore
    const info = __1.utils.getFileInfo(file);
    if (info.dcId != undefined) {
        dcId = info.dcId;
    }
    if (fileSize == undefined) {
        fileSize = info.size;
    }
    file = info.location;
    if (chunkSize == undefined) {
        chunkSize = requestSize;
    }
    if (limit == undefined && fileSize != undefined) {
        limit = Math.floor(fileSize.add(chunkSize).subtract(1).divide(chunkSize).toJSNumber());
    }
    if (stride == undefined) {
        stride = chunkSize;
    }
    else if (stride < chunkSize) {
        throw new Error("Stride must be >= chunkSize");
    }
    requestSize -= requestSize % MIN_CHUNK_SIZE;
    if (requestSize < MIN_CHUNK_SIZE) {
        requestSize = MIN_CHUNK_SIZE;
    }
    else if (requestSize > MAX_CHUNK_SIZE) {
        requestSize = MAX_CHUNK_SIZE;
    }
    let cls;
    if (chunkSize == requestSize &&
        offset.divide(MAX_CHUNK_SIZE).eq(big_integer_1.default.zero) &&
        stride % MIN_CHUNK_SIZE == 0 &&
        (limit == undefined || offset.divide(limit).eq(big_integer_1.default.zero))) {
        cls = DirectDownloadIter;
        client._log.info(`Starting direct file download in chunks of ${requestSize} at ${offset}, stride ${stride}`);
    }
    else {
        cls = GenericDownloadIter;
        client._log.info(`Starting indirect file download in chunks of ${requestSize} at ${offset}, stride ${stride}`);
    }
    return new cls(client, limit, {}, {
        fileLocation: file,
        dcId,
        offset,
        stride,
        chunkSize,
        requestSize,
        fileSize,
        msgData,
    });
}
function getWriter(outputFile) {
    if (!outputFile || Buffer.isBuffer(outputFile)) {
        return new extensions_1.BinaryWriter(Buffer.alloc(0));
    }
    else if (typeof outputFile == "string") {
        // We want to make sure that the path exists.
        return (0, fs_1.createWriteStream)(outputFile);
    }
    else {
        return outputFile;
    }
}
function closeWriter(writer) {
    if ("close" in writer && writer.close) {
        writer.close();
    }
}
function returnWriterValue(writer) {
    if (writer instanceof extensions_1.BinaryWriter) {
        return writer.getValue();
    }
    if (writer instanceof fs.WriteStream) {
        if (typeof writer.path == "string") {
            return path_1.default.resolve(writer.path);
        }
        else {
            return Buffer.from(writer.path);
        }
    }
}
/** @hidden */
async function downloadFileV2(client, inputLocation, { outputFile = undefined, partSizeKb = undefined, fileSize = undefined, progressCallback = undefined, dcId = undefined, msgData = undefined, }) {
    var _a, e_1, _b, _c;
    if (!partSizeKb) {
        if (!fileSize) {
            partSizeKb = 64;
        }
        else {
            partSizeKb = __1.utils.getAppropriatedPartSize(fileSize);
        }
    }
    const partSize = Math.floor(partSizeKb * 1024);
    if (partSize % MIN_CHUNK_SIZE != 0) {
        throw new Error("The part size must be evenly divisible by 4096");
    }
    const writer = getWriter(outputFile);
    let downloaded = big_integer_1.default.zero;
    try {
        try {
            for (var _d = true, _e = __asyncValues(iterDownload(client, {
                file: inputLocation,
                requestSize: partSize,
                dcId: dcId,
                msgData: msgData,
            })), _f; _f = await _e.next(), _a = _f.done, !_a; _d = true) {
                _c = _f.value;
                _d = false;
                const chunk = _c;
                await writer.write(chunk);
                downloaded = downloaded.add(chunk.length);
                if (progressCallback) {
                    await progressCallback(downloaded, (0, big_integer_1.default)(fileSize || big_integer_1.default.zero));
                }
            }
        }
        catch (e_1_1) { e_1 = { error: e_1_1 }; }
        finally {
            try {
                if (!_d && !_a && (_b = _e.return)) await _b.call(_e);
            }
            finally { if (e_1) throw e_1.error; }
        }
        return returnWriterValue(writer);
    }
    finally {
        closeWriter(writer);
    }
}
class Foreman {
    constructor(maxWorkers) {
        this.maxWorkers = maxWorkers;
        this.activeWorkers = 0;
    }
    requestWorker() {
        this.activeWorkers++;
        if (this.activeWorkers > this.maxWorkers) {
            this.deferred = createDeferred();
            return this.deferred.promise;
        }
        return Promise.resolve();
    }
    releaseWorker() {
        this.activeWorkers--;
        if (this.deferred && this.activeWorkers <= this.maxWorkers) {
            this.deferred.resolve();
        }
    }
}
function createDeferred() {
    let resolve;
    const promise = new Promise((_resolve) => {
        resolve = _resolve;
    });
    return {
        promise,
        resolve: resolve,
    };
}
/** @hidden */
async function downloadMedia(client, messageOrMedia, outputFile, thumb, progressCallback) {
    /*
      Downloading large documents may be slow enough to require a new file reference
      to be obtained mid-download. Store (input chat, message id) so that the message
      can be re-fetched.
     */
    let msgData;
    let date;
    let media;
    if (messageOrMedia instanceof tl_1.Api.Message) {
        media = messageOrMedia.media;
        date = messageOrMedia.date;
        msgData = messageOrMedia.inputChat
            ? [messageOrMedia.inputChat, messageOrMedia.id]
            : undefined;
    }
    else {
        media = messageOrMedia;
        date = Date.now();
    }
    if (typeof media == "string") {
        throw new Error("not implemented");
    }
    if (media instanceof tl_1.Api.MessageMediaWebPage) {
        if (media.webpage instanceof tl_1.Api.WebPage) {
            media = media.webpage.document || media.webpage.photo;
        }
    }
    if (media instanceof tl_1.Api.MessageMediaPhoto || media instanceof tl_1.Api.Photo) {
        return _downloadPhoto(client, media, outputFile, date, thumb, progressCallback);
    }
    else if (media instanceof tl_1.Api.MessageMediaDocument ||
        media instanceof tl_1.Api.Document) {
        return _downloadDocument(client, media, outputFile, date, thumb, progressCallback, msgData);
    }
    else if (media instanceof tl_1.Api.MessageMediaContact) {
        return _downloadContact(client, media, {});
    }
    else if (media instanceof tl_1.Api.WebDocument ||
        media instanceof tl_1.Api.WebDocumentNoProxy) {
        return _downloadWebDocument(client, media, {});
    }
    else {
        return Buffer.alloc(0);
    }
}
/** @hidden */
async function _downloadDocument(client, doc, outputFile, date, thumb, progressCallback, msgData) {
    if (doc instanceof tl_1.Api.MessageMediaDocument) {
        if (!doc.document) {
            return Buffer.alloc(0);
        }
        doc = doc.document;
    }
    if (!(doc instanceof tl_1.Api.Document)) {
        return Buffer.alloc(0);
    }
    let size;
    if (thumb == undefined) {
        outputFile = getProperFilename(outputFile, "document", "." + (__1.utils.getExtension(doc) || "bin"), date);
    }
    else {
        outputFile = getProperFilename(outputFile, "photo", ".jpg", date);
        size = getThumb(doc.thumbs || [], thumb);
        if (size instanceof tl_1.Api.PhotoCachedSize ||
            size instanceof tl_1.Api.PhotoStrippedSize) {
            return _downloadCachedPhotoSize(size, outputFile);
        }
    }
    return await downloadFileV2(client, new tl_1.Api.InputDocumentFileLocation({
        id: doc.id,
        accessHash: doc.accessHash,
        fileReference: doc.fileReference,
        thumbSize: size && "type" in size ? size.type : "",
    }), {
        outputFile: outputFile,
        fileSize: size && "size" in size ? (0, big_integer_1.default)(size.size) : doc.size,
        progressCallback: progressCallback,
        msgData: msgData,
    });
}
/** @hidden */
async function _downloadContact(client, media, args) {
    throw new Error("not implemented");
}
/** @hidden */
async function _downloadWebDocument(client, media, args) {
    throw new Error("not implemented");
}
function pickFileSize(sizes, sizeType) {
    if (!sizeType || !sizes || !sizes.length) {
        return undefined;
    }
    const indexOfSize = sizeTypes.indexOf(sizeType);
    let size;
    for (let i = indexOfSize; i < sizeTypes.length; i++) {
        size = sizes.find((s) => s.type === sizeTypes[i]);
        if (size && !(size instanceof tl_1.Api.PhotoPathSize)) {
            return size;
        }
    }
    return undefined;
}
/** @hidden */
function getThumb(thumbs, thumb) {
    function sortThumb(thumb) {
        if (thumb instanceof tl_1.Api.PhotoStrippedSize) {
            return thumb.bytes.length;
        }
        if (thumb instanceof tl_1.Api.PhotoCachedSize) {
            return thumb.bytes.length;
        }
        if (thumb instanceof tl_1.Api.PhotoSize) {
            return thumb.size;
        }
        if (thumb instanceof tl_1.Api.PhotoSizeProgressive) {
            return Math.max(...thumb.sizes);
        }
        if (thumb instanceof tl_1.Api.VideoSize) {
            return thumb.size;
        }
        return 0;
    }
    thumbs = thumbs.sort((a, b) => sortThumb(a) - sortThumb(b));
    const correctThumbs = [];
    for (const t of thumbs) {
        if (!(t instanceof tl_1.Api.PhotoPathSize)) {
            correctThumbs.push(t);
        }
    }
    if (thumb == undefined) {
        return correctThumbs.pop();
    }
    else if (typeof thumb == "number") {
        return correctThumbs[thumb];
    }
    else if (typeof thumb == "string") {
        for (const t of correctThumbs) {
            if ("type" in t && t.type == thumb) {
                return t;
            }
        }
    }
    else if (thumb instanceof tl_1.Api.PhotoSize ||
        thumb instanceof tl_1.Api.PhotoCachedSize ||
        thumb instanceof tl_1.Api.PhotoStrippedSize ||
        thumb instanceof tl_1.Api.VideoSize) {
        return thumb;
    }
}
/** @hidden */
async function _downloadCachedPhotoSize(size, outputFile) {
    // No need to download anything, simply write the bytes
    let data;
    if (size instanceof tl_1.Api.PhotoStrippedSize) {
        data = (0, Utils_1.strippedPhotoToJpg)(size.bytes);
    }
    else {
        data = size.bytes;
    }
    const writer = getWriter(outputFile);
    try {
        await writer.write(data);
    }
    finally {
        closeWriter(writer);
    }
    return returnWriterValue(writer);
}
/** @hidden */
function getProperFilename(file, fileType, extension, date) {
    if (!file || typeof file != "string") {
        return file;
    }
    if (fs.existsSync(file) && fs.lstatSync(file).isDirectory()) {
        let fullName = fileType + date + extension;
        return path_1.default.join(file, fullName);
    }
    return file;
}
/** @hidden */
async function _downloadPhoto(client, photo, file, date, thumb, progressCallback) {
    if (photo instanceof tl_1.Api.MessageMediaPhoto) {
        if (photo.photo instanceof tl_1.Api.PhotoEmpty || !photo.photo) {
            return Buffer.alloc(0);
        }
        photo = photo.photo;
    }
    if (!(photo instanceof tl_1.Api.Photo)) {
        return Buffer.alloc(0);
    }
    const photoSizes = [...(photo.sizes || []), ...(photo.videoSizes || [])];
    const size = getThumb(photoSizes, thumb);
    if (!size || size instanceof tl_1.Api.PhotoSizeEmpty) {
        return Buffer.alloc(0);
    }
    if (!date) {
        date = Date.now();
    }
    file = getProperFilename(file, "photo", ".jpg", date);
    if (size instanceof tl_1.Api.PhotoCachedSize ||
        size instanceof tl_1.Api.PhotoStrippedSize) {
        return _downloadCachedPhotoSize(size, file);
    }
    let fileSize;
    if (size instanceof tl_1.Api.PhotoSizeProgressive) {
        fileSize = Math.max(...size.sizes);
    }
    else {
        fileSize = "size" in size ? size.size : 512;
    }
    return downloadFileV2(client, new tl_1.Api.InputPhotoFileLocation({
        id: photo.id,
        accessHash: photo.accessHash,
        fileReference: photo.fileReference,
        thumbSize: "type" in size ? size.type : "",
    }), {
        outputFile: file,
        fileSize: (0, big_integer_1.default)(fileSize),
        progressCallback: progressCallback,
        dcId: photo.dcId,
    });
}
/** @hidden */
async function downloadProfilePhoto(client, entity, fileParams) {
    let photo;
    if (typeof entity == "object" && "photo" in entity) {
        photo = entity.photo;
    }
    else {
        entity = await client.getEntity(entity);
        if ("photo" in entity) {
            photo = entity.photo;
        }
        else {
            throw new Error(`Could not get photo from ${entity ? entity.className : undefined}`);
        }
    }
    let dcId;
    let loc;
    if (photo instanceof tl_1.Api.UserProfilePhoto ||
        photo instanceof tl_1.Api.ChatPhoto) {
        dcId = photo.dcId;
        loc = new tl_1.Api.InputPeerPhotoFileLocation({
            peer: __1.utils.getInputPeer(entity),
            photoId: photo.photoId,
            big: fileParams.isBig,
        });
    }
    else {
        return Buffer.alloc(0);
    }
    return client.downloadFile(loc, {
        outputFile: fileParams.outputFile,
        dcId,
    });
}
