--!strict
-- ROBLOX upstream: https://github.com/facebook/react/blob/8e5adfbd7e605bda9c5e96c10e015b3dc0df688e/packages/react-dom/src/client/ReactDOMRoot.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.Parent

local ReactRobloxHostTypes = require(script.Parent["ReactRobloxHostTypes.roblox"])
type Container = ReactRobloxHostTypes.Container
type RootType = ReactRobloxHostTypes.RootType
type RootOptions = ReactRobloxHostTypes.RootOptions

local ReactGlobals = require(Packages.ReactGlobals)
local ReconcilerTypes = require(Packages.ReactReconciler)
type RootTag = ReconcilerTypes.RootTag
local ReactTypes = require(Packages.Shared)
type MutableSource<T> = ReactTypes.MutableSource<T>
type ReactNodeList = ReactTypes.ReactNodeList
local ReactInternalTypes = require(Packages.ReactReconciler)
type FiberRoot = ReactInternalTypes.FiberRoot

type Array<T> = { [number]: T }

local ReactRobloxComponentTree = require(script.Parent.ReactRobloxComponentTree)
-- local isContainerMarkedAsRoot = ReactRobloxComponentTree.isContainerMarkedAsRoot
local markContainerAsRoot = ReactRobloxComponentTree.markContainerAsRoot
local unmarkContainerAsRoot = ReactRobloxComponentTree.unmarkContainerAsRoot
-- local listenToAllSupportedEvents = require(script.Parent.Parent.events.DOMPluginEventSystem).listenToAllSupportedEvents
-- local eagerlyTrapReplayableEvents = require(script.Parent.Parent.events.ReactDOMEventReplaying).eagerlyTrapReplayableEvents
-- local HTMLNodeType = require(script.Parent.Parent.shared.HTMLNodeType)
-- local ELEMENT_NODE = HTMLNodeType.ELEMENT_NODE
-- local COMMENT_NODE = HTMLNodeType.COMMENT_NODE
-- local DOCUMENT_NODE = HTMLNodeType.DOCUMENT_NODE
-- local DOCUMENT_FRAGMENT_NODE = HTMLNodeType.DOCUMENT_FRAGMENT_NODE
-- local ensureListeningTo = require(Packages.ReactDOMComponent).ensureListeningTo

-- ROBLOX deviation: Use the config-injecting entry point for the reconciler
local ReactFiberReconciler = require(script.Parent.Parent["ReactReconciler.roblox"])
local createContainer = ReactFiberReconciler.createContainer
local updateContainer = ReactFiberReconciler.updateContainer
-- local findHostInstanceWithNoPortals = ReactFiberReconciler.findHostInstanceWithNoPortals
-- local registerMutableSourceForHydration = ReactFiberReconciler.registerMutableSourceForHydration
local invariant = require(Packages.Shared).invariant
local enableEagerRootListeners =
	require(Packages.Shared).ReactFeatureFlags.enableEagerRootListeners

-- ROBLOX deviation: Used to fix unmount not being synchronous
local flushSync = ReactFiberReconciler.flushSync
local flushPassiveEffects = ReactFiberReconciler.flushPassiveEffects

local BlockingRoot = ReactFiberReconciler.ReactRootTags.BlockingRoot
local ConcurrentRoot = ReactFiberReconciler.ReactRootTags.ConcurrentRoot
local LegacyRoot = ReactFiberReconciler.ReactRootTags.LegacyRoot

local createRootImpl

local ReactRobloxRoot = {}
ReactRobloxRoot.__index = ReactRobloxRoot

function ReactRobloxRoot.new(container: Container, options: RootOptions?): RootType
	local root: RootType = (setmetatable({}, ReactRobloxRoot) :: any) :: RootType
	root._internalRoot = createRootImpl(container, ConcurrentRoot, options)

	return root
end

local function createBlockingRoot(
	container: Container,
	tag: RootTag,
	options: RootOptions?
): RootType
	-- deviation: We can just share the logic here via metatables
	local root: RootType = (setmetatable({}, ReactRobloxRoot) :: any) :: RootType
	root._internalRoot = createRootImpl(container, tag, options)

	return root
end

function ReactRobloxRoot:render(children: ReactNodeList)
	local root = self._internalRoot
	-- if ReactGlobals.__DEV__ then
	--   if typeof (arguments[1] == 'function')
	--     console.error(
	--       'render(...): does not support the second callback argument. ' +
	--         'To execute a side effect after rendering, declare it in a component body with useEffect().',
	--     )
	--   end
	--   local container = root.containerInfo

	--   if container.nodeType ~= COMMENT_NODE)
	--     local hostInstance = findHostInstanceWithNoPortals(root.current)
	--     if hostInstance)
	--       if hostInstance.parentNode ~= container)
	--         console.error(
	--           'render(...): It looks like the React-rendered content of the ' +
	--             'root container was removed without using React. This is not ' +
	--             'supported and will cause errors. Instead, call ' +
	--             "root.unmount() to empty a root's container.",
	--         )
	--       end
	--     end
	--   end
	-- end
	updateContainer(children, root, nil)
end

function ReactRobloxRoot:unmount()
	-- if ReactGlobals.__DEV__ then
	--   if typeof arguments[0] == 'function')
	--     console.error(
	--       'unmount(...): does not support a callback argument. ' +
	--         'To execute a side effect after rendering, declare it in a component body with useEffect().',
	--     )
	--   end
	-- end
	local root = self._internalRoot
	local container = root.containerInfo

	-- ROBLOX deviation: unmount() is synchronous in upstream, at least in modern versions
	flushSync(function()
		updateContainer(nil, root, nil, function()
			unmarkContainerAsRoot(container)
		end)
	end)

	flushPassiveEffects()
end

-- ROBLOX TODO: add Options type
-- createRootImpl = function(
--   container: Container,
--   tag: RootTag,
--   options: RootOptions
-- )
createRootImpl = function(container: Container, tag: RootTag, options: any)
	-- Tag is either LegacyRoot or Concurrent Root
	local hydrate = options ~= nil and options.hydrate == true
	local hydrationCallbacks = if options ~= nil then options.hydrationOptions else nil
	local mutableSources = (
		options ~= nil
		and options.hydrationOptions ~= nil
		and options.hydrationOptions.mutableSources
	) or nil
	local root = createContainer(container, tag, hydrate, hydrationCallbacks)
	markContainerAsRoot(root.current, container)
	-- local containerNodeType = container.nodeType

	if enableEagerRootListeners then
		--   local rootContainerElement =
		--     container.nodeType == COMMENT_NODE and container.parentNode or container
		--   listenToAllSupportedEvents(rootContainerElement)
		-- } else {
		--   if hydrate and tag ~= LegacyRoot)
		--     local doc =
		--       containerNodeType == DOCUMENT_NODE
		--         ? container
		--         : container.ownerDocument
		--     -- We need to cast this because Flow doesn't work
		--     -- with the hoisted containerNodeType. If we inline
		--     -- it, then Flow doesn't complain. We intentionally
		--     -- hoist it to reduce code-size.
		--     eagerlyTrapReplayableEvents(container, ((doc: any): Document))
		--   } else if
		--     containerNodeType ~= DOCUMENT_FRAGMENT_NODE and
		--     containerNodeType ~= DOCUMENT_NODE
		--   )
		--     ensureListeningTo(container, 'onMouseEnter')
		--   end
	end

	if mutableSources then
		-- for (local i = 0; i < mutableSources.length; i++)
		--   local mutableSource = mutableSources[i]
		--   registerMutableSourceForHydration(root, mutableSource)
		-- end
	end

	return root
end

local exports = {}

local function isValidContainer(node: any): boolean
	-- ROBLOX TODO: This behavior will deviate, for now just check that it's an
	-- instance, which should be good enough
	return typeof(node) == "Instance"
	-- return not not (
	--   node and
	--   (node.nodeType == ELEMENT_NODE or
	--     node.nodeType == DOCUMENT_NODE or
	--     node.nodeType == DOCUMENT_FRAGMENT_NODE or
	--     (node.nodeType == COMMENT_NODE and
	--       node.nodeValue == ' react-mount-point-unstable '))
	-- )
end

exports.isValidContainer = isValidContainer

--[[
	`createRoot` lets you create a root to display React components inside a
	Roblox instance.

	See [API reference for `createRoot`](https://react.dev/reference/react-dom/client/createRoot).
]]
-- deviation: Create `Container` from instance
exports.createRoot = function(container: Container, options: RootOptions?): RootType
	invariant(
		isValidContainer(container),
		-- ROBLOX deviation: Use roblox engine terminology
		"createRoot(...): Target container is not a Roblox Instance."
	)
	warnIfReactDOMContainerInDEV(container)
	return ReactRobloxRoot.new(container, options)
end

exports.createBlockingRoot = function(
	container: Container,
	options: RootOptions?
): RootType
	invariant(
		isValidContainer(container),
		-- ROBLOX deviation: Use roblox engine terminology
		"createRoot(...): Target container is not a Roblox Instance."
	)
	warnIfReactDOMContainerInDEV(container)
	return createBlockingRoot(container, BlockingRoot, options)
end

exports.createLegacyRoot = function(container: Container, options: RootOptions?): RootType
	return createBlockingRoot(container, LegacyRoot, options)
end

function warnIfReactDOMContainerInDEV(container)
	if ReactGlobals.__DEV__ then
		-- ROBLOX TODO: This behavior will deviate; should we validate that the
		-- container is not a PlayerGui of any sort?

		-- if
		--   container.nodeType == ELEMENT_NODE and
		--   container.tagName and
		--   container.tagName.toUpperCase() == 'BODY'
		-- then
		--   console.error(
		--     'createRoot(): Creating roots directly with document.body is ' ..
		--       'discouraged, since its children are often manipulated by third-party ' ..
		--       'scripts and browser extensions. This may lead to subtle ' ..
		--       'reconciliation issues. Try using a container element created ' ..
		--       'for your app.'
		--   )
		-- end
		-- if isContainerMarkedAsRoot(container) then
		--   if container._reactRootContainer then
		--     console.error(
		--       'You are calling ReactDOM.createRoot() on a container that was previously ' ..
		--         'passed to ReactDOM.render(). This is not supported.'
		--     )
		--   else
		--     console.error(
		--       'You are calling ReactDOM.createRoot() on a container that ' ..
		--         'has already been passed to createRoot() before. Instead, call ' ..
		--         'root.render() on the existing root instead if you want to update it.'
		--     )
		--   end
		-- end
	end
end

return exports
