-- IEEE floating point floats, doubles and quads

local struct = require "vstruct"
local io     = require "vstruct.io"

local sizes = {
	[4] = {1,  8, 23};
	[8] = {1, 11, 52};
    [16] = {1, 15, 112};
}

local function reader(data, size_exp, size_fraction)
	local fraction, exponent, sign
	local endian = io("endianness", "get") == "big" and ">" or "<"
	
	-- Split the unsigned integer into the 3 IEEE fields
	local bits = struct.unpack(endian.." m"..#data, data)[1]
	local fraction = struct.implode({unpack(bits, 1, size_fraction)}, size_fraction)
	local exponent = struct.implode({unpack(bits, size_fraction+1, size_fraction+size_exp)}, size_exp)
	local sign = bits[#bits] and -1 or 1
    
	-- special case: exponent is all 1s
	if exponent == 2^size_exp-1 then
		-- significand is 0? +- infinity
		if fraction == 0 then
			return sign * math.huge
		
		-- otherwise it's NaN
		else
			return 0/0
		end
	end
			
	-- restore the MSB of the significand, unless it's a subnormal number
	if exponent ~= 0 then
		fraction = fraction + (2 ^ size_fraction)
	else
        exponent = 1
    end
	
	-- remove the exponent bias
	exponent = exponent - 2 ^ (size_exp - 1) + 1

	-- Decrease the size of the exponent rather than make the fraction (0.5, 1]
	exponent = exponent - size_fraction
    
	return sign * math.ldexp(fraction, exponent)
end

local function writer(value, size_exp, size_fraction)
	local fraction, exponent, sign
	local width = (size_exp + size_fraction + 1)/8
	local endian = io("endianness", "get") == "big" and ">" or "<"
    local bias = 2^(size_exp-1)-1
	
	if value < 0 
    or 1/value == -math.huge then -- handle the case of -0
		sign = true
		value = -value
	else
		sign = false
	end

	-- special case: value is infinite
	if value == math.huge then
		exponent = bias+1
		fraction = 0
	
	-- special case: value is NaN
	elseif value ~= value then
		exponent = bias+1
		fraction = 2^(size_fraction-1)

    --special case: value is 0
    elseif value == 0 then
        exponent = -bias
        fraction = 0
        
	else
		fraction,exponent = math.frexp(value)
        
        -- subnormal number
        if exponent+bias <= 1 then
            fraction = fraction * 2^(size_fraction+(exponent+bias)-1)
            exponent = -bias

        else
            -- remove the most significant bit from the fraction and adjust exponent
            fraction = fraction - 0.5
            exponent = exponent - 1
            
            -- turn the fraction into an integer
            fraction = fraction * 2^(size_fraction+1)
        end
	end
	
    
    -- add the exponent bias
    exponent = exponent + bias

	local bits = struct.explode(fraction)
	local bits_exp = struct.explode(exponent)
	for i=1,size_exp do
		bits[size_fraction+i] = bits_exp[i]
	end
	bits[size_fraction+size_exp+1] = sign
    
	return struct.pack(endian.."m"..width, {bits})
end

local f = {}

function f.width(n)
    n = tonumber(n)
    assert(n == 4 or n == 8 or n == 16
        , "format 'f' only supports widths 4 (float), 8 (double) and 16 (quad)")
    
    return n
end

function f.unpack(_, buf, width)
    return reader(buf, unpack(sizes[width], 2))
end

function f.pack(_, data, width)
    return writer(data, unpack(sizes[width], 2))
end

return f
