--!nonstrict
-- ROBLOX upstream: https://github.com/facebook/react/blob/v17.0.2/packages/react/src/ReactLazy.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 Shared = require(Packages.Shared)
local console = Shared.console
local LuauPolyfill = require(Packages.LuauPolyfill)
local inspect = LuauPolyfill.util.inspect

local ReactTypes = require(Packages.Shared)
type Wakeable = ReactTypes.Wakeable
type Thenable<R> = ReactTypes.Thenable<R>
local ReactSymbols = require(Packages.Shared).ReactSymbols

local REACT_LAZY_TYPE = ReactSymbols.REACT_LAZY_TYPE

local Uninitialized = -1
local Pending = 0
local Resolved = 1
local Rejected = 2

type UninitializedPayload<T> = {
	-- ROBLOX deviaton: Luau doesn't support literals
	--   _status: -1,
	_status: number,
	_result: () -> Thenable<{ default: T, [string]: any }>,
}

type PendingPayload = {
	-- ROBLOX deviaton: Luau doesn't support literals
	-- _status: 0,
	_status: number,
	_result: Wakeable,
}

type ResolvedPayload<T> = {
	-- ROBLOX deviaton: Luau doesn't support literals
	-- _status: 1,
	_status: number,
	_result: { default: T, [string]: any },
}

type RejectedPayload = {
	-- ROBLOX deviaton: Luau doesn't support literals
	-- _status: 2,
	_status: number,
	_result: any,
}

type Payload<T> =
	UninitializedPayload<T>
	| PendingPayload
	| ResolvedPayload<T>
	| RejectedPayload

export type LazyComponent<T, P> = {
	["$$typeof"]: number,
	_payload: P,
	_init: (payload: P) -> T,
	--   ...
}

function lazyInitializer<T>(payload: Payload<T>): T
	if payload._status == Uninitialized then
		local ctor = payload._result
		local thenable = ctor()
		-- Transition to the next state.
		local pending: PendingPayload = payload :: any
		pending._status = Pending
		pending._result = thenable
		thenable:andThen(function(moduleObject)
			if payload._status == Pending then
				local defaultExport = moduleObject.default
				if ReactGlobals.__DEV__ then
					if defaultExport == nil then
						console.error(
							"lazy: Expected the result of a dynamic import() call. "
								.. "Instead received: `%s`\n\nYour code should look like: \n  "
								-- Break up imports to avoid accidentally parsing them as dependencies.
								-- ROBLOX deviation: Lua syntax in message
								.. "local MyComponent = lazy(function() return req"
								.. "quire(script.Parent.MyComponent) end)",
							inspect(moduleObject)
						)
					end
				end
				-- Transition to the next state.
				local resolved: ResolvedPayload<T> = payload :: any
				resolved._status = Resolved
				resolved._result = defaultExport
			end
		end, function(error_)
			if payload._status == Pending then
				-- Transition to the next state.
				local rejected: RejectedPayload = payload :: any
				rejected._status = Rejected
				rejected._result = error_
			end
		end)
	end
	if payload._status == Resolved then
		return payload._result
	else
		error(payload._result)
	end
end

local exports = {}

--[[
	Lets you defer loading a component’s code until it is rendered for the first
	time.

	See [API reference for `React.lazy`](https://react.dev/reference/react/lazy).

	@param load A function that returns a `Promise` or another thenable (a `Promise`-like object with an `andThen` method). React will not call `load` until the first time you attempt to render the returned component. After React first calls load, it will wait for it to resolve, and then render the resolved value’s `.default` as a React component. Both the returned `Promise` and the `Promise`’s resolved value will be cached, so React will not call load more than once. If the `Promise` rejects, React will throw the rejection reason for the nearest Error Boundary to handle.
]]
exports.lazy = function<T>(
	ctor: () -> Thenable<{ default: T, [string]: any }>
): LazyComponent<T, Payload<T>>
	local payload: Payload<T> = {
		-- We use these fields to store the result.
		_status = -1,
		_result = ctor,
	}

	local lazyType: LazyComponent<T, Payload<T>> = {
		["$$typeof"] = REACT_LAZY_TYPE,
		_payload = payload,
		-- ROBLOX FIXME Luau: needs something even beyond normalization to avoid Property '_init' is not compatible. Type '<T>(Payload<T>) -> T?' could not be converted into '(Payload<T>) -> T?'; different number of generic type parameters
		_init = lazyInitializer :: any,
	}

	if ReactGlobals.__DEV__ then
		-- In production, this would just set it on the object.
		local defaultProps
		local propTypes
		-- $FlowFixMe
		setmetatable(lazyType, {
			__index = function(self, key)
				if key == "defaultProps" then
					return defaultProps
				end
				if key == "propTypes" then
					return propTypes
				end
				return
			end,
			__newindex = function(self, key, value)
				if key == "defaultProps" then
					console.error(
						"React.lazy(...): It is not supported to assign `defaultProps` to "
							.. "a lazy component import. Either specify them where the component "
							.. "is defined, or create a wrapping component around it."
					)
					defaultProps = value
					-- Match production behavior more closely:
					-- $FlowFixMe
					setmetatable(self, {
						__index = function() end,
						__newindex = function() end,
					})
				end
				if key == "propTypes" then
					console.error(
						"React.lazy(...): It is not supported to assign `propTypes` to "
							.. "a lazy component import. Either specify them where the component "
							.. "is defined, or create a wrapping component around it."
					)
					propTypes = value
					-- Match production behavior more closely:
					-- $FlowFixMe
					setmetatable(self, {
						__index = function() end,
						__newindex = function() end,
					})
				end
			end,
		})
	end

	return lazyType
end

return exports
