--[[
| 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/
--]]


TOOL.Category = "Robotboy655"
TOOL.Name = "#tool.rb655_easy_animation.name"
TOOL.AnimationArray = {}

TOOL.ClientConVar[ "anim" ] = ""
TOOL.ClientConVar[ "speed" ] = "1.0"
TOOL.ClientConVar[ "delay" ] = "0"
TOOL.ClientConVar[ "nohide" ] = "0"
TOOL.ClientConVar[ "loop" ] = "0"
TOOL.ClientConVar[ "noglow" ] = "0"

TOOL.ServerConVar[ "nobbox_sv" ] = "0"
CreateConVar( "rb655_easy_animation_nobbox_sv", "0", FCVAR_ARCHIVE )

local function MakeNiceName( str )
	local newname = {}
	for _, s in pairs( string.Explode( "_", string.Replace( str, " ", "_" ) ) ) do
		if ( string.len( s ) == 0 ) then continue end
		if ( string.len( s ) == 1 ) then table.insert( newname, string.upper( s ) ) continue end
		table.insert( newname, string.upper( string.Left( s, 1 ) ) .. string.Right( s, string.len( s ) - 1 ) ) -- Ugly way to capitalize first letters.
	end

	return table.concat( newname, " " )
end

local function IsEntValid( ent )
	if ( !IsValid( ent ) or ent:IsWorld() ) then return false end
	if ( table.Count( ent:GetSequenceList() or {} ) != 0 ) then return true end
	return false
end

local function PlayAnimationBase( ent, anim, speed )
	if ( !IsValid( ent ) ) then return end

	-- HACK: This is not perfect, but it will have to do
	if ( ent:GetClass() == "prop_dynamic" ) then
		ent:Fire( "SetAnimation", anim )
		ent:Fire( "SetPlaybackRate", math.Clamp( tonumber( speed ), 0.05, 3.05 ) )
		return
	end

	ent:ResetSequence( ent:LookupSequence( anim ) )
	ent:ResetSequenceInfo()
	ent:SetCycle( 0 )
	ent:SetPlaybackRate( math.Clamp( tonumber( speed ), 0.05, 3.05 ) )

end

local UniqueID = 0
function PlayAnimation( ply, ent, anim, speed, delay, loop, isPreview )
	if ( !IsValid( ent ) ) then return end

	delay = tonumber( delay ) or 0
	loop = tobool( loop ) or false

	UniqueID = UniqueID + 1
	local tid = "rb655_animation_loop_" .. ply:UniqueID() .. "-" .. UniqueID

	if ( isPreview ) then tid = "rb655_animation_loop_preview" .. ply:UniqueID() end

	timer.Create( tid, delay, 1, function()
		PlayAnimationBase( ent, anim, speed )
		if ( loop == true and IsValid( ent ) ) then
			timer.Adjust( tid, ent:SequenceDuration() / speed, 0, function()
				if ( !IsValid( ent ) ) then timer.Remove( tid ) return end
				PlayAnimationBase( ent, anim, speed )
			end )
		end
	end )
end

function TOOL:GetSelectedEntity()
	return self:GetWeapon():GetNWEntity( 1 )
end

function TOOL:SetSelectedEntity( ent )
	if ( IsValid( ent ) and ent:GetClass() == "prop_effect" ) then ent = ent.AttachedEntity end
	if ( !IsValid( ent ) ) then ent = NULL end

	if ( self:GetSelectedEntity() == ent ) then return end

	self:GetWeapon():SetNWEntity( 1, ent )
end

local gOldCVar1 = GetConVarNumber( "ai_disabled" )
local gOldCVar2 = GetConVarNumber( "rb655_easy_animation_nohide" )

local gLastSelectedEntity = NULL
function TOOL:Think()
	local ent = self:GetSelectedEntity()
	if ( !IsValid( ent ) ) then self:SetSelectedEntity( NULL ) end

	if ( CLIENT ) then
		if ( gOldCVar1 != GetConVarNumber( "ai_disabled" ) or gOldCVar2 != GetConVarNumber( "rb655_easy_animation_nohide" ) ) then
			gOldCVar1 = GetConVarNumber( "ai_disabled" )
			gOldCVar2 = GetConVarNumber( "rb655_easy_animation_nohide" )
			if ( IsEntValid( ent ) and ent:IsNPC() ) then self:UpdateControlPanel() end
		end
		if ( ent:EntIndex() == gLastSelectedEntity ) then return end
		gLastSelectedEntity = ent:EntIndex()
		self:UpdateControlPanel()
		RunConsoleCommand( "rb655_easy_animation_anim", "" )
	end
end

function TOOL:RightClick( trace )
	if ( SERVER ) then self:SetSelectedEntity( trace.Entity ) end
	return true
end

function TOOL:Reload( trace )
	if ( SERVER ) then
		if ( #self.AnimationArray <= 0 and IsValid( self:GetSelectedEntity() ) ) then
			self:GetSelectedEntity():SetPlaybackRate( 0 )
		elseif ( #self.AnimationArray > 0 ) then
			for id, t in pairs( self.AnimationArray ) do
				if ( IsValid( t.ent ) ) then t.ent:SetPlaybackRate( 0 ) end
			end
		end

		-- Destroy all timers.
		for i = 0, UniqueID do timer.Remove( "rb655_animation_loop_" .. self:GetOwner():UniqueID() .. "-" .. i ) UniqueID = 0 end
		timer.Remove( "rb655_animation_loop_preview" .. self:GetOwner():UniqueID() )
	end
	return true
end

if ( SERVER ) then
	util.AddNetworkString( "rb655_easy_animation_array" )

	function TOOL:LeftClick( trace )
		local ent = self:GetSelectedEntity()
		local anim = self:GetClientInfo( "anim" )

		for i = 0, UniqueID do timer.Remove( "rb655_animation_loop_" .. self:GetOwner():UniqueID() .. "-" .. i ) UniqueID = 0 end
		timer.Remove( "rb655_animation_loop_preview" .. self:GetOwner():UniqueID() )

		if ( #self.AnimationArray > 0 ) then
			for id, t in pairs( self.AnimationArray ) do
				if ( !IsEntValid( t.ent ) or string.len( string.Trim( t.anim ) ) == 0 ) then continue end
				PlayAnimation( self:GetOwner(), t.ent, t.anim, t.speed, t.delay, t.loop )
			end
		else
			if ( !IsEntValid( ent ) or string.len( string.Trim( anim ) ) == 0 ) then return end
			PlayAnimation( self:GetOwner(), ent, anim, self:GetClientInfo( "speed" ), self:GetClientInfo( "delay" ), self:GetClientInfo( "loop" ), true )
		end

		return true
	end

	concommand.Add( "rb655_easy_animation_anim_do", function( ply, cmd, args )
		local tool = ply:GetTool( "rb655_easy_animation" )
		if ( !tool ) then return end

		local ent = tool:GetSelectedEntity()
		if ( !IsEntValid( ent ) ) then return end

		for i = 0, UniqueID do timer.Remove( "rb655_animation_loop_" .. ply:UniqueID() .. "-" .. i ) UniqueID = 0 end
		timer.Remove( "rb655_animation_loop_preview" .. ply:UniqueID() )

		PlayAnimation( ply, ent, args[ 1 ] or "", ply:GetTool( "rb655_easy_animation" ):GetClientInfo( "speed" ), 0, ply:GetTool():GetClientInfo( "loop" ), true )
	end )

	concommand.Add( "rb655_easy_animation_set_pp", function( ply, cmd, args )
		local tool = ply:GetTool( "rb655_easy_animation" )
		if ( !tool ) then return end

		local ent = tool:GetSelectedEntity()
		if ( !IsEntValid( ent ) ) then return end

		local pp_name = ent:GetPoseParameterName( math.floor( tonumber( args[ 1 ] ) ) )
		if ( !pp_name ) then return end

		ent:SetPoseParameter( pp_name, tonumber( args[ 2 ] ) )
	end )

	concommand.Add( "rb655_easy_animation_add", function( ply, cmd, args )
		local tool = ply:GetTool( "rb655_easy_animation" )
		if ( !tool ) then return end
		local e = tool:GetSelectedEntity()
		local a = tool:GetClientInfo( "anim" )
		local s = tool:GetClientInfo( "speed" )
		local d = tool:GetClientInfo( "delay" )
		local l = tool:GetClientInfo( "loop" )
		if ( !IsEntValid( e ) or string.len( string.Trim( a ) ) == 0 ) then return end

		table.insert( tool.AnimationArray, {ent = e, anim = a, speed = s, delay = d, loop = l, ei = e:EntIndex()} )
		net.Start( "rb655_easy_animation_array" )
			net.WriteTable( tool.AnimationArray )
		net.Send( ply )
	end )

	concommand.Add( "rb655_easy_animation_rid", function( ply, cmd, args ) -- rid is for RemoveID
		local tool = ply:GetTool( "rb655_easy_animation" )
		if ( !tool.AnimationArray[ tonumber( args[ 1 ] ) ] ) then return end
		if ( tool.AnimationArray[ tonumber( args[ 1 ] ) ].ei != tonumber( args[ 2 ] ) and tonumber( args[ 2 ] ) != 0 ) then return end

		table.remove( tool.AnimationArray, tonumber( args[ 1 ] ) )
		net.Start( "rb655_easy_animation_array" )
			net.WriteTable( tool.AnimationArray )
		net.Send( ply )
	end )
end

if ( SERVER ) then return end

TOOL.Information = {
	{ name = "info", stage = 1 },
	{ name = "left" },
	{ name = "right" },
	{ name = "reload" },
}

language.Add( "tool.rb655_easy_animation.left", "Play all animations" )
language.Add( "tool.rb655_easy_animation.right", "Select an object to play animations on" )
language.Add( "tool.rb655_easy_animation.reload", "Pause currently playing animation(s)" )

language.Add( "tool.rb655_easy_animation.name", "Easy Animation Tool" )
language.Add( "tool.rb655_easy_animation.desc", "Easy animations for everyone" )
language.Add( "tool.rb655_easy_animation.1", "Use context menu to play animations" )

language.Add( "tool.rb655_easy_animation.animations", "Animations" )
language.Add( "tool.rb655_easy_animation.add", "Add current selection" )
language.Add( "tool.rb655_easy_animation.add.help", "\nIf you want to play animations on multiple entities at one:\n1) Select entity\n2) Select animation from the list, if the entity has any.\n3) Configure sliders to your desire.\n4) Click \"Add current selection\"\n5) Do 1-4 steps as many times as you wish.\n6) Left-click\n\nYou cannot play two animations on the same entity at the same time. The last animation will cut off the first one." )
language.Add( "tool.rb655_easy_animation.speed", "Animation Speed" )
language.Add( "tool.rb655_easy_animation.speed.help", "How fast the animation will play." )
language.Add( "tool.rb655_easy_animation.delay", "Delay" )
language.Add( "tool.rb655_easy_animation.delay.help", "The time between you left-click and the animation is played." )
language.Add( "tool.rb655_easy_animation.loop", "Loop Animation" )
language.Add( "tool.rb655_easy_animation.loop.help", "Play animation again when it ends." )
language.Add( "tool.rb655_easy_animation.nohide", "Do not filter animations" )
language.Add( "tool.rb655_easy_animation.nohide.help", "Enabling this option will show you the full list of animations available for selected entity. Please note, that this list can be so long, that GMod may freeze for a few seconds. For this reason we hide a bunch of animations deemed \"useless\" by default, such as gestures and other delta animations." )
language.Add( "tool.rb655_easy_animation.poseparam.help", "The sliders above are the Pose Parameters. They affect how certain animations look, for example the direction for Team Fortress 2 run animations, etc." )
language.Add( "tool.rb655_easy_animation.poseparam.badent", "Changing Pose Parameters is only supported on Animatable props!" )

language.Add( "tool.rb655_easy_animation.ai", "NPC is selected, but NPC thinking is not disabled! Without that the NPC will reset its animations every frame." )
language.Add( "tool.rb655_easy_animation.ragdoll", "Ragdolls cannot be animated! Open context menu (Hold C) > right click on ragdoll > Make Animatable" )
language.Add( "tool.rb655_easy_animation.prop", "Props cannot be animated properly! Open context menu (Hold C) > right click on entity > Make Animatable" )
language.Add( "tool.rb655_easy_animation.badent", "This entity does not have any animations." )
language.Add( "tool.rb655_easy_animation.noent", "No entity selected." )

language.Add( "tool.rb655_easy_animation.noglow", "Don't render glow/halo around models" )
language.Add( "tool.rb655_easy_animation.noglow.help", "Don't render glow/halo around models when they are selected, and don't draw bounding boxes below animated models. Bounding boxes are a helper for when animations make the ragdolls go outside of their bounding box making them unselectable.\n" )

language.Add( "tool.rb655_easy_animation.property", "Make Animatable" )
language.Add( "tool.rb655_easy_animation.property_bodyxy", "Animate Movement Pose Parameters" )
language.Add( "tool.rb655_easy_animation.property_damageragdoll", "Ragdoll/Gib on Damage" )
language.Add( "tool.rb655_easy_animation.property_ragdoll", "Make Ragdoll" )
language.Add( "prop_animatable", "Animatable Entity" )

function TOOL:GetStage()
	if ( IsValid( self:GetSelectedEntity() ) ) then return 1 end
	return 0
end

net.Receive( "rb655_easy_animation_array", function( len )
	local tool = LocalPlayer():GetTool( "rb655_easy_animation" )
	tool.AnimationArray = net.ReadTable()
	if ( CLIENT ) then tool:UpdateControlPanel() end
end )

function TOOL:UpdateControlPanel( index )
	local panel = controlpanel.Get( "rb655_easy_animation" )
	if ( !panel ) then MsgN( "Couldn't find rb655_easy_animation panel!" ) return end

	panel:ClearControls()
	self.BuildCPanel( panel, self:GetSelectedEntity() )
end

local clr_err = Color( 200, 0, 0 )
function TOOL.BuildCPanel( panel, ent )

	local tool = LocalPlayer() and LocalPlayer():GetTool( "rb655_easy_animation" )
	local nohide = false

	if ( tool ) then
		if ( !IsValid( ent ) ) then ent = tool:GetSelectedEntity() end
		nohide = tool:GetClientNumber( "nohide" ) != 0
	end

	if ( !IsValid( ent ) ) then

		panel:AddControl( "Label", { Text = "#tool.rb655_easy_animation.noent" } ):SetTextColor( clr_err )

	elseif ( IsEntValid( ent ) ) then

		local fine = true

		if ( GetConVarNumber( "ai_disabled" ) == 0 and ent:IsNPC() ) then panel:AddControl( "Label", {Text = "#tool.rb655_easy_animation.ai"} ):SetTextColor( clr_err ) fine = false end
		if ( ent:GetClass() == "prop_ragdoll" ) then panel:AddControl( "Label", { Text = "#tool.rb655_easy_animation.ragdoll" } ):SetTextColor( clr_err ) fine = false end
		if ( ent:GetClass() == "prop_physics" or ent:GetClass() == "prop_physics_multiplayer" or ent:GetClass() == "prop_physics_override" ) then panel:AddControl( "Label", { Text = "#tool.rb655_easy_animation.prop" } ):SetTextColor( clr_err ) end

		local t = {}
		local badBegginings = { "g_", "p_", "e_", "b_", "bg_", "hg_", "tc_", "aim_", "turn", "gest_", "pose_", "pose_", "auto_", "layer_", "posture", "bodyaccent", "a_" }
		local badStrings = { "gesture", "posture", "_trans_", "_rot_", "gest", "aim", "bodyflex_", "delta", "ragdoll", "spine", "arms" }
		for k, v in SortedPairsByValue( ent:GetSequenceList() ) do
			local isbad = false

			for i, s in pairs( badStrings ) do if ( string.find( string.lower( v ), s, 1, true ) != nil ) then isbad = true break end end
			if ( isbad == true and !nohide ) then continue end

			for i, s in pairs( badBegginings ) do if ( s == string.Left( string.lower( v ), string.len( s ) ) ) then isbad = true break end end
			if ( isbad == true and !nohide ) then continue end

			language.Add( "rb655_anim_" .. v, MakeNiceName( v ) )
			t[ "#rb655_anim_" .. v ] = { rb655_easy_animation_anim = v, rb655_easy_animation_anim_do = v }
		end

		if ( fine ) then
			local filter = panel:AddControl( "TextBox", { Label = "#spawnmenu.quick_filter_tool" } )
			filter:SetUpdateOnType( true )

			local animList = panel:AddControl( "ListBox", { Label = "#tool.rb655_easy_animation.animations", Options = t, Height = 225 } )

			-- patch the function to take into account visiblity
			function animList:DataLayout()
				local y = 0
				for k, Line in ipairs( self.Sorted ) do
					if ( !Line:IsVisible() ) then continue end

					Line:SetPos( 1, y )
					Line:SetSize( self:GetWide() - 2, self.m_iDataHeight )
					Line:DataLayout( self )

					Line:SetAltLine( k % 2 == 1 )

					y = y + Line:GetTall()
				end

				return y
			end

			filter.OnValueChange = function( s, txt )
				for id, pnl in pairs( animList:GetCanvas():GetChildren() ) do
					if ( !pnl:GetValue( 1 ):lower():find( txt:lower(), 1, true ) ) then
						pnl:SetVisible( false )
					else
						pnl:SetVisible( true )
					end
				end
				animList:SetDirty( true )
				animList:InvalidateLayout()
			end
		end

	elseif ( !IsEntValid( ent ) ) then

		panel:AddControl( "Label", { Text = "#tool.rb655_easy_animation.badent" } ):SetTextColor( clr_err )

	end

	if ( IsValid( ent ) and ent:GetClass() == "prop_animatable" ) then
		for k = 0, ent:GetNumPoseParameters() - 1 do
			local min, max = ent:GetPoseParameterRange( k )
			local name = ent:GetPoseParameterName( k )

			local ctrl = panel:NumSlider( name, nil, min, max, 2 )
			ctrl:SetHeight( 11 ) -- This makes the controls all bunched up like how we want
			ctrl:DockPadding( 0, -6, 0, -4 ) -- Try to make the lower part of the text visible
			ctrl:SetValue( math.Remap( ent:GetPoseParameter( name ), 0, 1, min, max ) )

			ctrl.OnValueChanged = function( self, value )
				RunConsoleCommand( "rb655_easy_animation_set_pp", k, value )

				--ent:SetPoseParameter( ent:GetPoseParameterName( k ), math.Remap( value, min, max, 0, 1 ) )
				ent:SetPoseParameter( ent:GetPoseParameterName( k ), value )
			end
		end

		if ( ent:GetNumPoseParameters() > 0 ) then
			panel:ControlHelp( "#tool.rb655_easy_animation.poseparam.help" ):DockMargin( 32, 8, 32, 8 )
		end

	elseif ( IsValid( ent ) and ent:GetClass() != "prop_animatable" and ent:GetNumPoseParameters() > 0 ) then
		local errlbl = panel:ControlHelp( "#tool.rb655_easy_animation.poseparam.badent" )
		errlbl:DockMargin( 32, 8, 32, 8 )
		errlbl:SetTextColor( clr_err )
	end

	local pnl = vgui.Create( "DPanelList" )
	pnl:SetHeight( 225 )
	pnl:EnableHorizontal( false )
	pnl:EnableVerticalScrollbar()
	pnl:SetSpacing( 2 )
	pnl:SetPadding( 2 )
	Derma_Hook( pnl, "Paint", "Paint", "Panel" ) -- Awesome GWEN background

	if ( tool and tool.AnimationArray ) then
		for i, d in pairs( tool.AnimationArray ) do
			local s = vgui.Create( "RAnimEntry" )
			s:SetInfo( i, d.ent, d.anim, d.speed, d.delay, d.loop )
			pnl:AddItem( s )
		end
	end

	panel:AddPanel( pnl )

	panel:AddControl( "Button", { Label = "#tool.rb655_easy_animation.add", Command = "rb655_easy_animation_add" } )
	panel:ControlHelp( "#tool.rb655_easy_animation.add.help" )
	panel:AddControl( "Slider", { Label = "#tool.rb655_easy_animation.speed", Type = "Float", Min = 0.05, Max = 3.05, Command = "rb655_easy_animation_speed", Help = true } )
	panel:AddControl( "Slider", { Label = "#tool.rb655_easy_animation.delay", Type = "Float", Min = 0, Max = 32, Command = "rb655_easy_animation_delay", Help = true } )
	panel:AddControl( "Checkbox", { Label = "#tool.rb655_easy_animation.loop", Command = "rb655_easy_animation_loop", Help = true } )
	panel:AddControl( "Checkbox", { Label = "#tool.rb655_easy_animation.nohide", Command = "rb655_easy_animation_nohide", Help = true } )
	panel:AddControl( "Checkbox", { Label = "#tool.rb655_easy_animation.noglow", Command = "rb655_easy_animation_noglow", Help = true } )
end

function TOOL:DrawHUD()
	local ent = self:GetSelectedEntity()
	if ( !IsValid( ent ) or tobool( self:GetClientNumber( "noglow" ) ) ) then return end

	local t = { ent }
	if ( ent.GetActiveWeapon ) then table.insert( t, ent:GetActiveWeapon() ) end
	halo.Add( t, HSVToColor( ( CurTime() * 3 ) % 360, math.abs( math.sin( CurTime() / 2 ) ), 1 ), 2, 2, 1 )
end

local PANEL = {}

function PANEL:Init()
	self.ent = nil
	self.anim = "attack01"
	self.id = 0
	self.eid = 0
	self.speed = 1
	self.delay = 0
	self.loop = false

	self.rem = vgui.Create( "DImageButton", self )
	self.rem:SetImage( "icon16/cross.png" )
	self.rem:SetSize( 16, 16 )
	self.rem:SetPos( 4, 4 )
	self.rem.DoClick = function()
		self:RemoveFull()
	end
end

function PANEL:RemoveFull()
	self.rem:Remove()
	self:Remove()
	RunConsoleCommand( "rb655_easy_animation_rid", self.id, self.eid )
end

function PANEL:Paint( w, h )
	draw.RoundedBox( 2, 0, 0, w, h, Color( 50, 50, 50, 225 ) )
	if ( !self.ent or !IsValid( self.ent ) ) then self:RemoveFull() return end

	surface.SetFont( "DermaDefault" )
	draw.SimpleText( "#" .. self.ent:GetClass(), "DermaDefault", 24, 0, Color( 255, 255, 255, 255 ) )
	draw.SimpleText( "#rb655_anim_" .. self.anim, "DermaDefault", 24, 10, Color( 255, 255, 255, 255 ) )

	local tW = surface.GetTextSize( "#" .. self.ent:GetClass() )
	draw.SimpleText( " #" .. self.ent:EntIndex(), "DermaDefault", 24 + tW, 0, Color( 255, 255, 255, 255 ) )

	local tW2 = surface.GetTextSize( "#rb655_anim_" .. self.anim )
	local t = " [ S: " .. self.speed .. ", D: " .. self.delay
	if ( self.loop ) then t = t .. ", Looping" end
	draw.SimpleText( t .. " ]", "DermaDefault", 24 + tW2, 10, Color( 255, 255, 255, 255 ) )
end

function PANEL:SetInfo( id, e, a, s, d, l )
	self.id = id
	self.eid = e:EntIndex()
	self.ent = e
	self.anim = a
	self.speed = s
	self.delay = d
	self.loop = tobool( l )
end

vgui.Register( "RAnimEntry", PANEL, "Panel" )
