/**
 *
 * @enum {boolean}
 */
import { assert } from "../assert.js";
import { array_copy } from "../collection/array/array_copy.js";
import { array_buffer_copy } from "../collection/array/typed/array_buffer_copy.js";
import { align_4 } from "./align_4.js";
import { EndianType } from "./EndianType.js";
import { half_to_float_uint16 } from "./half_to_float_uint16.js";
import { to_half_float_uint16 } from "./to_half_float_uint16.js";

/**
 * Minimum number of bytes to grow the buffer by when the buffer is full.
 * @type {number}
 */
const MIN_GROWTH_STEP = 1024;

/**
 * 2^31-1, values above this will be cropped incorrectly when bit-shifting
 * @type {number}
 */
const MAX_SAFE_UINT_VAR = 2147483647;

/**
 * @readonly
 * @type {number}
 */
const DEFAULT_INITIAL_SIZE = 1024;


/**
 * Utility for reading/writing binary data.
 * Mostly useful for serialization/deserialization tasks.
 * The buffer is dynamically resized, so you do not need to manage the size manually.
 * It is useful to think of this structure as a "stream".
 *
 * @example
 *  const buffer = new BinaryBuffer();
 *
 *  buffer.writeUTF8String("Hello World");
 *
 *  buffer.position = 0; // rewind to the beginning
 *
 *  const deserialized = buffer.readUTF8String(); // "Hello World"
 *
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class BinaryBuffer {

    /**
     * Default is little-endian as most platforms operate in little-endian
     * The reason this is fixed is to ensure cross-platform compatibility as endianness in JavaScript is platform-dependent.
     * @see https://en.wikipedia.org/wiki/Endianness
     * @type {EndianType|boolean}
     */
    endianness = EndianType.LittleEndian;

    /**
     * Current position in the buffer, where read and write operations will occur.
     * Make sure to set this to the correct value before reading/writing data.
     * Typically, this is set to 0 before reading/writing data.
     * @type {number}
     */
    position = 0;

    /**
     * @deprecated
     */
    get length() {
        throw new Error("Deprecated, use 'capacity' instead");
    }

    /**
     * @deprecated
     */
    set length(v) {
        throw new Error("Deprecated, use 'capacity' instead");
    }

    /**
     * Managed by the buffer, do not modify directly
     * @type {number}
     */
    capacity = DEFAULT_INITIAL_SIZE;

    /**
     * Raw underlying bytes attached to the buffer, note that this is managed by the `BinaryBuffer` and can grow/shrink as needed.
     * @see setCapacity
     * @see trim
     * @type {ArrayBuffer}
     * @private
     */
    data = new ArrayBuffer(DEFAULT_INITIAL_SIZE);

    /**
     * Bound to the {@link data} buffer, do not modify directly.
     * @type {DataView}
     * @private
     */
    dataView = new DataView(this.data);

    /**
     * Bound to the {@link data} buffer, do not modify directly.
     * @type {Uint8Array}
     * @private
     */
    __data_uint8 = new Uint8Array(this.data);

    /**
     * When the buffer grows in size, this is the multiplication factor by which it grows.
     * @type {number}
     * @private
     */
    __growFactor = 1.1;

    /**
     * Access raw underlying bytes attached to the buffer
     * @return {Uint8Array}
     */
    get raw_bytes() {
        return this.__data_uint8;
    }

    /**
     * Sets {@link capacity} to the size of the input data.
     * Sets {@link position} to 0.
     *
     * Note: if you write to the buffer past the size of the input data {@link ArrayBuffer} - bound data will be re-allocated.
     * @param {ArrayBuffer} data
     */
    fromArrayBuffer(data) {
        assert.defined(data, 'data');
        assert.notNull(data, 'data');

        this.data = data;
        this.dataView = new DataView(data);
        this.__data_uint8 = new Uint8Array(data);

        this.capacity = data.byteLength;
        this.position = 0;
    }

    /**
     * Set capacity to contain data only up to the current `position`.
     * This will re-allocate the `data` buffer if necessary.
     * @returns {BinaryBuffer}
     */
    trim() {
        this.setCapacity(this.position);

        return this;
    }

    /**
     * Advance `position`(read/write cursor) a certain number of bytes forward.
     *
     * @param {number} byte_count
     */
    skip(byte_count) {
        assert.isNonNegativeInteger(byte_count, 'byte_count');

        this.position += byte_count;
    }

    /**
     * This will re-allocate {@link data} buffer if necessary.
     * Note that all data is retained.
     * Cannot shink past the current `position`.
     *
     * @param {number} capacity
     * @throws {Error} if requested capacity is less than current `position`
     *
     */
    setCapacity(capacity) {
        assert.isNonNegativeInteger(capacity, 'capacity');

        if (capacity < this.position) {
            throw new Error(`Attempting to set capacity(=${capacity}) below current position(=${this.position})`);
        }

        if (this.capacity === capacity) {
            // already the right capacity
            return;
        }

        const oldData = this.__data_uint8;
        const newData = new Uint8Array(capacity);

        if (this.position > 0) {
            //copy old data
            array_buffer_copy(
                oldData.buffer, 0,
                newData.buffer, 0,
                Math.min(
                    oldData.buffer.byteLength,
                    newData.buffer.byteLength,
                    this.position
                )
            );
        }

        this.data = newData.buffer;
        this.__data_uint8 = newData;
        this.dataView = new DataView(this.data);

        this.capacity = capacity;
    }

    /**
     *
     * @param {number} min_capacity
     */
    ensureCapacity(min_capacity) {
        assert.isNumber(min_capacity, 'min_capacity');
        assert.greaterThanOrEqual(min_capacity, 0, 'min_capacity');
        assert.notNaN(min_capacity, 'min_capacity');

        const existing_capacity = this.capacity;

        if (existing_capacity >= min_capacity) {
            return;
        }

        const rough_new_capacity = Math.ceil(Math.max(
            min_capacity,
            existing_capacity * this.__growFactor,
            existing_capacity + MIN_GROWTH_STEP
        ));

        // align for easier memory operations
        const aligned_new_capacity = align_4(rough_new_capacity);

        this.setCapacity(aligned_new_capacity);
    }

    /**
     *
     * @returns {number}
     */
    readFloat16() {
        const u16 = this.readUint16();

        return half_to_float_uint16(u16);
    }

    /**
     *
     * @returns {number}
     */
    readFloat32() {
        const result = this.dataView.getFloat32(this.position, this.endianness);

        this.position += 4;

        return result;
    }

    /**
     *
     * @returns {number}
     */
    readFloat64() {
        const result = this.dataView.getFloat64(this.position, this.endianness);

        this.position += 8;

        return result;
    }

    /**
     *
     * @return {number}
     */
    readInt8() {
        const result = this.dataView.getInt8(this.position);

        this.position += 1;

        return result;
    }

    /**
     *
     * @return {number}
     */
    readInt16() {
        const result = this.dataView.getInt16(this.position, this.endianness);

        this.position += 2;

        return result;
    }

    /**
     *
     * @returns {number}
     */
    readInt32() {
        const result = this.dataView.getInt32(this.position, this.endianness);

        this.position += 4;

        return result;
    }

    /**
     *
     * @returns {bigint}
     */
    readInt64() {
        const result = this.dataView.getBigInt64(this.position, this.endianness);

        this.position += 8;

        return result;
    }

    /**
     *
     * @returns {number}
     */
    readUint8() {
        const result = this.dataView.getUint8(this.position);

        this.position += 1;

        return result;
    }

    /**
     *
     * @returns {number}
     */
    readUint16() {
        const result = this.dataView.getUint16(this.position, this.endianness);

        this.position += 2;

        return result;
    }

    /**
     *
     * @returns {number}
     */
    readUint16LE() {
        const result = this.dataView.getUint16(this.position, EndianType.LittleEndian);

        this.position += 2;

        return result;
    }

    /**
     *
     * @returns {number}
     */
    readUint16BE() {
        const result = this.dataView.getUint16(this.position, EndianType.BigEndian);

        this.position += 2;

        return result;
    }

    /**
     *
     * @returns {number}
     */
    readUint24() {
        if (this.endianness === EndianType.BigEndian) {
            return this.readUint24BE();
        } else {
            return this.readUint24LE();
        }
    }

    /**
     *
     * @returns {number}
     */
    readUint24LE() {
        const b0 = this.dataView.getUint8(this.position);
        const b1 = this.dataView.getUint8(this.position + 1);
        const b2 = this.dataView.getUint8(this.position + 2);

        this.position += 3;

        return b0 | (b1 << 8) | (b2 << 16);
    }

    /**
     *
     * @returns {number}
     */
    readUint24BE() {
        const b0 = this.dataView.getUint8(this.position);
        const b1 = this.dataView.getUint8(this.position + 1);
        const b2 = this.dataView.getUint8(this.position + 2);

        this.position += 3;

        return b2 | (b1 << 8) | (b0 << 16);
    }

    /**
     *
     * @returns {number}
     */
    readUint32() {
        const result = this.dataView.getUint32(this.position, this.endianness);

        this.position += 4;

        return result;
    }

    /**
     *
     * @returns {number}
     */
    readUint32LE() {
        const result = this.dataView.getUint32(this.position, EndianType.LittleEndian);

        this.position += 4;

        return result;
    }

    /**
     *
     * @returns {number}
     */
    readUint32BE() {
        const result = this.dataView.getUint32(this.position, EndianType.BigEndian);

        this.position += 4;

        return result;
    }

    /**
     *
     * @returns {bigint}
     */
    readUint64() {
        const result = this.dataView.getBigUint64(this.position, this.endianness);

        this.position += 8;

        return result;
    }

    /**
     *
     * @param {number} destination_offset starting index in the destination array
     * @param {number} length number of elements to read
     * @param {Uint8Array} destination
     */
    readUint8Array(destination, destination_offset, length) {
        for (let i = 0; i < length; i++) {
            destination[i + destination_offset] = this.readUint8();
        }
    }

    /**
     *
     * @param {number} destination_offset starting index in the destination array
     * @param {number} length number of elements to read
     * @param {Uint16Array} destination
     */
    readUint16Array(destination, destination_offset, length) {
        for (let i = 0; i < length; i++) {
            destination[i + destination_offset] = this.readUint16();
        }
    }

    /**
     *
     * @param {number} destination_offset starting index in the destination array
     * @param {number} length number of elements to read
     * @param {Uint32Array|number[]|ArrayLike<number>} destination
     */
    readUint32Array(destination, destination_offset, length) {
        for (let i = 0; i < length; i++) {
            destination[i + destination_offset] = this.readUint32();
        }
    }

    /**
     *
     * @param {number} destination_offset starting index in the destination array
     * @param {number} length number of elements to read
     * @param {Int8Array} destination
     */
    readInt8Array(destination, destination_offset, length) {
        for (let i = 0; i < length; i++) {
            destination[i + destination_offset] = this.readInt8();
        }
    }

    /**
     *
     * @param {number} destination_offset starting index in the destination array
     * @param {number} length number of elements to read
     * @param {Int16Array} destination
     */
    readInt16Array(destination, destination_offset, length) {
        for (let i = 0; i < length; i++) {
            destination[i + destination_offset] = this.readInt16();
        }
    }

    /**
     *
     * @param {number} destination_offset starting index in the destination array
     * @param {number} length number of elements to read
     * @param {Int32Array} destination
     */
    readInt32Array(destination, destination_offset, length) {
        for (let i = 0; i < length; i++) {
            destination[i + destination_offset] = this.readInt32();
        }
    }

    /**
     *
     * @param {number} destination_offset starting index in the destination array
     * @param {number} length number of elements to read
     * @param {Float32Array|number[]} destination
     */
    readFloat32Array(destination, destination_offset, length) {
        for (let i = 0; i < length; i++) {
            destination[i + destination_offset] = this.readFloat32();
        }
    }

    /**
     *
     * @param {number} destination_offset starting index in the destination array
     * @param {number} length number of elements to read
     * @param {Float64Array} destination
     */
    readFloat64Array(destination, destination_offset, length) {
        for (let i = 0; i < length; i++) {
            destination[i + destination_offset] = this.readFloat64();
        }
    }

    /**
     *
     * @param {number} source_offset starting index in the source array
     * @param {number} length number of elements to read
     * @param {Float32Array|number[]} source
     */
    writeFloat32Array(source, source_offset, length) {
        assert.isArrayLike(source, 'source');
        assert.greaterThanOrEqual(source.length, source_offset + length, "source underflow");

        for (let i = 0; i < length; i++) {
            this.writeFloat32(source[i + source_offset]);
        }
    }

    /**
     *
     * @param {number} source_offset starting index in the source array
     * @param {number} length number of elements to read
     * @param {Float32Array|number[]} source
     */
    writeFloat16Array(source, source_offset, length){
        assert.isArrayLike(source, 'source');
        assert.greaterThanOrEqual(source.length, source_offset + length, "source underflow");

        for (let i = 0; i < length; i++) {
            this.writeFloat16(source[i + source_offset]);
        }
    }

    /**
     *
     * @param {number} destination_offset starting index in the destination array
     * @param {number} length number of elements to read
     * @param {Float32Array|number[]} destination
     */
    readFloat16Array(destination, destination_offset, length){
        for (let i = 0; i < length; i++) {
            destination[i + destination_offset] = this.readFloat16();
        }
    }

    /**
     *
     * @param {number} value
     */
    writeFloat16(value) {
        const u16 = to_half_float_uint16(value);

        this.writeUint16(u16);
    }

    /**
     *
     * @param {number} value
     */
    writeFloat32(value) {
        const end = this.position + 4;
        this.ensureCapacity(end);

        this.dataView.setFloat32(this.position, value, this.endianness);

        this.position = end;
    }

    /**
     *
     * @param {number} value
     */
    writeFloat64(value) {
        const end = this.position + 8;
        this.ensureCapacity(end);

        this.dataView.setFloat64(this.position, value, this.endianness);

        this.position = end;
    }

    /**
     *
     * @param {number} value
     */
    writeInt8(value) {
        const end = this.position + 1;
        this.ensureCapacity(end);

        this.dataView.setInt8(this.position, value);

        this.position = end;
    }

    /**
     *
     * @param {number} value
     */
    writeInt16(value) {
        const end = this.position + 2;
        this.ensureCapacity(end);

        this.dataView.setInt16(this.position, value, this.endianness);

        this.position = end;
    }

    /**
     *
     * @param {number} value
     */
    writeInt32(value) {
        const end = this.position + 4;
        this.ensureCapacity(end);

        this.dataView.setInt32(this.position, value, this.endianness);

        this.position = end;
    }

    /**
     *
     * @param {bigint} value
     */
    writeInt64(value) {
        const end = this.position + 8;
        this.ensureCapacity(end);

        this.dataView.setBigInt64(this.position, value, this.endianness);

        this.position = end;
    }

    /**
     *
     * @param {Int8Array|number[]|ArrayLike<number>} source
     * @param {number} source_offset
     * @param {number} length
     */
    writeInt8Array(source, source_offset, length) {
        assert.isArrayLike(source, 'source');
        assert.greaterThanOrEqual(source.length, source_offset + length, "source underflow");

        // prevent resizing mid-copy
        this.ensureCapacity(this.position + length);

        for (let i = 0; i < length; i++) {
            this.writeInt8(source[source_offset + i]);
        }
    }

    /**
     *
     * @param {Int16Array|number[]|ArrayLike<number>} source
     * @param {number} source_offset
     * @param {number} length
     */
    writeInt16Array(source, source_offset, length) {
        assert.isArrayLike(source, 'source');
        assert.greaterThanOrEqual(source.length, source_offset + length, "source underflow");

        // prevent resizing mid-copy
        this.ensureCapacity(this.position + 2 * length);

        for (let i = 0; i < length; i++) {
            this.writeInt16(source[source_offset + i]);
        }
    }

    /**
     *
     * @param {Int32Array|number[]|ArrayLike<number>} source
     * @param {number} source_offset
     * @param {number} length
     */
    writeInt32Array(source, source_offset, length) {
        assert.isArrayLike(source, 'source');
        assert.greaterThanOrEqual(source.length, source_offset + length, "source underflow");

        // prevent resizing mid-copy
        this.ensureCapacity(this.position + 4 * length);

        for (let i = 0; i < length; i++) {
            this.writeInt32(source[source_offset + i]);
        }
    }

    /**
     *
     * @param {number} value
     */
    writeUint8(value) {
        const end = this.position + 1;
        this.ensureCapacity(end);

        this.dataView.setUint8(this.position, value);

        this.position = end;
    }

    /**
     *
     * @param {Uint8Array|number[]} source
     * @param {number} source_offset
     * @param {number} length
     */
    writeUint8Array(source, source_offset, length) {
        assert.isArrayLike(source, 'source');
        assert.greaterThanOrEqual(source.length, source_offset + length, "source underflow");
        for (let i = 0; i < length; i++) {
            this.writeUint8(source[source_offset + i]);
        }
    }

    /**
     *
     * @param {number} value
     */
    writeUint16(value) {
        const end = this.position + 2;
        this.ensureCapacity(end);

        this.dataView.setUint16(this.position, value, this.endianness);

        this.position = end;
    }

    /**
     *
     * @param {number} value
     */
    writeUint16BE(value) {
        const end = this.position + 2;
        this.ensureCapacity(end);

        this.dataView.setUint16(this.position, value, EndianType.BigEndian);

        this.position = end;
    }

    /**
     *
     * @param {number} value
     */
    writeUint16LE(value) {
        const end = this.position + 2;
        this.ensureCapacity(end);

        this.dataView.setUint16(this.position, value, EndianType.LittleEndian);

        this.position = end;
    }

    /**
     *
     * @param {Uint16Array|number[]} source
     * @param {number} source_offset
     * @param {number} length
     */
    writeUint16Array(source, source_offset, length) {
        assert.isArrayLike(source, 'source');
        assert.greaterThanOrEqual(source.length, source_offset + length, "source underflow");
        for (let i = 0; i < length; i++) {
            this.writeUint16(source[source_offset + i]);
        }
    }

    /**
     *
     * @param {number} value
     */
    writeUint24(value) {
        if (this.endianness === EndianType.BigEndian) {
            this.writeUint24BE(value);
        } else {
            this.writeUint24LE(value);
        }
    }

    /**
     *
     * @param {number} value
     */
    writeUint24BE(value) {
        const end = this.position + 3;

        this.ensureCapacity(end);

        const b0 = value & 0xFF;
        const b1 = (value >> 8) & 0xFF;
        const b2 = (value >> 16) & 0xFF;

        this.dataView.setUint8(this.position, b2);
        this.dataView.setUint8(this.position + 1, b1);
        this.dataView.setUint8(this.position + 2, b0);

        this.position = end;
    }

    /**
     *
     * @param {number} value
     */
    writeUint24LE(value) {
        const end = this.position + 3;

        this.ensureCapacity(end);

        const b0 = value & 0xFF;
        const b1 = (value >> 8) & 0xFF;
        const b2 = (value >> 16) & 0xFF;

        this.dataView.setUint8(this.position, b0);
        this.dataView.setUint8(this.position + 1, b1);
        this.dataView.setUint8(this.position + 2, b2);

        this.position = end;
    }

    /**
     * Write uint using a minimum number of bytes.
     * Compact encoding scheme, if the value is 127 or less - only one byte will be used, if the value is 16383 or less - two bytes will be used, etc.
     * NOTE: uses 7-bit encoding with 1 bit used for carry-over flag.
     * NOTE: explicitly a little-endian format, {@link endianness} is ignored.
     * @param {number} value must be an unsigned integer
     */
    writeUintVar(value) {
        assert.isNonNegativeInteger(value, 'value');
        assert.ok(value <= MAX_SAFE_UINT_VAR, `value=[${value}] exceeds maximum safe limit[=${MAX_SAFE_UINT_VAR}]`);

        let first = true;
        let _v = value;

        while (first || _v !== 0) {
            first = false;

            let lower7bits = (_v & 0x7f);

            _v >>= 7;

            if (_v > 0) {
                //write carry-over flag
                lower7bits |= 128;
            }

            this.writeUint8(lower7bits);
        }
    }

    /**
     * Read Uint of variable length, a compliment to {@link #writeUintVar}
     * @returns {number}
     */
    readUintVar() {
        let more = true;
        let value = 0;
        let shift = 0;

        while (more) {
            let lower7bits = this.readUint8();

            //read carry-over flag
            more = (lower7bits & 128) !== 0;

            //read value part of the byte
            value |= (lower7bits & 0x7f) << shift;

            //increment shift
            shift += 7;
        }

        return value;
    }

    /**
     *
     * @param {number} value
     */
    writeUint32(value) {
        const end = this.position + 4;
        this.ensureCapacity(end);

        this.dataView.setUint32(this.position, value, this.endianness);

        this.position = end;
    }

    /**
     *
     * @param {number} value
     */
    writeUint32BE(value) {
        const end = this.position + 4;
        this.ensureCapacity(end);

        this.dataView.setUint32(this.position, value, EndianType.BigEndian);

        this.position = end;
    }

    /**
     *
     * @param {number} value
     */
    writeUint32LE(value) {
        const end = this.position + 4;
        this.ensureCapacity(end);

        this.dataView.setUint32(this.position, value, EndianType.LittleEndian);

        this.position = end;
    }

    /**
     *
     * @param {bigint} value
     */
    writeUint64(value) {
        const end = this.position + 8;
        this.ensureCapacity(end);

        this.dataView.setBigUint64(this.position, value, this.endianness);

        this.position = end;
    }

    /**
     *
     * @param {Uint32Array|number[]|ArrayLike<number>} source
     * @param {number} source_offset
     * @param {number} length
     */
    writeUint32Array(source, source_offset, length) {
        assert.isArrayLike(source, 'source');
        assert.greaterThanOrEqual(source.length, source_offset + length, "source underflow");

        // prevent resizing mid-copy
        this.ensureCapacity(this.position + 4 * length);

        for (let i = 0; i < length; i++) {
            this.writeUint32(source[source_offset + i]);
        }
    }

    /**
     *
     * @param {Uint8Array|Uint8ClampedArray} array
     * @param {number} source_offset
     * @param {number} length
     */
    writeBytes(array, source_offset, length) {
        const source_end = source_offset + length;
        assert.greaterThanOrEqual(array.length, source_end, 'source array underflow');

        const targetAddress = this.position;

        const end = targetAddress + length;

        this.ensureCapacity(end);

        if (source_offset === 0 && array.length === length) {
            // copying entire source array

            this.__data_uint8.set(array, targetAddress);

        } else if (typeof array.subarray === "function") {
            // typed array, use "subarray" method

            this.__data_uint8.set(array.subarray(source_offset, source_end), targetAddress);

        } else {
            // not a typed array, copy byte by byte manually

            for (let i = 0; i < length; i++) {
                this.__data_uint8[targetAddress + i] = array[source_offset + i];
            }

        }

        this.position = end;
    }

    /**
     *
     * @param {Uint8Array} destination
     * @param {number} destination_offset
     * @param {number} length
     */
    readBytes(destination, destination_offset, length) {
        const source_position = this.position;

        const end = source_position + length;

        const uint8 = this.__data_uint8;

        if (length < 128) {
            // small copy
            array_copy(uint8, source_position, destination, destination_offset, length);
        } else {
            destination.set(uint8.subarray(source_position, end), destination_offset);
        }

        this.position = end;
    }

    /**
     *
     * @param {string} string
     */
    writeUTF8String(string) {
        // Adapted from https://github.com/samthor/fast-text-encoding/blob/master/text.js
        // Original license is Apache 2.0

        if (string === null) {
            //mark NULL
            this.writeUint32(4294967295);
            //bail, no string data to write
            return;
        } else if (string === undefined) {
            //mark undefined
            this.writeUint32(4294967294);

            return;
        }


        let pos = 0;
        const len = string.length;

        if (len >= 4294967294) {
            throw new Error('String is too long');
        }

        //mark non-NULL
        this.writeUint32(len);

        let cursor = this.position;  // output position

        const expected_byte_size = Math.max(32, len + (len >> 1) + 7);  // 1.5x size

        this.ensureCapacity(expected_byte_size + cursor);

        let target = this.__data_uint8;
        let capacity = this.capacity;

        while (pos < len) {
            let value = string.charCodeAt(pos++);
            if (value >= 0xd800 && value <= 0xdbff) {
                // high surrogate
                if (pos < len) {
                    const extra = string.charCodeAt(pos);
                    if ((extra & 0xfc00) === 0xdc00) {
                        ++pos;
                        value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000;
                    }
                }
                if (value >= 0xd800 && value <= 0xdbff) {
                    continue;  // drop lone surrogate
                }
            }

            // expand the buffer if we couldn't write 4 bytes
            if (cursor + 4 > capacity) {

                this.ensureCapacity(cursor + 4);

                // rebind variables
                capacity = this.capacity;
                target = this.__data_uint8;
            }

            if ((value & 0xffffff80) === 0) {  // 1-byte
                target[cursor++] = value;  // ASCII
                continue;
            } else if ((value & 0xfffff800) === 0) {  // 2-byte
                target[cursor++] = ((value >> 6) & 0x1f) | 0xc0;
            } else if ((value & 0xffff0000) === 0) {  // 3-byte
                target[cursor++] = ((value >> 12) & 0x0f) | 0xe0;
                target[cursor++] = ((value >> 6) & 0x3f) | 0x80;
            } else if ((value & 0xffe00000) === 0) {  // 4-byte
                target[cursor++] = ((value >> 18) & 0x07) | 0xf0;
                target[cursor++] = ((value >> 12) & 0x3f) | 0x80;
                target[cursor++] = ((value >> 6) & 0x3f) | 0x80;
            } else {
                // FIXME: do we care?
                continue;
            }

            target[cursor++] = (value & 0x3f) | 0x80;
        }

        this.position = cursor;
    }

    /**
     *
     * @returns {string}
     */
    readUTF8String() {
        // Adapted from https://github.com/samthor/fast-text-encoding/blob/master/text.js
        // Original license is Apache 2.0

        //check for null
        const stringLength = this.readUint32();

        if (stringLength === 4294967295) {
            //null string
            return null;
        } else if (stringLength === 4294967294) {
            //undefined string
            return undefined;
        }

        const bytes = this.__data_uint8;


        let result = "";

        let i = this.position;

        let charCount = 0;

        while (i < this.capacity && charCount < stringLength) {
            const byte1 = bytes[i++];
            let codePoint;

            if (byte1 === 0) {
                break;  // NULL
            }

            if ((byte1 & 0x80) === 0) {  // 1-byte
                codePoint = byte1;
            } else if ((byte1 & 0xe0) === 0xc0) {  // 2-byte
                const byte2 = bytes[i++] & 0x3f;
                codePoint = (((byte1 & 0x1f) << 6) | byte2);
            } else if ((byte1 & 0xf0) === 0xe0) {
                const byte2 = bytes[i++] & 0x3f;
                const byte3 = bytes[i++] & 0x3f;
                codePoint = (((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3);
            } else if ((byte1 & 0xf8) === 0xf0) {
                const byte2 = bytes[i++] & 0x3f;
                const byte3 = bytes[i++] & 0x3f;
                const byte4 = bytes[i++] & 0x3f;

                // this can be > 0xffff, so possibly generate surrogates
                codePoint = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
                if (codePoint > 0xffff) {
                    // codepoint &= ~0x10000;
                    codePoint -= 0x10000;

                    result += String.fromCharCode((codePoint >>> 10) & 0x3ff | 0xd800);
                    charCount++;

                    codePoint = 0xdc00 | codePoint & 0x3ff;
                }
            } else {
                // FIXME: we're ignoring this
            }

            charCount++;
            result += String.fromCharCode(codePoint);

        }

        this.position = i;

        return result;
    }

    /**
     * Write an ASCII (American Standard Code for Information Interchange) string. If the string contains characters that are not representable by ASCII, an error will be thrown.
     * Note that ASCII only has 128 code points (characters), so this method is not suitable for representing UTF-8 strings.
     * If the string is not ASCII representable - use {@link writeUTF8String} instead.
     *
     * @see https://en.wikipedia.org/wiki/ASCII
     *
     * @param {string} string
     */
    writeASCIIString(string) {
        assert.isString(string, 'string');

        const char_count = string.length;

        const start = this.position;
        const end = start + char_count;

        this.ensureCapacity(end);

        for (let i = 0; i < char_count; i++) {
            const char_code = string.charCodeAt(i);

            if (char_code > 0x80) {
                throw new Error(`Character ${String.fromCharCode(char_code)} can\'t be represented by a US-ASCII byte.`);
            }

            this.__data_uint8[start + i] = char_code;
        }

        this.position = end;
    }

    /**
     * Read ASCII (American Standard Code for Information Interchange) characters to the buffer.
     * Input is not validated, if the string contains non-ASCII characters, the result is unspecified.
     *
     * @see https://en.wikipedia.org/wiki/ASCII
     *
     * @param {number} length maximum number of characters to read. If `null_terminated` flag is on, resulting string might be shorter than `length`
     * @param {boolean} [null_terminated] if true will stop reading when encountering 0 byte value character (NULL)
     * @returns {string}
     */
    readASCIICharacters(length, null_terminated = false) {
        assert.isNonNegativeInteger(length, 'length');
        assert.isBoolean(null_terminated, 'null_terminated');

        let result = "";

        for (let i = 0; i < length; i++) {
            const code = this.readUint8();

            if (null_terminated && code === 0) {
                // null-termination
                break;
            }

            result += String.fromCharCode(code);
        }

        return result;
    }

    /**
     * Represent the object as a string. Useful mainly for debugging.
     *
     * @return {string}
     */
    toString() {
        return `BinaryBuffer[position=${this.position}, capacity=${this.capacity}, endianness=${this.endianness}]`;
    }

    /**
     * Useful for debugging, outputs contents of the buffer in hex format.
     * Only includes data up to the .position
     *
     * @example
     *  const b = new BinaryBuffer();
     *  b.writeUint8(0xCA);
     *  b.writeUint8(0xFE);
     *  b.toHexString(); // "CAFE"
     *
     * @return {string}
     */
    toHexString() {
        const uint8 = this.__data_uint8;
        const end = Math.min(uint8.length, this.position);

        let result = '';

        for (let i = 0; i < end; i++) {
            const byte = uint8[i];
            result += byte.toString(16).padStart(2, '0').toUpperCase();
        }

        return result;
    }

    /**
     *
     * @param {EndianType} type
     * @return {BinaryBuffer}
     */
    static fromEndianness(type) {
        assert.enum(type, EndianType, 'type');

        const r = new BinaryBuffer();

        r.endianness = type;

        return r;
    }

    /**
     *
     * @param {ArrayBuffer} v
     * @return {BinaryBuffer}
     */
    static fromArrayBuffer(v) {
        const r = new BinaryBuffer();

        r.fromArrayBuffer(v);

        return r;
    }

    /**
     *
     * @param {BinaryBuffer} source
     * @param {BinaryBuffer} target
     * @returns {string} Copied value
     */
    static copyUTF8String(source, target) {
        const v = source.readUTF8String();

        target.writeUTF8String(v);

        return v;
    }

    /**
     *
     * @param {BinaryBuffer} source
     * @param {BinaryBuffer} target
     * @returns {number} Copied value
     */
    static copyUintVar(source, target) {
        const v = source.readUintVar();

        target.writeUintVar(v);

        return v;
    }

    /**
     *
     * @param {BinaryBuffer} source
     * @param {BinaryBuffer} target
     * @returns {number} Copied value
     */
    static copyUint8(source, target) {
        const v = source.readUint8();

        target.writeUint8(v);

        return v;
    }

    /**
     *
     * @param {BinaryBuffer} source
     * @param {BinaryBuffer} target
     * @returns {number} Copied value
     */
    static copyUint16(source, target) {
        const v = source.readUint16();

        target.writeUint16(v);

        return v;
    }

    /**
     *
     * @param {BinaryBuffer} source
     * @param {BinaryBuffer} target
     * @returns {number} Copied value
     */
    static copyUint32(source, target) {
        const v = source.readUint32();

        target.writeUint32(v);

        return v;
    }

    /**
     *
     * @param {BinaryBuffer} source
     * @param {BinaryBuffer} target
     * @returns {number} Copied value
     */
    static copyFloat32(source, target) {
        const v = source.readFloat32();

        target.writeFloat32(v);

        return v;
    }

    /**
     *
     * @param {BinaryBuffer} source
     * @param {BinaryBuffer} target
     * @returns {number} Copied value
     */
    static copyFloat64(source, target) {
        const v = source.readFloat64();

        target.writeFloat64(v);

        return v;
    }

    /**
     *
     * @param {BinaryBuffer} source
     * @param {BinaryBuffer} target
     * @param {number} length number of bytes to copy
     * @returns {Uint8Array} Copied data
     */
    static copyBytes(source, target, length) {
        assert.isNonNegativeInteger(length, 'length');
        assert.defined(source, 'source');
        assert.defined(target, 'target');

        const temp = new Uint8Array(length);

        source.readBytes(temp, 0, length);

        target.writeBytes(temp, 0, length);

        return temp;
    }
}

/**
 * @readonly
 * @type {boolean}
 */
BinaryBuffer.prototype.isBinaryBuffer = true;
