function StartDrag(obj_name, bLockCenter, deltaZ)
    bLockCenter = (bLockCenter == nil) and false or bLockCenter
    
    local object   = play_field:GetObject(obj_name)
    local mousePos = GetMousePos()
    
    local info     = {}
    info.obj_name  = obj_name
    info.dx        = (bLockCenter) and 0 or (object:GetScrX() - mousePos.x)
    info.dy        = (bLockCenter) and 0 or (object:GetScrY() - mousePos.y)
    info.oldZ      = object:GetScrZ()
    info.oldLayer  = object:GetLayer()
    
    if deltaZ then
        object:SetScrZ(  object:GetScrZ() + deltaZ)
    else
        object:SetScrZ(  play_field:GetMaxObjectZ() + 0.1 )
        object:SetLayer( play_field:GetLevelScreen():GetPanel():GetLayer() + 0.1 )
    end
    
    play_field:IgnoreOnTestPoint(obj_name)
    SetCursor('CursorTool')
    
    GetCurrentLevel().drag_info = info
end
----------------------------------------------------------------------------------

function StopDrag(set_old_z)
    local level = GetCurrentLevel()
    local info  = level.drag_info
    
    if info then
        set_old_z = (set_old_z == nil) and true or set_old_z
        
        if set_old_z then
            if info.oldZ then
                play_field:GetObject(info.obj_name):SetScrZ(info.oldZ)
            end
            
            if info.oldLayer then
                play_field:GetObject( info.obj_name ):SetLayer( info.oldLayer )
            end
        end
        
        play_field:DoNotIgnoreOnTestPoint(info.obj_name)
        level.cursor_to_restore = 'CursorDefault'
        level.drag_info = nil
    end
end
----------------------------------------------------------------------------------

function DropDragingObject(obj_name, time)
    if IsDragging() then
        time = time or DROP_OBJECT_TIME
        
        local level      = GetCurrentLevel()
        local drag_info  = level.drag_info
        local dragObject = play_field:GetObject(drag_info.obj_name)
        local info       = level.drag_objects[drag_info.obj_name]

        local oldZ      = drag_info.oldZ
        local oldLayer  = drag_info.oldLayer
        
        StopDrag(false)
        
        if info.bLockTargetArea then
            info.setTargetsIgnored(true)
        end
        
        if info.get_area then
            play_field:GetObject(info.get_area):SetVisible(true)
        end
        
        local event          = {}
        event.target         = obj_name
        event.objName        = drag_info.obj_name
        event.baseX          = info.baseX
        event.baseY          = info.baseY
        event.oldZ           = oldZ
        event.oldLayer       = oldLayer
        event.time           = time
        event.preventDefault = false
        
        if info.on_drop then
            info.on_drop(event)
        end
        
        if not event.preventDefault then
            if event.time == 0 then
                dragObject:SetX(event.baseX)
                dragObject:SetY(event.baseY)
                
                if oldZ then
                    dragObject:SetLayer(oldLayer)
                    dragObject:SetScrZ(oldZ)
                end
            else
                dragObject:MoveTo(event.baseX, event.baseY, event.time)
                info.obj_fly = true
                
                level.timer:Add(function()
                                    if oldZ then
                                        dragObject:SetLayer(oldLayer)
                                        dragObject:SetScrZ(oldZ)
                                    end
                                    info.obj_fly = nil
                                end,
                                event.time)
            end
        end
    end
end
----------------------------------------------------------------------------------

function IsDragging(obj_name)
    local info = GetCurrentLevel().drag_info
    
    if info then
        return (obj_name) and obj_name == info.obj_name or true
    end
    
    return false
end
----------------------------------------------------------------------------------

function GetDraggingObjectName()
    local info = GetCurrentLevel().drag_info
    
    if info then
        return info.obj_name
    end
    
    return nil
end
----------------------------------------------------------------------------------

function GetDragObjectNameByGetArea(getArea)
    local level = GetCurrentLevel()
    
    for objectName, info in pairs(level.drag_objects) do
        if info.getObjectArea() == getArea then
            return objectName
        end
    end
    
    return nil
end
----------------------------------------------------------------------------------

function IsObjectDropTarget(obj_name)
    local state = GetCurrentLevel().name..'('..obj_name..')'
    return HasGameState(OBJECT_DROP_TARGET, state)
end
----------------------------------------------------------------------------------

function StartDragObjectRequest(obj_name, bIgnoreZoneCheck)
    local level = GetCurrentLevel()
    
    if level.drag_objects
        and not level:IsLockedObject(obj_name)
        and (bIgnoreZoneCheck or (level.locker:GetNReferences() == 0 or IsActiveZoneObject(obj_name)))
    then
        for drag_obj_name, info in pairs(level.drag_objects) do
            if info.getObjectArea() == obj_name then
                if not info.obj_fly and not IsObjectDropTarget(drag_obj_name) then 
                    
                    if info.bLockTargetArea then
                        info.setTargetsIgnored(false)
                    end
                    
                    if info.get_area then
                        play_field:GetObject(info.get_area):SetVisible(false)
                    end
                    
                    info.baseX = play_field:GetObject(drag_obj_name):GetX()
                    info.baseY = play_field:GetObject(drag_obj_name):GetY()
                    
                    if info.on_get then
                        local event   = {}
                        event.objName = drag_obj_name
                        event.target  = info.get_area
                        event.baseX   = info.baseX
                        event.baseY   = info.baseY
                        
                        info.on_get(event)
                    end
                    
                    StartDrag(drag_obj_name, info.bLockCenter, info.deltaZ)
                end
                return true
            end
        end
    end
    return false
end
----------------------------------------------------------------------------------

function DropObjectRequest(aboveObject, bOnUpEvent)
    if IsDragging() then
        local level         = GetCurrentLevel()
        local drag_obj_name = level.drag_info.obj_name
        local drag_obj      = play_field:GetObject(drag_obj_name)
        local info          = level.drag_objects[drag_obj_name]
        
        if bOnUpEvent ~= info.bDropOnUp then
            return false
        end
        
        if aboveObject then
            if info.target_area then
                
                ------------------------
                local function dropTargetProcess(target)
                    StopDrag()
                    info.setDropTargetArea(target)
                    
                    local dropAllTargetAreas = type(info.target_area) == 'string' or
                                               (info.dropTargetsCount == #info.target_area)
                    
                    if dropAllTargetAreas then
                        AddGameState(OBJECT_DROP_TARGET, level.name..'('..drag_obj_name..')')
                        level.custom_cursors[info.getObjectArea()] = nil
                    else
                        if info.get_area then
                            play_field:GetObject(info.get_area):SetVisible(true)
                        end
                    end
                    
                    if info.on_drop_target then
                        local event              = {}
                        event.save               = false
                        event.target             = target
                        event.get_area           = info.get_area
                        event.objName            = drag_obj_name
                        event.baseX              = info.baseX
                        event.baseY              = info.baseY
                        event.dropAllTargetAreas = dropAllTargetAreas
                        
                        play_field:IgnoreOnTestPoint(target)
                        info.on_drop_target(event)
                    end
                end
                ------------------------
                
                if type(info.target_area) == 'table' then
                    for i, target in pairs(info.target_area) do
                        if ( info.bIntersectionMode and IsIntersect(drag_obj, Object(target), 2) ) or target == aboveObject:GetName() then
                            dropTargetProcess(target)
                            return true
                        end
                    end
                elseif ( info.bIntersectionMode and IsIntersect(drag_obj, Object(info.target_area), 2) ) or info.target_area == aboveObject:GetName() then
                    dropTargetProcess(info.target_area)
                    return true
                end
            end
            
            if not bOnUpEvent then
                local aboveObjectName = aboveObject:GetName()
                
                if not level:IsLockedObject(aboveObjectName) and
                    (level.locker:GetNReferences() == 0 or IsActiveZoneObject(aboveObjectName))
                then
                    for _, zone in pairs(level.zones) do
                        if zone.trigger_name == aboveObjectName and not zone:IsDisabled() then
                            zone:Open()
                            return true
                        end
                    end
                end
            end
        end
        
        local aboveObjectName = aboveObject and aboveObject:GetName() or nil
        DropDragingObject(aboveObjectName)
        return true
    end
    
    return false
end
----------------------------------------------------------------------------------

function ResetDragingObject()
    if IsDragging() then
        DropDragingObject(nil, 0)
    end
end
----------------------------------------------------------------------------------

function ResetDragObject(objectName)
    local level = GetCurrentLevel()
    local info  = level.drag_objects[objectName]
    
    assert(info)
    
    if type(info.target_area) == 'table' then
        for i, targetName in ipairs(info.target_area) do
            info.removeDropTargetArea(targetName)
        end
    else
        info.removeDropTargetArea(info.target_area)
    end
    
    local state = level.name..'('..objectName..')'
    if HasGameState(OBJECT_DROP_TARGET, state) then
        RemoveGameState(OBJECT_DROP_TARGET, state)
    end
    
    info.setTargetsIgnored(info.bLockTargetArea)
    
    level.custom_cursors[info.getObjectArea()] = 'CursorActive'
end
----------------------------------------------------------------------------------

function RegisterDragObject(obj_name, get_area, target_area, on_get, on_drop, on_drop_target, rect, bLockCenter, bLockTargetArea, bIntersectionMode, bDropOnUp)
    local level  = GetCurrentLevel()
    local object = play_field:GetObject(obj_name)
    -------------------------
    
    local info              = {}
    info.baseX              = object:GetX()
    info.baseY              = object:GetY()
    info.get_area           = (obj_name ~= get_area) and get_area or nil
    info.target_area        = target_area
    info.on_get             = on_get
    info.on_drop            = on_drop
    info.on_drop_target     = on_drop_target
    info.rect               = rect
    info.bLockCenter        = bLockCenter       or false
    info.bLockTargetArea    = bLockTargetArea   or false
    info.bIntersectionMode  = bIntersectionMode or false
    info.bDropOnUp          = bDropOnUp         or false
    info.dropTargetsCount   = 0
    
    level.drag_objects[obj_name] = info
    -------------------------
    
    info.getObjectArea = function()
        if info.get_area then
            return info.get_area
        end
        return obj_name
    end
    -------------------------
    
    info.getDropTargetAreaState = function(targetName)
        return level.name..'(OBJ:'..obj_name..'; TARGET:'..targetName..')'
    end
    -------------------------
    
    info.isDropTargetArea = function(targetName)
        local state = info.getDropTargetAreaState(targetName)
        return HasGameState(OBJECT_DROP_TARGET_AREA, state)
    end
    -------------------------
    
    info.setDropTargetArea = function(targetName)
        local state = info.getDropTargetAreaState(targetName)
        
        if not HasGameState(OBJECT_DROP_TARGET_AREA, state) then
            AddGameState(OBJECT_DROP_TARGET_AREA, state)
            
            info.dropTargetsCount = info.dropTargetsCount + 1
        end
    end
    -------------------------
    
    info.removeDropTargetArea = function(targetName)
        local state = info.getDropTargetAreaState(targetName)
        
        if HasGameState(OBJECT_DROP_TARGET_AREA, state) then
            RemoveGameState(OBJECT_DROP_TARGET_AREA, state)
            
            info.dropTargetsCount = info.dropTargetsCount - 1
        end
    end
    -------------------------
    
    info.setTargetsIgnored = function(bIgnored)
        ----------
        local ignoreProcess = function(target)
            if bIgnored then
                play_field:IgnoreOnTestPoint(target)
            elseif not info.isDropTargetArea(target) then
                play_field:DoNotIgnoreOnTestPoint(target)
            end
        end
        ----------
        
        if type(info.target_area) == 'string' then
            ignoreProcess(info.target_area)
        else
            for i, target in pairs(info.target_area) do
                ignoreProcess(target)
            end
        end
    end
    -------------------------
    
    if info.bLockTargetArea then
        info.setTargetsIgnored(true)
    end
    -------------------------
    
    if IsObjectDropTarget(obj_name) then 
        if info.get_area then
            play_field:GetObject(info.get_area):SetVisible(false)
        end
    else
        level.custom_cursors[info.getObjectArea()] = 'CursorActive'
    end
    -------------------------
    
    if info.on_drop_target and info.target_area then
        local target_area_type = type(info.target_area)
        
        if target_area_type == 'string' then
            if info.isDropTargetArea(info.target_area) then
                info.dropTargetsCount = info.dropTargetsCount + 1
                
                local event              = {}
                event.save               = true
                event.target             = info.target_area
                event.get_area           = info.get_area
                event.objName            = obj_name
                event.baseX              = info.baseX
                event.baseY              = info.baseY
                event.dropAllTargetAreas = true
                
                play_field:IgnoreOnTestPoint(info.target_area)
                on_drop_target(event)
            end
            
        elseif target_area_type == 'table' then
            for i, target in pairs(info.target_area) do
                if info.isDropTargetArea(target) then
                    info.dropTargetsCount = info.dropTargetsCount + 1
                    
                    local event              = {}
                    event.save               = true
                    event.target             = target
                    event.get_area           = info.get_area
                    event.objName            = obj_name
                    event.baseX              = info.baseX
                    event.baseY              = info.baseY
                    event.dropAllTargetAreas = (info.dropTargetsCount == #info.target_area)
                    
                    play_field:IgnoreOnTestPoint(target)
                    on_drop_target(event)
                end
            end
        end
    end
    
    return info
end
----------------------------------------------------------------------------------