--!strict
-- ROBLOX upstream: https://github.com/facebook/react/blob/702fad4b1b48ac8f626ed3f35e8f86f5ea728084/packages/react-reconciler/src/ReactStrictModeWarnings.new.js
--[[*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 ]]

local Packages = script.Parent.Parent
local ReactGlobals = require(Packages.ReactGlobals)
-- ROBLOX: use patched console from shared
local console = require(Packages.Shared).console

local ReactInternalTypes = require(script.Parent.ReactInternalTypes)
type Fiber = ReactInternalTypes.Fiber
local ReactCurrentFiber = require(script.Parent.ReactCurrentFiber)
local resetCurrentDebugFiberInDEV = ReactCurrentFiber.resetCurrentFiber
local setCurrentDebugFiberInDEV = ReactCurrentFiber.setCurrentFiber
local getComponentName = require(Packages.Shared).getComponentName
local StrictMode = require(script.Parent.ReactTypeOfMode).StrictMode

type Set<T> = { [T]: boolean }
type Array<T> = { [number]: T }
type Map<K, V> = { [K]: V }
type FiberArray = Array<Fiber>
type FiberToFiberComponentsMap = Map<Fiber, FiberArray>

local ReactStrictModeWarnings = {
	recordUnsafeLifecycleWarnings = function(fiber: Fiber, instance: any) end,
	flushPendingUnsafeLifecycleWarnings = function() end,
	recordLegacyContextWarning = function(fiber: Fiber, instance: any) end,
	flushLegacyContextWarning = function() end,
	discardPendingWarnings = function() end,
}

if ReactGlobals.__DEV__ then
	local findStrictRoot = function(fiber: Fiber): Fiber | nil
		local maybeStrictRoot = nil

		-- ROBLOX FIXME Luau: Luau needs to understand while not nil loops
		local node: Fiber? = fiber
		while node ~= nil do
			if bit32.band(node.mode, StrictMode) ~= 0 then
				maybeStrictRoot = node
			end
			node = node.return_
		end

		return maybeStrictRoot
	end

	local setToSortedString = function(set): string
		local array = {}
		for key, value in set do
			table.insert(array, key)
		end
		table.sort(array)
		return table.concat(array, ", ")
	end

	local pendingComponentWillMountWarnings: Array<Fiber> = {}
	local pendingUNSAFE_ComponentWillMountWarnings: Array<Fiber> = {}
	local pendingComponentWillReceivePropsWarnings: Array<Fiber> = {}
	local pendingUNSAFE_ComponentWillReceivePropsWarnings: Array<Fiber> = {}
	local pendingComponentWillUpdateWarnings: Array<Fiber> = {}
	local pendingUNSAFE_ComponentWillUpdateWarnings: Array<Fiber> = {}

	-- Tracks components we have already warned about.
	local didWarnAboutUnsafeLifecycles = {}

	ReactStrictModeWarnings.recordUnsafeLifecycleWarnings = function(
		fiber: Fiber,
		instance: any
	)
		-- Dedupe strategy: Warn once per component.
		if didWarnAboutUnsafeLifecycles[fiber.type] then
			return
		end

		if
			typeof(instance.componentWillMount) == "function"
			-- Don't warn about react-lifecycles-compat polyfilled components.
			-- ROBLOX deviation: Lua doesn't allow fields on function
			-- instance.componentWillMount.__suppressDeprecationWarning ~= true
		then
			table.insert(pendingComponentWillMountWarnings, fiber)
		end

		if
			bit32.band(fiber.mode, StrictMode) ~= 0
			and typeof(instance.UNSAFE_componentWillMount) == "function"
		then
			table.insert(pendingUNSAFE_ComponentWillMountWarnings, fiber)
		end

		if
			typeof(instance.componentWillReceiveProps) == "function"
			-- ROBLOX deviation: Lua doesn't allow fields on function
			-- instance.componentWillReceiveProps.__suppressDeprecationWarning ~= true
		then
			table.insert(pendingComponentWillReceivePropsWarnings, fiber)
		end

		if
			bit32.band(fiber.mode, StrictMode) ~= 0
			and typeof(instance.UNSAFE_componentWillReceiveProps) == "function"
		then
			table.insert(pendingUNSAFE_ComponentWillReceivePropsWarnings, fiber)
		end

		if
			typeof(instance.componentWillUpdate) == "function"
			-- ROBLOX deviation: Lua doesn't allow fields on function
			-- instance.componentWillUpdate.__suppressDeprecationWarning ~= true
		then
			table.insert(pendingComponentWillUpdateWarnings, fiber)
		end

		if
			bit32.band(fiber.mode, StrictMode) ~= 0
			and typeof(instance.UNSAFE_componentWillUpdate) == "function"
		then
			table.insert(pendingUNSAFE_ComponentWillUpdateWarnings, fiber)
		end
	end

	ReactStrictModeWarnings.flushPendingUnsafeLifecycleWarnings = function()
		-- We do an initial pass to gather component names
		local componentWillMountUniqueNames = {}
		if #pendingComponentWillMountWarnings > 0 then
			for i, fiber in pendingComponentWillMountWarnings do
				componentWillMountUniqueNames[getComponentName(fiber.type) or "Component"] =
					true
				didWarnAboutUnsafeLifecycles[fiber.type] = true
			end
			table.clear(pendingComponentWillMountWarnings)
		end

		local UNSAFE_componentWillMountUniqueNames = {}
		if #pendingUNSAFE_ComponentWillMountWarnings > 0 then
			for i, fiber in pendingUNSAFE_ComponentWillMountWarnings do
				UNSAFE_componentWillMountUniqueNames[getComponentName(fiber.type) or "Component"] =
					true
				didWarnAboutUnsafeLifecycles[fiber.type] = true
			end
			table.clear(pendingUNSAFE_ComponentWillMountWarnings)
		end

		local componentWillReceivePropsUniqueNames = {}
		if #pendingComponentWillReceivePropsWarnings > 0 then
			for i, fiber in pendingComponentWillReceivePropsWarnings do
				componentWillReceivePropsUniqueNames[getComponentName(fiber.type) or "Component"] =
					true
				didWarnAboutUnsafeLifecycles[fiber.type] = true
			end

			table.clear(pendingComponentWillReceivePropsWarnings)
		end

		local UNSAFE_componentWillReceivePropsUniqueNames = {}
		if #pendingUNSAFE_ComponentWillReceivePropsWarnings > 0 then
			for i, fiber in pendingUNSAFE_ComponentWillReceivePropsWarnings do
				UNSAFE_componentWillReceivePropsUniqueNames[getComponentName(fiber.type) or "Component"] =
					true
				didWarnAboutUnsafeLifecycles[fiber.type] = true
			end

			table.clear(pendingUNSAFE_ComponentWillReceivePropsWarnings)
		end

		local componentWillUpdateUniqueNames = {}
		if #pendingComponentWillUpdateWarnings > 0 then
			for i, fiber in pendingComponentWillUpdateWarnings do
				componentWillUpdateUniqueNames[getComponentName(fiber.type) or "Component"] =
					true
				didWarnAboutUnsafeLifecycles[fiber.type] = true
			end

			table.clear(pendingComponentWillUpdateWarnings)
		end

		local UNSAFE_componentWillUpdateUniqueNames = {}
		if #pendingUNSAFE_ComponentWillUpdateWarnings > 0 then
			for i, fiber in pendingUNSAFE_ComponentWillUpdateWarnings do
				UNSAFE_componentWillUpdateUniqueNames[getComponentName(fiber.type) or "Component"] =
					true
				didWarnAboutUnsafeLifecycles[fiber.type] = true
			end

			table.clear(pendingUNSAFE_ComponentWillUpdateWarnings)
		end

		-- Finally, we flush all the warnings
		-- UNSAFE_ ones before the deprecated ones, since they'll be 'louder'
		-- deviation: use `next` to determine whether set is empty
		if next(UNSAFE_componentWillMountUniqueNames) ~= nil then
			local sortedNames = setToSortedString(UNSAFE_componentWillMountUniqueNames)
			console.error(
				"Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. "
					.. "See https://reactjs.org/link/unsafe-component-lifecycles for details.\n\n"
					.. "* Move code with side effects to componentDidMount, and set initial state in the constructor.\n"
					.. "\nPlease update the following components: %s",
				sortedNames
			)
		end

		-- deviation: use `next` to determine whether set is empty
		if next(UNSAFE_componentWillReceivePropsUniqueNames) ~= nil then
			local sortedNames =
				setToSortedString(UNSAFE_componentWillReceivePropsUniqueNames)
			console.error(
				"Using UNSAFE_componentWillReceiveProps in strict mode is not recommended "
					.. "and may indicate bugs in your code. "
					.. "See https://reactjs.org/link/unsafe-component-lifecycles for details.\n\n"
					.. "* Move data fetching code or side effects to componentDidUpdate.\n"
					.. "* If you're updating state whenever props change, "
					.. "refactor your code to use memoization techniques or move it to "
					.. "static getDerivedStateFromProps. Learn more at: https://reactjs.org/link/derived-state\n"
					.. "\nPlease update the following components: %s",
				sortedNames
			)
		end

		-- deviation: use `next` to determine whether set is empty
		if next(UNSAFE_componentWillUpdateUniqueNames) ~= nil then
			local sortedNames = setToSortedString(UNSAFE_componentWillUpdateUniqueNames)
			console.error(
				"Using UNSAFE_componentWillUpdate in strict mode is not recommended "
					.. "and may indicate bugs in your code. "
					.. "See https://reactjs.org/link/unsafe-component-lifecycles for details.\n\n"
					.. "* Move data fetching code or side effects to componentDidUpdate.\n"
					.. "\nPlease update the following components: %s",
				sortedNames
			)
		end

		-- deviation: use `next` to determine whether set is empty
		if next(componentWillMountUniqueNames) ~= nil then
			local sortedNames = setToSortedString(componentWillMountUniqueNames)

			-- ROBLOX TODO: Make decisions about whether or not we'll support these
			-- methods in the first place
			-- deviation: Remove some non-applicable information
			console.warn(
				"componentWillMount has been renamed, and is not recommended for use. "
					.. "See https://reactjs.org/link/unsafe-component-lifecycles for details.\n\n"
					.. "* Move code with side effects to componentDidMount, and set initial state in the constructor.\n"
					.. "* Rename componentWillMount to UNSAFE_componentWillMount to suppress "
					.. "this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work.\n"
					.. "\nPlease update the following components: %s",
				sortedNames
			)
		end

		-- deviation: use `next` to determine whether set is empty
		if next(componentWillReceivePropsUniqueNames) ~= nil then
			local sortedNames = setToSortedString(componentWillReceivePropsUniqueNames)

			-- ROBLOX TODO: Make decisions about whether or not we'll support these
			-- methods in the first place
			-- deviation: Remove some non-applicable information
			console.warn(
				"componentWillReceiveProps has been renamed, and is not recommended for use. "
					.. "See https://reactjs.org/link/unsafe-component-lifecycles for details.\n\n"
					.. "* Move data fetching code or side effects to componentDidUpdate.\n"
					.. "* If you're updating state whenever props change, refactor your "
					.. "code to use memoization techniques or move it to "
					.. "static getDerivedStateFromProps. Learn more at: https://reactjs.org/link/derived-state\n"
					.. "* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress "
					.. "this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work.\n"
					.. "\nPlease update the following components: %s",
				sortedNames
			)
		end

		-- deviation: use `next` to determine whether set is empty
		if next(componentWillUpdateUniqueNames) ~= nil then
			local sortedNames = setToSortedString(componentWillUpdateUniqueNames)

			-- ROBLOX TODO: Make decisions about whether or not we'll support these
			-- methods in the first place
			-- deviation: Remove some non-applicable information
			console.warn(
				"componentWillUpdate has been renamed, and is not recommended for use. "
					.. "See https://reactjs.org/link/unsafe-component-lifecycles for details.\n\n"
					.. "* Move data fetching code or side effects to componentDidUpdate.\n"
					.. "* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress "
					.. "this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work.\n"
					.. "\nPlease update the following components: %s",
				sortedNames
			)
		end
	end

	local pendingLegacyContextWarning: FiberToFiberComponentsMap = {}

	-- Tracks components we have already warned about.
	local didWarnAboutLegacyContext = {}

	ReactStrictModeWarnings.recordLegacyContextWarning = function(
		fiber: Fiber,
		instance: any
	)
		local strictRoot = findStrictRoot(fiber)
		if strictRoot == nil then
			console.error(
				"Expected to find a StrictMode component in a strict mode tree. "
					.. "This error is likely caused by a bug in React. Please file an issue."
			)
			return
		end

		-- Dedup strategy: Warn once per component.
		if didWarnAboutLegacyContext[fiber.type] then
			return
		end

		-- ROBLOX FIXME Luau: Luau should narrow based on the nil guard
		local warningsForRoot = pendingLegacyContextWarning[strictRoot :: Fiber]

		-- ROBLOX deviation: Lua can't have fields on functions
		if
			typeof(fiber.type) ~= "function"
			and (
				fiber.type.contextTypes ~= nil
				or fiber.type.childContextTypes ~= nil
				or (instance ~= nil and typeof(instance.getChildContext) == "function")
			)
		then
			if warningsForRoot == nil then
				warningsForRoot = {}
				-- ROBLOX FIXME Luau: Luau should narrow based on the nil guard
				pendingLegacyContextWarning[strictRoot :: Fiber] = warningsForRoot
			end
			table.insert(warningsForRoot, fiber)
		end
	end

	ReactStrictModeWarnings.flushLegacyContextWarning = function()
		for strictRoot, fiberArray in pendingLegacyContextWarning do
			if #fiberArray == 0 then
				return
			end
			local firstFiber = fiberArray[1]

			local uniqueNames = {}
			for i, fiber in fiberArray do
				uniqueNames[getComponentName(fiber.type) or "Component"] = true
				didWarnAboutLegacyContext[fiber.type] = true
			end

			local sortedNames = setToSortedString(uniqueNames)

			local ok, error_ = pcall(function()
				setCurrentDebugFiberInDEV(firstFiber)
				console.error(
					"Legacy context API has been detected within a strict-mode tree."
						.. "\n\nThe old API will be supported in all 16.x releases, but applications "
						.. "using it should migrate to the new version."
						.. "\n\nPlease update the following components: %s"
						.. "\n\nLearn more about this warning here: https://reactjs.org/link/legacy-context",
					sortedNames
				)
			end)

			-- finally
			resetCurrentDebugFiberInDEV()

			if not ok then
				error(error_)
			end
		end
	end

	ReactStrictModeWarnings.discardPendingWarnings = function()
		-- ROBLOX performance? use table.clear instead of assigning new array
		table.clear(pendingComponentWillMountWarnings)
		table.clear(pendingUNSAFE_ComponentWillMountWarnings)
		table.clear(pendingComponentWillReceivePropsWarnings)
		table.clear(pendingUNSAFE_ComponentWillReceivePropsWarnings)
		table.clear(pendingComponentWillUpdateWarnings)
		table.clear(pendingUNSAFE_ComponentWillUpdateWarnings)
		table.clear(pendingLegacyContextWarning)
	end
end

return ReactStrictModeWarnings
