--!strict
-- upstream: https://github.com/facebook/react/blob/8e5adfbd7e605bda9c5e96c10e015b3dc0df688e/packages/react-dom/src/client/ReactDOMHostConfig.js
-- upstream: https://github.com/facebook/react/blob/efd8f6442d1aa7c4566fe812cba03e7e83aaccc3/packages/react-native-renderer/src/ReactNativeHostConfig.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
]]
-- FIXME (roblox): remove this when our unimplemented
local function unimplemented(message: string)
  print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
  print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
  print("UNIMPLEMENTED ERROR: " .. tostring(message))
  error("FIXME (roblox): " .. message .. " is unimplemented", 2)
end

local CollectionService = game:GetService("CollectionService")
local Packages = script.Parent.Parent.Parent
local LuauPolyfill = require(Packages.LuauPolyfill)
local inspect = LuauPolyfill.util.inspect
local console = require(Packages.Shared).console
local Object = LuauPolyfill.Object
local setTimeout = LuauPolyfill.setTimeout
local clearTimeout = LuauPolyfill.clearTimeout

-- local type {DOMEventName} = require(Packages.../events/DOMEventNames'
-- local type {Fiber, FiberRoot} = require(Packages.react-reconciler/src/ReactInternalTypes'
-- local type {
--   BoundingRect,
--   IntersectionObserverOptions,
--   ObserveVisibleRectsCallback,
-- } = require(Packages.react-reconciler/src/ReactTestSelectors'
local ReactRobloxHostTypes = require(script.Parent["ReactRobloxHostTypes.roblox"])
type RootType = ReactRobloxHostTypes.RootType
type Container = ReactRobloxHostTypes.Container
type HostInstance = ReactRobloxHostTypes.HostInstance
type SuspenseInstance = ReactRobloxHostTypes.SuspenseInstance
type TextInstance = ReactRobloxHostTypes.TextInstance
type Props = ReactRobloxHostTypes.Props
type Type = ReactRobloxHostTypes.Type
type HostContext = ReactRobloxHostTypes.HostContext

-- local type {ReactScopeInstance} = require(Packages.shared/ReactTypes'
-- local type {ReactDOMFundamentalComponentInstance} = require(Packages.../shared/ReactDOMTypes'

local ReactRobloxComponentTree = require(script.Parent.ReactRobloxComponentTree)
local precacheFiberNode = ReactRobloxComponentTree.precacheFiberNode
local uncacheFiberNode = ReactRobloxComponentTree.uncacheFiberNode
local updateFiberProps = ReactRobloxComponentTree.updateFiberProps
-- local getClosestInstanceFromNode = ReactRobloxComponentTree.getClosestInstanceFromNode
-- local getFiberFromScopeInstance = ReactRobloxComponentTree.getFiberFromScopeInstance
-- local getInstanceFromNodeDOMTree = ReactRobloxComponentTree.getInstanceFromNode
-- local isContainerMarkedAsRoot = ReactRobloxComponentTree.isContainerMarkedAsRoot

-- local {hasRole} = require(Packages../DOMAccessibilityRoles'
local ReactRobloxComponent = require(script.Parent.ReactRobloxComponent)
-- local createElement = ReactRobloxComponent.createElement
-- local createTextNode = ReactRobloxComponent.createTextNode
local setInitialProperties = ReactRobloxComponent.setInitialProperties
local diffProperties = ReactRobloxComponent.diffProperties
local updateProperties = ReactRobloxComponent.updateProperties
local cleanupHostComponent = ReactRobloxComponent.cleanupHostComponent
-- local diffHydratedProperties = ReactRobloxComponent.diffHydratedProperties
-- local diffHydratedText = ReactRobloxComponent.diffHydratedText
-- local trapClickOnNonInteractiveElement = ReactRobloxComponent.trapClickOnNonInteractiveElement
-- local warnForUnmatchedText = ReactRobloxComponent.warnForUnmatchedText
-- local warnForDeletedHydratableElement = ReactRobloxComponent.warnForDeletedHydratableElement
-- local warnForDeletedHydratableText = ReactRobloxComponent.warnForDeletedHydratableText
-- local warnForInsertedHydratedElement = ReactRobloxComponent.warnForInsertedHydratedElement
-- local warnForInsertedHydratedText = ReactRobloxComponent.warnForInsertedHydratedText
-- local {getSelectionInformation, restoreSelection} = require(Packages../ReactInputSelection'
-- local setTextContent = require(Packages../setTextContent'
-- local {validateDOMNesting, updatedAncestorInfo} = require(Packages../validateDOMNesting'
-- local {
--   isEnabled as ReactBrowserEventEmitterIsEnabled,
--   setEnabled as ReactBrowserEventEmitterSetEnabled,
-- } = require(Packages.../events/ReactDOMEventListener'
-- local {getChildNamespace} = require(Packages.../shared/DOMNamespaces'
-- local {
--   ELEMENT_NODE,
--   TEXT_NODE,
--   COMMENT_NODE,
--   DOCUMENT_NODE,
--   DOCUMENT_FRAGMENT_NODE,
-- } = require(Packages.../shared/HTMLNodeType'
-- local dangerousStyleValue = require(Packages.../shared/dangerousStyleValue'

-- local {REACT_OPAQUE_ID_TYPE} = require(Packages.shared/ReactSymbols'
-- local {retryIfBlockedOn} = require(Packages.../events/ReactDOMEventReplaying'

local ReactFeatureFlags = require(Packages.Shared).ReactFeatureFlags
-- local enableSuspenseServerRenderer = ReactFeatureFlags.enableSuspenseServerRenderer
-- local enableFundamentalAPI = ReactFeatureFlags.enableFundamentalAPI
local enableCreateEventHandleAPI = ReactFeatureFlags.enableCreateEventHandleAPI
-- local enableScopeAPI = ReactFeatureFlags.enableScopeAPI
-- local enableEagerRootListeners = ReactFeatureFlags.enableEagerRootListeners

-- local {HostComponent, HostText} = require(Packages.react-reconciler/src/ReactWorkTags'
-- local {
--   listenToReactEvent,
--   listenToAllSupportedEvents,
-- } = require(Packages.../events/DOMPluginEventSystem'

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

-- ROBLOX deviation: Moved to ReactRobloxHostTypes
-- export type Type = string;
-- export type Props = {
--   autoFocus: boolean?,
--   children: any,
--   disabled: boolean?,
--   hidden: boolean?,
--   suppressHydrationWarning: boolean?,
--   dangerouslySetInnerHTML: any,
--   style: { display: string, [any]: any }?,
--   bottom: number?,
--   left: number?,
--   right: number?,
--   top: number?,
--   -- ...
--   [any]: any,
-- };
-- export type EventTargetChildElement = {
--   type: string,
--   props: nil | {
--     style?: {
--       position?: string,
--       zIndex?: number,
--       bottom?: string,
--       left?: string,
--       right?: string,
--       top?: string,
--       ...
--     },
--     ...
--   },
--   ...
-- end


-- ROBLOX deviation: Moved to ReactRobloxHostTypes
-- export type SuspenseInstance = Comment & {_reactRetry?: () => void, ...}
-- export type HydratableInstance = Instance | TextInstance | SuspenseInstance

-- ROBLOX deviation: Moved to ReactRobloxHostTypes
-- export type PublicInstance = Element | Text
-- type HostContextDev = {
--   namespace: string,
--   ancestorInfo: any,
--   -- ...
--   [any]: any,
-- }
-- type HostContextProd = string
-- export type HostContext = HostContextDev | HostContextProd

-- export type UpdatePayload = Array<mixed>
-- ROBLOX FIXME: cannot create type equal to void
-- export type ChildSet = void; -- Unused
-- export type TimeoutHandle = TimeoutID
-- export type NoTimeout = -1
-- export type RendererInspectionConfig = $ReadOnly<{or}>

-- export opaque type OpaqueIDType =
--   | string
--   | {
--       toString: () => string | void,
--       valueOf: () => string | void,
--     end

-- type SelectionInformation = {|
--   focusedElem: nil | HTMLElement,
--   selectionRange: mixed,
-- |}

-- local SUPPRESS_HYDRATION_WARNING
-- if __DEV__)
--   SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning'
-- end

-- local SUSPENSE_START_DATA = '$'
-- local SUSPENSE_END_DATA = '/$'
-- local SUSPENSE_PENDING_START_DATA = '$?'
-- local SUSPENSE_FALLBACK_START_DATA = '$!'

-- local STYLE = 'style'

-- local eventsEnabled: boolean? = nil
-- local selectionInformation: nil | SelectionInformation = nil

-- function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
--   switch (type)
--     case 'button':
--     case 'input':
--     case 'select':
--     case 'textarea':
--       return !!props.autoFocus
--   end
--   return false
-- end

-- ROBLOX deviation: Use GetDescendants rather than recursion
local function recursivelyUncacheFiberNode(node: HostInstance)
  -- ROBLOX https://jira.rbx.com/browse/LUAFDN-713: Tables are somehow ending up
  -- in this function that expects Instances. In that case, we won't be able to
  -- iterate through its descendants.
  if typeof(node :: any) ~= "Instance" then
    return
  end

  uncacheFiberNode(node)

  for _, child in node:GetDescendants() do
    uncacheFiberNode(child)
  end
end

local exports: {[any]: any} = {}
Object.assign(exports, require(Packages.Shared).ReactFiberHostConfig.WithNoPersistence)

exports.getRootHostContext = function(
  rootContainerInstance: Container
): HostContext
  -- ROBLOX deviation: This is a lot of HTML-DOM specific logic; I'm not clear on
  -- whether there'll be an equivalent of `namespaceURI` for our use cases, but
  -- we may want to provide other kinds of context for host objects.

  -- For now, as a guess, we'll return the kind of instance we're attached to
  return rootContainerInstance.ClassName

  -- local type
  -- local namespace
  -- local nodeType = rootContainerInstance.nodeType
  -- switch (nodeType)
  --   case DOCUMENT_NODE:
  --   case DOCUMENT_FRAGMENT_NODE: {
  --     type = nodeType == DOCUMENT_NODE ? '#document' : '#fragment'
  --     local root = (rootContainerInstance: any).documentElement
  --     namespace = root ? root.namespaceURI : getChildNamespace(null, '')
  --     break
  --   end
  --   default: {
  --     local container: any =
  --       nodeType == COMMENT_NODE
  --         ? rootContainerInstance.parentNode
  --         : rootContainerInstance
  --     local ownNamespace = container.namespaceURI or nil
  --     type = container.tagName
  --     namespace = getChildNamespace(ownNamespace, type)
  --     break
  --   end
  -- end
  -- if _G.__DEV__ then
  --   local validatedTag = type.toLowerCase()
  --   local ancestorInfo = updatedAncestorInfo(null, validatedTag)
  --   return {namespace, ancestorInfo}
  -- end
  -- return namespace
end

exports.getChildHostContext = function(
  parentHostContext: HostContext,
  type: string,
  rootContainerInstance: Container
): HostContext
  -- ROBLOX deviation: unclear on the purpose here just yet, might be fine to
  -- just return parent's hostContext for now
  return parentHostContext
  -- if _G.__DEV__ then
  --   local parentHostContextDev = ((parentHostContext: any): HostContextDev)
  --   local namespace = getChildNamespace(parentHostContextDev.namespace, type)
  --   local ancestorInfo = updatedAncestorInfo(
  --     parentHostContextDev.ancestorInfo,
  --     type,
  --   )
  --   return {namespace, ancestorInfo}
  -- end
  -- local parentNamespace = ((parentHostContext: any): HostContextProd)
  -- return getChildNamespace(parentNamespace, type)
end

exports.getPublicInstance = function(instance: Instance): any
  return instance
end

exports.prepareForCommit = function(containerInfo: Container): Object?
  -- eventsEnabled = ReactBrowserEventEmitterIsEnabled()
  -- selectionInformation = getSelectionInformation()
  local activeInstance = nil
  if enableCreateEventHandleAPI then
    unimplemented("enableCreateEventHandleAPI")
  --   local focusedElem = selectionInformation.focusedElem
  --   if focusedElem ~= nil then
  --     activeInstance = getClosestInstanceFromNode(focusedElem)
  --   end
  end
  -- ReactBrowserEventEmitterSetEnabled(false)
  return activeInstance
end

exports.beforeActiveInstanceBlur = function()
  if enableCreateEventHandleAPI then
    unimplemented("enableCreateEventHandleAPI")
    -- ReactBrowserEventEmitterSetEnabled(true)
    -- dispatchBeforeDetachedBlur((selectionInformation: any).focusedElem)
    -- ReactBrowserEventEmitterSetEnabled(false)
  end
end

exports.afterActiveInstanceBlur = function()
  if enableCreateEventHandleAPI then
    unimplemented("enableCreateEventHandleAPI")
    -- ReactBrowserEventEmitterSetEnabled(true)
    -- dispatchAfterDetachedBlur((selectionInformation: any).focusedElem)
    -- ReactBrowserEventEmitterSetEnabled(false)
  end
end

exports.resetAfterCommit = function(containerInfo: Container)
  -- warn("Skip unimplemented: resetAfterCommit")
  -- restoreSelection(selectionInformation)
  -- ReactBrowserEventEmitterSetEnabled(eventsEnabled)
  -- eventsEnabled = nil
  -- selectionInformation = nil
end

exports.createInstance = function(
	type_: string,
	props: Props,
	rootContainerInstance: Container,
	hostContext: HostContext,
	internalInstanceHandle: Object
): HostInstance
	-- local hostKey = virtualNode.hostKey

	local domElement = Instance.new(type_)
	-- ROBLOX deviation: compatibility with old Roact where instances have their name
	-- set to the key value
	if internalInstanceHandle.key then
		domElement.Name = internalInstanceHandle.key
	else
		local currentHandle = internalInstanceHandle.return_
		while currentHandle do
			if currentHandle.key then
				domElement.Name = currentHandle.key
				break
			end
			currentHandle = currentHandle.return_
		end
	end

	precacheFiberNode(internalInstanceHandle, domElement)
	updateFiberProps(domElement, props)

	-- TODO: Support refs (does that actually happen here, or later?)
	-- applyRef(element.props[Ref], instance)

	-- Will have to be managed outside of createInstance
	-- if virtualNode.eventManager ~= nil then
	--   virtualNode.eventManager:resume()
	-- end

	return domElement

	-- return Instance.new("Frame")
	-- local parentNamespace: string
	-- if __DEV__)
	--   -- TODO: take namespace into account when validating.
	--   local hostContextDev = ((hostContext: any): HostContextDev)
	--   validateDOMNesting(type, nil, hostContextDev.ancestorInfo)
	--   if
	--     typeof props.children == 'string' or
	--     typeof props.children == 'number'
	--   )
	--     local string = '' + props.children
	--     local ownAncestorInfo = updatedAncestorInfo(
	--       hostContextDev.ancestorInfo,
	--       type,
	--     )
	--     validateDOMNesting(null, string, ownAncestorInfo)
	--   end
	--   parentNamespace = hostContextDev.namespace
	-- } else {
	--   parentNamespace = ((hostContext: any): HostContextProd)
	-- end
	-- local domElement: Instance = createElement(
	--   type,
	--   props,
	--   rootContainerInstance,
	--   parentNamespace,
	-- )
end

exports.appendInitialChild = function(
  parentInstance: Instance,
  child: Instance
)
  -- ROBLOX deviation: Establish hierarchy with Parent property
  child.Parent = parentInstance
end

exports.finalizeInitialChildren = function(
  domElement: HostInstance,
  type_: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext
): boolean
  setInitialProperties(domElement, type_, props, rootContainerInstance)
  return false
  -- return shouldAutoFocusHostComponent(type_, props)
end

local function prepareUpdate(
  domElement: Instance,
  type_: string,
  oldProps: Props,
  newProps: Props,
  rootContainerInstance: Container,
  hostContext: HostContext
): nil | Array<any>
  -- if _G.__DEV__ then
  --   local hostContextDev = ((hostContext: any): HostContextDev)
  --   if
  --     typeof newProps.children ~= typeof oldProps.children and
  --     (typeof newProps.children == 'string' or
  --       typeof newProps.children == 'number')
  --   )
  --     local string = '' + newProps.children
  --     local ownAncestorInfo = updatedAncestorInfo(
  --       hostContextDev.ancestorInfo,
  --       type,
  --     )
  --     validateDOMNesting(null, string, ownAncestorInfo)
  --   end
  -- end
  return diffProperties(
    domElement,
    type_,
    oldProps,
    newProps,
    rootContainerInstance
  )
end
exports.prepareUpdate = prepareUpdate

exports.shouldSetTextContent = function(_type: string, _props: Props): boolean
  -- ROBLOX deviation: Ignore TextInstance logic, which isn't applicable to Roblox
  return false
--   return (
--     type == 'textarea' or
--     type == 'option' or
--     type == 'noscript' or
--     typeof props.children == 'string' or
--     typeof props.children == 'number' or
--     (typeof props.dangerouslySetInnerHTML == 'table’' and
--       props.dangerouslySetInnerHTML ~= nil and
--       props.dangerouslySetInnerHTML.__html ~= nil)
--   )
end

-- ROBLOX deviation: Text nodes aren't supported in Roblox renderer, so error so that tests fail immediately
exports.createTextInstance = function(
  text: string,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object
): any
  unimplemented("createTextInstance")
  return nil
end

exports.isPrimaryRenderer = true
exports.warnsIfNotActing = true
-- This initialization code may run even on server environments
-- if a component just imports ReactDOM (e.g. for findDOMNode).
-- Some environments might not have setTimeout or clearTimeout.
-- ROBLOX deviation: We're only dealing with client right now, so these always populate
exports.scheduleTimeout = setTimeout
exports.cancelTimeout = clearTimeout
exports.noTimeout = -1

-- -------------------
--     Mutation
-- -------------------

exports.supportsMutation = true

exports.commitMount = function(
  domElement: Instance,
  type: string,
  newProps: Props,
  internalInstanceHandle: Object
)
  unimplemented("commitMount")
  -- -- Despite the naming that might imply otherwise, this method only
  -- -- fires if there is an `Update` effect scheduled during mounting.
  -- -- This happens if `finalizeInitialChildren` returns `true` (which it
  -- -- does to implement the `autoFocus` attribute on the client). But
  -- -- there are also other cases when this might happen (such as patching
  -- -- up text content during hydration mismatch). So we'll check this again.
  -- if shouldAutoFocusHostComponent(type, newProps))
  --   ((domElement: any):
  --     | HTMLButtonElement
  --     | HTMLInputElement
  --     | HTMLSelectElement
  --     | HTMLTextAreaElement).focus()
  -- end
end

exports.commitUpdate = function(
  domElement: Instance,
  updatePayload: Array<any>,
  type_: string,
  oldProps: Props,
  newProps: Props,
  internalInstanceHandle: Object
)
  -- Update the props handle so that we know which props are the ones with
  -- with current event handlers.
  updateFiberProps(domElement, newProps)
  -- Apply the diff to the DOM node.
  updateProperties(domElement, updatePayload, oldProps)
end

-- ROBLOX deviation: Ignore TextInstance logic, which isn't applicable to Roblox
-- exports.resetTextContent(domElement: Instance): void {
--   setTextContent(domElement, '')
-- end

-- ROBLOX deviation: Ignore TextInstance logic, which isn't applicable to Roblox
-- exports.commitTextUpdate(
--   textInstance: TextInstance,
--   oldText: string,
--   newText: string,
-- ): void {
--   textInstance.nodeValue = newText
-- end

local function checkTags(instance: Instance)
  if typeof(instance :: any) ~= "Instance" then
    console.warn("Could not check tags on non-instance %s.", inspect(instance))
    return
  end
  if not instance:IsDescendantOf(game) then
    if #CollectionService:GetTags(instance) > 0 then
      console.warn(
        'Tags applied to orphaned %s "%s" cannot be accessed via'
          .. " CollectionService:GetTagged. If you're relying on tag"
          .. " behavior in a unit test, consider mounting your test "
          .. "root into the DataModel.",
        instance.ClassName,
        instance.Name
      )
    end
  end
end

exports.appendChild = function(
  parentInstance: Instance,
  child: Instance
)
  -- ROBLOX deviation: Roblox's DOM is based on child->parent references
  child.Parent = parentInstance
  -- parentInstance.appendChild(child)
  if _G.__DEV__ then
    checkTags(child)
  end
end

exports.appendChildToContainer = function(
  container: Container,
  child: Instance
)
  -- ROBLOX TODO: Some of this logic may come back; for now, keep it simple
  local parentNode = container
  exports.appendChild(parentNode, child)

  -- if container.nodeType == COMMENT_NODE)
  --   parentNode = (container.parentNode: any)
  --   parentNode.insertBefore(child, container)
  -- } else {
  --   parentNode = container
  --   parentNode.appendChild(child)
  -- end
  -- -- This container might be used for a portal.
  -- -- If something inside a portal is clicked, that click should bubble
  -- -- through the React tree. However, on Mobile Safari the click would
  -- -- never bubble through the *DOM* tree unless an ancestor with onclick
  -- -- event exists. So we wouldn't see it and dispatch it.
  -- -- This is why we ensure that non React root containers have inline onclick
  -- -- defined.
  -- -- https://github.com/facebook/react/issues/11918
  -- local reactRootContainer = container._reactRootContainer
  -- if
  --   reactRootContainer == nil and parentNode.onclick == nil
  -- then
  --   -- TODO: This cast may not be sound for SVG, MathML or custom elements.
  --   trapClickOnNonInteractiveElement(((parentNode: any): HTMLElement))
  -- end
end

exports.insertBefore = function(
  parentInstance: Instance,
  child: Instance,
  _beforeChild: Instance
)
  -- ROBLOX deviation: Roblox's DOM is based on child->parent references
  child.Parent = parentInstance
  -- parentInstance.insertBefore(child, beforeChild)
  if _G.__DEV__ then
    checkTags(child)
  end
end

exports.insertInContainerBefore = function(
  container: Container,
  child: Instance,
  beforeChild: Instance
)
  -- ROBLOX deviation: use our container definition
  local parentNode = container
  exports.insertBefore(parentNode, child, beforeChild)
  -- if container.nodeType == COMMENT_NODE)
  --   (container.parentNode: any).insertBefore(child, beforeChild)
  -- } else {
  --   container.insertBefore(child, beforeChild)
  -- end
end

-- function createEvent(type: DOMEventName, bubbles: boolean): Event {
--   local event = document.createEvent('Event')
--   event.initEvent(((type: any): string), bubbles, false)
--   return event
-- end

-- function dispatchBeforeDetachedBlur(target: HTMLElement): void {
--   if enableCreateEventHandleAPI)
--     local event = createEvent('beforeblur', true)
--     -- Dispatch "beforeblur" directly on the target,
--     -- so it gets picked up by the event system and
--     -- can propagate through the React internal tree.
--     target.dispatchEvent(event)
--   end
-- end

-- function dispatchAfterDetachedBlur(target: HTMLElement): void {
--   if enableCreateEventHandleAPI)
--     local event = createEvent('afterblur', false)
--     -- So we know what was detached, make the relatedTarget the
--     -- detached target on the "afterblur" event.
--     (event: any).relatedTarget = target
--     -- Dispatch the event on the document.
--     document.dispatchEvent(event)
--   end
-- end

exports.removeChild = function(
  _parentInstance: Instance,
  child: Instance
)
  recursivelyUncacheFiberNode(child)
  -- ROBLOX deviation: The roblox renderer tracks bindings and event managers
  -- for instances, so make sure we clean those up when we remove the instance
  cleanupHostComponent(child)
  -- ROBLOX deviation: Roblox's DOM is based on child->parent references
  child.Parent = nil
  -- parentInstance.removeChild(child)
  -- ROBLOX deviation: Guard against misuse by locking parent and forcing external cleanup via Destroy
  child:Destroy()
end

exports.removeChildFromContainer = function(
  _container: Container,
  child: Instance
)
  -- ROBLOX deviation: Containers don't have special behavior and comment nodes
  -- have no datamodel equivalent, so just forward to the removeChild logic
  exports.removeChild(_container, child)
  -- if container.nodeType == COMMENT_NODE)
  --   (container.parentNode: any).removeChild(child)
  -- } else {
  --   container.removeChild(child)
  -- end
end

exports.clearSuspenseBoundary = function(
  parentInstance: Instance,
  suspenseInstance: SuspenseInstance
)
  -- ROBLOX FIXME: this is a major thing we need to fix for Suspense to work as a feature
  unimplemented("clearSuspenseBoundary")
--   local node = suspenseInstance
--   -- Delete all nodes within this suspense boundary.
--   -- There might be nested nodes so we need to keep track of how
--   -- deep we are and only break out when we're back on top.
--   local depth = 0
--   do {
--     local nextNode = node.nextSibling
--     parentInstance.removeChild(node)
--     if nextNode and nextNode.nodeType == COMMENT_NODE)
--       local data = ((nextNode: any).data: string)
--       if data == SUSPENSE_END_DATA)
--         if depth == 0)
--           parentInstance.removeChild(nextNode)
--           -- Retry if any event replaying was blocked on this.
--           retryIfBlockedOn(suspenseInstance)
--           return
--         } else {
--           depth--
--         end
--       } else if
--         data == SUSPENSE_START_DATA or
--         data == SUSPENSE_PENDING_START_DATA or
--         data == SUSPENSE_FALLBACK_START_DATA
--       )
--         depth++
--       end
--     end
--     node = nextNode
--   } while (node)
--   -- TODO: Warn, we didn't find the end comment boundary.
--   -- Retry if any event replaying was blocked on this.
--   retryIfBlockedOn(suspenseInstance)
end

exports.clearSuspenseBoundaryFromContainer = function(
  container: Container,
  suspenseInstance: SuspenseInstance
)
  -- ROBLOX FIXME: this is a major thing we need to fix for Suspense to work as a feature
  unimplemented("clearSuspenseBoundaryFromContainer")
--   if container.nodeType == COMMENT_NODE)
--     clearSuspenseBoundary((container.parentNode: any), suspenseInstance)
--   } else if container.nodeType == ELEMENT_NODE)
--     clearSuspenseBoundary((container: any), suspenseInstance)
--   } else {
--     -- Document nodes should never contain suspense boundaries.
--   end
--   -- Retry if any event replaying was blocked on this.
--   retryIfBlockedOn(container)
end

exports.hideInstance = function(instance: Instance)
  unimplemented("hideInstance")
  -- -- TODO: Does this work for all element types? What about MathML? Should we
  -- -- pass host context to this method?
  -- instance = ((instance: any): HTMLElement)
  -- local style = instance.style
  -- if typeof style.setProperty == 'function')
  --   style.setProperty('display', 'none', 'important')
  -- } else {
  --   style.display = 'none'
  -- end
end

-- ROBLOX deviation: error on TextInstance logic, which isn't applicable to Roblox
exports.hideTextInstance = function(textInstance: TextInstance): ()
  unimplemented("hideTextInstance")
--   textInstance.nodeValue = ''
end

exports.unhideInstance = function(instance: Instance, props: Props)
  unimplemented("unhideInstance")
  -- instance = ((instance: any): HTMLElement)
  -- local styleProp = props[STYLE]
  -- local display =
  --   styleProp ~= undefined and
  --   styleProp ~= nil and
  --   styleProp.hasOwnProperty('display')
  --     ? styleProp.display
  --     : nil
  -- instance.style.display = dangerousStyleValue('display', display)
end

-- ROBLOX deviation: error on TextInstance logic, which isn't applicable to Roblox
exports.unhideTextInstance = function(
  textInstance: TextInstance,
  text: string
): ()
  unimplemented("unhideTextInstance")
--   textInstance.nodeValue = text
end

exports.clearContainer = function(container: Container)
  -- ROBLOX deviation: with Roblox, we can simply enumerate and remove the children
  local parentInstance = container
  for _, child in parentInstance:GetChildren() do
    exports.removeChild(parentInstance, child)
  end
  -- if container.nodeType == ELEMENT_NODE)
  --   ((container: any): Element).textContent = ''
  -- } else if container.nodeType == DOCUMENT_NODE)
  --   local body = ((container: any): Document).body
  --   if body ~= nil)
  --     body.textContent = ''
  --   end
  -- end
end

-- -- -------------------
-- --     Hydration
-- -- -------------------

-- export local supportsHydration = true

-- exports.canHydrateInstance(
--   instance: HydratableInstance,
--   type: string,
--   props: Props,
-- ): nil | Instance {
--   if
--     instance.nodeType ~= ELEMENT_NODE or
--     type.toLowerCase() ~= instance.nodeName.toLowerCase()
--   )
--     return nil
--   end
--   -- This has now been refined to an element node.
--   return ((instance: any): Instance)
-- end

-- exports.canHydrateTextInstance(
--   instance: HydratableInstance,
--   text: string,
-- ): nil | TextInstance {
--   if text == '' or instance.nodeType ~= TEXT_NODE)
--     -- Empty strings are not parsed by HTML so there won't be a correct match here.
--     return nil
--   end
--   -- This has now been refined to a text node.
--   return ((instance: any): TextInstance)
-- end

-- exports.canHydrateSuspenseInstance(
--   instance: HydratableInstance,
-- ): nil | SuspenseInstance {
--   if instance.nodeType ~= COMMENT_NODE)
--     -- Empty strings are not parsed by HTML so there won't be a correct match here.
--     return nil
--   end
--   -- This has now been refined to a suspense node.
--   return ((instance: any): SuspenseInstance)
-- end

-- exports.isSuspenseInstanceFallback(instance: SuspenseInstance)
--   return instance.data == SUSPENSE_FALLBACK_START_DATA
-- end

-- exports.registerSuspenseInstanceRetry(
--   instance: SuspenseInstance,
--   callback: () => void,
-- )
--   instance._reactRetry = callback
-- end

-- function getNextHydratable(node)
--   -- Skip non-hydratable nodes.
--   for (; node ~= nil; node = node.nextSibling)
--     local nodeType = node.nodeType
--     if nodeType == ELEMENT_NODE or nodeType == TEXT_NODE)
--       break
--     end
--     if enableSuspenseServerRenderer)
--       if nodeType == COMMENT_NODE)
--         local nodeData = (node: any).data
--         if
--           nodeData == SUSPENSE_START_DATA or
--           nodeData == SUSPENSE_FALLBACK_START_DATA or
--           nodeData == SUSPENSE_PENDING_START_DATA
--         )
--           break
--         end
--       end
--     end
--   end
--   return (node: any)
-- end

-- exports.getNextHydratableSibling(
--   instance: HydratableInstance,
-- ): nil | HydratableInstance {
--   return getNextHydratable(instance.nextSibling)
-- end

-- exports.getFirstHydratableChild(
--   parentInstance: Container | Instance,
-- ): nil | HydratableInstance {
--   return getNextHydratable(parentInstance.firstChild)
-- end

-- exports.hydrateInstance(
--   instance: Instance,
--   type: string,
--   props: Props,
--   rootContainerInstance: Container,
--   hostContext: HostContext,
--   internalInstanceHandle: Object,
-- ): nil | Array<mixed> {
--   precacheFiberNode(internalInstanceHandle, instance)
--   -- TODO: Possibly defer this until the commit phase where all the events
--   -- get attached.
--   updateFiberProps(instance, props)
--   local parentNamespace: string
--   if __DEV__)
--     local hostContextDev = ((hostContext: any): HostContextDev)
--     parentNamespace = hostContextDev.namespace
--   } else {
--     parentNamespace = ((hostContext: any): HostContextProd)
--   end
--   return diffHydratedProperties(
--     instance,
--     type,
--     props,
--     parentNamespace,
--     rootContainerInstance,
--   )
-- end

-- exports.hydrateTextInstance(
--   textInstance: TextInstance,
--   text: string,
--   internalInstanceHandle: Object,
-- ): boolean {
--   precacheFiberNode(internalInstanceHandle, textInstance)
--   return diffHydratedText(textInstance, text)
-- end

-- exports.hydrateSuspenseInstance(
--   suspenseInstance: SuspenseInstance,
--   internalInstanceHandle: Object,
-- )
--   precacheFiberNode(internalInstanceHandle, suspenseInstance)
-- end

-- exports.getNextHydratableInstanceAfterSuspenseInstance(
--   suspenseInstance: SuspenseInstance,
-- ): nil | HydratableInstance {
--   local node = suspenseInstance.nextSibling
--   -- Skip past all nodes within this suspense boundary.
--   -- There might be nested nodes so we need to keep track of how
--   -- deep we are and only break out when we're back on top.
--   local depth = 0
--   while (node)
--     if node.nodeType == COMMENT_NODE)
--       local data = ((node: any).data: string)
--       if data == SUSPENSE_END_DATA)
--         if depth == 0)
--           return getNextHydratableSibling((node: any))
--         } else {
--           depth--
--         end
--       } else if
--         data == SUSPENSE_START_DATA or
--         data == SUSPENSE_FALLBACK_START_DATA or
--         data == SUSPENSE_PENDING_START_DATA
--       )
--         depth++
--       end
--     end
--     node = node.nextSibling
--   end
--   -- TODO: Warn, we didn't find the end comment boundary.
--   return nil
-- end

-- -- Returns the SuspenseInstance if this node is a direct child of a
-- -- SuspenseInstance. I.e. if its previous sibling is a Comment with
-- -- SUSPENSE_x_START_DATA. Otherwise, nil.
-- exports.getParentSuspenseInstance(
--   targetInstance: Node,
-- ): nil | SuspenseInstance {
--   local node = targetInstance.previousSibling
--   -- Skip past all nodes within this suspense boundary.
--   -- There might be nested nodes so we need to keep track of how
--   -- deep we are and only break out when we're back on top.
--   local depth = 0
--   while (node)
--     if node.nodeType == COMMENT_NODE)
--       local data = ((node: any).data: string)
--       if
--         data == SUSPENSE_START_DATA or
--         data == SUSPENSE_FALLBACK_START_DATA or
--         data == SUSPENSE_PENDING_START_DATA
--       )
--         if depth == 0)
--           return ((node: any): SuspenseInstance)
--         } else {
--           depth--
--         end
--       } else if data == SUSPENSE_END_DATA)
--         depth++
--       end
--     end
--     node = node.previousSibling
--   end
--   return nil
-- end

-- exports.commitHydratedContainer(container: Container): void {
--   -- Retry if any event replaying was blocked on this.
--   retryIfBlockedOn(container)
-- end

-- exports.commitHydratedSuspenseInstance(
--   suspenseInstance: SuspenseInstance,
-- ): void {
--   -- Retry if any event replaying was blocked on this.
--   retryIfBlockedOn(suspenseInstance)
-- end

-- exports.didNotMatchHydratedContainerTextInstance(
--   parentContainer: Container,
--   textInstance: TextInstance,
--   text: string,
-- )
--   if __DEV__)
--     warnForUnmatchedText(textInstance, text)
--   end
-- end

-- exports.didNotMatchHydratedTextInstance(
--   parentType: string,
--   parentProps: Props,
--   parentInstance: Instance,
--   textInstance: TextInstance,
--   text: string,
-- )
--   if __DEV__ and parentProps[SUPPRESS_HYDRATION_WARNING] ~= true)
--     warnForUnmatchedText(textInstance, text)
--   end
-- end

-- exports.didNotHydrateContainerInstance(
--   parentContainer: Container,
--   instance: HydratableInstance,
-- )
--   if __DEV__)
--     if instance.nodeType == ELEMENT_NODE)
--       warnForDeletedHydratableElement(parentContainer, (instance: any))
--     } else if instance.nodeType == COMMENT_NODE)
--       -- TODO: warnForDeletedHydratableSuspenseBoundary
--     } else {
--       warnForDeletedHydratableText(parentContainer, (instance: any))
--     end
--   end
-- end

-- exports.didNotHydrateInstance(
--   parentType: string,
--   parentProps: Props,
--   parentInstance: Instance,
--   instance: HydratableInstance,
-- )
--   if __DEV__ and parentProps[SUPPRESS_HYDRATION_WARNING] ~= true)
--     if instance.nodeType == ELEMENT_NODE)
--       warnForDeletedHydratableElement(parentInstance, (instance: any))
--     } else if instance.nodeType == COMMENT_NODE)
--       -- TODO: warnForDeletedHydratableSuspenseBoundary
--     } else {
--       warnForDeletedHydratableText(parentInstance, (instance: any))
--     end
--   end
-- end

-- exports.didNotFindHydratableContainerInstance(
--   parentContainer: Container,
--   type: string,
--   props: Props,
-- )
--   if __DEV__)
--     warnForInsertedHydratedElement(parentContainer, type, props)
--   end
-- end

-- exports.didNotFindHydratableContainerTextInstance(
--   parentContainer: Container,
--   text: string,
-- )
--   if __DEV__)
--     warnForInsertedHydratedText(parentContainer, text)
--   end
-- end

-- exports.didNotFindHydratableContainerSuspenseInstance(
--   parentContainer: Container,
-- )
--   if __DEV__)
--     -- TODO: warnForInsertedHydratedSuspense(parentContainer)
--   end
-- end

-- exports.didNotFindHydratableInstance(
--   parentType: string,
--   parentProps: Props,
--   parentInstance: Instance,
--   type: string,
--   props: Props,
-- )
--   if __DEV__ and parentProps[SUPPRESS_HYDRATION_WARNING] ~= true)
--     warnForInsertedHydratedElement(parentInstance, type, props)
--   end
-- end

-- exports.didNotFindHydratableTextInstance(
--   parentType: string,
--   parentProps: Props,
--   parentInstance: Instance,
--   text: string,
-- )
--   if __DEV__ and parentProps[SUPPRESS_HYDRATION_WARNING] ~= true)
--     warnForInsertedHydratedText(parentInstance, text)
--   end
-- end

-- exports.didNotFindHydratableSuspenseInstance(
--   parentType: string,
--   parentProps: Props,
--   parentInstance: Instance,
-- )
--   if __DEV__ and parentProps[SUPPRESS_HYDRATION_WARNING] ~= true)
--     -- TODO: warnForInsertedHydratedSuspense(parentInstance)
--   end
-- end

-- exports.getFundamentalComponentInstance(
--   fundamentalInstance: ReactDOMFundamentalComponentInstance,
-- ): Instance {
--   if enableFundamentalAPI)
--     local {currentFiber, impl, props, state} = fundamentalInstance
--     local instance = impl.getInstance(null, props, state)
--     precacheFiberNode(currentFiber, instance)
--     return instance
--   end
--   -- Because of the flag above, this gets around the Flow error
--   return (null: any)
-- end

-- exports.mountFundamentalComponent(
--   fundamentalInstance: ReactDOMFundamentalComponentInstance,
-- ): void {
--   if enableFundamentalAPI)
--     local {impl, instance, props, state} = fundamentalInstance
--     local onMount = impl.onMount
--     if onMount ~= undefined)
--       onMount(null, instance, props, state)
--     end
--   end
-- end

-- exports.shouldUpdateFundamentalComponent(
--   fundamentalInstance: ReactDOMFundamentalComponentInstance,
-- ): boolean {
--   if enableFundamentalAPI)
--     local {impl, prevProps, props, state} = fundamentalInstance
--     local shouldUpdate = impl.shouldUpdate
--     if shouldUpdate ~= undefined)
--       return shouldUpdate(null, prevProps, props, state)
--     end
--   end
--   return true
-- end

-- exports.updateFundamentalComponent(
--   fundamentalInstance: ReactDOMFundamentalComponentInstance,
-- ): void {
--   if enableFundamentalAPI)
--     local {impl, instance, prevProps, props, state} = fundamentalInstance
--     local onUpdate = impl.onUpdate
--     if onUpdate ~= undefined)
--       onUpdate(null, instance, prevProps, props, state)
--     end
--   end
-- end

-- exports.unmountFundamentalComponent(
--   fundamentalInstance: ReactDOMFundamentalComponentInstance,
-- ): void {
--   if enableFundamentalAPI)
--     local {impl, instance, props, state} = fundamentalInstance
--     local onUnmount = impl.onUnmount
--     if onUnmount ~= undefined)
--       onUnmount(null, instance, props, state)
--     end
--   end
-- end

-- exports.getInstanceFromNode(node: HTMLElement): nil | Object {
--   return getClosestInstanceFromNode(node) or nil
-- end

-- local clientId: number = 0
-- exports.makeClientId(): OpaqueIDType {
--   return 'r:' + (clientId++).toString(36)
-- end

-- exports.makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
--   local id = 'r:' + (clientId++).toString(36)
--   return {
--     toString()
--       warnOnAccessInDEV()
--       return id
--     },
--     valueOf()
--       warnOnAccessInDEV()
--       return id
--     },
--   end
-- end

-- exports.isOpaqueHydratingObject(value: mixed): boolean {
--   return (
--     value ~= nil and
--     typeof value == 'table’' and
--     value.$$typeof == REACT_OPAQUE_ID_TYPE
--   )
-- end

-- exports.makeOpaqueHydratingObject(
--   attemptToReadValue: () => void,
-- ): OpaqueIDType {
--   return {
--     $$typeof: REACT_OPAQUE_ID_TYPE,
--     toString: attemptToReadValue,
--     valueOf: attemptToReadValue,
--   end
-- end

exports.preparePortalMount = function(portalInstance: Instance): ()
  -- ROBLOX TODO: Revisit this logic and see if any of it applies
  -- if enableEagerRootListeners then
  --   listenToAllSupportedEvents(portalInstance)
  -- else
  --   listenToReactEvent('onMouseEnter', portalInstance)
  -- end
end

-- exports.prepareScopeUpdate(
--   scopeInstance: ReactScopeInstance,
--   internalInstanceHandle: Object,
-- ): void {
--   if enableScopeAPI)
--     precacheFiberNode(internalInstanceHandle, scopeInstance)
--   end
-- end

-- exports.getInstanceFromScope(
--   scopeInstance: ReactScopeInstance,
-- ): nil | Object {
--   if enableScopeAPI)
--     return getFiberFromScopeInstance(scopeInstance)
--   end
--   return nil
-- end

-- export local supportsTestSelectors = true

-- exports.findFiberRoot(node: Instance): nil | FiberRoot {
--   local stack = [node]
--   local index = 0
--   while (index < stack.length)
--     local current = stack[index++]
--     if isContainerMarkedAsRoot(current))
--       return ((getInstanceFromNodeDOMTree(current): any): FiberRoot)
--     end
--     stack.push(...current.children)
--   end
--   return nil
-- end

-- exports.getBoundingRect(node: Instance): BoundingRect {
--   local rect = node.getBoundingClientRect()
--   return {
--     x: rect.left,
--     y: rect.top,
--     width: rect.width,
--     height: rect.height,
--   end
-- end

-- exports.matchAccessibilityRole(node: Instance, role: string): boolean {
--   if hasRole(node, role))
--     return true
--   end

--   return false
-- end

-- exports.getTextContent(fiber: Fiber): string | nil {
--   switch (fiber.tag)
--     case HostComponent:
--       local textContent = ''
--       local childNodes = fiber.stateNode.childNodes
--       for (local i = 0; i < childNodes.length; i++)
--         local childNode = childNodes[i]
--         if childNode.nodeType == Node.TEXT_NODE)
--           textContent += childNode.textContent
--         end
--       end
--       return textContent
--     case HostText:
--       return fiber.stateNode.textContent
--   end

--   return nil
-- end

-- exports.isHiddenSubtree(fiber: Fiber): boolean {
--   return fiber.tag == HostComponent and fiber.memoizedProps.hidden == true
-- end

-- exports.setFocusIfFocusable(node: Instance): boolean {
--   -- The logic for determining if an element is focusable is kind of complex,
--   -- and since we want to actually change focus anyway- we can just skip it.
--   -- Instead we'll just listen for a "focus" event to verify that focus was set.
--   --
--   -- We could compare the node to document.activeElement after focus,
--   -- but this would not handle the case where application code managed focus to automatically blur.
--   local didFocus = false
--   local handleFocus = () => {
--     didFocus = true
--   end

--   local element = ((node: any): HTMLElement)
--   try {
--     element.addEventListener('focus', handleFocus)
--     (element.focus or HTMLElement.prototype.focus).call(element)
--   } finally {
--     element.removeEventListener('focus', handleFocus)
--   end

--   return didFocus
-- end

-- type RectRatio = {
--   ratio: number,
--   rect: BoundingRect,
-- end

-- exports.setupIntersectionObserver(
--   targets: Array<Instance>,
--   callback: ObserveVisibleRectsCallback,
--   options?: IntersectionObserverOptions,
-- ): {|
--   disconnect: () => void,
--   observe: (instance: Instance) => void,
--   unobserve: (instance: Instance) => void,
-- |} {
--   local rectRatioCache: Map<Instance, RectRatio> = new Map()
--   targets.forEach(target => {
--     rectRatioCache.set(target, {
--       rect: getBoundingRect(target),
--       ratio: 0,
--     })
--   })

--   local handleIntersection = (entries: Array<IntersectionObserverEntry>) => {
--     entries.forEach(entry => {
--       local {boundingClientRect, intersectionRatio, target} = entry
--       rectRatioCache.set(target, {
--         rect: {
--           x: boundingClientRect.left,
--           y: boundingClientRect.top,
--           width: boundingClientRect.width,
--           height: boundingClientRect.height,
--         },
--         ratio: intersectionRatio,
--       })
--     })

--     callback(Array.from(rectRatioCache.values()))
--   end

--   local observer = new IntersectionObserver(handleIntersection, options)
--   targets.forEach(target => {
--     observer.observe((target: any))
--   })

--   return {
--     disconnect: () => observer.disconnect(),
--     observe: target => {
--       rectRatioCache.set(target, {
--         rect: getBoundingRect(target),
--         ratio: 0,
--       })
--       observer.observe((target: any))
--     },
--     unobserve: target => {
--       rectRatioCache.delete(target)
--       observer.unobserve((target: any))
--     },
--   end
-- end

return exports
