--[[

Here is a MQTT v5.0 protocol implementation

MQTT v5.0 documentation (DOC):
	http://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html

]]

-- module table
local protocol5 = {}

-- load required stuff
local type = type
local error = error
local assert = assert
local ipairs = ipairs
local require = require
local tostring = tostring
local setmetatable = setmetatable

local table = require("table")
local unpack = table.unpack or unpack
local tbl_sort = table.sort

local string = require("string")
local str_char = string.char
local fmt = string.format

local const = require("mqtt.const")
local const_v50 = const.v50

local tools = require("mqtt.tools")
local sortedpairs = tools.sortedpairs

local bit = require("mqtt.bitwrap")
local bor = bit.bor
local band = bit.band
local lshift = bit.lshift
local rshift = bit.rshift

local protocol = require("mqtt.protocol")
local make_uint8 = protocol.make_uint8
local make_uint16 = protocol.make_uint16
local make_uint32 = protocol.make_uint32
local make_string = protocol.make_string
local make_var_length = protocol.make_var_length
local parse_var_length = protocol.parse_var_length
local make_uint8_0_or_1 = protocol.make_uint8_0_or_1
local make_uint16_nonzero = protocol.make_uint16_nonzero
local make_var_length_nonzero = protocol.make_var_length_nonzero
local parse_string = protocol.parse_string
local parse_uint8 = protocol.parse_uint8
local parse_uint8_0_or_1 = protocol.parse_uint8_0_or_1
local parse_uint16 = protocol.parse_uint16
local parse_uint16_nonzero = protocol.parse_uint16_nonzero
local parse_uint32 = protocol.parse_uint32
local parse_var_length_nonzero = protocol.parse_var_length_nonzero
local make_header = protocol.make_header
local check_qos = protocol.check_qos
local check_packet_id = protocol.check_packet_id
local combine = protocol.combine
local packet_type = protocol.packet_type
local packet_mt = protocol.packet_mt
local connack_packet_mt = protocol.connack_packet_mt
local start_parse_packet = protocol.start_parse_packet
local parse_packet_connect_input = protocol.parse_packet_connect_input

-- Returns true if given value is a valid Retain Handling option, DOC: 3.8.3.1 Subscription Options
local function check_retain_handling(val)
	return (val == 0) or (val == 1) or (val == 2)
end

-- Create Connect Flags data, DOC: 3.1.2.3 Connect Flags
local function make_connect_flags(args)
	local byte = 0 -- bit 0 should be zero
	-- DOC: 3.1.2.4 Clean Start
	if args.clean ~= nil then
		assert(type(args.clean) == "boolean", "expecting .clean to be a boolean")
		if args.clean then
			byte = bor(byte, lshift(1, 1))
		end
	end
	-- DOC: 3.1.2.5 Will Flag
	if args.will ~= nil then
		-- check required args are presented
		assert(type(args.will) == "table", "expecting .will to be a table")
		assert(type(args.will.payload) == "string", "expecting .will.payload to be a string")
		assert(type(args.will.topic) == "string", "expecting .will.topic to be a string")
		assert(type(args.will.qos) == "number", "expecting .will.qos to be a number")
		assert(check_qos(args.will.qos), "expecting .will.qos to be a valid QoS value")
		assert(type(args.will.retain) == "boolean", "expecting .will.retain to be a boolean")
		if args.will.properties ~= nil then
			assert(type(args.will.properties) == "table", "expecting .will.properties to be a table")
		end
		if args.will.user_properties ~= nil then
			assert(type(args.will.user_properties) == "table", "expecting .will.user_properties to be a table")
		end
		-- will flag should be set to 1
		byte = bor(byte, lshift(1, 2))
		-- DOC: 3.1.2.6 Will QoS
		byte = bor(byte, lshift(args.will.qos, 3))
		-- DOC: 3.1.2.7 Will Retain
		if args.will.retain then
			byte = bor(byte, lshift(1, 5))
		end
	end
	-- DOC: 3.1.2.8 User Name Flag
	if args.username ~= nil then
		assert(type(args.username) == "string", "expecting .username to be a string")
		byte = bor(byte, lshift(1, 7))
	end
	-- DOC: 3.1.2.9 Password Flag
	if args.password ~= nil then
		assert(type(args.password) == "string", "expecting .password to be a string")
		assert(args.username, "the .username is required to set .password")
		byte = bor(byte, lshift(1, 6))
	end
	return make_uint8(byte)
end

-- Known property names and its identifiers, DOC: 2.2.2.2 Property
local property_pairs = {
	{ 0x01, "payload_format_indicator",
		make = make_uint8_0_or_1,
		parse = parse_uint8_0_or_1, },
	{ 0x02, "message_expiry_interval",
		make = make_uint32,
		parse = parse_uint32, },
	{ 0x03, "content_type",
		make = make_string,
		parse = parse_string, },
	{ 0x08, "response_topic",
		make = make_string,
		parse = parse_string, },
	{ 0x09, "correlation_data",
		make = make_string,
		parse = parse_string, },
	{ 0x0B, "subscription_identifiers",
		make = function(value) return str_char(make_var_length_nonzero(value)) end,
		parse = parse_var_length_nonzero,
		multiple = true, },
	{ 0x11, "session_expiry_interval",
		make = make_uint32,
		parse = parse_uint32, },
	{ 0x12, "assigned_client_identifier",
		make = make_string,
		parse = parse_string, },
	{ 0x13, "server_keep_alive",
		make = make_uint16,
		parse = parse_uint16, },
	{ 0x15, "authentication_method",
		make = make_string,
		parse = parse_string, },
	{ 0x16, "authentication_data",
		make = make_string,
		parse = parse_string, },
	{ 0x17, "request_problem_information",
		make = make_uint8_0_or_1,
		parse = parse_uint8_0_or_1, },
	{ 0x18, "will_delay_interval",
		make = make_uint32,
		parse = parse_uint32, },
	{ 0x19, "request_response_information",
		make = make_uint8_0_or_1,
		parse = parse_uint8_0_or_1, },
	{ 0x1A, "response_information",
		make = make_string,
		parse = parse_string, },
	{ 0x1C, "server_reference",
		make = make_string,
		parse = parse_string, },
	{ 0x1F, "reason_string",
		make = make_string,
		parse = parse_string, },
	{ 0x21, "receive_maximum",
		make = make_uint16,
		parse = parse_uint16, },
	{ 0x22, "topic_alias_maximum",
		make = make_uint16,
		parse = parse_uint16, },
	{ 0x23, "topic_alias",
		make = make_uint16_nonzero,
		parse = parse_uint16_nonzero, },
	{ 0x24, "maximum_qos",
		make = make_uint8_0_or_1,
		parse = parse_uint8_0_or_1, },
	{ 0x25, "retain_available",
		make = make_uint8_0_or_1,
		parse = parse_uint8_0_or_1, },
	{ 0x26, "user_property", -- NOTE: not implemented intentionally
		make = function(value_) error("not implemented") end,
		parse = function(read_func_) error("not implemented") end, },
	{ 0x27, "maximum_packet_size",
		make = make_uint32,
		parse = parse_uint32, },
	{ 0x28, "wildcard_subscription_available",
		make = make_uint8_0_or_1,
		parse = parse_uint8_0_or_1, },
	{ 0x29, "subscription_identifiers_available",
		make = make_uint8_0_or_1,
		parse = parse_uint8_0_or_1, },
	{ 0x2A, "shared_subscription_available",
		make = make_uint8_0_or_1,
		parse = parse_uint8_0_or_1, },
}

-- properties table with keys in two directions: from name to identifier and back
local properties = {}
-- table with property value make functions
local property_make = {}
-- table with property value parse function
local property_parse = {}
-- table with property multiple flag
local property_multiple = {}
-- fill the properties and property_make tables
for _, prop in ipairs(property_pairs) do
	properties[prop[2]] = prop[1]				-- name ==> identifier
	properties[prop[1]] = prop[2]				-- identifier ==> name
	property_make[prop[1]] = prop.make			-- identifier ==> make function
	property_parse[prop[1]] = prop.parse		-- identifier ==> make function
	property_multiple[prop[1]] = prop.multiple	-- identifier ==> multiple flag
end

-- Allowed properties per packet type
local allowed_properties = {
	[packet_type.CONNECT] = {
		[0x11] = true, -- DOC: 3.1.2.11.2 Session Expiry Interval
		[0x21] = true, -- DOC: 3.1.2.11.3 Receive Maximum
		[0x27] = true, -- DOC: 3.1.2.11.4 Maximum Packet Size
		[0x22] = true, -- DOC: 3.1.2.11.5 Topic Alias Maximum
		[0x19] = true, -- DOC: 3.1.2.11.6 Request Response Information
		[0x17] = true, -- DOC: 3.1.2.11.7 Request Problem Information
		[0x26] = true, -- DOC: 3.1.2.11.8 User Property
		[0x15] = true, -- DOC: 3.1.2.11.9 Authentication Method
		[0x16] = true, -- DOC: 3.1.2.11.10 Authentication Data
	},
	[packet_type.CONNACK] = {
		[0x11] = true, -- DOC: 3.2.2.3.2 Session Expiry Interval
		[0x21] = true, -- DOC: 3.2.2.3.3 Receive Maximum
		[0x24] = true, -- DOC: 3.2.2.3.4 Maximum QoS
		[0x25] = true, -- DOC: 3.2.2.3.5 Retain Available
		[0x27] = true, -- DOC: 3.2.2.3.6 Maximum Packet Size
		[0x12] = true, -- DOC: 3.2.2.3.7 Assigned Client Identifier
		[0x22] = true, -- DOC: 3.2.2.3.8 Topic Alias Maximum
		[0x1F] = true, -- DOC: 3.2.2.3.9 Reason String
		[0x26] = true, -- DOC: 3.2.2.3.10 User Property
		[0x28] = true, -- DOC: 3.2.2.3.11 Wildcard Subscription Available
		[0x29] = true, -- DOC: 3.2.2.3.12 Subscription Identifiers Available
		[0x2A] = true, -- DOC: 3.2.2.3.13 Shared Subscription Available
		[0x13] = true, -- DOC: 3.2.2.3.14 Server Keep Alive
		[0x1A] = true, -- DOC: 3.2.2.3.15 Response Information
		[0x1C] = true, -- DOC: 3.2.2.3.16 Server Reference
		[0x15] = true, -- DOC: 3.2.2.3.17 Authentication Method
		[0x16] = true, -- DOC: 3.2.2.3.18 Authentication Data
	},
	[packet_type.PUBLISH] = {
		[0x01] = true, -- DOC: 3.3.2.3.2 Payload Format Indicator
		[0x02] = true, -- DOC: 3.3.2.3.3 Message Expiry Interval
		[0x23] = true, -- DOC: 3.3.2.3.4 Topic Alias
		[0x08] = true, -- DOC: 3.3.2.3.5 Response Topic
		[0x09] = true, -- DOC: 3.3.2.3.6 Correlation Data
		[0x26] = true, -- DOC: 3.3.2.3.7 User Property
		[0x0B] = true, -- DOC: 3.3.2.3.8 Subscription Identifier
		[0x03] = true, -- DOC: 3.3.2.3.9 Content Type
	},
	will = {
		[0x18] = true, -- DOC: 3.1.3.2.2 Will Delay Interval
		[0x01] = true, -- DOC: 3.1.3.2.3 Payload Format Indicator
		[0x02] = true, -- DOC: 3.1.3.2.4 Message Expiry Interval
		[0x03] = true, -- DOC: 3.1.3.2.5 Content Type
		[0x08] = true, -- DOC: 3.1.3.2.6 Response Topic
		[0x09] = true, -- DOC: 3.1.3.2.7 Correlation Data
		[0x26] = true, -- DOC: 3.1.3.2.8 User Property
	},
	[packet_type.PUBACK] = {
		[0x1F] = true, -- DOC: 3.4.2.2.2 Reason String
		[0x26] = true, -- DOC: 3.4.2.2.3 User Property
	},
	[packet_type.PUBREC] = {
		[0x1F] = true, -- DOC: 3.5.2.2.2 Reason String
		[0x26] = true, -- DOC: 3.5.2.2.3 User Property
	},
	[packet_type.PUBREL] = {
		[0x1F] = true, -- DOC: 3.6.2.2.2 Reason String
		[0x26] = true, -- DOC: 3.6.2.2.3 User Property
	},
	[packet_type.PUBCOMP] = {
		[0x1F] = true, -- DOC: 3.7.2.2.2 Reason String
		[0x26] = true, -- DOC: 3.7.2.2.3 User Property
	},
	[packet_type.SUBSCRIBE] = {
		[0x0B] = { multiple=false }, -- DOC: 3.8.2.1.2 Subscription Identifier -- DOC: It is a Protocol Error to include the Subscription Identifier more than once.
		[0x26] = true, -- DOC: 3.8.2.1.3 User Property
	},
	[packet_type.SUBACK] = {
		[0x1F] = true, -- DOC: 3.9.2.1.2 Reason String
		[0x26] = true, -- DOC: 3.9.2.1.3 User Property
	},
	[packet_type.UNSUBSCRIBE] = {
		[0x26] = true, -- DOC: 3.10.2.1.2 User Property
	},
	[packet_type.UNSUBACK] = {
		[0x1F] = true, -- DOC: 3.11.2.1.2 Reason String
		[0x26] = true, -- DOC: 3.11.2.1.3 User Property
	},
	-- NOTE: PINGREQ (3.12), PINGRESP (3.13) has no properties
	[packet_type.DISCONNECT] = {
		[0x11] = true, -- DOC: 3.14.2.2.2 Session Expiry Interval
		[0x1F] = true, -- DOC: 3.14.2.2.3 Reason String
		[0x26] = true, -- DOC: 3.14.2.2.4 User Property
		[0x1C] = true, -- DOC: 3.14.2.2.5 Server Reference
	},
	[packet_type.AUTH] = {
		[0x15] = true, -- DOC: 3.15.2.2.2 Authentication Method
		[0x16] = true, -- DOC: 3.15.2.2.3 Authentication Data
		[0x1F] = true, -- DOC: 3.15.2.2.4 Reason String
		[0x26] = true, -- DOC: 3.15.2.2.5 User Property
	},
}

-- Create properties field for various control packets, DOC: 2.2.2 Properties
local function make_properties(ptype, args)
	local allowed = assert(allowed_properties[ptype], "invalid packet type to detect allowed properties")
	local props = ""
	local uprop_id = properties.user_property
	-- writing known properties
	if args.properties ~= nil then
		assert(type(args.properties) == "table", "expecting .properties to be a table")
		-- validate all properties and append them to order list
		local order = {}
		for name, value in sortedpairs(args.properties) do
			assert(type(name) == "string", "expecting property name to be a string: "..tostring(name))
			-- detect property identifier and check it's allowed for that packet type
			local prop_id = assert(properties[name], "unknown property: "..tostring(name))
			assert(prop_id ~= uprop_id, "user properties should be passed in .user_properties table")
			if not allowed[prop_id] then
				error("property "..name.." is not allowed for packet type "..packet_type[ptype])
			end
			order[#order + 1] = { prop_id, name, value }
		end
		-- sort props in the identifier ascending order
		tbl_sort(order, function(a, b) return a[1] < b[1] end)
		for _, item in ipairs(order) do
			local prop_id, name,  value = unpack(item)
			if property_multiple[prop_id] then
				assert(type(value) == "table", "expecting list-table for property with multiple value")
				assert(#value == 1, "only one value for multiple-property supported")
				value = value[1]
			end
			-- make property data
			local ok, val = pcall(property_make[prop_id], value)
			if not ok then
				error("invalid property value: "..name.." = "..tostring(value)..": "..tostring(val))
			end
			local prop = combine(
				str_char(make_var_length(prop_id)),
				val
			)
			-- and append it to props
			if type(props) == "string" then
				props = combine(prop)
			else
				props:append(prop)
			end
		end
	end
	-- writing userproperties
	if args.user_properties ~= nil then
		assert(type(args.user_properties) == "table", "expecting .user_properties to be a table")
		assert(allowed[uprop_id], "user_property is not allowed for packet type "..ptype)
		local order = {}
		local dups = {}
		if args.user_properties[1] then
			-- at first use array items as they given as {name, value} pairs with stable order
			for i, pair in ipairs(args.user_properties) do
				-- validate types for name and value
				if type(pair) ~= "table" then
					error(fmt("user property at position %d should be {name, value} table", i))
				end
				if type(pair[1]) ~= "string" then
					error(fmt("user property name at position %d should be a string", i))
				end
				if type(pair[2]) ~= "string" then
					error(fmt("user property '%s' value at position %d should be a string", pair[1], i))
				end
				order[i] = pair
				dups[pair[1]] = pair[2]
			end
		end
		-- now add the rest of user properties given as string table keys
		for name, val in sortedpairs(args.user_properties) do
			if type(name) ~= "number" then -- skipping number keys as they already added above
				-- validate types for name and value
				if type(name) ~= "string" then
					error(fmt("user property name '%s' should be a string", name))
				end
				if type(val) ~= "string" then
					error(fmt("user property '%s' value '%s' should be a string", name, val))
				end
				-- check that name+value key already added
				if dups[name] ~= val then
					order[#order + 1] = {name, val}
				end
			end
		end
		for _, pair in ipairs(order) do
			local name = pair[1]
			local value = pair[2]
			-- make user property data
			local prop = combine(
				str_char(make_var_length(uprop_id)),
				make_string(name),
				make_string(value)
			)
			-- and append it to props
			if type(props) == "string" then
				props = combine(prop)
			else
				props:append(prop)
			end
		end
	end
	-- and combine properties with its length field
	return combine(
		str_char(make_var_length(props:len())),		-- DOC: 2.2.2.1 Property Length
		props										-- DOC: 2.2.2.2 Property
	)
end

-- Create CONNECT packet, DOC: 3.1 CONNECT – Connection Request
local function make_packet_connect(args)
	-- check args
	assert(type(args.id) == "string", "expecting .id to be a string with MQTT client id")
	-- DOC: 3.1.2.10 Keep Alive
	local keep_alive_ival = 0
	if args.keep_alive then
		assert(type(args.keep_alive) == "number")
		keep_alive_ival = args.keep_alive
	end
	-- DOC: 3.1.2.11 CONNECT Properties
	local props = make_properties(packet_type.CONNECT, args)
	-- DOC: 3.1.2 CONNECT Variable Header
	local variable_header = combine(
		make_string("MQTT"), 				-- DOC: 3.1.2.1 Protocol Name
		make_uint8(5), 						-- DOC: 3.1.2.2 Protocol Version (5 is for MQTT v5.0)
		make_connect_flags(args), 			-- DOC: 3.1.2.3 Connect Flags
		make_uint16(keep_alive_ival), 		-- DOC: 3.1.2.10 Keep Alive
		props								-- DOC: 3.1.2.11 CONNECT Properties
	)
	-- DOC: 3.1.3 CONNECT Payload
	-- DOC: 3.1.3.1 Client Identifier (ClientID)
	local payload = combine(
		make_string(args.id)
	)
	if args.will then
		-- DOC: 3.1.3.2 Will Properties
		payload:append(make_properties("will", args.will))
		-- DOC: 3.1.3.3 Will Topic
		assert(type(args.will.topic) == "string", "expecting will.topic to be a string")
		payload:append(make_string(args.will.topic))
		-- DOC: 3.1.3.4 Will Payload
		assert(args.will.payload == nil or type(args.will.payload) == "string", "expecting will.payload to be a string or nil")
		payload:append(make_string(args.will.payload))
	end
	if args.username then
		-- DOC: 3.1.3.5 User Name
		payload:append(make_string(args.username))
		if args.password then
			-- DOC: 3.1.3.6 Password
			payload:append(make_string(args.password))
		end
	end
	-- DOC: 3.1.1 Fixed header
	local header = make_header(packet_type.CONNECT, 0, variable_header:len() + payload:len())
	return combine(header, variable_header, payload)
end

-- Create CONNACK packet, DOC: 3.2 CONNACK – Connect acknowledgement
local function make_packet_connack(args)
	-- check args
	assert(type(args.sp) == "boolean", "expecting .sp to be a boolean with Session Present flag")
	assert(type(args.rc) == "number", "expecting .rc to be a number with Connect Reason Code")
	-- DOC: 3.2.2 CONNACK Variable Header
	local props = make_properties(packet_type.CONNACK, args)
	local variable_header = combine(
		make_uint8(args.sp and 1 or 0),		-- DOC: 3.2.2.1.1 Session Present
		make_uint8(args.rc),				-- DOC: 3.2.2.2 Connect Reason Code
		props								-- DOC: 3.2.2.3 CONNACK Properties
	)
	-- DOC: 3.2.3 CONNACK Payload
	-- DOC: The CONNACK packet has no Payload.
	-- DOC: 3.2.1 CONNACK Fixed Header
	local header = make_header(packet_type.CONNACK, 0, variable_header:len())
	return combine(header, variable_header)
end

-- Create PUBLISH packet, DOC: 3.3 PUBLISH – Publish message
local function make_packet_publish(args)
	-- check args
	assert(type(args.topic) == "string", "expecting .topic to be a string")
	if args.payload ~= nil then
		assert(type(args.payload) == "string", "expecting .payload to be a string")
	end
	if args.qos ~= nil then
		assert(type(args.qos) == "number", "expecting .qos to be a number")
		assert(check_qos(args.qos), "expecting .qos to be a valid QoS value")
	end
	if args.retain ~= nil then
		assert(type(args.retain) == "boolean", "expecting .retain to be a boolean")
	end
	if args.dup ~= nil then
		assert(type(args.dup) == "boolean", "expecting .dup to be a boolean")
	end

	-- DOC: 3.3.1 PUBLISH Fixed Header
	local flags = 0
	-- 3.3.1.3 RETAIN
	if args.retain then
		flags = bor(flags, 0x1)
	end
	-- DOC: 3.3.1.2 QoS
	flags = bor(flags, lshift(args.qos or 0, 1))
	-- DOC: 3.3.1.1 DUP
	if args.dup then
		flags = bor(flags, lshift(1, 3))
	end
	-- DOC: 3.3.2 PUBLISH Variable Header
	local variable_header = combine(
		make_string(args.topic)
	)
	-- DOC: 3.3.2.2 Packet Identifier
	if args.qos and args.qos > 0 then
		assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
		assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
		variable_header:append(make_uint16(args.packet_id))
	end
	-- DOC: 3.3.2.3 PUBLISH Properties
	variable_header:append(make_properties(packet_type.PUBLISH, args))
	-- DOC: 3.3.3 PUBLISH Payload
	local payload
	if args.payload then
		payload = args.payload
	else
		payload = ""
	end
	-- DOC: 3.3.1 Fixed header
	local header = make_header(packet_type.PUBLISH, flags, variable_header:len() + payload:len())
	return combine(header, variable_header, payload)
end

-- Create PUBACK packet, DOC: 3.4 PUBACK – Publish acknowledgement
local function make_packet_puback(args)
	-- check args
	assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
	assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
	assert(type(args.rc) == "number", "expecting .rc to be a number")
	-- DOC: 3.4.2 PUBACK Variable Header
	local variable_header = combine(make_uint16(args.packet_id))
	local props = make_properties(packet_type.PUBACK, args)		-- DOC: 3.4.2.2 PUBACK Properties
	-- DOC: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties. In this case the PUBACK has a Remaining Length of 2.
	if props:len() > 1 or args.rc ~= 0 then
		variable_header:append(make_uint8(args.rc))				-- DOC: 3.4.2.1 PUBACK Reason Code
		variable_header:append(props)							-- DOC: 3.4.2.2 PUBACK Properties
	end
	-- DOC: 3.4.1 PUBACK Fixed Header
	local header = make_header(packet_type.PUBACK, 0, variable_header:len())
	return combine(header, variable_header)
end

-- Create PUBREC packet, DOC: 3.5 PUBREC – Publish received (QoS 2 delivery part 1)
local function make_packet_pubrec(args)
	-- check args
	assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
	assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
	assert(type(args.rc) == "number", "expecting .rc to be a number")
	-- DOC: 3.5.2 PUBREC Variable Header
	local variable_header = combine(make_uint16(args.packet_id))
	local props = make_properties(packet_type.PUBREC, args)		-- DOC: 3.5.2.2 PUBREC Properties
	-- DOC: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties. In this case the PUBREC has a Remaining Length of 2.
	if props:len() > 1 or args.rc ~= 0 then
		variable_header:append(make_uint8(args.rc))				-- DOC: 3.5.2.1 PUBREC Reason Code
		variable_header:append(props)							-- DOC: 3.5.2.2 PUBREC Properties
	end
	-- DOC: 3.5.1 PUBREC Fixed Header
	local header = make_header(packet_type.PUBREC, 0, variable_header:len())
	return combine(header, variable_header)
end

-- Create PUBREL packet, DOC: 3.6 PUBREL – Publish release (QoS 2 delivery part 2)
local function make_packet_pubrel(args)
	-- check args
	assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
	assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
	assert(type(args.rc) == "number", "expecting .rc to be a number")
	-- DOC: 3.6.2 PUBREL Variable Header
	local variable_header = combine(make_uint16(args.packet_id))
	local props = make_properties(packet_type.PUBREL, args)		-- DOC: 3.6.2.2 PUBREL Properties
	-- DOC: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties. In this case the PUBREL has a Remaining Length of 2.
	if props:len() > 1 or args.rc ~= 0 then
		variable_header:append(make_uint8(args.rc))				-- DOC: 3.6.2.1 PUBREL Reason Code
		variable_header:append(props)							-- DOC: 3.6.2.2 PUBREL Properties
	end
	-- DOC: 3.6.1 PUBREL Fixed Header
	local header = make_header(packet_type.PUBREL, 2, variable_header:len()) -- flags: fixed 0010 bits, DOC: Figure 3‑14 – PUBREL packet Fixed Header
	return combine(header, variable_header)
end

-- Create PUBCOMP packet, DOC: 3.7 PUBCOMP – Publish complete (QoS 2 delivery part 3)
local function make_packet_pubcomp(args)
	-- check args
	assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
	assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
	assert(type(args.rc) == "number", "expecting .rc to be a number")
	-- DOC: 3.7.2 PUBCOMP Variable Header
	local variable_header = combine(make_uint16(args.packet_id))
	local props = make_properties(packet_type.PUBCOMP, args)	-- DOC: 3.7.2.2 PUBCOMP Properties
	-- DOC: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties. In this case the PUBCOMP has a Remaining Length of 2.
	if props:len() > 1 or args.rc ~= 0 then
		variable_header:append(make_uint8(args.rc))				-- DOC: 3.7.2.1 PUBCOMP Reason Code
		variable_header:append(props)							-- DOC: 3.7.2.2 PUBCOMP Properties
	end
	-- DOC: 3.7.1 PUBCOMP Fixed Header
	local header = make_header(packet_type.PUBCOMP, 0, variable_header:len())
	return combine(header, variable_header)
end

-- Create SUBSCRIBE packet, DOC: 3.8 SUBSCRIBE - Subscribe request
local function make_packet_subscribe(args)
	-- check args
	assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
	assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
	assert(type(args.subscriptions) == "table", "expecting .subscriptions to be a table")
	assert(#args.subscriptions > 0, "expecting .subscriptions to be a non-empty array")
	-- DOC: 3.8.2 SUBSCRIBE Variable Header
	local variable_header = combine(
		make_uint16(args.packet_id),
		make_properties(packet_type.SUBSCRIBE, args)	-- DOC: 3.8.2.1 SUBSCRIBE Properties
	)
	-- DOC: 3.8.3 SUBSCRIBE Payload
	local payload = combine()
	for i, subscription in ipairs(args.subscriptions) do
		assert(type(subscription) == "table", "expecting .subscriptions["..i.."] to be a table")
		assert(type(subscription.topic) == "string", "expecting .subscriptions["..i.."].topic to be a string")
		if subscription.qos ~= nil then
			assert(type(subscription.qos) == "number", "expecting .subscriptions["..i.."].qos to be a number")
			assert(check_qos(subscription.qos), "expecting .subscriptions["..i.."].qos to be a valid QoS value")
		end
		if subscription.retain_as_published ~= nil then
			assert(type(subscription.retain_as_published) == "boolean", "expecting .subscriptions["..i.."].retain_as_published to be a boolean")
		end
		if subscription.retain_handling ~= nil then
			assert(type(subscription.retain_handling) == "number", "expecting .subscriptions["..i.."].retain_handling to be a number")
			assert(check_retain_handling(subscription.retain_handling), "expecting .subscriptions["..i.."].retain_handling to be a valid Retain Handling option")
		end
		-- DOC: 3.8.3.1 Subscription Options
		local so = subscription.qos or 0
		if subscription.no_local then
			so = bor(so, 4) -- set Bit 2
		end
		if subscription.retain_as_published then
			so = bor(so, 8) -- set Bit 3
		end
		if subscription.retain_handling then
			so = bor(so, lshift(subscription.retain_handling, 4)) -- set Bit 4 and 5
		end
		payload:append(make_string(subscription.topic))
		payload:append(make_uint8(so))
	end
	-- DOC: 3.8.1 SUBSCRIBE Fixed Header
	local header = make_header(packet_type.SUBSCRIBE, 2, variable_header:len() + payload:len()) -- flags: fixed 0010 bits, DOC: Figure 3‑18 SUBSCRIBE packet Fixed Header
	return combine(header, variable_header, payload)
end

-- Create SUBACK packet, DOC: 3.9 SUBACK – Subscribe acknowledgement
local function make_packet_suback(args)
	-- check args
	assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
	assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
	assert(type(args.rc) == "table", "expecting .rc to be a table")
	assert(#args.rc > 0, "expecting .rc to be a non-empty array")
	-- DOC: 3.9.2 SUBACK Variable Header
	local variable_header = combine(
		make_uint16(args.packet_id),
		make_properties(packet_type.SUBACK, args)	-- DOC: 3.9.2.1 SUBACK Properties
	)
	-- DOC: 3.9.3 SUBACK Payload
	local payload = combine()
	for i, rc in ipairs(args.rc) do
		assert(type(rc) == "number", "expecting .rc["..i.."] to be a number")
		assert(rc >= 0 and rc <= 255, "expecting .rc["..i.."] to be in range [0, 255]")
		payload:append(make_uint8(rc))
	end
	-- DOC: 3.9.1 SUBACK Fixed Header
	local header = make_header(packet_type.SUBACK, 0, variable_header:len() + payload:len()) -- NOTE: fixed flags value 0x0
	return combine(header, variable_header, payload)
end

-- Create UNSUBSCRIBE packet, DOC: 3.10 UNSUBSCRIBE – Unsubscribe request
local function make_packet_unsubscribe(args)
	-- check args
	assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
	assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
	assert(type(args.subscriptions) == "table", "expecting .subscriptions to be a table")
	assert(#args.subscriptions > 0, "expecting .subscriptions to be a non-empty array")
	-- DOC: 3.10.2 UNSUBSCRIBE Variable Header
	local variable_header = combine(
		make_uint16(args.packet_id),
		make_properties(packet_type.UNSUBSCRIBE, args)	-- DOC: 3.10.2.1 UNSUBSCRIBE Properties
	)
	-- DOC: 3.10.3 UNSUBSCRIBE Payload
	local payload = combine()
	for i, subscription in ipairs(args.subscriptions) do
		assert(type(subscription) == "string", "expecting .subscriptions["..i.."] to be a string")
		payload:append(make_string(subscription))
	end
	-- DOC: 3.10.1 UNSUBSCRIBE Fixed Header
	local header = make_header(packet_type.UNSUBSCRIBE, 2, variable_header:len() + payload:len()) -- flags: fixed 0010 bits, DOC: Figure 3.28 – UNSUBSCRIBE packet Fixed Header
	return combine(header, variable_header, payload)
end

-- Create UNSUBACK packet, DOC: 3.11 UNSUBACK – Unsubscribe acknowledgement
local function make_packet_unsuback(args)
	-- check args
	assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
	assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
	assert(type(args.rc) == "table", "expecting .rc to be a table")
	assert(#args.rc > 0, "expecting .rc to be a non-empty array")
	-- DOC: 3.11.2 UNSUBACK Variable Header
	local variable_header = combine(
		make_uint16(args.packet_id),
		make_properties(packet_type.UNSUBACK, args)	-- DOC: 3.11.2.1 UNSUBACK Properties
	)
	-- DOC: 3.11.3 UNSUBACK Payload
	local payload = combine()
	for i, rc in ipairs(args.rc) do
		assert(type(rc) == "number", "expecting .rc["..i.."] to be a number")
		assert(rc >= 0 and rc <= 255, "expecting .rc["..i.."] to be in range [0, 255]")
		payload:append(make_uint8(rc))
	end
	-- DOC: 3.11.1 UNSUBACK Fixed Header
	local header = make_header(packet_type.UNSUBACK, 0, variable_header:len() + payload:len()) -- NOTE: fixed flags value 0x0
	return combine(header, variable_header, payload)
end

-- Create DISCONNECT packet, DOC: 3.14 DISCONNECT – Disconnect notification
local function make_packet_disconnect(args)
	-- check args
	assert(type(args.rc) == "number", "expecting .rc to be a number")
	-- DOC: 3.14.2 DISCONNECT Variable Header
	local variable_header = combine()
	local props = make_properties(packet_type.DISCONNECT, args)	-- DOC: 3.14.2.2 DISCONNECT Properties
	-- DOC: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Normal disconnecton) and there are no Properties. In this case the DISCONNECT has a Remaining Length of 0.
	if props:len() > 1 or args.rc ~= 0 then
		variable_header:append(make_uint8(args.rc))				-- DOC: 3.14.2.1 Disconnect Reason Code
		variable_header:append(props)							-- DOC: 3.14.2.2 DISCONNECT Properties
	end
	-- DOC: 3.14.1 DISCONNECT Fixed Header
	local header = make_header(packet_type.DISCONNECT, 0, variable_header:len()) -- flags: 0
	return combine(header, variable_header)
end

-- Create AUTH packet, DOC: 3.15 AUTH – Authentication exchange
local function make_packet_auth(args)
	-- check args
	assert(type(args.rc) == "number", "expecting .rc to be a number")
	-- DOC: 3.15.2 AUTH Variable Header
	local variable_header = combine()
	-- DOC: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties. In this case the AUTH has a Remaining Length of 0.
	local props = make_properties(packet_type.AUTH, args)		-- DOC: 3.15.2.2 AUTH Properties
	if props:len() > 1 or args.rc ~= 0 then
		variable_header:append(make_uint8(args.rc))				-- DOC: 3.15.2.1 Authenticate Reason Code
		variable_header:append(props)							-- DOC: 3.15.2.2 AUTH Properties
	end
	-- DOC: 3.15.1 AUTH Fixed Header
	local header = make_header(packet_type.AUTH, 0, variable_header:len())
	return combine(header, variable_header)
end

-- Create packet of given {type: number} in args
function protocol5.make_packet(args)
	assert(type(args) == "table", "expecting args to be a table")
	assert(type(args.type) == "number", "expecting .type number in args")
	local ptype = args.type
	if ptype == packet_type.CONNECT then			-- 1
		return make_packet_connect(args)
	elseif ptype == packet_type.CONNACK then		-- 3
		return make_packet_connack(args)
	elseif ptype == packet_type.PUBLISH then		-- 3
		return make_packet_publish(args)
	elseif ptype == packet_type.PUBACK then			-- 4
		return make_packet_puback(args)
	elseif ptype == packet_type.PUBREC then			-- 5
		return make_packet_pubrec(args)
	elseif ptype == packet_type.PUBREL then			-- 6
		return make_packet_pubrel(args)
	elseif ptype == packet_type.PUBCOMP then		-- 7
		return make_packet_pubcomp(args)
	elseif ptype == packet_type.SUBSCRIBE then		-- 8
		return make_packet_subscribe(args)
	elseif ptype == packet_type.SUBACK then			-- 9
		return make_packet_suback(args)
	elseif ptype == packet_type.UNSUBSCRIBE then	-- 10
		return make_packet_unsubscribe(args)
	elseif ptype == packet_type.UNSUBACK then		-- 11
		return make_packet_unsuback(args)
	elseif ptype == packet_type.PINGREQ then		-- 12
		-- DOC: 3.12 PINGREQ – PING request
		return combine("\192\000") -- 192 == 0xC0, type == 12, flags == 0
	elseif ptype == packet_type.PINGRESP then		-- 12
		-- DOC: 3.13 PINGRESP – PING response
		return combine("\208\000") -- 208 == 0xD0, type == 13, flags == 0
	elseif ptype == packet_type.DISCONNECT then		-- 14
		return make_packet_disconnect(args)
	elseif ptype == packet_type.AUTH then			-- 15
		return make_packet_auth(args)
	else
		error("unexpected packet type to make: "..ptype)
	end
end

-- Parse properties using given read_data function for specified packet type
-- Result will be stored in packet.properties and packet.user_properties
-- Returns false plus string with error message on failure
-- Returns true on success
local function parse_properties(ptype, read_data, input, packet)
	assert(type(read_data) == "function", "expecting read_data to be a function")
	-- DOC: 2.2.2 Properties
	-- parse Property Length
	-- create read_func for parse_var_length and other parse functions, reading from data string instead of network connector
	local len, err = parse_var_length(read_data)
	if not len then
		return false, "failed to parse properties length: "..err
	end
	-- check data contains enough bytes for reading properties
	if input.available < len then
		return false, "not enough data to parse properties of length "..len
	end
	-- ensure properties and user_properties are presented in packet
	if not packet.properties then
		packet.properties = {}
	end
	if not packet.user_properties then
		packet.user_properties = {}
	end
	local uprops = packet.user_properties
	-- parse allowed properties
	local uprop_id = properties.user_property
	local allowed = assert(allowed_properties[ptype], "no allowed properties for specified packet type: "..tostring(ptype))
	local props_end = input[1] + len
	while input[1] < props_end do
		-- property id, DOC: 2.2.2.2 Property
		local prop_id
		prop_id, err = parse_var_length(read_data)
		if not prop_id then
			return false, "failed to parse property length: "..err
		end
		if not allowed[prop_id] then
			if not allowed[prop_id] then
				return false, "property "..tostring(properties[prop_id]).." ("..prop_id..") is not allowed for that packet type"
			end
		end
		if prop_id == uprop_id then
			-- parse name=value string pair
			local name, value
			name, err = parse_string(read_data)
			if not name then
				return false, "failed to parse user property name: "..err
			end
			value, err = parse_string(read_data)
			if not value then
				return false, "failed to parse user property value: "..err
			end
			local old_val = uprops[name]
			if old_val ~= nil then
				-- ensure uprops contains pairs with name = <old value>
				local found = false
				for _, pair in ipairs(uprops) do
					if pair[1] == name and pair[2] == old_val then
						found = true
						break
					end
				end
				if not found then
					uprops[#uprops + 1] = {name, old_val}
				end
				uprops[#uprops + 1] = {name, value}
			end
			uprops[name] = value
		else
			-- parse property value according its identifier
			local value
			value, err = property_parse[prop_id](read_data)
			if err then
				return false, "failed to parse property "..prop_id.." value: "..err
			end
			if allowed[prop_id] ~= true then
				if packet.properties[properties[prop_id]] ~= nil then
					return false, "it is a Protocol Error to include the "..properties[prop_id].." ("..prop_id..") property more than once"
				end
			end
			-- make an array of property values, if it's allowed to send multiple such properties
			if property_multiple[prop_id] then
				local curr = packet.properties[properties[prop_id]] or {}
				curr[#curr + 1] = value
				packet.properties[properties[prop_id]] = curr
			else
				packet.properties[properties[prop_id]] = value
			end
		end
	end
	return true
end

-- Parse CONNACK packet, DOC: 3.2 CONNACK – Connect acknowledgement
local function parse_packet_connack(ptype, flags, input)
	-- DOC: 3.2.1 CONNACK Fixed Header
	if flags ~= 0 then -- Reserved
		return false, packet_type[ptype]..": unexpected flags value: "..flags
	end
	if input.available < 3 then
		return false, packet_type[ptype]..": expecting data of length 3 bytes or more"
	end
	local read_data = input.read_func
	-- DOC: 3.2.2 CONNACK Variable Header
	-- DOC: 3.2.2.1.1 Session Present
	-- DOC: 3.2.2.2 Connect Reason Code
	local byte1, byte2 = parse_uint8(read_data), parse_uint8(read_data)
	local sp = (band(byte1, 0x1) ~= 0)
	local packet = setmetatable({type=ptype, sp=sp, rc=byte2}, connack_packet_mt)
	-- DOC: 3.2.2.3 CONNACK Properties
	local ok, err = parse_properties(ptype, read_data, input, packet)
	if not ok then
		return false, packet_type[ptype]..": failed to parse packet properties: "..err
	end
	return packet
end

-- Parse PUBLISH packet, DOC: 3.3 PUBLISH – Publish message
local function parse_packet_publish(ptype, flags, input)
	-- DOC: 3.3.1 PUBLISH Fixed Header
	-- DOC: 3.3.1.1 DUP
	local dup = (band(flags, 0x8) ~= 0)
	-- DOC: 3.3.1.2 QoS
	local qos = band(rshift(flags, 1), 0x3)
	-- DOC: 3.3.1.3 RETAIN
	local retain = (band(flags, 0x1) ~= 0)
	-- DOC: 3.3.2 PUBLISH Variable Header
	-- DOC: 3.3.2.1 Topic Name
	local read_data = input.read_func
	local topic, err = parse_string(read_data)
	if not topic then
		return false, packet_type[ptype]..": failed to parse topic: "..err
	end
	-- DOC: 3.3.2.2 Packet Identifier
	local packet_id, ok
	if qos > 0 then
		packet_id, err = parse_uint16(read_data)
		if not packet_id then
			return false, packet_type[ptype]..": failed to parse packet_id: "..err
		end
	end
	-- DOC: 3.3.2.3 PUBLISH Properties
	local packet = setmetatable({type=ptype, dup=dup, qos=qos, retain=retain, packet_id=packet_id, topic=topic}, packet_mt)
	ok, err = parse_properties(ptype, read_data, input, packet)
	if not ok then
		return false, packet_type[ptype]..": failed to parse packet properties: "..err
	end
	if input.available > 0 then
		-- DOC: 3.3.3 PUBLISH Payload
		packet.payload = read_data(input.available)
	end
	return packet
end

-- Parse PUBACK packet, DOC: 3.4 PUBACK – Publish acknowledgement
local function parse_packet_puback(ptype, flags, input)
	-- DOC: 3.4.1 PUBACK Fixed Header
	if flags ~= 0 then -- Reserved
		return false, packet_type[ptype]..": unexpected flags value: "..flags
	end
	local read_data = input.read_func
	-- DOC: 3.4.2 PUBACK Variable Header
	local packet_id, err = parse_uint16(read_data)
	if not packet_id then
		return false, packet_type[ptype]..": failed to parse packet_id: "..err
	end
	local packet = setmetatable({type=ptype, packet_id=packet_id, rc=0, properties={}, user_properties={}}, packet_mt)
	if input.available > 0 then
		-- DOC: 3.4.2.1 PUBACK Reason Code
		local rc, ok
		rc, err = parse_uint8(read_data)
		if not rc then
			return false, packet_type[ptype]..": failed to parse rc: "..err
		end
		packet.rc = rc
		-- DOC: 3.4.2.2 PUBACK Properties
		ok, err = parse_properties(ptype, read_data, input, packet)
		if not ok then
			return false, packet_type[ptype]..": failed to parse packet properties: "..err
		end
	end
	return packet
end

-- Parse PUBREC packet, DOC: 3.5 PUBREC – Publish received (QoS 2 delivery part 1)
local function parse_packet_pubrec(ptype, flags, input)
	-- DOC: 3.5.1 PUBREC Fixed Header
	if flags ~= 0 then -- Reserved
		return false, packet_type[ptype]..": unexpected flags value: "..flags
	end
	local read_data = input.read_func
	-- DOC: 3.5.2 PUBREC Variable Header
	local packet_id, err = parse_uint16(read_data)
	if not packet_id then
		return false, packet_type[ptype]..": failed to parse packet_id: "..err
	end
	local packet = setmetatable({type=ptype, packet_id=packet_id, rc=0, properties={}, user_properties={}}, packet_mt)
	if input.available > 0 then
		-- DOC: 3.5.2.1 PUBREC Reason Code
		local rc
		rc, err = parse_uint8(read_data)
		if not rc then
			return false, packet_type[ptype]..": failed to parse rc: "..err
		end
		packet.rc = rc
		-- DOC: 3.5.2.2 PUBREC Properties
		local ok
		ok, err = parse_properties(ptype, read_data, input, packet)
		if not ok then
			return false, packet_type[ptype]..": failed to parse packet properties: "..err
		end
	end
	return packet
end

-- Parse PUBREL packet, DOC: 3.6 PUBREL – Publish release (QoS 2 delivery part 2)
local function parse_packet_pubrel(ptype, flags, input)
	-- DOC: 3.6.1 PUBREL Fixed Header
	if flags ~= 2 then -- Reserved
		return false, packet_type[ptype]..": unexpected flags value: "..flags
	end
	local read_data = input.read_func
	-- DOC: 3.6.2 PUBREL Variable Header
	local packet_id, err = parse_uint16(read_data)
	if not packet_id then
		return false, packet_type[ptype]..": failed to parse packet_id: "..err
	end
	local packet = setmetatable({type=ptype, packet_id=packet_id, rc=0, properties={}, user_properties={}}, packet_mt)
	if input.available > 0 then
		-- DOC: 3.6.2.1 PUBREL Reason Code
		local rc
		rc, err = parse_uint8(read_data)
		if not rc then
			return false, packet_type[ptype]..": failed to parse rc: "..err
		end
		packet.rc = rc
		-- DOC: 3.6.2.2 PUBREL Properties
		local ok
		ok, err = parse_properties(ptype, read_data, input, packet)
		if not ok then
			return false, packet_type[ptype]..": failed to parse packet properties: "..err
		end
	end
	return packet
end

-- Parse PUBCOMP packet, DOC: 3.7 PUBCOMP – Publish complete (QoS 2 delivery part 3)
local function parse_packet_pubcomp(ptype, flags, input)
	-- DOC: 3.7.1 PUBCOMP Fixed Header
	if flags ~= 0 then -- Reserved
		return false, packet_type[ptype]..": unexpected flags value: "..flags
	end
	local read_data = input.read_func
	-- DOC: 3.7.2 PUBCOMP Variable Header
	local packet_id, err = parse_uint16(read_data)
	if not packet_id then
		return false, packet_type[ptype]..": failed to parse packet_id: "..err
	end
	local packet = setmetatable({type=ptype, packet_id=packet_id, rc=0, properties={}, user_properties={}}, packet_mt)
	if input.available > 0 then
		-- DOC: 3.7.2.1 PUBCOMP Reason Code
		local rc
		rc, err = parse_uint8(read_data)
		if not rc then
			return false, packet_type[ptype]..": failed to parse rc: "..err
		end
		packet.rc = rc
		-- DOC: 3.7.2.2 PUBCOMP Properties
		local ok
		ok, err = parse_properties(ptype, read_data, input, packet)
		if not ok then
			return false, packet_type[ptype]..": failed to parse packet properties: "..err
		end
	end
	return packet
end

-- Parse SUBSCRIBE packet, DOC: 3.8 SUBSCRIBE - Subscribe request
local function parse_packet_subscribe(ptype, flags, input)
	-- DOC: 3.8.1 SUBSCRIBE Fixed Header
	if flags ~= 2 then -- Reserved
		return false, packet_type[ptype]..": unexpected flags value: "..flags
	end
	local read_data = input.read_func
	-- DOC: 3.8.2 SUBSCRIBE Variable Header
	local packet_id, err = parse_uint16(read_data)
	if not packet_id then
		return false, packet_type[ptype]..": failed to parse packet_id: "..err
	end
	local packet = setmetatable({type=ptype, packet_id=packet_id, properties={}, user_properties={}}, packet_mt)
	-- DOC: 3.8.2.1 SUBSCRIBE Properties
	local ok
	ok, err = parse_properties(ptype, read_data, input, packet)
	if not ok then
		return false, packet_type[ptype]..": failed to parse packet properties: "..err
	end
	-- DOC: 3.8.3 SUBSCRIBE Payload
	if input.available == 0 then
		-- DOC: A SUBSCRIBE packet with no Payload is a Protocol Error.
		return false, packet_type[ptype]..": empty subscriptions list"
	end
	local subscriptions = {}
	while input.available > 0 do
		local topic_filter
		topic_filter, err = parse_string(input.read_func)
		if not topic_filter then
			return false, packet_type[ptype]..": failed to parse SUBSCRIBE topic filter: "..err
		end
		-- DOC: 3.8.3.1 Subscription Options
		local subscription_options
		subscription_options, err = parse_uint8(input.read_func)
		if not subscription_options then
			return false, packet_type[ptype]..": failed to parse subscription_options: "..err
		end
		subscriptions[#subscriptions + 1] = {
			topic = topic_filter,
			qos = band(subscription_options, 0x3),
			no_local = band(subscription_options, 0x4) ~= 0,
			retain_as_published = band(subscription_options, 0x8) ~= 0, -- Retain As Published
			retain_handling = band(rshift(subscription_options, 4), 0x3), -- Retain Handling
		}
	end
	packet.subscriptions = subscriptions
	return packet
end

-- SUBACK return codes/reason code strings
-- DOC: Table 3‑8 - Subscribe Reason Codes
local suback_rc = {
	[0x00] = "Granted QoS 0",
	[0x01] = "Granted QoS 1",
	[0x02] = "Granted QoS 2",
	[0x80] = "Unspecified error",
	[0x83] = "Implementation specific error",
	[0x87] = "Not authorized",
	[0x8F] = "Topic Filter invalid",
	[0x91] = "Packet Identifier in use",
	[0x97] = "Quota exceeded",
	[0x9E] = "Shared Subscriptions not supported",
	[0xA1] = "Subscription Identifiers not supported",
	[0xA2] = "Wildcard Subscriptions not supported",
}
protocol5.suback_rc = suback_rc

--- Parsed SUBACK packet metatable
local suback_packet_mt = {
	__tostring = protocol.packet_tostring, -- packet-to-human-readable-string conversion metamethod using protocol.packet_tostring()
	reason_strings = function(self) -- Returns reason strings for the SUBACK packet according to its rc field
		local human_readable = {}
		for i, rc in ipairs(self.rc) do
			local reason_string = suback_rc[rc]
			if reason_string then
				human_readable[i] = reason_string
			else
				human_readable[i] = "Unknown: "..tostring(rc)
			end
		end
		return human_readable
	end,
}
suback_packet_mt.__index = suback_packet_mt
protocol5.suback_packet_mt = suback_packet_mt

-- Parse SUBACK packet, DOC: 3.9 SUBACK – Subscribe acknowledgement
local function parse_packet_suback(ptype, flags, input)
	-- DOC: 3.9.1 SUBACK Fixed Header
	if flags ~= 0 then -- Reserved
		return false, packet_type[ptype]..": unexpected flags value: "..flags
	end
	local read_data = input.read_func
	-- DOC: 3.9.2 SUBACK Variable Header
	local packet_id, err = parse_uint16(read_data)
	if not packet_id then
		return false, packet_type[ptype]..": failed to parse packet_id: "..err
	end
	-- DOC: 3.9.2.1 SUBACK Properties
	local packet = setmetatable({type=ptype, packet_id=packet_id}, suback_packet_mt)
	local ok
	ok, err = parse_properties(ptype, read_data, input, packet)
	if not ok then
		return false, packet_type[ptype]..": failed to parse packet properties: "..err
	end
	-- DOC: 3.9.3 SUBACK Payload
	local rcs = {}
	while input.available > 0 do
		local rc
		rc, err = parse_uint8(read_data)
		if not rc then
			return false, packet_type[ptype]..": failed to parse reason code: "..err
		end
		rcs[#rcs + 1] = rc
	end
	if not next(rcs) then
		return false, packet_type[ptype]..": expecting at least one reason code"
	end
	packet.rc = rcs
	return packet
end

-- Parse UNSUBSCRIBE packet, DOC: 3.10 UNSUBSCRIBE – Unsubscribe request
local function parse_packet_unsubscribe(ptype, flags, input)
	-- DOC: 3.10.1 UNSUBSCRIBE Fixed Header
	if flags ~= 2 then -- Reserved
		return false, packet_type[ptype]..": unexpected flags value: "..flags
	end
	local read_data = input.read_func
	-- DOC: 3.10.2 UNSUBSCRIBE Variable Header
	local packet_id, err = parse_uint16(read_data)
	if not packet_id then
		return false, packet_type[ptype]..": failed to parse packet_id: "..err
	end
	local packet = setmetatable({type=ptype, packet_id=packet_id, properties={}, user_properties={}}, packet_mt)
	-- DOC: 3.10.2.1 UNSUBSCRIBE Properties
	local ok
	ok, err = parse_properties(ptype, read_data, input, packet)
	if not ok then
		return false, packet_type[ptype]..": failed to parse packet properties: "..err
	end
	-- 3.10.3 UNSUBSCRIBE Payload
	-- DOC: An UNSUBSCRIBE packet with no Payload is a Protocol Error.
	if input.available == 0 then
		return false, packet_type[ptype]..": empty subscriptions list"
	end
	local subscriptions = {}
	while input.available > 0 do
		local topic_filter
		topic_filter, err = parse_string(input.read_func)
		if not topic_filter then
			return false, packet_type[ptype]..": failed to parse topic filter: "..err
		end
		subscriptions[#subscriptions + 1] = topic_filter
	end
	packet.subscriptions = subscriptions
	return packet
end

-- UNSUBACK Reason Codes
-- DOC[2]: Table 3‑9 - Unsubscribe Reason Codes
local unsuback_rc = {
	[0x00] = "Success",
	[0x11] = "No subscription existed",
	[0x80] = "Unspecified error",
	[0x83] = "Implementation specific error",
	[0x87] = "Not authorized",
	[0x8F] = "Topic Filter invalid",
	[0x91] = "Packet Identifier in use",
}
protocol5.unsuback_rc = unsuback_rc

--- Parsed UNSUBACK packet metatable
local unsuback_packet_mt = {
	__tostring = protocol.packet_tostring, -- packet-to-human-readable-string conversion metamethod using protocol.packet_tostring()
	reason_strings = function(self) -- Returns reason strings for the UNSUBACK packet according to its rc field
		local human_readable = {}
		for i, rc in ipairs(self.rc) do
			local reason_string = unsuback_rc[rc]
			if reason_string then
				human_readable[i] = reason_string
			else
				human_readable[i] = "Unknown: "..tostring(rc)
			end
		end
		return human_readable
	end,
}
unsuback_packet_mt.__index = unsuback_packet_mt
protocol5.unsuback_packet_mt = unsuback_packet_mt

-- Parse UNSUBACK packet, DOC: 3.11 UNSUBACK – Unsubscribe acknowledgement
local function parse_packet_unsuback(ptype, flags, input)
	-- DOC: 3.11.1 UNSUBACK Fixed Header
	if flags ~= 0 then -- Reserved
		return false, packet_type[ptype]..": unexpected flags value: "..flags
	end
	local read_data = input.read_func
	-- DOC: 3.11.2 UNSUBACK Variable Header
	local packet_id, err = parse_uint16(read_data)
	if not packet_id then
		return false, packet_type[ptype]..": failed to parse packet_id: "..err
	end
	-- 3.11.2.1 UNSUBACK Properties
	local packet = setmetatable({type=ptype, packet_id=packet_id}, unsuback_packet_mt)
	local ok
	ok, err = parse_properties(ptype, read_data, input, packet)
	if not ok then
		return false, packet_type[ptype]..": failed to parse packet properties: "..err
	end
	-- 3.11.3 UNSUBACK Payload
	local rcs = {}
	while input.available > 0 do
		local rc
		rc, err = parse_uint8(read_data)
		if not rc then
			return false, packet_type[ptype]..": failed to parse reason code: "..err
		end
		rcs[#rcs + 1] = rc
	end
	if not next(rcs) then
		return false, packet_type[ptype]..": expecting at least one reason code in"
	end
	packet.rc = rcs
	return packet
end

-- Parse PINGREQ packet, DOC: 3.12 PINGREQ – PING request
local function parse_packet_pingreq(ptype, flags, input_)
	-- DOC: 3.12.1 PINGREQ Fixed Header
	if flags ~= 0 then -- Reserved
		return false, packet_type[ptype]..": unexpected flags value: "..flags
	end
	return setmetatable({type=ptype, properties={}, user_properties={}}, packet_mt)
end

-- Parse PINGRESP packet, DOC: 3.13 PINGRESP – PING response
local function parse_packet_pingresp(ptype, flags, input_)
	-- DOC: 3.13.1 PINGRESP Fixed Header
	if flags ~= 0 then -- Reserved
		return false, packet_type[ptype]..": unexpected flags value: "..flags
	end
	return setmetatable({type=ptype, properties={}, user_properties={}}, packet_mt)
end

-- DISCONNECT reason codes
-- DOC: Table 3‑10 – Disconnect Reason Code values
local disconnect_rc = {
	[0x00] = "Normal disconnection",
	[0x04] = "Disconnect with Will Message",
	[0x80] = "Unspecified error",
	[0x81] = "Malformed Packet",
	[0x82] = "Protocol Error",
	[0x83] = "Implementation specific error",
	[0x87] = "Not authorized",
	[0x89] = "Server busy",
	[0x8B] = "Server shutting down",
	[0x8D] = "Keep Alive timeout",
	[0x8E] = "Session taken over",
	[0x8F] = "Topic Filter invalid",
	[0x90] = "Topic Name invalid",
	[0x93] = "Receive Maximum exceeded",
	[0x94] = "Topic Alias invalid",
	[0x95] = "Packet too large",
	[0x96] = "Message rate too high",
	[0x97] = "Quota exceeded",
	[0x98] = "Administrative action",
	[0x99] = "Payload format invalid",
	[0x9A] = "Retain not supported",
	[0x9B] = "QoS not supported",
	[0x9C] = "Use another server",
	[0x9D] = "Server moved",
	[0x9E] = "Shared Subscriptions not supported",
	[0x9F] = "Connection rate exceeded",
	[0xA0] = "Maximum connect time",
	[0xA1] = "Subscription Identifiers not supported",
	[0xA2] = "Wildcard Subscriptions not supported",
}
protocol5.disconnect_rc = disconnect_rc

--- Parsed DISCONNECT packet metatable
local disconnect_packet_mt = {
	__tostring = protocol.packet_tostring, -- packet-to-human-readable-string conversion metamethod using protocol.packet_tostring()
	reason_string = function(self) -- Returns reason string for the DISCONNECT packet according to its rc field
		local reason_string = disconnect_rc[self.rc]
		if not reason_string then
			reason_string = "Unknown: "..self.rc
		end
		return reason_string
	end,
}
disconnect_packet_mt.__index = disconnect_packet_mt
protocol5.disconnect_packet_mt = disconnect_packet_mt

-- Parse DISCONNECT packet, DOC: 3.14 DISCONNECT – Disconnect notification
local function parse_packet_disconnect(ptype, flags, input)
	-- DOC: 3.14.1 DISCONNECT Fixed Header
	if flags ~= 0 then -- Reserved
		return false, packet_type[ptype]..": unexpected flags value: "..flags
	end
	local read_data = input.read_func
	local packet = setmetatable({type=ptype, rc=0, properties={}, user_properties={}}, disconnect_packet_mt)
	if input.available > 0 then
		-- DOC: 3.14.2 DISCONNECT Variable Header
		-- DOC: 3.14.2.1 Disconnect Reason Code
		local rc, err = parse_uint8(read_data)
		if not rc then
			return false, packet_type[ptype]..": failed to parse rc: "..err
		end
		packet.rc = rc
		-- DOC: 3.14.2.2 DISCONNECT Properties
		local ok
		ok, err = parse_properties(ptype, read_data, input, packet)
		if not ok then
			return false, packet_type[ptype]..": failed to parse packet properties: "..err
		end
	end
	return packet
end

-- Parse AUTH packet, DOC: 3.15 AUTH – Authentication exchange
local function parse_packet_auth(ptype, flags, input)
	-- DOC: 3.15.1 AUTH Fixed Header
	if flags ~= 0 then -- Reserved
		return false, packet_type[ptype]..": unexpected flags value: "..flags
	end
	local read_data = input.read_func
	-- DOC: 3.15.2.1 Authenticate Reason Code
	local packet = setmetatable({type=ptype, rc=0, properties={}, user_properties={}}, packet_mt)
	if input.available > 1 then
		-- DOC: 3.15.2 AUTH Variable Header
		local rc, err = parse_uint8(read_data)
		if not rc then
			return false, packet_type[ptype]..": failed to parse Authenticate Reason Code: "..err
		end
		packet.rc = rc
		-- DOC: 3.15.2.2 AUTH Properties
		local ok
		ok, err = parse_properties(ptype, read_data, input, packet)
		if not ok then
			return false, packet_type[ptype]..": failed to parse packet properties: "..err
		end
	end
	return packet
end

-- Parse packet using given read_func
-- Returns packet on success or false and error message on failure
function protocol5.parse_packet(read_func)
	local ptype, flags, input = start_parse_packet(read_func)
	if not ptype then
		return false, flags
	end
	local packet, err

	-- parse read data according type in fixed header
	if ptype == packet_type.CONNECT then			-- 1
		packet, err = parse_packet_connect_input(input, const_v50)
	elseif ptype == packet_type.CONNACK then		-- 2
		packet, err = parse_packet_connack(ptype, flags, input)
	elseif ptype == packet_type.PUBLISH then		-- 3
		packet, err = parse_packet_publish(ptype, flags, input)
	elseif ptype == packet_type.PUBACK then			-- 4
		packet, err = parse_packet_puback(ptype, flags, input)
	elseif ptype == packet_type.PUBREC then			-- 5
		packet, err = parse_packet_pubrec(ptype, flags, input)
	elseif ptype == packet_type.PUBREL then			-- 6
		packet, err = parse_packet_pubrel(ptype, flags, input)
	elseif ptype == packet_type.PUBCOMP then		-- 7
		packet, err = parse_packet_pubcomp(ptype, flags, input)
	elseif ptype == packet_type.SUBSCRIBE then		-- 8
		packet, err = parse_packet_subscribe(ptype, flags, input)
	elseif ptype == packet_type.SUBACK then			-- 9
		packet, err = parse_packet_suback(ptype, flags, input)
	elseif ptype == packet_type.UNSUBSCRIBE then	-- 10
		packet, err = parse_packet_unsubscribe(ptype, flags, input)
	elseif ptype == packet_type.UNSUBACK then		-- 11
		packet, err = parse_packet_unsuback(ptype, flags, input)
	elseif ptype == packet_type.PINGREQ then		-- 12
		packet, err = parse_packet_pingreq(ptype, flags, input)
	elseif ptype == packet_type.PINGRESP then		-- 13
		packet, err = parse_packet_pingresp(ptype, flags, input)
	elseif ptype == packet_type.DISCONNECT then		-- 14
		packet, err = parse_packet_disconnect(ptype, flags, input)
	elseif ptype == packet_type.AUTH then			-- 15
		packet, err = parse_packet_auth(ptype, flags, input)
	else
		return false, "unexpected packet type received: "..tostring(ptype)
	end
	if packet and input.available > 0 then
		return false, packet_type[ptype]..": extra data in remaining length left after packet parsing"
	end
	return packet, err
end

-- Continue parsing of the MQTT v5.0 CONNECT packet
-- Internally called from the protocol.parse_packet_connect_input() function
-- Returns packet on success or false and error message on failure
function protocol5._parse_packet_connect_continue(input, packet)
	local read_func = input.read_func
	local ok, client_id, err

	-- DOC: 3.1.2.11 CONNECT Properties
	ok, err = parse_properties(packet_type.CONNECT, read_func, input, packet)
	if not ok then
		return false, "CONNECT: failed to parse packet properties: "..err
	end

	-- DOC: 3.1.3 CONNECT Payload

	-- DOC: 3.1.3.1 Client Identifier (ClientID)
	client_id, err = parse_string(read_func)
	if not client_id then
		return false, "CONNECT: failed to parse client_id: "..err
	end
	packet.id = client_id

	local will = packet.will
	if will then
		-- DOC: 3.1.3.2 Will Properties
		ok, err = parse_properties("will", read_func, input, will)
		if not ok then
			return false, "CONNECT: failed to parse will message properties: "..err
		end

		-- DOC: 3.1.3.3 Will Topic
		local will_topic, will_payload
		will_topic, err = parse_string(read_func)
		if not will_topic then
			return false, "CONNECT: failed to parse will_topic: "..err
		end
		will.topic = will_topic

		-- DOC: 3.1.3.4 Will Payload
		will_payload, err = parse_string(read_func)
		if not will_payload then
			return false, "CONNECT: failed to parse will_payload: "..err
		end
		will.payload = will_payload
	end

	-- DOC: 3.1.3.5 User Name
	if packet.username then
		local username
		username, err = parse_string(read_func)
		if not username then
			return false, "CONNECT: failed to parse username: "..err
		end
		packet.username = username
	else
		packet.username = nil
	end

	-- DOC: 3.1.3.6 Password
	if packet.password then
		local password
		password, err = parse_string(read_func)
		if not password then
			return false, "CONNECT: failed to parse password: "..err
		end
		packet.password = password
	else
		packet.password = nil
	end

	return setmetatable(packet, packet_mt)
end

-- export module table
return protocol5

-- vim: ts=4 sts=4 sw=4 noet ft=lua
