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

// MinDlg.cpp: implementation of the CMinDlg class.
//
//////////////////////////////////////////////////////////////////////


/*
 For add GENRE
 Example code from C:\WMSDK\WMPSDK10\samples\cpp\WMPML\MainDlg.cpp
 Probably also need macros for OLE2T etc

// This function use getByAttribute() to retrieve the playlist object 
// that fullfil the conditions. It then show the playlist in the media list
HRESULT CMainDlg::ShowGetByAttr(TCHAR *szAttrName, TCHAR *szAttrVal, TCHAR *szMediaType)
{
    HRESULT                     hr;
    CComPtr<IWMPPlaylist>       spPlaylist;

    hr = m_spMC->getByAttribute(CComBSTR(szAttrName), CComBSTR(szAttrVal), &spPlaylist);
    if(SUCCEEDED(hr) && (spPlaylist.p != NULL))
    {
        hr = ShowPlaylist(spPlaylist, szMediaType); // MFA: How to remove non-Audio from above
    }

    return hr;
}

* ShowPlaylist
* pPlaylist: playlist object shown in the media list
* szMediaType: when it's not NULL, only media with this mediaType can be listed
* 
* ListBox has only about 64K memory to save data for strings, so this is
* not a good way to show large media library. So deal with large media
* library, use custom-drawn ListBox

// MFA: Ignore most of this, only interested in limiting existing playlist object to Audio
HRESULT CMainDlg::ShowPlaylist(IWMPPlaylist* pPlaylist, TCHAR *szMediaType)
{
    if(pPlaylist == NULL)
    {
        return E_POINTER;
    }

    // Reset metadata list
    ::SendMessage(m_hMetadata, LB_RESETCONTENT, 0, 0);
    // Reset media list
    ::SendMessage(m_hList, LB_RESETCONTENT, 0, 0);

    // We always save the shown playlist for higher performance
    m_spSavedPlaylist = NULL;
    m_spSavedPlaylist = pPlaylist;

    HRESULT                 hr = E_POINTER;
    CComPtr<IWMPMedia>      spMedia;
    CComBSTR                bstrName, bstrType;
    long                    lCount, i, iListed = 0;

    USES_CONVERSION;

    // Get count
    if(pPlaylist != NULL)
    {
        hr = pPlaylist->get_count(&lCount);
    }

    // Enumerate the items
    if(SUCCEEDED(hr))
    {
        for(i = 0; i < lCount; i++)
        {
            hr = pPlaylist->get_item(i, &spMedia);

            // If the media type is restricted, then we only show those items
            // with given type
            if(szMediaType != NULL)
            {
                if(SUCCEEDED(hr) && (spMedia.p != NULL))
                {
                    hr = spMedia->getItemInfo(CComBSTR(_T("MediaType")), &bstrType);
                }

                if(SUCCEEDED(hr) && (bstrType.m_str != NULL) && (!_tcsicmp(OLE2T(bstrType), szMediaType)))
                {
                    hr = spMedia->get_name(&bstrName);
                }
            }
            else
            {
                if(SUCCEEDED(hr) && (spMedia.p != NULL))
                {
                    hr = spMedia->get_name(&bstrName);
                }
            }

            // When bstrName is not NULL, we add the name to the media list
            if(SUCCEEDED(hr) && bstrName.m_str != NULL)
            {
                iListed++;
                ::SendMessage(m_hList, LB_ADDSTRING, 0, (LPARAM)OLE2T(bstrName));
                bstrName.Empty();
            }

            // Now we can release media object
            spMedia = NULL;
        }
    }

    // Show how many items in the media list
    TCHAR szLine[20];
    _stprintf(szLine, _T("%d items"), iListed);
    SetDlgItemText(IDC_STATUS, szLine);

    m_bIsShowingMedia = TRUE;
    return hr;
}


*/

#include "Headers.h"

//// Using stc32 in OpenFileName template warns of redefinition
//#pragma warning(push)
//#pragma warning(disable : 4005)

#include "resource.h"

//// Restore settings in effect at warning(push) above
//#pragma warning(pop)


#include "Settings.h"
#include "MinDlg.h"
#include "SongArray.h"
#include "PrefDlg.h"
#include "ParentDlg.h"
#include "SelectDlg.h"
#include "SongsSelection.h"
#include <VersionHelpers.h>


// #include <time.h> // for alternate srand seed <== MOVED TO Headers.h
#include <hash_map>

#include <Cderr.h>   // Common dialog error codes for GetSaveFileName
//#include <process.h> // _beginthread, _endthread 

// LOCAL DEFINITIONS FOR USE WITH AddToSongArray
#define SONGARR_ADD_SEP    1
#define SONGARR_ADD_MULT   2
#define SONGARR_INCREMENT  3

// Global visible to both OfHookProc & CMinDlg. 
static INT gnLastIndex = -1; // Init before every Save As to ensure != nFilterIndex

// Callback for OpenFile Save As dialog
UINT_PTR CALLBACK OfHookProc(HWND hDlg, UINT uMsg, WPARAM wP, LPARAM lP)
{
  // Only care about change in output file type dropdown list
  LPOFNOTIFY pNotify;
//  TCHAR sFileName[_MAX_FNAME + _MAX_EXT];
  static LPCTSTR sWpl = _T(".wpl");
  static LPCTSTR sM3u = _T(".m3u");
//  static LPCTSTR sM3u8 = _T(".m3u8"); // Added version 3.0
//  LPTSTR spFind;
  LPCTSTR spCorrectDotExt = NULL; // Version 3.0, for default and fixing entered filename

  if (uMsg == WM_NOTIFY)   // else do nothing -- or InitDialog
  {
    pNotify = (LPOFNOTIFY) lP;
    if (pNotify && pNotify->hdr.code == CDN_TYPECHANGE
        && pNotify->lpOFN->nFilterIndex != gnLastIndex)   // else do nothing
    {
      gnLastIndex = pNotify->lpOFN->nFilterIndex; // 2 msgs on each change, ignore 2nd

      // Set default extension to match newly selected type (without '.')
      // Commented v3.0 for added .m3u8
      //if (pNotify->lpOFN->nFilterIndex == 1) pNotify->lpOFN->lpstrDefExt = &sWpl[1];
      //else pNotify->lpOFN->lpstrDefExt = &sM3u[1];

      //// Version 3.0 replace above with handling for 3 possible extensions
      //// Use spCorrectDotExt here and also for fixing full filename further down
      //if (pNotify->lpOFN->nFilterIndex == RB_FMT_WPL + 1) spCorrectDotExt = sWpl;
      //else if (pNotify->lpOFN->nFilterIndex == RB_FMT_M3U + 1) spCorrectDotExt = sM3u;
      if (pNotify->lpOFN->nFilterIndex == RB_FMT_WPL) spCorrectDotExt = sWpl; // v3.22 values shifted
//      else if (pNotify->lpOFN->nFilterIndex == RB_FMT_M3U) spCorrectDotExt = sM3u;
//      else spCorrectDotExt = sM3u8; // RB_FMT_M3U8
      else spCorrectDotExt = sM3u; // v3.3 default for either ANSI or UTF-8 M3U

      pNotify->lpOFN->lpstrDefExt = &spCorrectDotExt[1];

/* v3.22 just eliminate this; auto handling made it redundant all along?
      // Change extension in file name edit field
      HWND hDlgOfn = GetParent(hDlg);
      if (!GetDlgItemText(hDlgOfn, edt1, sFileName, _MAX_FNAME + _MAX_EXT))
      {
        // 0 chars copied. Could be error, but more likely edit field empty
        // Reset sFileName & edit field to default, then drop through to 
        // handle as if it was that way in the first place
        spFind = pNotify->lpOFN->lpstrFile; // Full path
        PathStripPath(spFind); // Shift spFind to point at just the file name
        _tcscpy(sFileName, spFind);
        SetDlgItemText(hDlgOfn, edt1, pNotify->lpOFN->lpstrFile);
      }

      // If file name ends with wrong extension (.wpl vs. .m3u), replace it
      spFind = StrRChrI(sFileName, NULL, _T('.'));
      // If found, compare what comes after the dot to the wrong extension
      if (spFind
//          && !_tcsicmp(spFind, (pNotify->lpOFN->nFilterIndex == 1 ? sM3u : sWpl)))
//          && _tcsicmp(spFind, spCorrectDotExt)) // Version 3.0
          && !_tcsicmp(spFind, (pNotify->lpOFN->nFilterIndex == 0 ? sWpl : sM3u))) // v3.22
      {
        // Last 4 chars match the old dot-extenstion.
        // Update sFileName by overwriting at start of old extension with new
//        _tcscpy(spFind, (pNotify->lpOFN->nFilterIndex == 1 ? sWpl : sM3u));
//        _tcscpy(spFind, spCorrectDotExt); // Version 3.0
        _tcscpy(spFind, _T(".")); // Version 3.0
        _tcscat(spFind, pNotify->lpOFN->lpstrDefExt); // Version 3.0
        SetDlgItemText(hDlgOfn, edt1, sFileName);
      }
*/

    } // if (pNotify && pNotify->hdr.code == CDN_TYPECHANGE &&
      //     pNotify->lpOFN->nFilterIndex != gnLastIndex)

  } // if (uMsg == WM_NOTIFY)
  //else if (uMsg == WM_INITDIALOG)
  //{
  //  MessageBox(NULL, _T("In Hook WM_INITDIALOG"), _T("INITDIALOG"), MB_OK);

  //}

  return 0; // Always let default processing proceed
}

CMinDlg * pMinDlg;

BOOL MinDlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
	return pMinDlg->OwnProc(hDlg, message, wParam, lParam);
}

CMinDlg::CMinDlg(HINSTANCE hInst, CSettings * pSet,
								 LPCTSTR sCapt1, LPCTSTR sCapt2,
                 ZWin * pOwner, ZWinAppData * pzwd)
	: ZDlg(hInst, sCapt1, sCapt2, pOwner, pzwd)
{
	ini = pSet;
	pMinDlg = this;
	m_pProc = MinDlgProc;
	m_uID = IDD_MINDLG;

  m_pParent = (CParentDlg*) pOwner;

  m_bInProgress = FALSE;

  // _stprintf format string for radio button text
  m_sFmtOneThirdAvgSep = _T("if song repeats more than 1 of every %d songs,")
                           _T( " minimum separation is 1/3 its average repeat rate.");
  m_sFmtSepOnly = _T("(Songs repeated more than once out of every %d")
                     _T(" songs may not be completely separated)");
  m_sSepNowPureRandom = _T("This shuffle is now set purely random")
                           _T(" (No minimum separation of repeats)");

//  m_sSourcePath[0] = '\0';
  m_bEdChangeBySpinner = FALSE;

  // WMP pointers
  m_cpPlayer.p = NULL;  // Smart pointer to IWMPPlayer interface.
  m_cpWmpPlaylistColl.p = NULL;
  m_cpWmpPlaylistArr.p = NULL;
  m_cpWmpPlaylistAll = NULL;

  scNameAllSongs = _T("ALL SONGS");
  scPrefixNewSongs = _T("New Songs: added in last "); // Remove in 3.0?

  bDidFirstShow = FALSE;
  m_bCacheFinished = FALSE;
  m_bStopCaching = FALSE;
  m_b1stSelectStarted = FALSE;
  m_b1stSelectFinished = FALSE;
  m_sStartLogMsg[0] = cNull;
  m_sStartLogSelectMsg[0] = cNull;
  m_bVistaOrLater = FALSE; // unless reset in OnInitDialog

  // New in 3.0
  for (int i = 0; i < MAX_SONG_SELECTIONS; i++) mpArr_SongsSelections[i] = NULL;
  scPrefixGenre = _T("Genre: ");
  scPrefixPlist = _T("Playlist: ");

  m_hTipLv = NULL;
  m_hTipOrig = NULL;

  m_nTest = 0;
}

CMinDlg::~CMinDlg()
{
  ReleaseWmpPointers();
  // New in 3.0
  for (int i = 0; i < MAX_SONG_SELECTIONS; i++)
  {
    if (mpArr_SongsSelections[i] != NULL) delete mpArr_SongsSelections[i];
  }
}

BOOL CMinDlg::OwnProc(HWND hDlg, UINT uMsg, WPARAM wP, LPARAM lP)
{
  NMUPDOWN* pud = NULL;
  NMLVGETINFOTIP * plv = NULL;
  //NMTTDISPINFO * pdi = NULL; // Only for custom tooltip

	switch (uMsg)
	{
    case WM_NOTIFY:

      if (wP == IDC_LV_SRC)
      {
        plv = (NMLVGETINFOTIP*) lP;
        if (plv->hdr.code == LVN_GETINFOTIP) OnNotifyGetInfoTip(plv);
        
        break; // to default handling, LVN_GETINFOTIP or not
      }

      pud = (NMUPDOWN*) lP;
      if (pud->hdr.code == UDN_DELTAPOS
          && (pud->hdr.hwndFrom == m_hSpinDist || pud->hdr.hwndFrom == m_hSpinSep
            || pud->hdr.hwndFrom == m_hSpinTrunc || pud->hdr.hwndFrom == m_hSpinMaxReps) )
      {
        OnNotifySpin(pud); // Same handler for all, checks which control
        // Then drop through to return 0;
      }
      break;

    // Doesn't occur on startup. 1st is on switch to other tag. (Because ParentDlg?)
    //case WM_SHOWWINDOW: 
    //  if (!bDidFirstShow)
    //  {
    //    MB(_T("On first show"));
    //    bDidFirstShow = TRUE;
    //    // Don't hold up window appearing while loading WMP library
    //    PostMessage(m_hWnd, WMU_AFTERSHOW, 0, 0);
    //  }
    //  break;

    //DSEEMSG(WMU_AFTERSHOW, LoadWmpLibrary);
    DSEEMSG(WMU_AFTERSHOW, OnFirstShow); // Includes LoadWmpLibrary

    DSEEMSG(WM_PAINT, OnWmPaint);

    case WM_TIMER:
      if (wP == U_TIMER_PAINT)
      {
        InvalidateRect(m_hWnd, &m_rcImgStars, FALSE);
        //UpdateWindow(m_hWnd);
      }
      break;

		case WM_COMMAND:
			switch(LOWORD(wP))
			{
				// Insert DTAKECMD(IDC_BN_ETC, OnEtc);
        DTAKECMD(IDC_BN_SOURCE, OnBnSource);
        DTAKECMD(IDC_BN_SHUFFLE, OnBnShuffle);
        DTAKECMD(IDC_BN_LV_REMOVE, OnBnLvRemove);
        DTAKECMD(IDC_BN_LV_EDIT, OnBnLvEdit);
        //DTAKECMD(IDC_BN_SRC_WMP, OnBnTestSrcWmp);
        
        // Same handler for all edit controls associated w/ spinner, checks which
        DSEECMDPARMS(IDC_ED_REPEAT_DIST, OnEdWithSpinner);
        DSEECMDPARMS(IDC_ED_REPEAT_SEP, OnEdWithSpinner);
        DSEECMDPARMS(IDC_ED_TRUNC, OnEdWithSpinner);
        DSEECMDPARMS(IDC_ED_MAX_REP, OnEdWithSpinner);

        // Whenever separation option changed, enable/disable option
        // to limit output playlist to no repetitions.
        DSEECMD(IDC_RB_REPEAT_DIST, EnableLimitNoReps);
        DSEECMD(IDC_RB_REPEAT_SEP, EnableLimitNoReps);

				default : break;

			} // END switch (wParam)
		  default : break;
	}

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

BOOL CMinDlg::OnInitDialog()
{
//	SendMessage(m_hWnd, WM_SETICON, TRUE,
//							(LPARAM) LoadIcon(m_hInst, MAKEINTRESOURCE(IDI_MAIN)));
	SendMessage(m_hWnd, WM_SETICON, ICON_SMALL,
							(LPARAM) LoadIcon(m_hInst, MAKEINTRESOURCE(IDI_MAIN)));
	SendMessage(m_hWnd, WM_SETICON, ICON_BIG,
							(LPARAM) LoadIcon(m_hInst, MAKEINTRESOURCE(IDI_MAIN)));

  // Store client coordinates for painting ListView stars image later
  GetControlRect(GetDlgItem(m_hWnd, IDC_RECT_STARS), &m_rcImgStars);
  // OnWmPaint ensures img top-left will match, but make sure size does too.
  m_rcImgStars.right = m_rcImgStars.left + 178; //162;
  m_rcImgStars.bottom = m_rcImgStars.top + 18;

  m_hnImgStars = LoadImage(m_hInst, MAKEINTRESOURCE(IDB_STARS_LVIEW), IMAGE_BITMAP,
                           0, 0, LR_DEFAULTCOLOR); // LR_LOADTRANSPARENT);
  // Don't include LR_LOADTRANSPARENT if painting with TransparentBlt


  // Playlist folder location must be partly inferred by Windows version
  // Folder name is "Playlists" in Vista & 7, "My Playlists" in XP
  //OSVERSIONINFO osver;
  //m_sAppendPlaylists = NULL;
  //if (GetVersionEx(&osver))
  //{
  //  // Vista is 6.0, Win 7 is 6.1
  //  // No reg entry for WMP 12 (Win 7) default playlist folder; observed subdir of Music:
  //  // Actually subdir of WMP's rip dir (default Music), can't find where setting recorded
  //  if(osver.dwMajorVersion >= 6)
  //  {
  //    m_sAppendPlaylists = _T("\\Playlists");
  //    m_bVistaOrLater = TRUE; // Is FALSE unless reset here (or in odd contingency below)
  //  }
  //  // No registry entry for WMP 10/11 (XP) either; observed subdir:
  //  else m_sAppendPlaylists = _T("\\My Playlists");
  //}
  //// else leave NULL, indicates infer version based on existence of registry values below

  if (IsWindowsVistaOrGreater()) // Replace deprecated GetVersionEx above
  {
	  m_sAppendPlaylists = _T("\\Playlists"); // Vista or later
	  m_bVistaOrLater = TRUE; // Is FALSE unless reset here (or in odd contingency below) <-- Change this
  }
  else
  {
    m_sAppendPlaylists = _T("\\My Playlists"); // Win XP
    m_bVistaOrLater = FALSE; // No longer assume set FALSE by default
  }


  LPTSTR sKey = _T("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders");
  if (ReadReg(HKEY_CURRENT_USER, sKey, _T("My Music"), sBS, FALSE))
  {
    // Typically is %USERPROFILE%\Music, convert to typically C:\Users\username\Music
    if (ExpandEnvironmentStrings(sBS, m_sWmpPlaylistDir, 260))
    {
      _tcscat(m_sWmpPlaylistDir, m_sAppendPlaylists); // Now m_sAppendPlaylists sure to be set
    }
    else m_sWmpPlaylistDir[0] = _T('\0'); // Mark as unusable
  }
  else 
  {
    sKey = _T("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders");
    if (ReadReg(HKEY_CURRENT_USER, sKey, _T("My Music"), sBS, FALSE)) // 3.2
    {
      //// Literal string, in XP like C:\Documents and Settings\username\My Documents\My Music
      //if (sAppendPlaylists) _tcscat(m_sWmpPlaylistDir, sAppendPlaylists);
      //// else assume this is XP because the first registry entry above didn't exist
      //else _tcscat(m_sWmpPlaylistDir, _T("\\My Playlists"));

      // if version still unknown, assume this is XP because My Music not in User Shell Folders above
      // 3.2: Now expand just in case isn't always literal in XP (success w/ no change if literal)

      if (ExpandEnvironmentStrings(sBS, m_sWmpPlaylistDir, 260))
      {
        _tcscat(m_sWmpPlaylistDir, m_sAppendPlaylists);
      }
      else m_sWmpPlaylistDir[0] = _T('\0');
    }
    else m_sWmpPlaylistDir[0] = _T('\0');
  }

  m_hProgress = GetItem(IDC_PROGRESS);
  m_hProgText = GetItem(IDC_TXT_PROGRESS);

  m_hSpinDist = GetItem(IDC_SPIN_REPEAT_DIST);
  m_hSpinSep = GetItem(IDC_SPIN_REPEAT_SEP);
  m_hSpinMaxReps = GetItem(IDC_SPIN_MAX_REP);
  m_hEdDist = GetItem(IDC_ED_REPEAT_DIST);
  m_hEdSep = GetItem(IDC_ED_REPEAT_SEP);
  m_hEdMaxReps = GetItem(IDC_ED_MAX_REP);
  m_hRepeatTxt = GetItem(IDC_TXT_SEP_HALF);
  m_hSepOnlyTxt = GetItem(IDC_TXT_SEP_ONLY);

  m_hEdTrunc = GetItem(IDC_ED_TRUNC);
  m_hSpinTrunc = GetItem(IDC_SPIN_TRUNC);

  m_hNoRepsCk = GetItem(IDC_CK_TRUNC_NOREP);
  m_hNoRepsTxt = GetItem(IDC_TXT_TRUNC_NOREP);
  m_hNoRepsRbMin = GetItem(IDC_RB_TRUNC_MIN);
  m_hNoRepsRbMax = GetItem(IDC_RB_TRUNC_MAX);

  EnableControls(TRUE); // Hide progress bar, show normal controls

  ini->SetDlg(m_hWnd);
  if (!ini->DlgEditFromKey(ini->nMaxReps, IDC_ED_MAX_REP) ||
      !ini->DlgCheckFromKey(ini->bLimitReps, IDC_CK_MAX_REP) || // Do after edit update sets checked

      !ini->DlgRBsFromKey(ini->uSepOption, IDC_RB_REPEAT_DIST, IDC_RB_REPEAT_SEP) ||
      !ini->DlgEditFromKey(ini->uSepDistSize, IDC_ED_REPEAT_DIST) ||
      !ini->DlgEditFromKey(ini->uSepCount, IDC_ED_REPEAT_SEP) ||

      !ini->DlgEditFromKey(ini->nTruncSize, IDC_ED_TRUNC) ||
      !ini->DlgCheckFromKey(ini->bTruncate, IDC_CK_TRUNC) || // Do after edit update sets checked

      !ini->DlgCheckFromKey(ini->bTruncNoReps, IDC_CK_TRUNC_NOREP) ||
      !ini->DlgRBsFromKey(ini->nUseMaxTruncSize, IDC_RB_TRUNC_MIN, IDC_RB_TRUNC_MAX) )
  {
    ini->ShowError(m_sCapt, _T("Error applying ini file settings to dialog"));
  }

  CheckBn(IDC_CK_SAVEWARN, TRUE); // Always checked on start up

  EnableLimitNoReps(); // Only enable these options if "Most even dist" RB checked

  _stprintf(sBS, m_sFmtOneThirdAvgSep, ini->uSepDistSize * 3);
  SetWindowText(m_hRepeatTxt, sBS);

  if (ini->uSepCount)
  {
    _stprintf(sBS, m_sFmtSepOnly, ini->uSepCount * 2);
    SetWindowText(m_hSepOnlyTxt, sBS);
  }
  else SetWindowText(m_hSepOnlyTxt, m_sSepNowPureRandom);

  // 0 on spin ctl is 1 in edit, 98 spin is 99 edit
  SendMessage(m_hSpinDist, UDM_SETRANGE, 0, MAKELONG(98, 0));
  SendMessage(m_hSpinDist, UDM_SETPOS, 0, MAKELONG((SHORT) ini->uSepDistSize - 1, 0));

  //SendMessage(m_hSpinSep, UDM_SETRANGE, 0, MAKELONG(98, 0));
  //SendMessage(m_hSpinSep, UDM_SETPOS, 0, MAKELONG((SHORT) ini->uSepCount - 1, 0));
  SendMessage(m_hSpinSep, UDM_SETRANGE, 0, MAKELONG(99, 0));
  SendMessage(m_hSpinSep, UDM_SETPOS, 0, MAKELONG((SHORT)ini->uSepCount, 0));

  SendItemMsg(IDC_ED_REPEAT_DIST, EM_LIMITTEXT, 2, 0);
  SendItemMsg(IDC_ED_REPEAT_SEP, EM_LIMITTEXT, 2, 0);

  // 0 on spin ctl is 1 in edit, 9998 spin is 9999 edit
  SendMessage(m_hSpinTrunc, UDM_SETRANGE, 0, MAKELONG(9998, 0));
  SendMessage(m_hSpinTrunc, UDM_SETPOS, 0, MAKELONG((SHORT) ini->nTruncSize - 1, 0));
  SendItemMsg(IDC_ED_TRUNC, EM_LIMITTEXT, 4, 0);

  SendMessage(m_hSpinMaxReps, UDM_SETRANGE, 0, MAKELONG(98, 0));
  SendMessage(m_hSpinMaxReps, UDM_SETPOS, 0, MAKELONG((SHORT)ini->nMaxReps - 1, 0));
  SendItemMsg(IDC_ED_MAX_REP, EM_LIMITTEXT, 2, 0);


//  if (!GetOwnDirSlash(m_sOwnDirSlash))
//    fnErr(_T("Error determining own folder, may be errors if logging turned on"),
//          m_sOwnDirSlash);

//  _stprintf(m_sStartLogPath, _T("%sstartup.log"), m_sOwnDirSlash);
  _stprintf(m_sStartLogPath, _T("%sstartup log.txt"), m_pzwd->sAppDocsDirSlash);

  // Set up ListView
  m_hList = GetItem(IDC_LV_SRC);
  if (!fnListViewInitColumns())
    fnErr(_T("Error initializing ListView columns"));

//  // 3.0 Replace ListView's default tooltip with explicit custom tooltip
//  ReplaceLvTooltip();

  if (!GetVersionNumStr(m_sVersionNum))
  {
    _tcscpy(m_sVersionNum, _T("3")); // Close enough
    fnErr(_T("Non-fatal error getting version number (for .wpl playlist entry)"),
          m_sError,  _T("OK to continue"));
  }

//  if (_beginthread(LoadPlaylistAll, 0, NULL) == -1)
  //if (_beginthread(LoadPlaylistWrapper, 0, NULL) == -1)
  //  fnErr(_T("Error starting background thread to load WMP library"));

  //m_hThreadLoad = CreateThread(NULL, 0, LoadWmpThreadProc,
  //                             NULL, 0, NULL);
  //if (!m_hThreadLoad)
  //  fnSysErr(_T("Error creating background thread to load WMP library"));


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

//void CMinDlg::LoadPlaylistAll() // To run in background thread on start up
//{
//  Sleep(10000L); // milliseconds
//  MessageBox(NULL, "End thread", "End Thread", MB_OK);
//  _endthread();
//}

BOOL CMinDlg::fnListViewInitColumns()
{
  LVCOLUMN lvc;
  //TCHAR sHdr[128];

  //ListView_SetExtendedListViewStyle(m_hList,
  //                         LVS_EX_FULLROWSELECT); // | LVS_EX_LABELTIP);
  ListView_SetExtendedListViewStyleEx(m_hList,
    // To leave other styles unchanged, mask 2nd arg must be same as style 3rd arg
    // Seems LVX_EX_LABELTIP gets turned on if LVS_EX_INFOTIP, but be explicit
            LVS_EX_FULLROWSELECT | LVS_EX_LABELTIP | LVS_EX_INFOTIP,
            LVS_EX_FULLROWSELECT | LVS_EX_LABELTIP | LVS_EX_INFOTIP);
//            LVS_EX_FULLROWSELECT | LVS_EX_LABELTIP,// | LVS_EX_INFOTIP,
//            LVS_EX_FULLROWSELECT | LVS_EX_LABELTIP); // | LVS_EX_INFOTIP);

  //// Normalize column width, specified in raw pixels, for resolution & font
  //INT nBaseWidth = LOWORD(GetDialogBaseUnits()); // avg pixels per char

  // Initialize the LVCOLUMN structure.
  // The mask specifies that the format, width, text, and
  // subitem members of the structure are valid. 
//  lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
  lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_SUBITEM;
  
  lvc.fmt = LVCFMT_LEFT; // 1st column left aligned
  //lvc.pszText = sHdr;    // always same string pointer, text changes
  lvc.iSubItem = 0;  // Same as col index param in ListView_InsertColumn

//  lvc.cx = 278;      // Column for name of playlist or song group
//  lvc.cx = 228; // failed test to fit "(all" and "same)"
  lvc.cx = 273;      // Column for name of playlist or song group
  if (ListView_InsertColumn(m_hList, lvc.iSubItem, &lvc) == -1) 
    return fnErr(_T("Failed to insert first listview column"));

// BEGIN TEST
//lvc.cx = 175; // = 35 * 5
lvc.cx = 180; // = 35 * 5
lvc.fmt = LVCFMT_RIGHT;
lvc.iSubItem ++;
if (ListView_InsertColumn(m_hList, lvc.iSubItem, &lvc) == -1) 
{
  _stprintf(sBS, _T("Failed to insert listview column %d"),
            lvc.iSubItem + 1);
  return fnErr(sBS);
}
// END TEST

// RESTORE BELOW IF SEPARATE COLUMNS
/*
  // Remaining 5 columns are for counts, all same width and right-aligned
  lvc.fmt = LVCFMT_RIGHT; // columns for counts
  lvc.cx = 35; // All remaining have same width
//  lvc.cx = 45; // failed test to fit "(all" and "same)"

  for (INT i = 0; i < 5; i++)
  {
    lvc.iSubItem ++;
    if (ListView_InsertColumn(m_hList, lvc.iSubItem, &lvc) == -1) 
    {
      _stprintf(sBS, _T("Failed to insert listview column %d"),
                lvc.iSubItem + 1);
      return fnErr(sBS);
    }
  }
*/

  return TRUE;
} // BOOL CMinDlg::fnListViewInitColumns()

BOOL CMinDlg::fnListViewInsertItem(LPCTSTR sName, INT nLparam, INT* naCounts)
{
  LVITEM lvi;
  TCHAR sTxt[2048];

  // Initialize LVITEM members that are common to all items. 
  //lvi.iItem = 0; // Where in list before sorting, 0/first always safe
  lvi.iItem = ListView_GetItemCount(m_hList); // Add to end of list (0 if none)

  lvi.cchTextMax = 2047;

  // For sorting list need LVIF_PARAM. Not used for sorting here
  lvi.mask = LVIF_TEXT | LVIF_PARAM;
  lvi.lParam = (LPARAM) nLparam; // Index in m_cpWmpPlaylistArr, -(day count), or type

  lvi.pszText = sTxt;  // Always same pointer, copy text each time

  lvi.iSubItem = 0;
  // First column for name of playlist or song group
  _tcscpy(sTxt, sName);
  if (ListView_InsertItem(m_hList, &lvi) == -1)
    return SetError(_T("Error group or playlist name"), sTxt);

  lvi.mask = LVIF_TEXT;

//  // Put full path as info tip
//#if WINVER >= 0x0501  // 500 is W2000, 501 is XP
//  //_tcscpy(sTxt, sName);
//  WCHAR swTip[2048];
//  if (!TcharToWchar(sName, swTip))
//  {
//    UtilGetError(sBS);
//    fnErr(_T("Error converting playlist path to wide chars"), sName, sBS);
//  }
//  else
//  {
//    LVSETINFOTIP lvsi;
//    lvsi.cbSize = sizeof(LVSETINFOTIP);
//    lvsi.dwFlags = 0;
//    lvsi.iItem = 0;
//    lvsi.iSubItem = lvi.iSubItem;
//    lvsi.pszText = swTip;
//
//    if (!ListView_SetInfoTip(m_hList, &lvsi))
//    {
//      fnErr(_T("Non-fatal error setting info tip:"), sName);
//    }
//  }
//#endif


  // Repitition count Sub Items
  lvi.iSubItem ++;
  if (naCounts[0]) // All songs same repetitions
  {
//    // String starts with space to indicate same repetitions all
//    _stprintf(sTxt, _T(" %d  (Same for all song ratings)    "), uaCounts[0]);

    // 3.0 addition: -1 means this is an exclusion filter
    // No longer use this string to get counts later, now stored in class
    if (naCounts[0] == -1)
    {
      _tcscpy(sTxt, _T("Exclude songs from output playlist"));
      // 3.0 need to add save counts in CSongsSelection
    }
    else
    {
      _stprintf(sTxt, _T(" %d  (Same for all song ratings)    "), naCounts[0]);
      // 3.0 need to add save counts in CSongsSelection
    }

    if (ListView_SetItem(m_hList, &lvi) == -1)
      return SetError(_T("Error inserting fixed repetition count subitem"), sTxt);
  }
  else // naCounts[0] == 0 so use naCounts[1 to 5]
  {
    TCHAR sPadBase[SPACE_PADDING_LV_REPS + 1];
    for (int i = 0; i < SPACE_PADDING_LV_REPS; i++) sPadBase[i] = _T(' ');
    sPadBase[SPACE_PADDING_LV_REPS] = _T('\0');
    TCHAR sThisCount[16];
    LPCTSTR sPadOneDigit = _T("  "); // 2 spaces same width as 1 digit
    sTxt[0] = _T('\0'); // Will use strcat, start empty

    // Counts param, uaCounts[i] is repetitons for i stars (1 to 5);
    // Text display is reverse order of stars, start with 5 stars on left
    for (INT i = 5; i > 0; i --) // uaCounts[0] not used here, is for fixed reps
    {
      // String starting with digits indicates repetitions vary with song rating
      if (i < 5) // space pad between counts, not before first
      {
        _tcscat(sTxt, sPadBase); // Amount of padding before 2 digit number
        if (naCounts[i] < 10) _tcscat(sTxt, sPadOneDigit); // More pad for 1 digit
      }

      _stprintf(sThisCount, _T("%d"), naCounts[i]);
      _tcscat(sTxt, sThisCount);
    } // for ( ; lvi.iSubItem <= nMaxSubItem; lvi.iSubItem ++)

    if (ListView_SetItem(m_hList, &lvi) == -1)
      return SetError(_T("Error inserting ratings repetition count subitem"), sTxt);
  } // else, i.e. use star ratings

  return TRUE;
} // BOOL CMinDlg::fnListViewInsertItem(LPCTSTR sName, INT nLparam, INT* naCounts)

void CMinDlg::LvCountArrayToStr(INT* naCounts, LPTSTR sCounts)
{
  if (naCounts[0] > 0) // All songs same repetitions
  {
    // String starts with space to indicate same repetitions all <== 3.0 No longer need space?
    _stprintf(sCounts, _T(" %d  (Same for all song ratings)    "), naCounts[0]);
  }
  else if (naCounts[0] < 0) // Exclusion filter
  {
    _tcscpy(sCounts, _T("Exclude songs from output playlist"));
  }
  else // repetitions vary by song rating, indicated by naCounts[0] = 0
  {
    TCHAR sPadBase[SPACE_PADDING_LV_REPS + 1];
    for (int i = 0; i < SPACE_PADDING_LV_REPS; i++) sPadBase[i] = _T(' ');
    sPadBase[SPACE_PADDING_LV_REPS] = cNull;
    TCHAR sThisCount[16];
    LPCTSTR sPadOneDigit = _T("  "); // 2 spaces same width as 1 digit
    sCounts[0] = cNull; // Will use strcat, start empty

    // Counts param, uaCounts[i] is repetitons for i stars (1 to 5);
    // Text display is reverse order of stars, start with 5 stars on left
    for (INT i = 5; i > 0; i --) // uaCounts[0] not used here, is for fixed reps
    {
      // String starting with digits indicates repetitions vary with song rating
      if (i < 5) // space pad between counts, not before first
      {
        _tcscat(sCounts, sPadBase); // Amount of padding before 2 digit number
        if (naCounts[i] < 10) _tcscat(sCounts, sPadOneDigit); // More pad for 1 digit
      }

      _stprintf(sThisCount, _T("%d"), naCounts[i]);
      _tcscat(sCounts, sThisCount);
    }
  }
} // void CMinDlg::LvCountArrayToStr(UINT* uaCounts, LPTSTR sCounts)

//DWORD WINAPI CacheWmpThreadProc(LPVOID lpIgnore)
VOID CacheWmpThreadProc(LPVOID lpIgnore)
{
  pMinDlg->CacheWmpData();
  //return 0;
  ExitThread(0);
}

// Mainly to start background WMP caching after the window is shown,
// but also see if Listview stuff that failed in initdlg is ok later.
void CMinDlg::OnFirstShow()
{
//  ReplaceLvTooltip(); // Return value doesn't matter?

  LoadWmpLibrary();
} // void CMinDlg::OnFirstShow()

/*
BOOL CMinDlg::ReplaceLvTooltip() // Actually assign; no default tooltip control
{
  //m_hTipOrig = ListView_GetToolTips(m_hList);

  //if (!m_hTipLv)
  //  return fnSysErr(_T("Error getting original tooltip handle"));

  m_hTipLv = CreateWindowEx(NULL, TOOLTIPS_CLASS, NULL,
                      WS_POPUP | TTS_NOPREFIX | TTS_BALLOON,
                      CW_USEDEFAULT, CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT,
                      m_hWnd, NULL, m_hInst, NULL);
  if (!m_hTipLv)
    return fnSysErr(_T("Error creating tooltip window"));

  // Standard for new tooltip window
  if (!SetWindowPos(m_hTipLv, HWND_TOPMOST,0, 0, 0, 0,
                    SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE))
    return fnSysErr(_T("Error setting tooltip topmost"));

  if (!m_hTipLv)
    return fnSysErr(_T("Error assigning tooltip to listview"));

  m_hTipOrig = ListView_SetToolTips(m_hList, m_hTipLv);


//  TOOLINFO ti;
//  ti.cbSize = sizeof(TOOLINFO);
////  ti.hwnd = m_hWnd;
////  ti.hwnd = m_hList;
////  ti.hwnd = m_hTipOrig;
////  ti.uId = IDC_LV_SRC;
//  ti.uFlags = TTF_IDISHWND;
//  ti.uId = (UINT_PTR) m_hTipLv;
//
//  if (!SendMessage(m_hTipLv, TTM_GETTOOLINFO, 0, (LPARAM) &ti))
//    return fnSysErr(_T("TTM_GETTOOLINFO error"));


//  TOOLINFO ti;
//  ti.cbSize = sizeof(ti);
////  ti.uFlags = TTF_IDISHWND | TTF_TRANSPARENT; // Mouse moves over tooltip to parent
////  ti.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
//  ti.uFlags = TTF_SUBCLASS;
//  ti.hwnd = m_hWnd; // window gets messages per LPSTR_TEXTCALLBACK
////  ti.hwnd = m_hList; // window gets messages per LPSTR_TEXTCALLBACK
////  ti.uId = (UINT_PTR) m_hList;
//  ti.uId = IDC_LV_SRC;
//  ti.hinst = NULL;
//  ti.lpszText = LPSTR_TEXTCALLBACK;
////  SendMessage(m_hTipLv, TTM_ADDTOOL, 0, (LPARAM) &ti );
//  SendMessage(m_hTipLv, TTM_SETTOOLINFO, 0, (LPARAM) &ti );

  TOOLINFO ti;
ti.cbSize    = sizeof(TOOLINFO);
ti.uFlags    = TTF_TRANSPARENT | TTF_CENTERTIP;
ti.hwnd      = m_hList;
ti.uId       = 0;
ti.hinst     = NULL;
ti.lpszText  = LPSTR_TEXTCALLBACK;
//ToolTip_AddTool(m_hTipLv, &ti);


if (!SendMessage(m_hTipLv, TTM_ADDTOOL, 0, (LPARAM) &ti))
  return fnSysErr(_T("TTM_ADDTOOL error"));

SendMessage(m_hTipLv, TTM_SETTOOLINFO, 0, (LPARAM) &ti);

TOOLINFO ti2;
ti2.cbSize    = sizeof(TOOLINFO);
ti2.hwnd      = m_hTipLv;
ti2.uId       = (UINT_PTR) m_hTipLv;
ti2.uFlags = TTF_IDISHWND;
if (!SendMessage(m_hTipLv, TTM_GETTOOLINFO, 0, (LPARAM) &ti2))
    return fnSysErr(_T("TTM_GETTOOLINFO error (ti2 test)"));


//// TEST
//MB(_T("ReplaceLvTooltip successful"),
//   (m_hTipOrig ? _T("Original not null") : _T("Original was NULL")));
//HWND hBS = ListView_GetToolTips(m_hList);
//if (!hBS)
//  fnSysErr(_T("ListView_GetToolTips error with new tooltip handle"));

  return TRUE;
} // BOOL CMinDlg::ReplaceLvTooltip()
*/

void CMinDlg::LoadWmpLibrary()
{
  if (ini->bLog)
  {
    _stprintf(sBS,
              _T("%s Begin caching Windows Media Player library data\r\n"),
              TimeStamp());
    AddToStartLogMsg(sBS);
    m_dwStartTime = GetTickCount();
  }

  HRESULT hr = S_OK;

  hr = m_cpPlayer.CoCreateInstance( __uuidof(WindowsMediaPlayer),
                                    0, CLSCTX_INPROC_SERVER );
  if (!SUCCEEDED(hr))
    return ReleaseWmpPointers(_T("CoCreateInstance (Media Player)"), hr);
//DWORD dw1CoCreated = GetTickCount();

/*
    //hr = m_cpPlayer->get_mediaCollection(&m_cpWmpAllMedia);
    //if (!SUCCEEDED(hr))
    //  return ReleaseWmpPointers(_T("get_mediaCollection"), hr);
//DWORD dw2GotMediaColl = GetTickCount();

//    CComBSTR cbsAttrLabel, cbsAttrValue;
//    cbsAttrLabel = _T("MediaType");
//    cbsAttrValue = _T("playlist");
//    hr = m_cpWmpAllMedia->getByAttribute(cbsAttrLabel, cbsAttrValue,
//                                         &m_cpWmpPlaylistOfLists);
//    if (!SUCCEEDED(hr))
//      return ReleaseWmpPointers(_T("getByAttribute"), hr);
//DWORD dw3GotPlaylistOfLists = GetTickCount();
*/

  hr = m_cpPlayer->get_playlistCollection(&m_cpWmpPlaylistColl);
  if (!SUCCEEDED(hr) || !m_cpWmpPlaylistColl.p)
    return ReleaseWmpPointers(_T("get_playlistCollection"), hr);
//DWORD dw4GotPlaylistColl = GetTickCount();

  // YES DO THIS HERE INSTEAD OF SELECT DLG
  hr = m_cpWmpPlaylistColl->getAll(&m_cpWmpPlaylistArr);
  if (!SUCCEEDED(hr) || !m_cpWmpPlaylistArr.p)
    return ReleaseWmpPointers(_T("m_cpWmpPlaylistColl->getAll"), hr);
//DWORD dw5GotPlayArr = GetTickCount();

  // MOVED FORM CacheWmpData
  if (!m_cpWmpAllMedia.p) // Just reality check, should always be NULL
    hr = m_cpPlayer->get_mediaCollection(&m_cpWmpAllMedia);

  if (!SUCCEEDED(hr))
    return ReleaseWmpPointers( _T("get_mediaCollection failed"));


/*
//long nlCount;
//hr = m_cpWmpPlaylistOfLists->get_count((long*) &nlCount);

TCHAR sMsg[4096];
_stprintf(sMsg, _T("%d ms CoCreateInstance\n")
//                _T("%d ms m_cpPlayer->get_mediaCollection\n")
//                _T("%d ms m_cpPlayer->getByAttribute\n")
                _T("%d ms m_cpPlayer->get_playlistCollection\n")
                _T("%d ms m_cpWmpPlaylistColl->getAll\n")
                _T("%d ms TOTAL"),
//                _T("\n\n%d items in m_cpWmpPlaylistOfLists"),
                (INT) (dw1CoCreated - dw0Start),
//                (INT) (dw2GotMediaColl - dw1CoCreated),
//                (INT) (dw3GotPlaylistOfLists - dw2GotMediaColl),
      //(INT) (dw4GotPlaylistColl - dw3GotPlaylistOfLists),
        (INT) (dw4GotPlaylistColl - dw1CoCreated),
                (INT) (dw5GotPlayArr - dw4GotPlaylistColl),
                (INT) (dw5GotPlayArr - dw0Start));

                //(INT) (dw4GotPlaylistColl - dw2GotMediaColl),
                //(INT) (dw4GotPlaylistColl - dw0Start)); //,
//                (INT) nlCount);
MB(sMsg);
*/

  if (ini->bLog)
  {
    DWORD dwInterval = GetTickCount() - m_dwStartTime;
    _stprintf(sBS,
              _T("%d milliseconds loading media and playlist collections\r\n"),
              (INT) dwInterval);
    AddToStartLogMsg(sBS);
  }

  // Now have all the WMP pointers needed for any button handlers,
  // so now safe to process window messages
  // Meanwhile, background thread to read all songs to get data cached
  HANDLE hThread = CreateThread(NULL, 0,
                                (LPTHREAD_START_ROUTINE) CacheWmpThreadProc,
                                NULL, 0, NULL);

  // If user goes right to SelectDlg while this thread is still running,
  // loading its listboxes is slow unless this thread yields priority
  if (hThread) SetThreadPriority(hThread, THREAD_PRIORITY_BELOW_NORMAL);
  else  fnSysErr(_T("Non-fatal error creating background thread to cache WMP library"));

} // void CMinDlg::LoadWmpLibrary()

void CMinDlg::AddToStartLogMsg(LPCTSTR sMsg1, LPCTSTR sMsg2, LPCTSTR sMsg3)
{
  // N.B.: Assumes any \r\n are supplied as appropriate
  _tcscat(m_sStartLogMsg, sMsg1);
  if (sMsg2) _tcscat(m_sStartLogMsg, sMsg2);
  if (sMsg3) _tcscat(m_sStartLogMsg, sMsg3);
}

void CMinDlg::CacheWmpData() // To run in background thread on start up
{
  if (ini->bLog)
  {
    m_dwStartTime = GetTickCount();
    _stprintf(sBS, _T("%s Begin cache WMP playlist and song data\r\n"),
              TimeStamp());
    AddToStartLogMsg(sBS);
  }
  DWORD dwPrev = m_dwStartTime;
  DWORD dwNow;

  USES_CONVERSION; // Macro that sets up for other macros like OLE2T

  // Do list of playlists first, moved from end of this function
  // Fetch all the info that Select Dlg would, to get WMP to cache it
  INT nCount;
  HRESULT hr = m_cpWmpPlaylistArr->get_count((long*) &nCount);

  if (!SUCCEEDED(hr))
    RET_V(_T("Can't load playlists, failed m_cpWmpPlaylistArr->get_count"));

  if (ini->bLog)
  {
    dwNow = GetTickCount();
    _stprintf(sBS,
        _T("%d milliseconds getting playlist count %d\r\n"),
              (INT) (dwNow - dwPrev), nCount);
    AddToStartLogMsg(sBS);
    dwPrev = dwNow; // For next entry
  }

  CComPtr<IWMPPlaylist> cpThisPlaylist;

  for (INT i = 0; i < nCount; i++)
  {
    // If SelectDlg starts, it's looking for the same data, so no point continuing
    // for cache purposes. Will sleep to avoid any disk/cpu competition while the
    // select dlg loads listbox of playlists, then will cache all songs data.
    if (m_b1stSelectStarted) break; // Will sleep per m_b1stSelectStarted after loop

    hr = m_cpWmpPlaylistArr->item(i, &cpThisPlaylist);

    // For individual item error, skip non-fatal
    if (SUCCEEDED(hr) && cpThisPlaylist.p)
    {
      // Fetching attributes doesn't noticibly speed up later calls.
      // Just running the item command above accomplishes the caching.
      // Just release the smart pointer, with all the old stuff commented out
      cpThisPlaylist.Release();
    } // if (SUCCEEDED(hr) && pThisPlaylist.p)
  } // for (INT i = 0; i < nCount; i++)  i.e. list of playlists

  if (ini->bLog) dwNow = GetTickCount(); // How it's used varies below

  // What to do after loop depends on whether finished, or halted by Select Dlg
  if (m_b1stSelectStarted)
  {
    if (ini->bLog)
    {
      _stprintf(sBS,
          _T("%s Caching thread suspended by start of Select Songs dialog after\r\n")
             _T("%d milliseconds of Playlists caching\r\n"),
                TimeStamp(), (INT) (dwNow - dwPrev));
      AddToStartLogMsg(sBS);
      dwPrev = dwNow; // For next entry after sleep loop
    }
    // Sleep 8 seconds or until Select Dlg returns, whichever sooner
    for (INT i = 0; i < 8; i++)
    {
      Sleep(1000);
      if (m_b1stSelectFinished) break;
    }
    if (ini->bLog)
    {
      dwNow = GetTickCount();
      _stprintf(sBS, _T("%s  Resume caching thread after\r\n")
                        _T("%d milliseconds sleep "),
                TimeStamp(), (INT) (dwNow - dwPrev));
      if (m_b1stSelectFinished)
        _tcscat(sBS, _T("until Select Dlg returned\r\n"));
      else // By now 
        _tcscat(sBS, _T("timed out before Select Dlg return\r\n"));
      AddToStartLogMsg(sBS);
      dwPrev = dwNow;
    }
  }
  else // i.e. (!m_b1stSelectStarted)
  {
    if (ini->bLog)
    {
      _stprintf(sBS,
          _T("%d milliseconds caching list of all playlists\r\n"),
                (INT) (dwNow - dwPrev));
      AddToStartLogMsg(sBS);
      dwPrev = dwNow;
    }
  }

  // Now cache data on all songs, maybe after pause abve

  LPCTSTR sErrIntro = _T("Non-fatal error caching list of all songs in WMP");
  BOOL bSelectHasRun = FALSE;

  CComBSTR cbsAttribute, cbsValue;
  cbsAttribute = _T("MediaType");
  cbsValue =_T("audio");
  if (!m_cpWmpPlaylistAll.p)
    hr = m_cpWmpAllMedia->getByAttribute(cbsAttribute, cbsValue,
                                        &m_cpWmpPlaylistAll);

  if (!SUCCEEDED(hr))
  {
    if (m_cpWmpPlaylistAll.p) m_cpWmpPlaylistAll.Release();
    if (m_cpWmpAllMedia.p) m_cpWmpAllMedia.Release();
    m_bCacheFinished = TRUE;
    RET_V2(sErrIntro, _T("getByAttribute failed"));
  }

  if (ini->bLog)
  {
    dwNow = GetTickCount();
    _stprintf(sBS, 
              _T("%d milliseconds load WMP all media (audio) pointers\r\n"),
              (INT) (dwNow - dwPrev));
    AddToStartLogMsg(sBS);
    dwPrev = dwNow;
  }


  if (m_bStopCaching) // Started shuffle
  {
    m_bCacheFinished = TRUE;
    if (ini->bLog)
    {
      AddToStartLogMsg(_T("Caching all songs halted for actual shuffle\r\n"));
      if (!WriteStrToFile(m_sStartLogMsg, m_sStartLogPath, FALSE, FALSE))
        fnErr(_T("Non-fatal error logging start up cache times"), UtilError());
    }
    return;
  }


  // Above got pointer members that will actually get used.
  // Here, read all songs info into temp vars that get discarded, just to
  // get WMP to cache the data so it reads faster in the real shuffle.
  long nlPlaylistCount;
  hr = m_cpWmpPlaylistAll->get_count(&nlPlaylistCount);
  if (!SUCCEEDED(hr))
  {
    m_bCacheFinished = TRUE;
    RET_V2(sErrIntro, _T("Playlist get_count failed"));
  }

  //CComBSTR cbsAttribUserRating = _T("UserRating");

  //CComPtr<IWMPMedia> cpSong;
  //cpSong.p = NULL;

  //CComBSTR cbsAttribAcqYear = _T("AcquisitionTimeYear");
  //CComBSTR cbsAttribAcqMonth = _T("AcquisitionTimeMonth");
  //CComBSTR cbsAttribAcqDay = _T("AcquisitionTimeDay");

  //// Read data on each song in m_cpWmpPlaylistAll
  //for (long i = 0; i < nlPlaylistCount; i++)
  //{

  INT nLoopsPerCall = 1000;

  // To stop CComPtr leak as in AddToSongArray, call function w/ vars in own scope
  for (INT nIxStart = 0; nIxStart < nlPlaylistCount; nIxStart += nLoopsPerCall)
  {
    INT nIxEnd = min((nlPlaylistCount - 1), (nIxStart + nLoopsPerCall - 1));
    
    if (!CacheWmpDataSegmentOfAll(nIxStart, nIxEnd)) return; // fnErr msg already

    // TRUE return above can be when cut short by m_bStopCaching set TRUE
    if (m_bStopCaching) // Started shuffle
    {
      m_bCacheFinished = TRUE;

      if (ini->bLog)
      {
         dwNow = GetTickCount();
        _stprintf(sBS, 
          _T("%d milliseconds caching each song halted for actual shuffle\r\n"),
                  (INT) (dwNow - dwPrev));
        AddToStartLogMsg(sBS);
        dwPrev = dwNow;

        if (!WriteStrToFile(m_sStartLogMsg, m_sStartLogPath, FALSE, FALSE))
          fnErr(_T("Non-fatal error logging start up cache times"), UtilError());
      }
      return; 
    }

  } // for (long i = 0; i < nlPlaylistCount; i++)
  
  m_bCacheFinished = TRUE;
  
  // Any error woud exit the loop
  //if (!SUCCEEDED(hr)) RET_V2(sErrIntro, sErrMsg);

  if (ini->bLog)
  {
    //DWORD dwFinishSongs = GetTickCount();
    dwNow = GetTickCount();
    _stprintf(sBS, _T("%d milliseconds read all songs\r\n")
                      _T("%d milliseconds TOTAL\r\n")
                      _T("%s Finished caching WMP data\r\n"),
              (INT) (dwNow - dwPrev),
              (INT) (dwNow - m_dwStartTime),
              TimeStamp());
    AddToStartLogMsg(sBS);


    if (!WriteStrToFile(m_sStartLogMsg, m_sStartLogPath, FALSE, FALSE))
      fnErr(_T("Non-fatal error logging start up cache times"), UtilError());

    m_sStartLogMsg[0] = cNull; // Flag for LogSelectDlgStartMsg

    if (m_sStartLogSelectMsg[0]) // SelectDlg recorded it while this was running
    {
      if (!WriteStrToFile(m_sStartLogSelectMsg, m_sStartLogPath, TRUE, FALSE))
        fnErr(_T("Non-fatal error logging Select dlg 1st load time"),
              UtilError());
    } // else not timed yet, will be written to file by LogSelectDlgStartMsg

  } // if (ini->bLog)

} // void CMinDlg::CacheWmpData()

BOOL CMinDlg::CacheWmpDataSegmentOfAll(INT nIxStart, INT nIxEnd)
{
  LPCTSTR sErrIntro = _T("Non-fatal error caching list of all songs in WMP");
  TCHAR sErrMsg[2048];

  CComPtr<IWMPMedia> cpSong;
  cpSong.p = NULL;

  CComBSTR cbsAttribUserRating = _T("UserRating");
  CComBSTR cbsAttribAcqYear = _T("AcquisitionTimeYear");
  CComBSTR cbsAttribAcqMonth = _T("AcquisitionTimeMonth");
  CComBSTR cbsAttribAcqDay = _T("AcquisitionTimeDay");
  CComBSTR cbsValue;

  HRESULT hr;

  // Read data on each song in m_cpWmpPlaylistAll
  for (long i = nIxStart; i <= nIxEnd; i++)
  {
    if (m_bStopCaching) // Started shuffle
    {
      m_bCacheFinished = TRUE;
      return TRUE; 
    }

    hr = m_cpWmpPlaylistAll->get_item(i, &cpSong);
    if (!SUCCEEDED(hr) || !cpSong.p)
    {
      _tcscpy(sErrMsg, _T("cpPlaylist->get_item failed"));
      break;
    }

    hr = cpSong->getItemInfo(cbsAttribAcqYear, &cbsValue);
    if (SUCCEEDED(hr))
    {
      cbsValue.Empty();
      hr = cpSong->getItemInfo(cbsAttribAcqMonth, &cbsValue);
    }
    if (SUCCEEDED(hr))
    {
      cbsValue.Empty();
      hr = cpSong->getItemInfo(cbsAttribAcqDay, &cbsValue);
    }

    if (SUCCEEDED(hr)) cbsValue.Empty();
    else
    {
      _tcscpy(sErrMsg, _T("Error getting date information on song"));
      break;
    }

    hr = cpSong->get_sourceURL(&cbsValue);
    if (SUCCEEDED(hr)) cbsValue.Empty();
    else
    {
      _tcscpy(sErrMsg, _T("cpSong->get_sourceURL failed"));
      break;
    }

    hr = cpSong->getItemInfo(cbsAttribUserRating, &cbsValue);
    if (SUCCEEDED(hr)) cbsValue.Empty();
    else
    {
      _tcscpy(sErrMsg, _T("cpSong->getItemInfo (rating) failed"));
      break;
    }

    cpSong.Release();
  } // for (long i = 0; i < nlPlaylistCount; i++)

  if (cpSong.p) cpSong.Release();

  if (!SUCCEEDED(hr)) return fnErr(sErrIntro, sErrMsg);
  return TRUE;
} // BOOL CMinDlg::CacheWmpDataSegmentOfAll(INT nIxStart, INT nIxEnd)

void CMinDlg::LogSelectDlgStartMsg(LPCTSTR sMsg) // Name now misleading, multiple uses
{
  // This allows unlikely exact timing coincidence where sMsg doesn't get written,
  // don't bother bullet proofing
  if (m_sStartLogMsg[0]) // CacheWmpData still running
    _tcscpy(m_sStartLogSelectMsg, sMsg); // CacheWmpData will write it (last if multiple)
  else // CacheWmpData already finished writing
  {
//      if (!WriteStrToFile(m_sStartLogSelectMsg, m_sStartLogPath, TRUE, FALSE))
      if (!WriteStrToFile(sMsg, m_sStartLogPath, TRUE, FALSE))
        fnErr(_T("Non-fatal error logging Select dlg 1st load time"),
              UtilError());
  }
}

void CMinDlg::ReleaseWmpPointers(LPCTSTR sErrMsg, HRESULT hr)
{
  if (sErrMsg)
  {
    TCHAR sMsgFinal[2048];
    _stprintf(sMsgFinal, _T("Error: %s\nHRESULT return was %d"),
              sErrMsg, (INT) hr);
    fnSysErr(sMsgFinal); 
  }
  // else part of normal shutdown

  if (m_cpWmpPlaylistAll.p) m_cpWmpPlaylistAll.Release();
  if (m_cpWmpAllMedia.p) m_cpWmpAllMedia.Release();
  if (m_cpWmpPlaylistArr.p) m_cpWmpPlaylistArr.Release();
  if (m_cpWmpPlaylistColl.p) m_cpWmpPlaylistColl.Release();
  if (m_cpPlayer.p) m_cpPlayer.Release();
} // void CMinDlg::ReleaseWmpPointers()

INT CMinDlg::AvailableSongSelIx()
{
  for (INT i = 0; i < MAX_SONG_SELECTIONS; i++)
  {
    if (mpArr_SongsSelections[i] == NULL) return i;
  }
  return -1; // No unused array member
}

CSongsSelection * CMinDlg::GetSongSelForLvIx(INT nLvIx)
{
  LVITEM lvi;
  lvi.iItem = nLvIx;
  lvi.iSubItem = 0;
  lvi.mask = LVIF_PARAM;

  if (!ListView_GetItem(m_hList, &lvi))
  {
    _stprintf(sBS, _T("Error getting songs selection in row %d"),
              nLvIx + 1); // Don't use 0 based row index for this msg
    return (CSongsSelection*) fnErr(sBS); // return NULL pointer on error
  }
  INT nLvLparam = lvi.lParam;

  if (nLvLparam < 0 || nLvLparam >= MAX_SONG_SELECTIONS)
  {
    _stprintf(sBS, _T("Invalid song selection index %d in row %d"),
              nLvLparam, nLvIx + 1); // Don't use 0 based row index for this msg
    return (CSongsSelection*) fnErr(sBS);
  }

  if (! mpArr_SongsSelections[nLvLparam])
  {
    _stprintf(sBS, _T("Song selection not found for row %d (index %d)"),
              nLvIx + 1, nLvLparam);
    return (CSongsSelection*) fnErr(sBS);
  }

  return mpArr_SongsSelections[nLvLparam];
} // CSongsSelection * CMinDlg::GetSongSelForLvIx(INT nLvIx)

// Changed from OnBnSource to shared handler for OnBnSource & OnBnLvEdit
void CMinDlg::LvItemNewOrEdit(INT nLvIx) // nLvIx == -1 for insert new item
//void CMinDlg::OnBnSource()
{
  // If caching thread still alive, have it sleep 10 seconds so SelectDlg
  // can load playlist listbox without any competition
  m_b1stSelectStarted = TRUE; // No effect if caching thread completed

  BOOL bIsUpdate = (nLvIx > -1);
  INT nLvLparam;
  CSongsSelection * pOldSongsSel = NULL; // Only used if updating selection
  LPTSTR sSelectCapt2; // new or update

  if (bIsUpdate)
  {
    sSelectCapt2 = _T("Change songs selection");

    LVITEM lvi;
    lvi.iItem = nLvIx;
    lvi.iSubItem = 0;
    lvi.mask = LVIF_PARAM;

    if (!ListView_GetItem(m_hList, &lvi))
    {
      _stprintf(sBS, _T("Error getting songs selection in row %d"),
                nLvIx + 1); // Don't use 0 based row index for this msg
      RET_V(sBS);
    }
    nLvLparam = lvi.lParam;

    if (nLvLparam < 0 || nLvLparam >= MAX_SONG_SELECTIONS)
    {
      _stprintf(sBS, _T("Invalid song selection index %d in row %d"),
                nLvLparam, nLvIx + 1); // Don't use 0 based row index for this msg
      RET_V(sBS);
    }

    pOldSongsSel = mpArr_SongsSelections[nLvLparam];
    if (!pOldSongsSel)
    {
      _stprintf(sBS, _T("Song selection not found for row %d (index %d)"),
                nLvIx + 1, nLvLparam);
      RET_V(sBS);
    }
  }
  else // New song selection
  {
    sSelectCapt2 = _T("Add songs to shuffle");
    nLvLparam = AvailableSongSelIx();
    if (nLvLparam == -1)
    {
      _stprintf(sBS,
            _T("Can't add song selection. Maximum number %d selections already in list"),
            MAX_SONG_SELECTIONS);
      RET_V(sBS);
    }
  }

	CSelectDlg* pDlg = NULL;
	pDlg = new CSelectDlg(m_hInst, ini,
                        //IDD_SELECT_SONGS,
//                        NULL,
                        m_sCapt, sSelectCapt2, this,
                        m_cpWmpPlaylistColl,
                        m_cpWmpPlaylistArr,
                        m_cpWmpAllMedia,
                        pOldSongsSel); // NULL if new song selection

	if (!pDlg) RET_V(_T("Error creating Add songs dialog"))

  INT nRet = pDlg->Run();

  m_b1stSelectFinished = TRUE; // Only matters on first select dlg, if then

  if (nRet != IDOK) // e.g. Cancel button
  {
    delete pDlg;
    return;
  }

  // On OK, expect to find info for new ListView entry. Check then proceed
  UINT uGroupType = pDlg->uRetGroupType;
  if (uGroupType == GROUP_TYPE_INVALID) // Some logic error, returned true without setting
  {
    delete pDlg;
    fnErr(_T("Data expected after OK from selection dialog not found"));
    return;
  }

  // Frequency of repetitions. Just copy, let Listview (or CSongsSelection) figure it out.
  INT nArrReps[6];
  for (INT i = 0; i <= 5; i++)
  {
    nArrReps[i] = pDlg->nRetArrReps[i];
  }

/*
Move this to the start of the function, to use if not editing existing row
  // 3.0 change. nLvLparam is index of CSongsSelection array
//  INT nLvLparam = (INT) pDlg->uRetIxOrDays; // Not final value unless it's a playlist.
  INT nLvLparam = AvailableSongSelIx();
  if (nLvLparam == -1)
  {
    delete pDlg;
    _stprintf(sBS,
          _T("Can't add song selection. Maximum number %d selections already in list"),
          MAX_SONG_SELECTIONS);
    RET_V(sBS);
  }
*/

  // If updating selection, array item already has one, need to delete
  //mpArr_SongsSelections[nLvLparam] = pDlg->GetAndClaimSongsSelection();

  CSongsSelection * pNewSongSel = pDlg->GetAndClaimSongsSelection();

  delete pDlg; // Have all info from dialog

//  if (!mpArr_SongsSelections[nLvLparam])
  if (!pNewSongSel)
    RET_V(_T("Error getting Songs Selection from Select dialog"));

  if (bIsUpdate) // Delete old selection, use same array slot for new
  {
    delete mpArr_SongsSelections[nLvLparam];
  }

  mpArr_SongsSelections[nLvLparam] = pNewSongSel;

// ==> TOTALLY REPLACE MUCH OR ALL OF BELOW:


  TCHAR sName[2048];

/*
  if (uGroupType == GROUP_TYPE_ALL)
  {
    _tcscpy(sName, scNameAllSongs);
    nLvLparam = N_LV_ROWTYPE_ALL; // -10000
  }
  //else if (uGroupType == GROUP_TYPE_NEW)  // 3.0 now a modifier of other 2 options
  //{
  //  _stprintf(sName, _T("%s%d days"), scPrefixNewSongs, nLvLparam);
  //  ini->SetAndSaveKey(ini->uDaysNew, nLvLparam); // Default next time
  //  // ListView LPARAM numbers -1 to -999 means new songs, in last that many days
  //  nLvLparam *= -1;
  //}
  else // GROUP_TYPE_PLAYLIST (was GROUP_TYPE_LIST_STATIC or GROUP_TYPE_LIST_AUTO)
  {
    CComBSTR cbsAttrLabelTitle, cbsAttrResult;
    cbsAttrLabelTitle =_T("Title");
    CComPtr<IWMPPlaylist> cpThisPlaylist;

    HRESULT hr = m_cpWmpPlaylistArr->item(nLvLparam, &cpThisPlaylist);
    if (!SUCCEEDED(hr))
    {
      fnErr(_T("Error getting playlist from array"));
      return;
    }

    hr = cpThisPlaylist->getItemInfo(cbsAttrLabelTitle, &cbsAttrResult);
    cpThisPlaylist.Release(); // Whether fail or not. Not necessary? Can't hurt
    if ((!SUCCEEDED(hr)) || cbsAttrResult.m_str == NULL ||
        cbsAttrResult.Length() == 0)
    {
      fnErr(_T("Error getting playlist title"));
      return;
    }
    
    USES_CONVERSION;

    _tcscpy(sName, OLE2T(cbsAttrResult));
    cbsAttrResult.Empty();
    //if (uGroupType == GROUP_TYPE_LIST_STATIC) _tcscat(sName, _T(" <playlist>"));
    //else _tcscat(sName, _T(" <auto playlist>"));
     _tcscat(sName, _T(" <playlist>")); // All playlists, static or auto
  }

  // Whatever the data, now is set for loading as ListView row
  // First make sure it's not already in the list
  LVFINDINFO lvf;
  lvf.flags = LVFI_PARAM;
  lvf.lParam = nLvLparam;
  if (ListView_FindItem(m_hList, -1, &lvf) != -1)
  {
    _stprintf(sBS, _T("\"%s\" is already in the shuffle list\n")
                      _T("To play its songs more often, click")
                       _T(" the \"Change play frequency\" button"),
              sName);
    fnErr(sBS);
    return;
  }
*/

  // 3.0 replacement for above block
  _tcscpy(sName, mpArr_SongsSelections[nLvLparam]->GetSelectionName());

  // ==> TO DO: Add ListView columns for age limit, deduping
  if (!bIsUpdate)
  {
    if (!fnListViewInsertItem(sName, nLvLparam, nArrReps))
      fnErr(_T("Error inserting ListView item"), m_sError);
  }
  else
  {
    ListView_SetItemText(m_hList, nLvIx, 0, sName);

    TCHAR sNewReps[1024];
    LvCountArrayToStr(nArrReps, sNewReps);
    ListView_SetItemText(m_hList, nLvIx, 1, sNewReps);
  }

} // void CMinDlg::LvItemNewOrEdit(INT nLvIx) WAS void CMinDlg::OnBnSource()

//BOOL CMinDlg::LvItemNewOrEdit(INT nLvIx)
void CMinDlg::OnBnSource()
{
  LvItemNewOrEdit(-1); // -1 to indicate new row, not edit existing
}

void CMinDlg::OnBnLvEdit()
{
  // With the single-selection style ListView, returned count is 1 or 0
  if (!ListView_GetSelectedCount(m_hList)) 
    RET_V(
    _T("No item in the list was selected when you clicked the \"Edit selected\" button"));

  INT nIxLv = GetListviewSingleSelected(m_hList);
  if (nIxLv == -1) // None found; ZWin::m_sError may have a message
    RET_V2(_T("Error finding the selected item in the list"), m_sError);

  // 3.0 Call LvItemNewOrEdit to do the rest of the work
  LvItemNewOrEdit(nIxLv);
/*
  // Get playlist/group name and current rep counts and for SelectDlg call
  TCHAR sPlaylistName[1024];
  ListView_GetItemText(m_hList, nIxLv, 0, sPlaylistName, 1000);

  INT naReps[6] = {0, 0, 0, 0, 0, 0}; // LvCountStrToArray may not set all
  if (!LvCountStrToArray(nIxLv, naReps)) return; // Error msg already

  CSelectDlg* pDlg = NULL;
  pDlg = new CSelectDlg(m_hInst, ini,
                        m_sCapt, _T("Change repetition frequency"), this,
                        sPlaylistName,
            (UINT*) naReps); // <== TEMP 3.0 WORK

	if (!pDlg) RET_V(_T("Error creating dialog for Change repetition frequency"))

  INT nRet = pDlg->Run();

  if (nRet != IDOK) // e.g. Cancel button
  {
    delete pDlg;
    return;
  }

  // Read new repetition setting into original variable
  for (INT i = 0; i <= 5; i++)
  {
    naReps[i] = pDlg->nRetArrReps[i];
  }

  TCHAR sNewReps[1024];
  LvCountArrayToStr(naReps, sNewReps);
  ListView_SetItemText(m_hList, nIxLv, 1, sNewReps);
*/
} // void CMinDlg::OnBnLvEdit()

void CMinDlg::OnBnLvRemove()
{
  //TCHAR sName[1024];
  //TCHAR sMsg[1024];

  // With the single-selection style ListView, returned count is 1 or 0
  if (!ListView_GetSelectedCount(m_hList)) 
    RET_V(
    _T("No item in the list was selected when you clicked the Remove button"));

// 3.0 Even in version 2, this loop pointless because this ListView is single-select
//  INT nItemCount = ListView_GetItemCount(m_hList);
//  for (INT nInd = 0; nInd < nItemCount; nInd++)
//  {
//    if (!ListView_GetItemState(m_hList, nInd, LVIS_SELECTED))
//      continue; // Not the selected item, try the next one

// Don't clutter settings w/ confirm option, easy to restore accidental delete
    //// Here, this is the selected item
    //ListView_GetItemText(m_hList, nInd, 0, sName, 1024);
    //_stprintf(sMsg, _T("Remove \"%s\" from the list?"), sName);
    //if (ini->bConfirmDeleteLv && MB(MB_OKCANCEL, sMsg) != IDOK)
    //  return; // Cancel button clicked

  // 3.0 (fix v2 waste) Get single selected item like in OnBnLvEdit, 
  INT nIxLv = GetListviewSingleSelected(m_hList);
  if (nIxLv == -1) // None found; ZWin::m_sError may have a message
    RET_V2(_T("Error finding the selected item in the list"), m_sError);

  // 3.0 Need to delete CSongsSelection for the row being deleted
  LVITEM lvi;
  lvi.iItem = nIxLv;
  lvi.iSubItem = 0;
  lvi.mask = LVIF_PARAM;

  if (!ListView_GetItem(m_hList, &lvi))
  {
    _stprintf(sBS, _T("Error getting songs selection in row %d"),
              nIxLv + 1); // Don't use 0 based row index for this msg
    RET_V(sBS);
  }
  INT nLvLparam = lvi.lParam;

  if (nLvLparam < 0 || nLvLparam >= MAX_SONG_SELECTIONS)
  {
    _stprintf(sBS, _T("Invalid song selection index %d in row %d"),
              nLvLparam, nIxLv + 1); // Don't use 0 based row index for this msg
    RET_V(sBS);
  }

  delete mpArr_SongsSelections[nLvLparam];
  mpArr_SongsSelections[nLvLparam] = NULL; // Mark array pointer deleted, available

  // 3.0 (fix v2 waste) Unindent below, formerly indented in loop
  if (!ListView_DeleteItem(m_hList, nIxLv))
    fnErr(_T("Error deleting selected item from the list"));
    return; // Single selection listview, no more are selected

//  } // for (INT nInd = 0; nInd < nItemCount; nInd++)

  // Here went through the whole list without finding the selected item
  fnErr(_T("Error finding the selected item in the list"));
} // void CMinDlg::OnBnLvRemove()

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

  // If here, normal case of clicking Shuffle button

  if (!ListView_GetItemCount(m_hList))
  {
    fnErr(_T("No music selected to shuffle."),
          _T("Click \"Add a song group ....\" button to choose."));
    m_bInProgress = FALSE;
    return;
  }

  TCHAR sOutPath[MAX_PATH];
  TCHAR sRetMsg[2048]; // For fnShuffleList's return message

  BOOL bRet = fnShuffleList(sOutPath, sRetMsg);

  EnableControls(TRUE); // No effect if fnShuffleList didn't set FALSE

//  if (ini->bLog && !CloseLog(_T("Finished OnShuffle")))
  if (m_hfLog && !CloseLog(_T("Finished OnShuffle")))
    fnErr(_T("Error closing log after shuffle completed OK."), m_sError);

  if (bRet)  MB(MB_ICONINFORMATION, _T("Shuffled playlist created:"),
                sOutPath, _T(""), sRetMsg); // _T("") to skip line in MB
  // else error message already

  m_bInProgress = FALSE;
}

BOOL CMinDlg::fnShuffleList(LPTSTR sPathRet, LPTSTR sRetMsg )//, FLOAT * fTotalSecs)
{

  ClearError(); // Will check m_sError later for non-fatal msg

  INT nMaxSizeFixed; // Original option for absolute number of songs
  LPCTSTR sBadTruncFmt =
               _T("The option to limit shuffled output size is checked, but the size\n")
               _T("limit entered is invalid. It must be a number between 1 and 9999.\n\n")
               _T("Proceed with shuffle using default limit %d?");

  if (!IsBnChecked(IDC_CK_TRUNC)) nMaxSizeFixed = -1; // No limit
  else
  {
    GetItemText(IDC_ED_TRUNC, sBS, 5);
    nMaxSizeFixed = _tstoi(sBS); // returns 0 if empty string
    if (nMaxSizeFixed < 1 || nMaxSizeFixed > 9999)
    {
      _stprintf(sBS, sBadTruncFmt, NTRUNCSIZE_DEFAULT);
      if (MB(MB_OKCANCEL | MB_ICONQUESTION, sBS) != IDOK) return FALSE;
      // Chose to use default. Also replace invalid value (0 or empty) in control
      nMaxSizeFixed = NTRUNCSIZE_DEFAULT;
      _stprintf(sBS, _T("%d"), NTRUNCSIZE_DEFAULT);
      SetWindowText(m_hEdTrunc, sBS); // OnEdSepCount will also update spinner
      // and drop through
    }
  }

  BOOL bWarnOverwrite = IsBnChecked(IDC_CK_SAVEWARN);

  INT nShuffleType;
  UINT uSepThisShuffle;
  INT nProgPctEachShuffle;

  if (IsBnChecked(IDC_RB_REPEAT_DIST)) // Default, most even distribution
  {
    nShuffleType = RB_REPEAT_DIST;
    nProgPctEachShuffle = (2 * PROG_PCT_EA_ARR_SHUFFLE) + PROG_PCT_SEP_DUPS_PT1
                          + PROG_PCT_SEP_DUPS_PT2; // All in one op
    GetItemText(IDC_ED_REPEAT_DIST, sBS, 3);
    uSepThisShuffle = _tstoi(sBS); // returns 0 if empty string
  }
  else // IDC_RB_REPEAT_SEP, now includes OLD IDC_RB_REPEAT_IGNORE case 
  {
    GetItemText(IDC_ED_REPEAT_SEP, sBS, 3);
    uSepThisShuffle = _tstoi(sBS); // returns 0 if empty string

    if (uSepThisShuffle == 0) // Formerly IDC_RB_REPEAT_IGNORE
    {
      nShuffleType = RB_REPEAT_IGNORE;
      nProgPctEachShuffle = PROG_PCT_EA_ARR_SHUFFLE
                          + ((PROG_PCT_SEP_DUPS_PT1 + PROG_PCT_SEP_DUPS_PT2) / 2);
    }
    else
    {
      nShuffleType = RB_REPEAT_SEP;
      nProgPctEachShuffle = PROG_PCT_EA_ARR_SHUFFLE;
    }
  } // WAS: else, i.e. IDC_RB_REPEAT_IGNORE


  // Output file name
  TCHAR sOutPath[MAX_PATH];

  LPCTSTR sDotM3u =  _T(".m3u");
//  LPCTSTR sDotM3u8 =  _T(".m3u8"); // Added in version 3.0
  LPCTSTR sDotWpl =  _T(".wpl");

  // For checking if output file already exists
  WIN32_FIND_DATA fdIgnore;
  HANDLE hFind;
  LPCTSTR sWarnExistsPt1 = _T("A file already exists with the same name");
  LPCTSTR sWarnExistsAskOverWrite =
               _T("\nOverwrite existing file with the new shuffled playlist?")
               _T("\n\nClick No if you want to choose a different file name.");

  // Declare here for use in OpenFileName case
  TCHAR sOutDirSlash[MAX_PATH];
  TCHAR sOutNameNoExt[_MAX_FNAME];
  TCHAR sOutDotExt[_MAX_EXT];

  // Pipes are placeholders below, to be replaced by \0 for OPENFILENAME
  TCHAR sFilter[1024];
  // Version 3.0 add .m3u8 option
  _tcscpy(sFilter, _T("Windows Media Playlists (*.wpl)|*.wpl|")
                   _T("M3U (Unicode UTF-8) Playlists (*.m3u,*.m3u8)|*.m3u;*.m3u8|") // v3.22 also allow same .m3u .ext
                   _T("M3U (ANSI character set) Playlists (*.m3u)|*.m3u|")); // v3.3 move to last option, after m3u UTF8


	for (INT nLoop = 0 ; sFilter[nLoop] != _T('\0') ; nLoop ++)
	{
		if (sFilter[nLoop] == _T('|')) sFilter[nLoop] = _T('\0');
	}

	// Initialize all members of var ofn to null
	OPENFILENAME ofn;
	memset(&ofn, 0, sizeof(OPENFILENAME));

	ofn.lStructSize = sizeof(OPENFILENAME);
	ofn.hwndOwner = m_hWnd;
	ofn.hInstance = m_hInst;

  TCHAR sOfnTitle[_MAX_PATH + 50];
  _tcscpy(sOfnTitle, _T("Save shuffled playlist as ..."));
	ofn.lpstrTitle = sOfnTitle;

  // Set default output type based on preference and source type
  // version 3.0: still 0=wpl, 1=m3u; add 2=m3u8
  // v3.22 now 0=last, => one of 1=wpl, 2=m3u, 3=m3u 
//  INT nPlistFmt = ini->nConvertWpl;
  INT nPlistFmt = ini->nPlaylistFmt; // v3.22
  if (nPlistFmt == RB_FMT_LAST) nPlistFmt =  ini->nPlaylistFmtLast;

  TCHAR sDefaultExt[8];
  // Version 3.0: Add option for .m3u8
  if (nPlistFmt == RB_FMT_WPL) _tcscpy(sDefaultExt, &sDotWpl[1]);
//  else if (nPlistFmt == RB_FMT_M3U) _tcscpy(sDefaultExt, &sDotM3u[1]);
//  else _tcscpy(sDefaultExt, &sDotM3u8[1]); // RB_FMT_M3U8
  else _tcscpy(sDefaultExt, &sDotM3u[1]); // v3.3 Now both ANSI & UTF-8 M3U default to .m3u

  ofn.lpstrDefExt = sDefaultExt; // w/o dot
//  ofn.nFilterIndex = nPlistFmt + 1; // ofn.nFilterIndex is 1-based
  ofn.nFilterIndex = nPlistFmt; // v3.22 now values shifted, RB_FMT_WPL = 1 etc 

	ofn.Flags =  OFN_HIDEREADONLY | OFN_NOREADONLYRETURN
				        | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
                                 // Normally default, w/ hook needs explicit
  ofn.Flags |=  OFN_ENABLEHOOK | OFN_EXPLORER | OFN_ENABLESIZING;
  ofn.lpfnHook = OfHookProc;
//  gnLastIndex = -1; // Ensure != ofn.nFilterIndex on 1st change type dropdown
  gnLastIndex = nPlistFmt; // Value used to save file if user doesn't change format; ergo -1 bad

  // WTF ... v3.3 guess at bug in file type save: if (gnLastIndex != -1) gnLastIndex = nPlistFmt; // Must be right after Save, even if not changed
  if (gnLastIndex != -1) gnLastIndex = nPlistFmt; // Must be right after Save, even if not changed

	ofn.lpstrFilter = sFilter;

	ofn.lpstrInitialDir = NULL; // Unless/until assigned value below

  // Now separately set default dir and file name
  TCHAR sInitialDir[MAX_PATH];
  sInitialDir[0] = _T('\0');

  if (ini->bTgtDirLast && ini->sTgtDirLast && ini->sTgtDirLast[0])
  {
    // Before assigning as default, check that last used dir still exists.
    // If not, assume was a temp folder and use WMP default.
    BOOL bUseLastDir = FALSE; // Unless set TRUE if dir found below
    WIN32_FIND_DATA fd;
    HANDLE hFind = FindFirstFile(ini->sTgtDirLast, &fd);
    BOOL bCreateDir = FALSE; // Unless reset below
    if (hFind != INVALID_HANDLE_VALUE) // Path exists, check if its a directory
    {
      // First deal with must close handle from successful FindFirstFile when done
      FindClose(hFind); // Nothing to do if failed (unlikely)

      if(fd.dwFileAttributes | FILE_ATTRIBUTE_DIRECTORY)
      {
        // Playlist path is a folder as expected
        bUseLastDir = TRUE;
      }
      // else don't try to figure out why it's not there
    }

    if (bUseLastDir)
    {
      _tcscpy(sInitialDir, ini->sTgtDirLast);
      ofn.lpstrInitialDir = sInitialDir;
    }
    // else leave sInitialDir empty for flow control below

  }

  // else if (m_sWmpPlaylistDir[0]) // and ! ini->bTgtDirLast or no ini->sTgtDirLast
  if (m_sWmpPlaylistDir[0] // Have valid default playlist path
      && !sInitialDir[0])  // ! ini->bTgtDirLast or no ini->sTgtDirLast or dir not found
  {
    // 3.2 Create Playlists directory if it doesn't exist. WMP deletes dir if it deletes last playlist.
    WIN32_FIND_DATA fd;
    HANDLE hFind = FindFirstFile(m_sWmpPlaylistDir, &fd);
    BOOL bCreateDir = FALSE; // Unless reset below
    if (hFind != INVALID_HANDLE_VALUE) // Path exists, check if its a directory
    {
      // First deal with must close handle from successful FindFirstFile when done
      FindClose(hFind); // Nothing to do if failed (unlikely)

      if(fd.dwFileAttributes | FILE_ATTRIBUTE_DIRECTORY)
      {
        // Playlist path is a folder as expected
        _tcscpy(sInitialDir, m_sWmpPlaylistDir);
        ofn.lpstrInitialDir = sInitialDir;
      }
      else
      {
        MB(_T("Windows' default Playlists folder path exists but is not a folder:"),
           m_sWmpPlaylistDir,
           _T("The existence of a file (not folder) with that name is probably a mistake you should delete,"),
           _T("but for now you can choose another folder to save your shuffled playlist in."));
        ofn.lpstrInitialDir = NULL; // If left this way, Windows chooses the intial dir for Save dialot 
      }
    }
    else // path m_sWmpPlaylistDir doesn't exist; offer to create it
    {
      _stprintf(sBS, _T("Normally WMP playlists are saved to a %s folder in \"My Music\"")
                     _T("\nbut the folder doesn't exist:"),
                &m_sAppendPlaylists[1]); // First char of m_sAppendPlaylists is backslash
      nBS = MB(MB_YESNO | MB_ICONQUESTION, sBS, m_sWmpPlaylistDir,
               _T("\nDo you want that folder created, to save your shuffled playlist in?\n"),
               _T("(Click NO if you prefer to save your playlist in a different folder.)"));

      if (nBS == IDYES)
      {
        // Security descriptor 2nd arg NULL for new dir inherits permissions from parent.
        if (CreateDirectory(m_sWmpPlaylistDir, NULL))
        {
          _tcscpy(sInitialDir, m_sWmpPlaylistDir);
          ofn.lpstrInitialDir = sInitialDir;
        }
        else
        {
          fnSysErr(_T("Error creating directory:"), m_sWmpPlaylistDir,
                   _T("You can still try manually creating the folder,"),
                   _T("or save your playlist in a different folder."));
          ofn.lpstrInitialDir = NULL;
        }
      }
      else ofn.lpstrInitialDir = NULL; // User chose NO, let Windows choose initial dir

    } //  else, i.e. m_sWmpPlaylistDir doesn't exist
 
  }

// THIS ALWAYS SETS IT NULL IF USER PREF FOR LAST DIR MADE ABOVE if FALSE (if ...   && !sInitialDir[0]) 
//  else // i.e. Not using previous dir and can't save in WMP default. Punt to Windows handling.
//    ofn.lpstrInitialDir = NULL;

  // 3.2 Formerly OnInitDialog set My Documents if Playlists default not found; now similar here.
  if (!ofn.lpstrInitialDir)
  {
    // Give up, default to My Documents.
    LPCTSTR sKey = _T("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders");
    if (ReadReg(HKEY_CURRENT_USER, sKey, _T("Personal"), sBS, FALSE))
    {
      if (ExpandEnvironmentStrings(sBS, sBS2, 260))
      {
          _tcscpy(sInitialDir, sBS2);
          ofn.lpstrInitialDir = sInitialDir;
      }
    }
  }
  // If either of above failed, leave ofn.lpstrInitialDir NULL for Windows to handle

  ///// NOW GENERATE DEFAULT SAVE NAME AT TIME OF SAVE DIALOG ///////

  // Generate name based on preferences and 1st listview item
  GetDefaultSaveName(sOutPath);

// Win7 ignores lpstrInitialDir if it's same as it was 1st time program ran, uses last dir instead.
// http://social.msdn.microsoft.com/Forums/en-US/windowsuidevelopment/thread/7c6614ea-bef9-4421-9778-f8e42b95456a/
// Work around is to set full path in ofn.lpstrFile, with ofn.lpstrInitialDir
  if (ofn.lpstrInitialDir && m_bVistaOrLater // && ! ini->bTgtDirLast
      && ! _tcscmp(m_sWmpPlaylistDir, sInitialDir) // ! ini->bTgtDirLast or no ini->sTgtDirLast
      && sOutPath[0] && ! _tcschr(sOutPath, _T('\\')) ) // last 2 cond should always be true
  {
    // sOutPath was misleadingly named, actually just the file name
    _stprintf(sBS, _T("%s\\%s"), ofn.lpstrInitialDir, sOutPath);
    _tcscpy(sOutPath, sBS); // Only now does sOutPath have the full path
  }

  //////// RESUME NORMAL FLOW - BELOW PROBABLY OK AS IS/////////////////////////////////


  ofn.lpstrFile= sOutPath; // Selected filename will also be copied into here

	ofn.nMaxFile = MAX_PATH;
  BOOL bSaveNameVerified = FALSE;

  while (!bSaveNameVerified) // Only reloop if warn file name exists
  {
	  if (!GetSaveFileName(&ofn))
	  {
		  //if (CommDlgExtendedError() != 0)
      //  return fnSysErr("Error getting filename to save shuffled list");
		  // else return FALSE; // Was Cancel button, don't show message

      DWORD dwErr = CommDlgExtendedError();
		  if (!dwErr) return FALSE; // Was Cancel button, don't show message
      {
        if (dwErr == FNERR_INVALIDFILENAME)
        {
          fnErr(_T("Non-fatal error, illegal default save name:"),
                sOutPath,
                _T("Retrying without default save name"));
//          _tcscpy(sOutPath, _T("*.m3u"));
          _stprintf(sOutPath, _T("*.%s"), sDefaultExt);
          continue;
        }
        return fnSysErr(_T("Error getting filename to save shuffled list"));
      }
	  }

    if (!ParsePath(sOutPath, sOutDirSlash, sOutNameNoExt, sOutDotExt, FALSE)) // OK if no .ext
      return fnErr(_T("Error parsing directory and file name in OpenFile path:"),
                  sOutPath, sOutDirSlash); // Error msg in sOutDirSlash

// v3.22 Don't enforce any extension. OpenFile dlg appends correct extension unless quoted
    //if (_tcsicmp(ofn.lpstrDefExt, &sOutDotExt[1])) // Compare ext without dot
    //{
    //  BOOL bSetDefault; // Whether to reset extension
    //  // Accept output extension if either of .wpl or .m3u, even if not default
    //  bSetDefault = (_tcsicmp(sOutDotExt, sDotWpl)
    //                  && _tcsicmp(sOutDotExt, sDotM3u)
    //                  && _tcsicmp(sOutDotExt, sDotM3u8)); // Added version 3.0
    //  if (bSetDefault) // Is not acceptable extension. Append the default.
    //  {
    //    _tcscat(sOutPath, _T("."));  // lpstrDefExt doesn't include dot
    //    _tcscat(sOutPath, ofn.lpstrDefExt); // Whichever was in the type dropdown list
    //  }
    //} // if (ofn.Flags | OFN_EXTENSIONDIFFERENT)

    // Output save name has been set by user, plus possible extension correction
    // Now check if exists if warn overwrite setting is on
    if (!bWarnOverwrite) bSaveNameVerified = TRUE; // Break from loop without asking
    else
    {
      hFind = FindFirstFile(sOutPath, &fdIgnore);
      if (hFind == INVALID_HANDLE_VALUE) bSaveNameVerified = TRUE; // Doesn't exist
      else // Warn about overwrite
      {
        FindClose(hFind);
        nBS = MB(MB_YESNO | MB_ICONQUESTION | MB_DEFBUTTON2,
                sWarnExistsPt1, sOutPath, sWarnExistsAskOverWrite);
        if (nBS == IDYES) bSaveNameVerified = TRUE; // Said yes overwrite
        // else bSaveNameVerified remains FALSE, will reloop to OpenFile dialog
      } // else, i.e (hFind != INVALID_HANDLE_VALUE) i.e. would overwrite
    } // else, i.e. bWarnOverwrite TURE

  } // while (!bSaveNameVerified)

// v3.22 No longer need 2nd parse, since no longer add/fix extension above
 // // Do even though done above, b/c sOutPath may have corrected .ext for type
 // // sOutNameNoExt will be used by WritePlaylist for the wpl title
 // if (!ParsePath(sOutPath, sOutDirSlash, sOutNameNoExt, sOutDotExt))
 //   return fnErr(_T("Error parsing folder and file name in output path:"),
 //                sOutPath, sOutDirSlash); // Error msg in sOutDirSlash

 //// IF HERE, HAVE VALID OUTPUT PATH BY WHATEVER MEANS, WITH EITHER .wpl OR .m3u EXT

  // Version 3.0: Reset nPlistFmt to one of 3 values, as chosen by use in OpenFile dlg
  //if (!_tcsicmp(sOutDotExt, sDotWpl)) nPlistFmt = RB_FMT_WPL;
  //else if (!_tcsicmp(sOutDotExt, sDotM3u)) nPlistFmt = RB_FMT_M3U;
  //else  nPlistFmt = RB_FMT_M3U8;
// v3.22 Can't use sOutDotExt for format, because both m3u UTF-8 & ANSI use .m3u
  nPlistFmt = gnLastIndex; // Set by OPENFILENAME hook procedure
  if (!ini->SetAndSaveKey(ini->nPlaylistFmtLast, nPlistFmt))
    fnErr(_T("Non-fatal error recording last format used"));

  //ToLog(_T("Output path parsed"));

  // Record last folder saved in, whether or not option to use as default
  _tcscpy(sBS, sOutDirSlash);
  sBS[_tcslen(sOutDirSlash) - 1] = cNull; // Trim off slash
  if (!ini->SetAndSaveKey(ini->sTgtDirLast, sBS))
    fnErr(_T("Non-fatal error recording last directory used"));
 
  // END INSERT OPENFILE (OUTPUT)

  // IF HERE, WILL PROCEED TO FINISH WRITING SHUFFLED PLAYLIST UNLESS ERROR

  zPeekMessage();

  if (ini->bLog && !StartLog(_T("last shuffle log.txt"), _T("Start OnShuffle"),
                             ini->bLogFlush))
    return fnErr(_T("StartLog failed"), m_sError);

  // SET UP FOR PROGRESS BAR - just to see it's going, estimates are crude


  //SetWindowText(m_hProgText, "Reading and parsing original playlist file ...");
  SetWindowText(m_hProgText, _T("Reading and calculating list selection ..."));
  EnableControls(FALSE);  // Undo in OnBnShuffle;
  SendMessage(m_hProgress, PBM_SETPOS, 0, 0); // in default range 0 to 100
  m_bAbort = FALSE;

// v.3 Don't need sarrMain yet, allocate later
  //CSongArray sarrMain(this);
  //ToLog(_T("Main song array allocated"));

  //LPTSTR spWplFileHead = NULL; // These two will remain null unless bOutWpl
  //LPTSTR spWplFileTail = NULL;

  ToLog(_T("Start read ListView rows"));

//  DWORD dw1StartParseWpl = GetTickCount();   // Start of gather LV data
  DWORD dw1StartReadLV = GetTickCount();   // Start of gather LV data


  //// Formerly v.1 had block here: Read the file and load to CSongArray


  // v.2 HAD REPLACED ABOVE BLOCK WITH LOADING ALL LISTVIEW ROWS INTO sarrMain

  /*******************************************************************************
    BEGIN V.3 REPLACEMENT FOR V.2 
  ********************************************************************************/

  INT nRows = (INT) ListView_GetItemCount(m_hList); // Verified > 0 in OnBnShuffle

  // Counts of types of selections, add up to nRows.
  // No longer separate count for New Songs, now optional filter in all 3 types.
  INT nSelAll = 0;
  INT nSelLists = 0; // Playlist(s) and/or Genre(s)
  INT nSelExclude = 0;

  //LVITEM lvi;
  //lvi.iSubItem = 0;      // Always
  //lvi.mask = LVIF_PARAM; // For loop below, change afterward

  // First read of ListView rows just to get counts
  for (INT nThisRow = 0; nThisRow < nRows; nThisRow++)
  {
    //lvi.iItem = nThisRow;

    CSongsSelection* pSongSel = GetSongSelForLvIx(nThisRow);
    if (!pSongSel) return FALSE; // fnErr msg already from GetSongSelForLvIx

    if (pSongSel->IsExclude()) nSelExclude ++; // Can exclude All Songs WITH age limit
    else if (pSongSel->IsAllSongs()) nSelAll ++;
    else nSelLists ++;

    // Maybe also collect estimates of each selection's work load, for prog bar?
    // e.g. use constants like
    // WORK_ALL_SONGS
    // WORK_ONE_GENRE
    // WORK_ONE_PLIST
    // WORK_MULTIPLIER_NEW  i.e. some value greater than 1, to multiply any of 3 above
  } // for (INT nThisRow = 0; nThisRow < nRows; nThisRow++)

  if (nSelExclude == nRows)
    return fnErr(_T("Nothing to shuffle.\nAll song selections are exclusions."));

  zPeekMessage();
  LPCTSTR sUserCancelMsg = _T("Processing stopped by user Cancel"); 
  if (m_bAbort) return fnErr(sUserCancelMsg);
  
  // If caching thread not finished yet, tell it to stop, no point now.
  if (!m_bCacheFinished)
  {
    m_bStopCaching = TRUE;
    for (INT i = 0; i < 20 && !m_bCacheFinished ; i++)
    {
      Sleep(100);
      zPeekMessage();
    }
    if (!m_bCacheFinished) // Still, after 2+ seconds for caching thread to stop
      return fnErr(_T("Unable to stop caching thread to start with real shuffle"),
                   _T("It may have finished on its own, so shuffle")
                   _T( " might work if you try again now."));
  }

  // Will add up absolute time estimates to share 95% of progress bar, then
  // use total as denominator allocating portion of progress bar to each step.
  INT nProgUnitsShuffle = (nShuffleType == RB_REPEAT_IGNORE) ? PROG_TIME_SHUFFLE_NORMAL
                                                             : PROG_TIME_SHUFFLE_NO_SEP;

  INT nTotalProgUnits = PGRS_TIME_READ_LVIEW
                        + (m_cpWmpPlaylistAll.p ? 0 : PGRS_TIME_LOAD_WMP)
                        + (nRows * PGRS_TIME_ONE_LV_ROW)
                        + PGRS_TIME_WORKARR_TO_MAIN
                        + (nSelExclude ? PGRS_TIME_FILTER_MAIN : 0)
                        + nProgUnitsShuffle;

  // Multiplying a step's PROG_TIME_... by this gets pct of total progress bar
  FLOAT fProgPctMultiplier = 95 / ((FLOAT) nTotalProgUnits);

  // Version 1 calculations of shuffle progress assume shuffle is 90% of total
  // Multiplier to adjust those calculations (used by "more truly random" shuffle)
  FLOAT fShufflePctMultiplier = (fProgPctMultiplier * (FLOAT) nProgUnitsShuffle) / 90;

  // Reuse nTotalProgUnits for cumulative progress
  nTotalProgUnits = PGRS_TIME_READ_LVIEW;
  m_nProgPos = (INT) (fProgPctMultiplier * (FLOAT) nTotalProgUnits);
// v.3 Keep v.2 above, undo change below
//  // v.3 Keep more accurate cumulative total as float, no longer use nTotalProgUnits
//  FLOAT fTotalPgrsUnitsDone = PGRS_TIME_READ_LVIEW;
//  m_nProgPos = (INT) (fProgPctMultiplier * fTotalPgrsUnitsDone);
	m_nProgLastPos = m_nProgPos;
  SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);
  zPeekMessage();
  if (m_bAbort) return fnErr(sUserCancelMsg);

  // V.3: This block about loading m_cpWmpPlaylistAll copied as is from commented v.2
  // block above. Don't remember why it's like this, so don't mess with it.
  if (!m_cpWmpPlaylistAll.p)
  {
    SetWindowText(m_hProgText,
      _T("Getting list of all songs from Windows Media Player ..."));

    // Now the point of this if block
    LPCTSTR sErrIntro = _T("Error getting list of all songs in WMP");
    TCHAR sErrMsg[2048];
    HRESULT hr = S_OK; // So SUCCEEDED(hr) if don't need to load

    // Know that m_cpWmpPlaylistAll.p is NULL, this might be too
    if (!m_cpWmpAllMedia.p)
    {
      ToLog(_T("Begin get_mediaCollection for all songs"));
      hr = m_cpPlayer->get_mediaCollection(&m_cpWmpAllMedia);
      if (!SUCCEEDED(hr)) _tcscpy(sErrMsg, _T("get_mediaCollection failed"));
    }

    CComBSTR cbsAttribute, cbsValue;
    cbsAttribute = _T("MediaType");
    cbsValue =_T("audio");
    if (SUCCEEDED(hr) && !m_cpWmpPlaylistAll.p)
    {
      zPeekMessage();
      if (m_bAbort) return fnErr(sUserCancelMsg);
      ToLog(_T("Begin get all songs playlist"));
      hr = m_cpWmpAllMedia->getByAttribute(cbsAttribute, cbsValue,
                                          &m_cpWmpPlaylistAll);
      if (!SUCCEEDED(hr)) _tcscpy(sErrMsg, _T("getByAttribute failed"));
    }

    // OK so far?
    if (!SUCCEEDED(hr))
    {
      if (m_cpWmpPlaylistAll.p) m_cpWmpPlaylistAll.Release();
      if (m_cpWmpAllMedia.p) m_cpWmpAllMedia.Release();
      return fnErr(sErrIntro, sErrMsg);
    }

    nTotalProgUnits += PGRS_TIME_LOAD_WMP;
    m_nProgPos = (INT) (fProgPctMultiplier * (FLOAT) nTotalProgUnits);
    m_nProgLastPos = m_nProgPos;
    SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);
    zPeekMessage();
    if (m_bAbort) return fnErr(sUserCancelMsg);

    ToLog(_T("Finished get m_cpWmpPlaylistAll"));
  } // if (bNeedToLoadPlaylistAll)

  // Allocate work song arrays to collect songs same way regardless of shuffle method.
  // Later, shuffle method will determine how copied to sarrMain
  CSongArray * psarrAdd = new CSongArray(this);
  if (!psarrAdd) return fnErr(_T("Error allocating working song array"));

  CSongArray * psarrExclude = NULL;
  if (nSelExclude) // else no use for exclusion song array
  {
    psarrExclude = new CSongArray(this);
    if (!psarrExclude)
    {
      delete psarrAdd;
      return fnErr(_T("Error allocating exclusion song array"));
    }
  }

  // Second read of ListView rows to add to song arrays
  // Now in v.3, just go through them in whatever order they're in
  TCHAR sProgTxt[1024];
  FLOAT fProgPctOneLvRow = fProgPctMultiplier * PGRS_TIME_ONE_LV_ROW; // each loop cycle

  
  _stprintf(sBS, _T("Begin reading %d song selections. Progress pct 1 row = %f"),
            nRows, fProgPctOneLvRow);
  ToLog(sBS);


  for (INT nThisRow = 0; nThisRow < nRows; nThisRow++)
  {
    _stprintf(sProgTxt, _T("Reading songs selection %d of %d"), nThisRow + 1, nRows);
    SetWindowText(m_hProgText, sProgTxt);

    CSongsSelection* pSongSel = GetSongSelForLvIx(nThisRow);
    if (!pSongSel) return FALSE; // fnErr msg already from GetSongSelForLvIx

    // 3.2 Start with empty temp song array each loop, so dedup doesn't apply across selections.
    CSongArray sarrThisLoop(this); // scope expires at end of loop
    FLOAT fProgPctThisCall; // Will be half previous amount unless selection is exclude;

    //if (pSongSel->IsExclude()) nSelExclude ++; // Can include All Songs WITH age limit
    //else if (pSongSel->IsAllSongs()) nSelAll ++;
    //else nSelLists ++;

    CSongArray * psarrTarget; // Where to copy this selection's songs to
    BOOL bDedup;

    if (pSongSel->IsExclude())
    {
      psarrTarget = psarrExclude;
      //bDedup = FALSE; // 3.2 Why was this false? For exclude, extra reps make no difference.
      bDedup = TRUE;
      fProgPctThisCall = 1; // 3.2 - Will subsequently multiply by old progress amount
    }
    else
    {
      //psarrTarget = psarrAdd;
      psarrTarget = &sarrThisLoop; // 3.2
      bDedup = pSongSel->IsDedup();
      fProgPctThisCall = (FLOAT) 0.7; // 3.2 - Will subsequently multiply by old progress amount
    }

    INT nMaxAge = pSongSel->GetMaxAge();

    if (pSongSel->IsAllSongs())
    {
      fProgPctThisCall *= fProgPctOneLvRow; // 3.2
      // Only one "playlist" to add. Depend on SelectDlg validation to ensure
      // this isn't used with exclusion option unless a max age is also set.
      if (!AddToSongArrayWrapper(*psarrTarget, m_cpWmpPlaylistAll,
                                 pSongSel->GetRepCounts(), bDedup,
//                                 fProgPctOneLvRow, SONGARR_INCREMENT,
                                 fProgPctThisCall, SONGARR_INCREMENT, // 3.2
                                 sProgTxt, pSongSel->GetMaxAge()))
      {
        delete psarrAdd;
        if (psarrExclude) delete psarrExclude;
        return FALSE; // Already fnErr message from AddToSongArrayWrapper
      }
      // Don't update progress bar here, done at bottom of "for (INT nThisRow" loop
    } // if (pSongSel->IsAllSongs())
    else
    {
      // Have playlist(s) and/or genre(s)
      // Separate call to AddToSongArrayWrapper for each
      // First get counts of genres and playlists
      INT nGenres = pSongSel->GetGenreNamesCount();
      INT nPlaylists = pSongSel->GetPlistIndexesCount();

      if (!(nGenres + nPlaylists)) // Should never happen with SelectDlg checks
      {
        delete psarrAdd;
        if (psarrExclude) delete psarrExclude;
        return fnErr(_T("Song selection of genres and playlists contains none"));
      }

      // Variables for setting progress bar after each genre or playlist.
      // Any approximation errors setting m_nProgPos in this "else" block will be
      // overwritten when progress bar is updated at finish of this ListView row.
      // fProgPctOneItem is also used in calls to AddToSongArrayWrapper, but may
      // be too small for any progress bar updates to occur during that.
      FLOAT fProgPctOneItem = fProgPctOneLvRow / ((FLOAT) (nGenres + nPlaylists));
      FLOAT fProgPctNow = (FLOAT) m_nProgPos; // Will update after each item
      fProgPctThisCall *= fProgPctOneItem; // 3.2

      // Either of nGenres or nPlaylists might be 0, but not both
      if (nGenres)
      {
        LPCTSTR * spArr_GenreNames = pSongSel->GetGenreNamesArray();
        for (INT i = 0; i < nGenres; i++)
        {
          CComBSTR cbsGenreName = spArr_GenreNames[i];
          CComPtr<IWMPPlaylist> cpThisGenrePlaylist;
          cpThisGenrePlaylist.p = NULL;
          HRESULT hr = m_cpWmpAllMedia->getByGenre(cbsGenreName, &cpThisGenrePlaylist);

          // For individual item error, skip non-fatal
          if (SUCCEEDED(hr) && cpThisGenrePlaylist.p)
          {
            if (!AddToSongArrayWrapper(*psarrTarget, cpThisGenrePlaylist,
                                      pSongSel->GetRepCounts(), bDedup,
//                                      fProgPctOneItem, SONGARR_INCREMENT,
                                      fProgPctThisCall, SONGARR_INCREMENT, // 3.2
                                      sProgTxt, pSongSel->GetMaxAge()))
            {
              delete psarrAdd;
              if (psarrExclude) delete psarrExclude;
              cpThisGenrePlaylist.Release();
              return FALSE; // Already fnErr message from AddToSongArrayWrapper
            }

            cpThisGenrePlaylist.Release();

            fProgPctNow += fProgPctOneItem;
            m_nProgPos = (INT) fProgPctNow;
            if (m_nProgPos > m_nProgLastPos)
            {
              m_nProgLastPos = m_nProgPos;
              SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);
            }
            // else adding fProgPctOneItem hasn't increased m_nProgPos to
            // next whole percentage number, no poing updating progress bar.

          } // if (SUCCEEDED(hr) && cpThisGenrePlaylist.p)

        } // for (INT i = 0; i < nGenres; i++)
      } // if (nGenres)

      if (nPlaylists)
      {
        const INT * nArr_PlistIndexes = pSongSel->GetPlistIndexesArray();
        for (INT i = 0; i < nPlaylists; i++)
        {
          CComPtr<IWMPPlaylist> cpThisPlaylist;
          cpThisPlaylist.p = NULL;
//          HRESULT hr = m_cpWmpPlaylistArr->item((long) i, &cpThisPlaylist);
          HRESULT hr = m_cpWmpPlaylistArr->item((long) nArr_PlistIndexes[i],
                                                &cpThisPlaylist);

          // For individual item error, skip non-fatal
          if (SUCCEEDED(hr) && cpThisPlaylist.p)
          {
            if (!AddToSongArrayWrapper(*psarrTarget, cpThisPlaylist,
                                      pSongSel->GetRepCounts(), bDedup,
//                                      fProgPctOneItem, SONGARR_INCREMENT,
                                      fProgPctThisCall, SONGARR_INCREMENT, // 3.2
                                      sProgTxt, pSongSel->GetMaxAge()))
            {
              delete psarrAdd;
              if (psarrExclude) delete psarrExclude;
              cpThisPlaylist.Release();
              return FALSE; // Already fnErr message from AddToSongArrayWrapper
            }

            cpThisPlaylist.Release();

             // ==> Add set progress bar here after each AddToSongArrayWrapper
          } // if (SUCCEEDED(hr) && cpThisGenrePlaylist.p)

        } // for (INT i = 0; i < nPlaylists; i++)
      } // if (nPlaylists)

    } // else, i.e. (! pSongSel->IsAllSongs()), is genre(s) and/or playlist(s)

    // 3.2 This loop's selection now in a temp array, unless this is an exclusiont selection
    if (! pSongSel->IsExclude())
    {
      // Update psarrAdd with songs added this loop.
      // Mechanism like copying to sarrMain after loop, but don't bother updating progress
      // bar. Let end of loop progress update add remaining 30% of this loop's progress.
      INT nSongsWork = sarrThisLoop.GetSize();
      for (INT nSongWorkIx = 0; nSongWorkIx < nSongsWork; nSongWorkIx++)
      {
        INT nReps = sarrThisLoop.GetCountOneSong(nSongWorkIx);
        LPCTSTR sSongPath = sarrThisLoop.GetSong(nSongWorkIx);
        psarrAdd->IncrementOrAddSong(sSongPath, nReps);
      }
    } // if (! pSongSel->IsExclude())
    // else already added directly to exclusion list above

    // Set progress bar here after each ListView row. In calculation, ignore any
    // prior partial calculations within the row, may have INT truncation errors.
    nTotalProgUnits += PGRS_TIME_ONE_LV_ROW;
    m_nProgPos = (INT) (fProgPctMultiplier * (FLOAT) nTotalProgUnits);
    m_nProgLastPos = m_nProgPos;
    SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);

    _stprintf(sBS, _T("Added selection %d of %d.  m_nProgPos = %d"),
              nThisRow, nRows, m_nProgPos);
    ToLog(sBS);

    zPeekMessage();
    if (m_bAbort) return fnErr(sUserCancelMsg);

  } // for (INT nThisRow = 0; nThisRow < nRows; nThisRow++)

  // No progress bar update here, was set correctly for last ListView row



  // VERSION 3.3 2016
  // BEGIN ADD GLOBAL MAXIMUM REPEATS OPTION ////////////////////////////////////////////////

  if (IsBnChecked(IDC_CK_MAX_REP))
  {
    INT nMaxReps = GetDlgItemInt(m_hWnd, IDC_ED_MAX_REP, NULL, FALSE);
    if (nMaxReps)
    {
      INT nSongs = psarrAdd->GetSize();
      for (INT nSongIx = 0; nSongIx < nSongs; nSongIx++)
      {
        if (psarrAdd->GetCountOneSong(nSongIx) > nMaxReps)
        {
          if (!psarrAdd->SetCountOneSong(nSongIx, nMaxReps))
          {
            _stprintf(sBS, _T("Error reducing frequency of song to maximum %d"), nMaxReps);
            INT nRet = MB(MB_OKCANCEL | MB_ICONEXCLAMATION, sBS, psarrAdd->GetError(),
              _T("Keep original frequencies and proceed anyway? Press Cancel to stop"));

            if (nRet == IDCANCEL) return FALSE;
            else break; // from loop of songs in work array, drop through to proceed with shuffle
          }
        } // if (psarrAdd->GetCountOneSong(nSongIx) > nMaxReps
      }
    }
    else // nMaxReps 0 either entered somehow or result of GetDlgItemInt failure
    {
      // Probably something failed preventing user input of 0 or blank in the edit field.
      // Don't try to figure it out, won't happen again without user doing something strange.
      INT nRet = MB(MB_OKCANCEL | MB_ICONEXCLAMATION,
                    _T("Invalid value in maximum frequencies input field - must be 1 to 99"),
                    _T("Keep original frequencies and proceed anyway? Press Cancel to stop"));

      if (nRet == IDCANCEL) return FALSE;
      // else drop through to proceed with shuffle
    }
  } // if (IsBnChecked(IDC_CK_MAX_REP))
  // else no change if button is unchecked

  // VERSION 3.3 2016
  // END ADD GLOBAL MAXIMUM REPEATS OPTION ////////////////////////////////////////////////



  CSongArray sarrMain(this);  // v.3 Allocation moved down here from earlier
  ToLog(_T("Main song array allocated"));

  SetWindowText(m_hProgText, _T("Creating list of all selected songs for shuffle"));

  // V.3 Copy working song array to sarrMain, and maybe filter with exclusion array.
  INT nSongsWork = psarrAdd->GetSize();
  for (INT nSongWorkIx = 0; nSongWorkIx < nSongsWork; nSongWorkIx++)
  {
    // ==> BEGIN INSERT PROGRESS UPDATE EVERY 10th SONG
    if (! (nSongWorkIx % 10) && nSongWorkIx)
    {
      // Modified version of set progress after end of this loop
//      nTotalProgUnits += PGRS_TIME_WORKARR_TO_MAIN;
      // Don't change nTotalProgUnits inside this loop
      FLOAT fTotalProgUnits = (FLOAT) nTotalProgUnits
                              + ( PGRS_TIME_WORKARR_TO_MAIN
                                  * (FLOAT) (nSongWorkIx + 1)
                                  / (FLOAT) nSongsWork );
//      m_nProgPos = (INT) (fProgPctMultiplier * (FLOAT) nTotalProgUnits);
      m_nProgPos = (INT) (fProgPctMultiplier * fTotalProgUnits);
      m_nProgLastPos = m_nProgPos;
      SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);
      zPeekMessage();
      if (m_bAbort) return fnErr(sUserCancelMsg);
    }
    // ==> END INSERT PROGRESS UPDATE EVERY 10th SONG

    LPCTSTR sSongPath = psarrAdd->GetSong(nSongWorkIx);

//    if (psarrExclude) // DOES COMPILER OPTIMIZE THESE TWO SEPARATE IF BLOCKS?
//    {
//      // List of songs to exclude exists. Skip this song if it's in the
//      // exclusion list, i.e. if there's an array index for its path.
//      if (psarrExclude->GetSongIndex(sSongPath) > -1)
//        continue;
//    }
    if (psarrExclude // List of songs to exclude exists.
        && psarrExclude->GetSongIndex(sSongPath) > -1) // This song is in exclusion list
      continue; // Skip this song as if it wasn't in the selection


    INT nReps = psarrAdd->GetCountOneSong(nSongWorkIx);
    BOOL bRet = TRUE; // Init for RB_REPEAT_SEP case
    if (nShuffleType == RB_REPEAT_DIST)
    {
      bRet = sarrMain.IncrementOrAddSong(sSongPath, nReps);
    }
    else // RB_REPEAT_SEP or its "virtual" RB_REPEAT_IGNORE
    {
      // This uses the old v.1 method, where each repetition
      // of a song is a separate array member.
//      for (INT i = 0; bRet && i <= nReps; i++) // <== IS THIS THE BUG? ONE EXTRA REP EACH SONG
      for (INT i = 0; bRet && i < nReps; i++)
      {
        bRet = sarrMain.AddSong(sSongPath, 1);
      }
    } // else, i.e. RB_REPEAT_SEP or its "virtual" RB_REPEAT_IGNORE
  } // for (INT nSongWorkIx = 0; nSongWorkIx < nSongsWork; nSongWorkIx++)

  nTotalProgUnits += PGRS_TIME_WORKARR_TO_MAIN;
  m_nProgPos = (INT) (fProgPctMultiplier * (FLOAT) nTotalProgUnits);
  m_nProgLastPos = m_nProgPos;
  SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);
  zPeekMessage();
  if (m_bAbort) return fnErr(sUserCancelMsg);


  /*******************************************************************************
    END V.3 REPLACEMENT FOR V.2 DELETED BLOCK
  ********************************************************************************/


  // Value used by various shuffle calls below
  INT nProgPctThruRead = m_nProgPos;

 // // These used for "continuous" progress calculations to know when to advance
	//m_nProgPos = PROG_PCT_READ;
	//m_nProgLastPos = PROG_PCT_READ;
	//m_nProgPos = nProgPctThruRead;
	//m_nProgLastPos = m_nProgPos;

 // SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);

  DWORD dw2StartShuffleArr1 = GetTickCount();

//V2  UINT uSongs = sarrMain.GetSize();  // was same as GetListLength when all nCount=1
  UINT uSongs = sarrMain.GetListLength();

  UINT * upSongIndArr = new UINT[uSongs];
  if (!upSongIndArr) return fnErr(_T("Error allocating song index array"));

  _stprintf(sBS, _T("Begin shuffling song index array with %u members"), uSongs);
  ToLog(sBS);

  MSG msg;
  // Check for any unprocessed window messages
  while (PeekMessage(&msg , NULL, NULL, NULL, PM_REMOVE))
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

  // Just in case it happened in brief time since ParseWpl last checked:
// Define earlier  LPCTSTR sUserCancelMsg = "Processing stopped by user Cancel"; 
  if (m_bAbort) return fnErr(sUserCancelMsg);

  DWORD dw3StartShuffleArr2 = 0; // Only used in if != RB_REPEAT_DIST block

  BOOL bSepDups = TRUE; // Unless set false below
  TCHAR sErrorSepDups[8192];

  // For now, just modify old calculations. Later integrate with new logic
  nProgPctEachShuffle = (INT) (fShufflePctMultiplier
                               * ((FLOAT) nProgPctEachShuffle + 0.5));

  if (nShuffleType == RB_REPEAT_DIST)
  {
    INT nFailures = 0;
    SetWindowText(m_hProgText, _T("Shuffling songs and distributing repeats ..."));
//V2    if (!DistributeList(&sarrMain, uSepThisShuffle, (INT *) upSongIndArr,
    if (!DistributeList(sarrMain, uSepThisShuffle, (INT *) upSongIndArr,
//                        nProgPctEachShuffle, &nFailures, bOutWpl))
                        nProgPctEachShuffle, &nFailures)) // , FALSE))
    {
      if (upSongIndArr) delete [] upSongIndArr;
      //if (spWplFileHead) delete [] spWplFileHead;
      //if (spWplFileTail) delete [] spWplFileTail;

      if (m_bAbort) return fnErr(sUserCancelMsg); // Why it returned false
      else return fnErr(_T("Error shuffling song array"), m_sError);
    }
    else // returned TRUE, but check for non-fatal song not separated enough
    {
      if (nFailures)
      {
        bSepDups = FALSE;
        _stprintf(sErrorSepDups,
                  _T("Non fatal error (playlist is still usable):\n")
                  _T("%d repeated songs were not separated as far as requested.\n\n")
                  _T("Details of last song not sufficiently separated:\n%s"),
                   nFailures, m_sError);
        // and continus as normal
      }
    } // if (nShuffleType == RB_REPEAT_DIST)
  } // 
  else // either RB_REPEAT_SEP or RB_REPEAT_IGNORE, same shuffle mechanism
  {
    SetWindowText(m_hProgText, _T("Generating random order (stage 1) ..."));

    // List in original order to shuffle
    for (UINT u = 0; u < uSongs; u++) upSongIndArr[u] = u;

    if (!fnShuffleNumArray(upSongIndArr, uSongs, nProgPctEachShuffle, // PROG_PCT_EA_ARR_SHUFFLE,
//                          PROG_PCT_READ))
                           nProgPctThruRead))
    {
      if (upSongIndArr) delete [] upSongIndArr;
      //if (spWplFileHead) delete [] spWplFileHead;
      //if (spWplFileTail) delete [] spWplFileTail;

      if (m_bAbort) return fnErr(sUserCancelMsg); // Why it returned false
      else return fnErr(_T("Error shuffling song index array method 1"), m_sError);
    }

    dw3StartShuffleArr2 = GetTickCount();

    ToLog(_T("Finished shuffle by method 1, start shuffle by method 2."));

    // In case still have trunctated progress fractions from fnShuffleNumArray
//	  m_nProgPos = PROG_PCT_READ + nProgPctEachShuffle; // PROG_PCT_EA_ARR_SHUFFLE;
	  m_nProgPos = nProgPctThruRead + nProgPctEachShuffle;
	  m_nProgLastPos = m_nProgPos;
    SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);
    SetWindowText(m_hProgText, _T("Generating random order (stage 2) ..."));

    // Check for any unprocessed window messages
    while (PeekMessage(&msg , NULL, NULL, NULL, PM_REMOVE))
    {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }

    if (!fnShuffleNumArrayByExch(upSongIndArr, uSongs, nProgPctEachShuffle, //PROG_PCT_EA_ARR_SHUFFLE,
//                                PROG_PCT_READ + nProgPctEachShuffle)) //PROG_PCT_EA_ARR_SHUFFLE))
                                 nProgPctThruRead + nProgPctEachShuffle))
    {
      if (upSongIndArr) delete [] upSongIndArr;
      //if (spWplFileHead) delete [] spWplFileHead;
      //if (spWplFileTail) delete [] spWplFileTail;

      if (m_bAbort) return fnErr(sUserCancelMsg); // Why it returned false
      else fnErr(_T("Error shuffling song index array method 2"), m_sError);
    }
  } // else, i.e. (nShuffleType != RB_REPEAT_DIST)

  // These need scope outside below if block, for later checks on time
  DWORD dw4StartSepDups = 0;

  // If here, repeat option is either separate or do nothing
  if (nShuffleType != RB_REPEAT_SEP)
  {
    ToLog(_T("Finished shuffling song index array. Start writing output file"));
    // Will SetWindowText for writing output file after skipping else block below
  }
  else
  {
    dw4StartSepDups = GetTickCount();

    ToLog(_T("Finished shuffling song index array. Start separating repeats"));
    SetWindowText(m_hProgText, _T("Moving songs that repeat too soon ..."));

//	  m_nProgPos = PROG_PCT_READ + (2 * PROG_PCT_EA_ARR_SHUFFLE);
	  m_nProgPos = nProgPctThruRead
                 + (INT) (fShufflePctMultiplier
                          * (FLOAT) (2 * PROG_PCT_EA_ARR_SHUFFLE));
	  m_nProgLastPos = m_nProgPos;
    SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);

    // Check for any unprocessed window messages
    while (PeekMessage(&msg , NULL, NULL, NULL, PM_REMOVE))
    {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }

    if (ini->bLog)
    {
      TCHAR sDupsNotSeparated[MAX_PATH];
//      _stprintf(sDupsNotSeparated, _T("%sPlaylist b4 move repeats.txt"), m_sOwnDirSlash);
      _stprintf(sDupsNotSeparated, _T("%sPlaylist b4 move repeats.txt"), m_pzwd->sAppDocsDirSlash);

//      // Writes .wpl format if spWplFileHead and spWplFileTail not null, else .m3u
      // Even if truncating, write full pre-separation playlist
      if (!WritePlaylist(sDupsNotSeparated, sarrMain, upSongIndArr,
//                         (bSrcWpl && !bOutWpl), -1, spWplFileHead, spWplFileTail))
                         //bIsM3u,
                         nPlistFmt, // Version 3.0 for 3 options (add .m3u8)
                         -1, sOutNameNoExt,
                         NULL)) // v3.22 NULL to force full paths regardless of ini->bSongRelativePath
      {
        if (upSongIndArr) delete [] upSongIndArr;
        //if (spWplFileHead) delete [] spWplFileHead;
        //if (spWplFileTail) delete [] spWplFileTail;

        return fnErr(_T("Error writing comparison (repeats not moved) shuffled playlist file"),
                    m_sError);
      }
    } // if (ini->bLog)

    bSepDups = SeparateDups(sarrMain, upSongIndArr, uSepThisShuffle);
    if (!bSepDups)
    {
      if (m_bAbort)
      {
        if (upSongIndArr) delete [] upSongIndArr;
        //if (spWplFileHead) delete [] spWplFileHead;
        //if (spWplFileTail) delete [] spWplFileTail; 

        return fnErr(sUserCancelMsg); // Why it returned false
      }
      // Else nonfatal error failed to separate, report after write
      _tcscpy(sErrorSepDups, m_sError);
    }

    ToLog(_T("Finished separating repeats. Start writing shuffled file"));
  } // else i.e. nShuffleType == RB_REPEAT_SEP

  DWORD dw5StartWrite = GetTickCount();

  SetWindowText(m_hProgText, _T("Writing shuffled playlist file to disk ..."));

//	m_nProgPos = PROG_PCT_READ + (2 * PROG_PCT_EA_ARR_SHUFFLE)
//               + PROG_PCT_SEP_DUPS_PT1 + PROG_PCT_SEP_DUPS_PT2;
  nBS = (2 * PROG_PCT_EA_ARR_SHUFFLE)
        + PROG_PCT_SEP_DUPS_PT1 + PROG_PCT_SEP_DUPS_PT2;
	m_nProgPos = nProgPctThruRead
               + (INT) (fShufflePctMultiplier * (FLOAT) nBS);
	m_nProgLastPos = m_nProgPos;
  SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);

  // Check for any unprocessed window messages
  while (PeekMessage(&msg , NULL, NULL, NULL, PM_REMOVE))
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

  TCHAR sTempPathPlaylist[_MAX_PATH];
//  _stprintf(sTempPathPlaylist, _T("%splaylist temp final.txt"), m_sOwnDirSlash);
  _stprintf(sTempPathPlaylist, _T("%splaylist temp final.txt"), m_pzwd->sAppDataDirSlash); // version 3.0

  // Add v. 2.1 handling for two possible output size limits
  INT nMaxSizeEffective;
//  BOOL bBothLimitsChecked = FALSE; // Unless reset below.
  BOOL bUseMaxLimit; // Only used if both limits checked

  BOOL bLimitNoReps = IsBnChecked(IDC_CK_TRUNC_NOREP)
               && (nShuffleType == RB_REPEAT_DIST); // Not for RB_REPEAT_SEP

  if (!bLimitNoReps) nMaxSizeEffective = nMaxSizeFixed; // Can be -1, unchecked
  else
  {
    // If absolute limit not checked, shortest distributed reps seg size applies
    if (nMaxSizeFixed == -1) nMaxSizeEffective = m_nMinSegmentSize;
    else
    {
      // Both limits checked
      bUseMaxLimit = IsBnChecked(IDC_RB_TRUNC_MAX);
      if (bUseMaxLimit)
        nMaxSizeEffective = max(nMaxSizeFixed, m_nMinSegmentSize);
      else nMaxSizeEffective = min(nMaxSizeFixed, m_nMinSegmentSize);
    }

    if (ini->bLog)
    {
      _stprintf(sBS, _T("Output limit values, truncate to prevent duplicates:")
                     _T("\r\nnMaxSizeFixed = %d  m_nMinSegmentSize = %d  ")
                     _T("nMaxSizeEffective = %d"),
                nMaxSizeFixed, m_nMinSegmentSize, nMaxSizeEffective);
      ToLog(sBS);
    }
  }


  BOOL bWriteRet = WritePlaylist(sTempPathPlaylist, sarrMain, upSongIndArr,
                                 // bIsM3u,
                                 nPlistFmt, // Version 3.0 for 3 options (add .m3u8)
                                 nMaxSizeEffective,
                                 sOutNameNoExt, // Arg ignored unless .wpl
                                 sOutDirSlash); // Arg ignored if !ini->bSongRelativePath
  // If returned false, message in m_sError.

  
  // First clean up regardless  of WritePlaylistreturn
  if (upSongIndArr) delete [] upSongIndArr;

  if (!bWriteRet) return fnErr(_T("Error writing shuffled playlist file"), m_sError);

  DWORD dw6FinishWrite = GetTickCount();
  DWORD dwTotalTime = dw6FinishWrite - dw1StartReadLV;

  if (m_hfLog)
  {
    DWORD dwDivisor = 2 * dwTotalTime; // Kludge for rounding percentages with ints
    if (nShuffleType == RB_REPEAT_SEP)
    {
      _stprintf(sBS, _T("Finished writing shuffled file.\r\n")
          _T("%5u millisecs (%02u%%) reading source playlists/groups\r\n")
          _T("%5u millisecs (%02u%%) array shuffle 1\r\n")
          _T("%5u millisecs (%02u%%) array shuffle 2\r\n")
          _T("          %u millisecs (%02u%%) shuffle total\r\n")
          _T("%5u millisecs (%02u%%) separating repeats\r\n")
          _T("%5u millisecs (%02u%%) writing file\r\n")
          _T("%5u millisecs TOTAL\r\n"),
          dw2StartShuffleArr1 - dw1StartReadLV,
            ((200 * (dw2StartShuffleArr1 - dw1StartReadLV)) + dwTotalTime)
                                                                       / dwDivisor,
          dw3StartShuffleArr2 - dw2StartShuffleArr1,
            ((200 * (dw3StartShuffleArr2 - dw2StartShuffleArr1)) + dwTotalTime)
                                                                       / dwDivisor,
          dw4StartSepDups - dw3StartShuffleArr2,
            ((200 * (dw4StartSepDups - dw3StartShuffleArr2)) + dwTotalTime)
                                                                       / dwDivisor,
                  dw4StartSepDups - dw2StartShuffleArr1,
                    ((200 * (dw4StartSepDups - dw2StartShuffleArr1))
                                                      + dwTotalTime) / dwDivisor,
          dw5StartWrite - dw4StartSepDups,
            ((200 * (dw5StartWrite - dw4StartSepDups)) + dwTotalTime) / dwDivisor,
          dw6FinishWrite - dw5StartWrite,
            ((200 * (dw6FinishWrite - dw5StartWrite)) + dwTotalTime) / dwDivisor,
          dwTotalTime);
    }
    else if (nShuffleType == RB_REPEAT_IGNORE)
    {
      _stprintf(sBS, _T("Finished writing shuffled file.\r\n")
          _T("%5u millisecs (%02u%%) reading source playlists/groups\r\n")
          _T("%5u millisecs (%02u%%) array shuffle 1\r\n")
          _T("%5u millisecs (%02u%%) array shuffle 2\r\n")
          _T("          %u millisecs (%02u%%) shuffle total\r\n")

          _T("%5u millisecs (%02u%%) writing file\r\n")
          _T("%5u millisecs TOTAL\r\n"),
          dw2StartShuffleArr1 - dw1StartReadLV,
            ((200 * (dw2StartShuffleArr1 - dw1StartReadLV)) + dwTotalTime)
                                                                      / dwDivisor,
          dw3StartShuffleArr2 - dw2StartShuffleArr1,
            ((200 * (dw3StartShuffleArr2 - dw2StartShuffleArr1)) + dwTotalTime)
                                                                      / dwDivisor,

          dw5StartWrite - dw3StartShuffleArr2, // Changed dw4 to dw5
            ((200 * (dw5StartWrite - dw3StartShuffleArr2)) + dwTotalTime)
                                                                       / dwDivisor,
                  dw5StartWrite - dw2StartShuffleArr1,
                    ((200 * (dw5StartWrite - dw2StartShuffleArr1)) + dwTotalTime)
                                                                       / dwDivisor,

          dw6FinishWrite - dw5StartWrite,
            ((200 * (dw6FinishWrite - dw5StartWrite)) + dwTotalTime) / dwDivisor,
          dwTotalTime);
    }
    else // nShuffleType == RB_REPEAT_DIST
    {
      _stprintf(sBS, _T("Finished writing shuffled file.\r\n")
          _T("%5u millisecs (%02u%%) reading source playlists/groups\r\n")

          _T("%5u millisecs (%02u%%) shuffling and distributing repeats\r\n")
          _T("%5u millisecs (%02u%%) writing file\r\n")
          _T("%5u millisecs TOTAL\r\n"),
          dw2StartShuffleArr1 - dw1StartReadLV,
            ((200 * (dw2StartShuffleArr1 - dw1StartReadLV)) + dwTotalTime)
                                                                       / dwDivisor,

            dw5StartWrite - dw2StartShuffleArr1, //  dw4 now dw2, total shuffle/dist
            ((200 * (dw5StartWrite - dw2StartShuffleArr1)) + dwTotalTime)
                                                                     / dwDivisor,
          dw6FinishWrite - dw5StartWrite,
            ((200 * (dw6FinishWrite - dw5StartWrite)) + dwTotalTime) / dwDivisor,
          dwTotalTime);
    }
   
    ToLog(sBS);
  }

  // Keep original. If change to MoveFile, first delete target if already exists
  if (!CopyFile(sTempPathPlaylist, sOutPath, FALSE))
  {
    GetSysErrorMsg(sBS);
    _stprintf(sBS2, _T("Error copying temp playlist\r\n%s\r\nto final location\r\n%s\r\n")
      _T("Windows' error message was:"), sTempPathPlaylist, sOutPath);
    return fnErr(sBS2, sBS);
  }

  // Add new playlist to WMP library if in preferences
  if (ini->bWmpNotify)
  {
    USES_CONVERSION; // Macro that sets up for other macros like OLE2T
    IWMPMedia* pItemIgnore;
    HRESULT hRes = m_cpWmpAllMedia->add(T2OLE(sOutPath), &pItemIgnore);
    // Ignore failure result - happens if replacing same name already in library
    //if (!SUCCEEDED(hRes))
    //  fnErr(_T("Non-fatal error:"),
    //        _T("Failed to add new playlist to Windows Media Player library"),
    //        sOutPath);
  }

  if (!bSepDups) // NonFatal, show message and continue
  {
    if (nShuffleType == RB_REPEAT_DIST) fnErr(sErrorSepDups); // Has full msg
    else
    {
      fnErr(_T("Non-fatal error:\nGenerated playlist is usable,")
        _T(" but repeats are close to each other at end of playlist."),
        sErrorSepDups,
        _T("This occurs more often as the specified separation between repeats is larger,"),
        _T("relative to playlist size divided by number of times the same song is included."));
    }
  }

  // Set return variables
  _tcscpy(sPathRet, sOutPath);

  if (nShuffleType == RB_REPEAT_DIST)
    _stprintf(sRetMsg,
              _T("%u unique songs in shuffled playlist")
              _T("\n%u total songs including repetitions"),
              sarrMain.GetSize(), sarrMain.GetListLength());
  else // RB_REPEAT_SEP, each rep has own array member, don't have unique count
    _stprintf(sRetMsg,
              _T("%u total songs in shuffled playlist, including repetitions"),
              sarrMain.GetListLength());

  //if (nMaxSize != -1 && nMaxSize < sarrMain.GetListLength())
  //{
  //  _stprintf(sBS, _T("\nTruncated to %d songs as requested"), nMaxSize);
  //  _tcscat(sRetMsg, sBS);
  //}
  if (nMaxSizeEffective != -1 && nMaxSizeEffective < sarrMain.GetListLength())
  {
    if (!bLimitNoReps) // Only absolute size limit checked, same as in v.1, 2.0
    {
      _stprintf(sBS, _T("\nTruncated to %d songs as requested."),
                nMaxSizeEffective);
      _tcscat(sRetMsg, sBS);
    }
    else // v.2.1 limit to no repetitions was checked
    {
      if (nMaxSizeEffective == m_nMinSegmentSize)
      {
        // Either absolute limit unchecked, or both checked but No Reps limit
        // prevailed per min/max option. Either way handle the same.
        // Main window already has all info that conditional text would provide.
        _stprintf(sBS,
            _T("\nTruncated to %d songs to prevent any repetitions as requested."),
            nMaxSizeEffective);
        _tcscat(sRetMsg, sBS);
      }
      else
      {
        // Both limits checked, AND absolute limit prevailed per min/max choice
        // Tell what m_nMinSegmentSize was generated even though it wasn't used.
        _stprintf(sBS,
            _T("\nTruncated to %d songs per specified options.\n")
            _T("\nBoth absolute limit and limit to no repetitions were checked.")
            _T("\nTruncated to the %s absolute limit as requested.")
            _T("\nLimit to ensure no repetitions would have been %d songs."),
            nMaxSizeEffective,
            (nMaxSizeFixed < m_nMinSegmentSize ? _T("smaller") : _T("larger")),
            m_nMinSegmentSize);
        _tcscat(sRetMsg, sBS);
      }
    } // else, i.e. bLimitNoReps TRUE
  } // if (nMaxSizeEffective != -1 && nMaxSizeEffective < sarrMain.GetListLength())

  if (ini->bElapsed)
  {
    FLOAT fSeconds = ((FLOAT) dwTotalTime) / 1000;
    _stprintf(sBS, _T("\n\nShuffle took %.1f seconds."), fSeconds);
    _tcscat(sRetMsg, sBS);
  }

  //if (fTotalSecs) *fTotalSecs = ((FLOAT) dwTotalTime) / 1000;

  return TRUE;
} // BOOL CMinDlg::fnShuffleList(LPTSTR sPathRet, LPTSTR sRetMsg)

//INT CMinDlg::TimeUnitsForLvItem(LPARAM lParam, BOOL bSameReps)
//{
//  // Estimate time to read a ListView item into the song array.
//  // Proportion of progress bar allocated to item will depend on other items.
//  INT nRet;
//  if (lParam == N_LV_ROWTYPE_ALL) nRet = 60; // All Songs
//  else if (lParam < 0) nRet = 20; // New Songs
//  else nRet = 10; // Playlist
//
//  // Faster if don't have to look up star ratings
//  if (bSameReps) nRet -= (nRet / 3); // integer result: 40, 14, or 7
//
//  return nRet;
//}

BOOL CMinDlg::LvCountStrToArray(LPCTSTR sCountStr, INT * naRet)
{
  //TCHAR sCountStr[1024];
  //ListView_GetItemText(m_hList, nThisRow, 1, sCountStr, 1000);

  TCHAR sQuotedCountStr[1024]; // Only used for error messages
  _stprintf(sQuotedCountStr, _T("\"%s\""), sCountStr);

  INT naReps[6] = {0, 0, 0, 0, 0, 0}; // naReps[0] needed; also validate below
  // Space at start of string indicates same repetitions all ratings
  if (sCountStr[0] == _T(' '))
  {
    if (!GetNextNumber(sCountStr, naReps)) // uaReps used as &uaReps[0]
//      return fnErr(_T("Error finding fixed repetition count in:"),
//                    sQuotedCountStr);
      // 3.0 change: now no digit after initial space means exclusion filter
      naReps[0] = -1;
  }
  else // 5 repetition counts space-separated, for each stars rating
  {
    LPCTSTR spRemainder = sCountStr;
    // String has rep counts for star rating in reverse order, 5 stars first
    // Load into array.
    for (INT nStars = 5; nStars > 0; nStars--)
    {
      spRemainder = GetNextNumber(spRemainder, &naReps[nStars]);
      if (!spRemainder)
      {
        _stprintf(sBS, _T("Error finding repetition count for %d stars in:"),
                  nStars);
        return fnErr(sBS, sQuotedCountStr);
      }
    }
  } // else, i.e. 5 repetition counts space-separated

  // Reality check, remove after long time with no problems
  BOOL bSomeNonZero = FALSE;
  for (INT i = 0; i < 6; i++)
  {
    if (naReps[i])
    {
      bSomeNonZero = TRUE;
      break;
    }
  }
  if (!bSomeNonZero)
    return fnErr(_T("No non-zero counts found in:"), sQuotedCountStr);

  // Looks ok, copy to out param
  for (INT i = 0; i < 6; i++)
  {
    naRet[i] = naReps[i];
  }

  return TRUE;
} // BOOL CMinDlg::LvCountStrToArray(LPCTSTR sCountStr, INT * naRet)

BOOL CMinDlg::LvCountStrToArray(INT nLvRowIndex, INT * naRet)
{
  TCHAR sCountStr[1024];
  ListView_GetItemText(m_hList, nLvRowIndex, 1, sCountStr, 1000);

  return LvCountStrToArray(sCountStr, naRet);
}

LPCTSTR CMinDlg::GetNextNumber(LPCTSTR spSpacesAndDigits, INT * nNextNum)
{
  // OK if chars neither space nor digit are AFTER first digit, but not before.
  // Puts atoi of next consecutive digit chars (maybe after spaces) in *uNextNum
  // Returns pointer to first char after the digits used; can be terminating \0
  // Returns NULL if digits not found at start, after space padding if any.
  LPCTSTR spFirstNonSpace;

  // Skip past any space padding before first digit, OK if none (i.e. nIx = 0)
  INT nIx = _tcsspn(spSpacesAndDigits, _T(" ")); // Count spaces at start
  spFirstNonSpace = &spSpacesAndDigits[nIx]; // First non-space should be digit

  nIx = _tcsspn(spFirstNonSpace, _T("1234567890")); // Count digits at start
  if (!nIx) return NULL; // First char (or \0) after space padding is not digit

  *nNextNum = _tstoi(spFirstNonSpace); // Ignores any non-digits after digit(s)
  return &spFirstNonSpace[nIx]; // First char or \0 after digits
}

// v.3 Nevermind this? All done inline in fnShuffleList
BOOL CMinDlg::SongsSelToSongArr(CSongsSelection & songssel, CSongArray & songarr)
{
  // Selection may have multiple part. First get data that applies to all.
  INT nType = songssel.GetType(); // SEL_TYPE_LISTS (genres,playlists) or SEL_TYPE_ALL
  BOOL bExclude = songssel.IsExclude(); // or just let it be inferred from reps uaReps?
  const INT * naReps = songssel.GetRepCounts();
  INT nMaxAge = songssel.GetMaxAge(); // 0 if no age limit
  BOOL bDedup = songssel.IsDedup();





// v.2 example of sProgPrefix arg for AddToSongArrayWrapper:
//    _tcscpy(sProgTxt, _T("Adding All Songs to the shuffle -"));
// Replace with simple     "Adding selection n of N to shuffle -" ?
// Or for here             "Getting songs for selection n of N -"






  return TRUE;
} // BOOL CMinDlg::SongsSelToSongArr(CSongsSelection & songssel, CSongArray & songarr)

/*
BOOL CMinDlg::AddToSongArray(CSongArray & songarray, IWMPPlaylist *pPlaylist,
                             UINT * uaReps, INT nProgPctThis,
                             INT nAddType, INT nMaxAge)
{
  ToLog(_T("AddToSongArray begin"));

  // if nMaxAge is 0 ignore date added. Otherwise must be newer than nMaxAge days
  LPCTSTR sErrIntro = _T("AddToSongArray error");
  TCHAR sErrMsg[2048];
  long nlPlaylistCount;

  HRESULT hr = pPlaylist->get_count(&nlPlaylistCount);
  if (!SUCCEEDED(hr)) return fnErr(_T("Playlist get_count failed"));

  CComBSTR cbsAttribUserRating = _T("UserRating");
  CComBSTR cbsValue;

  CComPtr<IWMPMedia> cpSong;
  cpSong.p = NULL;
  TCHAR sPath[1024];
  TCHAR sRating[1024];
  INT nStars;
  BOOL bRet;

  // These not used if nMaxAge = 0 (i.e. don't filter by age)
  INT nYYYYMMDD_min_date;
  CComBSTR cbsAttribAcqYear = _T("AcquisitionTimeYear");
  CComBSTR cbsAttribAcqMonth = _T("AcquisitionTimeMonth");
  CComBSTR cbsAttribAcqDay = _T("AcquisitionTimeDay");
  INT nAcqYear;
  INT nAcqMonth;
  INT nAcqDay;
  INT nAcqYYYYMMDD;
  if (nMaxAge)
  {
    nYYYYMMDD_min_date = YYYYMMDD_n_days_ago(nMaxAge);
    if (!nYYYYMMDD_min_date)
      return fnErr(sErrIntro, _T("Error calculating minimum date added"));
    if (m_hfLog) // OK to not check, but if NULL wastes time on _stprintf
    {
      _stprintf(sBS, _T("Filtering by age, nYYYYMMDD_min_date: %d"),
                nYYYYMMDD_min_date);
      ToLog(sBS);
    }
  }
  // else not nYYYYMMDD_min_date used

  INT nRepsSame = uaReps[0]; // = 0 if using song ratings
  INT nReps = nRepsSame;  // Keeps this value if > 0, else reset ea loop

  USES_CONVERSION; // Macro that sets up for other macros like OLE2T

  if (m_hfLog)
  {
    _stprintf(sBS,
              _T("Begin playlist loop, %d items (before date filter if any)"),
              nlPlaylistCount);
    ToLog(sBS);
  }

  FLOAT fPctTotalAdd;
  INT nNowTotalPct;
INT nLoopsToRpt = 1000;

  // Load each playlist item into the song array
  INT nRejectOld = 0;
  for (long i = 0; i < nlPlaylistCount; i++)
  {
// Exception the same whether declared here or before loop
//CComBSTR cbsValue;
//CComPtr<IWMPMedia> cpSong;
//cpSong.p = NULL;

    // Check for progress change and pending window messages every 10 loops
    if (!(i % 10) && i)
    {
      fPctTotalAdd = ((FLOAT) (i * nProgPctThis)) / ((FLOAT) nlPlaylistCount);
      nNowTotalPct = m_nProgLastPos + (INT) fPctTotalAdd;
      
      if (nNowTotalPct > m_nProgPos)
      {
        m_nProgPos = nNowTotalPct;
        SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);
      }

      zPeekMessage();
      //if (m_hfLog && !(i % 500))
      //{
      //  _stprintf(sBS, _T("In playlist loop, finished %d items"), i);
      //  ToLog(sBS);
      //}
    }
if (m_hfLog && !(i % nLoopsToRpt))
{
  if (i == 6000)
  {
    nLoopsToRpt = 50;
  }
  if (i == 6800)
  {
    nLoopsToRpt = 1;
  }
  _stprintf(sBS, _T("In playlist loop, finished %d items"), i);
  ToLog(sBS);
}

    if (m_bAbort) return fnErr(_T("Processing stopped by user Cancel"));

    hr = pPlaylist->get_item(i, &cpSong);
    if (!SUCCEEDED(hr) || !cpSong.p)
    {
      _tcscpy(sErrMsg, _T("cpPlaylist->get_item failed"));
      break;
    }

    if (nMaxAge) // Only keep newer than if nMaxAge != 0
    {
      hr = cpSong->getItemInfo(cbsAttribAcqYear, &cbsValue);
      if (SUCCEEDED(hr))
      {
        nAcqYear = _tstoi(OLE2T(cbsValue));
        cbsValue.Empty();
        hr = cpSong->getItemInfo(cbsAttribAcqMonth, &cbsValue);
      }
      if (SUCCEEDED(hr))
      {
        nAcqMonth = _tstoi(OLE2T(cbsValue));
        cbsValue.Empty();
        hr = cpSong->getItemInfo(cbsAttribAcqDay, &cbsValue);
      }
      if (SUCCEEDED(hr))
      {
        nAcqDay = _tstoi(OLE2T(cbsValue));
        cbsValue.Empty();
      }

      if (!SUCCEEDED(hr))
      {
        _tcscpy(sErrMsg, _T("Error getting date information on song"));
        break;
      }

      // Combine returned numbers into number like 20081231
      // Songs added in WMP 10 don't have date added info, return empty strings.
      // If so, _tstoi returns 0 => date is too old. Assume OK, WMP10 is old.
      nAcqYYYYMMDD = (10000 * nAcqYear) + (100 * nAcqMonth) + nAcqDay;

      if (nAcqYYYYMMDD < nYYYYMMDD_min_date)
      {
        nRejectOld ++;
        continue; // Ignore this song, try next song
      }
    } // if (nMaxAge)

    hr = cpSong->get_sourceURL(&cbsValue);
    if (!SUCCEEDED(hr))
    {
      _tcscpy(sErrMsg, _T("cpSong->get_sourceURL failed"));
      break;
    }
    _tcscpy(sPath, OLE2T(cbsValue));
    cbsValue.Empty();

    // Do we need the UserRating?
    if (!nRepsSame)
    {
      hr = cpSong->getItemInfo(cbsAttribUserRating, &cbsValue);
      if (!SUCCEEDED(hr))
      {
        _tcscpy(sErrMsg, _T("cpSong->getItemInfo (rating) failed"));
        break;
      }
      _tcscpy(sRating, OLE2T(cbsValue));
      cbsValue.Empty();

      nStars = SongRatingToStars(sRating); // String "0" to "99" => stars 1-5
      nReps = uaReps[nStars];
    }
    // else nReps keeps fixed value uaReps[0]
   
    _tcscat(sPath, _T("\r\n")); // Stored in CSongArray as line to write to file


//    if (!songarray.AddSong(sPath, nReps))
//    if (!songarray.IncrementOrAddSong(sPath, nReps))

    //if (bIncrement) bRet = songarray.AddSong(sPath, nReps);
    //else bRet = songarray.IncrementOrAddSong(sPath, nReps);

    if (nAddType == SONGARR_ADD_SEP) // Old shuffle, just min sep between songs
    {
      for (INT i = 0; i < nReps; i++)
      //FORLOOPS(nReps) // Macro works fine, but no real point
      {
        // Each repetition of song has separate SongArray index
        bRet = songarray.AddSong(sPath, 1);
        //if (!bRet) break; // Checked after this if/else block
      }
    }
    else // Distribute repetitions in segments of shuffle
    {
      if (nAddType == SONGARR_ADD_MULT) // For first time adding to array
        bRet = songarray.AddSong(sPath, nReps);
      else // SONGARR_INCREMENT  // Same song may already be in array
        bRet = songarray.IncrementOrAddSong(sPath, nReps);
    }

    if (!bRet)
    {
      _stprintf(sErrMsg, _T("Error adding song to song array:\r\n%s%s"),
                sPath, songarray.GetError());
      hr = S_OK - 10; // i.e. less than S_OK for SUCCEEDED(hr) test below
      break;
    }

    cpSong.Release();
  } // for (long i = 0; i < nlPlaylistCount; i++)
  if (cpSong.p) cpSong.Release();

  if (m_hfLog)
  {
    //ToLog(_T("LoadSongArray_Playlist finished"));
    if (nRejectOld)
      _stprintf(sBS, _T("%d songs skipped as too old"), nRejectOld);
    ToLog(_T("LoadSongArray_Playlist finished"), nRejectOld ? sBS : NULL);
  }

  if (!SUCCEEDED(hr)) return fnErr(sErrIntro, sErrMsg);
  else return TRUE;
} // BOOL CMinDlg::AddToSongArray(CSongArray & songarray, IWMPPlaylist *pPlaylist,
  //                           UINT * uaReps, INT nProgPctStart, INT nProgPctThis,
  //                           INT nMaxAge, BOOL bIncrement)
*/

BOOL CMinDlg::AddToSongArrayWrapper(CSongArray & songarray,
                             IWMPPlaylist *pPlaylist, UINT * uaReps,
                             INT nProgPctThis, INT nAddType,
                             LPCTSTR sProgPrefix, INT nMaxAge)
{
  // Replace call to old version above with one to this wrapper, which makes
  // repeated calls to clone of above that does 1000 loops of playlist index.
  // Kludge to force destructor of CComPtr vars by them passing out of scope.
  // With repeated use in loops, something accumulates despite .Release().
  // With stress test long playlists, access violation around 6800'th
  // playlist item, apparently (no call stack) cpSong->get_sourceURL(&cbsValue);

  ToLog(_T("AddToSongArrayWrapper begin"));
  LPCTSTR sErrIntro = _T("AddToSongArrayWrapper error");

  // if nMaxAge is 0 ignore date added. Otherwise must be newer than nMaxAge days
  long nlPlaylistCount;

  if (!SUCCEEDED(pPlaylist->get_count(&nlPlaylistCount)))
    return fnErr(sErrIntro, _T("pPlaylist->get_count failed"));

  INT nYYYYMMDD_min_date;
  if (nMaxAge)
  {
    nYYYYMMDD_min_date = YYYYMMDD_n_days_ago(nMaxAge);
    if (!nYYYYMMDD_min_date)
      return fnErr(sErrIntro, _T("Error calculating minimum date added"));
    if (m_hfLog) // OK to not check, but if NULL wastes time on _stprintf
    {
      _stprintf(sBS, _T("Filtering by age, nYYYYMMDD_min_date: %d"),
                nYYYYMMDD_min_date);
      ToLog(sBS);
    }
  }
  else nYYYYMMDD_min_date = 0; // Will indicate not filtering by age
  // NO MORE: // else not nYYYYMMDD_min_date used

  if (m_hfLog)
  {
    _stprintf(sBS,
              _T("Begin playlist loop, %d items (before date filter if any)"),
              nlPlaylistCount);
    ToLog(sBS);
  }

  INT nLoopsPerCall = 1000;
  INT nOldReject = 0; // Passed as pointer for running total
  for (INT nIxStart = 0; nIxStart < nlPlaylistCount; nIxStart += nLoopsPerCall)
  {
    INT nIxEnd = min((nlPlaylistCount - 1), (nIxStart + nLoopsPerCall - 1));
    if (!AddToSongArray(songarray, pPlaylist, uaReps, nProgPctThis, nAddType,
                        // After here params differ, no nMaxAge
                        nIxStart, nIxEnd, nlPlaylistCount, // Calc progress bar
                        nYYYYMMDD_min_date, &nOldReject, sProgPrefix))
      return FALSE; // error msg already
  }

  if (m_hfLog)
  {
    //ToLog(_T("LoadSongArray_Playlist finished"));
    if (nOldReject)
      _stprintf(sBS, _T("%d songs skipped as too old"), nOldReject);
    ToLog(_T("LoadSongArray_Playlist finished"), nOldReject ? sBS : NULL);
  }

  return TRUE;

} // BOOL CMinDlg::AddToSongArrayWrapper(CSongArray & songarray,
  //                              IWMPPlaylist *pPlaylist, UINT * uaReps,
  //                              INT nProgPctThis, INT nAddType, INT nMaxAge)

// V.3 add bDedup arg, change uaReps to INT, no longer default nMaxAge = 0
// ==> TO DO: Change nProgPctThis to float
BOOL CMinDlg::AddToSongArrayWrapper(CSongArray & songarray,
                             IWMPPlaylist *pPlaylist,
                             const INT * naReps, BOOL bDedup, FLOAT fProgPctThis,
                             INT nAddType, LPCTSTR sProgPrefix, INT nMaxAge)
{
  // Replace call to old version above with one to this wrapper, which makes
  // repeated calls to clone of above that does 1000 loops of playlist index.
  // Kludge to force destructor of CComPtr vars by them passing out of scope.
  // With repeated use in loops, something accumulates despite .Release().
  // With stress test long playlists, access violation around 6800'th
  // playlist item, apparently (no call stack) cpSong->get_sourceURL(&cbsValue);

  ToLog(_T("AddToSongArrayWrapper begin"));
  LPCTSTR sErrIntro = _T("AddToSongArrayWrapper error");

  // if nMaxAge is 0 ignore date added. Otherwise must be newer than nMaxAge days
  long nlPlaylistCount;

  if (!SUCCEEDED(pPlaylist->get_count(&nlPlaylistCount)))
    return fnErr(sErrIntro, _T("pPlaylist->get_count failed"));

  INT nYYYYMMDD_min_date;
  if (nMaxAge)
  {
    nYYYYMMDD_min_date = YYYYMMDD_n_days_ago(nMaxAge);
    if (!nYYYYMMDD_min_date)
      return fnErr(sErrIntro, _T("Error calculating minimum date added"));
    if (m_hfLog) // OK to not check, but if NULL wastes time on _stprintf
    {
      _stprintf(sBS, _T("Filtering by age, nYYYYMMDD_min_date: %d"),
                nYYYYMMDD_min_date);
      ToLog(sBS);
    }
  }
  else nYYYYMMDD_min_date = 0; // Will indicate not filtering by age
  // NO MORE: // else not nYYYYMMDD_min_date used

  if (m_hfLog)
  {
    _stprintf(sBS,
              _T("Begin playlist loop, %d items (before date filter if any)"),
              nlPlaylistCount);
    ToLog(sBS);
  }

  INT nLoopsPerCall = 1000;
  INT nOldReject = 0; // Passed as pointer for running total
  for (INT nIxStart = 0; nIxStart < nlPlaylistCount; nIxStart += nLoopsPerCall)
  {
    INT nIxEnd = min((nlPlaylistCount - 1), (nIxStart + nLoopsPerCall - 1));
    if (!AddToSongArray(songarray, pPlaylist, naReps, fProgPctThis,
                        nAddType, bDedup,
                        // After here args differ, replaced nMaxAge
                        nIxStart, nIxEnd, nlPlaylistCount, // Calc progress bar
                        nYYYYMMDD_min_date, &nOldReject, sProgPrefix))
      return FALSE; // error msg already
  }

  if (m_hfLog)
  {
    //ToLog(_T("LoadSongArray_Playlist finished"));
    if (nOldReject)
      _stprintf(sBS, _T("%d songs skipped as too old"), nOldReject);
    ToLog(_T("LoadSongArray_Playlist finished"), nOldReject ? sBS : NULL);
  }

  return TRUE;

} // BOOL CMinDlg::AddToSongArrayWrapper(CSongArray & songarray,
  //                           IWMPPlaylist *pPlaylist,
  //                           INT * naReps, BOOL bDedup, INT nProgPctThis,
  //                           INT nAddType, LPCTSTR sProgPrefix, INT nMaxAge))

BOOL CMinDlg::AddToSongArray(CSongArray & songarray, IWMPPlaylist *pPlaylist,
                             UINT * uaReps, INT nProgPctThis, INT nAddType,
                             INT nIxStart, INT nIxEnd, INT nPlaylistCount,
                             INT nYYYYMMDD_min_date, INT * npOldReject,
                             LPCTSTR sProgPrefix)
{
  if (m_hfLog)
  {
    _stprintf(sBS,
              _T("AddToSongArray for segment begin, indexes %d to %d)"),
              nIxStart, nIxEnd);
    ToLog(sBS);
  }

  // if nMaxAge is 0 ignore date added. Otherwise must be newer than nMaxAge days
  LPCTSTR sErrIntro = _T("AddToSongArray error");
  TCHAR sErrMsg[2048];

  CComBSTR cbsAttribUserRating = _T("UserRating");
  CComBSTR cbsValue;

  CComPtr<IWMPMedia> cpSong;
  cpSong.p = NULL;
  TCHAR sPath[1024];
  TCHAR sRating[1024];
  INT nStars;
  BOOL bRet;

  // These only used if filtering by age
  CComBSTR cbsAttribAcqYear = _T("AcquisitionTimeYear");
  CComBSTR cbsAttribAcqMonth = _T("AcquisitionTimeMonth");
  CComBSTR cbsAttribAcqDay = _T("AcquisitionTimeDay");
  INT nAcqYear;
  INT nAcqMonth;
  INT nAcqDay;
  INT nAcqYYYYMMDD;

  INT nRepsSame = uaReps[0]; // = 0 if using song ratings
  INT nReps = nRepsSame;  // Keeps this value if > 0, else reset ea loop

  USES_CONVERSION; // Macro that sets up for other macros like OLE2T

  FLOAT fPctTotalAdd;
  INT nNowTotalPct;

  if (nIxStart) // 2nd or later call, update window text with lines read
  {
    TCHAR sProgText[1024];
    if (nYYYYMMDD_min_date)
      _stprintf(sProgText, _T("%s %d songs screened for age"),
                sProgPrefix, nIxStart);
    else _stprintf(sProgText, _T("%s %d songs read"),
                   sProgPrefix, nIxStart);

    SetWindowText(m_hProgText, sProgText);
  }

  // Load each playlist item into the song array
  HRESULT hr;
  for (long i = nIxStart; i <= nIxEnd; i++)
  {
    // Check for progress change and pending window messages every 10 loops
    if (!(i % 10) && i)
    {
      fPctTotalAdd = ((FLOAT) (i * nProgPctThis)) / ((FLOAT) nPlaylistCount);
      nNowTotalPct = m_nProgLastPos + (INT) fPctTotalAdd;
      
      if (nNowTotalPct > m_nProgPos)
      {
        m_nProgPos = nNowTotalPct;
        SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);
      }

      zPeekMessage();
      //if (m_hfLog && !(i % 500))
      //{
      //  _stprintf(sBS, _T("In playlist loop, finished %d items"), i);
      //  ToLog(sBS);
      //}
    }

    if (m_bAbort) return fnErr(_T("Processing stopped by user Cancel"));

    hr = pPlaylist->get_item(i, &cpSong);
    if (!SUCCEEDED(hr) || !cpSong.p)
    {
      _tcscpy(sErrMsg, _T("cpPlaylist->get_item failed"));
      break;
    }

//    if (nMaxAge) // Only keep newer than if nMaxAge != 0
    if (nYYYYMMDD_min_date) // Only keep newer than if nYYYYMMDD_min_date != 0
    {
      hr = cpSong->getItemInfo(cbsAttribAcqYear, &cbsValue);
      if (SUCCEEDED(hr))
      {
        nAcqYear = _tstoi(OLE2T(cbsValue));
        cbsValue.Empty();
        hr = cpSong->getItemInfo(cbsAttribAcqMonth, &cbsValue);
      }
      if (SUCCEEDED(hr))
      {
        nAcqMonth = _tstoi(OLE2T(cbsValue));
        cbsValue.Empty();
        hr = cpSong->getItemInfo(cbsAttribAcqDay, &cbsValue);
      }
      if (SUCCEEDED(hr))
      {
        nAcqDay = _tstoi(OLE2T(cbsValue));
        cbsValue.Empty();
      }

      if (!SUCCEEDED(hr))
      {
        _tcscpy(sErrMsg, _T("Error getting date information on song"));
        break;
      }

      // Combine returned numbers into number like 20081231
      // Songs added in WMP 10 don't have date added info, return empty strings.
      // If so, _tstoi returns 0 => date is too old. Assume OK, WMP10 is old.
      nAcqYYYYMMDD = (10000 * nAcqYear) + (100 * nAcqMonth) + nAcqDay;

      if (nAcqYYYYMMDD < nYYYYMMDD_min_date)
      {
        //nRejectOld ++;
        *npOldReject ++;
        continue; // Ignore this song, try next song
      }
    } // if (nMaxAge)

    hr = cpSong->get_sourceURL(&cbsValue);
    if (!SUCCEEDED(hr))
    {
      _tcscpy(sErrMsg, _T("cpSong->get_sourceURL failed"));
      break;
    }
    _tcscpy(sPath, OLE2T(cbsValue));
    cbsValue.Empty();

    // Do we need the UserRating?
    if (!nRepsSame)
    {
      hr = cpSong->getItemInfo(cbsAttribUserRating, &cbsValue);
      if (!SUCCEEDED(hr))
      {
        _tcscpy(sErrMsg, _T("cpSong->getItemInfo (rating) failed"));
        break;
      }
      _tcscpy(sRating, OLE2T(cbsValue));
      cbsValue.Empty();

      nStars = SongRatingToStars(sRating); // String "0" to "99" => stars 1-5
      nReps = uaReps[nStars];
    }
    // else nReps keeps fixed value uaReps[0]
   
    // NO LONGER ADD \r\n. Useless for quoted in .wpl. Add if writing .m3u
    //_tcscat(sPath, _T("\r\n")); // Stored in CSongArray as line to write to file

    if (nAddType == SONGARR_ADD_SEP) // Old shuffle, just min sep between songs
    {
      for (INT i = 0; i < nReps; i++)
      //FORLOOPS(nReps) // Macro works fine, but no real point
      {
        // Each repetition of song has separate SongArray index
        bRet = songarray.AddSong(sPath, 1);
        //if (!bRet) break; // Checked after this if/else block
      }
    }
    else // Distribute repetitions in segments of shuffle
    {
      if (nReps) // Might have say 1-star songs set for 0 reps
      {
        if (nAddType == SONGARR_ADD_MULT) // Safe w/ All Songs as 1st item in list
          bRet = songarray.AddSong(sPath, nReps);
        else // SONGARR_INCREMENT  Slower, but same song may already be in array
          bRet = songarray.IncrementOrAddSong(sPath, nReps);
      }
    }

    if (!bRet)
    {
      _stprintf(sErrMsg, _T("Error adding song to song array:\r\n%s%s"),
                sPath, songarray.GetError());
      hr = S_OK - 10; // i.e. less than S_OK for SUCCEEDED(hr) test below
      break;
    }

    cpSong.Release();
  } // for (long i = 0; i < nlPlaylistCount; i++)
  if (cpSong.p) cpSong.Release();

  if (!SUCCEEDED(hr)) return fnErr(sErrIntro, sErrMsg);
  else return TRUE;
} // BOOL CMinDlg::AddToSongArray(CSongArray & songarray, IWMPPlaylist *pPlaylist,
  //                           UINT * uaReps, INT nProgPctThis, INT nAddType,
  //                           INT nIxStart, INT nIxEnd, INT nPlaylistCount,
  //                           INT nYYYYMMDD_min_date, INT * npOldReject)

// v.3 Add bDedup arg in AddToSongArray, handling for exclude.
// Also change uaReps to INT, because now naReps[0] is -1 for exclude.
// Change nProgPctThis to FLOAT
BOOL CMinDlg::AddToSongArray(CSongArray & songarray, IWMPPlaylist *pPlaylist,
                    const INT * naReps, FLOAT fProgPctThis, INT nAddType, BOOL bDedup,
                    INT nIxStart, INT nIxEnd, INT nPlaylistCount,
                    INT nYYYYMMDD_min_date, INT * npOldReject,
                    LPCTSTR sProgPrefix)
{
  if (m_hfLog)
  {
    _stprintf(sBS,
              _T("AddToSongArray for segment begin, indexes %d to %d)"),
              nIxStart, nIxEnd);
    ToLog(sBS);
  }

  // if nMaxAge is 0 ignore date added. Otherwise must be newer than nMaxAge days
  LPCTSTR sErrIntro = _T("AddToSongArray error");
  TCHAR sErrMsg[2048];

  CComBSTR cbsAttribUserRating = _T("UserRating");
  CComBSTR cbsValue;

  CComPtr<IWMPMedia> cpSong;
  cpSong.p = NULL;
  TCHAR sPath[1024];
  TCHAR sRating[1024];
  INT nStars;
  BOOL bRet = TRUE; // No error if nothing has set the value

  // These only used if filtering by age
  CComBSTR cbsAttribAcqYear = _T("AcquisitionTimeYear");
  CComBSTR cbsAttribAcqMonth = _T("AcquisitionTimeMonth");
  CComBSTR cbsAttribAcqDay = _T("AcquisitionTimeDay");
  INT nAcqYear;
  INT nAcqMonth;
  INT nAcqDay;
  INT nAcqYYYYMMDD;

  BOOL bRepsSame = (BOOL) naReps[0]; // = 0 if using song ratings
  INT nReps;  // Assign fixed value bRepsSame, else reset ea loop
  if (bRepsSame)
  {
    if (naReps[0] > 0) nReps = naReps[0]; // All songs have same frequency
    else // Negative value means use as exclusion list
    {
      // Caller's responsibility to make sure the referenced song array is the one
      // used only for excluded songs when this function called with naReps[0] < 0.
      // Here set mechanics for only needing to know if song is in the array or not.
      nReps = 1;
      bDedup = TRUE; // Should be redundant, with caller setting this arg TRUE
    }
  }

  USES_CONVERSION; // Macro that sets up for other macros like OLE2T

  FLOAT fPctTotalAdd;
  INT nNowTotalPct;

  if (nIxStart) // 2nd or later call, update window text with lines read
  {
    TCHAR sProgText[1024];
    if (nYYYYMMDD_min_date)
      _stprintf(sProgText, _T("%s %d songs screened for age"),  // <== No longer use this?
                sProgPrefix, nIxStart);
    else _stprintf(sProgText, _T("%s %d songs read"),
                   sProgPrefix, nIxStart);

    SetWindowText(m_hProgText, sProgText);
  }

  // Load each playlist item into the song array
  HRESULT hr;
  for (long i = nIxStart; i <= nIxEnd; i++)
  {
    // Check for progress change and pending window messages every 10 loops
    if (!(i % 10) && i)
    {
      fPctTotalAdd = ((FLOAT) i * fProgPctThis) / ((FLOAT) nPlaylistCount);
      nNowTotalPct = m_nProgLastPos + (INT) fPctTotalAdd;
      
      if (nNowTotalPct > m_nProgPos)
      {
        m_nProgPos = nNowTotalPct;
        SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);
      }

      zPeekMessage();
      //if (m_hfLog && !(i % 500))
      //{
      //  _stprintf(sBS, _T("In playlist loop, finished %d items"), i);
      //  ToLog(sBS);
      //}
    }

    if (m_bAbort) return fnErr(_T("Processing stopped by user Cancel"));

    hr = pPlaylist->get_item(i, &cpSong);
    if (!SUCCEEDED(hr) || !cpSong.p)
    {
      _tcscpy(sErrMsg, _T("cpPlaylist->get_item failed"));
      break;
    }

//    if (nMaxAge) // Only keep newer than if nMaxAge != 0
    if (nYYYYMMDD_min_date) // Only keep newer than if nYYYYMMDD_min_date != 0
    {
      hr = cpSong->getItemInfo(cbsAttribAcqYear, &cbsValue);
      if (SUCCEEDED(hr))
      {
        nAcqYear = _tstoi(OLE2T(cbsValue));
        cbsValue.Empty();
        hr = cpSong->getItemInfo(cbsAttribAcqMonth, &cbsValue);
      }
      if (SUCCEEDED(hr))
      {
        nAcqMonth = _tstoi(OLE2T(cbsValue));
        cbsValue.Empty();
        hr = cpSong->getItemInfo(cbsAttribAcqDay, &cbsValue);
      }
      if (SUCCEEDED(hr))
      {
        nAcqDay = _tstoi(OLE2T(cbsValue));
        cbsValue.Empty();
      }

      if (!SUCCEEDED(hr))
      {
        _tcscpy(sErrMsg, _T("Error getting date information on song"));
        break;
      }

      // Combine returned numbers into number like 20081231
      // Songs added in WMP 10 don't have date added info, return empty strings.
      // If so, _tstoi returns 0 => date is too old. Assume OK, WMP10 is old.
      nAcqYYYYMMDD = (10000 * nAcqYear) + (100 * nAcqMonth) + nAcqDay;

      if (nAcqYYYYMMDD < nYYYYMMDD_min_date)
      {
        //nRejectOld ++;
        *npOldReject ++;
        cpSong.Release();
        continue; // Ignore this song, try next song
      }
    } // if (nMaxAge)

    hr = cpSong->get_sourceURL(&cbsValue);
    if (!SUCCEEDED(hr))
    {
      _tcscpy(sErrMsg, _T("cpSong->get_sourceURL failed"));
      break;
    }
    _tcscpy(sPath, OLE2T(cbsValue));
    cbsValue.Empty();

    // Do we need the UserRating?
    if (!bRepsSame)
    {
      hr = cpSong->getItemInfo(cbsAttribUserRating, &cbsValue);
      if (!SUCCEEDED(hr))
      {
        _tcscpy(sErrMsg, _T("cpSong->getItemInfo (rating) failed"));
        break;
      }
      _tcscpy(sRating, OLE2T(cbsValue));
      cbsValue.Empty();

      nStars = SongRatingToStars(sRating); // String "0" to "99" => stars 1-5
      nReps = naReps[nStars];
    }
    // else nReps keeps fixed value uaReps[0]
   
    // NO LONGER ADD \r\n. Useless for quoted in .wpl. Add if writing .m3u
    //_tcscat(sPath, _T("\r\n")); // Stored in CSongArray as line to write to file

    if (nAddType == SONGARR_ADD_SEP) // Used in old shuffle, just min sep between songs
    {
// Will SONGARR_ADD_SEP only be used on final list, when deduping not an issue?
// If so, remove dup check below.
      if (!bDedup || songarray.GetSongIndex(sPath) == -1)
      {
        // Either no instance of song yet in array, and/or dups ok
        for (INT i = 0; i < nReps; i++) // specified repetitions is not "duplicate"
        //FORLOOPS(nReps) // Macro works fine, but no real point
        {
          // Each repetition of song has separate SongArray index
          bRet = songarray.AddSong(sPath, 1);
          //if (!bRet) break; // Checked after this if/else block
        }
      }
      // else song already in array and dups not allowed
    }
    else // Distribute repetitions in segments of shuffle
    {
      if (nReps) // Might have say 1-star songs set for 0 reps
      {
        // v.3 Ditch SONGARR_ADD_MULT, no longer obvious when to use

        //if (nAddType == SONGARR_ADD_MULT) // Safe w/ All Songs as 1st item in list
        //  bRet = songarray.AddSong(sPath, nReps);
        //else // SONGARR_INCREMENT  Slower, but same song may already be in array
        //  bRet = songarray.IncrementOrAddSong(sPath, nReps);
        
        INT nIndex = songarray.GetSongIndex(sPath);
        if (nIndex == -1) // Song not yet in array
          bRet = songarray.AddSong(sPath, nReps);
        else if (!bDedup)  // Song already in array, but dups ok
          bRet = songarray.IncrementSong(nIndex, sPath, nReps);
        // else song already in array, and dups not allowed
      }
    }

    if (!bRet) // default TRUE if not set just above
    {
      _stprintf(sErrMsg, _T("Error adding song to song array:\r\n%s%s"),
                sPath, songarray.GetError());
      hr = S_OK - 10; // i.e. less than S_OK for SUCCEEDED(hr) test below
      break;
    }

    cpSong.Release();
  } // for (long i = 0; i < nlPlaylistCount; i++)
  if (cpSong.p) cpSong.Release();

  if (!SUCCEEDED(hr)) return fnErr(sErrIntro, sErrMsg);
  else return TRUE;
} // BOOL CMinDlg::AddToSongArray(CSongArray & songarray, IWMPPlaylist *pPlaylist,
  //                       INT * naReps, INT nProgPctThis, INT nAddType, BOOL bDedup,
  //                       INT nIxStart, INT nIxEnd, INT nPlaylistCount,
  //                       INT nYYYYMMDD_min_date, INT * npOldReject)




// Returns date INT like 20081231 for comparing to similarly generated number
INT CMinDlg::YYYYMMDD_n_days_ago(INT nDaysAgo)
{
  __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

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

  // Minimal reality check
  if (t64_then < 0)
    return SetError(_T("Invalid _time64 subtraction result"));

  // 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 CMinDlg::SongRatingToStars(INT nRating)
{
  /************************************************************
  Rating  Usual    Range
  Unrated     0     0
  1 star      1     1 to 12
  2 stars    25    13 to 37
  3 stars    50    38 to 62
  4 stars    75    63 to 86
  5 stars    99    87 to 99
  http://msdn2.microsoft.com/en-us/library/bb249557(vs.85).aspx
  *************************************************************/
  //if (!nRating) return m_nDefaultStars;
  if (!nRating) return ini->nDefStars;
  if (nRating < 13) return 1;
  if (nRating < 38) return 2;
  if (nRating < 63) return 3;
  if (nRating < 87) return 4;
  return 5;
}

INT CMinDlg::SongRatingToStars(LPCTSTR sRating)
{
  INT nRating = _tstoi(sRating);
  return SongRatingToStars(nRating);
}

BOOL CMinDlg::fnShuffleNumArray(UINT * upArr, UINT uArrSize,
                                INT nProgPctThisFn, INT nProgPctSoFar)
{
  if (!uArrSize) return SetError(_T("Array size param is 0"));

  UINT * upTempArr = new UINT[uArrSize];
  if (!upTempArr) return SetError(_T("Error allocating temp number array"));

  UINT uMaxUnused = uArrSize - 1;
  UINT uRandInd;
  // Different seed than fnShuffleNumArrayByExch's srand((UINT) GetTickCount());
  srand((UINT) time(NULL));
	MSG msg;

  // Loop through all except last; assign what's left to the last
//  for (UINT u = 0; u < uArrSize - 1; u++)
  UINT u;
  for (u = 0; u < uArrSize - 1; u++)
  {
    // For progress so far, use u when is e.g. 1 after 1 completed cycle
    m_nProgPos = nProgPctSoFar + ((nProgPctThisFn * u) / uArrSize);
    if (m_nProgPos != m_nProgLastPos)
    {
      SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);
      m_nProgLastPos = m_nProgPos;
    }
    // else hasn't advanced far enough to next whole percentage point
    
    // Allow window messages to be processed, e.g. Stop button
    // For faster processing here, don't do it every loop
    if (! (u % 50)) // Interrupt every 50 loops
    {
      while (PeekMessage(&msg , NULL, NULL, NULL, PM_REMOVE))
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }
    }

    if (m_bAbort) // Stop button clicked
    {
      delete [] upTempArr;
      return FALSE; // Calling fn checks for m_bAbort to set msg
    }

    uRandInd = GetRandomUint(uMaxUnused); // Get random 0 to uMaxUnused
    upTempArr[u] = upArr[uRandInd]; // Copy from source array

    // Prepare for next loop cycle
    // Last member of source array that is "visible" this cycle will be
    // ignored next cycle. Move its value into the member who's value has
    // just been used. No effect if uRandInd == uMaxUnused
    upArr[uRandInd] = upArr[uMaxUnused];
    uMaxUnused --;
  }
  // Last one left unassigned
  upTempArr[uArrSize - 1] = upArr[0];

  // Don't progress for above assignment & below; calling fn updates total

  // Copy the shuffled temp array into the original source
  for(u = 0; u < uArrSize; u++)
  {
    upArr[u] = upTempArr[u];
  }
  delete [] upTempArr;

  return TRUE;
} // BOOL CMinDlg::fnShuffleNumArray(UINT * upArr, UINT uArrSize,
  //                              INT nProgPctThisFn, INT nProgPctSoFar)

BOOL CMinDlg::fnShuffleNumArrayByExch(UINT * upArr, UINT uArrSize,
                                      INT nProgPctThisFn, INT nProgPctSoFar)
{
  if (!uArrSize) return SetError(_T("Array size param is 0"));
  UINT uRandInd;
  UINT uSwap;
  // Use different seed than fnShuffleNumArray's srand((UINT) time(NULL));
  srand((UINT) GetTickCount());

  UINT uArrSizeMinus1 = uArrSize - 1; // Compiler probably would do this anyway
  MSG msg;

  // Loop through all except last; assign what's left to the last
  for (UINT u = uArrSize - 1; u > 0; u--)
  {
    // For progress so far, use u when is e.g. 1 after 1 completed cycle
    m_nProgPos = nProgPctSoFar
                 + ((nProgPctThisFn * (uArrSizeMinus1 - u)) / uArrSize);
    if (m_nProgPos != m_nProgLastPos)
    {
      SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);
      m_nProgLastPos = m_nProgPos;
    }
    // else hasn't advanced far enough to next whole percentage point
    
    // Allow window messages to be processed, e.g. Stop button
    // For faster processing here, don't do it every loop
    if (! (u % 50)) // Interrupt every 50 loops
    {
      while (PeekMessage(&msg , NULL, NULL, NULL, PM_REMOVE))
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }
    }

    if (m_bAbort) return FALSE; // Caller checks m_bAbort to set msg on FALSE

    // The actual randomizing section pretty short. Equal chance of getting
    // any not yet set array member's current value, inluding own value.
    uRandInd = GetRandomUint(u); // Get random 0 to u
    if (u != uRandInd)
    {
      uSwap = upArr[u];
      upArr[u] = upArr[uRandInd];
      upArr[uRandInd] = uSwap;
    }
  }
  return TRUE;
}

BOOL CMinDlg::SeparateDups(CSongArray & songarr, UINT * upArr, UINT uMinSep)
{
  UINT uSize = songarr.GetSize();
  UINT uSwap;
  INT uNewIx; // Change these UINT to INT for >= 0 test
  INT u;      // Should have avoided UINTs here altogether
  INT v;
  INT w;
  BOOL bRedo = FALSE;
  UINT uConsecRedo = 0;
  TCHAR sLogNote[8192];
  INT nExtraSep = 5; // NEW POSTBETA  When too close, move song uMinSep + nExtraSep
  INT nShift = (INT) uMinSep + nExtraSep; // NEW POSTBETA
  UINT uMaxConsecRedo = uMinSep; // Infinite loop protection
  //if (uMaxConsecRedo < 10) uMaxConsecRedo = 10;
  if (uMaxConsecRedo < 10) uMaxConsecRedo = uMinSep + (UINT) nExtraSep; // CHANGE POSTBETA

  UINT uSongsMoved = 0;
  TCHAR sLastFailedSong[4096];
  UINT uFailedSongs = 0;

  INT nProgPctBefore = m_nProgPos;
  MSG msg;

  // On first sweep, start from end of list backward; in comments below,
  // "forward" means in direction of sweep, i.e. toward lower index.
  // Creates bias for most duplicated songs toward start of playlist
//  for (u = 1; u < (uSize - 1); u++)
  for (u = uSize - 2; u >= 0; u--)
  {
    if (!bRedo) uConsecRedo = 0;
    else
    {
      uConsecRedo ++;
      bRedo = FALSE; // Initialize for next cycle
    }

    m_nProgPos = nProgPctBefore
                 + ((PROG_PCT_SEP_DUPS_PT1 * (uSize - u)) / uSize);
    if (m_nProgPos != m_nProgLastPos)
    {
      SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);
      m_nProgLastPos = m_nProgPos;
    }
    // else hasn't advanced far enough to next whole percentage point

    // Allow window messages to be processed, e.g. Stop button
    // For faster processing here, don't do it every loop
      while (PeekMessage(&msg , NULL, NULL, NULL, PM_REMOVE))
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }

    if (m_bAbort) return FALSE; // Calling fn checks for m_bAbort to set msg

    // Infinite loop protection
    if (uConsecRedo && // Usually 0, don't waste time on comparisons
        (uConsecRedo > uMaxConsecRedo ||
         uConsecRedo > (UINT) u)) // Near end of list, already tried all placements
    {
      if (ini->bLog)
      {
        _stprintf(sLogNote,
          _T("SeparateDups gave up distancing song after %u moves, still too close.\r\n")
          _T("This is first sweep of two, may be successfully distanced next sweep.\r\n")
          _T("%s\r\n") // NO LONGER: Song entry has \r\n
          _T("Current placement in shuffled playlist: %u\r\n"),
          uMaxConsecRedo, songarr.GetSong(upArr[u]), u);
        ToLog(sLogNote);
      }
      uFailedSongs ++;  // RESET TO 0 BEFORE SECOND SWEEP
      // _tcscpy(sLastFailedSong, songarr.GetSong(upArr[u])); Only needed 2nd sweep
      continue;
    }

    // Check preceeding uMinSep songs
//    for (v = u - 1; v >= (INT) u - (INT) uMinSep && v >= 0; v--)
    for (v = u + 1; v <= u + (INT) uMinSep && v < (INT) uSize; v++)
    {
      // Same song found in preceeding group, move further forward
      if (songarr.SongsTheSame(upArr[u], upArr[v]))
      {
//        uNewIx = u + (2 * uMinSep);
        //uNewIx = u - (2 * uMinSep);
        uNewIx = u - nShift; // CHANGE POSTBETA
//        if (uNewIx > (uSize - 1)) uNewIx = uSize - 1;
        if (uNewIx < 0) uNewIx = 0;

        uSwap = upArr[u];
        // Shift all songs from uNewIx through u-1 back one
//        for (w = u; w < uNewIx; w++)
        for (w = u; w > uNewIx; w--)
        {
//          upArr[w] = upArr[w + 1];
          upArr[w] = upArr[w - 1];
        }
        upArr[uNewIx] = uSwap; // Move repeated song forward

        if (ini->bLog)
        {
          uSongsMoved++;
          _stprintf(sLogNote,
            _T("SeparateDups moved repeated song too close to next play.\r\n")
            _T("%s\r\n") // NO LONGER: Song entry has \r\n
            _T("New placement in shuffled playlist: %u\r\n")
            _T("Old placement (too close) in shuffled playlist: %u\r\n")
            _T("Duplicate's placement in shuffled playlist: %d\r\n")
            _T("Moves done at this array index (old placement): %u\r\n"),
            songarr.GetSong(upArr[uNewIx]),
            uNewIx, u, v, uConsecRedo + 1);
          ToLog(sLogNote);
        }
      
        bRedo = TRUE; // For infinite loop protection
        
//        u--; // Song moved into current position now needs to be checked
        u++; // Song moved into current position now needs to be checked
        break;
      } // if (songarr.SongsTheSame(upArr[u], upArr[v]))
    } // for (v = u - 1; v >= u - uMinSep && v >= 0; v--)

  } // for (u = 1; u < uSize; u++)

  if (!uFailedSongs)
  {
    if (ini->bLog)
    {
      _stprintf(sLogNote,
          _T("Moved %u songs that were repeated too soon in subsequent play\r\n")
          _T("All were sufficiently separated on first sweep, skipping second sweep"),
          uSongsMoved);
      ToLog(sLogNote);
    }
      return TRUE;
  }
  else
  {
    if (ini->bLog)
    {
      _stprintf(sLogNote,
          _T("On first sweep moved %u songs repeated too soon in subsequent play,\r\n")
          _T("but still %u songs not sufficiently separated. Starting second sweep.\r\n"),
          uSongsMoved, uFailedSongs);
      ToLog(sLogNote);
    }
    uFailedSongs = 0; // Only count still failed after second sweep
  }

  // BEGIN SECOND SWEEP
  // Failed moves likely to be at start of list, i.e. near end of first sweep
  // Start in opposite direction from start of list for best chance of success
  nProgPctBefore = m_nProgPos;
  for (u = 1; u <= (INT) (uSize - 1); u++)
  {
    if (!bRedo) uConsecRedo = 0;
    else
    {
      uConsecRedo ++;
      bRedo = FALSE; // Initialize for next roung
    }

    m_nProgPos = nProgPctBefore
                 + ((PROG_PCT_SEP_DUPS_PT2 * u) / uSize);
    if (m_nProgPos != m_nProgLastPos)
    {
      SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);
      m_nProgLastPos = m_nProgPos;
    }
    // else hasn't advanced far enough to next whole percentage point

    // Allow window messages to be processed, e.g. Stop button
    // For faster processing here, don't do it every loop
      while (PeekMessage(&msg , NULL, NULL, NULL, PM_REMOVE))
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }

    if (m_bAbort) return FALSE; // Caller checks m_bAbort to set msg on FALSE

// FIRST SWEEP:   for (u = uSize - 2; u >= 0                 ; u--)
// THIS SWEEP:    for (u = 1;         u <= (INT) (uSize - 1)  ; u++)

    // Infinite loop protection
    if (uConsecRedo && // Usually 0, don't waste time on comparisons
        (uConsecRedo > uMaxConsecRedo ||
         uConsecRedo > (uSize - 1 - u))) // Near end, tried all possible placements
    {
      if (ini->bLog)
      {
        _stprintf(sLogNote,
          _T("SeparateDups gave up distancing song after %u moves, still too close.\r\n")
          _T("%s\r\n") // NO LONGER: Song entry has \r\n
          _T("Current placement in shuffled playlist: %u\r\n"),
          uMaxConsecRedo, songarr.GetSong(upArr[u]), u);
        ToLog(sLogNote);
      }
      uFailedSongs ++; // Count of songs still failed after this second sweep
      _tcscpy(sLastFailedSong, songarr.GetSong(upArr[u]));
      // Not sure if \r\n really wanted above when still included in GetSong.
      _tcscat(sLastFailedSong, _T("\r\n"));
      continue;
    }

    // Check preceeding uMinSep songs
    for (v = u - 1; v >= (INT) u - (INT) uMinSep && v >= 0; v--)
    {
      // Same song found in preceeding group, move further forward
      if (songarr.SongsTheSame(upArr[u], upArr[v]))
      {
        //uNewIx = u + (2 * uMinSep);
        uNewIx = u + nShift; // CHANGE POSTBETA
        if (uNewIx > (INT) (uSize - 1)) uNewIx = uSize - 1;

        uSwap = upArr[u];
        // Shift all songs from uNewIx through u+1 back one
        for (w = u; w < uNewIx; w++)
        {
          upArr[w] = upArr[w + 1];
        }
        upArr[uNewIx] = uSwap; // Move repeated song forward

        if (ini->bLog)
        {
          uSongsMoved++;
          _stprintf(sLogNote,
            _T("SeparateDups moved repeated song too close to previous play.\r\n")
            _T("%s\r\n") // NO LONGER: Song entry has \r\n
            _T("New placement in shuffled playlist: %u\r\n")
            _T("Old placement (too close) in shuffled playlist: %u\r\n")
            _T("Duplicate's placement in shuffled playlist: %d\r\n")
            _T("Moves done at this array index (old placement): %u\r\n"),
            songarr.GetSong(upArr[uNewIx]),
            uNewIx, u, v, uConsecRedo + 1);
          ToLog(sLogNote);
        }

        bRedo = TRUE; // For infinite loop protection
        
        u--; // Song moved into current position now needs to be checked
        break;
      } // if (songarr.SongsTheSame(upArr[u], upArr[v]))
    } // for (v = u - 1; v >= u - uMinSep && v >= 0; v--)

  } // for (u = 1; u < uSize; u++)


  if (ini->bLog)
  {
    _stprintf(sLogNote,
            _T("Moved %u songs that were repeated too soon after previous play\r\n")
            _T("Required two sweeps to sufficiently separate all songs\r\n"),
            uSongsMoved);
    ToLog(sLogNote);
  }
  
  if (uFailedSongs)
  {
    // Need sLogNote even if ! ini->bLog
    _stprintf(sLogNote,
        _T("Failed to separate %u songs from repeat within %u plays\r\n")
        _T("Last failure:\r\n%s"), uFailedSongs, uMinSep, sLastFailedSong);
    if (ini->bLog)  ToLog(sLogNote);
    return SetError(sLogNote); // Non fatal error, test m_bAbort, report failure
  }

  return TRUE;
}

// Version 3.0 change BOOL bM3u to INT nPlistFmt for 3 options (add .m3u8)
BOOL CMinDlg::WritePlaylist(LPCTSTR sOutPath, CSongArray & songarr,
                             UINT * upSongIndArr, INT nPlistFmt, INT nMax,
                             LPCTSTR sWplTitle, LPTSTR sFinalOutDir)
{
  // Reality Check
  if (!songarr.GetListLength() || !songarr.GetSize())
    return SetError(_T("Program logic error"),
                    _T("WritePlaylist called with 0 songs to write"));

//  _stprintf(sBS, _T("DEBUG: nPlistFmt = %d"), nPlistFmt);
//  MB(sBS);

  // For either UTF-8 .wpl or .m3u8, or ANSI .m3u, type is char[], not TCHAR[] (here maps to wchar_t)
  char sHeader[2048];

  if (nPlistFmt != RB_FMT_WPL) // v3.22 now 0=last used, => one of 1=wpl, 2=m3u8, 3=m3u 
    strcpy(sHeader, "#EXTM3U\r\n"); // Chars same whether ANSI or UTF-8
  else
  {
    // Convert title param to UTF-8 to insert in .wpl file
    char sTitleUtf8[1024];
    if (!Utf16ToUtf8((LPWSTR) sWplTitle, sTitleUtf8))
      return SetError(_T("Error converting UTF-16 playlist title to UTF-8"),
                      sWplTitle, UtilError());

    char sVersionUtf8[32];
    if (!Utf16ToUtf8((LPWSTR) m_sVersionNum, sVersionUtf8))
      return SetError(_T("Error converting UTF-16 version string to UTF-8"),
                      m_sVersionNum, UtilError());
  
 // Mistakenly thought these missing header items caused WMP to ignore wpl file
    //UINT uSongs = (nMax == -1) ? songarr.GetListLength()
    //                           : min(nMax, songarr.GetListLength());

    sprintf(sHeader,
            "<?wpl version=\"1.0\"?>\r\n<smil>\r\n<head>\r\n"
            "<meta name=\"Generator\" content=\"Shaken Not Stirred -- %s\"/>\r\n"
            //"<meta name=\"ItemCount\" content=\"%u\"/>\r\n"
            //"<meta name=\"IsFavorite\"/>\r\n"
            //"<meta name=\"ContentPartnerListID\"/>\r\n"
            //"<meta name=\"ContentPartnerNameType\"/>\r\n"
            //"<meta name=\"ContentPartnerName\"/>\r\n"
            //"<meta name=\"Subtitle\"/>\r\n"
            "<author/>\r\n<title>%s</title>\r\n"
            "</head>\r\n<body>\r\n<seq>\r\n",
            sVersionUtf8,
            //uSongs,
            sTitleUtf8);
  }

  BOOL bGotSysMsg;
  HANDLE hFile = CreateFile(sOutPath, GENERIC_WRITE, FILE_SHARE_READ, NULL,
                      CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  if (hFile == INVALID_HANDLE_VALUE)
  {
    bGotSysMsg = GetSysErrorMsg(sBS);
    _stprintf(sBS2, _T("Error opening file for write:\r\n%s\r\n%s%s"),
              sOutPath,
              bGotSysMsg ? _T("Windows' error message was:\r\n") : _T(""),
              sBS);
    return SetError(sBS2);
  }

  // For .m3u with TCHAR = UTF-8, this part has always been the same as ANSI
  // Now with UTF-16, must explicity specify, and avoid TCHAR macros/defs
  INT nTotalBytes = 0;

  INT nBytes;
  DWORD dwBytesWritten;

  nBytes = strlen(sHeader); // whether UTF-8 .wpl or .m3u8, or ANSI .m3u

//  if (!WriteFile(hFile, sM3uHeader, (DWORD) nBytes, &dwBytesWritten, NULL))
  if (!WriteFile(hFile, sHeader, (DWORD) nBytes, &dwBytesWritten, NULL))
  {
    bGotSysMsg = GetSysErrorMsg(sBS);
    _stprintf(sBS2, _T("Error writing start of file:\r\n%s\r\n%s%s"),
              sOutPath,
              bGotSysMsg ? _T("Windows' error message was:\r\n") : _T(""),
              sBS);
    CloseHandle(hFile);
    return SetError(sBS2);
  }
  else nTotalBytes += (INT) dwBytesWritten;

  nBytes = WriteSongArrayToFile(hFile, sOutPath, songarr,  upSongIndArr, nMax,
                                nPlistFmt, sFinalOutDir); // Version 3.0
  if (!nBytes) return FALSE; // fnSetError already from WriteSongArrayToFile

  nTotalBytes += nBytes;

  if (nPlistFmt == RB_FMT_WPL) // Version 3.3: .wpl is 1, no longer 0/false
  {
    LPSTR sWplTail = "</seq>\r\n</body>\r\n</smil>\r\n"; // NOT LPTSTR
    nBytes = strlen(sWplTail);

    if (!WriteFile(hFile, sWplTail, (DWORD) nBytes, &dwBytesWritten, NULL))
    {
      bGotSysMsg = GetSysErrorMsg(sBS);
      _stprintf(sBS2, _T("Error writing last part of file:\r\n%s\r\n%s%s"),
                sOutPath,
                bGotSysMsg ? _T("Windows' error message was:\r\n") : _T(""),
                sBS);
      CloseHandle(hFile);
      return SetError(sBS2);
    }
    else nTotalBytes += (INT) dwBytesWritten;
  }

  CloseHandle(hFile);

  if (m_hfLog)
  {
    _stprintf(sBS, _T("%d bytes written to %s"), nTotalBytes, sOutPath);
    ToLog(sBS);
  }

  return TRUE;
} // BOOL CMinDlg::WritePlaylist(LPCTSTR sOutPath, CSongArray & songarr,
  //                           UINT * upSongIndArr, INT nPlistFmt, INT nMax,
  //                           LPCTSTR sWplTitle)

INT CMinDlg::WriteSongArrayToFile(HANDLE hFile, LPCTSTR sOutPath,
                                  CSongArray & songarr, UINT * upSongIndArr, INT nMax,
                                  INT nPlistFmt, // Version 3.0 add 3rd option .m3u8
                                  LPTSTR sFinalOutDir) // v3.22 for relative path option
{
  BOOL bGotSysMsg;
  INT nTotalBytes = 0;

  INT nBytes;
  DWORD dwBytesWritten;

  // Now with UTF-16 TCHAR, must convert for either format to CHAR (utf8 or ascii)
  CHAR sByteChars[4096];
  TCHAR sPathUtf16[2048]; // Only used for .wpl
  CHAR sPathUtf8[2048];   // Only used for .wpl
  LPCVOID pvWrite; // Now always points to sByteChars

  // v3.22 Now option to use relative path
  LPTSTR spSongPath;
  TCHAR sFullPath[_MAX_PATH];
  TCHAR sRelativePath[_MAX_PATH];
  TCHAR sParentDelete[_MAX_PATH];
  TCHAR sParentReplacement[_MAX_PATH];
  INT nParentLength;
  INT nRelativeNotFound = 0;

  // Even if ini opt, some debugging cases supply NULL sFinalOutDir to force full paths.
  // N.B. sFinalOutDir is usually different from dir of temp work file sOutPath.
  BOOL bRelativePath = (sFinalOutDir && ini->bSongRelativePath);
  if (bRelativePath)
  {
    _tcscpy(sParentDelete, sFinalOutDir);
    // Determine the the start of path to be replaced by "..", or rarely ""
    // First handle silly case where where user picked root dir to save playlist in
    nParentLength = _tcslen(sParentDelete);
    if (nParentLength < 4)
    {
      // Leave sParentDelete and nParentLength for like "C:\", will replace with empty string
      sParentReplacement[0] = _T('\0');
    }
    else // Normal case like "C:\Users\acctname\Music\Playlists\"
    {
      // Caller sends path ending with "\", but don't assume it
      if (sParentDelete[_tcslen(sParentDelete) - 1] == _T('\\'))
        sParentDelete[_tcslen(sParentDelete) - 1] = _T('\0'); // Truncate to w/o ending '\'

      // Find last backslash still in path. String to left of it is parent dir's path.
      LPTSTR sFind = StrRChr(sParentDelete, NULL, _T('\\'));
      if (!sFind)
        return fnErr(_T("Error finding parent directory path in"), sFinalOutDir);
      // Truncate sParentDelete to containing directory's path
      sFind[0] = _T('\0'); // Now like "C:\Users\acctname\Music"
      nParentLength = _tcslen(sParentDelete); 
      _tcscpy(sParentReplacement, _T(".."));
    }
  } // if (bRelativePath)
  // else no need for those variables
  
  // Write each SongArray member as line(s) to the file
  INT nProgPctBefore = m_nProgPos; // Ignores file head & tail
  MSG msg;
  // If nMax set, only write that many songs from the shuffled list
  UINT uSongs = (nMax == -1) ? songarr.GetListLength()
                             : min(nMax, songarr.GetListLength());

  for (UINT u = 0; u < uSongs; u++)
  {
    m_nProgPos = nProgPctBefore + ((PROG_PCT_WRITE * u) / uSongs);
    if (m_nProgPos != m_nProgLastPos)
    {
      SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);
      m_nProgLastPos = m_nProgPos;
    }
    // else hasn't advanced far enough to next whole percentage point
    
    // Allow window messages to be processed, e.g. Stop button
    // For faster processing here, don't do it every loop
    if (! (u % 20)) // Interrupt every 20 loops
    {
      while (PeekMessage(&msg , NULL, NULL, NULL, PM_REMOVE))
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }
    }

    if (m_bAbort)
    {
      return SetError(_T("Processing stopped by user Cancel"),
                      _T("Shuffled playlist PARTIALLY written:"),
                      sOutPath);
    }

    // v3.22 Whether to use songarr.GetSong(upSongIndArr[u]) as is depends on bRelativePath
    _tcscpy(sFullPath, songarr.GetSong(upSongIndArr[u]));
    if (!bRelativePath) spSongPath = sFullPath;
    else
    {
      // Convert path like "C:\Users\username\Music\Artist Name\Album Name\Song Name.mp3"
      // to "..\Artist Name\Album Name\Song Name.mp3"
      // if playlist saved in typical folder like default "C:\Users\username\Music\Playlists"
      if (!_tcsnicmp(sFullPath, sParentDelete, nParentLength))
      {
        // Song path begins with the expected parent path to replace
        _stprintf(sRelativePath, _T("%s%s"), sParentReplacement, &sFullPath[nParentLength]);
        spSongPath = sRelativePath;
      }
      else
      {
        // Song has different parent, e.g. outside of "Music" dir or different drive.
        // Use full path, and record count to notify user some songs have full path
        spSongPath = sFullPath;
        nRelativeNotFound ++;
      }
    }

//    if (nPlistFmt) // Version 3.0: .wpl is 0, so either .m3u (ANSI) or .m3u8 (UTF-8)
    if (nPlistFmt!= RB_FMT_WPL)  // v3.22 now 0=last, => one of 1=wpl, 2=m3u, 3=m3u 
    {
      if (nPlistFmt == RB_FMT_M3U) // ANSI
      {
//        if (!Utf16ToAnsi((LPCWSTR) songarr.GetSong(upSongIndArr[u]), sByteChars))
        if (!Utf16ToAnsi((LPCWSTR) spSongPath, sByteChars))
        {
          UtilGetError(sBS);
          _stprintf(sBS2, _T("Error converting song path from UTF-16 to ANSI")
                          _T(" for playlist file\n%s\nSong path in UTF-16:\n%s"),
                    sBS, songarr.GetSong(upSongIndArr[u]));
          return SetError(sBS2);
        }
      }
      else // must be RB_FMT_M3U8 UTF-8 (already know not RB_FMT_WPL)
      {
//        if (!Utf16ToUtf8((LPCWSTR) songarr.GetSong(upSongIndArr[u]), sByteChars))
        if (!Utf16ToUtf8((LPCWSTR) spSongPath, sByteChars))
        {
          UtilGetError(sBS);
          _stprintf(sBS2, _T("Error converting song path from UTF-16 to UTF-8")
                          _T(" for playlist file\n%s\nSong path in UTF-16:\n%s"),
                    sBS, songarr.GetSong(upSongIndArr[u]));
          return SetError(sBS2);
        }
      }

      // v2.2 No longer have \r\n already appended to path in song array
      strcat(sByteChars, "\r\n"); // Version 3.0 no change, same for UTF-8 or ANSI
    }
    else // .wpl
    {
//      _tcscpy(sPathUtf16, songarr.GetSong(upSongIndArr[u]));
      _tcscpy(sPathUtf16, spSongPath);
      if (!SetXmlCharsForWpl(sPathUtf16))
        return SetError(_T("Error converting special chars to xml codes")
                            _T(" for playlist file"),
                        UtilError(), _T("Song path:"),
                        songarr.GetSong(upSongIndArr[u]));

      if (!Utf16ToUtf8((LPWSTR) sPathUtf16, sPathUtf8))
        return SetError(_T("Error converting song path from UTF-16 to UTF-8")
                            _T(" for playlist file"),
                        UtilError(), _T("Song path in UTF-16:"),
                        sPathUtf16);

      sprintf(sByteChars, "<media src=\"%s\"/>\r\n", sPathUtf8);
    }

    // Either UTF-8 or ANSI
    nBytes = strlen(sByteChars); // Not _tcslen
    pvWrite = sByteChars;

//    if (!WriteFile(hFile, songarr.GetSong(upSongIndArr[u]),
    if (!WriteFile(hFile, pvWrite,
                    (DWORD) nBytes, &dwBytesWritten, NULL))
    {
      bGotSysMsg = GetSysErrorMsg(sBS);
      _stprintf(sBS2,
                _T("Error writing song entry %u to file:\r\n%s\r\n%s%s"),
                u + 1, sOutPath,
                bGotSysMsg ? _T("Windows' error message was:\r\n") : _T(""),
                sBS);
      return SetError(sBS2);
    }
    else nTotalBytes += (INT) dwBytesWritten;
  } // for (UINT u = 0; u < uSongs; u++) upSongIndArr[u] = u;

  if (nRelativeNotFound) // Always 0 if !bRelativePath
  {
    // Notify that some songs written as full path, but proceed anyway
    _stprintf(sBS, _T("You have the preference for relative paths checked in Jukebox Shuffle Options,\n")
                   _T("but relative paths were not found for %d songs in your selection.\n")
                   _T("This can happen if your playlist isn't saved in the same My Music parent folder\n")
                   _T("containing your songs, or some songs are in a different location.\n\n")
                   _T("The playlist will still be written, but those songs will be listed by full path."),
              nRelativeNotFound);
    MB(MB_ICONEXCLAMATION, sBS);
  }

  return nTotalBytes;
} // BOOL CMinDlg::WritePlaylist(LPCTSTR sOutPath, CSongArray songarr, UINT * uArr,
  //                             LPCTSTR sFileHead, LPCTSTR sFileTail)

BOOL CMinDlg::SetXmlCharsForWpl(LPTSTR sLine)
{
  // In most cases none, don't waste time
  if (!_tcschr(sLine, '&')) return TRUE;

  // XML as used in .wpl only has five special char entities
  // Don't complain if not found, only if real error on ReplaceSubStr
  return (ReplaceSubStrIfThere(sLine, _T("&"), _T("&amp;")) && // Do & first!
          ReplaceSubStrIfThere(sLine, _T("'"), _T("&apos;")) &&
          ReplaceSubStrIfThere(sLine, _T("\""), _T("&quot;")) &&
          ReplaceSubStrIfThere(sLine, _T("<"), _T("&lt;")) &&
          ReplaceSubStrIfThere(sLine,_T( ">"), _T("&gt;")) );
  // If above returns FALSE, can call UtilGetError for error message
}
/*
BOOL CMinDlg::ReplaceXmlChars(LPTSTR sLine)
{
  // In most cases none, don't waste time
  if (!_tcschr(sLine, '&')) return TRUE;

  // XML (at least to MS) only has five special char entities
  // Don't complain if not found, only if real error on ReplaceSubStr
  return (ReplaceSubStrIfThere(sLine, "&apos;", "'") &&
          ReplaceSubStrIfThere(sLine, "&quot;", "\"") &&
          ReplaceSubStrIfThere(sLine, "&lt;", "<") &&
          ReplaceSubStrIfThere(sLine, "&gt;", ">") &&
          ReplaceSubStrIfThere(sLine, "&amp;", "&") );
  // If above returns FALSE, can call UtilGetError for error message
}
*/

// CHANGE VERSION 2: First created song array psa already has unigue entry for each
// song, with varying nCount, instead of nCount=1 for multiple entries same song.
// Since already like saUnique derived from old psa, and copy function assumes nCount=1,
// change function definition so "saUnique" is the input CSongArray, remove copying
//BOOL CMinDlg::DistributeList(CSongArray * psa, INT nMinSepBetweenSongs,
BOOL CMinDlg::DistributeList(CSongArray & saUnique, INT nMinSepBetweenSongs,
           INT * nIxArrWholeList, INT nProgPctThisFn, INT * pnFailures) //, BOOL bWpl)
{
  // AssignGroupToSegments will increment if any
  pnFailures[0] = 0; // Doesn't retain updates as *pnFailures, why??

  // In psa, any repeats have separate array members, i.e. m_nMaxSongCount = 1
//V2  INT nListLengthOrig = psa->GetListLength();
  INT nListLengthOrig = saUnique.GetListLength();
  if (nListLengthOrig < 10)
  {
    _stprintf(sBS,
              _T("Playlist length %d is less than minimum 10 for this shuffle method."),
              nListLengthOrig);
    return SetError(sBS);
  }

  // Temp SongArray, every member will be unique song; repeats -> nMaxSongCount > 1
//V2  CSongArray saUnique(m_pOwner);
//V2  LPCTSTR sThisSong;
  // Copy from original song array
  // Also in same loop, init all values of nIxArrWholeList to -1, i.e. not yet assigned
//  for (INT i = 0; i < nListLengthOrig; i++)
  INT i;
  for (i = 0; i < nListLengthOrig; i++)
  {
    nIxArrWholeList[i] = -1;
//V2    sThisSong = psa->GetSong(i);
//V2    if (!saUnique.IncrementOrAddSong(sThisSong))
//V2      // COMPILER ODDITY: Including saUnique.GetError in next line hoses hover
//V2      // hover hints in rest of this function. If saUnique.GetSong, no problem.
//V2      return SetError("Error copying song to temp array", saUnique.GetError());
  }

  // Now will handle songs in groups with the same number of repetitions.
  INT nMaxOccurrences = saUnique.GetMaxOccurrences();
  INT nListUniqueLength = saUnique.GetSize(); // Number of unique songs

  m_nMinSegmentSize = nListLengthOrig / nMaxOccurrences; // INT division

  // Array where index is play order, element is random array index from saUnique
  // Element number also identifies same song in pItems below
  // For repeated songs, same index number from saUnique will be repeated
  // Initialize to designate unused slots
  for (i = 0; i < nListLengthOrig; i++)
    nIxArrWholeList[i] = -1;

  // Larger array than needed except in silly case where all songs have same count
  INT * nIxArrThisGroup = new INT[nListUniqueLength];
  if (!nIxArrThisGroup)
    return SetError(_T("Error allocating repetition group array"));

  // Info to use moving songs for minimum separation
  SongItem * pItems = new SongItem[nListUniqueLength];
  if (!pItems)
  {
    delete [] nIxArrThisGroup;
    return SetError(_T("Error allocating SongItem array"));
  }

  for (i = 0; i < nListUniqueLength; i++)
  {
    pItems[i].nCount = saUnique.GetCountOneSong(i);
    if (pItems[i].nCount > SONG_MAX_REPEATS)
    {
      _stprintf(sBS, _T("Song repeated %d times, exceeds maximum allowed %d"),
                pItems[i].nCount, SONG_MAX_REPEATS);
      delete [] nIxArrThisGroup;
      delete [] pItems;
      return SetError(sBS, saUnique.GetSong(i));
    }

    for (INT j = 0; j < pItems[i].nCount; j++) // Higher values in nListIxArr ignored
    {
      pItems[i].nListIxArr[j] = -1; // Will assign places on the playlist
    }
  }

  // Assume took trivial time to get here in function.
  // Advance progress bar proportional to number of songs handled
  INT nProgPctBefore = m_nProgPos;
  INT nSongsDone = 0; // use to get percent of nListLengthOrig

  // ==> Same info in log entry for first group immediately after this
  //_stprintf(sBS, _T("Most occurrences of any song is %d   ")
  //               _T("Smallest segment size is %d"),
  //          nMaxOccurrences, m_nMinSegmentSize);

  ToLog(_T("Begin assigning by groups of same repetition count")); //, sBS);

  srand((UINT) GetTickCount()); // or could use srand((UINT) time(NULL));
  MSG msg;
  INT nSizeThisGroup;
  for (INT nOccurrences = nMaxOccurrences; nOccurrences > 0; nOccurrences--)
  {
    nSizeThisGroup = 0;
    for (i = 0; i < nListUniqueLength; i++)
    {
      if (pItems[i].nCount == nOccurrences) // same as saUnique.GetCountOneSong(i)
      {
        // Add to list of index numbers from song array for this group
        nIxArrThisGroup[nSizeThisGroup] = i;
        nSizeThisGroup ++;
      }

    } // for (i = 0; i < nListLengthOrig; i++)

    if (nSizeThisGroup)
    {
      if (!AssignGroupToSegments(nIxArrWholeList, nListLengthOrig, pItems,
                                 nIxArrThisGroup, nSizeThisGroup,
                                 nMinSepBetweenSongs, nProgPctThisFn,
                                 &saUnique, pnFailures))
      {
        delete [] nIxArrThisGroup;
        delete [] pItems;
        if (m_bAbort) return FALSE; // Stop button, no error message.
        _stprintf(sBS, _T("Error shuffling songs with %d repeats\r\n")
                       _T("%d songs previously done\r\n%s"),
                  nOccurrences, nSongsDone, m_sError);
        return SetError(sBS);
      }

      nSongsDone += (nOccurrences * nSizeThisGroup);

      _stprintf(sBS, _T("Finished group with %d repeats, %d unique songs, %d total"),
                nOccurrences, nSizeThisGroup, (nOccurrences * nSizeThisGroup));
      ToLog(sBS);

      // Progress bar updates in AssignGroupToSegments may have cumulative rounding
      // errors. Here reset baseline it uses.
      m_nProgPos = nProgPctBefore
                    + ( (nProgPctThisFn * nSongsDone)
                       / nListLengthOrig );
      if (m_nProgPos != m_nProgLastPos)
      {
        SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);
        m_nProgLastPos = m_nProgPos;
      } // else hasn't advanced far enough to next whole percentage point

      // Allow window messages to be processed, e.g. Stop button
      while (PeekMessage(&msg , NULL, NULL, NULL, PM_REMOVE))
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }

      if (m_bAbort)
      {
        delete [] nIxArrThisGroup;
        delete [] pItems;
        return FALSE; // Calling fn checks for m_bAbort to set msg
      }

    } // if (nSizeThisGroup)
    // else no songs to process with this repetition count

  } // for (INT nOccur = nMaxOccurrences; nOccur > 0; nOccur--)

  delete [] nIxArrThisGroup; // Done with this, but not pItems

  // If logging on, create spreadsheet with distribution info from pItems
  if (m_hfLog)
  {
    TCHAR sPath[MAX_PATH];
//    _stprintf(sPath, _T("%sRepeats distribution.txt"), m_sOwnDirSlash);
    _stprintf(sPath, _T("%sRepeats distribution.csv"), m_pzwd->sAppDocsDirSlash); // Version 3.0

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

    if (hFile == INVALID_HANDLE_VALUE)
    {
      GetSysErrorMsg(sBS);
      delete [] pItems;
      return SetError(_T("Error opening log file of repeat distribution:"),
                      sPath, sBS);
    }

//    LPCTSTR sIntro = _T("1st column is song path. 2nd is Count of times in playlist.")
//                     _T(" Then locations in playlist of each occurrence of song,")
//                     _T(" or on next line number of songs between repeats.\r\n");
    // Version 3.0 UTF-8, this string same as ansi
    LPCSTR sIntro = "\"1st column is song path. 2nd is Count of times in playlist."
                    " Then locations in playlist of each occurrence of song,"
                    " or on next line number of songs between repeats.\"\r\n";
    LPCTSTR sWinSez = _T("Windows' error message was:\r\n");
    INT nBytes;
    DWORD dwBytesWritten;
//    INT nBytesPerChar = sizeof(TCHAR);
    TCHAR sLine[32768]; // Overkill in case something bizarre in .wpl
    TCHAR sLine2[8192];
    TCHAR sCol[32];
    BOOL bGotSysMsg;

    // Version 3.0 Will convert UTF-16 lines to UTF-8, Excel doesn't like UTF-16
    CHAR sWriteUtf8[32768];

//    nBytes = _tcslen(sIntro) * nBytesPerChar; // Don't count null
    nBytes = strlen(sIntro); // Version 3.0 UTF-8. Don't count null
    if (!WriteFile(hFile, sIntro, (DWORD) nBytes, &dwBytesWritten, NULL))
    {
      bGotSysMsg = GetSysErrorMsg(sBS);
      _stprintf(sBS2, _T("Error writing start of file:\r\n%s\r\n%s%s"),
                sPath, bGotSysMsg ? sWinSez : _T(""), sBS);
      CloseHandle(hFile);
      delete [] pItems;
      return SetError(sBS2);
    }

    // Column headers for tab delimited spreadsheet
    // Version 3.0 Change to comma delimited
//    _tcscpy(sLine, _T("Song\tCount"));
    _tcscpy(sLine, _T("\"Song\",\"Count\""));
 //   INT nLastDigit;
    INT nOccurrences;
    for (nOccurrences = 1; nOccurrences <= nMaxOccurrences; nOccurrences++)
    {
//      _stprintf(sCol, _T("\t%d"), nOccurrences);
      _stprintf(sCol, _T(",\"%d"), nOccurrences); // Version 3.0 .csv
      //nLastDigit = nOccurrences % 10;

      //if (nLastDigit == 1) _tcscat(sCol, _T("st"));
      //else if (nLastDigit == 2) _tcscat(sCol, _T("nd"));
      //else if (nLastDigit == 3) _tcscat(sCol, _T("rd"));
      if (nOccurrences == 1) _tcscat(sCol, _T("st"));
      else if (nOccurrences == 2) _tcscat(sCol, _T("nd"));
      else if (nOccurrences == 3) _tcscat(sCol, _T("rd"));
      else _tcscat(sCol, _T("th"));

      _tcscat(sCol, _T("\"")); // Version 3.0 .csv
      
      _tcscat(sLine, sCol);
    }
    _tcscat(sLine, _T("\r\n"));

    // Version 3.0  Write .csv file as UTF-8 so Excel can open it
//    nBytes = _tcslen(sLine) * nBytesPerChar;
    if (!Utf16ToUtf8((LPCWSTR) sLine, sWriteUtf8))
    {
      GetSysErrorMsg(sBS);
      _stprintf(sBS2, _T("Error converting song placements log header line")
                      _T(" from UTF-16 to UTF-8\n%s%s"),
                sWinSez, sBS);
      CloseHandle(hFile);
      delete [] pItems;
      return SetError(sBS2);
    }
    nBytes = strlen(sWriteUtf8); // not _tcslen

//    if (!WriteFile(hFile, sLine, (DWORD) nBytes, &dwBytesWritten, NULL))
    if (!WriteFile(hFile, sWriteUtf8, (DWORD) nBytes, &dwBytesWritten, NULL))
    {
      bGotSysMsg = GetSysErrorMsg(sBS);
      _stprintf(sBS2, _T("Error writing column headers to log file:\r\n%s\r\n%s%s"),
                sPath, bGotSysMsg ? sWinSez : _T(""), sBS);
      CloseHandle(hFile);
      delete [] pItems;
      return SetError(sBS2);
    }

    //// Only used for .wpl to extract path from line
    //LPCTSTR sSongPathBefore = " src=\"";
    //LPCTSTR sSongPathAfter = "\""; // Search will find first \" after sSongPathBefore

    // List placements for all songs, grouped by most often repeated first
    for (nOccurrences = nMaxOccurrences; nOccurrences > 0; nOccurrences--)
    {
      // Write lines to spreadsheet for all songs occuring this many times
      for (i = 0; i < nListUniqueLength; i++) // Same index pItems & saUnique
      {
        // Write lines for all songs that occur in playlist this many times
        if (pItems[i].nCount == nOccurrences) // Will write line to file
        {
          // song path and repeat count first
//          _tcscpy(sLine, saUnique.GetSong(i)); // NO LONGER: has line break at end
          _tcscpy(sLine, _T("\"")); // Version 3.0 .csv
          _tcscat(sLine, saUnique.GetSong(i));
          _tcscat(sLine, _T("\""));

//          _tcscat(sLine, _T("\r\n")); // Pointless, just trim it off below

          //if (bWpl) // NOW NEVER
          //{
          //  // Only keep part of sLine that has the song file path
          //  if (!GetStrBetween(sLine, sSongPathBefore, sSongPathAfter)
          //      || !sLine[0]) // Path "found" but empty
          //  {
          //    CloseHandle(hFile);
          //    delete [] pItems;
          //    return SetError("Error parsing .wpl line for log file:", sPath, sLine);
          //  }
          //}
          //else // .m3u line, just trim off line break
          //{
//            nBS = _tcscspn(sLine, sRN); // Gets index of first of either
//            sLine[nBS] = _T('\0');
          //} // else, i.e. !bWpl

//          _stprintf(sCol,_T("\t%d"), pItems[i].nCount); // Number of times it occurs
          _stprintf(sCol,_T(",\"%d\""), pItems[i].nCount); // Version 3.0 .csv
          _tcscat(sLine, sCol);

          // Initialize following line for repeat intervals.
          // Next 2 columns empty (prev line occurrences count & ix of 1st occurrence)
//          _tcscpy(sLine2, _T("REPEAT INTERVAL:\t\t"));
          _tcscpy(sLine2, _T("\"REPEAT INTERVAL:\",\"\",\"\"")); // Version 3.0 .csv
          INT nIxThisOccurrence;
          INT nIxPrevOccurrence;

          // Write as many placement columns has song has occurrences
          for (INT j = 0; j < nOccurrences; j++)
          {
            nIxThisOccurrence = pItems[i].nListIxArr[j];
//            _stprintf(sCol, _T("\t%d"), nIxThisOccurrence + 1); // 0 base ix num
            // Version 3.0 .csv
            _stprintf(sCol, _T(",\"%d\""), nIxThisOccurrence + 1); // 0 base ix num
            _tcscat(sLine, sCol);
            // Build subsequent line with repeat intervals
            if (j > 0) // No repeat interval under first occurrence
            {
//              _stprintf(sCol, _T("\t%d"), nIxThisOccurrence - nIxPrevOccurrence);
              // Version 3.0 .csv
              _stprintf(sCol, _T(",\"%d\""), nIxThisOccurrence - nIxPrevOccurrence);
              _tcscat(sLine2, sCol);
            }
            nIxPrevOccurrence = nIxThisOccurrence;
          } // for (INT j = 0; j < nOccurrences; j++)
          _tcscat(sLine, sRN);

          if (nOccurrences > 1) // No subsequent line if no repeats
          {
            _tcscat(sLine, sLine2);
            _tcscat(sLine, sRN);
          }

          // Version 3.0  Write .csv file as UTF-8 so Excel can open it
//          nBytes = _tcslen(sLine) * nBytesPerChar;
          if (!Utf16ToUtf8((LPCWSTR) sLine, sWriteUtf8))
          {
            GetSysErrorMsg(sBS);
            _stprintf(sBS2, _T("Error converting song placements log line")
                            _T(" from UTF-16 to UTF-8\n%s%s"),
                      sWinSez, sBS);
            CloseHandle(hFile);
            delete [] pItems;
            return SetError(sBS2);
          }
          nBytes = strlen(sWriteUtf8); // not _tcslen

//          if (!WriteFile(hFile, sLine, (DWORD) nBytes, &dwBytesWritten, NULL))
          if (!WriteFile(hFile, sWriteUtf8, (DWORD) nBytes, &dwBytesWritten, NULL))
          {
            bGotSysMsg = GetSysErrorMsg(sBS);
            _stprintf(sBS2, _T("Error song placements line to log file:\r\n%s\r\n%s%s"),
                      sPath, bGotSysMsg ? sWinSez : _T(""), sBS);
            CloseHandle(hFile);
            delete [] pItems;
            return SetError(sBS2);
          }
        } // if (pItems[i].nCount == nOccurrences), i.e. write line to file
      } // for (i = 0; i < nListLengthOrig; i++)
    } // for (INT nOccurrences = nMaxOccurrences; nOccurrences > 0; nOccurrences--)

    CloseHandle(hFile);
  } // if (m_hfLog)

  delete [] pItems;

//V2  if (!saUnique.CopyToAnother(psa))
//V2    return SetError("Error replacing song array", saUnique.GetError());
//V2ToLog("Finished song array coppy");
  return TRUE;
} // BOOL CMinDlg::DistributeList(CSongArray * psa)

BOOL CMinDlg::AssignGroupToSegments(INT * nArrWholeList, INT nWholeListSize,
                          SongItem * pItems, INT * nArrIxGroup, INT nGroupSize,
                          INT nMinSepBetweenSongs, INT nProgPctParentFn,
                          CSongArray * sa, INT * pnFailures)
{
  INT nSegments = pItems[nArrIxGroup[0]].nCount; // All items in group have same nCount

  _stprintf(sBS,
          _T("Begin assigning songs repeated %d times each, %d unique songs"),
          nSegments, nGroupSize);
  ToLog(sBS);

  // Array where element is the index number of nArrWholeList elements that == -1
//  // More than needed, but saves reallocating every call.
//  static INT nArrAvailable[RAND_MAX + 1]; // Size equals max Song Array size
  // Now no maximum song array size. Allocate enough for whole array
  INT * nArrAvailable = new INT[nWholeListSize];
  if (!nArrAvailable)
    return SetError(_T("Error allocating array of available slots"));

  // Value in this will refer to nArrAvailable index
  static INT nArrMinIxForSeg[SONG_MAX_REPEATS]; // equals max number of segments

  // Temp for debug
  for (INT x = 0; x < nWholeListSize; x++)
    nArrAvailable[x] = -2; // Catch attempt to use unavailable slots

  INT nCountAvailable = 0;
//  for (INT i = 0; i < nWholeListSize; i++)
  INT i;
  for (i = 0; i < nWholeListSize; i++)
  {
    // nArrAvailable has list of not yet used slots in nArrWholeList
    if (nArrWholeList[i] == -1)
    {
      // Used part of nArrAvailable initially is ix 0 through (nCountAvailable - 1)
      nArrAvailable[nCountAvailable++] = i;
    }
  }

  if (nCountAvailable < (nGroupSize * nSegments))
  {
    delete [] nArrAvailable;
    _stprintf(sBS,
             _T("Logic error, %d slots available for assigning %d songs %d times each"),
             nCountAvailable, nGroupSize, nSegments);
    return SetError(sBS);
  }

  // First see if each equally divided segment of the whole list has enough
  // available slots for the group. If so, assign that way.
  // On first call to this (max nSegments), nSegSize equals m_nMinSegmentSize
  INT nSegSize = nWholeListSize / nSegments; // Ignore remainder from integer division
  INT nMinSep = min(nMinSepBetweenSongs, nSegSize / 3);
  BOOL bEnoughSlots = TRUE; // Unless reset below
  INT nOffset;
  INT nLoopMax;
  INT nSegAvailable;
  
  _stprintf(sBS, _T("%d unused slots available. Segment size %d.")
                 _T(" Min separation between songs %d"),
            nCountAvailable, nSegSize, nMinSep);
  ToLogNoStamp(sBS);

  // DEBUG
  _tcscpy(sBS, _T("Available slots in each segment:"));

  //for (i = 0; i < nSegments && bEnoughSlots; i++)
  // DEBUG - change condition
  for (i = 0; i < nSegments; i++)
  {
    nOffset = i * nSegSize;
    nLoopMax = nOffset + nSegSize - 1;
    nSegAvailable = 0;

    //for (INT j = nOffset; j <= nLoopMax && nSegAvailable <= nGroupSize; j++)
    // DEBUG - change condition
//    for (INT j = nOffset; j <= nLoopMax; j++)
    INT j;
    for (j = nOffset; j <= nLoopMax; j++)
    {
      if (nArrWholeList[j] == -1) nSegAvailable++;
    }
    // Here either exited loop because found enough available or ran out of slots

    // DEBUG
    _stprintf(sBS2, _T(" %d"), nSegAvailable);
    _tcscat(sBS, sBS2);

    if (nSegAvailable < nGroupSize)
    {
      bEnoughSlots = FALSE;
      //_stprintf(sBS2, _T(" (too few)"), nGroupSize); // Huh? sprintf?
      //_tcscat(sBS, sBS2);
      _tcscat(sBS, _T(" (too few)"));
    }
  } // for (i = 0; i < nSegments && bEnoughSlots; i++)
  ToLogNoStamp(sBS);

  if (bEnoughSlots)
  {
    ToLogNoStamp(_T("Sufficient unassigned slots in each segment of equally divided list"));
    // Divide equally across whole list, even if available slots differ in ea segment
    nArrMinIxForSeg[0] = 0; // ix of nArrAvailable, 1st available slot in entire list
    for (i = 1; i < nSegments; i++)
    {
      // Saw above that start and exit conditions will work ok
//      for (INT j = nArrMinIxForSeg[i - 1] + nGroupSize - 1; // Min possible find
      INT j;
      for (j = nArrMinIxForSeg[i - 1] + nGroupSize - 1; // Min possible find
           nArrAvailable[j] < (i * nSegSize); j++) // nArrAvailable[j] is ix in nArrWholeList
      {} // Do nothing, just find nArrAvailable index for start of whole list segment
      //nArrMinIxForSeg[i] = j + 1;
      nArrMinIxForSeg[i] = j; // j has failed condition, is in "next" segment // HAD TO MOVE THIS DECLARE OUT AN EXTRA LEVEL ABOVE
    }
  } // if (bEnoughSlots)
  else // have to divide segments evenly among available slots
  {
    _stprintf(sBS, _T("Will set segment ranges by equally dividing the %d unassigned slots"),
              nCountAvailable);
    ToLogNoStamp(_T("One or more segments of equally divided")
                 _T(" list without enough unassigned slots"),
                 sBS);

    INT nAvailablePerSeg = nCountAvailable / nSegments;
    for (i = 0; i < nSegments; i++)
    {
      nArrMinIxForSeg[i] = i * nAvailablePerSeg;
    }
  }
  // So last group can use condition < nArrMinIxForGroup[next_group_duznt_exist]
  nArrMinIxForSeg[nSegments] = nCountAvailable; // May be larger segment, includes modulus

  // Now for each segment assign one copy of each song to that part of nArrWholeList
  // For the first segment, can be completely random. For subsequent segments, assure
  // that each song doesn't occur too soon after previous occurrence
  // Using approach from hash_map::hash_map example
  typedef std::pair <INT, INT> ArrIxGrIx;
  stdext::hash_map <INT, INT> hashEven;
  stdext::hash_map <INT, INT> hashOdd;
//  stdext::hash_map <INT, INT,
//               stdext::hash_compare <INT, std::greater<INT> > > :: iterator pRevIter;
  stdext::hash_map <INT, INT> * hashCurrent;
  stdext::hash_map <INT, INT> * hashPrev;

  /**********************************************************************************
   ADD INT ARRAYS OF HASH KEYS IN KEY ORDER: NUMERICAL ORDER, BUT NOT CONSECUTIVE
   Use of hash_map here mistakenly assumed iterator returns elements in key order.
   Leads to some separations in adjacent segments less than intended minimum.
   Now store hash keys in array, initially not in numerical order; use qsort to order.
  ************************************************************************************/
  // Begin edit 1 of X for arrays of hash keys (also comment pRevIter above) 
  // Arrays will hold hash keys, which are nonconsecutive ix's of whole list array
  INT ** pnarr_HashKeysCurrent;
  INT ** pnarr_HashKeysPrev;
  INT * narr_HashKeysEven = new INT[nGroupSize];
  INT * narr_HashKeysOdd = new INT[nGroupSize];

  if (!narr_HashKeysEven || !narr_HashKeysOdd)
    return SetError(_T("Error allocating arrays for shuffle segment hash keys"));

  stdext::hash_map <INT, INT> :: const_iterator pKeyIter; // to look up value w/ key
  // End edit 1 of 7 for arrays of hash keys

  INT nRandIx;
  INT nIxWhole;

  // NO NEED TO INIT, LET LOOP TAKE CARE OF IT
  INT nMinIxThisSeg ;  // 0 on first loop
  INT nMinIx;  // constant on first, varies in later segments
  INT nMaxIx; // Will change after each assignment
  INT nRandMax;      // Will change after each assignment

  // For progress bar and check for Stop button
  INT nProgPctBefore = m_nProgPos;
  INT nSongsDone = 0; // use to get percent of nWholeListSize
  MSG msg;

  // Intitialize hashOdd to iterate (as hashPrev) on 1st loop (nSeg = 0)
  // On subsequent loops, will use values loaded on previous loop
  for (i = 0; i < nGroupSize; i++)
  {
    hashOdd.insert(ArrIxGrIx(i, i)); // Only second value used in 1st loop
    // Edit 2 of 7 for arrays of hash keys
    // Technically ix is 0..nGroupSize; value is 1st i in pair above, hash key.
    narr_HashKeysOdd[i] = i;
  }

  INT nPrevIxThisSong;
  INT nMinIxWhole;
  for (INT nSeg = 0; nSeg < nSegments; nSeg++)
  {
    // Edit 3 of 7 for arrays of hash keys
    // Assign odd/even array pointers along with assigning hash pointers
    if (nSeg % 2)
    {
      hashCurrent = &hashOdd;
      hashPrev = &hashEven;
      pnarr_HashKeysCurrent = &narr_HashKeysOdd;
      pnarr_HashKeysPrev = &narr_HashKeysEven;
    }
    else
    {
      hashCurrent = &hashEven;
      hashPrev = &hashOdd;
      pnarr_HashKeysCurrent = &narr_HashKeysEven;
      pnarr_HashKeysPrev = &narr_HashKeysOdd;
    }
    hashCurrent->clear();

    qsort((void *) *pnarr_HashKeysPrev, (size_t) nGroupSize, sizeof(INT), QsortCompareInt);

    nMinIxThisSeg = nArrMinIxForSeg[nSeg];
    nMinIx = nMinIxThisSeg + nSeg; // To force calculation on 2nd (ix 1) & later loops
    // nMinIx is reset for each song until no longer > nMinIxThisSeg
    nMaxIx = nArrMinIxForSeg[nSeg + 1] - 1; // Will change after each assignment
    //nRandMax = nMaxIx - nMinIx;        // Will calculate on each assignment

    _stprintf(sBS,
              _T("Segment %d  Min, Max nArrAvailable indexes for segment are %d, %d"),
              nSeg, nMinIxThisSeg, nMaxIx);
    ToLogNoStamp(sBS);

    // Loop through songs in reverse order they were assigned in previous segment
    // This loop is nGroupSize cycles whether iterating hash or directly
    // Edit 4 of 7 for arrays of hash keys
    // Replace hash iterator loop with INT loop; look up
//    for (pRevIter = hashPrev->begin(); pRevIter != hashPrev->end(); pRevIter++ )
    for (INT nPrevKeyArrIx = nGroupSize - 1; nPrevKeyArrIx > -1; nPrevKeyArrIx--)
    {
      // Begin edit 5 of 7 for arrays of hash keys
//      i = pRevIter->second; // nArrIxGroup ix (where VALUE is song array index)
      // Replace the one line above, & replace pRevIter->first (i.e. key) further down  
      nPrevIxThisSong = (*pnarr_HashKeysPrev)[nPrevKeyArrIx]; // index in nArrWholeList
      pKeyIter = hashPrev->find(nPrevIxThisSong);
      i = pKeyIter->second; // nArrIxGroup ix (where VALUE is song array index)
      // Using "i" for this var because in earlier version it was loop counter??
      // End edit 5 of X for arrays of hash keys

      if (nMinIx != nMinIxThisSeg)
      {
        // Still in range where song might repeat too soon
        // Edit 6 of 7 for arrays of hash keys - Already got nPrevIxThisSong above
//        nPrevIxThisSong = pRevIter->first; // index in nArrWholeList
        nMinIxWhole = nPrevIxThisSong + nMinSep + 1;
        // Find minimum index in nArrAvailable with enough separation
        nMinIx = -1; // To check if it was reset
        for (int j = nMinIxThisSeg; j <= nMaxIx; j++)
        {
          if (nArrAvailable[j] >= nMinIxWhole)
          {
            nMinIx = j; // nMinIxForThisSex 1st loop if all ok, and for all remaining
            break;
          }
        }
        if (nMinIx == -1) // Unlikely, but possible with extremely repetitious list
        {
          nMinIx = nMaxIx; // Assign as far away as available
          // Non fatal, but record info for fnShuffle to check on TRUE return
          pnFailures[0] ++;
          // Besides count, details of last error in m_sError (later failure overwrites)
          _stprintf(m_sError,
            _T("Less than minimum separation in segment %d of songs with %d repeats\n")
            _T("Minimum separation for this group: %d  Actual separation: %d\n")
            _T("Location in playlist (farthest unused slot in segment): %d\n")
            _T("Location of previous occurrence of same song: %d\r\n")
			_T("nArrAvailable[nMinIx] + 1 = '%d'  nPrevIxThisSong = '%d'\n")
			_T("sa->GetSong(nArrIxGroup[%d] = '%s'"),
            nSeg + 1, nSegments, nMinSep, nMinIx, nPrevIxThisSong,
			nArrAvailable[nMinIx] - nPrevIxThisSong,
            nArrAvailable[nMinIx] + 1,
            nPrevIxThisSong,
            i, sa->GetSong(nArrIxGroup[i]));

            ToLog(m_sError); // Log all failures if logging turned on
        } // if (nMinIx == -1)

      } // if (nMinIx != nMinIxThisSeg)
      // else has been set as nArrMinIxForSeg, will be ok for all remaining loops 

      nRandMax = nMaxIx - nMinIx; // OK if 0
      if (nRandMax > RAND_MAX_BIG || nRandMax < 0) // Reality check, should never happen
      {
        delete [] nArrAvailable;
        _stprintf(sBS, _T("nRandMax = %d  nMaxIx = %d  nMinIx = %d\r\n")
                          _T("nArrWholeList ix = %d  nArrIxGroup ix = %d  nSeg = %d"),
                       nRandMax, nMaxIx, nMinIx, nIxWhole, i, nSeg);
        return SetError(_T("Logic error, nRandMax not in range 0 to RAND_MAX_BIG"),
                        sBS);
      }
      nRandIx = nMinIx + GetRandomUint(nRandMax); // GetRandomUint returns 0 to nRandMax
      if (nArrAvailable[nRandIx] < 0)
      {
        delete [] nArrAvailable;
        _stprintf(sBS, _T("nArrWholeList ix = %d  nArrIxGroup ix = %d  ")
                          _T("nArrAvailable ix = %d  nMinIx = %d  nSeg = %d"),
                  nIxWhole, i, nRandIx, nMinIx, nSeg);
        return SetError(_T("Logic error, supposedly available")
                           _T(" segment slot already assigned"),
                        sBS);
      }

      nIxWhole = nArrAvailable[nRandIx];
      if (nArrWholeList[nIxWhole] != -1)
      {
        delete [] nArrAvailable;
        _stprintf(sBS, _T("nArrWholeList ix = %d  nArrIxGroup ix = %d   ")
                          _T("nArrAvailable ix = %d nMinIx = %d  nSeg = %d"),
                  nIxWhole, i, nRandIx, nMinIx, nSeg);
        return SetError(_T("Logic error, supposedly available list slot already assigned"),
                        sBS);
      }

      nArrWholeList[nIxWhole] = nArrIxGroup[i]; // Assign song array index number
      pItems[nArrIxGroup[i]].nListIxArr[nSeg] = nIxWhole;

      // Info for ensuring separation in next segment
      hashCurrent->insert(ArrIxGrIx(nIxWhole, i));
      // Edit 7 of 7 for arrays of hash keys
      // Store hash key in array. Use nPrevKeyArrIx as ix here just to use each slot
      // once; will reorder array in numerical (nonconsecutive) order of values.
      (*pnarr_HashKeysCurrent)[nPrevKeyArrIx] = nIxWhole;

      // Now rearrange this segment of nArrAvailable for next random number
      // Shift available list after just used slot down by one, so available contiguous
      for (int j = nRandIx; j < nMaxIx; j++) // Won't enter loop if nRandIx = nMaxIx
      {
        nArrAvailable[j] = nArrAvailable[j + 1];
      }
      nArrAvailable[nMaxIx] = -1;
      nMaxIx --; // One less available slot to pick from

      // End of allocation in this loop cycle. Do housekeeping
      // Update progress bar
      nSongsDone ++; // Songs done in this group, all segments
      m_nProgPos = nProgPctBefore
                    + ( (nProgPctParentFn * nSongsDone)
                       / nWholeListSize );

      if (m_nProgPos != m_nProgLastPos)
      {
        SendMessage(m_hProgress, PBM_SETPOS, (WPARAM) m_nProgPos, 0);
        m_nProgLastPos = m_nProgPos;
      } // else hasn't advanced far enough to next whole percentage point

      // Allow window messages to be processed, e.g. Stop button
      while (PeekMessage(&msg , NULL, NULL, NULL, PM_REMOVE))
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }

      if (m_bAbort)
      {
        delete [] nArrAvailable;
        return FALSE; // Calling fn checks for m_bAbort to set msg
      }

    } // for (pRevIter = hashPrev.begin(); pRevIter != hashPrev.end(); pRevIter++ )
  } // for (INT nSeg = 1; nSeg < nSegments; nSeg++)

  delete [] nArrAvailable;
  return TRUE;
} // BOOL CMinDlg::AssignGroupToSegments(INT * nArrWholeList, INT nWholeListSize,
  //                            INT * nArrIxGroup, INT nGroupSize, INT nSegments)



BOOL CMinDlg::fnOnlySpaceAfterIt(LPCTSTR sLine, LPCTSTR sSubStr, LPTSTR sError)
{
  // Return TRUE if sAll is only sSubStr plus maybe spaces & \r\n after
  return fnOnlySpaceAroundIt(sLine, sSubStr, sError, IGNORE_BEFORE);
}

BOOL CMinDlg::fnOnlySpaceBeforeIt(LPCTSTR sLine, LPCTSTR sSubStr, LPTSTR sError)
{
  // Return TRUE if sAll is only sSubStr plus maybe spaces & \r\n after
  return fnOnlySpaceAroundIt(sLine, sSubStr, sError, IGNORE_AFTER);
}

BOOL CMinDlg::fnOnlySpaceAroundIt(LPCTSTR sLine, LPCTSTR sSubStr,
                                  LPTSTR sError, UINT uIgnoreWhat)
{
  // Return TRUE if sAll is only sSubStr plus maybe spaces before and/or after
  // Also ok to have \r\n after
//  LPTSTR spStartSubStr; //MinDlg.cpp(6049): error C2440: '=' : cannot convert from 'const char *' to 'LPTSTR'
  LPCTSTR spStartSubStr; //MinDlg.cpp(6049): error C2440: '=' : cannot convert from 'const char *' to 'LPTSTR'
//  LPTSTR spAfterSubStr;
  LPCTSTR spAfterSubStr;

  // pointer to the start of the substring
  if (!(spStartSubStr = _tcsstr(sLine, sSubStr)))  // MinDlg.cpp(6049): error C2440: '=' : cannot convert from 'const char *' to 'LPTSTR'
  {
    _stprintf(sError, _T("\"%s\" not found in\n\"%s\""), sSubStr, sLine);
    return FALSE;
  }

  // Only spaces, if anything, before sSubStr
  if (uIgnoreWhat != IGNORE_BEFORE
      && sLine + _tcsspn(sLine, _T(" ")) != spStartSubStr)
  {
    _stprintf(sError, _T("Should only be spaces before \"%s\" in\n\"%s\""), sSubStr, sLine);
    return FALSE;
  }

  if (uIgnoreWhat == IGNORE_AFTER) return TRUE; // All done
    
  // pointer to first character or terminating \r\n after substring
  spAfterSubStr = spStartSubStr + _tcslen(sSubStr);
  // Should be empty string or all spaces after sSubStr, plus \r\n
  if (_tcsspn(spAfterSubStr, _T(" \r\n")) != _tcslen(spAfterSubStr))
  {
    _stprintf(sError, _T("Should only be spaces after \"%s\" in\n\"%s\""), sSubStr, sLine);
    return FALSE;
  }

  return TRUE;
}

void CMinDlg::UpdatePref()
{
  m_bFlushLog = ini->bLogFlush; // In case changed in preferences
  //if (m_sSourcePath[0]) _tcscpy(sBS2, m_sSourcePath);
  //else _tcscpy(sBS2, "C:\\<original playlist name>");

  //if (fnOutputNameFromSource(sBS2, sBS, FALSE, FALSE))
  //{
  //  //SetItemText(IDC_TXT_OUTPUT_NAME, sBS);
  //  _stprintf(sBS2, "Save as this default file name:  %s", sBS);
  //  SetItemText(IDC_RB_OUTPUT_DEFAULT, sBS2);
  //}
  //else fnErr("Error updating default output name display in main window", m_sError);
}


// Now derive name directly from first ListView entry (if option set)
// Name only, no dir
void CMinDlg::GetDefaultSaveName(LPTSTR sOutName)
{
  TCHAR sDotExt[16];

// v3.22 Default to last used; .m3u now used for UTF8 .m3u
  if (ini->nPlaylistFmt == RB_FMT_LAST)
  {
    //if (ini->nPlaylistFmtLast == RB_FMT_M3U) { _tcscpy(sDotExt, _T(".m3u")); }
    //else if (ini->nPlaylistFmtLast == RB_FMT_M3U8) { _tcscpy(sDotExt, _T(".m3u8")); }
    //else { _tcscpy(sDotExt, _T(".wpl")); }
    if (ini->nPlaylistFmtLast == RB_FMT_WPL) { _tcscpy(sDotExt, _T(".wpl")); }
    else { _tcscpy(sDotExt, _T(".m3u")); }
  }
  else // always revert to setting from Preferences dlg
  {
    //if (ini->nPlaylistFmt == RB_FMT_M3U) { _tcscpy(sDotExt, _T(".m3u")); }
    //else if (ini->nPlaylistFmt == RB_FMT_M3U8) { _tcscpy(sDotExt, _T(".m3u8")); }
    //else { _tcscpy(sDotExt, _T(".wpl")); }
    if (ini->nPlaylistFmt == RB_FMT_WPL) { _tcscpy(sDotExt, _T(".wpl")); }
    else { _tcscpy(sDotExt, _T(".m3u")); }
  }

  // May have selected in preferences any or all of 3 components for out name.
  // If none selected, no default output name.
  if (!ini->bSrcName && !ini->bLabel && !ini->bDate)
  {
    _stprintf(sOutName, _T("*%s"), sDotExt);
    return;
  }

  // At least one name component selected. Check for each.


  //// Parse input path to generate default output path
  //TCHAR sSrcDirSlash[_MAX_DIR];
  //TCHAR sSrcNameNoExt[200];
  //TCHAR sSrcDotExt[200];
  TCHAR sWork[2048];

  //if (!ParsePath(sSrcPath, sSrcDirSlash, sSrcNameNoExt, sSrcDotExt, bRequireExt))
  //  return SetError("Error deriving default output name from source path:",
  //                  sSrcPath, sSrcDirSlash); // Error msg in sSourceDirSlash

  // User settings for deriving file name
  TCHAR sBeforeName[128];
  sBeforeName[0] = _T('\0');
  TCHAR sAfterName[128];
  sAfterName[0] = _T('\0');
  LPTSTR sForNow;

  if (ini->bLabel) // Label before date stamp if both in same part
  {
    sForNow = (ini->uLabelPlace == RB_BEFORE) ? sBeforeName : sAfterName;
    _tcscat(sForNow, ini->sLabel);
  }

  if (ini->bDate) // Label before date stamp if both in same part
  {
    sForNow = (ini->uDatePlace == RB_BEFORE) ? sBeforeName : sAfterName;
//    GetDateFormat(LOCALE_SYSTEM_DEFAULT, 0, NULL, _T("yyMMdd"), sWork, 200);
    GetDateFormat(LOCALE_SYSTEM_DEFAULT, 0, NULL, _T("MMdd"), sWork, 200);

    // If both date and label in same part, separate with space
    if (sForNow[0]) _tcscat(sForNow, _T(" "));
    _tcscat(sForNow, sWork);
  }

  if (ini->bSrcName)
  {
    // Get first playlist or group name from listview
    // Caller already verified at least one item in the list

    TCHAR sPlaylistName[1024];
    LVITEM lvi;
    lvi.iItem = 0;
    lvi.iSubItem = 0;
    lvi.mask = LVIF_PARAM | LVIF_TEXT;
    lvi.pszText = sPlaylistName; // Can be a group name, not a real playlist
    lvi.cchTextMax = 1000;

    if (!ListView_GetItem(m_hList, &lvi))
    {
      fnErr(_T("Non-fatal error getting list data for default output name"),
            _T("No default name set."),
            _T("Otherwise OK to continue normally"));
      _stprintf(sOutName, _T("*%s"), sDotExt);
      return ;
    }

    //// MS re ListView_GetItem Macro:
    //// Applications should not assume that the text will necessarily be placed in
    //// the specified buffer. The control may instead change the pszText member of
    //// the structure to point to the new text rather than place it in the buffer.
    //if (lvi.pszText != sPlaylistName) _tcscpy(sPlaylistName, lvi.pszText);

    if (lvi.lParam > -1) // Is playlist array index, use playlist name
    {
      _tcscpy(sWork, lvi.pszText);
      // Trim off playlist name ending like " <playlist>"
      LPTSTR sTrim = _tcsstr(sWork, _T(" <"));
      if (sTrim) sTrim[0] = _T('\0');

      // If any other illegal chars, replace with underscore
      INT nLen = _tcslen(sWork);
      INT nIx;
      while ( (nIx = _tcscspn(sWork, _T("\\/:*?\"<>|"))) < nLen)
      {
        sWork[nIx] = _T('_');
      }
    } // if (lvi.lParam > -1)
    else if (lvi.lParam == N_LV_ROWTYPE_ALL)
    {
      _tcscpy(sWork, _T("All Songs"));
    }
    else // Other negative number is New songs. 
    {
      // Generate shorter name than used in list
      _stprintf(sWork, _T("New Songs %d days"), (-1 * lvi.lParam));
    }

    // If any name part chosen to be before source, is already in sBeforeName
    if (sBeforeName[0]) _tcscat(sBeforeName, _T(" "));
    _tcscat(sBeforeName, sWork);
  } // if (ini->bSrcName)

  // Assemble output path. Start with same dir (e.g. My Playlists) as source.
  // Know at least one of sBeforeName or sAfterName not empty
  sOutName[0] = _T('\0'); // Target for strcat

  if (sBeforeName[0]) _tcscat(sOutName, sBeforeName);

  if (sAfterName[0])
  {
    if (sOutName[0]) _tcscat(sOutName, _T(" "));
    _tcscat(sOutName, sAfterName);
  }

  _tcscat(sOutName, sDotExt);
} // void CMinDlg::GetDefaultSaveName(LPTSTR sOutName)
/*
// This only for custom tooltip assigned to ListView (replace default)
void CMinDlg::OnNotifyTipDispInfo(NMTTDISPINFO * pdi)
{
  m_nTest++;
  _stprintf(sBS, _T("TTN_GETDISPINFO  (occurrence %d)"), m_nTest);
  SetWindowText(m_pParent->GetWnd(), sBS);


} // void CMinDlg::OnNotifyTipDispInfo(NMTTDISPINFO * pdi)

void CMinDlg::OnNotifyAnyLv()
{
  m_nTest++;
  _stprintf(sBS, _T("OnNotifyAnyLv  (occurrence %d)"), m_nTest);
  SetWindowText(m_pParent->GetWnd(), sBS);
}
*/
// This only for ListView's own default tooltip
void CMinDlg::OnNotifyGetInfoTip(NMLVGETINFOTIP * plv)
{
          //if (plv->dwFlags & LVGIT_UNFOLDED) // Full text shown
          //{
          //  //MB(_T("ListView LVN_GETINFOTIP dwFlags LVGIT_UNFOLDED (full text)"));
          //  _stprintf(sBS, _T("(full text) %d "), m_nTest);
          //}
          //else // text doesn't fit in space (plv->dwFlags == 0, no other flag exists)
          //{
          //  //MB(_T("ListView LVN_GETINFOTIP dwFlags 0 (truncated text)"));
          //  _stprintf(sBS, _T("(truncated text) %d "), m_nTest);
          //}

  // Handling depends on whether selection has multiple playlists
  // Get info from CSongsSelection
  INT nIxLv = plv->iItem;

  LVITEM lvi;
  lvi.iItem = nIxLv;
  lvi.iSubItem = 0;
  lvi.mask = LVIF_PARAM;

  if (!ListView_GetItem(m_hList, &lvi))
  {
    _stprintf(sBS, _T("Error getting songs selection for row %d tooltip"),
              nIxLv + 1); // Don't use 0 based row index for this msg
    RET_V(sBS);
  }
  INT nLvLparam = lvi.lParam;

  if (nLvLparam < 0 || nLvLparam >= MAX_SONG_SELECTIONS)
  {
    _stprintf(sBS, _T("Invalid song selection index %d for row %d tooltip"),
              nLvLparam, nIxLv + 1); // Don't use 0 based row index for this msg
    RET_V(sBS);
  }

  CSongsSelection * pSongSel = mpArr_SongsSelections[nLvLparam];

  // If All Songs or only one item, leave to default handling: no tooltip if all
  // text visible, or if some hidden show in-place tooltip with all current text.
  if (pSongSel->GetLbSelectedIndexesCount() < 2) // Value is 0 for All Songs
    return;

  INT nMaxTip = plv->cchTextMax - 1; // as strlen, not counting end null

//  // TEST
//  m_nTest++;
//  if (plv->dwFlags & LVGIT_UNFOLDED) // Full text shown
//    _stprintf(sBS, _T("Full text, max %d (occurrence %d)"), nMaxTip, m_nTest);
//  else
//    _stprintf(sBS, _T("Truncated text, max %d  (occurrence %d)"), nMaxTip, m_nTest);
//
//m_hTipOrig = ListView_GetToolTips(m_hList); // Didn't get correctly in Initdialog?
//if (m_hTipOrig) _tcscat(sBS, _T(" Got m_hTipOrig"));
//else _tcscat(sBS, _T("  FAILED ListView_GetToolTips"));
//
//
//  SetWindowText(m_pParent->GetWnd(), sBS);

//  // Testing only - GetSelectionDisplay could return more than sBS max 4096
//  _stprintf(sBS, _T("Assigned:\n%s"), pSongSel->GetSelectionDisplay());
//  if ((INT) _tcslen(sBS) > nMaxTip) sBS[nMaxTip] = cNull;
//  _tcscpy(plv->pszText, sBS);

  m_hTipOrig = ListView_GetToolTips(m_hList);
  if (m_hTipOrig) // Set longer time visible (while mouse still over), millisecs LPARAM
    SendMessage(m_hTipOrig, TTM_SETDELAYTIME,
                (WPARAM) TTDT_AUTOPOP, (LPARAM) MAKELONG(30000, 0));

  LPCTSTR spTip = pSongSel->GetSelectionDisplay(nMaxTip);
  if (spTip) _tcscpy(plv->pszText, spTip);
  // else leave as is, i.e. show no tooltip
} // void CMinDlg::OnNotifyGetInfoTip(NMLVGETINFOTIP * plv)

void CMinDlg::OnNotifySpin(NMUPDOWN* pud)
{
  m_bEdChangeBySpinner = TRUE; // Avoid feedback loop with OnEdSepCount

  // Update associated Edit control, and static if applicable
  HWND hEdit; // = (pud->hdr.hwndFrom == m_hSpinDist) ? m_hEdDist : m_hEdSep;
  INT nMax;
  INT nEditAddToSpin;

  if (pud->hdr.hwndFrom == m_hSpinTrunc)
  {
    hEdit = m_hEdTrunc;
    nMax = 9999; // Edit max value
    nEditAddToSpin = 1;

    CheckBn(IDC_CK_TRUNC, TRUE); // User messing with setting, assume wants it checked
  }
  else if (pud->hdr.hwndFrom == m_hSpinDist)
  {
    nMax = 99;
    hEdit = m_hEdDist;
    nEditAddToSpin = 1;
    //    CheckRB(IDC_RB_REPEAT_DIST, IDC_RB_REPEAT_SEP, IDC_RB_REPEAT_DIST);
  }
  else if (pud->hdr.hwndFrom == m_hSpinMaxReps)
  {
    nMax = 99;
    hEdit = m_hEdMaxReps;
    nEditAddToSpin = 1;

    CheckBn(IDC_CK_MAX_REP, TRUE);
  }
  else if (pud->hdr.hwndFrom == m_hSpinSep) // Allow 0 sep purely random shuffle
  {
    nMax = 99;
    hEdit = m_hEdSep;
    nEditAddToSpin = 0;
//    CheckRB(IDC_RB_REPEAT_DIST, IDC_RB_REPEAT_SEP, IDC_RB_REPEAT_SEP);
  }
  else RET_V(_T("Spin notification from unknown spin control"));

  // Raw sum could be -1 or 99 for a 1 to 99 edit; keep in range 0 to 98, etc for other ranges
  INT nSpinSepCount = ((nMax + 1 - nEditAddToSpin + pud->iPos + pud->iDelta)
                       % (nMax + 1 - nEditAddToSpin));

  // If minimum edit value is 1, match spinner 0 to that, etc.
  nSpinSepCount += nEditAddToSpin;
  _stprintf(sBS, _T("%d"), nSpinSepCount);
  SetWindowText(hEdit, sBS);

  if (hEdit == m_hEdDist)
  {
    _stprintf(sBS, m_sFmtOneThirdAvgSep, nSpinSepCount * 3);
    SetWindowText(m_hRepeatTxt, sBS);
  }
  else if (hEdit == m_hEdSep)
  {
    if (nSpinSepCount)
    {
      _stprintf(sBS, m_sFmtSepOnly, nSpinSepCount * 2);
      //_stprintf(sBS, m_sFmtSepOnly, nSpinSepCount * 3);
      SetWindowText(m_hSepOnlyTxt, sBS);
    }
    else SetWindowText(m_hSepOnlyTxt, m_sSepNowPureRandom);
  }
  // else m_EdTrunc, no static to update

  m_bEdChangeBySpinner = FALSE;
}
/*
void CSelectDlg::OnNotifySpin(LPNMUPDOWN pud)
{
  m_bEdChangeBySpinner = TRUE; // Avoid feedback loop with associated edit handler

  HWND hSpin = pud->hdr.hwndFrom;
  HWND hEdit;
//  INT nEdMax = 99;   // Default correct for star ratings & same frequencies
  INT nAddToEd; // = 0;  // Default correct for star ratings
  INT nSpinMax; // = 99; // Default correct for star ratings

  if (hSpin == m_hSpinTrunc)
  {
    hEdit = m_hEdTrunc;
    nSpinMax = 9998; // Spinner max 9998 is Edit max 9999
    nAddToEd = 1;    // Spinner 0 is Edit 1
  }
  else if (hSpin == m_hSpinDist)
  {
    hEdit = m_hEdDist;
    nSpinMax = 98;   // Spinner max 98 is Edit max 99
    nAddToEd = 1;    // Spinner 0 is Edit 1
  }
  else if (hSpin == m_hSpinSep) // Allow 0 sep for purely random shuffle
  {
    hEdit = m_hEdSep;
    nSpinMax = 99;   // Spinner max 99 is Edit max 99
    nAddToEd = 0;    // Spinner 0 is Edit 0
  }
  else RET_V(_T("Spin notification from unknown spin control"));


  TCHAR sEdTxt[32];

//  // Raw sum could be -1 or 100; keep positive in range 0 to 99  <== FOR WRAPPING
//  INT nSpinCount = (nMax + pud->iPos + pud->iDelta) % (nMax + 1);

  // If goes outside min 0 or max 99, restore old value instead of wrapping
  INT nSpinCount = pud->iPos + pud->iDelta;
  BOOL bRestore = FALSE;
  if (nSpinCount < 0)
  {
    nSpinCount = 0;
    bRestore = TRUE;
  }
  if (nSpinCount > nSpinMax)
  {
    nSpinCount = nSpinMax;
    bRestore = TRUE;
  }
  if (bRestore)
  {
    SendMessage(hSpin, UDM_SETPOS, 0, MAKELONG((SHORT) nSpinCount + 1, 0));
  }

  _stprintf(sEdTxt, _T("%d"), nSpinCount + nAddToEd);
  SetWindowText(hEdit, sEdTxt);

  m_bEdChangeBySpinner = FALSE;
} // void CSelectDlg::OnNotifySpin(HWND hSpin, HWND hEdit, NMUPDOWN pud)
*/
void CMinDlg::OnEdWithSpinner(WPARAM wP, LPARAM lP)
{
  if (m_bEdChangeBySpinner ||   // Edit control was changed to match spinner
	    HIWORD(wP) != EN_CHANGE)  // Not the message we're interested in
    return;

  // Update associated spin control, and static if applicable

  HWND hEdit = GetItem(LOWORD(wP)); // m_hEdDist, m_hEdSep, or m_hEdTrunc
  HWND hSpin; // = (hEdit == m_hEdDist) ? m_hSpinDist : m_hSpinSep;
  INT nMinEdit; // = difference with 0 based spin index
  if (hEdit == m_hEdDist)
  {
    hSpin = m_hSpinDist;
    nMinEdit = 1;
  }
  else if (hEdit == m_hEdMaxReps)
  {
    hSpin = m_hSpinMaxReps;
    nMinEdit = 1;

    CheckBn(IDC_CK_MAX_REP, TRUE); // User messing with setting, assume wants it checked
  }
  else if (hEdit == m_hEdSep)
  {
    hSpin = m_hSpinSep;
    nMinEdit = 0;
  }
  else if (hEdit == m_hEdTrunc)
  {
    hSpin = m_hSpinTrunc;
    nMinEdit = 1;

    CheckBn(IDC_CK_TRUNC, TRUE);
  }
  else RET_V(_T("Update notification from unknown edit control"));

  // Text manually edited. Update spin control for new number
  // Control is digits only, max chars limits value, so only invalid values are "" & "0"
  TCHAR sNum[6];
  GetWindowText(hEdit, sNum, 5);
  INT nEdit = _tstoi(sNum); // returns 0 if empty string

  // Might be 0 because empty from delete before typing in a new value.
//  // Ignore for now except set spinner at 1, error message if still 0 at OnOk
//  if (nSpin == 0) nSpin = 1;

  if (nEdit < nMinEdit // Can only happen w/ Same or Days edit; others nMinEd = 0
      || !sNum[0])   // Empty string, reset to minimum value
  {
    nEdit = nMinEdit;
    _stprintf(sNum, _T("%d"), nEdit);
    m_bEdChangeBySpinner = TRUE; // Not really, but prevents feedback from next:
    SetWindowText(hEdit, sNum);
    m_bEdChangeBySpinner = FALSE;
  }

  // 0 to 98 (or 9998) range on spin ctl is 1 to 99 (or 9999) for edit
//  SendMessage(hSpin, UDM_SETPOS, 0, MAKELONG((SHORT) nSpin - 1, 0));
  SendMessage(hSpin, UDM_SETPOS, 0, MAKELONG((SHORT) (nEdit - nMinEdit), 0));

  if (hEdit == m_hEdDist)
  {
    _stprintf(sBS, m_sFmtOneThirdAvgSep, nEdit * 3);
    SetWindowText(m_hRepeatTxt, sBS);
  }
  else if (hEdit == m_hEdSep)
  {
    if (nEdit)
    {
      _stprintf(sBS, m_sFmtSepOnly, nEdit * 2);
      SetWindowText(m_hSepOnlyTxt, sBS);
    }
    else SetWindowText(m_hSepOnlyTxt, m_sSepNowPureRandom);
  }
  // else m_hEdTrunc, no static to update
}

//void CMinDlg::OnBnTest()
//{
//  // Keep this hidden button as default to discard Enter button
//	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?
//
//
//
//
////  TCHAR sMsg[4096];
////  sMsg[0] = '\0';
////  MB(sMsg);
//} // void CMinDlg::OnBnTest()

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

void CMinDlg::EnableControls(BOOL bNormalCtls)
{
  EnableWindow(GetItem(IDC_BN_SOURCE), bNormalCtls);
  EnableWindow(GetItem(IDC_BN_LV_EDIT), bNormalCtls);
  EnableWindow(GetItem(IDC_BN_LV_REMOVE), bNormalCtls);

  EnableWindow(GetItem(IDC_RB_REPEAT_DIST), bNormalCtls);
  EnableWindow(GetItem(IDC_RB_REPEAT_SEP), bNormalCtls);
//  EnableWindow(GetItem(IDC_RB_REPEAT_IGNORE), bNormalCtls);
  EnableWindow(m_hEdDist, bNormalCtls);
  EnableWindow(m_hEdSep, bNormalCtls);
  EnableWindow(m_hSpinDist, bNormalCtls);
  EnableWindow(m_hSpinSep, bNormalCtls);
  EnableWindow(m_hRepeatTxt, bNormalCtls);
  EnableWindow(m_hSepOnlyTxt, bNormalCtls);

  // Generic text and frame
  EnableWindow(GetItem(IDC_TXT_1), bNormalCtls);
  EnableWindow(GetItem(IDC_TXT_2), bNormalCtls);
//  EnableWindow(GetItem(IDC_TXT_3), bNormalCtls);
  EnableWindow(GetItem(IDC_TXT_4), bNormalCtls);
  EnableWindow(GetItem(IDC_FRAME_REPEAT), bNormalCtls);

  EnableWindow(m_hEdTrunc, bNormalCtls);
  EnableWindow(m_hSpinTrunc, bNormalCtls);
  EnableWindow(GetItem(IDC_FRAME_TRUNC), bNormalCtls);
  EnableWindow(GetItem(IDC_TXT_TRUNC), bNormalCtls);
  EnableWindow(GetItem(IDC_CK_TRUNC), bNormalCtls);
  EnableWindow(GetItem(IDC_CK_SAVEWARN), bNormalCtls);

  EnableWindow(m_hEdMaxReps, bNormalCtls);
  EnableWindow(m_hSpinMaxReps, bNormalCtls);
  EnableWindow(GetItem(IDC_CK_MAX_REP), bNormalCtls);
  EnableWindow(GetItem(IDC_TXT_MAX_REP), bNormalCtls);

  // Limit output to no reps may be disabled when all else enabled
  if (IsBnChecked(IDC_RB_REPEAT_DIST))
  {
    EnableWindow(m_hNoRepsCk, bNormalCtls);
    EnableWindow(m_hNoRepsTxt, bNormalCtls);
    EnableWindow(m_hNoRepsRbMin, bNormalCtls);
    EnableWindow(m_hNoRepsRbMax, bNormalCtls);
  }
  // else keep already disabled state, whether bNormalCtls or not

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

  SetWindowText(GetItem(IDC_BN_SHUFFLE),
    bNormalCtls ? _T("Save as Shuffled Playlist ...") : _T("Stop Shuffle"));
}

void CMinDlg::EnableLimitNoReps()
{
  BOOL bEnable = IsBnChecked(IDC_RB_REPEAT_DIST);

  EnableWindow(m_hNoRepsCk, bEnable);
  EnableWindow(m_hNoRepsTxt, bEnable);
  EnableWindow(m_hNoRepsRbMin, bEnable);
  EnableWindow(m_hNoRepsRbMax, bEnable);
}

void CMinDlg::Close()
{
  ini->SetDlg(m_hWnd);
  if (!ini->DlgRBsToKey(ini->uSepOption, IDC_RB_REPEAT_DIST, IDC_RB_REPEAT_SEP) ||
      !ini->DlgCheckToKey(ini->bTruncate, IDC_CK_TRUNC) ||
      !ini->DlgEditToKey(ini->nTruncSize, IDC_ED_TRUNC) ||
      !ini->DlgRBsToKey(ini->nUseMaxTruncSize, IDC_RB_TRUNC_MIN, IDC_RB_TRUNC_MAX) ||
      !ini->DlgCheckToKey(ini->bLimitReps, IDC_CK_MAX_REP) ||
      !ini->DlgEditToKey(ini->nMaxReps, IDC_ED_MAX_REP) ||
      !ini->DlgCheckToKey(ini->bTruncNoReps, IDC_CK_TRUNC_NOREP) ||
      !ini->DlgEditToKey(ini->uSepDistSize, IDC_ED_REPEAT_DIST) ||
      !ini->DlgEditToKey(ini->uSepCount, IDC_ED_REPEAT_SEP) ||
      !ini->WriteIniSection(ini->m_sectMain) )
  {
    ini->ShowError(m_sCapt, _T("Error saving settings from Shuffle dialog to ini file"));
  }

  ZDlg::OnOk();
}

void CMinDlg::OnWmPaint()
{
  //// Paint stars before default WM_PAINT handling only if part of stars area invalid
  //RECT rcInvalid;
  //if (!GetUpdateRect(m_hWnd, &rcInvalid, FALSE) ||
  //    !RectanglesOverlap(&m_rcImgStars, &rcInvalid) )
  //  return;

  HDC hdc = GetDC(m_hWnd);
  HDC image_dc = CreateCompatibleDC(hdc);
  SelectObject(image_dc, m_hnImgStars);

  BOOL bRet = TransparentBlt(
                  hdc,        // handle to destination DC
                  m_rcImgStars.left,   // x-coord of destination upper-left corner
                  m_rcImgStars.top,   // y-coord of destination upper-left corner
                  //162,  // width of destination rectangle  <== Set height/width of RECT to
                  178,  // width of destination rectangle  <== Set height/width of RECT to
                  //174,  // width of destination rectangle  <== Set height/width of RECT to
                  18,   // height of destination rectangle     same as image for OnWmPaint
                  image_dc,         // handle to source DC
                  0,    // x-coord of source upper-left corner
                  0,    // y-coord of source upper-left corner
                  //162,  // width of source rectangle   <== Same dimensions as destination
                  178,  // width of source rectangle   <== Same dimensions as destination
                  //174,  // width of source rectangle   <== Same dimensions as destination
                  18,   // height of source rectangle
                  //0xFF0000  // color to make transparent
                  RGB(255, 0, 0)  // color to make transparent
              );

  DeleteDC(image_dc);     // Won't paint on subsequent calls without this
  ReleaseDC(m_hWnd, hdc); // Mem usage grows without this
  // Don't let this paint be interpretd by parent as need to repaint same area
//  m_pParent->ValidateStarsRect();

  // Kludge - Don't want this to happen until after window is displayed
  if (!bDidFirstShow)
  {
    bDidFirstShow = TRUE;

    // Don't hold up window appearing while loading WMP library
    PostMessage(m_hWnd, WMU_AFTERSHOW, 0, 0);
  }
}

//// Public functions for ParentDlg to use
//HWND CMinDlg::GetStarsFrameHwnd()
//{
//  return GetItem(IDC_RECT_STARS);
//}

//void CMinDlg::SetFailsafePaintTimer()
//{
//  // Just in case paint that should do it is too soon, repaint later.
//  SetTimer(m_hWnd, U_TIMER_PAINT_FAILSAFE, 1500, NULL);
//}

void CMinDlg::SetPaintMonitor(BOOL bShow)
{
  if (bShow) SetTimer(m_hWnd, U_TIMER_PAINT, 100, NULL);
  else KillTimer(m_hWnd, U_TIMER_PAINT);
}

/*
void CMinDlg::OnBnTestSrcWmp()
{
  sBS[0] = _T('\0');
  //INT nDaysAgo;
  //INT nOldDate;

  //nDaysAgo = 0;
  //nOldDate = YYYYMMDD_n_days_ago(nDaysAgo);
  //_stprintf(sBS2, _T("%d days ago was %d\n"), nDaysAgo, nOldDate);
  //_tcscat(sBS, sBS2);

  //nDaysAgo = 3;
  //nOldDate = YYYYMMDD_n_days_ago(nDaysAgo);
  //_stprintf(sBS2, _T("%d days ago was %d\n"), nDaysAgo, nOldDate);
  //_tcscat(sBS, sBS2);

  //nDaysAgo = 54;
  //nOldDate = YYYYMMDD_n_days_ago(nDaysAgo);
  //_stprintf(sBS2, _T("%d days ago was %d\n"), nDaysAgo, nOldDate);
  //_tcscat(sBS, sBS2);

  //nDaysAgo = 55;
  //nOldDate = YYYYMMDD_n_days_ago(nDaysAgo);
  //_stprintf(sBS2, _T("%d days ago was %d\n"), nDaysAgo, nOldDate);
  //_tcscat(sBS, sBS2);

  //nDaysAgo = 56;
  //nOldDate = YYYYMMDD_n_days_ago(nDaysAgo);
  //_stprintf(sBS2, _T("%d days ago was %d\n"), nDaysAgo, nOldDate);
  //_tcscat(sBS, sBS2);

  //nDaysAgo = 365;
  //nOldDate = YYYYMMDD_n_days_ago(nDaysAgo);
  //_stprintf(sBS2, _T("%d days ago was %d\n"), nDaysAgo, nOldDate);
  //_tcscat(sBS, sBS2);


  //nDaysAgo = 4 *365;
  //nOldDate = YYYYMMDD_n_days_ago(nDaysAgo);
  //_stprintf(sBS2, _T("%d days ago was %d\n"), nDaysAgo, nOldDate);
  //_tcscat(sBS, sBS2);


  //nDaysAgo = 5 * 365;
  //nOldDate = YYYYMMDD_n_days_ago(nDaysAgo);
  //_stprintf(sBS2, _T("%d days ago was %d\n"), nDaysAgo, nOldDate);
  //_tcscat(sBS, sBS2);

  typedef struct Songx
  {
	  //LPTSTR sLine;   // Can be duplicated with nCount = 1, or can iterate nCount
	  //char nCount; // Still takes 4 bytes in structure <= if with just LPTSTR
	  char c2; // Takes 8 if with just dbl; up to 3 more chars "free"
	  char c3; // Takes 8 if with just dbl; up to 3 more chars "free"
	  char c4; // Takes 8 if with just dbl; up to 3 more chars "free"
	  char c5; // Takes 8 if with just dbl; up to 3 more chars "free"
	  double dbl; // sizeof(..) is multiple of largest type in structure
  } Songy;

  INT nSizeOf = sizeof(Songy);

  _stprintf(sBS2, _T("sizeof(Songy) = %d\n"), nSizeOf);
  _tcscat(sBS, sBS2);

  _stprintf(sBS2, _T("sizeof(char) = %d sizeof(wchar_t) = %d sizeof(TCHAR) = %d\n"), 
            sizeof(char), sizeof(wchar_t), sizeof(TCHAR));
  _tcscat(sBS, sBS2);

  //TCHAR sStr20[20];  // sizeof(sStr20) / sizeof(sStr20[0]) correct with declared array
  TCHAR * sStr20 = new TCHAR[20];  // sizeof(sStr20) / sizeof(sStr20[0]) wrong w pointer
  _tcscpy(sStr20, _T("1234567890"));
  _stprintf(sBS2, _T("sStr20 = '%s' (sizeof(sStr20) / sizeof(sStr20[0])) = %d\n"), 
            sStr20, (sizeof(sStr20) / sizeof(sStr20[0])));
  _tcscat(sBS, sBS2);
  delete sStr20;

  //_stprintf(sBS2, _T("300 * sizeof(Songy) = %d\n"), (300 *nSizeOf));
  //_tcscat(sBS, sBS2);

  _stprintf(sBS2, _T("RAND_MAX = %d\n"), RAND_MAX);
  _tcscat(sBS, sBS2);
  
  //_stprintf(sBS2, _T("(RAND_MAX << 15) = %d\n"), (RAND_MAX << 15));
  //_tcscat(sBS, sBS2);


  _stprintf(sBS2, _T("RAND_MAX + (RAND_MAX << 15) = %d\n"), RAND_MAX + (RAND_MAX << 15));
  _tcscat(sBS, sBS2);



  UINT uSeed = (UINT) GetTickCount();
  srand(uSeed);
  UINT uLow = rand();
  UINT uHigh = rand();
  UINT uHighShift = (uHigh << 15);
  UINT uLowShift = (uLow << 15);

  _stprintf(sBS2, _T("uLow = %u  uHigh = %u  Sum = %u  uLow + uHighShift = %u  uHigh + uLowShift = %u\n"),
            uLow, uHigh, uLow + uHigh, uLow + uHighShift, uHigh + uLowShift);
  _tcscat(sBS, sBS2);

  //srand(0);
  //uLow = rand();
  //uHigh = rand();
  //uHighShift = (uHigh << 15);

  //_stprintf(sBS2, _T("uLow = %u  uHigh = %u  Sum = %u  uHighShift = %u  Combined = %u\n"),
  //          uLow, uHigh, uLow + uHigh, uHighShift, uLow + uHighShift);
  //_tcscat(sBS, sBS2);

  srand(uSeed);
  _stprintf(sBS2, _T("(rand() + (rand() << 15)) = %u\n"),
            (rand() + (rand() << 15)) );
  _tcscat(sBS, sBS2);

  srand(uSeed);
  _stprintf(sBS2, _T("((rand() << 15) + rand()) = %u\n"),
            ((rand() << 15) + rand()) );
  _tcscat(sBS, sBS2);

  UINT u1, u2;

  srand(uSeed);
  _stprintf(sBS2, _T("((u1 = rand()) + (u2 = rand() << 15)) = %u\n"),
            ((u1 = rand()) + (u2 = rand() << 15)) );
  _tcscat(sBS, sBS2);

  srand(uSeed);
  _stprintf(sBS2, _T("(u1 = (rand() << 15) + (u2 = rand())) = %u\n"),
            ((u1 = rand() << 15) + (u2 = rand())) );
  _tcscat(sBS, sBS2);


  srand(uSeed);
  _stprintf(sBS2, _T("(rand() + rand()) = %u\n"),
            (rand() + rand()) );
  _tcscat(sBS, sBS2);


  srand(uSeed);
  _stprintf(sBS2, _T("rand() = %u\n"),
            rand());
  _tcscat(sBS, sBS2);


  
  _stprintf(sBS2, _T("(1 << 15) = %d\n"), (1 << 15));
  _tcscat(sBS, sBS2);

  _stprintf(sBS2, _T("(128 << 15) = %d\n"), (128 << 15));
  _tcscat(sBS, sBS2);

  _stprintf(sBS2, _T("(256 << 15) = %d\n"), (256 << 15));
  _tcscat(sBS, sBS2);



  MB(sBS);
} // void CMinDlg::OnBnTestSrcWmp()
*/

/*
void CMinDlg::OnBnTest()
{
//  TCHAR sSearchArt1[MAX_PATH];

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

//  LPTSTR sSearchDir = _T("D:\\Temp\\Test unicode in folder name\\Tommy heavenly6");
  TCHAR sSearchDir[MAX_PATH];
  _tcscpy(sSearchDir, _T("D:\\Temp\\Test unicode in folder name\\Tommy heavenly6"));
  TCHAR sSearch[MAX_PATH];
  _stprintf(sSearch, _T("%s\\*"), sSearchDir);

  WIN32_FIND_DATA found;
  HANDLE hFind;
 
  hFind = FindFirstFile(sSearch, &found);
  if (hFind == INVALID_HANDLE_VALUE)
    RET_V(_T("Nothing found for search pattern:"));

  BOOL bFound = TRUE;

  TCHAR sWrite[10000];

  while (bFound) // One Album folder per inner loop
  {
    // 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;
    }

    _tcscat(sWrite, found.cFileName);
    _tcscat(sWrite, _T("\r\n"));

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

  // Convert to UTF-8 and write to file
  TCHAR sOutPath[1000];
  _stprintf(sOutPath, _T("%s\\test_out.txt"), sSearchDir);

  HANDLE hFile = CreateFile(sOutPath, GENERIC_WRITE, FILE_SHARE_READ, NULL,
                      CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

  BOOL bGotSysMsg;
  if (hFile == INVALID_HANDLE_VALUE)
  {
    bGotSysMsg = GetSysErrorMsg(sBS);
    _stprintf(sBS2, _T("Error opening file for write:\r\n%s\r\n%s%s"),
              sOutPath,
              bGotSysMsg ? _T("Windows' error message was:\r\n") : _T(""),
              sBS);
    RET_V(sBS2);
  }

  INT nTotalBytes = 0;

  INT nBytes;
  DWORD dwBytesWritten;

  CHAR sAnsi[10000]; // .m3u from .wpl must convert UTF8 to Ansi for file path chars
                    // Now with UTF-16 TCHAR, must convert that to ANSI
  LPCVOID pvWrite;  // Will point to string from SongArray, or to Ansi conversion


//    if (!Utf16ToAnsi(sWrite, sAnsi))
    if (!Utf16ToUtf8((LPCWSTR) sWrite, sAnsi))
    {
      GetSysErrorMsg(sBS);
//       _stprintf(sBS2, _T("Error converting song path from UTF-16 to ANSI\n%s"),
     _stprintf(sBS2, _T("Error converting song path from UTF-16 to UTF-8\n%s"),
                sBS);
      RET_V(sBS2)
    }
    nBytes = strlen(sAnsi); // Not _tcslen
    pvWrite = sAnsi;

//    if (!WriteFile(hFile, songarr.GetSong(upSongIndArr[u]),
    if (!WriteFile(hFile, pvWrite,
                    (DWORD) nBytes, &dwBytesWritten, NULL))
    {
      bGotSysMsg = GetSysErrorMsg(sBS);
      _stprintf(sBS2,
                _T("Error writing song to file:\n%s"),
                sOutPath,
                bGotSysMsg ? _T("Windows' error message was:\n") : _T(""),
                sBS);
      RET_V(sBS2);
    }

    CloseHandle(hFile);

    MB(_T("Finished writing file:"), sOutPath, sWrite);

} // OnBnTest
*/
/*
void CMinDlg::OnBnTest()
{
  if (!GetVersionNumStr(sBS))
    RET_V2(_T("GetVersionNumStr error"), m_sError);
  _stprintf(sBS2, _T("Version number is '%s'"), sBS);
  MB(sBS2);
}
*/
