--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| 	Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]


if ( SERVER ) then return end

dragndrop = {}

function dragndrop.Clear()

	dragndrop.m_Receiver		= nil
	dragndrop.m_ReceiverSlot	= nil
	dragndrop.m_HoverStart		= nil
	dragndrop.m_MouseCode		= 0
	dragndrop.m_DragWatch		= nil
	dragndrop.m_MouseX			= 0
	dragndrop.m_MouseY			= 0
	dragndrop.m_DraggingMain	= nil
	dragndrop.m_Dragging		= nil
	dragndrop.m_DropMenu		= nil

end

function dragndrop.IsDragging()

	if ( dragndrop.m_Dragging != nil ) then return true end

	return false

end

function dragndrop.HandleDroppedInGame()

	local panel = vgui.GetHoveredPanel()
	if ( !IsValid( panel ) ) then return end
	if ( panel:GetClassName() != "CGModBase" ) then return end

end

function dragndrop.Drop()

	if ( dragndrop.HandleDroppedInGame() ) then
		dragndrop.StopDragging()
		return
	end

	-- Show the menu
	if ( dragndrop.m_MouseCode == MOUSE_RIGHT && dragndrop.m_ReceiverSlot && dragndrop.m_ReceiverSlot.Menu ) then

		local x, y = dragndrop.m_Receiver:LocalCursorPos()

		local menu = DermaMenu()
		menu.OnRemove = function( m ) -- If user clicks outside of the menu - drop the dragging
			dragndrop.StopDragging()
		end

		for k, v in pairs( dragndrop.m_ReceiverSlot.Menu ) do

			menu:AddOption( v, function()

				dragndrop.CallReceiverFunction( true, k, x, y )
				dragndrop.StopDragging()

			end )

		end

		menu:Open()

		dragndrop.m_DropMenu = menu

		return

	end

	dragndrop.CallReceiverFunction( true, nil, nil, nil )
	dragndrop.StopDragging()

end

function dragndrop.StartDragging()

	if ( !dragndrop.m_DragWatch:IsSelected() ) then

		dragndrop.m_Dragging = { dragndrop.m_DragWatch }

	else

		local canvas = dragndrop.m_DragWatch:GetSelectionCanvas()
		dragndrop.m_Dragging = {}

		for k, v in pairs( canvas:GetSelectedChildren() ) do

			if ( !v.m_DragSlot ) then continue end

			table.insert( dragndrop.m_Dragging, v )

		end

	end

	for k, v in pairs( dragndrop.m_Dragging ) do

		if ( !IsValid( v ) ) then continue end

		v:OnStartDragging()

	end

	dragndrop.m_DraggingMain = dragndrop.m_DragWatch
	dragndrop.m_DraggingMain:MouseCapture( true )
	dragndrop.m_DragWatch = nil

end

function dragndrop.StopDragging()

	if ( IsValid( dragndrop.m_Receiver ) ) then
		dragndrop.m_Receiver:DragHoverEnd()
		dragndrop.m_Receiver = nil
	end

	for k, v in pairs( dragndrop.m_Dragging or {} ) do

		if ( !IsValid( v ) ) then continue end
		v:OnStopDragging()

	end

	dragndrop.Clear()

end

function dragndrop.UpdateReceiver()

	local hovered = vgui.GetHoveredPanel()
	local receiver = nil
	local receiverslot = nil

	if ( IsValid( hovered ) ) then
		receiver, receiverslot = hovered:GetValidReceiverSlot()
	end

	if ( IsValid( dragndrop.m_Receiver ) ) then

		if ( receiver == dragndrop.m_Receiver ) then return end

		dragndrop.m_Receiver:DragHoverEnd()

	end

	if ( !IsValid( receiver ) ) then
		dragndrop.m_Receiver = nil
		dragndrop.m_ReceiverSlot = nil
	end

	dragndrop.m_Receiver = receiver
	dragndrop.m_ReceiverSlot = receiverslot

end

--
-- Return all the dragged panels that match this name
--
function dragndrop.GetDroppable( name )

	if ( !name ) then return dragndrop.m_Dragging end
	if ( !dragndrop.m_Dragging ) then return end

	local t = {}
	for id, pnl in pairs( dragndrop.m_Dragging ) do
		if ( pnl.m_DragSlot && pnl.m_DragSlot[ name ] ) then table.insert( t, pnl ) end
	end
	return t

end

function dragndrop.CallReceiverFunction( bDoDrop, command, mx, my )

	if ( !dragndrop.m_ReceiverSlot ) then return end
	if ( !IsValid( dragndrop.m_Receiver ) ) then return end

	local x, y = dragndrop.m_Receiver:LocalCursorPos()
	if ( mx ) then x = mx end
	if ( my ) then y = my end

	if ( dragndrop.m_ReceiverSlot.Func ) then

		local droppable = dragndrop.GetDroppable( dragndrop.m_ReceiverSlot.Name )

		dragndrop.m_ReceiverSlot.Func( dragndrop.m_Receiver, droppable, bDoDrop, command, x, y )

	end

end

function dragndrop.Think()

	if ( IsValid( dragndrop.m_DropMenu ) ) then return end

	--
	-- We're dragging but no mouse buttons are down..
	-- So force the drop whereever it is!
	--
	--[[if ( dragndrop.m_Dragging != nil && !input.IsMouseDown( MOUSE_LEFT ) && !input.IsMouseDown( MOUSE_RIGHT ) ) then
		dragndrop.m_Dragging:DragMouseRelease( dragndrop.m_MouseCode )
		return
	end]]

	--
	-- We're holding down a panel, watch for start of dragging
	--
	if ( IsValid( dragndrop.m_DragWatch ) ) then

		local dist = math.abs( dragndrop.m_MouseX - gui.MouseX() ) + math.abs( dragndrop.m_MouseY - gui.MouseY() )
		if ( dist > 20 ) then
			dragndrop.StartDragging()
			return
		end

	end

	if ( dragndrop.m_Dragging != nil ) then

		dragndrop.HoverThink()
		dragndrop.UpdateReceiver()

		if ( IsValid( dragndrop.m_Receiver ) ) then
			dragndrop.CallReceiverFunction( false )
		end

	end

end

hook.Add( "DrawOverlay", "DragNDropPaint", function()

	if ( dragndrop.m_Dragging == nil ) then return end
	if ( dragndrop.m_DraggingMain == nil ) then return end
	if ( IsValid( dragndrop.m_DropMenu ) ) then return end

	local hold_offset_x = 65535
	local hold_offset_y = 65535

	-- Find the top, left most panel
	for k, v in pairs( dragndrop.m_Dragging ) do

		if ( !IsValid( v ) ) then continue end

		hold_offset_x = math.min( hold_offset_x, v.x )
		hold_offset_y = math.min( hold_offset_y, v.y )

	end

	local wasEnabled = DisableClipping( true )

		local Alpha = 0.7
		if ( IsValid( dragndrop.m_Hovered ) ) then Alpha = 0.8 end
		surface.SetAlphaMultiplier( Alpha )

			local ox = gui.MouseX() - hold_offset_x + 8
			local oy = gui.MouseY() - hold_offset_y + 8

			for k, v in pairs( dragndrop.m_Dragging ) do

				if ( !IsValid( v ) ) then continue end

				local dist = 512 - v:Distance( dragndrop.m_DraggingMain )

				if ( dist < 0 ) then continue end

				dist = dist / 512
				surface.SetAlphaMultiplier( Alpha * dist )

				v.PaintingDragging = true
				v:PaintAt( ox + v.x - v:GetWide() / 2, oy + v.y - v:GetTall() / 2 ) -- fill the gap between the top left corner and the mouse position
				v.PaintingDragging = nil

			end

		surface.SetAlphaMultiplier( 1.0 )

	DisableClipping( wasEnabled )

end )
hook.Add( "Think", "DragNDropThink", dragndrop.Think )





--
--
--
-- Panel Drag n Drop Extensions
--
--
--

local meta = FindMetaTable( "Panel" )

--
-- Make this panel droppable
--
function meta:Droppable( name )

	self.m_DragSlot = self.m_DragSlot or {}

	self.m_DragSlot[ name ] = {}

	return self.m_DragSlot[ name ]

end

--
-- Make this pannel a drop target
--
function meta:Receiver( name, func, menu )

	self.m_ReceiverSlot = self.m_ReceiverSlot or {}

	self.m_ReceiverSlot[ name ] = {}
	self.m_ReceiverSlot[ name ].Name = name
	self.m_ReceiverSlot[ name ].Func = func
	self.m_ReceiverSlot[ name ].Menu = menu

end

--
-- Drag parent means that when we start to
-- drag this panel, we'll really start
-- dragging the defined parent
--
function meta:SetDragParent( parent )
	self.m_pDragParent = parent
end

function meta:GetValidReceiverSlot()

	if ( self.m_ReceiverSlot ) then

		-- Find matching slot..
		for k, v in pairs( self.m_ReceiverSlot ) do

			if ( !dragndrop.m_DraggingMain.m_DragSlot ) then continue end

			local slot = dragndrop.m_DraggingMain.m_DragSlot[ k ]
			if ( !slot ) then continue end

			return self, v

		end

	end

	if ( !IsValid( self:GetParent() ) ) then
		return false
	end

	return self:GetParent():GetValidReceiverSlot()

end

function meta:IsDraggable()

	return self.m_DragSlot != nil

end

function meta:IsDragging()

	if ( !self.m_DragSlot ) then return false end

	return self.Dragging

end

function meta:DroppedOn( pnl )
	-- For override.
end

function meta:OnDrop()

	-- We're being dropped on something
	-- we can create a new panel here and return it, so that instead of
	-- dropping us - it drops the new panel instead! We remain where we are!

	-- By default we return ourself

	return self

end

function meta:OnStartDragging()

	self.Dragging = true
	self:InvalidateLayout()

	if ( self:IsSelectable() ) then

		local canvas = self:GetSelectionCanvas()

		if ( IsValid( canvas ) && !self:IsSelected() ) then
			canvas:UnselectAll()
		end

	end

end

function meta:OnStopDragging()
	self.Dragging = false
end

function meta:DragMousePress( mcode )

	if ( IsValid( dragndrop.m_DropMenu ) ) then return end
	if ( dragndrop.IsDragging() ) then dragndrop.StopDragging() return end

	if ( IsValid( self.m_pDragParent ) and self.m_pDragParent ~= self ) then
		return self.m_pDragParent:DragMousePress( mcode )
	end

	if ( !self.m_DragSlot ) then return end

	dragndrop.Clear()
	dragndrop.m_MouseCode = mcode
	dragndrop.m_DragWatch = self
	dragndrop.m_MouseX = gui.MouseX()
	dragndrop.m_MouseY = gui.MouseY()

end

function meta:DragClick( mcode )

	self:MouseCapture( true )
	-- Clicking one mouse button while dragging with another!
	-- Return true to stop us clicking and selecting stuff below..
	return true

end

function meta:DragMouseRelease( mcode )

	if ( IsValid( dragndrop.m_DropMenu ) ) then return end

	-- This wasn't the button we clicked with - so don't release drag
	if ( dragndrop.IsDragging() && dragndrop.m_MouseCode != mcode ) then

		return self:DragClick( mcode )

	end

	if ( !dragndrop.IsDragging() ) then
		dragndrop.Clear()
		return false
	end

	dragndrop.Drop()

	-- Todo.. we should only do this if we enabled it!
	if ( gui.EnableScreenClicker ) then
		gui.EnableScreenClicker( false )
	end

	self:MouseCapture( false )
	return true

end

function meta:SetDropTarget( x, y, w, h )

	if ( !self.m_bDrawingPaintOver ) then
		self.m_OldPaintOver = self.PaintOver
		self.m_bDrawingPaintOver = true
	end

	self.PaintOver = function()

		if ( self.m_OldPaintOver ) then
			self:m_OldPaintOver()
		end

		self:DrawDragHover( x, y, w, h )

	end

end



--
-- Drag Hover
--
-- These functions are used for things like trees
-- So that when you hover over the tree while dragging something
-- it will open up the tree. This works regardless of whether the
-- is droppable or not.
--
-- Implement DragHoverClick in your panel class to get this functionality
--
function meta:DragHover( HoverTime )

	--
	-- Call DragHoverClick if we've been hovering for 0.1 seconds..
	--
	if ( HoverTime < 0.1 ) then dragndrop.m_bHoverClick = false end
	if ( HoverTime > 0.1 && !dragndrop.m_bHoverClick ) then

		self:DragHoverClick( HoverTime )
		dragndrop.m_bHoverClick = true

	end

end

function meta:DrawDragHover( x, y, w, h )

	DisableClipping( true )

	surface.SetDrawColor( 255, 0, 255, 100 )
	surface.DrawRect( x, y, w, h )

	surface.SetDrawColor( 255, 220, 255, 230 )
	surface.DrawOutlinedRect( x, y, w, h )

	surface.SetDrawColor( 255, 100, 255, 50 )
	surface.DrawOutlinedRect( x - 1, y - 1, w + 2, h + 2 )

	DisableClipping( false )

end

function meta:DragHoverEnd()

	if ( !self.m_bDrawingPaintOver ) then return end

	self.PaintOver = self.m_OldPaintOver
	self.m_bDrawingPaintOver = false

end

function meta:DragHoverClick( HoverTime )
end




--
--
-- This is called to open stuff when you're hovering over it.
--
--

local LastHoverThink = nil
local LastHoverChangeTime = 0
local LastX = 0
local LastY = 0

function dragndrop.HoverThink()

	local hovered = vgui.GetHoveredPanel()
	local x = gui.MouseX()
	local y = gui.MouseY()

	-- Hovering a different panel
	if ( LastHoverThink != hovered or x != LastX or y != LastY ) then

		LastHoverChangeTime = SysTime()
		LastHoverThink = hovered

	end

	-- Hovered panel might do stuff when we're hovering it
	-- so give it a chance to do that now.
	if ( IsValid( LastHoverThink ) ) then

		LastX = x
		LastY = y
		LastHoverThink:DragHover( SysTime() - LastHoverChangeTime )

	end

end
