// DirWatch.cpp: implementation of the CWatchFileSys class.
//
//////////////////////////////////////////////////////////////////////

#include "stdafx.h"
#include "DirWatch.h"
#include "Error.h"
#include "MT.h"
#include "AutoPtr.h"
#include "Error.h"
#include "iadmw.h"		// COM Interface header
#include "iiscnfg.h"	// MD_ & IIS_MD_ #defines

//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////

CWatchFileSys::CWatchFileSys()
	: m_WatchInfo(this), m_pOpQ(NULL)
{

}

CWatchFileSys::~CWatchFileSys()
{
	// verify that thread terminated
	ShutDown();
}

HRESULT CWatchFileSys::NewInit(COpQueue *pOpQ)
{
	_ASSERTE(pOpQ && !m_pOpQ);
	m_pOpQ = pOpQ;
	HRESULT hr = S_OK;
	CComPtr<IMSAdminBase> pIAdminBase;

	// check if we already have a metabase instance
	// create adminbase instance
	hr = CoCreateInstance(CLSID_MSAdminBase,
							NULL, 
							CLSCTX_ALL, 
							IID_IMSAdminBase, 
							(void **) &pIAdminBase);
	IF_FAIL_RTN1(hr,"CoCreateInstance IID_IMSAdminBase");

	METADATA_HANDLE hMD = NULL;
	WCHAR szKeyName[3+6+ 2* METADATA_MAX_NAME_LEN]			// /LM/W3SVC/sitename/vir_dir
		= L"/LM/W3SVC/";
	LPTSTR szSiteKeyName = &szKeyName[wcslen(szKeyName)];	// point to the end of string so we can append it
	DWORD iSiteEnumIndex = 0;
	LPTSTR szVDirKeyName = NULL;
	DWORD iVDirEnumIndex = 0;

	hr = pIAdminBase->OpenKey(METADATA_MASTER_ROOT_HANDLE,
								szKeyName,
								METADATA_PERMISSION_READ,
								20,
								&hMD);
	IF_FAIL_RTN1(hr,"IAdminBase::OpenKey");

	METADATA_RECORD MDRec;
	DWORD iBufLen = 1024;
	DWORD iReqBufLen = 0;
	PBYTE pbBuf = new BYTE[iBufLen];
	if(!pbBuf)
	{
		pIAdminBase->CloseKey(hMD);
		return E_OUTOFMEMORY;
	}
	DWORD iDataIndex = 0;

	while(SUCCEEDED(hr = pIAdminBase->EnumKeys(hMD,TEXT(""),szSiteKeyName,iSiteEnumIndex)))
	{
		// iterate through all virtual sites on this machine
		wcscat(szSiteKeyName,L"/ROOT/");
		szVDirKeyName = szSiteKeyName + wcslen(szSiteKeyName);
		
		iVDirEnumIndex = 0;
		while(SUCCEEDED(hr = pIAdminBase->EnumKeys(hMD,szSiteKeyName,szVDirKeyName,iVDirEnumIndex)))
		{
			// iterate through all virtual directories in each site
			MDRec.dwMDIdentifier = MD_VR_PATH;
			MDRec.dwMDAttributes = METADATA_INHERIT;
			MDRec.dwMDUserType = IIS_MD_UT_FILE;
			MDRec.dwMDDataType = ALL_METADATA;
			MDRec.dwMDDataLen = iBufLen;
			MDRec.pbMDData = pbBuf;
			hr = pIAdminBase->GetData(hMD,szSiteKeyName,&MDRec,&iReqBufLen);
			if(hr == RETURNCODETOHRESULT(ERROR_INSUFFICIENT_BUFFER))
			{
				delete [] pbBuf;
				pbBuf = new BYTE[iReqBufLen];
				if(!pbBuf)
				{
					pIAdminBase->CloseKey(hMD);
					return E_OUTOFMEMORY;
				}
				iBufLen = iReqBufLen;
				MDRec.dwMDDataLen = iBufLen;
				MDRec.pbMDData = pbBuf;
				hr = pIAdminBase->GetData(hMD,szSiteKeyName,&MDRec,&iReqBufLen);
			}

			// @todo: verify that this dir should be watched
			//        i.e. check if do-not-version flag is set

			if(SUCCEEDED(hr))
			{
				// add 
				wstring szPrj(L"/Files/");			//@todo: decide on prj
					szPrj.append(szSiteKeyName);
				hr = Add((LPCTSTR)MDRec.pbMDData,szPrj.c_str());
				IF_FAIL_RPT1(hr,"CWatchFileSys::Add");
			}
			else
			{
				CError::Trace("Can't get dir for ");
				CError::Trace(szVDirKeyName);
				CError::Trace("\n");
			}
			iVDirEnumIndex++;
		}
		iSiteEnumIndex++;	
	}
	pIAdminBase->CloseKey(hMD);
	delete [] pbBuf;

	return S_OK;
}

void CWatchFileSys::ShutDownHelper(CWatchInfo &rWatchInfo)
{
	if(rWatchInfo.m_hThread)
	{
		// end notification thread
		PostQueuedCompletionStatus(rWatchInfo.m_hCompPort,0,0,NULL);
		// wait for thread to finish
		WaitForSingleObject(rWatchInfo.m_hThread,INFINITE);
		CloseHandle(rWatchInfo.m_hThread);
		rWatchInfo.m_hThread = NULL;
		rWatchInfo.m_iThreadID = 0;
	}
	if(rWatchInfo.m_hCompPort)
	{
		// clean up
		CloseHandle(rWatchInfo.m_hCompPort);
		rWatchInfo.m_hCompPort = NULL;
	}
}

void CWatchFileSys::ShutDown()
{
	ShutDownHelper(m_WatchInfo);
	m_pOpQ = NULL;
}

DWORD WINAPI CWatchFileSys::NotificationThreadProc(LPVOID lpParam)
{
	_ASSERTE(lpParam);
	CWatchInfo *pWI = (CWatchInfo*) lpParam;
	CWatchFileSys *pWatchFileSys = pWI->m_pWatchFileSys;

	// vars for accessing the notification
	DWORD iBytes = 0;
	CDirInfo *pDirInfo = NULL;
	LPOVERLAPPED pOverlapped = NULL;
	PFILE_NOTIFY_INFORMATION pfni = NULL;
	DWORD cbOffset = 0;

	// vars for creating a file op
	HRESULT hr;
	COpFileSys *pOp = NULL;
	LPCTSTR szPrj = NULL;
	LPCTSTR szDir = NULL;
	wstring szFileName;
	wstring szOldFileName;

	do
	{
		_ASSERTE(pWI->m_hCompPort);
		GetQueuedCompletionStatus(pWI->m_hCompPort,
								  &iBytes,
								  (LPDWORD) &pDirInfo,
								  &pOverlapped,
								  INFINITE);
		if(pDirInfo)
		{
			// get ptr to first file_notify_info in buffer
			pfni = (PFILE_NOTIFY_INFORMATION) pDirInfo->m_cBuffer;

			// clean 
			szFileName.erase();			// empty to avoid compare wrong compares
			szOldFileName.erase();		// empty

			// remember dir and prj they are the same for all entries
			szPrj = pDirInfo->m_szPrj.c_str();
			szDir = pDirInfo->m_szDir.c_str();

			// process all file_notify_infos in buffer
			_ASSERTE(pWatchFileSys->m_pOpQ);
			do
			{
				cbOffset = pfni->NextEntryOffset;
				
				// sometime an errorous action #0 is send, let's ignore it
				switch(pfni->Action) {
					case FILE_ACTION_ADDED:
					case FILE_ACTION_REMOVED:
					case FILE_ACTION_MODIFIED:
					case FILE_ACTION_RENAMED_OLD_NAME:
					case FILE_ACTION_RENAMED_NEW_NAME:
						break;
					default:
						// unknown action, let's ignore it
						pfni = (PFILE_NOTIFY_INFORMATION) ((LPBYTE)pfni + cbOffset);// get next offset
						continue;
				}
				
				// on rename remember old filename
				szOldFileName.erase();
				if(pfni->Action == FILE_ACTION_RENAMED_OLD_NAME)
				{
					// make sure next entry exists and is new-name entry
					_ASSERTE(cbOffset);		// there is another entry
					PFILE_NOTIFY_INFORMATION pNextfni = (PFILE_NOTIFY_INFORMATION) ((LPBYTE)pfni + cbOffset);
					_ASSERTE(pNextfni->Action == FILE_ACTION_RENAMED_NEW_NAME); // the next entry contians the new name
					
					// assign old name
					szOldFileName.assign(pfni->FileName,pfni->FileNameLength/2);
					
					// skip to next (new-name) entry
					pfni = pNextfni;
					cbOffset = pNextfni->NextEntryOffset;

					// clear szFileName so it doesn't get skiped in next lines
					szFileName.erase();
				}

				// assign affected filename
				szFileName.assign(pfni->FileName,pfni->FileNameLength/2);

				// create new operation
				pOp = new COpFileSys(pfni->Action,szPrj,szDir,szFileName.c_str(),szOldFileName.c_str());
				if(!pOp)
				{
					// this is bad. no more mem? what to do? need to shutdown entire thread/process
					FAIL_RPT1(E_OUTOFMEMORY,"new COpFile()");

					// continue
					break;
				}

				// add operation
				hr = pWatchFileSys->m_pOpQ->Add(pOp);
				if(FAILED(hr))
				{
					// @todo log err
					FAIL_RPT1(E_FAIL,"COpQueue::Add failed");
					delete pOp;
				}
				if(hr == S_FALSE)	// op was a dupl
					delete pOp;		// so delete and ignore
				pOp = NULL;

				// get next offset
				pfni = (PFILE_NOTIFY_INFORMATION) ((LPBYTE)pfni + cbOffset);
			} while(cbOffset);
			
			// reissue the watch
			if(!pWatchFileSys->IssueWatch(pDirInfo))
			{
				// @todo: log error
			}
		}
	} while( pDirInfo );

	// end of thread
	return 0;
}

bool CWatchFileSys::AddHelper(CWatchInfo &rWatchInfo,CDirInfo *pDirInfo)
{
	// create completion port, or add to it
	rWatchInfo.m_hCompPort = CreateIoCompletionPort(pDirInfo->m_hDir,
													rWatchInfo.m_hCompPort,
													(DWORD)(CDirInfo*) pDirInfo,
													0);
	if(!rWatchInfo.m_hCompPort)
		return false;

	// watch directory
	if(!IssueWatch(pDirInfo))
		return false;

	// create notification thread (if not already exist)
	if(!rWatchInfo.m_hThread)
	{
		rWatchInfo.m_hThread = _beginthreadex(
									NULL,			// no security descriptor
									0,				// default stack size
									NotificationThreadProc,	//thread procedure
									&rWatchInfo,			// thread procedure argument
									0,				// run imideately
									&rWatchInfo.m_iThreadID);	// place to store id
		if(!rWatchInfo.m_hThread)
		return false;
	}
	
	// if everything was successfull, add dirinfo to list
	rWatchInfo.AddDirInfo(pDirInfo);
	return true;
}

HRESULT CWatchFileSys::Add(LPCTSTR szDir,LPCTSTR szRelPrj)
{
	CAutoPtr<CDirInfo> pDirInfo;
	_ASSERTE(szDir && szRelPrj);
	
	// @todo: check that dir is not already part of list (check in subtree as well)
	
	// @todo: convert szDir to Abstolute path
		
	// create dirinfo 
	pDirInfo = new CDirInfo(szDir,szRelPrj);
	if(!pDirInfo)
		FAIL_RTN1(E_OUTOFMEMORY,"new CDirInfo()");

	// get handle to dir
	pDirInfo->m_hDir = CreateFile(szDir,
								  FILE_LIST_DIRECTORY,
 								  FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
								  NULL,
								  OPEN_EXISTING,
								  FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OVERLAPPED,
								  NULL);
	if(pDirInfo->m_hDir == INVALID_HANDLE_VALUE)
		goto _Error;

	if(!AddHelper(m_WatchInfo,pDirInfo))
		goto _Error;
	// @todo: call only if startup flag set. 
	// the following call is slow!!!
	// the following line should only be called if you want to bring the 
	// versioning store to the same state as the file system. I.e. all files
	// will be checked in, and unnecessary files in the version store will be
	// marked deleted.
	
//	pVerEngine->SyncPrj(szPrj.c_str,szDir); // @todo: should only be called when 
	pDirInfo = NULL;

	CError::Trace("Watching: ");
	CError::Trace(szDir);
	CError::Trace("\n");

	return S_OK;

_Error:
	CError::ErrorMsgBox(GetLastError());
	return E_FAIL;
}

BOOL CWatchFileSys::IssueWatch(CDirInfo * pDirInfo)
{
	_ASSERTE(pDirInfo);
	BOOL b;
	DWORD dwNotifyFilter =  FILE_NOTIFY_CHANGE_FILE_NAME	
							| FILE_NOTIFY_CHANGE_DIR_NAME
//							| FILE_NOTIFY_CHANGE_SIZE
//							| FILE_NOTIFY_CHANGE_CREATION
							| FILE_NOTIFY_CHANGE_LAST_WRITE;

	b = ReadDirectoryChangesW(pDirInfo->m_hDir,
								 pDirInfo->m_cBuffer,
								 MAX_BUFFER,
								 TRUE,
								 dwNotifyFilter,
								 & pDirInfo->m_iBuffer,
								 & pDirInfo->m_Overlapped,
								 NULL);
	if(!b)
	{
		CError::ErrorTrace(GetLastError(),"ReadDirectoryChangesW failed",__FILE__,__LINE__);
	}
	return b;
}