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

// SelectDlg.cpp: implementation of the CSelectDlg class.
//
//////////////////////////////////////////////////////////////////////

#include "Headers.h"
#include "resource.h"
#include "Settings.h"
#include "SelectDlg.h"
#include "MinDlg.h"
#include "SongsSelection.h"
#include <VersionHelpers.h>


CSelectDlg * pSelectDlg;

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

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

// Constructor for adding playlist or song group to list
CSelectDlg::CSelectDlg(HINSTANCE hInst, CSettings * pSet,
								       LPCTSTR sCapt1, LPCTSTR sCapt2, ZWin * pOwner,
                       IWMPPlaylistCollection * pPlaylistColl,
                       IWMPPlaylistArray *  pPlaylistArr,
                       IWMPMediaCollection * pMediaCollection,
                       CSongsSelection * pSongSel)
	: ZDlg(hInst, sCapt1, sCapt2, pOwner)
{
	ini = pSet;

  m_uID = IDD_SELECT_SONGS;
//  m_uID = IDD_SELECT_SONGS_OLD;  // For testing hash in CSongArray

  // 3.0 No longer based on which constructor, use m_pSongsSel as opposite boolean
  // m_bIsSelect = TRUE;
  m_pSongsSel = pSongSel; // NULL if new selection

  m_cpWmpPlaylistArr = pPlaylistArr;
  m_cpWmpPlaylistColl = pPlaylistColl;
  m_cpWmpAllMedia = pMediaCollection;

// 3.0 No longer have two constructors. Copy CSelectDlg_Common to here
	pSelectDlg = this;
	m_pProc = SelectDlgProc;

// 3.0 Now using listbox for both new and update selection
  m_bListsStarted = FALSE;

  m_bEdChangeBySpinner = FALSE;
  uRetGroupType = SEL_TYPE_INVALID; // 3.0 SongsSelection.h

  // Not needed by both, but do here if don't need different values
  m_b1stListboxLoad = ((CMinDlg*) m_pOwner)->IsFirstSelectCall();

  // END COPY OLD CSelectDlg_Common

  // 3.0
  // Initialize array so Genres and unused indicate not playlist
  for (INT i = 0; i < MAX_SELDLG_LB_ITEMS; i++) m_nArrLbIxPlayArrIx[i] = -1;

//  m_pSongsSel = NULL;
  m_bSongsSelClaimed = FALSE;
  scPrefixGenre = _T("Genre: ");
  scPrefixPlist = _T("Playlist: ");

  // Shouldn't matter, play safe // 3.0 no longer used
  // m_sPlaylistToChangeReps[0] = cNull;
}

CSelectDlg::~CSelectDlg()
{
  // Delete CSongsSelection unless parent MinDlg has taken it
  if (m_pSongsSel && !m_bSongsSelClaimed) delete m_pSongsSel;
  // Never delete m_pSongsSel; caller who supplied it will handle.
}

//void CSelectDlg::OnDestroy() // OnClose not called by OK
//{
//}

BOOL CSelectDlg::OwnProc(HWND hDlg, UINT uMsg, WPARAM wP, LPARAM lP)
{
  NMUPDOWN* pud = NULL;
	switch (uMsg)
	{
    case WM_NOTIFY:
      pud = (NMUPDOWN*) lP;
      if (pud->hdr.code == UDN_DELTAPOS
//          && (pud->hdr.hwndFrom == m_hSpinDist || pud->hdr.hwndFrom == m_hSpinSep
//              || pud->hdr.hwndFrom == m_hSpinTrunc)
         )
      {
        OnNotifySpin(pud); // Same handler for all, checks which control
        // Then drop through to return 0;
      }
      break;

		// Insert DTAKEMSG(WM_WHATEVER, OnWhatever); except for WM_COMMAND
    DSEEMSG(WM_PAINT, OnWmPaint);

    DSEEMSG(WMU_LOADLIST, LoadListBox); // WMU_LOADLIST only once on startup

    case WM_SHOWWINDOW:
      // This condition catch all for run once after init dlg
      //if (m_bIsSelect && !m_bListsStarted)
      if (!m_bListsStarted) // 3.0 No longer require && m_bIsSelect
      {
        m_bListsStarted = TRUE;
        PostMessage(m_hWnd, WMU_LOADLIST, 0, 0);
        OnFrequencyRadioButton(); // Enable/disable relevant controls for whatever selected
      }
//      else EnableStarsCtls(); // OnInitDlg might check nondefault "Same" RB
      else OnFrequencyRadioButton(); //  Includes old EnableStarsCtls actions
      break;


// Don't bitch about no cases in WM_COMMAND's switch(wP) (warning 4065)
#pragma warning(push)
#pragma warning(disable : 4065)
		case WM_COMMAND:
			switch(LOWORD(wP))
			{
				// Insert DTAKECMD(IDC_BN_ETC, OnEtc);
				DTAKECMD(IDCS_BN_RESET, InitRepsSpinEdit);

        // OnFrequencyRadioButton includes old EnableStarsCtls, plus Exclude related
        DTAKECMD(IDCS_RB_STARS, OnFrequencyRadioButton);
        DTAKECMD(IDCS_RB_SAME, OnFrequencyRadioButton);
        DTAKECMD(IDCS_RB_EXCLUDE, OnFrequencyRadioButton);

        DSEECMD(IDCS_RB_SEL_ALL, ResetLbForRbAllSongs);

        // Same handler for all these edits. Update matching spin
        DSEECMDPARMS(IDCS_ED_SAME, OnEdReps);
        DSEECMDPARMS(IDCS_ED_STARS1, OnEdReps);
        DSEECMDPARMS(IDCS_ED_STARS2, OnEdReps);
        DSEECMDPARMS(IDCS_ED_STARS3, OnEdReps);
        DSEECMDPARMS(IDCS_ED_STARS4, OnEdReps);
        DSEECMDPARMS(IDCS_ED_STARS5, OnEdReps);
        DSEECMDPARMS(IDCS_ED_DAYS, OnEdReps); // Also checks its RB

        // Check associated RB if sel change; leave the rest to default Win proc
        DSEECMDPARMS(IDCS_LB_SONGGROUPS, OnListBox);
        //DSEECMDPARMS(IDCS_LB_AUTO, OnListBox);

        //DSEECMD(IDCS_CK_ALLPLAYLISTS, OnCheckBoxNoFilter);

        //// Radio buttons only in the Select dlg
        //DTAKECMD(IDCS_RB_SEL_ALL, EnableSelectCtls);
        //DTAKECMD(IDCS_RB_SEL_DAYS, EnableSelectCtls);
        //DTAKECMD(IDCS_RB_SEL_LIST, EnableSelectCtls);
        //DTAKECMD(IDCS_RB_SEL_AUTO, EnableSelectCtls);

				default : break;

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

		default : break;
	}

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


BOOL CSelectDlg::OnInitDialog()
{
  TCHAR sTxt[32];

	SendMessage(m_hWnd, WM_SETICON, TRUE,
							(LPARAM) LoadIcon(m_hInst, MAKEINTRESOURCE(IDI_MAIN)));

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

  m_hnImgStars = LoadImage(m_hInst, MAKEINTRESOURCE(IDB_STARS_SPIN), IMAGE_BITMAP,
                           0, 0, LR_DEFAULTCOLOR); // LR_LOADTRANSPARENT);

  SetWindowText(m_hWnd, m_sCaptLong); // Both versions

  // 3.0 Now use same dialog for new selection or edit selection
  // Move some initializations to here for always, formerly in if (m_bIsSelect)
  m_hLbSongGroups = GetItem(IDCS_LB_SONGGROUPS);
  m_hDaysSpin = GetItem(IDCS_SPIN_DAYS);
  m_hDaysEd = GetItem(IDCS_ED_DAYS);
  m_hDaysTxt = GetItem(IDCS_TXT_DAYS);

  SendMessage(m_hDaysSpin, UDM_SETRANGE, 0, MAKELONG(998, 0)); // Map to 999,1
  SendMessage(m_hDaysEd, EM_LIMITTEXT, 3, 0); // Already digits only

  // v.3 Here don't know if ini->nDaysNew (now INT) is days-ago or YYYYMMDD
//  _stprintf(sTxt, _T("%u"), ini->uDaysNew);
  INT nDaysAgo = ini->nDaysNew; // If not already days-ago, will convert

  // If 8 digits starting with 2, assume YYYYMMDD, attempt days-ago conversion
  if ((nDaysAgo / 10000000) == 2) // Truncated INT division
  {
    nDaysAgo = IntYYYYMMDD_to_N_DaysAgo(nDaysAgo);
    // Returns 0 if invalid YYYYMMDD (or if YYYYMMDD is illegally today).
    // Handle 0 or negative in same test w/ unconverted nDaysAgo further down.
    if (nDaysAgo > 999)
    {
      // Date stored was presumably valid at the time, i.e 999 days ago
      // or later, but now is too old. Set to earliest date allowed.
      nDaysAgo = 999;
    }
  } // if (nDaysAgo / 10000000 = 2)

  // Check for valid value, whether as is or after YYYYMMDD conversion
  if (nDaysAgo < 1 || nDaysAgo > 999)
  {
    nDaysAgo = NDAYSNEW_DEFAULT;
    _stprintf(sBS, _T("Invalid saved entry for new songs maximum age \"%d\"\n")
                   _T("Using default maximum %d"),
             ini->nDaysNew, nDaysAgo);
    fnErr(sBS);
  }

  _stprintf(sTxt, _T("%d"), nDaysAgo);

  SetWindowText(m_hDaysEd, sTxt);
  SetNewSongsDateText(nDaysAgo);
  SendMessage(m_hDaysSpin, UDM_SETPOS, 0,
              MAKELONG((SHORT) (nDaysAgo - 1), 0));

  // END MOVED FROM if (m_bIsSelect)

  // Controls in IDD_SELECT_SONGS but not in the set frequency dialog
  if (!m_pSongsSel)
  {
     // Handler for edit control checked its RB when set above. Set default all songs
    CheckRadioButton(m_hWnd, IDCS_RB_SEL_ALL, IDCS_RB_SEL_LB, IDCS_RB_SEL_ALL);

    // 3.0  With possible multiple playlists selected, dedup by default
    CheckBn(IDCS_CK_UNIQ, TRUE);

    // Handler for edit control checked its box when set above. Uncheck.
    CheckBn(IDCS_CK_LIMIT_NEW, FALSE);
  }
  else // Update existing selection
  {
    if (m_pSongsSel->IsAllSongs())
      CheckRadioButton(m_hWnd, IDCS_RB_SEL_ALL, IDCS_RB_SEL_LB, IDCS_RB_SEL_ALL);
    else
      CheckRadioButton(m_hWnd, IDCS_RB_SEL_ALL, IDCS_RB_SEL_LB, IDCS_RB_SEL_LB);

    // 3.0 Also change age limit and dedup settings from default to old selection
    CheckBn(IDCS_CK_UNIQ, m_pSongsSel->IsDedup());

    INT nAgeLimit = m_pSongsSel->GetMaxAge();
    if (nAgeLimit) // 0 if no age limit, then leave default
    {
      CheckBn(IDCS_CK_LIMIT_NEW, TRUE);
      _stprintf(sBS, _T("%d"), nAgeLimit);
      SetWindowText(m_hDaysEd, sBS);
      SetNewSongsDateText(nAgeLimit);
      SendMessage(m_hDaysSpin, UDM_SETPOS, 0, MAKELONG((SHORT) (nAgeLimit - 1), 0));
    }
    else CheckBn(IDCS_CK_LIMIT_NEW, FALSE);
  }

  // Store repeat frequencies in array. These are either the ini file
  // defaults or last used depending on ini->uRememberReps,
  // values set during start up in Minimal.cpp
  UINT naReps[6];
  naReps[0] = ini->nRepsSame; // Option all songs same frequency
  naReps[1] = ini->nReps1;    // Frequency for song ratings, 1-5 stars
  naReps[2] = ini->nReps2;
  naReps[3] = ini->nReps3;
  naReps[4] = ini->nReps4;
  naReps[5] = ini->nReps5;

  // May change this if m_pSongsSel
  CheckRadioButton(m_hWnd, IDCS_RB_STARS, IDCS_RB_EXCLUDE, IDCS_RB_STARS);

  // If changing frequency for existing item, change SOME of the above
  if (m_pSongsSel)
  {
    // 3.0 Leave default repeat frequencies & dedup option if exclusion list
    if (m_pSongsSel->IsExclude()) // m_pSongsSel->m_naRepCounts[0] == -1
    {
      CheckRadioButton(m_hWnd, IDCS_RB_STARS, IDCS_RB_EXCLUDE, IDCS_RB_EXCLUDE);
    }
    else
    {
      // 3.0 Replace member m_uaOldReps
      const INT * nOldReps = m_pSongsSel->GetRepCounts();
      // Whichever NOT currently set (constant or rating based), leave as default
      if (nOldReps[0]) // Same frequency for all songs
      {
        naReps[0] = nOldReps[0];
        CheckRadioButton(m_hWnd, IDCS_RB_STARS, IDCS_RB_EXCLUDE, IDCS_RB_SAME);
        // Leave ratings based frequencies uaReps[1 to 5] as defaults set above
      }
      else // Frequency varies by song rating, indicated by m_uaOldReps[0] = 0
      {
        for (INT i = 1; i <= 5; i++)
        {
          naReps[i] = nOldReps[i];
        }
        // Radio button already checked as default
        // Leave all songs frequency uaReps[0] as default set above
      }
    } // else, i.e. ! m_pSongsSel->IsExclude()

    // OnFrequencyRadioButton here fails to enable/disable, do on show window
  } // if (m_pSongsSel) i.e. old if (!m_bIsSelect)

  for (INT i = 0; i <= 5; i++) // i = 1 to 5 is star ratings, i=0 same frequency for all
  {
    m_haSpins[i] = GetItem(IDCS_SPIN_SAME + i);
    SendMessage(m_haSpins[i], UDM_SETRANGE, 0, MAKELONG((i ? 99 : 98), 0));

    m_haEds[i] = GetItem(IDCS_ED_SAME + i);
    SendMessage(m_haEds[i], EM_LIMITTEXT, 2, 0); // Already digits only
    _stprintf(sTxt, _T("%d"), naReps[i]);
    SetWindowText(m_haEds[i], sTxt);

    SendMessage(m_haSpins[i], UDM_SETPOS, 0,
                MAKELONG((SHORT) (naReps[i] - (i ? 0 : 1)), 0));
  }

  // Change if don't want CenterWnd and caption m_sCaptLong
//  return ZDlg::OnInitDialog(); // always TRUE
  // Window title was set conditional on m_bIsSelect

  CenterWnd(m_hOwner);

	return TRUE;

} // BOOL CSelectDlg::OnInitDialog()

void CSelectDlg::OnFrequencyRadioButton()
{
  BOOL bEnable = IsBnChecked(IDCS_RB_STARS);
  for (INT i = 1; i <= 5; i++) // 1-5 is star ratings, 0 is same frequency for all
  {
    EnableWindow(m_haEds[i], bEnable);
    EnableWindow(m_haSpins[i], bEnable);
  }

  // Now with 3 radio buttons, can't infer IDCS_RB_SAME state IDCS_RB_STARS
  bEnable = IsBnChecked(IDCS_RB_SAME);
  EnableWindow(m_haEds[0], bEnable);
  EnableWindow(m_haSpins[0], bEnable);

  // New RB in 3.0
  if (IsBnChecked(IDCS_RB_EXCLUDE))
  {
    // Option to ignore repetitions is irrelevant
    EnableItem(IDCS_CK_UNIQ, FALSE);
    SetItemText(IDCS_TXT_UNIQ,
        _T("Repeat occurrences irrelevant when option to exclude is selected"));
  }
  else
  {
    EnableItem(IDCS_CK_UNIQ, TRUE);
    SetItemText(IDCS_TXT_UNIQ, // Restore original text from .rc
                _T("(for example, if you select multiple playlists")
                _T(" and the same song is in two playlists)"));
  }
}

// If user selects an item in the list box, assume it means that's to be used
// for the song selection, so set the radio button if not already set
void CSelectDlg::OnListBox(WPARAM wP, LPARAM lP)
{
  if (HIWORD(wP) != LBN_SELCHANGE) return;

  // Most times the RB will already be selected, OK if redundant
  CheckRadioButton(m_hWnd, IDCS_RB_SEL_ALL, IDCS_RB_SEL_LB, IDCS_RB_SEL_LB);
}

void CSelectDlg::ResetLbForRbAllSongs()
{
  // Remove LB selection, misleading visual cue about which RB actually selected
//  SendMessage(m_hLbSongGroups, LB_SETCURSEL, -1, 0); // only for single select LB's
  SendMessage(m_hLbSongGroups, LB_SELITEMRANGE, 0, MAKELPARAM(0,-1));
}

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

  HWND hSpin = pud->hdr.hwndFrom;
  HWND hEdit = NULL;
//  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
  for (INT i = 0; i <= 5; i++) // 1-5 is star ratings, 0 is same frequency for all
  {
    if (hSpin == m_haSpins[i])
    {
      hEdit = m_haEds[i];
      if (!i)
      {
        nSpinMax = 98; // Spinner max 98 is Edit max 99
        nAddToEd = 1;  // Spinner 0 is Edit 1
      }
      break;
    }
  }
  if (!hEdit) 
  {
    if (hSpin == m_hDaysSpin) // 3.0 no longer && m_bIsSelect
    {
      hEdit = m_hDaysEd;
      nSpinMax = 998; // Spinner max 998 is Edit max 999
      nAddToEd = 1;   // Spinner 0 is Edit 1
      CheckBn(IDCS_CK_LIMIT_NEW, TRUE);
    }
    else
    {
      // Not set above, must be some spinner not relevant here
      m_bEdChangeBySpinner = FALSE;
      return;
    }
  }

  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);

  if (hSpin == m_hDaysSpin) SetNewSongsDateText(nSpinCount + 1);

  m_bEdChangeBySpinner = FALSE;
} // void CSelectDlg::OnNotifySpin(HWND hSpin, HWND hEdit, NMUPDOWN pud)

void CSelectDlg::OnEdReps(WPARAM wP, LPARAM lP) // Misleading name, also Days
{
  if (m_bEdChangeBySpinner ||   // Edit control was changed to match spinner
	    HIWORD(wP) != EN_CHANGE)  // Not the message we're interested in
    return;

  HWND hEdit = GetItem(LOWORD(wP));
  // Find spin control associated with this edit
  HWND hSpin = NULL;
  INT nMinEd = 0;
  for (INT i = 0; i <= 5; i++) // 1-5 is star ratings, 0 is same frequency for all
  {
    if (hEdit == m_haEds[i])
    {
      hSpin = m_haSpins[i];
      if (!i) nMinEd = 1;  // Edit value associated with spin value 0
      break;
    }
  }

  if (!hSpin // Not found in loop above, must be only other edit control linked to a spinner
      && hEdit == m_hDaysEd) // Reality check, should always be true
  {
    hSpin = m_hDaysSpin;
    nMinEd = 1;  // Edit value associated with spin value 0

    CheckBn(IDCS_CK_LIMIT_NEW, TRUE); // User messing with setting, assume wants it checked
  }
  if (!hSpin) // Not set in loop above, don't know why, give up
    RET_V(_T("Can't find spin control associated with changed edit control"));

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

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

  if (hSpin == m_hDaysSpin) SetNewSongsDateText(nSpin);

             // Why have to add 1 to match ed here & in Init?
//  SendMessage(hSpin, UDM_SETPOS, 0, MAKELONG((SHORT) nSpin + 1, 0));
  // Huh now don't add, after fiddling with no wrap??
  SendMessage(hSpin, UDM_SETPOS, 0, MAKELONG((SHORT) nSpin - nMinEd, 0));
} // void CSelectDlg::OnEdReps(WPARAM wP, LPARAM lP)

void CSelectDlg::InitRepsSpinEdit()
{
  // This called only by by Reset to Defaults button, no longer by startup
  m_bEdChangeBySpinner = FALSE; // Should already be, be sure

  // Init spin/edit controls.
  // 6 spins & edits, each group w/ consecutive ID's
  CheckRadioButton(m_hWnd, IDCS_RB_STARS, IDCS_RB_EXCLUDE, IDCS_RB_STARS);
//  EnableStarsCtls();
  OnFrequencyRadioButton(); // Includes old EnableStarsCtls actions

  // Reload ini settings from file.
  if (!ini->ReadIniSection(ini->m_sectPref))
    RET_V(_T("Error reading from ini file to reset to defaults"));

  UINT naRepsDef[6];

  // 3.0 Fix this to reset to global defaults; formerly just last selection
//  naRepsDef[0] = ini->nRepsSame; // Option all songs same frequency
//  naRepsDef[1] = ini->nReps1;    // Frequency for song ratings, 1-5 stars
//  naRepsDef[2] = ini->nReps2;
//  naRepsDef[3] = ini->nReps3;
//  naRepsDef[4] = ini->nReps4;
//  naRepsDef[5] = ini->nReps5;
//  naRepsDef[0] = ini->nRepsSame; // Option all songs same frequency
  naRepsDef[1] = ini->nReps1Default;    // Frequency for song ratings, 1-5 stars
  naRepsDef[2] = ini->nReps2Default;
  naRepsDef[3] = ini->nReps3Default;
  naRepsDef[4] = ini->nReps4Default;
  naRepsDef[5] = ini->nReps5Default;
  naRepsDef[0] = ini->nRepsSameDefault; // Option all songs same frequency

  TCHAR sTxt[32];
  for (INT i = 0; i <= 5; i++) // 1-5 is star ratings, 0 is same frequency for all
  {
    //SendMessage(m_haSpins[i], UDM_SETRANGE, 0, MAKELONG((i ? 99 : 98), 0));

    // Let edit control set spinner, by leaving m_bEdChangeBySpinner FALSE
//    _stprintf(sTxt, _T("%u"), m_uaRepsDef[i]);
    _stprintf(sTxt, _T("%d"), naRepsDef[i]);
    SetWindowText(m_haEds[i], sTxt);
  }
}

// Returns date INT like 20081231 for comparing to similarly generated number
BOOL CSelectDlg::Mon_DD_YYYY_n_days_ago(INT nDaysAgo, LPTSTR sDate)
{
  static LPCTSTR sMons[12] = { _T("Jan"), _T("Feb"), _T("Mar"), _T("Apr"),
                               _T("May"), _T("Jun"), _T("Jul"), _T("Aug"),
                               _T("Sep"), _T("Oct"), _T("Nov"), _T("Dec") };

  __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);

  _stprintf(sDate, _T("%s %02d, %d"), sMons[ptm_then_local->tm_mon],
            ptm_then_local->tm_mday, (ptm_then_local->tm_year + 1900));

  return TRUE;
}

BOOL CSelectDlg::SetNewSongsDateText(INT nDaysAgo)
{
  TCHAR sDateStr[32];
  TCHAR sFullStr[128];

  if (!Mon_DD_YYYY_n_days_ago(nDaysAgo, sDateStr))
  {
    SetWindowText(m_hDaysTxt, _T(""));
    return FALSE;
  }

  _stprintf(sFullStr, _T("(added on or after %s)"), sDateStr);
  SetWindowText(m_hDaysTxt, sFullStr);
  return TRUE;
}

void CSelectDlg::LoadListBox()
{
  INT nGenres;
  INT nPlaylists;

  if (ini->nListboxOrder == RB_GENRES_FIRST)
  {
    nGenres = ListBoxAppendGenres();
    nPlaylists = ListBoxAppendPlaylists();
  }
  else
  {
    nPlaylists = ListBoxAppendPlaylists();
    nGenres = ListBoxAppendGenres();
  }

  _stprintf(sBS, _T("%d Genres and %d Playlists"), nGenres, nPlaylists);
  SetItemText(IDCS_TXT_LB_COUNT, sBS);

  // If updating old selection, select LB items if previously selected
  if (!m_pSongsSel) return; // Is new selection
  if (m_pSongsSel->IsAllSongs()) return; // No LB items selected

  // Here need to select the old items in listbox.
  // Works to simply select same listbox indexes as before, because
  // any new shuffled playlists aren't added to initial playlist array.
  // May need to change this if later version updates the array.
  INT nSelected = m_pSongsSel->GetLbSelectedIndexesCount();
  const INT * nArrSelected = m_pSongsSel->GetLbSelectedIndexesArray();

  if (!nSelected || !nArrSelected)
  {
    _stprintf(sBS, _T("Error getting old list box selection, %d items"), nSelected);
    RET_V(sBS);
  }

  for (INT i = 0; i < nSelected; i++)
  {
    if (SendMessage(m_hLbSongGroups, LB_SETSEL, TRUE, nArrSelected[i]) == LB_ERR)
    {
      _stprintf(sBS, _T("Error selecting item %d in playlists/genres"),
                nArrSelected[i]);
      RET_V(sBS);
    }
  }

} // void CSelectDlg::LoadListBox()

INT CSelectDlg::ListBoxAppendGenres()
{
  HRESULT hr = E_POINTER; // Value in WMP example ShowStringCollection. What for?
  CComPtr<IWMPStringCollection> spStrColl;
  CComBSTR cbsGenreName;
  long nlCount;

  USES_CONVERSION;

  hr = m_cpWmpAllMedia->getAttributeStringCollection(CComBSTR(_T("Genre")),
                                            CComBSTR(_T("Audio")), &spStrColl);

  if (!SUCCEEDED(hr))
//    RET_V(_T("Can't load genres, failed getAttributeStringCollection"));
    return (INT) fnErr(_T("Can't load genres, failed getAttributeStringCollection"));

  nlCount = 0;
  if(SUCCEEDED(hr) && (spStrColl.p != NULL)) // p is CComPtrBasepointer data member var.
  {
    hr = spStrColl->get_count(&nlCount);
  }

  if (!SUCCEEDED(hr))
//    RET_V(_T("Can't load genres, failed get_count of AttributeStringCollection"));
    return (INT)
      fnErr(_T("Can't load genres, failed get_count of AttributeStringCollection"));

  if (!nlCount)
//    RET_V(_T("No genres found (Must have at least one if any songs in collection!)"));
    return (INT)
      fnErr(_T("No genres found (Must have at least one if any songs in collection!)"));

//  INT nLbIx = 0; // Same as loop i below unless skip some bad genres
  // May already have Playlists in listbox, get next LB index. 0 if Genres first.
  INT nLbIx = (INT) SendMessage(m_hLbSongGroups, LB_GETCOUNT, 0, 0);
  if (nLbIx == LB_ERR)
    return (INT) fnErr(_T("Error getting next listbox index for genres"));
  INT nInserted = 0;

  for(INT i = 0; i < nlCount; i++) // nlCount is of genres in spStrColl
  {
    hr = spStrColl->item(i, &cbsGenreName);
    if(SUCCEEDED(hr) && (cbsGenreName.m_str != NULL))
    {
      // Add a node for the attribute value
      if(cbsGenreName.Length() != 0)
      {
//        SendMessage(m_hLbStaticPlaylists, LB_INSERTSTRING,
//                    nLbIx, (LPARAM) OLE2T(cbsGenreName) );
        _stprintf(sBS, _T("Genre: %s"), OLE2T(cbsGenreName));
        SendMessage(m_hLbSongGroups, LB_INSERTSTRING, nLbIx, (LPARAM) sBS);

//        m_nArrLbStaticIxGenreArrIx[nLbIx] = i; // To match with playlist arr ix
        nLbIx ++; // For next insert
        nInserted ++; // For return. Same as nLbIx if genres first
      }
      // else ignore individual genre error

      cbsGenreName.Empty();
    } // if(SUCCEEDED(hr) && (bstrName.m_str != NULL))
    // else ignore individual genre error
  } // for(INT i = 0; i < nlCount; i++)

//  // Next (unused) listbox index is count of items
//  _stprintf(sBS, _T("%d Genres shown. Use Ctrl-Click to select multiple."), nLbIx);
//  SetItemText(IDCS_TXT_LB_COUNT, sBS);

  // 3.0 change: caller will set text with both counts, genres and playlists
  return nInserted;
} // INT CSelectDlg::ListBoxAppendGenres()

INT CSelectDlg::ListBoxAppendPlaylists()
{
  if (ini->bIncludeAutoPlists) return ListBoxAppendPlistsNoFilter();
  else return ListBoxAppendPlistsFiltered();
}

// This was void LoadListBoxesFiltered
// APPEND (3.0 change) to LB of user playlists, filter out All Users playlists
INT CSelectDlg::ListBoxAppendPlistsFiltered()
{
  DWORD dwStart = GetTickCount();

/*
// ==> REPLACE All Users profile path w/ registry's ObfuscatedSyncPlaylistsPath
  TCHAR sAllUsersRaw[MAX_PATH]; // All Users' music folder
  TCHAR sAllUsers[MAX_PATH];
	LPTSTR sKey =
    _T("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders");

  if (!ReadReg(HKEY_LOCAL_MACHINE, sKey, _T("CommonMusic"), sAllUsersRaw, FALSE))
  {
    fnErr(_T("Non-fatal error finding All Users music folder."),
          _T("Playlists will not be limited to user created only"));
  }
  else // Normal case, got CommonMusic folder
  {
    // May have environment strings %ALLUSERSPROFILE% (XP) or %PUBLIC% (Win7/Vista)
    // N.B. Can't use %ALLUSERSPROFILE% in Win7 is like C:\ProgramData
    if (!ExpandEnvironmentStrings(sAllUsersRaw, sAllUsers, MAX_PATH - 1))
    {
      fnSysErr(_T("Non-fatal error expanding Windows' All Users variable."),
               sAllUsersRaw,
               _T("Playlists will not be limited to user created only"));
    }
    else
    {
      // sAllUsers has something like "C:\Users\Public\Music" (Vista)
      // or "C:\Documents and Settings\All Users\Documents\My Music" (XP)
      // Windows XP plays games so same Documents dir can be referred to
      // as "My Documents or "Shared Documents". To avoid confusion, trim back 
      // to "..\All Users\" (XP) or "..\Public\" (Vista) for filtering
      LPTSTR sFind = StrRStrI(sAllUsers, NULL, _T("Documents\\"));
      if (sFind) // XP
      {
        sFind[0] = cNull;
        // sAllUsers now like "...\All Users\" or "...\All Users\Shared " 
        // Ensure it ends with "All Users\" (no effect if it already does);
        sFind = StrRChr(sAllUsers, NULL, _T('\\'));
        if (sFind) sFind[1] = cNull; // Toss all AFTER the last backslash
      }
      else // !sFind, probably means Vista
      {
        sFind = StrRStrI(sAllUsers, NULL, _T("\\Music")); // Vista
        if (sFind) sFind[1] = cNull; // Trim to typically C:\Users\Public\ 
        // else don't try to figure it out. Complete path PROBABLY ok for filter
      } // else, i.e. !sFind, probably means Vista
    } // else i.e. Normal case, ExpandEnvironmentStrings succeeded
  } // else // Normal case, got CommonMusic folder
*/

  // BEGIN REPLACE ABOVE ASSUMPTION THAT SYNC PLAYLISTS ARE IN C:\Public, USE REGISTRY
  // Also below replace check for substring sAllUsers with sSyncPlistsDir

  // For Vista/7, Sync playlists dir in HKEY_CURRENT_USER; in XP HKEY_LOCAL_MACHINE
  // Check version for where to look in registry
//  OSVERSIONINFO osver;
  HKEY hKeySyncExpected;
  HKEY hKeySyncAlternative;

//  if (GetVersionEx(&osver) && osver.dwMajorVersion < 6) // Vista = 6.0, Win 7 = 6.1
  if (! IsWindowsVistaOrGreater()) // Replace deprecated GetVersionEx
  {
    // Is XP or earlier, Sync playlists folder recorded in HKLM
    hKeySyncExpected = HKEY_LOCAL_MACHINE;
    hKeySyncAlternative = HKEY_CURRENT_USER; // Look here only if not found in expected
  }
  else // Is Vista or later, or assume so if GetVersionEx failed
  {
    hKeySyncExpected = HKEY_CURRENT_USER;
    hKeySyncAlternative = HKEY_LOCAL_MACHINE;
  }

  // These are the same with either HKLM or HKCU
  LPCTSTR spKeySync = _T("Software\\Microsoft\\MediaPlayer\\Preferences");
  LPCTSTR spValueNameSync = _T("ObfuscatedSyncPlaylistsPath");

  // This is only in XP, not in Vista/7. Check w/ both anyway in case in 8 etc.
  HKEY hKeySampleExpected = HKEY_LOCAL_MACHINE;
  HKEY hKeySampleAlternative = HKEY_CURRENT_USER;
  LPCTSTR spKeySample = spKeySync;
  LPCTSTR spValueNameSample = _T("ObfuscatedSamplePlaylistsPath");

  // In Win 7 Sync Playlists dir is in user's profile; in XP in All User's profile
  TCHAR sSyncPlistsDirRaw[MAX_PATH + 1];
  TCHAR sSyncPlistsDir[MAX_PATH + 1];
  LPCTSTR spNotSubstrOfAnyPath = _T("::?\\\\ no path can contain this");

  if (! ReadReg(hKeySyncExpected, spKeySync, spValueNameSync, sSyncPlistsDirRaw))
  {
    // Failed. Alternative will probably fail to, but give it a try.
    // First save original error msg from here, use it if alternative also fails.
    TCHAR sErrorReadReg[1024];
    _tcscpy(sErrorReadReg, m_sError);
    if (! ReadReg(hKeySyncAlternative, spKeySync, spValueNameSync, sSyncPlistsDirRaw))
    {
      sSyncPlistsDirRaw[0] = _T('\0'); // to indicate not found
//      sSyncPlistsDir[0] = _T('\0');
      _tcscpy(sSyncPlistsDir, spNotSubstrOfAnyPath);
      fnErr(_T("Non-fatal error reading Windows created playlists in registry"),
            sErrorReadReg,
            _T("Playlists will not be limited to user created only"));
    }
  }

  // Result of ReadReg is probably literal, but might include e.g. %LOCALAPPDATA%
  if (sSyncPlistsDirRaw[0])
  {
    if (!ExpandEnvironmentStrings(sSyncPlistsDirRaw, sSyncPlistsDir, _MAX_PATH))
    {
//      sSyncPlistsDir[0] = _T('\0'); // to indicate not found
      _tcscpy(sSyncPlistsDir, spNotSubstrOfAnyPath);
      fnSysErr(_T("Non-fatal error expanding Windows' environment variable in"),
               sSyncPlistsDirRaw,
               _T("Playlists will not be limited to user created only"));
    } // else literal path was copied to sSyncPlistsDir
  }

  // Sample Playlists dir in XP's HKLM, but not 7. Check regardless of Win version.
  TCHAR sSamplePlistsDirRaw[MAX_PATH + 1];
  TCHAR sSamplePlistsDir[MAX_PATH + 1];

  if (! ReadReg(hKeySampleExpected, spKeySample, spValueNameSample, sSamplePlistsDirRaw))
  {
    // Failed. Alternative will probably fail to, but give it a try.
    // First save original error msg from here, use it if alternative also fails.
    TCHAR sErrorReadReg[1024];
    _tcscpy(sErrorReadReg, m_sError);
    if (! ReadReg(hKeySampleAlternative, spKeySample, spValueNameSample, sSamplePlistsDirRaw))
    {
      sSamplePlistsDirRaw[0] = _T('\0'); // to indicate not found
      _tcscpy(sSamplePlistsDir, spNotSubstrOfAnyPath);
      // No error message. This is expected in Windows 7
    }
  }

  if (sSamplePlistsDirRaw[0])
  {
    if (!ExpandEnvironmentStrings(sSamplePlistsDirRaw, sSamplePlistsDir, _MAX_PATH))
      _tcscpy(sSamplePlistsDirRaw, spNotSubstrOfAnyPath);
  }

  // END REPLACE ABOVE ASSUMPTION THAT SYNC PLAYLISTS ARE IN C:\Public, USE REGISTRY
  // Also below replace check for substr sAllUsers w/ sSyncPlistsDir & sSamplePlistsDir

  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"));
    return (INT) fnErr(_T("Can't load playlists, failed m_cpWmpPlaylistArr->get_count"));

  CComBSTR cbsAttrLabelType, cbsAttrLabelTitle, cbsAttrResult;

  BOOL bIsAutoPlaylist;
  CComPtr<IWMPPlaylist> cpThisPlaylist;

  cbsAttrLabelType = _T("PlaylistType");
  cbsAttrLabelTitle =_T("Title");

  USES_CONVERSION;

  //INT nLbStaticIx = 0; // Now the only listbox, includes auto playlists
  // 3.0 Change.
  // May already have Genres in listbox, get next LB index. 0 if Playlists first.
  INT nLbStaticIx = (INT) SendMessage(m_hLbSongGroups, LB_GETCOUNT, 0, 0);
  if (nLbStaticIx == LB_ERR)
    return (INT) fnErr(_T("Error getting next listbox index for user playlists"));
  INT nInserted = 0;

//if (ini->bLog && m_b1stListboxLoad)
//{
//  _tcscpy(sBS, _T("--------------------------------------------------\r\n")
//                _T("PLAYLISTS INSERTED IN SELECT DLG LISTBOX\r\n") );
//  ((CMinDlg *) m_pOwner)->LogSelectDlgStartMsg(sBS);
//}

  // Vars for filtering auto-playlists for user only
  CComBSTR cbsAttrLabelMediaType, cbsAttrLabelSourceURL, cbsFilterResult;
  cbsAttrLabelMediaType =_T("MediaType");
  cbsAttrLabelSourceURL =_T("SourceURL");
  CComPtr<IWMPPlaylist> cpPlaylistOfName;
  CComPtr<IWMPMedia> cpThisMedia;
  long nlMediaThisName;

  BOOL bLoadThisPlaylist;

  for (INT i = 0; i < nCount; i++)
  {
    hr = m_cpWmpPlaylistArr->item(i, &cpThisPlaylist);

    // For individual item error, skip non-fatal
    if (SUCCEEDED(hr) && cpThisPlaylist.p)
    {
      // Use PlaylistType attribute to tell whether it's a static playlist or auto playlist
      hr = cpThisPlaylist->getItemInfo(cbsAttrLabelType, &cbsAttrResult);

      if (SUCCEEDED(hr) && (cbsAttrResult.m_str != NULL))
      {
        bIsAutoPlaylist = !_tcsicmp(OLE2T(cbsAttrResult), _T("auto"));
        cbsAttrResult.Empty();

        hr = cpThisPlaylist->getItemInfo(cbsAttrLabelTitle, &cbsAttrResult);
        if (SUCCEEDED(hr) && (cbsAttrResult.m_str != NULL)
            && cbsAttrResult.Length() != 0)
        {
          if (bIsAutoPlaylist) // Might be an All Users playlist, check it out
          {
            // Get all media with this name (usually only one), discard
            // if any playlist this name has All Users path
            bLoadThisPlaylist = TRUE; // unless reset in loop below

            hr = m_cpWmpAllMedia->getByName(cbsAttrResult, &cpPlaylistOfName);
            if (SUCCEEDED(hr))
              hr = cpPlaylistOfName->get_count(&nlMediaThisName);

            if (SUCCEEDED(hr))
            {
              for (long li = 0; li < nlMediaThisName && bLoadThisPlaylist; li++)
              {
                hr = cpPlaylistOfName->get_item(li, &cpThisMedia);
                if (SUCCEEDED(hr))
                  hr = cpThisMedia->getItemInfo(cbsAttrLabelMediaType,
                                                &cbsFilterResult);
                if (!SUCCEEDED(hr)) break;
                // Ignore this media item in unlikely event its not a playlist
                if (!_tcsicmp(OLE2T(cbsFilterResult), _T("playlist")))
                {
                  cbsFilterResult.Empty();
                  hr = cpThisMedia->getItemInfo(cbsAttrLabelSourceURL,
                                                &cbsFilterResult);
                  if (!SUCCEEDED(hr)) break;

                  // Will exit loop if next command sets bLoadThisPlaylist FALSE
                  // Case insensitive comparison (maps to correct char set StrStrIW)
//                  bLoadThisPlaylist = !StrStrI(OLE2T(cbsFilterResult), sAllUsers);
                  // Check for playlist path contains registry's
                  // ObfuscatedSyncPlaylistsPath or ObfuscatedSsmplecPlaylistsPath 
                  _tcscpy(sBS, OLE2T(cbsFilterResult));
                  bLoadThisPlaylist = (!StrStrI(sBS, sSyncPlistsDir) &&
                                       !StrStrI(sBS, sSamplePlistsDir));
                } // if (!_tcsicmp(OLE2T(cbsFilterResult), _T("playlist")))
                cbsFilterResult.Empty();
                cpThisMedia.Release();
              } // for (long li = 0; li < nlMediaThisName && bLoadThisPlaylist; li++)
            } // if (SUCCEEDED(hr)) i.e. on cpPlaylistOfName->get_count

            cpPlaylistOfName.Release();

            // Most auto-playlists are All Users, toss if unable to test
            if (!SUCCEEDED(hr)) bLoadThisPlaylist = FALSE;

          } // if (bIsAutoPlaylist)
          else bLoadThisPlaylist = TRUE; // Static playlist, never All Users
          
          if (bLoadThisPlaylist)
          {
//            SendMessage(m_hLbSongGroups, LB_INSERTSTRING,
//                        nLbStaticIx, (LPARAM) OLE2T(cbsAttrResult) );
            _stprintf(sBS, _T("Playlist: %s"), OLE2T(cbsAttrResult));
            SendMessage(m_hLbSongGroups, LB_INSERTSTRING, nLbStaticIx, (LPARAM) sBS);

            m_nArrLbIxPlayArrIx[nLbStaticIx] = i; // To match with playlist arr ix

//if (ini->bLog && m_b1stListboxLoad)
//{
//  _stprintf(sBS2,
//            _T("=>  Listbox Ix %d  Playlist Array Ix %d  Name:\r\n    %s\r\n"),
//            nLbStaticIx, i, sBS);
//  ((CMinDlg *) m_pOwner)->LogSelectDlgStartMsg(sBS2);
//}

            nLbStaticIx ++; // For next insert
            nInserted ++; // For return. Same as nLbStaticIx if playlists first
          } // if (bLoadThisPlaylist)

//else // i.e. ! bLoadThisPlaylist, debuggin only
//if (ini->bLog && m_b1stListboxLoad)
//{
//  _stprintf(sBS, _T("Playlist: %s"), OLE2T(cbsAttrResult));
//  _stprintf(sBS2,
//            _T("=>  SKIPPED Playlist Array Ix %d  Name:\r\n    %s\r\n"),
//            i, sBS);
//  ((CMinDlg *) m_pOwner)->LogSelectDlgStartMsg(sBS2);
//}

          cbsAttrResult.Empty();
        } // if (SUCCEEDED(hr) && (cbsAttrResult.m_str != NULL))
          // i.e. cpThisPlaylist->getItemInfo(cbsAttrLabelTitle, &cbsAttrResult);

      } // if (SUCCEEDED(hr) && (cbsAttrResult.m_str != NULL))
        // i.e. cpThisPlaylist->getItemInfo(cbsAttrLabelType, &cbsAttrResult)

      cpThisPlaylist.Release();
      //cpThisPlaylist = NULL; // From sample. Instead of .Release() ??

    } // if (SUCCEEDED(hr) && pThisPlaylist.p)
  } // for (INT i = 0; i < nCount; i++)

  if (ini->bLog && m_b1stListboxLoad)
  {
    DWORD dwEnd = GetTickCount();
    _stprintf(sBS,
              _T("--------------------------------------------------\r\n")
              _T("%s  Select Songs dialog first fetch of playlists\r\n")
              _T("Loaded list of playlists (filtered) in %d milliseconds\r\n"),
              TimeStamp(), (INT) (dwEnd - dwStart));
    ((CMinDlg *) m_pOwner)->LogSelectDlgStartMsg(sBS);
  }

//  // Next (unused) listbox index is count of items
//  _stprintf(sBS, _T("%d Playlists shown. Use Ctrl-Click to select multiple."),
//            nLbStaticIx);
//  SetItemText(IDCS_TXT_LB_COUNT, sBS);

  // 3.0 change: caller will set text with both counts, genres and playlists
  return nInserted;
} // INT CSelectDlg::ListBoxAppendPlistsFiltered()

// This was LoadListBoxesNoFilter
// APPEND (3.0 change) to LB all playlists, including All Users sync lists
INT CSelectDlg::ListBoxAppendPlistsNoFilter()
{
  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"));
    return (INT) fnErr(_T("Can't load playlists, failed m_cpWmpPlaylistArr->get_count"));

  CComBSTR cbsAttrLabelTitle, cbsAttrResult;
  cbsAttrLabelTitle =_T("Title");
  CComPtr<IWMPPlaylist> cpThisPlaylist;

//  INT nLbIx = 0;
  // 3.0 Change.
  // May already have Genres in listbox, get next LB index. 0 if Playlists first.
  INT nLbIx = (INT) SendMessage(m_hLbSongGroups, LB_GETCOUNT, 0, 0);
  if (nLbIx == LB_ERR)
    return (INT) fnErr(_T("Error getting next listbox index for all playlists"));
  INT nInserted = 0;

  USES_CONVERSION;

  for (INT i = 0; i < nCount; i++)
  {
    hr = m_cpWmpPlaylistArr->item(i, &cpThisPlaylist);

    // For individual item error, skip non-fatal
    if (!SUCCEEDED(hr) || !cpThisPlaylist.p) continue;

    hr = cpThisPlaylist->getItemInfo(cbsAttrLabelTitle, &cbsAttrResult);
    if (SUCCEEDED(hr) && (cbsAttrResult.m_str != NULL)
        && cbsAttrResult.Length() != 0)
    {
//      SendMessage(m_hLbSongGroups, LB_INSERTSTRING,
//                  nLbIx, (LPARAM) OLE2T(cbsAttrResult) );
      _stprintf(sBS, _T("Playlist: %s"), OLE2T(cbsAttrResult));
      SendMessage(m_hLbSongGroups, LB_INSERTSTRING, nLbIx, (LPARAM) sBS);

      m_nArrLbIxPlayArrIx[nLbIx] = i; // To match with playlist arr ix
      nLbIx ++; // For next insert
      nInserted ++; // For return. Same as nLbIx if playlists first
    }
    cpThisPlaylist.Release();
  } // for (INT i = 0; i < nCount; i++)

  // 3.0 change: caller will set text with both counts, genres and playlists
  return nInserted;
} // INT CSelectDlg::ListBoxAppendPlistsNoFilter()

//void CSelectDlg::OnCheckBoxNoFilter()
//{
//  m_b1stListboxLoad = FALSE;
//  m_nLastLbSel_Static = -1; // Don't remember last selection index in listbox
//  SendMessage(m_hLbStaticPlaylists, LB_RESETCONTENT, 0, 0);
//
//  if (IsBnChecked(IDCS_CK_ALLPLAYLISTS)) LoadListBoxNoFilter();
//  else LoadListBoxFiltered();
//}

void CSelectDlg::OnOk()
{
  // Set uRet... public vars for MinDlg to add/update Listview row
  TCHAR sEdTxt[8];

  // Get repetition frequencies, to use with whatever group selected
  BOOL bUseStarRatings = IsBnChecked(IDCS_RB_STARS);
  BOOL bSameRepsAll = IsBnChecked(IDCS_RB_SAME);
  BOOL bExclusionFilter = IsBnChecked(IDCS_RB_EXCLUDE);
  INT nAtLeastOneNonZero = 0; // Really using as boolean

  if (bUseStarRatings) // separate repetition counts for each rating
  {
    nRetArrReps[0] = 0; // 0 indicates frequency is based on stars rating
    for (INT i = 1; i <= 5; i++) // i = rating number of stars
    {
      // Consecutive resource IDs: IDCS_ED_STARS5 = IDCS_ED_STARS1 + 4
      GetItemText(IDCS_ED_STARS1 + i - 1, sEdTxt, 3);
      nRetArrReps[i] = _tstoi(sEdTxt); // _tstoi returns 0 if empty string
      nAtLeastOneNonZero += nRetArrReps[i];
    }
  }
  else if (bSameRepsAll)
  {
    GetItemText(IDCS_ED_SAME, sEdTxt, 3);
    // Positive uRetArrReps[0] means all same frequency, ignore [1-5]
    nRetArrReps[0] = _tstoi(sEdTxt); // If 0, caught below
    nAtLeastOneNonZero = nRetArrReps[0];
  }
  else if (bExclusionFilter)
  {
    nRetArrReps[0] = -1; // -1 indicates exclusion
    nAtLeastOneNonZero = 1;
  }
  else // This should never happen; should alway be one RB selected by default
  {
    RET_V(_T("No option chosen in How often to play songs"));
  }

/*
  // Load all values into array for now, even though not all will be used
  for (INT i = 0; i <= 5; i++) // 1-5 is star ratings, 0 is same frequency for all
  {
    GetItemText(IDCS_SPIN_SAME + i, sEdTxt, 3);
    uRetArrReps[i] = _tstoi(sEdTxt); // _tstoi returns 0 if empty string
    if (bUseStarRatings && i) // Don't increment for uRetArrReps[0]
      nAtLeastOneNonZero += uRetArrReps[i];
  }

  if (!bUseStarRatings) nAtLeastOneNonZero = uRetArrReps[0];
  // else set in loop above for bUseStarRatings case
*/
  if (!nAtLeastOneNonZero)
  {
    fnErr(_T("All songs set to play 0 times."),
      _T("To use in the shuffle at least some songs must play 1 or more times"));
    return;
  }

  // Save these repetition frequencies if that's the preference
  if (ini->uRememberReps != UREPS_REMEMBER_NO)
  {
    // Remember at least for this session by resetting ini vars
    // Only reset the value(s) being used this time
    if (bSameRepsAll) ini->nRepsSame = (UINT) nRetArrReps[0];
    else if (bUseStarRatings)  // 3.0: NOW 3 OPTIONS, NEED else if
    {
      ini->nReps1 = nRetArrReps[1];
      ini->nReps2 = nRetArrReps[2];
      ini->nReps3 = nRetArrReps[3];
      ini->nReps4 = nRetArrReps[4];
      ini->nReps5 = nRetArrReps[5];
    }
    // else exclusion filter, don't save anything

    // Preference whether to save current ini vars to ini file for next session
//    if (ini->uRememberReps == UREPS_REMEMBER_ALWAYS <== Wrong always save for != UREPS_REMEMBER_NO
//        && (!bExclusionFilter) // No frequencies to save if bExcludeFilter
    if (!bExclusionFilter // No frequencies to save if bExcludeFilter
        && !ini->WriteIniSection(ini->m_sectPref) ) // Vars not selected are same, ok
    {
      fnErr(_T("Non-fatal error saving repetition frequencies to ini file."),
            _T("No effect on this session, but next session will revert")
              _T(" to previous defaults."));
      // And drop through to proceed otherwise normally
    }

  } //  if (ini->uRememberReps != UREPS_REMEMBER_NO)

// 3.0 No longer early return for update selection
  //// All processing below for controls only in IDD_SELECT_SONGS. Finished if not.
  //if (!m_bIsSelect)
  //{
  //  ZDlg::OnOk();
  //  return;
  //}

//  LONG nlSelected;
//  LPCTSTR sErrNoSel = _T("No genres or playlists are selected in the listbox");

   // 3.0 Declare here to keep count in scope further down, for CSongsSelection
  INT nLbSelected = 0;

  INT nSongSelType = SEL_TYPE_INVALID; // 3.0 Or make member for MinDlg retrieval?
  // Which playlist or song group to use
  if (IsBnChecked(IDCS_RB_SEL_ALL))
  {
//    uRetIxOrDays = -1; // Not used
//    uRetGroupType = GROUP_TYPE_ALL;
    uRetGroupType = SEL_TYPE_ALL; // 3.0 Get rid of this member var?
    nSongSelType = SEL_TYPE_ALL; // SongsSelection.h
  }
//  else if (IsBnChecked(IDCS_RB_SEL_DAYS))
//  {
    //GetItemText(IDCS_ED_DAYS, sEdTxt, 4);
    //uRetIxOrDays = _tstoi(sEdTxt);
    //if (uRetIxOrDays < 1)
    //{
    //  _stprintf(sBS, _T("Invalid: songs added in the last %u days"),
    //            uRetIxOrDays);
    //  fnErr(sBS, _T("You must set a number between 1 and 999"));
    //  return;
    //}
    //uRetGroupType = GROUP_TYPE_NEW;
//  }
  else if (IsBnChecked(IDCS_RB_SEL_LB))     // <== RB's CHANGED, NEED NEW HANDLERS
  {
//    uRetGroupType = GROUP_TYPE_LBSEL;
    uRetGroupType = SEL_TYPE_LISTS;
    nSongSelType = SEL_TYPE_LISTS; // SongsSelection.h

    //nlSelected = SendMessage(m_hLbStaticPlaylists, LB_GETCURSEL, 0, 0);
    //if (nlSelected == LB_ERR)
    //{
    //  fnErr(sErrNoSel);
    //  return;
    //}
    //// Set uRetIxOrDays to index of playlist in m_cpWmpPlaylistArr
    //uRetIxOrDays = m_nArrLbStaticIxPlayArrIx[nlSelected];
    ////uRetGroupType = GROUP_TYPE_LIST_STATIC;
    //uRetGroupType = GROUP_TYPE_PLAYLIST;

    // 3.0 Will use this count further down, for CSongsSelection
    nLbSelected = (INT) SendMessage(m_hLbSongGroups, LB_GETSELCOUNT, 0, 0);
    if (nLbSelected == LB_ERR)
    {
      RET_V(_T("Error getting count of selected genres/playlists"));
    }
    else if (nLbSelected == 0)
      RET_V( _T("No genres or playlists are selected in the listbox"));
  }
  else
  {
    fnErr(_T("No checked radio button found for All Songs, Playlists/Genres"));
    return;
  }

  // 3.0: age limit formerly radio button, now checkbox; doesn't set uRetGroupType
  if (IsBnChecked(IDCS_CK_LIMIT_NEW))
  {
    GetItemText(IDCS_ED_DAYS, sEdTxt, 4); // Char count includes ending null
    nRetAgeLimit = _tstoi(sEdTxt);
    if (nRetAgeLimit < 1)
    {
      _stprintf(sBS, _T("Invalid: songs added in the last %d days"),
                nRetAgeLimit);
      fnErr(sBS, _T("You must set a number between 1 and 999"));
      return;
    }

    // Separately save max age for new songs if that option used this time
    // Save every time option used, i.e. not affected by UREPS_REMEMBER_...
    // Save as number of days ago or as fixed date per option
    INT nSaveDaysNew = nRetAgeLimit;
    if (ini->bDaysNewAsDate)
    {
      nSaveDaysNew = N_DaysAgo_to_IntYYYYMMDD(nRetAgeLimit);
    }
    // Minimal reality check
    if ( (nSaveDaysNew / 10000000) == 2 // YYYYMMDD in 2000 to 2999
        || (nSaveDaysNew > 0 && nSaveDaysNew < 1000) ) // 1 to 999 days ago
    {
      ini->SetAndSaveKey(ini->nDaysNew, nSaveDaysNew);
    }

  } // if (IsBnChecked(IDCS_CK_LIMIT_NEW))
  else nRetAgeLimit = 0; // Means no age limit

  // 3.0: option to dedup selected songs (b4 applying mulitpliers)
  bRetDedupSongs = IsBnChecked(IDCS_CK_UNIQ);

  // 3.0 new exclusion option invalid for all songs
//  if ( ! (bSameRepsAll || bUseStarRatings) // i.e. remaining RB exclude selected
  if (bExclusionFilter
      && uRetGroupType == GROUP_TYPE_ALL && !nRetAgeLimit)
  {
    fnErr(_T("Exclusion option not allowed for All Songs (except with \"new only\" filter)"));
    return;
  }

  /*******************************************************************
   3.0 Load data into CSongsSelection
   From here to end of OnOk
   Copied/adapted from Try 1 in MinDlg
  ********************************************************************/

//  BOOL bDedupSongs = pDlg->bRetDedupSongs;
//  INT nAgeLimit = pDlg->nRetAgeLimit;

//  INT nSongSelType; // Declare above before check RB's, assign where uGroupType assigned

  // These arrays must be allocated new, to pass on to CSongSelection to keep (& delete)
//  LPTSTR sDisplayList;     //////////// DITCH THIS?
  INT * nArrSelected = NULL;
  INT * nArr_PlistIndexes = NULL;   // For WMP lookup
  LPTSTR * spArr_PlistNames = NULL; // For tooltip
  LPTSTR * spArr_GenreNames = NULL; // Genres use name for both tooltip and WMP lookup
  LPTSTR sSelectionName = new TCHAR[1024]; // Overkill for possible long playlist names
  if (!sSelectionName) RET_SYSV(_T("Error allocating memory for selection name"));

  INT nPlistsSelected = 0; // These two moved up from SEL_TYPE_LISTS block
  INT nGenresSelected = 0;

//  if (uGroupType == GROUP_TYPE_ALL) // SelectDlg.h; change to use SongsSelection.h?
  if (nSongSelType == SEL_TYPE_ALL)
  {
    nSongSelType = SEL_TYPE_ALL;
    // Below set NULL in declaration
    //nArrSelected = NULL;
    //spArr_GenreNames = NULL;
    //nArr_PlistIndexes = NULL;
    //spArr_PlistNames = NULL;
    
    // One line summary to show in Listview row
    _tcscpy(sSelectionName, _T("ALL SONGS"));
    // If age filter, append info in parentheses. Deduping irrelevant for All Songs.
    if (nRetAgeLimit)
    {
      _stprintf(sBS, _T(" (New added in last %d days)"), nRetAgeLimit);
      _tcscat(sSelectionName, sBS);
    }
    // For SEL_TYPE_ALL deduping is irrelevant, so no note about that

    //// Multiline for tooltip
    //sDisplayList = new TCHAR[1024];
    //_tcscpy(sDisplayList, _T("ALL SONGS\n")); // Include \r for tooltip <== OR NOT \r?
    //if (nRetAgeLimit)
    //{
    //  _stprintf(sBS, _T("Filter for new songs only, added in last %d days"), nRetAgeLimit);
    //  _tcscat(sDisplayList, sBS);
    //}
    //else _tcscat(sDisplayList, _T("No filter for new songs"));
  }
  else // nSongSelType == SEL_TYPE_LISTS, a lot more convoluted than SEL_TYPE_ALL
  {
    // For extracting just the names from e.g. "Ambient" from "Genre: Ambient"
    INT nLenPrefixGenre = _tcslen(scPrefixGenre);
    INT nLenPrefixPlist = _tcslen(scPrefixPlist);

    // Read listbox to get genres and playlists
    LRESULT lrRet;

    // Array of selected item indexes
    nArrSelected = new INT[nLbSelected];
    if (!nArrSelected)
    {
      delete [] sSelectionName;
      RET_SYSV(_T("Error allocating memory for genres/playlists array"));
    }

    lrRet = SendMessage(m_hLbSongGroups, LB_GETSELITEMS,
                        (WPARAM) nLbSelected, (LPARAM) nArrSelected);
    if (lrRet != nLbSelected) // Return is number of items placed in buffer
    {
      _stprintf(sBS, _T("Error on LB_GETSELITEMS, returned %d, expected %d"),
                nLbSelected, (INT) lrRet);
      fnSysErr(sBS); // Before delete in case that resets lasterror to 0
      delete [] sSelectionName;
      delete [] nArrSelected;
      return;
    }

    // On first pass through selected, find what size arrays to allocate
    INT nCharsDisplayList = 0;
    nGenresSelected = 0;
    nPlistsSelected = 0;
    TCHAR sLbItem[2048]; // Assume no single item close to this big, to skip LB_GETTEXTLEN

    for (INT i = 0; i < nLbSelected; i++)
    {
      lrRet = SendMessage(m_hLbSongGroups, LB_GETTEXT, nArrSelected[i], (LPARAM) sLbItem);
      if (lrRet == LB_ERR)
      {
        _stprintf(sBS, _T("Error on getting text of selected item %d"), nArrSelected[i]);
        fnSysErr(sBS);
        delete [] sSelectionName;
        delete [] nArrSelected;
        return;
      }
      nCharsDisplayList += (INT) lrRet + 1; // LB_GETTEXT return is strlen (w\o \0)

      // Item indicates type by start with "Genre: " or "Playlist: "
      // Test for pointer to prefix = start of the LB string
      if (_tcsstr(sLbItem, scPrefixGenre) == sLbItem) nGenresSelected++;
      else if (_tcsstr(sLbItem, scPrefixPlist) == sLbItem) nPlistsSelected++;
      else
      {
        delete [] sSelectionName;
        delete [] nArrSelected;
        RET_V2(_T("Listbox item doesn't start with either Genre: or Playlist:"), sLbItem);
      }
    } // for (INT i = 0; i < nLbSelected; i++)  FIRST PASS

    // Allocate arrays for storing info to get songs from WMP later
//    sDisplayList = new TCHAR[nCharsDisplayList + 20000]; // with space for extra text
    // SelectDlg allows one of nGenresSelected or nPlistsSelected may be 0, but not both.
    if (nGenresSelected)
      spArr_GenreNames = new LPTSTR[nGenresSelected]; // For both WMP lookup & tooltip
    if (nPlistsSelected)
    {
      spArr_PlistNames = new LPTSTR[nPlistsSelected]; // For tooltip
      nArr_PlistIndexes = new INT[nPlistsSelected];   // For WMP lookup
    }

//    if (!sDisplayList || (nGenresSelected && !spArr_GenreNames) ||
    if ((nGenresSelected && !spArr_GenreNames) ||
        (nPlistsSelected && (!spArr_PlistNames || !nArr_PlistIndexes)) )
    {
      fnSysErr(_T("Error allocating memory for storing genres/playlists arrays"));
      delete [] sSelectionName;
      delete [] nArrSelected;
      // Don't know which failed allocation, test individually
//      if (sDisplayList) delete [] sDisplayList;
      if (spArr_GenreNames) delete [] spArr_GenreNames;
      if (spArr_PlistNames) delete [] spArr_PlistNames;
      if (nArr_PlistIndexes) delete [] nArr_PlistIndexes;
      return;
    }

    for (INT i = 0; i < nGenresSelected; i++) spArr_GenreNames[i] = NULL; 
    for (INT i = 0; i < nPlistsSelected; i++) spArr_PlistNames[i] = NULL; 

// Now CSongsSelection generates tooltip string
    //// Start sDisplayList (multiline tooltip) before append individual selections below
    //// At least one of nGenresSelected & nPlistsSelected > 0; don't know if both > 0.
    //sDisplayList[0] = _T('\0'); // so can be target of strcat
    //if (nGenresSelected)
    //{
    //  _stprintf(sBS, _T("%d Genre%s"), nGenresSelected,
    //            (nGenresSelected > 1 ? _T("s") : _T(""))); // "Genre" vs "Genres"
    //  _tcscat(sDisplayList, sBS);
    //}

    //if (nPlistsSelected)
    //{
    //  if (nGenresSelected) _tcscat(sDisplayList, _T(" and "));
    //  _stprintf(sBS, _T("%d Playlist%s"), nPlistsSelected,
    //            (nPlistsSelected > 1 ? _T("s") : _T("")));
    //  _tcscat(sDisplayList, sBS);
    //}

    // If more than one item, 1 line short name is like "3 Genres and 1 Playlist"
    BOOL bOnlyOneItemSelected = (nGenresSelected + nPlistsSelected == 1);
    if (!bOnlyOneItemSelected)
    {
//      _tcscpy(sSelectionName, sDisplayList);
    // No longer use sDisplayList; write sSelection name directly
      sSelectionName[0] = _T('\0'); // so can be target of strcat
      if (nPlistsSelected)
      {
        _stprintf(sBS, _T("%d Playlist%s"), nPlistsSelected,
                  (nPlistsSelected > 1 ? _T("s") : _T(""))); // "Playlist" vs "Playlists"
        _tcscat(sSelectionName, sBS);
      }
      if (nGenresSelected)
      {
        if (nPlistsSelected) _tcscat(sSelectionName, _T(" and "));
        _stprintf(sBS, _T("%d Genre%s"), nGenresSelected,
                  (nGenresSelected > 1 ? _T("s") : _T("")));
        _tcscat(sSelectionName, sBS);
      }
    } // if (!bOnlyOneItemSelected)
    // else will assign genre or playlist name to sSelectionName from listbox below

    //// Info on age limit and deduping.
    //if (nRetAgeLimit)
    //{
    //  _stprintf(sBS, _T("\r\nFilter for new songs only, added in last %d days"),
    //            nRetAgeLimit);
    //  _tcscat(sDisplayList, sBS);
    //}
    //else _tcscat(sDisplayList, _T("\r\nNo filter for new songs"));

    //if (!bExclusionFilter) // Deduping irrelevant for exclusion filter
    //{
    //  if (bRetDedupSongs)
    //    _tcscat(sDisplayList, _T("\r\nDedup songs before applying frequencies"));
    //  else _tcscat(sDisplayList,
    //              _T("\r\nNo deduping of songs before applying frequencies"));
    //}

    // Mechanism to avoid long list of deletes for every error contingency in loop
    TCHAR sErrMsg[1024];
    sErrMsg[0] = _T('\0'); // Will use sErrMsg[0] as boolean; OK so far if no error msg.

    // Go through selected items again, storing info to get songs from WMP later
    INT nGenreArrIx = 0;
    INT nPlistArrIx = 0;
    for (INT i = 0; i < nLbSelected; i++)
    {
      lrRet = SendMessage(m_hLbSongGroups, LB_GETTEXT, nArrSelected[i], (LPARAM) sLbItem);
      if (lrRet == LB_ERR)
      {
        GetSysErrorMsg(sBS);
        _stprintf(sErrMsg, _T("Error on reading text of selected item %d\nWindows says:\n%s"),
                  nArrSelected[i], sBS);
        break;
      }

      //_tcscat(sDisplayList, sRN);
      //_tcscat(sDisplayList, sLbItem);
      
      if (_tcsstr(sLbItem, scPrefixGenre) == sLbItem) // Starts with "Genre: "
      {
        if (nGenreArrIx >= nGenresSelected)
        {
          _stprintf(sErrMsg,
            _T("Counted %d selected genres on first pass, found extra on second:\n%s"),
            nGenresSelected, sLbItem);
          break;
        }

        spArr_GenreNames[nGenreArrIx] = new TCHAR[_tcslen(sLbItem) + 1 - nLenPrefixGenre];
        if (!spArr_GenreNames[nGenreArrIx])
        {
          GetSysErrorMsg(sBS);
          _stprintf(sErrMsg,
                    _T("Error allocating memory to store genre: %s\nWindows says:\n%s"),
                    sLbItem, sBS);
          break;
        }

        // Copy just the genre name, without "Genre: "
        _tcscpy(spArr_GenreNames[nGenreArrIx], &sLbItem[nLenPrefixGenre]);
        nGenreArrIx++;
        if (bOnlyOneItemSelected)
        {
          // This is the only cycle through this loop, use the single item name
          _tcscpy(sSelectionName, &sLbItem[nLenPrefixGenre]);
          //_tcscat(sSelectionName, _T(" <Genre>"));
        }

      } // if (_tcsstr(sLbItem, scPrefixGenre) == sLbItem)
      else if (_tcsstr(sLbItem, scPrefixPlist) == sLbItem)
      {
        if (nPlistArrIx >= nPlistsSelected)
        {
          _stprintf(sErrMsg,
            _T("Counted %d selected playlists on first pass, found extra on second:\n%s"),
            nPlistsSelected, sLbItem);
          break;
        }

        // nArrSelected[i] is the index of the selected playlist item in the listbox.
        // Need to store both name and the m_cpWmpPlaylistArr index of that playlist.
        nArr_PlistIndexes[nPlistArrIx] = GetWmpPlistArrIxForLbIx(nArrSelected[i]);
        if (nArr_PlistIndexes[nPlistArrIx] < 0) // Means no playlist in that LB ix
        {
          _stprintf(sErrMsg,
                _T("Listbox shows playlist at item %d:\n%s\nbut not found in WMP array"),
                nArrSelected[i], sLbItem);
          break;
        }

        spArr_PlistNames[nPlistArrIx] = new TCHAR[_tcslen(sLbItem) + 1 - nLenPrefixPlist];
        if (!spArr_PlistNames[nPlistArrIx])
        {
          GetSysErrorMsg(sBS);
          _stprintf(sErrMsg,
                    _T("Error allocating memory to store playlist: %s\nWindows says:\n%s"),
                    sLbItem, sBS);
          break;
        }

        // Copy just the playlist name, without "Playlist: "
        _tcscpy(spArr_PlistNames[nPlistArrIx], &sLbItem[nLenPrefixPlist]);

//if (ini->bLog && m_b1stListboxLoad)
//{
//  USES_CONVERSION;
//  CComBSTR cbsAttrLabelTitle, cbsAttrResult;
//  cbsAttrLabelTitle =_T("Title");
//  CComPtr<IWMPPlaylist> cpThisPlaylist;
//  HRESULT hr = m_cpWmpPlaylistArr->item(nArr_PlistIndexes[nPlistArrIx], &cpThisPlaylist);
//  if (SUCCEEDED(hr) && cpThisPlaylist.p)
//  {
//    hr = cpThisPlaylist->getItemInfo(cbsAttrLabelTitle, &cbsAttrResult);
//  }
//  if (SUCCEEDED(hr) && (cbsAttrResult.m_str != NULL) && cbsAttrResult.Length() != 0)
//  {
//    _tcscpy(sBS2, OLE2T(cbsAttrResult));
//  }
//  else _tcscpy(sBS2, _T("Failed HRESULT"));
//
//  _stprintf(sBS,
//            _T("=> SELECTED Listbox Ix %d  Playlist Array Ix %d  Name:\r\n    %s\r\n")
//            _T("  Name from playlist array:\r\n    %s\r\n"),
//            nArrSelected[i], nArr_PlistIndexes[nPlistArrIx],
//            spArr_PlistNames[nPlistArrIx], sBS2);
//  ((CMinDlg *) m_pOwner)->LogSelectDlgStartMsg(sBS);
//}

        nPlistArrIx++;

        if (bOnlyOneItemSelected)
        {
          // This is the only cycle through this loop, use the single item name
          _tcscpy(sSelectionName, &sLbItem[nLenPrefixPlist]); // Name w/o "Playlist: "
          //_tcscat(sSelectionName, _T(" <Playlist>"));
        }
      } // else if (_tcsstr(sLbItem, scPrefixPlist) == sLbItem)
      // No else check. Figure any other would have been caught in first pass


      if (sErrMsg[0]) break; // Some fatal error above set error message w/o its own break.

    } // for (INT i = 0; i < nLbSelected; i++)  SECOND PASS

    // ==> Add reality checks nPlistArrIx == nPlistsSelected, nGenreArrIx == nGenresSelected

    if (sErrMsg[0]) // Some fatal error above in loop or after set error message, bail.
    {
      delete [] sSelectionName;
      delete [] nArrSelected;
      if (nArr_PlistIndexes) delete [] nArr_PlistIndexes;

      if (spArr_GenreNames) // NULL if !nGenresSelected
      {
        for (INT i = 0; i < nGenresSelected; i++)
        {
          if (spArr_GenreNames[i] != NULL)  delete [] spArr_GenreNames[i];
        }
        delete [] spArr_GenreNames;
      }

      if (spArr_PlistNames)
      {
        for (INT i = 0; i < nPlistsSelected; i++)
        {
          if (spArr_PlistNames[i] != NULL)  delete [] spArr_PlistNames[i];
        }
        delete [] spArr_PlistNames;
      }

//      // Before deleting sDisplayList, show its content if any; OK if NULL or empty.
//      fnErr(sErrMsg, _T("Display list before error:"), sDisplayList); 
//      delete [] sDisplayList;
      return;
    } // if (sErrMsg[0])

    // If age filter and/or deduping, append info in parentheses
    if (nRetAgeLimit || (bRetDedupSongs && !bExclusionFilter) )
    {
      // Have at least one note to put inparentheses
      _tcscat(sSelectionName, _T(" ("));
      if (nRetAgeLimit)
      {
        _stprintf(sBS, _T("last %d days"), nRetAgeLimit);
        _tcscat(sSelectionName, sBS);
      }
      if (bRetDedupSongs && !bExclusionFilter) // Dedup irrelevant for exclusion
      {
        if (nRetAgeLimit) _tcscat(sSelectionName, _T(", "));
        _tcscat(sSelectionName, _T("deduped"));
      }
      _tcscat(sSelectionName, _T(")"));
    } // if (nRetAgeLimit || bRetDedupSongs)

    if (bOnlyOneItemSelected)
    {
      // Have only genre/playlist name (plus maybe age/dedup note). Append label
      if (nPlistsSelected) _tcscat(sSelectionName, _T(" <Playlist>"));
      else _tcscat(sSelectionName, _T(" <Genre>"));
    }

  } // else, i.e. uGroupType == GROUP_TYPE_LBSEL

//_stprintf(sBS, _T("nLbSelected = %d\nnPlistsSelected = %d\nnGenresSelected = %d"),
//          nLbSelected, nPlistsSelected, nGenresSelected);
//MB(sBS);

  // Create CSongsSelection using values assigned in whichever nSongSelType block above.
  m_pSongsSel = new CSongsSelection(nSongSelType, nRetArrReps, nRetAgeLimit,
                                    bRetDedupSongs, sSelectionName, // sDisplayList,
                                    nArrSelected, nLbSelected,
                                    nArr_PlistIndexes, spArr_PlistNames, nPlistsSelected,
                                    spArr_GenreNames, nGenresSelected);

  // DO NOT delete nArrSelected, nArr_PlistIndexes, spArr_GenreNames, sDisplayList, sSelectionName
  // CSongsSelection has kept them, and will delete them in its destructor.

	ZDlg::OnOk();
} // OnOk

INT CSelectDlg::GetWmpPlistArrIxForLbIx(INT nLbIx)
{
  return m_nArrLbIxPlayArrIx[nLbIx];
}

CSongsSelection * CSelectDlg::GetAndClaimSongsSelection()
{
  if (m_pSongsSel) m_bSongsSelClaimed = TRUE;
  return m_pSongsSel;
}
void CSelectDlg::OnWmPaint()
{
  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
       // Make sure height/width right in OnInitDialolg
                  m_rcImgStars.right - m_rcImgStars.left,  // width & height of
                  m_rcImgStars.bottom - m_rcImgStars.top,  // destination rect
                  image_dc,         // handle to source DC
                  0,    // x-coord of source upper-left corner
                  0,    // y-coord of source upper-left corner
                  m_rcImgStars.right - m_rcImgStars.left, // source dimensions,
                  m_rcImgStars.bottom - m_rcImgStars.top, // must == destination
                  //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
}

//void CSelectDlg::OnDestroy()
//{
//  // Delete here and in .h if no resources to free. Maybe use OnClose instead.
//  // ZDlg version does nothing, just for virtual WM_DESTROY handler in place.
//  // Windows' default handling happens after this.
//}
//

