/*****************************************************************************
Open Source MIT License

Copyright (c) 2017 Matthew Ahmed

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*****************************************************************************/

// Util.cpp
// Make functions available to WinMain etc, i.e. outside ZWin class

#include "Headers.h" // includes Util.h

TCHAR sgUtilError[UTIL_MAX_ERROR + 1];

// To return string that doesn't need to be deallocated.
// Make sure no function generates string longer than this.
TCHAR sgUtilRetStr[4096];

BOOL NewStrRaw(LPTSTR & spNew, BOOL bAddRN, INT nExtra, INT nStr, ...)
{
	LPCTSTR sRN = _T("\r\n");
	INT nAddPerStr;
	INT nSizeNewStr;
	LPTSTR spTemp;
	INT i;

	if (bAddRN) nAddPerStr = 2;
	else nAddPerStr = 0;

	// First determine size to allocate to new string
	nSizeNewStr = 0;
	va_list vaNextArg;
	va_start(vaNextArg, nStr); // points vaNextArg to after last required arg

	for (i = 0 ; i < nStr ; i++)
	{
		spTemp = va_arg(vaNextArg, LPTSTR); // increments vaNextArg
		if (spTemp) nSizeNewStr += (_tcslen(spTemp) + nAddPerStr);
	}

	va_end(vaNextArg); // reset to NULL

	nSizeNewStr += (nExtra + 1); // any extra requested plus null terminator

	if ((spNew = new TCHAR[nSizeNewStr]) == NULL) return FALSE;
	spNew[0] = _T('\0');

	// Now copy all non null optional strings to spNew
 	va_start(vaNextArg, nStr);

	for (i = 0 ; i < nStr ; i++)
	{
		spTemp = va_arg(vaNextArg, LPTSTR); // increments vaNextArg
		if (spTemp) // Allow NULLs so calling fn doesn't have to figure it out
		{
			_tcscat(spNew, spTemp);
			if (bAddRN) _tcscat(spNew, sRN);
		}
	}

	va_end(vaNextArg);

	return TRUE;
}

INT MsgB(HWND hWnd, LPCTSTR sCapt, LPCTSTR sMsg1, LPCTSTR sMsg2, LPCTSTR sMsg3,
						LPCTSTR sMsg4, LPCTSTR sMsg5, LPCTSTR sMsg6, UINT uType)
{
	LPTSTR sMsg;

	if (NewStr(sMsg, TRUE, sMsg1, sMsg2, sMsg3, sMsg4, sMsg5, sMsg6))
	{
		INT nRet = MessageBox(hWnd, sMsg, sCapt, uType);
		delete sMsg;
		return nRet;
	}

	// sMsg = new failed; get the message out without allocating string
	MessageBox(hWnd, _T("Error allocating memory for message\n\n")
								_T("The individual parts of the original message will be")
								_T(" displayed in up to 6 successive Message Boxes after")
								_T(" this one."), sCapt, MB_OK | MB_ICONEXCLAMATION);

	// If uType = MB_YESNO etc., only offer choices on last message box
	if (sMsg1)
	{
		if (sMsg2 || sMsg3 || sMsg4 || sMsg5 || sMsg6)
			MessageBox(hWnd, sMsg1, sCapt, MB_OK);
		else return MessageBox(hWnd, sMsg1, sCapt, uType);
	}

	if (sMsg2)
	{
		if (sMsg3 || sMsg4 || sMsg5 || sMsg6)
			MessageBox(hWnd, sMsg2, sCapt, MB_OK);
		else return MessageBox(hWnd, sMsg2, sCapt, uType);
	}
	
	if (sMsg3)
	{
		if (sMsg4 || sMsg5 || sMsg6) MessageBox(hWnd, sMsg3, sCapt, MB_OK);
		else return MessageBox(hWnd, sMsg3, sCapt, uType);
	}

	if (sMsg4)
	{
		if (sMsg5 || sMsg6) MessageBox(hWnd, sMsg4, sCapt, MB_OK);
		else return MessageBox(hWnd, sMsg4, sCapt, uType);
	}

	if (sMsg5)
	{
		if (sMsg6) MessageBox(hWnd, sMsg5, sCapt, MB_OK);
		else return MessageBox(hWnd, sMsg5, sCapt, uType);
	}

	return MessageBox(hWnd, sMsg6, sCapt, uType);
} // END INT MsgB(HWND hWnd, LPCTSTR sCapt, LPCTSTR sMsg1, LPCTSTR sMsg2, ...


BOOL fnError(HWND hWnd, LPCTSTR sCapt, LPCTSTR sErr1, LPCTSTR sErr2,
							LPCTSTR sErr3, LPCTSTR sErr4, LPCTSTR sErr5, LPCTSTR sErr6)
{
	MsgB(hWnd, sCapt, sErr1, sErr2, sErr3, sErr4,
				sErr5, sErr6, MB_OK | MB_ICONEXCLAMATION); ////- | MB_TOPMOST);
	return FALSE;
}

BOOL GetSysErrorMsg(LPTSTR sTarget, DWORD dwErrId)
{
  DWORD dwMessageId = dwErrId ? dwErrId : GetLastError();
	LPVOID lpvErr = NULL;

	BOOL bRet = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER
												      | FORMAT_MESSAGE_FROM_SYSTEM,
											      NULL, dwMessageId,
												    MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
													  (LPTSTR) &lpvErr, 0, NULL);

  if (bRet) _tcscpy(sTarget, (LPCTSTR) lpvErr);
  else _tcscpy(sTarget, _T("Error attempting to retrieve Windows' error message."));

  if (lpvErr) LocalFree(lpvErr);
  return bRet;
}

BOOL fnSysError(DWORD dwErr, HWND hWnd, LPCTSTR sCapt, LPCTSTR sErr1,
									LPCTSTR sErr2, LPCTSTR sErr3, LPCTSTR sErr4)
{
	//LPVOID lpvErr = NULL;
	//LPCTSTR sIntro = "\nWindows' error message was:\n";
  TCHAR sSysMsg[2048];

  if (GetSysErrorMsg(sSysMsg, dwErr))
    return fnError(hWnd, sCapt, sErr1, sErr2, sErr3, sErr4,
                   _T("\nWindows' error message was:"), sSysMsg); 
	else
    return fnError(hWnd, sCapt, sErr1, sErr2, sErr3, sErr4,
                   sSysMsg); // Says failed to get error msg
}

BOOL fnSysError(HWND hWnd, LPCTSTR sCapt, LPCTSTR sErr1,
									LPCTSTR sErr2, LPCTSTR sErr3, LPCTSTR sErr4)
{
  return fnSysError(0, hWnd, sCapt, sErr1, sErr2, sErr3, sErr4);
}

BOOL EnsureDirExists(LPCTSTR sDirPath)
{
  // Returns TRUE if full sDirPath exists already and is a directory, or if
  // succesfully creates lowest level directory in sDirPath; otherwise FALSE.
  // Mainly for ensuring that App Data or My Docs subdirs have been created.

  // This only handles case where the bottom level subdirectory in a path
  // does not exist, and creates it if not found. Will fail if parent
  // directory doesn't exist or otherwise unable to create the subdirectory.

  // Trim off trailing backslash if present
  TCHAR sWorkPath[_MAX_PATH];
  _tcscpy(sWorkPath, sDirPath);
  if ( sWorkPath[_tcslen(sWorkPath) - 1] == _T('\\') )
    sWorkPath[_tcslen(sWorkPath) - 1] = _T('\0');

  WIN32_FIND_DATA fd;
  HANDLE hFind = FindFirstFile(sWorkPath, &fd);
  if (hFind != INVALID_HANDLE_VALUE) // Path exists, check if its a directory
  {
    // First deal with must close handle from successful FindFirstFile when done
    if (!FindClose(hFind))
      return UtilSetSysError(_T("Error closing find handle for:"), sWorkPath);

    if (fd.dwFileAttributes | FILE_ATTRIBUTE_DIRECTORY) return TRUE;
    else return UtilSetError(_T("Path exists but not a directory:"), sWorkPath);
  }

  // Path doesn't exist. Try to create  lowest level subdirectory in the path,
  // but will fail if parent folder doesn't exist (with adequate permission etc).
  // Security descriptor 2nd arg NULL for new dir inherits permissions from parent.
  if (!CreateDirectory(sWorkPath, NULL))
    return UtilSetSysError(_T("Error creating directory:"), sWorkPath);
  return TRUE;
} // BOOL EnsureDirExists(LPCTSTR sDirPath)

BOOL EnsureContainingDirExists(LPCTSTR sFilePath)
{
  // Variation wrapper for EnsureDirExists, for path's containing/parent dir

  TCHAR sWorkPath[_MAX_PATH];
  _tcscpy(sWorkPath, sFilePath);

  // First handle unlikely case where sFilePath (for a dir) ends with a backslash
  if ( sWorkPath[_tcslen(sWorkPath) - 1] == _T('\\') )
    sWorkPath[_tcslen(sWorkPath) - 1] = _T('\0');

  // Find last backslash still in path. String to left of it is parent dir's path.
  LPTSTR sFind = StrRChr(sWorkPath, NULL, _T('\\'));
  if (!sFind)
    return UtilSetError(_T("Error finding parent directory path in:"), sWorkPath);

  // Truncate sWorkPath to containing directory's path
  sFind[0] = _T('\0');
  return EnsureDirExists(sWorkPath);
}

// Laziness function for where handle isn't kept open
BOOL WriteStrToFile(LPCTSTR sMsg, LPCTSTR sPath,
                    BOOL bAppend, BOOL bAddRN, BOOL bFlush)
{
  TCHAR sWork[2048];
  DWORD dwCreationDisposition;
  UtilClearError();

  if (!EnsureContainingDirExists(sPath)) // Create if not already there
    return FALSE;  // EnsureContainingDirExists already did UtilSetError

  if (bAppend) dwCreationDisposition = OPEN_ALWAYS; // Open existing or create
  else dwCreationDisposition = CREATE_ALWAYS;

  HANDLE hFile = CreateFile(sPath, GENERIC_WRITE, FILE_SHARE_READ, NULL,
                            dwCreationDisposition, FILE_ATTRIBUTE_NORMAL, NULL);

  if (hFile == INVALID_HANDLE_VALUE)
  {
    GetSysErrorMsg(sWork);
    return UtilSetError(_T("Error opening file for write:"), sPath, sWork);
  }

  if (bAppend
      && SetFilePointer(hFile, NULL, NULL, FILE_END) == INVALID_SET_FILE_POINTER)
  {
    GetSysErrorMsg(sWork);
    CloseHandle(hFile);
    return UtilSetError(_T("Error seeking end of file for append:"), sPath, sWork);
  }

  INT nBytes = _tcslen(sMsg) * sizeof(TCHAR);
  INT nBytesRN = 2 * sizeof(TCHAR);
  DWORD dwIgnore;

  if (!WriteFile(hFile, sMsg, (DWORD) nBytes, &dwIgnore, NULL)
      || (bAddRN && !WriteFile(hFile, sMsg, (DWORD) nBytesRN, &dwIgnore, NULL))
      || (bFlush && !FlushFileBuffers(hFile)) )
  {
    GetSysErrorMsg(sWork);
    CloseHandle(hFile); // Caller assumes file not open if this returns FALSE
    return UtilSetError(_T("Error writing to file:"), sPath, sWork);
  }

  if (!CloseHandle(hFile))
  {
    GetSysErrorMsg(sWork);
    return UtilSetError(_T("Error closing file:"), sPath, sWork);
  }

  return TRUE;
} // BOOL WriteStrToFile(LPCTSTR sMsg, LPCTSTR sPath,
  //                     BOOL bAppend, BOOL bAddRN, BOOL bFlush)

LPCTSTR TimeStamp()
{
  GetTimeFormat(LOCALE_USER_DEFAULT, NULL, NULL, _T("HH':'mm':'ss"),
                sgUtilRetStr, 16);
  return sgUtilRetStr;
}

/*
void DeleteStringArray(LPTSTR * & sppArr, INT nArrSize)
{
  if (nArrSize < 1) return;
  if (sppArr == NULL) return;

  for (INT i = 0; i < nArrSize; i++)
  {
    if (sppArr[i] == NULL) continue;
    delete sppArr[i];
    sppArr[i] = NULL;
  }
  delete [] sppArr;
  sppArr = NULL;
}

BOOL AllocateStringArray(LPTSTR * & sppArr, INT nArrSize,
                         INT nMemberLen, LPTSTR sError)
{
  sppArr = new LPTSTR[nArrSize];
  if (! sppArr)
  {
    _tcscpy(sError, _T("Error allocating array of string pointers"));
    return FALSE;
  }

  // Set all members NULL before allocating any,
  // so DeleteStringArray knows which to delete if error
  for (INT i = 0; i < nArrSize; i++) sppArr[i] = NULL;

  for (i = 0; i < nArrSize; i++)
  {
    sppArr[i] = new TCHAR[nMemberLen];
    if (!sppArr[i])
    {
      DeleteStringArray(sppArr, nArrSize);
      _stprintf(sError,
               _T("Error allocating string (index %d of %d array members)"),
                i, nArrSize);
      return FALSE;
    }
  }

  return TRUE;
}
*/

// Overloaded ParsePath gets filename w/ extension, instead of separate string
BOOL ParsePath(LPCTSTR sPath, LPTSTR sDirSlash, LPTSTR sFullName,
               BOOL bRequireExt)
{
  TCHAR slNameNoExt[_MAX_PATH];
  TCHAR slDotExt[128];

  if (!ParsePath(sPath, sDirSlash, slNameNoExt, slDotExt, bRequireExt))
    return FALSE; // Error message alread in sDirSlash

  _stprintf(sFullName, _T("%s%s"), slNameNoExt, slDotExt);
  return TRUE;
}


BOOL ParsePath(LPCTSTR sPath, LPTSTR sDirSlash, LPTSTR sNameNoExt,
               LPTSTR sDotExt, BOOL bRequireExt)
{
  // Assumes file name has dot extension, error if not
  // Error message in sDirSlash if return FALSE
  TCHAR sTempPath[_MAX_PATH];
  _tcscpy(sTempPath, sPath);

	LPTSTR spFind = _tcsrchr(sTempPath, _T('.'));
	if (!spFind)
  {
    if (bRequireExt)
    {
      _tcscpy(sDirSlash, _T("'.' not found in supplied sPath"));
      return FALSE;
    }
    else sDotExt[0] = _T('\0');
  }
  else
  {
	  _tcscpy(sDotExt, spFind);
    spFind[0] = _T('\0'); // Truncates sTempPath before .
  }

	spFind = _tcsrchr(sTempPath, _T('\\'));
	if (!spFind)
  {
    _tcscpy(sDirSlash,_T( "'\\' not found in supplied sPath"));
    return FALSE;
  }

	_tcscpy(sNameNoExt, &spFind[1]);
  spFind[1] = _T('\0'); // Truncates sTempPath after last '\'

  _tcscpy(sDirSlash, sTempPath);

  if (_tcslen(sDirSlash) < 3 || !_tcslen(sNameNoExt)) // Minimal reality check
  {
    _tcscpy(sDirSlash, _T("Directory name less than 3 chars, or empty file name"));
    return FALSE;
  }
  
  return TRUE;
}

/*
// COMMENT OUT THESE TWO LATER
BOOL GetOwnPathParsed(LPTSTR sDirSlash, LPTSTR sNameNoExt, LPTSTR sDotExt)
{
  // assumes sDirSlash _MAX_PATH. Error message in sDirSlash if return FALSE
  TCHAR sFullPath[_MAX_PATH];
  if (!GetModuleFileName(NULL, sFullPath, _MAX_PATH))
  {
    _tcscpy(sDirSlash, _T("GetModuleFileName failed"));
    return FALSE;
  }
  return ParsePath(sFullPath, sDirSlash, sNameNoExt, sDotExt);
}

BOOL GetOwnDirSlash(LPTSTR sDirSlash)
{
  // assumes sDirSlash _MAX_PATH. Error message in sDirSlash if return FALSE
  TCHAR sIgnoreName[128];
  TCHAR sIgnoreExt[32];

  return GetOwnPathParsed(sDirSlash, sIgnoreName, sIgnoreExt);
}
*/

// Replace both GetOwn... above with renamed to force errors to replace w appdata dir etc

BOOL GetProgPathParsed(LPTSTR sDirSlash, LPTSTR sNameNoExt, LPTSTR sDotExt)
{
  // assumes sDirSlash _MAX_PATH. Error message in sDirSlash if return FALSE
  TCHAR sFullPath[_MAX_PATH];
  if (!GetModuleFileName(NULL, sFullPath, _MAX_PATH))
  {
    _tcscpy(sDirSlash, _T("GetModuleFileName failed"));
    return FALSE;
  }
  return ParsePath(sFullPath, sDirSlash, sNameNoExt, sDotExt);
}
/*
BOOL GetProgDirSlash(LPTSTR sDirSlash)
{
  // assumes sDirSlash _MAX_PATH. Error message in sDirSlash if return FALSE
  TCHAR sIgnoreName[128];
  TCHAR sIgnoreExt[32];

  return GetOwnPathParsed(sDirSlash, sIgnoreName, sIgnoreExt);
}
*/
// Dirs in user profile to use instead of Prog Dir, to work with Win7 UAC
BOOL GetAppPathsInfo(LPTSTR sProgDirSlash, LPTSTR sDataDirSlash,
                     LPTSTR sDocsDirSlash, LPTSTR sExeNameNoDotExt,
                     LPCTSTR sSubfolderName, BOOL bProgForAllIfUser)
{
  // assumes sDirSlash _MAX_PATH. Error message in sProgDirSlash if return FALSE
//  TCHAR sExeNameNoExt[128];
  TCHAR sDotExt[32];
  LPCTSTR sEnvStrError = _T("Unable to expand \"%s\""); // for sprintf

  if (! GetProgPathParsed(sProgDirSlash, sExeNameNoDotExt, sDotExt))
    return FALSE; // Error message in sProgDirSlash

  if (bProgForAllIfUser) // default TRUE
  {
    // If prog installed in user profile dir, put all files in prog dir
    LPCTSTR sEnvStr = _T("%USERPROFILE%");
    TCHAR sUserProfileDir[_MAX_PATH];
    if (! ExpandEnvironmentStrings(sEnvStr, sUserProfileDir, _MAX_PATH))
    {
      _stprintf(sProgDirSlash, sEnvStrError, sEnvStr);
      return FALSE;
    }

    if (StrStrI(sProgDirSlash, sUserProfileDir))
    {
      // Prog dir is a subdirectory of user profile folder
      _tcscpy(sDataDirSlash, sProgDirSlash);
      _tcscpy(sDocsDirSlash, sProgDirSlash);
      return TRUE;
    }
    // else drop through, set output dirs as subdirs of app data and my documents
  } // if (bProgForAllIfUser)

  // Name of subfolder of app data & my docs. Use exe file name if none supplied
  TCHAR sAppendSlashSubdirSlash[128];
  if (sSubfolderName) _stprintf(sAppendSlashSubdirSlash, _T("\\%s\\"), sSubfolderName); 
  else _stprintf(sAppendSlashSubdirSlash, _T("\\%s\\"), sExeNameNoDotExt); 

  TCHAR sTempPath[_MAX_PATH];
  LPTSTR sKey = _T("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders");

  if (! ReadReg(HKEY_CURRENT_USER, sKey, _T("AppData"), sTempPath))
  {
    _stprintf(sProgDirSlash, _T("Error getting reg value AppData\n%s"), sTempPath);
    return FALSE;
  }

  if (! ExpandEnvironmentStrings(sTempPath, sDataDirSlash, _MAX_PATH))
  {
    _stprintf(sProgDirSlash, sEnvStrError, sTempPath);
    return FALSE;
  }
  // sDataDirSlash is now like "...\AppData\Roaming" (Win7) or "...\AppData" (XP)
  _tcscat(sDataDirSlash, sAppendSlashSubdirSlash);

  if (! ReadReg(HKEY_CURRENT_USER, sKey, _T("Personal"), sTempPath))
  {
    _stprintf(sProgDirSlash, _T("Error getting reg value Personal\n%s"), sTempPath);
    return FALSE;
  }

  if (! ExpandEnvironmentStrings(sTempPath, sDocsDirSlash, _MAX_PATH))
  {
    _stprintf(sProgDirSlash, sEnvStrError, sTempPath);
    return FALSE;
  }
  // sDocsDirSlash is now like "...\Documents" (Win7) or "...\My Documents" (XP)
  _tcscat(sDocsDirSlash, sAppendSlashSubdirSlash);

  return TRUE;
} // GetAppDirsProgDataDocs(LPTSTR sProgDirSlash, LPTSTR sDataDirSlash, LPTSTR sDocsDirSlash,
  //                        BOOL bProgForAllIfUser) // default TRUE

// Cloned from ZWin
BOOL ReadReg(HKEY hOpen, LPCTSTR sKey, LPCTSTR sValue,
						 LPTSTR sTarget, DWORD dwSize)
{
	HKEY hKey;
	LONG lRet = RegOpenKeyEx(hOpen, sKey, 0, KEY_READ, &hKey);
	if (lRet != ERROR_SUCCESS)
  {
    GetSysErrorMsg(sTarget, lRet);
    return FALSE;
  }

	DWORD dw = dwSize;
	lRet = RegQueryValueEx(hKey, sValue, 0, NULL, (BYTE*) sTarget, &dw);
	RegCloseKey(hKey); // whether or not error

	if (lRet != ERROR_SUCCESS)
  {
    GetSysErrorMsg(sTarget, lRet);
    return FALSE;
  }

	return TRUE;
}


BOOL StrCpyWhatFits(LPTSTR sTarget, INT nMaxCharsTarget, LPCTSTR sSource)
{
  //INT nMaxCharsTarget = (sizeof(sTarget) / sizeof(TCHAR)) - 1;
  if (nMaxCharsTarget < 1) return FALSE;

  if (_tcslen(sSource) <= (UINT) nMaxCharsTarget)
  {
    _tcscpy(sTarget, sSource);
    return TRUE; // All copied
  }

  // Concatenate part of sSource if any room
  _tcsncpy(sTarget, sSource, nMaxCharsTarget);
   sTarget[nMaxCharsTarget] = _T('\0');

   return FALSE; // Not all copied
}

BOOL StrCatWhatFits(LPTSTR sTarget, INT nMaxCharsTarget,
                    LPCTSTR sSource, BOOL bInsertLineBreak)
{
  //INT nMaxCharsTarget = (sizeof(sTarget) / sizeof(TCHAR)) - 1;
  if (nMaxCharsTarget < 1) return FALSE;

  INT nMaxCharsCat = nMaxCharsTarget - _tcslen(sTarget);
  if (bInsertLineBreak) nMaxCharsCat -= 2; // for \r\n

  if (nMaxCharsCat > 0 && bInsertLineBreak) _tcscat(sTarget, _T("\r\n"));

  if (_tcslen(sSource) <= (UINT) nMaxCharsCat)
  {
    _tcscat(sTarget, sSource);
    return TRUE; // All appended
  }

  // Concatenate part of sSource if any room
  if (nMaxCharsCat > 0)
  {
    _tcsncat(sTarget, sSource, nMaxCharsCat);
    sTarget[nMaxCharsTarget] = _T('\0');
  }
  return FALSE; // None or part was appended
}

INT StrCatArrToStr(LPTSTR sTarget, INT nMaxCharsTarget,
                   LPCTSTR * sSrcArr, INT nArrSize, LPCTSTR sSep)
{
  // Concats array members to string, up to max strlen nMaxCharsTarget (w/o \0).
  // N.B. sTarget must be initialized, either empty or with initial content to
  // append to. Any chars already in sTarget count against nMaxCharsTarget.
  // sSep (if any) is inserted between each string copied, but not at end. If sTarget
  // is not initially empty, sSep is also inserted before first string copied.
  // Returns number of array members copied, i.e. nArrSize if entire array copied.
  INT nLenSep;
  if (sSep && sSep[0]) nLenSep = _tcslen(sSep); // e.g. \n or \r\n
  else nLenSep = 0; // No separator string, null or empty

  for (INT i = 0; i < nArrSize; i++)
  {
    INT nCharsSoFar = _tcslen(sTarget); // 0 on 1st loop unless initial content
    INT nLenThis = _tcslen(sSrcArr[i]) + (nCharsSoFar ? nLenSep : 0);

    if (nCharsSoFar + nLenThis > nMaxCharsTarget) // Not enough space to copy more
      return i; // Number of array members copied (less than whole array)

    if (nLenSep && nCharsSoFar) _tcscat(sTarget, sSep);
    _tcscat(sTarget, sSrcArr[i]);
    nCharsSoFar += nLenThis;
  }

  return nArrSize; // Finished loop, copied entire array
}

/*
UINT GetRandomUint(UINT uMaxResult, LPTSTR sRetMsg)
{
  // Returns random number from 0 to uMaxResult

  if (sRetMsg) sRetMsg[0] = _T('\0'); // Return empty if result on 1st loop

  // Allow this for e.g. loops that don't check if more than one possible value
  if (!uMaxResult) return 0;

  // First determine max return value from rand() to accept, an integer
  // multiple of uMaxResult; will repeat rand() if result is too high.
  // When RAND_MAX (32767) + 1 is not exact multiple of uMaxResult + 1,
  // using (rand() % uMaxResult) is biased toward lower results.
  // Bias increases with larger uMaxResult.
  // Use integer division to discard remainder and get max unbiased random.
  // All the "+ 1" & "- 1" below are because minimum rand() is 0 instead of 1.
  UINT uMaxOkRand = ( ((RAND_MAX + 1) / (uMaxResult + 1)) * (uMaxResult + 1) )
                    - 1;

  UINT uLoops; // Only for logging info
  UINT uMaxLoops = 10;
  UINT uRand;
  for (uLoops = 1;
       (uRand = rand()) > uMaxOkRand && uLoops <= uMaxLoops;
       uLoops++) ; // Do nothing in loop body
 
  // Now either have good uRand, or reached max loops and accept bias.
  // Report if took more than one loop
  if (uLoops > 1 && sRetMsg) // If null, caller doesn't want message
  {
    _stprintf(sRetMsg, _T("GetRandomUint took %u loops to get uRand %u"
                          "  uMaxOkRand = %u  uMaxResult = %u"),
                       uLoops, uRand, uMaxOkRand, uMaxResult);
  }

  return (uRand % (uMaxResult + 1));
} // UINT GetRandomUint(UINT uMaxResult)
*/

UINT GetRandomUint(UINT uMaxResult, LPTSTR sRetMsg)
{
  // Always seed before first call to this, SeedForGetRandomUint or directly
  // Returns random number from 0 to uMaxResult
  // Generates up to RAND_MAX_BIG = 1073741823 = ((RAND_MAX << 15) + RAND_MAX)
  if (sRetMsg) sRetMsg[0] = _T('\0'); // Return empty if result on 1st loop

  // Allow this for e.g. loops that don't check if more than one possible value
  if (!uMaxResult) return 0;

  UINT uRand;
  UINT uLoops;
  UINT uMaxLoops = 10; // Give up and accept biased value after this many tries
  UINT uMaxOkRand;     // max return of rand() that isn't biased

  // If uMaxResult is more than small fraction of RAND_MAX (32767), use
  // the RAND_MAX_BIG version to reduce odds of biased uRand
  if (uMaxResult < 1000)
//  if (uMaxResult < 20) // TESTING, ALMOST ALWAYS DO BIG VERSION
  {
    // When RAND_MAX (32767) + 1 is not exact multiple of uMaxResult + 1,
    // using (rand() % (uMaxResult + 1)) is biased toward lower results.
    // Bias increases with larger uMaxResult.
    // Use integer division to discard remainder and get max unbiased random.
    // All the "+ 1" & "- 1" below are because minimum rand() is 0 instead of 1.
    uMaxOkRand = ( ( (RAND_MAX + 1) / (uMaxResult + 1) )
                   * (uMaxResult + 1)
                 )
                 - 1;

    for (uLoops = 1;
         (uRand = rand()) > uMaxOkRand && uLoops <= uMaxLoops;
         uLoops++) ; // Do nothing in loop body, just set uRand <= uMaxOkRand
  }
  else // For larger uMaxResult, use two rand() calls for wider range of uRand
  {
    uMaxOkRand = ( ( (RAND_MAX_BIG + 1) / (uMaxResult + 1) )
                   * (uMaxResult + 1)
                 )
                 - 1;

    // rand() + (rand() << 15) gets random num 0 to RAND_MAX_BIG = 1,073,741,823
    // For some reason, the (rand() << 15)) always executes before the rand(),
    // regardless of order in the statement. Doesn't matter here.
    for (uLoops = 1;
         (uRand = rand() + (rand() << 15)) > uMaxOkRand && uLoops <= uMaxLoops;
         uLoops++) ; // Do nothing in loop body, just set uRand
  }

  // Now either have good uRand, or very unlikely hit max loops & accept bias
  // Report if took more than one loop
  if (uLoops > 1 && sRetMsg) // If null sRetMsg, caller doesn't want message
  {
    _stprintf(sRetMsg, _T("GetRandomUintBig took %u loops to get uRand %u")
                          _T("  uMaxOkRand = %u  uMaxResult = %u"),
                       uLoops, uRand, uMaxOkRand, uMaxResult);
  }

  return (uRand % (uMaxResult + 1));
} // UINT GetRandomUint(UINT uMaxResult, LPTSTR sRetMsg)

void SeedForGetRandomUint()
{
  srand((UINT) GetTickCount()); // Caller could do this directly
}

BOOL UtilSetError(LPCTSTR sMsg1, LPCTSTR sMsg2, LPCTSTR sMsg3,
					        LPCTSTR sMsg4, LPCTSTR sMsg5, LPCTSTR sMsg6)
{
  // Always returns FALSE.
  sgUtilError[0] = _T('\0');
  // Concat any non-null sMsgN's until run out of space.
  // StrCatWhatFits return FALSE means no more room, so discard the rest.
  if (sMsg1 && !StrCatWhatFits(sgUtilError, UTIL_MAX_ERROR, sMsg1, TRUE))
    return FALSE;
  if (sMsg2 && !StrCatWhatFits(sgUtilError, UTIL_MAX_ERROR, sMsg2, TRUE))
    return FALSE;
  if (sMsg3 && !StrCatWhatFits(sgUtilError, UTIL_MAX_ERROR, sMsg3, TRUE))
    return FALSE;
  if (sMsg4 && !StrCatWhatFits(sgUtilError, UTIL_MAX_ERROR, sMsg4, TRUE))
    return FALSE;
  if (sMsg5 && !StrCatWhatFits(sgUtilError, UTIL_MAX_ERROR, sMsg5, TRUE))
    return FALSE;
  if (sMsg6 && !StrCatWhatFits(sgUtilError, UTIL_MAX_ERROR, sMsg6, TRUE))
    return FALSE;
  return FALSE;
}

BOOL UtilSetSysError(LPCTSTR sMsg1, LPCTSTR sMsg2, LPCTSTR sMsg3,
					           LPCTSTR sMsg4, LPCTSTR sMsg5)
{
  TCHAR sSysError[1024]; // Sizes are overkill
  TCHAR sFullSysMsg[2048];

  GetSysErrorMsg(sSysError);
  _stprintf(sFullSysMsg, _T("Windows' error message was:\r\n%s"), sSysError);

  return UtilSetError(sMsg1, sMsg2, sMsg3, sMsg4, sMsg5, sFullSysMsg);
}

BOOL UtilGetError(LPTSTR sTarget, BOOL bClear)
{
  // Just in case called before appropriate, i.e. uninitialized sgUtilError:
  if (_tcslen(sgUtilError) > UTIL_MAX_ERROR) UtilClearError();

  _tcscpy(sTarget, sgUtilError);
  BOOL bRet = (BOOL) sgUtilError[0]; // return FALSE, i.e. no error, if empty
  if (bClear) UtilClearError();
  return bRet;
}

LPCTSTR UtilError(BOOL bClearError)
{
  // Just in case called before appropriate, i.e. uninitialized sgUtilError:
  if (_tcslen(sgUtilError) > UTIL_MAX_ERROR) UtilClearError();
  _tcscpy(sgUtilRetStr, sgUtilError);
  if (bClearError) UtilClearError();
  return sgUtilRetStr;
}

void UtilClearError()
{
  sgUtilError[0] = _T('\0');
}

void StrCatMult(LPTSTR sTarget, LPCTSTR sCat1, LPCTSTR sCat2,
                LPCTSTR sCat3, LPCTSTR sCat4, LPCTSTR sCat5)
{
  // Allow caller to pass possibly null pointers for each parameter
  if (sCat1) _tcscat(sTarget, sCat1);
  if (sCat2) _tcscat(sTarget, sCat2);
  if (sCat3) _tcscat(sTarget, sCat3);
  if (sCat4) _tcscat(sTarget, sCat4);
  if (sCat5) _tcscat(sTarget, sCat5);
}

BOOL RectanglesOverlap(RECT* pr1, RECT* pr2)
{
  // For faster less work on FALSE, pr1 should be the rectangle
  // suspected to be to above or to the left of the other.
  if (pr1->bottom < pr2->top || pr1->right < pr2->left ||
      pr2->bottom < pr1->top || pr2->right < pr1->left)
    return FALSE;

  // Return FALSE if either rectangle empty (both dimensions).
  // Area theoretically zero can still return TRUE, i.e. like a line.
  if ( (pr1->left == pr1->right && pr1->top == pr1->bottom) ||
       (pr2->left == pr2->right && pr2->top == pr2->bottom) )
    return FALSE;

  return TRUE;
}

/*
char* Utf8toAnsi( const char * utf8, int len )
// From http://www.codeguru.com/forum/archive/index.php/t-288665.html
{
  char *ansistr = NULL;
  int length = MultiByteToWideChar(CP_UTF8, 0, utf8, len, NULL, NULL );
  WCHAR *lpszW = NULL;

  lpszW = new WCHAR[length+1];
  ansistr = ( char * ) calloc ( sizeof(char), length+5 );

  //this step intended only to use WideCharToMultiByte
  MultiByteToWideChar(CP_UTF8, 0, utf8, -1, lpszW, length );

  //Conversion to ANSI (CP_ACP)
  WideCharToMultiByte(CP_ACP, 0, lpszW, -1, ansistr, length, NULL, NULL);

  ansistr[length] = 0;

  delete[] lpszW;

  return ansistr;
}
// Wrapper for above that handles freeing memory
void Utf8ToAnsi(LPCTSTR sSource, LPSTR sTargetAnsi)
{
  INT nLenBytes = strlen((LPCSTR) sSource) + 1;

  LPSTR sReturnAnsi = Utf8toAnsi((LPCSTR) sSource, nLenBytes);
  strcpy(sTargetAnsi, sReturnAnsi);

  free(sReturnAnsi); // deletes string allocated by calloc
}

BOOL TcharToWchar(LPCTSTR sSourceT, LPWSTR sTargetW)
{
  // Get length in bytes, not multibyte character csount
  INT nLenBytes = strlen((LPCSTR) sSourceT) + 1; // Not as LPCTSTR

  INT nLenWchar = MultiByteToWideChar(CP_UTF8, 0, (LPCSTR) sSourceT, 
                                      nLenBytes, NULL, NULL );

  if (!nLenWchar)
    return UtilSetSysError(_T("Error getting wide char count for display string"));

  if (!MultiByteToWideChar(CP_UTF8, 0, (LPCSTR) sSourceT, -1, sTargetW, nLenWchar))
    return UtilSetSysError(
                 _T("Error converting display string from utf8 to wide char"));

  return TRUE;
}
*/

BOOL Utf16ToAnsi(LPCWSTR swSource, LPSTR sTarget)
{
  // Don't want either of these counts to include the null terminator
  INT nCharsSource = wcslen(swSource);

  if (!nCharsSource)
  {
    // Return empty string here, otherwise spurious error for nBytesTarget
    sTarget[0] = '\0'; // Not _T('\0')
    return TRUE;
  }

  // Assume caller set sTarget big enough, but here don't know what it is.
  // To know param size big enough not to cause WideCharToMultiByte error on the
  // acutal copy, first run with null to be told how big.
  INT nBytesTarget = WideCharToMultiByte(CP_ACP, 0, // code page, flags
                                         swSource, nCharsSource,
                                         NULL, NULL, // target, byte length
                                         NULL, NULL); // options
  if (!nBytesTarget)
    return UtilSetSysError(
               _T("WideCharToMultiByte error getting ANSI byte count"));

  // Now convert into the target buffer.
  nBytesTarget = WideCharToMultiByte(CP_ACP, 0, swSource, nCharsSource,
                                     sTarget, nBytesTarget, NULL, NULL);
  if (!nBytesTarget)
    return UtilSetSysError(_T("WideCharToMultiByte error converting to ANSI"));

  // Because nCharsSource didn't include null terminator, none was appended
  // to sTarget. nBytesTarget is the number of bytes copied, presumably
  // same as on the first call. Append \0 after that.
  sTarget[nBytesTarget] = '\0'; // Not _T('\0')

  return TRUE;
}

BOOL Utf16ToUtf8(LPCWSTR swSource, LPSTR sTarget)
{
  // Don't want either of these counts to include the null terminator
  INT nCharsSource = wcslen(swSource);

  if (!nCharsSource)
  {
    // Return empty string here, otherwise spurious error for nBytesTarget
    sTarget[0] = '\0'; // Not _T('\0')
    return TRUE;
  }

  // Assume caller set sTarget big enough, but here don't know what it is.
  // To know param size big enough not to cause WideCharToMultiByte error on the
  // acutal copy, first run with null to be told how big.
  INT nBytesTarget = WideCharToMultiByte(CP_UTF8, 0, // code page, flags
                                         swSource, nCharsSource,
                                         NULL, NULL, // target, byte length
                                         NULL, NULL); // options
  if (!nBytesTarget)
    return UtilSetSysError(
               _T("WideCharToMultiByte error getting UTF-8 byte count"));

  // Now convert into the target buffer.
  nBytesTarget = WideCharToMultiByte(CP_UTF8, 0, swSource, nCharsSource,
                                     sTarget, nBytesTarget, NULL, NULL);
  if (!nBytesTarget)
    return UtilSetSysError(_T("WideCharToMultiByte error converting to UTF-8"));

  // Because nCharsSource didn't include null terminator, none was appended
  // to sTarget. nBytesTarget is the number of bytes copied, presumably
  // same as on the first call. Append \0 after that.
  sTarget[nBytesTarget] = '\0'; // Not _T('\0')

  return TRUE;
}

// These date arithmetic functions depend on time.h in Headers.h

// Returns date INT like 20081231 for comparing to similarly generated number
//INT CMinDlg::YYYYMMDD_n_days_ago(INT nDaysAgo)
INT N_DaysAgo_to_IntYYYYMMDD(INT nDaysAgo) // Works ok with negative nDaysAgo
{
  // If 0 returned & is plausible value, must UtilGetError to see if error or not
  UtilClearError();

  __time64_t t64_now = (__time64_t) -1; // Init in case _time64 does nothing
  _time64(&t64_now); // system time as seconds since Jan 1, 1970 GMT

  // -1 on _time64 error
  if (t64_now < 0)
    return UtilSetError(_T("Invalid _time64 result"));

  __int64 nSecondsAgo = 60 * 60 * 24 * (__int64) nDaysAgo;
  __time64_t t64_then = t64_now - nSecondsAgo; // __time64_t of nDaysAgo GMT

  // Returned pointer to static tm structure valid until next _localtime64 call
  struct tm * ptm_then_local = _localtime64(&t64_then);

  // Adjust values from tm's version to "normal" versions (same as in WMP values)
  // Multiply as necessary for final value like 20081231, e.g. 2008 -> 20080000
  INT nReturn = (10000 * (ptm_then_local->tm_year + 1900))
                + (100 * (ptm_then_local->tm_mon + 1))     // Jan = 0 in tm_mon
                + ptm_then_local->tm_mday;
  return nReturn;
}

INT IntYYYYMMDD_to_N_DaysAgo(INT nYYYYMMDD)
{
  // If 0 returned & is plausible value, must UtilGetError to see if error or not
  UtilClearError();

  __time64_t t64_now = (__time64_t) -1; // Init in case _time64 does nothing
  _time64(&t64_now); // system time as seconds since Jan 1, 1970 GMT

  // For comparison with nYYYYMMDD, truncate t64_now date-time to just date
  // Returned pointer to static tm structure valid until next _localtime64 call
  struct tm * ptm_now_local = _localtime64(&t64_now);
  __int64 nSecondsAfterMidnight = (ptm_now_local->tm_hour * 360)
                                  + (ptm_now_local->tm_min * 60)
                                  + ptm_now_local->tm_sec;
  t64_now -= nSecondsAfterMidnight;
  // Minimal reality check
  if (t64_now < 0)
    return UtilSetError(_T("Invalid _time64 subtraction result"));

  // Convert nYYYYMMDD arg to minimal tm structure, then __time64_t from that
  struct tm tm_then;

  tm_then.tm_year = (nYYYYMMDD / 10000) - 1900; // e.g. 110 from 20101231
  tm_then.tm_mon = ((nYYYYMMDD % 10000) / 100) - 1; // 11 from 20101231
  tm_then.tm_mday = nYYYYMMDD % 100; // 31 from 20101231
  tm_then.tm_hour = 0;
  tm_then.tm_min = 0;
  tm_then.tm_sec = 0;
  tm_then.tm_isdst = -1; // -1 to use system's Daylight Savings Time setting

  __time64_t t64_then = _mktime64(&tm_then);
  if (t64_then < 0)
    return UtilSetError(_T("Invalid _mktime64 result"));

  __int64 nSecondsAgo = t64_now - t64_then; // OK if negative
  return (INT) (nSecondsAgo / (60 * 60 * 24)); // Number of days ago
}


// BEGIN REQUIRE shlwapi.h AND ADD shlwapi.lib TO LINK COMMAND LINE
#if defined(_INC_SHLWAPI)

// This function tested with UTF-16, not sure if it works with UTF-8
// Only converts combos most commonly seen in music names
// For use in normalization, assuming character already set to lower case.
// Note: with diaeresis conversions, maybe should also normalize "ae" to "a" etc
// Character codes at http://www.unicode.org/Public/UNIDATA/NamesList.txt
TCHAR LatinComponentForLower(const TCHAR c)
{
  // This range includes a lot of combos besides the ones converted.
  // Maybe add more conversions later.
  if (c >= _T('\x00E0') && c <= _T('\x00FF'))
  {
    if (c == _T('\x00E0')) return _T('a'); // WITH GRAVE
    if (c == _T('\x00E1')) return _T('a'); // WITH ACUTE
    if (c == _T('\x00E3')) return _T('a'); // WITH TILDE
    if (c == _T('\x00E4')) return _T('a'); // WITH DIAERESIS
    if (c == _T('\x00E6')) return _T('a'); // LATIN SMALL LETTER AE 
    if (c == _T('\x00E7')) return _T('c'); // WITH CEDILLA
    if (c == _T('\x00E8')) return _T('e'); // WITH GRAVE
    if (c == _T('\x00E9')) return _T('e'); // WITH ACUTE
    if (c == _T('\x00EB')) return _T('e'); // WITH DIAERESIS
    if (c == _T('\x00EC')) return _T('i'); // WITH GRAVE
    if (c == _T('\x00ED')) return _T('i'); // WITH ACUTE
    if (c == _T('\x00EF')) return _T('i'); // WITH DIAERESIS
    if (c == _T('\x00F1')) return _T('n'); // WITH TILDE
    if (c == _T('\x00F2')) return _T('o'); // WITH GRAVE
    if (c == _T('\x00F3')) return _T('o'); // WITH ACUTE
    if (c == _T('\x00F5')) return _T('o'); // WITH TILDE
    if (c == _T('\x00F6')) return _T('o'); // WITH DIAERESIS
    if (c == _T('\x00F8')) return _T('o'); // WITH STROKE (Norwegian etc)
    if (c == _T('\x00F9')) return _T('u'); // WITH GRAVE
    if (c == _T('\x00FA')) return _T('u'); // WITH ACUTE
    if (c == _T('\x00FC')) return _T('u'); // WITH DIAERESIS
    if (c == _T('\x00FF')) return _T('y'); // WITH DIAERESIS
    // else drop through
  }

  return c; // Return original if haven't returned converted above
}

LPCTSTR Normalized(LPCTSTR sSource, LPTSTR sTarget, INT nHowMuch)
// All nHowMuch levels case insensitive; omit leading/trailing articles,
// omit chars not letter, digit, space; omit leading 0 in consecutiv digits;
// collapse multiple spaces to single space.
// UTL_NML_MED = 2 also removes all spaces.
// UTL_NML_MAX = 3 also removes () & [] blocks.
{
  if (nHowMuch < UTL_NML_MIN || nHowMuch > UTL_NML_MAX) return NULL;

  //static LPCTSTR sOkChars = "abcdefghijklmnopqrstuvwxyz1234567890";

  INT nLen = _tcslen(sSource);
  LPTSTR sTmp = new TCHAR[nLen + 1];
  if (!sTmp) return NULL;

  // Use sTarget as a temp in intermediate steps. Init for strcat
  _tcscpy(sTarget, sSource);

//  if (bHeavy) 
  if (nHowMuch == UTL_NML_MAX) 
  {
    // Remove () & [] blocks from sTarget, eg [Disc 1] or (Hot Series)
    LPTSTR spFound;
    LPTSTR spCheck;
    LPTSTR spRemain = sTarget;
    sTmp[0] = _T('\0');

    // First to brackets, copying kept parts from sTarget to sTmp
    while ( (spFound = StrChr(spRemain, _T('['))) )
    {
      *spFound = _T('\0');
      _tcscat(sTmp, spRemain); // Copy as far as the NULL inserted above

      spRemain = ++spFound; // Remainder after the '('
      spFound = StrChr(spRemain, _T(']'));
      if (!spFound) break; // Unmatched. Any remaining '(' removed as non-alphnum

      // If opening parens before next closing, treat as unmatched,
      // but continue so can see if next opening parens is matched.
      // Could result in missed closing parens, they get removed as non-alphnum.
      spCheck = StrChr(spRemain, _T('['));
      if (spCheck && spCheck < spFound) continue;

      // If here, spFound points to matched closing parens. Skip past it.
      spRemain = ++spFound;
    }
    _tcscat(sTmp, spRemain); // Whatever's left; all if didn't enter loop

    // Now do same for parentheses, copying back from sTmp to sTarget
    sTarget[0] = _T('\0');
    spRemain = sTmp;
    while ( (spFound = StrChr(spRemain, _T('('))) )
    {
      *spFound = _T('\0');
      _tcscat(sTarget, spRemain); // Copy as far as the NULL inserted above

      spRemain = ++spFound; // Remainder after the '('
      spFound = StrChr(spRemain, _T(')'));
      if (!spFound) break; // Unmatched. Any remain '(' removed as non-alphnum

      // If opening parens before next closing, treat this one as unmatched,
      // but continue so can see if next opening parens is matched.
      // Could result in missed closing parens, they get removed as non-alphnum.
      spCheck = StrChr(spRemain, _T('('));
      if (spCheck && spCheck < spFound) continue;

      // If here, spFound points to matched closing parens. Skip past it.
      spRemain = ++spFound;
    }
    _tcscat(sTarget, spRemain); // Whatever's left; all if didn't enter loop

    // Removals above could have left leading or trailing space.
    StrTrim(sTarget, _T(" "));
  }

  // Here sTarget is either original string, or if UTL_NML_MAX without [] () blocks
  CharLower(sTarget);

  // Remove leading or trailing articles
  if (nLen > 4 && !_tcsncmp(sTarget, _T("the "), 4))
  {
    _tcscpy(sTmp, &sTarget[4]);
  }
  else if (nLen > 2 && !_tcsncmp(sTarget, _T("a "), 2))
  {
    _tcscpy(sTmp, &sTarget[2]);
  }
  else if (nLen > 5 && !_tcscmp(&sTarget[nLen - 5], _T(", the")))
  {
    _tcsncpy(sTmp, sTarget, nLen - 5);
    sTmp[nLen - 5] = _T('\0');
  }
  else if (nLen > 5 && !_tcscmp(&sTarget[nLen - 3], _T(", a")))
  {
    _tcsncpy(sTmp, sTarget, nLen - 3);
    sTmp[nLen - 3] = _T('\0');
  }
  else _tcscpy(sTmp, sTarget);

  INT nIxTarget = 0;
  BOOL bOkNext0 = FALSE; // Actually 0 always 0K if no digit after it
  TCHAR cPrev = _T('x');

  for (INT i = 0; i < (INT) _tcslen(sTmp); i++)
  {
    // Version 3.0 add change acute, grave, diaeresis to just latin component
    if (nHowMuch == UTL_NML_MAX)
    {
      if (sTmp[i] >= _T('\x00E0') && sTmp[i] <= _T('\x00FF'))
        sTmp[i] = LatinComponentForLower(sTmp[i]);

      // To match conversions like a umlaut to a instead of ae,
      // omit 'e' if it follows commone _e combos
      if (sTmp[i] == _T('e') &&
          (cPrev == _T('a') || cPrev == _T('o') || cPrev == _T('u')) )
        continue; // bOkNext0 is already FALSE
    }

//    if (!IsCharAlphaNumeric(sTmp[i]))  // allows diacritics
    if (! ( (sTmp[i] >= _T('a') && sTmp[i] <= _T('z')) ||
            (sTmp[i] >= _T('0') && sTmp[i] <= _T('9')) ||
            (sTmp[i] == _T(' ') && nHowMuch == UTL_NML_MIN
             && nIxTarget && sTarget[nIxTarget - 1] != ' ') )) // Collapse space
    {
      bOkNext0 = FALSE; // 0 only allowed after digit (except if next not digit)
      continue;
    }

    // Changes 015 to 15, 000 to 0
    if (sTmp[i] == _T('0') && !bOkNext0 && (sTmp[i+1] >= _T('0') && sTmp[i+1] <= _T('9')))
      continue;

    sTarget[nIxTarget++] = sTmp[i];
    bOkNext0 = (sTmp[i] >= _T('0') && sTmp[i] <= _T('9')); // 0 ok only after digit
  } // for (INT i = 0; i < (INT) _tcslen(sTmp); i++)

  sTarget[nIxTarget] = _T('\0'); // Terminate string

  delete sTmp;
  return sTarget;
}

// Simple normalize to alpha-only lowercase. Catches some that above Normalized misses
// where match is in parens, e.g. "Mark Twain (Samuel Clemens)" vs "Samuel Clemens"
LPCTSTR NormalizedAlpha(LPCTSTR sSource, LPTSTR sTarget, BOOL bDecompose)
{
  INT nLen = _tcslen(sSource);
  LPTSTR sTmp = new TCHAR[nLen + 1];
  if (!sTmp) return NULL;

  // Use sTarget as a temp in intermediate steps. Init for strcat
  _tcscpy(sTarget, sSource);

  CharLower(sTarget);

  // Remove leading or trailing articles
  if (nLen > 4 && !_tcsncmp(sTarget, _T("the "), 4))
  {
    _tcscpy(sTmp, &sTarget[4]);
  }
  else if (nLen > 2 && !_tcsncmp(sTarget, _T("a "), 2))
  {
    _tcscpy(sTmp, &sTarget[2]);
  }
  else if (nLen > 5 && !_tcscmp(&sTarget[nLen - 5], _T(", the")))
  {
    _tcsncpy(sTmp, sTarget, nLen - 5);
    sTmp[nLen - 5] = _T('\0');
  }
  else if (nLen > 5 && !_tcscmp(&sTarget[nLen - 3], _T(", a")))
  {
    _tcsncpy(sTmp, sTarget, nLen - 3);
    sTmp[nLen - 3] = _T('\0');
  }
  else _tcscpy(sTmp, sTarget);

  INT nIxTarget = 0;
  TCHAR cPrev = _T('x'); // Init as any char that doesn't cause change
  for (INT i = 0; i < (INT) _tcslen(sTmp); i++)
  {
    if (bDecompose)
    {
      // LatinComponentForLower does same range check
      // Is checking here to usually avoid fn call more efficient?
      if (sTmp[i] >= _T('\x00E0') && sTmp[i] <= _T('\x00FF'))
        sTmp[i] = LatinComponentForLower(sTmp[i]);

      // To match conversions like a umlaut to a instead of ae,
      // omit 'e' if it follows commone _e combos
      if (sTmp[i] == _T('e') &&
          (cPrev == _T('a') || cPrev == _T('o') || cPrev == _T('u')) )
        continue; // with cPrev unchanged, e.g. "aeeeeeb" => "ab"
    }

    if (sTmp[i] < _T('a') || sTmp[i] > _T('z'))
      continue; // Omit this non-alpha char from normalized

    cPrev = sTmp[i];
    sTarget[nIxTarget++] = sTmp[i];
  }

  sTarget[nIxTarget] = _T('\0'); // Terminate string

  delete sTmp;
  return sTarget;
}

BOOL GetStrBetween(LPTSTR sSource, LPCTSTR sBefore, LPCTSTR sAfter,
                   LPTSTR sTarget, BOOL bCaseSensitive)
// Even param strings that don't get changed can't be LPCTSTR
// Found string copied to sTarget if not NULL, else copied to sSource
// Returns FALSE if not found; on error FALSE and also UtilGetError returns TRUE
{
  // Basic reality check
  if (!sSource || !sSource[0] || !sBefore || !sBefore[0]
      || !sAfter || !sAfter[0])
    return UtilSetError(_T("GetStrBetween: ")
                           _T("NULL or empty required string param(s)"));
  else UtilClearError(); // So UtilGetError will return FALSE if caller checks

  LPTSTR spFind;

  if (bCaseSensitive) spFind = StrStr(sSource, sBefore);
  else  spFind = StrStrI(sSource, sBefore);

  if (!spFind) return FALSE;

  LPTSTR sptarget = new TCHAR[_tcslen(sSource) + 1]; // more than needed
  if (!sptarget)
    return UtilSetError(_T("GetStrBetween: Error allocating memory"));

  _tcscpy(sptarget, spFind + _tcslen(sBefore)); // Everything after end of sBefore

  if (bCaseSensitive) spFind = StrStr(sptarget, sAfter);
  else  spFind = StrStrI(sptarget, sAfter);

  if (!spFind)
  {
    delete sptarget;
    return FALSE;
  }

  spFind[0] = _T('\0'); // Truncates sptarget at start of sAfter

  // If NULL sTarget, overwrite sSource with found string
  _tcscpy((sTarget ? sTarget : sSource), sptarget);
  delete sptarget;
  return TRUE;
}

BOOL ReplaceStrBetween(LPTSTR sSource, LPCTSTR sBefore, LPCTSTR sAfter,
                       LPTSTR sInsert, LPTSTR sTarget, BOOL bCaseSensitive)
// N.B.: Assumes sAfter not contained in sInsert
{
  // Basic reality check
  if (!sSource || !sSource[0] || (sBefore && !sBefore[0])
      || (sAfter && !sAfter[0]) || (!sBefore && !sAfter) // Need at least one
      || !sInsert) // OK for sInsert to be empty
    return UtilSetError(_T("ReplaceStrBetween: ")
                           _T("NULL or empty required string param(s)"));
  else UtilClearError(); // So UtilGetError will return FALSE if caller checks

  LPTSTR spFind;
  LPTSTR sptarget = new TCHAR[_tcslen(sSource) + _tcslen(sInsert) + 1];
  if (!sptarget)
    return UtilSetError(_T("ReplaceStrBetween: Error allocating memory"));

  if (sBefore)
  {
    _tcscpy(sptarget, sSource);

    if (bCaseSensitive) spFind = StrStr(sptarget, sBefore);
    else  spFind = StrStrI(sptarget, sBefore);

    if (!spFind)
    {
      delete sptarget;
      return FALSE;
    }

    spFind[_tcslen(sBefore)] = _T('\0'); // Truncates sptarget just after sBefore
  }
  else sptarget[0] = _T('\0'); // Replacing everything before sAfter

  _tcscat(sptarget, sInsert);

  if (sAfter)
  {
    if (bCaseSensitive) spFind = StrStr(sSource, sAfter);
    else  spFind = StrStrI(sSource, sAfter);

    if (!spFind)
    {
      delete sptarget;
      return FALSE;
    }
    _tcscat(sptarget, spFind);
  } // else replacing everything after sBefore

  // If NULL sTarget, overwrite sSource with found string
  _tcscpy((sTarget ? sTarget : sSource), sptarget);
  delete sptarget;
  return TRUE;
} // BOOL ReplaceStrBetween(LPTSTR sSource, LPTSTR sBefore, LPTSTR sAfter,
  //                       LPTSTR sInsert, LPTSTR sTarget, BOOL bCaseSensitive)


BOOL ReplaceSubStr(LPTSTR sSource, LPCTSTR sSubStr, LPCTSTR sReplacement,
                   LPTSTR sTarget, BOOL bReplFirstOnly, BOOL bCaseSensitive)
// N.B.: Assumes sTarget is large enough (or sSource if NULL sTarget)
{
  // Basic reality check
  if (!sSource || !sSource[0] || !sSubStr || !sSubStr[0]
      || !sReplacement) // OK for sReplacement to be empty
    return UtilSetError(_T("ReplaceSubStr: ")
                           _T("NULL or empty required string param(s)"));
  else UtilClearError(); // So UtilGetError will return FALSE if caller checks

  
  // Make copy of sSource to manipulate
  LPTSTR spsource = new TCHAR[_tcslen(sSource) + 1];
  if (!spsource)
    return UtilSetError(_T("ReplaceSubStr: Error allocating memory"));
  spsource[0] = _T('\0'); // For strcat

  _tcscpy(spsource, sSource);
  LPTSTR spStillToGo = spsource; // spStillToGo will move forward

  LPTSTR sptarget;
  if (sTarget) sptarget = sTarget;
  else sptarget = sSource; // Caller wants original string replaced
  sptarget[0] = _T('\0'); // For strcat

  // Search for the first instance of sSubStr
  LPTSTR spFind;
  if (bCaseSensitive) spFind = StrStr(spStillToGo, sSubStr);
  else spFind = StrStrI(spStillToGo, sSubStr);

  BOOL bAtLeastOneFound = (BOOL) spFind;
  INT nLenSubStr = _tcslen(sSubStr);

  while (spFind)
  {
    // Put a terminating null in spStillToGo at start of substring to replace
    spFind[0] = _T('\0');
    _tcscat(sptarget, spStillToGo); // Appends what was just truncated
    _tcscat(sptarget, sReplacement);

    // For next search (or drop through), rest of string after replaced part
    spStillToGo = spFind + nLenSubStr;

    if (bReplFirstOnly) break;

    if (bCaseSensitive) spFind = StrStr(spStillToGo, sSubStr);
    else spFind = StrStrI(spStillToGo, sSubStr);
  }

  // Concatenate whetever is left after last found sSubStr
  // Copies whole string if none were found
  _tcscat(sptarget, spStillToGo);

  delete spsource;
  return bAtLeastOneFound;
} // BOOL ReplaceSubStr(LPTSTR sSource, LPTSTR sSubStr, LPTSTR sReplacement,
  //                  LPTSTR sTarget, BOOL bReplFirstOnly, BOOL bCaseSensitive)

BOOL ReplaceSubStrIfThere(LPTSTR sSource, LPCTSTR sSubStr, LPCTSTR sReplacement,
                       LPTSTR sTarget, BOOL bReplFirstOnly, BOOL bCaseSensitive)
// Returns FALSE only on error, otherwise TRUE even if not found
{
  TCHAR sIgnore[UTIL_MAX_ERROR + 1];
  return (ReplaceSubStr(sSource, sSubStr, sReplacement, sTarget,
                        bReplFirstOnly, bCaseSensitive)
          || !UtilGetError(sIgnore, FALSE)); // Leave sgUtilError as is
}

#endif // _INC_SHLWAPI

