/*
Copyright 2016-2020 Balena Ltd.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import * as utils from './utils';

/**
 * @module progress
 */

/**
 * @summary Get progress stream
 * @function
 * @private
 *
 * @param {Number} total - response total
 * @param {Function} [onState] - on state callback (state)
 * @returns {NodeJS.ReadableStream} progress stream
 *
 * @example
 * progressStream = getProgressStream response, (state) ->
 * 	console.log(state)
 *
 * return responseStream.pipe(progressStream).pipe(output)
 */
const getProgressStream = function (total, onState) {
	const progress = require('progress-stream');
	const progressStream = progress({
		time: 500,
		length: total,
	});

	progressStream.on('progress', function (state) {
		if (state.length === 0) {
			return typeof onState === 'function' ? onState(undefined) : undefined;
		}

		return typeof onState === 'function'
			? onState({
					total: state.length,
					received: state.transferred,
					eta: state.eta,
					percentage: state.percentage,
			  })
			: undefined;
	});

	return progressStream;
};

/**
 * @summary Make a node request with progress
 * @function
 * @protected
 *
 * @returns {(options) => Promise<NodeJS.ReadableStream>} request stream
 *
 * @example
 * progress.estimate(options).then (stream) ->
 * 		stream.pipe(fs.createWriteStream('foo/bar'))
 * 		stream.on 'progress', (state) ->
 * 			console.log(state)
 */
export function estimate(requestAsync, isBrowser) {
	return async function (options) {
		if (requestAsync == null) {
			requestAsync = utils.getRequestAsync();
		}

		options.gzip = false;
		options.headers['Accept-Encoding'] = 'gzip, deflate';

		let reader = null;

		if (options.signal != null) {
			options.signal.addEventListener(
				'abort',
				function () {
					// We need to react to Abort events at this level, because otherwise our
					// reader locks the stream and lower-level cancellation causes error.
					if (reader) {
						reader.cancel().catch(function () {
							// ignore
						});
						return reader.releaseLock();
					}
				},
				{ once: true },
			);
		}

		const response = await requestAsync(options);

		const stream = require('stream');
		const output = new stream.PassThrough();
		// @ts-expect-error
		output.response = response;

		const responseLength = utils.getResponseLength(response);
		const total = responseLength.uncompressed || responseLength.compressed;

		let responseStream;
		if (response.body.getReader) {
			const webStreams = require('@balena/node-web-streams');
			// Convert browser (WHATWG) streams to Node streams
			responseStream = webStreams.toNodeReadable(response.body);
			reader = responseStream._reader;
		} else {
			responseStream = response.body;
		}

		const progressStream = getProgressStream(total, (state) =>
			output.emit('progress', state),
		);

		if (!isBrowser && utils.isResponseCompressed(response)) {
			const zlib = require('zlib');

			// Be more lenient with decoding compressed responses, since (very rarely)
			// servers send slightly invalid gzip responses that are still accepted
			// by common browsers.
			// Always using Z_SYNC_FLUSH is what cURL does.
			var zlibOptions = {
				flush: zlib.constants.Z_SYNC_FLUSH,
				finishFlush: zlib.constants.Z_SYNC_FLUSH,
			};

			const gunzip = zlib.createGunzip(zlibOptions);

			// Uncompress after or before piping through progress
			// depending on the response length available to us
			if (
				responseLength.compressed != null &&
				responseLength.uncompressed == null
			) {
				responseStream.pipe(progressStream).pipe(gunzip).pipe(output);
			} else {
				responseStream.pipe(gunzip).pipe(progressStream).pipe(output);
			}
		} else {
			responseStream.pipe(progressStream).pipe(output);
		}

		// Stream any request errors on downstream
		responseStream.on('error', (e) => output.emit('error', e));

		return output;
	};
}

@@ -3761,52 +3761,564 @@
 %0A%09%09%09
-const gunzip = zlib.createGunzip(zlibOptions
+// Allow overriding this behaviour by setting the ZLIB_FLUSH env var%0A%09%09%09// to one of the allowed zlib flush values (process.env.ZLIB_FLUSH=%22Z_NO_FLUSH%22).%0A%09%09%09// https://github.com/nodejs/node/blob/master/doc/api/zlib.md#zlib-constants%0A%09%09%09if (process.env.ZLIB_FLUSH && process.env.ZLIB_FLUSH in zlib.constants) %7B%0A%09%09%09%09zlibOptions = %7B%0A%09%09%09%09%09flush: zlib.constants%5Bprocess.env.ZLIB_FLUSH%5D,%0A%09%09%09%09%09finishFlush: zlib.constants%5Bprocess.env.ZLIB_FLUSH%5D,%0A%09%09%09%09%7D;%0A%09%09%09%7D%0A%0A%09%09%09const gunzip = zlib.createGunzip(zlibOptions);%0A%09%09%09gunzip.on('error', (e) =%3E output.emit('error', e)
 );%0A%0A

Allow overriding the default zlib flush setting