visible_wielditem = {}

local entity_name = "visible_wielditem:visible_wielditem"

local entities = {}

local function create_entity(player)
	local pos = player:get_pos()
	if not pos then return end -- HACK deal with player object being invalidated before on_leaveplayer has been called
	local ent = minetest.add_entity(pos, entity_name)
	if ent then
		ent:get_luaentity():_set_player(player)
	end
end

minetest.register_on_joinplayer(create_entity)

minetest.register_on_leaveplayer(function(player)
	entities[player:get_player_name()]:_remove()
end)

visible_wielditem.model_attachments = {
	default = {
		bonename = "Arm_Right",
		position = vector.new(0, 0.375, 0),
		rotation = vector.new(180, 90, 0),
		scale = 0.25
	}
}

visible_wielditem.item_tweaks = {
	types = {
		unknown = {},
		node = {
			position = vector.new(0, 0.2, 0),
			rotation = vector.new(0, 0, 180)
		},
		tool = {
			position = vector.new(0, 0, 0.3),
			rotation = vector.new(0, 0, 135)
		},
		craft = {
			position = vector.new(0, 0, 0.3),
			rotation = vector.new(0, 0, 45),
		}
	},
	groups = {},
	names = {}
}

local quaternion_from_euler_rot_deg = modlib.quaternion.from_euler_rotation_deg

local quaternion_to_euler_rot_deg = modlib.quaternion.to_euler_rotation_irrlicht

-- HACK quaternion multiplication should be fixed in modlib eventually
local function quaternion_compose(other, self)
	local X, Y, Z, W = unpack(self)
	return modlib.quaternion.normalize {
		(other[4] * X) + (other[1] * W) + (other[2] * Z) - (other[3] * Y),
		(other[4] * Y) + (other[2] * W) + (other[3] * X) - (other[1] * Z),
		(other[4] * Z) + (other[3] * W) + (other[1] * Y) - (other[2] * X),
		(other[4] * W) - (other[1] * X) - (other[2] * Y) - (other[3] * Z),
	}
end

function visible_wielditem.get_attachment(modelname, itemname)
	local model_attachments = visible_wielditem.model_attachments
	local attachment = modlib.table.copy(model_attachments[modelname] or model_attachments.default)
	local rotation = quaternion_from_euler_rot_deg(attachment.rotation)
	local function apply_tweaks(tweaks)
		tweaks = tweaks or {}
		if tweaks.position then attachment.position = vector.add(attachment.position, tweaks.position) end
		if tweaks.rotation then
			rotation = quaternion_compose(rotation, quaternion_from_euler_rot_deg(tweaks.rotation))
		end
		if tweaks.scale then attachment.scale = attachment.scale * tweaks.scale end
	end
	local def = minetest.registered_items[itemname] or {}
	local item_tweaks = visible_wielditem.item_tweaks
	apply_tweaks(item_tweaks.types[def.type or "unknown"])
	for groupname, rating in pairs(def.groups or {}) do
		if rating ~= 0 then
			apply_tweaks(item_tweaks.groups[groupname])
		end
	end
	apply_tweaks(item_tweaks.names[itemname])
	attachment.rotation = quaternion_to_euler_rot_deg(rotation)
	return attachment
end

minetest.register_entity(entity_name, {
	initial_properties = {
		physical = false,
		collide_with_objects = false,
		pointable = false,
		visual = "wielditem",
		wield_item = "",
		is_visible = false,
		backface_culling = false,
		use_texture_alpha = true,
		glow = 0,
		static_save = false,
		shaded = true
	},
	on_activate = function(self)
		local object = self.object
		object:set_armor_groups { immortal = 1 }
	end,
	_force_resend = function(self) -- HACK
		self.object:set_pos(self.object:get_pos())
	end,
	_update_attachment = function(self)
		if not self._item then return end
		local object = self.object
		local attachment = visible_wielditem.get_attachment(self._player:get_properties().mesh,
			self._item:get_name() or "")
		-- TODO softcode
		local stack_scale = 1
		if self._item:get_definition().type ~= "tool" then
			stack_scale = 0.75 + 0.5 * math.min(1, self._item:get_count() / self._item:get_stack_max())
		end
		local vsize = attachment.scale * stack_scale
		object:set_properties { visual_size = vector.new(vsize, vsize, vsize) }
		object:set_attach(self._player,
			attachment.bonename,
			vector.multiply(vector.offset(attachment.position, 0, vsize / 2, 0), 10),
			attachment.rotation)
		self:_force_resend()
	end,
	_set_player = function(self, player)
		self._player = player -- HACK this assumes that PlayerRefs don't change
		self:_set_item(player:get_wielded_item())
		self:_update_attachment()
		entities[player:get_player_name()] = self
	end,
	on_deactivate = function(self)
		create_entity(self._player)
	end,
	_set_item = function(self, item)
		self._item = item
		local object = self.object
		if item:is_empty() then
			object:set_properties {
				is_visible = false,
				wield_item = ""
			}
			return
		end
		object:set_properties {
			is_visible = true,
			wield_item = item:to_string(),
			glow = item:get_definition().light_source or 0
		}
		self:_update_attachment()
	end,
	_remove = function(self)
		self.on_deactivate = modlib.func.no_op -- don't recreate entity; it's supposed to be removed
		self.object:remove()
		entities[self._player:get_player_name()] = nil
	end
	-- TODO on_step: reattach regularly to work around engine bugs?
})

modlib.minetest.register_on_wielditem_change(function(player, _, _, item)
	local entity = entities[player:get_player_name()]
	if entity.object:get_pos() then
		entity:_set_item(item)
	else -- recreate entity if necessary
		create_entity(player)
	end
end)
