const codeToType = {
	0: Int8Array,
	1: Uint8Array,
	2: Int16Array,
	3: Uint16Array,
	4: Int32Array,
	5: Uint32Array,
	7: Float32Array,
	8: Float64Array,
	9: BigInt64Array,
	10: BigUint64Array
};

const typeToBytes = {
	Int8Array: 1,
	Uint8Array: 1,
	Int16Array: 2,
	Uint16Array: 2,
	Int32Array: 4,
	Uint32Array: 4,
	Float32Array: 4,
	Float64Array: 8,
	BigInt64Array: 8,
	BigUint64Array: 8
};

function readTypedValue(view, offset, type) {
	switch (type) {
		case Int8Array: return view.getInt8(offset);
		case Uint8Array: return view.getUint8(offset);
		case Int16Array: return view.getInt16(offset, true);
		case Uint16Array: return view.getUint16(offset, true);
		case Int32Array: return view.getInt32(offset, true);
		case Uint32Array: return view.getUint32(offset, true);
		case Float32Array: return view.getFloat32(offset, true);
		case Float64Array: return view.getFloat64(offset, true);
		case BigInt64Array: return view.getBigInt64(offset, true);
		case BigUint64Array: return view.getBigUint64(offset, true);
		default: throw new Error(`Unsupported type: ${type.name}`);
	}
}

function writeTypedValue(view, offset, value, type) {
	switch (type) {
		case Int8Array: view.setInt8(offset, value); break;
		case Uint8Array: view.setUint8(offset, value); break;
		case Int16Array: view.setInt16(offset, value, true); break;
		case Uint16Array: view.setUint16(offset, value, true); break;
		case Int32Array: view.setInt32(offset, value, true); break;
		case Uint32Array: view.setUint32(offset, value, true); break;
		case Float32Array: view.setFloat32(offset, value, true); break;
		case Float64Array: view.setFloat64(offset, value, true); break;
		case BigInt64Array: view.setBigInt64(offset, BigInt(value), true); break;
		case BigUint64Array: view.setBigUint64(offset, BigInt(value), true); break;
		default: throw new Error(`Unsupported type: ${type.name}`);
	}
}

class DougalBinaryBundle extends ArrayBuffer {

	static HEADER_LENGTH = 4; // Length of a bundle header

	/** Clone an existing ByteArray into a DougalBinaryBundle
	 */
	static clone (buffer) {
		const clone = new DougalBinaryBundle(buffer.byteLength);
		const uint8Array = new Uint8Array(buffer);
		const uint8ArrayClone = new Uint8Array(clone);
		uint8ArrayClone.set(uint8Array);
		return clone;
	}

	constructor (length, options) {
		super (length, options);
	}

	/** Get the count of bundles in this ByteArray.
	 *
	 * Stops at the first non-bundle looking offset
	 */
	get bundleCount () {
		let count = 0;
		let currentBundleOffset = 0;
		const view = new DataView(this);

		while (currentBundleOffset < this.byteLength) {

			const currentBundleHeader = view.getUint32(currentBundleOffset, true);
			if ((currentBundleHeader & 0xff) !== 0x1c) {
				// This is not a bundle
				return count;
			}
			let currentBundleLength =	currentBundleHeader >>> 8;

			currentBundleOffset += currentBundleLength + DougalBinaryBundle.HEADER_LENGTH;
			count++;
		}

		return count;
	}


	/** Get the number of chunks in the bundles of this ByteArray
	 */
	get chunkCount () {
		let count = 0;
		let bundleOffset = 0;
		const view = new DataView(this);

		while (bundleOffset < this.byteLength) {
			const header = view.getUint32(bundleOffset, true);
			if ((header & 0xFF) !== 0x1C) break;
			const length = header >>> 8;
			if (bundleOffset + 4 + length > this.byteLength) break;

			let chunkOffset = bundleOffset + 4; // relative to buffer start

			while (chunkOffset < bundleOffset + 4 + length) {
				const chunkType = view.getUint8(chunkOffset);
				if (chunkType !== 0x11 && chunkType !== 0x12) break;

				const cCount = view.getUint16(chunkOffset + 2, true);
				const ΔelemC = view.getUint8(chunkOffset + 10);
				const elemC = view.getUint8(chunkOffset + 11);

				let localOffset = 12; // header size

				localOffset += ΔelemC + elemC; // preface

				// initial values
				for (let k = 0; k < ΔelemC; k++) {
					const typeByte = view.getUint8(chunkOffset + 12 + k);
					const baseCode = typeByte & 0xF;
					const baseType = codeToType[baseCode];
					if (!baseType) throw new Error('Invalid base type code');
					localOffset += typeToBytes[baseType.name];
				}

				// pad after initial
				while (localOffset % 4 !== 0) localOffset++;

				if (chunkType === 0x11) { // Sequential
					// record data: Δelems incrs
					for (let k = 0; k < ΔelemC; k++) {
						const typeByte = view.getUint8(chunkOffset + 12 + k);
						const incrCode = typeByte >> 4;
						const incrType = codeToType[incrCode];
						if (!incrType) throw new Error('Invalid incr type code');
						localOffset += cCount * typeToBytes[incrType.name];
					}

					// elems
					for (let k = 0; k < elemC; k++) {
						const typeCode = view.getUint8(chunkOffset + 12 + ΔelemC + k);
						const type = codeToType[typeCode];
						if (!type) throw new Error('Invalid elem type code');
						localOffset += cCount * typeToBytes[type.name];
					}
				} else { // Interleaved
					// Compute exact stride for interleaved record data
					let ΔelemStride = 0;
					for (let k = 0; k < ΔelemC; k++) {
						const typeByte = view.getUint8(chunkOffset + 12 + k);
						const incrCode = typeByte >> 4;
						const incrType = codeToType[incrCode];
						if (!incrType) throw new Error('Invalid incr type code');
						ΔelemStride += typeToBytes[incrType.name];
					}
					let elemStride = 0;
					for (let k = 0; k < elemC; k++) {
						const typeCode = view.getUint8(chunkOffset + 12 + ΔelemC + k);
						const type = codeToType[typeCode];
						if (!type) throw new Error('Invalid elem type code');
						elemStride += typeToBytes[type.name];
					}
					const recordStride = ΔelemStride + elemStride;
					localOffset += cCount * recordStride;
				}

				// pad after record
				while (localOffset % 4 !== 0) localOffset++;

				chunkOffset += localOffset;
				count++;
			}

			bundleOffset += 4 + length;
		}

		return count;
	}

	/** Return an array of DougalBinaryChunkSequential or DougalBinaryChunkInterleaved instances
	 */
	chunks () {
		const chunks = [];
		let bundleOffset = 0;
		const view = new DataView(this);

		while (bundleOffset < this.byteLength) {
			const header = view.getUint32(bundleOffset, true);
			if ((header & 0xFF) !== 0x1C) break;
			const length = header >>> 8;
			if (bundleOffset + 4 + length > this.byteLength) break;

			let chunkOffset = bundleOffset + 4;

			while (chunkOffset < bundleOffset + 4 + length) {
				const chunkType = view.getUint8(chunkOffset);
				if (chunkType !== 0x11 && chunkType !== 0x12) break;

				const cCount = view.getUint16(chunkOffset + 2, true);
				const ΔelemC = view.getUint8(chunkOffset + 10);
				const elemC = view.getUint8(chunkOffset + 11);

				let localOffset = 12;

				localOffset += ΔelemC + elemC;

				// initial values
				for (let k = 0; k < ΔelemC; k++) {
					const typeByte = view.getUint8(chunkOffset + 12 + k);
					const baseCode = typeByte & 0xF;
					const baseType = codeToType[baseCode];
					if (!baseType) throw new Error('Invalid base type code');
					localOffset += typeToBytes[baseType.name];
				}

				// pad after initial
				while (localOffset % 4 !== 0) localOffset++;

				if (chunkType === 0x11) { // Sequential
					// record data: Δelems incrs
					for (let k = 0; k < ΔelemC; k++) {
						const typeByte = view.getUint8(chunkOffset + 12 + k);
						const incrCode = typeByte >> 4;
						const incrType = codeToType[incrCode];
						if (!incrType) throw new Error('Invalid incr type code');
						localOffset += cCount * typeToBytes[incrType.name];
					}

					// elems
					for (let k = 0; k < elemC; k++) {
						const typeCode = view.getUint8(chunkOffset + 12 + ΔelemC + k);
						const type = codeToType[typeCode];
						if (!type) throw new Error('Invalid elem type code');
						localOffset += cCount * typeToBytes[type.name];
					}
				} else { // Interleaved
					// Compute exact stride for interleaved record data
					let ΔelemStride = 0;
					for (let k = 0; k < ΔelemC; k++) {
						const typeByte = view.getUint8(chunkOffset + 12 + k);
						const incrCode = typeByte >> 4;
						const incrType = codeToType[incrCode];
						if (!incrType) throw new Error('Invalid incr type code');
						ΔelemStride += typeToBytes[incrType.name];
					}
					let elemStride = 0;
					for (let k = 0; k < elemC; k++) {
						const typeCode = view.getUint8(chunkOffset + 12 + ΔelemC + k);
						const type = codeToType[typeCode];
						if (!type) throw new Error('Invalid elem type code');
						elemStride += typeToBytes[type.name];
					}
					const recordStride = ΔelemStride + elemStride;
					localOffset += cCount * recordStride;
				}

				// pad after record
				while (localOffset % 4 !== 0) localOffset++;

				switch (chunkType) {
					case 0x11:
						chunks.push(new DougalBinaryChunkSequential(this, chunkOffset, localOffset));
						break;
					case 0x12:
						chunks.push(new DougalBinaryChunkInterleaved(this, chunkOffset, localOffset));
						break;
					default:
						throw new Error('Invalid chunk type');
				}

				chunkOffset += localOffset;
			}

			bundleOffset += 4 + length;
		}

		return chunks;
	}

	/** Return a ByteArray containing all data from all
	 * chunks including reconstructed i, j and incremental
	 * values as follows:
	 *
	 * <i_0> <i_1> … <i_x> // i values (constant)
	 * <j_0> <j_1> … <j_x> // j values (j0 + Δj*i)
	 * <Δelem_0_0> <Δelem_0_1> … <Δelem_0_x> // reconstructed Δelem0 (uses baseType)
	 * <Δelem_1_0> <Δelem_1_1> … <Δelem_1_x> // reconstructed Δelem1
	 * …
	 * <Δelem_y_0> <Δelem_y_1> … <Δelem_y_x> // reconstructed Δelem1
	 * <elem_0_0> <elem_0_1> … <elem_0_x> // First elem
	 * <elem_1_0> <elem_1_1> … <elem_1_x> // Second elem
	 * …
	 * <elem_z_0> <elem_z_1> … <elem_z_x> // Last elem
	 *
	 * It does not matter whether the underlying chunks are
	 * sequential or interleaved. This function will transform
	 * as necessary.
	 *
	 */
	getDataSequentially () {
		const chunks = this.chunks();
		if (chunks.length === 0) return new ArrayBuffer(0);

		const firstChunk = chunks[0];
		const ΔelemC = firstChunk.ΔelemCount;
		const elemC = firstChunk.elemCount;

		// Check consistency across chunks
		for (const chunk of chunks) {
			if (chunk.ΔelemCount !== ΔelemC || chunk.elemCount !== elemC) {
				throw new Error('Inconsistent chunk structures');
			}
		}

		// Get types from first chunk
		const view = new DataView(firstChunk);
		const ΔelemBaseTypes = [];
		for (let k = 0; k < ΔelemC; k++) {
			const typeByte = view.getUint8(12 + k);
			const baseCode = typeByte & 0xF;
			const baseType = codeToType[baseCode];
			if (!baseType) throw new Error('Invalid base type code');
			ΔelemBaseTypes.push(baseType);
		}
		const elemTypes = [];
		for (let k = 0; k < elemC; k++) {
			const typeCode = view.getUint8(12 + ΔelemC + k);
			const type = codeToType[typeCode];
			if (!type) throw new Error('Invalid elem type code');
			elemTypes.push(type);
		}

		// Compute total records
		const totalN = chunks.reduce((sum, c) => sum + c.jCount, 0);

		// Compute sizes
		const size_i = totalN * 2; // Uint16 for i
		const size_j = totalN * 4; // Int32 for j
		let size_Δelems = 0;
		for (const t of ΔelemBaseTypes) {
			size_Δelems += totalN * typeToBytes[t.name];
		}
		let size_elems = 0;
		for (const t of elemTypes) {
			size_elems += totalN * typeToBytes[t.name];
		}
		const totalSize = size_i + size_j + size_Δelems + size_elems;

		const ab = new ArrayBuffer(totalSize);
		const dv = new DataView(ab);

		// Write i's
		let off = 0;
		for (const chunk of chunks) {
			const i = chunk.i;
			for (let idx = 0; idx < chunk.jCount; idx++) {
				dv.setUint16(off, i, true);
				off += 2;
			}
		}

		// Write j's
		off = size_i;
		for (const chunk of chunks) {
			const j0 = chunk.j0;
			const Δj = chunk.Δj;
			for (let idx = 0; idx < chunk.jCount; idx++) {
				const j = j0 + idx * Δj;
				dv.setInt32(off, j, true);
				off += 4;
			}
		}

		// Write Δelems
		off = size_i + size_j;
		for (let m = 0; m < ΔelemC; m++) {
			const type = ΔelemBaseTypes[m];
			const bytes = typeToBytes[type.name];
			for (const chunk of chunks) {
				const arr = chunk.Δelem(m);
				for (let idx = 0; idx < chunk.jCount; idx++) {
					writeTypedValue(dv, off, arr[idx], type);
					off += bytes;
				}
			}
		}

		// Write elems
		for (let m = 0; m < elemC; m++) {
			const type = elemTypes[m];
			const bytes = typeToBytes[type.name];
			for (const chunk of chunks) {
				const arr = chunk.elem(m);
				for (let idx = 0; idx < chunk.jCount; idx++) {
					writeTypedValue(dv, off, arr[idx], type);
					off += bytes;
				}
			}
		}

		return ab;
	}

	/** Return a ByteArray containing all data from all
	 * chunks including reconstructed i, j and incremental
	 * values, interleaved as follows:
	 *
	 * <i_0> <j_0> <Δelem_0_0> <Δelem_1_0> … <Δelem_y_0> <elem_0_0> <elem_1_0> … <elem_z_0>
	 * <i_1> <j_1> <Δelem_0_1> <Δelem_1_1> … <Δelem_y_1> <elem_0_1> <elem_1_1> … <elem_z_1>
	 * <i_x> <j_x> <Δelem_0_x> <Δelem_1_x> … <Δelem_y_x> <elem_0_x> <elem_1_x> … <elem_z_x>
	 *
	 * It does not matter whether the underlying chunks are
	 * sequential or interleaved. This function will transform
	 * as necessary.
	 *
	 */
	getDataInterleaved () {
		const chunks = this.chunks();
		if (chunks.length === 0) return new ArrayBuffer(0);

		const firstChunk = chunks[0];
		const ΔelemC = firstChunk.ΔelemCount;
		const elemC = firstChunk.elemCount;

		// Check consistency across chunks
		for (const chunk of chunks) {
			if (chunk.ΔelemCount !== ΔelemC || chunk.elemCount !== elemC) {
				throw new Error('Inconsistent chunk structures');
			}
		}

		// Get types from first chunk
		const view = new DataView(firstChunk);
		const ΔelemBaseTypes = [];
		for (let k = 0; k < ΔelemC; k++) {
			const typeByte = view.getUint8(12 + k);
			const baseCode = typeByte & 0xF;
			const baseType = codeToType[baseCode];
			if (!baseType) throw new Error('Invalid base type code');
			ΔelemBaseTypes.push(baseType);
		}
		const elemTypes = [];
		for (let k = 0; k < elemC; k++) {
			const typeCode = view.getUint8(12 + ΔelemC + k);
			const type = codeToType[typeCode];
			if (!type) throw new Error('Invalid elem type code');
			elemTypes.push(type);
		}

		// Compute total records
		const totalN = chunks.reduce((sum, c) => sum + c.jCount, 0);

		// Compute record size
		const recordSize = 2 + 4 + // i (Uint16) + j (Int32)
			ΔelemBaseTypes.reduce((sum, t) => sum + typeToBytes[t.name], 0) +
			elemTypes.reduce((sum, t) => sum + typeToBytes[t.name], 0);
		const totalSize = totalN * recordSize;

		const ab = new ArrayBuffer(totalSize);
		const dv = new DataView(ab);

		let off = 0;
		for (const chunk of chunks) {
			const i = chunk.i;
			const j0 = chunk.j0;
			const Δj = chunk.Δj;
			for (let idx = 0; idx < chunk.jCount; idx++) {
				dv.setUint16(off, i, true);
				off += 2;
				const j = j0 + idx * Δj;
				dv.setInt32(off, j, true);
				off += 4;
				for (let m = 0; m < ΔelemC; m++) {
					const type = ΔelemBaseTypes[m];
					const bytes = typeToBytes[type.name];
					const arr = chunk.Δelem(m);
					writeTypedValue(dv, off, arr[idx], type);
					off += bytes;
				}
				for (let m = 0; m < elemC; m++) {
					const type = elemTypes[m];
					const bytes = typeToBytes[type.name];
					const arr = chunk.elem(m);
					writeTypedValue(dv, off, arr[idx], type);
					off += bytes;
				}
			}
		}

		return ab;
	}

	get records () {
		const data = [];
		for (const record of this) {
			data.push(record.slice(1));
		}
		return data;
	}

	[Symbol.iterator]() {
		const chunks = this.chunks();
		let chunkIndex = 0;
		let chunkIterator = chunks.length > 0 ? chunks[0][Symbol.iterator]() : null;

		return {
			next() {
				if (!chunkIterator) {
					return { done: true };
				}

				let result = chunkIterator.next();
				while (result.done && chunkIndex < chunks.length - 1) {
					chunkIndex++;
					chunkIterator = chunks[chunkIndex][Symbol.iterator]();
					result = chunkIterator.next();
				}

				return result;
			}
		};
	}

}


class DougalBinaryChunkSequential extends ArrayBuffer {

	constructor (buffer, offset, length) {
		super(length);
		new Uint8Array(this).set(new Uint8Array(buffer, offset, length));
		this._ΔelemCaches = new Array(this.ΔelemCount);
		this._elemCaches = new Array(this.elemCount);
		this._ΔelemBlockOffsets = null;
		this._elemBlockOffsets = null;
		this._recordOffset = null;
	}

	_getRecordOffset() {
		if (this._recordOffset !== null) return this._recordOffset;
		const view = new DataView(this);
		const ΔelemC = this.ΔelemCount;
		const elemC = this.elemCount;

		let recordOffset = 12 + ΔelemC + elemC;
		for (let k = 0; k < ΔelemC; k++) {
			const tb = view.getUint8(12 + k);
			const bc = tb & 0xF;
			const bt = codeToType[bc];
			recordOffset += typeToBytes[bt.name];
		}
		while (recordOffset % 4 !== 0) recordOffset++;
		this._recordOffset = recordOffset;
		return recordOffset;
	}

	_initBlockOffsets() {
		if (this._ΔelemBlockOffsets !== null) return;
		const view = new DataView(this);
		const count = this.jCount;
		const ΔelemC = this.ΔelemCount;
		const elemC = this.elemCount;

		const recordOffset = this._getRecordOffset();

		this._ΔelemBlockOffsets = [];
		let o = recordOffset;
		for (let k = 0; k < ΔelemC; k++) {
			this._ΔelemBlockOffsets[k] = o;
			const tb = view.getUint8(12 + k);
			const ic = tb >> 4;
			const it = codeToType[ic];
			o += count * typeToBytes[it.name];
		}

		this._elemBlockOffsets = [];
		for (let k = 0; k < elemC; k++) {
			this._elemBlockOffsets[k] = o;
			const tc = view.getUint8(12 + ΔelemC + k);
			const t = codeToType[tc];
			o += count * typeToBytes[t.name];
		}
	}

	/** Return the user-defined value
	 */
	get udv () {
		return new DataView(this).getUint8(1);
	}

	/** Return the number of j elements in this chunk
	 */
	get jCount () {
		return new DataView(this).getUint16(2, true);
	}

	/** Return the i value in this chunk
	 */
	get i () {
		return new DataView(this).getUint16(4, true);
	}

	/** Return the j0 value in this chunk
	 */
	get j0 () {
		return new DataView(this).getUint16(6, true);
	}

	/** Return the Δj value in this chunk
	 */
	get Δj () {
		return new DataView(this).getInt16(8, true);
	}

	/** Return the Δelem_count value in this chunk
	 */
	get ΔelemCount () {
		return new DataView(this).getUint8(10);
	}

	/** Return the elem_count value in this chunk
	 */
	get elemCount () {
		return new DataView(this).getUint8(11);
	}

	/** Return a TypedArray (e.g., Uint16Array, …) for the n-th Δelem in the chunk
	 */
	Δelem (n) {
		if (this._ΔelemCaches[n]) return this._ΔelemCaches[n];

		if (n < 0 || n >= this.ΔelemCount) throw new Error(`Invalid Δelem index: ${n}`);
		const view = new DataView(this);
		const count = this.jCount;
		const ΔelemC = this.ΔelemCount;

		const typeByte = view.getUint8(12 + n);
		const baseCode = typeByte & 0xF;
		const incrCode = typeByte >> 4;
		const baseType = codeToType[baseCode];
		const incrType = codeToType[incrCode];
		if (!baseType || !incrType) throw new Error('Invalid type codes for Δelem');

		// Find offset for initial value of this Δelem
		let initialOffset = 12 + ΔelemC + this.elemCount;
		for (let k = 0; k < n; k++) {
			const tb = view.getUint8(12 + k);
			const bc = tb & 0xF;
			const bt = codeToType[bc];
			initialOffset += typeToBytes[bt.name];
		}

		let current = readTypedValue(view, initialOffset, baseType);

		// Advance to start of record data (after all initials and pad)
		const recordOffset = this._getRecordOffset();

		// Find offset for deltas of this Δelem (skip previous Δelems' delta blocks)
		this._initBlockOffsets();
		const deltaOffset = this._ΔelemBlockOffsets[n];

		// Reconstruct the array
		const arr = new baseType(count);
		const isBigInt = baseType === BigInt64Array || baseType === BigUint64Array;
		arr[0] = current;
		for (let idx = 1; idx < count; idx++) {
			let delta = readTypedValue(view, deltaOffset + idx * typeToBytes[incrType.name], incrType);
			if (isBigInt) {
				delta = BigInt(delta);
				current += delta;
			} else {
				current += delta;
			}
			arr[idx] = current;
		}

		this._ΔelemCaches[n] = arr;
		return arr;
	}

	/** Return a TypedArray (e.g., Uint16Array, …) for the n-th elem in the chunk
	 */
	elem (n) {
		if (this._elemCaches[n]) return this._elemCaches[n];

		if (n < 0 || n >= this.elemCount) throw new Error(`Invalid elem index: ${n}`);
		const view = new DataView(this);
		const count = this.jCount;
		const ΔelemC = this.ΔelemCount;
		const elemC = this.elemCount;

		const typeCode = view.getUint8(12 + ΔelemC + n);
		const type = codeToType[typeCode];
		if (!type) throw new Error('Invalid type code for elem');

		// Find offset for this elem's data block
		this._initBlockOffsets();
		const elemOffset = this._elemBlockOffsets[n];

		// Create and populate the array
		const arr = new type(count);
		const bytes = typeToBytes[type.name];
		for (let idx = 0; idx < count; idx++) {
			arr[idx] = readTypedValue(view, elemOffset + idx * bytes, type);
		}

		this._elemCaches[n] = arr;
		return arr;
	}

	getRecord (index) {
		if (index < 0 || index >= this.jCount) throw new Error(`Invalid record index: ${index}`);

		const arr = [this.udv, this.i, this.j0 + index * this.Δj];

		for (let m = 0; m < this.ΔelemCount; m++) {
			const values = this.Δelem(m);
			arr.push(values[index]);
		}

		for (let m = 0; m < this.elemCount; m++) {
			const values = this.elem(m);
			arr.push(values[index]);
		}

		return arr;
	}

	[Symbol.iterator]() {
		let index = 0;
		const chunk = this;
		return {
			next() {
				if (index < chunk.jCount) {
					return { value: chunk.getRecord(index++), done: false };
				} else {
					return { done: true };
				}
			}
		};
	}

}


class DougalBinaryChunkInterleaved extends ArrayBuffer {
	constructor(buffer, offset, length) {
		super(length);
		new Uint8Array(this).set(new Uint8Array(buffer, offset, length));
		this._incrStrides = [];
		this._elemStrides = [];
		this._incrOffsets = [];
		this._elemOffsets = [];
		this._recordStride = 0;
		this._recordOffset = null;
		this._initStrides();
		this._ΔelemCaches = new Array(this.ΔelemCount);
		this._elemCaches = new Array(this.elemCount);
	}

	_getRecordOffset() {
		if (this._recordOffset !== null) return this._recordOffset;
		const view = new DataView(this);
		const ΔelemC = this.ΔelemCount;
		const elemC = this.elemCount;

		let recordOffset = 12 + ΔelemC + elemC;
		for (let k = 0; k < ΔelemC; k++) {
			const tb = view.getUint8(12 + k);
			const bc = tb & 0xF;
			const bt = codeToType[bc];
			recordOffset += typeToBytes[bt.name];
		}
		while (recordOffset % 4 !== 0) recordOffset++;
		this._recordOffset = recordOffset;
		return recordOffset;
	}

	_initStrides() {
		const view = new DataView(this);
		const ΔelemC = this.ΔelemCount;
		const elemC = this.elemCount;

		// Compute incr strides and offsets
		let incrOffset = 0;
		for (let k = 0; k < ΔelemC; k++) {
			const typeByte = view.getUint8(12 + k);
			const incrCode = typeByte >> 4;
			const incrType = codeToType[incrCode];
			if (!incrType) throw new Error('Invalid incr type code');
			this._incrOffsets.push(incrOffset);
			const bytes = typeToBytes[incrType.name];
			this._incrStrides.push(bytes);
			incrOffset += bytes;
			this._recordStride += bytes;
		}

		// Compute elem strides and offsets
		let elemOffset = incrOffset;
		for (let k = 0; k < elemC; k++) {
			const typeCode = view.getUint8(12 + ΔelemC + k);
			const type = codeToType[typeCode];
			if (!type) throw new Error('Invalid elem type code');
			this._elemOffsets.push(elemOffset);
			const bytes = typeToBytes[type.name];
			this._elemStrides.push(bytes);
			elemOffset += bytes;
			this._recordStride += bytes;
		}
	}

	get udv() {
		return new DataView(this).getUint8(1);
	}

	get jCount() {
		return new DataView(this).getUint16(2, true);
	}

	get i() {
		return new DataView(this).getUint16(4, true);
	}

	get j0() {
		return new DataView(this).getUint16(6, true);
	}

	get Δj() {
		return new DataView(this).getInt16(8, true);
	}

	get ΔelemCount() {
		return new DataView(this).getUint8(10);
	}

	get elemCount() {
		return new DataView(this).getUint8(11);
	}

	Δelem(n) {
		if (this._ΔelemCaches[n]) return this._ΔelemCaches[n];

		if (n < 0 || n >= this.ΔelemCount) throw new Error(`Invalid Δelem index: ${n}`);
		const view = new DataView(this);
		const count = this.jCount;
		const ΔelemC = this.ΔelemCount;

		const typeByte = view.getUint8(12 + n);
		const baseCode = typeByte & 0xF;
		const incrCode = typeByte >> 4;
		const baseType = codeToType[baseCode];
		const incrType = codeToType[incrCode];
		if (!baseType || !incrType) throw new Error('Invalid type codes for Δelem');

		// Find offset for initial value of this Δelem
		let initialOffset = 12 + ΔelemC + this.elemCount;
		for (let k = 0; k < n; k++) {
			const tb = view.getUint8(12 + k);
			const bc = tb & 0xF;
			const bt = codeToType[bc];
			initialOffset += typeToBytes[bt.name];
		}

		let current = readTypedValue(view, initialOffset, baseType);

		// Find offset to start of record data
		const recordOffset = this._getRecordOffset();

		// Use precomputed offset for this Δelem
		const deltaOffset = recordOffset + this._incrOffsets[n];

		// Reconstruct the array
		const arr = new baseType(count);
		const isBigInt = baseType === BigInt64Array || baseType === BigUint64Array;
		arr[0] = current;
		for (let idx = 1; idx < count; idx++) {
			let delta = readTypedValue(view, deltaOffset + idx * this._recordStride, incrType);
			if (isBigInt) {
				delta = BigInt(delta);
				current += delta;
			} else {
				current += delta;
			}
			arr[idx] = current;
		}

		this._ΔelemCaches[n] = arr;
		return arr;
	}

	elem(n) {
		if (this._elemCaches[n]) return this._elemCaches[n];

		if (n < 0 || n >= this.elemCount) throw new Error(`Invalid elem index: ${n}`);
		const view = new DataView(this);
		const count = this.jCount;
		const ΔelemC = this.ΔelemCount;

		const typeCode = view.getUint8(12 + ΔelemC + n);
		const type = codeToType[typeCode];
		if (!type) throw new Error('Invalid type code for elem');

		// Find offset to start of record data
		const recordOffset = this._getRecordOffset();

		// Use precomputed offset for this elem (relative to start of record data)
		const elemOffset = recordOffset + this._elemOffsets[n];

		// Create and populate the array
		const arr = new type(count);
		const bytes = typeToBytes[type.name];
		for (let idx = 0; idx < count; idx++) {
			arr[idx] = readTypedValue(view, elemOffset + idx * this._recordStride, type);
		}

		this._elemCaches[n] = arr;
		return arr;
	}

	getRecord (index) {
		if (index < 0 || index >= this.jCount) throw new Error(`Invalid record index: ${index}`);

		const arr = [this.udv, this.i, this.j0 + index * this.Δj];

		for (let m = 0; m < this.ΔelemCount; m++) {
			const values = this.Δelem(m);
			arr.push(values[index]);
		}

		for (let m = 0; m < this.elemCount; m++) {
			const values = this.elem(m);
			arr.push(values[index]);
		}

		return arr;
	}

	[Symbol.iterator]() {
		let index = 0;
		const chunk = this;
		return {
			next() {
				if (index < chunk.jCount) {
					return { value: chunk.getRecord(index++), done: false };
				} else {
					return { done: true };
				}
			}
		};
	}

}


module.exports = { DougalBinaryBundle, DougalBinaryChunkSequential, DougalBinaryChunkInterleaved }
