-- ------------------------------------------------------------------------------ --
--                                TradeSkillMaster                                --
--                          https://tradeskillmaster.com                          --
--    All Rights Reserved - Detailed license information included with addon.     --
-- ------------------------------------------------------------------------------ --

--- Math Functions
-- @module Math

local _, TSM = ...
local Math = TSM.Init("Util.Math")
local TempTable = TSM.Include("Util.TempTable")
local NAN = math.huge * 0
local IS_NAN_GT_INF = NAN > math.huge
local NAN_STR = tostring(NAN)
local private = {
	keysTemp = {},
}



-- ============================================================================
-- Module Functions
-- ============================================================================

--- Returns NAN.
-- @treturn number NAN
function Math.GetNan()
	return NAN
end

--- Checks if a value is NAN.
-- @tparam number value The number to check
-- @treturn boolean Whether or not the value is NAN
function Math.IsNan(value)
	if IS_NAN_GT_INF then
		-- optimization if NAN > math.huge (which it is in Wow's version of lua)
		return value > math.huge and tostring(value) == NAN_STR
	else
		return tostring(value) == NAN_STR
	end
end

--- Rounds a value to a specified significant value.
-- @tparam number value The number to be rounded
-- @tparam number sig The value to round to the nearest multiple of
-- @treturn number The rounded value
function Math.Round(value, sig)
	sig = sig or 1
	return floor((value / sig) + 0.5) * sig
end

--- Rounds a value down to a specified significant value.
-- @tparam number value The number to be rounded
-- @tparam number sig The value to round down to the nearest multiple of
-- @treturn number The rounded value
function Math.Floor(value, sig)
	sig = sig or 1
	return floor(value / sig) * sig
end

--- Rounds a value up to a specified significant value.
-- @tparam number value The number to be rounded
-- @tparam number sig The value to round up to the nearest multiple of
-- @treturn number The rounded value
function Math.Ceil(value, sig)
	sig = sig or 1
	return ceil(value / sig) * sig
end

--- Scales a value from one range to another.
-- @tparam number value The number to be scaled
-- @tparam number fromStart The start value of the range to scale from
-- @tparam number fromEnd The end value of the range to scale from (can be less than fromStart)
-- @tparam number toStart The start value of the range to scale to
-- @tparam number toEnd The end value of the range to scale to (can be less than toStart)
-- @treturn number The scaled value
function Math.Scale(value, fromStart, fromEnd, toStart, toEnd)
	assert(value >= min(fromStart, fromEnd) and value <= max(fromStart, fromEnd))
	return toStart + ((value - fromStart) / (fromEnd - fromStart)) * (toEnd - toStart)
end

--- Bounds a number between a min and max value.
-- @tparam number value The number to be bounded
-- @tparam number minValue The min value
-- @tparam number maxValue The max value
-- @treturn number The bounded value
function Math.Bound(value, minValue, maxValue)
	return min(max(value, minValue), maxValue)
end

--- Calculates the has of the specified data
-- This data can handle data of type string or number. It can also handle a table being passed as the data assuming
-- all keys and values of the table are also hashable (strings, numbers, or tables with the same restriction). This
-- function uses the [djb2 algorithm](http://www.cse.yorku.ca/~oz/hash.html).
-- @param data The data to be hased
-- @tparam[opt] number hash The initial value of the hash
-- @treturn number The hash value
function Math.CalculateHash(data, hash)
	hash = hash or 5381
	local maxValue = 2 ^ 24
	local dataType = type(data)
	if dataType == "string" then
		-- iterate through 8 bytes at a time
		for i = 1, ceil(#data / 8) do
			local b1, b2, b3, b4, b5, b6, b7, b8 = strbyte(data, (i - 1) * 8 + 1, i * 8)
			hash = (hash * 33 + b1) % maxValue
			if not b2 then break end
			hash = (hash * 33 + b2) % maxValue
			if not b3 then break end
			hash = (hash * 33 + b3) % maxValue
			if not b4 then break end
			hash = (hash * 33 + b4) % maxValue
			if not b5 then break end
			hash = (hash * 33 + b5) % maxValue
			if not b6 then break end
			hash = (hash * 33 + b6) % maxValue
			if not b7 then break end
			hash = (hash * 33 + b7) % maxValue
			if not b8 then break end
			hash = (hash * 33 + b8) % maxValue
		end
	elseif dataType == "number" then
		assert(data == floor(data), "Invalid number")
		if data < 0 then
			data = data * -1
			hash = (hash * 33 + 59) % maxValue
		end
		while data > 0 do
			hash = (hash * 33 + data % 256) % maxValue
			data = floor(data / 256)
		end
	elseif dataType == "table" then
		local keys = nil
		if private.keysTemp.inUse then
			keys = TempTable.Acquire()
		else
			keys = private.keysTemp
			private.keysTemp.inUse = true
		end
		for k in pairs(data) do
			tinsert(keys, k)
		end
		sort(keys)
		for _, key in ipairs(keys) do
			hash = Math.CalculateHash(key, hash)
			hash = Math.CalculateHash(data[key], hash)
		end
		if keys == private.keysTemp then
			wipe(private.keysTemp)
		else
			TempTable.Release(keys)
		end
	elseif dataType == "boolean" then
		hash = (hash * 33 + (data and 1 or 0)) % maxValue
	elseif dataType == "nil" then
		hash = (hash * 33 + 17) % maxValue
	else
		error("Invalid data")
	end
	return hash
end
