-- ROBLOX deviation: since owner could be a function or a class component, we
-- need to do additional handling to get its name. It's easier to make this a
-- reusable function

local function describeOwner(owner: nil | ReactComponent<any>): string?
	if typeof(owner) == "function" then
		return debug.info(owner :: (any) -> any, "n")
	elseif typeof(owner) == "table" then
		return tostring(owner)
	end
	return nil
end

local function describeBuiltInComponentFrame(
	name: string,
	source: Source | nil,
	-- ROBLOX deviation: owner could be a class component
	owner: nil | ReactComponent<any>
): string
	-- ROBLOX deviation START: for built-in components, we can provide the full
	-- description regardless of `enableStackLocations` since we don't actually
	-- need to do any callstack trickery to get it

	-- if enableComponentStackLocations then
	-- 	if prefix == nil then
	-- 		-- Extract the VM specific prefix used by each line.
	-- 		local _, x = pcall(error, debug.traceback())

	-- 		local match = x.stack.trim().match("\n00:00:00.000 - ")
	-- 		if match then
	-- 			prefix = match[1]
	-- 		else
	-- 			prefix = ''
	-- 		end
	-- 	end
	-- 	-- We use the prefix to ensure our stacks line up with native stack frames.
	-- 	return "\n" .. prefix .. name
	-- else
	-- 	local ownerName = nil
	-- 	if _G.__DEV__ and owner then
	-- 		ownerName = describeOwner(owner)
	-- 	end

	-- 	return describeComponentFrame(name, source, ownerName)
	-- end
	local ownerName = nil
	if _G.__DEV__ and owner then
		ownerName = describeOwner(owner)
	end

	return describeComponentFrame(name, source, ownerName)
	-- ROBLOX deviation END
end

local reentry = false
local componentFrameCache = nil
if _G.__DEV__ then
	componentFrameCache = setmetatable({}, { __mode = "k" })
end

local function describeNativeComponentFrame(
	fn: nil | ReactComponent<any>, -- ROBLOX TODO: only accept tables with __tostring metamethod overridden
	construct: boolean
): string
	-- // If something asked for a stack inside a fake render, it should get ignored.
	if not fn or reentry then
		return ""
	end

	if _G.__DEV__ then
		local frame = componentFrameCache[fn]

		if frame ~= nil then
			return frame
		end
	end

	local control
	reentry = true

	-- deviation: Error.prepareStackTrace is not implemented
	-- local previousPrepareStackTrace = Error.prepareStackTrace
	-- Error.prepareStackTrace = undefined
	local previousDispatcher

	if _G.__DEV__ then
		previousDispatcher = ReactCurrentDispatcher.current
		-- Set the dispatcher in DEV because this might be call in the render
		-- function for warnings.
		ReactCurrentDispatcher.current = nil
		disableLogs()
	end

	-- // This should throw.
	-- deviation: Lua does not have stack traces with errors, so we
	-- use xpcall to convert the error and append a stack trace.
	-- This will change the theorical stack trace we want, because of
	-- the function where we call 'debug.traceback()', but the control
	-- stack will have the same added frame.
	local traceback
	local _, sample = xpcall(function()
		if construct then
			-- deviation: since we can't have a meaningful stack trace when
			-- constructing from a component class (because it does not locate
			-- component definition), we skip this case.
		else
			local _, x = pcall(function()
				traceback = debug.traceback()
				error({
					stack = traceback,
				})
			end)
			control = x;
			-- ROBLOX FIXME: Luau flow analysis bug workaround
			(fn :: (...any) -> ...any)()
		end
	end, function(message)
		return {
			message = message,
			stack = traceback,
		}
	end)

	-- deviation: Lua does not have a structure that works like a try-catch-finally
	-- so we a variable to know if the catch block returns a value. If it returns,
	-- 'earlyOutValue' will be set and we can return its value after running the
	-- instructions in the finally block.
	local earlyOutValue = nil

	if sample and control and typeof(sample.stack) == "string" then
		-- // This extracts the first frame from the sample that isn't also in the control.
		-- // Skipping one frame that we assume is the frame that calls the two.
		local sampleLines = string.split(sample.stack, "\n")
		local controlLines = string.split(control.stack, "\n")
		-- deviation: remove one because our array of lines contains an empty string
		-- at the end
		local sampleIndex = #sampleLines - 1
		local controlIndex = #controlLines - 1

		while
			sampleIndex >= 2
			and controlIndex >= 0
			and sampleLines[sampleIndex] ~= controlLines[controlIndex]
		do
			-- // We expect at least one stack frame to be shared.
			-- // Typically this will be the root most one. However, stack frames may be
			-- // cut off due to maximum stack limits. In this case, one maybe cut off
			-- // earlier than the other. We assume that the sample is longer or the same
			-- // and there for cut off earlier. So we should find the root most frame in
			-- // the sample somewhere in the control.
			controlIndex = controlIndex - 1
		end

		while sampleIndex >= 3 and controlIndex >= 1 do
			sampleIndex = sampleIndex - 1
			controlIndex = controlIndex - 1
			-- // Next we find the first one that isn't the same which should be the
			-- // frame that called our sample function and the control.
			if sampleLines[sampleIndex] ~= controlLines[controlIndex] then
				-- // In V8, the first line is describing the message but other VMs don't.
				-- // If we're about to return the first line, and the control is also on the same
				-- // line, that'sampleIndex a pretty good indicator that our sample threw at same line as
				-- // the control. I.e. before we entered the sample frame. So we ignore this result.
				-- // This can happen if you passed a class to function component, or non-function.
				if sampleIndex ~= 1 or controlIndex ~= 1 then
					repeat
						sampleIndex = sampleIndex - 1
						controlIndex = controlIndex - 1
						-- // We may still have similar intermediate frames from the construct call.
						-- // The next one that isn't the same should be our match though.
						if
							controlIndex < 0
							or sampleLines[sampleIndex] ~= controlLines[controlIndex]
						then
							-- deviation: add the '    in ' prefix to format the component stack
							-- similar to React
							local frame = "\n" .. prefix .. sampleLines[sampleIndex]

							if _G.__DEV__ then
								componentFrameCache[fn] = frame
							end
							-- // Return the line we found.
							-- deviation: to mimic the behavior of the try-catch-finally
							-- we cannot return the value here.
							earlyOutValue = frame
						end
					until not (sampleIndex >= 3 and controlIndex >= 1)
				end

				break
			end
		end
	end

	reentry = false
	if _G.__DEV__ then
		ReactCurrentDispatcher.current = previousDispatcher
		reenableLogs()
	end

	-- deviation: Error.prepareStackTrace is not implemented
	-- Error.prepareStackTrace = previousPrepareStackTrace

	-- deviation: return here to micmic the end of the finally block
	if earlyOutValue ~= nil then
		return earlyOutValue
	end

	-- // Fallback to just using the name if we couldn't make it throw.
	local name = ""

	-- ROBLOX deviation: Can't get displayName for functions
	if typeof(fn) == "function" then
		-- ROBLOX FIXME: type refinement
		name = debug.info(fn :: ((any) -> any), "n")
		-- ROBLOX deviation: since fn can be a class, we can get the class name here
	elseif typeof(fn) == "table" then
		name = tostring(fn)
	end

	local syntheticFrame = ""
	if name ~= nil and name ~= "" then
		syntheticFrame = describeBuiltInComponentFrame(name)
	end

	if _G.__DEV__ then
		componentFrameCache[fn] = syntheticFrame
	end

	return syntheticFrame
end
