//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
// $NoKeywords: $
//=============================================================================//

#include "stdafx.h"
#include "History.h"
#include "GlobalFunctions.h"
#include "MapDoc.h"
#include "MapWorld.h"
#include "SearchReplaceDlg.h"
#include "hammer.h"
#include "Selection.h"

// memdbgon must be the last include file in a .cpp file!!!
#include <tier0/memdbgon.h>


//
// Context data for a FindFirstObject/FindNextObject session.
//
struct FindObject_t
{
	//
	// Where to look: in the world or in the selection set.
	//
	FindReplaceIn_t eFindIn;

	CMapWorld *pWorld;
	EnumChildrenPos_t WorldPos;					// A position in the world tree for world searches.

	CUtlVector<CMapClass *> SelectionList;		// A copy of the selection list for selection only searches.
	int nSelectionIndex;						// The index into the selection list for iterating the selection list.

	//
	// What to look for.
	//	
	CString strFindText;
	bool bVisiblesOnly;
	bool bCaseSensitive;
	bool bWholeWord;
};


CMapClass *FindNextObject(FindObject_t &FindObject);
bool FindCheck(CMapClass *pObject, FindObject_t &FindObject);


//-----------------------------------------------------------------------------
// Purpose: Returns true if the string matches the search criteria, false if not.
// Input  : pszString - String to check.
//			FindObject - Search criteria, including string to search for.
//-----------------------------------------------------------------------------
bool MatchString(const char *pszString, FindObject_t &FindObject)
{
	if (FindObject.bWholeWord)
	{
		if (FindObject.bCaseSensitive)
		{
			return (!strcmp(pszString, FindObject.strFindText));
		}

		return (!stricmp(pszString, FindObject.strFindText));
	}

	if (FindObject.bCaseSensitive)
	{
		return (strstr(pszString, FindObject.strFindText) != NULL);
	}

	return (Q_stristr(pszString, FindObject.strFindText) != NULL);
}


//-----------------------------------------------------------------------------
// Purpose: Returns true if the string matches the search criteria, false if not.
// Input  : pszIn - 
//			pszOut - String to check.
//			FindObject - Search criteria, including string to search for.
//-----------------------------------------------------------------------------
bool ReplaceString(char *pszOut, const char *pszIn, FindObject_t &FindObject, const char *pszReplace)
{
	//
	// Whole matches are simple, just strcpy the replacement string into the out buffer.
	//
	if (FindObject.bWholeWord)
	{
		if (FindObject.bCaseSensitive && (!strcmp(pszIn, FindObject.strFindText)))
		{
			strcpy(pszOut, pszReplace);
			return true;
		}

		if (!stricmp(pszIn, FindObject.strFindText))
		{
			strcpy(pszOut, pszReplace);
			return true;
		}
	}

	//
	// Partial matches are a little tougher.
	//
	const char *pszStart = NULL;
	if (FindObject.bCaseSensitive)
	{
		pszStart = strstr(pszIn, FindObject.strFindText);
	}
	else
	{
		pszStart = Q_stristr(pszIn, FindObject.strFindText);
	}

	if (pszStart != NULL)
	{
		int nOffset = pszStart - pszIn;

		strncpy(pszOut, pszIn, nOffset);
		pszOut += nOffset;
		pszIn += nOffset + strlen(FindObject.strFindText);

		strcpy(pszOut, pszReplace);
		pszOut += strlen(pszReplace);

		strcpy(pszOut, pszIn);

		return true;
	}

	return false;
}


//-----------------------------------------------------------------------------
// Purpose: Begins a Find or Find/Replace operation.
//-----------------------------------------------------------------------------
CMapClass *FindFirstObject(FindObject_t &FindObject)
{
	CMapClass *pObject = NULL;

	if (FindObject.eFindIn == FindInWorld)
	{
		// Search the entire world.
		pObject = FindObject.pWorld->GetFirstDescendent(FindObject.WorldPos);
	}
	else
	{
		// Search the selection only.
		if (FindObject.SelectionList.Count())
		{
			pObject = FindObject.SelectionList.Element(0);
			FindObject.nSelectionIndex = 1;
		}
	}

	if (!pObject)
	{
		return NULL;
	}

	if (FindCheck(pObject, FindObject))
	{
		return pObject;
	}

	return FindNextObject(FindObject);
}


//-----------------------------------------------------------------------------
// Purpose: 
// Input  : pObject - 
//-----------------------------------------------------------------------------
CMapClass *FindNextObject(FindObject_t &FindObject)
{
	while (true)
	{
		CMapClass *pObject = NULL;
		if (FindObject.eFindIn == FindInWorld)
		{
			// Search the entire world.
			pObject = FindObject.pWorld->GetNextDescendent(FindObject.WorldPos);
		}
		else
		{
			// Search the selection only.
			if (FindObject.nSelectionIndex < FindObject.SelectionList.Count())
			{
				pObject = FindObject.SelectionList.Element(FindObject.nSelectionIndex);
				FindObject.nSelectionIndex++;
			}
		}

		if ((!pObject) || FindCheck(pObject, FindObject))
		{
			return pObject;
		}
	}
}


//-----------------------------------------------------------------------------
// Purpose: 
// Input  : pObject - 
//			FindObject - 
// Output : Returns true if the object matches the search criteria, false if not.
//-----------------------------------------------------------------------------
bool FindCheck(CMapClass *pObject, FindObject_t &FindObject)
{
	CMapEntity *pEntity = dynamic_cast <CMapEntity *>(pObject);
	if (!pEntity)
	{
		return false;
	}

	if (FindObject.bVisiblesOnly && !pObject->IsVisible())
	{
		return false;
	}

	//
	// Search keyvalues.
	//
	for ( int i=pEntity->GetFirstKeyValue(); i != pEntity->GetInvalidKeyValue(); i=pEntity->GetNextKeyValue( i ) )
	{
		const char *pszValue = pEntity->GetKeyValue(i);
		if (pszValue && MatchString(pszValue, FindObject))
		{
			return true;
		}
	}

	//
	// Search connections.
	//
	int nConnCount = pEntity->Connections_GetCount();
	for (int i = 0; i < nConnCount; i++)
	{
		CEntityConnection *pConn = pEntity->Connections_Get(i);
		if (pConn)
		{
			if (MatchString(pConn->GetTargetName(), FindObject) ||
				MatchString(pConn->GetParam(), FindObject))
			{
				return true;
			}
		}
	}

	return false;
}


//-----------------------------------------------------------------------------
// Purpose: 
// Input  : pLastFound - 
//			FindObject - 
//			pszReplaceText - 
// Output : Returns the number of occurrences of the find text that were replaced.
//-----------------------------------------------------------------------------
int FindReplace(CMapEntity *pEntity, FindObject_t &FindObject, const char *pszReplace)
{
	int nReplacedCount = 0;
	
	//
	// Replace keyvalues.
	//
	for ( int i=pEntity->GetFirstKeyValue(); i != pEntity->GetInvalidKeyValue(); i=pEntity->GetNextKeyValue( i ) )
	{
		const char *pszValue = pEntity->GetKeyValue(i);
		char szNewValue[MAX_PATH];
		if (pszValue && ReplaceString(szNewValue, pszValue, FindObject, pszReplace))
		{
			const char *pszKey = pEntity->GetKey(i);
			if (pszKey)
			{
				pEntity->SetKeyValue(pszKey, szNewValue);
				nReplacedCount++;
			}
		}
	}

	//
	// Replace connections.
	//
	int nConnCount = pEntity->Connections_GetCount();
	for (int i = 0; i < nConnCount; i++)
	{
		CEntityConnection *pConn = pEntity->Connections_Get(i);
		if (pConn)
		{
			char szNewValue[MAX_PATH];

			if (ReplaceString(szNewValue, pConn->GetTargetName(), FindObject, pszReplace))
			{
				pConn->SetTargetName(szNewValue);
				nReplacedCount++;
			}
			
			if (ReplaceString(szNewValue, pConn->GetParam(), FindObject, pszReplace))
			{
				pConn->SetParam(szNewValue);
				nReplacedCount++;
			}
		}
	}

	return nReplacedCount;
}


BEGIN_MESSAGE_MAP(CSearchReplaceDlg, CDialog)
	//{{AFX_MSG_MAP(CSearchReplaceDlg)
	ON_WM_SHOWWINDOW()
	ON_COMMAND_EX(IDC_FIND_NEXT, OnFindReplace)
	ON_COMMAND_EX(IDC_REPLACE, OnFindReplace)
	ON_COMMAND_EX(IDC_REPLACE_ALL, OnFindReplace)
	//}}AFX_MSG_MAP
END_MESSAGE_MAP()


//-----------------------------------------------------------------------------
// Purpose: 
// Input  : pParent - 
//-----------------------------------------------------------------------------
CSearchReplaceDlg::CSearchReplaceDlg(CWnd *pParent)
	: CDialog(CSearchReplaceDlg::IDD, pParent)
{
	m_bNewSearch = true;

	//{{AFX_DATA_INIT(CSearchReplaceDlg)
	m_bVisiblesOnly = FALSE;
	m_nFindIn = FindInWorld;
	m_bWholeWord = FALSE;
	m_bCaseSensitive = FALSE;
	//}}AFX_DATA_INIT
}


//-----------------------------------------------------------------------------
// Purpose: 
// Output : Returns TRUE on success, FALSE on failure.
//-----------------------------------------------------------------------------
BOOL CSearchReplaceDlg::Create(CWnd *pwndParent)
{
	return CDialog::Create(CSearchReplaceDlg::IDD, pwndParent);
}


//-----------------------------------------------------------------------------
// Purpose: 
// Input  : pDX - 
//-----------------------------------------------------------------------------
void CSearchReplaceDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialog::DoDataExchange(pDX);

	//{{AFX_DATA_MAP(CSearchReplaceDlg)
	DDX_Check(pDX, IDC_VISIBLES_ONLY, m_bVisiblesOnly);
	DDX_Check(pDX, IDC_WHOLE_WORD, m_bWholeWord);
	DDX_Check(pDX, IDC_CASE_SENSITIVE, m_bCaseSensitive);
	DDX_Text(pDX, IDC_FIND_TEXT, m_strFindText);
	DDX_Text(pDX, IDC_REPLACE_TEXT, m_strReplaceText);
	DDX_Radio(pDX, IDC_SELECTION, m_nFindIn);
	//}}AFX_DATA_MAP
}


//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CSearchReplaceDlg::OnCancel(void)
{
	ShowWindow(SW_HIDE);
}


//-----------------------------------------------------------------------------
// Purpose: Fill out the find criteria from the dialog controls.
// Input  : FindObject - 
//-----------------------------------------------------------------------------
void CSearchReplaceDlg::GetFindCriteria(FindObject_t &FindObject, CMapDoc *pDoc)
{
	FindObject.pWorld = pDoc->GetMapWorld();

	if (m_nFindIn == FindInSelection)
	{
		FindObject.eFindIn = FindInSelection;

		FindObject.SelectionList.RemoveAll();

		const CMapObjectList *pSelection = pDoc->GetSelection()->GetList();
		for (int i = 0; i < pSelection->Count(); i++)
		{
			CMapClass *pObject = pSelection->Element(i);
			if ( pObject->IsGroup() )
			{
				// If it's a group, get all the entities in the group.
				const CMapObjectList *pChildren = pObject->GetChildren();
				FOR_EACH_OBJ( *pChildren, pos )
				{
					FindObject.SelectionList.AddToTail( pChildren->Element(pos) );
				}
			}
			else				 
			{
				FindObject.SelectionList.AddToTail(pObject);
			}
		}
	}
	else
	{
		FindObject.eFindIn = FindInWorld;
	}

	FindObject.strFindText = m_strFindText;
	FindObject.bVisiblesOnly = (m_bVisiblesOnly == TRUE);
	FindObject.bWholeWord = (m_bWholeWord == TRUE);
	FindObject.bCaseSensitive = (m_bCaseSensitive == TRUE);
}


//-----------------------------------------------------------------------------
// Purpose: Called when they hit the Find, the Replace, or the Replace All button.
// Input  : uCmd - The ID of the button the user hit, IDC_FIND_NEXT or IDC_REPLACE.
// Output : Returns TRUE to indicate that the message was handled.
//-----------------------------------------------------------------------------
BOOL CSearchReplaceDlg::OnFindReplace(UINT uCmd)
{
	CMapDoc *pDoc = CMapDoc::GetActiveMapDoc();
	if (!pDoc)
	{
		return TRUE;
	}

	static FindObject_t FindObject;
	static CMapClass *pLastFound = NULL;
	static int nReplaceCount = 0;
	FindObject_t TempFindObject;

	bool bDone = false;

	UpdateData();
	GetFindCriteria(TempFindObject, pDoc);

	if ( strcmp(TempFindObject.strFindText, FindObject.strFindText) != 0 )
	{
		m_bNewSearch = true;
	}

	do
	{
		CMapClass *pObject = NULL;
		
		if (m_bNewSearch)
		{
			//
			// New search. Fetch the data from the controls.
			//
			UpdateData();
			GetFindCriteria(FindObject, pDoc);

			//
			// We have to keep track of the last object in the iteration for replacement,
			// because replacement is done when me advance to the next object.
			//
			pLastFound = NULL;
			nReplaceCount = 0;

			pObject = FindFirstObject(FindObject);
		}
		else
		{
			pObject = FindNextObject(FindObject);
		}

		//
		// Replace All is undone as single operation. Mark the undo position the first time
		// we find a match during a Replace All.
		//
		if (m_bNewSearch && (uCmd == IDC_REPLACE_ALL) && pObject)
		{
			GetHistory()->MarkUndoPosition(pDoc->GetSelection()->GetList(), "Replace Text");
		}

		//
		// If we have an object to do the replace on, do the replace.
		//
		if (pLastFound && ((uCmd == IDC_REPLACE) || (uCmd == IDC_REPLACE_ALL)))
		{
			if (uCmd == IDC_REPLACE)
			{
				// Allow for undo each time we do a Replace.
				GetHistory()->MarkUndoPosition(NULL, "Replace Text");
			}

			//
			// Do the replace on the last matching object we found. This lets the user see what
			// object will be modified before it is done.
			//
			GetHistory()->Keep(pLastFound);
			nReplaceCount += FindReplace((CMapEntity *)pLastFound, FindObject, m_strReplaceText);

			GetDlgItem(IDCANCEL)->SetWindowText("Close");
		}

		if (pObject)
		{
			//
			// We found an object that satisfies our search.
			//
			if ((uCmd == IDC_FIND_NEXT) || (uCmd == IDC_REPLACE))
			{
				//
				// Highlight the match.
				//
				pDoc->SelectObject(pObject, scClear | scSelect);
				pDoc->CenterViewsOnSelection();
			}

			//
			// Stop after one match unless we are doing a Replace All.
			//
			if (uCmd != IDC_REPLACE_ALL)
			{
				bDone = true;
			}

			m_bNewSearch = false;
			pLastFound = pObject;
		}
		else
		{
			//
			// No more objects in the search set match our criteria.
			//
			if ((m_bNewSearch) || (uCmd != IDC_REPLACE_ALL))
			{
				CString str;
				str.Format("Finished searching for '%s'.", m_strFindText.GetBuffer());
				MessageBox(str, "Find/Replace Text", MB_OK);

				// TODO: put the old selection back
			}
			else if (uCmd == IDC_REPLACE_ALL)
			{
				CString str;
				str.Format("Replaced %d occurrences of the string '%s' with '%s'.", nReplaceCount, m_strFindText.GetBuffer(), m_strReplaceText.GetBuffer());
				MessageBox(str, "Find/Replace Text", MB_OK);
			}

			m_bNewSearch = true;
			bDone = true;
		}

	} while (!bDone);

	return TRUE;
}


//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CSearchReplaceDlg::OnOK() 
{
}


//-----------------------------------------------------------------------------
// Purpose: Called any time we are hidden or shown.
// Input  : bShow - 
//			nStatus - 
//-----------------------------------------------------------------------------
void CSearchReplaceDlg::OnShowWindow(BOOL bShow, UINT nStatus)
{
	if (bShow)
	{
		m_bNewSearch = true;
		GetDlgItem(IDCANCEL)->SetWindowText("Cancel");

		m_nFindIn = FindInWorld;
		CMapDoc *pDoc = CMapDoc::GetActiveMapDoc();
		if (pDoc)
		{
			if ( !pDoc->GetSelection()->IsEmpty() )
			{
				m_nFindIn = FindInSelection;
			}
		}

		// Populate the controls with the current data.
		UpdateData(FALSE);
	}

	CDialog::OnShowWindow(bShow, nStatus);
}