local struct = {}

function struct.pack(format, ...)
	local stream = {}
	local vars = { ... }
	local endianness = true

	for i = 1, format:len() do
		local opt = format:sub(i, i)

		if opt == "<" then
			endianness = true
		elseif opt == ">" then
			endianness = false
		elseif opt:find("[bBhHiIlL]") then
			local n = opt:find("[hH]") and 2 or opt:find("[iI]") and 4 or opt:find("[lL]") and 8 or 1
			local val = tonumber(table.remove(vars, 1))

			local bytes = {}
			for _ = 1, n do
				table.insert(bytes, string.char(val % (2 ^ 8)))
				val = math.floor(val / (2 ^ 8))
			end

			if not endianness then
				table.insert(stream, string.reverse(table.concat(bytes)))
			else
				table.insert(stream, table.concat(bytes))
			end
		elseif opt:find("[fd]") then
			local val = tonumber(table.remove(vars, 1))
			local sign = 0

			if val < 0 then
				sign = 1
				val = -val
			end

			local mantissa, exponent = math.frexp(val)
			if val == 0 then
				mantissa = 0
				exponent = 0
			else
				mantissa = (mantissa * 2 - 1) * math.ldexp(0.5, (opt == "d") and 53 or 24)
				exponent = exponent + ((opt == "d") and 1022 or 126)
			end

			local bytes = {}
			if opt == "d" then
				val = mantissa
				for _ = 1, 6 do
					table.insert(bytes, string.char(math.floor(val) % (2 ^ 8)))
					val = math.floor(val / (2 ^ 8))
				end
			else
				table.insert(bytes, string.char(math.floor(mantissa) % (2 ^ 8)))
				val = math.floor(mantissa / (2 ^ 8))
				table.insert(bytes, string.char(math.floor(val) % (2 ^ 8)))
				val = math.floor(val / (2 ^ 8))
			end

			table.insert(bytes, string.char(math.floor(exponent * ((opt == "d") and 16 or 128) + val) % (2 ^ 8)))
			val = math.floor((exponent * ((opt == "d") and 16 or 128) + val) / (2 ^ 8))
			table.insert(bytes, string.char(math.floor(sign * 128 + val) % (2 ^ 8)))

			if not endianness then
				table.insert(stream, string.reverse(table.concat(bytes)))
			else
				table.insert(stream, table.concat(bytes))
			end
		elseif opt == "s" then
			table.insert(stream, tostring(table.remove(vars, 1)))
			table.insert(stream, string.char(0))
		elseif opt == "c" then
			local n = format:sub(i + 1):match("%d+")
			local str = tostring(table.remove(vars, 1))
			local len = tonumber(n)
			if len <= 0 then
				len = str:len()
			end
			if len - str:len() > 0 then
				str = str .. string.rep(" ", len - str:len())
			end
			table.insert(stream, str:sub(1, len))
		end
	end

	return table.concat(stream)
end

function struct.unpack(format, stream, pos)
	local vars = {}
	local iterator = pos or 1
	local endianness = true

	for i = 1, format:len() do
		local opt = format:sub(i, i)

		if opt == "<" then
			endianness = true
		elseif opt == ">" then
			endianness = false
		elseif opt:find("[bBhHiIlL]") then
			local n = opt:find("[hH]") and 2 or opt:find("[iI]") and 4 or opt:find("[lL]") and 8 or 1
			local signed = opt:lower() == opt

			local val = 0
			for j = 1, n do
				local byte = string.byte(stream:sub(iterator, iterator))
				if endianness then
					val = val + byte * (2 ^ ((j - 1) * 8))
				else
					val = val + byte * (2 ^ ((n - j) * 8))
				end
				iterator = iterator + 1
			end

			if signed and val >= 2 ^ (n * 8 - 1) then
				val = val - 2 ^ (n * 8)
			end

			table.insert(vars, math.floor(val))
		elseif opt:find("[fd]") then
			local n = (opt == "d") and 8 or 4
			local x = stream:sub(iterator, iterator + n - 1)
			iterator = iterator + n

			if not endianness then
				x = string.reverse(x)
			end

			local sign = 1
			local mantissa = string.byte(x, (opt == "d") and 7 or 3) % ((opt == "d") and 16 or 128)
			for j = n - 2, 1, -1 do
				mantissa = mantissa * (2 ^ 8) + string.byte(x, j)
			end

			if string.byte(x, n) > 127 then
				sign = -1
			end

			local exponent = (string.byte(x, n) % 128) * ((opt == "d") and 16 or 2)
				+ math.floor(string.byte(x, n - 1) / ((opt == "d") and 16 or 128))
			if exponent == 0 then
				table.insert(vars, 0.0)
			else
				mantissa = (math.ldexp(mantissa, (opt == "d") and -52 or -23) + 1) * sign
				table.insert(vars, math.ldexp(mantissa, exponent - ((opt == "d") and 1023 or 127)))
			end
		elseif opt == "s" then
			local bytes = {}
			for j = iterator, stream:len() do
				if stream:sub(j, j) == string.char(0) or stream:sub(j) == "" then
					break
				end

				table.insert(bytes, stream:sub(j, j))
			end

			local str = table.concat(bytes)
			iterator = iterator + str:len() + 1
			table.insert(vars, str)
		elseif opt == "c" then
			local n = format:sub(i + 1):match("%d+")
			local len = tonumber(n)
			if len <= 0 then
				len = table.remove(vars)
			end

			table.insert(vars, stream:sub(iterator, iterator + len - 1))
			iterator = iterator + len
		end
	end

	return unpack(vars)
end

return struct
