-- ROBLOX MOVED: expect/jasmineUtils.lua
-- Equality function lovingly adapted from isEqual in
--   [Underscore](http://underscorejs.org)

local function eq(
	a: any,
	b: any,
	aStack: Array<any>,
	bStack: Array<any>,
	customTesters: Array<Tester>,
	_hasKey: any
): boolean
	local result = true

	local asymmetricResult = asymmetricMatch(a, b)
	if asymmetricResult ~= nil then
		return asymmetricResult
	end

	for _, value in ipairs(customTesters) do
		local customTesterResult = value(a, b)
		if customTesterResult ~= nil then
			return customTesterResult
		end
	end

	-- ROBLOX deviation: code omitted because there are no error objects in Lua

	if Object.is(a, b) then
		return true
	end

	-- ROBLOX deviation: code omitted that does strict comparison because we don't
	-- have similar concerns in Lua where there are no distinct 'null' and
	-- 'undefined' types

	-- ROBLOX deviation: as part of the upstream className check, a type difference of
	-- array/non-array would be detected so we add this in manually
	local className = getType(a)
	if className ~= getType(b) then
		return false
	elseif Array.isArray(a) ~= Array.isArray(b) then
		return false
	end

	-- ROBLOX deviation: code omitted for elseif case with regular expressions since
	-- Lua doesn't have a built in regular expression type

	if className == "boolean" or className == "string" or className == "number" or className == "userdata" then
		return Object.is(a, b)
	elseif className == "DateTime" then
		return a == b
	elseif className == "regexp" then
		return tostring(a) == tostring(b)
	end

	if typeof(a) ~= "table" or typeof(b) ~= "table" then
		return false
	end

	-- DOM code omitted for now but translated here
	--[[
		if isDomNode(a) and isDomNode(b) then
		return a.isEqualNode(b)
		end
	--]]

	-- Used to detect circular references.
	local length = #aStack
	while length > 0 do
		-- Linear search. Performance is inversely proportional to the number of
		-- unique nested structures.
		-- circular references at same depth are equal
		-- circular reference is not equal to non-circular one
		if aStack[length] == a then
			return bStack[length] == b
		elseif bStack[length] == b then
			return false
		end

		length -= 1
	end

	-- Add the first object to the stack of traversed objects.
	table.insert(aStack, a)
	table.insert(bStack, b)

	-- Recursively compare objects and arrays.
	-- Compare array lengths to determine if a deep comparison is necessary.
	if Array.isArray(a) and #a ~= #b then
		return false
	end

	-- Deep compare objects
	local aKeys = keys(a, _hasKey)
	local size = #aKeys

	-- Ensure that both objects contain the same number of properties before comparing deep equality.
	if #keys(b, _hasKey) ~= size then
		return false
	end

	while size > 0 do
		local key = aKeys[size]

		-- Deep compare each member
		result = hasKey(b, key) and eq(a[key], b[key], aStack, bStack, customTesters, _hasKey)

		if not result then
			return false
		end

		size -= 1
	end

	-- Remove the first object from the stack of traversed objects.
	table.remove(aStack)
	table.remove(bStack)

	return result
end
