﻿import { BinarySchema } from "./binary";

const hexDigits: string = "0123456789abcdef";
const asciiToHex: Array<number> = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0,
    0, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
];

const guidDelimiter: string = "-";
const ticksBetweenEpochs: bigint = 621355968000000000n;
const dateMask: bigint = 0x3fffffffffffffffn;
const emptyByteArray: Uint8Array = new Uint8Array(0);
const emptyString: string = "";
const byteToHex: Array<string> = []; // A lookup table: ['00', '01', ..., 'ff']
for (const x of hexDigits) {
    for (const y of hexDigits) {
        byteToHex.push(x + y);
    }
}

// Cache the check for Crypto.getRandomValues
const hasCryptoGetRandomValues = typeof crypto !== 'undefined' &&
    typeof crypto.getRandomValues === 'function';

export class BebopRuntimeError extends Error {
    constructor(message: string) {
        super(message);
        this.name = "BebopRuntimeError";
    }
}



/**
 * Represents a globally unique identifier (GUID).
 */
export class Guid {
    public static readonly empty: Guid = new Guid("00000000-0000-0000-0000-000000000000");
    /**
       * Constructs a new Guid object with the specified value.
       * @param value The value of the GUID.
       */
    private constructor(private readonly value: string) { }

    /**
     * Gets the string value of the Guid.
     * @returns The string representation of the Guid.
     */
    public toString(): string {
        return this.value;
    }

    /**
      * Checks if the Guid is empty.
      * @returns true if the Guid is empty, false otherwise.
      */
    public isEmpty(): boolean {
        return this.value === Guid.empty.value;
    }

    /**
     * Checks if a value is a Guid.
     * @param value The value to be checked.
     * @returns true if the value is a Guid, false otherwise.
     */
    public static isGuid(value: any): value is Guid {
        return value instanceof Guid;
    }

    /**
    * Parses a string into a Guid.
    * @param value The string to be parsed.
    * @returns A new Guid that represents the parsed value.
    * @throws {BebopRuntimeError} If the input string is not a valid Guid.
    */
    public static parseGuid(value: string): Guid {
        let cleanedInput = '';
        let count = 0;

        // Iterate through each character in the input
        for (let i = 0; i < value.length; i++) {
            let ch = value[i].toLowerCase();
            if (hexDigits.indexOf(ch) !== -1) {
                // If the character is a hexadecimal digit, add it to cleanedInput
                cleanedInput += ch;
                count++;
            } else if (ch !== '-') {
                // If the character is not a hexadecimal digit or a hyphen, it's invalid
                throw new BebopRuntimeError(`Invalid GUID: ${value}`);
            }
        }
        // If the count is not 32, the input is not a valid GUID
        if (count !== 32) {
            throw new BebopRuntimeError(`Invalid GUID: ${value}`);
        }
        // Insert hyphens to make it a 8-4-4-4-12 character pattern
        const guidString =
            cleanedInput.slice(0, 8) + '-' +
            cleanedInput.slice(8, 12) + '-' +
            cleanedInput.slice(12, 16) + '-' +
            cleanedInput.slice(16, 20) + '-' +
            cleanedInput.slice(20);
        // Construct a new Guid object with the generated string and return it
        return new Guid(guidString);
    }

    /**
    * Creates a an insecure new Guid using Math.random.
    * @returns A new Guid.
    */
    public static newGuid(): Guid {
        let guid = "";
        // Obtain a single timestamp to help seed randomness
        const now = Date.now();

        // Iterate through the 36 characters of a UUID
        for (let i = 0; i < 36; i++) {
            // Insert hyphens at the appropriate indices (8, 13, 18, 23)
            if (i === 8 || i === 13 || i === 18 || i === 23) {
                guid += "-";
            }
            // According to the UUID v4 spec, the 14th character should be '4'
            else if (i === 14) {
                guid += "4";
            }
            // According to the UUID v4 spec, the 19th character should be one of '8', '9', 'a', or 'b'.
            // Here we're using 'a' or 'b' to simplify the code
            else if (i === 19) {
                guid += Math.random() > 0.5 ? "a" : "b";
            }
            // Generate the rest of the UUID using random hexadecimal digits
            else {
                // Add the current time to the random number to seed it, then modulo by 16 to get a number between 0 and 15
                // Use bitwise OR 0 to round the result down to an integer, and get the hexadecimal digit from the lookup table
                guid += hexDigits[(Math.random() * 16 + now) % 16 | 0];
            }
        }
        // Construct a new Guid object with the generated string and return it
        return new Guid(guid);
    }

    /**
    * Creates a new cryptographically secure Guid using Crypto.getRandomValues.
    * @returns A new secure Guid.
    * @throws {BebopRuntimeError} If Crypto.getRandomValues is not available.
    */
    public static newSecureGuid(): Guid {
        if (!hasCryptoGetRandomValues) {
            throw new BebopRuntimeError(
                "Crypto.getRandomValues is not available. " +
                "Please include a polyfill or use in an environment that supports it."
            );
        }

        const bytes = new Uint8Array(16);
        crypto.getRandomValues(bytes);

        // Set the version (4) and variant (RFC4122)
        bytes[6] = (bytes[6] & 0x0f) | 0x40;
        bytes[8] = (bytes[8] & 0x3f) | 0x80;

        return Guid.fromBytes(bytes, 0);
    }

    /**
    * Checks if the Guid is equal to another Guid.
    * @param other The other Guid to be compared with.
    * @returns true if the Guids are equal, false otherwise.
    */
    public equals(other: Guid): boolean {
        // Check if both GUIDs are the same instance
        if (this === other) {
            return true;
        }

        // Check if the other object is a GUID
        if (!(other instanceof Guid)) {
            return false;
        }

        // Compare the hexadecimal representations of both GUIDs
        for (let i = 0; i < this.value.length; i++) {
            if (this.value[i] !== other.value[i]) {
                return false;
            }
        }
        // All hexadecimal digits are equal, so the GUIDs are equal
        return true;
    }

    /**
    * Writes the Guid to a DataView.
    * @param view The DataView to write to.
    * @param length The position to start writing at.
    */
    public writeToView(view: DataView, length: number): void {
        var p = 0, a = 0;
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        p += (this.value.charCodeAt(p) === 45) as any;
        view.setUint32(length, a, true);
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        p += (this.value.charCodeAt(p) === 45) as any;
        view.setUint16(length + 4, a, true);
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        p += (this.value.charCodeAt(p) === 45) as any;
        view.setUint16(length + 6, a, true);
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        p += (this.value.charCodeAt(p) === 45) as any;
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        view.setUint32(length + 8, a, false);
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        a = (a << 4) | asciiToHex[this.value.charCodeAt(p++)];
        view.setUint32(length + 12, a, false);
    }

    /**
    * Creates a Guid from a byte array.
    * @param buffer The byte array to create the Guid from.
    * @param index The position in the array to start reading from.
    * @returns A new Guid that represents the byte array.
    */
    public static fromBytes(buffer: Uint8Array, index: number): Guid {
        // Order: 3 2 1 0 - 5 4 - 7 6 - 8 9 - a b c d e f
        var s = byteToHex[buffer[index + 3]];
        s += byteToHex[buffer[index + 2]];
        s += byteToHex[buffer[index + 1]];
        s += byteToHex[buffer[index]];
        s += guidDelimiter;
        s += byteToHex[buffer[index + 5]];
        s += byteToHex[buffer[index + 4]];
        s += guidDelimiter;
        s += byteToHex[buffer[index + 7]];
        s += byteToHex[buffer[index + 6]];
        s += guidDelimiter;
        s += byteToHex[buffer[index + 8]];
        s += byteToHex[buffer[index + 9]];
        s += guidDelimiter;
        s += byteToHex[buffer[index + 10]];
        s += byteToHex[buffer[index + 11]];
        s += byteToHex[buffer[index + 12]];
        s += byteToHex[buffer[index + 13]];
        s += byteToHex[buffer[index + 14]];
        s += byteToHex[buffer[index + 15]];
        return new Guid(s);
    }

    /**
    * Converts the Guid to a string when it's used as a primitive.
    * @returns The string representation of the Guid.
    */
    [Symbol.toPrimitive](hint: string): string {
        if (hint === "string" || hint === "default") {
            return this.toString();
        }
        throw new Error(`Guid cannot be converted to ${hint}`);
    }
}


/**
 * Represents a wrapper around the `Map` class with support for using `Guid` instances as keys.
 *
 * This class is designed to provide a 1:1 mapping between `Guid` instances and values, allowing `Guid` instances to be used as keys in the map.
 * The class handles converting `Guid` instances to their string representation for key storage and retrieval.
 * @remarks this is required because Javascript lacks true reference equality. Thus two `Guid` instances with the same value are not equal.
 */
export class GuidMap<TValue> {
    private readonly map: Map<string, TValue>;

    /**
     * Creates a new GuidMap instance.
     * @param entries - An optional array or iterable containing key-value pairs to initialize the map.
     */
    constructor(
        entries?:
            | readonly (readonly [Guid, TValue])[]
            | null
            | Iterable<readonly [Guid, TValue]>
    ) {
        if (entries instanceof Map) {
            this.map = new Map<string, TValue>(
                entries as unknown as Iterable<[string, TValue]>
            );
        } else if (entries && typeof entries[Symbol.iterator] === "function") {
            this.map = new Map<string, TValue>(
                [...entries].map(([key, value]) => [key.toString(), value])
            );
        } else {
            this.map = new Map<string, TValue>();
        }
    }

    /**
     * Sets the value associated with the specified `Guid` key in the map.
     * @param key The `Guid` key.
     * @param value The value to be set.
     * @returns The updated `GuidMap` instance.
     */
    set(key: Guid, value: TValue): this {
        this.map.set(key.toString(), value);
        return this;
    }

    /**
     * Retrieves the value associated with the specified `Guid` key from the map.
     * @param key The `Guid` key.
     * @returns The associated value, or `undefined` if the key is not found.
     */
    get(key: Guid): TValue | undefined {
        return this.map.get(key.toString());
    }

    /**
     * Deletes the value associated with the specified `Guid` key from the map.
     * @param key The `Guid` key.
     * @returns `true` if the key was found and deleted, or `false` otherwise.
     */
    delete(key: Guid): boolean {
        return this.map.delete(key.toString());
    }

    /**
     * Checks if the map contains the specified `Guid` key.
     * @param key The `Guid` key.
     * @returns `true` if the key is found, or `false` otherwise.
     */
    has(key: Guid): boolean {
        return this.map.has(key.toString());
    }

    /**
     * Removes all entries from the map.
     */
    clear(): void {
        this.map.clear();
    }

    /**
     * Returns the number of entries in the map.
     * @returns The number of entries in the map.
     */
    get size(): number {
        return this.map.size;
    }

    /**
     * Executes the provided callback function once for each key-value pair in the map.
     * @param callbackFn The callback function to execute.
     */
    forEach(
        callbackFn: (value: TValue, key: Guid, map: GuidMap<TValue>) => void
    ): void {
        this.map.forEach((value, keyString) => {
            callbackFn(value, Guid.parseGuid(keyString), this);
        });
    }

    /**
     * Returns an iterator that yields key-value pairs in the map.
     * @returns An iterator for key-value pairs in the map.
     */
    *entries(): Generator<[Guid, TValue]> {
        for (const [keyString, value] of this.map.entries()) {
            yield [Guid.parseGuid(keyString), value];
        }
    }

    /**
     * Returns an iterator that yields the keys of the map.
     * @returns An iterator for the keys of the map.
     */
    *keys(): Generator<Guid> {
        for (const keyString of this.map.keys()) {
            yield Guid.parseGuid(keyString);
        }
    }

    /**
     * Returns an iterator that yields the values in the map.
     * @returns An iterator for the values in the map.
     */
    *values(): Generator<TValue> {
        yield* this.map.values() as Generator<TValue>;
    }

    /**
     * Returns an iterator that yields key-value pairs in the map.
     * This method is invoked when using the spread operator or destructuring the map.
     * @returns An iterator for key-value pairs in the map.
     */
    [Symbol.iterator](): Generator<[Guid, TValue]> {
        return this.entries();
    }

    /**
     * The constructor function used to create derived objects.
     */
    get [Symbol.species](): typeof GuidMap {
        return GuidMap;
    }
}

/**
 * An interface which all generated Bebop interfaces implement.
 * @note this interface is not currently used by the runtime; it is reserved for future use.
 */
export interface BebopRecord {

}
export class BebopView {
    private static textDecoder: TextDecoder;
    private static writeBuffer: Uint8Array = new Uint8Array(256);
    private static writeBufferView: DataView = new DataView(BebopView.writeBuffer.buffer);
    private static instance: BebopView;
    public static getInstance(): BebopView {
        if (!BebopView.instance) {
            BebopView.instance = new BebopView();
        }
        return BebopView.instance;
    }

    minimumTextDecoderLength: number = 300;
    private buffer: Uint8Array;
    private view: DataView;
    index: number; // read pointer
    length: number; // write pointer

    private constructor() {
        this.buffer = BebopView.writeBuffer;
        this.view = BebopView.writeBufferView;
        this.index = 0;
        this.length = 0;
    }

    startReading(buffer: Uint8Array): void {
        this.buffer = buffer;
        this.view = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength);
        this.index = 0;
        this.length = buffer.length;
    }

    startWriting(): void {
        this.buffer = BebopView.writeBuffer;
        this.view = BebopView.writeBufferView;
        this.index = 0;
        this.length = 0;
    }

    private guaranteeBufferLength(length: number): void {
        if (length > this.buffer.length) {
            const data = new Uint8Array(length << 1);
            data.set(this.buffer);
            this.buffer = data;
            this.view = new DataView(data.buffer);
        }
    }

    private growBy(amount: number): void {
        this.length += amount;
        this.guaranteeBufferLength(this.length);
    }

    skip(amount: number) {
        this.index += amount;
    }

    toArray(): Uint8Array {
        return this.buffer.subarray(0, this.length);
    }

    readByte(): number { return this.buffer[this.index++]; }
    readUint16(): number { const result = this.view.getUint16(this.index, true); this.index += 2; return result; }
    readInt16(): number { const result = this.view.getInt16(this.index, true); this.index += 2; return result; }
    readUint32(): number { const result = this.view.getUint32(this.index, true); this.index += 4; return result; }
    readInt32(): number { const result = this.view.getInt32(this.index, true); this.index += 4; return result; }
    readUint64(): bigint { const result = this.view.getBigUint64(this.index, true); this.index += 8; return result; }
    readInt64(): bigint { const result = this.view.getBigInt64(this.index, true); this.index += 8; return result; }
    readFloat32(): number { const result = this.view.getFloat32(this.index, true); this.index += 4; return result; }
    readFloat64(): number { const result = this.view.getFloat64(this.index, true); this.index += 8; return result; }

    writeByte(value: number): void { const index = this.length; this.growBy(1); this.buffer[index] = value; }
    writeUint16(value: number): void { const index = this.length; this.growBy(2); this.view.setUint16(index, value, true); }
    writeInt16(value: number): void { const index = this.length; this.growBy(2); this.view.setInt16(index, value, true); }
    writeUint32(value: number): void { const index = this.length; this.growBy(4); this.view.setUint32(index, value, true); }
    writeInt32(value: number): void { const index = this.length; this.growBy(4); this.view.setInt32(index, value, true); }
    writeUint64(value: bigint): void { const index = this.length; this.growBy(8); this.view.setBigUint64(index, value, true); }
    writeInt64(value: bigint): void { const index = this.length; this.growBy(8); this.view.setBigInt64(index, value, true); }
    writeFloat32(value: number): void { const index = this.length; this.growBy(4); this.view.setFloat32(index, value, true); }
    writeFloat64(value: number): void { const index = this.length; this.growBy(8); this.view.setFloat64(index, value, true); }

    readBytes(): Uint8Array {
        const length = this.readUint32();
        if (length === 0) {
            return emptyByteArray;
        }
        const start = this.index, end = start + length;
        this.index = end;
        return this.buffer.subarray(start, end);
    }

    writeBytes(value: Uint8Array): void {
        const byteCount = value.length;
        this.writeUint32(byteCount);
        if (byteCount === 0) {
            return;
        }
        const index = this.length;
        this.growBy(byteCount);
        this.buffer.set(value, index);
    }

    /**
     * Reads a length-prefixed UTF-8-encoded string.
     */
    readString(): string {
        const lengthBytes = this.readUint32();
        // bail out early on an empty string
        if (lengthBytes === 0) {
            return emptyString;
        }
        if (lengthBytes >= this.minimumTextDecoderLength) {
            if (typeof require !== 'undefined') {
                if (typeof TextDecoder === 'undefined') {
                    throw new BebopRuntimeError("TextDecoder is not defined on 'global'. Please include a polyfill.");
                }
            }
            if (BebopView.textDecoder === undefined) {
                BebopView.textDecoder = new TextDecoder();
            }
            return BebopView.textDecoder.decode(this.buffer.subarray(this.index, this.index += lengthBytes));
        }

        const end = this.index + lengthBytes;
        let result = "";
        let codePoint: number;
        while (this.index < end) {
            // decode UTF-8
            const a = this.buffer[this.index++];
            if (a < 0xC0) {
                codePoint = a;
            } else {
                const b = this.buffer[this.index++];
                if (a < 0xE0) {
                    codePoint = ((a & 0x1F) << 6) | (b & 0x3F);
                } else {
                    const c = this.buffer[this.index++];
                    if (a < 0xF0) {
                        codePoint = ((a & 0x0F) << 12) | ((b & 0x3F) << 6) | (c & 0x3F);
                    } else {
                        const d = this.buffer[this.index++];
                        codePoint = ((a & 0x07) << 18) | ((b & 0x3F) << 12) | ((c & 0x3F) << 6) | (d & 0x3F);
                    }
                }
            }

            // encode UTF-16
            if (codePoint < 0x10000) {
                result += String.fromCharCode(codePoint);
            } else {
                codePoint -= 0x10000;
                result += String.fromCharCode((codePoint >> 10) + 0xD800, (codePoint & ((1 << 10) - 1)) + 0xDC00);
            }
        }

        // Damage control, if the input is malformed UTF-8.
        this.index = end;

        return result;
    }

    /**
     * Writes a length-prefixed UTF-8-encoded string.
     */
    writeString(value: string): void {

        // The number of characters in the string
        const stringLength = value.length;
        // If the string is empty avoid unnecessary allocations by writing the zero length and returning.
        if (stringLength === 0) {
            this.writeUint32(0);
            return;
        }
        // value.length * 3 is an upper limit for the space taken up by the string:
        // https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encodeInto#Buffer_Sizing
        // We add 4 for our length prefix.
        const maxBytes = 4 + stringLength * 3;

        // Reallocate if necessary, then write to this.length + 4.
        this.guaranteeBufferLength(this.length + maxBytes);

        // Start writing the string from here:
        let w = this.length + 4;
        const start = w;

        let codePoint: number;

        for (let i = 0; i < stringLength; i++) {
            // decode UTF-16
            const a = value.charCodeAt(i);
            if (i + 1 === stringLength || a < 0xD800 || a >= 0xDC00) {
                codePoint = a;
            } else {
                const b = value.charCodeAt(++i);
                codePoint = (a << 10) + b + (0x10000 - (0xD800 << 10) - 0xDC00);
            }

            // encode UTF-8
            if (codePoint < 0x80) {
                this.buffer[w++] = codePoint;
            } else {
                if (codePoint < 0x800) {
                    this.buffer[w++] = ((codePoint >> 6) & 0x1F) | 0xC0;
                } else {
                    if (codePoint < 0x10000) {
                        this.buffer[w++] = ((codePoint >> 12) & 0x0F) | 0xE0;
                    } else {
                        this.buffer[w++] = ((codePoint >> 18) & 0x07) | 0xF0;
                        this.buffer[w++] = ((codePoint >> 12) & 0x3F) | 0x80;
                    }
                    this.buffer[w++] = ((codePoint >> 6) & 0x3F) | 0x80;
                }
                this.buffer[w++] = (codePoint & 0x3F) | 0x80;
            }
        }

        // Count how many bytes we wrote.
        const written = w - start;

        // Write the length prefix, then skip over it and the written string.
        this.view.setUint32(this.length, written, true);
        this.length += 4 + written;
    }

    readGuid(): Guid {
        const guid = Guid.fromBytes(this.buffer, this.index);
        this.index += 16;
        return guid;
    }


    writeGuid(value: Guid): void {
        const i = this.length;
        this.growBy(16);
        value.writeToView(this.view, i);
    }

    // A note on these numbers:
    // 62135596800000 ms is the difference between the C# epoch (0001-01-01) and the Unix epoch (1970-01-01).
    // 0.0001 is the number of milliseconds per "tick" (a tick is 100 ns).
    // 429496.7296 is the number of milliseconds in 2^32 ticks.
    // 0x3fffffff is a mask to ignore the "Kind" bits of the Date.ToBinary value.
    // 0x40000000 is a mask to set the "Kind" bits to "DateTimeKind.Utc".

    readDate(): Date {
        const ticks = this.readUint64() & dateMask;
        const ms = (ticks - ticksBetweenEpochs) / 10000n;
        return new Date(Number(ms));
    }

    writeDate(date: Date) {
        const ms = BigInt(date.getTime());
        const ticks = ms * 10000n + ticksBetweenEpochs;
        this.writeUint64(ticks & dateMask);
    }

    /**
     * Reserve some space to write a message's length prefix, and return its index.
     * The length is stored as a little-endian fixed-width unsigned 32-bit integer, so 4 bytes are reserved.
     */
    reserveMessageLength(): number {
        const i = this.length;
        this.growBy(4);
        return i;
    }

    /**
     * Fill in a message's length prefix.
     */
    fillMessageLength(position: number, messageLength: number): void {
        this.view.setUint32(position, messageLength, true);
    }

    /**
     * Read out a message's length prefix.
     */
    readMessageLength(): number {
        const result = this.view.getUint32(this.index, true);
        this.index += 4;
        return result;
    }
}
const typeMarker = '#btype';
const keyMarker = '#ktype';
const mapTag = 1;
const dateTag = 2;
const uint8ArrayTag = 3;
const bigIntTag = 4;
const guidTag = 5;
const mapGuidTag = 6;
const boolTag = 7;
const stringTag = 8;
const numberTag = 9;

const castScalarByTag = (value: any, tag: number): any => {
    switch (tag) {
        case bigIntTag:
            return BigInt(value);
        case boolTag:
            return Boolean(value);
        case stringTag:
            return value;
        case numberTag:
            return Number(value);
        default:
            throw new BebopRuntimeError(`Unknown scalar tag: ${tag}`);
    }
};


/**
 * Determines the tag for the keys of a given map based on the type of the first key.
 * @param map - The map whose key tag is to be determined.
 * @returns The tag for the keys of the map.
 * @throws BebopRuntimeError if the map is empty or if the type of the first key is not a string, number, boolean, or BigInt.
 */
const getMapKeyTag = (map: Map<unknown, unknown>): number => {
    if (map.size === 0) {
        throw new BebopRuntimeError("Cannot determine key type of an empty map.");
    }
    const keyType = typeof map.keys().next().value;
    let keyTag: number;
    switch (keyType) {
        case "string":
            keyTag = stringTag;
            break;
        case "number":
            keyTag = numberTag;
            break;
        case "boolean":
            keyTag = boolTag;
            break;
        case "bigint":
            keyTag = bigIntTag;
            break;
        default:
            throw new BebopRuntimeError(`Not suitable map type tag found. Keys must be strings, numbers, booleans, or BigInts: ${keyType}`);
    }
    return keyTag;
};

/**
 * A custom replacer function for JSON.stringify that supports BigInt, Map,
 * Date, Uint8Array, including BigInt values inside Map and Array.
 * @param _key - The key of the property being stringified.
 * @param value - The value of the property being stringified.
 * @returns The modified value for the property, or the original value if not a BigInt or Map.
 */
const replacer = (_key: string | number, value: any): any => {
    if (value === null) return value;

    switch (typeof value) {
        case 'bigint':
            return { [typeMarker]: bigIntTag, value: value.toString() };
        case 'string':
        case 'number':
        case 'boolean':
            return value;
    }

    if (value instanceof Date) {
        const ms = BigInt(value.getTime());
        const ticks = ms * 10000n + ticksBetweenEpochs;
        return { [typeMarker]: dateTag, value: (ticks & dateMask).toString() };
    }

    if (value instanceof Uint8Array) {
        return { [typeMarker]: uint8ArrayTag, value: Array.from(value) };
    }

    if (value instanceof Guid) {
        return { [typeMarker]: guidTag, value: value.toString() };
    }

    if (value instanceof GuidMap) {
        const obj: Record<any, any> = {};
        for (let [k, v] of value.entries()) {
            obj[k.toString()] = replacer(_key, v);
        }
        return { [typeMarker]: mapGuidTag, value: obj };
    }

    if (value instanceof Map) {
        const obj: Record<any, any> = {};
        let keyTag = getMapKeyTag(value);
        if (keyTag === undefined) {
            throw new BebopRuntimeError("Not suitable map key type tag found.");
        }
        for (let [k, v] of value.entries()) {
            obj[k] = replacer(_key, v);
        }
        return { [typeMarker]: mapTag, [keyMarker]: keyTag, value: obj };
    }

    if (Array.isArray(value)) {
        return value.map((v, i) => replacer(i, v));
    }

    if (typeof value === 'object') {
        const newObj: Record<any, any> = {};
        for (let k in value) {
            newObj[k] = replacer(k, value[k]);
        }
        return newObj;
    }

    return value;
};

/**
 * A custom reviver function for JSON.parse that supports BigInt, Map, Date,
 * Uint8Array, including nested values
 * @param _key - The key of the property being parsed.
 * @param value - The value of the property being parsed.
 * @returns The modified value for the property, or the original value if not a marked type.
 */
const reviver = (_key: string | number, value: any): any => {
    if (_key === "__proto__" || _key === "prototype" || _key === "constructor")
        throw new BebopRuntimeError("potential prototype pollution");
    if (value && typeof value === "object" && !Array.isArray(value)) {
        if (value[typeMarker]) {
            switch (value[typeMarker]) {
                case bigIntTag:
                    return BigInt(value.value);
                case dateTag:
                    const ticks = BigInt(value.value) & dateMask;
                    const ms = (ticks - ticksBetweenEpochs) / 10000n;
                    return new Date(Number(ms));
                case uint8ArrayTag:
                    return new Uint8Array(value.value);
                case mapTag:
                    const keyTag = value[keyMarker];
                    if (keyTag === undefined || keyTag === null) {
                        throw new BebopRuntimeError("Map key type tag not found.");
                    }
                    const map = new Map();
                    for (let k in value.value) {
                        const trueKey = castScalarByTag(k, keyTag);
                        map.set(trueKey, reviver(k, value.value[k]));
                    }
                    return map;
                case guidTag:
                    return Guid.parseGuid(value.value);
                case mapGuidTag:
                    const guidMap = new GuidMap();
                    for (let k in value.value) {
                        guidMap.set(Guid.parseGuid(k), reviver(k, value.value[k]));
                    }
                    return guidMap;
                default:
                    throw new BebopRuntimeError(`Unknown type marker: ${value[typeMarker]}`);
            }
        }
    }
    return value;
};


/**
 * A collection of functions for working with Bebop-encoded JSON.
 */
export const BebopJson = {
    /**
     * A custom replacer function for JSON.stringify that supports BigInt, Map,
     * Date, Uint8Array, including BigInt values inside Map and Array.
     * @param _key - The key of the property being stringified.
     * @param value - The value of the property being stringified.
     * @returns The modified value for the property, or the original value if not a BigInt or Map.
     */
    replacer,

    /**
     * A custom reviver function for JSON.parse that supports BigInt, Map, Date,
     * Uint8Array, including nested values
     * @param _key - The key of the property being parsed.
     * @param value - The value of the property being parsed.
     * @returns The modified value for the property, or the original value if not a marked type.
     */
    reviver,
};

/**
 * Ensures that the given value is a valid boolean.
 * @param value - The value to check.
 * @throws {BebopRuntimeError} - If the value is not a valid boolean.
 */
const ensureBoolean = (value: any): void => {
    if (!(value === false || value === true || value instanceof Boolean || typeof value === "boolean")) {
        throw new BebopRuntimeError(`Invalid value for Boolean: ${value} / typeof ${typeof value}`);
    }
};

/**
 * Ensures that the given value is a valid Uint8 number (0 to 255).
 * @param value - The value to check.
 * @throws {BebopRuntimeError} - If the value is not a valid Uint8 number.
 */
const ensureUint8 = (value: any): void => {
    if (!Number.isInteger(value) || value < 0 || value > 255) {
        throw new BebopRuntimeError(`Invalid value for Uint8: ${value}`);
    }
};


/**
 * Ensures that the given value is a valid Int16 number (-32768 to 32767).
 * @param value - The value to check.
 * @throws {BebopRuntimeError} - If the value is not a valid Int16 number.
 */
const ensureInt16 = (value: any): void => {
    if (!Number.isInteger(value) || value < -32768 || value > 32767) {
        throw new BebopRuntimeError(`Invalid value for Int16: ${value}`);
    }
};

/**
 * Ensures that the given value is a valid Uint16 number (0 to 65535).
 * @param value - The value to check.
 * @throws {BebopRuntimeError} - If the value is not a valid Uint16 number.
 */
const ensureUint16 = (value: any): void => {
    if (!Number.isInteger(value) || value < 0 || value > 65535) {
        throw new BebopRuntimeError(`Invalid value for Uint16: ${value}`);
    }
};
/**
 * Ensures that the given value is a valid Int32 number (-2147483648 to 2147483647).
 * @param value - The value to check.
 * @throws {BebopRuntimeError} - If the value is not a valid Int32 number.
 */
const ensureInt32 = (value: any): void => {
    if (!Number.isInteger(value) || value < -2147483648 || value > 2147483647) {
        throw new BebopRuntimeError(`Invalid value for Int32: ${value}`);
    }
};

/**
* Ensures that the given value is a valid Uint32 number (0 to 4294967295).
* @param value - The value to check.
* @throws {BebopRuntimeError} - If the value is not a valid Uint32 number.
*/
const ensureUint32 = (value: any): void => {
    if (!Number.isInteger(value) || value < 0 || value > 4294967295) {
        throw new BebopRuntimeError(`Invalid value for Uint32: ${value}`);
    }
};
/**
* Ensures that the given value is a valid Int64 number (-9223372036854775808 to 9223372036854775807).
* @param value - The value to check.
* @throws {BebopRuntimeError} - If the value is not a valid Int64 number.
*/
const ensureInt64 = (value: bigint | number): void => {
    const min = BigInt("-9223372036854775808");
    const max = BigInt("9223372036854775807");
    value = BigInt(value);
    if (value < min || value > max) {
        throw new BebopRuntimeError(`Invalid value for Int64: ${value}`);
    }
};
/**
* Ensures that the given value is a valid Uint64 number (0 to 18446744073709551615).
* @param value - The value to check.
* @throws {BebopRuntimeError} - If the value is not a valid Uint64 number.
*/
const ensureUint64 = (value: bigint | number): void => {
    const max = BigInt("18446744073709551615");
    value = BigInt(value);
    if (value < BigInt(0) || value > max) {
        throw new BebopRuntimeError(`Invalid value for Uint64: ${value}`);
    }
};

/**
 * Ensures that the given value is a valid BigInt number.
 * @param value - The value to check.
 * @throws {BebopRuntimeError} - If the value is not a valid BigInt number.
 */
const ensureBigInt = (value: any): void => {
    if (typeof value !== 'bigint') {
        throw new BebopRuntimeError(`Invalid value for BigInt: ${value}`);
    }
};


/**
 * Ensures that the given value is a valid float number.
 * @param value - The value to check.
 * @throws {Error} - If the value is not a valid float number.
 */
const ensureFloat = (value: any): void => {
    if (typeof value !== 'number' || !Number.isFinite(value)) {
        throw new BebopRuntimeError(`Invalid value for Float: ${value}`);
    }
};


/**
 * Ensures that the given value is a valid Map object, with keys and values that pass the specified validators.
 * @param value - The value to check.
 * @param keyTypeValidator - A function that validates the type of each key in the Map.
 * @param valueTypeValidator - A function that validates the type of each value in the Map.
 * @throws {BebopRuntimeError} - If the value is not a valid Map object, or if any key or value fails validation.
 */
const ensureMap = (value: any, keyTypeValidator: (key: any) => void, valueTypeValidator: (value: any) => void): void => {
    if (!(value instanceof Map || value instanceof GuidMap)) {
        throw new BebopRuntimeError(`Invalid value for Map: ${value}`);
    }
    for (let [k, v] of value) {
        keyTypeValidator(k);
        valueTypeValidator(v);
    }
};

/**
 * Ensures that the given value is a valid Array object, with elements that pass the specified validator.
 * @param value - The value to check.
 * @param elementTypeValidator - A function that validates the type of each element in the Array.
 * @throws {BebopRuntimeError} - If the value is not a valid Array object, or if any element fails validation.
 */
const ensureArray = (value: any, elementTypeValidator: (element: any) => void): void => {
    if (!Array.isArray(value)) {
        throw new BebopRuntimeError(`Invalid value for Array: ${value}`);
    }
    for (let element of value) {
        elementTypeValidator(element);
    }
};

/**
 * Ensures that the given value is a valid Date object.
 * @param value - The value to check.
 * @throws {BebopRuntimeError} - If the value is not a valid Date object.
 */
const ensureDate = (value: any): void => {
    if (!(value instanceof Date)) {
        throw new BebopRuntimeError(`Invalid value for Date: ${value}`);
    }
};

/**
 * Ensures that the given value is a valid Uint8Array object.
 * @param value - The value to check.
 * @throws {BebopRuntimeError} - If the value is not a valid Uint8Array object.
 */
const ensureUint8Array = (value: any): void => {
    if (!(value instanceof Uint8Array)) {
        throw new BebopRuntimeError(`Invalid value for Uint8Array: ${value}`);
    }
};

/**
 * Ensures that the given value is a valid string.
 * @param value - The value to check.
 * @throws {BebopRuntimeError} - If the value is not a valid string.
 */
const ensureString = (value: any): void => {
    if (typeof value !== 'string') {
        throw new BebopRuntimeError(`Invalid value for String: ${value}`);
    }
};


/**
 * Ensures that the given value is a valid enum value.
 * @param value - The value to check.
 * @param enumValue - An object representing the enum values.
 * @throws {BebopRuntimeError} - If the value is not a valid enum value.
 */
const ensureEnum = (value: any, enumValue: object): void => {
    if (!Number.isInteger(value)) {
        throw new BebopRuntimeError(`Invalid value for enum, not an int: ${value}`);
    }
    if (!(value in enumValue)) {
        throw new BebopRuntimeError(`Invalid value for enum, not in enum: ${value}`);
    }
};

/**
 * Ensures that the given value is a valid Guid object.
 * @param value - The value to check.
 * @throws {BebopRuntimeError} - If the value is not a valid Guid object.
 */
const ensureGuid = (value: any): void => {
    if (!(value instanceof Guid)) {
        throw new BebopRuntimeError(`Invalid value for Guid: ${value}`);
    }
};


/**
 * This object contains functions for ensuring that values conform to specific types.
 */
export const BebopTypeGuard = {
    /**
     * Ensures that the given value is a valid boolean.
     * @param value - The value to check.
     * @throws {BebopRuntimeError} - If the value is not a valid boolean.
     */
    ensureBoolean,
    /**
     * Ensures that the given value is a valid Uint8 number (0 to 255).
     * @param value - The value to check.
     * @throws {BebopRuntimeError} - If the value is not a valid Uint8 number.
     */
    ensureUint8,
    /**
     * Ensures that the given value is a valid Int16 number (-32768 to 32767).
     * @param value - The value to check.
     * @throws {BebopRuntimeError} - If the value is not a valid Int16 number.
     */
    ensureInt16,
    /**
     * Ensures that the given value is a valid Uint16 number (0 to 65535).
     * @param value - The value to check.
     * @throws {BebopRuntimeError} - If the value is not a valid Uint16 number.
     */
    ensureUint16,
    /**
     * Ensures that the given value is a valid Int32 number (-2147483648 to 2147483647).
     * @param value - The value to check.
     * @throws {BebopRuntimeError} - If the value is not a valid Int32 number.
     */
    ensureInt32,
    /**
     * Ensures that the given value is a valid Uint32 number (0 to 4294967295).
     * @param value - The value to check.
     * @throws {BebopRuntimeError} - If the value is not a valid Uint32 number.
     */
    ensureUint32,
    /**
     * Ensures that the given value is a valid Int64 number (-9223372036854775808 to 9223372036854775807).
     * @param value - The value to check.
     * @throws {BebopRuntimeError} - If the value is not a valid Int64 number.
     */
    ensureInt64,
    /**
     * Ensures that the given value is a valid Uint64 number (0 to 18446744073709551615).
     * @param value - The value to check.
     * @throws {BebopRuntimeError} - If the value is not a valid Uint64 number.
     */
    ensureUint64,
    /**
     * Ensures that the given value is a valid BigInt number.
     * @param value - The value to check.
     * @throws {BebopRuntimeError} - If the value is not a valid BigInt number.
     */
    ensureBigInt,
    /**
     * Ensures that the given value is a valid float number.
     * @param value - The value to check.
     * @throws {BebopRuntimeError} - If the value is not a valid float number.
     */
    ensureFloat,
    /**
     * Ensures that the given value is a valid Map object, with keys and values that pass the specified validators.
     * @param value - The value to check.
     * @param keyTypeValidator - A function that validates the type of each key in the Map.
     * @param valueTypeValidator - A function that validates the type of each value in the Map.
     * @throws {BebopRuntimeError} - If the value is not a valid Map object, or if any key or value fails validation.
     */
    ensureMap,
    /**
     * Ensures that the given value is a valid Array object, with elements that pass the specified validator.
     * @param value - The value to check.
     * @param elementTypeValidator - A function that validates the type of each element in the Array.
     * @throws {BebopRuntimeError} - If the value is not a valid Array object, or if any element fails validation.
     */
    ensureArray,
    /**
     * Ensures that the given value is a valid Date object.
     * @param value - The value to check.
     * @throws {BebopRuntimeError} - If the value is not a valid Date object.
     */
    ensureDate,
    /**
     * Ensures that the given value is a valid Uint8Array object.
     * @param value - The value to check.
     * @throws {BebopRuntimeError} - If the value is not a valid Uint8Array object.
     */
    ensureUint8Array,
    /**
     * Ensures that the given value is a valid string.
     * @param value - The value to check.
     * @throws {BebopRuntimeError} - If the value is not a valid string.
     */
    ensureString,
    /**
     * Ensures that the given value is a valid enum value.
     * @param value - The value to check.
     * @param enumValues - An array of valid enum values.
     * @throws {BebopRuntimeError} - If the value is not a valid enum value.
     */
    ensureEnum,
    /**
     * Ensures that the given value is a valid GUID string.
     * @param value - The value to check.
     * @throws {BebopRuntimeError} - If the value is not a valid GUID string.
     */
    ensureGuid
};

export { BinarySchema };