local mod_name = (...):match ( "^(.*)%..-$" )

local assert , error = assert , error
local pairs = pairs
local getmetatable = getmetatable
local type = type
local tonumber , tostring = tonumber , tostring
local t_insert = table.insert
local t_concat = table.concat
local strformat = string.format
local strmatch = string.match
local strbyte = string.byte

local ll = require ( mod_name .. ".ll" )
local le_uint_to_num = ll.le_uint_to_num
local le_int_to_num = ll.le_int_to_num
local num_to_le_uint = ll.num_to_le_uint
local num_to_le_int = ll.num_to_le_int
local from_double = ll.from_double
local to_double = ll.to_double

local getlib = require ( mod_name .. ".get" )
local read_terminated_string = getlib.read_terminated_string

local obid = require ( mod_name .. ".object_id" )
local new_object_id = obid.new
local object_id_mt = obid.metatable
local binary_mt = {}
local utc_date = {}


local function read_document ( get , numerical )
	local bytes = le_uint_to_num ( get ( 4 ) )

	local ho , hk , hv = false , false , false
	local t = { }
	while true do
		local op = get ( 1 )
		if op == "\0" then break end

		local e_name = read_terminated_string ( get )
		local v
		if op == "\1" then -- Double
			v = from_double ( get ( 8 ) )
		elseif op == "\2" then -- String
			local len = le_uint_to_num ( get ( 4 ) )
			v = get ( len - 1 )
			assert ( get ( 1 ) == "\0" )
		elseif op == "\3" then -- Embedded document
			v = read_document ( get , false )
		elseif op == "\4" then -- Array
			v = read_document ( get , true )
		elseif op == "\5" then -- Binary
			local len = le_uint_to_num ( get ( 4 ) )
			local subtype = get ( 1 )
			v = get ( len )
		elseif op == "\7" then -- ObjectId
			v = new_object_id ( get ( 12 ) )
		elseif op == "\8" then -- false
			local f = get ( 1 )
			if f == "\0" then
				v = false
			elseif f == "\1" then
				v = true
			else
				error ( f:byte ( ) )
			end
		elseif op == "\9" then -- UTC datetime milliseconds
			v = le_uint_to_num ( get ( 8 ) , 1 , 8 )
		elseif op == "\10" then -- Null
			v = nil
		elseif op == "\16" then --int32
			v = le_int_to_num ( get ( 4 ) , 1 , 8 )
        elseif op == "\17" then --int64
            v = le_int_to_num(get(8), 1, 8)
        elseif op == "\18" then --int64
            v = le_int_to_num(get(8), 1, 8)
		else
			error ( "Unknown BSON type: " .. strbyte ( op ) )
		end

		if numerical then
			t [ tonumber ( e_name ) ] = v
		else
			t [ e_name ] = v
		end

		-- Check for special universal map
		if e_name == "_keys" then
			hk = v
		elseif e_name == "_vals" then
			hv = v
		else
			ho = true
		end
	end

	if not ho and hk and hv then
		t = { }
			for i=1,#hk do
			t [ hk [ i ] ] = hv [ i ]
		end
	end

	return t
end

local function get_utc_date(v)
    return setmetatable({v = v}, utc_date)
end

local function get_bin_data(v)
    return setmetatable({v = v, st = "\0"}, binary_mt)
end

local function from_bson ( get )
	local t = read_document ( get , false )
	return t
end

local to_bson
local function pack ( k , v )
	local ot = type ( v )
	local mt = getmetatable ( v )

	if ot == "number" then
		return "\1" .. k .. "\0" .. to_double ( v )
	elseif ot == "nil" then
		return "\10" .. k .. "\0"
	elseif ot == "string" then
		return "\2" .. k .. "\0" .. num_to_le_uint ( #v + 1 ) .. v .. "\0"
	elseif ot == "boolean" then
		if v == false then
			return "\8" .. k .. "\0\0"
		else
			return "\8" .. k .. "\0\1"
		end
	elseif mt == object_id_mt then
		return "\7" .. k .. "\0" .. v.id
	elseif mt == utc_date then
		return "\9" .. k .. "\0" .. num_to_le_int(v.v, 8)
	elseif mt == binary_mt then
		return "\5" .. k .. "\0" .. num_to_le_uint(string.len(v.v)) .. 
               v.st .. v.v
	elseif ot == "table" then
		local doc , array = to_bson(v)
		if array then
			return "\4" .. k .. "\0" .. doc
		else
			return "\3" .. k .. "\0" .. doc
		end
	else
		error ( "Failure converting " .. ot ..": " .. tostring ( v ) )
	end
end

function to_bson(ob)
	-- Find out if ob if an array; string->value map; or general table
	local onlyarray = true
	local seen_n , high_n = { } , 0
	local onlystring = true
	for k , v in pairs ( ob ) do
		local t_k = type ( k )
		onlystring = onlystring and ( t_k == "string" )
		if onlyarray then
			if t_k == "number" and k >= 0 then
				if k >= high_n then
					high_n = k
					seen_n [ k ] = v
				end
			else
				onlyarray = false
			end
		end
		if not onlyarray and not onlystring then break end
	end

	local retarray , m = false
	if onlystring then -- Do string first so the case of an empty table is done properly
		local r = { }
        for k , v in pairs ( ob ) do
--ngx.log(ngx.ERR,"="..k..i)
            t_insert ( r , pack ( k , v ) )
        end
		m = t_concat ( r )
	elseif onlyarray then
		local r = { }

		local low = 0
		--if seen_n [ 0 ] then low = 0 end
		for i=low , high_n do
			r [ i ] = pack ( i , seen_n [ i ] )
		end

		m = t_concat ( r , "" , low , high_n )
		retarray = true
	else
		local ni = 1
		local keys , vals = { } , { }
		for k , v in pairs ( ob ) do
			keys [ ni ] = k
			vals [ ni ] = v
			ni = ni + 1
		end
		return to_bson ( { _keys = keys , _vals = vals } )
	end

	return num_to_le_uint ( #m + 4 + 1 ) .. m .. "\0" , retarray
end

return {
	from_bson = from_bson ;
	to_bson = to_bson ;
    get_bin_data = get_bin_data;
    get_utc_date = get_utc_date;
}
