local module = {}

--[[
this feature allows get object without making some created callback and local var
but, it can't allows mulit id and object
this feature should be upgraded
]]
local wrap   = coroutine.wrap
local insert = table.insert
local remove = table.remove
local gmatch = string.gmatch
local gsub   = string.gsub
local match  = string.match
local find  = table.find

if not find then
	find = function (table,item)
		for i,v in pairs(table) do
			if v == item then return i end
		end
		return nil
	end
end

local function catch(...)
	local passed,err = pcall(...)
	if not passed then
		warn ("[QUAD] Error occured while operating async task\n" .. tostring(err))
	end
end

local week = {__mode = "v"}

---@param shared quad_export
---@return quad_module_store
function module.init(shared)
	local warn = shared.warn
	---@class quad_module_store
	local new = {__type = "quad_module_store"}
	local items = {}
	new.Items = items

	-- id space (array of object)
	local objectListClass = {__type = "quad_objectlist"}
	function objectListClass:EachAsync(func)
		local index = 1
		for i,v in pairs(self) do
			wrap(catch)(func,v,index)
			index = index + 1
		end
	end
	function objectListClass:Each(func)
		local index = 1
		for _,v in pairs(self) do
			local ret = func(v,index)
			index = index + 1
			if ret then
				break
			end
		end
	end
	function objectListClass:Remove(indexOrItem)
		if self.__locked then
			error("This objectList is locked, maybe used Store.GetObjects('a,b')? AdvancedObjectQuery does not support :Remove")
		end
		local thisType = type(indexOrItem)
		if thisType == "number" then
			return remove(self,indexOrItem),indexOrItem
		else
			for i,v in pairs(self) do
				if v == indexOrItem then
					return remove(self,i),i
				end
			end
		end
	end
	function objectListClass:IsEmpty()
		if next(self) then return true end
		return false
	end
	function objectListClass.__new()
		return setmetatable({},objectListClass)
	end
	function objectListClass:__newIndex(key,value) -- props setter
		self:Each(function (this)
			this[key] = value
		end)
	end
	objectListClass.__mode = "v" -- week link for gc
	objectListClass.__index = objectListClass

	-- get object array with id (objSpace)
	function new.GetObjects(ids)
		if match(ids,"[,&]") then
			local list = objectListClass.__new()
			objectListClass.__locked = true
			local checked = {}
			for query in gmatch(ids,"[^,]+") do -- split by ,
				if match(query,"&") then
					-- multi ids (and)

					-- make list of combined ids
					local queryIds = {}
					for id in gmatch(query,"[^&]+") do
						id = gsub(gsub(id,"^ +","")," +$","")
						insert(queryIds,id)
					end

					-- loop as first list
					local targetList = queryIds[1] and items[remove(queryIds,1)]
					if targetList then
						for _,item in pairs(targetList) do
							if not checked[item] then
								-- check item has all of ids from queryIds
								local ok = true
								for _,id in ipairs(queryIds) do
									local checkList = items[id]
									if (not checkList) or (not find(checkList,item)) then
										ok = false
										break
									end
								end

								-- insert in return list
								if ok then
									insert(list,item)
									-- prevent multi insert
									checked[item] = true
								end
							end
						end
					end
				else
					query = gsub(gsub(query,"^ +","")," +$","")
					local idItem = items[query]
					if idItem then
						for _,item in pairs(idItem) do
							if not checked[item] then
								insert(list,item)
								checked[item] = true -- prevent multi insert
							end
						end
					end
				end
			end
			return list
		else
			local list = items[ids]
			if list then return list end
			list = objectListClass.__new()
			items[ids] = list
			return list
		end
	end
	-- get first object with id (not array)
	function new.GetObject(id)
		local item = items[id]
		return item and item[next(item)]
	end
	-- adding object with id
	function new.AddObject(ids,object)
		-- prevent gc
		if type(object) == "userdata" then
			object:GetPropertyChangedSignal("ClassName"):Connect(function()
				return object
			end)
		end
		for id in gmatch(ids,"[^,]+") do -- split by ,
			-- remove trailing, heading spaces
			id = gsub(gsub(id,"^ +","")," +$","")
			local array = items[id]
			if not array then
				array = objectListClass.__new()
				items[id] = array
			end
			if not find(array,object) then
				insert(array, object)
			end
		end
	end

	local registerClass = {
		__type = "quad_register";
		Register = function (s,efunc)
			local self = s.store
			local events = self.__evt
			for key in gmatch(s.key,"[^,]+") do
				key = gsub(gsub(key,"^ +","")," +$","")
				local event = events[key]
				if not event then
					event = setmetatable({},week)
					events[key] = event
				end
				insert(event,efunc)
			end
			return efunc
		end;
		With = function (s,wfunc)
			-- s.wfunc = wfunc
			-- return s
			return setmetatable({wfunc = wfunc},{__index = s})
		end;
		Default = function (s,dvalue)
			-- s.dvalue = dvalue
			-- return s
			return setmetatable({dvalue = dvalue},{__index = s})
		end;
		Tween = function (s,tvalue)
			-- s.tvalue = tvalue
			-- return s
			return setmetatable({tvalue = tvalue},{__index = s})
		end;
		---@deprecated
		From = function (s,fvalue)
			warn "[QUAD] register:from() is deprecated. Use register:add(t:table|function) instead"
			-- s.fvalue = fvalue
			-- return s
			return setmetatable({fvalue = fvalue},{__index = s})
		end;
		Add = function (s,avalue)
			-- s.avalue = avalue
			-- return s
			return setmetatable({avalue = avalue},{__index = s})
		end;
		-- return init data
		CalcWithDefault = function (s,withItem)
			local with = s.wfunc
			local tstore = s.store
			local rawKey = s.key
			local tween = s.tvalue or tstore.__tweens[rawKey]
			local from = s.fvalue
			local add = s.avalue
			local set = tstore[rawKey]
			if set ~= nil or (match(rawKey,",") and with) then
				if add then
					set = set + add
				end
				if from then
					set = from[set]
				end
				if with then
					local typeWith = type(with)
					if typeWith == "table" then
						set = with[set]
					elseif typeWith == "function" then
						set = with(tstore,set,rawKey,withItem)
					else
						error(("Unsupported type :With(). got %s"):format(typeWith))
					end
				end
				return set,tween
			else
				local dset = s.dvalue
				if dset ~= nil then
					return dset,tween
				end
				warn "[Quad] no default value found."
			end
		end;
		CalcWithNewValue = function(s,withItem,updatedValue,updatedKey)
			local with = s.wfunc
			local tstore = s.store
			local rawKey = s.key
			local tween = s.tvalue or tstore.__tweens[rawKey]
			local from = s.fvalue
			local add = s.avalue
			if add then
				updatedValue = updatedValue + add
			end
			if from then
				updatedValue = from[updatedValue]
			end
			if with then
				updatedValue = with(tstore,updatedValue,updatedKey,withItem)
			end
			return updatedValue,tween
		end
	}
	registerClass.__index = registerClass

	-- bindable store object
	local store = {__type = "quad_store"}
	local storeIdSpace = {}
	function store:__index(key)
		local this = self.__self[key]
		if this ~= nil then
			return this
		end
		return store[key]
	end
	function store:__newindex(key,value)
		-- if got register, just copy data to self and connect
		if type(value) == "table" and value.__type == "quad_register" then
			-- warn "[Quad] adding register value on store is only allowed when init store. set value request was ignored"

			local selfValues = self.__self
			local selfTweens = self.__tweens

			-- fetch data from origin
			do
				local tstore = value.store
				for tkey in gmatch(value.key,"^[,]") do
					if not selfValues[tkey] then
						selfValues[tkey] = tstore[tkey]
					end
				end
			end

			-- calc value
			do
				local setValue,tween = value:CalcWithDefault(self)
				selfValues[key] = setValue
				selfTweens[key] = tween
			end

			-- make event connection
			value:Register(function (_,newValue,eventKey)
				-- !HOLD IT SELF TO PREVENT THIS REGISTER BEGIN REMOVED FROM MEMORY
				local setValue = value:CalcWithNewValue(self,newValue,eventKey)
				self[key] = setValue
			end)

			return
		end
		self.__self[key] = value
		local event = self.__evt[key]
		if event then
			for _,v in pairs(event) do -- NO ipairs here
				wrap(catch)(v,store,value,key)
			end
		end
	end
	-- init bindings (reflect all changes into class or other store, just wrapping it)
	function new.__initStoreRegisterBinding(self,withItem)
		local selfTweens = self.__tweens
		local selfValues = self.__self
		local selfKeep = self.__keep
		for key,item in pairs(selfValues) do
			if type(item) == "table" and item.__type == "quad_register" then
				-- fetch data from origin
				do
					local tstore = item.store
					for tkey in item.key:gmatch("^[,]") do
						if not selfValues[tkey] then
							selfValues[tkey] = tstore[tkey]
						end
					end
				end

				-- calc value
				do
					local setValue,tween = item:CalcWithDefault(withItem)
					selfValues[key] = setValue
					selfTweens[key] = tween
				end

				-- make event connection
				local function regFn(_,newValue,eventKey)
					-- !HOLD IT SELF TO PREVENT THIS REGISTER BEGIN REMOVED FROM MEMORY
					local setValue = item:CalcWithNewValue(withItem,newValue,eventKey)
					self[key] = setValue
				end
				item:Register(regFn)
				insert(selfKeep,item)
				insert(selfKeep,regFn)
			end
		end
	end
	-- create register
	function store:__call(key,func)
		local last = self.__self[key]
		if last and type(last) == "table" and getmetatable(last) == registerClass then
			return last
		end
		local register = self.__reg[key]
		if not register then
			register = setmetatable({
				key = key;
				store = self;
			},registerClass)
			self.__reg[key] = register
		end

		if func then
			register.Register(func)
		end
		return register
	end
	function store:Default(key,value)
		if self[key] == nil then
			self[key] = value
			return
		end
	end
	function new.New(self,id)
		local this = setmetatable(
			{
				__self = self or {}, -- real value storage
				__evt = {}, -- changed event handler functions (dict of array)
				__reg = setmetatable({},week), -- save registers
				__tweens = {}, -- tweens (if inited with register, save tween and use as default tween data)
				__keep = {} -- store data which should not be destoryed
			},store
		)
		if id then -- save in id space
			storeIdSpace[id] = this
		end
		return this
	end
	local storeNew = new.New
	function new.GetStore(id)
		return storeIdSpace[id] or storeNew({},id)
	end
	local getStore = new.GetStore

	setmetatable(new,{
		__call = function (self,...)
			return getStore(...)
		end;
	})

	return new
end

return module
