class 'CollectObjectsManagerBase'
-----------------------------------------------------------------------------------

function CollectObjectsManagerBase:__init(playfield)
	self.playfield = playfield

	self.collect_objects        = {}
	self.register_objects       = {}
	self.collect_object_groups  = {}
	self.unique_collect_object_groups = {}

	self.find_global_callback = nil
	self.callback_delay = 0
	self.collect_object = nil

	-- default settings
	-- to customize: override function "getSettings()" in the derived class
	self.settings = {
		sprite_path = '',
		use_name_as_suffix = false,
		num_chars_as_group_id = 0,
		icon_start_x = 512,
		icon_start_y = 384,
		fly_target_name = '',
		fly_target_x = 128,
		fly_target_y = 743,
		sound_on_found = '',
		default_msg = 'COLLECT_OBJECT',
		state_name = COLLECT_OBJECT_FOUND,
		state_tag = nil
	}
	-------------------------

	self:updateSettings()

	self.collect_objects_names = self:getCollectObjectsNames()
	self.unique_collect_objects_names = self:getUniqueCollectObjectsNames()
	
	self:createCollectObjectsGroups()

	self:createCollectObjects()
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:updateSettings()
	local custom_settings = self:getSettings()

	for parName, parValue in pairs( custom_settings ) do
		self.settings[ parName ] = parValue
	end
end
-----------------------------------------------------------------------------------

function CollectObjectsManagerBase:createCollectObjectsGroups()
	if ( self.settings.num_chars_as_group_id ~= 0 ) then
		for _, name in pairs( self.collect_objects_names ) do
			local groupId = self:getCollectObjectsGroupId( name )

			if ( self.collect_object_groups[ groupId ] == nil ) then
				self.collect_object_groups[ groupId ] = {}
			end

			table.insert( self.collect_object_groups[ groupId ], name )
		end

		for _, name in pairs( self.unique_collect_objects_names ) do
			local groupId = self:getCollectObjectsGroupId( name )

			if ( self.unique_collect_object_groups[ groupId ] == nil ) then
				self.unique_collect_object_groups[ groupId ] = {}
			end

			table.insert( self.unique_collect_object_groups[ groupId ], name )
		end
	end
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:getSettings()
	return self.settings
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:getCollectObjectsNames()
	return {}
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:getUniqueCollectObjectsNames()
	return {}
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:getCollectObjectsGroupId( name )
	return string.sub( name, 1, self.settings.num_chars_as_group_id )
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:createCollectObjects()
	for _, v in pairs(self.collect_objects_names) do
		self:createCollectObject(v)
	end
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:createCollectObject(name)
	local info                  = {}
	info.name                   = name
	
	local function create_icon()
		local icon = nil

		if self.settings.use_name_as_suffix then
			icon = CreateSpriteEntityInstant(self.settings.sprite_path .. name ..'.png')
		else
			icon = CreateSpriteEntityInstant(self.settings.sprite_path)
		end

		icon:setName(name)
		icon:attachTo(self.playfield:getPanel():getControlsPanel():getMainEntity())
		icon:setScrXY(self.settings.icon_start_x, self.settings.icon_start_y)
		icon:setVisible(false)
		icon:setOrder(2000)
		icon:setRespondable(false)
		return icon
	end
	info.icon = nil --create_icon()
	info.create_collect_object  = create_icon

	self.collect_objects[name] = info
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:setFindGlobalCallback(callback, delay)
	self.find_global_callback = callback
	self.callback_delay = delay or self.callback_delay
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:getTotalObjectsCount()
	local count = 0
	for _ ,_ in pairs(self.collect_objects) do
		count = count + 1
	end
	return count
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:getTotalUniqueObjectsCount()
	return #self.unique_collect_objects_names
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:getFoundObjectsCount()
	return HasGameState( self.settings.state_name, nil, self.settings.state_tag )
		and GetGameState( self.settings.state_name, self.settings.state_tag ):getSize()
		or 0
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:isCollectObjectFound( name )
	return HasGameState( self.settings.state_name, name, self.settings.state_tag )
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:areAllCollectObjectsFound()
	local objects_total_number = self:getTotalObjectsCount() + self:getTotalUniqueObjectsCount()
	local found_objects_number = self:getFoundObjectsCount()

	return ( found_objects_number == objects_total_number ) 
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:isGroupCollected( groupId )
	if ( not table.has_key( self.collect_object_groups, groupId ) ) then
		return
	end

	for _, obj_name in ipairs( self.collect_object_groups[ groupId ] ) do
		if ( not self:isCollectObjectFound( obj_name ) ) then
			return false
		end
	end

	return true
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:findUniqueCollectObjectsRequest( groupId )
	if self:isGroupCollected( groupId ) then
		for _, obj_name in ipairs( self.unique_collect_object_groups[ groupId ] ) do
			AddGameState( self.settings.state_name, obj_name, self.settings.state_tag )
		end
	end
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:findObject(name, target_name, text_alias, destination)

	if self:isCollectObjectFound(name) then
		return
	end
	
	self:onFindObject(name, target_name, text_alias, destination)

	self.playfield:getMouseBlocker():push()
	self.playfield:getTimer():add(
		function()
			self.playfield:getMouseBlocker():pop()
			if self.find_global_callback then
				local event  = {}
				event.sender = self
				event.name   = name
				event.target = target_name
				event.text_alias = text_alias

				self.find_global_callback(event)
				-- level.mouseBlocker:Pop()
			end
		end, self.callback_delay)

	if self.register_objects[name] ~= nil then
		self.register_objects[name] = nil
	end

	setCollectObjectsNum( getCollectObjectsNum() + 1 )

	return time
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:onFindObject(name, target_name, text_alias, destination)
	if self.collect_objects[name].icon == nil then
	    self.collect_objects[name].icon = self.collect_objects[name].create_collect_object()
	end

	local icon = self.collect_objects[name].icon
	local mouse_pos = GetCursorEntity():getScrPosition()
	icon:setVisible(true)

	local trigger_x = mouse_pos.x
	local trigger_y = mouse_pos.y

	local dest_x
	local dest_y

	if destination then
		dest_x = destination.x + math.random( -75, 75 )
		dest_y = destination.y + math.random( -75, 75 )
	else
		dest_x = SCR_W / 2 + math.random( -75, 75 )
		dest_y = SCR_H / 2 + math.random( -75, 75 )
	end 

	local start_scale = 0.3
	local zoom_in_time = 0.3
	local move_in_time =  0.4
	local k_max_move_amp = 3
	local oscillation_time = 1
	local frequency = 10
	local damping_time = .1
	local amplitude = .005
	local num_iterations = 50
	-- move from trigger zone to the center
	local eff = CreateAnimationEffector()
	eff:setDestroysOnEnd(true)
	eff:attachTo(icon)
	eff:setKeysMode(_KEYS_XY_SCR)
	eff:setKey(_TRACK_X, 0.0, trigger_x)
	eff:setKey(_TRACK_Y, 0.0, trigger_y)
	eff:setKey(_TRACK_X, move_in_time, dest_x)
	eff:setKey(_TRACK_Y, move_in_time, dest_y)
	--------------------------------------
	
	eff:setKey(_TRACK_SX, 0, start_scale)
	eff:setKey(_TRACK_SY, 0, start_scale)
	
	local dx = dest_x - trigger_x
	local move_amplitude_x = (dx / dest_x) * k_max_move_amp
	
	local dy = dest_y - trigger_y
	local move_amplitude_y = (dy / dest_y) * k_max_move_amp
	
	for i = 1, num_iterations do
		local time = i*(oscillation_time / num_iterations)
		local damp_coeff = math.sin(time*frequency)*math.exp(-time/damping_time+M_E)
		local scale = amplitude * damp_coeff + 1
		eff:setKey(_TRACK_SX, time+zoom_in_time, scale)
		eff:setKey(_TRACK_SY, time+zoom_in_time, scale)
		
		local x = dest_x + (move_amplitude_x * damp_coeff + 1)
		local y = dest_y + (move_amplitude_y * damp_coeff + 1)
		
		eff:setKey(_TRACK_X, time+move_in_time, x)
		eff:setKey(_TRACK_Y, time+move_in_time, y)
	end
	--------------------------------------

	local finish_x
	local finish_y

	if ( self.settings.fly_target_name ~= '' ) then
		local fly_target_obj = current_scene:getPanel():getControlsPanel():getEntityNamed(self.settings.fly_target_name)
		finish_x = fly_target_obj:getScrPosition().x
		finish_y = fly_target_obj:getScrPosition().y
	else
		finish_x = self.settings.fly_target_x
		finish_y = self.settings.fly_target_y
	end
	
	eff:setKey(_TRACK_SX, oscillation_time+zoom_in_time, 1)
	eff:setKey(_TRACK_SY, oscillation_time+zoom_in_time, 1)

	eff:setKey(_TRACK_SX, oscillation_time+zoom_in_time + .4, .3)
	eff:setKey(_TRACK_SY, oscillation_time+zoom_in_time + .4, .3)
	
	eff:setKey(_TRACK_X, oscillation_time+zoom_in_time + .6, finish_x)
	eff:setKey(_TRACK_Y, oscillation_time+zoom_in_time + .6, finish_y)
	
	eff:setKey(_TRACK_O, oscillation_time+zoom_in_time, 1)
	eff:setKey(_TRACK_O, oscillation_time+zoom_in_time + .6, 0.3)
	eff:setKey(_TRACK_O, oscillation_time+zoom_in_time + .7, 0)

	AddGameState(self.settings.state_name, name, self.settings.state_tag)
	
	self.playfield:getTimer():add(	function()
									icon:hide()
								end, oscillation_time+zoom_in_time + .6)
	eff:play()
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:getRegisterInfoByLevel(level_name)
	local result = {}

	for name, registerInfo in pairs(self.register_objects) do
		if (not level_name or registerInfo.level_name == level_name) then
			table.insert(result, registerInfo)
		end
	end

	return result
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:onUpdate(dt)
	if self.objectToSetOnNextUpdate then
		local info = self.collect_objects[self.objectToSetOnNextUpdate]

		self.collect_object = self.objectToSetOnNextUpdate

		if info.on_get then
			info.on_get(self.objectToSetOnNextUpdate)
		end
		info.icon:setVisible(true)
		local eff = CreateAnimationEffector()
		eff:attachTo(info.icon)
		eff:SetMode(_ANIM_LOOP)
		eff:setKey(_TRACK_S, 0, 0.9)
		eff:setKey(_TRACK_S, 0.65, 1.0)
		eff:setKey(_TRACK_S, 1., 0.9)
		eff:play()

		self.objectToSetOnNextUpdate = nil
	end
	if self.collect_object then
		local info = self.collect_objects[self.collect_object]
		local mouse_pos = GetMousePos()
		info.icon:SetScrXY(mouse_pos.x, mouse_pos.y)
		info.icon:SetZ(TOOL_Z_IN_INVENTORY)
	end
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:setCollectObject(name)
	self.objectToSetOnNextUpdate = name
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:dropCollectObject(time)
	if self.collect_object then
		local info = self.collect_objects[self.collect_object]
		self.collect_object = nil
		
		local time = time or 0.5
		local icon = info.icon
		local finish_x
		local finish_y

		if ( self.settings.fly_target_name ~= '' ) then
			local fly_target_obj = current_scene:getPanel():getControlsPanel():getEntityNamed(self.settings.fly_target_name)
			finish_x = fly_target_obj:getScrPosition().x
			finish_y = fly_target_obj:getScrPosition().y
		else
			finish_x = self.settings.fly_target_x
			finish_y = self.settings.fly_target_y
		end

		icon:MoveTo(finish_x, finish_y, time)
		icon:ChangeScale(icon:GetScale(), 0.5, time)
		
		GetCurrentLevel().timer:Add(function()
			icon:setVisible(false)
			self.playfield:GetLevelScreen():GetPanel():GetAssociatedLuaObject():CloseMetaBox()
			end,
			time)

		SetCursor('CursorDefault')
	end
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:getCollectObject()
	return self.collect_object
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:isCollectObjectInHand()
	return self:getCollectObject() and not self.do_not_set_cursor
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:onCollectObjectApply(object)
	if self.collect_object then
		UpdateComicsRequest()
		
		if not object then
			self:dropCollectObject()
			return true
		end
		
		local collect_object_applied 	= false
		local obj_name     				= object:GetName()
		local name 						= self.collect_object
		local info         				= self.collect_objects[name]
		local level 					= GetCurrentLevel()
		
		if info.target_name and info.target_name == obj_name then

			level.custom_cursors[obj_name] = ''
			
			local state, value = generateCollectObjectAppliedState(name, obj_name)
			assert(not HasGameState(state, value))
			AddGameState(state, value)
			
			if info.on_apply then
				local event                  = {}
				event.save                   = false
				event.target                 = info.target_name
				event.collectObjectName      = name
				
				info.on_apply(event)
			end
			
			self.playfield:IgnoreOnTestPoint(obj_name)
			
			collect_object_applied = true
			if info.time_to_drop then
				info.icon:setVisible(false)
				self.do_not_set_cursor = true
				level.timer:Add(function()
					info.icon:setVisible(true)
					info.icon:SetScrXY(object:GetScrX(), object:GetScrY())
					self:dropCollectObject()
					self.do_not_set_cursor = nil
					end, info.time_to_drop)
			else
				self:dropCollectObject()
			end
		end
		
		
		if not collect_object_applied then
			local bOpenZoomAreaClicked = false
			
			if not level:IsLockedObject(obj_name)
				and (level.locker:GetNReferences() == 0 or IsActiveZoneObject(obj_name))
				then
				for _, zone in pairs(level.zones) do
					if zone.trigger_name == obj_name and not zone:isDisabled() then
						zone.setVisible(true)
						bOpenZoomAreaClicked = true
						break
					end
				end
			end
			
			if not bOpenZoomAreaClicked then				
				if getColectObjectNameByTargetName(obj_name) then
					ShowMonolog('ToolDenyMonolog', 'C_COLLECT_OBJECT_DENY_'..tostring(math.random(1, 2)))
				end
				self:dropCollectObject()
			end
		end
		return true
	else
		return false
	end
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:registerCollectObject(name, 
													target_name, 
													on_found, 
													text_alias, 
													destination,
													cursor_name)

	local text = text_alias
	if not text then
		text = self.settings.default_msg -- default collect object msg
	end

	if self:isCollectObjectFound(name) then
		if target_name then
			self.playfield:getEntityNamed(target_name):setVisible(false)
		end

		if on_found then
			local event  = {}
			event.save   = true
			event.name   = name
			event.target = target_name

			on_found(event)
		end
	else
		local registerInfo       = {}
		registerInfo.name        = name
		registerInfo.target_name = target_name
		registerInfo.level_name  = self.playfield:getName()

		self.register_objects[name] = registerInfo

		local function on_click(event)
			assert(not event.save)
			local time = self:findObject(name, target_name, text, destination)

			if on_found then
				local event      = {}
				event.save       = false
				event.name       = name
				event.target     = target_name
				event.flyingTime = time

				on_found(event)

				if ( self.settings.sound_on_found ~= '' ) then
					playCommonSound(self.settings.sound_on_found)
				end
			end
		end

		local cursor_name = cursor_name or 'CursorActive'

		self.playfield:registerActiveObject(target_name, on_click, false, cursor_name)
	end
end

-----------------------------------------------------------------------------------
function CollectObjectsManagerBase:registerCollectObjectApplyInfo(name,
	target_name,
	on_apply,
	text_alias, 
	time_to_drop)
	assert(target_name)

	local info            = GetCurrentLevel().collectManager.collect_objects[name]
	info.target_name      = target_name
	info.on_apply         = on_apply
	info.on_get           = nil
	info.on_drop          = nil
	info.time_to_drop	  = time_to_drop

	local targetCursor = 'CursorGlass'
	local bCollectObjectApplied = isCollectObjectApplied(name, target_name)

	if bCollectObjectApplied then		
		if on_apply then
			local event                  = {}
			event.save                   = true
			event.target                 = info.target_name
			event.collectObjectName      = name

			on_apply(event)
		end
		self.playfield:IgnoreOnTestPoint(target_name)
	else
		GetCurrentLevel().custom_cursors[info.target_name] = targetCursor
		if text_alias then
			RegisterMonologObject(target_name, text_alias)
		end
	end

	return info
end

-----------------------------------------------------------------------------------
function isCollectObjectApplied(name, target_name, level_name)
	local state, value = generateCollectObjectAppliedState(name, target_name, level_name)
	return HasGameState(state, value)
end

-----------------------------------------------------------------------------------
function generateCollectObjectAppliedState(name, target_name, level_name)
	if not level_name then
		level_name = self.playfield:getName()
	end

	local value = level_name..': collect_object_name('..name..') target_name('..target_name..')'
	return COLLECT_OBJECT_APPLIED, value
end

-----------------------------------------------------------------------------------
function getColectObjectNameByTargetName(target_name)
	bNotUsed = bNotUsed or false

	for collect_object_name, info in pairs(GetCurrentLevel().collectManager.collect_objects) do
		if info.target_name then
			if type(info.target_name) == 'string' then
				if info.target_name == target_name then
					return collect_object_name
				end

			else
				for i, tmp_target_name in pairs(info.target_name) do
					if tmp_target_name == target_name then
						return collect_object_name
					end
				end
			end
		end
	end

	return nil
end

-----------------------------------------------------------------------------------
function setCollectObjectsNum(number)
    SetGameState(COLLECT_OBJECTS_NUM, tostring(number) )
end

-----------------------------------------------------------------------------------
function getCollectObjectsNum()
    local number = 0
    if HasGameState(COLLECT_OBJECTS_NUM) then
        number = tonumber( GetGameState(COLLECT_OBJECTS_NUM):get(0) ) or 0
    end
    return number
end
-----------------------------------------------------------------------------------