--!strict
-- ROBLOX upstream: https://github.com/facebook/react/blob/d17086c7c813402a550d15a2f56dc43f1dbd1735/packages/react-reconciler/src/SchedulerWithReactIntegration.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)
local LuauPolyfill = require(Packages.LuauPolyfill)
local Array = LuauPolyfill.Array

local ReactInternalTypes = require(script.Parent.ReactInternalTypes)
export type ReactPriorityLevel = ReactInternalTypes.ReactPriorityLevel

-- Intentionally not named imports because Rollup would use dynamic dispatch for
-- CommonJS interop named imports.
local Scheduler = require(Packages.Scheduler)
-- local __interactionsRef = require(Packages.Scheduler.tracing).__interactionsRef
local ReactFeatureFlags = require(Packages.Shared).ReactFeatureFlags
-- local enableSchedulerTracing = ReactFeatureFlags.enableSchedulerTracing
local decoupleUpdatePriorityFromScheduler =
	ReactFeatureFlags.decoupleUpdatePriorityFromScheduler
local invariant = require(Packages.Shared).invariant
local describeError = require(Packages.Shared).describeError
local ReactFiberLane = require(script.Parent.ReactFiberLane)
local SyncLanePriority = ReactFiberLane.SyncLanePriority
local getCurrentUpdateLanePriority = ReactFiberLane.getCurrentUpdateLanePriority
local setCurrentUpdateLanePriority = ReactFiberLane.setCurrentUpdateLanePriority

local Scheduler_runWithPriority = Scheduler.unstable_runWithPriority
local Scheduler_scheduleCallback = Scheduler.unstable_scheduleCallback
local Scheduler_cancelCallback = Scheduler.unstable_cancelCallback
local Scheduler_shouldYield = Scheduler.unstable_shouldYield
local Scheduler_requestPaint = Scheduler.unstable_requestPaint
local Scheduler_now = Scheduler.unstable_now
local Scheduler_getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel
local Scheduler_ImmediatePriority = Scheduler.unstable_ImmediatePriority
local Scheduler_UserBlockingPriority = Scheduler.unstable_UserBlockingPriority
local Scheduler_NormalPriority = Scheduler.unstable_NormalPriority
local Scheduler_LowPriority = Scheduler.unstable_LowPriority
local Scheduler_IdlePriority = Scheduler.unstable_IdlePriority

-- deviation: Instead of defining these here, we require them from a small file
-- with _just_ these constant definitions; it helps us avoid a circular require
-- issue with `ReactFiberLanes`
local ReactFiberSchedulerPriorities =
	require(script.Parent["ReactFiberSchedulerPriorities.roblox"])
local ImmediatePriority = ReactFiberSchedulerPriorities.ImmediatePriority
local UserBlockingPriority = ReactFiberSchedulerPriorities.UserBlockingPriority
local NormalPriority = ReactFiberSchedulerPriorities.NormalPriority
local LowPriority = ReactFiberSchedulerPriorities.LowPriority
local IdlePriority = ReactFiberSchedulerPriorities.IdlePriority
local NoPriority = ReactFiberSchedulerPriorities.NoPriority

-- deviation: common types
type Array<T> = { [number]: T }

-- deviation: pre-declare functions
local flushSyncCallbackQueueImpl

-- if enableSchedulerTracing)
--   -- Provide explicit error message when production+profiling bundle of e.g.
--   -- react-dom is used with production (non-profiling) bundle of
--   -- scheduler/tracing
--   invariant(
--     __interactionsRef ~= nil and __interactionsRef.current ~= nil,
--     'It is not supported to run the profiling version of a renderer (for ' +
--       'example, `react-dom/profiling`) without also replacing the ' +
--       '`scheduler/tracing` module with `scheduler/tracing-profiling`. Your ' +
--       'bundler might have a setting for aliasing both modules. Learn more at ' +
--       'https://reactjs.org/link/profiling',
--   )
-- end

export type SchedulerCallback = (isSync: boolean) -> SchedulerCallback | nil

-- ROBLOX deviation START: don't allow extension unless we need to
-- type SchedulerCallbackOptions = { timeout: number?, ... };
type SchedulerCallbackOptions = { timeout: number? }
-- ROBLOX deviation END
local fakeCallbackNode = {}

local shouldYield = Scheduler_shouldYield
local requestPaint
-- Fall back gracefully if we're running an older version of Scheduler.
if Scheduler_requestPaint ~= nil then
	requestPaint = Scheduler_requestPaint
else
	requestPaint = function() end
end

local syncQueue: Array<SchedulerCallback>? = nil
local immediateQueueCallbackNode: any? = nil
local isFlushingSyncQueue: boolean = false
local initialTimeMs: number = Scheduler_now()

-- -- If the initial timestamp is reasonably small, use Scheduler's `now` directly.
-- -- This will be the case for modern browsers that support `performance.now`. In
-- -- older browsers, Scheduler falls back to `Date.now`, which returns a Unix
-- -- timestamp. In that case, subtract the module initialization time to simulate
-- -- the behavior of performance.now and keep our times small enough to fit
-- -- within 32 bits.
-- -- TODO: Consider lifting this into Scheduler.
-- ROBLOX FIXME: properly account for ms vs s from tick
-- local now = initialTimeMs < 10000
--   and Scheduler_now
--   or function()
--     return Scheduler_now() - initialTimeMs
--   end

-- deviation: Roblox uses `tick` under the hood, which is more like the unix
-- timestamp behavior referenced above
local function now(): number
	return Scheduler_now() - initialTimeMs
end

local function getCurrentPriorityLevel(): ReactPriorityLevel
	local currentPriorityLevel = Scheduler_getCurrentPriorityLevel()
	if currentPriorityLevel == Scheduler_ImmediatePriority then
		return ImmediatePriority
	elseif currentPriorityLevel == Scheduler_UserBlockingPriority then
		return UserBlockingPriority
	elseif currentPriorityLevel == Scheduler_NormalPriority then
		return NormalPriority
	elseif currentPriorityLevel == Scheduler_LowPriority then
		return LowPriority
	elseif currentPriorityLevel == Scheduler_IdlePriority then
		return IdlePriority
	else
		invariant(false, "Unknown priority level.")
		-- deviation: FIXME: Luau needs explicit return here
		return NoPriority
	end
end

function reactPriorityToSchedulerPriority(reactPriorityLevel)
	if reactPriorityLevel == ImmediatePriority then
		return Scheduler_ImmediatePriority
	elseif reactPriorityLevel == UserBlockingPriority then
		return Scheduler_UserBlockingPriority
	elseif reactPriorityLevel == NormalPriority then
		return Scheduler_NormalPriority
	elseif reactPriorityLevel == LowPriority then
		return Scheduler_LowPriority
	elseif reactPriorityLevel == IdlePriority then
		return Scheduler_IdlePriority
	else
		invariant(false, "Unknown priority level.")
		-- deviation: return appeases Luau
		return nil
	end
end

-- ROBLOX FIXME Luau: should be T... but hits CLI-50289: failure to unify
local function runWithPriority<T...>(
	reactPriorityLevel: ReactPriorityLevel,
	fn: () -> T...
): ...any
	local priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel)
	return Scheduler_runWithPriority(priorityLevel, fn)
end

local function scheduleCallback(
	reactPriorityLevel: ReactPriorityLevel,
	callback: SchedulerCallback,
	options: SchedulerCallbackOptions | nil
)
	local priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel)
	return Scheduler_scheduleCallback(priorityLevel, callback, options)
end

local function scheduleSyncCallback(callback: SchedulerCallback)
	-- Push this callback into an internal queue. We'll flush these either in
	-- the next tick, or earlier if something calls `flushSyncCallbackQueue`.
	if syncQueue == nil then
		syncQueue = { callback }
		-- Flush the queue in the next tick, at the earliest.
		immediateQueueCallbackNode = Scheduler_scheduleCallback(
			Scheduler_ImmediatePriority,
			flushSyncCallbackQueueImpl
		)
	else
		-- Push onto existing queue. Don't need to schedule a callback because
		-- we already scheduled one when we created the queue.
		local coercedSyncQueue = syncQueue
		table.insert(coercedSyncQueue, callback)
	end
	return fakeCallbackNode
end

local function cancelCallback(callbackNode: any)
	if callbackNode ~= fakeCallbackNode then
		Scheduler_cancelCallback(callbackNode)
	end
end

local function flushSyncCallbackQueue(): boolean
	if immediateQueueCallbackNode ~= nil then
		local node = immediateQueueCallbackNode
		immediateQueueCallbackNode = nil
		Scheduler_cancelCallback(node)
	end
	return flushSyncCallbackQueueImpl()
end

flushSyncCallbackQueueImpl = function()
	if not isFlushingSyncQueue and syncQueue ~= nil then
		-- Prevent re-entrancy.
		isFlushingSyncQueue = true
		local i = 1
		if decoupleUpdatePriorityFromScheduler then
			local previousLanePriority = getCurrentUpdateLanePriority()
			-- ROBLOX deviation: YOLO flag for disabling pcall
			local ok = true
			local result
			if not ReactGlobals.__YOLO__ then
				-- ROBLOX performance: hoist non-throwables out of try{} to eliminate anon function
				local isSync = true
				local queue = syncQueue

				setCurrentUpdateLanePriority(SyncLanePriority)
				ok, result = xpcall(
					runWithPriority,
					describeError,
					ImmediatePriority,
					-- ROBLOX FIXME Luau: Luau sees this as returning void, but then sees an explicit return in runWithPriority and errors
					function(): ...any
						for index, callback in queue do
							i = index
							repeat
								-- ROBLOX FIXME Luau: Luau doesn't understand loop until nil construct
								callback = callback(isSync) :: any
							until callback == nil
						end
					end
				)
				syncQueue = nil
			else
				ok = true
				local isSync = true
				local queue = syncQueue

				setCurrentUpdateLanePriority(SyncLanePriority)
				-- ROBLOX FIXME Luau: Luau sees this as returning void, but then sees an explicit return in runWithPriority and errors
				runWithPriority(ImmediatePriority, function(): ...any
					for index, callback in queue do
						i = index
						repeat
							-- ROBLOX FIXME Luau: Luau doesn't understand loop until nil construct
							callback = callback(isSync) :: any
						until callback == nil
						i += 1
					end
				end)
				syncQueue = nil
			end

			-- finally
			setCurrentUpdateLanePriority(previousLanePriority)
			isFlushingSyncQueue = false

			if not ok then
				-- If something throws, leave the remaining callbacks on the queue.
				if syncQueue ~= nil then
					syncQueue = Array.slice(syncQueue, i + 1)
				end
				-- Resume flushing in the next tick
				Scheduler_scheduleCallback(
					Scheduler_ImmediatePriority,
					flushSyncCallbackQueue
				)
				error(result)
			end
		else
			-- ROBLOX deviation: YOLO flag for disabling pcall
			local ok, result
			if not ReactGlobals.__YOLO__ then
				-- ROBLOX performance: hoist non-throwables out of try{} to eliminate anon function
				local isSync = true
				local queue = syncQueue

				ok, result = xpcall(
					runWithPriority,
					describeError,
					ImmediatePriority,
					-- ROBLOX FIXME Luau: Luau sees this as returning void, but then sees an explicit return in runWithPriority and errors
					function(): ...any
						for index, callback in queue do
							i = index
							repeat
								-- ROBLOX FIXME Luau: Luau doesn't understand loop until nil construct
								callback = callback(isSync) :: any
							until callback == nil
						end
					end
				)
				syncQueue = nil
			else
				ok = true
				local isSync = true
				local queue = syncQueue
				-- ROBLOX FIXME Luau: Luau sees this as returning void, but then sees an explicit return in runWithPriority and errors
				runWithPriority(ImmediatePriority, function(): ...any
					for index, callback in queue do
						i = index
						repeat
							-- ROBLOX FIXME Luau: Luau doesn't understand loop until nil construct
							callback = callback(isSync) :: any
						until callback == nil
					end
				end)
				syncQueue = nil
			end

			-- finally
			isFlushingSyncQueue = false

			if not ok then
				-- If something throws, leave the remaining callbacks on the queue.
				if syncQueue ~= nil then
					syncQueue = Array.slice(syncQueue, i + 1)
				end
				-- Resume flushing in the next tick
				Scheduler_scheduleCallback(
					Scheduler_ImmediatePriority,
					flushSyncCallbackQueue
				)
				error(result)
			end
		end
		return true
	else
		return false
	end
end

return {
	ImmediatePriority = ImmediatePriority,
	UserBlockingPriority = UserBlockingPriority,
	NormalPriority = NormalPriority,
	LowPriority = LowPriority,
	IdlePriority = IdlePriority,
	NoPriority = NoPriority,

	getCurrentPriorityLevel = getCurrentPriorityLevel,
	flushSyncCallbackQueue = flushSyncCallbackQueue,
	runWithPriority = runWithPriority,
	scheduleCallback = scheduleCallback,
	scheduleSyncCallback = scheduleSyncCallback,
	cancelCallback = cancelCallback,
	now = now,
	requestPaint = requestPaint,
	shouldYield = shouldYield,
}
