/* Customer Cloud Gallery — minimal ZIP writer.
 * Method 0 (stored, no compression). Zwei Betriebsmodi:
 *
 *   1. Blob-Modus  (default): Bytes werden intern in _parts gesammelt,
 *      finish() liefert einen application/zip-Blob.
 *      Nutzt CRC-im-Local-Header (kein Data Descriptor).
 *
 *   2. Streaming-Modus: Konstruktor bekommt einen emit(chunk)-Callback.
 *      Bytes werden direkt durchgereicht (z. B. an einen Service Worker
 *      oder an eine WritableStream-Pipeline). Kein internes Sammeln,
 *      Speicherbedarf konstant unabhängig von der Archiv-Größe. Verwendet
 *      Data Descriptors (Flag 0x0008), damit CRC/size erst NACH dem Daten-
 *      Strom geschrieben werden müssen — analog zur PHP-Variante in
 *      class-zip-stream.php.
 *
 * Kein ZIP64. Aufrufer muss sicherstellen, dass weder einzelne Datei noch
 * Archiv-Total 4 GB überschreiten. Im Streaming-Modus ist das in der Praxis
 * nur durch einzelne Files limitiert, da das Archiv-Total beliebig wachsen
 * kann (kein RAM-Hold).
 */
(function (global) {
	'use strict';

	const CRC_TABLE = (function () {
		const t = new Uint32Array(256);
		for (let n = 0; n < 256; n++) {
			let c = n;
			for (let k = 0; k < 8; k++) {
				c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
			}
			t[n] = c >>> 0;
		}
		return t;
	})();

	function crc32(buf, init) {
		let c = (init === undefined ? 0xFFFFFFFF : init);
		for (let i = 0; i < buf.length; i++) {
			c = CRC_TABLE[(c ^ buf[i]) & 0xFF] ^ (c >>> 8);
		}
		return c >>> 0;
	}
	function crc32Final(c) { return (c ^ 0xFFFFFFFF) >>> 0; }

	function utf8(name) {
		return new TextEncoder().encode(name);
	}

	function dosTime(d) {
		return ((d.getHours() & 0x1F) << 11)
			| ((d.getMinutes() & 0x3F) << 5)
			| ((d.getSeconds() >>> 1) & 0x1F);
	}

	function dosDate(d) {
		return (((d.getFullYear() - 1980) & 0x7F) << 9)
			| (((d.getMonth() + 1) & 0x0F) << 5)
			| (d.getDate() & 0x1F);
	}

	function sanitizeEntryName(raw) {
		let s = String(raw || '');
		s = s.replace(/[\x00\r\n]/g, '');
		s = s.replace(/\\+/g, '/');
		s = s.replace(/^\/+/, '');
		if (!s) s = 'file';
		return s;
	}

	function CCGALZipWriter(emit) {
		this._emit = (typeof emit === 'function') ? emit : null;
		this._parts = [];
		this._central = [];
		this._offset = 0;
		this._finished = false;
		this._currentEntry = null;
	}

	CCGALZipWriter.prototype._push = function (chunk) {
		// Reihenfolge wichtig: offset zuerst, weil emit() den ArrayBuffer
		// per postMessage Transferable übernehmen kann — danach wäre
		// chunk.byteLength === 0 (detached buffer).
		this._offset += chunk.byteLength;
		if (this._emit) {
			this._emit(chunk);
		} else {
			this._parts.push(chunk);
		}
	};

	/**
	 * One-shot: fügt eine Datei aus einem komplett geladenen Uint8Array hinzu.
	 * CRC + size sind vorab bekannt, daher KEIN data descriptor.
	 */
	CCGALZipWriter.prototype.addFile = function (name, data) {
		if (this._finished) throw new Error('ZIP already finished');
		if (this._currentEntry) throw new Error('streaming entry still open');
		const entryName = sanitizeEntryName(name);
		const nameBytes = utf8(entryName);
		const now = new Date();
		const mtime = dosTime(now);
		const mdate = dosDate(now);
		const size = data.byteLength;
		const crc = crc32Final(crc32(data));
		const localOffset = this._offset;

		const lfh = new Uint8Array(30 + nameBytes.byteLength);
		const lv = new DataView(lfh.buffer);
		lv.setUint32(0, 0x04034b50, true);
		lv.setUint16(4, 20, true);
		lv.setUint16(6, 0x0800, true);  // UTF-8 name; KEIN data descriptor
		lv.setUint16(8, 0, true);
		lv.setUint16(10, mtime, true);
		lv.setUint16(12, mdate, true);
		lv.setUint32(14, crc, true);
		lv.setUint32(18, size, true);
		lv.setUint32(22, size, true);
		lv.setUint16(26, nameBytes.byteLength, true);
		lv.setUint16(28, 0, true);
		lfh.set(nameBytes, 30);

		this._push(lfh);
		this._push(data);

		this._central.push({
			name: nameBytes,
			crc: crc,
			size: size,
			offset: localOffset,
			mtime: mtime,
			mdate: mdate,
			flags: 0x0800,
		});
	};

	/**
	 * Streaming-Modus: Datei beginnen. CRC + size noch unbekannt — werden
	 * inkrementell mit writeChunk() befüllt und am Ende mit endFile()
	 * als Data Descriptor nachgereicht.
	 */
	CCGALZipWriter.prototype.beginFile = function (name) {
		if (this._finished) throw new Error('ZIP already finished');
		if (this._currentEntry) throw new Error('previous entry not closed');
		const entryName = sanitizeEntryName(name);
		const nameBytes = utf8(entryName);
		const now = new Date();
		const mtime = dosTime(now);
		const mdate = dosDate(now);
		const localOffset = this._offset;

		const lfh = new Uint8Array(30 + nameBytes.byteLength);
		const lv = new DataView(lfh.buffer);
		lv.setUint32(0, 0x04034b50, true);
		lv.setUint16(4, 20, true);
		lv.setUint16(6, 0x0808, true);  // UTF-8 + data descriptor
		lv.setUint16(8, 0, true);
		lv.setUint16(10, mtime, true);
		lv.setUint16(12, mdate, true);
		lv.setUint32(14, 0, true);       // CRC placeholder
		lv.setUint32(18, 0, true);       // comp-size placeholder
		lv.setUint32(22, 0, true);       // uncomp-size placeholder
		lv.setUint16(26, nameBytes.byteLength, true);
		lv.setUint16(28, 0, true);
		lfh.set(nameBytes, 30);

		this._push(lfh);

		this._currentEntry = {
			name: nameBytes,
			offset: localOffset,
			mtime: mtime,
			mdate: mdate,
			flags: 0x0808,
			crc: 0xFFFFFFFF, // running, vor Final-XOR
			size: 0,
		};
	};

	CCGALZipWriter.prototype.writeChunk = function (chunk) {
		if (!this._currentEntry) throw new Error('no entry open');
		if (chunk.byteLength === 0) return;
		this._currentEntry.crc  = crc32(chunk, this._currentEntry.crc);
		this._currentEntry.size += chunk.byteLength;
		this._push(chunk);
	};

	CCGALZipWriter.prototype.endFile = function () {
		const e = this._currentEntry;
		if (!e) throw new Error('no entry open');
		// Kein ZIP64-Support — bei Überlauf besser hart failen als ein
		// stillschweigend korruptes Archiv produzieren.
		if (e.size >= 0xFFFFFFFF || this._offset >= 0xFFFFFFFF) {
			throw new Error('zip64 required, not supported');
		}
		const finalCrc = crc32Final(e.crc);

		const dd = new Uint8Array(16);
		const dv = new DataView(dd.buffer);
		dv.setUint32(0, 0x08074b50, true);
		dv.setUint32(4, finalCrc, true);
		dv.setUint32(8, e.size, true);
		dv.setUint32(12, e.size, true);
		this._push(dd);

		this._central.push({
			name: e.name,
			crc: finalCrc,
			size: e.size,
			offset: e.offset,
			mtime: e.mtime,
			mdate: e.mdate,
			flags: e.flags,
		});

		this._currentEntry = null;
	};

	CCGALZipWriter.prototype.finish = function () {
		if (this._finished) return this._emit ? null : this.getBlob();
		if (this._currentEntry) throw new Error('streaming entry still open');
		if (this._central.length >= 0xFFFF) {
			throw new Error('zip64 required, not supported (>65535 entries)');
		}
		this._finished = true;

		const centralStart = this._offset;
		let centralSize = 0;

		for (let i = 0; i < this._central.length; i++) {
			const e = this._central[i];
			const rec = new Uint8Array(46 + e.name.byteLength);
			const rv = new DataView(rec.buffer);
			rv.setUint32(0, 0x02014b50, true);
			rv.setUint16(4, 0x031E, true);
			rv.setUint16(6, 20, true);
			rv.setUint16(8, e.flags, true);
			rv.setUint16(10, 0, true);
			rv.setUint16(12, e.mtime, true);
			rv.setUint16(14, e.mdate, true);
			rv.setUint32(16, e.crc, true);
			rv.setUint32(20, e.size, true);
			rv.setUint32(24, e.size, true);
			rv.setUint16(28, e.name.byteLength, true);
			rv.setUint16(30, 0, true);
			rv.setUint16(32, 0, true);
			rv.setUint16(34, 0, true);
			rv.setUint16(36, 0, true);
			rv.setUint32(38, 0, true);
			rv.setUint32(42, e.offset, true);
			rec.set(e.name, 46);
			this._push(rec);
			centralSize += rec.byteLength;
		}

		const eocd = new Uint8Array(22);
		const ev = new DataView(eocd.buffer);
		ev.setUint32(0, 0x06054b50, true);
		ev.setUint16(4, 0, true);
		ev.setUint16(6, 0, true);
		ev.setUint16(8, this._central.length, true);
		ev.setUint16(10, this._central.length, true);
		ev.setUint32(12, centralSize, true);
		ev.setUint32(16, centralStart, true);
		ev.setUint16(20, 0, true);
		this._push(eocd);

		return this._emit ? null : this.getBlob();
	};

	CCGALZipWriter.prototype.getBlob = function () {
		return new Blob(this._parts, { type: 'application/zip' });
	};

	CCGALZipWriter.prototype.totalSize = function () {
		return this._offset;
	};

	global.CCGALZipWriter = CCGALZipWriter;
})(window);
