
require ( "usermessage" )
require ( "timer" )

module( "undo", package.seeall )

// undo.Create("Wheel")
// undo.AddEntity( axis )
// undo.AddEntity( constraint )
// undo.SetPlayer( self.Owner )
// undo.Finish()

if ( CLIENT ) then

	local ClientUndos = {}
	local bIsDirty = true
	
	/*---------------------------------------------------------
		GetTable
		Returns the undo table for whatever reason
	---------------------------------------------------------*/
	local function GetTable()
		return ClientUndos
	end
	
	
	/*---------------------------------------------------------
		UpdateUI
		Actually updates the UI. Removes old controls and 
		re-creates them using the new data.
	---------------------------------------------------------*/
	local function UpdateUI()
	
		local Panel = GetControlPanel( "Undo" )
		if (!Panel) then return end
		
		Panel:ClearControls()
		
		local ComboBox = Panel:ComboBox()
		ComboBox:SetTall( 500 )
		
		local NUM = 32
		local Count = 0
		for k, v in ipairs( ClientUndos ) do
		
			local Item = ComboBox:AddItem( tostring( v.Name ) )
			Item.DoClick = function() RunConsoleCommand( "gmod_undonum", tostring( v.Key ) ) end
			
			Count = Count + 1
			if ( Count > NUM ) then break end

		end

	end
	
	/*---------------------------------------------------------
		AddUndo
		Called from server. Adds a new undo to our UI
	---------------------------------------------------------*/
	local function AddUndo( message )

		local k 	= message:ReadLong();
		local v 	= message:ReadString();

		// Move all the undos up one to make room
		local NewUndo = {}
		for k, v in ipairs( ClientUndos ) do
			NewUndo [ k + 1 ] = v
		end

		NewUndo[ 1 ] = { Key = k, Name = v }
		
		ClientUndos = NewUndo
		NewUndo = nil
		
		MakeUIDirty()

	end
	
	usermessage.Hook( "AddUndo", AddUndo )
	
	
	/*---------------------------------------------------------
		Undone
		Called from server. Notifies us that one of our undos
		has been undone or made redundant. We act by updating 
		out data (We wait until the UI is viewed until updating)
	---------------------------------------------------------*/
	local function Undone( message )

		local key = message:ReadLong();
		
		local NewUndo = {}
		local i = 1
		for k, v in ipairs( ClientUndos ) do
		
			if ( v.Key != key ) then
				NewUndo [ i ] = v
				i = i + 1
			end
		end
		
		ClientUndos = NewUndo
		NewUndo = nil
		
		MakeUIDirty()

	end
	
	usermessage.Hook( "Undone", Undone )
	
	/*---------------------------------------------------------
		MakeUIDirty
		Makes the UI dirty - it will re-create the controls
		the next time it is viewed. We also take this opportun
	---------------------------------------------------------*/
	function MakeUIDirty()
	
		bIsDirty = true
	
	end
	
	/*---------------------------------------------------------
		CPanelPaint
		Called when the inner panel of the undo CPanel is painted
		We hook onto this to determine when the panel is viewed.
		When it's viewed we update the UI if it's marked as dirty
	---------------------------------------------------------*/
	local function CPanelUpdate( panel )
	
		// This is kind of a shitty way of doing it - but we only want
		// to update the UI when it's visible.
		if ( bIsDirty ) then
		
			// Doing this in a timer so it calls it in a sensible place
			// Calling in the paint function could cause all kinds of problems
			// It's a hack but hey welcome to GMod!
			timer.Simple( 0, UpdateUI )
			bIsDirty = false
		
		end
	
	end
	
	/*---------------------------------------------------------
		SetupUI
		Adds a hook (CPanelPaint) to the control panel paint
		function so we can determine when it is being drawn.
	---------------------------------------------------------*/
	function SetupUI()
	
		local UndoPanel = GetControlPanel( "Undo" )
		if (!UndoPanel) then return end

		// Panels only think when they're visible
		UndoPanel.Think = CPanelUpdate
	
	end
	
	hook.Add( "PostReloadToolsMenu", "BuildUndoUI", SetupUI )

end


if (!SERVER) then return end

local PlayerUndo = {}
// PlayerUndo
//	- Player UniqueID
//		- Undo Table
//			- Name
//			- Entities (table of ents)
//			- Owner (player)

local Current_Undo = nil

/*---------------------------------------------------------
	GetTable
	Returns the undo table for whatever reason
---------------------------------------------------------*/
local function GetTable()
	return PlayerUndo
end


/*---------------------------------------------------------
	GetTable
	Save/Restore the undo tavles
---------------------------------------------------------*/
local function Save( save )

	saverestore.WriteTable( PlayerUndo, save )

end

local function Restore( restore )

	PlayerUndo = saverestore.ReadTable( restore )

end

saverestore.AddSaveHook( "UndoTable", Save )
saverestore.AddRestoreHook( "UndoTable", Restore )

/*---------------------------------------------------------
   Start a new undo
---------------------------------------------------------*/
function Create( text )

	Current_Undo = {}
	Current_Undo.Name 			= text
	Current_Undo.Entities 		= {}
	Current_Undo.Owner 			= nil
	Current_Undo.Functions 		= {}
	
end

/*---------------------------------------------------------
   Adds an entity to this undo (The entity is removed on undo)
---------------------------------------------------------*/
function SetCustomUndoText( CustomUndoText )

	if ( !Current_Undo ) then return end
	
	Current_Undo.CustomUndoText = CustomUndoText
	
end

/*---------------------------------------------------------
   Adds an entity to this undo (The entity is removed on undo)
---------------------------------------------------------*/
function AddEntity( ent )

	if ( Current_Undo == nil ) then return end
	if ( !ent ) then return end
	if ( !ent:IsValid() ) then return end
	
	table.insert( Current_Undo.Entities, ent )
	
end

/*---------------------------------------------------------
   Add a function to call to this undo
---------------------------------------------------------*/
function AddFunction( func, ... )

	if ( Current_Undo == nil ) then return end
	if ( !func ) then return end
	
	table.insert( Current_Undo.Functions, { func, arg } )
	
end

/*---------------------------------------------------------
   Replace Entity
---------------------------------------------------------*/
function ReplaceEntity( from, to )

	local ActionTaken = false

	for _, PlayerTable in pairs(PlayerUndo) do
		for _, UndoTable in pairs(PlayerTable) do
			if ( UndoTable.Entities ) then
		
				for key, ent in pairs( UndoTable.Entities ) do
					if ( ent == from ) then
						UndoTable.Entities[ key ] = to
						ActionTaken = true
					end
				end
				
			end
		end	
	end
	
	return ActionTaken
	
end


/*---------------------------------------------------------
   Sets who's undo this is
---------------------------------------------------------*/
function SetPlayer( player )

	if (Current_Undo == nil) then return end	
	if ( !player || !player:IsValid() ) then return end
	
	Current_Undo.Owner = player
	
end

/*---------------------------------------------------------
   SendUndoneMessage
   Sends a message to notify the client that one of their 
   undos has been removed. They can then update their GUI.
---------------------------------------------------------*/
local function SendUndoneMessage( ent, id, ply )

	if (!ply || !ply:IsValid()) then return end
	
	// For further optimization we could queue up the ids and send them
	// in one batch ever 0.5 seconds or something along those lines.
	
	umsg.Start( "Undone", ply )
		umsg.Long( id )
	umsg.End()

end

/*---------------------------------------------------------
   Finish
---------------------------------------------------------*/
function Finish( NiceText )

	if (Current_Undo == nil) then return end
	if (Current_Undo.Owner == nil) then return end
	if (!Current_Undo.Owner:IsValid()) then return end
	
	local index = Current_Undo.Owner:UniqueID()
	PlayerUndo[ index ] = PlayerUndo[ index ] or {}
	
	local id = table.insert( PlayerUndo[ index ], Current_Undo )
	
	NiceText = NiceText or Current_Undo.Name
	
	umsg.Start( "AddUndo", Current_Undo.Owner )
		umsg.Long( id )
		umsg.String( NiceText )
	umsg.End()
	
	// Have one of the entities in the undo tell us when it gets undone.
	if ( Current_Undo.Entities[1] ) then
	
		local ent = Current_Undo.Entities[ 1 ]
		ent:CallOnRemove( "undo"..id, SendUndoneMessage, id, Current_Undo.Owner )
	
	end
	
			
	Current_Undo = nil
	
end

/*---------------------------------------------------------
   Undos an undo
---------------------------------------------------------*/
function Do_Undo( undo )

	if ( !undo ) then return false end
	
	local count = 0
	
	// Call each function
	if ( undo.Functions ) then
		for index, func in pairs( undo.Functions ) do
		
			func[1]( undo, unpack(func[2]) )
			count = count + 1
			
		end
	end
	
	// Remove each entity in this undo
	if ( undo.Entities ) then
		for index, entity in pairs( undo.Entities ) do
		
			if ( entity:IsValid() ) then
				entity:Remove()
				count = count + 1
			end
			
		end
	end
	
	if (count > 0) then
		if ( undo.CustomUndoText ) then
			undo.Owner:SendLua( "GAMEMODE:OnUndo( '"..undo.Name.."', '"..undo.CustomUndoText.."' )" )
		else
			undo.Owner:SendLua( "GAMEMODE:OnUndo( '"..undo.Name.."' )" )
		end
	end
	
	return count;
	
end

/*---------------------------------------------------------
   Console command 
---------------------------------------------------------*/
local function CC_UndoLast( pl, command, args )

	local index = pl:UniqueID()
	PlayerUndo[ index ] = PlayerUndo[ index ] or {}
	
	local last = nil
	local lastk = nil
	
	for k, v in pairs( PlayerUndo[ index ] ) do
	
		lastk = k
		last = v
	
	end
	
	// No undos
	if (!last) then	return end
	
	// This is quite messy, but if the player rejoined the server
	// 'Owner' might no longer be a valid entity. So replace the Owner
	// with the player that is doing the undoing
	last.Owner = pl
	
	local count = Do_Undo( last )
	
	umsg.Start( "Undone", pl )
		umsg.Long( lastk )
	umsg.End()
	
	PlayerUndo[ index ][ lastk ] = nil
	
	// If our last undo object is already deleted then compact
	// the undos until we hit one that does something
	if ( count == 0 ) then
		CC_UndoLast( pl )
	end

end

/*---------------------------------------------------------
   Console command 
---------------------------------------------------------*/
local function CC_UndoNum( pl, command, args )

	if (!args[1]) then return end	
	local index = pl:UniqueID()
	PlayerUndo[ index ] = PlayerUndo[ index ] or {}
	
	local UndoNum = tonumber( args[1] )
	
	// Delete the undo whatever happens
	//umsg.Start( "Undone", pl )
	//	umsg.Long( UndoNum )
	//umsg.End()
	
	if ( !PlayerUndo[ index ][ UndoNum ] ) then return end
	
	// Undo!
	Do_Undo( PlayerUndo[ index ][ UndoNum ] )
	PlayerUndo[ index ][ UndoNum ] = nil

end

concommand.Add("undo",					CC_UndoLast		)
concommand.Add("gmod_undo",				CC_UndoLast		)
concommand.Add("gmod_undonum",			CC_UndoNum		)

	
