-- FIXME Luau: doesn't see `if element` as nilable table, so we get TypeError: Type 'any?' could not be converted into '{| _owner: {| type: nil |}, _source: Source?, type: any |}'

local function setCurrentlyValidatingElement(element: any?)
	if _G.__DEV__ then
		if element then
			local owner = element._owner
			local stack = describeUnknownElementTypeFrameInDEV(
				element.type,
				element._source,
				if owner ~= nil then owner.type else nil
			);
			-- FIXME Luau: Cannot call non-function ((string?) -> ()) | ((string?) -> ())
			(ReactDebugCurrentFrame.setExtraStackFrame :: any)(stack)
		else
			(ReactDebugCurrentFrame.setExtraStackFrame :: any)(nil)
		end
	end
end

local function checkPropTypes<P>(
	-- deviation START: also checks validateProps if present
	propTypes: Object?,
	validateProps: (P) -> (boolean, string?)?,
	props: P,
	-- deviation END
	location: string,
	componentName: string?,
	element: any?
): ()
	if _G.__DEV__ or _G.__DISABLE_ALL_WARNINGS_EXCEPT_PROP_VALIDATION__ then
		-- deviation: hasOwnProperty shouldn't be relevant to lua objects
		-- $FlowFixMe This is okay but Flow doesn't know it.
		-- local has = Function.call.bind(Object.prototype.hasOwnProperty)

		-- deviation: warns if both propType and validateProps defined.
		if propTypes and validateProps then
			console.warn("You've defined both propTypes and validateProps on " .. (componentName or "a component"))
		end

		-- deviation: also checks validateProps if present
		if validateProps then
			if typeof(validateProps) ~= "function" then
				console.error(
					("validateProps must be a function, but it is a %s.\nCheck the definition of the component %q."):format(
						typeof(validateProps),
						componentName or ""
					)
				)
			else
				local success, failureReason = validateProps(props)

				if not success then
					failureReason = failureReason or "<Validator function did not supply a message>"
					local message = string.format(
						"validateProps failed on a %s type in %s: %s",
						location,
						componentName or "<UNKNOWN Component>",
						tostring(failureReason)
					)
					-- deviation: In legacy Roact, prop validation
					-- failures throw. We replicate that behavior, even though
					-- it differs from propTypes (which only warns)
					-- FIXME: align with upstream behavior during React 18 Lua transition
					error(message)
				end
			end
		end

		if propTypes then
			-- deviation: since we can't constrain the generic, we assert so Luau knows propTypes is a table
			assert(typeof(propTypes) == "table", "propTypes needs to be a table")
			for typeSpecName, _ in propTypes do
				-- deviation: since our loop won't hit metatable members, we don't
				-- need to worry about encountering inherited properties here
				-- if has(propTypes, typeSpecName) then

				-- Prop type validation may throw. In case they do, we don't want to
				-- fail the render phase where it didn't fail before. So we log it.
				-- After these have been cleaned up, we'll local them throw.
				local _, result = xpcall(function()
					-- This is intentionally an invariant that gets caught. It's the same
					-- behavior as without this statement except with a better message.
					if typeof(propTypes[typeSpecName]) ~= "function" then
						local err = Error.new(
							(componentName or "React class")
								.. ": "
								.. location
								.. " type `"
								.. typeSpecName
								.. "` is invalid; "
								.. "it must be a function, usually from the `prop-types` package, but received `"
								.. typeof(propTypes[typeSpecName])
								.. "`."
								.. "This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`."
						)
						err.name = "Invariant Violation"
						error(err)
					end

					return (propTypes[typeSpecName] :: Function)(
						props,
						typeSpecName,
						componentName,
						location,
						nil,
						"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"
					)
				end, describeError)

				-- deviation: FIXME: Can we expose something from JSPolyfill that
				-- will let us verify that this is specifically the Error object
				-- defined there? if we check for result.message ~= nil, ReactNewContext.spec:1368 fails
				local isErrorObject = typeof(result) == "table"
				if result ~= nil and not isErrorObject then
					setCurrentlyValidatingElement(element)
					console.error(string.format(
						-- deviation: s/null/nil
						"%s: type specification of %s"
							.. " `%s` is invalid; the type checker "
							.. "function must return `nil` or an `Error` but returned a %s. "
							.. "You may have forgotten to pass an argument to the type checker "
							.. "creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and "
							.. "shape all require an argument).",
						componentName or "React class",
						location,
						typeSpecName,
						typeof(result)
					))
					setCurrentlyValidatingElement(nil)
				end

				-- FIXME: Luau analyze doesn't understand isErrorObject's effect as a predicate meaning result ~= nil
				if isErrorObject and loggedTypeFailures[(result :: any).message] == nil then
					-- Only monitor this failure once because there tends to be a lot of the
					-- same error.
					loggedTypeFailures[tostring((result :: any).message)] = true
					setCurrentlyValidatingElement(element)
					console.warn(string.format("Failed %s type: %s", location, tostring((result :: any).message)))
					setCurrentlyValidatingElement(nil)
				end
			end
		end
	end
end

return checkPropTypes
