math.randomseed(os.time())

local module = {}
setmetatable(module, {__index=_G})
setfenv(1, module)


--===== Components =====--

-- Components provide a kind-of-type-safe way of storing values, since they are
-- guaranteed to have a specific set of keys and no other values in them. They also serialize
-- nicely for storage.

Component = {}

function Component.newFactory(name, params, serialize)
	-- create the metatable for this component type
	local metatable = {}
	metatable.__tostring = serialize or Component.serialize -- nice serialization
	metatable.__newindex = function(tbl, index, value)
		if params[index] ~= nil then
			tbl[index] = value
		else
			-- throw an error if we try to set a key that is not in the valid set
			error(string.format("%q is not a valid key for a component of type %q", index, name))
		end
	end

	-- the factory function for creating components
	return function(tbl)
		local tbl = tbl or {}
		local self = { __type=name }

		-- ensure that all keys are present
		for k, v in pairs(params) do
			self[k] = v
		end
		setmetatable(self, metatable)

		-- set the keys from the factory call
		for k, v in pairs(tbl) do
			self[k] = v
		end

		return self
	end
end


function Component.serialize(self)
	local str = "{"
	for k, v in pairs(self) do
		if type(v) == "string" then
			str = str .. string.format("%s=%q,", k, v)
		else
			str = str .. string.format("%s=%s,", k, tostring(v))
		end
	end
	str = string.sub(str, 1, -2) .. "}"
	return str
end


--===== EntityDb =====--


-- EntityDb is a database of entities and their associated components
-- it should be quite efficient to query for all entities with a given component, and reasonably
-- efficient to query for all components of a given entity


EntityDb = {}
EntityDb.__index = EntityDb


function EntityDb.new(_)
	local self = {
		entities = {},
		components = {},
	}
	setmetatable(self, EntityDb)
	return self
end
setmetatable(EntityDb, {__call=EntityDb.new})


-- check if a given entity id is legitimate
function EntityDb.checkIsValid(self, id)
	if not self.entities[id] then
		error(string.format("invalid entity id: %s", tostring(id)))
	end
end


local random = math.random
local function uuid()
	local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
	return string.gsub(template, '[xy]', function (c)
		local v = (c == 'x') and random(0, 0xf) or random(8, 0xb)
		return string.format('%x', v)
	end)
end


-- create a new entity
function EntityDb.createEntity(self, id)
	local id = id or uuid() -- allow inserting entities at preset ids for loading
	self.entities[id] = true
	return id
end


-- add a component to an entity
function EntityDb.addComponent(self, id, name, value)
	self:checkIsValid(id)
	
	-- create the relevant component table if it doesn't exist
	if not self.components[name] then
		self.components[name] = { count=0 }
	end

	local component = self.components[name]
	component[id] = value
	component.count = component.count + 1
end


local function shallowCopy(tbl)
	local copy = {}
	for k, v in pairs(tbl) do copy[k] = v end
	return copy
end


-- get all entities with a given component
function EntityDb.queryComponent(self, name)
	local query = shallowCopy(self.components[name])
	query.count = nil
	return query
end


-- get all components associated with an entity
function EntityDb.queryEntity(self, id)
	self:checkIsValid(id)
	local query = {}
	for name, component in pairs(self.components) do
		query[name] = component[id]
	end
	return query
end


-- get a specific component from an entity
function EntityDb.getComponent(self, id, name)
	self:checkIsValid(id)
	return self.components[name][id]
end


-- remove a component from an entity
function EntityDb.removeComponent(self, id, name)
	self:checkIsValid(id)
	local component = self.components[name]
	if component[id] ~= nil then
		component[id] = nil
		component.count = component.count - 1
		if component.count == 0 then
			self.components[name] = nil
		end
	end
end


-- remove an entity from the db
function EntityDb.deleteEntity(self, id)
	self:checkIsValid(id)
	for name in pairs(self.components) do
		self:removeComponent(id, name)
	end
	self.entities[id] = nil
end


--===== SystemDb =====--

SystemDb = {}
SystemDb.__index = SystemDb


function SystemDb.new(_)
	local self = {
		systems = {},
		sorted = {},
	}
	setmetatable(self, SystemDb)
	return self
end
setmetatable(SystemDb, {__call=SystemDb.new})


local function systemId()
	local template = "xx:xx:xx"
	return string.gsub(template, "x", function(c)
		return string.format("%x", random(0, 0xf))
	end)
end


function SystemDb.addSystem(self, systemFunc, params)
	local system
	if type(systemFunc) == "table" then
		system = systemFunc
	else
		system = systemFunc(params)
	end

	local id = systemId()
	self.systems[id] = system
	table.insert(self.sorted, id)
	self:sort()
	return id
end


function SystemDb.sort(self)
	table.sort(self.sorted, function(a, b) return (self.systems[a].priority or 100) < (self.systems[b].priority or 100) end)
end


function SystemDb.update(self, dt)
	for _, system in ipairs(self.sorted) do
		self.systems[system]:update(dt)
	end
end


function SystemDb.removeSystem(self, id)
	self.systems[id] = nil
	for i=#self.sorted,1,-1 do
		if self.sorted[i] == id then table.remove(self.sorted, i) end
	end
end


return module