local sharedstate={}
local logging=require((...):gsub("(.)$", "%1.") .. 'logging')

-- function names
local is_initialized, callback_value, set_value,
get_value, resolve_key, resolve_index, get_table

--schema: similar to state_table, nested table only contains value
local state_queue={}

local function ternary(condition, val_if_true, val_if_false)
	if condition then return val_if_true else return val_if_false end
end

-- we're protecting internal variables here, i'm *that* scared of writing bad
-- code again.
-- this section is purely for making sure the tables are in a consistent state
-- maybe this should be a separate library for general state tracking, then i
-- could use it in other parts of my code for local state tracking
-- (aka how to unnecessarily increase complexity, if i want to do something i
-- can just make a function for it)
do
	-- schema: {key: {value: val, old_value: val, callback: function,
	-- index: integer}}
	-- wrapping the value in a table should allow the user to store nil
	local state_table={}

	-- schema: {1: key_name_for_val_1, ...}
	-- this is to save bandwidth in pings
	local state_map={}

	--- Check if initialized
	---Check if a key is initialized
	---@param key string key
	---@return true if initialized, else false
	function is_initialized(key)
		logging.trace('is_initialized', key)
		return state_table[key] ~= nil
	end


	--- Internal; ensure key is initialized
	---Properly initialize a key in all tables.
	---already initialized
	---@param key string key name
	---@return boolean if key has been initialized
	local function init_key(key)
		logging.trace('init_key', key)
		if key == nil then
			local errormsg="sharedstate: A key name is required"
			error(errormsg)
		end
		
		if is_initialized(key) then
			return false
		end
		-- lua is fucking asinine and starts tables at 1
		local index=#state_map+1
		state_map[index]=key

		local tbl={}
		tbl["index"]=index
		state_table[key]=tbl
		return true
	end

	--- Run callback function
	---Run the callback function of the given key
	---@param key string Key
	function callback_value(key)
		logging.trace("callback_value", key)
		local new_value=state_table[key]["value"]
		local old_value=state_table[key]["old_value"]
		if type(state_table[key]["callback"]) == "function"
			and old_value ~= new_value then
			state_table[key]["callback"](new_value, old_value)
		end
	end

	--- Set value and run callback
	-- Sets a value in the state table, initializing it if neede, and runs
	-- the callback if it has been previously set. This function should
	-- only be called within ping receiver functions
	---@param key string key
	---@param value any value
	---@param callback? function callback function
	function set_value(key, value, callback)
		logging.debug("sharedstate: key " .. tostring(key) .. " set to " .. tostring(value))
		local initialized=init_key(key)
		local entry=state_table[key]
		if initialized then
			entry["value"]=value
			entry["old_value"]=value
			entry["callback"]=callback

		else
			entry["value"]=ternary(value ~= nil, value, entry["value"])
			callback_value(key)
			entry["callback"]=callback or entry["callback"]
			entry["old_value"]=value
		end
	end

	--- Get value
	---Get a value from the state table
	---@param key string key
	---@return any value from state table if set, else nil
	function get_value(key)
		logging.trace("get_value", key)
		if not is_initialized(key) then return nil end
		return state_table[key]["value"]
	end

	--- Resolve key
	---Resolve key of given index
	---@param index integer index
	---@return any key
	function resolve_key(index)
		logging.trace("resolve_key", index)
		return state_map[index]
	end

	--- Resolve index
	---Resolve index of given key
	---@param key string key
	---@return integer index
	function resolve_index(key)
		logging.trace("resolve_index", key)
		return state_table[key]["index"]
	end

	--- Get table
	--Internal; Gets a copy of the shared state table with internal data
	--removed
	function get_table()
		local t={}
		for k, v in pairs(state_table) do
			t[k]={ ["value"]=v["value"] }
		end
		return t
	end
end

---Add an item to the shared state store.
---Adds an item to the shared state store, as well as initializes it with a
---value. This does not send a ping and should only be used during avatar
---initialization.
---@param key string key name
---@param value any initial value
---@param callback? function Callback function to run on value change
function sharedstate.init(key, value, callback)
	logging.trace("sharedstate.init", key, value, callback)
	set_value(key, value, callback)
end
---@deprecated use sharedstate.init instead
sharedstate.add=sharedstate.init

--- Internal; transfer value over network
---Ping used to transfer a value over the network, should not be called
---directly
---@param index integer Index of key
---@param value any New value
function pings.sharedstate_recv(index, value)
	logging.trace("pings.sharedstate_recv", index, value)
	set_value(resolve_key(index), value)
end

--- Internal; transfer value over network using key names
---Ping used to transfer a value over the network, should not be called
---directly
---@param key string Key
---@param value any New value
function pings.sharedstate_recv_named(key, value)
	logging.trace("pings.sharedstate_recv_named", key, value)
	set_value(key, value)
end

--- Internal; unpack a table of state into the local state store
---@param tbl table Table to unpack
function pings.sharedstate_recv_table(tbl)
	logging.debug("Table sync received")
	for k, v in pairs(tbl) do
		set_value(k, v["value"])
	end
end

--- Set shared value
---Sets a shared value. This sends a ping to transfer it over the network.
---@param key string key name
---@param value any value
function sharedstate.set(key, value)
	logging.trace("sharedstate.set", key, value)
	if not is_initialized(key) then
		-- this makes the error traceback less confusing
		local errormsg="sharedstate: Key " .. key .. " has not been initialized."
		error(errormsg)
	end
	-- don't bother sending unchanged values
	if value ~= get_value(key) then
		-- pings.sharedstate_recv(resolve_index(key), value)
		pings.sharedstate_recv_named(key, value)
	end
end

---Queue entries for sending
--Queues entries for sending over a single ping, as to prevent being rate
--limited. Values will not be accessible until sharedstate.commit() is run.
---@param key string key
---@param value any value
function sharedstate.queue(key, value)
	local t={
		["value"] = value
	}
	state_queue[key]=t
end

---Commit queued entries
--Commit queued table entries and send in one ping.
function sharedstate.commit()
	--schema: similar to state_table, nested table only contains value
	-- local send_queue={}
	-- -- this is intentional values in state_queued are key names
	-- for _, key in pairs(state_queued) do
	-- 	local t={
	-- 		["value"]=get_value(key)
	-- 	}
	-- 	send_queue[key]=t
	-- end

	pings.sharedstate_recv_table(state_queue)
	sharedstate.clear()
end

--- Clear a value from the shared state queue
--Clears a value from the shared state queue. If no key is specified, clears
--all queued values.
---@param key? string Key to clear
function sharedstate.clear(key)
	if key ~= nil then
		state_queue[key] = nil
	else
		state_queue={}
	end
end

--- Sync full state
--Syncs the full shared state table. This should be used sparsely
function sharedstate.sync()
	if host:isHost() then
		ping.sharedstate_recv_table(get_table())
	end
end

-- this can be copied directly from the internal functions as it is read only
sharedstate.get=get_value

return sharedstate