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

#include "stdafx.h"
#include "FileChangeWatcher.h"
#include "tier1/utldict.h"
#include "filesystem_tools.h"


CFileChangeWatcher::CFileChangeWatcher()
{
	m_pCallbacks = NULL;
}

CFileChangeWatcher::~CFileChangeWatcher()
{
	Term();
}

void CFileChangeWatcher::Init( ICallbacks *pCallbacks )
{
	Term();
	m_pCallbacks = pCallbacks;
}

bool CFileChangeWatcher::AddDirectory( const char *pSearchPathBase, const char *pDirName, bool bRecursive )
{
	char fullDirName[MAX_PATH];
	V_ComposeFileName( pSearchPathBase, pDirName, fullDirName, sizeof( fullDirName ) );
	
	HANDLE hDir = CreateFile( fullDirName, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OVERLAPPED, NULL );
	if ( hDir == INVALID_HANDLE_VALUE )
	{
		Warning( "CFileChangeWatcher::AddDirectory - can't get a handle to directory %s.\n", pDirName );
		return false;
	}

	// Call this once to start the ball rolling.. Next time we call it, it'll tell us the changes that
	// have happened since this call.
	CDirWatch *pDirWatch = new CDirWatch;
	V_strncpy( pDirWatch->m_SearchPathBase, pSearchPathBase, sizeof( pDirWatch->m_SearchPathBase ) );
	V_strncpy( pDirWatch->m_DirName, pDirName, sizeof( pDirWatch->m_DirName ) );
	V_strncpy( pDirWatch->m_FullDirName, fullDirName, sizeof( pDirWatch->m_FullDirName ) );
	pDirWatch->m_hDir = hDir;
	pDirWatch->m_hEvent = CreateEvent( NULL, false, false, NULL );
	memset( &pDirWatch->m_Overlapped, 0, sizeof( pDirWatch->m_Overlapped ) );
	pDirWatch->m_Overlapped.hEvent = pDirWatch->m_hEvent;
	if ( !CallReadDirectoryChanges( pDirWatch ) )
	{
		CloseHandle( pDirWatch->m_hEvent );
		CloseHandle( pDirWatch->m_hDir );
		delete pDirWatch;
		return false;
	}

	m_DirWatches.AddToTail( pDirWatch );
	return true;
}

void CFileChangeWatcher::Term()
{
	for ( int i=0; i < m_DirWatches.Count(); i++ )
	{
		CloseHandle( m_DirWatches[i]->m_hDir );
		CloseHandle( m_DirWatches[i]->m_hEvent );
	}
	m_DirWatches.PurgeAndDeleteElements();
	m_pCallbacks = NULL;
}

int CFileChangeWatcher::Update()
{
	CUtlDict< int, int > queuedChanges;
	int nTotalChanges = 0;
	
	// Check each CDirWatch.
	int i = 0;
	while ( i < m_DirWatches.Count() )
	{
		CDirWatch *pDirWatch = m_DirWatches[i];
	
		DWORD dwBytes = 0;
		if ( GetOverlappedResult( pDirWatch->m_hDir, &pDirWatch->m_Overlapped, &dwBytes, FALSE ) )
		{
			// Read through the notifications.
			int nBytesLeft = (int)dwBytes;
			char *pCurPos = pDirWatch->m_Buffer;
			while ( nBytesLeft >= sizeof( FILE_NOTIFY_INFORMATION ) )
			{
				FILE_NOTIFY_INFORMATION *pNotify = (FILE_NOTIFY_INFORMATION*)pCurPos;
			
				if ( m_pCallbacks )
				{
					// Figure out what happened to this file.
					WCHAR nullTerminated[2048];
					int nBytesToCopy = min( (int)pNotify->FileNameLength, 2047 );
					memcpy( nullTerminated, pNotify->FileName, nBytesToCopy );
					nullTerminated[nBytesToCopy/2] = 0;
					char ansiFilename[1024];
					V_UnicodeToUTF8( nullTerminated, ansiFilename, sizeof( ansiFilename ) );
					
					// Now add it to the queue.	We use this queue because sometimes Windows will give us multiple
					// of the same modified notification back to back, and we want to reduce redundant calls.
					int iExisting = queuedChanges.Find( ansiFilename );
					if ( iExisting == queuedChanges.InvalidIndex() )
					{
						iExisting = queuedChanges.Insert( ansiFilename, 0 );
						++nTotalChanges;
					}
				}		
			
				if ( pNotify->NextEntryOffset == 0 )
					break;
					
				pCurPos += pNotify->NextEntryOffset;
				nBytesLeft -= (int)pNotify->NextEntryOffset;
			}
			
			CallReadDirectoryChanges( pDirWatch );
			continue;	// Check again because sometimes it queues up duplicate notifications.
		}

		// Process all the entries in the queue.
		for ( int iQueuedChange=queuedChanges.First(); iQueuedChange != queuedChanges.InvalidIndex(); iQueuedChange=queuedChanges.Next( iQueuedChange ) )
		{
			SendNotification( pDirWatch, queuedChanges.GetElementName( iQueuedChange ) );
		}
		queuedChanges.Purge();
		++i;
	}
	
	return nTotalChanges;
}

void CFileChangeWatcher::SendNotification( CFileChangeWatcher::CDirWatch *pDirWatch, const char *pRelativeFilename )
{
	// Use this for full filenames although you don't strictly need it.. 
	char fullFilename[MAX_PATH];
	V_ComposeFileName( pDirWatch->m_FullDirName, pRelativeFilename, fullFilename, sizeof( fullFilename ) );

	m_pCallbacks->OnFileChange( pRelativeFilename, fullFilename );
}

BOOL CFileChangeWatcher::CallReadDirectoryChanges( CFileChangeWatcher::CDirWatch *pDirWatch )
{
	return ReadDirectoryChangesW( pDirWatch->m_hDir, 
		pDirWatch->m_Buffer, 
		sizeof( pDirWatch->m_Buffer ), 
		true, 
		FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE, 
		NULL, 
		&pDirWatch->m_Overlapped, 
		NULL );
}