"use strict";
/**
 * @fileoverview Iterator for paged responses
 */
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 () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const qs = __importStar(require("querystring"));
const bluebird_1 = require("bluebird");
const promise_queue_1 = __importDefault(require("promise-queue"));
// -----------------------------------------------------------------------------
// Typedefs
// -----------------------------------------------------------------------------
/**
 * The iterator response object
 * @typedef {Object} IteratorData
 * @property {Array} [value] - The next set of values from the iterator
 * @property {boolean} done - Whether the iterator is completed
 */
/**
 * Iterator callback
 * @callback IteratorCallback
 * @param {?Error} err - An error if the iterator encountered one
 * @param {IteratorData} [data] - New data from the iterator
 * @returns {void}
 */
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
const errors_1 = __importDefault(require("./errors"));
promise_queue_1.default.configure(bluebird_1.Promise);
// -----------------------------------------------------------------------------
// Private
// -----------------------------------------------------------------------------
const PAGING_MODES = Object.freeze({
    MARKER: 'marker',
    OFFSET: 'offset',
});
// -----------------------------------------------------------------------------
// Public
// -----------------------------------------------------------------------------
/**
 * Asynchronous iterator for paged collections
 */
class PagingIterator {
    /**
     * Determine if a response is iterable
     * @param {Object} response - The API response
     * @returns {boolean} Whether the response is iterable
     */
    static isIterable(response /* FIXME */) {
        // POST responses for uploading a file are explicitly excluded here because, while the response is iterable,
        // it always contains only a single entry and historically has not been handled as iterable in the SDK.
        // This behavior is being preserved here to avoid a breaking change.
        let UPLOAD_PATTERN = /.*upload\.box\.com.*\/content/;
        var isGetOrPostRequest = response.request &&
            (response.request.method === 'GET' ||
                (response.request.method === 'POST' &&
                    !UPLOAD_PATTERN.test(response.request.uri.href))), hasEntries = response.body && Array.isArray(response.body.entries), notEventStream = response.body && !response.body.next_stream_position;
        return Boolean(isGetOrPostRequest && hasEntries && notEventStream);
    }
    /**
     * @constructor
     * @param {Object} response - The original API response
     * @param {BoxClient} client - An API client to make further requests
     * @returns {void}
     * @throws {Error} Will throw when collection cannot be paged
     */
    constructor(response /* FIXME */, client /* FIXME */) {
        if (!PagingIterator.isIterable(response)) {
            throw new Error('Cannot create paging iterator for non-paged response!');
        }
        var data = response.body;
        if (Number.isSafeInteger(data.offset)) {
            this.nextField = PAGING_MODES.OFFSET;
            this.nextValue = data.offset;
        }
        else if (typeof data.next_marker === 'undefined') {
            // Default to a finished marker collection when there's no field present,
            // since some endpoints indicate completed paging this way
            this.nextField = PAGING_MODES.MARKER;
            this.nextValue = null;
        }
        else {
            this.nextField = PAGING_MODES.MARKER;
            this.nextValue = data.next_marker;
        }
        this.limit = data.limit || data.entries.length;
        this.done = false;
        var href = response.request.href.split('?')[0];
        this.options = {
            headers: response.request.headers,
            qs: qs.parse(response.request.uri.query),
        };
        if (response.request.body) {
            if (Object.prototype.toString.call(response.request.body) ===
                '[object Object]') {
                this.options.body = response.request.body;
            }
            else {
                this.options.body = JSON.parse(response.request.body);
            }
        }
        // querystring.parse() makes everything a string, ensure numeric params are the correct type
        if (this.options.qs.limit) {
            this.options.qs.limit = parseInt(this.options.qs.limit, 10);
        }
        if (this.options.qs.offset) {
            this.options.qs.offset = parseInt(this.options.qs.offset, 10);
        }
        delete this.options.headers.Authorization;
        if (response.request.method === 'GET') {
            this.fetch = client.get.bind(client, href);
        }
        if (response.request.method === 'POST') {
            this.fetch = client.post.bind(client, href);
        }
        this.buffer = response.body.entries;
        this.queue = new promise_queue_1.default(1, Infinity);
        this._updatePaging(response);
    }
    /**
     * Update the paging parameters for the iterator
     * @private
     * @param {Object} response - The latest API response
     * @returns {void}
     */
    _updatePaging(response /* FIXME */) {
        var data = response.body;
        if (this.nextField === PAGING_MODES.OFFSET) {
            this.nextValue += this.limit;
            if (Number.isSafeInteger(data.total_count)) {
                this.done = data.offset + this.limit >= data.total_count;
            }
            else {
                this.done = data.entries.length === 0;
            }
        }
        else if (this.nextField === PAGING_MODES.MARKER) {
            if (data.next_marker) {
                this.nextValue = data.next_marker;
            }
            else {
                this.nextValue = null;
                this.done = true;
            }
        }
        if (response.request.method === 'GET') {
            this.options.qs[this.nextField] = this.nextValue;
        }
        else if (response.request.method === 'POST') {
            if (!this.options.body) {
                this.options.body = {};
            }
            this.options.body[this.nextField] = this.nextValue;
            let bodyString = JSON.stringify(this.options.body);
            this.options.headers['content-length'] = bodyString.length;
        }
    }
    /**
     * Fetch the next page of results
     * @returns {Promise} Promise resolving to iterator state
     */
    _getData() {
        return this.fetch(this.options).then((response /* FIXME */) => {
            if (response.statusCode !== 200) {
                throw errors_1.default.buildUnexpectedResponseError(response);
            }
            this._updatePaging(response);
            this.buffer = this.buffer.concat(response.body.entries);
            if (this.buffer.length === 0) {
                if (this.done) {
                    return {
                        value: undefined,
                        done: true,
                    };
                }
                // If we didn't get any data in this page, but the paging
                // parameters indicate that there is more data, attempt
                // to fetch more.  This occurs in multiple places in the API
                return this._getData();
            }
            return {
                value: this.buffer.shift(),
                done: false,
            };
        });
    }
    /**
     * Fetch the next page of the collection
     * @returns {Promise} Promise resolving to iterator state
     */
    next() {
        if (this.buffer.length > 0) {
            return bluebird_1.Promise.resolve({
                value: this.buffer.shift(),
                done: false,
            });
        }
        if (this.done) {
            return bluebird_1.Promise.resolve({
                value: undefined,
                done: true,
            });
        }
        return this.queue.add(this._getData.bind(this));
    }
    /**
     * Fetch the next marker
     * @returns {string|int} String that is the next marker or int that is the next offset
     */
    getNextMarker() {
        return this.nextValue;
    }
}
exports.default = PagingIterator;
//# sourceMappingURL=paging-iterator.js.map