import ErrorWithStatus, { HttpStatusCode } from "./errorWithStatus.js";
import * as base64 from "./base64.js";
import type { LoggerWrapper } from "./logging/index.browser.js";

export type Remote<T> = { [key in keyof T]: T[key] extends (...args) => infer X ? X extends Promise<unknown> ? X : Promise<X> : (T[key] | undefined) }
export type Serializable = string | number | string[] | number[] | boolean | boolean[] | SerializableObject | SerializableObject[];
export type TypedSerializable<T> = T extends Array<infer U> ? TypedSerializable<U>[] : string | number | boolean | TypedSerializableObject<T>;
export type SerializableObject = { [key: string]: Serializable };
export type TypedSerializableObject<T> = { [key in keyof T]: TypedSerializable<T> };

export interface Context<TState = unknown>
{
    state?: TState;
    logger: LoggerWrapper;
    abort: AbortController;
}

export let defaultContext: Context;

export function setDefaultContext(context: Context)
{
    defaultContext = context;
}

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function
export function noop() { }

export function lazy<T>(factory: () => T)
{
    let instance: T;
    return function ()
    {
        return instance || (instance = factory());
    }
}

export function spread<A>(a: A): A
export function spread<A, B>(a: A, b: B): A & B
export function spread<A, B, C>(a: A, b: B, c: C): A & B & C
export function spread<A, B, C, D>(a: A, b: B, c: C, d: D): A & B & C & D
export function spread<A, B, C, D, E>(a: A, b: B, c: C, d: D, e: E): A & B & C & D & E
export function spread(...args: object[]): object
export function spread(...args: object[]): object
{
    const result = {};
    for (let i = 0; i < args.length; i++)
    {
        const element = args[i];
        const descriptors = Object.getOwnPropertyDescriptors(element);
        Object.defineProperties(result, descriptors);
    }

    return result;
}

export interface Translator
{
    locale: string | Intl.Locale;
    translate(key: string): string;
    translate(format: string, ...parameters: unknown[]): string;
    translate(obj: { key: string, fallback: string }): string;
    translate(obj: { key: string, fallback: string }, ...parameters: unknown[]): string;
}


export type BufferEncoding =
    | "ascii"
    | "utf8"
    | "utf-8"
    | "base64"
    | "base64url"
    | "binary"
    | "hex";

export class IsomorphicBuffer implements Iterable<number, number, number>
{
    private readonly buffer: Uint8Array<ArrayBuffer>
    constructor(buffer: Uint8Array<ArrayBufferLike> | number | number[], private readonly offset?: number, private readonly end?: number)
    {
        if (typeof buffer == 'number')
            this.buffer = new Uint8Array(buffer);
        else if (Array.isArray(buffer))
            this.buffer = new Uint8Array(buffer);
        else
            this.buffer = buffer as Uint8Array<ArrayBuffer>;
        if (!offset)
            this.offset = 0;
        if (!end)
            this.end = this.buffer.byteLength;
        this.length = this.end - this.offset;
    }

    public readonly length: number;

    public static fromBuffer(buffer: Uint8Array<ArrayBufferLike>)
    {
        return new IsomorphicBuffer(buffer)
    }

    public static fromArrayBuffer(abuffer: ArrayBufferLike)
    {
        const buffer = new Uint8Array(abuffer.byteLength);
        const view = new DataView(abuffer);
        for (let i = 0; i < abuffer.byteLength; i++)
        {
            buffer[i] = view.getUint8(i);

        }
        return new IsomorphicBuffer(buffer)
    }

    public copy(source: IsomorphicBuffer, offset: number, sourceOffset: number = 0, length?: number)
    {
        if (length === 0 || !length && source.length == 0)
            return;
        offset = this.ensureOffset(offset);
        if (sourceOffset == 0 && (typeof length == 'undefined' || length === source.length))
            this.buffer.set(source.toArray(), offset);
        else
            this.buffer.set(source.subarray(sourceOffset, sourceOffset + length).toArray(), offset);
    }
    toArray(): Uint8Array<ArrayBuffer>
    {
        return this.buffer.slice(this.offset, this.end);
    }

    public indexOf(value: number, offset: number = 0)
    {
        return this.buffer.indexOf(value, offset + this.offset) - this.offset;
    }

    public static concat(buffers: IsomorphicBuffer[])
    {
        const totalLength = buffers.reduce((previous, current) => previous + current?.length, 0);
        const target = new IsomorphicBuffer(totalLength);
        let offset = 0;
        for (const buffer of buffers)
        {
            if (!buffer?.length)
                continue;
            target.copy(buffer, offset)
            offset += buffer.length;
        }
        return target;
    }

    public static getInitLength(s: string, encoding: BufferEncoding = 'utf8'): number
    {
        switch (encoding)
        {
            case "ascii":
                return s.length;
            case "utf8":
            case "utf-8":
                return base64.strUTF8ByteLength(s);
            case "base64":
            case "base64url":
                return base64.base64ByteLength(s);
            case "hex":
                return s.length / 2;
            case "binary":
                return s.length;
        }
    }

    public static from(s: string, encoding: BufferEncoding = 'utf8'): IsomorphicBuffer
    {
        switch (encoding)
        {
            case "ascii":
                {
                    const result = new Uint8Array(s.length);
                    for (let i = 0; i < s.length; i++)
                        result[i] = s.charCodeAt(i);
                    return new IsomorphicBuffer(result);
                }
            case "utf8":
            case "utf-8":
                return new IsomorphicBuffer(base64.strToUTF8Arr(s));
            case "base64":
                return new IsomorphicBuffer(base64.base64DecToArr(s));
            case "hex":
                {
                    const result = new Uint8Array(s.length / 2);
                    for (let i = 0; i < s.length; i++)
                        switch (s[i])
                        {
                            case '0':
                                break;
                            case '1':
                                result[i / 2] += i % 2 === 0 ? 0x10 : 0x1;
                                break;
                            case '2':
                                result[i / 2] += i % 2 === 0 ? 0x20 : 0x2;
                                break;
                            case '3':
                                result[i / 2] += i % 2 === 0 ? 0x30 : 0x3;
                                break;
                            case '4':
                                result[i / 2] += i % 2 === 0 ? 0x40 : 0x4;
                                break;
                            case '5':
                                result[i / 2] += i % 2 === 0 ? 0x50 : 0x5;
                                break;
                            case '6':
                                result[i / 2] += i % 2 === 0 ? 0x60 : 0x6;
                                break;
                            case '7':
                                result[i / 2] += i % 2 === 0 ? 0x70 : 0x7;
                                break;
                            case '8':
                                result[i / 2] += i % 2 === 0 ? 0x80 : 0x8;
                                break;
                            case '9':
                                result[i / 2] += i % 2 === 0 ? 0x90 : 0x9;
                                break;
                            case 'A':
                            case 'a':
                                result[i / 2] += i % 2 === 0 ? 0xa0 : 0xa;
                                break;
                            case 'B':
                            case 'b':
                                result[i / 2] += i % 2 === 0 ? 0xB0 : 0xB;
                                break;
                            case 'C':
                            case 'c':
                                result[i / 2] += i % 2 === 0 ? 0xC0 : 0xc;
                                break;
                            case 'D':
                            case 'd':
                                result[i / 2] += i % 2 === 0 ? 0xD0 : 0xD;
                                break;
                            case 'E':
                            case 'e':
                                result[i / 2] += i % 2 === 0 ? 0xE0 : 0xE;
                                break;
                            case 'F':
                            case 'f':
                                result[i / 2] += i % 2 === 0 ? 0xF0 : 0xF;
                                break;
                            default:
                                throw new ErrorWithStatus(HttpStatusCode.BadRequest);
                        }
                    return new IsomorphicBuffer(result)
                }
            case "base64url":
                return new IsomorphicBuffer(base64.base64UrlDecToArr(s));
            case "binary":
                return new IsomorphicBuffer(base64.strToUTF8Arr(s));
        }
    }

    public toString(encoding: BufferEncoding, offset?: number, end?: number): string
    {
        if (offset)
            return this.subarray(offset, end).toString(encoding);

        if (!end)
            end = this.length;

        switch (encoding)
        {
            case "ascii": {
                end += this.offset;
                const result = new Array<string>(end - this.offset);
                for (let i = this.offset; i < end; i++)
                    result[i - this.offset] = String.fromCharCode(this.buffer[i]);
                return result.join('');
            }
            case "utf8":
            case "utf-8":
                return base64.UTF8IsomorphicBufferToStr(this);
            case "base64":
                return base64.base64EncIsomorphicBuffer(this);
            case "hex":
                const result = new Array<string>(this.length);
                for (let i = 0; i < this.length; i++)
                    result[i] = this.buffer[i].toString(16);

                return result.join('');
            case "base64url":
                return base64.base64UrlEncIsomorphicBuffer(this);
            case "binary":
                return base64.UTF8IsomorphicBufferToStr(this);
        }
    }

    public toJSON()
    {
        return {
            type: 'Buffer' as const,
            data: Array.from(this.buffer.subarray(this.offset, this.end))
        };
    }

    public write(s: string, offset: number, length?: number, encoding?: BufferEncoding)
    {
        if (typeof length === 'undefined')
            length = s.length;

        if (length !== s.length)
            return this.write(s.substring(0, length), offset, undefined, encoding);

        this.copy(IsomorphicBuffer.from(s, encoding), offset);
    }

    private ensureOffset(offset?: number, length: number = 1)
    {
        // console.log(`${offset} + ${length} = ${offset + length} <= ${this.end}`)
        if (typeof offset == 'undefined')
            offset = 0;
        offset += this.offset;
        if (offset < this.offset || offset + length > this.end)
            throw new Error('Out of limits')
        return offset;
    }

    public fill(value: number, start?: number, end?: number)
    {
        start = this.ensureOffset(start, end - start);
        end = this.ensureOffset(typeof end === 'undefined' ? this.length : end, 0);
        this.buffer.fill(value, start, end)
    }

    public readInt8(index: number = 0): number
    {
        index = this.ensureOffset(index);
        const val = this.buffer[index];
        return val & 0x80 ? val - 0x100 : val;
    }

    public writeInt8(value: number, index: number = 0)
    {
        index = this.ensureOffset(index);
        this.buffer[index] = value & 0xff;
    }

    public readUInt8(index: number = 0)
    {
        index = this.ensureOffset(index);
        return this.buffer[index];
    }

    public writeUInt8(value: number, index: number = 0)
    {
        index = this.ensureOffset(index);
        this.buffer[index] = value & 0xff;
    }

    public readDoubleBE(index: number = 0): number
    {
        index = this.ensureOffset(index, 8);

        const highWord = (this.buffer[index] << 24) |
            (this.buffer[index + 1] << 16) |
            (this.buffer[index + 2] << 8) |
            this.buffer[index + 3];

        const lowWord = (this.buffer[index + 4] << 24) |
            (this.buffer[index + 5] << 16) |
            (this.buffer[index + 6] << 8) |
            this.buffer[index + 7];

        // Combine into 64-bit value
        const bits = BigInt(highWord) * BigInt(0x100000000) + BigInt(lowWord >>> 0);

        // Handle special cases
        if (bits === BigInt(0)) return 0;

        const sign = ((highWord >>> 31) & 0x1) ? -1 : 1;
        const exponent = ((highWord >>> 20) & 0x7FF) - 1023;
        const fraction = Number((bits & BigInt(0xFFFFFFFFFFFFF)) | BigInt(0x10000000000000));

        return sign * fraction * Math.pow(2, exponent - 52);
    }

    public writeDoubleBE(value: number, index: number = 0)
    {
        index = this.ensureOffset(index, 8);

        // Handle special cases
        if (value === 0)
        {
            for (let i = 0; i < 8; i++)
            {
                this.buffer[index + i] = 0;
            }
            return;
        }
        if (!Number.isFinite(value))
        {
            if (value === Infinity)
            {
                this.buffer[index] = 0x7F;
                this.buffer[index + 1] = 0xF0;
                this.buffer[index + 2] = 0;
                this.buffer[index + 3] = 0;
                this.buffer[index + 4] = 0;
                this.buffer[index + 5] = 0;
                this.buffer[index + 6] = 0;
                this.buffer[index + 7] = 0;
                return;
            }
            if (value === -Infinity)
            {
                this.buffer[index] = 0xFF;
                this.buffer[index + 1] = 0xF0;
                this.buffer[index + 2] = 0;
                this.buffer[index + 3] = 0;
                this.buffer[index + 4] = 0;
                this.buffer[index + 5] = 0;
                this.buffer[index + 6] = 0;
                this.buffer[index + 7] = 0;
                return;
            }
            // NaN
            this.buffer[index] = 0x7F;
            this.buffer[index + 1] = 0xF8;
            this.buffer[index + 2] = 0;
            this.buffer[index + 3] = 0;
            this.buffer[index + 4] = 0;
            this.buffer[index + 5] = 0;
            this.buffer[index + 6] = 0;
            this.buffer[index + 7] = 0;
            return;
        }

        const sign = value < 0 ? 1 : 0;
        value = Math.abs(value);

        let exponent = Math.floor(Math.log2(value));
        let fraction = value * Math.pow(2, -exponent) - 1;

        exponent += 1023;
        fraction = Math.round(fraction * 0x10000000000000);

        const low = Number(BigInt.asIntN(32, BigInt(fraction)));
        const high = Number(BigInt.asIntN(32, BigInt(fraction) >> BigInt(32))) | (exponent << 20) | (sign << 31);

        this.buffer[index] = (high >>> 24) & 0xFF;
        this.buffer[index + 1] = (high >>> 16) & 0xFF;
        this.buffer[index + 2] = (high >>> 8) & 0xFF;
        this.buffer[index + 3] = high & 0xFF;
        this.buffer[index + 4] = (low >>> 24) & 0xFF;
        this.buffer[index + 5] = (low >>> 16) & 0xFF;
        this.buffer[index + 6] = (low >>> 8) & 0xFF;
        this.buffer[index + 7] = low & 0xFF;
    }

    public readDoubleLE(index: number = 0): number
    {
        index = this.ensureOffset(index, 8);

        const lowWord = this.buffer[index] |
            (this.buffer[index + 1] << 8) |
            (this.buffer[index + 2] << 16) |
            (this.buffer[index + 3] << 24);

        const highWord = this.buffer[index + 4] |
            (this.buffer[index + 5] << 8) |
            (this.buffer[index + 6] << 16) |
            (this.buffer[index + 7] << 24);

        // Combine into 64-bit value
        const bits = BigInt(highWord) * BigInt(0x100000000) + BigInt(lowWord >>> 0);

        // Handle special cases
        if (bits === BigInt(0)) return 0;

        const sign = ((highWord >>> 31) & 0x1) ? -1 : 1;
        const exponent = ((highWord >>> 20) & 0x7FF) - 1023;
        const fraction = Number((bits & BigInt(0xFFFFFFFFFFFFF)) | BigInt(0x10000000000000));

        return sign * fraction * Math.pow(2, exponent - 52);
    }

    public writeDoubleLE(value: number, index: number = 0)
    {
        index = this.ensureOffset(index, 8);

        // Handle special cases
        if (value === 0)
        {
            for (let i = 0; i < 8; i++)
            {
                this.buffer[index + i] = 0;
            }
            return;
        }
        if (!Number.isFinite(value))
        {
            if (value === Infinity)
            {
                this.buffer[index] = 0;
                this.buffer[index + 1] = 0;
                this.buffer[index + 2] = 0;
                this.buffer[index + 3] = 0;
                this.buffer[index + 4] = 0;
                this.buffer[index + 5] = 0;
                this.buffer[index + 6] = 0xF0;
                this.buffer[index + 7] = 0x7F;
                return;
            }
            if (value === -Infinity)
            {
                this.buffer[index] = 0;
                this.buffer[index + 1] = 0;
                this.buffer[index + 2] = 0;
                this.buffer[index + 3] = 0;
                this.buffer[index + 4] = 0;
                this.buffer[index + 5] = 0;
                this.buffer[index + 6] = 0xF0;
                this.buffer[index + 7] = 0xFF;
                return;
            }
            // NaN
            this.buffer[index] = 0;
            this.buffer[index + 1] = 0;
            this.buffer[index + 2] = 0;
            this.buffer[index + 3] = 0;
            this.buffer[index + 4] = 0;
            this.buffer[index + 5] = 0;
            this.buffer[index + 6] = 0xF8;
            this.buffer[index + 7] = 0x7F;
            return;
        }

        const sign = value < 0 ? 1 : 0;
        value = Math.abs(value);

        let exponent = Math.floor(Math.log2(value));
        let fraction = value * Math.pow(2, -exponent) - 1;

        exponent += 1023;
        fraction = Math.round(fraction * 0x10000000000000);

        const low = Number(BigInt.asIntN(32, BigInt(fraction)));
        const high = Number(BigInt.asIntN(32, BigInt(fraction) >> BigInt(32))) | (exponent << 20) | (sign << 31);

        this.buffer[index] = low & 0xFF;
        this.buffer[index + 1] = (low >>> 8) & 0xFF;
        this.buffer[index + 2] = (low >>> 16) & 0xFF;
        this.buffer[index + 3] = (low >>> 24) & 0xFF;
        this.buffer[index + 4] = high & 0xFF;
        this.buffer[index + 5] = (high >>> 8) & 0xFF;
        this.buffer[index + 6] = (high >>> 16) & 0xFF;
        this.buffer[index + 7] = (high >>> 24) & 0xFF;
    }

    public readFloatBE(index: number = 0): number
    {
        index = this.ensureOffset(index, 4);
        const bytes = (this.buffer[index] << 24) |
            (this.buffer[index + 1] << 16) |
            (this.buffer[index + 2] << 8) |
            this.buffer[index + 3];

        // Handle special cases
        if (bytes === 0) return 0;
        if (bytes === 0x7F800000) return Infinity;
        if (bytes === 0xFF800000) return -Infinity;
        if ((bytes & 0x7F800000) === 0x7F800000) return NaN;

        const sign = bytes >>> 31 ? -1 : 1;
        const exponent = ((bytes >>> 23) & 0xFF) - 127;
        const fraction = (bytes & 0x7FFFFF) | 0x800000;

        return sign * fraction * Math.pow(2, exponent - 23);
    }

    public writeFloatBE(value: number, index: number = 0)
    {
        index = this.ensureOffset(index, 4);

        // Handle special cases
        if (value === 0)
        {
            this.buffer[index] = 0;
            this.buffer[index + 1] = 0;
            this.buffer[index + 2] = 0;
            this.buffer[index + 3] = 0;
            return;
        }
        if (!Number.isFinite(value))
        {
            if (value === Infinity)
            {
                this.buffer[index] = 0x7F;
                this.buffer[index + 1] = 0x80;
                this.buffer[index + 2] = 0;
                this.buffer[index + 3] = 0;
                return;
            }
            if (value === -Infinity)
            {
                this.buffer[index] = 0xFF;
                this.buffer[index + 1] = 0x80;
                this.buffer[index + 2] = 0;
                this.buffer[index + 3] = 0;
                return;
            }
            // NaN
            this.buffer[index] = 0x7F;
            this.buffer[index + 1] = 0xC0;
            this.buffer[index + 2] = 0;
            this.buffer[index + 3] = 0;
            return;
        }

        const sign = value < 0 ? 1 : 0;
        value = Math.abs(value);
        let exponent = Math.floor(Math.log2(value));
        let fraction = value * Math.pow(2, -exponent) - 1;

        exponent += 127;
        fraction = Math.round(fraction * 0x800000);

        const bytes = (sign << 31) | (exponent << 23) | fraction;

        this.buffer[index] = (bytes >>> 24) & 0xFF;
        this.buffer[index + 1] = (bytes >>> 16) & 0xFF;
        this.buffer[index + 2] = (bytes >>> 8) & 0xFF;
        this.buffer[index + 3] = bytes & 0xFF;
    }

    public readFloatLE(index: number = 0): number
    {
        index = this.ensureOffset(index, 4);
        const bytes = this.buffer[index] |
            (this.buffer[index + 1] << 8) |
            (this.buffer[index + 2] << 16) |
            (this.buffer[index + 3] << 24);

        // Handle special cases
        if (bytes === 0) return 0;
        if (bytes === 0x7F800000) return Infinity;
        if (bytes === 0xFF800000) return -Infinity;
        if ((bytes & 0x7F800000) === 0x7F800000) return NaN;

        const sign = bytes >>> 31 ? -1 : 1;
        const exponent = ((bytes >>> 23) & 0xFF) - 127;
        const fraction = (bytes & 0x7FFFFF) | 0x800000;

        return sign * fraction * Math.pow(2, exponent - 23);
    }

    public writeFloatLE(value: number, index: number = 0)
    {
        index = this.ensureOffset(index, 4);

        // Handle special cases
        if (value === 0)
        {
            this.buffer[index] = 0;
            this.buffer[index + 1] = 0;
            this.buffer[index + 2] = 0;
            this.buffer[index + 3] = 0;
            return;
        }
        if (!Number.isFinite(value))
        {
            if (value === Infinity)
            {
                this.buffer[index] = 0;
                this.buffer[index + 1] = 0;
                this.buffer[index + 2] = 0x80;
                this.buffer[index + 3] = 0x7F;
                return;
            }
            if (value === -Infinity)
            {
                this.buffer[index] = 0;
                this.buffer[index + 1] = 0;
                this.buffer[index + 2] = 0x80;
                this.buffer[index + 3] = 0xFF;
                return;
            }
            // NaN
            this.buffer[index] = 0;
            this.buffer[index + 1] = 0;
            this.buffer[index + 2] = 0xC0;
            this.buffer[index + 3] = 0x7F;
            return;
        }

        const sign = value < 0 ? 1 : 0;
        value = Math.abs(value);
        let exponent = Math.floor(Math.log2(value));
        let fraction = value * Math.pow(2, -exponent) - 1;

        exponent += 127;
        fraction = Math.round(fraction * 0x800000);

        const bytes = (sign << 31) | (exponent << 23) | fraction;

        this.buffer[index] = bytes & 0xFF;
        this.buffer[index + 1] = (bytes >>> 8) & 0xFF;
        this.buffer[index + 2] = (bytes >>> 16) & 0xFF;
        this.buffer[index + 3] = (bytes >>> 24) & 0xFF;
    }

    public readUInt16LE(index: number = 0): number
    {
        index = this.ensureOffset(index, 2);
        return this.buffer[index] | (this.buffer[index + 1] << 8);
    }

    public writeUInt16LE(value: number, index: number = 0)
    {
        index = this.ensureOffset(index, 2);
        this.buffer[index] = value & 0xff;
        this.buffer[index + 1] = (value >>> 8) & 0xff;
    }

    public readUInt16BE(index: number = 0): number
    {
        index = this.ensureOffset(index, 2);
        return (this.buffer[index] << 8) | this.buffer[index + 1];
    }

    public writeUInt16BE(value: number, index: number = 0)
    {
        index = this.ensureOffset(index, 2);
        this.buffer[index] = (value >>> 8) & 0xff;
        this.buffer[index + 1] = value & 0xff;
    }

    public readInt16LE(index: number = 0): number
    {
        index = this.ensureOffset(index, 2);
        const val = this.buffer[index] | (this.buffer[index + 1] << 8);
        return val & 0x8000 ? val - 0x10000 : val;
    }

    public writeInt16LE(value: number, index: number = 0)
    {
        index = this.ensureOffset(index, 2);
        this.buffer[index] = value & 0xff;
        this.buffer[index + 1] = (value >>> 8) & 0xff;
    }

    public readInt16BE(index: number = 0): number
    {
        index = this.ensureOffset(index, 2);
        const val = (this.buffer[index] << 8) | this.buffer[index + 1];
        return val & 0x8000 ? val - 0x10000 : val;
    }

    public writeInt16BE(value: number, index: number = 0)
    {
        index = this.ensureOffset(index, 2);
        this.buffer[index] = (value >>> 8) & 0xff;
        this.buffer[index + 1] = value & 0xff;
    }

    public readUInt32LE(index: number = 0): number
    {
        index = this.ensureOffset(index, 4);
        return ((this.buffer[index + 3] << 24) >>> 0) +
            ((this.buffer[index + 2] << 16) |
                (this.buffer[index + 1] << 8) |
                this.buffer[index]);
    }

    public writeUInt32LE(value: number, index: number = 0)
    {
        index = this.ensureOffset(index, 4);
        this.buffer[index + 3] = (value >>> 24) & 0xff;
        this.buffer[index + 2] = (value >>> 16) & 0xff;
        this.buffer[index + 1] = (value >>> 8) & 0xff;
        this.buffer[index] = value & 0xff;
    }

    public readUInt32BE(index: number = 0): number
    {
        index = this.ensureOffset(index, 4);
        return ((this.buffer[index] << 24) >>> 0) +
            ((this.buffer[index + 1] << 16) |
                (this.buffer[index + 2] << 8) |
                this.buffer[index + 3]);
    }

    public writeUInt32BE(value: number, index: number = 0)
    {
        index = this.ensureOffset(index, 4);
        this.buffer[index] = (value >>> 24) & 0xff;
        this.buffer[index + 1] = (value >>> 16) & 0xff;
        this.buffer[index + 2] = (value >>> 8) & 0xff;
        this.buffer[index + 3] = value & 0xff;
    }

    public readInt32BE(index: number = 0): number
    {
        index = this.ensureOffset(index, 4);
        return (this.buffer[index] << 24) |
            (this.buffer[index + 1] << 16) |
            (this.buffer[index + 2] << 8) |
            this.buffer[index + 3];
    }

    public writeInt32BE(value: number, index: number = 0)
    {
        index = this.ensureOffset(index, 4);
        this.buffer[index] = (value >>> 24) & 0xff;
        this.buffer[index + 1] = (value >>> 16) & 0xff;
        this.buffer[index + 2] = (value >>> 8) & 0xff;
        this.buffer[index + 3] = value & 0xff;
    }

    public readInt32LE(index: number = 0): number
    {
        index = this.ensureOffset(index, 4);
        return this.buffer[index] |
            (this.buffer[index + 1] << 8) |
            (this.buffer[index + 2] << 16) |
            (this.buffer[index + 3] << 24);
    }

    public writeInt32LE(value: number, index: number = 0)
    {
        index = this.ensureOffset(index, 4);
        this.buffer[index] = value & 0xff;
        this.buffer[index + 1] = (value >>> 8) & 0xff;
        this.buffer[index + 2] = (value >>> 16) & 0xff;
        this.buffer[index + 3] = (value >>> 24) & 0xff;
    }

    public readBigUInt64LE(index: number = 0): bigint
    {
        index = this.ensureOffset(index, 8);
        const lo = this.readUInt32LE(index);
        const hi = this.readUInt32LE(index + 4);
        return (BigInt(hi) << BigInt(32)) | BigInt(lo);
    }

    public writeBigUInt64LE(value: bigint, index: number = 0)
    {
        index = this.ensureOffset(index, 8);
        const lo = Number(value & BigInt(0xFFFFFFFF));
        const hi = Number(value >> BigInt(32));
        this.writeUInt32LE(lo, index);
        this.writeUInt32LE(hi, index + 4);
    }

    public readBigUInt64BE(index: number = 0): bigint
    {
        index = this.ensureOffset(index, 8);
        const hi = this.readUInt32BE(index);
        const lo = this.readUInt32BE(index + 4);
        return (BigInt(hi) << BigInt(32)) | BigInt(lo);
    }

    public writeBigUInt64BE(value: bigint, index: number = 0)
    {
        index = this.ensureOffset(index, 8);
        const lo = Number(value & BigInt(0xFFFFFFFF));
        const hi = Number(value >> BigInt(32));
        this.writeUInt32BE(hi, index);
        this.writeUInt32BE(lo, index + 4);
    }

    public readBigInt64LE(index: number = 0): bigint
    {
        const val = this.readBigUInt64LE(index);
        return BigInt.asIntN(64, val);
    }

    public writeBigInt64LE(value: bigint, index: number = 0)
    {
        this.writeBigUInt64LE(BigInt.asUintN(64, value), index);
    }

    public readBigInt64BE(index: number = 0): bigint
    {
        const val = this.readBigUInt64BE(index);
        return BigInt.asIntN(64, val);
    }

    public writeBigInt64BE(value: bigint, index: number = 0)
    {
        this.writeBigUInt64BE(BigInt.asUintN(64, value), index);
    }

    public readUIntLE(index: number, byteLength: number): number
    {
        index = this.ensureOffset(index, byteLength);
        let val = this.buffer[index];
        let mul = 1;

        for (let i = 0; i < byteLength - 1; i++)
        {
            mul *= 0x100;
            val += this.buffer[index + i + 1] * mul;
        }

        return val;
    }

    public writeUIntLE(value: number, index: number, byteLength: number): void
    {
        index = this.ensureOffset(index, byteLength);
        let remaining = value;

        for (let i = 0; i < byteLength; i++)
        {
            this.buffer[index + i] = remaining & 0xFF;
            remaining = Math.floor(remaining / 256);
        }
    }

    public readUIntBE(index: number, byteLength: number): number
    {
        index = this.ensureOffset(index, byteLength);
        let val = this.buffer[index + byteLength - 1];
        let mul = 1;

        for (let i = byteLength - 1; i > 0; i--)
        {
            mul *= 0x100;
            val += this.buffer[index + i - 1] * mul;
        }

        return val;
    }

    public writeUIntBE(value: number, index: number, byteLength: number): void
    {
        index = this.ensureOffset(index, byteLength);
        let remaining = value;

        for (let i = byteLength - 1; i >= 0; i--)
        {
            this.buffer[index + i] = remaining & 0xFF;
            remaining = Math.floor(remaining / 256);
        }
    }

    public subarray(start: number, end?: number)
    {
        if (typeof end == 'undefined')
            end = this.end - this.offset;
        if (start == 0 && end == this.end)
            return this;
        if (end < start)
            throw new Error('end is before start');
        if (start < 0 || this.offset + end > this.end)
            throw new Error('Out of limits');
        return new IsomorphicBuffer(this.buffer, this.offset + start, this.offset + end);
    }

    [Symbol.iterator](): Iterator<number, number, number>
    {
        return this.buffer[Symbol.iterator]();
    }

    [Symbol.for('nodejs.util.inspect.custom')](): { type: 'Buffer'; data: number[] }
    {
        return {
            type: 'Buffer',
            data: Array.from(this.buffer.subarray(this.offset, this.end))
        };
    }

    // This is used by util.inspect and assert.deepStrictEqual
    inspect()
    {
        return this[Symbol.for('nodejs.util.inspect.custom')]();
    }

    // This is used by assert.deepStrictEqual for comparison
    [Symbol.for('nodejs.util.inspect.custom.primitive')]()
    {
        return this.toJSON()
    }

    valueOf(): { type: 'Buffer'; data: number[] }
    {
        return this.toJSON()
    }

    equals(other: IsomorphicBuffer): boolean
    {
        if (!(other instanceof IsomorphicBuffer))
        {
            return false;
        }
        if (this.length !== other.length)
        {
            return false;
        }
        for (let i = 0; i < this.length; i++)
        {
            if (this.buffer[this.offset + i] !== other.buffer[other.offset + i])
            {
                return false;
            }
        }
        return true;
    }
}

export function throttle<T>(threshold: number)
{
    if (threshold < 1)
        throw new Error('Threshold must be greater than 0');
    if (threshold == Number.POSITIVE_INFINITY || threshold == Number.MAX_SAFE_INTEGER)
        return function (handler: () => Promise<T>): Promise<T>
        {
            return handler();
        }

    const open = new Array<Promise<unknown>>(threshold);
    let rr = -1;
    console.time('throttle');
    return function (handler: () => Promise<T>): Promise<T>
    {
        rr = (rr + 1) % threshold;
        if (!open[rr])
        {
            const result = handler();
            open[rr] = result
            return result;
        }
        else
        {
            const result = open[rr].then(() => handler())
            open[rr] = result;
            return result;
        }
    }
}
