local max_entity_id = 1000000000000 -- If you need more, there's a problem with your code

luaentity = {}

luaentity.registered_entities = {}

local filename = minetest.get_worldpath().."/luaentities"
local function read_file()
	local f = io.open(filename, "r")
	if f == nil then return {} end
    	local t = f:read("*all")
    	f:close()
	if t == "" or t == nil then return {} end
	return minetest.deserialize(t) or {}
end

local function write_file(tbl)
	local f = io.open(filename, "w")
    	f:write(minetest.serialize(tbl))
    	f:close()
end

local function read_entities()
	local t = read_file()
	for _, entity in pairs(t) do
		setmetatable(entity, luaentity.registered_entities[entity.name])
	end
	return t
end

local function write_entities()
	for _, entity in pairs(luaentity.entities) do
		setmetatable(entity, nil)
		for _, attached in pairs(entity._attached_entities) do
			if attached.entity then
				attached.entity:remove()
				attached.entity = nil
			end
		end
		entity._attached_entities_master = nil
	end
	write_file(luaentity.entities)
end

minetest.register_on_shutdown(write_entities)
luaentity.entities_index = 0

local function get_blockpos(pos)
	return {x = math.floor(pos.x / 16),
	        y = math.floor(pos.y / 16),
	        z = math.floor(pos.z / 16)}
end

local active_blocks = {} -- These only contain active blocks near players (i.e., not forceloaded ones)
local handle_active_blocks_step = 2
local handle_active_blocks_timer = 0
minetest.register_globalstep(function(dtime)
	handle_active_blocks_timer = handle_active_blocks_timer + dtime
	if handle_active_blocks_timer >= handle_active_blocks_step then
		handle_active_blocks_timer = handle_active_blocks_timer - handle_active_blocks_step
		local active_block_range = tonumber(minetest.setting_get("active_block_range")) or 2
		local new_active_blocks = {}
		for _, player in ipairs(minetest.get_connected_players()) do
			local blockpos = get_blockpos(player:getpos())
			local minp = vector.subtract(blockpos, active_block_range)
			local maxp = vector.add(blockpos, active_block_range)

			for x = minp.x, maxp.x do
			for y = minp.y, maxp.y do
			for z = minp.z, maxp.z do
				local pos = {x = x, y = y, z = z}
				new_active_blocks[minetest.hash_node_position(pos)] = pos
			end
			end
			end
		end
		active_blocks = new_active_blocks
		-- todo: callbacks on block load/unload
	end
end)

local function is_active(pos)
	return active_blocks[minetest.hash_node_position(get_blockpos(pos))] ~= nil
end

local entitydef_default = {
	_attach = function(self, attached, attach_to)
		local attached_def = self._attached_entities[attached]
		local attach_to_def = self._attached_entities[attach_to]
		attached_def.entity:set_attach(
			attach_to_def.entity, "",
			vector.subtract(attached_def.offset, attach_to_def.offset), -- todo: Does not work because is object space
			vector.new(0, 0, 0)
		)
	end,
	_set_master = function(self, index)
		self._attached_entities_master = index
		if not index then
			return
		end
		local def = self._attached_entities[index]
		if not def.entity then
			return
		end
		def.entity:setpos(vector.add(self._pos, def.offset))
		def.entity:setvelocity(self._velocity)
		def.entity:setacceleration(self._acceleration)
	end,
	_attach_all = function(self)
		local master = self._attached_entities_master
		if not master then
			return
		end
		for id, entity in pairs(self._attached_entities) do
			if id ~= master and entity.entity then
				self:_attach(id, master)
			end
		end
	end,
	_detach_all = function(self)
		local master = self._attached_entities_master
		for id, entity in pairs(self._attached_entities) do
			if id ~= master and entity.entity then
				entity.entity:set_detach()
			end
		end
	end,
	_add_attached = function(self, index)
		local entity = self._attached_entities[index]
		if entity.entity then
			return
		end
		local entity_pos = vector.add(self._pos, entity.offset)
		if not is_active(entity_pos) then
			return
		end
		local ent = minetest.add_entity(entity_pos, entity.name):get_luaentity()
		ent:from_data(entity.data)
		ent.parent_id = self._id
		ent.attached_id = index
		entity.entity = ent.object
		local master = self._attached_entities_master
		if master then
			self:_attach(index, master)
		else
			self:_set_master(index)
		end
	end,
	_remove_attached = function(self, index)
		local master = self._attached_entities_master
		local entity = self._attached_entities[index]
		local ent = entity and entity.entity
		if entity then entity.entity = nil end
		if index == master then
			self:_detach_all()
			local newmaster
			for id, attached in pairs(self._attached_entities) do
				if id ~= master and attached.entity then
					newmaster = id
					break
				end
			end
			self:_set_master(newmaster)
			self:_attach_all()
		elseif master and ent then
			ent:set_detach()
		end
		if ent then
			ent:remove()
		end
	end,
	_add_loaded = function(self)
		for id, _ in pairs(self._attached_entities) do
			self:_add_attached(id)
		end
	end,
	getid = function(self)
		return self._id
	end,
	getpos = function(self)
		return vector.new(self._pos)
	end,
	setpos = function(self, pos)
		self._pos = vector.new(pos)
		--for _, entity in pairs(self._attached_entities) do
		--	if entity.entity then
		--		entity.entity:setpos(vector.add(self._pos, entity.offset))
		--	end
		--end
		local master = self._attached_entities_master
		if master then
			local master_def = self._attached_entities[master]
			master_def.entity:setpos(vector.add(self._pos, master_def.offset))
		end
	end,
	getvelocity = function(self)
		return vector.new(self._velocity)	
	end,
	setvelocity = function(self, velocity)
		self._velocity = vector.new(velocity)
		local master = self._attached_entities_master
		if master then
			self._attached_entities[master].entity:setvelocity(self._velocity)
		end
	end,
	getacceleration = function(self)
		return vector.new(self._acceleration)
	end,
	setacceleration = function(self, acceleration)
		self._acceleration = vector.new(acceleration)
		local master = self._attached_entities_master
		if master then
			self._attached_entities[master].entity:setacceleration(self._acceleration)
		end
	end,
	remove = function(self)
		self:_detach_all()
		for _, entity in pairs(self._attached_entities) do
			if entity.entity then
				entity.entity:remove()
			end
		end
		luaentity.entities[self._id] = nil
	end,
	add_attached_entity = function(self, name, data, offset)
		local index = #self._attached_entities + 1
		self._attached_entities[index] = {
			name = name,
			data = data,
			offset = vector.new(offset),
		}
		self:_add_attached(index)
		return index
	end,
	remove_attached_entity = function(self, index)
		self:_remove_attached(index)
		self._attached_entities[index] = nil
	end,
}

function luaentity.register_entity(name, prototype)
	-- name = check_modname_prefix(name)
	prototype.name = name
	setmetatable(prototype, {__index = entitydef_default})
	prototype.__index = prototype -- Make it possible to use it as metatable
	luaentity.registered_entities[name] = prototype
end

-- function luaentity.get_entity_definition(entity)
--	 return luaentity.registered_entities[entity.name]
-- end

function luaentity.add_entity(pos, name)
	if not luaentity.entities then
		minetest.after(0, luaentity.add_entity, vector.new(pos), name)
		return
	end
	local index = luaentity.entities_index
	while luaentity.entities[index] do
		index = index + 1
		if index >= max_entity_id then
			index = 0
		end
	end
	luaentity.entities_index = index

	local entity = {
		name = name,
		_id = index,
		_pos = vector.new(pos),
		_velocity = {x = 0, y = 0, z = 0},
		_acceleration = {x = 0, y = 0, z = 0},
		_attached_entities = {},
	}
	
	local prototype = luaentity.registered_entities[name]
	setmetatable(entity, prototype) -- Default to prototype for other methods
	luaentity.entities[index] = entity

	if entity.on_activate then
		entity:on_activate()
	end
	return entity
end

-- todo: check if remove in get_staticdata works
function luaentity.get_staticdata(self)
	local parent = luaentity.entities[self.parent_id]
	if parent and parent._remove_attached then
		parent:_remove_attached(self.attached_id)
	end
	return "toremove"
end

function luaentity.on_activate(self, staticdata)
	if staticdata == "toremove" then
		self.object:remove()
	end
end

function luaentity.get_objects_inside_radius(pos, radius)
	local objects = {}
	local index = 1
	for id, entity in pairs(luaentity.entities) do
		if vector.distance(pos, entity:getpos()) <= radius then
			objects[index] = entity
			index = index + 1
		end
	end
end

minetest.register_globalstep(function(dtime)
	if not luaentity.entities then
		luaentity.entities = read_entities()
	end
	for id, entity in pairs(luaentity.entities) do
		local master = entity._attached_entities_master
		local master_def = master and entity._attached_entities[master]
		local master_entity = master_def and master_def.entity
		local master_entity_pos = master_entity and master_entity:getpos()
		if master_entity_pos then
			entity._pos = vector.subtract(master_entity_pos, master_def.offset)
			entity._velocity = master_entity:getvelocity()
			entity._acceleration = master_entity:getacceleration()
		else
			entity._pos = vector.add(vector.add(
				entity._pos,
				vector.multiply(entity._velocity, dtime)),
				vector.multiply(entity._acceleration, 0.5 * dtime * dtime))
			entity._velocity = vector.add(
				entity._velocity,
				vector.multiply(entity._acceleration, dtime))
		end
		if master and not master_entity_pos then -- The entity has somehow been cleared
			if pipeworks.delete_item_on_clearobject then
				entity:remove()
			else
				entity:_remove_attached(master)
				entity:_add_loaded()
				if entity.on_step then
					entity:on_step(dtime)
				end
			end
		else
			entity:_add_loaded()
			if entity.on_step then
				entity:on_step(dtime)
			end
		end
	end
end)
