"use strict";

import { type InflateState } from "./zlib/inflate";
import * as zlibInflate from "./zlib/inflate";
import { ZStream } from "./zlib/zstream";
import { GZheader } from "./zlib/gzheader";

/* Public constants ==========================================================*/
/* ===========================================================================*/

import {
	Z_NO_FLUSH,
	Z_FINISH,
	Z_OK,
	Z_STREAM_END,
	Z_NEED_DICT,
	Z_STREAM_ERROR,
	Z_DATA_ERROR,
	Z_MEM_ERROR,
} from "./zlib/constants";
import { Uint8Array } from "./utils/buffs";
import { assign, flattenChunks } from "./utils/common";
import messages from "./zlib/messages";

/* ===========================================================================*/

/**
 * class Inflate
 *
 * Generic JS-style wrapper for zlib calls. If you don't need
 * streaming behaviour - use more simple functions: [[inflate]]
 * and [[inflateRaw]].
 **/

/* internal
 * inflate.chunks -> Array
 *
 * Chunks of output data, if [[Inflate#onData]] not overridden.
 **/

/**
 * Inflate.result -> Uint8Array|String
 *
 * Uncompressed result, generated by default [[Inflate#onData]]
 * and [[Inflate#onEnd]] handlers. Filled after you push last chunk
 * (call [[Inflate#push]] with `Z_FINISH` / `true` param).
 **/

/**
 * Inflate.err -> Number
 *
 * Error code after inflate finished. 0 (Z_OK) on success.
 * Should be checked if broken data possible.
 **/

/**
 * Inflate.msg -> String
 *
 * Error message, if [[Inflate.err]] != 0
 **/

/**
 * new Inflate(options)
 * - options (Object): zlib inflate options.
 *
 * Creates new inflator instance with specified params. Throws exception
 * on bad params. Supported options:
 *
 * - `windowBits`
 * - `dictionary`
 *
 * [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced)
 * for more information on these.
 *
 * Additional options, for internal needs:
 *
 * - `chunkSize` - size of generated data chunks (16K by default)
 * - `raw` (Boolean) - do raw inflate
 * - `to` (String) - if equal to 'string', then result will be converted
 *   from utf8 to utf16 (javascript) string. When string output requested,
 *   chunk length can differ from `chunkSize`, depending on content.
 *
 * By default, when no options set, autodetect deflate/gzip data format via
 * wrapper header.
 *
 * ##### Example:
 *
 * ```javascript
 * const pako = require('pako')
 * const chunk1 = new Uint8Array([1,2,3,4,5,6,7,8,9])
 * const chunk2 = new Uint8Array([10,11,12,13,14,15,16,17,18,19]);
 *
 * const inflate = new pako.Inflate({ level: 3});
 *
 * inflate.push(chunk1, false);
 * inflate.push(chunk2, true);  // true -> last chunk
 *
 * if (inflate.err) { throw new Error(inflate.err); }
 *
 * console.log(inflate.result);
 * ```
 **/

type Options = {
	chunkSize: number;
	windowBits: number;
	to: string;
	raw: boolean;
	dictionary?: string | Uint8Array;
};

export class Inflate {
	public options: Exclude<Options, "dictionary"> & { dictionary?: Uint8Array };
	public err: keyof typeof messages = Z_OK;
	public msg?: string;
	public ended = false;
	public chunks: Uint8Array[] = [];
	public strm = new ZStream<InflateState>();
	public header = new GZheader();
	public result?: buffer | string;

	constructor(options?: Partial<Options>) {
		this.options = assign(
			{
				chunkSize: 1024 * 64,
				windowBits: 15,
				to: "",
				raw: false,
				dictionary: undefined,
			},
			options ?? {},
		) as unknown as typeof this.options;

		const opt = this.options;

		// Force window size for `raw` data, if not set directly,
		// because we have no header for autodetect.
		if (opt.raw && opt.windowBits >= 0 && opt.windowBits < 16) {
			opt.windowBits = -opt.windowBits;
			if (opt.windowBits === 0) {
				opt.windowBits = -15;
			}
		}

		// If `windowBits` not defined (and mode not raw) - set autodetect flag for gzip/deflate
		if (opt.windowBits >= 0 && opt.windowBits < 16 && !(options && options.windowBits)) {
			opt.windowBits += 32;
		}

		// Gzip header has no info about windows size, we can do autodetect only
		// for deflate. So, if window size not set, force it to max when gzip possible
		if (opt.windowBits > 15 && opt.windowBits < 48) {
			// bit 3 (16) -> gzipped data
			// bit 4 (32) -> autodetect gzip/deflate
			if ((opt.windowBits & 15) === 0) {
				opt.windowBits |= 15;
			}
		}

		if (opt.dictionary) {
			// Convert data if needed
			let dict = opt.dictionary;
			if (typeIs(dict, "string")) {
				dict = Uint8Array.from(buffer.fromstring(dict));
			}

			opt.dictionary = dict;
		}

		this.strm.avail_out = 0;

		const { windowBits, dictionary, raw } = opt;
		const status = zlibInflate.inflateInit2(this.strm, windowBits);

		if (status !== Z_OK) {
			error(messages[status]);
		}

		zlibInflate.inflateGetHeader(this.strm, this.header);

		if (dictionary && raw) {
			//In raw mode we need to set the dictionary early
			const status = zlibInflate.inflateSetDictionary(this.strm, dictionary);
			if (status !== Z_OK) {
				error(messages[status]);
			}
		}
	}

	/**
	 * Inflate#push(data[, flush_mode]) -> Boolean
	 * - data (Uint8Array|ArrayBuffer): input data
	 * - flush_mode (Number|Boolean): 0..6 for corresponding Z_NO_FLUSH..Z_TREE
	 *   flush modes. See constants. Skipped or `false` means Z_NO_FLUSH,
	 *   `true` means Z_FINISH.
	 *
	 * Sends input data to inflate pipe, generating [[Inflate#onData]] calls with
	 * new output chunks. Returns `true` on success. If end of stream detected,
	 * [[Inflate#onEnd]] will be called.
	 *
	 * `flush_mode` is not needed for normal operation, because end of stream
	 * detected automatically. You may try to use it for advanced things, but
	 * this functionality was not tested.
	 *
	 * On fail call [[Inflate#onEnd]] with error code and return false.
	 *
	 * ##### Example
	 *
	 * ```javascript
	 * push(chunk, false); // push one of data chunks
	 * ...
	 * push(chunk, true);  // push last chunk
	 * ```
	 **/
	push(data: Uint8Array, flush_mode: number | boolean) {
		const strm = this.strm;
		const { chunkSize, dictionary } = this.options;
		let status, _flush_mode, last_avail_out;

		if (this.ended) return false;

		if (flush_mode === ~~flush_mode) _flush_mode = flush_mode;
		else _flush_mode = flush_mode === true ? Z_FINISH : Z_NO_FLUSH;

		strm.input = data;

		strm.next_in = 0;
		strm.avail_in = strm.input.length;

		for (;;) {
			if (strm.avail_out === 0) {
				strm.output = new Uint8Array(chunkSize);
				strm.next_out = 0;
				strm.avail_out = chunkSize;
			}

			status = zlibInflate.inflate(strm, _flush_mode);

			if (status === Z_NEED_DICT && dictionary) {
				status = zlibInflate.inflateSetDictionary(strm, dictionary);

				if (status === Z_OK) {
					status = zlibInflate.inflate(strm, _flush_mode);
				} else if (status === Z_DATA_ERROR) {
					// Replace code with more verbose
					status = Z_NEED_DICT;
				}
			}

			// Skip snyc markers if more data follows and not raw mode
			while (strm.avail_in > 0 && status === Z_STREAM_END && strm.state.wrap > 0 && data[strm.next_in] !== 0) {
				zlibInflate.inflateReset(strm);
				status = zlibInflate.inflate(strm, _flush_mode);
			}

			switch (status) {
				case Z_STREAM_ERROR:
				case Z_DATA_ERROR:
				case Z_NEED_DICT:
				case Z_MEM_ERROR:
					this.onEnd(status);
					this.ended = true;
					return false;
			}

			// Remember real `avail_out` value, because we may patch out buffer content
			// to align utf8 strings boundaries.
			last_avail_out = strm.avail_out;

			if (strm.next_out && (strm.avail_out === 0 || status === Z_STREAM_END)) {
				this.onData(
					strm.output.length === strm.next_out ? strm.output : strm.output.subarray(0, strm.next_out),
				);
			}

			// Must repeat iteration if out buffer is full
			if (status === Z_OK && last_avail_out === 0) continue;

			// Finalize if end of stream reached.
			if (status === Z_STREAM_END) {
				const status = zlibInflate.inflateEnd(this.strm);
				this.onEnd(status);
				this.ended = true;
				return true;
			}

			if (strm.avail_in === 0) break;
		}

		return true;
	}

	/**
	 * Inflate#onData(chunk) -> Void
	 * - chunk (Uint8Array|String): output data. When string output requested,
	 *   each chunk will be string.
	 *
	 * By default, stores data blocks in `chunks[]` property and glue
	 * those in `onEnd`. Override this handler, if you need another behaviour.
	 **/
	onData(chunk: Uint8Array) {
		this.chunks.push(chunk);
	}

	/**
	 * Inflate#onEnd(status) -> Void
	 * - status (Number): inflate status. 0 (Z_OK) on success,
	 *   other if not.
	 *
	 * Called either after you tell inflate that the input stream is
	 * complete (Z_FINISH). By default - join collected chunks,
	 * free memory and fill `results` / `err` properties.
	 **/
	onEnd(status: keyof typeof messages) {
		// On success - join
		if (status === Z_OK) {
			if (this.options.to === "string") {
				this.result = this.chunks.join("");
			} else {
				this.result = flattenChunks(this.chunks.map((chunk) => chunk.buf));
			}
		}
		this.chunks = [];
		this.err = status;
		this.msg = this.strm.msg;
	}
}

/**
 * inflate(data[, options]) -> Uint8Array|String
 * - data (Uint8Array|ArrayBuffer): input data to decompress.
 * - options (Object): zlib inflate options.
 *
 * Decompress `data` with inflate/ungzip and `options`. Autodetect
 * format via wrapper header by default. That's why we don't provide
 * separate `ungzip` method.
 *
 * Supported options are:
 *
 * - windowBits
 *
 * [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced)
 * for more information.
 *
 * Sugar (options):
 *
 * - `raw` (Boolean) - say that we work with raw stream, if you don't wish to specify
 *   negative windowBits implicitly.
 * - `to` (String) - if equal to 'string', then result will be converted
 *   from utf8 to utf16 (javascript) string. When string output requested,
 *   chunk length can differ from `chunkSize`, depending on content.
 *
 *
 * ##### Example:
 *
 * ```javascript
 * const pako = require('pako');
 * const input = pako.deflate(new Uint8Array([1,2,3,4,5,6,7,8,9]));
 * let output;
 *
 * try {
 *   output = pako.inflate(input);
 * } catch (err) {
 *   console.log(err);
 * }
 * ```
 **/
export function inflate(input: Uint8Array, options: Partial<Options> = {}) {
	const inflator = new Inflate(options);

	inflator.push(input, true);

	// That will never happens, if you don't cheat with options :)
	if (inflator.err) error(inflator.msg || messages[inflator.err]);

	return inflator.result;
}

/**
 * inflateRaw(data[, options]) -> Uint8Array|String
 * - data (Uint8Array|ArrayBuffer): input data to decompress.
 * - options (Object): zlib inflate options.
 *
 * The same as [[inflate]], but creates raw data, without wrapper
 * (header and adler32 crc).
 **/
export function inflateRaw(input: Uint8Array, options: Exclude<Partial<Options>, "raw"> = {}) {
	options = options || {};
	options.raw = true;
	return inflate(input, options);
}

export * from "./zlib/constants";
