--[[
 * Реализация базовой логики перетаскивания объектов.
 * Пример:
			local dragObj

			lcoal function onTake(e)
			end

			lcoal function onDrop(e)
			end

			lcoal function beforeMove(e)
			end

			lcoal function afterMove(e)
			end
			
			dragObj = DragObjectBase(playfield:getGameScene(), 'entityName')
			dragObj:setCallback('onTake',		onTake)
			dragObj:setCallback('onDrop',		onDrop)
			dragObj:setCallback('beforeMove',	beforeMove)
			dragObj:setCallback('afterMove',	afterMove)
			dragObj:activate()
--]]

class 'DragObjectBase'
-------------------------------------------------------------------------------

--[[
 * Инициализация перетаскиваемого объекта. Создает "обертка" для спрайта, в которой регистрируются обработчики сигналов,
 * указываются границы перетаскивания.
 * @param   gameScene		Указатель на текущее игровое поле. Нужен для получения объекта, если передано только его имя, и
							для определения относится ли объект под курсором этому игровому полю (метод onDragFocusIn).
 * @param   draggedEntity	Имя спрайта либо сам спрайт, который будет перетаскиваться.
--]]
function DragObjectBase:__init(gameScene, draggedEntity)
	assert(gameScene)

	self.scene			= gameScene
	self.draggedEntity	= draggedEntity and self:recognizeObject(draggedEntity) or nil
	self.anchor			= nil

	self.bounds			= nil --Rectanglef(0,0,WORK_W,WORK_H)
	self.draggingCursor	= 'CursorTool'
	self.draggingOffset	= nil
	self.offsetEffector = nil
	self.offsetAnimTime	= 0.05
	self.anchorNameFormat = '%s_dragAnchor'

	-- dragging screenFocus hotSpot
	if OS_MOBILE then
		self.draggingSFHotSpot	= MOBILE_DRAGGING_OFFSET
	else
		self.draggingSFHotSpot	= nil
	end
	self.oldSFHotSpot		= nil

	self.clickConnector	= nil
	self.dragConnectors = nil
	self.sessionData	= nil

	self.bDragging		  = false
	self.bStickyDrag	  = true
	self.bZoomOpenEnabled = true	-- возможность открывать во время перетаскивания (только для stickyDrag)
	self.callbacks = {}
end
-------------------------------------------------------------------------------

function DragObjectBase:destroy()
	if self:isDragging() then
		self:drop()
	end
	
	self:deactivate()

	if self.draggedEntity then
		self.draggedEntity:destroy()
		self.draggedEntity = nil
	end
end
-------------------------------------------------------------------------------

--[[
 * Активировать возможность перетаскивания.
 * @param   takeTarget      Объект, при клике на который будет стартовать перетаскивание ( Не обязательный параметр ).
							По-умолчанию: takeTarget == self.draggedEntity
--]]
function DragObjectBase:activate(takeTarget)
	assert(self.draggedEntity)
	takeTarget = self:recognizeObject(takeTarget or self.draggedEntity)

	local callback		= bind(self.takeProcess, self)
	self.clickConnector	= takeTarget.onClickBegin:add( callback )
end
-------------------------------------------------------------------------------

--[[
 * Деактивировать возможность перетаскивания.
--]]
function DragObjectBase:deactivate()
	if self.clickConnector then
		self.clickConnector:remove()
		self.clickConnector = nil
	end
end
-------------------------------------------------------------------------------

--[[
 * Проверка того, возможно ли в данный мометн перетаскивание объекта.
--]]
function DragObjectBase:isActivated()
	return (self.clickConnector ~= nil)
end
-------------------------------------------------------------------------------

function DragObjectBase:take(screenFocus)
	if not self:isDragging() then
		self:takeProcess(self.draggedEntity, screenFocus)
	end
end
-------------------------------------------------------------------------------

--[[
 * Метод вручную прекращает перетаскивание. При этом выполнятся все те же бработчики,
 * что и штатном прекращении перетаскивания.
--]]
function DragObjectBase:drop()
	if self:isDragging() then
		self:getSessionData().screenFocus:finishDrag()
	end
end
-------------------------------------------------------------------------------

--[[
 * Метод подписывает перетаскиваемый объект на получение всех сигналов, необходимых
 * для корректной работы DragObjectBase.
--]]
function DragObjectBase:subscribeDragEvents()
	self.dragConnectors = {}

	table.insert(self.dragConnectors, self.draggedEntity.onDragFocusIn:add(   bind(self.onDragFocusIn,	self) ))
	table.insert(self.dragConnectors, self.draggedEntity.onDragFocusKeep:add( bind(self.dragProcess,	self) ))
	table.insert(self.dragConnectors, self.draggedEntity.onDragFinish:add(    bind(self.dropProcess,	self, false) ))
	table.insert(self.dragConnectors, self.draggedEntity.onDragAbort:add(     bind(self.dropProcess,	self, true ) ))

	if self.bStickyDrag then
		local connector = self.draggedEntity.onDragClickBegin:add( bind(self.onDragClickBegin, self) )
		table.insert(self.dragConnectors, connector)
	end
end
-------------------------------------------------------------------------------

--[[
 * Метод отписывает перетаскиваемый объект от получения всех сигналов, необходимых
 * для корректной работы DragObjectBase.
--]]
function DragObjectBase:unsubscribeDragEvents()
	for i, connector in ipairs(self.dragConnectors) do
		connector:remove()
	end
	self.dragConnectors = nil
end
-------------------------------------------------------------------------------

--[[
 * Метод возвращает объект, который должен перетаскиваться.
--]]
function DragObjectBase:getDraggedEntity()
	return self.draggedEntity
end
-------------------------------------------------------------------------------

--[[
 * Метод устанавливает объект, который должен перетаскиваться.
--]]
function DragObjectBase:setDraggedEntity(entity)
	assert(not self.draggedEntity)
	self.draggedEntity = self:recognizeObject(entity)
end
-------------------------------------------------------------------------------

--[[
 * Метод устанавливает курсор, который будет отображаться на протяжении всего
 * перетаскивания.
--]]
function DragObjectBase:setDraggingCursor(draggingCursor)
	self.draggingCursor = draggingCursor
end
-------------------------------------------------------------------------------

--[[
 * Метод устанавливает позицию объекта относительно якоря на время всего перетаскивания.
--]]
function DragObjectBase:setDraggingOffset(draggingOffset)
	self.draggingOffset = draggingOffset
end
-------------------------------------------------------------------------------

--[[
 * метод устанавливает время, за которое объект переместится в позицию draggingOffset
 * при начале перетаскивания.
--]]
function DragObjectBase:setOffsetAnimTime(offsetAnimTime)
	self.offsetAnimTime = offsetAnimTime or 0
end
-------------------------------------------------------------------------------

--[[
 * Метод устанавливает тип перетаскивания. Если для bStickyDrag установлено true, то
 * сброс объекта будет происходить только после повторного клика. Иначе - при отпускании
 * ЛКМ. На мобильных устройствах поведение всега будет такое, как будто bStickyDrag == false.
--]]
function DragObjectBase:setStickyDrag(bStickyDrag)
	self.bStickyDrag = bStickyDrag
end
-------------------------------------------------------------------------------

--[[
 * Установка смещения screenFocus-а на время перетаскивания объекта. Необходим для мобильных
 * устройств. Обеспечивает визуальное смещение объекта над пальцем и определение таргета сброса
 * не под пальцем, а под самим объектом.
--]]
function DragObjectBase:setDraggingScreenFocusHotSpot(hotSpot)
	self.draggingSFHotSpot = hotSpot
end
-------------------------------------------------------------------------------

--[[
 * Установка возможности открывать зумы во время перетаскивания объекты. 
--]]
function DragObjectBase:setZoomOpenEnabled(bEnabled)
	self.bZoomOpenEnabled = bEnabled
end
-------------------------------------------------------------------------------

--[[
 * Метод создает так называемый 'якорь', который будет помещаться в позицию курсора/тапа
 * в процессе перетаксивания. К данному якорю прикрепляется объект self.draggedEntity.
--]]
function DragObjectBase:createAnchor(screenFocus)
	local anchorName = string.format(self.anchorNameFormat, self.draggedEntity:getName())

	self.anchor = CreateRenderedObject()
	self.anchor:setName ( anchorName )
	self.anchor:attachTo( self.scene:getMainEntity() )
	self.anchor:setScrX ( screenFocus:getScrX() )
	self.anchor:setScrY ( screenFocus:getScrY() )
	self.anchor:setMaxOrder()
	self.anchor:setRespondable(false)

	return self.anchor
end
-------------------------------------------------------------------------------

--[[
 * Удаляет якорь перетаскивания.
--]]
function DragObjectBase:destroyAnchor()
	self.anchor:destroy()
	self.anchor = nil
end
-------------------------------------------------------------------------------

--[[
 * Метод возвращает якорь перетаскивания. Так как якорь существует только во время
 * перетаскивания, то данный метод вернет nil, если перетаскивание не происходит.
--]]
function DragObjectBase:getAnchor()
	return self.anchor
end
-------------------------------------------------------------------------------

--[[
 * Метод позволяет установить обработчик для одного из событий, связаных в процессом
 * перетаскивания. В данном классе такими событиями являются:
 *	- onTake
 *	- onDrop
 *	- beforeMove
 *	- afterMove
 * @param	eventName		Название события.
 * @param	callback		Функция, которая вызовится в ответ на данное событие.
--]]
function DragObjectBase:setCallback(eventName, callback)
	self.callbacks[eventName] = callback
end
-------------------------------------------------------------------------------

--[[
 * Метод возвращает функцию-callback, установленую для указаного события.
--]]
function DragObjectBase:getCallback(eventName)
	return self.callbacks[eventName]
end
-------------------------------------------------------------------------------

--[[
 * Метод предназначен для универсальной инициализации полей, хранящих ссылки на объекты.
 * Он определяет было ли передано имя объекта или сам объект. Если передано имя, то он
 * пытается получить сам объект по имени. Иначе - возвращает тот же объект, который пришел
 * параметру object.
 * @param	object		Параметр представляет объект либо его имя.
--]]
function DragObjectBase:recognizeObject(object)
	return (type(object) == 'string') and self.scene:getEntityNamed(object)
									   or object
end
-------------------------------------------------------------------------------

--[[
 * Установка границ перетаскивания по двум точкам.
--]]
function DragObjectBase:setBounds(letfTop, rightBottom)
	assert(letfTop.x <= rightBottom.x and letfTop.y <= rightBottom.y)

	local width  = math.abs(rightBottom.x - letfTop.x)
	local height = math.abs(rightBottom.y - letfTop.y)

	if not self.bounds then
		self.bounds = Rectanglef()
	end

	self.bounds:setOrigin(letfTop)
	self.bounds:setSize(Vector2f(width, height))
end
-------------------------------------------------------------------------------

--[[
 * Установка границ перетаскивания по точке, ширине и высоте.
--]]
function DragObjectBase:setBoundsBySize(letfTop, size)
	if not self.bounds then
		self.bounds = Rectanglef()
	end

	self.bounds:setOrigin(leftTop)
	self.bounds:setSize(size)
end
-------------------------------------------------------------------------------

--[[
 * Метод возвращает ссылку на экземпляр класса Rectanglef, содержащий границы
 * перетаскивания объекта.
--]]
function DragObjectBase:getBounds()
	return self.bounds
end
-------------------------------------------------------------------------------

--[[
 * Метод создает таблицу, которая содержит информацию про текущую сессию перетаскивания.
 * Среди прочих в данной таблице содержатся данные, необходимые для востановления
 * некоторых параметров объекта по окончанию его перетаскивания. Например, таблица
 * содержит ссылку на объект, к которому перетаскиваемый объект был прикреплен ранее.
 * Таблица sessionData - это аналог таблицы drag_info в предыдущей реализации Drag.
--]]
function DragObjectBase:createSessionData(screenFocus)
	local info	 = {}

	info.parent		 = self.draggedEntity:getParent()
	info.order		 = self.draggedEntity:getOrder()
	info.scale		 = self.draggedEntity:getScale()
	info.pos		 = self.draggedEntity:getPosition()
	info.screenFocus = screenFocus

	self.sessionData = info
	return info
end
-------------------------------------------------------------------------------

--[[
 * Метод возвращает таблицу, которая содержит информацию про текущую сессию перетаскивания.
 * Важным моменто есть то, что данная таблица не удаляется после окончания перетаскивания.
 * Очищается только поле screenFocus. Это позволяет распределить по времени возврат тех или
 * иных параметров объекта.
--]]
function DragObjectBase:getSessionData()
	return self.sessionData
end
-------------------------------------------------------------------------------

--[[
 * В методе выполняется перемещение объекта в позицию draggingOffset относительно якоря
 * в момент начала перетаскивания.
--]]
function DragObjectBase:createOffsetAnimation()
	if self.draggingOffset then
		if self.offsetAnimTime == 0 then
			self.draggedEntity:setPosition(self.draggingOffset)
		else
			self.offsetEffector = self.draggedEntity:MoveTo(self.draggingOffset.x,
															self.draggingOffset.y,
															self.offsetAnimTime)
            self.offsetEffector:setDestroysOnEnd(false)
		end
	end
end
-------------------------------------------------------------------------------

--[[
 * Метод описывает механизм начала перетаскивания.
--]]
function DragObjectBase:startDrag(screenFocus)
	assert(not self.bDragging)

	if self.draggingSFHotSpot then
		self.oldSFHotSpot = Vector2f(screenFocus:getHotSpotX(), screenFocus:getHotSpotY())
		screenFocus:setHotSpotXY(self.draggingSFHotSpot.x, self.draggingSFHotSpot.y)
	end

	self.bDragging = true

	self:createSessionData(screenFocus)
	self:createAnchor(screenFocus)
	self:subscribeDragEvents()

	if self.offsetEffector then
		self.offsetEffector:destroy()
		self.offsetEffector = nil
	end

	Helpers.attachInPlace(self.draggedEntity, self.anchor)
	self:createOffsetAnimation()

	screenFocus:setDragFocusingPassed( true )

	if self.bStickyDrag then
		screenFocus:setDragClickingPassed(true)
		screenFocus:startStickyDrag(self.draggedEntity)
	else
		screenFocus:startDrag(self.draggedEntity)
	end

	if self.draggingCursor then
		screenFocus:getCursor():setState(self.draggingCursor)
		screenFocus:getCursor():lockState()
	end
end
-------------------------------------------------------------------------------

--[[
 * Метод описывает механизм окончания перетаскивания.
--]]
function DragObjectBase:stopDrag()
	local parent	  = self.sessionData.parent
	local screenFocus = self.sessionData.screenFocus

	if self.offsetEffector then
		self.offsetEffector:destroy()
		self.offsetEffector = nil
	end

	Helpers.attachInPlace( self.draggedEntity, parent )
	self.draggedEntity:setMaxOrder()

	if self.draggingCursor and screenFocus:getCursor():stateIsLocked() then
		screenFocus:getCursor():unlockState()
		screenFocus:getCursor():setState('CursorDefault')
	end

	self:destroyAnchor()
	self:unsubscribeDragEvents()

	self.bDragging = false

	if self.oldSFHotSpot then
		screenFocus:setHotSpotXY(self.oldSFHotSpot.x, self.oldSFHotSpot.y)
		self.oldSFHotSpot = nil
	end
end
-------------------------------------------------------------------------------

--[[
 * Проверка, перетаскивается ли в данный момент объект.
--]]
function DragObjectBase:isDragging()
	return self.bDragging
end
-------------------------------------------------------------------------------

--[[
 * Метод вызывается после окончания перетаскивания зразу за callback-ом onDrop,
 * если в нем не произошла отмена вызова. Предназначен для выполнения каких-либо
 * действий по-умолчанию, направленых на корректировку параметров объекта перетаскивания.
 * Например: в данном базовом классе выполняется возврат прежнего размера объекта и
 * установка слоя (order), который он занимал до начала перетаскивания. Вызов данного
 * метода можно отменять или подменять в callback-е onDrop. Таким образом можно
 * задавать поведение DragObjectBase в зависимости от конкретной ситуации.
--]]
function DragObjectBase:defaultAfterDrop()
	self.draggedEntity:setScale( self.sessionData.scale.x, self.sessionData.scale.y )
	self.draggedEntity:setOrder( self.sessionData.order )
end
---------------------------------------------------------------------------

--[[
 * Метод помощает якорь перетаскивания в указанные экранные координаты, с поправкой
 * на пределы перетаскивания.
--]]
function DragObjectBase:placeTo(scrPos)
	local parent = self.anchor:getParent()
	local pos	 = parent:screenToLocal( scrPos )

	if self.bounds then
		local leftTop	  = self.bounds:getLeftTop()
		local rightBottom = self.bounds:getRightBottom()

		pos.x = math.min(rightBottom.x, math.max(leftTop.x, pos.x))
		pos.y = math.min(rightBottom.y, math.max(leftTop.y, pos.y))
	end

	self.anchor:setPosition( pos )
end
-------------------------------------------------------------------------------

--[[
 * Метод, в котором описана логика начала перемещения объекта. Вызывается в ответ
 * на движковый сигнал onClickBegin.
--]]
function DragObjectBase:takeProcess(sender, screenFocus)
	--self:startDrag(screenFocus)

	local onTake = self:getCallback('onTake')
	if onTake then
		local event	  = {}
		event.sender  = self
		event.objName = self.draggedEntity:getName()

		onTake(event)
	end

	self:startDrag(screenFocus)

	local afterTake = self:getCallback('afterTake')
	if afterTake then
		local event	  = {}
		event.sender  = self
		event.objName = self.draggedEntity:getName()

		afterTake(event)
	end
end
-------------------------------------------------------------------------------

--[[
 * Метод, в котором описана логика самого перемещения объекта. Вызывается в ответ
 * на движковый сигнал onDragFocusKeep.
--]]
function DragObjectBase:dragProcess(sender, screenFocus)
	local bPreventDefaultMove = false
	local beforeMove = self:getCallback('beforeMove')

	if beforeMove then
		local event				  = {}
		event.sender			  = self
		event.objName			  = self.draggedEntity:getName()
		event.bPreventDefaultMove = false

		beforeMove(event)
		bPreventDefaultMove = event.bPreventDefaultMove
	end
	
	if not bPreventDefaultMove then
		local scrPos = Vector2f(screenFocus:getScrX(),
								screenFocus:getScrY())

		self:placeTo( scrPos )
	end

	local afterMove = self:getCallback('afterMove')

	if afterMove then
		local event	  = {}
		event.sender  = self
		event.objName = self.draggedEntity:getName()

		afterMove(event)
	end
end
-------------------------------------------------------------------------------

--[[
 * Метод, в котором описывается вся логика сброса (drop) объекта. Вызывается в ответ
 * на движковые сигналы onDragFinish и onDragAbort.
--]]
function DragObjectBase:dropProcess(bAborted, sender, screenFocus)
	self:stopDrag()
	
	local callback	= self:getCallback('onDrop')
	local afterDrop	= bind(self.defaultAfterDrop, self)

	if callback then
		local event		= {}
		event.sender	= self
		event.objName	= self.draggedEntity:getName()
		event.afterDrop	= afterDrop
		event.bAborted	= bAborted

		callback(event)

		afterDrop = event.afterDrop
	end

	if afterDrop then
		afterDrop()
	end

	self.sessionData.screenFocus = nil
end
-------------------------------------------------------------------------------

--[[
 * Обработчик сигнала перетаскивания объекта на другой объект. В этом классе
 * используется для определения, открылся ли другой screen. Если открылся, то
 * происходит сбрасывание перетаскиваемого объекта.
 * @param   sender          Объект, который перетаскивается.
 * @param   screenFocus     Объект, отвечающий за логику курсора/тача.
--]]
function DragObjectBase:onDragFocusIn(sender, screenFocus)
	if not self:isDragWithinGameScene() then
		self:drop()
	end
end
-------------------------------------------------------------------------------

--[[
 * Метод проверяет перетаскивается ли объект в пределах игровой сцены или уже
 * был открыт другой скрин. Если открылся, то под курсором будет объект, не
 * относящийся к главному объекту игровой сцены.
--]]
function DragObjectBase:isDragWithinGameScene()
	local screenFocus = self.sessionData.screenFocus
	local entity	  = screenFocus:getPointedResponder()

	return  entity:getName() == self.scene:getMainEntity():getName() or
			entity:hasParent( self.scene:getMainEntity() )
end
-------------------------------------------------------------------------------

function DragObjectBase:onDragClickBegin(sender, screenFocus)
	local callback = self:getCallback('onDragClick')
	local bCancelDrop = false
	local bPassClick  = self:checkPassClickBegin(screenFocus)

	if callback then
		local event		  = {}
		event.sender	  = self
		event.target	  = screenFocus:getPointedResponder()
		event.bCancelDrop = bCancelDrop 
		event.bPassClick  = bPassClick

		callback(event)
		bCancelDrop	= event.bCancelDrop
		bPassClick	= event.bPassClick
	end

	if not bPassClick then
		if not bCancelDrop then
			self:drop()
		end
		sender:discardEvent()
	end
end
-------------------------------------------------------------------------------

function DragObjectBase:checkPassClickBegin(screenFocus)
	if self.bZoomOpenEnabled then
		local zoomZones = self.scene:getPlayField():getZones()
		local responder = screenFocus:getPointedResponder()

		for i, zoom in ipairs(zoomZones) do
			if not zoom:isDisabled() and zoom:getTriggerName() == responder:getName() then
				return true
			end
		end
	end

	return false
end
-------------------------------------------------------------------------------