/*****************************************************************************
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.
*****************************************************************************/

// CopyDlg.cpp: implementation of the CCopyDlg class.
//
//////////////////////////////////////////////////////////////////////

#include "Headers.h"
#include "resource.h"
#include "Settings.h"
#include "CopyDlg.h"
#include "MatchDlg.h"
#include "ParentDlg.h"
#include "JpgFolders.h"
#include <math.h>
#include "FileCopyDlg.h"

//CCopyDlg * pCopyDlg;
//
//BOOL CopyDlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
//{
//	return pCopyDlg->OwnProc(hDlg, message, wParam, lParam);
//}

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

CCopyDlg::CCopyDlg(HINSTANCE hInst, CSettings * pSet, LPCTSTR sCapt1, LPCTSTR sCapt2,
								 CParentDlg * pOwner, ZWinAppDataClone * pzwd) // CWnd * pOwner)
//								 LPCTSTR sCapt1, LPCTSTR sCapt2, ZWin * pOwner)
	//: ZDlg(hInst, sCapt1, sCapt2, pOwner)
	: CZDialog(IDD_COPY, sCapt1, NULL, pSet, pzwd)
{
	ini = pSet;
//	pCopyDlg = this;
//	m_pProc = CopyDlgProc;
//	m_uID = IDD_COPY;

  m_bInProgress = FALSE;
  m_hProgress = NULL;
  m_sSource[0] = _T('\0');
  m_sTarget[0] = _T('\0');

 // m_sArrJpgNames = { "Folder.jpg", "AlbumArt.jpg", "Album Art.jpg" };
  m_sArrJpgNames[RB_JPG_F] = _T("Folder.jpg");
  m_sArrJpgNames[RB_JPG_A] = _T("AlbumArt.jpg");
  m_sArrJpgNames[RB_JPG_AA] = _T("Album Art.jpg");

  // Insert for ZDialog
  _tcscpy(m_sCapt, sCapt1);
  m_csCaptMain.Format(_T("%s  -  %s"), sCapt1, sCapt2);
  m_hInst = hInst;
  m_pOwner = pOwner;
}

CCopyDlg::~CCopyDlg()
{

}

/**************************************************************
 ZDialog insert - use message map instead of OwnProc
***************************************************************/
BEGIN_MESSAGE_MAP(CCopyDlg, CDialog)
	ON_BN_CLICKED(IDC_CBN_SRC, OnBnSource)
	ON_BN_CLICKED(IDC_CBN_TGT, OnBnTarget)
	ON_BN_CLICKED(IDC_CBN_FIND, OnBnFind)
	//ON_BN_CLICKED(IDC_CBN_CLOSE, OnBnClose)
	//ON_BN_CLICKED(IDC_CBN_TEST, OnBnTest)
END_MESSAGE_MAP()

/*
BOOL CCopyDlg::OwnProc(HWND hDlg, UINT uMsg, WPARAM wP, LPARAM lP)
{
	switch (uMsg)
	{
		// Insert DTAKEMSG(WM_WHATEVER, OnWhatever); except for WM_COMMAND

// Don't bitch about no cases in WM_COMMAND's switch(wP) (warning 4065)
#pragma warning(push)
#pragma warning(disable : 4065)
		case WM_COMMAND:
			switch(LOWORD(wP))
			{
				// Insert DTAKECMD(IDC_BN_ETC, OnEtc);
        DTAKECMD(IDC_CBN_SRC, OnBnSource);
        DTAKECMD(IDC_CBN_TGT, OnBnTarget);
        DTAKECMD(IDC_CBN_FIND, OnBnFind);
        DTAKECMD(IDC_CBN_CLOSE, OnBnClose);


DTAKECMD(IDC_CBN_TEST, OnBnTest);

				default : break;

			} // END switch(LOWORD(wP))
// Restore settings in effect at warning(push) above
#pragma warning(pop)

		default : break;
	}

	// ZDlg already calls OnInitDialog, OnOK, OnCancel, or returns FALSE
	return ZDlg::OwnProc(hDlg, uMsg, wP, lP);
}
*/

BOOL CCopyDlg::OnInitDialog()
{
  CZDialog::OnInitDialog();
  ::SendMessage(m_hWnd, WM_SETICON, TRUE,
							(LPARAM) LoadIcon(m_hInst, MAKEINTRESOURCE(IDI_MAIN)));


  ini->SetDlg(m_hWnd);
  if (!ini->DlgRBsFromKey(ini->uSrcJpg, IDC_CRB_F_F, IDC_CRB_F_O) ||
      !ini->DlgRBsFromKey(ini->uTgtJpg, IDC_CRB_T_F, IDC_CRB_T_O) ||
      !ini->DlgEditFromKey(ini->sSrcJpgOther, IDC_CED_F_OTHER) ||
      !ini->DlgEditFromKey(ini->sTgtJpgOther, IDC_CED_T_OTHER) )
  {
    ini->ShowError(m_sCapt, _T("Error applying ini file settings to dialog"));
  }

  ::SendDlgItemMessage(m_hWnd, IDC_CED_F_OTHER, EM_LIMITTEXT, 80, 0);
  ::SendDlgItemMessage(m_hWnd, IDC_CED_T_OTHER, EM_LIMITTEXT, 80, 0);

  // Have both registry values available in case user resets pref during session
	LPTSTR sKey =
    _T("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders");
  if (!ReadReg(HKEY_CURRENT_USER, sKey, _T("My Music"), m_sWinLibDir, FALSE))
  {
    m_sWinLibDir[0] = _T('\0');
    fnErr(_T("Error getting location of \"My Music\" folder"), m_sError);
  }

  // This registry value may not exist. If not Media player defaults to My Music.
	sKey = _T("Software\\Microsoft\\MediaPlayer\\Preferences");
  if (!ReadReg(HKEY_CURRENT_USER, sKey, _T("SavedTrackFoldersRipDir"),
               m_sWinRipDir, FALSE))
    _tcscpy(m_sWinRipDir, m_sWinLibDir);

  //if (ini->uArtSrcDir == RB_SRC_OTH) _tcscpy(m_sSource, ini->sArtSrcDirOth);
  //else if (ini->uArtSrcDir == RB_SRC_RIP) _tcscpy(m_sSource, m_sWinRipDir);
  //else _tcscpy(m_sSource, m_sWinLibDir); // RB_SRC_LIB

  //if (m_sSource[0]) // Should be true unless error
  //{
  //  SetDlgItemText(IDC_CTX_DIR_FROM, m_sSource);
  //  ListSubFolders(m_sSource, IDC_CLB_FROM);
  //}
  //// else leave text from dialog resource


  //if (GetDefaultTarget(m_sTarget)) // Sets m_sTarget empty if return FALSE
  //{
  //  ::SetDlgItemText(m_hWnd, IDC_CTX_DIR_TO, m_sTarget);
  //  ListSubFolders(m_sTarget, IDC_CLB_TO);
  //}
  //// else leave text from dialog resource. Player probably not connected yet.

  // Set selected folders based on ini defaults. Us same fn as after update prefs.
  UpdateForPref();

  m_hProgress = GetDlgItem(IDC_CPROGRESS)->m_hWnd;
  m_hTextProg = GetDlgItem(IDC_CTXT_PROGRESS)->m_hWnd;

	//return ZDlg::OnInitDialog(); // always TRUE
  return TRUE;
}

void CCopyDlg::UpdateForPref()
{
/*
Win7/WMP12 setting for Rip Dir has no API access
Change this choice to just Library dir or Last Used
  // If set to default last source, but last source empty, use WMP rip dir
  if (ini->uArtSrcDir == RB_SRC_LAST)
    _tcscpy(m_sSource, ini->sArtLastSrc[0] ? ini->sArtLastSrc : m_sWinRipDir);
  else if (ini->uArtSrcDir == RB_SRC_RIP) _tcscpy(m_sSource, m_sWinRipDir);
  else _tcscpy(m_sSource, m_sWinLibDir); // RB_SRC_LIB
*/

  // If set to default last source, but last source empty, use WMP library dir
  if (ini->uArtSrcDir == RB_SRC_LAST && ini->sArtLastSrc[0])
    _tcscpy(m_sSource, ini->sArtLastSrc);
  else // RB_SRC_LIB or obsolete RB_SRC_RIP; or RB_SRC_LAST but !ini->sArtLastSrc[0]
    _tcscpy(m_sSource, m_sWinLibDir);

  if (m_sSource[0]) // Should be true with valid path, or empty on error
  {
    SetDlgItemText(IDC_CTX_DIR_FROM, m_sSource);
    ini->SetStr(ini->sArtLastSrc, m_sSource); // redundant if RB_SRC_LAST
  }
  else SetDlgItemText(IDC_CTX_DIR_FROM, _T("Click Browse button to select")); 

  ListSubFolders(m_sSource, IDC_CLB_FROM); // Clears listbox if empty m_sSource

  // This may well return false, if player not connected
  if (GetDefaultTarget(m_sTarget)) // Sets m_sTarget empty if return FALSE
    SetDlgItemText(IDC_CTX_DIR_TO, m_sTarget);
  else SetDlgItemText(IDC_CTX_DIR_TO,
          _T("Connect player in UMS (or MSC) mode && click Browse button to select"));

  ListSubFolders(m_sTarget, IDC_CLB_TO); 
}

void CCopyDlg::OnBnSource()
{
  //if (BrowseForFolder(m_hWnd, m_sSource,
  if (SelectFolder(m_hWnd, m_sSource,
                      _T("Select source folder on computer to copy images from"),
                      m_sSource[0] ? m_sSource : NULL,
//                      CSIDL_DRIVES)) // Root for browse is My Computer
                      CSIDL_DESKTOP)) // Root for browse is Desktop
  {
    SetDlgItemText(IDC_CTX_DIR_FROM, m_sSource);
    ListSubFolders(m_sSource, IDC_CLB_FROM);
    ini->SetStr(ini->sArtLastSrc, m_sSource);
  }
  else
  {
    if (UtilGetError(sBS)) fnErr(sBS);
    // else Cancel button, do nothing
  }
}

void CCopyDlg::OnBnTarget()
{
  TCHAR sDefault[MAX_PATH];
  if (m_sTarget[0]) _tcscpy(sDefault, m_sTarget);
  else GetDefaultTarget(sDefault); // sDefault empty if returns FALSE

  if (SelectFolder(m_hWnd, m_sTarget,
                      _T("Select target folder on player to copy images to"),
                      sDefault, // Default is root if sDefault empty or NULL
                      CSIDL_DRIVES)) // Root for browse is My Computer
  {
    SetDlgItemText(IDC_CTX_DIR_TO, m_sTarget);
    ListSubFolders(m_sTarget, IDC_CLB_TO);
  }
  else
  {
    if (UtilGetError(sBS)) fnErr(sBS);
    // else Cancel button, do nothing
  }

}

BOOL CCopyDlg::GetDefaultTarget(LPTSTR sTarget)
{
  sTarget[0] = _T('\0'); // Return false if not filled in at end
  TCHAR sDrives[1024]; // Overkill
  LPTSTR psThisDrive;

  // Gets null separated list of drive strings like "C:\", ending with two nulls
  DWORD dwLen = GetLogicalDriveStrings(1023, sDrives);
  if (!dwLen) return SetError(_T("Error searching for drives"));

  // Check each drive. Looking for something like "E:\Music" on removable drive.
  DWORD dwFileSysFlags;
  WIN32_FIND_DATA found;
  HANDLE hFind;
  TCHAR sMusicPath[32];
  ULARGE_INTEGER uliDriveSize;

  // Directory to search for in removable drive. Empty sArtTgtDirOth is allowed.
  LPTSTR sDirName = ini->uArtTgtDir ? ini->sArtTgtDirOth : _T("MUSIC");
  

  // Don't prompt to insert disk when checking e.g. empty floppy drive
  SetErrorMode(SEM_FAILCRITICALERRORS); // MUST UNDO AFTER LOOP

  for (psThisDrive = sDrives; psThisDrive[0];   // Exit at 2 consecutive \0
        psThisDrive += _tcslen(psThisDrive) + 1) // Move past \0 to next string
  {
    // Reality check, should never happen
    if (_tcslen(psThisDrive) != 3) continue;

    // Ignore hard drives. Connected player is removable drive.
    if (GetDriveType(psThisDrive) != DRIVE_REMOVABLE) continue;

    // Exclude CD drives, read-only (unless writable in drive)
    if (GetVolumeInformation(psThisDrive, NULL, 0, NULL, NULL,
                              &dwFileSysFlags, NULL, 0))
    {
      if (dwFileSysFlags & FILE_READ_ONLY_VOLUME) continue;
    }
    else continue; // Ignore, can happen e.g. when no CD in its drive

    StrCatMult(sBS, _T("\n'"), psThisDrive, _T("'"));

    // If player, should have well over 100 MB. Exclude floppy in drive.
    if (GetDiskFreeSpaceEx(psThisDrive, NULL, &uliDriveSize, NULL))
    {
      if (uliDriveSize.QuadPart < 100000000) continue;
    }
    else continue; // Ignore, can happen e.g. when no CD in its drive

    // Expect drive w/ default Music dir, or user set name which can be empty
    // If not found, default to first drive that passes drive check above
    if (!sTarget[0]) _tcscpy(sTarget, psThisDrive); // else was set previous loop

    // See drive has top level directory with specified name (usually MUSIC)
//    _stprintf(sMusicPath, "%smusic", psThisDrive); // case insensitive search
    _stprintf(sMusicPath, _T("%s%s"), psThisDrive, sDirName); // case insensitive search
    hFind = FindFirstFile(sMusicPath, &found);
    if (hFind != INVALID_HANDLE_VALUE)
    {
      FindClose(hFind);
      if (found.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
      {
        // This is what we've been looking for. Don't search any more.
        _stprintf(sTarget, _T("%s%s"), psThisDrive, found.cFileName);
        break;
      }
    } // if (hFind != INVALID_HANDLE_VALUE)

  } //while (psThisDrive[0])
  SetErrorMode(0); // Restore default notifications

  return (BOOL) sTarget[0]; // return FALSE if no likely target found
}

void CCopyDlg::ListSubFolders(LPCTSTR sParent, INT nListId)
{
  // First delete existing contents if any
  INT nCount = SendDlgItemMessage(nListId, LB_GETCOUNT, 0, 0);
	for (INT i = 0 ; i < nCount ; i++)
	{
    // Delete 1st item each loop
		SendDlgItemMessage(nListId, LB_DELETESTRING, 0, 0);
	}

  if (!sParent || !sParent[0]) return; // Sent as request to clear

  TCHAR sSearch[MAX_PATH];
  _stprintf(sSearch, _T("%s\\*"), sParent);

  WIN32_FIND_DATA found;
  BOOL bNoSubFolders = TRUE;
  HANDLE hFind = FindFirstFile(sSearch, &found);

  if (hFind != INVALID_HANDLE_VALUE)
  {
    BOOL bFound = TRUE;
    while (bFound)
    {
      // Only want folders; omit . && .. folders
      if (!(found.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
              // Faster to eliminate most . .. by first checking only first char?
          || (_tcschr(found.cFileName, _T('.')) == found.cFileName
              // Still could be legit, e.g. ".38 Special"
              && (nBS = _tcslen(found.cFileName)) < 3
              && _tcsspn(found.cFileName, _T(".")) == nBS ) )
      {
        bFound = FindNextFile(hFind, &found);
        continue;
      }

      // Not used by processing, info only, so ignore return
      SendDlgItemMessage(nListId, LB_ADDSTRING, 0, (LPARAM) found.cFileName);
      bNoSubFolders = FALSE; // at least one found

      bFound = FindNextFile(hFind, &found);
    } // while (bFound)

    FindClose(hFind);
  } // if (hFind != INVALID_HANDLE_VALUE)

  if (bNoSubFolders)
  {
    SendDlgItemMessage(nListId, LB_INSERTSTRING, -1,
                (LPARAM) _T("--> No subfolders!"));
    SendDlgItemMessage(nListId, LB_INSERTSTRING, -1,
                (LPARAM) _T("--> Wrong parent folder selected?"));
  }
} // void CCopyDlg::ListSubFolders(LPCTSTR sParent, INT nListId)

//void CCopyDlg::OnBnClose()
//{
////  ((CParentDlg*) m_pOwner)->CloseAll();
////  ((CParentDlg*) m_pParent)->CloseAll(); // ZDialog
//  m_pOwner->CloseAll();
//}

void CCopyDlg::Close()
{
  ControlsToIni(TRUE); // Saves Copy tab settings
	CDialog::OnOK();
}

BOOL CCopyDlg::InProgress()
{
  return m_bInProgress;
}

void CCopyDlg::OnBnFind()
{
  if (m_bInProgress)
  {
    // Button clicked while it had been earlier changed to Stop button during shuffle
    m_bAbort = TRUE;
    return;
    // Next time CompareFolders checks m_bAbort, it will return FALSE to call below
  }
  m_bInProgress = TRUE;

	if (!m_sSource[0] || ! m_sTarget[0])
  {
    fnErr(_T("One or both of Source and Target folders are not specified"),
          _T("Click the Browse button to select"));
    m_bInProgress = FALSE;
    return;
  }

  // Get current info from controls on album art file names
  if (!ControlsToIni(FALSE))
  {
    m_bInProgress = FALSE;
    return; // msg already
  }

  m_spSrcJpg = ini->uSrcJpg == RB_JPG_O ? ini->sSrcJpgOther
                                        : m_sArrJpgNames[ini->uSrcJpg];
  m_spTgtJpg = ini->uTgtJpg == RB_JPG_O ? ini->sTgtJpgOther
                                        : m_sArrJpgNames[ini->uTgtJpg];

  //m_bInProgress = TRUE;
  m_bAbort = FALSE;
  ::SetWindowText(m_hTextProg, _T("Starting search ..."));
  ::SendMessage(m_hProgress, PBM_SETPOS, 0, 0); // in default range 0 to 100
  m_nProgPos = 0;
  m_nProgLastPos = 0;
  EnableControls(FALSE);

  // CJpgFolders for CompareFolders to fill in, then pass to CMatchDlg
  CJpgFolders folders(NULL, m_sTarget, m_sSource);

  m_bLogDetail = ini->bArtLog && ini->bArtLogDetail;
  if (ini->bArtLog) StartLog(_T("album art compare.txt"), _T("Start compare folders"),
                             ini->bArtLogFlush);

  BOOL bRet = CompareFolders(m_sSource, m_sTarget, m_spSrcJpg, m_spTgtJpg, folders);

  if (ini->bArtLog) CloseLog(_T("End compare"));

  if (!bRet)
  {
    EnableControls(TRUE);
    m_bInProgress = FALSE;
    return; // Error message already
  }

  // Call match dialog

	CMatchDlg* pDlg = NULL;
	//pDlg = new CMatchDlg(&folders, m_spSrcJpg, m_spTgtJpg, m_hInst, ini, m_sCapt,
 //                      "Select images to copy to player", this);

  // REPLACE ABOVE FOR ZDialog version with Cimage
//  _stprintf(sBS, "%s  -  Select images to copy to player", m_sCapt);
//	pDlg = new CMatchDlg(&folders, m_spSrcJpg, m_spTgtJpg, m_hInst, ini, sBS);
	pDlg = new CMatchDlg(&folders, m_spSrcJpg, m_spTgtJpg, m_hInst, ini,
                       m_sCapt, _T("Copy checked images to player"), NULL);

	if (!pDlg)
  {
    fnErr(_T("Error creating album art selection dialog"));
    EnableControls(TRUE);
    m_bInProgress = FALSE;
    return;
  }

  INT nCount;
  INT nArrToCopy[MAX_ALBUMS];

  EnableControls(TRUE); // Modal Match dlg will block access

  //pDlg->Run(); // Ignore return, dlg handles details
  INT nRet = pDlg->DoModal(); // ZDialog version

  ini->SetDlg(m_hWnd); // Take back from MatchDlg

  if (nRet == IDOK)  // Files were selected to copy, get info before delete
  {
    nCount = pDlg->m_nCountToCopy;
    for (int i = 0; i < nCount; i++) nArrToCopy[i] = pDlg->m_nArrToCopy[i];
  }

  delete pDlg; // Had no problem while this was forgotten. Just wasted memory?

  if (nRet != IDOK) // either cancelled, or message already if error
  {
    m_bInProgress = FALSE;
    return;
  }

  //if (nRet == IDOK)
  //{

  // Use info from Match dlg to copy files from PC to player
  CFileCopyDlg * pDlgCF = new CFileCopyDlg(&folders, nCount, nArrToCopy,
                                            m_spSrcJpg, m_spTgtJpg,
                                            m_hInst, this, m_sCapt);
  if (!pDlgCF)
  {
    fnErr(_T("Error file copy progress dialog"));
    m_bInProgress = FALSE;
    return;
  }

  nRet = pDlgCF->DoModal();

  INT nCopied = pDlgCF->m_nCopied;
  if (nRet == IDOK)
  {
    // Should be all copied. Reality check
    if (nCopied == nCount)
    {
      //csBS.Format("%d files copied to player.", nCopied);
      //MB(MB_ICONINFORMATION, "Finished.", csBS);
      MB(MB_ICONINFORMATION, _T("Files copy completed."));
    }
    else
    {
      csBS.Format(_T("Error not detected, but count of files copied %d\n")
                  _T("differs from number of files selected to copy %d"),
                  nCopied, nCount);
      fnErr(csBS);
    }
  }
  else // nRet == IDCANCEL
  {
    if (pDlgCF->m_bAbort)
    {
      csBS.Format(_T("%d files were already copied to player."), nCopied);
      MB(MB_ICONINFORMATION, _T("Copying stopped by user Cancel."), csBS);
    }
    // else error, message already
  }

  delete pDlgCF;

//  } // if nRet IDOK return from Match Dialog

  m_bInProgress = FALSE;
} // void CCopyDlg::OnBnFind()

BOOL CCopyDlg::CompareFolders(LPCTSTR sSrc, LPCTSTR sTgt, LPCTSTR sSrcJpg,
                              LPCTSTR sTgtJpg, CJpgFolders & jpf)
{

  DWORD dwStart1 = GetTickCount();


  // For intermediate processing, final to folders param
  CJpgFolders jfSrc(NULL, m_sSource);
  CJpgFolders jfTgt(NULL, m_sTarget);

  // Frontload 3% for long directory read, OK that base for 1st PathToJpgFolders'
  // base is 0. On first nDone, it will still be 3, move after that
  ::SendMessage(m_hProgress, PBM_SETPOS, 3, 0); // in default range 0 to 100
//  ::SetWindowText(m_hTextProg, "Reading subfolders of parent folders ...");
  // Have PathToJpgFolders set text to above after getting first folder info
  ::SetWindowText(m_hTextProg,
    _T("Waiting for directory info from Windows, may take several seconds ..."));

  // Find all source album folders that have album jpeg
  // Initial read of folder paths is slow to start, assign large progress pct
  INT nPct1stRead = 30;
  INT nPct2ndRead = 30;

//  if (!PathToJpgFolders(sSrc, sSrcJpg, jfSrc, TRUE, 0, nPct1stRead))
  // Get all source folders, even if no Jpg, to show matched folders (w grey indicator)
  if (!PathToJpgFolders(sSrc, sSrcJpg, jfSrc, FALSE, 0, nPct1stRead))
    return FALSE; // msg already
  INT nCountSrc = jfSrc.GetCount();
  
  _stprintf(sBS, _T("%d album folders with image"), nCountSrc);
  ToLog(_T("Finished loading source folder info"), sSrc, sBS);
  ::SendMessage(m_hProgress, PBM_SETPOS, nPct1stRead, 0); // in default range 0 to 100
  m_nProgPos = nPct1stRead;
  m_nProgLastPos = nPct1stRead;

  zPeekMessage();
  if (m_bAbort) return FALSE;

  // Find all target album folders, with or without album jpeg
  if (!PathToJpgFolders(sTgt, sTgtJpg, jfTgt, FALSE, nPct1stRead, nPct2ndRead))
    return FALSE; // msg already
  INT nCountTgt = jfTgt.GetCount();
  
  _stprintf(sBS, _T("%d album folders"), nCountTgt);
  ToLog(_T("Finished loading target folder info"), sTgt, sBS);

  ::SetWindowText(m_hTextProg, _T("Comparing subfolders of source and target ..."));
  m_nProgPos = nPct1stRead + nPct2ndRead;
  m_nProgLastPos = m_nProgPos;
  ::SendMessage(m_hProgress, PBM_SETPOS, m_nProgPos, 0); // in default range 0 to 100

  // Variables for progress bar
  INT nBase = m_nProgPos;
  INT nBaseAddPerNml = 2; // Will apply 3 times with different normalize methods
  INT nDirsProg = max(nCountSrc, nCountTgt);
  INT nDirsProgDone = 0;
  DOUBLE fProgPctPerDir = (DOUBLE) (100 - nBase - (3 * nBaseAddPerNml))
                          / nDirsProg;

  LPTSTR sArrMatchLevel[] = { _T("BRIGHT GREEN"), _T("DULL GREEN"), _T("YELLOW"),
                              _T("ORANGE"), _T("RED"), _T("GREY") };
  LPCTSTR sDiv =
    _T("\r\n===============================================================================\r\n");
  LPCTSTR sFmtMatch = _T("Matched \"%s\" and \"%s\"\r\n");
  LPCTSTR sFmtPair = _T("%s\r\n%s\r\n");
  CString csArtMatch;
  CString csAlbMatch;
  CString csPair;

  /***********************************************************************************
   FIND MATCH LEVEL 0, BRIGHT GREEN
  ************************************************************************************/

  INT nStage = 1;
  INT nStageLevel = 0;
  if (m_bLogDetail)
  {
    _stprintf(sBS, _T("%s  Comparison %d, ")
                   _T("exact case insensitive match")
                   _T(" (match quality %s)%s"),
                   sDiv, nStage, sArrMatchLevel[nStageLevel], sDiv);
    LogRaw(sBS);
  }
  
  // First compare, exact case insensitive match in folder names. This test on all
  // before level 0 normalized matches to reduce number of possible false positives.
  INT nMatchThisTest = 0;
  INT nMatchLevel;
  for (INT t = 0; t < nCountTgt; t++)
  {
    for (INT s = 0; s < nCountSrc; s++)
    {
      if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
        return FALSE; // Abort button clicked since last check

      if (!_tcsicmp(jfTgt.GetArt1(t), jfSrc.GetArt1(s))
          && !_tcsicmp(jfTgt.GetAlb2(t), jfSrc.GetAlb2(s)) )
      {
        if (jfSrc.GetJpgBytes(s) == 0) nMatchLevel = MATCH_LEVEL_GRAY_NO_IMG; // 5
        else if (jfTgt.GetJpgBytes(t) == 0) nMatchLevel = 0;
        else if (jfTgt.GetJpgBytes(t) == jfSrc.GetJpgBytes(s)) nMatchLevel = 4;
        else nMatchLevel = 3; // Both have jpegs, but different sizes

        if (m_bLogDetail)
        {
          csPair.Format(sFmtPair, jfSrc.GetPathRel(s), jfTgt.GetPathRel(t));
          LogRaw(csPair);
          // No substrings yet
        }

        jfTgt.SetMatch(t, jfSrc.GetPathRel(s), 0);
        jfSrc.SetMatch(s, jfTgt.GetPathRel(t), 0);
        nMatchThisTest ++;
        nDirsProgDone++;

        if (jpf.AddAlbum(jfTgt.GetArt1(t), jfTgt.GetAlb2(t),
                         jfTgt.GetJpgBytes(t), jfSrc.GetJpgBytes(s),
                         jfSrc.GetPathRel(s), nMatchLevel) == -1)
        {
          return fnErr(_T("Error adding to matched folders array (exact)"),
                       jfTgt.GetPathFull(t), jpf.GetError());
        }

        break;
      } // if (!_tcsicmp(jfTgt.GetArt1(t), jfSrc.GetArt1(s))
        //     && !_tcsicmp(jfTgt.GetAlb2(t), jfSrc.GetAlb2(s)) )

    } // for (INT j = 0; j < nCountSrc; j++)
  } // for (INT i = 0; i < nCountTgt; i++)

  _stprintf(sBS, _T("Finished compare literal paths, %d matches (level 0)"), nMatchThisTest);
  ToLog(sBS);

  if (!jfSrc.SetNormalized(UTL_NML_MIN))
    return fnErr(_T("Error normalize (min) source folder names"));
  if (!jfTgt.SetNormalized(UTL_NML_MIN))
    return fnErr(_T("Error normalize (min) target folder names"));

  nBase += nBaseAddPerNml;

  ToLog(_T("Finished normalize (lite) folder names"));

  // Second compare, lightly normalized

  nStage ++;
  nStageLevel = 0;
  if (m_bLogDetail)
  {
    _stprintf(sBS, _T("%s  Comparison %d, ")
                   //"exact case insensitive match"
                   _T("minimal normalization match")
                   _T(" (match quality %s)%s"),
                   sDiv, nStage, sArrMatchLevel[nStageLevel], sDiv);
    LogRaw(sBS);
  }

  nMatchThisTest = 0;
  for (INT t = 0; t < nCountTgt; t++)
  {
    // Could cycle a while here outside loop below
    if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
      return FALSE;

    if (jfTgt.GetMatchLevel(t) != -1) continue; // Already matched

    for (INT s = 0; s < nCountSrc; s++)
    {
      if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
        return FALSE;

      if (jfSrc.GetMatchLevel(s) != -1) continue;

      if (!_tcsicmp(jfTgt.GetArtNml1(t), jfSrc.GetArtNml1(s))
          && !_tcsicmp(jfTgt.GetAlbNml2(t), jfSrc.GetAlbNml2(s)) )
      {
        if (jfSrc.GetJpgBytes(s) == 0) nMatchLevel = MATCH_LEVEL_GRAY_NO_IMG; // 5
        else if (jfTgt.GetJpgBytes(t) == 0) nMatchLevel = 0;
        else if (jfTgt.GetJpgBytes(t) == jfSrc.GetJpgBytes(s)) nMatchLevel = 4;
        else nMatchLevel = 3; // Both have jpegs, but different sizes

        if (m_bLogDetail)
        {
          csPair.Format(sFmtPair, jfSrc.GetPathRel(s), jfTgt.GetPathRel(t));
          csArtMatch.Format(sFmtMatch, jfTgt.GetArtNml1(t), jfSrc.GetArtNml1(s));
          csAlbMatch.Format(sFmtMatch, jfTgt.GetAlbNml2(t), jfSrc.GetAlbNml2(s));
          LogRaw(csPair + csArtMatch + csAlbMatch);
        }

        jfTgt.SetMatch(t, jfSrc.GetPathRel(s), 0);
        jfSrc.SetMatch(s, jfTgt.GetPathRel(t), 0);
        nMatchThisTest++;
        nDirsProgDone++;

        if (jpf.AddAlbum(jfTgt.GetArt1(t), jfTgt.GetAlb2(t),
                         jfTgt.GetJpgBytes(t), jfSrc.GetJpgBytes(s),
                         jfSrc.GetPathRel(s), nMatchLevel) == -1)
        {
          return fnErr(_T("Error adding to matched folders array (nml min)"),
                       jfTgt.GetPathFull(t), jpf.GetError());
        }

        break;
      } // if (!_tcsicmp(jfTgt.GetArt1(t), jfSrc.GetArt1(s))
        //     && !_tcsicmp(jfTgt.GetAlb2(t), jfSrc.GetAlb2(s)) )

    } // for (INT j = 0; j < nCountSrc; j++)
  } // for (INT i = 0; i < nCountTgt; i++)

  _stprintf(sBS, _T("Finished compare normalized (min) paths, %d matches (level 0)"),
            nMatchThisTest);
  ToLog(sBS);

  /***********************************************************************************
   FIND MATCH LEVEL 1, DULL GREEN-YELLOW
  ************************************************************************************/

  // Third compare, exact case insensitive match in reversed folder names
  nStage ++;
  nStageLevel = 1;
  if (m_bLogDetail)
  {
    _stprintf(sBS, _T("%s  Comparison %d, ")
                   _T("exact case insensitive reverse match")
                   //"minimal normalization match"
                   _T(" (match quality %s)%s"),
                   sDiv, nStage, sArrMatchLevel[nStageLevel], sDiv);
    LogRaw(sBS);
  }

  nMatchThisTest = 0;
  for (INT t = 0; t < nCountTgt; t++)
  {
    if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
      return FALSE;

    if (jfTgt.GetMatchLevel(t) != -1) continue; // Already matched

    for (INT s = 0; s < nCountSrc; s++)
    {
      if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
        return FALSE;

      if (jfSrc.GetMatchLevel(s) != -1) continue;

      if (!_tcsicmp(jfTgt.GetArt1(t), jfSrc.GetAlb2(s))
          && !_tcsicmp(jfTgt.GetAlb2(t), jfSrc.GetArt1(s)) )
      {
        if (jfSrc.GetJpgBytes(s) == 0) nMatchLevel = MATCH_LEVEL_GRAY_NO_IMG; // 5
        else if (jfTgt.GetJpgBytes(t) == 0) nMatchLevel = 1;
        else if (jfTgt.GetJpgBytes(t) == jfSrc.GetJpgBytes(s)) nMatchLevel = 4;
        else nMatchLevel = 3; // Both have jpegs, but different sizes

        if (m_bLogDetail)
        {
          csPair.Format(sFmtPair, jfSrc.GetPathRel(s), jfTgt.GetPathRel(t));
          LogRaw(csPair);
        }

        jfTgt.SetMatch(t, jfSrc.GetPathRel(s), 1);
        jfSrc.SetMatch(s, jfTgt.GetPathRel(t), 1);
        nMatchThisTest++;
        nDirsProgDone++;

        if (jpf.AddAlbum(jfTgt.GetArt1(t), jfTgt.GetAlb2(t),
                         jfTgt.GetJpgBytes(t), jfSrc.GetJpgBytes(s),
                         jfSrc.GetPathRel(s), nMatchLevel) == -1)
        {
          return fnErr(_T("Error adding to matched folders array (reverse)"),
                       jfTgt.GetPathFull(t), jpf.GetError());
        }

        break;
      } // if (!_tcsicmp(jfTgt.GetArt1(t), jfSrc.GetArt1(s))
        //     && !_tcsicmp(jfTgt.GetAlb2(t), jfSrc.GetAlb2(s)) )

    } // for (INT j = 0; j < nCountSrc; j++)
  } // for (INT i = 0; i < nCountTgt; i++)

  _stprintf(sBS, _T("Finished compare literal paths reversed, %d matches (level 1)"),
            nMatchThisTest);
  ToLog(sBS);

  // Fourth compare, lightly normalized match in reversed folder names
  nStage ++;
  nStageLevel = 1;
  if (m_bLogDetail)
  {
    _stprintf(sBS, _T("%s  Comparison %d, ")
                   //"exact case insensitive reverse match"
                   _T("minimal normalization reverse match")
                   _T(" (match quality %s)%s"),
                   sDiv, nStage, sArrMatchLevel[nStageLevel], sDiv);
    LogRaw(sBS);
  }

  nMatchThisTest = 0;
  for (INT t = 0; t < nCountTgt; t++)
  {
    if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
      return FALSE;

    if (jfTgt.GetMatchLevel(t) != -1) continue; // Already matched

    for (INT s = 0; s < nCountSrc; s++)
    {
      if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
        return FALSE;

      if (jfSrc.GetMatchLevel(s) != -1) continue;

      if (!_tcsicmp(jfTgt.GetArtNml1(t), jfSrc.GetAlbNml2(s))
          && !_tcsicmp(jfTgt.GetAlbNml2(t), jfSrc.GetArtNml1(s)) )
      {
        if (jfSrc.GetJpgBytes(s) == 0) nMatchLevel = MATCH_LEVEL_GRAY_NO_IMG; // 5
        else if (jfTgt.GetJpgBytes(t) == 0) nMatchLevel = 1;
        else if (jfTgt.GetJpgBytes(t) == jfSrc.GetJpgBytes(s)) nMatchLevel = 4;
        else nMatchLevel = 3; // Both have jpegs, but different sizes

        if (m_bLogDetail)
        {
          csPair.Format(sFmtPair, jfSrc.GetPathRel(s), jfTgt.GetPathRel(t));
          csArtMatch.Format(sFmtMatch, jfTgt.GetArtNml1(t), jfSrc.GetAlbNml2(s));
          csAlbMatch.Format(sFmtMatch, jfTgt.GetAlbNml2(t), jfSrc.GetArtNml1(s));
          LogRaw(csPair + csArtMatch + csAlbMatch);
        }

        jfTgt.SetMatch(t, jfSrc.GetPathRel(s), 1);
        jfSrc.SetMatch(s, jfTgt.GetPathRel(t), 1);
        nMatchThisTest++;
        nDirsProgDone++;

        if (jpf.AddAlbum(jfTgt.GetArt1(t), jfTgt.GetAlb2(t),
                         jfTgt.GetJpgBytes(t), jfSrc.GetJpgBytes(s),
                         jfSrc.GetPathRel(s), nMatchLevel) == -1)
        {
          return fnErr(_T("Error adding to matched folders array (normalized min reverse)"),
                       jfTgt.GetPathFull(t), jpf.GetError());
        }

        break;
      } // if (!_tcsicmp(jfTgt.GetArt1(t), jfSrc.GetArt1(s))
        //     && !_tcsicmp(jfTgt.GetAlb2(t), jfSrc.GetAlb2(s)) )

    } // for (INT j = 0; j < nCountSrc; j++)
  } // for (INT i = 0; i < nCountTgt; i++)

  _stprintf(sBS, _T("Finished compare normalized (min) paths reversed, %d matches (level 1)"),
            nMatchThisTest);
  ToLog(sBS);

  // Fifth compare, medium normalized

  if (!jfSrc.SetNormalized(UTL_NML_MED))
    return fnErr(_T("Error normalize (med) source folder names"));
  if (!jfTgt.SetNormalized(UTL_NML_MED))
    return fnErr(_T("Error normalize (med) target folder names"));

  nBase += nBaseAddPerNml;

  ToLog(_T("Finished normalize (medium) folder names"));

  nStage ++;
  nStageLevel = 1;
  if (m_bLogDetail)
  {
    _stprintf(sBS, _T("%s  Comparison %d, ")
                   //"exact case insensitive reverse match"
                   _T("medium normalization match")
                   _T(" (match quality %s)%s"),
                   sDiv, nStage, sArrMatchLevel[nStageLevel], sDiv);
    LogRaw(sBS);
  }

  nMatchThisTest = 0;
  for (INT t = 0; t < nCountTgt; t++)
  {
    if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
      return FALSE;

     if (jfTgt.GetMatchLevel(t) != -1) continue; // Already matched

     for (INT s = 0; s < nCountSrc; s++)
    {
      if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
        return FALSE;

      if (jfSrc.GetMatchLevel(s) != -1) continue;

      if (!_tcsicmp(jfTgt.GetArtNml1(t), jfSrc.GetArtNml1(s))
          && !_tcsicmp(jfTgt.GetAlbNml2(t), jfSrc.GetAlbNml2(s)) )
      {
        if (jfSrc.GetJpgBytes(s) == 0) nMatchLevel = MATCH_LEVEL_GRAY_NO_IMG; // 5
        else if (jfTgt.GetJpgBytes(t) == 0) nMatchLevel = 1;
        else if (jfTgt.GetJpgBytes(t) == jfSrc.GetJpgBytes(s)) nMatchLevel = 4;
        else nMatchLevel = 3; // Both have jpegs, but different sizes

        if (m_bLogDetail)
        {
          csPair.Format(sFmtPair, jfSrc.GetPathRel(s), jfTgt.GetPathRel(t));
          csArtMatch.Format(sFmtMatch, jfTgt.GetArtNml1(t), jfSrc.GetArtNml1(s));
          csAlbMatch.Format(sFmtMatch, jfTgt.GetAlbNml2(t), jfSrc.GetAlbNml2(s));
          LogRaw(csPair + csArtMatch + csAlbMatch);
        }

        jfTgt.SetMatch(t, jfSrc.GetPathRel(s), 0);
        jfSrc.SetMatch(s, jfTgt.GetPathRel(t), 0);
        nMatchThisTest++;
        nDirsProgDone++;

        if (jpf.AddAlbum(jfTgt.GetArt1(t), jfTgt.GetAlb2(t),
                         jfTgt.GetJpgBytes(t), jfSrc.GetJpgBytes(s),
                         jfSrc.GetPathRel(s), nMatchLevel) == -1)
        {
          return fnErr(_T("Error adding to matched folders array (nml med)"),
                       jfTgt.GetPathFull(t), jpf.GetError());
        }

        break;
      } // if (!_tcsicmp(jfTgt.GetArt1(t), jfSrc.GetArt1(s))
        //     && !_tcsicmp(jfTgt.GetAlb2(t), jfSrc.GetAlb2(s)) )

    } // for (INT j = 0; j < nCountSrc; j++)
  } // for (INT i = 0; i < nCountTgt; i++)

  _stprintf(sBS, _T("Finished compare normalized (med) paths, %d matches (level 1)"),
            nMatchThisTest);
  ToLog(sBS);

  // Sixth compare, medium normalized match in reversed folder names
  nStage ++;
  nStageLevel = 1;
  if (m_bLogDetail)
  {
    _stprintf(sBS, _T("%s  Comparison %d, ")
                   //"exact case insensitive reverse match"
                   _T("medium normalization reverse match")
                   _T(" (match quality %s)%s"),
                   sDiv, nStage, sArrMatchLevel[nStageLevel], sDiv);
    LogRaw(sBS);
  }

  nMatchThisTest = 0;
  for (INT t = 0; t < nCountTgt; t++)
  {
    if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
      return FALSE;

    if (jfTgt.GetMatchLevel(t) != -1) continue; // Already matched

    for (INT s = 0; s < nCountSrc; s++)
    {
      if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
        return FALSE;

      if (jfSrc.GetMatchLevel(s) != -1) continue;

      if (!_tcsicmp(jfTgt.GetArtNml1(t), jfSrc.GetAlbNml2(s))
          && !_tcsicmp(jfTgt.GetAlbNml2(t), jfSrc.GetArtNml1(s)) )
      {
        if (jfSrc.GetJpgBytes(s) == 0) nMatchLevel = MATCH_LEVEL_GRAY_NO_IMG; // 5
        else if (jfTgt.GetJpgBytes(t) == 0) nMatchLevel = 1;
        else if (jfTgt.GetJpgBytes(t) == jfSrc.GetJpgBytes(s)) nMatchLevel = 4;
        else nMatchLevel = 3; // Both have jpegs, but different sizes

        if (m_bLogDetail)
        {
          csPair.Format(sFmtPair, jfSrc.GetPathRel(s), jfTgt.GetPathRel(t));
          csArtMatch.Format(sFmtMatch, jfTgt.GetArtNml1(t), jfSrc.GetAlbNml2(s));
          csAlbMatch.Format(sFmtMatch, jfTgt.GetAlbNml2(t), jfSrc.GetArtNml1(s));
          LogRaw(csPair + csArtMatch + csAlbMatch);
        }

        jfTgt.SetMatch(t, jfSrc.GetPathRel(s), 1);
        jfSrc.SetMatch(s, jfTgt.GetPathRel(t), 1);
        nMatchThisTest++;
        nDirsProgDone++;

        if (jpf.AddAlbum(jfTgt.GetArt1(t), jfTgt.GetAlb2(t),
                         jfTgt.GetJpgBytes(t), jfSrc.GetJpgBytes(s),
                         jfSrc.GetPathRel(s), nMatchLevel) == -1)
        {
          return fnErr(_T("Error adding to matched folders array (normalized med reverse)"),
                       jfTgt.GetPathFull(t), jpf.GetError());
        }

        break;
      } // if (!_tcsicmp(jfTgt.GetArt1(t), jfSrc.GetArt1(s))
        //     && !_tcsicmp(jfTgt.GetAlb2(t), jfSrc.GetAlb2(s)) )

    } // for (INT j = 0; j < nCountSrc; j++)
  } // for (INT i = 0; i < nCountTgt; i++)

  _stprintf(sBS, _T("Finished compare normalized (med) paths reversed, %d matches (level 1)"),
            nMatchThisTest);
  ToLog(sBS);

  if (!jfSrc.SetNormalized(UTL_NML_MAX))
    return fnErr(_T("Error normalize (max) source folder names"));
  if (!jfTgt.SetNormalized(UTL_NML_MAX))
    return fnErr(_T("Error normalize (max) target folder names"));

  nBase += nBaseAddPerNml;

  ToLog(_T("Finished normalize (max) folder names"));

  // Seventh compare, heavy normalized

  nStage ++;
  nStageLevel = 1;
  if (m_bLogDetail)
  {
    _stprintf(sBS, _T("%s  Comparison %d, ")
                   //"exact case insensitive reverse match"
                   _T("maximum normalization match")
                   _T(" (match quality %s)%s"),
                   sDiv, nStage, sArrMatchLevel[nStageLevel], sDiv);
    LogRaw(sBS);
  }

  nMatchThisTest = 0;
  for (INT t = 0; t < nCountTgt; t++)
  {
    if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
      return FALSE;

    if (jfTgt.GetMatchLevel(t) != -1) continue; // Already matched

    for (INT s = 0; s < nCountSrc; s++)
    {
      if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
      return FALSE;

      // Unlike previous loops, search already matched source, in case some, e.g.,
      // one target song's album might be missing "[Disc 1]" but others already matched

      if (!_tcsicmp(jfTgt.GetArtNml1(t), jfSrc.GetArtNml1(s))
          && !_tcsicmp(jfTgt.GetAlbNml2(t), jfSrc.GetAlbNml2(s)) )
      {
        if (jfSrc.GetJpgBytes(s) == 0) nMatchLevel = MATCH_LEVEL_GRAY_NO_IMG; // 5
        else if (jfTgt.GetJpgBytes(t) == 0) nMatchLevel = 1;
        else if (jfTgt.GetJpgBytes(t) == jfSrc.GetJpgBytes(s)) nMatchLevel = 4;
        else nMatchLevel = 3; // Both have jpegs, but different sizes

        if (m_bLogDetail)
        {
          csPair.Format(sFmtPair, jfSrc.GetPathRel(s), jfTgt.GetPathRel(t));
          csArtMatch.Format(sFmtMatch, jfTgt.GetArtNml1(t), jfSrc.GetArtNml1(s));
          csAlbMatch.Format(sFmtMatch, jfTgt.GetAlbNml2(t), jfSrc.GetAlbNml2(s));
          LogRaw(csPair + csArtMatch + csAlbMatch);
        }

        jfTgt.SetMatch(t, jfSrc.GetPathRel(s), 0);
        jfSrc.SetMatch(s, jfTgt.GetPathRel(t), 0);
        nMatchThisTest++;
        nDirsProgDone++;

        if (jpf.AddAlbum(jfTgt.GetArt1(t), jfTgt.GetAlb2(t),
                         jfTgt.GetJpgBytes(t), jfSrc.GetJpgBytes(s),
                         jfSrc.GetPathRel(s), nMatchLevel) == -1)
        {
          return fnErr(_T("Error adding to matched folders array (nml max)"),
                       jfTgt.GetPathFull(t), jpf.GetError());
        }

        break;
      } // if (!_tcsicmp(jfTgt.GetArt1(t), jfSrc.GetArt1(s))
        //     && !_tcsicmp(jfTgt.GetAlb2(t), jfSrc.GetAlb2(s)) )

    } // for (INT j = 0; j < nCountSrc; j++)
  } // for (INT i = 0; i < nCountTgt; i++)

  _stprintf(sBS, _T("Finished compare normalized (max) paths, %d matches (level 1)"),
            nMatchThisTest);
  ToLog(sBS);

  // Eighth compare, reverse heavy normalized
  nStage ++;
  nStageLevel = 1;
  if (m_bLogDetail)
  {
    _stprintf(sBS, _T("%s  Comparison %d, ")
                   //"exact case insensitive reverse match"
                   _T("maximum normalization reverse match")
                   _T(" (match quality %s)%s"),
                   sDiv, nStage, sArrMatchLevel[nStageLevel], sDiv);
    LogRaw(sBS);
  }

  nMatchThisTest = 0;
  for (INT t = 0; t < nCountTgt; t++)
  {
    if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
      return FALSE;

    if (jfTgt.GetMatchLevel(t) != -1) continue; // Already matched

    for (INT s = 0; s < nCountSrc; s++)
    {
      if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
        return FALSE;

      if (!_tcsicmp(jfTgt.GetArtNml1(t), jfSrc.GetAlbNml2(s))
          && !_tcsicmp(jfTgt.GetAlbNml2(t), jfSrc.GetArtNml1(s)) )
      {
        if (jfSrc.GetJpgBytes(s) == 0) nMatchLevel = MATCH_LEVEL_GRAY_NO_IMG; // 5
        else if (jfTgt.GetJpgBytes(t) == 0) nMatchLevel = 1;
        else if (jfTgt.GetJpgBytes(t) == jfSrc.GetJpgBytes(s)) nMatchLevel = 4;
        else nMatchLevel = 3; // Both have jpegs, but different sizes

        if (m_bLogDetail)
        {
          csPair.Format(sFmtPair, jfSrc.GetPathRel(s), jfTgt.GetPathRel(t));
          csArtMatch.Format(sFmtMatch, jfTgt.GetArtNml1(t), jfSrc.GetAlbNml2(s));
          csAlbMatch.Format(sFmtMatch, jfTgt.GetAlbNml2(t), jfSrc.GetArtNml1(s));
          LogRaw(csPair + csArtMatch + csAlbMatch);
        }

        jfTgt.SetMatch(t, jfSrc.GetPathRel(s), 1);
        jfSrc.SetMatch(s, jfTgt.GetPathRel(t), 1);
        nMatchThisTest++;
        nDirsProgDone++;

        if (jpf.AddAlbum(jfTgt.GetArt1(t), jfTgt.GetAlb2(t),
                         jfTgt.GetJpgBytes(t), jfSrc.GetJpgBytes(s),
                         jfSrc.GetPathRel(s), nMatchLevel) == -1)
        {
          return fnErr(_T("Error adding to matched folders array (normalized max reverse)"),
                       jfTgt.GetPathFull(t), jpf.GetError());
        }

        break;
      } // if (!_tcsicmp(jfTgt.GetArt1(t), jfSrc.GetArt1(s))
        //     && !_tcsicmp(jfTgt.GetAlb2(t), jfSrc.GetAlb2(s)) )

    } // for (INT j = 0; j < nCountSrc; j++)
  } // for (INT i = 0; i < nCountTgt; i++)

  _stprintf(sBS, _T("Finished compare normalized (max) paths reversed, %d matches (level 1)"),
            nMatchThisTest);
  ToLog(sBS);

  /***********************************************************************************
   FIND MATCH LEVEL 2, YELLOW  See if one max normalized is substring of the other
  ************************************************************************************/

  LPCTSTR sArt1Long;
  LPCTSTR sArt1Sub;
  LPCTSTR sAlb2Long;
  LPCTSTR sAlb2Sub;

  // Ninth compare, heavy normalized substring
  nStage ++;
  nStageLevel = 2;
  if (m_bLogDetail)
  {
    _stprintf(sBS, _T("%s  Comparison %d, ")
                   //"exact case insensitive reverse match"
                   _T("maximum normalization substring match")
                   _T(" (match quality %s)%s"),
                   sDiv, nStage, sArrMatchLevel[nStageLevel], sDiv);
    LogRaw(sBS);
  }

  nMatchThisTest = 0;
  for (INT t = 0; t < nCountTgt; t++)
  {
    if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
      return FALSE;

    if (jfTgt.GetMatchLevel(t) != -1) continue; // Already matched

    for (INT s = 0; s < nCountSrc; s++)
    {
      if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
        return FALSE;

      if (_tcslen(jfTgt.GetArtNml1(t)) > _tcslen(jfSrc.GetArtNml1(s)))
      {
        sArt1Long = jfTgt.GetArtNml1(t);
        sArt1Sub = jfSrc.GetArtNml1(s); 
      }
      else
      {
        sArt1Long = jfSrc.GetArtNml1(s);
        sArt1Sub = jfTgt.GetArtNml1(t);
      }

      if (_tcslen(jfTgt.GetAlbNml2(t)) > _tcslen(jfSrc.GetAlbNml2(s)))
      {
        sAlb2Long = jfTgt.GetAlbNml2(t);
        sAlb2Sub = jfSrc.GetAlbNml2(s); 
      }
      else
      {
        sAlb2Long = jfSrc.GetAlbNml2(s);
        sAlb2Sub = jfTgt.GetAlbNml2(t);
      }

      if (_tcsstr(sArt1Long, sArt1Sub) && _tcsstr(sAlb2Long, sAlb2Sub))
      {
        if (jfSrc.GetJpgBytes(s) == 0) nMatchLevel = MATCH_LEVEL_GRAY_NO_IMG; // 5
        else if (jfTgt.GetJpgBytes(t) == 0) nMatchLevel = 2;
        else if (jfTgt.GetJpgBytes(t) == jfSrc.GetJpgBytes(s)) nMatchLevel = 4;
        else nMatchLevel = 3; // Both have jpegs, but different sizes

        if (m_bLogDetail)
        {
          csPair.Format(sFmtPair, jfSrc.GetPathRel(s), jfTgt.GetPathRel(t));
          csArtMatch.Format(sFmtMatch, sArt1Long, sArt1Sub);
          csAlbMatch.Format(sFmtMatch, sAlb2Long, sAlb2Sub);
          LogRaw(csPair + csArtMatch + csAlbMatch);
        }

        jfTgt.SetMatch(t, jfSrc.GetPathRel(s), 0);
        jfSrc.SetMatch(s, jfTgt.GetPathRel(t), 0);
        nMatchThisTest++;
        nDirsProgDone++;

        if (jpf.AddAlbum(jfTgt.GetArt1(t), jfTgt.GetAlb2(t),
                         jfTgt.GetJpgBytes(t), jfSrc.GetJpgBytes(s),
                         jfSrc.GetPathRel(s), nMatchLevel) == -1)
        {
          return fnErr(_T("Error adding to matched folders array (substr nml max)"),
                       jfTgt.GetPathFull(t), jpf.GetError());
        }

        break;
      } // if (!_tcsicmp(jfTgt.GetArt1(t), jfSrc.GetArt1(s))
        //     && !_tcsicmp(jfTgt.GetAlb2(t), jfSrc.GetAlb2(s)) )

    } // for (INT j = 0; j < nCountSrc; j++)
  } // for (INT i = 0; i < nCountTgt; i++)

  _stprintf(sBS, _T("Finished compare normalized (max) substrings, %d matches (level 2)"),
            nMatchThisTest);
  ToLog(sBS);

  // Tenth compare, heavy normalized reverse substring
  nStage ++;
  nStageLevel = 2;
  if (m_bLogDetail)
  {
    _stprintf(sBS, _T("%s  Comparison %d, ")
                   //"exact case insensitive reverse match"
                   _T("maximum normalization reverse substring match")
                   _T(" (match quality %s)%s"),
                   sDiv, nStage, sArrMatchLevel[nStageLevel], sDiv);
    LogRaw(sBS);
  }

  nMatchThisTest = 0;
  for (INT t = 0; t < nCountTgt; t++)
  {
    if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
      return FALSE;

    if (jfTgt.GetMatchLevel(t) != -1) continue; // Already matched

    for (INT s = 0; s < nCountSrc; s++)
    {
      if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
        return FALSE;

      if (_tcslen(jfTgt.GetArtNml1(t)) > _tcslen(jfSrc.GetAlbNml2(s)))
      {
        sArt1Long = jfTgt.GetArtNml1(t);
        sArt1Sub = jfSrc.GetAlbNml2(s); 
      }
      else
      {
//        sArt1Long = jfSrc.GetArtNml1(s);
//        sArt1Sub = jfTgt.GetAlbNml2(t);
        sArt1Long = jfSrc.GetAlbNml2(s);
        sArt1Sub = jfTgt.GetArtNml1(t);
      }

      if (_tcslen(jfTgt.GetAlbNml2(t)) > _tcslen(jfSrc.GetArtNml1(s)))
      {
        sAlb2Long = jfTgt.GetAlbNml2(t);
        sAlb2Sub = jfSrc.GetArtNml1(s); 
      }
      else
      {
        sAlb2Long = jfSrc.GetArtNml1(s);
        sAlb2Sub = jfTgt.GetAlbNml2(t);
      }

      if (_tcsstr(sArt1Long, sArt1Sub) && _tcsstr(sAlb2Long, sAlb2Sub))
      {
        if (jfSrc.GetJpgBytes(s) == 0) nMatchLevel = MATCH_LEVEL_GRAY_NO_IMG; // 5
        else if (jfTgt.GetJpgBytes(t) == 0) nMatchLevel = 2;
        else if (jfTgt.GetJpgBytes(t) == jfSrc.GetJpgBytes(s)) nMatchLevel = 4;
        else nMatchLevel = 3; // Both have jpegs, but different sizes

        if (m_bLogDetail)
        {
          csPair.Format(sFmtPair, jfSrc.GetPathRel(s), jfTgt.GetPathRel(t));
          csArtMatch.Format(sFmtMatch, sArt1Long, sArt1Sub);
          csAlbMatch.Format(sFmtMatch, sAlb2Long, sAlb2Sub);
          LogRaw(csPair + csArtMatch + csAlbMatch);
        }

        jfTgt.SetMatch(t, jfSrc.GetPathRel(s), 0);
        jfSrc.SetMatch(s, jfTgt.GetPathRel(t), 0);
        nMatchThisTest++;
        nDirsProgDone++;

        if (jpf.AddAlbum(jfTgt.GetArt1(t), jfTgt.GetAlb2(t),
                         jfTgt.GetJpgBytes(t), jfSrc.GetJpgBytes(s),
                         jfSrc.GetPathRel(s), nMatchLevel) == -1)
        {
          return fnErr(_T("Error adding to matched folders array (substr nml max)"),
                       jfTgt.GetPathFull(t), jpf.GetError());
        }

        break;
      } // if (!_tcsicmp(jfTgt.GetArt1(t), jfSrc.GetArt1(s))
        //     && !_tcsicmp(jfTgt.GetAlb2(t), jfSrc.GetAlb2(s)) )

    } // for (INT j = 0; j < nCountSrc; j++)
  } // for (INT i = 0; i < nCountTgt; i++)

  _stprintf(sBS, _T("Finished compare reverse normalized (max) substrings, %d matches (level 2)"),
            nMatchThisTest);
  ToLog(sBS);

  // Eleventh, compare - "Various Artists" on target, performer name on source
  // Normalized target album name matches either source album or artist
  nStage ++;
  nStageLevel = 2;
  if (m_bLogDetail)
  {
    _stprintf(sBS, _T("%s  Comparison %d, ")
                   //"exact case insensitive reverse match"
                   _T("Various Artists and maximum normalization match of source album or artist on target album")
                   _T(" (match quality %s)%s"),
                   sDiv, nStage, sArrMatchLevel[nStageLevel], sDiv);
    LogRaw(sBS);
  }

  nMatchThisTest = 0;
  for (INT t = 0; t < nCountTgt; t++)
  {
    if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
      return FALSE;

    if (jfTgt.GetMatchLevel(t) != -1) continue; // Already matched

    for (INT s = 0; s < nCountSrc; s++)
    {
      if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
        return FALSE;

      if (jfSrc.GetMatchLevel(s) != -1) continue; // Already matched

      if ( !_tcsicmp(jfTgt.GetArtNml1(t), _T("variousartists"))
           && (!_tcsicmp(jfTgt.GetAlbNml2(t), jfSrc.GetAlbNml2(s)) ||
               !_tcsicmp(jfTgt.GetAlbNml2(t), jfSrc.GetArtNml1(s)) ) )
      {
        if (jfSrc.GetJpgBytes(s) == 0) nMatchLevel = MATCH_LEVEL_GRAY_NO_IMG; // 5
        else if (jfTgt.GetJpgBytes(t) == 0) nMatchLevel = 2;
        else if (jfTgt.GetJpgBytes(t) == jfSrc.GetJpgBytes(s)) nMatchLevel = 4;
        else nMatchLevel = 3; // Both have jpegs, but different sizes

        if (m_bLogDetail)
        {
          csPair.Format(sFmtPair, jfSrc.GetPathRel(s), jfTgt.GetPathRel(t));
          csArtMatch.Format(sFmtMatch, jfTgt.GetArtNml1(t), _T("variousartists"));
          csAlbMatch.Format(_T("Either matched \"%s\" and \"%s\"\r\n")
                            _T("    or matched \"%s\" and \"%s\"\r\n"),
                            jfTgt.GetAlbNml2(t), jfSrc.GetAlbNml2(s),
                            jfTgt.GetAlbNml2(t), jfSrc.GetArtNml1(s));
          LogRaw(csPair + csArtMatch + csAlbMatch);
        }

        jfTgt.SetMatch(t, jfSrc.GetPathRel(s), 0);
        jfSrc.SetMatch(s, jfTgt.GetPathRel(t), 0);
        nMatchThisTest++;
        nDirsProgDone++;

        if (jpf.AddAlbum(jfTgt.GetArt1(t), jfTgt.GetAlb2(t),
                         jfTgt.GetJpgBytes(t), jfSrc.GetJpgBytes(s),
                         jfSrc.GetPathRel(s), nMatchLevel) == -1)
        {
          return fnErr(_T("Error adding to matched folders array (various artists)"),
                       jfTgt.GetPathFull(t), jpf.GetError());
        }

        break;
      } // if (!_tcsicmp(jfTgt.GetArt1(t), jfSrc.GetArt1(s))
        //     && !_tcsicmp(jfTgt.GetAlb2(t), jfSrc.GetAlb2(s)) )

    } // for (INT j = 0; j < nCountSrc; j++)
  } // for (INT i = 0; i < nCountTgt; i++)

  _stprintf(sBS, _T("Finished compare to Various Artists, %d matches (level 2)"),
            nMatchThisTest);
  ToLog(sBS);

  // Twelfth, 2nd to last chance compare - "Various Artists"
  // Normalized target album name and source album substring match
  if (m_bLogDetail)
  {
    _stprintf(sBS, _T("%s  Comparison %d, ")
                   //"exact case insensitive reverse match"
                   _T("Various Artists and maximum normalization substring match on album")
                   _T(" (match quality %s)%s"),
                   sDiv, nStage, sArrMatchLevel[nStageLevel], sDiv);
    LogRaw(sBS);
  }

  nMatchThisTest = 0;
  for (INT t = 0; t < nCountTgt; t++)
  {
    if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
      return FALSE;

    if (jfTgt.GetMatchLevel(t) != -1) continue; // Already matched

    for (INT s = 0; s < nCountSrc; s++)
    {
      if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
        return FALSE;

      if (jfSrc.GetMatchLevel(s) != -1) continue; // Already matched

      if (_tcslen(jfTgt.GetAlbNml2(t)) > _tcslen(jfSrc.GetAlbNml2(s)))
      {
        sAlb2Long = jfTgt.GetAlbNml2(t);
        sAlb2Sub = jfSrc.GetAlbNml2(s); 
      }
      else
      {
        sAlb2Long = jfSrc.GetAlbNml2(s);
        sAlb2Sub = jfTgt.GetAlbNml2(t);
      }

//if (_tcsstr(jfTgt.GetPathRel(t), _T("Various")))
//{
//_stprintf(sBS, _T("jfSrc.GetPathRel(s) = %s\njfTgt.GetPathRel(t) = %s")
//          _T("sAlb2Long = %s\nsAlb2Sub = %s"),
//          jfSrc.GetPathRel(s), jfTgt.GetPathRel(t), sAlb2Long, sAlb2Sub);
//MB(sBS);
//}

      if ( !_tcsicmp(jfTgt.GetArtNml1(t), _T("variousartists"))
           && _tcsstr(sAlb2Long, sAlb2Sub) )
      {
        if (jfSrc.GetJpgBytes(s) == 0) nMatchLevel = MATCH_LEVEL_GRAY_NO_IMG; // 5
        else if (jfTgt.GetJpgBytes(t) == 0) nMatchLevel = 2;
        else if (jfTgt.GetJpgBytes(t) == jfSrc.GetJpgBytes(s)) nMatchLevel = 4;
        else nMatchLevel = 3; // Both have jpegs, but different sizes

        if (m_bLogDetail)
        {
          csPair.Format(sFmtPair, jfSrc.GetPathRel(s), jfTgt.GetPathRel(t));
          csArtMatch.Format(sFmtMatch, jfTgt.GetArtNml1(t), _T("variousartists"));
          csAlbMatch.Format(sFmtMatch, sAlb2Long, sAlb2Sub);
          LogRaw(csPair + csArtMatch + csAlbMatch);
        }

        jfTgt.SetMatch(t, jfSrc.GetPathRel(s), 0);
        jfSrc.SetMatch(s, jfTgt.GetPathRel(t), 0);
        nMatchThisTest++;
        nDirsProgDone++;

        if (jpf.AddAlbum(jfTgt.GetArt1(t), jfTgt.GetAlb2(t),
                         jfTgt.GetJpgBytes(t), jfSrc.GetJpgBytes(s),
                         jfSrc.GetPathRel(s), nMatchLevel) == -1)
        {
          return fnErr(_T("Error adding to matched folders array (various artists, substr)"),
                       jfTgt.GetPathFull(t), jpf.GetError());
        }

        break;
      } // if (!_tcsicmp(jfTgt.GetArt1(t), jfSrc.GetArt1(s))
        //     && !_tcsicmp(jfTgt.GetAlb2(t), jfSrc.GetAlb2(s)) )

    } // for (INT j = 0; j < nCountSrc; j++)
  } // for (INT i = 0; i < nCountTgt; i++)

  _stprintf(sBS, _T("Finished compare to Various Artists (substring), %d matches (level 2)"),
            nMatchThisTest);
  ToLog(sBS);

  // Thirteenth, last chance compare - "Various Artists"
  // Normalized target album name and source ARTIST reverse substring match
  if (m_bLogDetail)
  {
    _stprintf(sBS, _T("%s  Comparison %d, ")
                   //"exact case insensitive reverse match"
                   _T("Various Artists and maximum normalization reverse substring match on target album")
                   _T(" (match quality %s)%s"),
                   sDiv, nStage, sArrMatchLevel[nStageLevel], sDiv);
    LogRaw(sBS);
  }

  nMatchThisTest = 0;
  for (INT t = 0; t < nCountTgt; t++)
  {
    if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
      return FALSE;

    if (jfTgt.GetMatchLevel(t) != -1) continue; // Already matched

    for (INT s = 0; s < nCountSrc; s++)
    {
      if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
        return FALSE;

      if (jfSrc.GetMatchLevel(s) != -1) continue; // Already matched

      if (_tcslen(jfTgt.GetAlbNml2(t)) > _tcslen(jfSrc.GetArtNml1(s)))
      {
        sAlb2Long = jfTgt.GetAlbNml2(t);
        sAlb2Sub = jfSrc.GetArtNml1(s); // Assign artist name to album var
      }
      else
      {
        sAlb2Long = jfSrc.GetArtNml1(s);
        sAlb2Sub = jfTgt.GetAlbNml2(t);
      }

      if ( !_tcsicmp(jfTgt.GetArtNml1(t), _T("variousartists"))
            && _tcsstr(sAlb2Long, sAlb2Sub) )
      {
        if (jfSrc.GetJpgBytes(s) == 0) nMatchLevel = MATCH_LEVEL_GRAY_NO_IMG; // 5
        else if (jfTgt.GetJpgBytes(t) == 0) nMatchLevel = 2;
        else if (jfTgt.GetJpgBytes(t) == jfSrc.GetJpgBytes(s)) nMatchLevel = 4;
        else nMatchLevel = 3; // Both have jpegs, but different sizes

        if (m_bLogDetail)
        {
          csPair.Format(sFmtPair, jfSrc.GetPathRel(s), jfTgt.GetPathRel(t));
          csArtMatch.Format(sFmtMatch, jfTgt.GetArtNml1(t), _T("variousartists"));
          csAlbMatch.Format(sFmtMatch, sAlb2Long, sAlb2Sub);
          LogRaw(csPair + csArtMatch + csAlbMatch);
        }

        jfTgt.SetMatch(t, jfSrc.GetPathRel(s), 0);
        jfSrc.SetMatch(s, jfTgt.GetPathRel(t), 0);
        nMatchThisTest++;
        nDirsProgDone++;

        if (jpf.AddAlbum(jfTgt.GetArt1(t), jfTgt.GetAlb2(t),
                          jfTgt.GetJpgBytes(t), jfSrc.GetJpgBytes(s),
                          jfSrc.GetPathRel(s), nMatchLevel) == -1)
        {
          return fnErr(_T("Error adding to matched folders array (various artists, rev substr)"),
                        jfTgt.GetPathFull(t), jpf.GetError());
        }

        break;
      } // if (!_tcsicmp(jfTgt.GetArt1(t), jfSrc.GetArt1(s))
        //     && !_tcsicmp(jfTgt.GetAlb2(t), jfSrc.GetAlb2(s)) )

    } // for (INT j = 0; j < nCountSrc; j++)
  } // for (INT i = 0; i < nCountTgt; i++)

  _stprintf(sBS, _T("Finished compare to Various Artists (reverse substring), %d matches (level 2)"),
            nMatchThisTest);
  ToLog(sBS);

  // Version 3 Add 14th test - substring match WITHOUT previous remove parenthesized
  nStage ++;
  nStageLevel = 2;
  if (m_bLogDetail)
  {
    _stprintf(sBS, _T("%s  Comparison %d, ")
                   //"exact case insensitive reverse match"
                   _T("alpha normalization substring match keeping parenthesized blocks")
                   _T(" (match quality %s)%s"),
                   sDiv, nStage, sArrMatchLevel[nStageLevel], sDiv);
    LogRaw(sBS);
  }

  nMatchThisTest = 0;
  for (INT t = 0; t < nCountTgt; t++)
  {
    if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
      return FALSE;

    if (jfTgt.GetMatchLevel(t) != -1) continue; // Already matched

    TCHAR sArtistTgt[MAX_PATH];
    TCHAR sAlbumTgt[MAX_PATH];
    NormalizedAlpha(jfTgt.GetArt1(t), sArtistTgt);
    NormalizedAlpha(jfTgt.GetAlb2(t), sAlbumTgt);

    for (INT s = 0; s < nCountSrc; s++)
    {
      if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
        return FALSE;

      if (jfSrc.GetMatchLevel(s) != -1) continue; // Already matched

      // This is only test that doesn't rely on CJpgFolders' normalization
      TCHAR sArtistSrc[MAX_PATH];
      TCHAR sAlbumSrc[MAX_PATH];
      NormalizedAlpha(jfSrc.GetArt1(s), sArtistSrc);
      NormalizedAlpha(jfSrc.GetAlb2(s), sAlbumSrc);

      if (_tcslen(sArtistTgt) > _tcslen(sArtistSrc))
      {
        sArt1Long = sArtistTgt;
        sArt1Sub = sArtistSrc; 
      }
      else
      {
        sArt1Long = sArtistSrc;
        sArt1Sub = sArtistTgt;
      }

      if (_tcslen(sAlbumTgt) > _tcslen(sAlbumSrc))
      {
        sAlb2Long = sAlbumTgt;
        sAlb2Sub = sAlbumSrc; 
      }
      else
      {
        sAlb2Long = sAlbumSrc;
        sAlb2Sub = sAlbumTgt;
      }

      if (_tcsstr(sArt1Long, sArt1Sub) && _tcsstr(sAlb2Long, sAlb2Sub))
      {
        if (jfSrc.GetJpgBytes(s) == 0) nMatchLevel = MATCH_LEVEL_GRAY_NO_IMG; // 5
        else if (jfTgt.GetJpgBytes(t) == 0) nMatchLevel = 2;
        else if (jfTgt.GetJpgBytes(t) == jfSrc.GetJpgBytes(s)) nMatchLevel = 4;
        else nMatchLevel = 3; // Both have jpegs, but different sizes

        if (m_bLogDetail)
        {
          csPair.Format(sFmtPair, jfSrc.GetPathRel(s), jfTgt.GetPathRel(t));
          csArtMatch.Format(sFmtMatch, sArt1Long, sArt1Sub);
          csAlbMatch.Format(sFmtMatch, sAlb2Long, sAlb2Sub);
          LogRaw(csPair + csArtMatch + csAlbMatch);
        }

        jfTgt.SetMatch(t, jfSrc.GetPathRel(s), 0);
        jfSrc.SetMatch(s, jfTgt.GetPathRel(t), 0);
        nMatchThisTest++;
        nDirsProgDone++;

        if (jpf.AddAlbum(jfTgt.GetArt1(t), jfTgt.GetAlb2(t),
                         jfTgt.GetJpgBytes(t), jfSrc.GetJpgBytes(s),
                         jfSrc.GetPathRel(s), nMatchLevel) == -1)
        {
          return fnErr(_T("Error adding to matched folders array (substr nml max)"),
                       jfTgt.GetPathFull(t), jpf.GetError());
        }

        break;
      } // if (!_tcsicmp(jfTgt.GetArt1(t), jfSrc.GetArt1(s))
        //     && !_tcsicmp(jfTgt.GetAlb2(t), jfSrc.GetAlb2(s)) )

    } // for (INT j = 0; j < nCountSrc; j++)
  } // for (INT i = 0; i < nCountTgt; i++)

  _stprintf(sBS, _T("Finished alpha normalization substring match keeping parenthesized blocks, %d matches (level 2)"),
            nMatchThisTest);
  ToLog(sBS);

  /***********************************************************************************
   GENERATE LISTS OF UNMATCHED FOLDERS
  ************************************************************************************/

  TCHAR sArtistAlpha[MAX_PATH];
  TCHAR sAlbumAlpha[MAX_PATH];

  if (m_bLogDetail)
  {
    _stprintf(sBS, _T("%s  Folders with match not found between source and target%s"),
                   sDiv, sDiv);
    LogRaw(sBS);
  }
  nMatchThisTest = 0;
  for (INT t = 0; t < nCountTgt; t++)
  {
    if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
      return FALSE;

    if (jfTgt.GetMatchLevel(t) != -1) continue;

    NormalizedAlpha(jfTgt.GetArt1(t), sArtistAlpha);
    NormalizedAlpha(jfTgt.GetAlb2(t), sAlbumAlpha);

    _stprintf(sBS, _T("Normalized: \"%s\\%s\""), jfTgt.GetArtNml1(t), jfTgt.GetAlbNml2(t));
    _stprintf(sBS2, _T("\r\nAlpha Normalized: \"%s\\%s\""), sArtistAlpha, sAlbumAlpha);
    _tcscat(sBS, sBS2);
    ToLog(_T("Unmatched target folder"), jfTgt.GetPathRel(t), sBS);

    nMatchThisTest ++;
    // Only count half, to avoid double count with source
    if (nMatchThisTest % 2) nDirsProgDone++;
    if (jpf.AddAlbum(jfTgt.GetArt1(t), jfTgt.GetAlb2(t),
                     jfTgt.GetJpgBytes(t), -1,
                     _T("<No match found in source folders>"),
                     MATCH_LEVEL_GRAY_NO_SRC) == -1)
    {
      return fnErr(_T("Error adding to unmatched folders array (target)"),
                    jfTgt.GetPathFull(t), jpf.GetError());
    }
  } // for (INT i = 0; i < nCountTgt; i++)

  _stprintf(sBS, _T("Finished list unmatched target folders, %d\r\n"), nMatchThisTest);
  ToLog(sBS);

  // Avoid double count on unmatched, only progress if higher count
  INT nNoMatchTarget = nMatchThisTest;

  nMatchThisTest = 0;
  for (INT s = 0; s < nCountSrc; s++)
  {
    if (!CheckProgress(nBase, nDirsProgDone, fProgPctPerDir))
      return FALSE;

    if (jfSrc.GetMatchLevel(s) != -1) continue;

    NormalizedAlpha(jfSrc.GetArt1(s), sArtistAlpha);
    NormalizedAlpha(jfSrc.GetAlb2(s), sAlbumAlpha);

    _stprintf(sBS, _T("Normalized: \"%s\\%s\""), jfSrc.GetArtNml1(s), jfSrc.GetAlbNml2(s));
    _stprintf(sBS2, _T("\r\nAlpha Normalized: \"%s\\%s\""), sArtistAlpha, sAlbumAlpha);
    _tcscat(sBS, sBS2);
    ToLog(_T("Unmatched source folder"), jfSrc.GetPathRel(s), sBS);

    nMatchThisTest ++;
    if (nMatchThisTest % 2) nDirsProgDone++;
    if (jpf.AddAlbum(_T(""), _T("<No match found in target folders>"),
                     -1, jfSrc.GetJpgBytes(s),
                     jfSrc.GetPathRel(s),
                     MATCH_LEVEL_GRAY_NO_TGT) == -1)
    {
      return fnErr(_T("Error adding to unmatched folders array (source)"),
                    jfSrc.GetPathFull(s), jpf.GetError());
    }
  } // for (INT i = 0; i < nCountTgt; i++)

  _stprintf(sBS, _T("Finished list unmatched source folders, %d\r\n"),
                 nMatchThisTest);
  ToLog(sBS);

  ::SetWindowText(m_hTextProg, _T("Folder matching completed"));
  ::SendMessage(m_hProgress, PBM_SETPOS, 100, 0); // in default range 0 to 100

  return TRUE;

} // BOOL CCopyDlg::CompareFolders(LPCTSTR sSrc, LPCTSTR sTgt, LPCTSTR sSrcJpg,
  //                               LPCTSTR sTgtJpg, CJpgFolders & folders)

BOOL CCopyDlg::PathToJpgFolders(LPCTSTR sSearchDir, LPCTSTR sJpgName,
                                CJpgFolders & jpf, BOOL bSkipNoJpg,
                                INT nProgBase, INT nProgMaxAdd)
{
  TCHAR sSearchArt1[MAX_PATH];
  TCHAR sSearchAlb2[MAX_PATH];
  TCHAR sSearchJpg3[MAX_PATH];

  _stprintf(sSearchArt1, _T("%s\\*"), sSearchDir); // The real one
  //_stprintf(sSearchArt1, "%s\\Trancelaciya Euro Edition [Disc 2]", sSearchDir);
  //_stprintf(sSearchArt1, "%s\\KMFDM*", sSearchDir);          //////////////////////////////////////////////

  WIN32_FIND_DATA foundArt1;
  HANDLE hFindArt1;
  WIN32_FIND_DATA foundAlb2;
  HANDLE hFindAlb2;
  WIN32_FIND_DATA foundJpg3;
  HANDLE hFindJpg3;
  BOOL bNoAlbums;
  BOOL bNoJpgs;
  INT nJpgBytes;
 
  hFindArt1 = FindFirstFile(sSearchArt1, &foundArt1);
  if (hFindArt1 == INVALID_HANDLE_VALUE)
    return fnErr(_T("Nothing found for search pattern:"), sSearchArt1);

  BOOL bFoundArt1 = TRUE;
  BOOL bFoundAlb2;

  INT nAllDirsProcessed = 0; // For progress bar, count both artist and album

  while (bFoundArt1) // One Artist folder per loop, contains Album folders
  {
    if (!nAllDirsProcessed) // i.e. on first loop
    {
      // Finally here after maybe a few seconds waiting for FindFirstFile to return
      // Change the "waiting for directory listing text"
      ::SetWindowText(m_hTextProg, _T("Reading subfolders of parent folders ..."));
    }

    nAllDirsProcessed++;
    if (!CheckProgressFrontLoad(nProgBase, nProgMaxAdd, nAllDirsProcessed))
    {
      // Abort button was clicked since last check
      FindClose(hFindArt1);
      return FALSE;
    }

    // Only want folders; omit . && .. folders
    if (!(foundArt1.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
            // Faster to eliminate most . .. by first checking only first char?
        || (_tcschr(foundArt1.cFileName, _T('.')) == foundArt1.cFileName
            // Still could be legit, e.g. ".38 Special"
            && (nBS = _tcslen(foundArt1.cFileName)) < 3
            && _tcsspn(foundArt1.cFileName, _T(".")) == nBS ) )
    {
      bFoundArt1 = FindNextFile(hFindArt1, &foundArt1);
      continue;
    }

    _stprintf(sSearchAlb2, _T("%s\\%s\\*"), sSearchDir, foundArt1.cFileName);
    hFindAlb2 = FindFirstFile(sSearchAlb2, &foundAlb2);

    bFoundAlb2 = (hFindAlb2 != INVALID_HANDLE_VALUE);
    if (!bFoundAlb2)
    {
      ToLog(_T("Nothing found for search pattern:"), sSearchAlb2); // Should be error?
      bFoundArt1 = FindNextFile(hFindArt1, &foundArt1);
      continue;
    }

    bNoAlbums = TRUE; // Unless reset in loop below
    bNoJpgs = TRUE;

    while (bFoundAlb2) // One Album folder per inner loop
    {
      nAllDirsProcessed ++; // For progress bar
      if (!CheckProgressFrontLoad(nProgBase, nProgMaxAdd, nAllDirsProcessed))
      {
        // Abort button was clicked since last check
        FindClose(hFindArt1);
        FindClose(hFindAlb2);
        return FALSE;
      }

      if (!(foundAlb2.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
          || (_tcschr(foundAlb2.cFileName, _T('.')) == foundAlb2.cFileName
              && (nBS = _tcslen(foundAlb2.cFileName)) < 3
              && _tcsspn(foundAlb2.cFileName, _T(".")) == nBS ) )
      {
        bFoundAlb2 = FindNextFile(hFindAlb2, &foundAlb2);
        continue;
      }

      bNoAlbums = FALSE;

      _stprintf(sSearchJpg3, _T("%s\\%s\\%s\\%s"), sSearchDir,
                foundArt1.cFileName, foundAlb2.cFileName, sJpgName);
      hFindJpg3 = FindFirstFile(sSearchJpg3, &foundJpg3);

      if (hFindJpg3 != INVALID_HANDLE_VALUE)
      {
        FindClose(hFindJpg3);
        bNoJpgs = FALSE;
        nJpgBytes = foundJpg3.nFileSizeLow; // Full file size assuming < 4 GB
      }
      else
      {
        if (bSkipNoJpg)
        {
          bFoundAlb2 = FindNextFile(hFindAlb2, &foundAlb2);
          continue;
        }
        nJpgBytes = 0;
      }

      if (jpf.AddAlbum(foundArt1.cFileName,
                       foundAlb2.cFileName, nJpgBytes) == -1)
      {
        FindClose(hFindArt1);
        FindClose(hFindAlb2);
        _stprintf(sBS, _T("%s\\%s"), foundArt1.cFileName, foundAlb2.cFileName);
        return fnErr(_T("Error adding album for:"), sBS, jpf.GetError());
      }

      bFoundAlb2 = FindNextFile(hFindAlb2, &foundAlb2);
    } // while (bFoundAlb2)
    FindClose(hFindAlb2);

    if (bNoAlbums) ToLog(_T("No Album folders in"), sSearchAlb2);
    else if (bNoJpgs && bSkipNoJpg)
      ToLog(_T("No Album folders with jpeg in"), sSearchAlb2);

    bFoundArt1 = FindNextFile(hFindArt1, &foundArt1);
  } // while (bFoundArt1)
   FindClose(hFindArt1);

   if (!jpf.GetCount())
   {
     if (bSkipNoJpg) _stprintf(sBS, _T("No album folders with \"%s\" file found in %s"),
                               sJpgName, sSearchDir);
     else _stprintf(sBS, _T("No album folders found in %s"), sSearchDir);
     return fnErr(sBS);
   }

   return TRUE;
} // BOOL CCopyDlg::PathToJpgFolders(LPCTSTR sSearchDir, LPCTSTR sJpgName,
  //                                 CJpgFolders & jpf, BOOL bSkipNoJpg,
  //                                 INT nProgBase, INT nProgMaxAdd)

BOOL CCopyDlg::CheckProgress(INT nBase, INT nDirsDone, DOUBLE fPctPerDir)
{
  m_nProgPos = nBase + (INT) ((DOUBLE) nDirsDone * fPctPerDir);
  if (m_nProgPos != m_nProgLastPos
      && m_nProgPos <= 100) // Just reality check
  {
    ::SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);
    m_nProgLastPos = m_nProgPos;
  }
  // else change is less than whole pct, minimum progress bar unit

  zPeekMessage(); // Process window messages, including abort button

  return !m_bAbort; // return FALSE if abort button clicked
} // void CCopyDlg::CheckProgress(INT nBase, INT nDirsDone, FLOAT fPctPerDir)

BOOL CCopyDlg::CheckProgressFrontLoad(INT nBase, INT nMaxAddToBase, INT nDone)
{
  // For initial disk read, slow at first, then speeds up with cached
  m_nProgPos = nBase + (nMaxAddToBase * (INT) (1 - (pow((double) 0.9, nDone))));

  if (m_nProgPos != m_nProgPos)
  {
    ::SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);
    m_nProgLastPos = m_nProgPos;
  }
  // else change is less than whole pct, minimum progress bar unit

  zPeekMessage(); // Process window messages, including abort button

  return !m_bAbort; // return FALSE if abort button clicked
} // void CCopyDlg::CheckProgress(INT nBase, INT nDirsDone, FLOAT fPctPerDir)


void CCopyDlg::EnableControls(BOOL bNormalCtls)
{
  ::EnableWindow(GetDlgItem(IDC_CTX_DIR_FROM)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CBN_SRC)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CRB_F_F)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CRB_F_A)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CRB_F_AA)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CRB_F_O)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CED_F_OTHER)->m_hWnd, bNormalCtls);

  ::EnableWindow(GetDlgItem(IDC_CTX_DIR_TO)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CBN_TGT)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CRB_T_F)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CRB_T_A)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CRB_T_AA)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CRB_T_O)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CED_T_OTHER)->m_hWnd, bNormalCtls);

  ::EnableWindow(GetDlgItem(IDC_CLB_FROM)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CLB_TO)->m_hWnd, bNormalCtls);

//  ::EnableWindow(GetDlgItem(IDC_CBN_CLOSE)->m_hWnd, bNormalCtls);
//  ::EnableWindow(GetDlgItem(IDC_CBN_TEST)->m_hWnd, bNormalCtls);   /// REPLACE WITH HELP BUTTON

  // Fixed texts and frames have ID's just for this
  ::EnableWindow(GetDlgItem(IDC_CFR1)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CFR2)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CFR3)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CTX1)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CTX2)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CTX3)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CTX4)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CTX5)->m_hWnd, bNormalCtls);
  ::EnableWindow(GetDlgItem(IDC_CTX6)->m_hWnd, bNormalCtls);

  // Show progress when regular controls disabled
  ::ShowWindow(m_hTextProg, bNormalCtls ? SW_HIDE : SW_SHOW);
  ::ShowWindow(m_hProgress, bNormalCtls ? SW_HIDE : SW_SHOW);

  ::SetWindowText(GetDlgItem(IDC_CBN_FIND)->m_hWnd,
                bNormalCtls ?
                _T("Find matching album art subfolders in source && target")
                : _T("Stop Search"));
} // void CCopyDlg::EnableControls(BOOL bNormalCtls)

BOOL CCopyDlg::ControlsToIni(BOOL bResetBadEdit)
{
  ini->SetDlg(m_hWnd);
  if (!ini->DlgRBsToKey(ini->uSrcJpg, IDC_CRB_F_F, IDC_CRB_F_O) ||
      !ini->DlgRBsToKey(ini->uTgtJpg, IDC_CRB_T_F, IDC_CRB_T_O) )
  {
    return ini->ShowError(m_sCapt, _T("Error reading radio buttons"));
  }

  // Ignore jpg name edit fields unless corresponding radio button selected
  if (ini->uSrcJpg != RB_JPG_O) ini->SetStr(ini->sSrcJpgOther, _T(""));
  else // Radio button says get name from edit box
  {
    GetDlgItemText(IDC_CED_F_OTHER, sBS, 99);
    if (!_tcslen(sBS))
    {
      if (bResetBadEdit) // Closing window, just restore default
      {
        ini->uSrcJpg = RB_JPG_F;
        ini->SetStr(ini->sSrcJpgOther, _T(""));

      }
      else // Starting search, notify user
        return fnErr(_T("\"Other\" filename selected for source album art filename"),
                     _T("but no filename entered in its input field."));
    }
    else 
    {
      if (!ini->SetStr(ini->sSrcJpgOther, sBS))
        return ini->ShowError(m_sCapt,
               _T("Error setting user specified source album art filename"));
    }
  }

  if (ini->uTgtJpg != RB_JPG_O) ini->SetStr(ini->sTgtJpgOther, _T(""));
  else
  {
    GetDlgItemText(IDC_CED_T_OTHER, sBS, 99);
    if (!_tcslen(sBS))
    {
      if (bResetBadEdit) // Closing window, just restore default
        ini->uTgtJpg = RB_JPG_F;
      else // Starting search, notify user
        return fnErr(_T("\"Other\" filename selected for album art target filename"),
                     _T("but no filename entered in its input field."));
    }
    else 
    {
      if (!ini->SetStr(ini->sTgtJpgOther, sBS))
        return ini->ShowError(m_sCapt,
               _T("Error setting user specified album art target filename"));
    }
  }

  // ini->sArtLastSrc already set (but not saved to file) when/if folder was selected

  ini->WriteIniSection(ini->m_sectCopy);
  return TRUE;
} // BOOL CCopyDlg::ControlsToIni()

//void CCopyDlg::OnBnTest()
//{
//	//CMatchDlg* pDlg = NULL;
//	//pDlg = new CMatchDlg(m_hInst, ini, m_sCapt,
// //                      "Select images to copy to player", this);
//	//if (!pDlg) RET_V("Error creating Image Selection dialog")
//
// // pDlg->Run(); // Ignore return, dlg's ok/cancel decides whether ini updated
//
// // delete pDlg; // Had no problem while this was forgotten. Just wasted memory?
//}
