#include "pch.h"

char g_chFileChangeBuff[4<<10];

DirectoryMonitor::DirectoryMonitor()
    : m_nPollInterval(0), m_cFiles(0), m_pargFiles(NULL), m_hCompPort(NULL), m_hDir(NULL), m_pFileNamesBuffer(NULL),
      m_pNewestFileCached(NULL)
{
    m_szDir[0] = '\0';
    m_szDirWithSpec[0] = '\0';

    m_overlapped.Internal = 0;
    m_overlapped.InternalHigh = 0;
    m_overlapped.Offset = 0;
    m_overlapped.OffsetHigh = 0;
    m_overlapped.hEvent = NULL;
}

DirectoryMonitor::~DirectoryMonitor()
{
    if (m_hDir)
        CloseHandle(m_hDir);

    if (m_pFileNamesBuffer)
        delete[] m_pFileNamesBuffer;

    if (m_pargFiles)
        delete [] m_pargFiles;
}



/*-------------------------------------------------------------------------
 * FilterFile
 *-------------------------------------------------------------------------
 * Purpose:
 *    Consider filtering out the file.  Some files should never be sent.
 * 
 * Returns:
 *    true if file should be filtered.
 *
 * Remarks:
 *    The client also filters out some files.  The client needs it's own
 *    filter because the server doesn't know everything about the client, like
 *    what build flavor it is running.
 */
bool DirectoryMonitor::FilterFile(WIN32_FIND_DATA & finddata)
{
    //
    // skip directory listings and hidden files
    //
    if (finddata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
        return true;
        
    if (finddata.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
        return true;

    char * szFileName = finddata.cFileName;

    unsigned cLen;

    // we check the first X chars because compressed files have an _ at the
    // end of their filenames
    cLen = 11;
    assert(strlen("Artwork.ini") == cLen);

    if (_strnicmp(szFileName, "Artwork.ini", cLen) == 0)
      return true;

    return false; // false == do not filter
}



/*-------------------------------------------------------------------------
 * SetupMonitor
 *-------------------------------------------------------------------------
 * Purpose:
 *    Get names and time stamps of all files, and setup change notification
 * 
 * Side Effects:
 *    Notification setup, where we can poll for any changes since last poll,
 *      which is done on WM_TIMER
 */
BOOL DirectoryMonitor::SetupMonitor(LPCSTR szDir, int nPollInterval)
{
  m_nPollInterval = nPollInterval;
  // take snapshot of files
  WIN32_FIND_DATA finddata;
  HANDLE hsearchFiles = 0;

  // clean up from any previous times
  m_cFiles = 0;

  int cbValue = strlen(szDir);
  lstrcpy(m_szDir, szDir);
  if (cbValue < 1 || '\\' != szDir[cbValue - 1]) // needs to end in "\"
  {  
    m_szDir[cbValue++] = '\\';
    m_szDir[cbValue] = '\0';
  }

  memcpy(m_szDirWithSpec, m_szDir, cbValue+1);
  m_szDirWithSpec[cbValue++] = '*';
  m_szDirWithSpec[cbValue] = '\0';

  // count the files in the file path
  int cFiles = 0;
  int cFileNameBufferBytes = 0;
  hsearchFiles = FindFirstFile(m_szDirWithSpec, &finddata);
  if (INVALID_HANDLE_VALUE == hsearchFiles)
  {
      return false;
  }
  while (INVALID_HANDLE_VALUE != hsearchFiles)
  {
    if (!FilterFile(finddata))
    {
        // include this file
        cFiles++;
        cFileNameBufferBytes += strlen(finddata.cFileName) + 1;
    }

    if (!FindNextFile(hsearchFiles, &finddata))
    {
      FindClose(hsearchFiles);
      hsearchFiles = INVALID_HANDLE_VALUE;
    }
  }
  
  m_cFiles    = cFiles;

  if (m_pargFiles)
    delete [] m_pargFiles;

  m_pargFiles = new FileChangeInfo[cFiles];

  if (m_pFileNamesBuffer)
    delete[] m_pFileNamesBuffer;

  m_pFileNamesBuffer = new char[cFileNameBufferBytes];
  
  char* pNextSlot = m_pFileNamesBuffer;
  char* pEnd = m_pFileNamesBuffer + cFileNameBufferBytes;

  // now do it again, this time storing the stuff
  hsearchFiles = FindFirstFile(m_szDirWithSpec, &finddata); // search handle
  for(int i=0; (i < m_cFiles) && (INVALID_HANDLE_VALUE != hsearchFiles);)
  {
     // skip directory listings and hidden files
    if (!FilterFile(finddata)) 
    {
        int cbFileName = lstrlen(finddata.cFileName) + 1;

        m_pargFiles[i].szFileName = pNextSlot; 
        pNextSlot += cbFileName;
        if(pNextSlot > pEnd)
        {
            _AGCModule.TriggerEvent(NULL, AllsrvEventID_ArtChangedInInit, "", -1, -1, -1, 0);
            return false;
        }
        CopyMemory(m_pargFiles[i].szFileName, finddata.cFileName, cbFileName);
        m_pargFiles[i].ftLastWriteTime = finddata.ftLastWriteTime;
        m_pargFiles[i].cFileSize = finddata.nFileSizeLow; // note: we assume the FileSize High is zero
        i++;
    }
    if (!FindNextFile(hsearchFiles, &finddata))
    {
      FindClose(hsearchFiles);
      hsearchFiles = INVALID_HANDLE_VALUE;
    }
  }
  FindClose(hsearchFiles);

  if (m_hDir)
    CloseHandle(m_hDir);

  // setup change notification
  m_hDir = CreateFile( m_szDir,
                          FILE_LIST_DIRECTORY,
                          FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                          NULL,
                          OPEN_EXISTING,
                          FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
                          NULL);
  m_hCompPort = CreateIoCompletionPort(m_hDir, NULL, NULL, 0);

  ZAssert(m_hDir != INVALID_HANDLE_VALUE);
  DWORD dwBytesReturned = 0;

  if (IsWinNT())
  {
    ReadDirectoryChangesW(m_hDir,
                          g_chFileChangeBuff,
                          sizeof(g_chFileChangeBuff),
                          TRUE,
                          FILE_NOTIFY_CHANGE_LAST_WRITE,
                          &dwBytesReturned,
                          &m_overlapped,
                          NULL);
  }
  return true;
}

/*-------------------------------------------------------------------------
 * CheckForFileChanges
 *-------------------------------------------------------------------------
 * Purpose:
 *    See if any files have changed, and if so, update our files list
 */
void DirectoryMonitor::CheckForFileChanges()
{
  // Check for new files
  DWORD dwBytesTransferred;
  LPOVERLAPPED pOverlapped = &m_overlapped;
  DWORD dwCompletionKey;
  static char szPrevFileName[c_cbFileName]; // don't process changes twice--bug in ReadDirectoryChangesW
  if (GetQueuedCompletionStatus(m_hCompPort, &dwBytesTransferred, &dwCompletionKey, &pOverlapped, 0))
  {

    DWORD cbOffset;
    PFILE_NOTIFY_INFORMATION pfni = (PFILE_NOTIFY_INFORMATION) g_chFileChangeBuff;

    do
    {
      // convert the string from Unicode to OEM
      char szFileName[c_cbFileName];
      WideCharToMultiByte(CP_OEMCP, 0, pfni->FileName, -1, szFileName, 
          sizeof(szFileName), NULL, NULL);
      cbOffset = pfni->NextEntryOffset;

      if(lstrcmp(szPrevFileName, szFileName)) // skip if same as last file
      {
        if (pfni->Action == FILE_ACTION_MODIFIED)
        {
          lstrcpy(szPrevFileName, szFileName); // save this file name for next time
        
          // Check for known files--one could argue that we need to handle additions...
          int iFile = 0;
          for (iFile = 0; iFile < m_cFiles; iFile++)
          {
            if (!lstrcmpi(m_pargFiles[iFile].szFileName, szFileName)) // equality check
            {
              char szFullPath[128];
              lstrcpy(szFullPath, m_szDir);
              lstrcat(szFullPath, "\\");
              lstrcat(szFullPath, szFileName);
          
              HANDLE hFile = CreateFile(szFullPath, GENERIC_READ, FILE_SHARE_READ,
                                NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0);

              BY_HANDLE_FILE_INFORMATION FileInfo;
              GetFileInformationByHandle(hFile, &FileInfo);
          
              m_pargFiles[iFile].ftLastWriteTime = FileInfo.ftLastWriteTime;
              m_pargFiles[iFile].cFileSize = FileInfo.nFileSizeLow; // note: we are assume the FileSize High is zero

              CloseHandle(hFile);
            }
          }
        }
      }
      else // files matched
        szPrevFileName[0] = 0;
      
      pfni = (PFILE_NOTIFY_INFORMATION)((LPBYTE) pfni + cbOffset);

    } while( cbOffset );

    if (IsWinNT())
    {
      ReadDirectoryChangesW(m_hDir,
                            g_chFileChangeBuff,
                            sizeof(g_chFileChangeBuff),
                            TRUE,
                            FILE_NOTIFY_CHANGE_LAST_WRITE,
                            &cbOffset, // recycling the variable--don't care about it anymore
                            &m_overlapped,
                            NULL);
    }
  }
}

#if 0
    // TODO:  periodically check for changed files every m_nPollingInterval milliseconds
    CheckForChangedFiles();
#endif
    
// nType    one of: edmNewerThan=1, edmEqualTo=2, edmOlderThan=4
// ft       the filetime compared against each file based on nType
// pfci     the function that will be called with each file
// pData    data needed by the callback function to understand the caller's context
bool DirectoryMonitor::IterateFiles(int nType, FILETIME* pft, LPIFFCIPROC FileChangeCallback, void* pData)
{
  bool bAnyMatches = false;

  m_pNewestFileCached = NULL;

  for (int iFile = 0; iFile < m_cFiles; iFile++)
  {
    DWORD dwHighFile = m_pargFiles[iFile].ftLastWriteTime.dwHighDateTime;
    DWORD dwLowFile = m_pargFiles[iFile].ftLastWriteTime.dwLowDateTime;
    BOOL bMatch = false;
    switch(nType)
    {
    case edmNewerThan:
        if (dwHighFile > pft->dwHighDateTime || (dwHighFile == pft->dwHighDateTime && dwLowFile > pft->dwLowDateTime))
            bMatch = true;
        break;
    case edmEqualTo:
        if (dwHighFile == pft->dwHighDateTime && dwLowFile == pft->dwLowDateTime)
            bMatch = true;
        break;
    case edmOlderThan:
        if (dwHighFile < pft->dwHighDateTime || (dwHighFile == pft->dwHighDateTime && dwLowFile < pft->dwLowDateTime))
            bMatch = true;
        break;
    }
    if (bMatch)
    {
        bAnyMatches = true;
        if (!FileChangeCallback(&m_pargFiles[iFile], pData)) // make callback...
            return false; // if any errors, skip art download by returning "no matches"
    }
  }
  return bAnyMatches;
}

FileChangeInfo* DirectoryMonitor::GetNewestFile()
{
  if (m_pNewestFileCached == NULL)
  {
      DWORD dwHighFile = (DWORD)0;
      DWORD dwLowFile = (DWORD)0;
      for (int iFile = 0; iFile < m_cFiles; iFile++)
      {
        if (
            m_pargFiles[iFile].ftLastWriteTime.dwHighDateTime > dwHighFile || 
            (
             m_pargFiles[iFile].ftLastWriteTime.dwHighDateTime == dwHighFile && 
             m_pargFiles[iFile].ftLastWriteTime.dwLowDateTime > dwLowFile
            )
           )
        {
            dwHighFile = m_pargFiles[iFile].ftLastWriteTime.dwHighDateTime;
            dwLowFile = m_pargFiles[iFile].ftLastWriteTime.dwLowDateTime;  
            m_pNewestFileCached = &m_pargFiles[iFile];
        }
      }
  }

  return m_pNewestFileCached;
}

FileChangeInfo* DirectoryMonitor::GetOldestFile() 
{
  assert(0); // note: if this function ever gets used, 
             //       it should use a cached value like GetNewestFile() does with m_pNewestFileCached
  /*
  DWORD dwHighFile = (DWORD)-1;
  DWORD dwLowFile = (DWORD)-1;
  FileChangeInfo* pFile = NULL;
  for (int iFile = 0; iFile < m_cFiles; iFile++)
  {
    if (
        m_pargFiles[iFile].ftLastWriteTime.dwHighDateTime < dwHighFile || 
        (
         m_pargFiles[iFile].ftLastWriteTime.dwHighDateTime == dwHighFile && 
         m_pargFiles[iFile].ftLastWriteTime.dwLowDateTime < dwLowFile 
        )
       )
    {
        dwHighFile = m_pargFiles[iFile].ftLastWriteTime.dwHighDateTime;
        dwLowFile = m_pargFiles[iFile].ftLastWriteTime.dwLowDateTime;  
        pFile = &m_pargFiles[iFile];
    }
  }
  return pFile*/
  return 0; // allow to compile
}