--!strict
-- upstream: https://github.com/facebook/react/blob/8e5adfbd7e605bda9c5e96c10e015b3dc0df688e/packages/react-dom/src/client/ReactDOMComponent.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 LuauPolyfill = require(Packages.LuauPolyfill)
local Object = LuauPolyfill.Object

local RobloxComponentProps = require(script.Parent.roblox.RobloxComponentProps)

local ReactRobloxHostTypes = require(script.Parent["ReactRobloxHostTypes.roblox"])
type HostInstance = ReactRobloxHostTypes.HostInstance;

-- deviation: Essentially a placeholder for dom-specific logic, taking the place
-- of ReactDOMComponent. Most of the logic will differ pretty dramatically

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

local exports: { [string]: any } = {}

exports.setInitialProperties = RobloxComponentProps.setInitialProperties

-- Calculate the diff between the two objects.
local function diffProperties(
  domElement: HostInstance,
  tag: string,
  lastRawProps: Object,
  nextRawProps: Object,
  rootContainerElement: HostInstance
): (nil | Array<any>)
  -- if _G.__DEV__ then
  --   validatePropertiesInDevelopment(tag, nextRawProps)
  -- end

  -- ROBLOX FIXME: Type refinement
  -- local updatePayload: nil | Array<any> = nil
  local updatePayload = nil

  local lastProps = lastRawProps
  local nextProps = nextRawProps
  -- local lastProps: Object
  -- local nextProps: Object
  -- switch (tag) {
  --   case 'input':
  --     lastProps = ReactDOMInputGetHostProps(domElement, lastRawProps);
  --     nextProps = ReactDOMInputGetHostProps(domElement, nextRawProps);
  --     updatePayload = [];
  --     break;
  --   case 'option':
  --     lastProps = ReactDOMOptionGetHostProps(domElement, lastRawProps);
  --     nextProps = ReactDOMOptionGetHostProps(domElement, nextRawProps);
  --     updatePayload = [];
  --     break;
  --   case 'select':
  --     lastProps = ReactDOMSelectGetHostProps(domElement, lastRawProps);
  --     nextProps = ReactDOMSelectGetHostProps(domElement, nextRawProps);
  --     updatePayload = [];
  --     break;
  --   case 'textarea':
  --     lastProps = ReactDOMTextareaGetHostProps(domElement, lastRawProps);
  --     nextProps = ReactDOMTextareaGetHostProps(domElement, nextRawProps);
  --     updatePayload = [];
  --     break;
  --   default:
  --     lastProps = lastRawProps;
  --     nextProps = nextRawProps;
  --     if (
  --       typeof lastProps.onClick !== 'function' &&
  --       typeof nextProps.onClick === 'function'
  --     ) {
  --       // TODO: This cast may not be sound for SVG, MathML or custom elements.
  --       trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
  --     }
  --     break;
  -- }

  -- assertValidProps(tag, nextProps);

  -- let propKey;
  -- let styleName;
  -- let styleUpdates = null;
  for propKey, _ in lastProps do
    if nextProps[propKey] ~= nil then
      continue
    end
    -- if (propKey === STYLE) {
    --   const lastStyle = lastProps[propKey];
    --   for (styleName in lastStyle) {
    --     if (lastStyle.hasOwnProperty(styleName)) {
    --       if (!styleUpdates) {
    --         styleUpdates = {};
    --       }
    --       styleUpdates[styleName] = '';
    --     }
    --   }
    -- } else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) {
    --   // Noop. This is handled by the clear text mechanism.
    -- } else if (
    --   propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
    --   propKey === SUPPRESS_HYDRATION_WARNING
    -- ) {
    --   // Noop
    -- } else if (propKey === AUTOFOCUS) {
    --   // Noop. It doesn't work on updates anyway.
    -- } else if (registrationNameDependencies.hasOwnProperty(propKey)) {
    --   // This is a special case. If any listener updates we need to ensure
    --   // that the "current" fiber pointer gets updated so we need a commit
    --   // to update this element.
    --   if (!updatePayload) {
    --     updatePayload = [];
    --   }
    -- } else {
      -- For all other deleted properties we add it to the queue. We use
      -- the allowed property list in the commit phase instead.
      -- ROBLOX performance: prealloc table size 2 for these 2 items at least
      updatePayload = updatePayload or table.create(2)
      table.insert(updatePayload, propKey)
      table.insert(updatePayload, Object.None)
    -- }
  end
  for propKey, nextProp in nextProps do
    local lastProp = if lastProps ~= nil then lastProps[propKey] else nil
    if nextProp == lastProp then
      continue
    end
    -- if (propKey === STYLE) {
    --   if (__DEV__) {
    --     if (nextProp) {
    --       // Freeze the next style object so that we can assume it won't be
    --       // mutated. We have already warned for this in the past.
    --       Object.freeze(nextProp);
    --     }
    --   }
    --   if (lastProp) {
    --     // Unset styles on `lastProp` but not on `nextProp`.
    --     for (styleName in lastProp) {
    --       if (
    --         lastProp.hasOwnProperty(styleName) &&
    --         (!nextProp || !nextProp.hasOwnProperty(styleName))
    --       ) {
    --         if (!styleUpdates) {
    --           styleUpdates = {};
    --         }
    --         styleUpdates[styleName] = '';
    --       }
    --     }
    --     // Update styles that changed since `lastProp`.
    --     for (styleName in nextProp) {
    --       if (
    --         nextProp.hasOwnProperty(styleName) &&
    --         lastProp[styleName] !== nextProp[styleName]
    --       ) {
    --         if (!styleUpdates) {
    --           styleUpdates = {};
    --         }
    --         styleUpdates[styleName] = nextProp[styleName];
    --       }
    --     }
    --   } else {
    --     // Relies on `updateStylesByID` not mutating `styleUpdates`.
    --     if (!styleUpdates) {
    --       if (!updatePayload) {
    --         updatePayload = [];
    --       }
    --       updatePayload.push(propKey, styleUpdates);
    --     }
    --     styleUpdates = nextProp;
    --   }
    -- } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
    --   const nextHtml = nextProp ? nextProp[HTML] : undefined;
    --   const lastHtml = lastProp ? lastProp[HTML] : undefined;
    --   if (nextHtml != null) {
    --     if (lastHtml !== nextHtml) {
    --       (updatePayload = updatePayload || []).push(propKey, nextHtml);
    --     }
    --   } else {
    --     // TODO: It might be too late to clear this if we have children
    --     // inserted already.
    --   }
    -- } else if (propKey === CHILDREN) {
    --   if (typeof nextProp === 'string' || typeof nextProp === 'number') {
    --     (updatePayload = updatePayload || []).push(propKey, '' + nextProp);
    --   }
    -- } else if (
    --   propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
    --   propKey === SUPPRESS_HYDRATION_WARNING
    -- ) {
    --   // Noop
    -- } else if (registrationNameDependencies.hasOwnProperty(propKey)) {
    --   if (nextProp != null) {
    --     // We eagerly listen to this even though we haven't committed yet.
    --     if (__DEV__ && typeof nextProp !== 'function') {
    --       warnForInvalidEventListener(propKey, nextProp);
    --     }
    --     if (!enableEagerRootListeners) {
    --       ensureListeningTo(rootContainerElement, propKey, domElement);
    --     } else if (propKey === 'onScroll') {
    --       listenToNonDelegatedEvent('scroll', domElement);
    --     }
    --   }
    --   if (!updatePayload && lastProp !== nextProp) {
    --     // This is a special case. If any listener updates we need to ensure
    --     // that the "current" props pointer gets updated so we need a commit
    --     // to update this element.
    --     updatePayload = [];
    --   }
    -- } else if (
    --   typeof nextProp === 'object' &&
    --   nextProp !== null &&
    --   nextProp.$$typeof === REACT_OPAQUE_ID_TYPE
    -- ) {
    --   // If we encounter useOpaqueReference's opaque object, this means we are hydrating.
    --   // In this case, call the opaque object's toString function which generates a new client
    --   // ID so client and server IDs match and throws to rerender.
    --   nextProp.toString();
    -- } else {
      -- For any other property we always add it to the queue and then we
      -- filter it out using the allowed property list during the commit.
      -- ROBLOX performance: prealloc table size 2 for these 2 items at least
      -- ROBLOX performance TODO: don't create a table here, return multiple values!
      updatePayload = updatePayload or table.create(2)
      table.insert(updatePayload, propKey)
      table.insert(updatePayload, nextProp)
    -- }
  end
  -- if (styleUpdates) {
  --   if (__DEV__) {
  --     validateShorthandPropertyCollisionInDev(styleUpdates, nextProps[STYLE]);
  --   }
  --   (updatePayload = updatePayload || []).push(STYLE, styleUpdates);
  -- }
  return updatePayload
end
exports.diffProperties = diffProperties
exports.updateProperties = RobloxComponentProps.updateProperties
exports.cleanupHostComponent = RobloxComponentProps.cleanupHostComponent

return exports
