math.randomseed(os.time())

local glm = require 'honey.glm'

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


--===== 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})


local function serialize(tbl)
	local tostr = function(x, value)
		if type(x) == "table" then
			if x.__tostring then
				return tostring(x)
			else
				return serialize(x)
			end
		elseif type(x) == "string" then
			if value then
				return string.format("\"%s\"", x)
			else
				return x
			end
		else
			return tostring(x)
		end
	end
	local str = "{"
	for key, value in pairs(tbl) do
		if type(key) == "string" and string.match(key, "^_") then
			-- ignore keys starting with an underscore
		else
			str = str .. string.format("%s=%s,", tostr(key), tostr(value, true))
		end
	end
	str = string.sub(str, 1, -2) .. "}"
	return str
end


-- save current database to file
function EntityDb.save(self, filename)
	local file, err = io.open(filename, "w")
	if not file then error(err) end

	for entity in pairs(self.entities) do
		local components = self:queryEntity(entity)
		file:write(string.format("Entity(\"%s\", %s)\n", entity, serialize(components)))
	end

	file:close()
end


-- load database from file
function EntityDb.load(self, filename)
	print(collectgarbage("count"))
	self.entities = {}
	self.components = {}
	collectgarbage()
	print(collectgarbage("count"))
	local env = {
		Entity = function(id, components)
			self:createEntity(id)
			self:addComponents(id, components)
		end,
		Vec3 = glm.Vec3,
		Mat4 = glm.Mat4,
	}
	local f, err = loadfile(filename)
	if not f then error(err) end
	setfenv(f, env)
	f()
end


-- 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, data={} }
	end

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


-- add multiple components at once, for convenience
function EntityDb.addComponents(self, id, components)
	for name, value in pairs(components) do
		self:addComponent(id, name, value)
	end
end


-- create an entity with components
function EntityDb.createEntityWithComponents(self, components)
	local id = self:createEntity()
	self:addComponents(id, components)
	return id
end


-- get all entities with a given component
function EntityDb.queryComponent(self, name)
	local component = self.components[name]
	if component then 
		return component.data
	else
		return {}
	end
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.data[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].data[id]
end


-- remove a component from an entity
function EntityDb.removeComponent(self, id, name)
	self:checkIsValid(id)
	local component = self.components[name]
	if component.data[id] ~= nil then
		component.data[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(_, entityDb)
	local self = {
		systems = {},
		sorted = {},
		entityDb = entityDb,
	}
	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
		local params = params or {}
		params.db = self.entityDb
		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


--===== Access helper =====--


function Accessor(db, id)
	local tbl = {
		__db = db,
		__id = id,
	}

	setmetatable(tbl, {
		__index=function(self, key)
			return self.__db:getComponent(self.__id, key)
		end,
		__newindex=function(self, key, value)
			self.__db:addComponent(self.__id, key, value)
		end,
		__tostring=function(self)
			return string.format("Accessor<%s>", self.__id)
		end,
	})

	return tbl
end


return module