local p=require(script.Parent.Parent.FunctionUtils.noop)local G=require(script.Parent.Parent.TableUtils.join)local g=require(script.Parent.Parent.Roact)local t=require(script.Parent.shallowEqual)local a=require(script.Parent.StoreContext)local function m(p,G)return(table.concat(p,"\n")):format(unpack(G or{}))end local function Z(p)return function(g,t,a)if a==nil then a=t.mapStateToProps(p:getState(),g)end local m=G(g,a,t.mappedStoreDispatch)return{mappedStoreState=a;propsForChild=m}end end local function J(G,J)local d=debug.traceback()if G~=nil then if type(G)~="\102\117\110\099\116\105\111\110"then error("\109\097\112\083\116\097\116\101\084\111\080\114\111\112\115 \109\117\115\116 \098\101 \097 \102\117\110\099\116\105\111\110 \111\114 \110\105\108\033",2)end else G=p end local K=type(J)if J~=nil then if K~="\102\117\110\099\116\105\111\110"and K~="\116\097\098\108\101"then error("\109\097\112\068\105\115\112\097\116\099\104\084\111\080\114\111\112\115 \109\117\115\116 \098\101 \097 \102\117\110\099\116\105\111\110\044 \116\097\098\108\101\044 \111\114 \110\105\108\033")end else J=p end return function(p)if p==nil then error(m({"\099\111\110\110\101\099\116 \114\101\116\117\114\110\115 \097 \102\117\110\099\116\105\111\110 \116\104\097\116 \109\117\115\116 \098\101 \112\097\115\115\101\100 \097 \099\111\109\112\111\110\101\110\116\046","\067\104\101\099\107 \116\104\101 \099\111\110\110\101\099\116\105\111\110 \097\116\058","\037\115"},{d}),2)end local S=("\082\111\100\117\120\067\111\110\110\101\099\116\105\111\110\040\037\115\041"):format(tostring(p))local T=g.Component:extend(S)function T.getDerivedStateFromProps(p,G)if G.stateUpdater~=nil then return G.stateUpdater(p.innerProps,G)end end function T.init(t,g)t.store=g.store if t.store==nil then local G=m({"\067\097\110\110\111\116 \105\110\105\116\105\097\108\105\122\101 \082\111\097\099\116-\082\111\100\117\120 \099\111\110\110\101\099\116\105\111\110 \119\105\116\104\111\117\116 \098\101\105\110\103 \097 \100\101\115\099\101\110\100\101\110\116 \111\102 \083\116\111\114\101\080\114\111\118\105\100\101\114\033","\084\114\105\101\100 \116\111 \119\114\097\112 \099\111\109\112\111\110\101\110\116 \037\113";"\077\097\107\101 \115\117\114\101 \116\104\101\114\101 \105\115 \097 \083\116\111\114\101\080\114\111\118\105\100\101\114 \097\098\111\118\101 \116\104\105\115 \099\111\109\112\111\110\101\110\116 \105\110 \116\104\101 \116\114\101\101\046"},{tostring(p)})error(G)end local a=t.store:getState()local d=G local S=d(a,t.props.innerProps)if type(S)=="\102\117\110\099\116\105\111\110"then d=S S=d(a,t.props.innerProps)end if S~=nil and type(S)~="\116\097\098\108\101"then local p=m({"\109\097\112\083\116\097\116\101\084\111\080\114\111\112\115 \109\117\115\116 \101\105\116\104\101\114 \114\101\116\117\114\110 \097 \116\097\098\108\101\044 \111\114 \114\101\116\117\114\110 \097\110\111\116\104\101\114 \102\117\110\099\116\105\111\110 \116\104\097\116 \114\101\116\117\114\110\115 \097 \116\097\098\108\101\046","\073\110\115\116\101\097\100\044 \105\116 \114\101\116\117\114\110\101\100 \037\113\044 \119\104\105\099\104 \105\115 \111\102 \116\121\112\101 \037\115\046"},{tostring(S);typeof(S)})error(p)end local function T(...)return t.store:dispatch(...)end local C if K=="\116\097\098\108\101"then C={}for p,G in J do if type(G)~="\102\117\110\099\116\105\111\110"then error("\109\097\112\068\105\115\112\097\116\099\104\084\111\080\114\111\112\115 \109\117\115\116 \099\111\110\116\097\105\110 \102\117\110\099\116\105\111\110 \118\097\108\117\101\115",2)end C[p]=function(...)T(G(...))end end elseif K=="\102\117\110\099\116\105\111\110"then C=J(T)end local e=Z(t.store)t.state={stateUpdater=e,mapStateToProps=d,mappedStoreDispatch=C,propsForChild=nil}local B=e(t.props.innerProps,t.state,S)for p,G in B do t.state[p]=G end end function T.didMount(p)local G=function(G)p:setState(function(p,g)local a=p.mapStateToProps(G,g.innerProps)if t(a,p.mappedStoreState)then return nil end return p.stateUpdater(g.innerProps,p,a)end)end G(p.store:getState())p.disconnectStoreChangedConnection=p.store.changed:connect(G)end function T.willUnmount(p)local G=p.disconnectStoreChangedConnection if G then p.disconnectStoreChangedConnection=nil G()end end function T.render(G)return g.createElement(p,G.state.propsForChild)end local C=g.Component:extend(S)function C.render(p)return g.createElement(a.Consumer,{render=function(G)return g.createElement(T,{innerProps=p.props;store=G})end})end return C end end return J
local noop = require(script.Parent.Parent.FunctionUtils.noop)
local join = require(script.Parent.Parent.TableUtils.join)
local Roact = require(script.Parent.Parent.Roact)

local shallowEqual = require(script.Parent.shallowEqual)
local StoreContext = require(script.Parent.StoreContext)

--[[
	Formats a multi-line message with printf-style placeholders.
]]
local function formatMessage(lines, parameters)
	return table.concat(lines, "\n"):format(unpack(parameters or {}))
end

--[[
	The stateUpdater accepts props when they update and computes the
	complete set of props that should be passed to the wrapped component.

	Each connected component will have a stateUpdater created for it.

	stateUpdater is put into the component's state in order for
	getDerivedStateFromProps to be able to access it. It is not mutated.
]]
local function makeStateUpdater(store)
	return function(nextProps, prevState, mappedStoreState)
		-- The caller can optionally provide mappedStoreState if it needed that
		-- value beforehand. Doing so is purely an optimization.
		if mappedStoreState == nil then
			mappedStoreState = prevState.mapStateToProps(store:getState(), nextProps)
		end

		local propsForChild = join(nextProps, mappedStoreState, prevState.mappedStoreDispatch)

		return {
			mappedStoreState = mappedStoreState,
			propsForChild = propsForChild,
		}
	end
end

--[[
	mapStateToProps:
		(storeState, props) -> partialProps
		OR
		() -> (storeState, props) -> partialProps
	mapDispatchToProps: (dispatch) -> partialProps
]]
local function connect(mapStateToPropsOrThunk, mapDispatchToProps)
	local connectTrace = debug.traceback()

	if mapStateToPropsOrThunk ~= nil then
		if type(mapStateToPropsOrThunk) ~= "function" then
			error("mapStateToProps must be a function or nil!", 2)
		end
	else
		mapStateToPropsOrThunk = noop
	end

	local mapDispatchType = type(mapDispatchToProps)
	if mapDispatchToProps ~= nil then
		if mapDispatchType ~= "function" and mapDispatchType ~= "table" then
			error("mapDispatchToProps must be a function, table, or nil!")
		end
	else
		mapDispatchToProps = noop
	end

	return function(innerComponent)
		if innerComponent == nil then
			error(
				formatMessage({
					"connect returns a function that must be passed a component.",
					"Check the connection at:",
					"%s",
				}, {
					connectTrace,
				}),
				2
			)
		end

		local componentName = ("RoduxConnection(%s)"):format(tostring(innerComponent))

		local Connection = Roact.Component:extend(componentName)

		function Connection.getDerivedStateFromProps(nextProps, prevState)
			if prevState.stateUpdater ~= nil then
				return prevState.stateUpdater(nextProps.innerProps, prevState)
			end
		end

		function Connection:init(props)
			self.store = props.store

			if self.store == nil then
				local message = formatMessage({
					"Cannot initialize Roact-Rodux connection without being a descendent of StoreProvider!",
					"Tried to wrap component %q",
					"Make sure there is a StoreProvider above this component in the tree.",
				}, {
					tostring(innerComponent),
				})

				error(message)
			end

			local storeState = self.store:getState()

			local mapStateToProps = mapStateToPropsOrThunk
			local mappedStoreState = mapStateToProps(storeState, self.props.innerProps)

			-- mapStateToPropsOrThunk can return a function instead of a state
			-- value. In this variant, we keep that value as mapStateToProps
			-- instead of the original mapStateToProps. This matches react-redux
			-- and enables connectors to keep instance-level state.
			if type(mappedStoreState) == "function" then
				mapStateToProps = mappedStoreState
				mappedStoreState = mapStateToProps(storeState, self.props.innerProps)
			end

			if mappedStoreState ~= nil and type(mappedStoreState) ~= "table" then
				local message = formatMessage({
					"mapStateToProps must either return a table, or return another function that returns a table.",
					"Instead, it returned %q, which is of type %s.",
				}, {
					tostring(mappedStoreState),
					typeof(mappedStoreState),
				})

				error(message)
			end

			local function dispatch(...)
				return self.store:dispatch(...)
			end

			local mappedStoreDispatch
			if mapDispatchType == "table" then
				mappedStoreDispatch = {}

				for key, actionCreator in mapDispatchToProps do
					if type(actionCreator) ~= "function" then
						error("mapDispatchToProps must contain function values", 2)
					end

					mappedStoreDispatch[key] = function(...)
						dispatch(actionCreator(...))
					end
				end
			elseif mapDispatchType == "function" then
				mappedStoreDispatch = mapDispatchToProps(dispatch)
			end

			local stateUpdater = makeStateUpdater(self.store)

			self.state = {
				-- Combines props, mappedStoreDispatch, and the result of
				-- mapStateToProps into propsForChild. Stored in state so that
				-- getDerivedStateFromProps can access it.
				stateUpdater = stateUpdater,

				-- Used by the store changed connection and stateUpdater to
				-- construct propsForChild.
				mapStateToProps = mapStateToProps,

				-- Used by stateUpdater to construct propsForChild.
				mappedStoreDispatch = mappedStoreDispatch,

				-- Passed directly into the component that Connection is
				-- wrapping.
				propsForChild = nil,
			}

			local extraState = stateUpdater(self.props.innerProps, self.state, mappedStoreState)

			for key, value in extraState do
				self.state[key] = value
			end
		end

		function Connection:didMount()
			local updateStateWithStore = function(storeState)
				self:setState(function(prevState, props)
					local mappedStoreState = prevState.mapStateToProps(storeState, props.innerProps)

					-- We run this check here so that we only check shallow
					-- equality with the result of mapStateToProps, and not the
					-- other props that could be passed through the connector.
					if shallowEqual(mappedStoreState, prevState.mappedStoreState) then
						return nil
					end

					return prevState.stateUpdater(props.innerProps, prevState, mappedStoreState)
				end)
			end

			-- Update store state on mount to catch missed state updates between
			-- init and mount. State could be stale otherwise.
			updateStateWithStore(self.store:getState())

			-- Connect state updater to the Rodux store
			self.disconnectStoreChangedConnection = self.store.changed:connect(updateStateWithStore)
		end

		function Connection:willUnmount()
			local disconnectStoreChangedConnection = self.disconnectStoreChangedConnection
			if disconnectStoreChangedConnection then
				self.disconnectStoreChangedConnection = nil
				disconnectStoreChangedConnection()
			end
		end

		function Connection:render()
			return Roact.createElement(innerComponent, self.state.propsForChild)
		end

		local ConnectedComponent = Roact.Component:extend(componentName)

		function ConnectedComponent:render()
			return Roact.createElement(StoreContext.Consumer, {
				render = function(store)
					return Roact.createElement(Connection, {
						innerProps = self.props,
						store = store,
					})
				end,
			})
		end

		return ConnectedComponent
	end
end

return connect
