local CHAR_SET = [[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/]]

-- Tradition is to use chars for the lookup table instead of codepoints.
-- But due to how we're running the encode function, it's faster to use codepoints.
local encode_char_set = {}
local decode_char_set = {}
for i = 1, 64 do
    encode_char_set[i - 1] = string.byte(CHAR_SET, i, i)
    decode_char_set[string.byte(CHAR_SET, i, i)] = i - 1
end

-- stylua: ignore
local HEX_TO_BIN = {
    ["0"] = "0000", ["1"] = "0001", ["2"] = "0010", ["3"] = "0011",
    ["4"] = "0100", ["5"] = "0101", ["6"] = "0110", ["7"] = "0111",
    ["8"] = "1000", ["9"] = "1001", ["a"] = "1010", ["b"] = "1011",
    ["c"] = "1100", ["d"] = "1101", ["e"] = "1110", ["f"] = "1111"
}

-- stylua: ignore
local NORMAL_ID_VECTORS = { -- [Enum.Value] = Vector3.fromNormalId(Enum)
    [0] = Vector3.new(1, 0, 0), -- Enum.NormalId.Right
    [1] = Vector3.new(0, 1, 0), -- Enum.NormalId.Top
    [2] = Vector3.new(0, 0, 1), -- Enum.NormalId.Back
    [3] = Vector3.new(-1, 0, 0), -- Enum.NormalId.Left
    [4] = Vector3.new(0, -1, 0), -- Enum.NormalId.Bottom
    [5] = Vector3.new(0, 0, -1) -- Enum.NormalId.Front
}

local ONES_VECTOR = Vector3.new(1, 1, 1)

local BOOL_TO_BIT = { [true] = 1, [false] = 0 }

local CRC32_POLYNOMIAL = 0xedb88320

local crc32_poly_lookup = {}
for i = 0, 255 do
    local crc = i
    for _ = 1, 8 do
        local mask = -bit32.band(crc, 1)
        crc = bit32.bxor(bit32.rshift(crc, 1), bit32.band(CRC32_POLYNOMIAL, mask))
    end
    crc32_poly_lookup[i] = crc
end

local powers_of_2 = {}
for i = 0, 64 do
    powers_of_2[i] = 2 ^ i
end

local byte_to_hex = {}
for i = 0, 255 do
    byte_to_hex[i] = string.format("%02x", i)
end

local function bitBuffer(stream)
    if stream ~= nil then
        assert(type(stream) == "string", "argument to BitBuffer constructor must be either nil or a string")
    end

    -- The bit buffer works by keeping an array of bytes, a 'final' byte, and how many bits are currently in that last byte
    -- Bits are not kept track of on their own, and are instead combined to form a byte, which is stored in the last space in the array.
    -- This byte is also stored seperately, so that table operations aren't needed to read or modify its value.
    -- The byte array is called `bytes`. The last byte is stored in `lastByte`. The bit counter is stored in `bits`.

    local bits = 0 -- How many free floating bits there are.
    local bytes = {} --! -- Array of bytes currently in the buffer
    local lastByte = 0 -- The most recent byte in the buffer, made up of free floating bits

    local byteCount = 0 -- This variable keeps track of how many bytes there are total in the bit buffer.
    local bitCount = 0 -- This variable keeps track of how many bits there are total in the bit buffer

    local pointer = 0 -- This variable keeps track of what bit the read functions start at
    local pointerByte = 1 -- This variable keeps track of what byte the pointer is at. It starts at 1 since the byte array starts at 1.

    if stream then
        byteCount = #stream
        bitCount = byteCount * 8

        bytes = table.create(#stream)

        for i = 1, byteCount do
            bytes[i] = string.byte(stream, i, i)
        end
    end

    local function dumpBinary()
        -- This function is for debugging or analysis purposes.
        -- It dumps the contents of the byte array and the remaining bits into a string of binary digits.
        -- Thus, bytes [97, 101] with bits [1, 1, 0] would output "01100001 01100101 110"
        local output = table.create(byteCount) --!
        for i, v in ipairs(bytes) do
            output[i] = string.gsub(byte_to_hex[v], "%x", HEX_TO_BIN)
        end
        if bits ~= 0 then
            -- Because the last byte (where the free floating bits are stored) is in the byte array, it has to be overwritten.
            output[byteCount] = string.sub(output[byteCount], 1, bits)
        end

        return table.concat(output, " ")
    end

    local function dumpStringOld()
        -- This function is for accessing the total contents of the bitbuffer.
        -- This function combines all the bytes, including the last byte, into a string of binary data.
        -- Thus, bytes [97, 101] and bits [1, 1, 0] would become (in hex) "0x61 0x65 0x06"

        -- It's substantially faster to create several smaller strings before using table.concat. (well maybe it was, but it isn't now post 2022)
        local output = table.create(math.ceil(byteCount / 4096)) --!
        local c = 1
        for i = 1, byteCount, 4096 do -- groups of 4096 bytes is the point at which there are diminishing returns
            output[c] = string.char(table.unpack(bytes, i, math.min(byteCount, i + 4095)))
            c = c + 1
        end

        return table.concat(output, "")
	end
	
	--Let lua be lua	
	local function dumpString()
		return string.char(table.unpack(bytes))
	end
	
 
    local function dumpHex()
        -- This function is for getting the hex of the bitbuffer's contents, should that be desired
        local output = table.create(byteCount) --!
        for i, v in ipairs(bytes) do
            output[i] = byte_to_hex[v]
        end

        return table.concat(output, "")
    end

    local function dumpBase64()
        -- Base64 is a safe and easy way to convert binary data to be entirely printable
        -- It works on the principle that groups of 3 bytes (24 bits) can evenly be divided into 4 groups of 6
        -- And 2^6 is a mere 64, far less than the number of printable characters.
        -- If there are any missing bytes, `=` is added to the end as padding.
        -- Base64 increases the size of its input by 33%.
        local output = table.create(math.ceil(byteCount * 1.333)) --!

        local c = 1
        for i = 1, byteCount, 3 do
            local b1, b2, b3 = bytes[i], bytes[i + 1], bytes[i + 2]
            local packed = bit32.bor(bit32.lshift(b1, 16), bit32.lshift(b2 or 0, 8), b3 or 0)

            -- This can be done with bit32.extract (and/or bit32.lshift, bit32.band, bit32.rshift)
            -- But bit masking and shifting is more eloquent in my opinion.
            output[c] = encode_char_set[bit32.rshift(bit32.band(packed, 0xfc0000), 0x12)]
            output[c + 1] = encode_char_set[bit32.rshift(bit32.band(packed, 0x3f000), 0xc)]
            output[c + 2] = b2 and encode_char_set[bit32.rshift(bit32.band(packed, 0xfc0), 0x6)] or 0x3d -- 0x3d == "="
            output[c + 3] = b3 and encode_char_set[bit32.band(packed, 0x3f)] or 0x3d

            c = c + 4
        end
        c = c - 1 -- c will always be 1 more than the length of `output`

        local realOutput = table.create(math.ceil(c / 0x1000)) --!
        local k = 1
        for i = 1, c, 0x1000 do
            realOutput[k] = string.char(table.unpack(output, i, math.min(c, i + 0xfff)))
            k = k + 1
        end

        return table.concat(realOutput, "")
    end

    local function exportChunk(chunkLength)
        assert(type(chunkLength) == "number", "argument #1 to BitBuffer.exportChunk should be a number")
        assert(chunkLength > 0, "argument #1 to BitBuffer.exportChunk should be above zero")
        assert(chunkLength % 1 == 0, "argument #1 to BitBuffer.exportChunk should be an integer")

        -- Since `i` is being returned, the most eloquent way to handle this is with a coroutine
        -- This allows returning the existing value of `i` without having to increment it first.
        -- The alternative was starting at `i = -(chunkLength-1)` and incrementing at the start of the iterator function.
        return coroutine.wrap(function()
            local realChunkLength = chunkLength - 1
            -- Since this function only has one 'state', it's perfectly fine to use a for-loop.
            for i = 1, byteCount, chunkLength do
                local chunk = string.char(table.unpack(bytes, i, math.min(byteCount, i + realChunkLength)))
                coroutine.yield(i, chunk)
            end
        end)
    end

    local function exportBase64Chunk(chunkLength)
        chunkLength = chunkLength or 76
        assert(type(chunkLength) == "number", "argument #1 to BitBuffer.exportBase64Chunk should be a number")
        assert(chunkLength > 0, "argument #1 to BitBuffer.exportBase64Chunk should be above zero")
        assert(chunkLength % 1 == 0, "argument #1 to BitBuffer.exportBase64Chunk should be an integer")

        local output = table.create(math.ceil(byteCount * 0.333)) --!

        local c = 1
        for i = 1, byteCount, 3 do
            local b1, b2, b3 = bytes[i], bytes[i + 1], bytes[i + 2]
            local packed = bit32.bor(bit32.lshift(b1, 16), bit32.lshift(b2 or 0, 8), b3 or 0)

            output[c] = encode_char_set[bit32.rshift(bit32.band(packed, 0xfc0000), 0x12)]
            output[c + 1] = encode_char_set[bit32.rshift(bit32.band(packed, 0x3f000), 0xc)]
            output[c + 2] = b2 and encode_char_set[bit32.rshift(bit32.band(packed, 0xfc0), 0x6)] or 0x3d
            output[c + 3] = b3 and encode_char_set[bit32.band(packed, 0x3f)] or 0x3d

            c = c + 4
        end
        c = c - 1

        return coroutine.wrap(function()
            local realChunkLength = chunkLength - 1
            for i = 1, c, chunkLength do
                local chunk = string.char(table.unpack(output, i, math.min(c, i + realChunkLength)))
                coroutine.yield(chunk)
            end
        end)
    end

    local function exportHexChunk(chunkLength)
        assert(type(chunkLength) == "number", "argument #1 to BitBuffer.exportHexChunk should be a number")
        assert(chunkLength > 0, "argument #1 to BitBuffer.exportHexChunk should be above zero")
        assert(chunkLength % 1 == 0, "argument #1 to BitBuffer.exportHexChunk should be an integer")

        local halfLength = math.floor(chunkLength / 2)

        if chunkLength % 2 == 0 then
            return coroutine.wrap(function()
                local output = {} --!
                for i = 1, byteCount, halfLength do
                    for c = 0, halfLength - 1 do
                        output[c] = byte_to_hex[bytes[i + c]]
                    end
                    coroutine.yield(table.concat(output, "", 0))
                end
            end)
        else
            return coroutine.wrap(function()
                local output = { [0] = "" } --!
                local remainder = ""

                local i = 1
                while i <= byteCount do
                    if remainder == "" then
                        output[0] = ""
                        for c = 0, halfLength - 1 do
                            output[c + 1] = byte_to_hex[bytes[i + c]]
                        end
                        local endByte = byte_to_hex[bytes[i + halfLength]]
                        if endByte then
                            output[halfLength + 1] = string.sub(endByte, 1, 1)
                            remainder = string.sub(endByte, 2, 2)
                        end
                        i = i + 1
                    else
                        output[0] = remainder
                        for c = 0, halfLength - 1 do
                            output[c + 1] = byte_to_hex[bytes[i + c]]
                        end
                        output[halfLength + 1] = ""
                        remainder = ""
                    end

                    coroutine.yield(table.concat(output, "", 0))
                    i = i + halfLength
                end
            end)
        end
    end

    local function crc32()
        local crc = 0xffffffff -- 2^32

        for _, v in ipairs(bytes) do
            local poly = crc32_poly_lookup[bit32.band(bit32.bxor(crc, v), 255)]
            crc = bit32.bxor(bit32.rshift(crc, 8), poly)
        end

        return bit32.bnot(crc) % 0xffffffff -- 2^32
    end

    local function getLength()
        return bitCount
    end

    local function getByteLength()
        return byteCount
    end

    local function getPointer()
        -- This function gets the value of the pointer. This is self-explanatory.
        return pointer
    end

    local function setPointer(n)
        assert(type(n) == "number", "argument #1 to BitBuffer.setPointer should be a number")
        assert(n >= 0, "argument #1 to BitBuffer.setPointer should be zero or higher")
        assert(n % 1 == 0, "argument #1 to BitBuffer.setPointer should be an integer")
        assert(n <= bitCount, "argument #1 to BitBuffer.setPointerByte should within range of the buffer")
        -- This function sets the value of pointer. This is self-explanatory.
        pointer = n
        pointerByte = math.floor(n / 8) + 1
    end

    local function setPointerFromEnd(n)
        assert(type(n) == "number", "argument #1 to BitBuffer.setPointerFromEnd should be a number")
        assert(n >= 0, "argument #1 to BitBuffer.setPointerFromEnd should be zero or higher")
        assert(n % 1 == 0, "argument #1 to BitBuffer.setPointerFromEnd should be an integer")
        assert(n <= bitCount, "argument #1 to BitBuffer.setPointerFromEnd should within range of the buffer")

        pointer = bitCount - n
        pointerByte = math.floor(pointer / 8 + 1)
    end

    local function getPointerByte()
        return pointerByte
    end

    local function setPointerByte(n)
        assert(type(n) == "number", "argument #1 to BitBuffer.setPointerByte should be a number")
        assert(n > 0, "argument #1 to BitBuffer.setPointerByte should be positive")
        assert(n % 1 == 0, "argument #1 to BitBuffer.setPointerByte should be an integer")
        assert(n <= byteCount, "argument #1 to BitBuffer.setPointerByte should be within range of the buffer")
        -- Sets the value of the pointer in bytes instead of bits
        pointer = n * 8
        pointerByte = n
    end

    local function setPointerByteFromEnd(n)
        assert(type(n) == "number", "argument #1 to BitBuffer.setPointerByteFromEnd should be a number")
        assert(n >= 0, "argument #1 to BitBuffer.setPointerByteFromEnd should be zero or higher")
        assert(n % 1 == 0, "argument #1 to BitBuffer.setPointerByteFromEnd should be an integer")
        assert(n <= byteCount, "argument #1 to BitBuffer.setPointerByteFromEnd should be within range of the buffer")

        pointerByte = byteCount - n
        pointer = pointerByte * 8
    end

    local function isFinished()
        return pointer == bitCount
    end

    local function writeBits(...)
        -- The first of two main functions for the actual 'writing' of the bitbuffer.
        -- This function takes a vararg of 1s and 0s and writes them to the buffer.
        local bitN = select("#", ...)
        if bitN == 0 then
            return
        end -- Throwing here seems unnecessary
        bitCount = bitCount + bitN
        local packed = table.pack(...)
        for _, v in ipairs(packed) do
            assert(v == 1 or v == 0, "arguments to BitBuffer.writeBits should be either 1 or 0")
            if bits == 0 then -- If the bit count is 0, increment the byteCount
                -- This is the case at the beginning of the buffer as well as when the the buffer reaches 7 bits,
                -- so it's done at the beginning of the loop.
                byteCount = byteCount + 1
            end
            lastByte = lastByte + (v == 1 and powers_of_2[7 - bits] or 0) -- Add the current bit to lastByte, from right to left
            bits = bits + 1
            if bits == 8 then -- If the bit count is 8, set it to 0, write lastByte to the byte list, and set lastByte to 0
                bits = 0
                bytes[byteCount] = lastByte
                lastByte = 0
            end
        end
        if bits ~= 0 then -- If there are some bits in lastByte, it has to be put into lastByte
            -- If this is done regardless of the bit count, there might be a trailing zero byte
            bytes[byteCount] = lastByte
        end
    end

    local function writeByte(n)
        --assert(type(n) == "number", "argument #1 to BitBuffer.writeByte should be a number")
        --assert(n >= 0 and n <= 255, "argument #1 to BitBuffer.writeByte should be in the range [0, 255]")
		--assert(n % 1 == 0, "argument #1 to BitBuffer.writeByte should be an integer")
		
        -- The second of two main functions for the actual 'writing' of the bitbuffer.
        -- This function takes a byte (an 8-bit integer) and writes it to the buffer.
        if bits == 0 then
            -- If there aren't any free-floating bits, this is easy.
            byteCount = byteCount + 1
            bytes[byteCount] = n
        else
            local nibble = bit32.rshift(n, bits) -- Shift `bits` number of bits out of `n` (they go into the aether)
            bytes[byteCount] = lastByte + nibble -- Manually set the most recent byte to the lastByte + the front part of `n`
            byteCount = byteCount + 1
            lastByte = bit32.band(bit32.lshift(n, 8 - bits), 255) -- Shift `n` forward `8-bits` and get what remains in the first 8 bits
            bytes[byteCount] = lastByte
        end
        bitCount = bitCount + 8 -- Increment the bit counter
	end
	
	local function writeBytesFast(tab)
		assert(bits == 0, "writeBytesFast can only work for whole byte streams")
		local count = #tab
		table.move(tab, 1 , count, byteCount + 1, bytes)
		byteCount+= count
		bitCount += count * 8
	end

    local function writeUnsigned(width, n)
        assert(type(width) == "number", "argument #1 to BitBuffer.writeUnsigned should be a number")
        assert(width >= 1 and width <= 64, "argument #1 to BitBuffer.writeUnsigned should be in the range [1, 64]")
        assert(width % 1 == 0, "argument #1 to BitBuffer.writeUnsigned should be an integer")

        assert(type(n) == "number", "argument #2 to BitBuffer.writeUnsigned should be a number")
        assert(n >= 0 and n <= powers_of_2[width] - 1, "argument #2 to BitBuffer.writeUnsigned is out of range")
        assert(n % 1 == 0, "argument #2 to BitBuffer.writeUnsigned should be an integer")
        -- Writes unsigned integers of arbitrary length to the buffer.
        -- This is the first function that uses other functions in the buffer to function.
        -- This is done because the space taken up would be rather large for very little performance gain.

        -- Get the number of bytes and number of floating bits in the specified width
        local bytesInN, bitsInN = math.floor(width / 8), width % 8
        local extractedBits = table.create(bitsInN) --!

        -- If the width is less than or equal to 32-bits, bit32 can be used without any problem.
        if width <= 32 then
            -- Counting down from the left side, the bytes are written to the buffer
            local c = width
            for _ = 1, bytesInN do
                c = c - 8
                writeByte(bit32.extract(n, c, 8))
            end
            -- Any remaining bits are stored in an array
            for i = bitsInN - 1, 0, -1 do
                extractedBits[bitsInN - i] = BOOL_TO_BIT[bit32.btest(n, powers_of_2[i])]
            end
            -- Said array is then used to write them to the buffer
            writeBits(table.unpack(extractedBits))
        else
            -- If the width is greater than 32, the number has to be divided up into a few 32-bit or less numbers
            local leastSignificantChunk = n % 0x100000000 -- Get bits 0-31 (counting from the right side). 0x100000000 is 2^32.
            local mostSignificantChunk = math.floor(n / 0x100000000) -- Get any remaining bits by manually right shifting by 32 bits

            local c = width - 32 -- The number of bits in mostSignificantChunk is variable, but a counter is still needed
            for _ = 1, bytesInN - 4 do -- 32 bits is 4 bytes
                c = c - 8
                writeByte(bit32.extract(mostSignificantChunk, c, 8))
            end
            -- `bitsInN` is always going to be the number of spare bits in `mostSignificantChunk`
            -- which comes before `leastSignificantChunk`
            for i = bitsInN - 1, 0, -1 do
                extractedBits[bitsInN - i] = BOOL_TO_BIT[bit32.btest(mostSignificantChunk, powers_of_2[i])]
            end
            writeBits(table.unpack(extractedBits))

            for i = 3, 0, -1 do -- Then of course, write all 4 bytes of leastSignificantChunk
                writeByte(bit32.extract(leastSignificantChunk, i * 8, 8))
            end
        end
    end

    local function writeSigned(width, n)
        assert(type(width) == "number", "argument #1 to BitBuffer.writeSigned should be a number")
        assert(width >= 2 and width <= 64, "argument #1 to BitBuffer.writeSigned should be in the range [2, 64]")
        assert(width % 1 == 0, "argument #1 to BitBuffer.writeSigned should be an integer")

        assert(type(n) == "number", "argument #2 to BitBuffer.writeSigned should be a number")
        assert(
            n >= -powers_of_2[width - 1] and n <= powers_of_2[width - 1] - 1,
            "argument #2 to BitBuffer.writeSigned is out of range"
        )
        assert(n % 1 == 0, "argument #2 to BitBuffer.writeSigned should be an integer")
        -- Writes signed integers of arbitrary length to the buffer.
        -- These integers are stored using two's complement.
        -- Essentially, this means the first bit in the number is used to store whether it's positive or negative
        -- If the number is positive, it's stored normally.
        -- If it's negative, the number that's stored is equivalent to the max value of the width + the number
        if n >= 0 then
            writeBits(0)
            writeUnsigned(width - 1, n) -- One bit is used for the sign, so the stored number's width is actually width-1
        else
            writeBits(1)
            writeUnsigned(width - 1, powers_of_2[width - 1] + n)
        end
    end

    local function writeFloat(exponentWidth, mantissaWidth, n)
        assert(type(exponentWidth) == "number", "argument #1 to BitBuffer.writeFloat should be a number")
        assert(
            exponentWidth >= 1 and exponentWidth <= 64,
            "argument #1 to BitBuffer.writeFloat should be in the range [1, 64]"
        )
        assert(exponentWidth % 1 == 0, "argument #1 to BitBuffer.writeFloat should be an integer")

        assert(type(mantissaWidth) == "number", "argument #2 to BitBuffer.writeFloat should be a number")
        assert(
            mantissaWidth >= 1 and mantissaWidth <= 64,
            "argument #2 to BitBuffer.writeFloat should be in the range [1, 64]"
        )
        assert(mantissaWidth % 1 == 0, "argument #2 to BitBuffer.writeFloat should be an integer")

        assert(type(n) == "number", "argument #3 to BitBuffer.writeFloat should be a number")

        -- Given that floating point numbers are particularly hard to grasp, this function is annotated heavily.
        -- This stackoverflow answer is a great help if you just want an overview:
        -- https://stackoverflow.com/a/7645264
        -- Essentially, floating point numbers are scientific notation in binary.
        -- Instead of expressing numbers like 10^e*m, floating points instead use 2^e*m.
        -- For the sake of this function, `e` is referred to as `exponent` and `m` is referred to as `mantissa`.

        -- Floating point numbers are stored in memory as a sequence of bitfields.
        -- Every float has a set number of bits assigned for exponent values and mantissa values, along with one bit for the sign.
        -- The order of the bits in the memory is: sign, exponent, mantissa.

        -- Given that floating points have to represent numbers less than zero as well as those above them,
        -- some parts of the exponent are set aside to be negative exponents. In the case of floats,
        -- this is about half of the values. To calculate the 'real' value of an exponent a number that's half of the max exponent
        -- is added to the exponent. More info can be found here: https://stackoverflow.com/q/2835278
        -- This number is called the 'bias'.
        local bias = powers_of_2[exponentWidth - 1] - 1

        local sign = n < 0 -- The sign of a number is important.
        -- In this case, since we're using a lookup table for the sign bit, we want `sign` to indicate if the number is negative or not.
        n = math.abs(n) -- But it's annoying to work with negative numbers and the sign isn't important for decomposition.

        -- Lua has a function specifically for decomposing (or taking apart) a floating point number into its pieces.
        -- These pieces, as listed above, are the mantissa and exponent.
        local mantissa, exponent = math.frexp(n)

        -- Before we go further, there are some concepts that get special treatment in the floating point format.
        -- These have to be accounted for before normal floats are written to the buffer.

        if n == math.huge then
            -- Positive and negative infinities are specifically indicated with an exponent that's all 1s
            -- and a mantissa that's all 0s.
            writeBits(BOOL_TO_BIT[sign]) -- As previously said, there's a bit for the sign
            writeUnsigned(exponentWidth, powers_of_2[exponentWidth] - 1) -- Then comes the exponent
            writeUnsigned(mantissaWidth, 0) -- And finally the mantissa
            return
        elseif n ~= n then
            -- NaN is indicated with an exponent that's all 1s and a mantissa that isn't 0.
            -- In theory, the individual bits of NaN should be maintained but Lua doesn't allow that,
            -- so the mantissa is just being set to 10 for no particular reason.
            writeBits(BOOL_TO_BIT[sign])
            writeUnsigned(exponentWidth, powers_of_2[exponentWidth] - 1)
            writeUnsigned(mantissaWidth, 10)
            return
        elseif n == 0 then
            -- Zero is represented with an exponent that's zero and a mantissa that's also zero.
            -- Lua doesn't have a signed zero, so that translates to the entire number being all 0s.
            writeUnsigned(exponentWidth + mantissaWidth + 1, 0)
            return
        elseif exponent + bias <= 1 then
            -- Subnormal numbers are a number that's exponent (when biased) is zero.
            -- Because of a quirk with the way Lua and C decompose numbers, subnormal numbers actually have an exponent of one when biased.

            -- The process behind this is explained below, so for the sake of brevity it isn't explained here.
            -- The only difference between processing subnormal and normal numbers is with the mantissa.
            -- As subnormal numbers always start with a 0 (in binary), it doesn't need to be removed or shifted out
            -- so it's a simple shift and round.
            mantissa = math.floor(mantissa * powers_of_2[mantissaWidth] + 0.5)

            writeBits(BOOL_TO_BIT[sign])
            writeUnsigned(exponentWidth, 0) -- Subnormal numbers always have zero for an exponent
            writeUnsigned(mantissaWidth, mantissa)
            return
        end

        -- In every normal case, the mantissa of a number will have a 1 directly after the decimal point (in binary).
        -- As an example, 0.15625 has a mantissa of 0.625, which is 0.101 in binary. The 1 after the decimal point is always there.
        -- That means that for the sake of space efficiency that can be left out.
        -- The bit has to be removed. This uses subtraction and multiplication to do it since bit32 is for integers only.
        -- The mantissa is then shifted up by the width of the mantissa field and rounded.
        mantissa = math.floor((mantissa - 0.5) * 2 * powers_of_2[mantissaWidth] + 0.5)
        -- (The first fraction bit is equivalent to 0.5 in decimal)

        -- After that, it's just a matter of writing to the stream:
        writeBits(BOOL_TO_BIT[sign])
        writeUnsigned(exponentWidth, exponent + bias - 1) -- The bias is added to the exponent to properly offset it
        -- The extra -1 is added because Lua, for whatever reason, doesn't normalize its results
        -- This is the cause of the 'quirk' mentioned when handling subnormal number
        -- As an example, math.frexp(0.15625) = 0.625, -2
        -- This means that 0.15625 = 0.625*2^-2
        -- Or, in binary: 0.00101 = 0.101 >> 2
        -- This is a correct statement but the actual result is meant to be:
        -- 0.00101 = 1.01 >> 3, or 0.15625 = 1.25*2^-3
        -- A small but important distinction that has made writing this module frustrating because no documentation notates this.
        writeUnsigned(mantissaWidth, mantissa)
    end

    local function writeBase64(input)
        assert(type(input) == "string", "argument #1 to BitBuffer.writeBase64 should be a string")
        assert(
            not string.find(input, "[^%w%+/=]"),
            "argument #1 to BitBuffer.writeBase64 should only contain valid base64 characters"
        )

        for i = 1, #input, 4 do
            local b1, b2, b3, b4 = string.byte(input, i, i + 3)

            b1 = decode_char_set[b1]
            b2 = decode_char_set[b2]
            b3 = decode_char_set[b3]
            b4 = decode_char_set[b4]

            local packed = bit32.bor(bit32.lshift(b1, 18), bit32.lshift(b2, 12), bit32.lshift(b3 or 0, 6), b4 or 0)

            writeByte(bit32.rshift(packed, 16))
            if not b3 then
                break
            end
            writeByte(bit32.band(bit32.rshift(packed, 8), 0xff))
            if not b4 then
                break
            end
            writeByte(bit32.band(packed, 0xff))
        end
    end

    local function writeString(str)
        assert(type(str) == "string", "argument #1 to BitBuffer.writeString  should be a string")
        -- The default mode of writing strings is length-prefixed.
        -- This means that the length of the string is written before the contents of the string.
        -- For the sake of speed it has to be an even byte.
        -- One and two bytes is too few characters (255 bytes and 65535 bytes respectively), so it has to be higher.
        -- Three bytes is roughly 16.77mb, and four is roughly 4.295gb. Given this is Lua and is thus unlikely to be processing strings
        -- that large, this function uses three bytes, or 24 bits for the length

        writeUnsigned(24, #str)

        for i = 1, #str do
            writeByte(string.byte(str, i, i))
        end
    end

    local function writeTerminatedString(str)
        assert(type(str) == "string", "argument #1 to BitBuffer.writeTerminatedString should be a string")
        -- This function writes strings that are null-terminated.
        -- Null-terminated strings are strings of bytes that end in a 0 byte (\0)
        -- This isn't the default because it doesn't allow for binary data to be written cleanly.

        for i = 1, #str do
            writeByte(string.byte(str, i, i))
        end
        writeByte(0)
    end

    local function writeSetLengthString(str)
        assert(type(str) == "string", "argument #1 to BitBuffer.writeSetLengthString should be a string")
        -- This function writes strings as a pure string of bytes
        -- It doesn't store any data about the length of the string,
        -- so reading it requires knowledge of how many characters were stored

        for i = 1, #str do
            writeByte(string.byte(str, i, i))
        end
    end

    local function writeField(...)
        -- This is equivalent to having a writeBitfield function.
        -- It combines all of the passed 'bits' into an unsigned number, then writes it.
        local field = 0
        local bools = table.pack(...)
        for i = 1, bools.n do
            field = field * 2 -- Shift `field`. Equivalent to field<<1. At the beginning of the loop to avoid an extra shift.

            local v = bools[i]
            if v then
                field = field + 1 -- If the bit is truthy, turn it on (it defaults to off so it's fine to not have a branch)
            end
        end

        writeUnsigned(bools.n, field)
    end

    -- All write functions below here are shorthands. For the sake of performance, these functions are implemented manually.
    -- As an example, while it would certainly be easier to make `writeInt16(n)` just call `writeUnsigned(16, n),
    -- it's more performant to just manually call writeByte twice for it.

    local function writeUInt8(n)
        assert(type(n) == "number", "argument #1 to BitBuffer.writeUInt8 should be a number")
        assert(n >= 0 and n <= 255, "argument #1 to BitBuffer.writeUInt8 should be in the range [0, 255]")
        assert(n % 1 == 0, "argument #1 to BitBuffer.writeUInt8 should be an integer")

        writeByte(n)
    end

    local function writeUInt16(n)
        assert(type(n) == "number", "argument #1 to BitBuffer.writeUInt16 should be a number")
        assert(n >= 0 and n <= 65535, "argument #1 to BitBuffer.writeInt16 should be in the range [0, 65535]")
        assert(n % 1 == 0, "argument #1 to BitBuffer.writeUInt16 should be an integer")

        writeByte(bit32.rshift(n, 8))
        writeByte(bit32.band(n, 255))
    end

    local function writeUInt32(n)
        assert(type(n) == "number", "argument #1 to BitBuffer.writeUInt32 should be a number")
        assert(
            n >= 0 and n <= 4294967295,
            "argument #1 to BitBuffer.writeUInt32 should be in the range [0, 4294967295]"
        )
        assert(n % 1 == 0, "argument #1 to BitBuffer.writeUInt32 should be an integer")

        writeByte(bit32.rshift(n, 24))
        writeByte(bit32.band(bit32.rshift(n, 16), 255))
        writeByte(bit32.band(bit32.rshift(n, 8), 255))
        writeByte(bit32.band(n, 255))
    end

    local function writeInt8(n)
        assert(type(n) == "number", "argument #1 to BitBuffer.writeInt8 should be a number")
        assert(n >= -128 and n <= 127, "argument #1 to BitBuffer.writeInt8 should be in the range [-128, 127]")
        assert(n % 1 == 0, "argument #1 to BitBuffer.writeInt8 should be an integer")

        if n < 0 then
            n = (128 + n) + 128
        end

        writeByte(n)
    end

    local function writeInt16(n)
        assert(type(n) == "number", "argument #1 to BitBuffer.writeInt16 should be a number")
        assert(n >= -32768 and n <= 32767, "argument #1 to BitBuffer.writeInt16 should be in the range [-32768, 32767]")
        assert(n % 1 == 0, "argument #1 to BitBuffer.writeInt16 should be an integer")

        if n < 0 then
            n = (32768 + n) + 32768
        end

        writeByte(bit32.rshift(n, 8))
        writeByte(bit32.band(n, 255))
    end

    local function writeInt32(n)
        assert(type(n) == "number", "argument #1 to BitBuffer.writeInt32 should be a number")
        assert(
            n >= -2147483648 and n <= 2147483647,
            "argument #1 to BitBuffer.writeInt32 should be in the range [-2147483648, 2147483647]"
        )
        assert(n % 1 == 0, "argument #1 to BitBuffer.writeInt32 should be an integer")

        if n < 0 then
            n = (2147483648 + n) + 2147483648
        end

        writeByte(bit32.rshift(n, 24))
        writeByte(bit32.band(bit32.rshift(n, 16), 255))
        writeByte(bit32.band(bit32.rshift(n, 8), 255))
        writeByte(bit32.band(n, 255))
    end

    local function writeFloat16(n)
        --assert(type(n) == "number", "argument #1 to BitBuffer.writeFloat16 should be a number")

        local sign = n < 0
        n = math.abs(n)

        local mantissa, exponent = math.frexp(n)

        if n == math.huge then
            if sign then
                writeByte(252) -- 11111100
            else
                writeByte(124) -- 01111100
            end
            writeByte(0) -- 00000000
            return
        elseif n ~= n then
            -- 01111111 11111111
            writeByte(127)
            writeByte(255)
            return
        elseif n == 0 then
            writeByte(0)
            writeByte(0)
            return
        elseif exponent + 15 <= 1 then -- Bias for halfs is 15
            mantissa = math.floor(mantissa * 1024 + 0.5)
            if sign then
                writeByte(128 + bit32.rshift(mantissa, 8)) -- Sign bit, 5 empty bits, 2 from mantissa
            else
                writeByte(bit32.rshift(mantissa, 8))
            end
            writeByte(bit32.band(mantissa, 255)) -- Get last 8 bits from mantissa
            return
        end

        mantissa = math.floor((mantissa - 0.5) * 2048 + 0.5)

        -- The bias for halfs is 15, 15-1 is 14
        if sign then
            writeByte(128 + bit32.lshift(exponent + 14, 2) + bit32.rshift(mantissa, 8))
        else
            writeByte(bit32.lshift(exponent + 14, 2) + bit32.rshift(mantissa, 8))
        end
        writeByte(bit32.band(mantissa, 255))
    end

    local function writeFloat32(n)
        --assert(type(n) == "number", "argument #1 to BitBuffer.writeFloat32 should be a number")

        local sign = n < 0
        n = math.abs(n)

        local mantissa, exponent = math.frexp(n)

        if n == math.huge then
            if sign then
                writeByte(255) -- 11111111
            else
                writeByte(127) -- 01111111
            end
            writeByte(128) -- 10000000
            writeByte(0) -- 00000000
            writeByte(0) -- 00000000
            return
        elseif n ~= n then
            -- 01111111 11111111 11111111 11111111
            writeByte(127)
            writeByte(255)
            writeByte(255)
            writeByte(255)
            return
        elseif n == 0 then
            writeByte(0)
            writeByte(0)
            writeByte(0)
            writeByte(0)
            return
        elseif exponent + 127 <= 1 then -- bias for singles is 127
            mantissa = math.floor(mantissa * 8388608 + 0.5)
            if sign then
                writeByte(128) -- Sign bit, 7 empty bits for exponent
            else
                writeByte(0)
            end
            writeByte(bit32.rshift(mantissa, 16))
            writeByte(bit32.band(bit32.rshift(mantissa, 8), 255))
            writeByte(bit32.band(mantissa, 255))
            return
        end

        mantissa = math.floor((mantissa - 0.5) * 16777216 + 0.5)

        -- 127-1 = 126
        if sign then -- sign + 7 exponent
            writeByte(128 + bit32.rshift(exponent + 126, 1))
        else
            writeByte(bit32.rshift(exponent + 126, 1))
        end
        writeByte(bit32.band(bit32.lshift(exponent + 126, 7), 255) + bit32.rshift(mantissa, 16)) -- 1 exponent + 7 mantissa
        writeByte(bit32.band(bit32.rshift(mantissa, 8), 255)) -- 8 mantissa
        writeByte(bit32.band(mantissa, 255)) -- 8 mantissa
    end

    local function writeFloat64(n)
        assert(type(n) == "number", "argument #1 to BitBuffer.writeFloat64 should be a number")

        local sign = n < 0
        n = math.abs(n)

        local mantissa, exponent = math.frexp(n)

        if n == math.huge then
            if sign then
                writeByte(255) -- 11111111
            else
                writeByte(127) -- 01111111
            end
            writeByte(240) -- 11110000
            writeByte(0) -- 00000000
            writeByte(0) -- 00000000
            writeByte(0) -- 00000000
            writeByte(0) -- 00000000
            writeByte(0) -- 00000000
            writeByte(0) -- 00000000
            return
        elseif n ~= n then
            -- 01111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
            writeByte(127)
            writeByte(255)
            writeByte(255)
            writeByte(255)
            writeByte(255)
            writeByte(255)
            writeByte(255)
            writeByte(255)
            return
        elseif n == 0 then
            writeByte(0)
            return
        elseif exponent + 1023 <= 1 then -- bias for doubles is 1023
            mantissa = math.floor(mantissa * 4503599627370496 + 0.5)
            if sign then
                writeByte(128) -- Sign bit, 7 empty bits for exponent
            else
                writeByte(0)
            end

            -- This is labeled better below
            local leastSignificantChunk = mantissa % 0x100000000 -- 32 bits
            local mostSignificantChunk = math.floor(mantissa / 0x100000000) -- 20 bits

            writeByte(bit32.rshift(mostSignificantChunk, 16))
            writeByte(bit32.band(bit32.rshift(mostSignificantChunk, 8), 255))
            writeByte(bit32.band(mostSignificantChunk, 255))
            writeByte(bit32.rshift(leastSignificantChunk, 24))
            writeByte(bit32.band(bit32.rshift(leastSignificantChunk, 16), 255))
            writeByte(bit32.band(bit32.rshift(leastSignificantChunk, 8), 255))
            writeByte(bit32.band(leastSignificantChunk, 255))
            return
        end

        mantissa = math.floor((mantissa - 0.5) * 9007199254740992 + 0.5)

        --1023-1 = 1022
        if sign then
            writeByte(128 + bit32.rshift(exponent + 1022, 4)) -- shift out 4 of the bits in exponent
        else
            writeByte(bit32.rshift(exponent + 1022, 4)) -- 01000001 0110
        end
        -- Things start to get a bit wack here because the mantissa is 52 bits, so bit32 *can't* be used.
        -- As the Offspring once said... You gotta keep 'em seperated.
        local leastSignificantChunk = mantissa % 0x100000000 -- 32 bits
        local mostSignificantChunk = math.floor(mantissa / 0x100000000) -- 20 bits

        -- First, the last 4 bits of the exponent and the first 4 bits of the mostSignificantChunk:
        writeByte(bit32.band(bit32.lshift(exponent + 1022, 4), 255) + bit32.rshift(mostSignificantChunk, 16))
        -- Then, the next 16 bits:
        writeByte(bit32.band(bit32.rshift(mostSignificantChunk, 8), 255))
        writeByte(bit32.band(mostSignificantChunk, 255))
        -- Then... 4 bytes of the leastSignificantChunk
        writeByte(bit32.rshift(leastSignificantChunk, 24))
        writeByte(bit32.band(bit32.rshift(leastSignificantChunk, 16), 255))
        writeByte(bit32.band(bit32.rshift(leastSignificantChunk, 8), 255))
        writeByte(bit32.band(leastSignificantChunk, 255))
    end

    -- All write functions below here are Roblox specific datatypes.

    local function writeBrickColor(n)
        assert(typeof(n) == "BrickColor", "argument #1 to BitBuffer.writeBrickColor should be a BrickColor")

        writeUInt16(n.Number)
    end

    local function writeColor3(c3)
        assert(typeof(c3) == "Color3", "argument #1 to BitBuffer.writeColor3 should be a Color3")

        writeByte(math.floor(c3.R * 0xff + 0.5))
        writeByte(math.floor(c3.G * 0xff + 0.5))
        writeByte(math.floor(c3.B * 0xff + 0.5))
    end

    local function writeCFrame(cf)
        assert(typeof(cf) == "CFrame", "argument #1 to BitBuffer.writeCFrame should be a CFrame")
        -- CFrames can be rather lengthy (if stored naively, they would each be 48 bytes long) so some optimization is done here.
        -- Specifically, if a CFrame is axis-aligned (it's only rotated in 90 degree increments), the rotation matrix isn't stored.
        -- Instead, an 'id' for its orientation is generated and that's stored instead of the rotation.
        -- This means that for the most common rotations, only 13 bytes are used.
        -- The downside is that non-axis-aligned CFrames use 49 bytes instead of 48, but that's a small price to pay.

        local upVector = cf.UpVector
        local rightVector = cf.RightVector

        -- This is an easy trick to check if a CFrame is axis-aligned:
        -- Essentially, in order for a vector to be axis-aligned, two of the components have to be 0
        -- This means that the dot product between the vector and a vector of all 1s will be 1 (0*x = 0)
        -- Since these are all unit vectors, there is no other combination that results in 1.
        local rightAligned = math.abs(rightVector:Dot(ONES_VECTOR))
        local upAligned = math.abs(upVector:Dot(ONES_VECTOR))
        -- At least one of these two vectors is guaranteed to not result in 0.

        local axisAligned = (math.abs(1 - rightAligned) < 0.00001 or rightAligned == 0)
            and (math.abs(1 - upAligned) < 0.00001 or upAligned == 0)
        -- There are limitations to `math.abs(a-b) < epsilon` but they're not relevant:
        -- The range of numbers is [0, 1] and this just needs to know if the number is approximately 1

        --todo special code for quaternions (0x01 in Roblox's format, would clash with 0x00 here)
        if axisAligned then
            local position = cf.Position
            -- The ID of an orientation is generated through what can best be described as 'hand waving';
            -- This is how Roblox does it and it works, so it was chosen to do it this way too.
            local rightNormal, upNormal
            for i = 0, 5 do
                local v = NORMAL_ID_VECTORS[i]
                if 1 - v:Dot(rightVector) < 0.00001 then
                    rightNormal = i
                end
                if 1 - v:Dot(upVector) < 0.00001 then
                    upNormal = i
                end
            end
            -- The ID generated here is technically off by 1 from what Roblox would store, but that's not important
            -- It just means that 0x02 is actually 0x01 for the purposes of this module's implementation.
            writeByte(rightNormal * 6 + upNormal)
            writeFloat32(position.X)
            writeFloat32(position.Y)
            writeFloat32(position.Z)
        else
            -- If the CFrame isn't axis-aligned, the entire rotation matrix has to be written...
            writeByte(0) -- Along with a byte to indicate the matrix was written.
            local x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22 = cf:GetComponents()
            writeFloat32(x)
            writeFloat32(y)
            writeFloat32(z)
            writeFloat32(r00)
            writeFloat32(r01)
            writeFloat32(r02)
            writeFloat32(r10)
            writeFloat32(r11)
            writeFloat32(r12)
            writeFloat32(r20)
            writeFloat32(r21)
            writeFloat32(r22)
        end
    end

    local function writeVector3(v3)
        --assert(typeof(v3) == "Vector3", "argument #1 to BitBuffer.writeVector3 should be a Vector3")

        writeFloat32(v3.X)
        writeFloat32(v3.Y)
        writeFloat32(v3.Z)
    end

    local function writeVector2(v2)
        assert(typeof(v2) == "Vector2", "argument #1 to BitBuffer.writeVector2 should be a Vector2")

        writeFloat32(v2.X)
        writeFloat32(v2.Y)
    end

    local function writeUDim2(u2)
        assert(typeof(u2) == "UDim2", "argument #1 to BitBuffer.writeUDim2 should be a UDim2")

        writeFloat32(u2.X.Scale)
        writeInt32(u2.X.Offset)
        writeFloat32(u2.Y.Scale)
        writeInt32(u2.Y.Offset)
    end

    local function writeUDim(u)
        assert(typeof(u) == "UDim", "argument #1 to BitBuffer.writeUDim should be a UDim")

        writeFloat32(u.Scale)
        writeInt32(u.Offset)
    end

    local function writeRay(ray)
        assert(typeof(ray) == "Ray", "argument #1 to BitBuffer.writeRay should be a Ray")

        writeFloat32(ray.Origin.X)
        writeFloat32(ray.Origin.Y)
        writeFloat32(ray.Origin.Z)

        writeFloat32(ray.Direction.X)
        writeFloat32(ray.Direction.Y)
        writeFloat32(ray.Direction.Z)
    end

    local function writeRect(rect)
        assert(typeof(rect) == "Rect", "argument #1 to BitBuffer.writeRect should be a Rect")

        writeFloat32(rect.Min.X)
        writeFloat32(rect.Min.Y)

        writeFloat32(rect.Max.X)
        writeFloat32(rect.Max.Y)
    end

    local function writeRegion3(region)
        assert(typeof(region) == "Region3", "argument #1 to BitBuffer.writeRegion3 should be a Region3")

        local min = region.CFrame.Position - (region.Size / 2)
        local max = region.CFrame.Position + (region.Size / 2)

        writeFloat32(min.X)
        writeFloat32(min.Y)
        writeFloat32(min.Z)

        writeFloat32(max.X)
        writeFloat32(max.Y)
        writeFloat32(max.Z)
    end

    local function writeEnum(enum)
        assert(typeof(enum) == "EnumItem", "argument #1 to BitBuffer.writeEnum should be an EnumItem")

        -- Relying upon tostring is generally not good, but there's not any other options for this.
        writeTerminatedString(tostring(enum.EnumType))
        writeUInt16(enum.Value) -- Optimistically assuming no Roblox Enum value will ever pass 65,535
    end

    local function writeNumberRange(range)
        assert(typeof(range) == "NumberRange", "argument #1 to BitBuffer.writeNumberRange should be a NumberRange")

        writeFloat32(range.Min)
        writeFloat32(range.Max)
    end

    local function writeNumberSequence(sequence)
        assert(
            typeof(sequence) == "NumberSequence",
            "argument #1 to BitBuffer.writeNumberSequence should be a NumberSequence"
        )

        writeUInt32(#sequence.Keypoints)
        for _, keypoint in ipairs(sequence.Keypoints) do
            writeFloat32(keypoint.Time)
            writeFloat32(keypoint.Value)
            writeFloat32(keypoint.Envelope)
        end
    end

    local function writeColorSequence(sequence)
        assert(
            typeof(sequence) == "ColorSequence",
            "argument #1 to BitBuffer.writeColorSequence should be a ColorSequence"
        )

        writeUInt32(#sequence.Keypoints)
        for _, keypoint in ipairs(sequence.Keypoints) do
            local c3 = keypoint.Value
            writeFloat32(keypoint.Time)
            writeByte(math.floor(c3.R * 0xff + 0.5))
            writeByte(math.floor(c3.G * 0xff + 0.5))
            writeByte(math.floor(c3.B * 0xff + 0.5))
        end
    end

    -- These are the read functions for the 'abstract' data types. At the bottom, there are shorthand read functions.

    local function readBits(n)
        assert(type(n) == "number", "argument #1 to BitBuffer.readBits should be a number")
        assert(n > 0, "argument #1 to BitBuffer.readBits should be greater than zero")
        assert(n % 1 == 0, "argument #1 to BitBuffer.readBits should be an integer")

        assert(pointer + n <= bitCount, "BitBuffer.readBits cannot read past the end of the stream")

        -- The first of two main functions for the actual 'reading' of the bitbuffer.
        -- Reads `n` bits and returns an array of their values.
        local output = table.create(n) --!
        local byte = bytes[pointerByte] -- For the sake of efficiency, the current byte that the bits are coming from is stored
        local c = pointer % 8 -- A counter is set with the current position of the pointer in the byte
        for i = 1, n do
            -- Then, it's as easy as moving through the bits of the byte
            -- And getting the individiual bit values
            local pow = powers_of_2[7 - c]
            output[i] = BOOL_TO_BIT[bit32.btest(byte, pow)] -- Test if a bit is on by &ing it by 2^[bit position]
            c = c + 1
            if c == 8 then -- If the byte boundary is reached, increment pointerByte and store the new byte in `byte`
                pointerByte = pointerByte + 1
                byte = bytes[pointerByte]
                c = 0
            end
        end
        pointer = pointer + n -- Move the pointer forward
        return output
	end
	
	--Skip to the end of the current byte
	local function skipStrayBits()
		local c = pointer % 8
		if (c > 0) then
			pointer += 8 - c
			pointerByte += 1
		end
	end

	local function readBytesFast()
		return bytes
	end


    local function readByte()
        assert(pointer + 8 <= bitCount, "BitBuffer.readByte cannot read past the end of the stream")
        -- The second of two main functions for the actual 'reading' of the bitbuffer.
        -- Reads a byte and returns it
        local c = pointer % 8 -- How far into the pointerByte the pointer is
        local byte1 = bytes[pointerByte] -- The pointerByte
        pointer = pointer + 8
        if c == 0 then -- Trivial if the pointer is at the beginning of the pointerByte
            pointerByte = pointerByte + 1
            return byte1
        else
            pointerByte = pointerByte + 1
            -- Get the remainder of the first pointerByte and add it to the part of the new pointerByte that's required
            -- Both these methods are explained in writeByte
            return bit32.band(bit32.lshift(byte1, c), 255) + bit32.rshift(bytes[pointerByte], 8 - c)
        end
    end

    local function readUnsigned(width)
        assert(type(width) == "number", "argument #1 to BitBuffer.readUnsigned should be a number")
        assert(width >= 1 and width <= 64, "argument #1 to BitBuffer.readUnsigned should be in the range [1, 64]")
        assert(width % 1 == 0, "argument #1 to BitBuffer.readUnsigned should be an integer")

        assert(pointer + width <= bitCount, "BitBuffer.readUnsigned cannot read past the end of the stream")
        -- Implementing this on its own was considered because of a worry that it would be inefficient to call
        -- readByte and readBit several times, but it was decided the simplicity is worth a minor performance hit.
        local bytesInN, bitsInN = math.floor(width / 8), width % 8

        -- No check is required for if the width is greater than 32 because bit32 isn't used.
        local n = 0
        -- Shift and add a read byte however many times is necessary
        -- Adding after shifting is importnat - it prevents there from being 8 empty bits of space
        for _ = 1, bytesInN do
            n = n * 0x100 -- 2^8; equivalent to n << 8
            n = n + readByte()
        end
        -- The bits are then read and added to the number
        if bitsInN ~= 0 then
            for _, v in ipairs(readBits(width % 8)) do --todo benchmark against concat+tonumber; might be worth the code smell
                n = n * 2
                n = n + v
            end
        end
        return n
    end

    local function readSigned(width)
        assert(type(width) == "number", "argument #1 to BitBuffer.readSigned should be a number")
        assert(width >= 2 and width <= 64, "argument #1 to BitBuffer.readSigned should be in the range [2, 64]")
        assert(width % 1 == 0, "argument #1 to BitBuffer.readSigned should be an integer")

        assert(pointer + 8 <= bitCount, "BitBuffer.readSigned cannot read past the end of the stream")
        local sign = readBits(1)[1]
        local n = readUnsigned(width - 1) -- Again, width-1 is because one bit is used for the sign

        -- As said in writeSigned, the written number is unmodified if the number is positive (the sign bit is 0)
        if sign == 0 then
            return n
        else
            -- And the number is equal to max value of the width + the number if the number is negative (the sign bit is 1)
            -- To reverse that, the max value is subtracted from the stored number.
            return n - powers_of_2[width - 1]
        end
    end

    local function readFloat(exponentWidth, mantissaWidth)
        assert(type(exponentWidth) == "number", "argument #1 to BitBuffer.readFloat should be a number")
        assert(
            exponentWidth >= 1 and exponentWidth <= 64,
            "argument #1 to BitBuffer.readFloat should be in the range [1, 64]"
        )
        assert(exponentWidth % 1 == 0, "argument #1 to BitBuffer.readFloat should be an integer")

        assert(type(mantissaWidth) == "number", "argument #2 to BitBuffer.readFloat should be a number")
        assert(
            mantissaWidth >= 1 and mantissaWidth <= 64,
            "argument #2 to BitBuffer.readFloat should be in the range [1, 64]"
        )
        assert(mantissaWidth % 1 == 0, "argument #2 to BitBuffer.readFloat should be an integer")

        assert(
            pointer + exponentWidth + mantissaWidth + 1 <= bitCount,
            "BitBuffer.readFloat cannot read past the end of the stream"
        )
        -- Recomposing floats is rather straightfoward.
        -- The bias is subtracted from the exponent, the mantissa is shifted back by mantissaWidth, one is added to the mantissa
        -- and the whole thing is recomposed with math.ldexp (this is identical to mantissa*(2^exponent)).

        local bias = powers_of_2[exponentWidth - 1] - 1

        local sign = readBits(1)[1]
        local exponent = readUnsigned(exponentWidth)
        local mantissa = readUnsigned(mantissaWidth)

        -- Before normal numbers are handled though, special cases and subnormal numbers are once again handled seperately
        if exponent == powers_of_2[exponentWidth] - 1 then
            if mantissa ~= 0 then -- If the exponent is all 1s and the mantissa isn't zero, the number is NaN
                return 0 / 0
            else -- Otherwise, it's positive or negative infinity
                return sign == 0 and math.huge or -math.huge
            end
        elseif exponent == 0 then
            if mantissa == 0 then -- If the exponent and mantissa are both zero, the number is zero.
                return 0
            else -- If the exponent is zero and the mantissa is not zero, the number is subnormal
                -- Subnormal numbers are straightforward: shifting the mantissa so that it's a fraction is all that's required
                mantissa = mantissa / powers_of_2[mantissaWidth]

                -- Since the exponent is 0, it's actual value is just -bias (it would be exponent-bias)
                -- As previously touched on in writeFloat, the exponent value is off by 1 in Lua though.
                return sign == 1 and -math.ldexp(mantissa, -bias + 1) or math.ldexp(mantissa, -bias + 1)
            end
        end

        -- First, the mantissa is shifted back by the mantissaWidth
        -- Then, 1 is added to it to 'normalize' it.
        mantissa = (mantissa / powers_of_2[mantissaWidth]) + 1

        -- Because the mantissa is normalized above (the leading 1 is in the ones place), it's accurate to say exponent-bias
        return sign == 1 and -math.ldexp(mantissa, exponent - bias) or math.ldexp(mantissa, exponent - bias)
    end

    local function readString()
        assert(pointer + 24 <= bitCount, "BitBuffer.readString cannot read past the end of the stream")
        -- Reading a length-prefixed string is rather straight forward.
        -- The length is read, then that many bytes are read and put in a string.

        local stringLength = readUnsigned(24)
        assert(pointer + (stringLength * 8) <= bitCount, "BitBuffer.readString cannot read past the end of the stream")

        local outputCharacters = table.create(stringLength) --!

        for i = 1, stringLength do
            outputCharacters[i] = readByte()
        end

        local output = table.create(math.ceil(stringLength / 4096))
        local k = 1
        for i = 1, stringLength, 4096 do
            output[k] = string.char(table.unpack(outputCharacters, i, math.min(stringLength, i + 4095)))
            k = k + 1
        end

        return table.concat(output)
    end

    local function readTerminatedString()
        local outputCharacters = {}

        -- Bytes are read continuously until either a nul-character is reached or until the stream runs out.
        local length = 0
        while true do
            local byte = readByte()
            if not byte then -- Stream has ended
                error("BitBuffer.readTerminatedString cannot read past the end of the stream", 2)
            elseif byte == 0 then -- String has ended
                break
            else -- Add byte to string
                length = length + 1
                outputCharacters[length] = byte
            end
        end

        local output = table.create(math.ceil(length / 4096))
        local k = 1
        for l = 1, length, 4096 do
            output[k] = string.char(table.unpack(outputCharacters, l, math.min(length, l + 4095)))
            k = k + 1
        end

        return table.concat(output)
    end

    local function readSetLengthString(length)
        assert(type(length) == "number", "argument #1 to BitBuffer.readSetLengthString should be a number")
        assert(length >= 0, "argument #1 to BitBuffer.readSetLengthString should be zero or higher.")
        assert(length % 1 == 0, "argument #1 to BitBuffer.readSetLengthString should be an integer")

        assert(
            pointer + (length * 8) <= bitCount,
            "BitBuffer.readSetLengthString cannot read past the end of the stream"
        )
        -- `length` number of bytes are read and put into a string

        local outputCharacters = table.create(length) --!

        for i = 1, length do
            outputCharacters[i] = readByte()
        end

        local output = table.create(math.ceil(length / 4096))
        local k = 1
        for i = 1, length, 4096 do
            output[k] = string.char(table.unpack(outputCharacters, i, math.min(length, i + 4095)))
            k = k + 1
        end

        return table.concat(output)
    end

    local function readField(n)
        assert(type(n) == "number", "argument #1 to BitBuffer.readField should be a number")
        assert(n > 0, "argument #1 to BitBuffer.readField should be above 0")
        assert(n % 1 == 0, "argument #1 to BitBuffer.readField should be an integer")

        assert(pointer + n <= bitCount, "BitBuffer.readField cannot read past the end of the stream")
        -- Reading a bit field is again rather simple. You read the actual field, then take the bits out.
        local readInt = readUnsigned(n)
        local output = table.create(n) --!

        for i = n, 1, -1 do -- In reverse order since we're pulling bits out from lsb to msb
            output[i] = readInt % 2 == 1 -- Equivalent to an extraction of the lsb
            readInt = math.floor(readInt / 2) -- Equivalent to readInt>>1
        end

        return output
    end

    -- All read functions below here are shorthands.
    -- As with their write variants, these functions are implemented manually using readByte for performance reasons.

    local function readUInt8()
        assert(pointer + 8 <= bitCount, "BitBuffer.readUInt8 cannot read past the end of the stream")

        return readByte()
    end

    local function readUInt16()
        assert(pointer + 16 <= bitCount, "BitBuffer.readUInt16 cannot read past the end of the stream")

        return bit32.lshift(readByte(), 8) + readByte()
    end

    local function readUInt32()
        assert(pointer + 32 <= bitCount, "BitBuffer.readUInt32 cannot read past the end of the stream")

        return bit32.lshift(readByte(), 24) + bit32.lshift(readByte(), 16) + bit32.lshift(readByte(), 8) + readByte()
    end

    local function readInt8()
        assert(pointer + 8 <= bitCount, "BitBuffer.readInt8 cannot read past the end of the stream")

        local n = readByte()
        local sign = bit32.btest(n, 128)
        n = bit32.band(n, 127)

        if sign then
            return n - 128
        else
            return n
        end
    end

    local function readInt16()
        assert(pointer + 16 <= bitCount, "BitBuffer.readInt16 cannot read past the end of the stream")

        local n = bit32.lshift(readByte(), 8) + readByte()
        local sign = bit32.btest(n, 32768)
        n = bit32.band(n, 32767)

        if sign then
            return n - 32768
        else
            return n
        end
    end

    local function readInt32()
        assert(pointer + 32 <= bitCount, "BitBuffer.readInt32 cannot read past the end of the stream")

        local n = bit32.lshift(readByte(), 24) + bit32.lshift(readByte(), 16) + bit32.lshift(readByte(), 8) + readByte()
        local sign = bit32.btest(n, 2147483648)
        n = bit32.band(n, 2147483647)

        if sign then
            return n - 2147483648
        else
            return n
        end
    end

    local function readFloat16()
        assert(pointer + 16 <= bitCount, "BitBuffer.readFloat16 cannot read past the end of the stream")

        local b0 = readByte()
        local sign = bit32.btest(b0, 128)
        local exponent = bit32.rshift(bit32.band(b0, 127), 2)
        local mantissa = bit32.lshift(bit32.band(b0, 3), 8) + readByte()

        if exponent == 31 then --2^5-1
            if mantissa ~= 0 then
                return 0 / 0
            else
                return sign and -math.huge or math.huge
            end
        elseif exponent == 0 then
            if mantissa == 0 then
                return 0
            else
                return sign and -math.ldexp(mantissa / 1024, -14) or math.ldexp(mantissa / 1024, -14)
            end
        end

        mantissa = (mantissa / 1024) + 1

        return sign and -math.ldexp(mantissa, exponent - 15) or math.ldexp(mantissa, exponent - 15)
    end

    local function readFloat32()
        assert(pointer + 32 <= bitCount, "BitBuffer.readFloat32 cannot read past the end of the stream")

        local b0 = readByte()
        local b1 = readByte()
        local sign = bit32.btest(b0, 128)
        local exponent = bit32.band(bit32.lshift(b0, 1), 255) + bit32.rshift(b1, 7)
        local mantissa = bit32.lshift(bit32.band(b1, 127), 23 - 7)
            + bit32.lshift(readByte(), 23 - 7 - 8)
            + bit32.lshift(readByte(), 23 - 7 - 8 - 8)

        if exponent == 255 then -- 2^8-1
            if mantissa ~= 0 then
                return 0 / 0
            else
                return sign and -math.huge or math.huge
            end
        elseif exponent == 0 then
            if mantissa == 0 then
                return 0
            else
                -- -126 is the 0-bias+1
                return sign and -math.ldexp(mantissa / 8388608, -126) or math.ldexp(mantissa / 8388608, -126)
            end
        end

        mantissa = (mantissa / 8388608) + 1

        return sign and -math.ldexp(mantissa, exponent - 127) or math.ldexp(mantissa, exponent - 127)
    end

    local function readFloat64()
        assert(pointer + 64 <= bitCount, "BitBuffer.readFloat64 cannot read past the end of the stream")

        local b0 = readByte()
        local b1 = readByte()

        local sign = bit32.btest(b0, 128)
        local exponent = bit32.lshift(bit32.band(b0, 127), 4) + bit32.rshift(b1, 4)
        local mostSignificantChunk = bit32.lshift(bit32.band(b1, 15), 16) + bit32.lshift(readByte(), 8) + readByte()
        local leastSignificantChunk = bit32.lshift(readByte(), 24)
            + bit32.lshift(readByte(), 16)
            + bit32.lshift(readByte(), 8)
            + readByte()

        -- local mantissa = (bit32.lshift(bit32.band(b1, 15), 16)+bit32.lshift(readByte(), 8)+readByte())*0x100000000+
        --     bit32.lshift(readByte(), 24)+bit32.lshift(readByte(), 16)+bit32.lshift(readByte(), 8)+readByte()

        local mantissa = mostSignificantChunk * 0x100000000 + leastSignificantChunk

        if exponent == 2047 then -- 2^11-1
            if mantissa ~= 0 then
                return 0 / 0
            else
                return sign and -math.huge or math.huge
            end
        elseif exponent == 0 then
            if mantissa == 0 then
                return 0
            else
                return sign and -math.ldexp(mantissa / 4503599627370496, -1022)
                    or math.ldexp(mantissa / 4503599627370496, -1022)
            end
        end

        mantissa = (mantissa / 4503599627370496) + 1

        return sign and -math.ldexp(mantissa, exponent - 1023) or math.ldexp(mantissa, exponent - 1023)
    end

    -- All read functions below here are Roblox specific datatypes.

    local function readBrickColor()
        assert(pointer + 16 <= bitCount, "BitBuffer.readBrickColor cannot read past the end of the stream")

        return BrickColor.new(readUInt16())
    end

    local function readColor3()
        assert(pointer + 24 <= bitCount, "BitBuffer.readColor3 cannot read past the end of the stream")

        return Color3.fromRGB(readByte(), readByte(), readByte())
    end

    local function readCFrame()
        assert(pointer + 8 <= bitCount, "BitBuffer.readCFrame cannot read past the end of the stream")

        local id = readByte()

        if id == 0 then
            assert(pointer + 384 <= bitCount, "BitBuffer.readCFrame cannot read past the end of the stream") -- 4*12 bytes = 383 bits

            -- stylua: ignore
            return CFrame.new(
                readFloat32(), readFloat32(), readFloat32(),
                readFloat32(), readFloat32(), readFloat32(),
                readFloat32(), readFloat32(), readFloat32(),
                readFloat32(), readFloat32(), readFloat32()
            )
        else
            assert(pointer + 96 <= bitCount, "BitBuffer.readCFrame cannot read past the end of the stream") -- 4*3 bytes = 96 bits

            local rightVector = NORMAL_ID_VECTORS[math.floor(id / 6)]
            local upVector = NORMAL_ID_VECTORS[id % 6]
            local lookVector = rightVector:Cross(upVector)

            -- CFrame's full-matrix constructor takes right/up/look vectors as columns...
            -- stylua: ignore
            return CFrame.new(
                readFloat32(), readFloat32(), readFloat32(),
                rightVector.X, upVector.X, lookVector.X,
                rightVector.Y, upVector.Y, lookVector.Y,
                rightVector.Z, upVector.Z, lookVector.Z
            )
        end
    end

    local function readVector3()
        assert(pointer + 96 <= bitCount, "BitBuffer.readVector3 cannot read past the end of the stream")

        return Vector3.new(readFloat32(), readFloat32(), readFloat32())
    end

    local function readVector2()
        assert(pointer + 64 <= bitCount, "BitBuffer.readVector2 cannot read past the end of the stream")

        return Vector2.new(readFloat32(), readFloat32())
    end

    local function readUDim2()
        assert(pointer + 128 <= bitCount, "BitBuffer.readUDim2 cannot read past the end of the stream")

        return UDim2.new(readFloat32(), readInt32(), readFloat32(), readInt32())
    end

    local function readUDim()
        assert(pointer + 64 <= bitCount, "BitBuffer.readUDim cannot read past the end of the stream")

        return UDim.new(readFloat32(), readInt32())
    end

    local function readRay()
        assert(pointer + 192 <= bitCount, "BitBuffer.readRay cannot read past the end of the stream")

        return Ray.new(
            Vector3.new(readFloat32(), readFloat32(), readFloat32()),
            Vector3.new(readFloat32(), readFloat32(), readFloat32())
        )
    end

    local function readRect()
        assert(pointer + 128 <= bitCount, "BitBuffer.readRect cannot read past the end of the stream")

        return Rect.new(readFloat32(), readFloat32(), readFloat32(), readFloat32())
    end

    local function readRegion3()
        assert(pointer + 192 <= bitCount, "BitBuffer.readRegion3 cannot read past the end of the stream")

        return Region3.new(
            Vector3.new(readFloat32(), readFloat32(), readFloat32()),
            Vector3.new(readFloat32(), readFloat32(), readFloat32())
        )
    end

    local function readEnum()
        assert(pointer + 8 <= bitCount, "BitBuffer.readEnum cannot read past the end of the stream")

        local name = readTerminatedString() -- This might expose an error from readString to the end-user but it's not worth the hassle to fix.

        assert(pointer + 16 <= bitCount, "BitBuffer.readEnum cannot read past the end of the stream")

        local value = readUInt16() -- Again, optimistically assuming no Roblox Enum value will ever pass 65,535

        -- Catching a potential error only to throw it with different formatting seems... Superfluous.
        -- Open an issue on github if you feel otherwise.
        for _, v in ipairs(Enum[name]:GetEnumItems()) do
            if v.Value == value then
                return v
            end
        end

        error(
            "BitBuffer.readEnum could not get value: `"
                .. tostring(value)
                .. "` is not a valid member of `"
                .. name
                .. "`",
            2
        )
    end

    local function readNumberRange()
        assert(pointer + 64 <= bitCount, "BitBuffer.readNumberRange cannot read past the end of the stream")

        return NumberRange.new(readFloat32(), readFloat32())
    end

    local function readNumberSequence()
        assert(pointer + 32 <= bitCount, "BitBuffer.readNumberSequence cannot read past the end of the stream")

        local keypointCount = readUInt32()

        assert(pointer + keypointCount * 96, "BitBuffer.readColorSequence cannot read past the end of the stream")

        local keypoints = table.create(keypointCount)

        -- As it turns out, creating a NumberSequence with a negative value as its first argument (in the first and second constructor)
        -- creates NumberSequenceKeypoints with negative envelopes. The envelope is read and saved properly, as you would expect,
        -- but you can't create a NumberSequence with a negative envelope if you're using a table of keypoints (which is happening here).
        -- If you're confused, run this snippet: NumberSequence.new(NumberSequence.new(-1).Keypoints)
        -- As a result, there has to be some branching logic in this function.
        -- ColorSequences don't have envelopes so it's not necessary for them.

        for i = 1, keypointCount do
            local time, value, envelope = readFloat32(), readFloat32(), readFloat32()
            if value < 0 then
                envelope = nil
            end
            keypoints[i] = NumberSequenceKeypoint.new(time, value, envelope)
        end

        return NumberSequence.new(keypoints)
    end

    local function readColorSequence()
        assert(pointer + 32 <= bitCount, "BitBuffer.readColorSequence cannot read past the end of the stream")

        local keypointCount = readUInt32()

        assert(pointer + keypointCount * 56, "BitBuffer.readColorSequence cannot read past the end of the stream")

        local keypoints = table.create(keypointCount)

        for i = 1, keypointCount do
            keypoints[i] = ColorSequenceKeypoint.new(readFloat32(), Color3.fromRGB(readByte(), readByte(), readByte()))
        end

        return ColorSequence.new(keypoints)
    end

    return {
        dumpBinary = dumpBinary,
        dumpString = dumpString,
        dumpHex = dumpHex,
        dumpBase64 = dumpBase64,
        exportChunk = exportChunk,
        exportBase64Chunk = exportBase64Chunk,
        exportHexChunk = exportHexChunk,

        crc32 = crc32,
        getLength = getLength,
        getByteLength = getByteLength,
        getPointer = getPointer,
        setPointer = setPointer,
        setPointerFromEnd = setPointerFromEnd,
        getPointerByte = getPointerByte,
        setPointerByte = setPointerByte,
        setPointerByteFromEnd = setPointerByteFromEnd,
        isFinished = isFinished,

        writeBits = writeBits,
		writeByte = writeByte,
		writeBytesFast = writeBytesFast,
        writeUnsigned = writeUnsigned,
        writeSigned = writeSigned,
        writeFloat = writeFloat,
        writeBase64 = writeBase64,
        writeString = writeString,
        writeTerminatedString = writeTerminatedString,
        writeSetLengthString = writeSetLengthString,
        writeField = writeField,

        writeUInt8 = writeUInt8,
        writeUInt16 = writeUInt16,
        writeUInt32 = writeUInt32,
        writeInt8 = writeInt8,
        writeInt16 = writeInt16,
        writeInt32 = writeInt32,

        writeFloat16 = writeFloat16,
        writeFloat32 = writeFloat32,
        writeFloat64 = writeFloat64,

        writeBrickColor = writeBrickColor,
        writeColor3 = writeColor3,
        writeCFrame = writeCFrame,
        writeVector3 = writeVector3,
        writeVector2 = writeVector2,
        writeUDim2 = writeUDim2,
        writeUDim = writeUDim,
        writeRay = writeRay,
        writeRect = writeRect,
        writeRegion3 = writeRegion3,
        writeEnum = writeEnum,
        writeNumberRange = writeNumberRange,
        writeNumberSequence = writeNumberSequence,
        writeColorSequence = writeColorSequence,

        readBits = readBits,
        readByte = readByte,
        readUnsigned = readUnsigned,
        readSigned = readSigned,
        readFloat = readFloat,
        readString = readString,
        readTerminatedString = readTerminatedString,
        readSetLengthString = readSetLengthString,
		readField = readField,
		readBytesFast = readBytesFast,
		skipStrayBits = skipStrayBits,

        readUInt8 = readUInt8,
        readUInt16 = readUInt16,
        readUInt32 = readUInt32,
        readInt8 = readInt8,
        readInt16 = readInt16,
        readInt32 = readInt32,

        readFloat16 = readFloat16,
        readFloat32 = readFloat32,
        readFloat64 = readFloat64,

        readBrickColor = readBrickColor,
        readColor3 = readColor3,
        readCFrame = readCFrame,
        readVector3 = readVector3,
        readVector2 = readVector2,
        readUDim2 = readUDim2,
        readUDim = readUDim,
        readRay = readRay,
        readRect = readRect,
        readRegion3 = readRegion3,
        readEnum = readEnum,
        readNumberRange = readNumberRange,
        readNumberSequence = readNumberSequence,
        readColorSequence = readColorSequence,
    }
end

return bitBuffer
